test(server): adapt fixtures to IAM dual-gate fail-closed

Boot memory IAM only via explicit test env opt-in; strict auth cases
disable AllowInMemory after startup.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
weichen
2026-09-16 09:42:07 +00:00
co-authored by Cursor
parent 514db3a086
commit 7fdbee6c0c
12 changed files with 115 additions and 86 deletions
+3
View File
@@ -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)
+25 -4
View File
@@ -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)
}
}
+12 -11
View File
@@ -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{
+4 -8
View File
@@ -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))
+7 -4
View File
@@ -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("[]")))
+1 -1
View File
@@ -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()
@@ -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()
+5 -6
View File
@@ -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)
+13 -30
View File
@@ -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)
+9 -11
View File
@@ -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")
+22
View File
@@ -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)
+5 -10
View File
@@ -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)