package app import ( "database/sql" "reflect" "testing" "time" ) // helperTestStruct mirrors the shape of the domain structs GetFieldsAndValues // is used against: an ID field that must always be excluded, plus a mix of // field types (string, int, bool, pointer) carrying unrelated db/json struct // tags. GetFieldsAndValues keys off reflect field Name, not struct tags, so // these tags exist purely to prove they have no effect on the result. type helperTestStruct struct { ID int64 `db:"id" json:"id"` Name string `db:"name" json:"full_name"` Age int `db:"age" json:"years"` Active bool `db:"active" json:"is_active"` Score *float64 } func TestGetFieldsAndValues(t *testing.T) { t.Run("excludes ID and zero-value fields, keeps struct tags out of the result", func(t *testing.T) { obj := &helperTestStruct{ ID: 100, Name: "Alice", Age: 30, Active: true, Score: nil, } got := GetFieldsAndValues(obj) wantNames := []string{"Name", "Age", "Active"} if !reflect.DeepEqual(got.fieldNames, wantNames) { t.Fatalf("fieldNames = %v, want %v", got.fieldNames, wantNames) } wantValues := []interface{}{"Alice", 30, true} if !reflect.DeepEqual(got.fieldValues, wantValues) { t.Fatalf("fieldValues = %v, want %v", got.fieldValues, wantValues) } for _, name := range got.fieldNames { if name == "ID" { t.Fatalf("ID field must never be included, got names %v", got.fieldNames) } // Struct tags on this type use snake_case/alternate names // (e.g. "full_name", "is_active"); confirm none of those // leaked into the result in place of the Go field name. if name == "full_name" || name == "is_active" || name == "years" { t.Fatalf("result used a struct tag name instead of the field name: %v", got.fieldNames) } } }) t.Run("ID excluded even when non-zero and all other fields zero", func(t *testing.T) { obj := &helperTestStruct{ID: 42} got := GetFieldsAndValues(obj) // Name is a string field: per IsZero's documented behavior, strings // (including "") are never considered zero, so it is always kept. wantNames := []string{"Name"} if !reflect.DeepEqual(got.fieldNames, wantNames) { t.Fatalf("fieldNames = %v, want %v", got.fieldNames, wantNames) } wantValues := []interface{}{""} if !reflect.DeepEqual(got.fieldValues, wantValues) { t.Fatalf("fieldValues = %v, want %v", got.fieldValues, wantValues) } }) t.Run("non-nil pointer to a zero value is treated as zero and excluded", func(t *testing.T) { zero := 0.0 obj := &helperTestStruct{Name: "x", Score: &zero} got := GetFieldsAndValues(obj) for _, name := range got.fieldNames { if name == "Score" { t.Fatalf("Score pointing at a zero float64 should be excluded, got names %v", got.fieldNames) } } }) t.Run("non-nil pointer to a non-zero value is included as the pointer itself", func(t *testing.T) { score := 87.5 obj := &helperTestStruct{Name: "x", Score: &score} got := GetFieldsAndValues(obj) idx := -1 for i, name := range got.fieldNames { if name == "Score" { idx = i } } if idx == -1 { t.Fatalf("expected Score field to be included, got names %v", got.fieldNames) } gotPtr, ok := got.fieldValues[idx].(*float64) if !ok { t.Fatalf("Score value has type %T, want *float64", got.fieldValues[idx]) } if gotPtr != &score { t.Fatalf("Score value pointer = %p, want the original field pointer %p", gotPtr, &score) } if *gotPtr != score { t.Fatalf("Score value = %v, want %v", *gotPtr, score) } }) t.Run("empty struct fields collapse to an empty result besides always-kept strings", func(t *testing.T) { obj := &helperTestStruct{} got := GetFieldsAndValues(obj) wantNames := []string{"Name"} if !reflect.DeepEqual(got.fieldNames, wantNames) { t.Fatalf("fieldNames = %v, want %v", got.fieldNames, wantNames) } }) } func TestIsZero(t *testing.T) { // numericOnly has no string fields, so its zero value genuinely trips // the struct-recursion base case in IsZero. type numericOnly struct { A int B bool } // nested carries a string field, which per IsZero's documented // "strings are never zero" rule makes the whole struct read as // non-zero even when every field is at its zero value. type nested struct { A int B string } zero := 0 nonZero := 5 tests := []struct { name string v reflect.Value want bool }{ {"empty string is treated as non-zero", reflect.ValueOf(""), false}, {"non-empty string is non-zero", reflect.ValueOf("hi"), false}, {"zero int is zero", reflect.ValueOf(0), true}, {"non-zero int is non-zero", reflect.ValueOf(7), false}, {"false bool is zero", reflect.ValueOf(false), true}, {"true bool is non-zero", reflect.ValueOf(true), false}, {"zero float64 is zero", reflect.ValueOf(0.0), true}, {"non-zero float64 is non-zero", reflect.ValueOf(1.5), false}, {"nil slice is zero (len 0)", reflect.ValueOf([]int(nil)), true}, {"empty non-nil slice is zero (len 0)", reflect.ValueOf([]int{}), true}, {"non-empty slice is non-zero even if elements are zero", reflect.ValueOf([]int{0}), false}, {"zero-length array is zero", reflect.ValueOf([0]int{}), true}, {"non-zero-length array of zero elements is non-zero (len-based check)", reflect.ValueOf([3]int{0, 0, 0}), false}, {"zero-value struct with no string fields is zero", reflect.ValueOf(numericOnly{}), true}, {"struct with any non-zero field is non-zero", reflect.ValueOf(numericOnly{A: 1}), false}, {"struct containing a string field (even empty) is always non-zero", reflect.ValueOf(nested{}), false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := IsZero(tt.v); got != tt.want { t.Errorf("IsZero(%v) = %v, want %v", tt.v, got, tt.want) } }) } t.Run("nil pointer is zero", func(t *testing.T) { var p *int if got := IsZero(reflect.ValueOf(p)); got != true { t.Errorf("IsZero(nil *int) = %v, want true", got) } }) t.Run("non-nil pointer to zero value is zero", func(t *testing.T) { p := &zero if got := IsZero(reflect.ValueOf(p)); got != true { t.Errorf("IsZero(&0) = %v, want true", got) } }) t.Run("non-nil pointer to non-zero value is non-zero", func(t *testing.T) { p := &nonZero if got := IsZero(reflect.ValueOf(p)); got != false { t.Errorf("IsZero(&5) = %v, want false", got) } }) t.Run("nil interface is zero", func(t *testing.T) { var i interface{} v := reflect.ValueOf(&i).Elem() if got := IsZero(v); got != true { t.Errorf("IsZero(nil interface) = %v, want true", got) } }) t.Run("interface holding a zero value is zero", func(t *testing.T) { var i interface{} = 0 v := reflect.ValueOf(&i).Elem() if got := IsZero(v); got != true { t.Errorf("IsZero(interface holding 0) = %v, want true", got) } }) t.Run("interface holding a non-zero value is non-zero", func(t *testing.T) { var i interface{} = 9 v := reflect.ValueOf(&i).Elem() if got := IsZero(v); got != false { t.Errorf("IsZero(interface holding 9) = %v, want false", got) } }) } func TestSqlDateToString(t *testing.T) { t.Run("nil pointer returns nil", func(t *testing.T) { if got := SqlDateToString(nil); got != nil { t.Fatalf("SqlDateToString(nil) = %v, want nil", got) } }) t.Run("invalid NullTime returns nil", func(t *testing.T) { d := &sql.NullTime{Valid: false, Time: time.Date(2024, 3, 5, 9, 15, 30, 0, time.UTC)} if got := SqlDateToString(d); got != nil { t.Fatalf("SqlDateToString(invalid) = %v, want nil", got) } }) t.Run("valid NullTime in UTC formats with the dateTimeFormat literal offset suffix", func(t *testing.T) { d := &sql.NullTime{Valid: true, Time: time.Date(2024, 3, 5, 9, 15, 30, 0, time.UTC)} got := SqlDateToString(d) if got == nil { t.Fatal("SqlDateToString(valid) = nil, want non-nil") } want := "2024-03-05T09:15:30-0800" if *got != want { t.Fatalf("SqlDateToString(valid) = %q, want %q", *got, want) } }) t.Run("valid NullTime in a different zone still carries the literal -0800 suffix", func(t *testing.T) { // dateTimeFormat's "-0800" is not a recognized Go reference-time zone // token (the reference offset is -0700), so Format treats it as a // literal suffix rather than the Time's actual UTC offset. This test // pins down that existing behavior rather than "correct" tz math. est := time.FixedZone("EST", -5*3600) d := &sql.NullTime{Valid: true, Time: time.Date(2024, 3, 5, 9, 15, 30, 0, est)} got := SqlDateToString(d) if got == nil { t.Fatal("SqlDateToString(valid) = nil, want non-nil") } want := "2024-03-05T09:15:30-0800" if *got != want { t.Fatalf("SqlDateToString(valid, EST) = %q, want %q", *got, want) } }) } func TestParseDateTime(t *testing.T) { t.Run("nil pointer returns an error", func(t *testing.T) { got, err := ParseDateTime(nil) if err == nil { t.Fatal("ParseDateTime(nil) error = nil, want error") } if got != nil { t.Fatalf("ParseDateTime(nil) time = %v, want nil", got) } }) t.Run("empty string returns an error", func(t *testing.T) { s := "" if _, err := ParseDateTime(&s); err == nil { t.Fatal("ParseDateTime(\"\") error = nil, want error") } }) t.Run("unparseable string returns an error", func(t *testing.T) { s := "not-a-date" if _, err := ParseDateTime(&s); err == nil { t.Fatal("ParseDateTime(garbage) error = nil, want error") } }) t.Run("dateFormat (date only)", func(t *testing.T) { s := "2024-01-15" got, err := ParseDateTime(&s) if err != nil { t.Fatalf("unexpected error: %v", err) } assertDateTimeParts(t, got, 2024, time.January, 15, 0, 0, 0) }) t.Run("dateTimeFormat requires the literal -0800 suffix", func(t *testing.T) { s := "2024-01-15T10:30:00-0800" got, err := ParseDateTime(&s) if err != nil { t.Fatalf("unexpected error: %v", err) } assertDateTimeParts(t, got, 2024, time.January, 15, 10, 30, 0) }) t.Run("a differently-offset string that matches no supported format errors", func(t *testing.T) { // -0700 does not literal-match dateTimeFormat's "-0800" suffix, has // no weekday/month name for RFC1123, and lacks the colon RFC3339 // requires in its offset - so it matches none of the four formats. s := "2024-01-15T10:30:00-0700" if _, err := ParseDateTime(&s); err == nil { t.Fatal("expected error for an offset not matching any supported format") } }) t.Run("RFC1123", func(t *testing.T) { s := "Mon, 15 Jan 2024 15:04:05 UTC" got, err := ParseDateTime(&s) if err != nil { t.Fatalf("unexpected error: %v", err) } assertDateTimeParts(t, got, 2024, time.January, 15, 15, 4, 5) }) t.Run("RFC3339 with Z", func(t *testing.T) { s := "2024-01-15T10:30:00Z" got, err := ParseDateTime(&s) if err != nil { t.Fatalf("unexpected error: %v", err) } assertDateTimeParts(t, got, 2024, time.January, 15, 10, 30, 0) }) t.Run("RFC3339 with a colon offset", func(t *testing.T) { s := "2024-01-15T10:30:00+05:00" got, err := ParseDateTime(&s) if err != nil { t.Fatalf("unexpected error: %v", err) } assertDateTimeParts(t, got, 2024, time.January, 15, 10, 30, 0) _, offset := got.Zone() if offset != 5*3600 { t.Fatalf("offset = %d seconds, want %d seconds", offset, 5*3600) } }) } // assertDateTimeParts checks the wall-clock components a caller actually // wrote in the input string. It deliberately avoids asserting on // time.Time's Location, since ParseDateTime falls back to time.Local for // formats with no zone info in the string, and that varies by host/CI // timezone configuration - the numeric components do not. func assertDateTimeParts(t *testing.T, got *time.Time, year int, month time.Month, day, hour, min, sec int) { t.Helper() if got == nil { t.Fatal("parsed time is nil") } if got.Year() != year || got.Month() != month || got.Day() != day { t.Fatalf("date = %04d-%02d-%02d, want %04d-%02d-%02d", got.Year(), got.Month(), got.Day(), year, month, day) } if got.Hour() != hour || got.Minute() != min || got.Second() != sec { t.Fatalf("time = %02d:%02d:%02d, want %02d:%02d:%02d", got.Hour(), got.Minute(), got.Second(), hour, min, sec) } }