package service import ( "context" "errors" "testing" "time" "github.com/certctl-io/certctl/internal/domain" ) // mockOwnerRepo is a test implementation of OwnerRepository type mockOwnerRepo struct { owners map[string]*domain.Owner CreateErr error UpdateErr error DeleteErr error GetErr error ListErr error } func (m *mockOwnerRepo) List(ctx context.Context) ([]*domain.Owner, error) { if m.ListErr != nil { return nil, m.ListErr } var owners []*domain.Owner for _, o := range m.owners { owners = append(owners, o) } return owners, nil } func (m *mockOwnerRepo) Get(ctx context.Context, id string) (*domain.Owner, error) { if m.GetErr != nil { return nil, m.GetErr } owner, ok := m.owners[id] if !ok { return nil, errNotFound } return owner, nil } func (m *mockOwnerRepo) Create(ctx context.Context, owner *domain.Owner) error { if m.CreateErr != nil { return m.CreateErr } m.owners[owner.ID] = owner return nil } func (m *mockOwnerRepo) Update(ctx context.Context, owner *domain.Owner) error { if m.UpdateErr != nil { return m.UpdateErr } m.owners[owner.ID] = owner return nil } func (m *mockOwnerRepo) Delete(ctx context.Context, id string) error { if m.DeleteErr != nil { return m.DeleteErr } delete(m.owners, id) return nil } func (m *mockOwnerRepo) AddOwner(owner *domain.Owner) { m.owners[owner.ID] = owner } func newMockOwnerRepository() *mockOwnerRepo { return &mockOwnerRepo{ owners: make(map[string]*domain.Owner), } } // TestOwnerService_List tests paginated listing of owners. func TestOwnerService_List(t *testing.T) { ctx := context.Background() now := time.Now() owner1 := &domain.Owner{ ID: "owner-001", Name: "Alice Smith", Email: "alice@example.com", TeamID: "team-001", CreatedAt: now, UpdatedAt: now, } owner2 := &domain.Owner{ ID: "owner-002", Name: "Bob Jones", Email: "bob@example.com", TeamID: "team-001", CreatedAt: now, UpdatedAt: now, } ownerRepo := newMockOwnerRepository() ownerRepo.AddOwner(owner1) ownerRepo.AddOwner(owner2) auditRepo := newMockAuditRepository() auditService := NewAuditService(auditRepo) ownerService := NewOwnerService(ownerRepo, auditService) owners, total, err := ownerService.List(ctx, 1, 50) if err != nil { t.Fatalf("List failed: %v", err) } if len(owners) != 2 { t.Errorf("expected 2 owners, got %d", len(owners)) } if total != 2 { t.Errorf("expected total 2, got %d", total) } } // TestOwnerService_List_DefaultPagination tests that default pagination values are applied. func TestOwnerService_List_DefaultPagination(t *testing.T) { ctx := context.Background() now := time.Now() owner := &domain.Owner{ ID: "owner-001", Name: "Alice Smith", Email: "alice@example.com", TeamID: "team-001", CreatedAt: now, UpdatedAt: now, } ownerRepo := newMockOwnerRepository() ownerRepo.AddOwner(owner) auditRepo := newMockAuditRepository() auditService := NewAuditService(auditRepo) ownerService := NewOwnerService(ownerRepo, auditService) // Test with page < 1 (should default to 1) owners, total, err := ownerService.List(ctx, 0, 0) if err != nil { t.Fatalf("List failed: %v", err) } if len(owners) != 1 { t.Errorf("expected 1 owner with default pagination, got %d", len(owners)) } if total != 1 { t.Errorf("expected total 1, got %d", total) } } // TestOwnerService_List_RepositoryError tests handling of repository errors. func TestOwnerService_List_RepositoryError(t *testing.T) { ctx := context.Background() ownerRepo := newMockOwnerRepository() ownerRepo.ListErr = errors.New("database error") auditRepo := newMockAuditRepository() auditService := NewAuditService(auditRepo) ownerService := NewOwnerService(ownerRepo, auditService) _, _, err := ownerService.List(ctx, 1, 50) if err == nil { t.Fatal("expected error from List, got nil") } } // TestOwnerService_List_EmptyResult tests listing with no owners. func TestOwnerService_List_EmptyResult(t *testing.T) { ctx := context.Background() ownerRepo := newMockOwnerRepository() auditRepo := newMockAuditRepository() auditService := NewAuditService(auditRepo) ownerService := NewOwnerService(ownerRepo, auditService) owners, total, err := ownerService.List(ctx, 1, 50) if err != nil { t.Fatalf("List failed: %v", err) } if len(owners) != 0 { t.Errorf("expected 0 owners, got %d", len(owners)) } if total != 0 { t.Errorf("expected total 0, got %d", total) } } // TestOwnerService_List_PageBeyondRange tests pagination when page exceeds available data. func TestOwnerService_List_PageBeyondRange(t *testing.T) { ctx := context.Background() now := time.Now() owner := &domain.Owner{ ID: "owner-001", Name: "Alice Smith", Email: "alice@example.com", TeamID: "team-001", CreatedAt: now, UpdatedAt: now, } ownerRepo := newMockOwnerRepository() ownerRepo.AddOwner(owner) auditRepo := newMockAuditRepository() auditService := NewAuditService(auditRepo) ownerService := NewOwnerService(ownerRepo, auditService) // Request page 3 with only 1 owner owners, total, err := ownerService.List(ctx, 3, 1) if err != nil { t.Fatalf("List failed: %v", err) } if len(owners) != 0 { t.Errorf("expected 0 owners on page beyond range, got %d", len(owners)) } if total != 1 { t.Errorf("expected total 1, got %d", total) } } // TestOwnerService_Get tests retrieving a single owner by ID. func TestOwnerService_Get(t *testing.T) { ctx := context.Background() now := time.Now() owner := &domain.Owner{ ID: "owner-001", Name: "Alice Smith", Email: "alice@example.com", TeamID: "team-001", CreatedAt: now, UpdatedAt: now, } ownerRepo := newMockOwnerRepository() ownerRepo.AddOwner(owner) auditRepo := newMockAuditRepository() auditService := NewAuditService(auditRepo) ownerService := NewOwnerService(ownerRepo, auditService) retrieved, err := ownerService.Get(ctx, "owner-001") if err != nil { t.Fatalf("Get failed: %v", err) } if retrieved.Name != "Alice Smith" { t.Errorf("expected name Alice Smith, got %s", retrieved.Name) } if retrieved.Email != "alice@example.com" { t.Errorf("expected email alice@example.com, got %s", retrieved.Email) } } // TestOwnerService_Get_NotFound tests Get with a nonexistent owner. func TestOwnerService_Get_NotFound(t *testing.T) { ctx := context.Background() ownerRepo := newMockOwnerRepository() auditRepo := newMockAuditRepository() auditService := NewAuditService(auditRepo) ownerService := NewOwnerService(ownerRepo, auditService) _, err := ownerService.Get(ctx, "nonexistent") if err == nil { t.Fatal("expected error for nonexistent owner, got nil") } } // TestOwnerService_Create tests creating a new owner with audit recording. func TestOwnerService_Create(t *testing.T) { ctx := context.Background() ownerRepo := newMockOwnerRepository() auditRepo := newMockAuditRepository() auditService := NewAuditService(auditRepo) ownerService := NewOwnerService(ownerRepo, auditService) owner := &domain.Owner{ Name: "Alice Smith", Email: "alice@example.com", TeamID: "team-001", } err := ownerService.Create(ctx, owner, "user-1") if err != nil { t.Fatalf("Create failed: %v", err) } if owner.ID == "" { t.Fatal("expected non-empty owner ID after creation") } if !owner.CreatedAt.IsZero() && owner.CreatedAt.After(time.Now().Add(-time.Second)) { // CreatedAt should have been set } else if owner.CreatedAt.IsZero() { t.Fatal("expected CreatedAt to be set") } if len(ownerRepo.owners) != 1 { t.Errorf("expected 1 owner in repo, got %d", len(ownerRepo.owners)) } if len(auditRepo.Events) != 1 { t.Errorf("expected 1 audit event, got %d", len(auditRepo.Events)) } auditEvent := auditRepo.Events[0] if auditEvent.Action != "create_owner" { t.Errorf("expected action create_owner, got %s", auditEvent.Action) } if auditEvent.ResourceType != "owner" { t.Errorf("expected resource type owner, got %s", auditEvent.ResourceType) } } // TestOwnerService_Create_EmptyName tests that Create rejects empty name. func TestOwnerService_Create_EmptyName(t *testing.T) { ctx := context.Background() ownerRepo := newMockOwnerRepository() auditRepo := newMockAuditRepository() auditService := NewAuditService(auditRepo) ownerService := NewOwnerService(ownerRepo, auditService) owner := &domain.Owner{ Name: "", Email: "alice@example.com", TeamID: "team-001", } err := ownerService.Create(ctx, owner, "user-1") if err == nil { t.Fatal("expected error for empty owner name") } if len(ownerRepo.owners) != 0 { t.Errorf("expected 0 owners in repo after validation failure, got %d", len(ownerRepo.owners)) } if len(auditRepo.Events) != 0 { t.Errorf("expected 0 audit events after validation failure, got %d", len(auditRepo.Events)) } } // TestOwnerService_Create_WithExistingID tests that Create preserves existing ID. func TestOwnerService_Create_WithExistingID(t *testing.T) { ctx := context.Background() ownerRepo := newMockOwnerRepository() auditRepo := newMockAuditRepository() auditService := NewAuditService(auditRepo) ownerService := NewOwnerService(ownerRepo, auditService) owner := &domain.Owner{ ID: "custom-id-123", Name: "Alice Smith", Email: "alice@example.com", TeamID: "team-001", } err := ownerService.Create(ctx, owner, "user-1") if err != nil { t.Fatalf("Create failed: %v", err) } if owner.ID != "custom-id-123" { t.Errorf("expected ID custom-id-123, got %s", owner.ID) } stored, ok := ownerRepo.owners["custom-id-123"] if !ok { t.Fatal("expected owner with custom ID in repo") } if stored.Name != "Alice Smith" { t.Errorf("expected name Alice Smith, got %s", stored.Name) } } // TestOwnerService_Create_RepositoryError tests Create with repository failure. func TestOwnerService_Create_RepositoryError(t *testing.T) { ctx := context.Background() ownerRepo := newMockOwnerRepository() ownerRepo.CreateErr = errors.New("database error") auditRepo := newMockAuditRepository() auditService := NewAuditService(auditRepo) ownerService := NewOwnerService(ownerRepo, auditService) owner := &domain.Owner{ Name: "Alice Smith", Email: "alice@example.com", TeamID: "team-001", } err := ownerService.Create(ctx, owner, "user-1") if err == nil { t.Fatal("expected error from Create") } } // TestOwnerService_Update tests updating an existing owner. func TestOwnerService_Update(t *testing.T) { ctx := context.Background() now := time.Now() originalOwner := &domain.Owner{ ID: "owner-001", Name: "Alice Smith", Email: "alice@example.com", TeamID: "team-001", CreatedAt: now, UpdatedAt: now, } ownerRepo := newMockOwnerRepository() ownerRepo.AddOwner(originalOwner) auditRepo := newMockAuditRepository() auditService := NewAuditService(auditRepo) ownerService := NewOwnerService(ownerRepo, auditService) updatedOwner := &domain.Owner{ Name: "Alice Johnson", Email: "alice.j@example.com", TeamID: "team-002", } err := ownerService.Update(ctx, "owner-001", updatedOwner, "user-1") if err != nil { t.Fatalf("Update failed: %v", err) } stored := ownerRepo.owners["owner-001"] if stored.Name != "Alice Johnson" { t.Errorf("expected updated name Alice Johnson, got %s", stored.Name) } if stored.Email != "alice.j@example.com" { t.Errorf("expected updated email alice.j@example.com, got %s", stored.Email) } if stored.ID != "owner-001" { t.Errorf("expected ID to remain owner-001, got %s", stored.ID) } if len(auditRepo.Events) != 1 { t.Errorf("expected 1 audit event, got %d", len(auditRepo.Events)) } auditEvent := auditRepo.Events[0] if auditEvent.Action != "update_owner" { t.Errorf("expected action update_owner, got %s", auditEvent.Action) } } // TestOwnerService_Update_EmptyName tests that Update rejects empty name. func TestOwnerService_Update_EmptyName(t *testing.T) { ctx := context.Background() now := time.Now() originalOwner := &domain.Owner{ ID: "owner-001", Name: "Alice Smith", Email: "alice@example.com", TeamID: "team-001", CreatedAt: now, UpdatedAt: now, } ownerRepo := newMockOwnerRepository() ownerRepo.AddOwner(originalOwner) auditRepo := newMockAuditRepository() auditService := NewAuditService(auditRepo) ownerService := NewOwnerService(ownerRepo, auditService) updatedOwner := &domain.Owner{ Name: "", Email: "alice.j@example.com", TeamID: "team-002", } err := ownerService.Update(ctx, "owner-001", updatedOwner, "user-1") if err == nil { t.Fatal("expected error for empty owner name") } if len(auditRepo.Events) != 0 { t.Errorf("expected 0 audit events after validation failure, got %d", len(auditRepo.Events)) } } // TestOwnerService_Update_RepositoryError tests Update with repository failure. func TestOwnerService_Update_RepositoryError(t *testing.T) { ctx := context.Background() ownerRepo := newMockOwnerRepository() ownerRepo.UpdateErr = errors.New("database error") auditRepo := newMockAuditRepository() auditService := NewAuditService(auditRepo) ownerService := NewOwnerService(ownerRepo, auditService) updatedOwner := &domain.Owner{ Name: "Alice Johnson", Email: "alice.j@example.com", TeamID: "team-002", } err := ownerService.Update(ctx, "owner-001", updatedOwner, "user-1") if err == nil { t.Fatal("expected error from Update") } } // TestOwnerService_Delete tests deleting an owner with audit recording. func TestOwnerService_Delete(t *testing.T) { ctx := context.Background() now := time.Now() owner := &domain.Owner{ ID: "owner-001", Name: "Alice Smith", Email: "alice@example.com", TeamID: "team-001", CreatedAt: now, UpdatedAt: now, } ownerRepo := newMockOwnerRepository() ownerRepo.AddOwner(owner) auditRepo := newMockAuditRepository() auditService := NewAuditService(auditRepo) ownerService := NewOwnerService(ownerRepo, auditService) err := ownerService.Delete(ctx, "owner-001", "user-1") if err != nil { t.Fatalf("Delete failed: %v", err) } if len(ownerRepo.owners) != 0 { t.Errorf("expected 0 owners in repo after delete, got %d", len(ownerRepo.owners)) } if len(auditRepo.Events) != 1 { t.Errorf("expected 1 audit event, got %d", len(auditRepo.Events)) } auditEvent := auditRepo.Events[0] if auditEvent.Action != "delete_owner" { t.Errorf("expected action delete_owner, got %s", auditEvent.Action) } if auditEvent.ResourceID != "owner-001" { t.Errorf("expected resource ID owner-001, got %s", auditEvent.ResourceID) } } // TestOwnerService_Delete_RepositoryError tests Delete with repository failure. func TestOwnerService_Delete_RepositoryError(t *testing.T) { ctx := context.Background() ownerRepo := newMockOwnerRepository() ownerRepo.DeleteErr = errors.New("database error") auditRepo := newMockAuditRepository() auditService := NewAuditService(auditRepo) ownerService := NewOwnerService(ownerRepo, auditService) err := ownerService.Delete(ctx, "owner-001", "user-1") if err == nil { t.Fatal("expected error from Delete") } } // TestOwnerService_ListOwners_HandlerInterface tests the handler interface method ListOwners. func TestOwnerService_ListOwners_HandlerInterface(t *testing.T) { now := time.Now() owner1 := &domain.Owner{ ID: "owner-001", Name: "Alice Smith", Email: "alice@example.com", TeamID: "team-001", CreatedAt: now, UpdatedAt: now, } owner2 := &domain.Owner{ ID: "owner-002", Name: "Bob Jones", Email: "bob@example.com", TeamID: "team-001", CreatedAt: now, UpdatedAt: now, } ownerRepo := newMockOwnerRepository() ownerRepo.AddOwner(owner1) ownerRepo.AddOwner(owner2) auditRepo := newMockAuditRepository() auditService := NewAuditService(auditRepo) ownerService := NewOwnerService(ownerRepo, auditService) owners, total, err := ownerService.ListOwners(context.Background(), 1, 50) if err != nil { t.Fatalf("ListOwners failed: %v", err) } if len(owners) != 2 { t.Errorf("expected 2 owners, got %d", len(owners)) } if total != 2 { t.Errorf("expected total 2, got %d", total) } // Verify value type conversion worked if owners[0].ID == "" { t.Fatal("expected non-empty owner ID in result") } } // TestOwnerService_GetOwner_HandlerInterface tests the handler interface method GetOwner. func TestOwnerService_GetOwner_HandlerInterface(t *testing.T) { now := time.Now() owner := &domain.Owner{ ID: "owner-001", Name: "Alice Smith", Email: "alice@example.com", TeamID: "team-001", CreatedAt: now, UpdatedAt: now, } ownerRepo := newMockOwnerRepository() ownerRepo.AddOwner(owner) auditRepo := newMockAuditRepository() auditService := NewAuditService(auditRepo) ownerService := NewOwnerService(ownerRepo, auditService) retrieved, err := ownerService.GetOwner(context.Background(), "owner-001") if err != nil { t.Fatalf("GetOwner failed: %v", err) } if retrieved.Name != "Alice Smith" { t.Errorf("expected name Alice Smith, got %s", retrieved.Name) } } // TestOwnerService_CreateOwner_HandlerInterface tests the handler interface method CreateOwner. func TestOwnerService_CreateOwner_HandlerInterface(t *testing.T) { ownerRepo := newMockOwnerRepository() auditRepo := newMockAuditRepository() auditService := NewAuditService(auditRepo) ownerService := NewOwnerService(ownerRepo, auditService) owner := domain.Owner{ Name: "Alice Smith", Email: "alice@example.com", TeamID: "team-001", } created, err := ownerService.CreateOwner(context.Background(), owner) if err != nil { t.Fatalf("CreateOwner failed: %v", err) } if created.ID == "" { t.Fatal("expected non-empty owner ID after creation") } if created.Name != "Alice Smith" { t.Errorf("expected name Alice Smith, got %s", created.Name) } if len(ownerRepo.owners) != 1 { t.Errorf("expected 1 owner in repo, got %d", len(ownerRepo.owners)) } // Note: handler interface method does NOT record audit events (no actor parameter) if len(auditRepo.Events) != 0 { t.Errorf("expected 0 audit events from handler interface method, got %d", len(auditRepo.Events)) } } // TestOwnerService_UpdateOwner_HandlerInterface tests the handler interface method UpdateOwner. func TestOwnerService_UpdateOwner_HandlerInterface(t *testing.T) { now := time.Now() originalOwner := &domain.Owner{ ID: "owner-001", Name: "Alice Smith", Email: "alice@example.com", TeamID: "team-001", CreatedAt: now, UpdatedAt: now, } ownerRepo := newMockOwnerRepository() ownerRepo.AddOwner(originalOwner) auditRepo := newMockAuditRepository() auditService := NewAuditService(auditRepo) ownerService := NewOwnerService(ownerRepo, auditService) updatedOwner := domain.Owner{ Name: "Alice Johnson", Email: "alice.j@example.com", TeamID: "team-002", } updated, err := ownerService.UpdateOwner(context.Background(), "owner-001", updatedOwner) if err != nil { t.Fatalf("UpdateOwner failed: %v", err) } if updated.ID != "owner-001" { t.Errorf("expected ID owner-001, got %s", updated.ID) } if updated.Name != "Alice Johnson" { t.Errorf("expected updated name Alice Johnson, got %s", updated.Name) } // Verify in repo stored := ownerRepo.owners["owner-001"] if stored.Email != "alice.j@example.com" { t.Errorf("expected updated email alice.j@example.com, got %s", stored.Email) } // Note: handler interface method does NOT record audit events if len(auditRepo.Events) != 0 { t.Errorf("expected 0 audit events from handler interface method, got %d", len(auditRepo.Events)) } } // TestOwnerService_DeleteOwner_HandlerInterface tests the handler interface method DeleteOwner. func TestOwnerService_DeleteOwner_HandlerInterface(t *testing.T) { now := time.Now() owner := &domain.Owner{ ID: "owner-001", Name: "Alice Smith", Email: "alice@example.com", TeamID: "team-001", CreatedAt: now, UpdatedAt: now, } ownerRepo := newMockOwnerRepository() ownerRepo.AddOwner(owner) auditRepo := newMockAuditRepository() auditService := NewAuditService(auditRepo) ownerService := NewOwnerService(ownerRepo, auditService) err := ownerService.DeleteOwner(context.Background(), "owner-001") if err != nil { t.Fatalf("DeleteOwner failed: %v", err) } if len(ownerRepo.owners) != 0 { t.Errorf("expected 0 owners in repo after delete, got %d", len(ownerRepo.owners)) } // Note: handler interface method does NOT record audit events if len(auditRepo.Events) != 0 { t.Errorf("expected 0 audit events from handler interface method, got %d", len(auditRepo.Events)) } }