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.
379 lines
11 KiB
Go
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)
|
|
} |