Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
Restructure the monorepo into clear top-level directories: - backend/: Go module root (cmd, internal, pkg, configs, migrations, docs/swagger, scripts, tests, go.mod, Makefile, .air.toml) - deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd - docs/: project documentation + reports/ (moved from repo root) - AGENTS.md: new AI coding-agent guide at repo root Update all references to the new layout: - Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root) - docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile, env_file ../../.env, volume mounts ../../backend:/app - deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile - CI: working-directory: backend for go commands, file deploy/docker/Dockerfile, coverage path backend/coverage.out, health_check backend/scripts/ - backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../ - README: architecture tree, quickstart, config paths updated Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh, gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to preserve history. Build, vet, SQLite tests, and docker compose config verified.
This commit is contained in:
@@ -0,0 +1,465 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
handler "github.com/gochat/gochat/internal/handler/api/v1"
|
||||
"github.com/gochat/gochat/internal/middleware"
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
"github.com/gochat/gochat/internal/repository"
|
||||
"github.com/gochat/gochat/internal/service"
|
||||
"github.com/gochat/gochat/pkg/crypto"
|
||||
testhelpers "github.com/gochat/gochat/tests/helpers"
|
||||
)
|
||||
|
||||
// DashboardAppE2ETestSuite tests the full DashboardApp CRUD flow end-to-end:
|
||||
// Register → Login → Create DashboardApp → List → Get → Update → Delete
|
||||
// Reference: Chatwoot spec/controllers/api/v1/accounts/dashboard_apps_controller_spec.rb
|
||||
// P2B M12 spec — PG/SQLite dual mode, Redis via miniredis mock.
|
||||
type DashboardAppE2ETestSuite struct {
|
||||
E2ETestSuite
|
||||
authToken string
|
||||
userID uint
|
||||
accountID uint
|
||||
}
|
||||
|
||||
// SetupSuite overrides the base E2ETestSuite.SetupSuite to add DashboardApp routes.
|
||||
// The base suite creates the DB, config, miniredis, and auth routes. We add
|
||||
// DashboardApp handler/service/repo wiring on top.
|
||||
func (s *DashboardAppE2ETestSuite) SetupSuite() {
|
||||
// Call parent SetupSuite — creates DB, config, miniredis, auth routes, httptest server
|
||||
s.E2ETestSuite.SetupSuite()
|
||||
|
||||
// Wire DashboardApp handler/service/repo onto the existing router
|
||||
dashboardAppRepo := repository.NewDashboardAppRepo(s.DB())
|
||||
dashboardAppService := service.NewDashboardAppService(dashboardAppRepo)
|
||||
dashboardAppHandler := handler.NewDashboardAppHandler(dashboardAppService)
|
||||
|
||||
// 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))
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// SetupTest creates per-test auth context: account, user, login token.
|
||||
// The parent E2ETestSuite.SetupTest clears all DB tables, so we must
|
||||
// re-create the user and re-login for every test.
|
||||
func (s *DashboardAppE2ETestSuite) SetupTest() {
|
||||
// Call parent SetupTest (clears DB tables)
|
||||
s.E2ETestSuite.SetupTest()
|
||||
|
||||
// Step 1: Create Account for the test user
|
||||
now := time.Now()
|
||||
account := &model.Account{
|
||||
Name: "DashboardApp Test Account",
|
||||
}
|
||||
err := s.DB().Create(account).Error
|
||||
assert.NoError(s.T(), err, "Failed to create account")
|
||||
s.accountID = account.ID
|
||||
|
||||
// Step 2: Create User with a proper password that satisfies the min=6 validation
|
||||
hashedPassword, err := crypto.HashPassword("SecurePass123!")
|
||||
assert.NoError(s.T(), err, "Failed to hash password")
|
||||
user := &model.User{
|
||||
Name: "DashboardApp Tester",
|
||||
Email: "dashapp@test.com",
|
||||
Password: hashedPassword,
|
||||
PasswordDigest: hashedPassword,
|
||||
Provider: "email",
|
||||
Active: true,
|
||||
Available: true,
|
||||
ConfirmedAt: &now,
|
||||
}
|
||||
err = s.DB().Create(user).Error
|
||||
assert.NoError(s.T(), err, "Failed to create user")
|
||||
s.userID = user.ID
|
||||
|
||||
// Step 3: Create AccountUser association so the user belongs to the account
|
||||
accountUser := &model.AccountUser{
|
||||
UserID: user.ID,
|
||||
AccountID: account.ID,
|
||||
Role: "administrator",
|
||||
}
|
||||
err = s.DB().Create(accountUser).Error
|
||||
assert.NoError(s.T(), err, "Failed to create AccountUser association")
|
||||
|
||||
// Step 4: Login via API to obtain JWT token
|
||||
loginPayload := map[string]interface{}{
|
||||
"email": "dashapp@test.com",
|
||||
"password": "SecurePass123!",
|
||||
}
|
||||
body, _ := json.Marshal(loginPayload)
|
||||
resp, err := http.Post(
|
||||
fmt.Sprintf("%s/api/v1/auth/login", s.ServerURL()),
|
||||
"application/json",
|
||||
bytes.NewBuffer(body),
|
||||
)
|
||||
assert.NoError(s.T(), err, "Login request failed")
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
s.T().Logf("Login response: status=%d body=%s", resp.StatusCode, string(respBody))
|
||||
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, "Login should succeed: %s", string(respBody))
|
||||
|
||||
var loginResp map[string]interface{}
|
||||
err = json.Unmarshal(respBody, &loginResp)
|
||||
assert.NoError(s.T(), err, "Failed to parse login response")
|
||||
|
||||
data, _ := loginResp["data"].(map[string]interface{})
|
||||
s.authToken, _ = data["access_token"].(string)
|
||||
assert.NotEmpty(s.T(), s.authToken, "JWT token should not be empty")
|
||||
}
|
||||
|
||||
// authRequest sends an authenticated HTTP request with the JWT token.
|
||||
func (s *DashboardAppE2ETestSuite) authRequest(method, path string, body interface{}) *http.Response {
|
||||
var reqBody io.Reader
|
||||
if body != nil {
|
||||
b, _ := json.Marshal(body)
|
||||
reqBody = bytes.NewBuffer(b)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, fmt.Sprintf("%s%s", s.ServerURL(), path), reqBody)
|
||||
assert.NoError(s.T(), err)
|
||||
req.Header.Set("Authorization", "Bearer "+s.authToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
assert.NoError(s.T(), err)
|
||||
return resp
|
||||
}
|
||||
|
||||
// parseResponse reads and unmarshals the response body.
|
||||
func parseDashAppResponse(resp *http.Response) map[string]interface{} {
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
var result map[string]interface{}
|
||||
json.Unmarshal(body, &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)
|
||||
payload := map[string]interface{}{
|
||||
"title": "销售仪表盘",
|
||||
"content": json.RawMessage(`[{"type":"frame","url":"https://example.com/widget"}]`),
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
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")
|
||||
|
||||
// Verify in database
|
||||
appID := uint(data["id"].(float64))
|
||||
var app model.DashboardApp
|
||||
err := s.DB().First(&app, appID).Error
|
||||
assert.NoError(s.T(), err, "Dashboard app should exist in database")
|
||||
assert.Equal(s.T(), "销售仪表盘", app.Title)
|
||||
assert.Equal(s.T(), s.accountID, app.AccountID)
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
// Invalid content — content must be a JSON array of valid widgets, not a string
|
||||
payload := map[string]interface{}{
|
||||
"title": "InvalidApp",
|
||||
"content": "not a json array",
|
||||
}
|
||||
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")
|
||||
}
|
||||
|
||||
// TestDashboardAppGet verifies retrieving a dashboard app by ID.
|
||||
func (s *DashboardAppE2ETestSuite) TestDashboardAppGet() {
|
||||
// First create a dashboard app directly in DB
|
||||
app := &model.DashboardApp{
|
||||
AccountID: s.accountID,
|
||||
Title: "获取测试仪表盘",
|
||||
Content: json.RawMessage(`[{"type":"frame","url":"https://example.com/widget2"}]`),
|
||||
}
|
||||
err := s.DB().Create(app).Error
|
||||
assert.NoError(s.T(), err, "Failed to create test dashboard app")
|
||||
|
||||
path := fmt.Sprintf("/api/v1/accounts/%d/dashboard_apps/%d", s.accountID, app.ID)
|
||||
resp := s.authRequest("GET", path, nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
// TestDashboardAppGet_NotFound verifies 404 for non-existent dashboard app.
|
||||
func (s *DashboardAppE2ETestSuite) TestDashboardAppGet_NotFound() {
|
||||
path := fmt.Sprintf("/api/v1/accounts/%d/dashboard_apps/99999", s.accountID)
|
||||
resp := s.authRequest("GET", path, nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(s.T(), http.StatusNotFound, resp.StatusCode, "Get non-existent should return 404")
|
||||
}
|
||||
|
||||
// TestDashboardAppList verifies listing dashboard apps for an account.
|
||||
func (s *DashboardAppE2ETestSuite) TestDashboardAppList() {
|
||||
// Create multiple dashboard apps directly in DB
|
||||
for i := 0; i < 3; i++ {
|
||||
app := &model.DashboardApp{
|
||||
AccountID: s.accountID,
|
||||
Title: fmt.Sprintf("列表仪表盘-%d", i+1),
|
||||
Content: json.RawMessage(`[{"type":"frame","url":"https://example.com/widget"}]`),
|
||||
}
|
||||
err := s.DB().Create(app).Error
|
||||
assert.NoError(s.T(), err, "Failed to create test dashboard app")
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
// TestDashboardAppList_Empty verifies listing returns empty array when no apps exist.
|
||||
func (s *DashboardAppE2ETestSuite) TestDashboardAppList_Empty() {
|
||||
// Use a different account that has no apps
|
||||
emptyAccount := &model.Account{Name: "Empty Account"}
|
||||
err := s.DB().Create(emptyAccount).Error
|
||||
assert.NoError(s.T(), err)
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
// TestDashboardAppUpdate verifies updating a dashboard app's title and content.
|
||||
func (s *DashboardAppE2ETestSuite) TestDashboardAppUpdate() {
|
||||
app := &model.DashboardApp{
|
||||
AccountID: s.accountID,
|
||||
Title: "更新前仪表盘",
|
||||
Content: json.RawMessage(`[{"type":"frame","url":"https://old.example.com"}]`),
|
||||
}
|
||||
err := s.DB().Create(app).Error
|
||||
assert.NoError(s.T(), err, "Failed to create test dashboard app")
|
||||
|
||||
path := fmt.Sprintf("/api/v1/accounts/%d/dashboard_apps/%d", s.accountID, app.ID)
|
||||
payload := map[string]interface{}{
|
||||
"title": "更新后仪表盘",
|
||||
"content": json.RawMessage(`[{"type":"frame","url":"https://new.example.com/widget"}]`),
|
||||
}
|
||||
|
||||
resp := s.authRequest("PUT", path, payload)
|
||||
defer resp.Body.Close()
|
||||
|
||||
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")
|
||||
|
||||
// Verify in database
|
||||
var updated model.DashboardApp
|
||||
err = s.DB().First(&updated, app.ID).Error
|
||||
assert.NoError(s.T(), err)
|
||||
assert.Equal(s.T(), "更新后仪表盘", updated.Title)
|
||||
}
|
||||
|
||||
// TestDashboardAppUpdate_NotFound verifies updating a non-existent app returns error.
|
||||
func (s *DashboardAppE2ETestSuite) TestDashboardAppUpdate_NotFound() {
|
||||
path := fmt.Sprintf("/api/v1/accounts/%d/dashboard_apps/99999", s.accountID)
|
||||
payload := map[string]interface{}{
|
||||
"title": "不存在",
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
// TestDashboardAppDelete verifies deleting a dashboard app (soft-delete).
|
||||
func (s *DashboardAppE2ETestSuite) TestDashboardAppDelete() {
|
||||
app := &model.DashboardApp{
|
||||
AccountID: s.accountID,
|
||||
Title: "删除测试仪表盘",
|
||||
Content: json.RawMessage(`[{"type":"frame","url":"https://example.com/widget3"}]`),
|
||||
}
|
||||
err := s.DB().Create(app).Error
|
||||
assert.NoError(s.T(), err, "Failed to create test dashboard app")
|
||||
|
||||
path := fmt.Sprintf("/api/v1/accounts/%d/dashboard_apps/%d", s.accountID, app.ID)
|
||||
resp := s.authRequest("DELETE", path, nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(s.T(), http.StatusNoContent, resp.StatusCode, "Delete should return 204")
|
||||
|
||||
// Verify soft-delete in database (DeletedAt should be set)
|
||||
var deleted model.DashboardApp
|
||||
err = s.DB().Unscoped().First(&deleted, app.ID).Error
|
||||
assert.NoError(s.T(), err, "Record should still exist (soft-deleted)")
|
||||
assert.NotNil(s.T(), deleted.DeletedAt, "DeletedAt should be set after soft-delete")
|
||||
}
|
||||
|
||||
// TestDashboardAppDelete_NotFound verifies deleting a non-existent app.
|
||||
func (s *DashboardAppE2ETestSuite) TestDashboardAppDelete_NotFound() {
|
||||
path := fmt.Sprintf("/api/v1/accounts/%d/dashboard_apps/99999", s.accountID)
|
||||
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")
|
||||
}
|
||||
|
||||
// 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)
|
||||
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)
|
||||
|
||||
result := parseDashAppResponse(resp)
|
||||
data := result["data"].(map[string]interface{})
|
||||
appID := uint(data["id"].(float64))
|
||||
assert.NotZero(s.T(), appID)
|
||||
|
||||
// Step 2: Get
|
||||
getPath := fmt.Sprintf("/api/v1/accounts/%d/dashboard_apps/%d", s.accountID, appID)
|
||||
resp = s.authRequest("GET", getPath, nil)
|
||||
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"])
|
||||
|
||||
// Step 3: Update
|
||||
updatePayload := map[string]interface{}{
|
||||
"title": "全流程更新后",
|
||||
"content": json.RawMessage(`[{"type":"frame","url":"https://updated.example.com"}]`),
|
||||
}
|
||||
resp = s.authRequest("PUT", getPath, updatePayload)
|
||||
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"])
|
||||
|
||||
// Step 4: Delete
|
||||
resp = s.authRequest("DELETE", getPath, nil)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(s.T(), http.StatusNoContent, resp.StatusCode)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// PG-Specific Tests (M12 features that require PostgreSQL)
|
||||
// ============================================================
|
||||
|
||||
// TestDashboardApp_PG_JSONBQuery verifies JSONB content storage on PG.
|
||||
// This test is skipped on SQLite since it lacks native JSONB support.
|
||||
func (s *DashboardAppE2ETestSuite) TestDashboardApp_PG_JSONBQuery() {
|
||||
if !testhelpers.UsePostgres() {
|
||||
s.T().Skip("Skipping: JSONB query test requires PostgreSQL")
|
||||
}
|
||||
|
||||
// Create apps with different content structures
|
||||
for i, content := range []string{
|
||||
`[{"type":"frame","url":"https://sales.example.com"}]`,
|
||||
`[{"type":"frame","url":"https://support.example.com"}]`,
|
||||
`[]`,
|
||||
} {
|
||||
app := &model.DashboardApp{
|
||||
AccountID: s.accountID,
|
||||
Title: fmt.Sprintf("JSONB仪表盘-%d", i+1),
|
||||
Content: json.RawMessage(content),
|
||||
}
|
||||
err := s.DB().Create(app).Error
|
||||
assert.NoError(s.T(), err)
|
||||
}
|
||||
|
||||
// Verify all apps are stored with content on PG
|
||||
var apps []model.DashboardApp
|
||||
err := s.DB().Where("account_id = ?", s.accountID).Find(&apps).Error
|
||||
assert.NoError(s.T(), err)
|
||||
assert.Len(s.T(), apps, 3, "Should find all 3 dashboard apps on PG")
|
||||
|
||||
for _, app := range apps {
|
||||
assert.NotEmpty(s.T(), app.Content, "JSONB content should be populated on PG")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Suite runner
|
||||
// ============================================================
|
||||
|
||||
func TestDashboardAppE2ESuite(t *testing.T) {
|
||||
suite.Run(t, new(DashboardAppE2ETestSuite))
|
||||
}
|
||||
Reference in New Issue
Block a user