| 1 | package applog |
| 2 | |
| 3 | import ( |
| 4 | "regexp" |
| 5 | "testing" |
| 6 | "time" |
| 7 | ) |
| 8 | |
| 9 | func at(sec int) time.Time { return time.Date(2026, 8, 10, 12, 0, sec, 0, time.UTC) } |
| 10 | |
| 11 | func TestFilterMatch(t *testing.T) { |
| 12 | rec := Record{ |
| 13 | TS: at(30), Box: "b1", Host: "ledgerpay.bde.oncarlos.com", |
| 14 | Stream: StreamApp, Level: LevelWarn, Msg: "slow query on /checkout", |
| 15 | Fields: map[string]string{"request_id": "r1"}, |
| 16 | } |
| 17 | cases := []struct { |
| 18 | name string |
| 19 | f Filter |
| 20 | want bool |
| 21 | }{ |
| 22 | {"zero filter matches", Filter{}, true}, |
| 23 | {"since inclusive window", Filter{Since: at(30)}, true}, |
| 24 | {"since excludes older", Filter{Since: at(31)}, false}, |
| 25 | {"until excludes newer", Filter{Until: at(29)}, false}, |
| 26 | {"stream match", Filter{Stream: StreamApp}, true}, |
| 27 | {"stream mismatch", Filter{Stream: StreamPlatform}, false}, |
| 28 | {"host match", Filter{Host: "ledgerpay.bde.oncarlos.com"}, true}, |
| 29 | {"host mismatch", Filter{Host: "other"}, false}, |
| 30 | {"level threshold met", Filter{Level: LevelWarn}, true}, |
| 31 | {"level below threshold", Filter{Level: LevelError}, false}, |
| 32 | {"grep on msg", Filter{Grep: regexp.MustCompile(`checkout`)}, true}, |
| 33 | {"grep miss", Filter{Grep: regexp.MustCompile(`nope`)}, false}, |
| 34 | {"field match", Filter{Fields: map[string]string{"request_id": "r1"}}, true}, |
| 35 | {"field mismatch", Filter{Fields: map[string]string{"request_id": "r2"}}, false}, |
| 36 | {"field absent", Filter{Fields: map[string]string{"user": "u"}}, false}, |
| 37 | } |
| 38 | for _, tc := range cases { |
| 39 | t.Run(tc.name, func(t *testing.T) { |
| 40 | if got := tc.f.Match(rec); got != tc.want { |
| 41 | t.Fatalf("Match = %v, want %v", got, tc.want) |
| 42 | } |
| 43 | }) |
| 44 | } |
| 45 | // A record with NO level never satisfies a level threshold — levels are |
| 46 | // never guessed, so it cannot claim to clear a bar it never set. |
| 47 | unleveled := rec |
| 48 | unleveled.Level = "" |
| 49 | if (Filter{Level: LevelDebug}).Match(unleveled) { |
| 50 | t.Fatal("unleveled record passed a level threshold") |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | // TestMergeRecords: cross-box merge orders by (ts, box, seq) — per-box |
| 55 | // order preserved, best-effort across boxes, deterministic tiebreak. |
| 56 | func TestMergeRecords(t *testing.T) { |
| 57 | b1 := []Record{ |
| 58 | {TS: at(1), Box: "b1", Seq: 10, Msg: "b1-first"}, |
| 59 | {TS: at(3), Box: "b1", Seq: 11, Msg: "b1-second"}, |
| 60 | } |
| 61 | b2 := []Record{ |
| 62 | {TS: at(2), Box: "b2", Seq: 5, Msg: "b2-first"}, |
| 63 | {TS: at(3), Box: "a0", Seq: 1, Msg: "a0-tiebreak"}, // same ts as b1-second |
| 64 | } |
| 65 | got := MergeRecords(b1, b2) |
| 66 | want := []string{"b1-first", "b2-first", "a0-tiebreak", "b1-second"} |
| 67 | if len(got) != len(want) { |
| 68 | t.Fatalf("merged %d records", len(got)) |
| 69 | } |
| 70 | for i, m := range want { |
| 71 | if got[i].Msg != m { |
| 72 | t.Fatalf("order[%d] = %q, want %q (full: %+v)", i, got[i].Msg, m, got) |
| 73 | } |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | func TestParseSinceUntil(t *testing.T) { |
| 78 | now := time.Date(2026, 8, 10, 12, 0, 0, 0, time.UTC) |
| 79 | if got, err := ParseTimeArg("15m", now); err != nil || !got.Equal(now.Add(-15*time.Minute)) { |
| 80 | t.Fatalf("duration shorthand: %v %v", got, err) |
| 81 | } |
| 82 | if got, err := ParseTimeArg("2026-08-10T11:00:00Z", now); err != nil || got.Hour() != 11 { |
| 83 | t.Fatalf("RFC3339: %v %v", got, err) |
| 84 | } |
| 85 | if _, err := ParseTimeArg("yesterday-ish", now); err == nil { |
| 86 | t.Fatal("garbage parsed") |
| 87 | } |
| 88 | if got, err := ParseTimeArg("", now); err != nil || !got.IsZero() { |
| 89 | t.Fatalf("empty should be zero time, no error: %v %v", got, err) |
| 90 | } |
| 91 | } |
| 92 | |