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.
325 lines
12 KiB
Go
325 lines
12 KiB
Go
package model_test
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"github.com/gochat/gochat/internal/model"
|
|
)
|
|
|
|
// TestUserAccountIDAssociation verifies User references Account via AccountID FK.
|
|
func TestUserAccountIDAssociation(t *testing.T) {
|
|
db := newTestDB(t)
|
|
defer cleanupDB(t, db)
|
|
|
|
acc := createAccount(t, db, "AssocOrg")
|
|
user := createUser(t, db, acc.ID, "Assoc User", "assoc@example.com")
|
|
|
|
// User doesn't have explicit Account relation field, verify FK consistency
|
|
var fetched model.User
|
|
assert.NoError(t, db.First(&fetched, user.ID).Error)
|
|
assert.Equal(t, acc.ID, fetched.AccountID)
|
|
|
|
// Verify account exists for the FK
|
|
var accFetched model.Account
|
|
assert.NoError(t, db.First(&accFetched, fetched.AccountID).Error)
|
|
assert.Equal(t, "AssocOrg", accFetched.Name)
|
|
}
|
|
|
|
// TestAccountHasManyUsers verifies Account has multiple Users.
|
|
func TestAccountHasManyUsers(t *testing.T) {
|
|
db := newTestDB(t)
|
|
defer cleanupDB(t, db)
|
|
|
|
acc := createAccount(t, db, "MultiUserOrg")
|
|
user1 := createUser(t, db, acc.ID, "User One", "one@example.com")
|
|
user2 := createUser(t, db, acc.ID, "User Two", "two@example.com")
|
|
user3 := createUser(t, db, acc.ID, "User Three", "three@example.com")
|
|
|
|
// Verify all users belong to the account
|
|
var users []model.User
|
|
assert.NoError(t, db.Where("account_id = ?", acc.ID).Find(&users).Error)
|
|
assert.Len(t, users, 3)
|
|
assert.Equal(t, user1.ID, users[0].ID)
|
|
assert.Equal(t, user2.ID, users[1].ID)
|
|
assert.Equal(t, user3.ID, users[2].ID)
|
|
}
|
|
|
|
// TestInboxAccountIDAssociation verifies Inbox references Account via AccountID FK.
|
|
func TestInboxAccountIDAssociation(t *testing.T) {
|
|
db := newTestDB(t)
|
|
defer cleanupDB(t, db)
|
|
|
|
acc := createAccount(t, db, "InboxAssocOrg")
|
|
inbox := createInbox(t, db, acc.ID, "Assoc Inbox", "web_widget")
|
|
|
|
var fetched model.Inbox
|
|
assert.NoError(t, db.First(&fetched, inbox.ID).Error)
|
|
assert.Equal(t, acc.ID, fetched.AccountID)
|
|
}
|
|
|
|
// TestConversationFKAssociations verifies Conversation FK references (Inbox, Contact, Account).
|
|
func TestConversationFKAssociations(t *testing.T) {
|
|
db := newTestDB(t)
|
|
defer cleanupDB(t, db)
|
|
|
|
acc := createAccount(t, db, "ConvAssocOrg")
|
|
inbox := createInbox(t, db, acc.ID, "Conv Inbox", "web_widget")
|
|
contact := createContact(t, db, acc.ID, "Conv Contact")
|
|
conv := createConversation(t, db, acc.ID, inbox.ID, contact.ID, "open")
|
|
|
|
var fetched model.Conversation
|
|
assert.NoError(t, db.First(&fetched, conv.ID).Error)
|
|
assert.Equal(t, inbox.ID, fetched.InboxID)
|
|
assert.Equal(t, contact.ID, fetched.ContactID)
|
|
assert.Equal(t, acc.ID, fetched.AccountID)
|
|
|
|
// Verify referenced entities exist
|
|
var accFetched model.Account
|
|
assert.NoError(t, db.First(&accFetched, fetched.AccountID).Error)
|
|
var inboxFetched model.Inbox
|
|
assert.NoError(t, db.First(&inboxFetched, fetched.InboxID).Error)
|
|
var contactFetched model.Contact
|
|
assert.NoError(t, db.First(&contactFetched, fetched.ContactID).Error)
|
|
}
|
|
|
|
// TestConversationAssigneeAssociation verifies Conversation -> User (assignee) optional association.
|
|
func TestConversationAssigneeAssociation(t *testing.T) {
|
|
db := newTestDB(t)
|
|
defer cleanupDB(t, db)
|
|
|
|
acc := createAccount(t, db, "AssigneeOrg")
|
|
inbox := createInbox(t, db, acc.ID, "Assign Inbox", "web_widget")
|
|
contact := createContact(t, db, acc.ID, "Assign Contact")
|
|
agent := createUser(t, db, acc.ID, "Agent Smith", "agent@example.com")
|
|
|
|
conv := createConversation(t, db, acc.ID, inbox.ID, contact.ID, "open")
|
|
|
|
// Assign agent to conversation
|
|
assert.NoError(t, db.Model(&model.Conversation{}).Where("id = ?", conv.ID).
|
|
Update("AssigneeID", agent.ID).Error)
|
|
|
|
var fetched model.Conversation
|
|
assert.NoError(t, db.First(&fetched, conv.ID).Error)
|
|
assert.NotNil(t, fetched.AssigneeID)
|
|
assert.Equal(t, agent.ID, *fetched.AssigneeID)
|
|
|
|
// Unassign
|
|
assert.NoError(t, db.Model(&model.Conversation{}).Where("id = ?", conv.ID).
|
|
Update("AssigneeID", nil).Error)
|
|
var unassigned model.Conversation
|
|
assert.NoError(t, db.First(&unassigned, conv.ID).Error)
|
|
assert.Nil(t, unassigned.AssigneeID)
|
|
}
|
|
|
|
// TestMessageConversationAssociation verifies Message -> Conversation association.
|
|
func TestMessageConversationAssociation(t *testing.T) {
|
|
db := newTestDB(t)
|
|
defer cleanupDB(t, db)
|
|
|
|
acc := createAccount(t, db, "MsgAssocOrg")
|
|
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")
|
|
|
|
msg := &model.Message{
|
|
ConversationID: conv.ID, AccountID: acc.ID, InboxID: inbox.ID,
|
|
SenderType: "contact", Content: "Assoc message", ContentType: "text",
|
|
MessageType: "incoming",
|
|
}
|
|
assert.NoError(t, db.Create(msg).Error)
|
|
|
|
// Multiple messages in one conversation
|
|
msg2 := &model.Message{
|
|
ConversationID: conv.ID, AccountID: acc.ID, InboxID: inbox.ID,
|
|
SenderType: "agent", Content: "Reply", ContentType: "text",
|
|
MessageType: "outgoing",
|
|
}
|
|
assert.NoError(t, db.Create(msg2).Error)
|
|
|
|
var messages []model.Message
|
|
assert.NoError(t, db.Where("conversation_id = ?", conv.ID).Find(&messages).Error)
|
|
assert.Len(t, messages, 2)
|
|
}
|
|
|
|
// TestContactInboxAssociation verifies Contact <-> Inbox join association.
|
|
func TestContactInboxAssociation(t *testing.T) {
|
|
db := newTestDB(t)
|
|
defer cleanupDB(t, db)
|
|
|
|
acc := createAccount(t, db, "CIAssocOrg")
|
|
inbox := createInbox(t, db, acc.ID, "CI Inbox", "web_widget")
|
|
contact := createContact(t, db, acc.ID, "CI Contact")
|
|
|
|
// Create ContactInbox join
|
|
ci := &model.ContactInbox{
|
|
ContactID: contact.ID, InboxID: inbox.ID,
|
|
SourceID: "web_session_123", HMACToken: "hmac_token_val",
|
|
PubsubToken: "pubsub_token_val",
|
|
}
|
|
assert.NoError(t, db.Create(ci).Error)
|
|
assert.NotZero(t, ci.ID)
|
|
|
|
// Preload associations (ContactInbox has Contact and Inbox relation fields)
|
|
var fetched model.ContactInbox
|
|
assert.NoError(t, db.Preload("Contact").Preload("Inbox").First(&fetched, ci.ID).Error)
|
|
assert.Equal(t, contact.ID, fetched.Contact.ID)
|
|
assert.Equal(t, inbox.ID, fetched.Inbox.ID)
|
|
assert.Equal(t, "web_session_123", fetched.SourceID)
|
|
}
|
|
|
|
// TestInboxMemberAssociation verifies Inbox <-> User (agent) join association.
|
|
func TestInboxMemberAssociation(t *testing.T) {
|
|
db := newTestDB(t)
|
|
defer cleanupDB(t, db)
|
|
|
|
acc := createAccount(t, db, "IMAssocOrg")
|
|
inbox := createInbox(t, db, acc.ID, "IM Inbox", "web_widget")
|
|
agent1 := createUser(t, db, acc.ID, "IM Agent1", "im1@example.com")
|
|
agent2 := createUser(t, db, acc.ID, "IM Agent2", "im2@example.com")
|
|
|
|
// Assign both agents to the inbox
|
|
im1 := &model.InboxMember{InboxID: inbox.ID, UserID: agent1.ID}
|
|
assert.NoError(t, db.Create(im1).Error)
|
|
assert.NotZero(t, im1.ID)
|
|
|
|
im2 := &model.InboxMember{InboxID: inbox.ID, UserID: agent2.ID}
|
|
assert.NoError(t, db.Create(im2).Error)
|
|
assert.NotZero(t, im2.ID)
|
|
|
|
// Preload and verify (InboxMember has Inbox and User relation fields)
|
|
var fetched1 model.InboxMember
|
|
assert.NoError(t, db.Preload("Inbox").Preload("User").First(&fetched1, im1.ID).Error)
|
|
assert.Equal(t, inbox.ID, fetched1.Inbox.ID)
|
|
assert.Equal(t, agent1.ID, fetched1.User.ID)
|
|
|
|
var fetched2 model.InboxMember
|
|
assert.NoError(t, db.Preload("Inbox").Preload("User").First(&fetched2, im2.ID).Error)
|
|
assert.Equal(t, agent2.ID, fetched2.User.ID)
|
|
|
|
// Count inbox members
|
|
var members []model.InboxMember
|
|
assert.NoError(t, db.Where("inbox_id = ?", inbox.ID).Find(&members).Error)
|
|
assert.Len(t, members, 2)
|
|
}
|
|
|
|
// TestAccountUserAssociation verifies Account <-> User membership association.
|
|
func TestAccountUserAssociation(t *testing.T) {
|
|
db := newTestDB(t)
|
|
defer cleanupDB(t, db)
|
|
|
|
acc1 := createAccount(t, db, "AUAssocOrg1")
|
|
acc2 := createAccount(t, db, "AUAssocOrg2")
|
|
user := createUser(t, db, acc1.ID, "Multi User", "multi@example.com")
|
|
|
|
// User belongs to both accounts with different roles
|
|
au1 := createAccountUser(t, db, user.ID, acc1.ID, string(model.AccountUserRoleAdministrator))
|
|
au2 := createAccountUser(t, db, user.ID, acc2.ID, string(model.AccountUserRoleAgent))
|
|
|
|
// Preload User and Account in AccountUser (has both relation fields)
|
|
var fetched1 model.AccountUser
|
|
assert.NoError(t, db.Preload("User").Preload("Account").First(&fetched1, au1.ID).Error)
|
|
assert.Equal(t, user.ID, fetched1.User.ID)
|
|
assert.Equal(t, acc1.ID, fetched1.Account.ID)
|
|
assert.Equal(t, string(model.AccountUserRoleAdministrator), fetched1.Role)
|
|
|
|
var fetched2 model.AccountUser
|
|
assert.NoError(t, db.Preload("User").Preload("Account").First(&fetched2, au2.ID).Error)
|
|
assert.Equal(t, acc2.ID, fetched2.Account.ID)
|
|
assert.Equal(t, string(model.AccountUserRoleAgent), fetched2.Role)
|
|
}
|
|
|
|
// TestCustomRoleAccountUserAssociation verifies CustomRole -> AccountUser association.
|
|
func TestCustomRoleAccountUserAssociation(t *testing.T) {
|
|
db := newTestDB(t)
|
|
defer cleanupDB(t, db)
|
|
|
|
acc := createAccount(t, db, "CRAssocOrg")
|
|
user := createUser(t, db, acc.ID, "CR User", "cr@example.com")
|
|
role := createCustomRole(t, db, acc.ID, "Custom Agent",
|
|
`{"conversation_manage":"read","contact_manage":"none"}`)
|
|
|
|
// Create AccountUser with CustomRole
|
|
au := &model.AccountUser{
|
|
UserID: user.ID, AccountID: acc.ID,
|
|
Role: "agent", CustomRoleID: role.ID,
|
|
Availability: "online",
|
|
}
|
|
assert.NoError(t, db.Create(au).Error)
|
|
|
|
var fetched model.AccountUser
|
|
assert.NoError(t, db.First(&fetched, au.ID).Error)
|
|
assert.Equal(t, role.ID, fetched.CustomRoleID)
|
|
assert.Equal(t, "agent", fetched.Role)
|
|
}
|
|
|
|
// TestAttachmentMessageAssociation verifies Attachment -> Message association.
|
|
func TestAttachmentMessageAssociation(t *testing.T) {
|
|
db := newTestDB(t)
|
|
defer cleanupDB(t, db)
|
|
|
|
acc := createAccount(t, db, "AttachOrg")
|
|
inbox := createInbox(t, db, acc.ID, "Attach Inbox", "web_widget")
|
|
contact := createContact(t, db, acc.ID, "Attach Contact")
|
|
conv := createConversation(t, db, acc.ID, inbox.ID, contact.ID, "open")
|
|
|
|
msg := &model.Message{
|
|
ConversationID: conv.ID, AccountID: acc.ID, InboxID: inbox.ID,
|
|
SenderType: "contact", Content: "See attached file", ContentType: "text",
|
|
MessageType: "incoming",
|
|
}
|
|
assert.NoError(t, db.Create(msg).Error)
|
|
|
|
att := &model.Attachment{
|
|
MessageID: msg.ID, FileType: "image", ExternalURL: "https://example.com/img.png",
|
|
FileName: "photo.png", FileSize: 2048,
|
|
}
|
|
assert.NoError(t, db.Create(att).Error)
|
|
|
|
// Attachment has Message relation field
|
|
var fetched model.Attachment
|
|
assert.NoError(t, db.Preload("Message").First(&fetched, att.ID).Error)
|
|
assert.Equal(t, msg.ID, fetched.Message.ID)
|
|
assert.Equal(t, "image", fetched.FileType)
|
|
}
|
|
|
|
// TestNotificationUserAssociation verifies Notification -> User association.
|
|
func TestNotificationUserAssociation(t *testing.T) {
|
|
db := newTestDB(t)
|
|
defer cleanupDB(t, db)
|
|
|
|
acc := createAccount(t, db, "NotifOrg")
|
|
user := createUser(t, db, acc.ID, "Notif User", "notif@example.com")
|
|
|
|
notif := &model.Notification{
|
|
UserID: user.ID, AccountID: &acc.ID,
|
|
NotificationType: "conversation_created",
|
|
PrimaryActorType: "Conversation", PrimaryActorID: 1,
|
|
}
|
|
assert.NoError(t, db.Create(notif).Error)
|
|
|
|
// Notification has User relation field
|
|
var fetched model.Notification
|
|
assert.NoError(t, db.Preload("User").First(&fetched, notif.ID).Error)
|
|
assert.Equal(t, user.ID, fetched.User.ID)
|
|
assert.Equal(t, "conversation_created", fetched.NotificationType)
|
|
}
|
|
|
|
// TestCascadeSoftDelete verifies that soft-deleting Account does not cascade-delete Users.
|
|
func TestCascadeSoftDelete(t *testing.T) {
|
|
db := newTestDB(t)
|
|
defer cleanupDB(t, db)
|
|
|
|
acc := createAccount(t, db, "CascadeOrg")
|
|
user := createUser(t, db, acc.ID, "Cascade User", "cascade@example.com")
|
|
|
|
// Soft-delete the account
|
|
assert.NoError(t, db.Delete(&model.Account{}, acc.ID).Error)
|
|
|
|
// User should still exist (GORM doesn't cascade soft-deletes by default)
|
|
var fetched model.User
|
|
assert.NoError(t, db.First(&fetched, user.ID).Error)
|
|
assert.Equal(t, acc.ID, fetched.AccountID)
|
|
}
|