Files
gochat/internal/handler/api/v1/platform_e2e_test.go
T

517 lines
19 KiB
Go

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)
}