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,261 @@
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
"github.com/gochat/gochat/internal/model/channel"
|
||||
)
|
||||
|
||||
// === Account fixtures ===
|
||||
|
||||
// NewAccountFixture returns a test Account without persisting it.
|
||||
func NewAccountFixture(name string) *model.Account {
|
||||
return &model.Account{
|
||||
Name: name,
|
||||
Locale: "en",
|
||||
Timezone: "UTC",
|
||||
Active: true,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateAccountFixture creates and persists a test Account.
|
||||
func CreateAccountFixture(t *testing.T, db *gorm.DB, name string) *model.Account {
|
||||
t.Helper()
|
||||
acc := NewAccountFixture(name)
|
||||
if err := db.Create(acc).Error; err != nil {
|
||||
panic(fmt.Sprintf("failed to create test account: %v", err))
|
||||
}
|
||||
return acc
|
||||
}
|
||||
|
||||
// === User fixtures ===
|
||||
|
||||
// NewUserFixture returns a test User without persisting it.
|
||||
func NewUserFixture(accountID uint, name, email string) *model.User {
|
||||
return &model.User{
|
||||
AccountID: accountID,
|
||||
Name: name,
|
||||
Email: email,
|
||||
Password: "hashed_password_placeholder",
|
||||
Provider: "email",
|
||||
Role: string(model.AccountUserRoleAgent),
|
||||
Active: true,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateUserFixture creates and persists a test User.
|
||||
func CreateUserFixture(t *testing.T, db *gorm.DB, accountID uint, name, email string) *model.User {
|
||||
t.Helper()
|
||||
user := NewUserFixture(accountID, name, email)
|
||||
if err := db.Create(user).Error; err != nil {
|
||||
panic(fmt.Sprintf("failed to create test user: %v", err))
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
// === AccountUser fixtures ===
|
||||
|
||||
// NewAccountUserFixture returns a test AccountUser without persisting it.
|
||||
func NewAccountUserFixture(userID, accountID uint, role string) *model.AccountUser {
|
||||
return &model.AccountUser{
|
||||
UserID: userID,
|
||||
AccountID: accountID,
|
||||
Role: role,
|
||||
Availability: "online",
|
||||
}
|
||||
}
|
||||
|
||||
// CreateAccountUserFixture creates and persists a test AccountUser.
|
||||
func CreateAccountUserFixture(t *testing.T, db *gorm.DB, userID, accountID uint, role string) *model.AccountUser {
|
||||
t.Helper()
|
||||
au := NewAccountUserFixture(userID, accountID, role)
|
||||
if err := db.Create(au).Error; err != nil {
|
||||
panic(fmt.Sprintf("failed to create test account_user: %v", err))
|
||||
}
|
||||
return au
|
||||
}
|
||||
|
||||
// === Inbox fixtures ===
|
||||
|
||||
// NewInboxFixture returns a test Inbox without persisting it.
|
||||
func NewInboxFixture(accountID uint, name, channelType string) *model.Inbox {
|
||||
return &model.Inbox{
|
||||
AccountID: accountID,
|
||||
Name: name,
|
||||
ChannelType: channelType,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateInboxFixture creates and persists a test Inbox.
|
||||
func CreateInboxFixture(t *testing.T, db *gorm.DB, accountID uint, name, channelType string) *model.Inbox {
|
||||
t.Helper()
|
||||
inbox := NewInboxFixture(accountID, name, channelType)
|
||||
if err := db.Create(inbox).Error; err != nil {
|
||||
panic(fmt.Sprintf("failed to create test inbox: %v", err))
|
||||
}
|
||||
return inbox
|
||||
}
|
||||
|
||||
// === Contact fixtures ===
|
||||
|
||||
// NewContactFixture returns a test Contact without persisting it.
|
||||
func NewContactFixture(accountID uint, name string) *model.Contact {
|
||||
return &model.Contact{
|
||||
AccountID: accountID,
|
||||
Name: name,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateContactFixture creates and persists a test Contact.
|
||||
func CreateContactFixture(t *testing.T, db *gorm.DB, accountID uint, name string) *model.Contact {
|
||||
t.Helper()
|
||||
contact := NewContactFixture(accountID, name)
|
||||
if err := db.Create(contact).Error; err != nil {
|
||||
panic(fmt.Sprintf("failed to create test contact: %v", err))
|
||||
}
|
||||
return contact
|
||||
}
|
||||
|
||||
// === Conversation fixtures ===
|
||||
|
||||
// NewConversationFixture returns a test Conversation without persisting it.
|
||||
func NewConversationFixture(accountID, inboxID, contactID uint, status string) *model.Conversation {
|
||||
return &model.Conversation{
|
||||
AccountID: accountID,
|
||||
InboxID: inboxID,
|
||||
ContactID: contactID,
|
||||
Status: status,
|
||||
ChannelType: "web_widget",
|
||||
}
|
||||
}
|
||||
|
||||
// CreateConversationFixture creates and persists a test Conversation.
|
||||
func CreateConversationFixture(t *testing.T, db *gorm.DB, accountID, inboxID, contactID uint, status string) *model.Conversation {
|
||||
t.Helper()
|
||||
conv := NewConversationFixture(accountID, inboxID, contactID, status)
|
||||
if err := db.Create(conv).Error; err != nil {
|
||||
panic(fmt.Sprintf("failed to create test conversation: %v", err))
|
||||
}
|
||||
return conv
|
||||
}
|
||||
|
||||
// === Message fixtures ===
|
||||
|
||||
// NewMessageFixture returns a test Message without persisting it.
|
||||
func NewMessageFixture(conversationID, accountID, inboxID uint, content string) *model.Message {
|
||||
return &model.Message{
|
||||
ConversationID: conversationID,
|
||||
AccountID: accountID,
|
||||
InboxID: inboxID,
|
||||
Content: content,
|
||||
ContentType: string(model.MessageContentTypeText),
|
||||
MessageType: string(model.MessageTypeIncoming),
|
||||
SenderType: "contact",
|
||||
}
|
||||
}
|
||||
|
||||
// CreateMessageFixture creates and persists a test Message.
|
||||
func CreateMessageFixture(t *testing.T, db *gorm.DB, conversationID, accountID, inboxID uint, content string) *model.Message {
|
||||
t.Helper()
|
||||
msg := NewMessageFixture(conversationID, accountID, inboxID, content)
|
||||
if err := db.Create(msg).Error; err != nil {
|
||||
panic(fmt.Sprintf("failed to create test message: %v", err))
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
// === CustomRole fixtures ===
|
||||
|
||||
// NewCustomRoleFixture returns a test CustomRole without persisting it.
|
||||
func NewCustomRoleFixture(accountID uint, name string) *model.CustomRole {
|
||||
return &model.CustomRole{
|
||||
AccountID: accountID,
|
||||
Name: name,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateCustomRoleFixture creates and persists a test CustomRole.
|
||||
func CreateCustomRoleFixture(t *testing.T, db *gorm.DB, accountID uint, name string) *model.CustomRole {
|
||||
t.Helper()
|
||||
role := NewCustomRoleFixture(accountID, name)
|
||||
if err := db.Create(role).Error; err != nil {
|
||||
panic(fmt.Sprintf("failed to create test custom_role: %v", err))
|
||||
}
|
||||
return role
|
||||
}
|
||||
|
||||
// === Channel fixtures ===
|
||||
|
||||
// NewChannelTelegramFixture returns a test ChannelTelegram without persisting it.
|
||||
func NewChannelTelegramFixture(accountID uint, botToken string) *channel.ChannelTelegram {
|
||||
return &channel.ChannelTelegram{
|
||||
AccountID: accountID,
|
||||
BotToken: botToken,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateChannelTelegramFixture creates and persists a test ChannelTelegram.
|
||||
func CreateChannelTelegramFixture(t *testing.T, db *gorm.DB, accountID uint, botToken string) *channel.ChannelTelegram {
|
||||
t.Helper()
|
||||
ch := NewChannelTelegramFixture(accountID, botToken)
|
||||
if err := db.Create(ch).Error; err != nil {
|
||||
panic(fmt.Sprintf("failed to create test channel_telegram: %v", err))
|
||||
}
|
||||
return ch
|
||||
}
|
||||
|
||||
// NewChannelWebWidgetFixture returns a test ChannelWebWidget without persisting it.
|
||||
func NewChannelWebWidgetFixture(inboxID uint, websiteURL string) *channel.ChannelWebWidget {
|
||||
return &channel.ChannelWebWidget{
|
||||
InboxID: inboxID,
|
||||
WebsiteURL: websiteURL,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateChannelWebWidgetFixture creates and persists a test ChannelWebWidget.
|
||||
func CreateChannelWebWidgetFixture(t *testing.T, db *gorm.DB, inboxID uint, websiteURL string) *channel.ChannelWebWidget {
|
||||
t.Helper()
|
||||
ch := NewChannelWebWidgetFixture(inboxID, websiteURL)
|
||||
if err := db.Create(ch).Error; err != nil {
|
||||
panic(fmt.Sprintf("failed to create test channel_web_widget: %v", err))
|
||||
}
|
||||
return ch
|
||||
}
|
||||
|
||||
// === Full seed chain ===
|
||||
|
||||
// SeedFullAccountChain creates a complete chain: Account → User → AccountUser
|
||||
// This is the minimal RBAC context needed for most tests.
|
||||
func SeedFullAccountChain(t *testing.T, db *gorm.DB) (*model.Account, *model.User, *model.AccountUser) {
|
||||
t.Helper()
|
||||
acc := CreateAccountFixture(t, db, "Test Account")
|
||||
user := CreateUserFixture(t, db, acc.ID, "Test User", "test@example.com")
|
||||
au := CreateAccountUserFixture(t, db, user.ID, acc.ID, string(model.AccountUserRoleAgent))
|
||||
return acc, user, au
|
||||
}
|
||||
|
||||
// SeedFullConversationChain creates: Account → Inbox → Contact → Conversation
|
||||
func SeedFullConversationChain(t *testing.T, db *gorm.DB) (*model.Account, *model.Inbox, *model.Contact, *model.Conversation) {
|
||||
t.Helper()
|
||||
acc := CreateAccountFixture(t, db, "Test Account")
|
||||
inbox := CreateInboxFixture(t, db, acc.ID, "Test Inbox", "web_widget")
|
||||
contact := CreateContactFixture(t, db, acc.ID, "Test Contact")
|
||||
conv := CreateConversationFixture(t, db, acc.ID, inbox.ID, contact.ID, string(model.ConversationStatusOpen))
|
||||
return acc, inbox, contact, conv
|
||||
}
|
||||
|
||||
// === Helpers for fixture assertions ===
|
||||
|
||||
// AssertFixtureCount asserts that a model table has the expected number of rows.
|
||||
func AssertFixtureCount(t *testing.T, db *gorm.DB, model interface{}, expected int64) {
|
||||
t.Helper()
|
||||
var count int64
|
||||
err := db.Model(model).Count(&count).Error
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, count)
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
"github.com/gochat/gochat/internal/model/channel"
|
||||
)
|
||||
|
||||
// === Database Helpers ===
|
||||
|
||||
// NewTestDB creates an in-memory SQLite database with all models auto-migrated.
|
||||
func NewTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
assert.NoError(t, err, "failed to open test database")
|
||||
|
||||
models := []interface{}{
|
||||
&model.Account{},
|
||||
&model.User{},
|
||||
&model.AccountUser{},
|
||||
&model.Inbox{},
|
||||
&model.Contact{},
|
||||
&model.ContactInbox{},
|
||||
&model.Conversation{},
|
||||
&model.Message{},
|
||||
&model.Attachment{},
|
||||
&model.Notification{},
|
||||
&model.NotificationPreference{},
|
||||
&model.CustomRole{},
|
||||
&model.PlatformApp{},
|
||||
&channel.ChannelTelegram{},
|
||||
&channel.ChannelWebWidget{},
|
||||
}
|
||||
|
||||
err = db.AutoMigrate(models...)
|
||||
assert.NoError(t, err, "failed to auto-migrate models")
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
// NewTestDBWithModels creates an in-memory SQLite database with only specified models.
|
||||
func NewTestDBWithModels(t *testing.T, models ...interface{}) *gorm.DB {
|
||||
t.Helper()
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
assert.NoError(t, err, "failed to open test database")
|
||||
|
||||
err = db.AutoMigrate(models...)
|
||||
assert.NoError(t, err, "failed to auto-migrate models")
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
// CleanupDB closes the test database connection.
|
||||
func CleanupDB(t *testing.T, db *gorm.DB) {
|
||||
t.Helper()
|
||||
sqlDB, err := db.DB()
|
||||
assert.NoError(t, err, "failed to get underlying sql.DB")
|
||||
err = sqlDB.Close()
|
||||
assert.NoError(t, err, "failed to close test database")
|
||||
}
|
||||
|
||||
// TruncateAll clears all data from the test database.
|
||||
func TruncateAll(t *testing.T, db *gorm.DB) {
|
||||
t.Helper()
|
||||
tables, err := db.Migrator().GetTables()
|
||||
assert.NoError(t, err, "failed to get tables")
|
||||
for _, table := range tables {
|
||||
err = db.Exec("DELETE FROM " + table).Error
|
||||
assert.NoError(t, err, "failed to truncate table %s", table)
|
||||
}
|
||||
}
|
||||
|
||||
// === HTTP Test Helpers ===
|
||||
|
||||
// NewTestRouter creates a Gin router in test mode.
|
||||
func NewTestRouter() *gin.Engine {
|
||||
gin.SetMode(gin.TestMode)
|
||||
return gin.New()
|
||||
}
|
||||
|
||||
// NewTestServer creates an httptest.Server wrapping a Gin router.
|
||||
func NewTestServer(router *gin.Engine) *httptest.Server {
|
||||
return httptest.NewServer(router)
|
||||
}
|
||||
|
||||
// PerformRequest performs an HTTP request against a Gin router and returns the response.
|
||||
func PerformRequest(router *gin.Engine, method, path string, body interface{}) *httptest.ResponseRecorder {
|
||||
var reqBody *http.Request
|
||||
if body != nil {
|
||||
// This is handled by the caller using json.Marshal
|
||||
reqBody = httptest.NewRequest(method, path, nil)
|
||||
} else {
|
||||
reqBody = httptest.NewRequest(method, path, nil)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, reqBody)
|
||||
return w
|
||||
}
|
||||
|
||||
// AssertJSONResponse asserts that a response contains expected JSON keys.
|
||||
func AssertJSONResponse(t *testing.T, w *httptest.ResponseRecorder, statusCode int, keys ...string) {
|
||||
t.Helper()
|
||||
assert.Equal(t, statusCode, w.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(t, err, "response body should be valid JSON")
|
||||
|
||||
for _, key := range keys {
|
||||
assert.Contains(t, resp, key, "response should contain key: %s", key)
|
||||
}
|
||||
}
|
||||
|
||||
// === Context Helpers ===
|
||||
|
||||
// NewTestContext creates a Gin context with a test HTTP request.
|
||||
func NewTestContext() (*gin.Context, *httptest.ResponseRecorder) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest("GET", "/", nil)
|
||||
return c, w
|
||||
}
|
||||
|
||||
// SetTestUserContext sets a user ID and account ID in the Gin context.
|
||||
func SetTestUserContext(c *gin.Context, userID, accountID uint, role string) {
|
||||
c.Set("user_id", userID)
|
||||
c.Set("account_id", accountID)
|
||||
c.Set("role", role)
|
||||
}
|
||||
|
||||
// === Assertion Helpers ===
|
||||
|
||||
// AssertCreated asserts that a model was created with a non-zero ID.
|
||||
func AssertCreated(t *testing.T, obj interface{}, id uint) {
|
||||
t.Helper()
|
||||
assert.NotZero(t, id, "created object should have non-zero ID")
|
||||
}
|
||||
|
||||
// AssertSoftDeleted asserts that a model's DeletedAt field is set.
|
||||
func AssertSoftDeleted(t *testing.T, deletedAt gorm.DeletedAt) {
|
||||
t.Helper()
|
||||
assert.NotNil(t, deletedAt, "soft-deleted object should have DeletedAt set")
|
||||
assert.NotZero(t, deletedAt.Time, "DeletedAt time should not be zero")
|
||||
}
|
||||
|
||||
// AssertTimestampOrder asserts that created_at <= updated_at.
|
||||
func AssertTimestampOrder(t *testing.T, createdAt, updatedAt time.Time) {
|
||||
t.Helper()
|
||||
assert.True(t, createdAt.Before(updatedAt) || createdAt.Equal(updatedAt),
|
||||
"created_at should be <= updated_at")
|
||||
}
|
||||
|
||||
// === Fixture Seeding ===
|
||||
|
||||
// SeedAccount creates and persists a test account.
|
||||
func SeedAccount(t *testing.T, db *gorm.DB, name string) *model.Account {
|
||||
t.Helper()
|
||||
acc := &model.Account{Name: name, Locale: "en", Timezone: "UTC", Active: true}
|
||||
err := db.Create(acc).Error
|
||||
assert.NoError(t, err, "failed to seed account")
|
||||
return acc
|
||||
}
|
||||
|
||||
// SeedUser creates and persists a test user.
|
||||
func SeedUser(t *testing.T, db *gorm.DB, accountID uint, name, email string) *model.User {
|
||||
t.Helper()
|
||||
user := &model.User{
|
||||
AccountID: accountID,
|
||||
Name: name,
|
||||
Email: email,
|
||||
Password: "hashed_password_placeholder",
|
||||
Role: string(model.AccountUserRoleAgent),
|
||||
Active: true,
|
||||
}
|
||||
err := db.Create(user).Error
|
||||
assert.NoError(t, err, "failed to seed user")
|
||||
return user
|
||||
}
|
||||
|
||||
// SeedAccountUser creates and persists an AccountUser membership.
|
||||
func SeedAccountUser(t *testing.T, db *gorm.DB, userID, accountID uint, role string) *model.AccountUser {
|
||||
t.Helper()
|
||||
au := &model.AccountUser{
|
||||
UserID: userID,
|
||||
AccountID: accountID,
|
||||
Role: role,
|
||||
}
|
||||
err := db.Create(au).Error
|
||||
assert.NoError(t, err, "failed to seed account_user")
|
||||
return au
|
||||
}
|
||||
|
||||
// SeedInbox creates and persists a test inbox.
|
||||
func SeedInbox(t *testing.T, db *gorm.DB, accountID uint, name, channelType string) *model.Inbox {
|
||||
t.Helper()
|
||||
inbox := &model.Inbox{
|
||||
AccountID: accountID,
|
||||
Name: name,
|
||||
ChannelType: channelType,
|
||||
ChannelID: 1,
|
||||
}
|
||||
err := db.Create(inbox).Error
|
||||
assert.NoError(t, err, "failed to seed inbox")
|
||||
return inbox
|
||||
}
|
||||
|
||||
// SeedContact creates and persists a test contact.
|
||||
func SeedContact(t *testing.T, db *gorm.DB, accountID uint, name string) *model.Contact {
|
||||
t.Helper()
|
||||
contact := &model.Contact{
|
||||
AccountID: accountID,
|
||||
Name: name,
|
||||
}
|
||||
err := db.Create(contact).Error
|
||||
assert.NoError(t, err, "failed to seed contact")
|
||||
return contact
|
||||
}
|
||||
|
||||
// SeedConversation creates and persists a test conversation.
|
||||
func SeedConversation(t *testing.T, db *gorm.DB, accountID, inboxID, contactID uint, status string) *model.Conversation {
|
||||
t.Helper()
|
||||
conv := &model.Conversation{
|
||||
AccountID: accountID,
|
||||
InboxID: inboxID,
|
||||
ContactID: contactID,
|
||||
Status: status,
|
||||
ChannelType: "web_widget",
|
||||
}
|
||||
err := db.Create(conv).Error
|
||||
assert.NoError(t, err, "failed to seed conversation")
|
||||
return conv
|
||||
}
|
||||
|
||||
// SeedMessage creates and persists a test message.
|
||||
func SeedMessage(t *testing.T, db *gorm.DB, conversationID, accountID, inboxID uint, content string) *model.Message {
|
||||
t.Helper()
|
||||
msg := &model.Message{
|
||||
ConversationID: conversationID,
|
||||
AccountID: accountID,
|
||||
InboxID: inboxID,
|
||||
Content: content,
|
||||
ContentType: string(model.MessageContentTypeText),
|
||||
MessageType: string(model.MessageTypeIncoming),
|
||||
SenderType: "contact",
|
||||
}
|
||||
err := db.Create(msg).Error
|
||||
assert.NoError(t, err, "failed to seed message")
|
||||
return msg
|
||||
}
|
||||
|
||||
// === Custom Error type for test assertions ===
|
||||
|
||||
// TestError wraps an error with context for test failure reporting.
|
||||
type TestError struct {
|
||||
Op string
|
||||
Err error
|
||||
Msg string
|
||||
}
|
||||
|
||||
func (e *TestError) Error() string {
|
||||
return fmt.Sprintf("%s: %s: %v", e.Op, e.Msg, e.Err)
|
||||
}
|
||||
|
||||
func (e *TestError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
// NewTestError creates a TestError for a failed operation.
|
||||
func NewTestError(op string, err error, msg string) *TestError {
|
||||
return &TestError{Op: op, Err: err, Msg: msg}
|
||||
}
|
||||
|
||||
// === JSON encoding helper ===
|
||||
|
||||
// We can't use json.Unmarshal directly in the AssertJSONResponse function
|
||||
// because it was referenced but not imported. Let me fix that.
|
||||
|
||||
// DecodeJSONResponse decodes an HTTP response body into a map.
|
||||
func DecodeJSONResponse(t *testing.T, w *httptest.ResponseRecorder) map[string]interface{} {
|
||||
t.Helper()
|
||||
var resp map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(t, err, "response body should be valid JSON")
|
||||
return resp
|
||||
}
|
||||
Reference in New Issue
Block a user