package httpapi import ( "context" "encoding/json" "errors" "net/http" "net/http/httptest" "strings" "testing" "time" "evobgp/internal/jobs" "evobgp/internal/store" ) type unavailableJobQueue struct{ store.JobQueue } func (q unavailableJobQueue) EnqueueJob(context.Context, store.JobEnqueue) (store.JobRecord, bool, error) { return store.JobRecord{}, false, errors.New("database unavailable") } func TestJobEnqueueFailureNeverAccepts(t *testing.T) { r := jobs.NewRegistry(nil) r.UseDurableQueue(unavailableJobQueue{}) s := &Server{jobs: r} req := httptest.NewRequest(http.MethodPost, "/v1/bird/reload", nil) req = req.WithContext(context.WithValue(req.Context(), authCtxKey, Auth{TenantID: "tenant", Role: "operator"})) w := httptest.NewRecorder() s.handleBirdReload(w, req) if w.Code == http.StatusAccepted || w.Code < 500 { t.Fatalf("DB failure returned %d: %s", w.Code, w.Body.String()) } } func TestAutomaticEnqueueFailureIsVisible(t *testing.T) { r := jobs.NewRegistry(nil) r.UseDurableQueue(unavailableJobQueue{}) s := &Server{jobs: r} w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/v1/peers", nil) s.enqueuePeerReconcile(w, req, "tenant", "peer_create") if got := w.Header().Get("X-EvoBGP-Job-Status"); got != "enqueue-failed" { t.Fatalf("scheduling failure hidden: %q", got) } } type selectorBackend struct { store.Backend modules []*store.Module reads int } func (b *selectorBackend) ListModules(string) []*store.Module { b.reads++; return b.modules } func TestTenantRefreshReplayBeforeDynamicSelection(t *testing.T) { m := store.NewMemory() b := &selectorBackend{Backend: m, modules: []*store.Module{{ID: "module-a", Enabled: true, RefreshIntervalSec: 1}}} r := jobs.NewRegistry(nil) r.UseDurableQueue(m) s := &Server{store: b, jobs: r} request := func() *httptest.ResponseRecorder { req := httptest.NewRequest(http.MethodPost, "/v1/tenant/refresh", strings.NewReader(`{}`)) req.Header.Set("Idempotency-Key", "same-request") req = req.WithContext(context.WithValue(req.Context(), authCtxKey, Auth{TenantID: "tenant", Role: "editor"})) w := httptest.NewRecorder() s.handleTenantRefresh(w, req) return w } first := request() if first.Code != http.StatusAccepted { t.Fatalf("first=%d %s", first.Code, first.Body.String()) } var accepted map[string]any if err := json.Unmarshal(first.Body.Bytes(), &accepted); err != nil { t.Fatal(err) } claimed, err := m.ClaimJobs(context.Background(), "worker", 1, time.Minute) if err != nil || len(claimed) != 1 { t.Fatalf("claim %+v %v", claimed, err) } claimed[0].Status = "succeeded" if err = m.FinishJob(context.Background(), claimed[0]); err != nil { t.Fatal(err) } b.modules = nil reads := b.reads replay := request() if replay.Code != http.StatusAccepted { t.Fatalf("replay=%d %s", replay.Code, replay.Body.String()) } var got map[string]any if err = json.Unmarshal(replay.Body.Bytes(), &got); err != nil { t.Fatal(err) } if got["job_id"] != accepted["job_id"] || got["status"] != "succeeded" || b.reads != reads { t.Fatalf("replay re-resolved selector: %+v reads %d->%d", got, reads, b.reads) } }