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:
@@ -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)
|
||||
|
||||
@@ -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,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{
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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("[]")))
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
Reference in New Issue
Block a user