968 lines
28 KiB
Go
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 }
|