package web import ( "testing" "time" "github.com/m/projax/store" ) func TestParsePER(t *testing.T) { cases := []struct { in string wantBase string wantDate string // empty == nil }{ {"dev.projax", "dev.projax", ""}, {"dev.projax.260515", "dev.projax", "2026-05-15"}, {"mfin.house1.260515", "mfin.house1", "2026-05-15"}, // Six-digit but not a valid date → leave unchanged. {"foo.260230", "foo.260230", ""}, // Feb 30 doesn't exist {"foo.260000", "foo.260000", ""}, // month 00 {"foo.261301", "foo.261301", ""}, // month 13 {"foo.999999", "foo.999999", ""}, // not a real date // Wrong length → leave unchanged. {"foo.bar", "foo.bar", ""}, {"foo.12345", "foo.12345", ""}, {"foo.1234567", "foo.1234567", ""}, // Empty trailing segment. {"foo.", "foo.", ""}, // No dot at all. {"260515", "260515", ""}, } for _, tc := range cases { gotBase, gotDate := parsePER(tc.in) if gotBase != tc.wantBase { t.Errorf("parsePER(%q) base = %q, want %q", tc.in, gotBase, tc.wantBase) } if tc.wantDate == "" { if gotDate != nil { t.Errorf("parsePER(%q) date = %v, want nil", tc.in, gotDate) } continue } want, _ := time.Parse("2006-01-02", tc.wantDate) if gotDate == nil || !gotDate.Equal(want) { t.Errorf("parsePER(%q) date = %v, want %v", tc.in, gotDate, want) } } } func TestCollisionTag(t *testing.T) { cases := []struct { n int want string }{ {0, ""}, {1, "a"}, {2, "b"}, {26, "z"}, {27, "aa"}, {28, "ab"}, {52, "az"}, {53, "ba"}, } for _, tc := range cases { if got := collisionTag(tc.n); got != tc.want { t.Errorf("collisionTag(%d) = %q, want %q", tc.n, got, tc.want) } } } func TestComputePERsBareThenAB(t *testing.T) { d := time.Date(2026, 5, 15, 0, 0, 0, 0, time.UTC) d2 := time.Date(2026, 5, 16, 0, 0, 0, 0, time.UTC) links := []*store.ItemLink{ // 2026-05-15 group: 2 entries (sorted by created_at ASC; first bare, second .a) {ID: "x1", EventDate: &d, CreatedAt: time.Date(2026, 5, 15, 9, 0, 0, 0, time.UTC)}, {ID: "x2", EventDate: &d, CreatedAt: time.Date(2026, 5, 15, 10, 0, 0, 0, time.UTC)}, // 2026-05-16 group: 1 entry (bare). {ID: "x3", EventDate: &d2, CreatedAt: time.Date(2026, 5, 16, 9, 0, 0, 0, time.UTC)}, } // Caller invariant: DatedLinks returns event_date DESC, created_at ASC. // Test data above is created_at ASC; reverse the date groups to match. desc := []*store.ItemLink{links[2], links[0], links[1]} rows := computePERs("mfin.house1", desc) if len(rows) != 3 { t.Fatalf("expected 3 rows, got %d", len(rows)) } if rows[0].PER != "mfin.house1.260516" { t.Errorf("row 0 PER = %q", rows[0].PER) } if rows[1].PER != "mfin.house1.260515" || rows[1].Tag != "" { t.Errorf("row 1 should be bare, got PER=%q tag=%q", rows[1].PER, rows[1].Tag) } if rows[2].PER != "mfin.house1.260515.a" || rows[2].Tag != "a" { t.Errorf("row 2 should be .a, got PER=%q tag=%q", rows[2].PER, rows[2].Tag) } } func TestComputePERsSkipsUndated(t *testing.T) { d := time.Date(2026, 5, 15, 0, 0, 0, 0, time.UTC) links := []*store.ItemLink{ {ID: "with", EventDate: &d, CreatedAt: time.Now()}, {ID: "without", EventDate: nil, CreatedAt: time.Now()}, } rows := computePERs("dev.x", links) if len(rows) != 1 || rows[0].Link.ID != "with" { t.Errorf("undated link should be skipped, got %v", rows) } }