test(12.2-05): cover every Go behaviour of phase 12.2
- lagoon: Date and TimeOfDay through JSON, text and real DATE/TIME columns; Fill text fallback without changing earlier conversions; required on zero dates; deferred_bindings shape, store isolation and envelope; PurgeDeferred cut-off, after-commit blobs, SKIP LOCKED, skipped types and the deferred:purge command - attach: Store limits, extensions, MIME patterns, default lists, key shape and blob cleanup; IsAllowedImage formats, polyglots, ceiling - conga and pact: framework purge schedule entry and forged jobs; the six relation child hook interfaces - cabana: fileupload and datepicker compile, upload, remove, caption, reorder and bounds; deferred commit order, rollback, applied-only and concurrent saves; relation contracts, forms, CRUD, deferral, schema
This commit is contained in:
@@ -135,3 +135,250 @@ func TestValidateRequiredZeroDateSmoke(t *testing.T) {
|
||||
t.Fatalf("set date: %v %v", errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDateParseAndConstructors covers the constructors and the strict
|
||||
// YYYY-MM-DD parser.
|
||||
func TestDateParseAndConstructors(t *testing.T) {
|
||||
for _, bad := range []string{"", "2026-1-2", "2026/10/02", "02.10.2026", "2026-13-01", "2026-02-30", "2026-10-02 ", "2026-10-02T00:00:00Z", "next tuesday"} {
|
||||
if d, err := ParseDate(bad); err == nil {
|
||||
t.Errorf("ParseDate(%q) = %v, want an error", bad, d)
|
||||
}
|
||||
}
|
||||
d := NewDate(2024, time.February, 29)
|
||||
if d.String() != "2024-02-29" || d.IsZero() {
|
||||
t.Fatalf("leap day %v", d)
|
||||
}
|
||||
if got := NewDate(2025, time.February, 29).String(); got != "2025-03-01" {
|
||||
t.Fatalf("NewDate normalises a non-leap 29th to %s", got)
|
||||
}
|
||||
if !DateOf(time.Time{}).IsZero() {
|
||||
t.Fatal("DateOf(zero time) must be the zero Date")
|
||||
}
|
||||
if got := d.Time(nil); !got.Equal(time.Date(2024, 2, 29, 0, 0, 0, 0, time.UTC)) {
|
||||
t.Fatalf("Time(nil) = %v", got)
|
||||
}
|
||||
warsaw := time.FixedZone("CET", 3600)
|
||||
if got := d.Time(warsaw); got.Location() != warsaw || got.Hour() != 0 || got.Day() != 29 {
|
||||
t.Fatalf("Time(loc) = %v", got)
|
||||
}
|
||||
if !(Date{}).Time(time.UTC).IsZero() || (Date{}).String() != "" {
|
||||
t.Fatal("zero Date must give the zero time and an empty string")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDateTextAndJSON round-trips Date through JSON and text, including
|
||||
// null and the empty string.
|
||||
func TestDateTextAndJSON(t *testing.T) {
|
||||
d := NewDate(2026, 10, 2)
|
||||
text, err := d.MarshalText()
|
||||
if err != nil || string(text) != "2026-10-02" {
|
||||
t.Fatalf("MarshalText %q %v", text, err)
|
||||
}
|
||||
var back Date
|
||||
if err := back.UnmarshalText(text); err != nil || back != d {
|
||||
t.Fatalf("UnmarshalText %v %v", back, err)
|
||||
}
|
||||
if err := back.UnmarshalText([]byte(" ")); err != nil || !back.IsZero() {
|
||||
t.Fatalf("empty text %v %v", back, err)
|
||||
}
|
||||
if err := back.UnmarshalText([]byte("2026-10-32")); err == nil {
|
||||
t.Fatal("invalid text must fail")
|
||||
}
|
||||
for raw, wantZero := range map[string]bool{`null`: true, `""`: true, `"2026-10-02"`: false} {
|
||||
var got Date
|
||||
if err := json.Unmarshal([]byte(raw), &got); err != nil || got.IsZero() != wantZero {
|
||||
t.Fatalf("UnmarshalJSON(%s) = %v %v", raw, got, err)
|
||||
}
|
||||
}
|
||||
for _, raw := range []string{`20261002`, `true`, `"2026-10-02T10:00:00Z"`, `{}`} {
|
||||
var got Date
|
||||
if err := json.Unmarshal([]byte(raw), &got); err == nil {
|
||||
t.Fatalf("UnmarshalJSON(%s) accepted %v", raw, got)
|
||||
}
|
||||
}
|
||||
var ptr *Date
|
||||
if err := json.Unmarshal([]byte(`"2026-10-02"`), &ptr); err != nil || ptr == nil || *ptr != d {
|
||||
t.Fatalf("pointer unmarshal %v %v", ptr, err)
|
||||
}
|
||||
if err := json.Unmarshal([]byte(`null`), &ptr); err != nil || ptr != nil {
|
||||
t.Fatalf("pointer null %v %v", ptr, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDateScan covers every Scan source.
|
||||
func TestDateScan(t *testing.T) {
|
||||
var d Date
|
||||
for src, want := range map[any]string{
|
||||
"2026-10-02": "2026-10-02",
|
||||
"2026-10-02T13:14:15Z": "2026-10-02",
|
||||
"2026-10-02 13:14:15": "2026-10-02",
|
||||
time.Date(2026, 10, 2, 0, 0, 0, 0, time.UTC): "2026-10-02",
|
||||
// The calendar date is taken as the driver returned it: no zone
|
||||
// conversion moves it to the previous or next day.
|
||||
time.Date(2026, 10, 2, 23, 0, 0, 0, time.FixedZone("x", -10*3600)): "2026-10-02",
|
||||
} {
|
||||
if err := d.Scan(src); err != nil || d.String() != want {
|
||||
t.Fatalf("Scan(%v) = %v %v, want %s", src, d, err, want)
|
||||
}
|
||||
}
|
||||
if err := d.Scan([]byte("2026-10-03")); err != nil || d.String() != "2026-10-03" {
|
||||
t.Fatalf("Scan([]byte) = %v %v", d, err)
|
||||
}
|
||||
for _, bad := range []any{"garbage", 42, 3.5, true} {
|
||||
if err := d.Scan(bad); err == nil {
|
||||
t.Fatalf("Scan(%v) must fail", bad)
|
||||
}
|
||||
}
|
||||
var nilDate *Date
|
||||
if err := nilDate.Scan("2026-10-02"); err == nil {
|
||||
t.Fatal("Scan on a nil receiver must fail")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTimeOfDayParseAndText covers the parser, the accessors and the text
|
||||
// and JSON forms.
|
||||
func TestTimeOfDayParseAndText(t *testing.T) {
|
||||
for in, want := range map[string]string{
|
||||
"00:00": "00:00:00",
|
||||
"23:59": "23:59:00",
|
||||
"23:59:59": "23:59:59",
|
||||
"07:08:09.000001": "07:08:09",
|
||||
} {
|
||||
got, err := ParseTimeOfDay(in)
|
||||
if err != nil || got.String() != want {
|
||||
t.Fatalf("ParseTimeOfDay(%q) = %v %v, want %s", in, got, err, want)
|
||||
}
|
||||
}
|
||||
for _, bad := range []string{"", "7", "24:00", "12:60", "12:00:60", "12:00:00.", "12:00:00.1a", "12:00.5", "noon", "12-00"} {
|
||||
if got, err := ParseTimeOfDay(bad); err == nil {
|
||||
t.Errorf("ParseTimeOfDay(%q) = %v, want an error", bad, got)
|
||||
}
|
||||
}
|
||||
if got := NewTimeOfDay(25, 61, 0).String(); got != "02:01:00" {
|
||||
t.Fatalf("NewTimeOfDay wraps to one day: %s", got)
|
||||
}
|
||||
tod := NewTimeOfDay(9, 5, 7)
|
||||
if tod.Hour() != 9 || tod.Minute() != 5 || tod.Second() != 7 {
|
||||
t.Fatalf("accessors %d %d %d", tod.Hour(), tod.Minute(), tod.Second())
|
||||
}
|
||||
text, err := tod.MarshalText()
|
||||
if err != nil || string(text) != "09:05:07" {
|
||||
t.Fatalf("MarshalText %q %v", text, err)
|
||||
}
|
||||
var back TimeOfDay
|
||||
if err := back.UnmarshalText(text); err != nil || back != tod {
|
||||
t.Fatalf("UnmarshalText %v %v", back, err)
|
||||
}
|
||||
if err := back.UnmarshalText(nil); err != nil || !back.IsZero() {
|
||||
t.Fatalf("empty text %v %v", back, err)
|
||||
}
|
||||
if (TimeOfDay{}).String() != "" {
|
||||
t.Fatal("zero TimeOfDay string")
|
||||
}
|
||||
for raw, wantZero := range map[string]bool{`null`: true, `""`: true, `"09:05"`: false} {
|
||||
var got TimeOfDay
|
||||
if err := json.Unmarshal([]byte(raw), &got); err != nil || got.IsZero() != wantZero {
|
||||
t.Fatalf("UnmarshalJSON(%s) = %v %v", raw, got, err)
|
||||
}
|
||||
}
|
||||
var bad TimeOfDay
|
||||
if err := json.Unmarshal([]byte(`930`), &bad); err == nil {
|
||||
t.Fatal("a JSON number must fail")
|
||||
}
|
||||
if err := json.Unmarshal([]byte(`"25:00"`), &bad); err == nil {
|
||||
t.Fatal("an invalid JSON string must fail")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTimeOfDayScan covers every Scan source.
|
||||
func TestTimeOfDayScan(t *testing.T) {
|
||||
var tod TimeOfDay
|
||||
for src, want := range map[any]string{
|
||||
"14:30": "14:30:00",
|
||||
"14:30:15": "14:30:15",
|
||||
time.Date(0, 1, 1, 6, 7, 8, 0, time.UTC): "06:07:08",
|
||||
} {
|
||||
if err := tod.Scan(src); err != nil || tod.String() != want {
|
||||
t.Fatalf("Scan(%v) = %v %v", src, tod, err)
|
||||
}
|
||||
}
|
||||
if err := tod.Scan([]byte("01:02:03")); err != nil || tod.String() != "01:02:03" {
|
||||
t.Fatalf("Scan([]byte) = %v %v", tod, err)
|
||||
}
|
||||
if err := tod.Scan(nil); err != nil || !tod.IsZero() {
|
||||
t.Fatalf("Scan(nil) = %v %v", tod, err)
|
||||
}
|
||||
for _, bad := range []any{"x", []byte("99:00"), 5, false} {
|
||||
if err := tod.Scan(bad); err == nil {
|
||||
t.Fatalf("Scan(%v) must fail", bad)
|
||||
}
|
||||
}
|
||||
var nilTOD *TimeOfDay
|
||||
if err := nilTOD.Scan("10:00"); err == nil {
|
||||
t.Fatal("Scan on a nil receiver must fail")
|
||||
}
|
||||
if v, err := NewTimeOfDay(0, 0, 0).Value(); v != "00:00:00" || err != nil {
|
||||
t.Fatalf("midnight Value = %v %v", v, err)
|
||||
}
|
||||
}
|
||||
|
||||
// dateRoundTrip spells the TIME columns as "time without time zone": GORM
|
||||
// reads a bare `type:time` tag as its own time data type and AutoMigrate
|
||||
// would create a timestamptz column.
|
||||
type dateRoundTrip struct {
|
||||
ID uint `gorm:"column:id;primaryKey"`
|
||||
Day Date `gorm:"column:day;type:date"`
|
||||
DayPtr *Date `gorm:"column:day_ptr;type:date"`
|
||||
Clock TimeOfDay `gorm:"column:clock;type:time without time zone"`
|
||||
ClockPtr *TimeOfDay `gorm:"column:clock_ptr;type:time without time zone"`
|
||||
At *time.Time `gorm:"column:at"`
|
||||
}
|
||||
|
||||
func (dateRoundTrip) TableName() string { return "lagoon_date_round_trip" }
|
||||
|
||||
// TestDatePostgresRoundTrip writes Date and TimeOfDay through real DATE and
|
||||
// TIME columns: set values come back unchanged, the zero values store NULL
|
||||
// and read back as zero, and nil pointers stay nil.
|
||||
func TestDatePostgresRoundTrip(t *testing.T) {
|
||||
db, _ := dedicatedDB(t, "lagoon_date_round_trip")
|
||||
gdb, err := Use(t.Context(), db)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := gdb.AutoMigrate(&dateRoundTrip{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
day := NewDate(2026, 10, 2)
|
||||
clock := NewTimeOfDay(23, 59, 58)
|
||||
at := time.Date(2026, 10, 2, 10, 30, 0, 0, time.FixedZone("CEST", 2*3600))
|
||||
set := dateRoundTrip{Day: day, DayPtr: &day, Clock: clock, ClockPtr: &clock, At: &at}
|
||||
zero := dateRoundTrip{}
|
||||
if err := gdb.Create(&set).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := gdb.Create(&zero).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var nulls int64
|
||||
if err := gdb.Raw(`SELECT COUNT(*) FROM lagoon_date_round_trip WHERE id = ? AND day IS NULL AND day_ptr IS NULL AND clock IS NULL AND clock_ptr IS NULL`, zero.ID).Scan(&nulls).Error; err != nil || nulls != 1 {
|
||||
t.Fatalf("zero values did not store NULL (%d, %v)", nulls, err)
|
||||
}
|
||||
var text string
|
||||
if err := gdb.Raw(`SELECT day::text || ' ' || clock::text FROM lagoon_date_round_trip WHERE id = ?`, set.ID).Scan(&text).Error; err != nil || text != "2026-10-02 23:59:58" {
|
||||
t.Fatalf("stored text %q (%v)", text, err)
|
||||
}
|
||||
var got dateRoundTrip
|
||||
if err := gdb.First(&got, set.ID).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Day != day || got.DayPtr == nil || *got.DayPtr != day || got.Clock != clock || got.ClockPtr == nil || *got.ClockPtr != clock || got.At == nil || !got.At.Equal(at) {
|
||||
t.Fatalf("round trip = %+v", got)
|
||||
}
|
||||
var empty dateRoundTrip
|
||||
if err := gdb.First(&empty, zero.ID).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !empty.Day.IsZero() || empty.DayPtr != nil || !empty.Clock.IsZero() || empty.ClockPtr != nil || empty.At != nil {
|
||||
t.Fatalf("NULL round trip = %+v", empty)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user