package auth import ( "context" "fmt" "strings" "testing" "chat-agent-system/internal/config" ) func TestNewStores_FailClosed_WhenIAMUnavailable(t *testing.T) { ctx := context.Background() cases := []struct { name string cfg *config.Config }{ {name: "main_nil_pool", cfg: &config.Config{Env: "main", AllowInMemory: false}}, {name: "prod_nil_pool", cfg: &config.Config{Env: "prod", AllowInMemory: false}}, {name: "empty_env_nil_pool", cfg: &config.Config{Env: "", AllowInMemory: true}}, {name: "unknown_env_nil_pool", cfg: &config.Config{Env: "staging", AllowInMemory: true}}, {name: "dev_without_opt_in", cfg: &config.Config{Env: "dev", AllowInMemory: false}}, {name: "test_without_opt_in", cfg: &config.Config{Env: "test", AllowInMemory: false}}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { stores, err := NewStores(ctx, nil, tc.cfg) if err == nil { t.Fatalf("expected fail-closed error, got stores=%v", stores) } if stores != nil { t.Fatalf("expected nil stores on failure, got %#v", stores) } if !strings.Contains(err.Error(), "IAM") && !strings.Contains(err.Error(), "memory") { t.Fatalf("error should mention IAM/memory policy: %v", err) } }) } } func TestNewStores_AllowsMemoryOnlyWithDevTestOptIn(t *testing.T) { ctx := context.Background() for _, env := range []string{"dev", "test"} { t.Run(env, func(t *testing.T) { cfg := &config.Config{ Env: env, AllowInMemory: true, AdminUsername: "admin", AdminPassword: "admin-test-pass", } stores, err := NewStores(ctx, nil, cfg) if err != nil { t.Fatalf("expected memory fixture: %v", err) } if stores == nil || stores.Auth == nil || stores.Sessions == nil { t.Fatal("expected auth+session stores") } if _, ok := stores.Auth.(*MemoryStore); !ok { t.Fatalf("Auth should be MemoryStore, got %T", stores.Auth) } }) } } // brokenSessionStore simulates IAM/session dependency outage after process start. type brokenSessionStore struct{} func (brokenSessionStore) CreateSession(ctx context.Context, userID int64, tokenHash string, meta SessionMeta) error { return fmt.Errorf("session store unavailable") } func (brokenSessionStore) ResolveUserID(ctx context.Context, tokenHash string) (int64, error) { return 0, fmt.Errorf("session store unavailable") } func (brokenSessionStore) TouchSession(ctx context.Context, tokenHash string) error { return fmt.Errorf("session store unavailable") } func (brokenSessionStore) RevokeByTokenHash(ctx context.Context, tokenHash string) error { return fmt.Errorf("session store unavailable") } func (brokenSessionStore) RevokeAllForUser(ctx context.Context, userID int64) error { return fmt.Errorf("session store unavailable") } func (brokenSessionStore) ListActiveSessions(ctx context.Context, userID int64, currentTokenHash string) ([]SessionRecord, error) { return nil, fmt.Errorf("session store unavailable") } func (brokenSessionStore) RevokeSessionByID(ctx context.Context, userID int64, sessionID string) error { return fmt.Errorf("session store unavailable") } func TestAuthStore_SessionInterruptFailClosed(t *testing.T) { // After a session was "established", subsequent lookups must not silently // succeed with cached authority when the session store is down. var ss SessionStore = brokenSessionStore{} _, err := ss.ResolveUserID(context.Background(), "hash-already-issued") if err == nil { t.Fatal("interrupted session store must fail-closed on ResolveUserID") } if !strings.Contains(err.Error(), "unavailable") { t.Fatalf("want unavailable error, got %v", err) } }