Files
gochat/backend/internal/model/crud_test.go
T
rogee aeddedf2a3 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.
2026-07-07 14:44:12 +08:00

379 lines
11 KiB
Go

package model_test
import (
"testing"
"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"
)
// 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{},
&model.AccessToken{},
&model.InboxMember{},
&channel.ChannelTelegram{},
&channel.ChannelWebWidget{},
}
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)
assert.NoError(t, sqlDB.Close())
}
// --- Fixture helpers (inline to avoid import cycle) ---
func createAccount(t *testing.T, db *gorm.DB, name string) *model.Account {
t.Helper()
acc := &model.Account{Name: name, Locale: "en", Timezone: "UTC", Active: true}
assert.NoError(t, db.Create(acc).Error)
return acc
}
func createUser(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_pw", Provider: "email",
Role: string(model.AccountUserRoleAgent), Active: true,
}
assert.NoError(t, db.Create(user).Error)
return user
}
func createInbox(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,
}
assert.NoError(t, db.Create(inbox).Error)
return inbox
}
func createContact(t *testing.T, db *gorm.DB, accountID uint, name string) *model.Contact {
t.Helper()
contact := &model.Contact{AccountID: accountID, Name: name}
assert.NoError(t, db.Create(contact).Error)
return contact
}
func createConversation(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", Channel: "web_widget",
}
assert.NoError(t, db.Create(conv).Error)
return conv
}
func createAccountUser(t *testing.T, db *gorm.DB, userID, accountID uint, role string) *model.AccountUser {
t.Helper()
au := &model.AccountUser{
UserID: userID, AccountID: accountID, Role: role, Availability: "online",
}
assert.NoError(t, db.Create(au).Error)
return au
}
func createCustomRole(t *testing.T, db *gorm.DB, accountID uint, name, permissions string) *model.CustomRole {
t.Helper()
role := &model.CustomRole{
AccountID: accountID, Name: name, Permissions: permissions,
}
assert.NoError(t, db.Create(role).Error)
return role
}
// ===================== CRUD Tests =====================
func TestAccountCRUD(t *testing.T) {
db := newTestDB(t)
defer cleanupDB(t, db)
// Create
acc := createAccount(t, db, "TestOrg")
assert.NotZero(t, acc.ID)
assert.Equal(t, "TestOrg", acc.Name)
assert.Equal(t, "en", acc.Locale)
assert.Equal(t, "UTC", acc.Timezone)
assert.True(t, acc.Active)
assert.Equal(t, "active", acc.Status)
// Read
var fetched model.Account
assert.NoError(t, db.First(&fetched, acc.ID).Error)
assert.Equal(t, acc.Name, fetched.Name)
// Update
assert.NoError(t, db.Model(&fetched).Updates(map[string]interface{}{
"Name": "UpdatedOrg",
"Domain": "updated.example.com",
}).Error)
var updated model.Account
assert.NoError(t, db.First(&updated, acc.ID).Error)
assert.Equal(t, "UpdatedOrg", updated.Name)
assert.Equal(t, "updated.example.com", updated.Domain)
// Delete (soft)
assert.NoError(t, db.Delete(&updated).Error)
var deleted model.Account
assert.Error(t, db.First(&deleted, acc.ID).Error)
// Unscoped read verifies soft-delete
var softDeleted model.Account
assert.NoError(t, db.Unscoped().First(&softDeleted, acc.ID).Error)
assert.NotNil(t, softDeleted.DeletedAt)
}
func TestUserCRUD(t *testing.T) {
db := newTestDB(t)
defer cleanupDB(t, db)
acc := createAccount(t, db, "OrgForUser")
// Create
user := createUser(t, db, acc.ID, "John Doe", "john@example.com")
assert.NotZero(t, user.ID)
assert.Equal(t, acc.ID, user.AccountID)
assert.Equal(t, "John Doe", user.Name)
assert.Equal(t, "email", user.Provider)
assert.Equal(t, string(model.AccountUserRoleAgent), user.Role)
assert.True(t, user.Active)
// Read
var fetched model.User
assert.NoError(t, db.First(&fetched, user.ID).Error)
assert.Equal(t, user.Name, fetched.Name)
// Update
assert.NoError(t, db.Model(&fetched).Updates(map[string]interface{}{
"Name": "Jane Doe",
"Available": true,
"Role": string(model.AccountUserRoleAdministrator),
}).Error)
var updated model.User
assert.NoError(t, db.First(&updated, user.ID).Error)
assert.Equal(t, "Jane Doe", updated.Name)
assert.True(t, updated.Available)
assert.Equal(t, string(model.AccountUserRoleAdministrator), updated.Role)
// Delete (soft)
assert.NoError(t, db.Delete(&updated).Error)
var deleted model.User
assert.Equal(t, gorm.ErrRecordNotFound, db.First(&deleted, user.ID).Error)
}
func TestInboxCRUD(t *testing.T) {
db := newTestDB(t)
defer cleanupDB(t, db)
acc := createAccount(t, db, "OrgForInbox")
// Create
inbox := createInbox(t, db, acc.ID, "Support Inbox", "web_widget")
assert.NotZero(t, inbox.ID)
assert.Equal(t, "Support Inbox", inbox.Name)
assert.Equal(t, "web_widget", inbox.ChannelType)
// Read
var fetched model.Inbox
assert.NoError(t, db.First(&fetched, inbox.ID).Error)
assert.Equal(t, inbox.Name, fetched.Name)
assert.False(t, fetched.EnableAutoAssignment) // default is false per Chatwoot
// Update
assert.NoError(t, db.Model(&fetched).Updates(map[string]interface{}{
"Name": "Updated Inbox",
"EnableAutoAssignment": false,
}).Error)
var updated model.Inbox
assert.NoError(t, db.First(&updated, inbox.ID).Error)
assert.Equal(t, "Updated Inbox", updated.Name)
assert.False(t, updated.EnableAutoAssignment)
// Delete
assert.NoError(t, db.Delete(&updated).Error)
}
func TestConversationCRUD(t *testing.T) {
db := newTestDB(t)
defer cleanupDB(t, db)
acc := createAccount(t, db, "OrgForConv")
inbox := createInbox(t, db, acc.ID, "Conv Inbox", "web_widget")
contact := createContact(t, db, acc.ID, "Conv Contact")
// Create
conv := createConversation(t, db, acc.ID, inbox.ID, contact.ID, "open")
assert.NotZero(t, conv.ID)
assert.Equal(t, "open", conv.Status)
// Read
var fetched model.Conversation
assert.NoError(t, db.First(&fetched, conv.ID).Error)
assert.Equal(t, conv.AccountID, fetched.AccountID)
assert.Equal(t, conv.InboxID, fetched.InboxID)
assert.Equal(t, conv.ContactID, fetched.ContactID)
// Update (resolve conversation)
assert.NoError(t, db.Model(&fetched).Update("Status", string(model.ConversationStatusResolved)).Error)
var updated model.Conversation
assert.NoError(t, db.First(&updated, conv.ID).Error)
assert.Equal(t, string(model.ConversationStatusResolved), updated.Status)
// Delete
assert.NoError(t, db.Delete(&updated).Error)
}
func TestMessageCRUD(t *testing.T) {
db := newTestDB(t)
defer cleanupDB(t, db)
acc := createAccount(t, db, "OrgForMsg")
inbox := createInbox(t, db, acc.ID, "Msg Inbox", "web_widget")
contact := createContact(t, db, acc.ID, "Msg Contact")
conv := createConversation(t, db, acc.ID, inbox.ID, contact.ID, "open")
// Create
msg := &model.Message{
ConversationID: conv.ID, AccountID: acc.ID, InboxID: inbox.ID,
SenderType: "contact", Content: "Hello!", ContentType: "text",
MessageType: "incoming", Private: false,
}
assert.NoError(t, db.Create(msg).Error)
assert.NotZero(t, msg.ID)
// Read
var fetched model.Message
assert.NoError(t, db.First(&fetched, msg.ID).Error)
assert.Equal(t, "Hello!", fetched.Content)
assert.Equal(t, "contact", fetched.SenderType)
// Update
assert.NoError(t, db.Model(&fetched).Updates(map[string]interface{}{
"Content": "Updated", "Private": true,
}).Error)
var updated model.Message
assert.NoError(t, db.First(&updated, msg.ID).Error)
assert.Equal(t, "Updated", updated.Content)
assert.True(t, updated.Private)
// Delete
assert.NoError(t, db.Delete(&updated).Error)
}
func TestContactCRUD(t *testing.T) {
db := newTestDB(t)
defer cleanupDB(t, db)
acc := createAccount(t, db, "OrgForContact")
// Create
contact := createContact(t, db, acc.ID, "Alice")
assert.NotZero(t, contact.ID)
assert.Equal(t, "Alice", contact.Name)
// Read
var fetched model.Contact
assert.NoError(t, db.First(&fetched, contact.ID).Error)
assert.Equal(t, "Alice", fetched.Name)
// Update
assert.NoError(t, db.Model(&fetched).Updates(map[string]interface{}{
"Name": "Alice Updated", "Email": "alice@example.com",
}).Error)
var updated model.Contact
assert.NoError(t, db.First(&updated, contact.ID).Error)
assert.Equal(t, "Alice Updated", updated.Name)
assert.Equal(t, "alice@example.com", updated.Email)
// Delete
assert.NoError(t, db.Delete(&updated).Error)
}
func TestCustomRoleCRUD(t *testing.T) {
db := newTestDB(t)
defer cleanupDB(t, db)
acc := createAccount(t, db, "OrgForRole")
// Create
role := createCustomRole(t, db, acc.ID, "Support Lead",
`{"conversation_manage":"full","contact_manage":"read"}`)
assert.NotZero(t, role.ID)
assert.Equal(t, "Support Lead", role.Name)
// Read
var fetched model.CustomRole
assert.NoError(t, db.First(&fetched, role.ID).Error)
assert.Equal(t, "Support Lead", fetched.Name)
// Update
assert.NoError(t, db.Model(&fetched).Updates(map[string]interface{}{
"Name": "Updated Lead", "Description": "Updated desc",
}).Error)
var updated model.CustomRole
assert.NoError(t, db.First(&updated, role.ID).Error)
assert.Equal(t, "Updated Lead", updated.Name)
// Delete
assert.NoError(t, db.Delete(&updated).Error)
}
func TestAccountUserCRUD(t *testing.T) {
db := newTestDB(t)
defer cleanupDB(t, db)
acc := createAccount(t, db, "OrgForAU")
user := createUser(t, db, acc.ID, "AU User", "au@example.com")
// Create
au := createAccountUser(t, db, user.ID, acc.ID, string(model.AccountUserRoleAgent))
assert.NotZero(t, au.ID)
assert.Equal(t, string(model.AccountUserRoleAgent), au.Role)
// Read
var fetched model.AccountUser
assert.NoError(t, db.First(&fetched, au.ID).Error)
assert.Equal(t, user.ID, fetched.UserID)
assert.Equal(t, acc.ID, fetched.AccountID)
// Update
assert.NoError(t, db.Model(&fetched).Update("Role", string(model.AccountUserRoleAdministrator)).Error)
var updated model.AccountUser
assert.NoError(t, db.First(&updated, au.ID).Error)
assert.Equal(t, string(model.AccountUserRoleAdministrator), updated.Role)
// Delete
assert.NoError(t, db.Delete(&updated).Error)
}