package store import ( "testing" pb "git.ipng.ch/ipng/nginx-logtail/proto/logtailpb" ) // --- ParseStatusExpr --- func TestParseStatusExprEQ(t *testing.T) { n, op, ok := ParseStatusExpr("200") if !ok || n != 200 || op != pb.StatusOp_EQ { t.Fatalf("got (%d,%v,%v)", n, op, ok) } } func TestParseStatusExprExplicitEQ(t *testing.T) { for _, expr := range []string{"=200", "==200"} { n, op, ok := ParseStatusExpr(expr) if !ok || n != 200 || op != pb.StatusOp_EQ { t.Fatalf("expr %q: got (%d,%v,%v)", expr, n, op, ok) } } } func TestParseStatusExprNE(t *testing.T) { n, op, ok := ParseStatusExpr("!=200") if !ok || n != 200 || op != pb.StatusOp_NE { t.Fatalf("got (%d,%v,%v)", n, op, ok) } } func TestParseStatusExprGE(t *testing.T) { n, op, ok := ParseStatusExpr(">=400") if !ok || n != 400 || op != pb.StatusOp_GE { t.Fatalf("got (%d,%v,%v)", n, op, ok) } } func TestParseStatusExprGT(t *testing.T) { n, op, ok := ParseStatusExpr(">400") if !ok || n != 400 || op != pb.StatusOp_GT { t.Fatalf("got (%d,%v,%v)", n, op, ok) } } func TestParseStatusExprLE(t *testing.T) { n, op, ok := ParseStatusExpr("<=500") if !ok || n != 500 || op != pb.StatusOp_LE { t.Fatalf("got (%d,%v,%v)", n, op, ok) } } func TestParseStatusExprLT(t *testing.T) { n, op, ok := ParseStatusExpr("<500") if !ok || n != 500 || op != pb.StatusOp_LT { t.Fatalf("got (%d,%v,%v)", n, op, ok) } } func TestParseStatusExprEmpty(t *testing.T) { _, _, ok := ParseStatusExpr("") if ok { t.Fatal("expected ok=false for empty string") } } func TestParseStatusExprInvalid(t *testing.T) { for _, expr := range []string{"abc", "!=", ">=", "2xx"} { _, _, ok := ParseStatusExpr(expr) if ok { t.Fatalf("expr %q: expected ok=false", expr) } } } // --- MatchesFilter --- func compiledEQ(status int32) *CompiledFilter { v := status return CompileFilter(&pb.Filter{HttpResponse: &v, StatusOp: pb.StatusOp_EQ}) } func TestMatchesFilterNil(t *testing.T) { if !MatchesFilter(Tuple5{Website: "x"}, nil) { t.Fatal("nil filter should match everything") } if !MatchesFilter(Tuple5{Website: "x"}, &CompiledFilter{}) { t.Fatal("empty compiled filter should match everything") } } func TestMatchesFilterExactWebsite(t *testing.T) { w := "example.com" cf := CompileFilter(&pb.Filter{Website: &w}) if !MatchesFilter(Tuple5{Website: "example.com"}, cf) { t.Fatal("expected match") } if MatchesFilter(Tuple5{Website: "other.com"}, cf) { t.Fatal("expected no match") } } func TestMatchesFilterWebsiteRegex(t *testing.T) { re := "gouda.*" cf := CompileFilter(&pb.Filter{WebsiteRegex: &re}) if !MatchesFilter(Tuple5{Website: "gouda.example.com"}, cf) { t.Fatal("expected match") } if MatchesFilter(Tuple5{Website: "edam.example.com"}, cf) { t.Fatal("expected no match") } } func TestMatchesFilterURIRegex(t *testing.T) { re := "^/api/.*" cf := CompileFilter(&pb.Filter{UriRegex: &re}) if !MatchesFilter(Tuple5{URI: "/api/users"}, cf) { t.Fatal("expected match") } if MatchesFilter(Tuple5{URI: "/health"}, cf) { t.Fatal("expected no match") } } func TestMatchesFilterInvalidRegexMatchesNothing(t *testing.T) { re := "[invalid" cf := CompileFilter(&pb.Filter{WebsiteRegex: &re}) if MatchesFilter(Tuple5{Website: "anything"}, cf) { t.Fatal("invalid regex should match nothing") } } func TestMatchesFilterStatusEQ(t *testing.T) { cf := compiledEQ(200) if !MatchesFilter(Tuple5{Status: "200"}, cf) { t.Fatal("expected match") } if MatchesFilter(Tuple5{Status: "404"}, cf) { t.Fatal("expected no match") } } func TestMatchesFilterStatusNE(t *testing.T) { v := int32(200) cf := CompileFilter(&pb.Filter{HttpResponse: &v, StatusOp: pb.StatusOp_NE}) if MatchesFilter(Tuple5{Status: "200"}, cf) { t.Fatal("expected no match for 200 != 200") } if !MatchesFilter(Tuple5{Status: "404"}, cf) { t.Fatal("expected match for 404 != 200") } } func TestMatchesFilterStatusGE(t *testing.T) { v := int32(400) cf := CompileFilter(&pb.Filter{HttpResponse: &v, StatusOp: pb.StatusOp_GE}) if !MatchesFilter(Tuple5{Status: "400"}, cf) { t.Fatal("expected match: 400 >= 400") } if !MatchesFilter(Tuple5{Status: "500"}, cf) { t.Fatal("expected match: 500 >= 400") } if MatchesFilter(Tuple5{Status: "200"}, cf) { t.Fatal("expected no match: 200 >= 400") } } func TestMatchesFilterStatusLT(t *testing.T) { v := int32(400) cf := CompileFilter(&pb.Filter{HttpResponse: &v, StatusOp: pb.StatusOp_LT}) if !MatchesFilter(Tuple5{Status: "200"}, cf) { t.Fatal("expected match: 200 < 400") } if MatchesFilter(Tuple5{Status: "400"}, cf) { t.Fatal("expected no match: 400 < 400") } } func TestMatchesFilterStatusNonNumeric(t *testing.T) { cf := compiledEQ(200) if MatchesFilter(Tuple5{Status: "ok"}, cf) { t.Fatal("non-numeric status should not match") } } func TestMatchesFilterCombined(t *testing.T) { w := "example.com" v := int32(200) cf := CompileFilter(&pb.Filter{ Website: &w, HttpResponse: &v, StatusOp: pb.StatusOp_EQ, }) if !MatchesFilter(Tuple5{Website: "example.com", Status: "200"}, cf) { t.Fatal("expected match") } if MatchesFilter(Tuple5{Website: "other.com", Status: "200"}, cf) { t.Fatal("expected no match: wrong website") } if MatchesFilter(Tuple5{Website: "example.com", Status: "404"}, cf) { t.Fatal("expected no match: wrong status") } } // --- IsTor label encoding and filtering --- func TestEncodeLabelTupleRoundtripWithTor(t *testing.T) { for _, isTor := range []bool{false, true} { orig := Tuple5{Website: "a.com", Prefix: "1.2.3.0/24", URI: "/x", Status: "200", IsTor: isTor} got := LabelTuple(EncodeTuple(orig)) if got != orig { t.Errorf("roundtrip mismatch: got %+v, want %+v", got, orig) } } } func TestLabelTupleBackwardCompat(t *testing.T) { // Old 4-field label (no is_tor field) should decode with IsTor=false. label := "a.com\x001.2.3.0/24\x00/x\x00200" got := LabelTuple(label) if got.IsTor { t.Errorf("expected IsTor=false for old label, got true") } if got.Website != "a.com" || got.Status != "200" { t.Errorf("unexpected tuple: %+v", got) } } func TestMatchesFilterTorYes(t *testing.T) { cf := CompileFilter(&pb.Filter{Tor: pb.TorFilter_TOR_YES}) if !MatchesFilter(Tuple5{IsTor: true}, cf) { t.Fatal("TOR_YES should match TOR tuple") } if MatchesFilter(Tuple5{IsTor: false}, cf) { t.Fatal("TOR_YES should not match non-TOR tuple") } } func TestMatchesFilterTorNo(t *testing.T) { cf := CompileFilter(&pb.Filter{Tor: pb.TorFilter_TOR_NO}) if !MatchesFilter(Tuple5{IsTor: false}, cf) { t.Fatal("TOR_NO should match non-TOR tuple") } if MatchesFilter(Tuple5{IsTor: true}, cf) { t.Fatal("TOR_NO should not match TOR tuple") } } func TestMatchesFilterTorAny(t *testing.T) { cf := CompileFilter(&pb.Filter{Tor: pb.TorFilter_TOR_ANY}) if !MatchesFilter(Tuple5{IsTor: true}, cf) { t.Fatal("TOR_ANY should match TOR tuple") } if !MatchesFilter(Tuple5{IsTor: false}, cf) { t.Fatal("TOR_ANY should match non-TOR tuple") } }