Files
fm_be/internal/transport/http/handlers/contact_handler_test.go
2026-07-16 22:16:45 +07:00

1842 lines
68 KiB
Go

package handlers
import (
"bytes"
"context"
"encoding/hex"
"errors"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
"wucher/internal/domain/contact"
filemanager "wucher/internal/domain/file_manager"
"wucher/internal/shared/pkg/uuidv7"
"wucher/internal/transport/http/dto"
)
type stubContactService struct {
listItems []contact.ContactListItem
listTotal int64
listErr error
lastFilter contact.ListFilter
getItem *contact.ContactListItem
getErr error
resolveRoleCode string
resolveRoleCodeByID map[string]string
resolveRoleErr error
lastResolveID []byte
createUserID []byte
createErr error
lastCreate contact.CreateInput
updateErr error
lastUpdate contact.UpdateInput
deleteErr error
lastDeleteID []byte
updateStatusErr error
lastUpdateStatusID []byte
lastIsActive bool
updateRoleErr error
lastUpdateRoleID []byte
lastRoleCode string
updatePilotCategoryErr error
lastUpdatePilotID []byte
lastPilotCategory string
updateLicenseErr error
lastUpdateLicenseID []byte
lastLicenseNo *string
updateChiefFlagsErr error
lastUpdateChiefFlagsID []byte
lastChiefFlags contact.ProfilePatch
updateResponsibleErr error
lastUpdateResponsibleID []byte
lastResponsibleComplaints bool
bulkUpdateErr error
bulkUpdateCount int64
lastBulkIDs [][]byte
lastBulkIsActive bool
checkSensitiveActionCalled bool
isActorAdmin bool
isActorAdminErr error
}
func (s *stubContactService) ListContacts(_ context.Context, filter contact.ListFilter) ([]contact.ContactListItem, int64, error) {
s.lastFilter = filter
if s.listErr != nil {
return nil, 0, s.listErr
}
return s.listItems, s.listTotal, nil
}
func (s *stubContactService) GetContactByID(_ context.Context, userID []byte) (*contact.ContactListItem, error) {
if s.getErr != nil {
return nil, s.getErr
}
if s.getItem != nil && len(s.getItem.UserID) == 0 {
copyItem := *s.getItem
copyItem.UserID = append([]byte(nil), userID...)
return &copyItem, nil
}
return s.getItem, nil
}
func (s *stubContactService) IsActorAdmin(_ context.Context) (bool, error) {
if s.isActorAdminErr != nil {
return false, s.isActorAdminErr
}
return s.isActorAdmin, nil
}
func (s *stubContactService) ResolveRoleCodeByID(_ context.Context, roleID []byte) (string, error) {
s.lastResolveID = append([]byte(nil), roleID...)
if s.resolveRoleErr != nil {
return "", s.resolveRoleErr
}
if s.resolveRoleCodeByID != nil {
if code, ok := s.resolveRoleCodeByID[string(roleID)]; ok {
return code, nil
}
}
return s.resolveRoleCode, nil
}
func (s *stubContactService) CreateContact(_ context.Context, in contact.CreateInput) ([]byte, error) {
s.lastCreate = in
if s.createErr != nil {
return nil, s.createErr
}
if len(s.createUserID) == 0 {
s.createUserID = uuidv7.MustBytes()
}
return s.createUserID, nil
}
func (s *stubContactService) UpdateContact(_ context.Context, in contact.UpdateInput) error {
s.lastUpdate = in
return s.updateErr
}
func (s *stubContactService) DeleteContact(_ context.Context, userID []byte) error {
s.lastDeleteID = append([]byte(nil), userID...)
return s.deleteErr
}
func (s *stubContactService) UpdateContactStatus(_ context.Context, userID []byte, isActive bool) error {
s.lastUpdateStatusID = append([]byte(nil), userID...)
s.lastIsActive = isActive
return s.updateStatusErr
}
func (s *stubContactService) UpdateContactRole(_ context.Context, userID []byte, roleCode string) error {
s.lastUpdateRoleID = append([]byte(nil), userID...)
s.lastRoleCode = roleCode
return s.updateRoleErr
}
func (s *stubContactService) UpdatePilotCategory(_ context.Context, userID []byte, category string) error {
s.lastUpdatePilotID = append([]byte(nil), userID...)
s.lastPilotCategory = category
return s.updatePilotCategoryErr
}
func (s *stubContactService) UpdateLicense(_ context.Context, userID []byte, licenseNo *string) error {
s.lastUpdateLicenseID = append([]byte(nil), userID...)
s.lastLicenseNo = licenseNo
return s.updateLicenseErr
}
func (s *stubContactService) UpdateChiefFlags(_ context.Context, userID []byte, patch contact.ProfilePatch) error {
s.lastUpdateChiefFlagsID = append([]byte(nil), userID...)
s.lastChiefFlags = patch
return s.updateChiefFlagsErr
}
func (s *stubContactService) UpdateResponsibleComplaints(_ context.Context, userID []byte, value bool) error {
s.lastUpdateResponsibleID = append([]byte(nil), userID...)
s.lastResponsibleComplaints = value
return s.updateResponsibleErr
}
func (s *stubContactService) BulkUpdateStatus(_ context.Context, userIDs [][]byte, isActive bool) (int64, error) {
s.lastBulkIDs = append([][]byte(nil), userIDs...)
s.lastBulkIsActive = isActive
if s.bulkUpdateErr != nil {
return 0, s.bulkUpdateErr
}
return s.bulkUpdateCount, nil
}
func (s *stubContactService) CheckSensitiveAction(_ context.Context, _ []byte, _ string, _ string) error {
s.checkSensitiveActionCalled = true
return nil
}
type stubContactInviter struct {
err error
lastUserID []byte
}
func (s *stubContactInviter) SendInviteSetPasswordForUser(_ context.Context, userID []byte) error {
s.lastUserID = append([]byte(nil), userID...)
return s.err
}
func newContactTestApp(h *ContactHandler) *fiber.App {
app := fiber.New()
app.Post("/contacts/create", h.Create)
app.Get("/contacts", h.List)
app.Get("/contacts/dt", h.ListDatatable)
app.Get("/contacts/:user_id", h.Get)
app.Patch("/contacts/:user_id", h.Update)
app.Delete("/contacts/:user_id", h.Delete)
app.Patch("/contacts/:user_id/status", h.UpdateContactStatus)
app.Patch("/contacts/:user_id/role", h.UpdateContactRole)
app.Patch("/contacts/:user_id/pilot-category", h.UpdatePilotCategory)
app.Patch("/contacts/:user_id/license", h.UpdateLicense)
app.Patch("/contacts/:user_id/chief-flags", h.UpdateChiefFlags)
app.Post("/contacts/bulk-update", h.BulkUpdate)
return app
}
func doJSONRequestContact(t *testing.T, app *fiber.App, method, path string, body []byte) (*http.Response, []byte) {
t.Helper()
req, err := http.NewRequest(method, path, bytes.NewReader(body))
if err != nil {
t.Fatalf("new request: %v", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
if err != nil {
t.Fatalf("app.Test: %v", err)
}
respBody, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("read body: %v", err)
}
return resp, respBody
}
func mustUUIDStringContact(t *testing.T, id []byte) string {
t.Helper()
s, err := uuidv7.BytesToString(id)
if err != nil {
t.Fatalf("uuid to string: %v", err)
}
return s
}
func mustRoleRawContact(t *testing.T, roleID []byte, roleName, roleCode string) string {
t.Helper()
return strings.ToLower(hex.EncodeToString(roleID)) + "|" +
strings.ToLower(hex.EncodeToString([]byte(roleName))) + "|" + roleCode
}
func strPtrContact(v string) *string { return &v }
func boolPtrContact(v bool) *bool { return &v }
func intPtrContact(v int) *int { return &v }
func floatPtrContact(v float64) *float64 {
return &v
}
func TestNewContactHandler(t *testing.T) {
h := NewContactHandler(&stubContactService{}, &stubContactInviter{})
if h == nil || h.validate == nil {
t.Fatalf("expected handler and validator initialized")
}
h = h.WithFileManagerService(nil).WithFileStorage(nil)
if h == nil {
t.Fatalf("expected chain methods return handler")
}
}
func TestContactHandlerCreate(t *testing.T) {
t.Run("invalid json", func(t *testing.T) {
app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
resp, _ := doJSONRequestContact(t, app, http.MethodPost, "/contacts/create", []byte(`{"data":`))
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", resp.StatusCode)
}
})
t.Run("validation error", func(t *testing.T) {
app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
resp, _ := doJSONRequestContact(t, app, http.MethodPost, "/contacts/create", []byte(`{}`))
if resp.StatusCode != http.StatusUnprocessableEntity {
t.Fatalf("expected 422, got %d", resp.StatusCode)
}
})
t.Run("invalid role id", func(t *testing.T) {
app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
body := []byte(`{"data":{"type":"contact_create","attributes":{"role_id":"bad","email":"user@example.com"}}}`)
resp, _ := doJSONRequestContact(t, app, http.MethodPost, "/contacts/create", body)
if resp.StatusCode != http.StatusUnprocessableEntity {
t.Fatalf("expected 422, got %d", resp.StatusCode)
}
})
t.Run("invalid create type", func(t *testing.T) {
svc := &stubContactService{resolveRoleCode: "doctor"}
app := newContactTestApp(NewContactHandler(svc, nil))
roleID := mustUUIDStringContact(t, uuidv7.MustBytes())
body := []byte(`{"data":{"type":"contact_pilot_create","attributes":{"role_id":"` + roleID + `","email":"user@example.com"}}}`)
resp, _ := doJSONRequestContact(t, app, http.MethodPost, "/contacts/create", body)
if resp.StatusCode != http.StatusUnprocessableEntity {
t.Fatalf("expected 422, got %d", resp.StatusCode)
}
})
t.Run("mismatched profile payload is rejected", func(t *testing.T) {
svc := &stubContactService{resolveRoleCode: "pilot"}
app := newContactTestApp(NewContactHandler(svc, nil))
roleID := mustUUIDStringContact(t, uuidv7.MustBytes())
body := []byte(`{"data":{"type":"contact_create","attributes":{"role_id":"` + roleID + `","email":"user@example.com","doctor":{"short_name":"Dr"}}}}`)
resp, _ := doJSONRequestContact(t, app, http.MethodPost, "/contacts/create", body)
if resp.StatusCode != http.StatusUnprocessableEntity {
t.Fatalf("expected 422, got %d", resp.StatusCode)
}
})
t.Run("create error", func(t *testing.T) {
svc := &stubContactService{resolveRoleCode: "pilot", createErr: errors.New("create failed")}
app := newContactTestApp(NewContactHandler(svc, nil))
roleID := mustUUIDStringContact(t, uuidv7.MustBytes())
body := []byte(`{"data":{"type":"contact_create","attributes":{"role_id":"` + roleID + `","email":"user@example.com","username":"john","pilot":{"pilot_category":"regular"}}}}`)
resp, _ := doJSONRequestContact(t, app, http.MethodPost, "/contacts/create", body)
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", resp.StatusCode)
}
})
t.Run("success with invite and fetched item", func(t *testing.T) {
roleID := mustUUIDStringContact(t, uuidv7.MustBytes())
userID := uuidv7.MustBytes()
svc := &stubContactService{
resolveRoleCode: "pilot",
createUserID: userID,
getItem: &contact.ContactListItem{
RoleCode: "pilot",
RoleName: "Pilot",
PilotCategory: "regular",
Username: " pilot.user ",
Email: "pilot@example.com",
FirstName: "John",
LastName: "Doe",
IsActive: true,
PilotShortName: strPtrContact("PJ"),
},
}
inviter := &stubContactInviter{}
app := newContactTestApp(NewContactHandler(svc, inviter))
body := []byte(`{"data":{"type":"contact_create","attributes":{"role_id":"` + roleID + `","email":"pilot@example.com","username":"pilot.user","first_name":" John ","last_name":" Doe ","mobile_phone":" 081 ","timezone":" Asia/Jakarta ","is_active":true,"pilot":{"pilot_category":"regular","short_name":" PJ ","license_no":" LIC-1 "}}}}`)
resp, _ := doJSONRequestContact(t, app, http.MethodPost, "/contacts/create", body)
if resp.StatusCode != http.StatusCreated {
t.Fatalf("expected 201, got %d", resp.StatusCode)
}
if svc.lastCreate.User.Email == nil || *svc.lastCreate.User.Email != "pilot@example.com" {
t.Fatalf("expected trimmed email, got %#v", svc.lastCreate.User.Email)
}
if svc.lastCreate.Profile.PilotCategory == nil || *svc.lastCreate.Profile.PilotCategory != "regular" {
t.Fatalf("expected pilot category patched")
}
if string(inviter.lastUserID) != string(userID) {
t.Fatalf("expected inviter called with created user id")
}
})
t.Run("success when invite fails and refetch nil", func(t *testing.T) {
roleID := mustUUIDStringContact(t, uuidv7.MustBytes())
userID := uuidv7.MustBytes()
svc := &stubContactService{resolveRoleCode: "staff", createUserID: userID}
inviter := &stubContactInviter{err: errors.New("ses down")}
app := newContactTestApp(NewContactHandler(svc, inviter))
body := []byte(`{"data":{"type":"contact_create","attributes":{"role_id":"` + roleID + `","email":"user@example.com","staff":{"short_name":"Dina"}}}}`)
resp, _ := doJSONRequestContact(t, app, http.MethodPost, "/contacts/create", body)
if resp.StatusCode != http.StatusCreated {
t.Fatalf("expected 201, got %d", resp.StatusCode)
}
})
t.Run("get after create error", func(t *testing.T) {
roleID := mustUUIDStringContact(t, uuidv7.MustBytes())
svc := &stubContactService{resolveRoleCode: "staff", createUserID: uuidv7.MustBytes(), getErr: errors.New("lookup failed")}
app := newContactTestApp(NewContactHandler(svc, nil))
body := []byte(`{"data":{"type":"contact_create","attributes":{"role_id":"` + roleID + `","email":"user@example.com","staff":{"short_name":"Dina"}}}}`)
resp, _ := doJSONRequestContact(t, app, http.MethodPost, "/contacts/create", body)
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", resp.StatusCode)
}
})
}
func TestContactHelperFunctions(t *testing.T) {
t.Run("contactBaseRoles and contactDULBases", func(t *testing.T) {
baseID := uuidv7.MustBytes()
baseHex := strings.ToLower(hex.EncodeToString(baseID))
nameHex := strings.ToLower(hex.EncodeToString([]byte("Halim Base")))
catHex := strings.ToLower(hex.EncodeToString([]byte("hems")))
raw := baseHex + "|" + nameHex + "|dul|" + catHex + "," + baseHex + "|" + nameHex + "|pilot|" + catHex
roles := contactBaseRoles(&raw)
if len(roles) != 2 {
t.Fatalf("expected 2 base roles, got %d", len(roles))
}
dulBases := contactDULBases(&raw)
if len(dulBases) != 1 {
t.Fatalf("expected only dul role base, got %d", len(dulBases))
}
if dulBases[0]["category"] != "hems" {
t.Fatalf("expected dul base category hems")
}
invalid := "invalid"
if got := contactBaseRoles(&invalid); got != nil {
t.Fatalf("invalid raw should return nil")
}
if got := contactDULBases(&invalid); got != nil {
t.Fatalf("invalid raw should return nil dul bases")
}
})
t.Run("contactRoles", func(t *testing.T) {
roleID := uuidv7.MustBytes()
roleRaw := strings.ToLower(hex.EncodeToString(roleID)) + "|" +
strings.ToLower(hex.EncodeToString([]byte("Pilot"))) + "|pilot"
out := contactRoles(&roleRaw)
if len(out) != 1 || out[0]["role_code"] != "pilot" {
t.Fatalf("expected pilot role parsed")
}
blank := " "
if got := contactRoles(&blank); len(got) != 0 {
t.Fatalf("blank raw should return empty slice")
}
bad := "abc"
if got := contactRoles(&bad); len(got) != 0 {
t.Fatalf("bad raw should return empty slice")
}
})
t.Run("applyContactFoto no attachment id", func(t *testing.T) {
h := NewContactHandler(&stubContactService{}, nil)
attrs := map[string]any{}
h.applyContactFoto(context.Background(), &contact.ContactListItem{}, attrs)
if _, ok := attrs["foto"]; ok {
t.Fatalf("foto should not be set without attachment id")
}
})
t.Run("resolveContactProfileAttachmentID and applyContactFoto success path", func(t *testing.T) {
raw := mustUUIDStringContact(t, uuidv7.MustBytes())
profileID := uuidv7.MustBytes()
thumb := "thumb-key"
h := NewContactHandler(&stubContactService{}, nil).
WithFileManagerService(&fileManagerAttachmentHandlerServiceMock{
createAttachmentFn: func(context.Context, filemanager.CreateAttachmentParams) (*filemanager.Attachment, error) {
return &filemanager.Attachment{ID: profileID}, nil
},
getAttachmentFn: func(context.Context, []byte) (*filemanager.Attachment, error) {
return &filemanager.Attachment{
ID: profileID,
File: &filemanager.File{
ObjectKey: "photo-key",
SizeBytes: 100,
ThumbnailObjectKey: &thumb,
ThumbnailSizeBytes: 10,
},
}, nil
},
}).
WithFileStorage(&basePresignStorageMock{url: "https://example.com/signed"})
app := fiber.New()
app.Get("/x", func(c *fiber.Ctx) error {
c.Locals("user_id", uuidv7.MustBytes())
id, handled := h.resolveContactProfileAttachmentID(c, uuidv7.MustBytes(), &raw)
if handled || len(id) != 16 {
t.Fatalf("expected resolved attachment id")
}
return nil
})
resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/x", nil))
if err != nil || resp.StatusCode != http.StatusOK {
t.Fatalf("unexpected resolveContactProfileAttachmentID result")
}
attrs := map[string]any{}
h.applyContactFoto(context.Background(), &contact.ContactListItem{ProfileAttachmentID: profileID}, attrs)
foto, ok := attrs["foto"].(map[string]any)
if !ok || strings.TrimSpace(foto["download_url"].(string)) == "" || strings.TrimSpace(foto["thumbnail_download_url"].(string)) == "" {
t.Fatalf("expected foto urls to be set")
}
})
t.Run("resolveContactProfileAttachmentID invalid uuid", func(t *testing.T) {
raw := "bad"
h := NewContactHandler(&stubContactService{}, nil).WithFileManagerService(&fileManagerAttachmentHandlerServiceMock{})
app := fiber.New()
app.Get("/x", func(c *fiber.Ctx) error {
_, handled := h.resolveContactProfileAttachmentID(c, uuidv7.MustBytes(), &raw)
if !handled {
t.Fatalf("expected handled invalid uuid")
}
return nil
})
resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/x", nil))
if err != nil || resp.StatusCode != http.StatusUnprocessableEntity {
t.Fatalf("expected 422 for invalid uuid")
}
})
t.Run("ensureContactProfileAttachmentSet error when missing profile id", func(t *testing.T) {
svc := &stubContactService{getItem: &contact.ContactListItem{UserID: uuidv7.MustBytes()}}
h := NewContactHandler(svc, nil)
if err := h.ensureContactProfileAttachmentSet(context.Background(), uuidv7.MustBytes()); err == nil {
t.Fatalf("expected error when profile attachment id is missing")
}
})
t.Run("inferContactCreateRoleCodes", func(t *testing.T) {
codes := inferContactCreateRoleCodes(dto.ContactCreateAttributes{
Pilot: &dto.PilotProfileAttributes{},
Staff: &dto.StaffProfileAttributes{},
})
if len(codes) != 2 {
t.Fatalf("expected inferred role codes from payloads, got %d", len(codes))
}
})
t.Run("validatePilotDULBasePayload", func(t *testing.T) {
errs := validatePilotDULBasePayload(&dto.PilotProfileAttributes{
DULBaseIDs: []string{"bad-uuid"},
})
if len(errs) == 0 {
t.Fatalf("expected validation error for invalid dul_base_ids")
}
errs = validatePilotDULBasePayload(&dto.PilotProfileAttributes{
DULBase: []dto.DULBase{{ID: "bad-uuid"}},
})
if len(errs) == 0 {
t.Fatalf("expected validation error for invalid dul_base")
}
errs = validatePilotDULBasePayload(&dto.PilotProfileAttributes{
DULBaseIDs: []string{mustUUIDStringContact(t, uuidv7.MustBytes())},
DULBase: []dto.DULBase{{ID: mustUUIDStringContact(t, uuidv7.MustBytes())}},
})
if len(errs) == 0 {
t.Fatalf("expected validation error when both dul_base_ids and dul_base are provided")
}
})
}
func TestContactHandlerList(t *testing.T) {
t.Run("success with normalized pagination", func(t *testing.T) {
userID := uuidv7.MustBytes()
svc := &stubContactService{
listItems: []contact.ContactListItem{{
UserID: userID,
RoleCode: "staff",
RoleName: "Staff",
Email: "staff@example.com",
FirstName: "Jane",
LastName: "Doe",
IsActive: true,
}},
listTotal: 1,
}
app := newContactTestApp(NewContactHandler(svc, nil))
resp, _ := doJSONRequestContact(t, app, http.MethodGet, "/contacts?role=staff&pilot_category=regular&search=jane&sort=-sortkey&limit=500&offset=-4", nil)
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
if svc.lastFilter.Limit != 0 || svc.lastFilter.Offset != 0 {
t.Fatalf("expected normalized pagination, got %#v", svc.lastFilter)
}
if svc.lastFilter.Sort != "-sortkey" {
t.Fatalf("expected sort forwarded, got %#v", svc.lastFilter)
}
})
t.Run("supports role alias and special flag filters", func(t *testing.T) {
svc := &stubContactService{listTotal: 0}
app := newContactTestApp(NewContactHandler(svc, nil))
resp, _ := doJSONRequestContact(
t,
app,
http.MethodGet,
"/contacts?role=technicians&chief_doctor=true&chief_technician=1&responsible_complaint=yes&chief_pilot=true&ifr_qualified=true",
nil,
)
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
if svc.lastFilter.Role != "technician" {
t.Fatalf("expected role alias normalized, got %q", svc.lastFilter.Role)
}
if svc.lastFilter.ChiefDoctor == nil || !*svc.lastFilter.ChiefDoctor {
t.Fatalf("expected chief_doctor=true")
}
if svc.lastFilter.ChiefTechnician == nil || !*svc.lastFilter.ChiefTechnician {
t.Fatalf("expected chief_technician=true")
}
if svc.lastFilter.ResponsibleComplaint == nil || !*svc.lastFilter.ResponsibleComplaint {
t.Fatalf("expected responsible_complaint=true")
}
if svc.lastFilter.ChiefPilot == nil || !*svc.lastFilter.ChiefPilot {
t.Fatalf("expected chief_pilot=true")
}
if svc.lastFilter.IFRQualified == nil || !*svc.lastFilter.IFRQualified {
t.Fatalf("expected ifr_qualified=true")
}
})
t.Run("list error", func(t *testing.T) {
svc := &stubContactService{listErr: errors.New("list failed")}
app := newContactTestApp(NewContactHandler(svc, nil))
resp, _ := doJSONRequestContact(t, app, http.MethodGet, "/contacts", nil)
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", resp.StatusCode)
}
})
}
func TestContactHandlerListDatatable(t *testing.T) {
t.Run("success with page size fallback", func(t *testing.T) {
svc := &stubContactService{listTotal: 2}
app := newContactTestApp(NewContactHandler(svc, nil))
resp, _ := doJSONRequestContact(t, app, http.MethodGet, "/contacts/dt?page=0&size=0&draw=9&role=pilot&sort=note", nil)
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
if svc.lastFilter.Limit != 20 || svc.lastFilter.Offset != 0 || svc.lastFilter.Role != "pilot" {
t.Fatalf("unexpected filter %#v", svc.lastFilter)
}
if svc.lastFilter.Sort != "note" {
t.Fatalf("expected sort forwarded, got %#v", svc.lastFilter)
}
})
t.Run("supports special flag filters", func(t *testing.T) {
svc := &stubContactService{listTotal: 0}
app := newContactTestApp(NewContactHandler(svc, nil))
resp, _ := doJSONRequestContact(
t,
app,
http.MethodGet,
"/contacts/dt?chief_doctor=false&chief_technician=0&responsible_complaint=no&chief_pilot=false&ifr_qualified=false",
nil,
)
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
if svc.lastFilter.ChiefDoctor == nil || *svc.lastFilter.ChiefDoctor {
t.Fatalf("expected chief_doctor=false")
}
if svc.lastFilter.ChiefTechnician == nil || *svc.lastFilter.ChiefTechnician {
t.Fatalf("expected chief_technician=false")
}
if svc.lastFilter.ResponsibleComplaint == nil || *svc.lastFilter.ResponsibleComplaint {
t.Fatalf("expected responsible_complaint=false")
}
if svc.lastFilter.ChiefPilot == nil || *svc.lastFilter.ChiefPilot {
t.Fatalf("expected chief_pilot=false")
}
if svc.lastFilter.IFRQualified == nil || *svc.lastFilter.IFRQualified {
t.Fatalf("expected ifr_qualified=false")
}
})
t.Run("list error", func(t *testing.T) {
svc := &stubContactService{listErr: errors.New("datatable failed")}
app := newContactTestApp(NewContactHandler(svc, nil))
resp, _ := doJSONRequestContact(t, app, http.MethodGet, "/contacts/dt", nil)
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", resp.StatusCode)
}
})
}
func TestContactHandlerGetAndWriteByID(t *testing.T) {
t.Run("invalid uuid", func(t *testing.T) {
app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
resp, _ := doJSONRequestContact(t, app, http.MethodGet, "/contacts/not-uuid", nil)
if resp.StatusCode != http.StatusUnprocessableEntity {
t.Fatalf("expected 422, got %d", resp.StatusCode)
}
})
t.Run("service error", func(t *testing.T) {
id := mustUUIDStringContact(t, uuidv7.MustBytes())
svc := &stubContactService{getErr: errors.New("get failed")}
app := newContactTestApp(NewContactHandler(svc, nil))
resp, _ := doJSONRequestContact(t, app, http.MethodGet, "/contacts/"+id, nil)
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", resp.StatusCode)
}
})
t.Run("not found", func(t *testing.T) {
id := mustUUIDStringContact(t, uuidv7.MustBytes())
app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
resp, _ := doJSONRequestContact(t, app, http.MethodGet, "/contacts/"+id, nil)
if resp.StatusCode != http.StatusNotFound {
t.Fatalf("expected 404, got %d", resp.StatusCode)
}
})
t.Run("success", func(t *testing.T) {
id := mustUUIDStringContact(t, uuidv7.MustBytes())
svc := &stubContactService{getItem: &contact.ContactListItem{
RoleCode: "doctor",
RoleName: "Doctor",
Email: "doctor@example.com",
FirstName: "Doc",
LastName: "Tor",
IsActive: true,
}}
app := newContactTestApp(NewContactHandler(svc, nil))
resp, _ := doJSONRequestContact(t, app, http.MethodGet, "/contacts/"+id, nil)
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
})
}
func TestContactHandlerUpdate(t *testing.T) {
userID := uuidv7.MustBytes()
id := mustUUIDStringContact(t, userID)
t.Run("invalid uuid", func(t *testing.T) {
app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/not-uuid", []byte(`{}`))
if resp.StatusCode != http.StatusUnprocessableEntity {
t.Fatalf("expected 422, got %d", resp.StatusCode)
}
})
t.Run("invalid json", func(t *testing.T) {
app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id, []byte(`{"data":`))
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", resp.StatusCode)
}
})
t.Run("validation error", func(t *testing.T) {
app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id, []byte(`{}`))
if resp.StatusCode != http.StatusUnprocessableEntity {
t.Fatalf("expected 422, got %d", resp.StatusCode)
}
})
t.Run("not found", func(t *testing.T) {
svc := &stubContactService{}
app := newContactTestApp(NewContactHandler(svc, nil))
body := []byte(`{"data":{"type":"contact_update","attributes":{"first_name":"Jane"}}}`)
resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id, body)
if resp.StatusCode != http.StatusNotFound {
t.Fatalf("expected 404, got %d", resp.StatusCode)
}
})
t.Run("type must be general contact_update", func(t *testing.T) {
svc := &stubContactService{getItem: &contact.ContactListItem{RoleCode: "staff", RoleName: "Staff"}}
app := newContactTestApp(NewContactHandler(svc, nil))
body := []byte(`{"data":{"type":"contact_staff_update","attributes":{"first_name":"Jane"}}}`)
resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id, body)
if resp.StatusCode != http.StatusUnprocessableEntity {
t.Fatalf("expected 422, got %d", resp.StatusCode)
}
})
t.Run("mismatched profile payload is rejected", func(t *testing.T) {
svc := &stubContactService{getItem: &contact.ContactListItem{RoleCode: "staff", RoleName: "Staff"}}
app := newContactTestApp(NewContactHandler(svc, nil))
body := []byte(`{"data":{"type":"contact_update","attributes":{"pilot":{"short_name":"PJ"}}}}`)
resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id, body)
if resp.StatusCode != http.StatusUnprocessableEntity {
t.Fatalf("expected 422, got %d", resp.StatusCode)
}
})
t.Run("success with multiple role specific payloads for assigned roles", func(t *testing.T) {
rolePilotID := uuidv7.MustBytes()
roleAirRescuerID := uuidv7.MustBytes()
rolesRaw := mustRoleRawContact(t, rolePilotID, "Pilot", "pilot") + "," +
mustRoleRawContact(t, roleAirRescuerID, "Air Rescuer", "air_rescuer")
svc := &stubContactService{
getItem: &contact.ContactListItem{
RoleCode: "pilot",
RoleName: "Pilot",
RolesRaw: &rolesRaw,
},
resolveRoleCodeByID: map[string]string{
string(rolePilotID): "pilot",
string(roleAirRescuerID): "air_rescuer",
},
}
app := newContactTestApp(NewContactHandler(svc, nil))
body := []byte(`{"data":{"type":"contact_update","attributes":{"role_id":"` +
mustUUIDStringContact(t, rolePilotID) + `","role_ids":["` + mustUUIDStringContact(t, rolePilotID) + `","` +
mustUUIDStringContact(t, roleAirRescuerID) + `"],"pilot":{"short_name":"P"},"air_rescuer":{"short_name":"AR"}}}}`)
resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id, body)
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
if len(svc.lastUpdate.Profiles) != 2 {
t.Fatalf("expected 2 profile patches, got %d", len(svc.lastUpdate.Profiles))
}
})
t.Run("success", func(t *testing.T) {
svc := &stubContactService{getItem: &contact.ContactListItem{
RoleCode: "staff",
RoleName: "Staff",
Email: "updated@example.com",
FirstName: "Jane",
LastName: "Doe",
}}
app := newContactTestApp(NewContactHandler(svc, nil))
body := []byte(`{"data":{"type":"contact_update","attributes":{"first_name":"Jane","staff":{"short_name":"PJ"}}}}`)
resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id, body)
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
if svc.lastUpdate.Profile.ShortName == nil || *svc.lastUpdate.Profile.ShortName != "PJ" {
t.Fatalf("expected profile patch on update")
}
})
t.Run("empty strings in update payload clear role profile fields", func(t *testing.T) {
svc := &stubContactService{getItem: &contact.ContactListItem{
RoleCode: "air_rescuer",
RoleName: "Air Rescuer",
}}
app := newContactTestApp(NewContactHandler(svc, nil))
body := []byte(`{"data":{"type":"contact_update","attributes":{"air_rescuer":{"short_name":"","location":" ","note":"","postcode":"","street_line":""}}}}`)
resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id, body)
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
if svc.lastUpdate.Profile.ShortName == nil || *svc.lastUpdate.Profile.ShortName != "" {
t.Fatalf("expected short_name to be explicitly cleared, got %#v", svc.lastUpdate.Profile.ShortName)
}
if svc.lastUpdate.Profile.Location == nil || *svc.lastUpdate.Profile.Location != "" {
t.Fatalf("expected location to be explicitly cleared, got %#v", svc.lastUpdate.Profile.Location)
}
if svc.lastUpdate.Profile.Note == nil || *svc.lastUpdate.Profile.Note != "" {
t.Fatalf("expected note to be explicitly cleared, got %#v", svc.lastUpdate.Profile.Note)
}
if svc.lastUpdate.Profile.Postcode == nil || *svc.lastUpdate.Profile.Postcode != "" {
t.Fatalf("expected postcode to be explicitly cleared, got %#v", svc.lastUpdate.Profile.Postcode)
}
if svc.lastUpdate.Profile.StreetLine == nil || *svc.lastUpdate.Profile.StreetLine != "" {
t.Fatalf("expected street_line to be explicitly cleared, got %#v", svc.lastUpdate.Profile.StreetLine)
}
})
t.Run("success update role_ids and profile for assigned role", func(t *testing.T) {
roleStaffID := uuidv7.MustBytes()
roleAirRescuerID := uuidv7.MustBytes()
rolesRaw := mustRoleRawContact(t, roleStaffID, "Staff", "staff") + "," +
mustRoleRawContact(t, roleAirRescuerID, "Air Rescuer", "air_rescuer")
svc := &stubContactService{
getItem: &contact.ContactListItem{
RoleCode: "staff",
RoleName: "Staff",
Email: "updated@example.com",
FirstName: "Jane",
LastName: "Doe",
RolesRaw: &rolesRaw,
},
resolveRoleCodeByID: map[string]string{
string(roleStaffID): "staff",
string(roleAirRescuerID): "air_rescuer",
},
}
app := newContactTestApp(NewContactHandler(svc, nil))
body := []byte(`{"data":{"type":"contact_update","attributes":{"role_id":"` +
mustUUIDStringContact(t, roleAirRescuerID) +
`","role_ids":["` + mustUUIDStringContact(t, roleAirRescuerID) + `","` + mustUUIDStringContact(t, roleStaffID) +
`"],"air_rescuer":{"short_name":"AR"},"staff":{"short_name":"S"}}}}`)
resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id, body)
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
if !svc.lastUpdate.UpdateRoles {
t.Fatalf("expected role updates enabled")
}
if len(svc.lastUpdate.RoleID) != 16 || string(svc.lastUpdate.RoleID) != string(roleAirRescuerID) {
t.Fatalf("expected primary role updated to air_rescuer")
}
if len(svc.lastUpdate.RoleIDs) != 2 {
t.Fatalf("expected 2 role ids, got %d", len(svc.lastUpdate.RoleIDs))
}
if len(svc.lastUpdate.Profiles) != 2 {
t.Fatalf("expected two role profile patches")
}
foundAirRescuer := false
foundStaff := false
for i := range svc.lastUpdate.Profiles {
switch svc.lastUpdate.Profiles[i].RoleCode {
case "air_rescuer":
foundAirRescuer = svc.lastUpdate.Profiles[i].Patch.ShortName != nil && *svc.lastUpdate.Profiles[i].Patch.ShortName == "AR"
case "staff":
foundStaff = svc.lastUpdate.Profiles[i].Patch.ShortName != nil && *svc.lastUpdate.Profiles[i].Patch.ShortName == "S"
}
}
if !foundAirRescuer || !foundStaff {
t.Fatalf("expected air_rescuer and staff patches")
}
})
t.Run("success update role_ids only promotes first role_id as primary", func(t *testing.T) {
rolePilotID := uuidv7.MustBytes()
roleAirRescuerID := uuidv7.MustBytes()
rolesRaw := mustRoleRawContact(t, rolePilotID, "Pilot", "pilot") + "," +
mustRoleRawContact(t, roleAirRescuerID, "Air Rescuer", "air_rescuer")
svc := &stubContactService{
getItem: &contact.ContactListItem{
RoleCode: "pilot",
RoleName: "Pilot",
Email: "updated@example.com",
FirstName: "Jane",
LastName: "Doe",
RolesRaw: &rolesRaw,
},
resolveRoleCodeByID: map[string]string{
string(rolePilotID): "pilot",
string(roleAirRescuerID): "air_rescuer",
},
}
app := newContactTestApp(NewContactHandler(svc, nil))
body := []byte(`{"data":{"type":"contact_update","attributes":{"role_ids":["` +
mustUUIDStringContact(t, roleAirRescuerID) + `","` + mustUUIDStringContact(t, rolePilotID) +
`"],"air_rescuer":{"short_name":"AR"},"pilot":{"short_name":"P"}}}}`)
resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id, body)
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
if !svc.lastUpdate.UpdateRoles {
t.Fatalf("expected role updates enabled")
}
if len(svc.lastUpdate.RoleID) != 16 || string(svc.lastUpdate.RoleID) != string(roleAirRescuerID) {
t.Fatalf("expected primary role updated to first role_ids item (air_rescuer)")
}
if len(svc.lastUpdate.RoleIDs) != 2 {
t.Fatalf("expected 2 role ids, got %d", len(svc.lastUpdate.RoleIDs))
}
})
t.Run("role_ids update requires matching profile payloads", func(t *testing.T) {
roleStaffID := uuidv7.MustBytes()
roleAirRescuerID := uuidv7.MustBytes()
rolesRaw := mustRoleRawContact(t, roleStaffID, "Staff", "staff") + "," +
mustRoleRawContact(t, roleAirRescuerID, "Air Rescuer", "air_rescuer")
svc := &stubContactService{
getItem: &contact.ContactListItem{
RoleCode: "staff",
RoleName: "Staff",
RolesRaw: &rolesRaw,
},
resolveRoleCodeByID: map[string]string{
string(roleStaffID): "staff",
string(roleAirRescuerID): "air_rescuer",
},
}
app := newContactTestApp(NewContactHandler(svc, nil))
body := []byte(`{"data":{"type":"contact_update","attributes":{"role_ids":["` +
mustUUIDStringContact(t, roleAirRescuerID) + `","` + mustUUIDStringContact(t, roleStaffID) +
`"],"air_rescuer":{"short_name":"AR"}}}}`)
resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id, body)
if resp.StatusCode != http.StatusUnprocessableEntity {
t.Fatalf("expected 422, got %d", resp.StatusCode)
}
})
t.Run("invalid role_id format", func(t *testing.T) {
svc := &stubContactService{
getItem: &contact.ContactListItem{RoleCode: "staff", RoleName: "Staff"},
}
app := newContactTestApp(NewContactHandler(svc, nil))
body := []byte(`{"data":{"type":"contact_update","attributes":{"role_id":"bad"}}}`)
resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id, body)
if resp.StatusCode != http.StatusUnprocessableEntity {
t.Fatalf("expected 422, got %d", resp.StatusCode)
}
})
t.Run("invalid role_ids format", func(t *testing.T) {
svc := &stubContactService{
getItem: &contact.ContactListItem{RoleCode: "staff", RoleName: "Staff"},
}
app := newContactTestApp(NewContactHandler(svc, nil))
body := []byte(`{"data":{"type":"contact_update","attributes":{"role_ids":["bad"]}}}`)
resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id, body)
if resp.StatusCode != http.StatusUnprocessableEntity {
t.Fatalf("expected 422, got %d", resp.StatusCode)
}
})
t.Run("success clear all roles with empty role_ids", func(t *testing.T) {
roleStaffID := uuidv7.MustBytes()
rolesRaw := mustRoleRawContact(t, roleStaffID, "Staff", "staff")
svc := &stubContactService{
getItem: &contact.ContactListItem{
RoleCode: "staff",
RoleName: "Staff",
RolesRaw: &rolesRaw,
},
}
app := newContactTestApp(NewContactHandler(svc, nil))
body := []byte(`{"data":{"type":"contact_update","attributes":{"role_ids":[]}}}`)
resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id, body)
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
if !svc.lastUpdate.UpdateRoles {
t.Fatalf("expected role updates enabled")
}
if len(svc.lastUpdate.RoleID) != 0 {
t.Fatalf("expected empty primary role id")
}
if len(svc.lastUpdate.RoleIDs) != 0 {
t.Fatalf("expected empty role ids")
}
})
t.Run("success clear all roles with empty role_id string", func(t *testing.T) {
roleStaffID := uuidv7.MustBytes()
rolesRaw := mustRoleRawContact(t, roleStaffID, "Staff", "staff")
svc := &stubContactService{
getItem: &contact.ContactListItem{
RoleCode: "staff",
RoleName: "Staff",
RolesRaw: &rolesRaw,
},
}
app := newContactTestApp(NewContactHandler(svc, nil))
body := []byte(`{"data":{"type":"contact_update","attributes":{"role_id":"","role_ids":[]}}}`)
resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id, body)
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
if !svc.lastUpdate.UpdateRoles {
t.Fatalf("expected role updates enabled")
}
if len(svc.lastUpdate.RoleID) != 0 {
t.Fatalf("expected empty primary role id")
}
if len(svc.lastUpdate.RoleIDs) != 0 {
t.Fatalf("expected empty role ids")
}
})
t.Run("success clear all roles with only empty role_id string", func(t *testing.T) {
roleStaffID := uuidv7.MustBytes()
rolesRaw := mustRoleRawContact(t, roleStaffID, "Staff", "staff")
svc := &stubContactService{
getItem: &contact.ContactListItem{
RoleCode: "staff",
RoleName: "Staff",
RolesRaw: &rolesRaw,
},
}
app := newContactTestApp(NewContactHandler(svc, nil))
body := []byte(`{"data":{"type":"contact_update","attributes":{"role_id":""}}}`)
resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id, body)
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
if !svc.lastUpdate.UpdateRoles || len(svc.lastUpdate.RoleID) != 0 || len(svc.lastUpdate.RoleIDs) != 0 {
t.Fatalf("expected clear roles request")
}
})
t.Run("success clear all roles with role_id null string", func(t *testing.T) {
roleStaffID := uuidv7.MustBytes()
rolesRaw := mustRoleRawContact(t, roleStaffID, "Staff", "staff")
svc := &stubContactService{
getItem: &contact.ContactListItem{
RoleCode: "staff",
RoleName: "Staff",
RolesRaw: &rolesRaw,
},
}
app := newContactTestApp(NewContactHandler(svc, nil))
body := []byte(`{"data":{"type":"contact_update","attributes":{"role_id":"null"}}}`)
resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id, body)
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
if !svc.lastUpdate.UpdateRoles || len(svc.lastUpdate.RoleID) != 0 || len(svc.lastUpdate.RoleIDs) != 0 {
t.Fatalf("expected clear roles request")
}
})
}
func TestContactHandlerDelete(t *testing.T) {
t.Run("invalid uuid", func(t *testing.T) {
app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
resp, _ := doJSONRequestContact(t, app, http.MethodDelete, "/contacts/not-uuid", nil)
if resp.StatusCode != http.StatusUnprocessableEntity {
t.Fatalf("expected 422, got %d", resp.StatusCode)
}
})
t.Run("not found", func(t *testing.T) {
id := mustUUIDStringContact(t, uuidv7.MustBytes())
svc := &stubContactService{deleteErr: gorm.ErrRecordNotFound}
app := newContactTestApp(NewContactHandler(svc, nil))
resp, _ := doJSONRequestContact(t, app, http.MethodDelete, "/contacts/"+id, nil)
if resp.StatusCode != http.StatusNotFound {
t.Fatalf("expected 404, got %d", resp.StatusCode)
}
})
t.Run("success", func(t *testing.T) {
id := mustUUIDStringContact(t, uuidv7.MustBytes())
app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
resp, _ := doJSONRequestContact(t, app, http.MethodDelete, "/contacts/"+id, nil)
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
})
}
func TestContactHandlerSimpleMutations(t *testing.T) {
userID := mustUUIDStringContact(t, uuidv7.MustBytes())
getItem := &contact.ContactListItem{
RoleCode: "technician",
RoleName: "Technician",
Email: "tech@example.com",
FirstName: "Tech",
LastName: "Nician",
IsActive: true,
}
tests := []struct {
name string
method string
path string
body []byte
assert func(t *testing.T, svc *stubContactService)
}{
{
name: "status success",
method: http.MethodPatch,
path: "/contacts/" + userID + "/status",
body: []byte(`{"data":{"type":"contact_status_update","attributes":{"is_active":true}}}`),
assert: func(t *testing.T, svc *stubContactService) {
if !svc.lastIsActive {
t.Fatalf("expected active flag true")
}
},
},
{
name: "role success",
method: http.MethodPatch,
path: "/contacts/" + userID + "/role",
body: []byte(`{"data":{"type":"contact_role_update","attributes":{"role_code":"pilot"}}}`),
assert: func(t *testing.T, svc *stubContactService) {
if svc.lastRoleCode != "pilot" {
t.Fatalf("expected role code recorded")
}
},
},
{
name: "pilot category success",
method: http.MethodPatch,
path: "/contacts/" + userID + "/pilot-category",
body: []byte(`{"data":{"type":"contact_pilot_category_update","attributes":{"pilot_category":"freelance"}}}`),
assert: func(t *testing.T, svc *stubContactService) {
if svc.lastPilotCategory != "freelance" {
t.Fatalf("expected pilot category recorded")
}
},
},
{
name: "license success",
method: http.MethodPatch,
path: "/contacts/" + userID + "/license",
body: []byte(`{"data":{"type":"contact_license_update","attributes":{"license_no":"LIC"}}}`),
assert: func(t *testing.T, svc *stubContactService) {
if svc.lastLicenseNo == nil || *svc.lastLicenseNo != "LIC" {
t.Fatalf("expected license fields recorded")
}
},
},
{
name: "chief flags success",
method: http.MethodPatch,
path: "/contacts/" + userID + "/chief-flags",
body: []byte(`{"data":{"type":"contact_chief_flags_update","attributes":{"is_chief_pilot":true,"is_chief_technician":false}}}`),
assert: func(t *testing.T, svc *stubContactService) {
if svc.lastChiefFlags.IsChiefPilot == nil || !*svc.lastChiefFlags.IsChiefPilot {
t.Fatalf("expected chief pilot flag recorded")
}
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
svc := &stubContactService{getItem: getItem}
app := newContactTestApp(NewContactHandler(svc, nil))
resp, _ := doJSONRequestContact(t, app, tc.method, tc.path, tc.body)
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
tc.assert(t, svc)
})
}
}
func TestContactHandlerSimpleMutationsErrors(t *testing.T) {
t.Run("status invalid uuid", func(t *testing.T) {
app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/not-uuid/status", []byte(`{}`))
if resp.StatusCode != http.StatusUnprocessableEntity {
t.Fatalf("expected 422, got %d", resp.StatusCode)
}
})
t.Run("status service error", func(t *testing.T) {
id := mustUUIDStringContact(t, uuidv7.MustBytes())
svc := &stubContactService{updateStatusErr: errors.New("status failed")}
app := newContactTestApp(NewContactHandler(svc, nil))
body := []byte(`{"data":{"type":"contact_status_update","attributes":{"is_active":false}}}`)
resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id+"/status", body)
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", resp.StatusCode)
}
})
t.Run("role invalid json", func(t *testing.T) {
id := mustUUIDStringContact(t, uuidv7.MustBytes())
app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id+"/role", []byte(`{"data":`))
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", resp.StatusCode)
}
})
t.Run("role validation error", func(t *testing.T) {
id := mustUUIDStringContact(t, uuidv7.MustBytes())
app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
body := []byte(`{"data":{"type":"contact_role_update","attributes":{"role_code":"wrong"}}}`)
resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id+"/role", body)
if resp.StatusCode != http.StatusUnprocessableEntity {
t.Fatalf("expected 422, got %d", resp.StatusCode)
}
})
t.Run("role service error", func(t *testing.T) {
id := mustUUIDStringContact(t, uuidv7.MustBytes())
svc := &stubContactService{updateRoleErr: errors.New("role failed")}
app := newContactTestApp(NewContactHandler(svc, nil))
body := []byte(`{"data":{"type":"contact_role_update","attributes":{"role_code":"pilot"}}}`)
resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id+"/role", body)
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", resp.StatusCode)
}
})
t.Run("pilot category invalid json", func(t *testing.T) {
id := mustUUIDStringContact(t, uuidv7.MustBytes())
app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id+"/pilot-category", []byte(`{"data":`))
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", resp.StatusCode)
}
})
t.Run("pilot category validation error", func(t *testing.T) {
id := mustUUIDStringContact(t, uuidv7.MustBytes())
app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
body := []byte(`{"data":{"type":"contact_pilot_category_update","attributes":{"pilot_category":"wrong"}}}`)
resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id+"/pilot-category", body)
if resp.StatusCode != http.StatusUnprocessableEntity {
t.Fatalf("expected 422, got %d", resp.StatusCode)
}
})
t.Run("pilot category service error", func(t *testing.T) {
id := mustUUIDStringContact(t, uuidv7.MustBytes())
svc := &stubContactService{updatePilotCategoryErr: errors.New("pilot category failed")}
app := newContactTestApp(NewContactHandler(svc, nil))
body := []byte(`{"data":{"type":"contact_pilot_category_update","attributes":{"pilot_category":"regular"}}}`)
resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id+"/pilot-category", body)
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", resp.StatusCode)
}
})
t.Run("license invalid json", func(t *testing.T) {
id := mustUUIDStringContact(t, uuidv7.MustBytes())
app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id+"/license", []byte(`{"data":`))
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", resp.StatusCode)
}
})
t.Run("license service error", func(t *testing.T) {
id := mustUUIDStringContact(t, uuidv7.MustBytes())
svc := &stubContactService{updateLicenseErr: errors.New("license failed")}
app := newContactTestApp(NewContactHandler(svc, nil))
body := []byte(`{"data":{"type":"contact_license_update","attributes":{"license_no":"LIC"}}}`)
resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id+"/license", body)
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", resp.StatusCode)
}
})
t.Run("chief flags invalid json", func(t *testing.T) {
id := mustUUIDStringContact(t, uuidv7.MustBytes())
app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id+"/chief-flags", []byte(`{"data":`))
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", resp.StatusCode)
}
})
t.Run("chief flags service error", func(t *testing.T) {
id := mustUUIDStringContact(t, uuidv7.MustBytes())
svc := &stubContactService{updateChiefFlagsErr: errors.New("chief failed")}
app := newContactTestApp(NewContactHandler(svc, nil))
body := []byte(`{"data":{"type":"contact_chief_flags_update","attributes":{"is_chief_physician":true}}}`)
resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id+"/chief-flags", body)
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", resp.StatusCode)
}
})
}
func TestContactHandlerBulkUpdate(t *testing.T) {
t.Run("invalid json", func(t *testing.T) {
app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
resp, _ := doJSONRequestContact(t, app, http.MethodPost, "/contacts/bulk-update", []byte(`{"data":`))
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", resp.StatusCode)
}
})
t.Run("validation error", func(t *testing.T) {
app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
resp, _ := doJSONRequestContact(t, app, http.MethodPost, "/contacts/bulk-update", []byte(`{}`))
if resp.StatusCode != http.StatusUnprocessableEntity {
t.Fatalf("expected 422, got %d", resp.StatusCode)
}
})
t.Run("invalid uuid", func(t *testing.T) {
app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
body := []byte(`{"data":{"type":"contact_bulk_update","attributes":{"user_ids":["bad"],"is_active":true}}}`)
resp, _ := doJSONRequestContact(t, app, http.MethodPost, "/contacts/bulk-update", body)
if resp.StatusCode != http.StatusUnprocessableEntity {
t.Fatalf("expected 422, got %d", resp.StatusCode)
}
})
t.Run("service error", func(t *testing.T) {
id := mustUUIDStringContact(t, uuidv7.MustBytes())
svc := &stubContactService{bulkUpdateErr: errors.New("bulk failed")}
app := newContactTestApp(NewContactHandler(svc, nil))
body := []byte(`{"data":{"type":"contact_bulk_update","attributes":{"user_ids":["` + id + `"],"is_active":false}}}`)
resp, _ := doJSONRequestContact(t, app, http.MethodPost, "/contacts/bulk-update", body)
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", resp.StatusCode)
}
})
t.Run("success", func(t *testing.T) {
id1 := mustUUIDStringContact(t, uuidv7.MustBytes())
id2 := mustUUIDStringContact(t, uuidv7.MustBytes())
svc := &stubContactService{bulkUpdateCount: 2}
app := newContactTestApp(NewContactHandler(svc, nil))
body := []byte(`{"data":{"type":"contact_bulk_update","attributes":{"user_ids":["` + id1 + `","` + id2 + `"],"is_active":true}}}`)
resp, _ := doJSONRequestContact(t, app, http.MethodPost, "/contacts/bulk-update", body)
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
if len(svc.lastBulkIDs) != 2 || !svc.lastBulkIsActive {
t.Fatalf("expected bulk update recorded")
}
})
}
func TestContactHandlerProfilePatchHelpers(t *testing.T) {
now := time.Now().UTC()
later := now.Add(24 * time.Hour)
createPatch := profilePatchFromCreate("pilot", dto.ContactCreateAttributes{
Pilot: &dto.PilotProfileAttributes{
PilotCategory: "regular",
ShortName: "P",
LicenseNo: "L",
LicenseIssuedAt: &now,
LicenseExpiredAt: &later,
TechnicianLicenseNo: "T",
HasIFRQualification: boolPtrContact(true),
IsChiefPilot: boolPtrContact(true),
DULBaseIDs: []string{},
TotalFlightMinutes: intPtrContact(1),
ResponsiblePilotMinutes: intPtrContact(2),
SecondPilotMinutes: intPtrContact(3),
DoubleCommandMinutes: intPtrContact(4),
FlightInstructorMinutes: intPtrContact(5),
NightFlightMinutes: intPtrContact(6),
IFRFlightMinutes: intPtrContact(7),
Location: "Loc",
Postcode: "123",
StreetLine: "Street",
WeightKG: floatPtrContact(70.5),
},
Doctor: &dto.DoctorProfileAttributes{
ShortName: "D",
Specialization: "Spec",
SubSpecialization: "Sub",
IsChiefPhysician: boolPtrContact(true),
Location: "DocLoc",
Postcode: "456",
StreetLine: "DocStreet",
},
AirRescuer: &dto.AirRescuerProfileAttributes{
ShortName: "A",
ResponsibleFlightRescuer: boolPtrContact(true),
Location: "AirLoc",
Postcode: "789",
StreetLine: "AirStreet",
},
Technician: &dto.TechnicianProfileAttributes{
ShortName: "T",
LicenseNo: "TEC",
IsChiefTechnician: boolPtrContact(true),
IsResponsibleComplaints: boolPtrContact(true),
Location: "TechLoc",
Postcode: "101",
StreetLine: "TechStreet",
},
FlightAssistant: &dto.FlightAssistantProfileAttributes{
ShortName: "F",
Location: "FlightLoc",
Postcode: "202",
StreetLine: "FlightStreet",
},
Staff: &dto.StaffProfileAttributes{
ShortName: "S",
RoleLabel: "Admin",
Location: "StaffLoc",
Postcode: "303",
StreetLine: "StaffStreet",
},
})
if createPatch.WeightKG == nil || *createPatch.WeightKG != 70.5 {
t.Fatalf("expected pilot weight copied")
}
if createPatch.ResponsibleFlightRescuer != nil {
t.Fatalf("expected pilot responsible flight rescuer ignored on create")
}
if createPatch.RoleLabel != nil {
t.Fatalf("expected non-pilot fields to be ignored on create")
}
airRescuerPatch := profilePatchFromCreate("air_rescuer", dto.ContactCreateAttributes{
AirRescuer: &dto.AirRescuerProfileAttributes{
ShortName: "Air",
ResponsibleFlightRescuer: boolPtrContact(true),
},
})
if airRescuerPatch.ResponsibleFlightRescuer == nil || !*airRescuerPatch.ResponsibleFlightRescuer {
t.Fatalf("expected air rescuer responsible flight rescuer copied on create")
}
updatePatch := profilePatchFromUpdate("staff", dto.ContactUpdateAttributes{
Pilot: &dto.PilotProfileUpdateAttributes{
PilotCategory: strPtrContact("dry_lease"),
ShortName: strPtrContact("Pilot"),
},
Doctor: &dto.DoctorProfileUpdateAttributes{},
AirRescuer: &dto.AirRescuerProfileUpdateAttributes{
ResponsibleFlightRescuer: boolPtrContact(true),
},
Technician: &dto.TechnicianProfileUpdateAttributes{
IsResponsibleComplaints: boolPtrContact(true),
},
FlightAssistant: &dto.FlightAssistantProfileUpdateAttributes{ShortName: strPtrContact("FA")},
Staff: &dto.StaffProfileUpdateAttributes{
ShortName: strPtrContact("Staff"),
RoleLabel: strPtrContact("Coordinator"),
},
})
if updatePatch.ShortName == nil || *updatePatch.ShortName != "Staff" {
t.Fatalf("expected update profile patch")
}
if updatePatch.RoleLabel == nil || *updatePatch.RoleLabel != "Coordinator" {
t.Fatalf("expected staff fields copied on update")
}
if updatePatch.PilotCategory != nil || updatePatch.ResponsibleFlightRescuer != nil || updatePatch.IsResponsibleComplaints != nil {
t.Fatalf("expected non-staff fields to be ignored on update")
}
pilotPatch := contact.ProfilePatch{}
dulBaseID := mustUUIDStringContact(t, uuidv7.MustBytes())
fillPatchFromPilot(&pilotPatch, &dto.PilotProfileAttributes{
DULBase: []dto.DULBase{{ID: dulBaseID, Name: "Base A", Category: "hems"}},
})
if !pilotPatch.DULBaseIDsSet || len(pilotPatch.DULBaseIDs) != 1 {
t.Fatalf("expected dul_base object payload mapped to DUL base ids")
}
fillPatchFromPilot(nil, nil)
fillPatchFromDoctor(nil, nil)
fillPatchFromAirRescuer(nil, nil)
fillPatchFromTechnician(nil, nil)
fillPatchFromFlightAssistant(nil, nil)
fillPatchFromStaff(nil, nil)
}
func TestContactHandlerHelpers(t *testing.T) {
if optionalStr(" ") != nil {
t.Fatalf("expected nil optional string")
}
if got := optionalStr(" abc "); got == nil || *got != "abc" {
t.Fatalf("expected trimmed optional string")
}
if got := strPtr("x"); got == nil || *got != "x" {
t.Fatalf("expected string pointer")
}
if got := contactUUIDString([]byte("bad")); got != "" {
t.Fatalf("expected empty uuid string on invalid bytes")
}
if attrs := contactAttributes(nil, false); len(attrs) != 0 {
t.Fatalf("expected empty attrs for nil item")
}
if roles := contactRoles(nil); roles == nil || len(roles) != 0 {
t.Fatalf("expected empty roles for nil raw")
}
roleID := uuidv7.MustBytes()
roleIDHex := strings.ToLower(hex.EncodeToString(roleID))
roleNameHex := strings.ToLower(hex.EncodeToString([]byte("Pilot")))
raw := roleIDHex + "|" + roleNameHex + "|pilot"
roles := contactRoles(&raw)
if len(roles) != 1 {
t.Fatalf("expected one role, got %d", len(roles))
}
if roles[0]["role_code"] != "pilot" || roles[0]["role_name"] != "Pilot" {
t.Fatalf("unexpected role payload: %#v", roles[0])
}
if intStatus(http.StatusBadRequest) != "400" || intStatus(http.StatusNotFound) != "404" {
t.Fatalf("unexpected int status mapping")
}
}
func TestContactAttributesByRole(t *testing.T) {
now := time.Now().UTC()
creator := uuidv7.MustBytes()
str := "value"
yes := true
n := 10
kg := 80.5
cases := []contact.ContactListItem{
{
CreatedBy: creator,
RoleCode: "pilot",
RoleName: "Pilot",
Email: "p@example.com",
FirstName: "P",
LastName: "One",
PilotCategory: "regular",
PilotShortName: &str,
PilotLicenseNo: &str,
PilotLicenseIssuedAt: &now,
PilotLicenseExpiredAt: &now,
PilotTechnicianLicenseNo: &str,
PilotHasIFRQualification: &yes,
PilotIsChiefPilot: &yes,
PilotIsDUL: &yes,
PilotTotalFlightMinutes: &n,
PilotResponsiblePilotMinutes: &n,
PilotSecondPilotMinutes: &n,
PilotDoubleCommandMinutes: &n,
PilotFlightInstructorMinutes: &n,
PilotNightFlightMinutes: &n,
PilotIFRFlightMinutes: &n,
PilotLocation: &str,
PilotPostcode: &str,
PilotStreetLine: &str,
PilotWeightKG: &kg,
PilotPhotoFileID: &str,
},
{
CreatedBy: creator,
RoleCode: "doctor",
RoleName: "Doctor",
Email: "d@example.com",
FirstName: "D",
LastName: "One",
DoctorShortName: &str,
DoctorSpecialization: &str,
DoctorSubSpecialization: &str,
DoctorIsChiefPhysician: &yes,
DoctorLocation: &str,
DoctorPostcode: &str,
DoctorStreetLine: &str,
DoctorPhotoFileID: &str,
},
{
CreatedBy: creator,
RoleCode: "air_rescuer",
RoleName: "Air Rescuer",
Email: "a@example.com",
FirstName: "A",
LastName: "One",
AirRescuerShortName: &str,
AirRescuerResponsibleFlightRescuer: &yes,
AirRescuerLocation: &str,
AirRescuerPostcode: &str,
AirRescuerStreetLine: &str,
AirRescuerPhotoFileID: &str,
},
{
CreatedBy: creator,
RoleCode: "technician",
RoleName: "Technician",
Email: "t@example.com",
FirstName: "T",
LastName: "One",
TechnicianShortName: &str,
TechnicianLicenseNo: &str,
TechnicianIsChiefTechnician: &yes,
TechnicianIsResponsibleComplaints: &yes,
TechnicianLocation: &str,
TechnicianPostcode: &str,
TechnicianStreetLine: &str,
TechnicianPhotoFileID: &str,
},
{
CreatedBy: creator,
RoleCode: "flight_assistant",
RoleName: "Flight Assistant",
Email: "f@example.com",
FirstName: "F",
LastName: "One",
FlightAssistantShortName: &str,
FlightAssistantLocation: &str,
FlightAssistantPostcode: &str,
FlightAssistantStreetLine: &str,
FlightAssistantPhotoFileID: &str,
},
{
CreatedBy: creator,
RoleCode: "staff",
RoleName: "Staff",
Email: "s@example.com",
FirstName: "S",
LastName: "One",
StaffShortName: &str,
StaffRoleLabel: &str,
StaffLocation: &str,
StaffPostcode: &str,
StaffStreetLine: &str,
StaffPhotoFileID: &str,
},
}
for _, item := range cases {
attrs := contactAttributes(&item, false)
if len(attrs) == 0 {
t.Fatalf("expected attrs for role %s", item.RoleCode)
}
if attrs["email"] == nil {
t.Fatalf("expected base attrs for role %s", item.RoleCode)
}
if _, ok := attrs["set_password"].(bool); !ok {
t.Fatalf("expected set_password bool for role %s", item.RoleCode)
}
if attrs["created_by"] == "" {
t.Fatalf("expected created_by for role %s", item.RoleCode)
}
if _, ok := attrs["updated_by"].(string); !ok {
t.Fatalf("expected updated_by string for role %s", item.RoleCode)
}
if _, ok := attrs["created_at"].(string); !ok {
t.Fatalf("expected created_at string for role %s", item.RoleCode)
}
if _, ok := attrs["updated_at"].(string); !ok {
t.Fatalf("expected updated_at string for role %s", item.RoleCode)
}
if item.RoleCode == "pilot" {
if profile, ok := attrs["pilot"].(map[string]any); ok && profile["responsible_flight_rescuer"] != nil {
t.Fatalf("expected pilot responsible_flight_rescuer removed from response")
}
if _, ok := attrs["license_no"]; ok {
t.Fatalf("expected top-level license_no removed for pilot")
}
if _, ok := attrs["pilot_category"]; ok {
t.Fatalf("expected top-level pilot_category removed for pilot")
}
}
if item.RoleCode == "doctor" {
if _, ok := attrs["medical_license"]; ok {
t.Fatalf("expected top-level medical_license removed for doctor")
}
if _, ok := attrs["license_no"]; ok {
t.Fatalf("expected top-level license_no removed for doctor")
}
if _, ok := attrs["pilot_category"]; ok {
t.Fatalf("expected top-level pilot_category removed for doctor")
}
}
if item.RoleCode == "air_rescuer" {
profile, ok := attrs["air_rescuer"].(map[string]any)
if !ok || profile["responsible_flight_rescuer"] == nil {
t.Fatalf("expected air rescuer responsible_flight_rescuer in response")
}
if _, ok := attrs["license_no"]; ok {
t.Fatalf("expected top-level license_no removed for air_rescuer")
}
if _, ok := attrs["medical_license"]; ok {
t.Fatalf("expected top-level medical_license removed for air_rescuer")
}
if _, ok := attrs["pilot_category"]; ok {
t.Fatalf("expected top-level pilot_category removed for air_rescuer")
}
}
if item.RoleCode == "flight_assistant" {
if _, ok := attrs["license_no"]; ok {
t.Fatalf("expected top-level license_no removed for flight_assistant")
}
if _, ok := attrs["medical_license"]; ok {
t.Fatalf("expected top-level medical_license removed for flight_assistant")
}
if _, ok := attrs["pilot_category"]; ok {
t.Fatalf("expected top-level pilot_category removed for flight_assistant")
}
}
if item.RoleCode == "technician" {
if _, ok := attrs["medical_license"]; ok {
t.Fatalf("expected top-level medical_license removed for technician")
}
if _, ok := attrs["pilot_category"]; ok {
t.Fatalf("expected top-level pilot_category removed for technician")
}
}
if item.RoleCode == "staff" {
if _, ok := attrs["license_no"]; ok {
t.Fatalf("expected top-level license_no removed for staff")
}
if _, ok := attrs["medical_license"]; ok {
t.Fatalf("expected top-level medical_license removed for staff")
}
if _, ok := attrs["pilot_category"]; ok {
t.Fatalf("expected top-level pilot_category removed for staff")
}
}
}
baseOnly := contactAttributes(&contact.ContactListItem{
RoleCode: "pilot",
RoleName: "Pilot",
Email: "base@example.com",
FirstName: "Base",
LastName: "Only",
SortKey: intPtrContactHandler(2),
Note: "base-note",
}, false)
if baseOnly["sortkey"] == nil || baseOnly["note"] != "base-note" {
t.Fatalf("expected sortkey and note in base attrs")
}
if baseOnly["set_password"] != true {
t.Fatalf("expected set_password=true when password is empty")
}
if _, ok := baseOnly["pilot"]; ok {
t.Fatalf("expected pilot branch skipped when profile fields nil")
}
categoryOnly := contactAttributes(&contact.ContactListItem{
RoleCode: "pilot",
RoleName: "Pilot",
Email: "cat@example.com",
FirstName: "Cat",
LastName: "Only",
PilotCategory: "freelance",
}, false)
pilotProfile, ok := categoryOnly["pilot"].(map[string]any)
if !ok {
t.Fatalf("expected pilot branch included when pilot_category is present")
}
if pilotProfile["pilot_category"] != "freelance" {
t.Fatalf("expected pilot_category propagated, got %#v", pilotProfile["pilot_category"])
}
pilotRoleID := uuidv7.MustBytes()
staffRoleID := uuidv7.MustBytes()
rolesRaw := strings.ToLower(hex.EncodeToString(pilotRoleID)) + "|" + strings.ToLower(hex.EncodeToString([]byte("Pilot"))) + "|pilot," +
strings.ToLower(hex.EncodeToString(staffRoleID)) + "|" + strings.ToLower(hex.EncodeToString([]byte("Staff"))) + "|staff"
multiRole := contactAttributes(&contact.ContactListItem{
RoleCode: "staff",
RoleName: "staff",
RolesRaw: &rolesRaw,
Email: "multi@example.com",
FirstName: "Multi",
LastName: "Role",
PilotCategory: "regular",
PilotShortName: strPtrContact("Pilot"),
StaffShortName: strPtrContact("Staff"),
StaffRoleLabel: strPtrContact("Ops"),
StaffLocation: strPtrContact("Jakarta"),
StaffPostcode: strPtrContact("12950"),
StaffStreetLine: strPtrContact("Jl"),
}, false)
if _, ok := multiRole["pilot"]; !ok {
t.Fatalf("expected pilot profile in multi-role payload")
}
if _, ok := multiRole["staff"]; !ok {
t.Fatalf("expected staff profile in multi-role payload")
}
pilotTechRolesRaw := strings.ToLower(hex.EncodeToString(pilotRoleID)) + "|" + strings.ToLower(hex.EncodeToString([]byte("Pilot"))) + "|pilot," +
strings.ToLower(hex.EncodeToString(staffRoleID)) + "|" + strings.ToLower(hex.EncodeToString([]byte("Technician"))) + "|technician"
pilotTechnician := contactAttributes(&contact.ContactListItem{
RoleCode: "pilot",
RoleName: "pilot",
RolesRaw: &pilotTechRolesRaw,
Email: "pilot-tech@example.com",
FirstName: "Pilot",
LastName: "Tech",
PilotCategory: "regular",
PilotShortName: strPtrContact("Pilot"),
TechnicianShortName: strPtrContact("Tech"),
TechnicianLicenseNo: strPtrContact("TECH-1"),
TechnicianIsChiefTechnician: boolPtrContact(true),
TechnicianIsResponsibleComplaints: boolPtrContact(true),
}, false)
if _, ok := pilotTechnician["pilot"]; !ok {
t.Fatalf("expected pilot profile in pilot+technician payload")
}
if _, ok := pilotTechnician["technician"]; !ok {
t.Fatalf("expected technician profile in pilot+technician payload")
}
}
func intPtrContactHandler(v int) *int { return &v }