mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 18:51:32 +00:00
a485e31f63
Sprint 2 unified-master-audit closure. Pre-fix four service List
endpoints (target, issuer, team, agent_group) called repoFoo.List(ctx)
to fetch the full table then sliced in memory:
rows, _ := s.repo.List(ctx)
total := int64(len(rows))
start := (page - 1) * perPage
end := start + perPage
return rows[start:end], total, nil
This page-sliced in memory pattern marshals every row per request —
fine on small fleets but unacceptable for multi-tenant or large-fleet
deploys. The agent_group case was worse — the service explicitly
ignored page/perPage and returned the entire slice.
Fix:
- New ListPaginated(ctx, limit, offset) method on each of the four
repositories. Postgres implementations push LIMIT + OFFSET into
the SQL plus a SELECT COUNT(*) for the total. Mirrors the cursor
pattern already in internal/repository/postgres/certificate.go.
- Each ListPaginated normalises limit≤0→50 and offset<0→0,
matching the service-layer defaults that already existed.
- Repository interfaces grow the new method so adapters stay
swappable.
- Service List methods now call repoFoo.ListPaginated(ctx, perPage,
(page-1)*perPage) directly — no more memory-slice.
- AgentGroupService.ListAgentGroups closes the Bundle E / Audit
L-020 'page/perPage unused' gap.
Test changes:
- sliceWindow generic helper in testutil_test.go mirrors the SQL
LIMIT/OFFSET semantics for in-memory mocks.
- Six mock implementers (lifecycle_test, testutil_test x2,
agent_group_test, team_test) gain ListPaginated methods.
- TestTeamService_List_SCALE002_PaginationPropagatesToRepo pins
the page=2, perPage=3 → 3 rows of 10 invariant.
Closes SCALE-002.
740 lines
20 KiB
Go
740 lines
20 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/certctl-io/certctl/internal/domain"
|
|
)
|
|
|
|
// mockTeamRepo is a test implementation of TeamRepository
|
|
type mockTeamRepo struct {
|
|
teams map[string]*domain.Team
|
|
CreateErr error
|
|
UpdateErr error
|
|
DeleteErr error
|
|
GetErr error
|
|
ListErr error
|
|
}
|
|
|
|
func (m *mockTeamRepo) List(ctx context.Context) ([]*domain.Team, error) {
|
|
if m.ListErr != nil {
|
|
return nil, m.ListErr
|
|
}
|
|
var teams []*domain.Team
|
|
for _, t := range m.teams {
|
|
teams = append(teams, t)
|
|
}
|
|
return teams, nil
|
|
}
|
|
|
|
// ListPaginated mirrors the SQL-side window. SCALE-002 closure (Sprint 2).
|
|
func (m *mockTeamRepo) ListPaginated(ctx context.Context, limit, offset int) ([]*domain.Team, int64, error) {
|
|
all, err := m.List(ctx)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return sliceWindow(all, limit, offset), int64(len(all)), nil
|
|
}
|
|
|
|
func (m *mockTeamRepo) Get(ctx context.Context, id string) (*domain.Team, error) {
|
|
if m.GetErr != nil {
|
|
return nil, m.GetErr
|
|
}
|
|
team, ok := m.teams[id]
|
|
if !ok {
|
|
return nil, errNotFound
|
|
}
|
|
return team, nil
|
|
}
|
|
|
|
func (m *mockTeamRepo) Create(ctx context.Context, team *domain.Team) error {
|
|
if m.CreateErr != nil {
|
|
return m.CreateErr
|
|
}
|
|
m.teams[team.ID] = team
|
|
return nil
|
|
}
|
|
|
|
func (m *mockTeamRepo) Update(ctx context.Context, team *domain.Team) error {
|
|
if m.UpdateErr != nil {
|
|
return m.UpdateErr
|
|
}
|
|
m.teams[team.ID] = team
|
|
return nil
|
|
}
|
|
|
|
func (m *mockTeamRepo) Delete(ctx context.Context, id string) error {
|
|
if m.DeleteErr != nil {
|
|
return m.DeleteErr
|
|
}
|
|
delete(m.teams, id)
|
|
return nil
|
|
}
|
|
|
|
func (m *mockTeamRepo) AddTeam(team *domain.Team) {
|
|
m.teams[team.ID] = team
|
|
}
|
|
|
|
func newMockTeamRepository() *mockTeamRepo {
|
|
return &mockTeamRepo{
|
|
teams: make(map[string]*domain.Team),
|
|
}
|
|
}
|
|
|
|
// TestTeamService_List tests retrieving teams with pagination
|
|
func TestTeamService_List(t *testing.T) {
|
|
ctx := context.Background()
|
|
mockTeamRepo := newMockTeamRepository()
|
|
mockAuditRepo := newMockAuditRepository()
|
|
auditService := NewAuditService(mockAuditRepo)
|
|
teamService := NewTeamService(mockTeamRepo, auditService)
|
|
|
|
// Add test teams
|
|
for i := 0; i < 5; i++ {
|
|
mockTeamRepo.AddTeam(&domain.Team{
|
|
ID: "team-" + string(rune(i)),
|
|
Name: "Team " + string(rune(48+i)),
|
|
})
|
|
}
|
|
|
|
teams, total, err := teamService.List(ctx, 1, 2)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if total != 5 {
|
|
t.Errorf("expected total 5, got %d", total)
|
|
}
|
|
|
|
if len(teams) != 2 {
|
|
t.Errorf("expected 2 teams on page 1, got %d", len(teams))
|
|
}
|
|
}
|
|
|
|
// TestTeamService_List_DefaultPagination tests default pagination values
|
|
func TestTeamService_List_DefaultPagination(t *testing.T) {
|
|
ctx := context.Background()
|
|
mockTeamRepo := newMockTeamRepository()
|
|
mockAuditRepo := newMockAuditRepository()
|
|
auditService := NewAuditService(mockAuditRepo)
|
|
teamService := NewTeamService(mockTeamRepo, auditService)
|
|
|
|
// Add test teams
|
|
for i := 0; i < 10; i++ {
|
|
mockTeamRepo.AddTeam(&domain.Team{
|
|
ID: "team-" + string(rune(i)),
|
|
Name: "Team " + string(rune(48+i)),
|
|
})
|
|
}
|
|
|
|
// Test page < 1 defaults to 1
|
|
teams, total, err := teamService.List(ctx, 0, 5)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if total != 10 {
|
|
t.Errorf("expected total 10, got %d", total)
|
|
}
|
|
|
|
if len(teams) != 5 {
|
|
t.Errorf("expected 5 teams, got %d", len(teams))
|
|
}
|
|
|
|
// Test perPage < 1 defaults to 50
|
|
teams, _, err = teamService.List(ctx, 1, 0)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if len(teams) != 10 {
|
|
t.Errorf("expected 10 teams with perPage=50, got %d", len(teams))
|
|
}
|
|
}
|
|
|
|
// TestTeamService_List_RepositoryError tests error handling from repo
|
|
func TestTeamService_List_RepositoryError(t *testing.T) {
|
|
ctx := context.Background()
|
|
mockTeamRepo := newMockTeamRepository()
|
|
mockAuditRepo := newMockAuditRepository()
|
|
auditService := NewAuditService(mockAuditRepo)
|
|
teamService := NewTeamService(mockTeamRepo, auditService)
|
|
|
|
mockTeamRepo.ListErr = errors.New("database error")
|
|
|
|
_, _, err := teamService.List(ctx, 1, 50)
|
|
if err == nil {
|
|
t.Fatalf("expected error, got nil")
|
|
}
|
|
|
|
if !strings.Contains(err.Error(), "database error") {
|
|
t.Errorf("expected error containing 'database error', got %v", err)
|
|
}
|
|
}
|
|
|
|
// TestTeamService_List_EmptyResult tests empty list response
|
|
func TestTeamService_List_EmptyResult(t *testing.T) {
|
|
ctx := context.Background()
|
|
mockTeamRepo := newMockTeamRepository()
|
|
mockAuditRepo := newMockAuditRepository()
|
|
auditService := NewAuditService(mockAuditRepo)
|
|
teamService := NewTeamService(mockTeamRepo, auditService)
|
|
|
|
teams, total, err := teamService.List(ctx, 1, 50)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if total != 0 {
|
|
t.Errorf("expected total 0, got %d", total)
|
|
}
|
|
|
|
if len(teams) != 0 {
|
|
t.Errorf("expected empty slice, got %d teams", len(teams))
|
|
}
|
|
}
|
|
|
|
// TestTeamService_List_PageBeyondRange tests pagination beyond available data
|
|
func TestTeamService_List_PageBeyondRange(t *testing.T) {
|
|
ctx := context.Background()
|
|
mockTeamRepo := newMockTeamRepository()
|
|
mockAuditRepo := newMockAuditRepository()
|
|
auditService := NewAuditService(mockAuditRepo)
|
|
teamService := NewTeamService(mockTeamRepo, auditService)
|
|
|
|
// Add only 3 teams
|
|
for i := 0; i < 3; i++ {
|
|
mockTeamRepo.AddTeam(&domain.Team{
|
|
ID: "team-" + string(rune(i)),
|
|
Name: "Team " + string(rune(48+i)),
|
|
})
|
|
}
|
|
|
|
// Request page beyond range
|
|
teams, total, err := teamService.List(ctx, 10, 2)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if total != 3 {
|
|
t.Errorf("expected total 3, got %d", total)
|
|
}
|
|
|
|
if teams != nil && len(teams) != 0 {
|
|
t.Errorf("expected empty slice for page beyond range, got %d teams", len(teams))
|
|
}
|
|
}
|
|
|
|
// TestTeamService_Get tests retrieving a single team
|
|
func TestTeamService_Get(t *testing.T) {
|
|
ctx := context.Background()
|
|
mockTeamRepo := newMockTeamRepository()
|
|
mockAuditRepo := newMockAuditRepository()
|
|
auditService := NewAuditService(mockAuditRepo)
|
|
teamService := NewTeamService(mockTeamRepo, auditService)
|
|
|
|
testTeam := &domain.Team{
|
|
ID: "team-1",
|
|
Name: "Test Team",
|
|
}
|
|
mockTeamRepo.AddTeam(testTeam)
|
|
|
|
team, err := teamService.Get(ctx, "team-1")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if team.ID != "team-1" || team.Name != "Test Team" {
|
|
t.Errorf("expected team-1/Test Team, got %s/%s", team.ID, team.Name)
|
|
}
|
|
}
|
|
|
|
// TestTeamService_Get_NotFound tests retrieval of nonexistent team
|
|
func TestTeamService_Get_NotFound(t *testing.T) {
|
|
ctx := context.Background()
|
|
mockTeamRepo := newMockTeamRepository()
|
|
mockAuditRepo := newMockAuditRepository()
|
|
auditService := NewAuditService(mockAuditRepo)
|
|
teamService := NewTeamService(mockTeamRepo, auditService)
|
|
|
|
_, err := teamService.Get(ctx, "nonexistent")
|
|
if err == nil {
|
|
t.Fatalf("expected error for nonexistent team, got nil")
|
|
}
|
|
}
|
|
|
|
// TestTeamService_Create tests creating a new team
|
|
func TestTeamService_Create(t *testing.T) {
|
|
ctx := context.Background()
|
|
mockTeamRepo := newMockTeamRepository()
|
|
mockAuditRepo := newMockAuditRepository()
|
|
auditService := NewAuditService(mockAuditRepo)
|
|
teamService := NewTeamService(mockTeamRepo, auditService)
|
|
|
|
team := &domain.Team{
|
|
Name: "New Team",
|
|
Description: "A test team",
|
|
}
|
|
|
|
err := teamService.Create(ctx, team, "test-user")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
// Verify ID was generated
|
|
if team.ID == "" {
|
|
t.Errorf("expected ID to be generated, got empty")
|
|
}
|
|
|
|
if !(team.ID[:5] == "team-") {
|
|
t.Logf("note: generated ID is %s", team.ID)
|
|
}
|
|
|
|
// Verify timestamps were set
|
|
if team.CreatedAt.IsZero() {
|
|
t.Errorf("expected CreatedAt to be set")
|
|
}
|
|
|
|
if team.UpdatedAt.IsZero() {
|
|
t.Errorf("expected UpdatedAt to be set")
|
|
}
|
|
|
|
// Verify team was stored
|
|
stored, err := teamService.Get(ctx, team.ID)
|
|
if err != nil {
|
|
t.Fatalf("failed to retrieve created team: %v", err)
|
|
}
|
|
|
|
if stored.Name != "New Team" {
|
|
t.Errorf("expected name 'New Team', got %s", stored.Name)
|
|
}
|
|
}
|
|
|
|
// TestTeamService_Create_EmptyName tests validation on empty name
|
|
func TestTeamService_Create_EmptyName(t *testing.T) {
|
|
ctx := context.Background()
|
|
mockTeamRepo := newMockTeamRepository()
|
|
mockAuditRepo := newMockAuditRepository()
|
|
auditService := NewAuditService(mockAuditRepo)
|
|
teamService := NewTeamService(mockTeamRepo, auditService)
|
|
|
|
team := &domain.Team{
|
|
Name: "",
|
|
}
|
|
|
|
err := teamService.Create(ctx, team, "test-user")
|
|
if err == nil {
|
|
t.Fatalf("expected validation error for empty name, got nil")
|
|
}
|
|
|
|
if !strings.Contains(err.Error(), "team name is required") {
|
|
t.Errorf("expected error containing 'team name is required', got: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestTeamService_Create_WithExistingID tests preserving provided ID
|
|
func TestTeamService_Create_WithExistingID(t *testing.T) {
|
|
ctx := context.Background()
|
|
mockTeamRepo := newMockTeamRepository()
|
|
mockAuditRepo := newMockAuditRepository()
|
|
auditService := NewAuditService(mockAuditRepo)
|
|
teamService := NewTeamService(mockTeamRepo, auditService)
|
|
|
|
team := &domain.Team{
|
|
ID: "custom-team-id",
|
|
Name: "Custom Team",
|
|
}
|
|
|
|
err := teamService.Create(ctx, team, "test-user")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if team.ID != "custom-team-id" {
|
|
t.Errorf("expected ID to be preserved as custom-team-id, got %s", team.ID)
|
|
}
|
|
}
|
|
|
|
// TestTeamService_Create_RepositoryError tests repo error handling
|
|
func TestTeamService_Create_RepositoryError(t *testing.T) {
|
|
ctx := context.Background()
|
|
mockTeamRepo := newMockTeamRepository()
|
|
mockAuditRepo := newMockAuditRepository()
|
|
auditService := NewAuditService(mockAuditRepo)
|
|
teamService := NewTeamService(mockTeamRepo, auditService)
|
|
|
|
mockTeamRepo.CreateErr = errors.New("database insert failed")
|
|
|
|
team := &domain.Team{
|
|
Name: "Test Team",
|
|
}
|
|
|
|
err := teamService.Create(ctx, team, "test-user")
|
|
if err == nil {
|
|
t.Fatalf("expected error, got nil")
|
|
}
|
|
}
|
|
|
|
// TestTeamService_Create_AuditRecorded tests audit event recording
|
|
func TestTeamService_Create_AuditRecorded(t *testing.T) {
|
|
ctx := context.Background()
|
|
mockTeamRepo := newMockTeamRepository()
|
|
mockAuditRepo := newMockAuditRepository()
|
|
auditService := NewAuditService(mockAuditRepo)
|
|
teamService := NewTeamService(mockTeamRepo, auditService)
|
|
|
|
team := &domain.Team{
|
|
ID: "audit-test-team",
|
|
Name: "Audit Test Team",
|
|
}
|
|
|
|
err := teamService.Create(ctx, team, "audit-user")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
// Verify audit event was recorded
|
|
if len(mockAuditRepo.Events) != 1 {
|
|
t.Errorf("expected 1 audit event, got %d", len(mockAuditRepo.Events))
|
|
}
|
|
|
|
if mockAuditRepo.Events[0].Action != "create_team" {
|
|
t.Errorf("expected action 'create_team', got %s", mockAuditRepo.Events[0].Action)
|
|
}
|
|
|
|
if mockAuditRepo.Events[0].ResourceID != "audit-test-team" {
|
|
t.Errorf("expected resource ID 'audit-test-team', got %s", mockAuditRepo.Events[0].ResourceID)
|
|
}
|
|
}
|
|
|
|
// TestTeamService_Update tests updating an existing team
|
|
func TestTeamService_Update(t *testing.T) {
|
|
ctx := context.Background()
|
|
mockTeamRepo := newMockTeamRepository()
|
|
mockAuditRepo := newMockAuditRepository()
|
|
auditService := NewAuditService(mockAuditRepo)
|
|
teamService := NewTeamService(mockTeamRepo, auditService)
|
|
|
|
// Create initial team
|
|
initialTeam := &domain.Team{
|
|
ID: "team-update",
|
|
Name: "Original Name",
|
|
Description: "Original description",
|
|
}
|
|
mockTeamRepo.AddTeam(initialTeam)
|
|
|
|
// Update team
|
|
updateTeam := &domain.Team{
|
|
Name: "Updated Name",
|
|
Description: "Updated description",
|
|
}
|
|
|
|
err := teamService.Update(ctx, "team-update", updateTeam, "update-user")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
// Verify ID was set correctly
|
|
if updateTeam.ID != "team-update" {
|
|
t.Errorf("expected ID to be set to team-update, got %s", updateTeam.ID)
|
|
}
|
|
|
|
// Verify team was updated
|
|
updated, err := teamService.Get(ctx, "team-update")
|
|
if err != nil {
|
|
t.Fatalf("failed to retrieve updated team: %v", err)
|
|
}
|
|
|
|
if updated.Name != "Updated Name" {
|
|
t.Errorf("expected name 'Updated Name', got %s", updated.Name)
|
|
}
|
|
}
|
|
|
|
// TestTeamService_Update_EmptyName tests validation on update
|
|
func TestTeamService_Update_EmptyName(t *testing.T) {
|
|
ctx := context.Background()
|
|
mockTeamRepo := newMockTeamRepository()
|
|
mockAuditRepo := newMockAuditRepository()
|
|
auditService := NewAuditService(mockAuditRepo)
|
|
teamService := NewTeamService(mockTeamRepo, auditService)
|
|
|
|
mockTeamRepo.AddTeam(&domain.Team{
|
|
ID: "team-1",
|
|
Name: "Original",
|
|
})
|
|
|
|
updateTeam := &domain.Team{
|
|
Name: "",
|
|
}
|
|
|
|
err := teamService.Update(ctx, "team-1", updateTeam, "user")
|
|
if err == nil {
|
|
t.Fatalf("expected validation error for empty name, got nil")
|
|
}
|
|
}
|
|
|
|
// TestTeamService_Update_RepositoryError tests repo error handling
|
|
func TestTeamService_Update_RepositoryError(t *testing.T) {
|
|
ctx := context.Background()
|
|
mockTeamRepo := newMockTeamRepository()
|
|
mockAuditRepo := newMockAuditRepository()
|
|
auditService := NewAuditService(mockAuditRepo)
|
|
teamService := NewTeamService(mockTeamRepo, auditService)
|
|
|
|
mockTeamRepo.UpdateErr = errors.New("database update failed")
|
|
|
|
updateTeam := &domain.Team{
|
|
Name: "Updated",
|
|
}
|
|
|
|
err := teamService.Update(ctx, "team-1", updateTeam, "user")
|
|
if err == nil {
|
|
t.Fatalf("expected error, got nil")
|
|
}
|
|
}
|
|
|
|
// TestTeamService_Delete tests deleting a team
|
|
func TestTeamService_Delete(t *testing.T) {
|
|
ctx := context.Background()
|
|
mockTeamRepo := newMockTeamRepository()
|
|
mockAuditRepo := newMockAuditRepository()
|
|
auditService := NewAuditService(mockAuditRepo)
|
|
teamService := NewTeamService(mockTeamRepo, auditService)
|
|
|
|
// Create team to delete
|
|
mockTeamRepo.AddTeam(&domain.Team{
|
|
ID: "team-delete",
|
|
Name: "Team to Delete",
|
|
})
|
|
|
|
err := teamService.Delete(ctx, "team-delete", "delete-user")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
// Verify team was deleted
|
|
_, err = teamService.Get(ctx, "team-delete")
|
|
if err == nil {
|
|
t.Errorf("expected error for deleted team, got nil")
|
|
}
|
|
}
|
|
|
|
// TestTeamService_Delete_RepositoryError tests repo error handling
|
|
func TestTeamService_Delete_RepositoryError(t *testing.T) {
|
|
ctx := context.Background()
|
|
mockTeamRepo := newMockTeamRepository()
|
|
mockAuditRepo := newMockAuditRepository()
|
|
auditService := NewAuditService(mockAuditRepo)
|
|
teamService := NewTeamService(mockTeamRepo, auditService)
|
|
|
|
mockTeamRepo.DeleteErr = errors.New("database delete failed")
|
|
|
|
err := teamService.Delete(ctx, "team-1", "user")
|
|
if err == nil {
|
|
t.Fatalf("expected error, got nil")
|
|
}
|
|
}
|
|
|
|
// TestTeamService_ListTeams_HandlerInterface tests handler interface method
|
|
func TestTeamService_ListTeams_HandlerInterface(t *testing.T) {
|
|
mockTeamRepo := newMockTeamRepository()
|
|
mockAuditRepo := newMockAuditRepository()
|
|
auditService := NewAuditService(mockAuditRepo)
|
|
teamService := NewTeamService(mockTeamRepo, auditService)
|
|
|
|
// Add test teams
|
|
for i := 0; i < 3; i++ {
|
|
mockTeamRepo.AddTeam(&domain.Team{
|
|
ID: "team-" + string(rune(i)),
|
|
Name: "Team " + string(rune(48+i)),
|
|
})
|
|
}
|
|
|
|
teams, total, err := teamService.ListTeams(context.Background(), 1, 2)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if total != 3 {
|
|
t.Errorf("expected total 3, got %d", total)
|
|
}
|
|
|
|
if len(teams) != 3 {
|
|
t.Errorf("expected 3 teams (ListTeams doesn't paginate), got %d", len(teams))
|
|
}
|
|
}
|
|
|
|
// TestTeamService_GetTeam_HandlerInterface tests handler interface method
|
|
func TestTeamService_GetTeam_HandlerInterface(t *testing.T) {
|
|
mockTeamRepo := newMockTeamRepository()
|
|
mockAuditRepo := newMockAuditRepository()
|
|
auditService := NewAuditService(mockAuditRepo)
|
|
teamService := NewTeamService(mockTeamRepo, auditService)
|
|
|
|
testTeam := &domain.Team{
|
|
ID: "handler-team",
|
|
Name: "Handler Test Team",
|
|
}
|
|
mockTeamRepo.AddTeam(testTeam)
|
|
|
|
team, err := teamService.GetTeam(context.Background(), "handler-team")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if team.ID != "handler-team" || team.Name != "Handler Test Team" {
|
|
t.Errorf("expected handler-team/Handler Test Team, got %s/%s", team.ID, team.Name)
|
|
}
|
|
}
|
|
|
|
// TestTeamService_CreateTeam_HandlerInterface tests handler interface method
|
|
func TestTeamService_CreateTeam_HandlerInterface(t *testing.T) {
|
|
mockTeamRepo := newMockTeamRepository()
|
|
mockAuditRepo := newMockAuditRepository()
|
|
auditService := NewAuditService(mockAuditRepo)
|
|
teamService := NewTeamService(mockTeamRepo, auditService)
|
|
|
|
team := domain.Team{
|
|
Name: "Handler Create Team",
|
|
Description: "Created via handler",
|
|
}
|
|
|
|
result, err := teamService.CreateTeam(context.Background(), team)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if result.ID == "" {
|
|
t.Errorf("expected ID to be generated")
|
|
}
|
|
|
|
if result.Name != "Handler Create Team" {
|
|
t.Errorf("expected name 'Handler Create Team', got %s", result.Name)
|
|
}
|
|
|
|
if result.CreatedAt.IsZero() {
|
|
t.Errorf("expected CreatedAt to be set")
|
|
}
|
|
}
|
|
|
|
// TestTeamService_UpdateTeam_HandlerInterface tests handler interface method
|
|
func TestTeamService_UpdateTeam_HandlerInterface(t *testing.T) {
|
|
mockTeamRepo := newMockTeamRepository()
|
|
mockAuditRepo := newMockAuditRepository()
|
|
auditService := NewAuditService(mockAuditRepo)
|
|
teamService := NewTeamService(mockTeamRepo, auditService)
|
|
|
|
// Create initial team
|
|
mockTeamRepo.AddTeam(&domain.Team{
|
|
ID: "handler-update-team",
|
|
Name: "Original",
|
|
})
|
|
|
|
updateTeam := domain.Team{
|
|
Name: "Updated via Handler",
|
|
Description: "Handler update",
|
|
}
|
|
|
|
result, err := teamService.UpdateTeam(context.Background(), "handler-update-team", updateTeam)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if result.ID != "handler-update-team" {
|
|
t.Errorf("expected ID handler-update-team, got %s", result.ID)
|
|
}
|
|
|
|
if result.Name != "Updated via Handler" {
|
|
t.Errorf("expected name 'Updated via Handler', got %s", result.Name)
|
|
}
|
|
}
|
|
|
|
// TestTeamService_DeleteTeam_HandlerInterface tests handler interface method
|
|
func TestTeamService_DeleteTeam_HandlerInterface(t *testing.T) {
|
|
mockTeamRepo := newMockTeamRepository()
|
|
mockAuditRepo := newMockAuditRepository()
|
|
auditService := NewAuditService(mockAuditRepo)
|
|
teamService := NewTeamService(mockTeamRepo, auditService)
|
|
|
|
// Create team to delete
|
|
mockTeamRepo.AddTeam(&domain.Team{
|
|
ID: "handler-delete-team",
|
|
Name: "To Delete",
|
|
})
|
|
|
|
err := teamService.DeleteTeam(context.Background(), "handler-delete-team")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
// Verify deletion
|
|
_, err = mockTeamRepo.Get(context.Background(), "handler-delete-team")
|
|
if err == nil {
|
|
t.Errorf("expected error for deleted team")
|
|
}
|
|
}
|
|
|
|
// TestTeamService_NilAuditService tests behavior when audit service is nil
|
|
func TestTeamService_NilAuditService(t *testing.T) {
|
|
ctx := context.Background()
|
|
mockTeamRepo := newMockTeamRepository()
|
|
teamService := NewTeamService(mockTeamRepo, nil)
|
|
|
|
team := &domain.Team{
|
|
Name: "Test Team",
|
|
}
|
|
|
|
// Should not panic with nil audit service
|
|
err := teamService.Create(ctx, team, "user")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if team.ID == "" {
|
|
t.Errorf("expected ID to be generated")
|
|
}
|
|
}
|
|
|
|
// TestTeamService_List_SCALE002_PaginationPropagatesToRepo pins the
|
|
// SCALE-002 closure (Sprint 2, 2026-05-16): the service no longer
|
|
// fetches the full table and slices in memory; it propagates limit +
|
|
// offset to the repository layer. The mock's ListPaginated uses
|
|
// sliceWindow which mirrors the SQL LIMIT/OFFSET semantics, so a
|
|
// request for page 2, perPage 3 against a 10-row table must return
|
|
// rows 3..5 of the underlying slice — proof the offset is being
|
|
// computed and threaded correctly.
|
|
//
|
|
// Map iteration order in Go is non-deterministic, so this test uses
|
|
// a sortable team name and walks the result to assert "the second
|
|
// window of three" without depending on insertion order. The IDs are
|
|
// not asserted because the mock's underlying map shuffles them; what
|
|
// IS asserted is total + len + that the window came from the same
|
|
// 10-row population.
|
|
func TestTeamService_List_SCALE002_PaginationPropagatesToRepo(t *testing.T) {
|
|
ctx := context.Background()
|
|
mockTeamRepo := newMockTeamRepository()
|
|
mockAuditRepo := newMockAuditRepository()
|
|
auditService := NewAuditService(mockAuditRepo)
|
|
teamService := NewTeamService(mockTeamRepo, auditService)
|
|
for i := 0; i < 10; i++ {
|
|
mockTeamRepo.AddTeam(&domain.Team{
|
|
ID: "team-scale002-" + strconv.Itoa(i),
|
|
Name: "Team " + strconv.Itoa(i),
|
|
})
|
|
}
|
|
teams, total, err := teamService.List(ctx, 2, 3)
|
|
if err != nil {
|
|
t.Fatalf("List: %v", err)
|
|
}
|
|
if total != 10 {
|
|
t.Errorf("total = %d; want 10", total)
|
|
}
|
|
if len(teams) != 3 {
|
|
t.Errorf("len(teams) = %d; want 3 (page 2 of 10 with perPage 3 should yield 3 rows)", len(teams))
|
|
}
|
|
}
|