f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
241 lines
5.9 KiB
Go
241 lines
5.9 KiB
Go
package timeutil
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestPtr(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
in time.Time
|
|
want *string
|
|
}{
|
|
{
|
|
name: "zero time returns nil",
|
|
in: time.Time{},
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "non-zero returns RFC3339Nano UTC",
|
|
in: time.Date(2024, 6, 15, 12, 30, 45, 123000000, time.UTC),
|
|
want: new("2024-06-15T12:30:45.123Z"),
|
|
},
|
|
{
|
|
name: "converts to UTC",
|
|
in: time.Date(2024, 6, 15, 7, 30, 0, 0, time.FixedZone("EST", -5*60*60)),
|
|
want: new("2024-06-15T12:30:00Z"),
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := Ptr(tt.in)
|
|
if tt.want == nil {
|
|
assert.Nil(t, got)
|
|
return
|
|
}
|
|
require.NotNil(t, got)
|
|
assert.Equal(t, *tt.want, *got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFormat(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
in time.Time
|
|
want string
|
|
}{
|
|
{"zero time returns empty", time.Time{}, ""},
|
|
{"non-zero returns RFC3339Nano UTC", time.Date(2024, 6, 15, 12, 30, 45, 0, time.UTC), "2024-06-15T12:30:45Z"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert.Equal(t, tt.want, Format(tt.in))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsValidDate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
in string
|
|
want bool
|
|
}{
|
|
{"valid date", "2024-06-15", true},
|
|
{"empty string", "", false},
|
|
{"wrong separator", "2024/06/15", false},
|
|
{"two-digit year", "24-06-15", false},
|
|
{"includes time", "2024-06-15T00:00:00Z", false},
|
|
{"impossible month", "2024-13-01", false},
|
|
{"impossible day", "2024-02-30", false},
|
|
{"non-numeric", "not-a-date", false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert.Equal(t, tt.want, IsValidDate(tt.in))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsValidTimestamp(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
in string
|
|
want bool
|
|
}{
|
|
{"RFC3339 UTC", "2024-06-15T12:30:45Z", true},
|
|
{"RFC3339 offset", "2024-06-15T12:30:45-05:00", true},
|
|
{"RFC3339Nano", "2024-06-15T12:30:45.123456789Z", true},
|
|
{"empty string", "", false},
|
|
{"date only", "2024-06-15", false},
|
|
{"missing timezone", "2024-06-15T12:30:45", false},
|
|
{"non-numeric", "not-a-timestamp", false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert.Equal(t, tt.want, IsValidTimestamp(tt.in))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseSince(t *testing.T) {
|
|
now := time.Date(2026, 3, 15, 10, 0, 0, 0, time.UTC)
|
|
|
|
tests := []struct {
|
|
name string
|
|
in string
|
|
want time.Time
|
|
}{
|
|
{"hours", "3h", now.Add(-3 * time.Hour)},
|
|
{"days", "14d", now.Add(-14 * 24 * time.Hour)},
|
|
{"weeks", "2w", now.Add(-14 * 24 * time.Hour)},
|
|
{"months", "3m", now.AddDate(0, -3, 0)},
|
|
{"years", "1y", now.AddDate(-1, 0, 0)},
|
|
{"single digit unit", "1d", now.Add(-24 * time.Hour)},
|
|
{
|
|
"absolute date is midnight in now's location",
|
|
"2026-01-01",
|
|
time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC),
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := ParseSince(now, tt.in)
|
|
require.NoError(t, err)
|
|
assert.True(t, tt.want.Equal(got),
|
|
"ParseSince(%q) = %v, want %v", tt.in, got, tt.want)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestParseSince_MonthArithmeticCrossesYearBoundary verifies month/year units
|
|
// use calendar-aware AddDate rather than a fixed-duration approximation, so
|
|
// "2m" from mid-January lands in the previous November/year rather than a
|
|
// rough 60-day subtraction.
|
|
func TestParseSince_MonthArithmeticCrossesYearBoundary(t *testing.T) {
|
|
now := time.Date(2026, 1, 15, 8, 30, 0, 0, time.UTC)
|
|
got, err := ParseSince(now, "2m")
|
|
require.NoError(t, err)
|
|
want := time.Date(2025, 11, 15, 8, 30, 0, 0, time.UTC)
|
|
assert.True(t, want.Equal(got), "got %v, want %v", got, want)
|
|
}
|
|
|
|
// TestParseSince_AbsoluteDateUsesNowsLocation verifies the YYYY-MM-DD form
|
|
// resolves to that date's midnight in now's location rather than always UTC.
|
|
func TestParseSince_AbsoluteDateUsesNowsLocation(t *testing.T) {
|
|
ny, err := time.LoadLocation("America/New_York")
|
|
require.NoError(t, err)
|
|
now := time.Date(2026, 3, 15, 10, 0, 0, 0, ny)
|
|
|
|
got, err := ParseSince(now, "2026-01-01")
|
|
require.NoError(t, err)
|
|
want := time.Date(2026, 1, 1, 0, 0, 0, 0, ny)
|
|
assert.True(t, want.Equal(got), "got %v, want %v", got, want)
|
|
assert.Equal(t, ny.String(), got.Location().String())
|
|
}
|
|
|
|
func TestParseSince_RejectsInvalidForms(t *testing.T) {
|
|
now := time.Date(2026, 3, 15, 10, 0, 0, 0, time.UTC)
|
|
|
|
tests := []struct {
|
|
name string
|
|
in string
|
|
}{
|
|
{"empty string", ""},
|
|
{"unknown unit", "3x"},
|
|
{"unit before number", "m3"},
|
|
{"negative number", "-3d"},
|
|
{"zero is not positive", "0d"},
|
|
{"unit only", "d"},
|
|
{"double unit", "3dd"},
|
|
{"decimal number", "3.5d"},
|
|
{"trailing space", "3d "},
|
|
{"leading space", " 3d"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
_, err := ParseSince(now, tt.in)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), tt.in)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBestEffortLocalTimezone(t *testing.T) {
|
|
la, err := time.LoadLocation("America/Los_Angeles")
|
|
require.NoError(t, err)
|
|
berlin, err := time.LoadLocation("Europe/Berlin")
|
|
require.NoError(t, err)
|
|
|
|
tests := []struct {
|
|
name string
|
|
envTZ string
|
|
loc *time.Location
|
|
want string
|
|
}{
|
|
{
|
|
name: "valid env wins over local",
|
|
envTZ: "Europe/Berlin",
|
|
loc: la,
|
|
want: "Europe/Berlin",
|
|
},
|
|
{
|
|
name: "invalid env falls back to local",
|
|
envTZ: "not/a-zone",
|
|
loc: la,
|
|
want: "America/Los_Angeles",
|
|
},
|
|
{
|
|
name: "local sentinel is blank",
|
|
envTZ: "",
|
|
loc: time.FixedZone("Local", 0),
|
|
want: "",
|
|
},
|
|
{
|
|
name: "invalid local name is blank",
|
|
envTZ: "",
|
|
loc: time.FixedZone("DuckLocal", 0),
|
|
want: "",
|
|
},
|
|
{
|
|
name: "valid local IANA name passes through",
|
|
envTZ: "",
|
|
loc: berlin,
|
|
want: "Europe/Berlin",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert.Equal(t, tt.want,
|
|
bestEffortLocalTimezone(tt.envTZ, tt.loc))
|
|
})
|
|
}
|
|
}
|