Files
EvoBGP/internal/httpapi/job_queue_test.go
T
DenozordecandCursor f8e0520817 feat(httpapi): harden control plane and speed up prefix refresh
Очередь job получила lease и fencing, телеметрия и прогресс deploy сохраняются, ревизия пишет manifest входов. Обновление префиксов не переписывает неизменённый снимок, большие наборы идут через COPY, а устаревшая ревизия не попадает в deploy. Бандл ноды кэшируется, prune и постановка job больше не держат горячий путь.

Co-authored-by: Cursor <[email protected]>
2026-11-02 17:23:59 +07:00

100 lines
3.1 KiB
Go

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)
}
}