package geo import ( "net" "testing" "git.difuse.io/Difuse/Mellaris/ruleset/builtins/geo/v2geo" ) func TestParseGeoSiteName(t *testing.T) { tests := []struct { input string wantBase string wantAttrs []string }{ {"google", "google", nil}, {"google@ads", "google", []string{"ads"}}, {"google@ads@news", "google", []string{"ads", "news"}}, {" google ", "google", nil}, {" google @ ads ", "google", []string{"ads"}}, {"openai@ ads @ news ", "openai", []string{"ads", "news"}}, {"@onlyattrs", "", []string{"onlyattrs"}}, {"", "", nil}, } for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { base, attrs := parseGeoSiteName(tt.input) if base != tt.wantBase { t.Errorf("parseGeoSiteName(%q) base = %q, want %q", tt.input, base, tt.wantBase) } if len(attrs) != len(tt.wantAttrs) { t.Fatalf("parseGeoSiteName(%q) attrs len = %d, want %d", tt.input, len(attrs), len(tt.wantAttrs)) } for i, attr := range attrs { if attr != tt.wantAttrs[i] { t.Errorf("parseGeoSiteName(%q) attrs[%d] = %q, want %q", tt.input, i, attr, tt.wantAttrs[i]) } } }) } } func TestHostInfo_String(t *testing.T) { h := HostInfo{ Name: "example.com", IPv4: net.ParseIP("1.2.3.4"), IPv6: net.ParseIP("::1"), } want := "example.com|1.2.3.4|::1" if got := h.String(); got != want { t.Errorf("HostInfo.String() = %q, want %q", got, want) } } func TestHostInfo_String_Partial(t *testing.T) { h := HostInfo{ Name: "test.com", IPv4: net.ParseIP("10.0.0.1"), } want := "test.com|10.0.0.1|" if got := h.String(); got != want { t.Errorf("HostInfo.String() = %q, want %q", got, want) } } func TestGeoipMatcher_Match(t *testing.T) { _, n4, _ := net.ParseCIDR("10.0.0.0/8") _, n4_2, _ := net.ParseCIDR("192.168.0.0/16") m := &geoipMatcher{ N4: []*net.IPNet{n4, n4_2}, } tests := []struct { name string host HostInfo want bool }{ {"ipv4 match", HostInfo{IPv4: net.ParseIP("10.1.2.3")}, true}, {"ipv4 no match", HostInfo{IPv4: net.ParseIP("172.16.0.1")}, false}, {"ipv4 match second net", HostInfo{IPv4: net.ParseIP("192.168.1.1")}, true}, {"no ip", HostInfo{}, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := m.Match(tt.host); got != tt.want { t.Errorf("Match() = %v, want %v", got, tt.want) } }) } } func TestGeoipMatcher_Match_Inverse(t *testing.T) { _, n4, _ := net.ParseCIDR("10.0.0.0/8") m := &geoipMatcher{ N4: []*net.IPNet{n4}, Inverse: true, } if m.Match(HostInfo{IPv4: net.ParseIP("10.1.2.3")}) { t.Error("Inverse: inside range should return false") } if !m.Match(HostInfo{IPv4: net.ParseIP("172.16.0.1")}) { t.Error("Inverse: outside range should return true") } if !m.Match(HostInfo{}) { t.Error("Inverse: no IP should return true") } } func TestGeoipMatcher_Match_IPv6(t *testing.T) { _, n6, _ := net.ParseCIDR("2001:db8::/32") m := &geoipMatcher{ N6: []*net.IPNet{n6}, } if !m.Match(HostInfo{IPv6: net.ParseIP("2001:db8::1")}) { t.Error("IPv6 match failed") } if m.Match(HostInfo{IPv6: net.ParseIP("2001:db9::1")}) { t.Error("IPv6 should not match") } } func TestGeositeMatcher_matchDomain_Plain(t *testing.T) { m := &geositeMatcher{} d := geositeDomain{ Type: geositeDomainPlain, Value: "openai", } if !m.matchDomain(d, HostInfo{Name: "api.openai.com"}) { t.Error("plain domain should match via substring") } if m.matchDomain(d, HostInfo{Name: "google.com"}) { t.Error("plain domain should not match unrelated host") } } func TestGeositeMatcher_matchDomain_Full(t *testing.T) { m := &geositeMatcher{} d := geositeDomain{ Type: geositeDomainFull, Value: "example.com", } if !m.matchDomain(d, HostInfo{Name: "example.com"}) { t.Error("full domain should match exact") } if m.matchDomain(d, HostInfo{Name: "www.example.com"}) { t.Error("full domain should not match subdomain") } } func TestGeositeMatcher_matchDomain_Root(t *testing.T) { m := &geositeMatcher{} d := geositeDomain{ Type: geositeDomainRoot, Value: "example.com", } if !m.matchDomain(d, HostInfo{Name: "example.com"}) { t.Error("root domain should match exact") } if !m.matchDomain(d, HostInfo{Name: "www.example.com"}) { t.Error("root domain should match subdomain") } if m.matchDomain(d, HostInfo{Name: "www.example.com.au"}) { t.Error("root domain should not match unrelated suffix") } } func TestGeositeMatcher_matchDomain_Attrs(t *testing.T) { m := &geositeMatcher{Attrs: []string{"ads"}} d := geositeDomain{ Type: geositeDomainPlain, Value: "google", Attrs: map[string]bool{"ads": true}, } if !m.matchDomain(d, HostInfo{Name: "google.com"}) { t.Error("should match when domain has required attr") } dNoAttrs := geositeDomain{ Type: geositeDomainPlain, Value: "google", Attrs: map[string]bool{}, } if m.matchDomain(dNoAttrs, HostInfo{Name: "google.com"}) { t.Error("should not match when domain lacks required attr") } dOtherAttrs := geositeDomain{ Type: geositeDomainPlain, Value: "google", Attrs: map[string]bool{"news": true}, } if m.matchDomain(dOtherAttrs, HostInfo{Name: "google.com"}) { t.Error("should not match when domain has wrong attr") } } func TestGeositeMatcher_Match(t *testing.T) { m := &geositeMatcher{ Domains: []geositeDomain{ {Type: geositeDomainFull, Value: "exact.com"}, {Type: geositeDomainPlain, Value: "partial"}, }, } if !m.Match(HostInfo{Name: "exact.com"}) { t.Error("should match full domain") } if !m.Match(HostInfo{Name: "www.partial.net"}) { t.Error("should match partial domain") } if m.Match(HostInfo{Name: "other.net"}) { t.Error("should not match unrelated host") } } func TestDomainAttributeToMap(t *testing.T) { attrs := []*v2geo.Domain_Attribute{ {Key: "ads"}, {Key: "news"}, } got := domainAttributeToMap(attrs) if len(got) != 2 || !got["ads"] || !got["news"] { t.Errorf("domainAttributeToMap = %v, want {ads:true, news:true}", got) } got2 := domainAttributeToMap(nil) if len(got2) != 0 { t.Errorf("domainAttributeToMap(nil) = %v, want empty map", got2) } } func TestNewGeoIPMatcher(t *testing.T) { list := &v2geo.GeoIP{ Cidr: []*v2geo.CIDR{ {Ip: net.IPv4(10, 0, 0, 0).To4(), Prefix: 8}, {Ip: net.IPv4(192, 168, 0, 0).To4(), Prefix: 16}, }, InverseMatch: false, } m, err := newGeoIPMatcher(list) if err != nil { t.Fatalf("newGeoIPMatcher error: %v", err) } if len(m.N4) != 2 { t.Errorf("expected 2 IPv4 nets, got %d", len(m.N4)) } if m.Inverse { t.Error("Inverse should be false") } // Verify sorted order: 10.0.0.0/8 < 192.168.0.0/16 if m.N4[0].IP.String() != "10.0.0.0" { t.Errorf("N4[0] = %s, want 10.0.0.0", m.N4[0].IP) } if m.N4[1].IP.String() != "192.168.0.0" { t.Errorf("N4[1] = %s, want 192.168.0.0", m.N4[1].IP) } } func TestNewGeoIPMatcher_IPv6(t *testing.T) { list := &v2geo.GeoIP{ Cidr: []*v2geo.CIDR{ {Ip: net.ParseIP("2001:db8::"), Prefix: 32}, }, } m, err := newGeoIPMatcher(list) if err != nil { t.Fatalf("newGeoIPMatcher error: %v", err) } if len(m.N6) != 1 { t.Errorf("expected 1 IPv6 net, got %d", len(m.N6)) } } func TestNewGeoIPMatcher_InvalidIPLength(t *testing.T) { list := &v2geo.GeoIP{ Cidr: []*v2geo.CIDR{ {Ip: []byte{1, 2, 3}, Prefix: 24}, }, } _, err := newGeoIPMatcher(list) if err == nil { t.Error("expected error for invalid IP length") } } func TestNewGeositeMatcher(t *testing.T) { list := &v2geo.GeoSite{ Domain: []*v2geo.Domain{ {Type: v2geo.Domain_Plain, Value: "google"}, {Type: v2geo.Domain_Full, Value: "exact.com"}, }, } m, err := newGeositeMatcher(list, nil) if err != nil { t.Fatalf("newGeositeMatcher error: %v", err) } if len(m.Domains) != 2 { t.Errorf("expected 2 domains, got %d", len(m.Domains)) } } func TestNewGeositeMatcher_WithAttrs(t *testing.T) { list := &v2geo.GeoSite{ Domain: []*v2geo.Domain{ { Type: v2geo.Domain_RootDomain, Value: "google.com", Attribute: []*v2geo.Domain_Attribute{ {Key: "ads"}, }, }, }, } m, err := newGeositeMatcher(list, []string{"ads"}) if err != nil { t.Fatalf("newGeositeMatcher error: %v", err) } if !m.Match(HostInfo{Name: "www.google.com"}) { t.Error("should match with root domain and attr") } }