Files
fm_be/internal/service/contact_service_test.go
2026-07-16 22:16:45 +07:00

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)
}
}