- Date (DATE) and TimeOfDay (TIME) with Scanner, Valuer, JSON and text forms - Fill falls back to encoding.TextUnmarshaler for string sources after every existing conversion, so time.Time and the new types fill from JSON - required treats a zero time.Time, Date or TimeOfDay as empty - lagoon README, models and casts-and-validation docs
138 lines
4.4 KiB
Go
138 lines
4.4 KiB
Go
package lagoon
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestDateSmoke(t *testing.T) {
|
|
d, err := ParseDate("2026-10-02")
|
|
if err != nil || d.String() != "2026-10-02" || d.IsZero() {
|
|
t.Fatalf("parse: %v %v", d, err)
|
|
}
|
|
if _, err := ParseDate("2026-10-02T00:00:00Z"); err == nil {
|
|
t.Fatal("ParseDate must accept only YYYY-MM-DD")
|
|
}
|
|
b, err := json.Marshal(struct {
|
|
Day Date `json:"day"`
|
|
None *Date `json:"none"`
|
|
Zero Date `json:"zero"`
|
|
}{Day: d})
|
|
if err != nil || string(b) != `{"day":"2026-10-02","none":null,"zero":null}` {
|
|
t.Fatalf("marshal %s %v", b, err)
|
|
}
|
|
var back struct {
|
|
Day Date `json:"day"`
|
|
}
|
|
if err := json.Unmarshal([]byte(`{"day":"2026-10-02"}`), &back); err != nil || back.Day != d {
|
|
t.Fatalf("unmarshal %v %v", back.Day, err)
|
|
}
|
|
|
|
// pgx returns DATE as UTC midnight; the calendar date is kept whatever
|
|
// the process time zone.
|
|
var scanned Date
|
|
if err := scanned.Scan(time.Date(2026, 10, 2, 0, 0, 0, 0, time.UTC)); err != nil || scanned != d {
|
|
t.Fatalf("scan time %v %v", scanned, err)
|
|
}
|
|
if err := scanned.Scan("2026-10-03"); err != nil || scanned.String() != "2026-10-03" {
|
|
t.Fatalf("scan string %v %v", scanned, err)
|
|
}
|
|
if err := scanned.Scan(nil); err != nil || !scanned.IsZero() {
|
|
t.Fatalf("scan nil %v %v", scanned, err)
|
|
}
|
|
if v, err := (Date{}).Value(); v != nil || err != nil {
|
|
t.Fatalf("zero value %v %v", v, err)
|
|
}
|
|
if v, err := d.Value(); v != "2026-10-02" || err != nil {
|
|
t.Fatalf("value %v %v", v, err)
|
|
}
|
|
if got := NewDate(2026, 9, 31).String(); got != "2026-10-01" {
|
|
t.Fatalf("NewDate normalises: %s", got)
|
|
}
|
|
if got := DateOf(time.Date(2026, 10, 2, 23, 30, 0, 0, time.FixedZone("x", -7*3600))).String(); got != "2026-10-02" {
|
|
t.Fatalf("DateOf keeps the location's date: %s", got)
|
|
}
|
|
}
|
|
|
|
func TestTimeOfDaySmoke(t *testing.T) {
|
|
tod, err := ParseTimeOfDay("14:30")
|
|
if err != nil || tod.String() != "14:30:00" {
|
|
t.Fatalf("parse: %v %v", tod, err)
|
|
}
|
|
if tod, err := ParseTimeOfDay("14:30:15.987654"); err != nil || tod.String() != "14:30:15" {
|
|
t.Fatalf("fraction: %v %v", tod, err)
|
|
}
|
|
if _, err := ParseTimeOfDay("25:00"); err == nil {
|
|
t.Fatal("25:00 must fail")
|
|
}
|
|
midnight := NewTimeOfDay(0, 0, 0)
|
|
if midnight.IsZero() {
|
|
t.Fatal("midnight is a set value")
|
|
}
|
|
b, err := json.Marshal([]TimeOfDay{tod, {}})
|
|
if err != nil || string(b) != `["14:30:00",null]` {
|
|
t.Fatalf("marshal %s %v", b, err)
|
|
}
|
|
var scanned TimeOfDay
|
|
if err := scanned.Scan("09:05:07"); err != nil || scanned.Hour() != 9 || scanned.Minute() != 5 || scanned.Second() != 7 {
|
|
t.Fatalf("scan %v %v", scanned, err)
|
|
}
|
|
if v, err := (TimeOfDay{}).Value(); v != nil || err != nil {
|
|
t.Fatalf("zero value %v %v", v, err)
|
|
}
|
|
}
|
|
|
|
type fillDates struct {
|
|
At time.Time `gorm:"column:at"`
|
|
AtPtr *time.Time `gorm:"column:at_ptr"`
|
|
Day Date `gorm:"column:day"`
|
|
DayPtr *Date `gorm:"column:day_ptr"`
|
|
Clock TimeOfDay `gorm:"column:clock"`
|
|
ClkPtr *TimeOfDay `gorm:"column:clock_ptr"`
|
|
}
|
|
|
|
func TestFillTextSmoke(t *testing.T) {
|
|
var m fillDates
|
|
allowed := []string{"at", "at_ptr", "day", "day_ptr", "clock", "clock_ptr"}
|
|
err := Fill(&m, allowed, map[string]any{
|
|
"at": "2026-10-02T12:30:00Z",
|
|
"at_ptr": "2026-10-02T14:30:00+02:00",
|
|
"day": "2026-10-02",
|
|
"day_ptr": "2026-10-03",
|
|
"clock": "14:30",
|
|
"clock_ptr": "08:15:00",
|
|
}, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := time.Date(2026, 10, 2, 12, 30, 0, 0, time.UTC)
|
|
if !m.At.Equal(want) || m.AtPtr == nil || !m.AtPtr.Equal(want) {
|
|
t.Fatalf("times %v %v", m.At, m.AtPtr)
|
|
}
|
|
if m.Day.String() != "2026-10-02" || m.DayPtr == nil || m.DayPtr.String() != "2026-10-03" {
|
|
t.Fatalf("dates %v %v", m.Day, m.DayPtr)
|
|
}
|
|
if m.Clock.String() != "14:30:00" || m.ClkPtr == nil || m.ClkPtr.String() != "08:15:00" {
|
|
t.Fatalf("clocks %v %v", m.Clock, m.ClkPtr)
|
|
}
|
|
err = Fill(&m, allowed, map[string]any{"day": "next tuesday"}, true)
|
|
var fte *FillTypeError
|
|
if !errors.As(err, &fte) || fte.Key != "day" {
|
|
t.Fatalf("garbage date: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateRequiredZeroDateSmoke(t *testing.T) {
|
|
rules := map[string]string{"day": "required"}
|
|
errs, err := Validate(t.Context(), nil, &fillDates{}, rules, map[string]any{"day": Date{}}, nil)
|
|
if err != nil || len(errs["day"]) != 1 {
|
|
t.Fatalf("zero date: %v %v", errs, err)
|
|
}
|
|
errs, err = Validate(t.Context(), nil, &fillDates{}, rules, map[string]any{"day": NewDate(2026, 10, 2)}, nil)
|
|
if err != nil || errs != nil {
|
|
t.Fatalf("set date: %v %v", errs, err)
|
|
}
|
|
}
|