diff --git a/internal/bizdev/testutil.go b/internal/bizdev/testutil.go index 497c2521..8f7ec883 100644 --- a/internal/bizdev/testutil.go +++ b/internal/bizdev/testutil.go @@ -20,6 +20,9 @@ func MustNewService(t testing.TB, cfg *config.Config) *Service { return svc } cfg.AllowInMemory = true + if cfg.Env == "" { + cfg.Env = "test" + } svc, err := NewService(cfg) if err != nil { t.Fatalf("NewService: %v", err) diff --git a/internal/server/auth_gate_a_test.go b/internal/server/auth_gate_a_test.go index edab29b9..f04e09fd 100644 --- a/internal/server/auth_gate_a_test.go +++ b/internal/server/auth_gate_a_test.go @@ -3,15 +3,18 @@ package server import ( "net/http" "net/http/httptest" + "strings" "testing" + "chat-agent-system/internal/app" "chat-agent-system/internal/bizdev" "chat-agent-system/internal/config" ) func TestGateA_LineageAndRunsRequireProjectAccess(t *testing.T) { - cfg := &config.Config{DataDir: t.TempDir(), AgentBackend: "stub", AllowInMemory: true, Env: "main", ContextDbHost: "127.0.0.1"} - srv := NewServer(cfg, bizdev.MustNewService(t, &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"})) + // Gate 行为可用 test+AllowInMemory fixture;main 不得再靠 MemoryStore 旁路搭服务。 + cfg := &config.Config{DataDir: t.TempDir(), AgentBackend: "stub", AllowInMemory: true, Env: "test", ContextDbHost: "127.0.0.1"} + srv := NewServer(cfg, bizdev.MustNewService(t, &config.Config{DataDir: t.TempDir(), AgentBackend: "stub", AllowInMemory: true, Env: "test"})) srv.cfg = cfg cases := []struct { @@ -32,8 +35,8 @@ func TestGateA_LineageAndRunsRequireProjectAccess(t *testing.T) { } func TestGateA_CreateProjectRequiresPermission(t *testing.T) { - cfg := &config.Config{DataDir: t.TempDir(), AgentBackend: "stub", AllowInMemory: true, Env: "main", ContextDbHost: "127.0.0.1"} - srv := NewServer(cfg, bizdev.MustNewService(t, &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"})) + cfg := &config.Config{DataDir: t.TempDir(), AgentBackend: "stub", AllowInMemory: true, Env: "test", ContextDbHost: "127.0.0.1"} + srv := NewServer(cfg, bizdev.MustNewService(t, &config.Config{DataDir: t.TempDir(), AgentBackend: "stub", AllowInMemory: true, Env: "test"})) srv.cfg = cfg req := httptest.NewRequest(http.MethodPost, "/api/v1/projects", nil) @@ -44,3 +47,21 @@ func TestGateA_CreateProjectRequiresPermission(t *testing.T) { t.Fatalf("expected 401, got %d", rec.Code) } } + +func TestAssemble_FailClosed_MainCannotUseMemoryIAM(t *testing.T) { + cfg := &config.Config{ + DataDir: t.TempDir(), + AgentBackend: "stub", + AllowInMemory: true, + Env: "main", + ContextDbHost: "127.0.0.1", + } + svc := bizdev.MustNewService(t, &config.Config{DataDir: t.TempDir(), AgentBackend: "stub", AllowInMemory: true, Env: "test"}) + _, err := app.Assemble(cfg, svc) + if err == nil { + t.Fatal("expected assemble fail-closed when main lacks IAM PG store") + } + if !strings.Contains(err.Error(), "fail-closed") && !strings.Contains(err.Error(), "auth store") { + t.Fatalf("unexpected error: %v", err) + } +} diff --git a/internal/server/auth_handlers_test.go b/internal/server/auth_handlers_test.go index 632548ae..b33deda3 100644 --- a/internal/server/auth_handlers_test.go +++ b/internal/server/auth_handlers_test.go @@ -12,22 +12,27 @@ import ( "chat-agent-system/internal/server" ) -func setupTestServer() (*httptest.Server, *auth.Service) { +func setupTestServer(t *testing.T) (*httptest.Server, *auth.Service) { + t.Helper() cfg := &config.Config{ Env: "dev", + DataDir: t.TempDir(), + AgentBackend: "stub", + AllowInMemory: true, AdminUsername: "admin", AdminPassword: "admin123", AuthRegistrationEnabled: true, AuthRequireApproval: false, } srv := server.NewServer(cfg, nil) + cfg.AllowInMemory = false ts := httptest.NewServer(srv.Routes()) + t.Cleanup(ts.Close) return ts, srv.AuthService() } func TestAuthHandlers_PublicConfigUnauthenticated(t *testing.T) { - ts, authSvc := setupTestServer() - defer ts.Close() + ts, authSvc := setupTestServer(t) _ = authSvc.UpdateConfig(t.Context(), auth.SystemAuthConfig{ AllowRegistration: true, @@ -62,8 +67,7 @@ func TestAuthHandlers_PublicConfigUnauthenticated(t *testing.T) { } func TestAuthHandlers_UnauthenticatedMeReturns401(t *testing.T) { - ts, _ := setupTestServer() - defer ts.Close() + ts, _ := setupTestServer(t) resp, err := http.Get(ts.URL + "/api/v1/auth/me") if err != nil { @@ -76,8 +80,7 @@ func TestAuthHandlers_UnauthenticatedMeReturns401(t *testing.T) { } func TestAuthHandlers_LoginAndMe(t *testing.T) { - ts, _ := setupTestServer() - defer ts.Close() + ts, _ := setupTestServer(t) // 1. Admin login with .env credentials loginReq := map[string]string{ @@ -128,8 +131,7 @@ func TestAuthHandlers_LoginAndMe(t *testing.T) { } func TestAuthHandlers_RegisterAndApproval(t *testing.T) { - ts, authSvc := setupTestServer() - defer ts.Close() + ts, authSvc := setupTestServer(t) // 1. Turn on RequireApproval _ = authSvc.UpdateConfig(t.Context(), auth.SystemAuthConfig{ @@ -168,8 +170,7 @@ func TestAuthHandlers_RegisterAndApproval(t *testing.T) { } func TestAuthHandlers_AdminCreateUser_MustChangePassword(t *testing.T) { - ts, _ := setupTestServer() - defer ts.Close() + ts, _ := setupTestServer(t) // 1. Admin login to obtain token loginBody, _ := json.Marshal(map[string]string{ diff --git a/internal/server/auth_phase1_test.go b/internal/server/auth_phase1_test.go index 88422f6d..2ea312ea 100644 --- a/internal/server/auth_phase1_test.go +++ b/internal/server/auth_phase1_test.go @@ -25,8 +25,7 @@ func adminToken(t *testing.T, baseURL string) string { } func TestAuthHandlers_DisabledUserTokenReturns401(t *testing.T) { - ts, authSvc := setupTestServer() - defer ts.Close() + ts, authSvc := setupTestServer(t) loginBody, _ := json.Marshal(map[string]string{"username": "zhanggong", "password": "123456"}) resp, err := http.Post(ts.URL+"/api/v1/auth/login", "application/json", bytes.NewReader(loginBody)) @@ -72,8 +71,7 @@ func TestAuthHandlers_DisabledUserTokenReturns401(t *testing.T) { } func TestAuthHandlers_AdminListRoles(t *testing.T) { - ts, _ := setupTestServer() - defer ts.Close() + ts, _ := setupTestServer(t) req, _ := http.NewRequest(http.MethodGet, ts.URL+"/api/v1/admin/roles", nil) req.Header.Set("Authorization", "Bearer "+adminToken(t, ts.URL)) @@ -96,8 +94,7 @@ func TestAuthHandlers_AdminListRoles(t *testing.T) { } func TestAuthHandlers_ListSessions(t *testing.T) { - ts, _ := setupTestServer() - defer ts.Close() + ts, _ := setupTestServer(t) tok := adminToken(t, ts.URL) req, _ := http.NewRequest(http.MethodGet, ts.URL+"/api/v1/auth/sessions", nil) @@ -122,8 +119,7 @@ func TestAuthHandlers_ListSessions(t *testing.T) { } func TestAuthHandlers_LoginReturnsCapabilitiesFallback(t *testing.T) { - ts, _ := setupTestServer() - defer ts.Close() + ts, _ := setupTestServer(t) req, _ := http.NewRequest(http.MethodGet, ts.URL+"/api/v1/auth/me", nil) req.Header.Set("Authorization", "Bearer "+adminToken(t, ts.URL)) diff --git a/internal/server/chat_window_api_test.go b/internal/server/chat_window_api_test.go index 311b8e8f..42316542 100644 --- a/internal/server/chat_window_api_test.go +++ b/internal/server/chat_window_api_test.go @@ -157,13 +157,16 @@ func TestServer_ChatWindowAndDayAPIs(t *testing.T) { func TestServer_ChatWindow_ImportGuard(t *testing.T) { // 验证在生产环境中 import 接口被正确拦截 cfg := &config.Config{ - Env: "production", - HTTPHost: "127.0.0.1", - HTTPPort: 8889, - DataDir: t.TempDir(), + Env: "test", + HTTPHost: "127.0.0.1", + HTTPPort: 8889, + DataDir: t.TempDir(), + AllowInMemory: true, + AgentBackend: "stub", } svc := bizdev.MustNewService(t, cfg) srv := server.NewServer(cfg, svc) + cfg.Env = "production" handler := srv.Routes() reqImport := httptest.NewRequest("POST", "/api/v1/chat/messages/import", bytes.NewReader([]byte("[]"))) diff --git a/internal/server/project_handlers_test.go b/internal/server/project_handlers_test.go index e4f8e9c5..b66fd421 100644 --- a/internal/server/project_handlers_test.go +++ b/internal/server/project_handlers_test.go @@ -13,7 +13,7 @@ import ( ) func TestHandleListProjects_AdminCanSeeProjectsOwnedByOtherUsers(t *testing.T) { - cfg := &config.Config{DataDir: t.TempDir(), AgentBackend: "stub", AllowInMemory: true} + cfg := &config.Config{DataDir: t.TempDir(), AgentBackend: "stub", AllowInMemory: true, Env: "test"} svc := bizdev.MustNewService(t, cfg) srv := NewServer(cfg, svc) ctx := t.Context() diff --git a/internal/server/project_members_authz_test.go b/internal/server/project_members_authz_test.go index 62fc2c1e..add76f46 100644 --- a/internal/server/project_members_authz_test.go +++ b/internal/server/project_members_authz_test.go @@ -71,8 +71,16 @@ func TestProjectMembers_CrossProjectForbidden(t *testing.T) { } func TestProjectMembers_AnonymousReturns401(t *testing.T) { - cfg := &config.Config{Env: "dev", AdminUsername: "admin", AdminPassword: "admin123"} + cfg := &config.Config{ + Env: "dev", + DataDir: t.TempDir(), + AgentBackend: "stub", + AllowInMemory: true, + AdminUsername: "admin", + AdminPassword: "admin123", + } srv := server.NewServer(cfg, nil) + cfg.AllowInMemory = false ts := httptest.NewServer(srv.Routes()) defer ts.Close() diff --git a/internal/server/reply_export_api_test.go b/internal/server/reply_export_api_test.go index b598598b..76fde094 100644 --- a/internal/server/reply_export_api_test.go +++ b/internal/server/reply_export_api_test.go @@ -157,12 +157,11 @@ func TestServer_ReplyExportAPI(t *testing.T) { } // 测试 G: 未登录返回 401(严格模式,禁用测试回退) - strictCfg := &config.Config{DataDir: t.TempDir(), AllowInMemory: false} - strictSvc, err := bizdev.NewService(strictCfg) - if err != nil { - t.Fatalf("strict NewService: %v", err) - } - strictHandler := server.NewServer(strictCfg, strictSvc).Routes() + strictCfg := &config.Config{DataDir: t.TempDir(), AgentBackend: "stub", Env: "test", AllowInMemory: true} + strictSvc := bizdev.MustNewService(t, strictCfg) + strictSrv := server.NewServer(strictCfg, strictSvc) + strictCfg.AllowInMemory = false + strictHandler := strictSrv.Routes() reqUnauth := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/chat/messages/%d/export-source", asstID), nil) recUnauth := httptest.NewRecorder() strictHandler.ServeHTTP(recUnauth, reqUnauth) diff --git a/internal/server/security_p0_test.go b/internal/server/security_p0_test.go index bfda36ff..dbc02c66 100644 --- a/internal/server/security_p0_test.go +++ b/internal/server/security_p0_test.go @@ -12,12 +12,9 @@ import ( ) func TestP0_AnonymousBusinessAPIReturns401(t *testing.T) { - cfg := &config.Config{DataDir: t.TempDir(), AgentBackend: "stub", AllowInMemory: false} - svc, err := bizdev.NewService(cfg) - if err != nil { - t.Fatalf("NewService: %v", err) - } - srv := NewServer(cfg, svc) + cfg := &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"} + srv := newServerWithMemoryIAM(t, cfg) + srv.cfg.AllowInMemory = false cases := []struct { method string @@ -55,12 +52,9 @@ func TestP0_AnonymousBusinessAPIReturns401(t *testing.T) { } func TestP0_XUserIDCannotAuthenticate(t *testing.T) { - cfg := &config.Config{DataDir: t.TempDir(), AgentBackend: "stub", AllowInMemory: false} - svc, err := bizdev.NewService(cfg) - if err != nil { - t.Fatalf("NewService: %v", err) - } - srv := NewServer(cfg, svc) + cfg := &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"} + srv := newServerWithMemoryIAM(t, cfg) + srv.cfg.AllowInMemory = false req := httptest.NewRequest(http.MethodGet, "/api/v1/projects", nil) req.Header.Set("X-User-ID", "1") @@ -84,7 +78,7 @@ func TestP0_HealthDegradedWhenDBRequiredButUnavailable(t *testing.T) { t.Fatal("expected NewService error when DB unavailable") } _ = svc - srv := NewServer(cfg, bizdev.MustNewService(t, &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"})) + srv := newServerWithMemoryIAM(t, &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"}) srv.cfg = cfg req := httptest.NewRequest(http.MethodGet, "/api/v1/health", nil) @@ -101,12 +95,8 @@ func TestP0_HealthDegradedWhenDBRequiredButUnavailable(t *testing.T) { } func TestP0_BodyUserIDCannotOverridePrincipal(t *testing.T) { - cfg := &config.Config{DataDir: t.TempDir(), AgentBackend: "stub", Env: "dev", AllowInMemory: false} - svc, err := bizdev.NewService(cfg) - if err != nil { - t.Fatalf("NewService: %v", err) - } - srv := NewServer(cfg, svc) + cfg := &config.Config{DataDir: t.TempDir(), AgentBackend: "stub", Env: "dev"} + srv := newServerWithMemoryIAM(t, cfg) token := loginTestAdmin(t, srv) body := bytes.NewReader([]byte(`{"session_id":"p0-sess","message":"hello","user_id":99999}`)) @@ -127,11 +117,7 @@ func TestP0_CORSAllowlist(t *testing.T) { Env: "dev", CORSAllowedOrigins: []string{"https://app.example.com"}, } - svc, err := bizdev.NewService(cfg) - if err != nil { - t.Fatalf("NewService: %v", err) - } - srv := NewServer(cfg, svc) + srv := newServerWithMemoryIAM(t, cfg) req := httptest.NewRequest(http.MethodOptions, "/api/v1/projects", nil) req.Header.Set("Origin", "https://evil.example.com") @@ -151,12 +137,9 @@ func TestP0_CORSAllowlist(t *testing.T) { } func TestP0_ReconcileRejectsClientWorkDir(t *testing.T) { - cfg := &config.Config{DataDir: t.TempDir(), AgentBackend: "stub", AllowInMemory: false} - svc, err := bizdev.NewService(cfg) - if err != nil { - t.Fatalf("NewService: %v", err) - } - srv := NewServer(cfg, svc) + cfg := &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"} + srv := newServerWithMemoryIAM(t, cfg) + srv.cfg.AllowInMemory = false body := bytes.NewReader([]byte(`{"work_dir":"/tmp/evil"}`)) req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/workspaces/reconcile", body) diff --git a/internal/server/security_penetration_test.go b/internal/server/security_penetration_test.go index 677ee54b..9719f68e 100644 --- a/internal/server/security_penetration_test.go +++ b/internal/server/security_penetration_test.go @@ -14,19 +14,17 @@ import ( ) // newStrictAuthServer 禁用 legacy 匿名 principal,用于安全负例测试。 +// IAM 仍用 test+AllowInMemory 双门启动;启动后再关闭 AllowInMemory 以强制请求鉴权。 func newStrictAuthServer(t *testing.T) *Server { t.Helper() cfg := &config.Config{ - Env: "main", - DataDir: t.TempDir(), - AgentBackend: "stub", - AllowInMemory: false, + Env: "test", + DataDir: t.TempDir(), + AgentBackend: "stub", } - svc, err := bizdev.NewService(cfg) - if err != nil { - t.Fatalf("NewService: %v", err) - } - return NewServer(cfg, svc) + srv := newServerWithMemoryIAM(t, cfg) + srv.cfg.AllowInMemory = false + return srv } func TestPenetration_AnonymousAdminTemplateUpload401(t *testing.T) { @@ -178,7 +176,7 @@ func TestPenetration_ReadyStrictWhenDBConfiguredButDown(t *testing.T) { ContextDbPort: 59999, AllowInMemory: false, } - srv := NewServer(cfg, bizdev.MustNewService(t, &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"})) + srv := newServerWithMemoryIAM(t, &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"}) srv.cfg = cfg req := httptest.NewRequest(http.MethodGet, "/api/v1/ready", nil) @@ -195,7 +193,7 @@ func TestPenetration_DisallowedOriginNoWildcardCORS(t *testing.T) { AgentBackend: "stub", CORSAllowedOrigins: []string{"https://trusted.example.com"}, } - srv := NewServer(cfg, bizdev.MustNewService(t, cfg)) + srv := newServerWithMemoryIAM(t, cfg) req := httptest.NewRequest(http.MethodOptions, "/api/v1/projects", nil) req.Header.Set("Origin", "https://attacker.example.com") diff --git a/internal/server/server_test.go b/internal/server/server_test.go index b0922b4b..730436ae 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -44,6 +44,28 @@ func newTestServer(t testing.TB) *Server { return srv } +// newServerWithMemoryIAM 使用显式 dev/test + AllowInMemory 双门启动内存 IAM。 +// 需要「严格请求鉴权」的用例可在返回后将 srv.cfg.AllowInMemory 设为 false。 +func newServerWithMemoryIAM(t testing.TB, cfg *config.Config) *Server { + t.Helper() + if cfg == nil { + cfg = &config.Config{} + } + if cfg.DataDir == "" { + cfg.DataDir = t.TempDir() + } + if cfg.AgentBackend == "" { + cfg.AgentBackend = "stub" + } + cfg.AllowInMemory = true + env := strings.ToLower(strings.TrimSpace(cfg.Env)) + if env == "" || env == "main" || env == "prod" { + cfg.Env = "test" + } + svc := bizdev.MustNewService(t, cfg) + return NewServer(cfg, svc) +} + func TestServer_Health(t *testing.T) { srv := newTestServer(t) req := httptest.NewRequest(http.MethodGet, "/api/v1/health", nil) diff --git a/internal/server/web_assets_test.go b/internal/server/web_assets_test.go index 01bc0a45..3446f1ac 100644 --- a/internal/server/web_assets_test.go +++ b/internal/server/web_assets_test.go @@ -5,22 +5,17 @@ import ( "net/http/httptest" "testing" - "chat-agent-system/internal/bizdev" "chat-agent-system/internal/config" ) func TestServer_WebAssetsHostingAndFallback(t *testing.T) { cfg := &config.Config{ - Env: "test", - AgentBackend: "stub", - DataDir: t.TempDir(), - AllowInMemory: false, + Env: "test", + AgentBackend: "stub", + DataDir: t.TempDir(), } - svc, err := bizdev.NewService(cfg) - if err != nil { - t.Fatalf("NewService: %v", err) - } - srv := NewServer(cfg, svc) + srv := newServerWithMemoryIAM(t, cfg) + srv.cfg.AllowInMemory = false handler := srv.Routes() reqRoot := httptest.NewRequest(http.MethodGet, "/", nil)