package chlog import ( "strconv" "strings" "testing" "time" ) var ( tSince = time.Date(2026, 8, 23, 5, 0, 0, 0, time.UTC) tUntil = time.Date(2026, 8, 23, 6, 0, 0, 0, time.UTC) ) func baseFilter() Filter { return Filter{Since: tSince, Until: tUntil} } func TestBuildRequiresBounds(t *testing.T) { if _, err := Build(Filter{Until: tUntil}); err == nil { t.Fatal("expected error when since missing") } if _, err := Build(Filter{Since: tSince}); err == nil { t.Fatal("expected error when until missing") } if _, err := Build(Filter{Since: tUntil, Until: tSince}); err == nil { t.Fatal("expected error when since >= until") } } func TestBuildAlwaysTimeBounded(t *testing.T) { q, err := Build(baseFilter()) if err != nil { t.Fatal(err) } for _, want := range []string{ "timestamp >= fromUnixTimestamp64Milli({since_ms:Int64})", "timestamp < fromUnixTimestamp64Milli({until_ms:Int64})", "ORDER BY timestamp ASC", } { if !strings.Contains(q.SQL, want) { t.Errorf("SQL missing %q:\n%s", want, q.SQL) } } if q.Params["since_ms"] != strconv.FormatInt(tSince.UnixMilli(), 10) { t.Errorf("since_ms = %s", q.Params["since_ms"]) } if q.Params["until_ms"] != strconv.FormatInt(tUntil.UnixMilli(), 10) { t.Errorf("until_ms = %s", q.Params["until_ms"]) } } func TestBuildNoUserInputInSQL(t *testing.T) { f := baseFilter() f.Namespace = "evil'; DROP TABLE logs.raw; --" f.Pattern = "inject{p:String}" f.App = "x' OR 1=1" f.Fields = map[string]string{"k'": "v\""} q, err := Build(f) if err != nil { t.Fatal(err) } for _, needle := range []string{f.Namespace, f.Pattern, f.App, "k'", "v\"", "DROP"} { if strings.Contains(q.SQL, needle) { t.Errorf("user input %q leaked into SQL:\n%s", needle, q.SQL) } } if q.Params["ns"] != f.Namespace || q.Params["pattern"] != f.Pattern { t.Errorf("params missing user values: %v", q.Params) } } func TestBuildFilters(t *testing.T) { f := baseFilter() f.Namespace = "logging" f.Host = "node1" f.Pod = "vector-abc" f.Container = "vector" f.Stream = "stderr" f.Source = "k8s" f.App = "vector" f.Severity = "ERROR" f.Limit = 100 q, err := Build(f) if err != nil { t.Fatal(err) } for clause, param := range map[string]string{ "namespace = {ns:String}": "ns", "host = {host:String}": "host", "pod = {pod:String}": "pod", "container = {container:String}": "container", "stream = {stream:String}": "stream", "source = {source:String}": "source", "labels['app'] = {app:String}": "app", "lowerUTF8(severity) = {severity:String}": "severity", "LIMIT {limit:UInt64}": "limit", } { if !strings.Contains(q.SQL, clause) { t.Errorf("SQL missing %q", clause) } if _, ok := q.Params[param]; !ok { t.Errorf("param %q missing", param) } } if q.Params["severity"] != "error" { t.Errorf("severity not lowercased: %q", q.Params["severity"]) } if q.Params["limit"] != "100" { t.Errorf("limit = %q", q.Params["limit"]) } } func TestBuildEmptyFiltersOmitted(t *testing.T) { q, err := Build(baseFilter()) if err != nil { t.Fatal(err) } _, where, ok := strings.Cut(q.SQL, " WHERE ") if !ok { t.Fatalf("no WHERE clause:\n%s", q.SQL) } for _, clause := range []string{"namespace =", "host =", "pod =", "labels[", "lowerUTF8", "position", "match", "LIMIT"} { if strings.Contains(where, clause) { t.Errorf("unexpected clause %q in WHERE:\n%s", clause, where) } } if len(q.Params) != 2 { t.Errorf("want only time params, got %v", q.Params) } } func TestBuildGrepVariants(t *testing.T) { cases := []struct { regex, ignoreCase bool wantClause string wantPattern string }{ {false, false, "position(message, {pattern:String}) > 0", "Timeout"}, {false, true, "positionCaseInsensitive(message, {pattern:String}) > 0", "Timeout"}, {true, false, "match(message, {pattern:String})", "Timeout"}, {true, true, "match(message, {pattern:String})", "(?i)Timeout"}, } for _, c := range cases { f := baseFilter() f.Pattern = "Timeout" f.Regex = c.regex f.IgnoreCase = c.ignoreCase q, err := Build(f) if err != nil { t.Fatal(err) } if !strings.Contains(q.SQL, c.wantClause) { t.Errorf("regex=%v i=%v: SQL missing %q:\n%s", c.regex, c.ignoreCase, c.wantClause, q.SQL) } if q.Params["pattern"] != c.wantPattern { t.Errorf("regex=%v i=%v: pattern param = %q, want %q", c.regex, c.ignoreCase, q.Params["pattern"], c.wantPattern) } } } func TestBuildFieldsDeterministic(t *testing.T) { f := baseFilter() f.Fields = map[string]string{"b": "2", "a": "1"} q1, err := Build(f) if err != nil { t.Fatal(err) } q2, _ := Build(f) if q1.SQL != q2.SQL { t.Error("field clause order not deterministic") } if !strings.Contains(q1.SQL, "fields[{fk0:String}] = {fv0:String}") || !strings.Contains(q1.SQL, "fields[{fk1:String}] = {fv1:String}") { t.Errorf("field clauses missing:\n%s", q1.SQL) } if q1.Params["fk0"] != "a" || q1.Params["fv0"] != "1" || q1.Params["fk1"] != "b" || q1.Params["fv1"] != "2" { t.Errorf("field params wrong: %v", q1.Params) } } func TestSelective(t *testing.T) { f := baseFilter() if f.Selective() { t.Error("empty filter should not be selective") } for _, set := range []func(*Filter){ func(f *Filter) { f.Namespace = "x" }, func(f *Filter) { f.Host = "x" }, func(f *Filter) { f.App = "x" }, } { g := baseFilter() set(&g) if !g.Selective() { t.Errorf("filter %+v should be selective", g) } } g := baseFilter() g.Pod = "x" g.Container = "x" if g.Selective() { t.Error("pod/container alone should not count as selective") } }