| 1 | package applog |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "strings" |
| 6 | "testing" |
| 7 | ) |
| 8 | |
| 9 | // TestFamilyBuildersRefuseBadSegments: no builder in the apps/<sqid>/<app>/ |
| 10 | // families can produce a key from an empty or invalid account or app, or an |
| 11 | // empty or slash-bearing box or hour. The empty app is the regression: an |
| 12 | // app-less route was filed under its account with app "", and the errors |
| 13 | // shipper wrote apps/bab//errors/..., which hostops.Apps refuses to walk. |
| 14 | func TestFamilyBuildersRefuseBadSegments(t *testing.T) { |
| 15 | type build func(acct, app, box, hour string) (string, error) |
| 16 | builders := map[string]build{} |
| 17 | for _, fam := range []Family{FamilyLogs, FamilyErrors, FamilyAnalytics} { |
| 18 | builders["FamilyPrefix/"+string(fam)] = func(acct, app, _, _ string) (string, error) { return FamilyPrefix(fam, acct, app) } |
| 19 | builders["FamilyBoxPrefix/"+string(fam)] = func(acct, app, box, _ string) (string, error) { return FamilyBoxPrefix(fam, acct, app, box) } |
| 20 | builders["FamilyHourPrefix/"+string(fam)] = func(acct, app, box, hour string) (string, error) { |
| 21 | return FamilyHourPrefix(fam, acct, app, box, hour) |
| 22 | } |
| 23 | builders["FamilyChunkKey/"+string(fam)] = func(acct, app, box, hour string) (string, error) { |
| 24 | return FamilyChunkKey(fam, acct, app, box, hour, 1, 2) |
| 25 | } |
| 26 | } |
| 27 | builders["Prefix"] = func(acct, app, _, _ string) (string, error) { return Prefix(acct, app) } |
| 28 | builders["BoxPrefix"] = func(acct, app, box, _ string) (string, error) { return BoxPrefix(acct, app, box) } |
| 29 | builders["HourPrefix"] = func(acct, app, box, hour string) (string, error) { return HourPrefix(acct, app, box, hour) } |
| 30 | builders["ChunkKey"] = func(acct, app, box, hour string) (string, error) { return ChunkKey(acct, app, box, hour, 1, 2) } |
| 31 | |
| 32 | // Each case breaks exactly one segment. A box or hour case only applies |
| 33 | // to builders that take that segment: a prefix above the box level |
| 34 | // never sees the box. |
| 35 | cases := []struct { |
| 36 | name string |
| 37 | acct, app, box, hr string |
| 38 | needsBox, needsHour bool |
| 39 | }{ |
| 40 | {"empty app", "bab", "", "box1", "2026-10-16-06", false, false}, |
| 41 | {"empty account", "", "web", "box1", "2026-10-16-06", false, false}, |
| 42 | {"invalid app", "bab", "Web/x", "box1", "2026-10-16-06", false, false}, |
| 43 | {"invalid account", "b", "web", "box1", "2026-10-16-06", false, false}, |
| 44 | {"empty box", "bab", "web", "", "2026-10-16-06", true, false}, |
| 45 | {"slash in box", "bab", "web", "a/b", "2026-10-16-06", true, false}, |
| 46 | {"empty hour", "bab", "web", "box1", "", false, true}, |
| 47 | {"slash in hour", "bab", "web", "box1", "2026/10", false, true}, |
| 48 | } |
| 49 | for name, b := range builders { |
| 50 | usesBox := !strings.HasPrefix(name, "FamilyPrefix") && name != "Prefix" |
| 51 | usesHour := strings.Contains(name, "Hour") || strings.Contains(name, "ChunkKey") |
| 52 | for _, c := range cases { |
| 53 | if (c.needsBox && !usesBox) || (c.needsHour && !usesHour) { |
| 54 | continue |
| 55 | } |
| 56 | key, err := b(c.acct, c.app, c.box, c.hr) |
| 57 | if !errors.Is(err, ErrBadKeySegment) || key != "" { |
| 58 | t.Errorf("%s with %s = (%q, %v), want (\"\", ErrBadKeySegment)", name, c.name, key, err) |
| 59 | } |
| 60 | } |
| 61 | } |
| 62 | } |
| 63 | |
| 64 | // TestFamilyKeysByteIdentical pins every builder's output for valid names: |
| 65 | // the refusal must not move a single byte of a key that was already good. |
| 66 | func TestFamilyKeysByteIdentical(t *testing.T) { |
| 67 | m := must(t) |
| 68 | for _, c := range []struct{ got, want string }{ |
| 69 | {m(FamilyPrefix(FamilyErrors, "bab", "web")), "apps/bab/web/errors/"}, |
| 70 | {m(FamilyBoxPrefix(FamilyErrors, "bab", "web", "leo-climbing")), "apps/bab/web/errors/leo-climbing/"}, |
| 71 | {m(FamilyHourPrefix(FamilyErrors, "bab", "web", "leo-climbing", "2026-10-16-06")), "apps/bab/web/errors/leo-climbing/2026-10-16-06/"}, |
| 72 | {m(FamilyChunkKey(FamilyErrors, "bab", "web", "leo-climbing", "2026-10-16-06", 1792015319229, 257)), "apps/bab/web/errors/leo-climbing/2026-10-16-06/0001792015319229-257.jsonl.gz"}, |
| 73 | {m(FamilyPrefix(FamilyAnalytics, "bde", "ledgerpay")), "apps/bde/ledgerpay/analytics/"}, |
| 74 | {m(FamilyBoxPrefix(FamilyAnalytics, "bde", "ledgerpay", "box-a")), "apps/bde/ledgerpay/analytics/box-a/"}, |
| 75 | {m(Prefix("bde", "ledgerpay")), "apps/bde/ledgerpay/logs/"}, |
| 76 | {m(BoxPrefix("bde", "ledgerpay", "box1")), "apps/bde/ledgerpay/logs/box1/"}, |
| 77 | {m(HourPrefix("bde", "ledgerpay", "box1", "2026-08-10-21")), "apps/bde/ledgerpay/logs/box1/2026-08-10-21/"}, |
| 78 | {m(ChunkKey("bde", "ledgerpay", "box1", "2026-08-10-21", 41822, 8123)), "apps/bde/ledgerpay/logs/box1/2026-08-10-21/0000000000041822-8123.jsonl.gz"}, |
| 79 | } { |
| 80 | if c.got != c.want { |
| 81 | t.Errorf("key = %q, want %q", c.got, c.want) |
| 82 | } |
| 83 | } |
| 84 | } |
| 85 | |