Files
EvoBGP/internal/httpapi/bootstrap_outbound_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

111 lines
3.2 KiB
Go

package httpapi
import (
"context"
"encoding/base64"
"net"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"time"
"evobgp/internal/httpclient"
"evobgp/internal/jobs"
"evobgp/internal/pipeline"
"evobgp/internal/store"
"github.com/miekg/dns"
)
func TestBootstrapWorkerPreservesExplicitPrivateDOH(t *testing.T) {
t.Setenv("EVOBGP_NODE_DISPATCH_ENABLED", "0")
t.Setenv("EVOBGP_BIRDC_SOCKET", "")
t.Setenv("EVOBGP_CDN_ALLOW_PRIVATE", "0")
var calls atomic.Int32
resolver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls.Add(1)
raw, err := base64.RawURLEncoding.DecodeString(r.URL.Query().Get("dns"))
if err != nil {
w.WriteHeader(400)
return
}
query := new(dns.Msg)
if err = query.Unpack(raw); err != nil || len(query.Question) == 0 {
w.WriteHeader(400)
return
}
answer := new(dns.Msg)
answer.SetReply(query)
if query.Question[0].Qtype == dns.TypeA {
answer.Answer = []dns.RR{&dns.A{Hdr: dns.RR_Header{Name: query.Question[0].Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 60}, A: net.IPv4(192, 0, 2, 20)}}
}
body, err := answer.Pack()
if err != nil {
t.Error(err)
return
}
w.Header().Set("Content-Type", "application/dns-message")
_, _ = w.Write(body)
}))
defer resolver.Close()
st, reg, _, err := BootstrapWorkers(context.Background(), Options{SeedDemo: true})
if err != nil {
t.Fatal(err)
}
defer func() {
reg.RequestCancelAll()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if e := reg.Drain(ctx); e != nil {
t.Error(e)
}
}()
tenant, _, _, _, _ := st.DemoIDs()
profile, err := st.CreateDohProfile(tenant, &store.DohProfile{Name: "explicit private resolver", URL: resolver.URL})
if err != nil {
t.Fatal(err)
}
mod, err := st.CreateModule(tenant, &store.Module{Name: "private-doh", Type: "DOMAINS", Enabled: true, DohProfileIDs: []string{profile.ID}})
if err != nil {
t.Fatal(err)
}
if _, err = st.CreateDomainEntry(tenant, mod.ID, &store.DomainEntry{FQDN: "private-resolver.example.test"}); err != nil {
t.Fatal(err)
}
j, _, err := reg.Enqueue(tenant, jobs.KindModuleRefresh, nil, &mod.ID, map[string]any{"module_id": mod.ID})
if err != nil {
t.Fatal(err)
}
deadline := time.After(5 * time.Second)
ticker := time.NewTicker(5 * time.Millisecond)
defer ticker.Stop()
for {
snap := j.Snapshot()
status := snap["status"]
if status == jobs.StatusSucceeded {
break
}
if status == jobs.StatusFailed || status == jobs.StatusCancelled {
t.Fatalf("worker refresh failed: %+v", snap)
}
select {
case <-deadline:
t.Fatal("worker did not finish")
case <-ticker.C:
}
}
if calls.Load() < 2 {
t.Fatalf("bootstrap worker blocked private DoH: %d requests", calls.Load())
}
// The same general-client choice must not remove CDN's independent ACL.
cdn := httptest.NewTLSServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { t.Error("private CDN reached") }))
defer cdn.Close()
req, err := http.NewRequest(http.MethodGet, cdn.URL, nil)
if err != nil {
t.Fatal(err)
}
if _, err = pipeline.UpstreamHTTPDo(context.Background(), httpclient.New(httpclient.DefaultTimeout), req); err == nil {
t.Fatal("private CDN accepted")
}
}