Files
gochat/backend/internal/model/associations_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

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)
}