Files
certctl/internal/service/team_test.go
T
shankar0123 8b75e0311b chore: rename Go module path to github.com/certctl-io/certctl
Mechanical sed across the main go.mod's module declaration, the f5-mock-icontrol
sub-module's go.mod, every Go file's import path (361 files), and a rebuild of
the checked-in f5-mock-icontrol binary so its embedded build-info reflects the
new module path. No behavior change.

Choice B from cowork/transfer-certctl-to-org.md, executed 2026-05-04. Choice A
(keep module path declared as github.com/shankar0123/certctl regardless of
repo URL) shipped on the day of the org transfer (2026-05-03) since we had no
external Go consumers; this commit closes that deferral.

Backward-compat: GitHub HTTP redirects continue to forward
github.com/shankar0123/certctl → github.com/certctl-io/certctl at the URL
level, but Go's module proxy uses the path declared in go.mod as the
canonical name. Pre-fix, anyone trying `go get github.com/certctl-io/certctl/...`
hit a "module path mismatch" error because go.mod said
github.com/shankar0123/certctl and the URL they fetched it from said
certctl-io/certctl. Post-fix, the canonical name and the URL agree, so
go get / go install / external Go consumers / Go-tooling integrations
work cleanly via either the new path (preferred) or the old path (which
redirects and Go follows the redirect for source fetch).

Anyone still importing the old path inside their own code keeps working
provided they update their go.mod's `require` line to match — the module
path declared in their consumer's go.sum / go.mod is the authoritative
import name, so a mass sed across their import statements is the migration
on the consumer side. No external consumers exist today.

Diff shape:
  361 *.go files  — import path replacement only
    2 go.mod     — module declaration replacement only
    1 binary     — deploy/test/f5-mock-icontrol/f5-mock-icontrol rebuilt
                   so embedded build-info reflects the new path (8618965 vs
                   8618933 bytes; 32-byte diff is the build-info change)

  Total: 364 files, 730 insertions / 730 deletions, net-zero size, pure
  mechanical substitution.

Verification:
  gofmt: 17 files needed re-alignment after sed (the new path is one char
    shorter than the old, so column-aligned import groups drifted). Applied
    `gofmt -w` to fix.
  go mod tidy: clean exit on both modules.
  go vet ./...: clean exit.
  go build ./...: clean exit.
  go test -short -count=1 on representative packages: all green
    (internal/domain, internal/validation, internal/crypto, internal/crypto/signer,
    cmd/agent). Test output now reads `ok github.com/certctl-io/certctl/...`
    confirming the module path resolves correctly.
  binary: f5-mock-icontrol rebuilt; `strings | grep shankar0123` returns
    nothing; `strings | grep certctl-io/certctl` shows the new module path
    embedded in build-info.

Files intentionally NOT touched in this commit:
  README.md / CHANGELOG.md / docs/ / etc. — already swept to certctl-io
    URLs in commit 0729ee4 (the post-transfer URL refresh). This commit is
    purely the Go-tooling layer.
  Scarf pixels (`shankar0123.docker.scarf.sh/...`) — Scarf-account
    namespace, not a Go import or GitHub repo URL. Stays.

This is a non-blocking, non-customer-impacting change. Operators pulling
container images, running `make verify`, hitting the API, or installing the
agent see no functional difference. Only Go-tooling consumers (none today)
are affected, and they're enabled — not broken — by this commit.
2026-05-04 00:30:29 +00:00

691 lines
18 KiB
Go

package service
import (
"context"
"errors"
"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
}
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")
}
}