505 lines
17 KiB
Go
505 lines
17 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"wucher/internal/domain/contact"
|
|
"wucher/internal/shared/pkg/uuidv7"
|
|
)
|
|
|
|
type mockContactRepo struct {
|
|
listItems []contact.ContactListItem
|
|
listTotal int64
|
|
listErr error
|
|
lastFilter contact.ListFilter
|
|
|
|
getItem *contact.ContactListItem
|
|
getErr error
|
|
lastGetID []byte
|
|
isActorAdmin bool
|
|
isActorAdminErr error
|
|
resolveRoleCode string
|
|
resolveRoleErr error
|
|
lastResolveID []byte
|
|
|
|
createID []byte
|
|
createErr error
|
|
lastCreate contact.CreateInput
|
|
|
|
updateErr error
|
|
lastUpdate contact.UpdateInput
|
|
|
|
deleteErr error
|
|
lastDeleteID []byte
|
|
|
|
updateStatusErr error
|
|
lastStatusID []byte
|
|
lastStatusIsActive bool
|
|
|
|
updateRoleErr error
|
|
lastRoleID []byte
|
|
lastRoleCode string
|
|
|
|
updatePilotErr error
|
|
lastPilotID []byte
|
|
lastCategory string
|
|
|
|
updateLicenseErr error
|
|
lastLicenseID []byte
|
|
lastLicenseNo *string
|
|
|
|
updateChiefFlagsErr error
|
|
lastChiefID []byte
|
|
lastChiefPatch contact.ProfilePatch
|
|
|
|
updateResponsibleErr error
|
|
lastResponsibleID []byte
|
|
lastResponsibleValue bool
|
|
|
|
bulkErr error
|
|
bulkCount int64
|
|
lastBulkIDs [][]byte
|
|
lastBulkActive bool
|
|
}
|
|
|
|
func (m *mockContactRepo) ListContacts(_ context.Context, filter contact.ListFilter) ([]contact.ContactListItem, int64, error) {
|
|
m.lastFilter = filter
|
|
if m.listErr != nil {
|
|
return nil, 0, m.listErr
|
|
}
|
|
return m.listItems, m.listTotal, nil
|
|
}
|
|
|
|
func (m *mockContactRepo) GetContactByID(_ context.Context, userID []byte) (*contact.ContactListItem, error) {
|
|
m.lastGetID = append([]byte(nil), userID...)
|
|
if m.getErr != nil {
|
|
return nil, m.getErr
|
|
}
|
|
return m.getItem, nil
|
|
}
|
|
|
|
func (m *mockContactRepo) IsActorAdmin(_ context.Context) (bool, error) {
|
|
if m.isActorAdminErr != nil {
|
|
return false, m.isActorAdminErr
|
|
}
|
|
return m.isActorAdmin, nil
|
|
}
|
|
|
|
func (m *mockContactRepo) ResolveRoleCodeByID(_ context.Context, roleID []byte) (string, error) {
|
|
m.lastResolveID = append([]byte(nil), roleID...)
|
|
if m.resolveRoleErr != nil {
|
|
return "", m.resolveRoleErr
|
|
}
|
|
return m.resolveRoleCode, nil
|
|
}
|
|
|
|
func (m *mockContactRepo) CreateContact(_ context.Context, in contact.CreateInput) ([]byte, error) {
|
|
m.lastCreate = in
|
|
if m.createErr != nil {
|
|
return nil, m.createErr
|
|
}
|
|
return m.createID, nil
|
|
}
|
|
|
|
func (m *mockContactRepo) UpdateContact(_ context.Context, in contact.UpdateInput) error {
|
|
m.lastUpdate = in
|
|
return m.updateErr
|
|
}
|
|
|
|
func (m *mockContactRepo) DeleteContact(_ context.Context, userID []byte) error {
|
|
m.lastDeleteID = append([]byte(nil), userID...)
|
|
return m.deleteErr
|
|
}
|
|
|
|
func (m *mockContactRepo) UpdateContactStatus(_ context.Context, userID []byte, isActive bool) error {
|
|
m.lastStatusID = append([]byte(nil), userID...)
|
|
m.lastStatusIsActive = isActive
|
|
return m.updateStatusErr
|
|
}
|
|
|
|
func (m *mockContactRepo) UpdateContactRole(_ context.Context, userID []byte, roleCode string) error {
|
|
m.lastRoleID = append([]byte(nil), userID...)
|
|
m.lastRoleCode = roleCode
|
|
return m.updateRoleErr
|
|
}
|
|
|
|
func (m *mockContactRepo) UpdatePilotCategory(_ context.Context, userID []byte, category string) error {
|
|
m.lastPilotID = append([]byte(nil), userID...)
|
|
m.lastCategory = category
|
|
return m.updatePilotErr
|
|
}
|
|
|
|
func (m *mockContactRepo) UpdateLicense(_ context.Context, userID []byte, licenseNo *string) error {
|
|
m.lastLicenseID = append([]byte(nil), userID...)
|
|
m.lastLicenseNo = licenseNo
|
|
return m.updateLicenseErr
|
|
}
|
|
|
|
func (m *mockContactRepo) UpdateChiefFlags(_ context.Context, userID []byte, patch contact.ProfilePatch) error {
|
|
m.lastChiefID = append([]byte(nil), userID...)
|
|
m.lastChiefPatch = patch
|
|
return m.updateChiefFlagsErr
|
|
}
|
|
|
|
func (m *mockContactRepo) UpdateResponsibleComplaints(_ context.Context, userID []byte, value bool) error {
|
|
m.lastResponsibleID = append([]byte(nil), userID...)
|
|
m.lastResponsibleValue = value
|
|
return m.updateResponsibleErr
|
|
}
|
|
|
|
func (m *mockContactRepo) BulkUpdateStatus(_ context.Context, userIDs [][]byte, isActive bool) (int64, error) {
|
|
m.lastBulkIDs = append([][]byte(nil), userIDs...)
|
|
m.lastBulkActive = isActive
|
|
if m.bulkErr != nil {
|
|
return 0, m.bulkErr
|
|
}
|
|
return m.bulkCount, nil
|
|
}
|
|
|
|
type mockPINVerifier struct {
|
|
err error
|
|
lastUserID []byte
|
|
lastAction string
|
|
lastToken string
|
|
}
|
|
|
|
func (m *mockPINVerifier) ConsumeSecurityPINActionToken(_ context.Context, userID []byte, action, token string) error {
|
|
m.lastUserID = append([]byte(nil), userID...)
|
|
m.lastAction = action
|
|
m.lastToken = token
|
|
return m.err
|
|
}
|
|
|
|
func TestNewContactService(t *testing.T) {
|
|
repo := &mockContactRepo{}
|
|
verifier := &mockPINVerifier{}
|
|
svc := NewContactService(repo, verifier)
|
|
if svc == nil || svc.repo != repo || svc.pinVerifier != verifier {
|
|
t.Fatalf("expected service to hold dependencies")
|
|
}
|
|
}
|
|
|
|
func TestContactServiceRepoNotConfigured(t *testing.T) {
|
|
svc := NewContactService(nil, nil)
|
|
ctx := context.Background()
|
|
userID := uuidv7.MustBytes()
|
|
|
|
if _, _, err := svc.ListContacts(ctx, contact.ListFilter{}); err == nil {
|
|
t.Fatalf("expected list error")
|
|
}
|
|
if _, err := svc.GetContactByID(ctx, userID); err == nil {
|
|
t.Fatalf("expected get error")
|
|
}
|
|
if _, err := svc.ResolveRoleCodeByID(ctx, userID); err == nil {
|
|
t.Fatalf("expected resolve role error")
|
|
}
|
|
if _, err := svc.CreateContact(ctx, contact.CreateInput{}); err == nil {
|
|
t.Fatalf("expected create error")
|
|
}
|
|
if err := svc.UpdateContact(ctx, contact.UpdateInput{}); err == nil {
|
|
t.Fatalf("expected update error")
|
|
}
|
|
if err := svc.DeleteContact(ctx, userID); err == nil {
|
|
t.Fatalf("expected delete error")
|
|
}
|
|
if err := svc.UpdateContactStatus(ctx, userID, true); err == nil {
|
|
t.Fatalf("expected status error")
|
|
}
|
|
if err := svc.UpdateContactRole(ctx, userID, "pilot"); err == nil {
|
|
t.Fatalf("expected role error")
|
|
}
|
|
if err := svc.UpdatePilotCategory(ctx, userID, "regular"); err == nil {
|
|
t.Fatalf("expected pilot category error")
|
|
}
|
|
if err := svc.UpdateLicense(ctx, userID, nil); err == nil {
|
|
t.Fatalf("expected license error")
|
|
}
|
|
if err := svc.UpdateChiefFlags(ctx, userID, contact.ProfilePatch{}); err == nil {
|
|
t.Fatalf("expected chief flags error")
|
|
}
|
|
if err := svc.UpdateResponsibleComplaints(ctx, userID, true); err == nil {
|
|
t.Fatalf("expected responsible complaints error")
|
|
}
|
|
if _, err := svc.BulkUpdateStatus(ctx, [][]byte{userID}, true); err == nil {
|
|
t.Fatalf("expected bulk update error")
|
|
}
|
|
}
|
|
|
|
func TestContactServiceDelegatesToRepo(t *testing.T) {
|
|
ctx := context.Background()
|
|
userID := uuidv7.MustBytes()
|
|
roleID := uuidv7.MustBytes()
|
|
licenseNo := "LIC"
|
|
shortName := "PJ"
|
|
repoErr := errors.New("repo failed")
|
|
|
|
t.Run("list success and error", func(t *testing.T) {
|
|
repo := &mockContactRepo{
|
|
listItems: []contact.ContactListItem{{UserID: userID, Email: "user@example.com"}},
|
|
listTotal: 1,
|
|
}
|
|
svc := NewContactService(repo, nil)
|
|
filter := contact.ListFilter{Role: "pilot", Sort: "sortkey", Limit: 10, Offset: 5}
|
|
items, total, err := svc.ListContacts(ctx, filter)
|
|
if err != nil || total != 1 || len(items) != 1 {
|
|
t.Fatalf("unexpected list result: total=%d len=%d err=%v", total, len(items), err)
|
|
}
|
|
if repo.lastFilter.Role != "pilot" || repo.lastFilter.Limit != 10 || repo.lastFilter.Offset != 5 {
|
|
t.Fatalf("expected filter forwarded, got %#v", repo.lastFilter)
|
|
}
|
|
if repo.lastFilter.Sort != "users.is_active DESC, CASE WHEN users.is_active = 1 AND users.sortkey IS NOT NULL AND users.sortkey >= 0 THEN 0 ELSE 1 END ASC, CASE WHEN users.is_active = 1 AND users.sortkey IS NOT NULL AND users.sortkey >= 0 THEN users.sortkey END ASC, users.first_name ASC" {
|
|
t.Fatalf("unexpected normalized sort, got %q", repo.lastFilter.Sort)
|
|
}
|
|
|
|
repo.listErr = repoErr
|
|
if _, _, err := svc.ListContacts(ctx, filter); !errors.Is(err, repoErr) {
|
|
t.Fatalf("expected repo error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("get success and error", func(t *testing.T) {
|
|
repo := &mockContactRepo{getItem: &contact.ContactListItem{UserID: userID, Email: "user@example.com"}}
|
|
svc := NewContactService(repo, nil)
|
|
item, err := svc.GetContactByID(ctx, userID)
|
|
if err != nil || item == nil || string(repo.lastGetID) != string(userID) {
|
|
t.Fatalf("unexpected get result: item=%v err=%v", item, err)
|
|
}
|
|
repo.getErr = repoErr
|
|
if _, err := svc.GetContactByID(ctx, userID); !errors.Is(err, repoErr) {
|
|
t.Fatalf("expected repo error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("resolve role success and error", func(t *testing.T) {
|
|
repo := &mockContactRepo{resolveRoleCode: "pilot"}
|
|
svc := NewContactService(repo, nil)
|
|
roleCode, err := svc.ResolveRoleCodeByID(ctx, roleID)
|
|
if err != nil || roleCode != "pilot" || string(repo.lastResolveID) != string(roleID) {
|
|
t.Fatalf("unexpected resolve role result: role=%q err=%v", roleCode, err)
|
|
}
|
|
repo.resolveRoleErr = repoErr
|
|
if _, err := svc.ResolveRoleCodeByID(ctx, roleID); !errors.Is(err, repoErr) {
|
|
t.Fatalf("expected repo error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("create success and error", func(t *testing.T) {
|
|
repo := &mockContactRepo{createID: userID}
|
|
svc := NewContactService(repo, nil)
|
|
in := contact.CreateInput{
|
|
RoleID: roleID,
|
|
User: contact.UserPatch{Email: &licenseNo},
|
|
Profile: contact.ProfilePatch{ShortName: &shortName},
|
|
}
|
|
gotID, err := svc.CreateContact(ctx, in)
|
|
if err != nil || string(gotID) != string(userID) {
|
|
t.Fatalf("unexpected create result: id=%v err=%v", gotID, err)
|
|
}
|
|
if string(repo.lastCreate.RoleID) != string(roleID) || repo.lastCreate.Profile.ShortName == nil || *repo.lastCreate.Profile.ShortName != shortName {
|
|
t.Fatalf("expected create input forwarded")
|
|
}
|
|
repo.createErr = repoErr
|
|
if _, err := svc.CreateContact(ctx, in); !errors.Is(err, repoErr) {
|
|
t.Fatalf("expected repo error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("update success and error", func(t *testing.T) {
|
|
repo := &mockContactRepo{}
|
|
svc := NewContactService(repo, nil)
|
|
in := contact.UpdateInput{UserID: userID, Profile: contact.ProfilePatch{ShortName: &shortName}}
|
|
if err := svc.UpdateContact(ctx, in); err != nil {
|
|
t.Fatalf("unexpected update error: %v", err)
|
|
}
|
|
if string(repo.lastUpdate.UserID) != string(userID) || repo.lastUpdate.Profile.ShortName == nil || *repo.lastUpdate.Profile.ShortName != shortName {
|
|
t.Fatalf("expected update input forwarded")
|
|
}
|
|
repo.updateErr = repoErr
|
|
if err := svc.UpdateContact(ctx, in); !errors.Is(err, repoErr) {
|
|
t.Fatalf("expected repo error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("delete success and error", func(t *testing.T) {
|
|
repo := &mockContactRepo{}
|
|
svc := NewContactService(repo, nil)
|
|
if err := svc.DeleteContact(ctx, userID); err != nil {
|
|
t.Fatalf("unexpected delete error: %v", err)
|
|
}
|
|
if string(repo.lastDeleteID) != string(userID) {
|
|
t.Fatalf("expected delete id forwarded")
|
|
}
|
|
repo.deleteErr = repoErr
|
|
if err := svc.DeleteContact(ctx, userID); !errors.Is(err, repoErr) {
|
|
t.Fatalf("expected repo error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("status success and error", func(t *testing.T) {
|
|
repo := &mockContactRepo{}
|
|
svc := NewContactService(repo, nil)
|
|
if err := svc.UpdateContactStatus(ctx, userID, true); err != nil {
|
|
t.Fatalf("unexpected status error: %v", err)
|
|
}
|
|
if string(repo.lastStatusID) != string(userID) || !repo.lastStatusIsActive {
|
|
t.Fatalf("expected status args forwarded")
|
|
}
|
|
repo.updateStatusErr = repoErr
|
|
if err := svc.UpdateContactStatus(ctx, userID, true); !errors.Is(err, repoErr) {
|
|
t.Fatalf("expected repo error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("role success and error", func(t *testing.T) {
|
|
repo := &mockContactRepo{}
|
|
svc := NewContactService(repo, nil)
|
|
if err := svc.UpdateContactRole(ctx, userID, "pilot"); err != nil {
|
|
t.Fatalf("unexpected role error: %v", err)
|
|
}
|
|
if string(repo.lastRoleID) != string(userID) || repo.lastRoleCode != "pilot" {
|
|
t.Fatalf("expected role args forwarded")
|
|
}
|
|
repo.updateRoleErr = repoErr
|
|
if err := svc.UpdateContactRole(ctx, userID, "pilot"); !errors.Is(err, repoErr) {
|
|
t.Fatalf("expected repo error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("pilot category success and error", func(t *testing.T) {
|
|
repo := &mockContactRepo{}
|
|
svc := NewContactService(repo, nil)
|
|
if err := svc.UpdatePilotCategory(ctx, userID, "regular"); err != nil {
|
|
t.Fatalf("unexpected pilot category error: %v", err)
|
|
}
|
|
if string(repo.lastPilotID) != string(userID) || repo.lastCategory != "regular" {
|
|
t.Fatalf("expected pilot category args forwarded")
|
|
}
|
|
repo.updatePilotErr = repoErr
|
|
if err := svc.UpdatePilotCategory(ctx, userID, "regular"); !errors.Is(err, repoErr) {
|
|
t.Fatalf("expected repo error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("license success and error", func(t *testing.T) {
|
|
repo := &mockContactRepo{}
|
|
svc := NewContactService(repo, nil)
|
|
if err := svc.UpdateLicense(ctx, userID, &licenseNo); err != nil {
|
|
t.Fatalf("unexpected license error: %v", err)
|
|
}
|
|
if string(repo.lastLicenseID) != string(userID) || repo.lastLicenseNo == nil || *repo.lastLicenseNo != licenseNo {
|
|
t.Fatalf("expected license args forwarded")
|
|
}
|
|
repo.updateLicenseErr = repoErr
|
|
if err := svc.UpdateLicense(ctx, userID, &licenseNo); !errors.Is(err, repoErr) {
|
|
t.Fatalf("expected repo error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("chief flags success and error", func(t *testing.T) {
|
|
repo := &mockContactRepo{}
|
|
svc := NewContactService(repo, nil)
|
|
patch := contact.ProfilePatch{ShortName: &shortName}
|
|
if err := svc.UpdateChiefFlags(ctx, userID, patch); err != nil {
|
|
t.Fatalf("unexpected chief flags error: %v", err)
|
|
}
|
|
if string(repo.lastChiefID) != string(userID) || repo.lastChiefPatch.ShortName == nil || *repo.lastChiefPatch.ShortName != shortName {
|
|
t.Fatalf("expected chief flags args forwarded")
|
|
}
|
|
repo.updateChiefFlagsErr = repoErr
|
|
if err := svc.UpdateChiefFlags(ctx, userID, patch); !errors.Is(err, repoErr) {
|
|
t.Fatalf("expected repo error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("responsible complaints success and error", func(t *testing.T) {
|
|
repo := &mockContactRepo{}
|
|
svc := NewContactService(repo, nil)
|
|
if err := svc.UpdateResponsibleComplaints(ctx, userID, true); err != nil {
|
|
t.Fatalf("unexpected responsible complaints error: %v", err)
|
|
}
|
|
if string(repo.lastResponsibleID) != string(userID) || !repo.lastResponsibleValue {
|
|
t.Fatalf("expected responsible complaints args forwarded")
|
|
}
|
|
repo.updateResponsibleErr = repoErr
|
|
if err := svc.UpdateResponsibleComplaints(ctx, userID, true); !errors.Is(err, repoErr) {
|
|
t.Fatalf("expected repo error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("bulk update success and error", func(t *testing.T) {
|
|
repo := &mockContactRepo{bulkCount: 2}
|
|
svc := NewContactService(repo, nil)
|
|
ids := [][]byte{userID, uuidv7.MustBytes()}
|
|
count, err := svc.BulkUpdateStatus(ctx, ids, true)
|
|
if err != nil || count != 2 {
|
|
t.Fatalf("unexpected bulk update result: count=%d err=%v", count, err)
|
|
}
|
|
if len(repo.lastBulkIDs) != 2 || !repo.lastBulkActive {
|
|
t.Fatalf("expected bulk update args forwarded")
|
|
}
|
|
repo.bulkErr = repoErr
|
|
if _, err := svc.BulkUpdateStatus(ctx, ids, true); !errors.Is(err, repoErr) {
|
|
t.Fatalf("expected repo error, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestNormalizeContactSort(t *testing.T) {
|
|
cases := map[string]string{
|
|
"sortkey": "users.is_active DESC, CASE WHEN users.is_active = 1 AND users.sortkey IS NOT NULL AND users.sortkey >= 0 THEN 0 ELSE 1 END ASC, CASE WHEN users.is_active = 1 AND users.sortkey IS NOT NULL AND users.sortkey >= 0 THEN users.sortkey END ASC, users.first_name ASC",
|
|
"-sortkey": "users.is_active DESC, CASE WHEN users.is_active = 1 AND users.sortkey IS NOT NULL AND users.sortkey >= 0 THEN 0 ELSE 1 END ASC, CASE WHEN users.is_active = 1 AND users.sortkey IS NOT NULL AND users.sortkey >= 0 THEN users.sortkey END DESC, users.first_name ASC",
|
|
"note": "COALESCE(pp.note, dp.note, arp.note, tp.note, fap.note, sp.note, '') ASC",
|
|
"-note": "COALESCE(pp.note, dp.note, arp.note, tp.note, fap.note, sp.note, '') DESC",
|
|
"is_active": "users.is_active ASC",
|
|
"-is_active": "users.is_active DESC",
|
|
"name": "users.first_name ASC",
|
|
"email": "users.email ASC",
|
|
"unknown": "",
|
|
"": "",
|
|
}
|
|
|
|
for in, want := range cases {
|
|
if got := normalizeContactSort(in); got != want {
|
|
t.Fatalf("normalizeContactSort(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestContactServiceCheckSensitiveAction(t *testing.T) {
|
|
ctx := context.Background()
|
|
validUserID := uuidv7.MustBytes()
|
|
verifierErr := errors.New("pin verify failed")
|
|
|
|
svc := NewContactService(&mockContactRepo{}, nil)
|
|
if err := svc.CheckSensitiveAction(ctx, []byte("short"), "token", "action"); !errors.Is(err, ErrInvalidPINAction) {
|
|
t.Fatalf("expected invalid pin action for invalid user id, got %v", err)
|
|
}
|
|
if err := svc.CheckSensitiveAction(ctx, validUserID, "token", "action"); err == nil || err.Error() != "pin verifier not configured" {
|
|
t.Fatalf("expected pin verifier not configured error, got %v", err)
|
|
}
|
|
|
|
verifier := &mockPINVerifier{}
|
|
svc = NewContactService(&mockContactRepo{}, verifier)
|
|
if err := svc.CheckSensitiveAction(ctx, validUserID, " ", "action"); !errors.Is(err, ErrInvalidPINAction) {
|
|
t.Fatalf("expected invalid pin action for empty token, got %v", err)
|
|
}
|
|
if err := svc.CheckSensitiveAction(ctx, validUserID, "token", " "); !errors.Is(err, ErrInvalidPINAction) {
|
|
t.Fatalf("expected invalid pin action for empty action, got %v", err)
|
|
}
|
|
|
|
verifier.err = verifierErr
|
|
if err := svc.CheckSensitiveAction(ctx, validUserID, " token ", " action "); !errors.Is(err, verifierErr) {
|
|
t.Fatalf("expected verifier error, got %v", err)
|
|
}
|
|
if string(verifier.lastUserID) != string(validUserID) || verifier.lastAction != "action" || verifier.lastToken != "token" {
|
|
t.Fatalf("expected trimmed action/token forwarded")
|
|
}
|
|
|
|
verifier.err = nil
|
|
if err := svc.CheckSensitiveAction(ctx, validUserID, " token ", " action "); err != nil {
|
|
t.Fatalf("unexpected verifier error: %v", err)
|
|
}
|
|
}
|