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,428 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// setupModelTestDB creates an in-memory SQLite database with core model tables.
|
||||
func setupModelTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open test db: %v", err)
|
||||
}
|
||||
err = db.AutoMigrate(
|
||||
&Account{},
|
||||
&User{},
|
||||
&AccountUser{},
|
||||
&Inbox{},
|
||||
&Contact{},
|
||||
&ContactInbox{},
|
||||
&Conversation{},
|
||||
&Message{},
|
||||
&Attachment{},
|
||||
&Notification{},
|
||||
&CustomRole{},
|
||||
&PlatformApp{},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to migrate: %v", err)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func cleanupDB(t *testing.T, db *gorm.DB) {
|
||||
t.Helper()
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get sql.DB: %v", err)
|
||||
}
|
||||
sqlDB.Close()
|
||||
}
|
||||
|
||||
// --- Account CRUD Tests ---
|
||||
|
||||
func TestAccount_Create(t *testing.T) {
|
||||
db := setupModelTestDB(t)
|
||||
defer cleanupDB(t, db)
|
||||
|
||||
account := &Account{Name: "TestOrg", Locale: "en", Timezone: "UTC", Active: true}
|
||||
err := db.Create(account).Error
|
||||
assert.NoError(t, err)
|
||||
assert.NotZero(t, account.ID)
|
||||
assert.Equal(t, "TestOrg", account.Name)
|
||||
assert.Equal(t, "en", account.Locale)
|
||||
assert.Equal(t, "UTC", account.Timezone)
|
||||
assert.True(t, account.Active)
|
||||
assert.Equal(t, "active", account.Status)
|
||||
}
|
||||
|
||||
func TestAccount_GetByID(t *testing.T) {
|
||||
db := setupModelTestDB(t)
|
||||
defer cleanupDB(t, db)
|
||||
|
||||
acc := &Account{Name: "LookupOrg", Locale: "en", Timezone: "UTC", Active: true}
|
||||
db.Create(acc)
|
||||
|
||||
var found Account
|
||||
err := db.First(&found, acc.ID).Error
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "LookupOrg", found.Name)
|
||||
assert.Equal(t, acc.ID, found.ID)
|
||||
}
|
||||
|
||||
func TestAccount_Update(t *testing.T) {
|
||||
db := setupModelTestDB(t)
|
||||
defer cleanupDB(t, db)
|
||||
|
||||
acc := &Account{Name: "UpdateOrg", Locale: "en", Timezone: "UTC", Active: true}
|
||||
db.Create(acc)
|
||||
|
||||
acc.Name = "UpdatedOrg"
|
||||
acc.Domain = "updated.example.com"
|
||||
err := db.Save(acc).Error
|
||||
assert.NoError(t, err)
|
||||
|
||||
var found Account
|
||||
db.First(&found, acc.ID)
|
||||
assert.Equal(t, "UpdatedOrg", found.Name)
|
||||
assert.Equal(t, "updated.example.com", found.Domain)
|
||||
}
|
||||
|
||||
func TestAccount_SoftDelete(t *testing.T) {
|
||||
db := setupModelTestDB(t)
|
||||
defer cleanupDB(t, db)
|
||||
|
||||
acc := &Account{Name: "DeleteOrg", Locale: "en", Timezone: "UTC", Active: true}
|
||||
db.Create(acc)
|
||||
|
||||
err := db.Delete(acc).Error
|
||||
assert.NoError(t, err)
|
||||
|
||||
var found Account
|
||||
err = db.First(&found, acc.ID).Error
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, gorm.ErrRecordNotFound, err)
|
||||
}
|
||||
|
||||
func TestAccount_FeatureFlags(t *testing.T) {
|
||||
db := setupModelTestDB(t)
|
||||
defer cleanupDB(t, db)
|
||||
|
||||
acc := &Account{
|
||||
Name: "FlagsOrg",
|
||||
Locale: "en",
|
||||
Timezone: "UTC",
|
||||
Active: true,
|
||||
FeatureFlags: `{"auto_resolve":true,"copilot":false}`,
|
||||
AutoResolveDuration: 7,
|
||||
}
|
||||
err := db.Create(acc).Error
|
||||
assert.NoError(t, err)
|
||||
|
||||
var found Account
|
||||
db.First(&found, acc.ID)
|
||||
assert.Equal(t, `{"auto_resolve":true,"copilot":false}`, found.FeatureFlags)
|
||||
assert.Equal(t, 7, found.AutoResolveDuration)
|
||||
}
|
||||
|
||||
// --- User CRUD Tests ---
|
||||
|
||||
func TestUser_Create(t *testing.T) {
|
||||
db := setupModelTestDB(t)
|
||||
defer cleanupDB(t, db)
|
||||
|
||||
acc := &Account{Name: "UserOrg", Locale: "en", Timezone: "UTC", Active: true}
|
||||
db.Create(acc)
|
||||
|
||||
user := &User{
|
||||
AccountID: acc.ID,
|
||||
Name: "John Agent",
|
||||
Email: "john@example.com",
|
||||
Password: "hashed_password_placeholder",
|
||||
Provider: "email",
|
||||
Role: string(AccountUserRoleAgent),
|
||||
Active: true,
|
||||
}
|
||||
err := db.Create(user).Error
|
||||
assert.NoError(t, err)
|
||||
assert.NotZero(t, user.ID)
|
||||
assert.Equal(t, acc.ID, user.AccountID)
|
||||
assert.Equal(t, "John Agent", user.Name)
|
||||
assert.Equal(t, "john@example.com", user.Email)
|
||||
}
|
||||
|
||||
func TestUser_UpdateRole(t *testing.T) {
|
||||
db := setupModelTestDB(t)
|
||||
defer cleanupDB(t, db)
|
||||
|
||||
acc := &Account{Name: "RoleOrg", Locale: "en", Timezone: "UTC", Active: true}
|
||||
db.Create(acc)
|
||||
|
||||
user := &User{
|
||||
AccountID: acc.ID,
|
||||
Name: "AgentUser",
|
||||
Email: "agent_role@example.com",
|
||||
Password: "hashed",
|
||||
Provider: "email",
|
||||
Role: string(AccountUserRoleAgent),
|
||||
Active: true,
|
||||
}
|
||||
db.Create(user)
|
||||
|
||||
user.Role = string(AccountUserRoleAdministrator)
|
||||
err := db.Save(user).Error
|
||||
assert.NoError(t, err)
|
||||
|
||||
var found User
|
||||
db.First(&found, user.ID)
|
||||
assert.Equal(t, string(AccountUserRoleAdministrator), found.Role)
|
||||
}
|
||||
|
||||
func TestUser_TOTPFields(t *testing.T) {
|
||||
db := setupModelTestDB(t)
|
||||
defer cleanupDB(t, db)
|
||||
|
||||
acc := &Account{Name: "TOTPOrg", Locale: "en", Timezone: "UTC", Active: true}
|
||||
db.Create(acc)
|
||||
|
||||
user := &User{
|
||||
AccountID: acc.ID,
|
||||
Name: "TOTP User",
|
||||
Email: "totp@example.com",
|
||||
Password: "hashed",
|
||||
Provider: "email",
|
||||
Role: string(AccountUserRoleAgent),
|
||||
Active: true,
|
||||
TOTPSecret: "JBSWY3DPEHPK3PXP",
|
||||
TOTPEnabled: true,
|
||||
}
|
||||
err := db.Create(user).Error
|
||||
assert.NoError(t, err)
|
||||
|
||||
var found User
|
||||
db.First(&found, user.ID)
|
||||
assert.Equal(t, "JBSWY3DPEHPK3PXP", found.TOTPSecret)
|
||||
assert.True(t, found.TOTPEnabled)
|
||||
}
|
||||
|
||||
// --- AccountUser Tests ---
|
||||
|
||||
func TestAccountUser_Create(t *testing.T) {
|
||||
db := setupModelTestDB(t)
|
||||
defer cleanupDB(t, db)
|
||||
|
||||
acc := &Account{Name: "AssocOrg", Locale: "en", Timezone: "UTC", Active: true}
|
||||
db.Create(acc)
|
||||
user := &User{
|
||||
AccountID: acc.ID, Name: "AssocUser", Email: "assoc@example.com",
|
||||
Password: "hashed", Provider: "email", Role: string(AccountUserRoleAgent), Active: true,
|
||||
}
|
||||
db.Create(user)
|
||||
|
||||
au := &AccountUser{
|
||||
UserID: user.ID,
|
||||
AccountID: acc.ID,
|
||||
Role: string(AccountUserRoleAgent),
|
||||
Availability: "online",
|
||||
}
|
||||
err := db.Create(au).Error
|
||||
assert.NoError(t, err)
|
||||
assert.NotZero(t, au.ID)
|
||||
assert.Equal(t, user.ID, au.UserID)
|
||||
assert.Equal(t, acc.ID, au.AccountID)
|
||||
}
|
||||
|
||||
func TestAccountUser_IsAdministrator(t *testing.T) {
|
||||
au := &AccountUser{Role: string(AccountUserRoleAdministrator)}
|
||||
assert.True(t, au.IsAdministrator())
|
||||
|
||||
au2 := &AccountUser{Role: string(AccountUserRoleAgent)}
|
||||
assert.False(t, au2.IsAdministrator())
|
||||
}
|
||||
|
||||
// --- Inbox CRUD Tests ---
|
||||
|
||||
func TestInbox_Create(t *testing.T) {
|
||||
db := setupModelTestDB(t)
|
||||
defer cleanupDB(t, db)
|
||||
|
||||
acc := &Account{Name: "InboxOrg", Locale: "en", Timezone: "UTC", Active: true}
|
||||
db.Create(acc)
|
||||
|
||||
inbox := &Inbox{
|
||||
AccountID: acc.ID,
|
||||
Name: "Support",
|
||||
ChannelType: "web_widget",
|
||||
ChannelID: 1,
|
||||
EnableAutoAssignment: false,
|
||||
AutoAssignmentLimit: 0,
|
||||
}
|
||||
err := db.Create(inbox).Error
|
||||
assert.NoError(t, err)
|
||||
assert.NotZero(t, inbox.ID)
|
||||
assert.Equal(t, acc.ID, inbox.AccountID)
|
||||
assert.Equal(t, "Support", inbox.Name)
|
||||
assert.Equal(t, "web_widget", inbox.ChannelType)
|
||||
}
|
||||
|
||||
// --- Conversation CRUD Tests ---
|
||||
|
||||
func TestConversation_Create(t *testing.T) {
|
||||
db := setupModelTestDB(t)
|
||||
defer cleanupDB(t, db)
|
||||
|
||||
acc := &Account{Name: "ConvOrg", Locale: "en", Timezone: "UTC", Active: true}
|
||||
db.Create(acc)
|
||||
|
||||
contact := &Contact{AccountID: acc.ID, Name: "Test Contact"}
|
||||
db.Create(contact)
|
||||
|
||||
inbox := &Inbox{
|
||||
AccountID: acc.ID, Name: "Chat", ChannelType: "web_widget", ChannelID: 1,
|
||||
}
|
||||
db.Create(inbox)
|
||||
|
||||
conv := &Conversation{
|
||||
AccountID: acc.ID,
|
||||
InboxID: inbox.ID,
|
||||
ContactID: contact.ID,
|
||||
Status: string(ConversationStatusOpen),
|
||||
ChannelType: "web_widget",
|
||||
Channel: "web_widget",
|
||||
}
|
||||
err := db.Create(conv).Error
|
||||
assert.NoError(t, err)
|
||||
assert.NotZero(t, conv.ID)
|
||||
assert.Equal(t, string(ConversationStatusOpen), conv.Status)
|
||||
}
|
||||
|
||||
// --- Message CRUD Tests ---
|
||||
|
||||
func TestMessage_Create(t *testing.T) {
|
||||
db := setupModelTestDB(t)
|
||||
defer cleanupDB(t, db)
|
||||
|
||||
acc := &Account{Name: "MsgOrg", Locale: "en", Timezone: "UTC", Active: true}
|
||||
db.Create(acc)
|
||||
contact := &Contact{AccountID: acc.ID, Name: "Msg Contact"}
|
||||
db.Create(contact)
|
||||
inbox := &Inbox{
|
||||
AccountID: acc.ID, Name: "Chat", ChannelType: "web_widget", ChannelID: 1,
|
||||
}
|
||||
db.Create(inbox)
|
||||
conv := &Conversation{
|
||||
AccountID: acc.ID, InboxID: inbox.ID, ContactID: contact.ID,
|
||||
Status: string(ConversationStatusOpen), ChannelType: "web_widget", Channel: "web_widget",
|
||||
}
|
||||
db.Create(conv)
|
||||
|
||||
msg := &Message{
|
||||
ConversationID: conv.ID,
|
||||
AccountID: acc.ID,
|
||||
InboxID: inbox.ID,
|
||||
SenderType: "contact",
|
||||
Content: "Hello world",
|
||||
ContentType: "text",
|
||||
MessageType: "incoming",
|
||||
}
|
||||
err := db.Create(msg).Error
|
||||
assert.NoError(t, err)
|
||||
assert.NotZero(t, msg.ID)
|
||||
assert.Equal(t, "Hello world", msg.Content)
|
||||
assert.Equal(t, "incoming", msg.MessageType)
|
||||
assert.Equal(t, "contact", msg.SenderType)
|
||||
}
|
||||
|
||||
// --- Contact CRUD Tests ---
|
||||
|
||||
func TestContact_Create(t *testing.T) {
|
||||
db := setupModelTestDB(t)
|
||||
defer cleanupDB(t, db)
|
||||
|
||||
acc := &Account{Name: "ContactOrg", Locale: "en", Timezone: "UTC", Active: true}
|
||||
db.Create(acc)
|
||||
|
||||
contact := &Contact{
|
||||
AccountID: acc.ID,
|
||||
Name: "Jane Doe",
|
||||
Email: "jane@example.com",
|
||||
PhoneNumber: "+1234567890",
|
||||
}
|
||||
err := db.Create(contact).Error
|
||||
assert.NoError(t, err)
|
||||
assert.NotZero(t, contact.ID)
|
||||
assert.Equal(t, "Jane Doe", contact.Name)
|
||||
assert.Equal(t, "lead", contact.ContactType)
|
||||
}
|
||||
|
||||
func TestContact_CreateDefaultsToVisitorWithoutDetails(t *testing.T) {
|
||||
db := setupModelTestDB(t)
|
||||
defer cleanupDB(t, db)
|
||||
|
||||
acc := &Account{Name: "ContactDefaultOrg", Locale: "en", Timezone: "UTC", Active: true}
|
||||
require.NoError(t, db.Create(acc).Error)
|
||||
|
||||
contact := &Contact{AccountID: acc.ID, Name: "Visitor"}
|
||||
err := db.Create(contact).Error
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "visitor", contact.ContactType)
|
||||
}
|
||||
|
||||
// --- Enum Tests ---
|
||||
|
||||
func TestConversationStatus_Values(t *testing.T) {
|
||||
assert.Equal(t, "open", string(ConversationStatusOpen))
|
||||
assert.Equal(t, "resolved", string(ConversationStatusResolved))
|
||||
assert.Equal(t, "pending", string(ConversationStatusPending))
|
||||
assert.Equal(t, "snoozed", string(ConversationStatusSnoozed))
|
||||
}
|
||||
|
||||
func TestMessageType_Values(t *testing.T) {
|
||||
assert.Equal(t, "incoming", string(MessageTypeIncoming))
|
||||
assert.Equal(t, "outgoing", string(MessageTypeOutgoing))
|
||||
assert.Equal(t, "activity", string(MessageTypeActivity))
|
||||
}
|
||||
|
||||
func TestAccountUserRole_Values(t *testing.T) {
|
||||
assert.Equal(t, "agent", string(AccountUserRoleAgent))
|
||||
assert.Equal(t, "administrator", string(AccountUserRoleAdministrator))
|
||||
}
|
||||
|
||||
func TestMessageContentType_Values(t *testing.T) {
|
||||
assert.Equal(t, "text", string(MessageContentTypeText))
|
||||
}
|
||||
|
||||
// --- PlatformApp Tests ---
|
||||
|
||||
func uintPtr(v uint) *uint { return &v }
|
||||
|
||||
func TestPlatformApp_IsPlatformLevel(t *testing.T) {
|
||||
pa := &PlatformApp{AccountID: nil}
|
||||
assert.True(t, pa.IsPlatformLevel())
|
||||
|
||||
pa2 := &PlatformApp{AccountID: uintPtr(1)}
|
||||
assert.False(t, pa2.IsPlatformLevel())
|
||||
}
|
||||
|
||||
// --- CustomRole Permission Level Tests (using model's own constants) ---
|
||||
|
||||
func TestPermissionLevel_Constants(t *testing.T) {
|
||||
assert.Equal(t, "full", string(PermissionLevelFull))
|
||||
assert.Equal(t, "read", string(PermissionLevelRead))
|
||||
assert.Equal(t, "none", string(PermissionLevelNone))
|
||||
}
|
||||
|
||||
func TestPermissionDimension_Constants(t *testing.T) {
|
||||
assert.Equal(t, "conversation_manage", string(DimensionConversationManage))
|
||||
assert.Equal(t, "conversation_delete", string(DimensionConversationDelete))
|
||||
assert.Equal(t, "contact_manage", string(DimensionContactManage))
|
||||
assert.Equal(t, "report_manage", string(DimensionReportManage))
|
||||
assert.Equal(t, "knowledge_base_manage", string(DimensionKnowledgeBaseManage))
|
||||
assert.Equal(t, "automation_manage", string(DimensionAutomationManage))
|
||||
}
|
||||
Reference in New Issue
Block a user