package lookup import ( "context" "errors" "net/netip" "testing" "evobgp/internal/store" ) func TestLookupIPEntryAndSnapshot(t *testing.T) { m := store.NewMemory() m.SeedDemo() tenant, _, modIP, _, _ := m.DemoIDs() cid := "" comms, err := m.ListCommunities(tenant) if err != nil || len(comms) == 0 { t.Fatal("expected demo community") } cid = comms[0].ID def := cid if _, err := m.UpdateModule(tenant, modIP, &store.ModulePatch{DefaultCommunityID: &def}); err != nil { t.Fatal(err) } entryComm := cid e, err := m.CreateIPRangeEntry(tenant, modIP, &store.IPRangeEntry{ Prefix: "203.0.113.0/24", CommunityID: &entryComm, }) if err != nil { t.Fatal(err) } if err := m.SetModulePrefixSnapshot(tenant, modIP, "hash1", []store.PrefixRow{ {Prefix: "203.0.113.0/24", CommunityID: &cid, Source: "ip_range"}, }); err != nil { t.Fatal(err) } res, err := Lookup(context.Background(), m, tenant, "203.0.113.10") if err != nil { t.Fatal(err) } if res.QueryKind != KindIP || res.Normalized != "203.0.113.10" { t.Fatalf("kind/normalized: %+v", res) } if !res.Matched || res.MatchCount < 2 { t.Fatalf("expected entry+snapshot matches, got %+v", res) } var entryHit, snapHit bool for _, hit := range res.Matches { if hit.Layer == LayerEntry && hit.EntryID == e.ID { entryHit = true if hit.Community != "demo-comm" || hit.CommunityTitle != "Demo" { t.Fatalf("entry community: %+v", hit) } } if hit.Layer == LayerSnapshot && hit.MatchedValue == "203.0.113.0/24" { snapHit = true } } if !entryHit || !snapHit { t.Fatalf("missing layers entry=%v snap=%v matches=%+v", entryHit, snapHit, res.Matches) } } func TestLookupIPCommunityFallback(t *testing.T) { m := store.NewMemory() m.SeedDemo() tenant, _, modIP, _, _ := m.DemoIDs() comms, _ := m.ListCommunities(tenant) cid := comms[0].ID if _, err := m.UpdateModule(tenant, modIP, &store.ModulePatch{DefaultCommunityID: &cid}); err != nil { t.Fatal(err) } if _, err := m.CreateIPRangeEntry(tenant, modIP, &store.IPRangeEntry{Prefix: "10.0.0.0/8"}); err != nil { t.Fatal(err) } res, err := Lookup(context.Background(), m, tenant, "10.1.2.3") if err != nil { t.Fatal(err) } if !res.Matched { t.Fatal("expected match") } found := false for _, hit := range res.Matches { if hit.Layer == LayerEntry { found = true if hit.CommunityID == nil || *hit.CommunityID != cid { t.Fatalf("expected default community, got %+v", hit) } if hit.Community != "demo-comm" { t.Fatalf("community value: %+v", hit) } } } if !found { t.Fatal("no entry match") } } func TestLookupDomainEntryAndSnapshot(t *testing.T) { m := store.NewMemory() m.SeedDemo() tenant, _, _, _, _ := m.DemoIDs() mod, err := m.CreateModule(tenant, &store.Module{ Type: "DOMAINS", Name: "demo-domains", Enabled: true, }) if err != nil { t.Fatal(err) } comms, _ := m.ListCommunities(tenant) cid := comms[0].ID e, err := m.CreateDomainEntry(tenant, mod.ID, &store.DomainEntry{ FQDN: "Example.COM.", CommunityID: &cid, }) if err != nil { t.Fatal(err) } if err := m.SetModulePrefixSnapshot(tenant, mod.ID, "hash-d", []store.PrefixRow{ {Prefix: "198.51.100.1/32", CommunityID: &cid, Source: "domain"}, {Prefix: "203.0.113.9/32", CommunityID: &cid, Source: "other"}, }); err != nil { t.Fatal(err) } noDNS := func(context.Context, string) ([]netip.Addr, error) { return nil, nil } res, err := LookupWithResolver(context.Background(), m, tenant, "example.com", noDNS) if err != nil { t.Fatal(err) } if res.QueryKind != KindDomain || res.Normalized != "example.com" { t.Fatalf("kind/normalized: %+v", res) } if !res.Matched { t.Fatal("expected match") } var entryHit, snapHit, otherSnap bool for _, hit := range res.Matches { if hit.Layer == LayerEntry && hit.EntryID == e.ID { entryHit = true } if hit.Layer == LayerSnapshot && hit.MatchedValue == "198.51.100.1/32" { snapHit = true } if hit.MatchedValue == "203.0.113.9/32" { otherSnap = true } } if !entryHit || !snapHit { t.Fatalf("entry=%v snap=%v matches=%+v", entryHit, snapHit, res.Matches) } if otherSnap { t.Fatal("non-domain snapshot source should be excluded") } } func TestLookupDomainResolvedIPAgainstRanges(t *testing.T) { m := store.NewMemory() m.SeedDemo() tenant, _, modIP, _, _ := m.DemoIDs() comms, _ := m.ListCommunities(tenant) cid := comms[0].ID if _, err := m.UpdateModule(tenant, modIP, &store.ModulePatch{DefaultCommunityID: &cid}); err != nil { t.Fatal(err) } if _, err := m.CreateIPRangeEntry(tenant, modIP, &store.IPRangeEntry{ Prefix: "203.0.113.0/24", CommunityID: &cid, }); err != nil { t.Fatal(err) } if err := m.SetModulePrefixSnapshot(tenant, modIP, "hash-r", []store.PrefixRow{ {Prefix: "203.0.113.0/24", CommunityID: &cid, Source: "ip_range"}, }); err != nil { t.Fatal(err) } fake := func(_ context.Context, host string) ([]netip.Addr, error) { if host != "google.com" { t.Fatalf("unexpected host %q", host) } return []netip.Addr{netip.MustParseAddr("203.0.113.50")}, nil } res, err := LookupWithResolver(context.Background(), m, tenant, "google.com", fake) if err != nil { t.Fatal(err) } if res.QueryKind != KindDomain { t.Fatalf("kind: %+v", res) } if len(res.ResolvedIPs) != 1 || res.ResolvedIPs[0] != "203.0.113.50" { t.Fatalf("resolved_ips: %+v", res.ResolvedIPs) } if !res.Matched { t.Fatalf("expected IP membership via resolve, got %+v", res) } var viaResolve bool for _, hit := range res.Matches { if hit.ResolvedIP == "203.0.113.50" && hit.MatchedValue == "203.0.113.0/24" { viaResolve = true } } if !viaResolve { t.Fatalf("missing resolved-ip match: %+v", res.Matches) } } func TestLookupCIDREntryAndSnapshot(t *testing.T) { m := store.NewMemory() m.SeedDemo() tenant, _, modIP, _, _ := m.DemoIDs() comms, _ := m.ListCommunities(tenant) cid := comms[0].ID e, err := m.CreateIPRangeEntry(tenant, modIP, &store.IPRangeEntry{ Prefix: "203.0.113.0/24", CommunityID: &cid, }) if err != nil { t.Fatal(err) } if err := m.SetModulePrefixSnapshot(tenant, modIP, "hash-cidr", []store.PrefixRow{ {Prefix: "203.0.113.0/24", CommunityID: &cid, Source: "ip_range"}, }); err != nil { t.Fatal(err) } res, err := Lookup(context.Background(), m, tenant, "203.0.113.0/24") if err != nil { t.Fatal(err) } if res.QueryKind != KindCIDR || res.Normalized != "203.0.113.0/24" { t.Fatalf("kind/normalized: %+v", res) } if !res.Matched || res.MatchCount < 2 { t.Fatalf("expected entry+snapshot, got %+v", res) } var entryHit, snapHit bool for _, hit := range res.Matches { if hit.Layer == LayerEntry && hit.EntryID == e.ID { entryHit = true } if hit.Layer == LayerSnapshot && hit.MatchedValue == "203.0.113.0/24" { snapHit = true } } if !entryHit || !snapHit { t.Fatalf("entry=%v snap=%v matches=%+v", entryHit, snapHit, res.Matches) } // Narrower query covered by wider entry. res2, err := Lookup(context.Background(), m, tenant, "203.0.113.128/25") if err != nil { t.Fatal(err) } if res2.QueryKind != KindCIDR || !res2.Matched { t.Fatalf("expected cover match: %+v", res2) } } func TestLookupNoMatch(t *testing.T) { m := store.NewMemory() m.SeedDemo() tenant, _, _, _, _ := m.DemoIDs() res, err := Lookup(context.Background(), m, tenant, "192.0.2.1") if err != nil { t.Fatal(err) } if res.Matched || res.MatchCount != 0 || len(res.Matches) != 0 { t.Fatalf("expected empty: %+v", res) } } func TestLookupInvalid(t *testing.T) { m := store.NewMemory() m.SeedDemo() tenant, _, _, _, _ := m.DemoIDs() ctx := context.Background() _, err := Lookup(ctx, m, tenant, "") if !errors.Is(err, store.ErrInvalidInput) { t.Fatalf("empty: %v", err) } _, err = Lookup(ctx, m, tenant, "not a host") if !errors.Is(err, store.ErrInvalidInput) { t.Fatalf("spaces: %v", err) } _, err = Lookup(ctx, m, tenant, "localhost") if !errors.Is(err, store.ErrInvalidInput) { t.Fatalf("single label: %v", err) } } func TestNormalizeFQDN(t *testing.T) { got, ok := normalizeFQDN(" Example.COM. ") if !ok || got != "example.com" { t.Fatalf("got %q ok=%v", got, ok) } }