Files
EvoBGP/internal/pipeline/refresh_aggregate_test.go
T
Denozordec 0428b83eb5
CI / changes (push) Successful in 6s
CI / openapi (push) Has been skipped
CI / go (push) Successful in 38s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / bird2 (push) Successful in 15s
CI / docker-go-prime (push) Successful in 23s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m0s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 2m13s
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m23s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m27s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m23s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m10s
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m24s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m27s
feat: add smart aggregation for IPv4 CIDR prefixes in refresh pipeline. Implement functionality to aggregate prefixes based on community and source attributes, ensuring preservation of BIRD semantics. Enhance test coverage for aggregation logic and module refresh behavior.
2026-04-06 19:53:17 +07:00

122 lines
3.8 KiB
Go

package pipeline
import (
"context"
"net/http"
"testing"
"evobgp/internal/store"
)
func TestRefreshModule_AggregatesAllEnabledModules(t *testing.T) {
t.Setenv("EVOBGP_ASN_RESOLVE", "0")
m := store.NewMemory()
m.SeedDemo()
tenant, _, modIP, _, _ := m.DemoIDs()
for _, mod := range m.ListModules(tenant) {
if mod == nil || mod.ID == modIP {
continue
}
disabled := false
if _, err := m.UpdateModule(tenant, mod.ID, &store.ModulePatch{Enabled: &disabled}); err != nil {
t.Fatal(err)
}
}
mod2, err := m.CreateModule(tenant, &store.Module{Type: "IP_RANGES", Name: "extra-ip", Enabled: true, Priority: 30})
if err != nil {
t.Fatal(err)
}
if _, err := m.CreateIPRangeEntry(tenant, modIP, &store.IPRangeEntry{Prefix: "10.0.0.0/24"}); err != nil {
t.Fatal(err)
}
if _, err := m.CreateIPRangeEntry(tenant, mod2.ID, &store.IPRangeEntry{Prefix: "192.168.0.0/24"}); err != nil {
t.Fatal(err)
}
ctx := context.Background()
before, _, _ := m.ListRevisions(tenant, "", "", 200)
beforeN := len(before)
rev1, err := RefreshModule(ctx, m, http.DefaultClient, tenant, modIP)
if err != nil {
t.Fatal(err)
}
after1, _, _ := m.ListRevisions(tenant, "", "", 200)
if len(after1) != beforeN+1 {
t.Fatalf("first refresh: want one new tenant revision, got %d -> %d", beforeN, len(after1))
}
px, _, _ := m.ListRevisionPrefixes(tenant, rev1, "", 1000)
if len(px) != 2 {
t.Fatalf("first refresh: want 2 aggregated prefixes, got %d: %+v", len(px), px)
}
rev2, err := RefreshModule(ctx, m, http.DefaultClient, tenant, mod2.ID)
if err != nil {
t.Fatal(err)
}
if rev2 == "" {
t.Fatal("second refresh: expected non-empty revision id")
}
after2, _, _ := m.ListRevisions(tenant, "", "", 200)
if len(after2) < len(after1) {
t.Fatalf("second refresh: revisions count should not decrease, had %d now %d", len(after1), len(after2))
}
if _, err := m.CreateIPRangeEntry(tenant, mod2.ID, &store.IPRangeEntry{Prefix: "10.0.1.0/24"}); err != nil {
t.Fatal(err)
}
rev3, err := RefreshModule(ctx, m, http.DefaultClient, tenant, mod2.ID)
if err != nil {
t.Fatal(err)
}
if rev3 == rev1 {
t.Fatal("after prefix change, expected a new revision")
}
after3, _, _ := m.ListRevisions(tenant, "", "", 200)
if len(after3) < len(after2) {
t.Fatalf("third refresh: revisions count should not decrease, had %d now %d", len(after2), len(after3))
}
px3, _, _ := m.ListRevisionPrefixes(tenant, rev3, "", 1000)
if len(px3) != 2 {
t.Fatalf("third refresh: smart aggregation should merge adjacent /24, want 2 prefixes, got %d", len(px3))
}
}
func TestSmartAggregatePrefixRows_RespectsCommunityAndSource(t *testing.T) {
commA := "c-a"
commB := "c-b"
rows := []store.PrefixRow{
{Prefix: "10.0.0.0/24", CommunityID: &commA, Source: "ip_range"},
{Prefix: "10.0.1.0/24", CommunityID: &commA, Source: "ip_range"},
{Prefix: "10.0.2.0/24", CommunityID: &commA, Source: "ip_range"},
{Prefix: "10.0.3.0/24", CommunityID: &commA, Source: "ip_range"},
{Prefix: "10.0.4.0/24", CommunityID: &commB, Source: "ip_range"},
{Prefix: "10.0.5.0/24", CommunityID: &commB, Source: "cdn:x"},
}
out := smartAggregatePrefixRows(rows)
got := make(map[string]struct{}, len(out))
for _, r := range out {
c := ""
if r.CommunityID != nil {
c = *r.CommunityID
}
got[r.Prefix+"|"+c+"|"+r.Source] = struct{}{}
}
// First four /24 collapse into /22 because attributes are identical.
if _, ok := got["10.0.0.0/22|c-a|ip_range"]; !ok {
t.Fatalf("expected merged prefix for c-a/ip_range, got: %+v", out)
}
// Different community/source must stay separate.
if _, ok := got["10.0.4.0/24|c-b|ip_range"]; !ok {
t.Fatalf("expected distinct prefix for c-b/ip_range, got: %+v", out)
}
if _, ok := got["10.0.5.0/24|c-b|cdn:x"]; !ok {
t.Fatalf("expected distinct prefix for c-b/cdn:x, got: %+v", out)
}
if len(out) != 3 {
t.Fatalf("expected 3 resulting rows, got %d: %+v", len(out), out)
}
}