Files
types/window_test.go
zeekay 1e81bc7b64 a window is a count and a unit, so 90d and all answer
The relative half of ParseWindow was a table of four spellings. A window nobody
had enumerated — 14d, 90d, all — was an unknown range and a 400, on the surface
an operator reads to see where money went. The stores behind these series keep
two years, so the door was refusing questions the data could answer.

Read the count off the label instead: any <N>h or <N>d resolves, day/week/month
still name the common ones, and all reaches back to Horizon, the retention those
stores keep. Every label the table admitted resolves to the very same window, so
the change only adds.

Past the horizon is still a refusal, by name, rather than a window served short:
on a series that carries money a quiet zero reads as "nothing was spent".
2026-08-13 23:56:10 -07:00

406 lines
14 KiB
Go

package types
import (
"fmt"
"strconv"
"strings"
"testing"
"time"
)
var now = time.Date(2026, 7, 26, 15, 4, 5, 0, time.UTC)
func TestParseWindowRelativeLabels(t *testing.T) {
for _, tc := range []struct {
label string
back time.Duration
want Interval
}{
{"", 24 * time.Hour, Hour},
{"24h", 24 * time.Hour, Hour},
{"1d", 24 * time.Hour, Hour},
{"day", 24 * time.Hour, Hour},
{"today", 24 * time.Hour, Hour},
{"7d", 7 * 24 * time.Hour, Day},
{"week", 7 * 24 * time.Hour, Day},
{"30d", 30 * 24 * time.Hour, Day},
{"month", 30 * 24 * time.Hour, Day},
{" 7D ", 7 * 24 * time.Hour, Day},
} {
t.Run(strings.TrimSpace(tc.label), func(t *testing.T) {
w, err := ParseWindow(tc.label, "", "", now)
if err != nil {
t.Fatalf("ParseWindow(%q) errored: %v", tc.label, err)
}
if !w.End.Equal(now) {
t.Errorf("End = %v, want %v", w.End, now)
}
if !w.Start.Equal(now.Add(-tc.back)) {
t.Errorf("Start = %v, want %v", w.Start, now.Add(-tc.back))
}
if w.Interval != tc.want {
t.Errorf("Interval = %q, want %q", w.Interval, tc.want)
}
})
}
}
// The label a response echoes is the caller's, defaulted when absent. Every
// caller used to re-derive this after the call; the window now carries it.
func TestParseWindowLabelIsDefaultedNotCanonicalised(t *testing.T) {
for _, tc := range []struct{ in, want string }{
{"", "24h"},
{" ", "24h"},
{"7d", "7d"},
{" 30d ", "30d"},
{"7D", "7D"},
{"custom", "custom"},
} {
in := tc.in
start := ""
if strings.TrimSpace(in) == "custom" {
start = "2026-07-01T00:00:00Z"
}
w, err := ParseWindow(in, start, "", now)
if err != nil {
t.Fatalf("ParseWindow(%q) errored: %v", in, err)
}
if w.Label != tc.want {
t.Errorf("ParseWindow(%q).Label = %q, want %q", in, w.Label, tc.want)
}
}
}
func TestParseWindowCustom(t *testing.T) {
t.Run("rfc3339 under 48h buckets hourly", func(t *testing.T) {
w, err := ParseWindow("custom", "2026-07-25T00:00:00Z", "2026-07-26T00:00:00Z", now)
if err != nil {
t.Fatalf("errored: %v", err)
}
if w.Interval != Hour {
t.Errorf("Interval = %q, want %q", w.Interval, Hour)
}
if !w.Start.Equal(time.Date(2026, 7, 25, 0, 0, 0, 0, time.UTC)) {
t.Errorf("Start = %v", w.Start)
}
})
t.Run("over 48h buckets daily", func(t *testing.T) {
w, err := ParseWindow("custom", "2026-07-01T00:00:00Z", "2026-07-26T00:00:00Z", now)
if err != nil {
t.Fatalf("errored: %v", err)
}
if w.Interval != Day {
t.Errorf("Interval = %q, want %q", w.Interval, Day)
}
})
t.Run("exactly 48h stays hourly", func(t *testing.T) {
w, err := ParseWindow("custom", "2026-07-24T00:00:00Z", "2026-07-26T00:00:00Z", now)
if err != nil {
t.Fatalf("errored: %v", err)
}
if w.Interval != Hour {
t.Errorf("Interval = %q, want %q", w.Interval, Hour)
}
})
t.Run("unix seconds accepted", func(t *testing.T) {
w, err := ParseWindow("custom", "1753401600", "1753488000", now)
if err != nil {
t.Fatalf("errored: %v", err)
}
if w.Start.Location() != time.UTC || w.End.Location() != time.UTC {
t.Errorf("bounds must be UTC, got %v/%v", w.Start.Location(), w.End.Location())
}
})
t.Run("absent end defaults to now", func(t *testing.T) {
w, err := ParseWindow("custom", "2026-07-25T00:00:00Z", "", now)
if err != nil {
t.Fatalf("errored: %v", err)
}
if !w.End.Equal(now) {
t.Errorf("End = %v, want %v", w.End, now)
}
})
}
// A rejected window must not hand back a zero Window a caller could mistake for
// a valid range.
func TestParseWindowRejects(t *testing.T) {
for _, tc := range []struct{ name, label, start, end, wantErr string }{
{"unknown label", "yesterday", "", "", `unknown range "yesterday"`},
{"unknown label keeps caller casing", "LastWeek", "", "", `unknown range "LastWeek"`},
{"custom without start", "custom", "", "", "custom range requires a valid start: empty time"},
{"custom bad start", "custom", "nonsense", "", `custom range requires a valid start: not RFC3339 or unix seconds: "nonsense"`},
{"custom bad end", "custom", "2026-07-01T00:00:00Z", "nope", `custom range has an invalid end: not RFC3339 or unix seconds: "nope"`},
{"custom end before start", "custom", "2026-07-26T00:00:00Z", "2026-07-01T00:00:00Z", "custom range end must be after start"},
{"custom end equals start", "custom", "2026-07-01T00:00:00Z", "2026-07-01T00:00:00Z", "custom range end must be after start"},
{"custom non-positive unix start", "custom", "0", "", `custom range requires a valid start: not RFC3339 or unix seconds: "0"`},
} {
t.Run(tc.name, func(t *testing.T) {
w, err := ParseWindow(tc.label, tc.start, tc.end, now)
if err == nil {
t.Fatalf("want error, got window %+v", w)
}
if err.Error() != tc.wantErr {
t.Errorf("error = %q, want %q", err.Error(), tc.wantErr)
}
if w != (Window{}) {
t.Errorf("rejected window must be zero, got %+v", w)
}
})
}
}
func TestIntervalStep(t *testing.T) {
if got := Hour.Step(); got != time.Hour {
t.Errorf("Hour.Step() = %v, want %v", got, time.Hour)
}
if got := Day.Step(); got != 24*time.Hour {
t.Errorf("Day.Step() = %v, want %v", got, 24*time.Hour)
}
}
// ParseWindow only ever yields Hour or Day, so a caller interpolating the
// interval into a query has no third case to guard against.
func TestParseWindowOnlyYieldsTheClosedSet(t *testing.T) {
labels := []string{"", "24h", "1d", "day", "today", "7d", "week", "30d", "month", "custom"}
for _, l := range labels {
start := ""
if l == "custom" {
start = "2026-07-01T00:00:00Z"
}
w, err := ParseWindow(l, start, "", now)
if err != nil {
t.Fatalf("ParseWindow(%q) errored: %v", l, err)
}
if w.Interval != Hour && w.Interval != Day {
t.Fatalf("ParseWindow(%q) yielded interval %q outside {hour, day}", l, w.Interval)
}
}
}
// resolveLegacy is the implementation this package replaced, kept verbatim as
// the oracle the parity test compares against.
func resolveLegacy(rangeLabel, startStr, endStr string, now time.Time) (start, end time.Time, interval string, err error) {
parseParam := func(s string) (time.Time, error) {
s = strings.TrimSpace(s)
if s == "" {
return time.Time{}, fmt.Errorf("empty time")
}
if t, err := time.Parse(time.RFC3339, s); err == nil {
return t.UTC(), nil
}
if secs, err := strconv.ParseInt(s, 10, 64); err == nil && secs > 0 {
return time.Unix(secs, 0).UTC(), nil
}
return time.Time{}, fmt.Errorf("not RFC3339 or unix seconds: %q", s)
}
now = now.UTC()
switch strings.ToLower(strings.TrimSpace(rangeLabel)) {
case "", "24h", "1d", "day", "today":
return now.Add(-24 * time.Hour), now, "hour", nil
case "7d", "week":
return now.Add(-7 * 24 * time.Hour), now, "day", nil
case "30d", "month":
return now.Add(-30 * 24 * time.Hour), now, "day", nil
case "custom":
start, err = parseParam(startStr)
if err != nil {
return time.Time{}, time.Time{}, "", fmt.Errorf("custom range requires a valid start: %w", err)
}
if strings.TrimSpace(endStr) == "" {
end = now
} else if end, err = parseParam(endStr); err != nil {
return time.Time{}, time.Time{}, "", fmt.Errorf("custom range has an invalid end: %w", err)
}
if !end.After(start) {
return time.Time{}, time.Time{}, "", fmt.Errorf("custom range end must be after start")
}
interval = "hour"
if end.Sub(start) > 48*time.Hour {
interval = "day"
}
return start.UTC(), end.UTC(), interval, nil
default:
return time.Time{}, time.Time{}, "", fmt.Errorf("unknown range %q", rangeLabel)
}
}
// Bounds, interval and error text must match the replaced implementation
// exactly: these values reach a live dashboard and a 400 body.
func TestParseWindowMatchesTheReplacedImplementation(t *testing.T) {
labels := []string{
"", " ", "24h", "1d", "day", "today", "7d", "week", "30d", "month",
"24H", " 7d ", "custom", "CUSTOM", "yesterday",
}
bounds := []string{
"", " ", "2026-07-01T00:00:00Z", "2026-07-25T12:00:00Z", "2026-07-26T00:00:00Z",
"1753401600", "0", "-5", "nonsense", "2026-07-01",
}
cases := 0
for _, l := range labels {
for _, s := range bounds {
for _, e := range bounds {
cases++
w, gotErr := ParseWindow(l, s, e, now)
wantStart, wantEnd, wantInterval, wantErr := resolveLegacy(l, s, e, now)
switch {
case gotErr == nil && wantErr != nil:
t.Fatalf("ParseWindow(%q,%q,%q) succeeded; legacy errored %v", l, s, e, wantErr)
case gotErr != nil && wantErr == nil:
t.Fatalf("ParseWindow(%q,%q,%q) errored %v; legacy succeeded", l, s, e, gotErr)
case gotErr != nil:
if gotErr.Error() != wantErr.Error() {
t.Fatalf("ParseWindow(%q,%q,%q) error = %q, legacy = %q", l, s, e, gotErr, wantErr)
}
continue
}
if !w.Start.Equal(wantStart) {
t.Fatalf("ParseWindow(%q,%q,%q) Start = %v, legacy = %v", l, s, e, w.Start, wantStart)
}
if !w.End.Equal(wantEnd) {
t.Fatalf("ParseWindow(%q,%q,%q) End = %v, legacy = %v", l, s, e, w.End, wantEnd)
}
if string(w.Interval) != wantInterval {
t.Fatalf("ParseWindow(%q,%q,%q) Interval = %q, legacy = %q", l, s, e, w.Interval, wantInterval)
}
if got, want := w.Interval.Step(), legacyStep(wantInterval); got != want {
t.Fatalf("Step(%q) = %v, legacy = %v", w.Interval, got, want)
}
}
}
}
t.Logf("compared %d label/start/end combinations against the replaced implementation", cases)
}
// legacyStep is the bucket-width helper this package replaced.
func legacyStep(interval string) time.Duration {
if strings.EqualFold(interval, "day") {
return 24 * time.Hour
}
return time.Hour
}
// A count and a unit resolve on their own, so a window nobody enumerated still
// answers. These are the ones a spend view asks for; the point is that the set
// is not a list anyone has to remember to extend.
func TestParseWindowReadsTheCount(t *testing.T) {
for _, tc := range []struct {
label string
back time.Duration
want Interval
}{
{"1h", time.Hour, Hour},
{"6h", 6 * time.Hour, Hour},
{"12h", 12 * time.Hour, Hour},
{"48h", 48 * time.Hour, Hour},
{"49h", 49 * time.Hour, Day},
{"14d", 14 * 24 * time.Hour, Day},
{"60d", 60 * 24 * time.Hour, Day},
{"90d", 90 * 24 * time.Hour, Day},
{"180d", 180 * 24 * time.Hour, Day},
{"365d", 365 * 24 * time.Hour, Day},
{"90D", 90 * 24 * time.Hour, Day},
{" 90d ", 90 * 24 * time.Hour, Day},
{"730d", Horizon, Day},
{"17520h", Horizon, Day},
{"all", Horizon, Day},
{"ALL", Horizon, Day},
} {
t.Run(strings.TrimSpace(tc.label), func(t *testing.T) {
w, err := ParseWindow(tc.label, "", "", now)
if err != nil {
t.Fatalf("ParseWindow(%q) errored: %v", tc.label, err)
}
if !w.End.Equal(now) {
t.Errorf("End = %v, want %v", w.End, now)
}
if !w.Start.Equal(now.Add(-tc.back)) {
t.Errorf("Start = %v, want %v (span %v, want %v)", w.Start, now.Add(-tc.back), w.End.Sub(w.Start), tc.back)
}
if w.Interval != tc.want {
t.Errorf("Interval = %q, want %q", w.Interval, tc.want)
}
})
}
}
// Past the horizon the answer would be a window the store cannot fill, so it is
// refused by name. Serving it short would read as a quiet zero on a spend view.
func TestParseWindowRefusesPastTheHorizon(t *testing.T) {
for _, label := range []string{"731d", "1000d", "17521h", "3650d"} {
t.Run(label, func(t *testing.T) {
w, err := ParseWindow(label, "", "", now)
if err == nil {
t.Fatalf("want a refusal, got window %+v", w)
}
want := fmt.Sprintf("range %q reaches past the 730-day horizon; \"all\" is the longest window", label)
if err.Error() != want {
t.Errorf("error = %q, want %q", err.Error(), want)
}
if w != (Window{}) {
t.Errorf("refused window must be zero, got %+v", w)
}
})
}
}
// A count is read, not guessed: a unit this grammar does not carry stays an
// unknown range rather than becoming some nearby window.
func TestParseWindowStillRefusesNonCounts(t *testing.T) {
for _, label := range []string{"1w", "3mo", "0d", "0h", "-5d", "+5d", "d", "h", "lastweek", "1.5d", "d7"} {
t.Run(label, func(t *testing.T) {
w, err := ParseWindow(label, "", "", now)
if err == nil {
t.Fatalf("want a refusal, got window %+v", w)
}
if want := fmt.Sprintf("unknown range %q", label); err.Error() != want {
t.Errorf("error = %q, want %q", err.Error(), want)
}
if w != (Window{}) {
t.Errorf("refused window must be zero, got %+v", w)
}
})
}
}
// The widening is strictly additive: every label the replaced implementation
// admitted still resolves to the very same window (the oracle test above), and
// every label this one adds is one that used to be a 400. Nothing quietly
// changed meaning underneath a caller.
func TestParseWindowOnlyAddsToWhatWasAdmitted(t *testing.T) {
for _, label := range []string{"1h", "6h", "14d", "90d", "180d", "365d", "all"} {
t.Run(label, func(t *testing.T) {
if _, _, _, err := resolveLegacy(label, "", "", now); err == nil {
t.Fatalf("%q was already admitted; it is not part of the widening", label)
}
if _, err := ParseWindow(label, "", "", now); err != nil {
t.Fatalf("ParseWindow(%q) still refuses: %v", label, err)
}
})
}
}
// Horizon is the retention the warehouses behind these series keep, and "all"
// has to be exactly it — an "all" longer than the store would report empty
// buckets as though nothing had been spent in them.
func TestAllIsTheHorizon(t *testing.T) {
w, err := ParseWindow("all", "", "", now)
if err != nil {
t.Fatalf("errored: %v", err)
}
if got := w.End.Sub(w.Start); got != Horizon {
t.Errorf("all spans %v, want the %v horizon", got, Horizon)
}
if got, want := int(Horizon/(24*time.Hour)), 730; got != want {
t.Errorf("horizon = %d days, want %d", got, want)
}
}