1842 lines
68 KiB
Go
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 ©Item, 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 }
|