From a3b01e664ec3fbffed676e7c090fcc879fdbe336 Mon Sep 17 00:00:00 2001 From: Rogee Date: Tue, 18 Aug 2026 00:55:45 +0800 Subject: [PATCH] H-263 restore PostgreSQL E2E coverage (#41) Co-authored-by: Rogee --- backend/internal/auth/policy.go | 14 +-- backend/internal/middleware/auth.go | 17 ++- backend/internal/middleware/csrf.go | 8 ++ backend/tests/e2e/account_e2e_test.go | 48 ++++----- backend/tests/e2e/auth_e2e_test.go | 74 ++++++------- backend/tests/e2e/crm_e2e_test.go | 109 +++++++++++--------- backend/tests/e2e/csrf_e2e_test.go | 10 +- backend/tests/e2e/dashboard_app_e2e_test.go | 100 ++++++++---------- backend/tests/e2e/e2e_test.go | 62 +++-------- backend/tests/e2e/middleware_e2e_test.go | 13 +-- backend/tests/e2e/session_e2e_test.go | 20 ++-- backend/tests/helpers/pg_helper.go | 13 ++- 12 files changed, 222 insertions(+), 266 deletions(-) diff --git a/backend/internal/auth/policy.go b/backend/internal/auth/policy.go index c3a3a029..300b8606 100644 --- a/backend/internal/auth/policy.go +++ b/backend/internal/auth/policy.go @@ -85,12 +85,14 @@ var AgentDefaultPermissions = PermissionMatrixMap{ // AdministratorPermissions defines the permission matrix for administrator role. // All dimensions are set to "full". var AdministratorPermissions = PermissionMatrixMap{ - DimensionConversationManage: PermissionFull, - DimensionConversationDelete: PermissionFull, - DimensionContactManage: PermissionFull, - DimensionReportManage: PermissionFull, - DimensionKnowledgeBaseManage: PermissionFull, - DimensionAutomationManage: PermissionFull, + DimensionConversationManage: PermissionFull, + DimensionConversationUnassignedManage: PermissionFull, + DimensionConversationParticipatingManage: PermissionFull, + DimensionConversationDelete: PermissionFull, + DimensionContactManage: PermissionFull, + DimensionReportManage: PermissionFull, + DimensionKnowledgeBaseManage: PermissionFull, + DimensionAutomationManage: PermissionFull, } // ToJSON serializes the permission matrix to JSON bytes (for JSONB storage). diff --git a/backend/internal/middleware/auth.go b/backend/internal/middleware/auth.go index 0ef911cf..8f3e0bef 100644 --- a/backend/internal/middleware/auth.go +++ b/backend/internal/middleware/auth.go @@ -7,7 +7,6 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/golang-jwt/jwt/v5" "github.com/gochat/gochat/internal/auth" "github.com/gochat/gochat/internal/config" @@ -119,14 +118,12 @@ func AuthMiddlewareWithServiceAndDB(jwtSvc *auth.JWTService, db *gorm.DB) gin.Ha // DEPRECATED: Prefer JWTService.GenerateTokenPair which produces proper typed Claims. // Kept for backward compatibility with existing test helpers. func GenerateToken(cfg *config.JWTConfig, userID uint, accountID uint, role string) (string, error) { - claims := jwt.MapClaims{ - "user_id": userID, - "account_id": accountID, - "role": role, - "exp": time.Now().Add(cfg.ExpiryDuration()).Unix(), - "iat": time.Now().Unix(), + pair, err := auth.NewJWTService(cfg).GenerateTokenPair(&model.User{ + Base: model.Base{ID: userID}, + Provider: "email", + }, accountID, role) + if err != nil { + return "", err } - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - return token.SignedString([]byte(cfg.Secret)) + return pair.AccessToken, nil } diff --git a/backend/internal/middleware/csrf.go b/backend/internal/middleware/csrf.go index 2b724cb5..792ea625 100644 --- a/backend/internal/middleware/csrf.go +++ b/backend/internal/middleware/csrf.go @@ -199,6 +199,14 @@ func setCSRFTokenCookie(c *gin.Context, token string, cfg CSRFConfig) { if maxAge <= 0 { maxAge = 3600 } + switch strings.ToLower(cfg.CookieSameSite) { + case "lax": + c.SetSameSite(http.SameSiteLaxMode) + case "none": + c.SetSameSite(http.SameSiteNoneMode) + default: + c.SetSameSite(http.SameSiteStrictMode) + } c.SetCookie( cfg.CookieName, diff --git a/backend/tests/e2e/account_e2e_test.go b/backend/tests/e2e/account_e2e_test.go index 79a0bbe1..11c3dd70 100644 --- a/backend/tests/e2e/account_e2e_test.go +++ b/backend/tests/e2e/account_e2e_test.go @@ -43,14 +43,14 @@ func (s *AccountCRUDE2ETestSuite) SetupTest() { now := time.Now() user := &model.User{ - Name: "E2E Tester", - Email: "e2e@test.com", - Password: hashedPassword, + Name: "E2E Tester", + Email: "e2e@test.com", + Password: hashedPassword, PasswordDigest: hashedPassword, - Provider: "email", - Active: true, - Available: true, - ConfirmedAt: &now, + Provider: "email", + Active: true, + Available: true, + ConfirmedAt: &now, } err = s.DB().Create(user).Error assert.NoError(s.T(), err, "Failed to create user") @@ -165,7 +165,7 @@ func (s *AccountCRUDE2ETestSuite) TestAccountCreate() { resp := s.authRequest("POST", "/api/v1/accounts", createPayload) defer resp.Body.Close() - assert.Equal(s.T(), http.StatusCreated, resp.StatusCode, "Create account should return 201") + assert.Equal(s.T(), http.StatusOK, resp.StatusCode, "Create account should return 200") result := parseResponse(resp) assert.NotNil(s.T(), result) @@ -213,14 +213,14 @@ func (s *AccountCRUDE2ETestSuite) TestAccountDelete() { resp := s.authRequest("DELETE", path, nil) defer resp.Body.Close() - assert.Equal(s.T(), http.StatusOK, resp.StatusCode, "Delete account should return 200") - - result := parseResponse(resp) - assert.NotNil(s.T(), result) + assert.Equal(s.T(), http.StatusNoContent, resp.StatusCode, "Delete account should return 204") + body, err := io.ReadAll(resp.Body) + assert.NoError(s.T(), err) + assert.Empty(s.T(), body, "204 response must not include a body") // Verify account is soft-deleted in DB (DeletedAt should be set) var deletedAccount model.Account - err := s.DB().Unscoped().First(&deletedAccount, accountToDelete.ID).Error + err = s.DB().Unscoped().First(&deletedAccount, accountToDelete.ID).Error assert.NoError(s.T(), err, "Soft-deleted account should still exist in DB (unscoped)") assert.NotZero(s.T(), deletedAccount.DeletedAt, "DeletedAt should be set after soft delete") @@ -282,7 +282,7 @@ func (s *AccountCRUDE2ETestSuite) TestContactCreate() { resp := s.authRequest("POST", path, createPayload) defer resp.Body.Close() - assert.Equal(s.T(), http.StatusCreated, resp.StatusCode, "Create contact should return 201") + assert.Equal(s.T(), http.StatusOK, resp.StatusCode, "Create contact should return 200") result := parseResponse(resp) assert.NotNil(s.T(), result) @@ -385,9 +385,9 @@ func (s *AccountCRUDE2ETestSuite) TestInboxGet() { // TestInboxCreate verifies creating a new inbox via the API. func (s *AccountCRUDE2ETestSuite) TestInboxCreate() { createPayload := map[string]interface{}{ - "name": "New E2E Inbox", - "channel_type": "web_widget", - "enabled": true, + "name": "New E2E Inbox", + "channel_type": "web_widget", + "enabled": true, "enable_auto_assignment": false, } @@ -395,7 +395,7 @@ func (s *AccountCRUDE2ETestSuite) TestInboxCreate() { resp := s.authRequest("POST", path, createPayload) defer resp.Body.Close() - assert.Equal(s.T(), http.StatusCreated, resp.StatusCode, "Create inbox should return 201") + assert.Equal(s.T(), http.StatusOK, resp.StatusCode, "Create inbox should return 200") result := parseResponse(resp) assert.NotNil(s.T(), result) @@ -479,7 +479,7 @@ func (s *AccountCRUDE2ETestSuite) TestAccountFullCRUDLifecycle() { "locale": "en", } resp := s.authRequest("POST", "/api/v1/accounts", createPayload) - assert.Equal(s.T(), http.StatusCreated, resp.StatusCode) + assert.Equal(s.T(), http.StatusOK, resp.StatusCode) result := parseResponse(resp) // Extract account ID from response @@ -516,9 +516,11 @@ func (s *AccountCRUDE2ETestSuite) TestAccountFullCRUDLifecycle() { // Step 4: Delete account via API resp = s.authRequest("DELETE", path, nil) - assert.True(s.T(), resp.StatusCode == http.StatusOK, - "Delete should return 200: got %d", resp.StatusCode) - parseResponse(resp) + assert.Equal(s.T(), http.StatusNoContent, resp.StatusCode) + body, err := io.ReadAll(resp.Body) + assert.NoError(s.T(), err) + assert.Empty(s.T(), body, "204 response must not include a body") + resp.Body.Close() // Verify soft-delete in DB err = s.DB().First(&dbAccount, accountID).Error @@ -591,4 +593,4 @@ func (s *AccountCRUDE2ETestSuite) TestInboxFullCRUDLifecycle() { // TestAccountCRUDE2ESuite runs the Account CRUD e2e test suite. func TestAccountCRUDE2ESuite(t *testing.T) { suite.Run(t, new(AccountCRUDE2ETestSuite)) -} \ No newline at end of file +} diff --git a/backend/tests/e2e/auth_e2e_test.go b/backend/tests/e2e/auth_e2e_test.go index c6c1ca78..3e47ee58 100644 --- a/backend/tests/e2e/auth_e2e_test.go +++ b/backend/tests/e2e/auth_e2e_test.go @@ -22,13 +22,13 @@ type AuthE2ETestSuite struct { E2ETestSuite } -func (s *AuthE2ETestSuite) TestRegisterNewUser() { +func (s *AuthE2ETestSuite) TestRegistrationEndpointIsNotExposed() { account := s.CreateTestAccount("Registration Org") registerPayload := map[string]interface{}{ - "name": "New Test User", - "email": "register@test.com", - "password": "SecurePass123!", + "name": "New Test User", + "email": "register@test.com", + "password": "SecurePass123!", "account_id": account.ID, } @@ -39,29 +39,22 @@ func (s *AuthE2ETestSuite) TestRegisterNewUser() { bytes.NewBuffer(body), ) - if err != nil { - s.T().Logf("Register endpoint not available yet: %v", err) - return - } + s.Require().NoError(err) defer resp.Body.Close() - - // Verify user was created in database - if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusCreated { - var user model.User - err = s.DB().Where("email = ?", "register@test.com").First(&user).Error - assert.NoError(s.T(), err, "Registered user should exist in database") - assert.Equal(s.T(), "New Test User", user.Name) - } + assert.Equal(s.T(), http.StatusNotFound, resp.StatusCode) + var count int64 + s.Require().NoError(s.DB().Model(&model.User{}).Where("email = ?", "register@test.com").Count(&count).Error) + assert.Zero(s.T(), count, "an unregistered route must not create a user") } -func (s *AuthE2ETestSuite) TestRegisterDuplicateEmailFails() { +func (s *AuthE2ETestSuite) TestRegistrationEndpointCannotMutateExistingUser() { account := s.CreateTestAccount("Dup Org") s.CreateTestUser("dup@test.com", "Existing", "hashedpass", "agent", account.ID) registerPayload := map[string]interface{}{ - "name": "Duplicate User", - "email": "dup@test.com", - "password": "AnotherPass!", + "name": "Duplicate User", + "email": "dup@test.com", + "password": "AnotherPass!", "account_id": account.ID, } @@ -72,17 +65,12 @@ func (s *AuthE2ETestSuite) TestRegisterDuplicateEmailFails() { bytes.NewBuffer(body), ) - if err != nil { - s.T().Logf("Register endpoint not available: %v", err) - return - } + s.Require().NoError(err) defer resp.Body.Close() - - // Duplicate email should be rejected - assert.True(s.T(), - resp.StatusCode == http.StatusConflict || resp.StatusCode == http.StatusBadRequest, - "Duplicate email registration should fail", - ) + assert.Equal(s.T(), http.StatusNotFound, resp.StatusCode) + var count int64 + s.Require().NoError(s.DB().Model(&model.User{}).Where("email = ?", "dup@test.com").Count(&count).Error) + assert.Equal(s.T(), int64(1), count, "an unregistered route must not mutate existing users") } func (s *AuthE2ETestSuite) TestLoginWithValidCredentials() { @@ -335,8 +323,8 @@ func (s *AuthE2ETestSuite) TestAccessProtectedEndpointWithValidToken() { func (s *AuthE2ETestSuite) TestJWTTokenExpiry() { // Test that expired tokens are rejected cfg := crypto.JWTConfig{ - Secret: []byte("e2e-test-secret-key"), - ExpiryHours: -1, // Already expired + Secret: []byte("e2e-test-secret-key"), + ExpiryHours: -1, // Already expired RefreshExpiryHours: 24, } expiredToken, err := crypto.GenerateToken(cfg, 1, "expired@test.com", "agent") @@ -348,8 +336,8 @@ func (s *AuthE2ETestSuite) TestJWTTokenExpiry() { func (s *AuthE2ETestSuite) TestJWTTokenValidation() { cfg := crypto.JWTConfig{ - Secret: []byte("e2e-test-secret-key"), - ExpiryHours: 1, + Secret: []byte("e2e-test-secret-key"), + ExpiryHours: 1, RefreshExpiryHours: 24, } @@ -367,15 +355,15 @@ func (s *AuthE2ETestSuite) TestJWTTokenValidation() { func (s *AuthE2ETestSuite) TestInvalidJWTTokenRejected() { cfg := crypto.JWTConfig{ - Secret: []byte("e2e-test-secret-key"), - ExpiryHours: 1, + Secret: []byte("e2e-test-secret-key"), + ExpiryHours: 1, RefreshExpiryHours: 24, } // Token signed with different secret wrongCfg := crypto.JWTConfig{ - Secret: []byte("wrong-secret-key"), - ExpiryHours: 1, + Secret: []byte("wrong-secret-key"), + ExpiryHours: 1, RefreshExpiryHours: 24, } wrongToken, err := crypto.GenerateToken(wrongCfg, 1, "wrong@test.com", "agent") @@ -462,8 +450,8 @@ func (s *AuthE2ETestSuite) TestMultipleUsersCanLoginIndependently() { // Both users should be able to generate valid JWT tokens independently cfg := crypto.JWTConfig{ - Secret: []byte("e2e-test-secret-key"), - ExpiryHours: 1, + Secret: []byte("e2e-test-secret-key"), + ExpiryHours: 1, RefreshExpiryHours: 24, } @@ -516,8 +504,8 @@ func (s *AuthE2ETestSuite) TestInactiveUserLoginRejected() { func (s *AuthE2ETestSuite) TestAuthTokensContainCorrectClaims() { cfg := crypto.JWTConfig{ - Secret: []byte("e2e-test-secret-key"), - ExpiryHours: 1, + Secret: []byte("e2e-test-secret-key"), + ExpiryHours: 1, RefreshExpiryHours: 24, } @@ -534,4 +522,4 @@ func (s *AuthE2ETestSuite) TestAuthTokensContainCorrectClaims() { // TestAuthE2ESuite runs the Auth E2E test suite. func TestAuthE2ESuite(t *testing.T) { suite.Run(t, new(AuthE2ETestSuite)) -} \ No newline at end of file +} diff --git a/backend/tests/e2e/crm_e2e_test.go b/backend/tests/e2e/crm_e2e_test.go index 3cc965b1..4f938e86 100644 --- a/backend/tests/e2e/crm_e2e_test.go +++ b/backend/tests/e2e/crm_e2e_test.go @@ -136,13 +136,12 @@ func (s *CRME2ETestSuite) TestContactListEmpty() { s.Require().NoError(err, "Should parse contact list response") assert.Equal(s.T(), http.StatusOK, resp.StatusCode, "List contacts should return 200") - assert.Equal(s.T(), fmt.Sprintf("%d", s.account.ID), result["account_id"], "Response should contain account_id") - meta, ok := result["meta"].(map[string]interface{}) - if ok { - count := meta["count"] - assert.Equal(s.T(), float64(0), count, "Empty contact list should have count=0") - } + s.Require().True(ok, "Response should contain meta") + assert.Equal(s.T(), float64(0), meta["count"], "Empty contact list should have count=0") + payload, ok := result["payload"].([]interface{}) + s.Require().True(ok, "Response should contain a payload array") + assert.Empty(s.T(), payload) } // TestContactCreate verifies creating a new contact via the API. @@ -170,9 +169,10 @@ func (s *CRME2ETestSuite) TestContactGet() { s.Require().NoError(err, "Should parse contact get response") assert.Equal(s.T(), http.StatusOK, resp.StatusCode, "Get contact should return 200") - idStr, ok := result["id"].(string) - s.Require().True(ok, "Response id should be a string") - assert.Equal(s.T(), strconv.FormatUint(uint64(contact.ID), 10), idStr, "Response should contain contact id") + payload, ok := result["payload"].(map[string]interface{}) + s.Require().True(ok, "Response should contain contact payload") + assert.Equal(s.T(), float64(contact.ID), payload["id"], "Response should contain contact id") + assert.Equal(s.T(), "Get Contact", payload["name"]) } // TestContactListWithContacts verifies listing contacts after creating some. @@ -188,7 +188,9 @@ func (s *CRME2ETestSuite) TestContactListWithContacts() { s.Require().NoError(err, "Should parse contact list response") assert.Equal(s.T(), http.StatusOK, resp.StatusCode, "List contacts should return 200") - assert.Equal(s.T(), fmt.Sprintf("%d", s.account.ID), result["account_id"], "Response should contain account_id") + payload, ok := result["payload"].([]interface{}) + s.Require().True(ok, "Response should contain a payload array") + assert.Len(s.T(), payload, 2) } // TestContactDBCRUD verifies the full Contact CRUD cycle at the DB level @@ -197,9 +199,9 @@ func (s *CRME2ETestSuite) TestContactDBCRUD() { // Create contact := &model.Contact{ - AccountID: s.account.ID, - Name: "DB CRUD Contact", - Email: "db_crud@test.com", + AccountID: s.account.ID, + Name: "DB CRUD Contact", + Email: "db_crud@test.com", PhoneNumber: "+1234567890", } err := s.DB().Create(contact).Error @@ -216,8 +218,8 @@ func (s *CRME2ETestSuite) TestContactDBCRUD() { // Update err = s.DB().Model(&retrieved).Updates(map[string]interface{}{ - "name": "Updated Contact", - "email": "updated@test.com", + "name": "Updated Contact", + "email": "updated@test.com", "phone_number": "+9876543210", }).Error s.Require().NoError(err, "Should update contact") @@ -245,43 +247,54 @@ func (s *CRME2ETestSuite) TestContactDBCRUD() { assert.NotNil(s.T(), unscoped.DeletedAt) } -// TestContactSearchNotImplemented verifies the search endpoint returns 501. -func (s *CRME2ETestSuite) TestContactSearchNotImplemented() { +// TestContactSearch verifies the implemented search endpoint. +func (s *CRME2ETestSuite) TestContactSearch() { + s.CreateTestContact("Search Target", "search-target@test.com", s.account.ID) path := fmt.Sprintf("/api/v1/accounts/%d/contacts/search?q=test", s.account.ID) resp := s.makeAuthRequest("GET", path, nil) result, err := parseResponseBody(resp) s.Require().NoError(err, "Should parse contact search response") - assert.Equal(s.T(), http.StatusNotImplemented, resp.StatusCode, "Search should return 501") - assert.Equal(s.T(), "test", result["query"], "Response should contain the query parameter") + assert.Equal(s.T(), http.StatusOK, resp.StatusCode, "Search should return 200") + payload, ok := result["payload"].([]interface{}) + s.Require().True(ok, "Search response should contain a payload array") + s.Require().Len(payload, 1) + contact, ok := payload[0].(map[string]interface{}) + s.Require().True(ok) + assert.Equal(s.T(), "Search Target", contact["name"]) } -// TestContactUpdateNotImplemented verifies the update endpoint returns 501. -func (s *CRME2ETestSuite) TestContactUpdateNotImplemented() { +// TestContactUpdate verifies the implemented update endpoint. +func (s *CRME2ETestSuite) TestContactUpdate() { contact := s.CreateTestContact("Update Contact", "update@test.com", s.account.ID) path := fmt.Sprintf("/api/v1/accounts/%d/contacts/%d", s.account.ID, contact.ID) resp := s.makeAuthRequest("PUT", path, map[string]interface{}{ "name": "Updated Name", }) + defer resp.Body.Close() - assert.Equal(s.T(), http.StatusNotImplemented, resp.StatusCode, "Update should return 501") + assert.Equal(s.T(), http.StatusOK, resp.StatusCode, "Update should return 200") + var updated model.Contact + s.Require().NoError(s.DB().First(&updated, contact.ID).Error) + assert.Equal(s.T(), "Updated Name", updated.Name) } -// TestContactDeleteNotImplemented verifies the delete endpoint returns 501. -func (s *CRME2ETestSuite) TestContactDeleteNotImplemented() { +// TestContactDelete verifies the implemented delete endpoint. +func (s *CRME2ETestSuite) TestContactDelete() { contact := s.CreateTestContact("Delete Contact", "delete@test.com", s.account.ID) path := fmt.Sprintf("/api/v1/accounts/%d/contacts/%d", s.account.ID, contact.ID) resp := s.makeAuthRequest("DELETE", path, nil) + defer resp.Body.Close() - assert.Equal(s.T(), http.StatusNotImplemented, resp.StatusCode, "Delete should return 501") + assert.Equal(s.T(), http.StatusOK, resp.StatusCode, "Delete should return 200") - // Verify the contact still exists in the DB var retrieved model.Contact err := s.DB().First(&retrieved, contact.ID).Error - assert.NoError(s.T(), err, "Contact should still exist in DB after 501 delete") + assert.Error(s.T(), err, "Deleted contact should not be visible") + assert.NoError(s.T(), s.DB().Unscoped().First(&retrieved, contact.ID).Error, "Contact delete should be soft") } // TestContactCrossAccountIsolation verifies that contacts from one account @@ -321,13 +334,9 @@ func (s *CRME2ETestSuite) TestInboxListEmpty() { s.Require().NoError(err, "Should parse inbox list response") assert.Equal(s.T(), http.StatusOK, resp.StatusCode, "List inboxes should return 200") - assert.Equal(s.T(), fmt.Sprintf("%d", s.account.ID), result["account_id"], "Response should contain account_id") - - meta, ok := result["meta"].(map[string]interface{}) - if ok { - count := meta["count"] - assert.Equal(s.T(), float64(0), count, "Empty inbox list should have count=0") - } + payload, ok := result["payload"].([]interface{}) + s.Require().True(ok, "Response should contain a payload array") + assert.Empty(s.T(), payload) } // TestInboxCreate verifies creating a new inbox via the API. @@ -355,9 +364,8 @@ func (s *CRME2ETestSuite) TestInboxGet() { s.Require().NoError(err, "Should parse inbox get response") assert.Equal(s.T(), http.StatusOK, resp.StatusCode, "Get inbox should return 200") - idStr, ok := result["id"].(string) - s.Require().True(ok, "Response id should be a string") - assert.Equal(s.T(), strconv.FormatUint(uint64(inbox.ID), 10), idStr, "Response should contain inbox id") + assert.Equal(s.T(), float64(inbox.ID), result["id"], "Response should contain inbox id") + assert.Equal(s.T(), "Support Inbox", result["name"]) } // TestInboxListWithInboxes verifies listing inboxes after creating some. @@ -372,7 +380,9 @@ func (s *CRME2ETestSuite) TestInboxListWithInboxes() { s.Require().NoError(err, "Should parse inbox list response") assert.Equal(s.T(), http.StatusOK, resp.StatusCode, "List inboxes should return 200") - assert.Equal(s.T(), fmt.Sprintf("%d", s.account.ID), result["account_id"], "Response should contain account_id") + payload, ok := result["payload"].([]interface{}) + s.Require().True(ok, "Response should contain a payload array") + assert.Len(s.T(), payload, 2) } // TestInboxDBCRUD verifies the full Inbox CRUD cycle at the DB level. @@ -427,31 +437,36 @@ func (s *CRME2ETestSuite) TestInboxDBCRUD() { assert.NotNil(s.T(), unscoped.DeletedAt) } -// TestInboxUpdateNotImplemented verifies the update endpoint returns 501. -func (s *CRME2ETestSuite) TestInboxUpdateNotImplemented() { +// TestInboxUpdate verifies the implemented update endpoint. +func (s *CRME2ETestSuite) TestInboxUpdate() { inbox := s.CreateTestInbox("Update Inbox", "web_widget", s.account.ID) path := fmt.Sprintf("/api/v1/accounts/%d/inboxes/%d", s.account.ID, inbox.ID) resp := s.makeAuthRequest("PUT", path, map[string]interface{}{ "name": "Updated Inbox", }) + defer resp.Body.Close() - assert.Equal(s.T(), http.StatusNotImplemented, resp.StatusCode, "Update should return 501") + assert.Equal(s.T(), http.StatusOK, resp.StatusCode, "Update should return 200") + var updated model.Inbox + s.Require().NoError(s.DB().First(&updated, inbox.ID).Error) + assert.Equal(s.T(), "Updated Inbox", updated.Name) } -// TestInboxDeleteNotImplemented verifies the delete endpoint returns 501. -func (s *CRME2ETestSuite) TestInboxDeleteNotImplemented() { +// TestInboxDelete verifies the implemented delete endpoint. +func (s *CRME2ETestSuite) TestInboxDelete() { inbox := s.CreateTestInbox("Delete Inbox", "web_widget", s.account.ID) path := fmt.Sprintf("/api/v1/accounts/%d/inboxes/%d", s.account.ID, inbox.ID) resp := s.makeAuthRequest("DELETE", path, nil) + defer resp.Body.Close() - assert.Equal(s.T(), http.StatusNotImplemented, resp.StatusCode, "Delete should return 501") + assert.Equal(s.T(), http.StatusOK, resp.StatusCode, "Delete should return 200") - // Verify the inbox still exists in the DB var retrieved model.Inbox err := s.DB().First(&retrieved, inbox.ID).Error - assert.NoError(s.T(), err, "Inbox should still exist in DB after 501 delete") + assert.Error(s.T(), err, "Deleted inbox should not be visible") + assert.NoError(s.T(), s.DB().Unscoped().First(&retrieved, inbox.ID).Error, "Inbox delete should be soft") } // TestInboxCrossAccountIsolation verifies that inboxes from one account @@ -619,4 +634,4 @@ func (s *CRME2ETestSuite) TestCRMFlowIntegration() { // TestCRME2ESuite runs the CRM end-to-end test suite. func TestCRME2ESuite(t *testing.T) { suite.Run(t, &CRME2ETestSuite{}) -} \ No newline at end of file +} diff --git a/backend/tests/e2e/csrf_e2e_test.go b/backend/tests/e2e/csrf_e2e_test.go index 850ca1f5..ac19a4b8 100644 --- a/backend/tests/e2e/csrf_e2e_test.go +++ b/backend/tests/e2e/csrf_e2e_test.go @@ -26,12 +26,6 @@ type CSRFE2ETestSuite struct { router *gin.Engine } -// SetupSuite builds a minimal Gin router with CSRF middleware and a test endpoint. -func (s *CSRFE2ETestSuite) SetupSuite() { - // E2E tests require PostgreSQL; skip in SQLite test mode. - s.T().Skip("E2E tests require PostgreSQL; skipping in SQLite test mode") -} - // TearDownSuite shuts down the httptest server (if running). func (s *CSRFE2ETestSuite) TearDownSuite() { if s.server != nil { @@ -729,7 +723,7 @@ func (s *CSRFE2ETestSuite) TestStaleToken_AfterRefresh_Returns403() { // we send the new cookie + old header, it should fail. postReq, err := http.NewRequest("POST", server.URL+"/api/test", nil) s.Require().NoError(err) - postReq.AddCookie(newCookie) // New cookie + postReq.AddCookie(newCookie) // New cookie postReq.Header.Set("X-CSRF-Token", oldToken) // Old header value postResp, err := http.DefaultClient.Do(postReq) @@ -808,4 +802,4 @@ func (s *CSRFE2ETestSuite) TestCookieHTTPOnlyFlag() { func TestCSRFE2ETestSuite(t *testing.T) { suite.Run(t, new(CSRFE2ETestSuite)) -} \ No newline at end of file +} diff --git a/backend/tests/e2e/dashboard_app_e2e_test.go b/backend/tests/e2e/dashboard_app_e2e_test.go index 5a1f2dfe..56aa7172 100644 --- a/backend/tests/e2e/dashboard_app_e2e_test.go +++ b/backend/tests/e2e/dashboard_app_e2e_test.go @@ -46,24 +46,20 @@ func (s *DashboardAppE2ETestSuite) SetupSuite() { // Register DashboardApp routes under the existing auth-protected group // Route pattern matches router.go: accountScoped.Group("/dashboard_apps") - // Note: "accountScoped" uses :id for account_id; DashboardApp sub-routes - // also use :id for app_id. Gin resolves this correctly: - // - List/Create use :id as account_id from the parent group - // - Get/Update/Delete use :dashboard_app_id from the sub-resource path - accountScoped := s.Router().Group("/api/v1/accounts/:id", middleware.AuthMiddleware(&s.Config().JWT)) + accountScoped := s.Router().Group("/api/v1/accounts/:account_id", middleware.AuthMiddleware(&s.Config().JWT)) dashboardApps := accountScoped.Group("/dashboard_apps") { dashboardApps.GET("", dashboardAppHandler.List) dashboardApps.POST("", dashboardAppHandler.Create) dashboardApps.GET("/search", dashboardAppHandler.Search) - dashboardApps.GET("/:dashboard_app_id", dashboardAppHandler.Get) - dashboardApps.PUT("/:dashboard_app_id", dashboardAppHandler.Update) - dashboardApps.PATCH("/:dashboard_app_id", dashboardAppHandler.Patch) - dashboardApps.DELETE("/:dashboard_app_id", dashboardAppHandler.Delete) - dashboardApps.GET("/:dashboard_app_id/widgets", dashboardAppHandler.GetWidgets) - dashboardApps.POST("/:dashboard_app_id/widgets", dashboardAppHandler.AddWidget) - dashboardApps.PUT("/:dashboard_app_id/widgets/:widget_index", dashboardAppHandler.UpdateWidget) - dashboardApps.DELETE("/:dashboard_app_id/widgets/:widget_index", dashboardAppHandler.RemoveWidget) + dashboardApps.GET("/:id", dashboardAppHandler.Get) + dashboardApps.PUT("/:id", dashboardAppHandler.Update) + dashboardApps.PATCH("/:id", dashboardAppHandler.Patch) + dashboardApps.DELETE("/:id", dashboardAppHandler.Delete) + dashboardApps.GET("/:id/widgets", dashboardAppHandler.GetWidgets) + dashboardApps.POST("/:id/widgets", dashboardAppHandler.AddWidget) + dashboardApps.PUT("/:id/widgets/:widget_index", dashboardAppHandler.UpdateWidget) + dashboardApps.DELETE("/:id/widgets/:widget_index", dashboardAppHandler.RemoveWidget) } } @@ -163,13 +159,20 @@ func parseDashAppResponse(resp *http.Response) map[string]interface{} { return result } +func parseDashAppList(resp *http.Response) []map[string]interface{} { + defer resp.Body.Close() + var result []map[string]interface{} + _ = json.NewDecoder(resp.Body).Decode(&result) + return result +} + // ============================================================ // DashboardApp CRUD E2E Tests (M12) // ============================================================ // TestDashboardAppCreate verifies creating a dashboard app end-to-end. func (s *DashboardAppE2ETestSuite) TestDashboardAppCreate() { - path := fmt.Sprintf("/api/v1/accounts/%d/dashboard_apps/", s.accountID) + path := fmt.Sprintf("/api/v1/accounts/%d/dashboard_apps", s.accountID) payload := map[string]interface{}{ "title": "销售仪表盘", "content": json.RawMessage(`[{"type":"frame","url":"https://example.com/widget"}]`), @@ -178,19 +181,15 @@ func (s *DashboardAppE2ETestSuite) TestDashboardAppCreate() { resp := s.authRequest("POST", path, payload) defer resp.Body.Close() - // Assert HTTP 201 Created - assert.Equal(s.T(), http.StatusCreated, resp.StatusCode, "Create should return 201") + assert.Equal(s.T(), http.StatusOK, resp.StatusCode, "Create should return 200") result := parseDashAppResponse(resp) assert.NotNil(s.T(), result) - - data, ok := result["data"].(map[string]interface{}) - assert.True(s.T(), ok, "Response should contain data field") - assert.NotEmpty(s.T(), data["id"], "Created dashboard app should have an ID") - assert.Equal(s.T(), "销售仪表盘", data["title"], "Title should match") + assert.NotEmpty(s.T(), result["id"], "Created dashboard app should have an ID") + assert.Equal(s.T(), "销售仪表盘", result["title"], "Title should match") // Verify in database - appID := uint(data["id"].(float64)) + appID := uint(result["id"].(float64)) var app model.DashboardApp err := s.DB().First(&app, appID).Error assert.NoError(s.T(), err, "Dashboard app should exist in database") @@ -200,7 +199,7 @@ func (s *DashboardAppE2ETestSuite) TestDashboardAppCreate() { // TestDashboardAppCreate_ValidationError verifies that invalid content returns an error. func (s *DashboardAppE2ETestSuite) TestDashboardAppCreate_ValidationError() { - path := fmt.Sprintf("/api/v1/accounts/%d/dashboard_apps/", s.accountID) + path := fmt.Sprintf("/api/v1/accounts/%d/dashboard_apps", s.accountID) // Invalid content — content must be a JSON array of valid widgets, not a string payload := map[string]interface{}{ @@ -210,9 +209,7 @@ func (s *DashboardAppE2ETestSuite) TestDashboardAppCreate_ValidationError() { resp := s.authRequest("POST", path, payload) defer resp.Body.Close() - // The service validates content and should return an error - // Handler maps service errors to 500, but the key thing is it should not return 201 - assert.NotEqual(s.T(), http.StatusCreated, resp.StatusCode, "Invalid content should not return 201") + assert.Equal(s.T(), http.StatusUnprocessableEntity, resp.StatusCode) } // TestDashboardAppGet verifies retrieving a dashboard app by ID. @@ -233,10 +230,8 @@ func (s *DashboardAppE2ETestSuite) TestDashboardAppGet() { assert.Equal(s.T(), http.StatusOK, resp.StatusCode, "Get should return 200") result := parseDashAppResponse(resp) - data, ok := result["data"].(map[string]interface{}) - assert.True(s.T(), ok, "Response should contain data field") - assert.Equal(s.T(), float64(app.ID), data["id"], "ID should match") - assert.Equal(s.T(), "获取测试仪表盘", data["title"], "Title should match") + assert.Equal(s.T(), float64(app.ID), result["id"], "ID should match") + assert.Equal(s.T(), "获取测试仪表盘", result["title"], "Title should match") } // TestDashboardAppGet_NotFound verifies 404 for non-existent dashboard app. @@ -261,16 +256,14 @@ func (s *DashboardAppE2ETestSuite) TestDashboardAppList() { assert.NoError(s.T(), err, "Failed to create test dashboard app") } - path := fmt.Sprintf("/api/v1/accounts/%d/dashboard_apps/", s.accountID) + path := fmt.Sprintf("/api/v1/accounts/%d/dashboard_apps", s.accountID) resp := s.authRequest("GET", path, nil) defer resp.Body.Close() assert.Equal(s.T(), http.StatusOK, resp.StatusCode, "List should return 200") - result := parseDashAppResponse(resp) - data, ok := result["data"].([]interface{}) - assert.True(s.T(), ok, "Response data should be an array") - assert.Len(s.T(), data, 3, "Should return 3 dashboard apps") + result := parseDashAppList(resp) + assert.Len(s.T(), result, 3, "Should return 3 dashboard apps") } // TestDashboardAppList_Empty verifies listing returns empty array when no apps exist. @@ -280,16 +273,14 @@ func (s *DashboardAppE2ETestSuite) TestDashboardAppList_Empty() { err := s.DB().Create(emptyAccount).Error assert.NoError(s.T(), err) - path := fmt.Sprintf("/api/v1/accounts/%d/dashboard_apps/", emptyAccount.ID) + path := fmt.Sprintf("/api/v1/accounts/%d/dashboard_apps", emptyAccount.ID) resp := s.authRequest("GET", path, nil) defer resp.Body.Close() assert.Equal(s.T(), http.StatusOK, resp.StatusCode) - result := parseDashAppResponse(resp) - data, ok := result["data"].([]interface{}) - assert.True(s.T(), ok) - assert.Empty(s.T(), data, "Should return empty list") + result := parseDashAppList(resp) + assert.Empty(s.T(), result, "Should return empty list") } // TestDashboardAppUpdate verifies updating a dashboard app's title and content. @@ -314,9 +305,7 @@ func (s *DashboardAppE2ETestSuite) TestDashboardAppUpdate() { assert.Equal(s.T(), http.StatusOK, resp.StatusCode, "Update should return 200") result := parseDashAppResponse(resp) - data, ok := result["data"].(map[string]interface{}) - assert.True(s.T(), ok) - assert.Equal(s.T(), "更新后仪表盘", data["title"], "Title should be updated") + assert.Equal(s.T(), "更新后仪表盘", result["title"], "Title should be updated") // Verify in database var updated model.DashboardApp @@ -335,9 +324,8 @@ func (s *DashboardAppE2ETestSuite) TestDashboardAppUpdate_NotFound() { resp := s.authRequest("PUT", path, payload) defer resp.Body.Close() - // Service returns error for non-existent ID → handler returns 500 - assert.Equal(s.T(), http.StatusInternalServerError, resp.StatusCode, - "Update non-existent should return 500") + assert.Equal(s.T(), http.StatusNotFound, resp.StatusCode, + "Update non-existent should return 404") } // TestDashboardAppDelete verifies deleting a dashboard app (soft-delete). @@ -369,27 +357,25 @@ func (s *DashboardAppE2ETestSuite) TestDashboardAppDelete_NotFound() { resp := s.authRequest("DELETE", path, nil) defer resp.Body.Close() - // GORM Delete doesn't error on missing rows → handler returns 204 - assert.Equal(s.T(), http.StatusNoContent, resp.StatusCode, - "Deleting non-existent app should return 204") + assert.Equal(s.T(), http.StatusNotFound, resp.StatusCode, + "Deleting non-existent app should return 404") } // TestDashboardAppFullCRUDFlow verifies the complete lifecycle: // Create → Get → List → Update → Delete func (s *DashboardAppE2ETestSuite) TestDashboardAppFullCRUDFlow() { // Step 1: Create - path := fmt.Sprintf("/api/v1/accounts/%d/dashboard_apps/", s.accountID) + path := fmt.Sprintf("/api/v1/accounts/%d/dashboard_apps", s.accountID) createPayload := map[string]interface{}{ "title": "全流程仪表盘", "content": json.RawMessage(`[{"type":"frame","url":"https://full-crud.example.com"}]`), } resp := s.authRequest("POST", path, createPayload) defer resp.Body.Close() - assert.Equal(s.T(), http.StatusCreated, resp.StatusCode) + assert.Equal(s.T(), http.StatusOK, resp.StatusCode) result := parseDashAppResponse(resp) - data := result["data"].(map[string]interface{}) - appID := uint(data["id"].(float64)) + appID := uint(result["id"].(float64)) assert.NotZero(s.T(), appID) // Step 2: Get @@ -398,8 +384,7 @@ func (s *DashboardAppE2ETestSuite) TestDashboardAppFullCRUDFlow() { defer resp.Body.Close() assert.Equal(s.T(), http.StatusOK, resp.StatusCode) result = parseDashAppResponse(resp) - data = result["data"].(map[string]interface{}) - assert.Equal(s.T(), "全流程仪表盘", data["title"]) + assert.Equal(s.T(), "全流程仪表盘", result["title"]) // Step 3: Update updatePayload := map[string]interface{}{ @@ -410,8 +395,7 @@ func (s *DashboardAppE2ETestSuite) TestDashboardAppFullCRUDFlow() { defer resp.Body.Close() assert.Equal(s.T(), http.StatusOK, resp.StatusCode) result = parseDashAppResponse(resp) - data = result["data"].(map[string]interface{}) - assert.Equal(s.T(), "全流程更新后", data["title"]) + assert.Equal(s.T(), "全流程更新后", result["title"]) // Step 4: Delete resp = s.authRequest("DELETE", getPath, nil) @@ -462,4 +446,4 @@ func (s *DashboardAppE2ETestSuite) TestDashboardApp_PG_JSONBQuery() { func TestDashboardAppE2ESuite(t *testing.T) { suite.Run(t, new(DashboardAppE2ETestSuite)) -} \ No newline at end of file +} diff --git a/backend/tests/e2e/e2e_test.go b/backend/tests/e2e/e2e_test.go index c86f8697..53cfd308 100644 --- a/backend/tests/e2e/e2e_test.go +++ b/backend/tests/e2e/e2e_test.go @@ -5,16 +5,13 @@ import ( "fmt" "net/http" "net/http/httptest" - "testing" "time" - "github.com/gin-gonic/gin" - "github.com/stretchr/testify/suite" "github.com/alicebob/miniredis/v2" + "github.com/gin-gonic/gin" "github.com/redis/go-redis/v9" - "gorm.io/driver/sqlite" + "github.com/stretchr/testify/suite" "gorm.io/gorm" - "gorm.io/gorm/logger" "github.com/gochat/gochat/internal/auth" "github.com/gochat/gochat/internal/config" @@ -23,10 +20,11 @@ import ( "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/repository" "github.com/gochat/gochat/internal/service" + testhelpers "github.com/gochat/gochat/tests/helpers" ) // E2ETestSuite provides a full end-to-end test environment with a live HTTP server -// and an in-memory SQLite database. All E2E test files use this suite. +// and a PostgreSQL database. All database-backed E2E test files use this suite. // Reference: Chatwoot's integration test setup pattern (spec/integration/ helpers) type E2ETestSuite struct { suite.Suite @@ -40,35 +38,10 @@ type E2ETestSuite struct { // SetupSuite initializes the test database and HTTP server once for the entire suite. func (s *E2ETestSuite) SetupSuite() { - // E2E tests require a full PostgreSQL-backed environment with real services. - // Skipping in SQLite-only test mode. - s.T().Skip("E2E tests require PostgreSQL; skipping in SQLite test mode") - - // Create in-memory SQLite database - db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{ - Logger: logger.Default.LogMode(logger.Silent), - }) - s.Require().NoError(err, "Failed to open in-memory SQLite database") - - // Auto-migrate all models - err = db.AutoMigrate( - &model.User{}, - &model.Account{}, - &model.AccountUser{}, - &model.CustomRole{}, - &model.Inbox{}, - &model.Conversation{}, - &model.Message{}, - &model.Contact{}, - &model.ContactInbox{}, - &model.Attachment{}, - &model.Notification{}, - &model.NotificationPreference{}, - &model.InboxMember{}, - &model.DashboardApp{}, - ) - s.Require().NoError(err, "Failed to auto-migrate models") - + if !testhelpers.UsePostgres() { + s.T().Skip("database-backed E2E tests require PostgreSQL") + } + db := testhelpers.SetupTestDB(s.T()) s.db = db // Create test config @@ -137,13 +110,13 @@ func (s *E2ETestSuite) SetupSuite() { { // Account CRUD protected.GET("/accounts", accountHandler.List) - protected.GET("/accounts/:id", accountHandler.Get) + protected.GET("/accounts/:account_id", accountHandler.Get) protected.POST("/accounts", accountHandler.Create) - protected.PUT("/accounts/:id", accountHandler.Update) - protected.DELETE("/accounts/:id", accountHandler.Delete) + protected.PUT("/accounts/:account_id", accountHandler.Update) + protected.DELETE("/accounts/:account_id", accountHandler.Delete) - // Contact CRUD (scoped under /accounts/:id — matches handler's c.Param("id")) - contacts := protected.Group("/accounts/:id") + // Account-scoped routes mirror the production router's :account_id parameter. + contacts := protected.Group("/accounts/:account_id") { contacts.GET("/contacts", contactHandler.List) contacts.POST("/contacts", contactHandler.Create) @@ -153,8 +126,8 @@ func (s *E2ETestSuite) SetupSuite() { contacts.GET("/contacts/search", contactHandler.Search) } - // Inbox CRUD (scoped under /accounts/:id) - inboxes := protected.Group("/accounts/:id") + // Inbox CRUD + inboxes := protected.Group("/accounts/:account_id") { inboxes.GET("/inboxes", inboxHandler.List) inboxes.POST("/inboxes", inboxHandler.Create) @@ -321,8 +294,3 @@ func (s *E2ETestSuite) makeRequestWithClient(method, path string, body interface // Implementation left for individual test files return nil } - -// TestE2EHealthCheck verifies the test suite health endpoint works. -func TestE2EHealthCheck(t *testing.T) { - // Basic sanity check — will be expanded in individual e2e test files -} \ No newline at end of file diff --git a/backend/tests/e2e/middleware_e2e_test.go b/backend/tests/e2e/middleware_e2e_test.go index 1a094dcd..c25d4784 100644 --- a/backend/tests/e2e/middleware_e2e_test.go +++ b/backend/tests/e2e/middleware_e2e_test.go @@ -24,16 +24,13 @@ import ( // Reference: Chatwoot's rack-cors configuration and middleware integration specs. type MiddlewareE2ETestSuite struct { suite.Suite - server *httptest.Server - router *gin.Engine - jwtCfg *config.JWTConfig + server *httptest.Server + router *gin.Engine + jwtCfg *config.JWTConfig } // SetupSuite builds a minimal Gin router with CORS + Auth middleware and a health endpoint. func (s *MiddlewareE2ETestSuite) SetupSuite() { - // E2E tests require PostgreSQL; skip in SQLite test mode. - s.T().Skip("E2E tests require PostgreSQL; skipping in SQLite test mode") - s.jwtCfg = &config.JWTConfig{ Secret: "middleware-e2e-test-secret", ExpiryHours: 1, @@ -72,7 +69,7 @@ func (s *MiddlewareE2ETestSuite) SetupSuite() { c.JSON(200, gin.H{ "user_id": userID, "account_id": accountID, - "role": role, + "role": role, }) }) } @@ -544,4 +541,4 @@ func (s *MiddlewareE2ETestSuite) TestCORSAndAuth_DisallowedOriginBlocksEvenWithV func TestMiddlewareE2ESuite(t *testing.T) { suite.Run(t, new(MiddlewareE2ETestSuite)) -} \ No newline at end of file +} diff --git a/backend/tests/e2e/session_e2e_test.go b/backend/tests/e2e/session_e2e_test.go index af784b5a..76d30210 100644 --- a/backend/tests/e2e/session_e2e_test.go +++ b/backend/tests/e2e/session_e2e_test.go @@ -18,7 +18,7 @@ import ( // SessionE2ETestSuite tests session store and middleware integration. type SessionE2ETestSuite struct { suite.Suite - store *auth.SessionStore + store *auth.SessionStore sessionCfg config.SessionConfig } @@ -26,14 +26,12 @@ func TestSessionE2ETestSuite(t *testing.T) { suite.Run(t, new(SessionE2ETestSuite)) } -func (s *SessionE2ETestSuite) SetupSuite() { - // E2E tests require PostgreSQL; skip in SQLite test mode. - s.T().Skip("E2E tests require PostgreSQL; skipping in SQLite test mode") +func (s *SessionE2ETestSuite) SetupTest() { s.sessionCfg = config.SessionConfig{ - Enabled: true, - ExpirySeconds: 3600, - TokenLength: 32, - HeaderName: "X-Session-ID", + Enabled: true, + ExpirySeconds: 3600, + TokenLength: 32, + HeaderName: "X-Session-ID", CleanupInterval: 300, } s.store = auth.NewSessionStore(&s.sessionCfg) @@ -81,7 +79,7 @@ func (s *SessionE2ETestSuite) TestSessionDelete() { func (s *SessionE2ETestSuite) TestSessionDeleteByUserID() { s.store.Create(4, 400, "agent", "email") - s.store.Create(4, 500, "agent", "email") // same user, different account + s.store.Create(4, 500, "agent", "email") // same user, different account s.store.Create(5, 600, "administrator", "email") // different user count := s.store.DeleteByUserID(4) @@ -138,7 +136,7 @@ func (s *SessionE2ETestSuite) TestSessionCleanupExpired() { func (s *SessionE2ETestSuite) TestSessionCount() { initialCount := s.store.Count() s.store.Create(9, 1000, "agent", "email") - assert.Equal(s.T(), initialCount + 1, s.store.Count()) + assert.Equal(s.T(), initialCount+1, s.store.Count()) } // --- Session Middleware Tests --- @@ -276,4 +274,4 @@ func (s *SessionE2ETestSuite) TestRequireSessionMiddleware() { req2, _ := http.NewRequest("GET", "/test", nil) router2.ServeHTTP(w2, req2) assert.Equal(s.T(), 200, w2.Code) -} \ No newline at end of file +} diff --git a/backend/tests/helpers/pg_helper.go b/backend/tests/helpers/pg_helper.go index b8dd5d09..361b68e1 100644 --- a/backend/tests/helpers/pg_helper.go +++ b/backend/tests/helpers/pg_helper.go @@ -44,6 +44,9 @@ func SetupTestDB(t *testing.T) *gorm.DB { if UsePostgres() { db := setupPostgresDB(t) + if err := db.Exec("CREATE EXTENSION IF NOT EXISTS vector").Error; err != nil { + t.Fatalf("failed to enable pgvector: %v", err) + } autoMigrateAll(t, db) return db } @@ -71,8 +74,8 @@ func setupPostgresDB(t *testing.T) *gorm.DB { dsn := testDSN() db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ - Logger: logger.Default.LogMode(logger.Silent), - PrepareStmt: true, + Logger: logger.Default.LogMode(logger.Silent), + PrepareStmt: true, DisableForeignKeyConstraintWhenMigrating: true, }) if err != nil { @@ -107,8 +110,8 @@ func setupPostgresIsolatedDB(t *testing.T) *gorm.DB { dsn := testDSN() db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ - Logger: logger.Default.LogMode(logger.Silent), - PrepareStmt: true, + Logger: logger.Default.LogMode(logger.Silent), + PrepareStmt: true, DisableForeignKeyConstraintWhenMigrating: true, }) if err != nil { @@ -257,4 +260,4 @@ func sanitizeSchemaName(name string) string { result = result[:63] } return string(result) -} \ No newline at end of file +}