Files
go-sip/internal/dispatcher/current_config_test.go
T

166 lines
5.7 KiB
Go

package dispatcher
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"git.ipao.vip/rogee/go-sip/internal/configread"
"git.ipao.vip/rogee/go-sip/internal/store"
)
func currentConfigExample(t *testing.T, name string) []byte {
t.Helper()
raw, err := os.ReadFile(filepath.Join("..", "..", "contracts", "local", "examples", name+".json"))
if err != nil {
t.Fatal(err)
}
return raw
}
func TestCurrentBootstrapRequiresSIPLoadingAndControlDrain(t *testing.T) {
id := "c046b893-8628-4589-ae50-619d049248a6"
called := make([]string, 0)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
called = append(called, r.URL.Path)
var response []byte
switch r.URL.Path {
case "/internal/v1/dispatcher/sip":
response = currentConfigExample(t, "config-read-sip")
case "/internal/v1/dispatcher/tasks":
if r.URL.Query().Get("after") == "" {
response = currentConfigExample(t, "task-discovery-page")
} else {
response = currentConfigExample(t, "task-discovery-end")
}
case "/internal/v1/dispatcher/ai-providers":
response = currentConfigExample(t, "config-read-providers")
case "/internal/v1/dispatcher/task/task-asr":
response = currentConfigExample(t, "config-read-task-asr")
case "/internal/v1/dispatcher/tenant/1001/quota":
response = currentConfigExample(t, "config-read-quota")
default:
t.Errorf("unapproved config path %s", r.URL.Path)
w.WriteHeader(http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(response)
}))
defer server.Close()
client, err := configread.NewClient(server.URL, id, "test-secret", server.Client())
if err != nil {
t.Fatal(err)
}
db, err := store.OpenCurrent(filepath.Join(t.TempDir(), "current.db"))
if err != nil {
t.Fatal(err)
}
defer db.Close()
verifierCalled, drained := false, false
var cursor string
var approvedSIP configread.CurrentSIP
bootstrap := CurrentBootstrap{
Client: client, Store: db, DispatcherID: id, Cursor: &cursor, SIP: &approvedSIP,
VerifySIP: func(_ context.Context, sip configread.CurrentSIP) error {
verifierCalled = true
if sip.Revision != 8 {
t.Fatalf("verified wrong SIP revision %d", sip.Revision)
}
return nil
},
DrainControls: func(context.Context) error {
drained = true
if admitted, err := db.CanAdmit(id, 1001, "task-asr"); err != nil || admitted {
t.Fatalf("opened before draining control queue: admitted=%v, err=%v", admitted, err)
}
return nil
},
}
if err := bootstrap.Run(context.Background()); err != nil {
t.Fatal(err)
}
admitted, err := db.CanAdmit(id, 1001, "task-asr")
if err != nil || !admitted || !verifierCalled || !drained || cursor != "opaque-end-token" || approvedSIP.Revision != 8 || len(called) < 6 || called[0] != "/internal/v1/dispatcher/sip" {
t.Fatalf("bootstrap: admitted=%v, err=%v, verified=%v, drained=%v, paths=%v", admitted, err, verifierCalled, drained, called)
}
if err := db.NoteSIPChange(id, 9); err != nil {
t.Fatal(err)
}
if err := bootstrap.Run(context.Background()); !errors.Is(err, store.ErrCurrentSIPPending) {
t.Fatalf("startup lost durable newer SIP notification: %v", err)
}
if cursor != "opaque-end-token" || approvedSIP.Revision != 8 {
t.Fatalf("pending SIP startup lost verified snapshot/cursor: revision=%d cursor=%q", approvedSIP.Revision, cursor)
}
if admitted, err := db.CanAdmit(id, 1001, "task-asr"); err != nil || admitted {
t.Fatalf("old SIP opened during pending notification: %v %v", admitted, err)
}
}
func TestCurrentBootstrapFailsClosedOnVerifierOrDrainError(t *testing.T) {
id := "c046b893-8628-4589-ae50-619d049248a6"
for _, failure := range []string{"verify", "drain"} {
t.Run(failure, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var response []byte
switch r.URL.Path {
case "/internal/v1/dispatcher/sip":
response = currentConfigExample(t, "config-read-sip")
case "/internal/v1/dispatcher/tasks":
if r.URL.Query().Get("after") == "" {
response = currentConfigExample(t, "task-discovery-page")
} else {
response = currentConfigExample(t, "task-discovery-end")
}
case "/internal/v1/dispatcher/ai-providers":
response = currentConfigExample(t, "config-read-providers")
case "/internal/v1/dispatcher/task/task-asr":
response = currentConfigExample(t, "config-read-task-asr")
case "/internal/v1/dispatcher/tenant/1001/quota":
response = currentConfigExample(t, "config-read-quota")
default:
t.Fatalf("unexpected path %s", r.URL.Path)
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(response)
}))
defer server.Close()
client, err := configread.NewClient(server.URL, id, "test-secret", server.Client())
if err != nil {
t.Fatal(err)
}
db, err := store.OpenCurrent(filepath.Join(t.TempDir(), "current.db"))
if err != nil {
t.Fatal(err)
}
defer db.Close()
b := CurrentBootstrap{
Client: client, Store: db, DispatcherID: id,
VerifySIP: func(context.Context, configread.CurrentSIP) error {
if failure == "verify" {
return errors.New("applied SIP revision unavailable")
}
return nil
},
DrainControls: func(context.Context) error {
if failure == "drain" {
return errors.New("control queue not drained")
}
return nil
},
}
if err := b.Run(context.Background()); err == nil {
t.Fatal("bootstrapped without required SIP/control gate")
}
if admitted, err := db.CanAdmit(id, 1001, "task-asr"); err != nil || admitted {
t.Fatalf("opened admission after failed bootstrap: admitted=%v err=%v", admitted, err)
}
})
}
}