Files
fm_be/internal/repository/mysql/contact_repo_test.go
2026-07-16 22:16:45 +07:00

968 lines
28 KiB
Go

package mysql
import (
"context"
"errors"
"fmt"
"strings"
"testing"
"time"
"gorm.io/gorm"
"wucher/internal/domain/auth"
"wucher/internal/domain/contact"
"wucher/internal/shared/pkg/appctx"
"wucher/internal/shared/pkg/uuidv7"
)
func TestContactDetailSelectSQL_IncludesProfileAttachmentID(t *testing.T) {
sql := contactDetailSelectSQL()
if !strings.Contains(sql, "users.profile_attachment_id AS profile_attachment_id") {
t.Fatalf("expected detail select to include users.profile_attachment_id alias")
}
}
func TestContactListSelectSQL_IncludesProfileAttachmentID(t *testing.T) {
sql := contactListSelectSQL()
if !strings.Contains(sql, "users.profile_attachment_id AS profile_attachment_id") {
t.Fatalf("expected list select to include users.profile_attachment_id alias")
}
}
func TestContactRepositoryUpdateContactClearSSOEmailUnlinksMicrosoftIdentity(t *testing.T) {
db := openDBTestSQLite(t)
if err := AutoMigrate(db); err != nil {
t.Fatalf("auto migrate: %v", err)
}
repo := NewContactRepository(db)
adminRoleID := uuidv7.MustBytes()
pilotRoleID := uuidv7.MustBytes()
if err := db.Create(&auth.Role{ID: adminRoleID, Code: "admin", Name: "Admin"}).Error; err != nil {
t.Fatalf("create admin role: %v", err)
}
if err := db.Create(&auth.Role{ID: pilotRoleID, Code: auth.RoleCodePilot, Name: "Pilot"}).Error; err != nil {
t.Fatalf("create pilot role: %v", err)
}
adminUser := &auth.User{
ID: uuidv7.MustBytes(),
Email: "admin3@example.com",
FirstName: "Admin",
LastName: "User",
Timezone: "UTC",
RoleID: adminRoleID,
IsActive: true,
}
if err := db.Create(adminUser).Error; err != nil {
t.Fatalf("create admin user: %v", err)
}
ctx := appctx.WithUserID(context.Background(), adminUser.ID)
sso := "a@microsoft.com"
target := &auth.User{
ID: uuidv7.MustBytes(),
Email: "target@example.com",
SSOEmail: &sso,
FirstName: "Target",
LastName: "User",
Timezone: "UTC",
RoleID: pilotRoleID,
IsActive: true,
}
if err := db.Create(target).Error; err != nil {
t.Fatalf("create target user: %v", err)
}
if err := db.Create(&auth.UserIdentity{
ID: uuidv7.MustBytes(),
UserID: target.ID,
Provider: "microsoft",
ProviderSubject: "subject-a",
Email: "a@microsoft.com",
}).Error; err != nil {
t.Fatalf("create identity: %v", err)
}
empty := " "
if err := repo.UpdateContact(ctx, contact.UpdateInput{
UserID: target.ID,
User: contact.UserPatch{
SSOEmail: &empty,
},
}); err != nil {
t.Fatalf("update contact clear sso email: %v", err)
}
var refreshed auth.User
if err := db.Where("id = ?", target.ID).Take(&refreshed).Error; err != nil {
t.Fatalf("reload user: %v", err)
}
if refreshed.SSOEmail != nil {
t.Fatalf("expected user sso_email NULL, got %v", *refreshed.SSOEmail)
}
var count int64
if err := db.Table("user_identities").
Where("user_id = ? AND provider = ?", target.ID, "microsoft").
Count(&count).Error; err != nil {
t.Fatalf("count identities: %v", err)
}
if count != 0 {
t.Fatalf("expected microsoft identity unlinked, got %d rows", count)
}
}
func TestContactRepositoryDeleteContactRemovesUserAndProfiles(t *testing.T) {
db := openDBTestSQLite(t)
if err := AutoMigrate(db); err != nil {
t.Fatalf("auto migrate: %v", err)
}
repo := NewContactRepository(db)
roleID := uuidv7.MustBytes()
if err := db.Create(&auth.Role{ID: roleID, Code: auth.RoleCodePilot, Name: "Pilot"}).Error; err != nil {
t.Fatalf("create role: %v", err)
}
username := "pilot.delete"
user := &auth.User{
ID: uuidv7.MustBytes(),
Email: "pilot-delete@example.com",
Username: &username,
FirstName: "Pilot",
LastName: "Delete",
Timezone: "Asia/Jakarta",
RoleID: roleID,
IsActive: true,
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
}
if err := db.Create(user).Error; err != nil {
t.Fatalf("create user: %v", err)
}
if err := db.Create(&auth.PilotProfile{
UserID: user.ID,
PilotCategory: auth.PilotCategoryRegular,
ShortName: "PD",
}).Error; err != nil {
t.Fatalf("create pilot profile: %v", err)
}
if err := db.Create(&auth.DoctorProfile{
UserID: user.ID,
ShortName: "Dr PD",
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
}).Error; err != nil {
t.Fatalf("create extra profile: %v", err)
}
if err := repo.DeleteContact(context.Background(), user.ID); err != nil {
t.Fatalf("delete contact: %v", err)
}
var userCount int64
if err := db.Table("users").Where("id = ?", user.ID).Count(&userCount).Error; err != nil {
t.Fatalf("count users: %v", err)
}
if userCount != 0 {
t.Fatalf("expected user deleted, got %d rows", userCount)
}
for _, table := range contactProfileTables {
var count int64
if err := db.Table(table).Where("user_id = ?", user.ID).Count(&count).Error; err != nil {
t.Fatalf("count %s: %v", table, err)
}
if count != 0 {
t.Fatalf("expected %s deleted, got %d rows", table, count)
}
}
}
func TestContactRepositoryDeleteContactBlockedWhenUsedByBase(t *testing.T) {
db := openDBTestSQLite(t)
if err := AutoMigrate(db); err != nil {
t.Fatalf("auto migrate: %v", err)
}
roleID := uuidv7.MustBytes()
if err := db.Create(&auth.Role{ID: roleID, Code: auth.RoleCodePilot, Name: "Pilot"}).Error; err != nil {
t.Fatalf("create role: %v", err)
}
user := &auth.User{
ID: uuidv7.MustBytes(),
Email: "pilot-base-ref@example.com",
FirstName: "Pilot",
LastName: "Ref",
Timezone: "UTC",
RoleID: roleID,
IsActive: true,
}
if err := db.Create(user).Error; err != nil {
t.Fatalf("create user: %v", err)
}
if err := db.Exec(`CREATE TABLE IF NOT EXISTS base_contact_roles (
id BLOB PRIMARY KEY,
base_id BLOB NOT NULL,
contact_id BLOB NOT NULL,
role_code TEXT NOT NULL
)`).Error; err != nil {
t.Fatalf("create base_contact_roles: %v", err)
}
repo := NewContactRepository(db)
if err := db.Table("base_contact_roles").Create(map[string]any{
"id": uuidv7.MustBytes(),
"base_id": uuidv7.MustBytes(),
"contact_id": user.ID,
"role_code": "responsible_flight_rescuer",
}).Error; err != nil {
t.Fatalf("seed base_contact_roles: %v", err)
}
err := repo.DeleteContact(context.Background(), user.ID)
if err == nil {
t.Fatalf("expected delete to be blocked")
}
if !strings.Contains(strings.ToLower(err.Error()), "bases") {
t.Fatalf("expected bases dependency in error, got: %v", err)
}
}
func TestContactRepositoryUpdateContactSameValuesDoesNotReturnNotFound(t *testing.T) {
db := openDBTestSQLite(t)
if err := AutoMigrate(db); err != nil {
t.Fatalf("auto migrate: %v", err)
}
repo := NewContactRepository(db)
roleID := uuidv7.MustBytes()
if err := db.Create(&auth.Role{ID: roleID, Code: auth.RoleCodePilot, Name: "Pilot"}).Error; err != nil {
t.Fatalf("create role: %v", err)
}
issuedAt := time.Date(2004, 10, 3, 0, 0, 0, 0, time.UTC)
expiredAt := time.Date(2029, 3, 27, 0, 0, 0, 0, time.UTC)
weight := 0.0
username := "pilot_test_2"
user := &auth.User{
ID: uuidv7.MustBytes(),
Email: "pilot2@test.com",
Username: &username,
FirstName: "a",
LastName: "a",
MobilePhone: "2",
Timezone: "Asia/Jakarta",
RoleID: roleID,
IsActive: true,
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
}
if err := db.Create(user).Error; err != nil {
t.Fatalf("create user: %v", err)
}
if err := db.Create(&auth.PilotProfile{
UserID: user.ID,
PilotCategory: auth.PilotCategoryRegular,
ShortName: "a",
LicenseNo: "a",
LicenseIssuedAt: &issuedAt,
LicenseExpiredAt: &expiredAt,
TechnicianLicenseNo: "",
HasIFRQualification: true,
IsChiefPilot: false,
IsDUL: true,
TotalFlightMinutes: 2,
ResponsiblePilotMinutes: 2,
SecondPilotMinutes: 2,
DoubleCommandMinutes: 2,
FlightInstructorMinutes: 0,
NightFlightMinutes: 2,
IFRFlightMinutes: 2,
Location: "string",
Postcode: "2",
StreetLine: "2",
WeightKG: &weight,
PhotoFileID: "a",
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
}).Error; err != nil {
t.Fatalf("create pilot profile: %v", err)
}
email := "pilot2@test.com"
firstName := "a"
lastName := "a"
mobilePhone := "2"
timezone := "Asia/Jakarta"
isActive := true
pilotCategory := auth.PilotCategoryRegular
shortName := "a"
licenseNo := "a"
technicianLicenseNo := ""
hasIFRQualification := true
isChiefPilot := false
isDUL := true
totalFlightMinutes := 2
responsiblePilotMinutes := 2
secondPilotMinutes := 2
doubleCommandMinutes := 2
flightInstructorMinutes := 0
nightFlightMinutes := 2
ifrFlightMinutes := 2
location := "string"
postcode := "2"
streetLine := "2"
photoFileID := "a"
err := repo.UpdateContact(context.Background(), contact.UpdateInput{
UserID: user.ID,
User: contact.UserPatch{
Email: &email,
Username: &username,
FirstName: &firstName,
LastName: &lastName,
MobilePhone: &mobilePhone,
Timezone: &timezone,
IsActive: &isActive,
},
Profile: contact.ProfilePatch{
PilotCategory: &pilotCategory,
ShortName: &shortName,
LicenseNo: &licenseNo,
LicenseIssuedAt: &issuedAt,
LicenseExpiredAt: &expiredAt,
TechnicianLicenseNo: &technicianLicenseNo,
HasIFRQualification: &hasIFRQualification,
IsChiefPilot: &isChiefPilot,
IsDUL: &isDUL,
TotalFlightMinutes: &totalFlightMinutes,
ResponsiblePilotMinutes: &responsiblePilotMinutes,
SecondPilotMinutes: &secondPilotMinutes,
DoubleCommandMinutes: &doubleCommandMinutes,
FlightInstructorMinutes: &flightInstructorMinutes,
NightFlightMinutes: &nightFlightMinutes,
IFRFlightMinutes: &ifrFlightMinutes,
Location: &location,
Postcode: &postcode,
StreetLine: &streetLine,
WeightKG: &weight,
PhotoFileID: &photoFileID,
},
})
if err != nil {
t.Fatalf("update contact with same values: %v", err)
}
var pilotProfileCount int64
if err := db.Table("pilot_profiles").Where("user_id = ?", user.ID).Count(&pilotProfileCount).Error; err != nil {
t.Fatalf("count pilot profiles: %v", err)
}
if pilotProfileCount != 1 {
t.Fatalf("expected single pilot profile row after update, got %d", pilotProfileCount)
}
got, err := repo.GetContactByID(context.Background(), user.ID)
if err != nil {
t.Fatalf("get contact after update: %v", err)
}
if got == nil || got.RoleCode != auth.RoleCodePilot {
t.Fatalf("expected pilot contact after update, got %#v", got)
}
}
func TestContactRepositoryUpdateContactClearsAirRescuerProfileStringFields(t *testing.T) {
db := openDBTestSQLite(t)
if err := AutoMigrate(db); err != nil {
t.Fatalf("auto migrate: %v", err)
}
repo := NewContactRepository(db)
roleID := uuidv7.MustBytes()
if err := db.Create(&auth.Role{ID: roleID, Code: auth.RoleCodeAirRescuer, Name: "Air Rescuer"}).Error; err != nil {
t.Fatalf("create role: %v", err)
}
user := &auth.User{
ID: uuidv7.MustBytes(),
Email: "air-rescuer@test.com",
FirstName: "Air",
LastName: "Rescuer",
Timezone: "UTC",
RoleID: roleID,
IsActive: true,
}
if err := db.Create(user).Error; err != nil {
t.Fatalf("create user: %v", err)
}
if err := db.Create(&auth.AirRescuerProfile{
UserID: user.ID,
ShortName: "CINDY",
Location: "12",
Postcode: "12",
StreetLine: "12",
Note: "12",
ResponsibleFlightRescuer: false,
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
}).Error; err != nil {
t.Fatalf("create air rescuer profile: %v", err)
}
empty := ""
if err := repo.UpdateContact(context.Background(), contact.UpdateInput{
UserID: user.ID,
Profile: contact.ProfilePatch{
ShortName: &empty,
Location: &empty,
Postcode: &empty,
StreetLine: &empty,
Note: &empty,
},
}); err != nil {
t.Fatalf("update contact clear profile strings: %v", err)
}
var profile auth.AirRescuerProfile
if err := db.Where("user_id = ?", user.ID).Take(&profile).Error; err != nil {
t.Fatalf("reload air rescuer profile: %v", err)
}
if profile.ShortName != "" || profile.Location != "" || profile.Postcode != "" || profile.StreetLine != "" || profile.Note != "" {
t.Fatalf("expected cleared string fields, got %#v", profile)
}
}
func TestContactRepositoryCreateContactRejectsDuplicateUsername(t *testing.T) {
db := openDBTestSQLite(t)
if err := AutoMigrate(db); err != nil {
t.Fatalf("auto migrate: %v", err)
}
repo := NewContactRepository(db)
roleID := uuidv7.MustBytes()
if err := db.Create(&auth.Role{ID: roleID, Code: auth.RoleCodePilot, Name: "Pilot"}).Error; err != nil {
t.Fatalf("create role: %v", err)
}
username := "pilot.duplicate"
first, err := repo.CreateContact(context.Background(), contact.CreateInput{
RoleID: roleID,
User: contact.UserPatch{
Email: strPtrContactRepo("first@example.com"),
Username: &username,
FirstName: strPtrContactRepo("First"),
LastName: strPtrContactRepo("User"),
},
})
if err != nil {
t.Fatalf("create first contact: %v", err)
}
if len(first) != 16 {
t.Fatalf("expected first user id, got len=%d", len(first))
}
secondUsername := "pilot.duplicate"
_, err = repo.CreateContact(context.Background(), contact.CreateInput{
RoleID: roleID,
User: contact.UserPatch{
Email: strPtrContactRepo("second@example.com"),
Username: &secondUsername,
FirstName: strPtrContactRepo("Second"),
LastName: strPtrContactRepo("User"),
},
})
if !errors.Is(err, contact.ErrUsernameAlreadyExists) {
t.Fatalf("expected duplicate username error, got %v", err)
}
}
func TestContactRepositoryUpdateContactRejectsDuplicateUsername(t *testing.T) {
db := openDBTestSQLite(t)
if err := AutoMigrate(db); err != nil {
t.Fatalf("auto migrate: %v", err)
}
repo := NewContactRepository(db)
roleID := uuidv7.MustBytes()
if err := db.Create(&auth.Role{ID: roleID, Code: auth.RoleCodePilot, Name: "Pilot"}).Error; err != nil {
t.Fatalf("create role: %v", err)
}
firstUsername := "pilot.update.1"
firstID, err := repo.CreateContact(context.Background(), contact.CreateInput{
RoleID: roleID,
User: contact.UserPatch{
Email: strPtrContactRepo("first-update@example.com"),
Username: &firstUsername,
FirstName: strPtrContactRepo("First"),
LastName: strPtrContactRepo("User"),
},
})
if err != nil {
t.Fatalf("create first contact: %v", err)
}
secondUsername := "pilot.update.2"
secondID, err := repo.CreateContact(context.Background(), contact.CreateInput{
RoleID: roleID,
User: contact.UserPatch{
Email: strPtrContactRepo("second-update@example.com"),
Username: &secondUsername,
FirstName: strPtrContactRepo("Second"),
LastName: strPtrContactRepo("User"),
},
})
if err != nil {
t.Fatalf("create second contact: %v", err)
}
newUsername := firstUsername
err = repo.UpdateContact(context.Background(), contact.UpdateInput{
UserID: secondID,
User: contact.UserPatch{
Username: &newUsername,
},
})
if !errors.Is(err, contact.ErrUsernameAlreadyExists) {
t.Fatalf("expected duplicate username error on update, got %v", err)
}
got, err := repo.GetContactByID(context.Background(), firstID)
if err != nil {
t.Fatalf("get first contact: %v", err)
}
if got == nil || got.Username != firstUsername {
t.Fatalf("unexpected first contact after duplicate update: %#v", got)
}
}
func TestContactRepositoryListContactsDefaultSortByActiveAndSortKey(t *testing.T) {
db := openDBTestSQLite(t)
if err := AutoMigrate(db); err != nil {
t.Fatalf("auto migrate: %v", err)
}
repo := NewContactRepository(db)
roleID := uuidv7.MustBytes()
if err := db.Create(&auth.Role{ID: roleID, Code: auth.RoleCodeStaff, Name: "Staff"}).Error; err != nil {
t.Fatalf("create role: %v", err)
}
seedContactStaffUser(t, db, roleID, "Gamma", nil, true, "g-note")
seedContactStaffUser(t, db, roleID, "Beta", intPtrContactRepo(0), true, "b-note")
seedContactStaffUser(t, db, roleID, "Charlie", intPtrContactRepo(2), true, "c-note")
seedContactStaffUser(t, db, roleID, "Alpha", intPtrContactRepo(1), true, "a-note")
seedContactStaffUser(t, db, roleID, "Zulu", nil, false, "z-note")
seedContactStaffUser(t, db, roleID, "Bravo", intPtrContactRepo(9), false, "br-note")
rows, total, err := repo.ListContacts(context.Background(), contact.ListFilter{})
if err != nil {
t.Fatalf("list contacts: %v", err)
}
if total != 6 || len(rows) != 6 {
t.Fatalf("unexpected total/len total=%d len=%d", total, len(rows))
}
got := []string{rows[0].FirstName, rows[1].FirstName, rows[2].FirstName, rows[3].FirstName, rows[4].FirstName, rows[5].FirstName}
want := []string{"Beta", "Alpha", "Charlie", "Gamma", "Bravo", "Zulu"}
for i := range want {
if got[i] != want[i] {
t.Fatalf("unexpected order got=%v want=%v", got, want)
}
}
}
func TestContactRepositoryListContactsSearchByNote(t *testing.T) {
db := openDBTestSQLite(t)
if err := AutoMigrate(db); err != nil {
t.Fatalf("auto migrate: %v", err)
}
repo := NewContactRepository(db)
roleID := uuidv7.MustBytes()
if err := db.Create(&auth.Role{ID: roleID, Code: auth.RoleCodeStaff, Name: "Staff"}).Error; err != nil {
t.Fatalf("create role: %v", err)
}
seedContactStaffUser(t, db, roleID, "Alpha", nil, true, "dispatcher")
seedContactStaffUser(t, db, roleID, "Bravo", nil, true, "maintenance")
rows, total, err := repo.ListContacts(context.Background(), contact.ListFilter{Search: "dispatch"})
if err != nil {
t.Fatalf("list contacts: %v", err)
}
if total != 1 || len(rows) != 1 {
t.Fatalf("unexpected total/len total=%d len=%d", total, len(rows))
}
if rows[0].FirstName != "Alpha" || rows[0].Note != "dispatcher" {
t.Fatalf("unexpected row %+v", rows[0])
}
}
func TestContactRepositoryListContactsIncludesPilotProfileFields(t *testing.T) {
db := openDBTestSQLite(t)
if err := AutoMigrate(db); err != nil {
t.Fatalf("auto migrate: %v", err)
}
repo := NewContactRepository(db)
roleID := uuidv7.MustBytes()
if err := db.Create(&auth.Role{ID: roleID, Code: auth.RoleCodePilot, Name: "Pilot"}).Error; err != nil {
t.Fatalf("create role: %v", err)
}
userID := uuidv7.MustBytes()
username := "pilot_list_detail"
issuedAt := time.Date(2026, 4, 20, 0, 0, 0, 0, time.UTC)
expiredAt := time.Date(2029, 4, 20, 0, 0, 0, 0, time.UTC)
user := &auth.User{
ID: userID,
Email: "pilot.list@example.com",
Username: &username,
FirstName: "Pilot",
LastName: "List",
Timezone: "UTC",
RoleID: roleID,
IsActive: true,
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
}
if err := db.Create(user).Error; err != nil {
t.Fatalf("create user: %v", err)
}
if err := db.Create(&auth.PilotProfile{
UserID: userID,
PilotCategory: auth.PilotCategoryFreelance,
LicenseIssuedAt: &issuedAt,
LicenseExpiredAt: &expiredAt,
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
}).Error; err != nil {
t.Fatalf("create pilot profile: %v", err)
}
rows, total, err := repo.ListContacts(context.Background(), contact.ListFilter{Role: auth.RoleCodePilot})
if err != nil {
t.Fatalf("list contacts: %v", err)
}
if total != 1 || len(rows) != 1 {
t.Fatalf("unexpected total/len total=%d len=%d", total, len(rows))
}
if rows[0].PilotLicenseIssuedAt == nil || !rows[0].PilotLicenseIssuedAt.Equal(issuedAt) {
t.Fatalf("expected pilot license_issued_at returned, got %#v", rows[0].PilotLicenseIssuedAt)
}
if rows[0].PilotLicenseExpiredAt == nil || !rows[0].PilotLicenseExpiredAt.Equal(expiredAt) {
t.Fatalf("expected pilot license_expired_at returned, got %#v", rows[0].PilotLicenseExpiredAt)
}
if rows[0].PilotCategory != auth.PilotCategoryFreelance {
t.Fatalf("expected pilot category returned, got %q", rows[0].PilotCategory)
}
}
func TestContactRepositoryUpdateContactRoleAssignsRoleWhenCurrentRoleIsEmpty(t *testing.T) {
db := openDBTestSQLite(t)
if err := AutoMigrate(db); err != nil {
t.Fatalf("auto migrate: %v", err)
}
repo := NewContactRepository(db)
staffRoleID := uuidv7.MustBytes()
if err := db.Create(&auth.Role{ID: staffRoleID, Code: auth.RoleCodeStaff, Name: "Staff"}).Error; err != nil {
t.Fatalf("create role: %v", err)
}
userID := uuidv7.MustBytes()
user := &auth.User{
ID: userID,
Email: "no-role@example.com",
FirstName: "No",
LastName: "Role",
Timezone: "UTC",
IsActive: true,
}
if err := db.Create(user).Error; err != nil {
t.Fatalf("create user: %v", err)
}
if err := repo.UpdateContactRole(context.Background(), userID, auth.RoleCodeStaff); err != nil {
t.Fatalf("update contact role: %v", err)
}
var refreshed auth.User
if err := db.Where("id = ?", userID).Take(&refreshed).Error; err != nil {
t.Fatalf("reload user: %v", err)
}
if len(refreshed.RoleID) != 16 || string(refreshed.RoleID) != string(staffRoleID) {
t.Fatalf("expected role_id updated to staff, got %x", refreshed.RoleID)
}
roleIDs, err := listUserRoleIDsTx(db, userID)
if err != nil {
t.Fatalf("list user roles: %v", err)
}
if len(roleIDs) != 1 || string(roleIDs[0]) != string(staffRoleID) {
t.Fatalf("expected user_roles to contain only staff role, got %d rows", len(roleIDs))
}
}
func TestContactRepositoryUpdateContact_IsAdminTruePersistsAdminRoleWhenUpdatingRoles(t *testing.T) {
db := openDBTestSQLite(t)
if err := AutoMigrate(db); err != nil {
t.Fatalf("auto migrate: %v", err)
}
repo := NewContactRepository(db)
adminRoleID := uuidv7.MustBytes()
pilotRoleID := uuidv7.MustBytes()
if err := db.Create(&auth.Role{ID: adminRoleID, Code: "admin", Name: "Admin"}).Error; err != nil {
t.Fatalf("create admin role: %v", err)
}
if err := db.Create(&auth.Role{ID: pilotRoleID, Code: auth.RoleCodePilot, Name: "Pilot"}).Error; err != nil {
t.Fatalf("create pilot role: %v", err)
}
user := &auth.User{
ID: uuidv7.MustBytes(),
Email: "admin-sync@example.com",
FirstName: "Admin",
LastName: "Sync",
Timezone: "UTC",
RoleID: pilotRoleID,
IsActive: true,
}
if err := db.Create(user).Error; err != nil {
t.Fatalf("create user: %v", err)
}
if err := replaceUserRolesTx(context.Background(), db, user.ID, pilotRoleID, [][]byte{pilotRoleID}); err != nil {
t.Fatalf("seed user roles: %v", err)
}
trueVal := true
if err := repo.UpdateContact(context.Background(), contact.UpdateInput{
UserID: user.ID,
UpdateRoles: true,
RoleID: pilotRoleID,
RoleIDs: [][]byte{pilotRoleID},
User: contact.UserPatch{
IsAdmin: &trueVal,
},
}); err != nil {
t.Fatalf("update contact: %v", err)
}
roleIDs, err := listUserRoleIDsTx(db, user.ID)
if err != nil {
t.Fatalf("list user roles: %v", err)
}
if len(roleIDs) != 2 {
t.Fatalf("expected 2 roles (pilot + admin), got %d", len(roleIDs))
}
foundAdmin := false
for i := range roleIDs {
if string(roleIDs[i]) == string(adminRoleID) {
foundAdmin = true
break
}
}
if !foundAdmin {
t.Fatalf("expected admin role assigned")
}
}
func TestContactRepositoryUpdateContact_StaffCanBeAdmin(t *testing.T) {
db := openDBTestSQLite(t)
if err := AutoMigrate(db); err != nil {
t.Fatalf("auto migrate: %v", err)
}
repo := NewContactRepository(db)
adminRoleID := uuidv7.MustBytes()
staffRoleID := uuidv7.MustBytes()
if err := db.Create(&auth.Role{ID: adminRoleID, Code: "admin", Name: "Admin"}).Error; err != nil {
t.Fatalf("create admin role: %v", err)
}
if err := db.Create(&auth.Role{ID: staffRoleID, Code: auth.RoleCodeStaff, Name: "Staff"}).Error; err != nil {
t.Fatalf("create staff role: %v", err)
}
user := &auth.User{
ID: uuidv7.MustBytes(),
Email: "staff-admin@example.com",
FirstName: "Staff",
LastName: "Admin",
Timezone: "UTC",
RoleID: staffRoleID,
IsActive: true,
}
if err := db.Create(user).Error; err != nil {
t.Fatalf("create user: %v", err)
}
if err := replaceUserRolesTx(context.Background(), db, user.ID, staffRoleID, [][]byte{staffRoleID}); err != nil {
t.Fatalf("seed user roles: %v", err)
}
trueVal := true
if err := repo.UpdateContact(context.Background(), contact.UpdateInput{
UserID: user.ID,
User: contact.UserPatch{
IsAdmin: &trueVal,
},
}); err != nil {
t.Fatalf("update contact: %v", err)
}
roleIDs, err := listUserRoleIDsTx(db, user.ID)
if err != nil {
t.Fatalf("list user roles: %v", err)
}
if len(roleIDs) != 2 {
t.Fatalf("expected 2 roles (staff + admin), got %d", len(roleIDs))
}
foundAdmin := false
for i := range roleIDs {
if string(roleIDs[i]) == string(adminRoleID) {
foundAdmin = true
break
}
}
if !foundAdmin {
t.Fatalf("expected admin role assigned")
}
}
func TestContactRepositoryUpdateContactNilSortKeyDoesNotClearWithoutSetFlag(t *testing.T) {
db := openDBTestSQLite(t)
if err := AutoMigrate(db); err != nil {
t.Fatalf("auto migrate: %v", err)
}
repo := NewContactRepository(db)
roleID := uuidv7.MustBytes()
if err := db.Create(&auth.Role{ID: roleID, Code: auth.RoleCodeStaff, Name: "Staff"}).Error; err != nil {
t.Fatalf("create role: %v", err)
}
sortKey := 7
user := &auth.User{
ID: uuidv7.MustBytes(),
Email: "sortkey-clear@example.com",
FirstName: "Sort",
LastName: "Key",
Timezone: "UTC",
RoleID: roleID,
SortKey: &sortKey,
IsActive: true,
}
if err := db.Create(user).Error; err != nil {
t.Fatalf("create user: %v", err)
}
firstName := "Sorted"
if err := repo.UpdateContact(context.Background(), contact.UpdateInput{
UserID: user.ID,
User: contact.UserPatch{
FirstName: &firstName,
SortKey: nil,
SortKeySet: false,
},
}); err != nil {
t.Fatalf("update contact: %v", err)
}
var refreshed auth.User
if err := db.Where("id = ?", user.ID).Take(&refreshed).Error; err != nil {
t.Fatalf("reload user: %v", err)
}
if refreshed.SortKey == nil || *refreshed.SortKey != 7 {
t.Fatalf("expected sortkey to remain 7, got %#v", refreshed.SortKey)
}
}
func TestContactRepositoryUpdateContactNullSortKeyClearsValue(t *testing.T) {
db := openDBTestSQLite(t)
if err := AutoMigrate(db); err != nil {
t.Fatalf("auto migrate: %v", err)
}
repo := NewContactRepository(db)
roleID := uuidv7.MustBytes()
if err := db.Create(&auth.Role{ID: roleID, Code: auth.RoleCodeStaff, Name: "Staff"}).Error; err != nil {
t.Fatalf("create role: %v", err)
}
sortKey := 7
user := &auth.User{
ID: uuidv7.MustBytes(),
Email: "sortkey-null@example.com",
FirstName: "Sort",
LastName: "Key",
Timezone: "UTC",
RoleID: roleID,
SortKey: &sortKey,
IsActive: true,
}
if err := db.Create(user).Error; err != nil {
t.Fatalf("create user: %v", err)
}
if err := repo.UpdateContact(context.Background(), contact.UpdateInput{
UserID: user.ID,
User: contact.UserPatch{
SortKey: nil,
SortKeySet: true,
},
}); err != nil {
t.Fatalf("update contact: %v", err)
}
var refreshed auth.User
if err := db.Where("id = ?", user.ID).Take(&refreshed).Error; err != nil {
t.Fatalf("reload user: %v", err)
}
if refreshed.SortKey != nil {
t.Fatalf("expected sortkey NULL, got %d", *refreshed.SortKey)
}
}
func seedContactStaffUser(t *testing.T, db *gorm.DB, roleID []byte, firstName string, sortKey *int, isActive bool, note string) []byte {
t.Helper()
userID := uuidv7.MustBytes()
username := fmt.Sprintf("%s_user_%d", firstName, time.Now().UnixNano())
user := &auth.User{
ID: userID,
Email: fmt.Sprintf("%s@example.com", username),
Username: &username,
FirstName: firstName,
LastName: "Contact",
Timezone: "UTC",
RoleID: roleID,
SortKey: sortKey,
IsActive: isActive,
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
}
if err := db.Create(user).Error; err != nil {
t.Fatalf("create user: %v", err)
}
if err := db.Model(&auth.User{}).Where("id = ?", userID).UpdateColumn("is_active", isActive).Error; err != nil {
t.Fatalf("sync user is_active: %v", err)
}
if err := db.Create(&auth.StaffProfile{
UserID: userID,
ShortName: firstName,
Note: note,
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
}).Error; err != nil {
t.Fatalf("create staff profile: %v", err)
}
return userID
}
func intPtrContactRepo(v int) *int { return &v }
func strPtrContactRepo(v string) *string { return &v }