summaryrefslogtreecommitdiff
path: root/internal/controlplane/manager_test.go
diff options
context:
space:
mode:
authorChia <Chia@93.nz>2026-08-04 19:58:52 +1200
committerChia <Chia@93.nz>2026-08-04 20:43:23 +1200
commit5b651488b081b65fda8a323f228e139adb79a35d (patch)
tree08baf40efb8fe103b32721cd991ff712323e3173 /internal/controlplane/manager_test.go
Build AI gateway control plane and admin UI
Diffstat (limited to 'internal/controlplane/manager_test.go')
-rw-r--r--internal/controlplane/manager_test.go212
1 files changed, 212 insertions, 0 deletions
diff --git a/internal/controlplane/manager_test.go b/internal/controlplane/manager_test.go
new file mode 100644
index 0000000..8dd4012
--- /dev/null
+++ b/internal/controlplane/manager_test.go
@@ -0,0 +1,212 @@
+package controlplane
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "log/slog"
+ "strings"
+ "sync"
+ "sync/atomic"
+ "testing"
+ "time"
+
+ "aigw/internal/auth"
+ "aigw/internal/catalog"
+)
+
+type fakeManagerStore struct {
+ redisEnabled bool
+ snapshot atomic.Value
+ databaseGeneration atomic.Int64
+ publishCalls atomic.Int64
+ subscribeCalls atomic.Int64
+ publishErr error
+ published chan ChangeEvent
+ subscribe func(context.Context, int64) (<-chan ChangeMessage, func() error, error)
+}
+
+type safeLogBuffer struct {
+ mu sync.Mutex
+ buf bytes.Buffer
+}
+
+func (b *safeLogBuffer) Write(data []byte) (int, error) {
+ b.mu.Lock()
+ defer b.mu.Unlock()
+ return b.buf.Write(data)
+}
+
+func (b *safeLogBuffer) String() string {
+ b.mu.Lock()
+ defer b.mu.Unlock()
+ return b.buf.String()
+}
+
+func newFakeManagerStore(generation int64) *fakeManagerStore {
+ store := &fakeManagerStore{published: make(chan ChangeEvent, 8)}
+ store.snapshot.Store(Snapshot{Generation: generation})
+ store.databaseGeneration.Store(generation)
+ return store
+}
+
+func (s *fakeManagerStore) LoadSnapshot(context.Context) (Snapshot, error) {
+ return s.snapshot.Load().(Snapshot), nil
+}
+
+func (s *fakeManagerStore) DatabaseGeneration(context.Context) (int64, error) {
+ return s.databaseGeneration.Load(), nil
+}
+
+func (s *fakeManagerStore) PublishChange(_ context.Context, event ChangeEvent) error {
+ s.publishCalls.Add(1)
+ s.published <- event
+ return s.publishErr
+}
+
+func (s *fakeManagerStore) Subscribe(ctx context.Context) (<-chan ChangeMessage, func() error, error) {
+ call := s.subscribeCalls.Add(1)
+ if s.subscribe != nil {
+ return s.subscribe(ctx, call)
+ }
+ channel := make(chan ChangeMessage)
+ return channel, func() error { return nil }, nil
+}
+
+func (s *fakeManagerStore) RedisEnabled() bool {
+ return s.redisEnabled
+}
+
+func newTestManager(store managerStore, logger *slog.Logger, interval time.Duration) *Manager {
+ return NewManager(store, catalog.NewModels(nil), auth.NewDynamic(nil, false), logger, interval)
+}
+
+func TestAfterMutationReloadsLocallyWhenRedisPublishFails(t *testing.T) {
+ store := newFakeManagerStore(7)
+ store.redisEnabled = true
+ store.publishErr = errors.New("redis unavailable")
+ var logs safeLogBuffer
+ manager := newTestManager(store, slog.New(slog.NewTextHandler(&logs, nil)), 10*time.Millisecond)
+ ctx, cancel := context.WithCancel(context.Background())
+ done := make(chan struct{})
+ go func() {
+ manager.Run(ctx)
+ close(done)
+ }()
+
+ if err := manager.AfterMutation(context.Background(), 7, "model", "model-1"); err != nil {
+ t.Fatalf("mutation unexpectedly failed: %v", err)
+ }
+ if manager.Generation() != 7 {
+ t.Fatalf("local generation = %d, want 7", manager.Generation())
+ }
+ select {
+ case event := <-store.published:
+ if event.Generation != 7 || event.Resource != "model" {
+ t.Fatalf("unexpected event: %+v", event)
+ }
+ case <-time.After(time.Second):
+ t.Fatal("broadcast was not attempted")
+ }
+ waitUntil(t, time.Second, func() bool { return strings.Contains(logs.String(), "control_plane_publish_failed") })
+
+ cancel()
+ select {
+ case <-done:
+ case <-time.After(time.Second):
+ t.Fatal("manager did not stop")
+ }
+}
+
+func TestPollingContinuesWhileRedisSubscribeIsBlocked(t *testing.T) {
+ store := newFakeManagerStore(1)
+ store.redisEnabled = true
+ store.subscribe = func(ctx context.Context, _ int64) (<-chan ChangeMessage, func() error, error) {
+ <-ctx.Done()
+ return nil, nil, ctx.Err()
+ }
+ manager := newTestManager(store, slog.New(slog.NewTextHandler(&safeLogBuffer{}, nil)), 10*time.Millisecond)
+ if _, err := manager.Reload(context.Background()); err != nil {
+ t.Fatal(err)
+ }
+
+ ctx, cancel := context.WithCancel(context.Background())
+ done := make(chan struct{})
+ go func() {
+ manager.Run(ctx)
+ close(done)
+ }()
+ store.snapshot.Store(Snapshot{Generation: 2})
+ store.databaseGeneration.Store(2)
+ waitUntil(t, time.Second, func() bool { return manager.Generation() == 2 })
+
+ cancel()
+ select {
+ case <-done:
+ case <-time.After(time.Second):
+ t.Fatal("manager did not stop")
+ }
+}
+
+func TestSubscriptionReconnectsAfterChannelCloses(t *testing.T) {
+ store := newFakeManagerStore(1)
+ store.redisEnabled = true
+ first := make(chan ChangeMessage)
+ second := make(chan ChangeMessage, 1)
+ store.subscribe = func(_ context.Context, call int64) (<-chan ChangeMessage, func() error, error) {
+ if call == 1 {
+ return first, func() error { return nil }, nil
+ }
+ return second, func() error { return nil }, nil
+ }
+ manager := newTestManager(store, slog.New(slog.NewTextHandler(&safeLogBuffer{}, nil)), 10*time.Millisecond)
+ if _, err := manager.Reload(context.Background()); err != nil {
+ t.Fatal(err)
+ }
+
+ ctx, cancel := context.WithCancel(context.Background())
+ done := make(chan struct{})
+ go func() {
+ manager.Run(ctx)
+ close(done)
+ }()
+ waitUntil(t, time.Second, func() bool { return store.subscribeCalls.Load() == 1 })
+ close(first)
+ waitUntil(t, time.Second, func() bool { return store.subscribeCalls.Load() >= 2 && manager.RedisConnected() })
+ store.snapshot.Store(Snapshot{Generation: 2})
+ second <- ChangeMessage{Payload: `{"generation":2,"resource":"model"}`}
+ waitUntil(t, time.Second, func() bool { return manager.Generation() == 2 })
+
+ cancel()
+ select {
+ case <-done:
+ case <-time.After(time.Second):
+ t.Fatal("manager did not stop")
+ }
+}
+
+func TestRedisCanBeDisabled(t *testing.T) {
+ store := newFakeManagerStore(3)
+ manager := newTestManager(store, slog.New(slog.NewTextHandler(&bytes.Buffer{}, nil)), 10*time.Millisecond)
+ if err := manager.AfterMutation(context.Background(), 3, "tenant", "tenant-1"); err != nil {
+ t.Fatal(err)
+ }
+ if manager.RedisConfigured() || manager.RedisConnected() {
+ t.Fatal("Redis unexpectedly reported as available")
+ }
+ if store.publishCalls.Load() != 0 || store.subscribeCalls.Load() != 0 {
+ t.Fatal("Redis operations were attempted while disabled")
+ }
+}
+
+func waitUntil(t *testing.T, timeout time.Duration, condition func() bool) {
+ t.Helper()
+ deadline := time.Now().Add(timeout)
+ for time.Now().Before(deadline) {
+ if condition() {
+ return
+ }
+ time.Sleep(5 * time.Millisecond)
+ }
+ t.Fatal("condition was not met before timeout")
+}