package pipeline import ( "context" "net/http" "net/http/httptest" "strings" "sync/atomic" "testing" "time" "evobgp/internal/store" ) func TestResolveASNForEntry_UsesTTLCache(t *testing.T) { m := store.NewMemory() t.Setenv("EVOBGP_ASN_CACHE_TTL_SEC", "3600") var calls atomic.Int32 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls.Add(1) if strings.Contains(r.URL.Path, "/announced") { _, _ = w.Write([]byte(`{"status":"ok","data":{"prefixes":[{"prefix":"203.0.113.0/24"}]}}`)) return } _, _ = w.Write([]byte(`{"status":"ok","data":{"holder":"Test AS"}}`)) })) defer srv.Close() t.Setenv("EVOBGP_RIPESTAT_ANNOUNCED_PREFIXES_URL", srv.URL+"/announced") t.Setenv("EVOBGP_RIPESTAT_AS_OVERVIEW_URL", srv.URL+"/overview") ctx := context.Background() hc := srv.Client() p1, h1, err := resolveASNForEntry(ctx, m, hc, 64512) if err != nil { t.Fatal(err) } if len(p1) != 1 || h1 != "Test AS" { t.Fatalf("unexpected first resolve: %+v holder=%q", p1, h1) } if calls.Load() < 2 { t.Fatalf("expected ripestat calls on first resolve, got %d", calls.Load()) } firstCalls := calls.Load() _, _, err = resolveASNForEntry(ctx, m, hc, 64512) if err != nil { t.Fatal(err) } if calls.Load() != firstCalls { t.Fatalf("expected cache hit (no new HTTP), calls went from %d to %d", firstCalls, calls.Load()) } } func TestResolveASNForEntry_HolderFailureIsNonFatal(t *testing.T) { m := store.NewMemory() t.Setenv("EVOBGP_ASN_CACHE_TTL_SEC", "1") var holderCalls atomic.Int32 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.Contains(r.URL.Path, "/announced") { _, _ = w.Write([]byte(`{"status":"ok","data":{"prefixes":[{"prefix":"203.0.113.0/24"}]}}`)) return } holderCalls.Add(1) w.WriteHeader(http.StatusInternalServerError) })) defer srv.Close() t.Setenv("EVOBGP_RIPESTAT_ANNOUNCED_PREFIXES_URL", srv.URL+"/announced") t.Setenv("EVOBGP_RIPESTAT_AS_OVERVIEW_URL", srv.URL+"/overview") ctx := context.Background() hc := srv.Client() pfxs, holder, err := resolveASNForEntry(ctx, m, hc, 64512) if err != nil { t.Fatalf("holder failure must not fail ingest: %v", err) } if len(pfxs) != 1 { t.Fatalf("expected 1 prefix, got %+v", pfxs) } if holder != "" { t.Fatalf("expected empty holder on upstream failure, got %q", holder) } if holderCalls.Load() == 0 { t.Fatal("expected holder endpoint to be attempted") } } func TestResolveASNForEntry_KeepsPreviousHolderOnFailure(t *testing.T) { m := store.NewMemory() t.Setenv("EVOBGP_ASN_CACHE_TTL_SEC", "1") t.Setenv("EVOBGP_ASN_HOLDER_TTL_SEC", "3600") holderOK := atomic.Bool{} holderOK.Store(true) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.Contains(r.URL.Path, "/announced") { _, _ = w.Write([]byte(`{"status":"ok","data":{"prefixes":[{"prefix":"203.0.113.0/24"}]}}`)) return } if holderOK.Load() { _, _ = w.Write([]byte(`{"status":"ok","data":{"holder":"Good AS"}}`)) return } w.WriteHeader(http.StatusInternalServerError) })) defer srv.Close() t.Setenv("EVOBGP_RIPESTAT_ANNOUNCED_PREFIXES_URL", srv.URL+"/announced") t.Setenv("EVOBGP_RIPESTAT_AS_OVERVIEW_URL", srv.URL+"/overview") ctx := context.Background() hc := srv.Client() if _, h, err := resolveASNForEntry(ctx, m, hc, 64512); err != nil || h != "Good AS" { t.Fatalf("first resolve: holder=%q err=%v", h, err) } // Prefix cache expired; holder endpoint now broken — previous holder must survive. holderOK.Store(false) time.Sleep(1100 * time.Millisecond) pfxs, h, err := resolveASNForEntry(ctx, m, hc, 64512) if err != nil { t.Fatal(err) } if len(pfxs) != 1 { t.Fatalf("expected 1 prefix, got %+v", pfxs) } if h != "Good AS" { t.Fatalf("expected previous holder preserved, got %q", h) } } func TestModuleDueForScheduler_BucketRollover(t *testing.T) { mod := &store.Module{ID: "mod-jitter", Enabled: true, Type: "CDN_CIDRS", RefreshIntervalSec: 300} win := int64(300) off := moduleSchedulerOffset(mod.ID, win) boundary := time.Unix(win+off, 0) if !ModuleDueForScheduler(mod, boundary) { t.Fatal("expected due when refresh bucket rolls") } mid := time.Unix(win+off+SchedulerTickSec, 0) if ModuleDueForScheduler(mod, mid) { t.Fatal("expected not due within same bucket") } }