package v1_test import ( "bytes" "encoding/json" "fmt" "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/gochat/gochat/internal/handler/api/v1" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/repository" "github.com/gochat/gochat/internal/service" "github.com/gochat/gochat/pkg/testutil" ) // unpackData extracts the "data" field from GoChat's standard API envelope: // {"success": true, "data": {...}} func unpackData(t *testing.T, body []byte) map[string]interface{} { t.Helper() var envelope map[string]interface{} err := json.Unmarshal(body, &envelope) require.NoError(t, err, "response is not valid JSON: %s", string(body)) data, ok := envelope["data"] require.True(t, ok, "response has no 'data' field: %s", string(body)) dataMap, ok := data.(map[string]interface{}) require.True(t, ok, "'data' is not a map: %v", data) return dataMap } func unpackRawObject(t *testing.T, body []byte) map[string]interface{} { t.Helper() var data map[string]interface{} err := json.Unmarshal(body, &data) require.NoError(t, err, "response is not valid JSON: %s", string(body)) return data } // parseID extracts the numeric "id" from a data envelope and returns it as a string for URL paths. func parseID(t *testing.T, data map[string]interface{}) string { t.Helper() id := data["id"] require.NotNil(t, id, "data has no 'id' field") return fmt.Sprint(uint(id.(float64))) } func setupPlatformTokenTestE2E(t *testing.T) (*gin.Engine, *repository.PermissibleRepo, *repository.UserRepo, *repository.AccountRepo) { t.Helper() gin.SetMode(gin.TestMode) db := testutil.NewTestDBWithModels(t, &model.User{}, &model.Account{}, &model.AgentBot{}, &model.AgentBotInbox{}, &model.Inbox{}, &model.Permissible{}, &model.AccessToken{}, &model.PlatformApp{}, &model.AccountUser{}, ) userRepo := repository.NewUserRepo(db) accountRepo := repository.NewAccountRepo(db) accountUserRepo := repository.NewAccountUserRepo(db) accessTokenRepo := repository.NewAccessTokenRepo(db) agentBotRepo := repository.NewAgentBotRepo(db) permissibleRepo := repository.NewPermissibleRepo(db) accountService := service.NewAccountService(accountRepo) platformUserService := service.NewPlatformUserService(userRepo, permissibleRepo, accessTokenRepo, accountUserRepo) platformUser := v1.NewPlatformUserHandler(platformUserService) platformAccount := v1.NewPlatformAccountHandler(accountRepo, permissibleRepo, accountService) platformAgentBot := v1.NewPlatformAgentBotHandler(agentBotRepo, permissibleRepo) platformAccountUser := v1.NewPlatformAccountUserHandler(accountRepo, userRepo, permissibleRepo) engine := gin.New() platformGroup := engine.Group("/platform/api/v1") platformGroup.Use(func(c *gin.Context) { c.Set("platform_app_id", uint(1)) c.Set("access_token_owner_type", "PlatformApp") c.Next() }) // Register routes (mirrors registerPlatformTokenRoutes in router.go) platformGroup.GET("/users", platformUser.List) platformGroup.GET("/users/:id", platformUser.Show) platformGroup.POST("/users", platformUser.Create) platformGroup.GET("/users/:id/login", platformUser.Login) platformGroup.POST("/users/:id/login", platformUser.Login) platformGroup.POST("/users/:id/token", platformUser.Token) platformGroup.PATCH("/users/:id", platformUser.Update) platformGroup.DELETE("/users/:id", platformUser.Destroy) platformGroup.GET("/accounts", platformAccount.List) platformGroup.GET("/accounts/:id", platformAccount.Show) platformGroup.POST("/accounts", platformAccount.Create) platformGroup.PATCH("/accounts/:id", platformAccount.Update) platformGroup.DELETE("/accounts/:id", platformAccount.Destroy) platformGroup.GET("/agent_bots", platformAgentBot.List) platformGroup.GET("/agent_bots/:id", platformAgentBot.Show) platformGroup.POST("/agent_bots", platformAgentBot.Create) platformGroup.PUT("/agent_bots/:id", platformAgentBot.Update) platformGroup.DELETE("/agent_bots/:id", platformAgentBot.Destroy) platformGroup.POST("/agent_bots/:id/delete_avatar", platformAgentBot.DeleteAvatar) // Gin wildcard constraint: nested routes under accounts/:id must reuse :id. platformGroup.GET("/accounts/:id/account_users", platformAccountUser.Index) platformGroup.POST("/accounts/:id/account_users", platformAccountUser.Create) platformGroup.DELETE("/accounts/:id/account_users/destroy", platformAccountUser.Destroy) platformGroup.DELETE("/accounts/:id/account_users/:user_id", platformAccountUser.Destroy) return engine, permissibleRepo, userRepo, accountRepo } // --- Platform User E2E Tests --- func TestPlatformUserE2E_Create(t *testing.T) { engine, _, _, _ := setupPlatformTokenTestE2E(t) body := `{"name": "Test User", "email": "test@example.com", "password": "secret123"}` w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/platform/api/v1/users", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") engine.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) data := unpackRawObject(t, w.Body.Bytes()) assert.Equal(t, "Test User", data["name"]) assert.Equal(t, "test@example.com", data["email"]) assert.NotEmpty(t, data["access_token"]) assert.Contains(t, data, "accounts") } func TestPlatformUserE2E_CreateExistingUserReturnsExistingAndPermits(t *testing.T) { engine, permissibleRepo, userRepo, _ := setupPlatformTokenTestE2E(t) ctx := t.Context() existing := &model.User{Name: "Old Name", Email: "existing@example.com", Provider: "email", Active: true} require.NoError(t, userRepo.Create(ctx, existing)) body := `{"name": "New Name", "email": "existing@example.com", "password": "secret123"}` w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/platform/api/v1/users", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") engine.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) data := unpackRawObject(t, w.Body.Bytes()) assert.Equal(t, "Old Name", data["name"]) assert.Equal(t, float64(existing.ID), data["id"]) _, err := permissibleRepo.FindByPlatformAppAndResource(ctx, uint(1), model.PermissibleTypeUser, existing.ID) assert.NoError(t, err) } func TestPlatformUserE2E_Show(t *testing.T) { engine, _, _, _ := setupPlatformTokenTestE2E(t) // Create user first body := `{"name": "Show User", "email": "show@example.com"}` w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/platform/api/v1/users", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") engine.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) createData := unpackRawObject(t, w.Body.Bytes()) userID := parseID(t, createData) // Show user w = httptest.NewRecorder() req, _ = http.NewRequest("GET", "/platform/api/v1/users/"+userID, nil) engine.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) showData := unpackRawObject(t, w.Body.Bytes()) assert.Equal(t, "Show User", showData["name"]) } func TestPlatformUserE2E_Update(t *testing.T) { engine, _, _, _ := setupPlatformTokenTestE2E(t) // Create user body := `{"name": "Original", "email": "original@example.com"}` w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/platform/api/v1/users", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") engine.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) createData := unpackRawObject(t, w.Body.Bytes()) userID := parseID(t, createData) // Update user updateBody := `{"name": "Updated Name", "email": "updated@example.com", "custom_attributes": {"tier": "gold"}}` w = httptest.NewRecorder() req, _ = http.NewRequest("PATCH", "/platform/api/v1/users/"+userID, bytes.NewBufferString(updateBody)) req.Header.Set("Content-Type", "application/json") engine.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) updateData := unpackRawObject(t, w.Body.Bytes()) assert.Equal(t, "Updated Name", updateData["name"]) attrs := updateData["custom_attributes"].(map[string]interface{}) assert.Equal(t, "gold", attrs["tier"]) } func TestPlatformUserE2E_Destroy(t *testing.T) { engine, _, _, _ := setupPlatformTokenTestE2E(t) // Create user body := `{"name": "Delete Me", "email": "delete@example.com"}` w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/platform/api/v1/users", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") engine.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) createData := unpackRawObject(t, w.Body.Bytes()) userID := parseID(t, createData) // Delete user w = httptest.NewRecorder() req, _ = http.NewRequest("DELETE", "/platform/api/v1/users/"+userID, nil) engine.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestPlatformUserE2E_Login(t *testing.T) { engine, _, _, _ := setupPlatformTokenTestE2E(t) // Create user body := `{"name": "SSO User", "email": "sso@example.com"}` w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/platform/api/v1/users", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") engine.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) createData := unpackRawObject(t, w.Body.Bytes()) userID := parseID(t, createData) // Login w = httptest.NewRecorder() req, _ = http.NewRequest("GET", "/platform/api/v1/users/"+userID+"/login", nil) engine.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) loginData := unpackRawObject(t, w.Body.Bytes()) assert.Contains(t, loginData["url"], "email=sso%40example.com") assert.Contains(t, loginData["url"], "sso_auth_token=") } func TestPlatformUserE2E_Token(t *testing.T) { engine, _, _, _ := setupPlatformTokenTestE2E(t) // Create user body := `{"name": "Token User", "email": "token@example.com"}` w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/platform/api/v1/users", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") engine.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) createData := unpackRawObject(t, w.Body.Bytes()) userID := parseID(t, createData) // Token w = httptest.NewRecorder() req, _ = http.NewRequest("POST", "/platform/api/v1/users/"+userID+"/token", nil) engine.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) tokenData := unpackRawObject(t, w.Body.Bytes()) assert.NotEmpty(t, tokenData["access_token"]) assert.Nil(t, tokenData["expiry"]) userInfo := tokenData["user"].(map[string]interface{}) assert.Equal(t, "Token User", userInfo["name"]) } func TestPlatformUserE2E_List(t *testing.T) { engine, _, _, _ := setupPlatformTokenTestE2E(t) // Create 2 users for i := 0; i < 2; i++ { body := fmt.Sprintf(`{"name": "User %d", "email": "list%d@example.com"}`, i, i) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/platform/api/v1/users", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") engine.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) } // List users w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/platform/api/v1/users", nil) engine.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } // --- Platform Account E2E Tests --- func TestPlatformAccountE2E_Create(t *testing.T) { engine, _, _, _ := setupPlatformTokenTestE2E(t) body := `{"name": "Test Account", "locale": "zh", "timezone": "Asia/Shanghai"}` w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/platform/api/v1/accounts", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") engine.ServeHTTP(w, req) assert.Equal(t, http.StatusCreated, w.Code) data := unpackData(t, w.Body.Bytes()) assert.Equal(t, "Test Account", data["name"]) } func TestPlatformAccountE2E_Show(t *testing.T) { engine, _, _, _ := setupPlatformTokenTestE2E(t) // Create account body := `{"name": "Show Account"}` w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/platform/api/v1/accounts", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") engine.ServeHTTP(w, req) require.Equal(t, http.StatusCreated, w.Code) createData := unpackData(t, w.Body.Bytes()) accountID := parseID(t, createData) // Show account (permissible since Create auto-creates Permissible) w = httptest.NewRecorder() req, _ = http.NewRequest("GET", "/platform/api/v1/accounts/"+accountID, nil) engine.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) showData := unpackData(t, w.Body.Bytes()) assert.Equal(t, "Show Account", showData["name"]) } func TestPlatformAccountE2E_Destroy(t *testing.T) { engine, _, _, _ := setupPlatformTokenTestE2E(t) // Create account body := `{"name": "Delete Account"}` w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/platform/api/v1/accounts", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") engine.ServeHTTP(w, req) require.Equal(t, http.StatusCreated, w.Code) createData := unpackData(t, w.Body.Bytes()) accountID := parseID(t, createData) // Delete w = httptest.NewRecorder() req, _ = http.NewRequest("DELETE", "/platform/api/v1/accounts/"+accountID, nil) engine.ServeHTTP(w, req) assert.Equal(t, http.StatusNoContent, w.Code) } // --- Platform Agent Bot E2E Tests --- func TestPlatformAgentBotE2E_Create(t *testing.T) { engine, _, _, _ := setupPlatformTokenTestE2E(t) body := `{"name": "Test Bot", "description": "A test bot"}` w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/platform/api/v1/agent_bots", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") engine.ServeHTTP(w, req) assert.Equal(t, http.StatusCreated, w.Code) data := unpackData(t, w.Body.Bytes()) assert.Equal(t, "Test Bot", data["name"]) } func TestPlatformAgentBotE2E_Show(t *testing.T) { engine, _, _, _ := setupPlatformTokenTestE2E(t) // Create bot body := `{"name": "Show Bot", "description": "A show bot"}` w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/platform/api/v1/agent_bots", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") engine.ServeHTTP(w, req) require.Equal(t, http.StatusCreated, w.Code) createData := unpackData(t, w.Body.Bytes()) botID := parseID(t, createData) // Show bot w = httptest.NewRecorder() req, _ = http.NewRequest("GET", "/platform/api/v1/agent_bots/"+botID, nil) engine.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) showData := unpackData(t, w.Body.Bytes()) assert.Equal(t, "Show Bot", showData["name"]) } func TestPlatformAgentBotE2E_DeleteAvatar(t *testing.T) { engine, _, _, _ := setupPlatformTokenTestE2E(t) // Create bot body := `{"name": "Avatar Bot"}` w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/platform/api/v1/agent_bots", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") engine.ServeHTTP(w, req) require.Equal(t, http.StatusCreated, w.Code) createData := unpackData(t, w.Body.Bytes()) botID := parseID(t, createData) // Delete avatar w = httptest.NewRecorder() req, _ = http.NewRequest("POST", "/platform/api/v1/agent_bots/"+botID+"/delete_avatar", nil) engine.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } // --- Platform AccountUser E2E Tests --- func TestPlatformAccountUserE2E_Create(t *testing.T) { engine, _, _, _ := setupPlatformTokenTestE2E(t) // Create account via Platform API (auto-permissible) acctBody := `{"name": "AcctUser Test Account"}` w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/platform/api/v1/accounts", bytes.NewBufferString(acctBody)) req.Header.Set("Content-Type", "application/json") engine.ServeHTTP(w, req) require.Equal(t, http.StatusCreated, w.Code) acctData := unpackData(t, w.Body.Bytes()) accountID := parseID(t, acctData) // Create user via Platform API (auto-permissible) userBody := `{"name": "AcctUser Test User", "email": "acctuser@example.com"}` w = httptest.NewRecorder() req, _ = http.NewRequest("POST", "/platform/api/v1/users", bytes.NewBufferString(userBody)) req.Header.Set("Content-Type", "application/json") engine.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) userData := unpackRawObject(t, w.Body.Bytes()) userID := parseID(t, userData) // Create AccountUser acctUserBody := fmt.Sprintf(`{"user_id": %s, "role": "agent"}`, userID) w = httptest.NewRecorder() req, _ = http.NewRequest("POST", "/platform/api/v1/accounts/"+accountID+"/account_users", bytes.NewBufferString(acctUserBody)) req.Header.Set("Content-Type", "application/json") engine.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) var accountUser map[string]interface{} require.NoError(t, json.Unmarshal(w.Body.Bytes(), &accountUser)) assert.Equal(t, "agent", accountUser["role"]) assert.NotContains(t, accountUser, "success") // Chatwoot find_or_initialize_by updates existing memberships instead of failing duplicates. acctUserBody = fmt.Sprintf(`{"user_id": %s, "role": "administrator"}`, userID) w = httptest.NewRecorder() req, _ = http.NewRequest("POST", "/platform/api/v1/accounts/"+accountID+"/account_users", bytes.NewBufferString(acctUserBody)) req.Header.Set("Content-Type", "application/json") engine.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) require.NoError(t, json.Unmarshal(w.Body.Bytes(), &accountUser)) assert.Equal(t, "administrator", accountUser["role"]) } func TestPlatformAccountUserE2E_Index(t *testing.T) { engine, _, _, _ := setupPlatformTokenTestE2E(t) // Create account acctBody := `{"name": "Index Account"}` w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/platform/api/v1/accounts", bytes.NewBufferString(acctBody)) req.Header.Set("Content-Type", "application/json") engine.ServeHTTP(w, req) require.Equal(t, http.StatusCreated, w.Code) acctData := unpackData(t, w.Body.Bytes()) accountID := parseID(t, acctData) // Create user userBody := `{"name": "Index User", "email": "index@example.com"}` w = httptest.NewRecorder() req, _ = http.NewRequest("POST", "/platform/api/v1/users", bytes.NewBufferString(userBody)) req.Header.Set("Content-Type", "application/json") engine.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) userData := unpackRawObject(t, w.Body.Bytes()) userID := parseID(t, userData) // Add user to account acctUserBody := fmt.Sprintf(`{"user_id": %s, "role": "agent"}`, userID) w = httptest.NewRecorder() req, _ = http.NewRequest("POST", "/platform/api/v1/accounts/"+accountID+"/account_users", bytes.NewBufferString(acctUserBody)) req.Header.Set("Content-Type", "application/json") engine.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) // Index account_users w = httptest.NewRecorder() req, _ = http.NewRequest("GET", "/platform/api/v1/accounts/"+accountID+"/account_users", nil) engine.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) var accountUsers []map[string]interface{} require.NoError(t, json.Unmarshal(w.Body.Bytes(), &accountUsers)) require.Len(t, accountUsers, 1) assert.Equal(t, "agent", accountUsers[0]["role"]) assert.NotContains(t, accountUsers[0], "success") // Chatwoot destroy is a collection route: DELETE /account_users/destroy with user_id param. w = httptest.NewRecorder() req, _ = http.NewRequest("DELETE", "/platform/api/v1/accounts/"+accountID+"/account_users/destroy?user_id="+userID, nil) engine.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) }