Files
certctl/internal/service/bulk_reassignment_test.go
T
shankar0123 7cb453a336 chore(fmt): repo-wide gofmt -w sweep — close drift surfaced by ci-pipeline-cleanup Phase 4
Mechanical reformat. The new 'gofmt drift' CI step (added in
ci-pipeline-cleanup Phase 4, commit 0f205a8) surfaced 111 files
with accumulated gofmt drift across cmd/, internal/, and deploy/test/.

Each file's diff is gofmt-standard: whitespace adjustments, intra-
group import sorting (alphabetical by import path within blank-line-
separated groups), and struct-tag column alignment. No semantic
changes — verified via 'git diff --ignore-all-space' which shows only
the line-position deltas from import reordering.

The gate stays in place after this commit. Going forward it catches
gofmt drift at PR time.
2026-04-30 22:33:57 +00:00

222 lines
8.1 KiB
Go

package service
import (
"context"
"errors"
"log/slog"
"testing"
"time"
"github.com/shankar0123/certctl/internal/domain"
)
func newBulkReassignmentTestService() (*BulkReassignmentService, *mockCertRepo, *mockOwnerRepo, *mockAuditRepo) {
certRepo := newMockCertificateRepository()
ownerRepo := newMockOwnerRepository()
auditRepo := newMockAuditRepository()
auditService := NewAuditService(auditRepo)
svc := NewBulkReassignmentService(certRepo, ownerRepo, auditService, slog.Default())
return svc, certRepo, ownerRepo, auditRepo
}
// addOwnedCert seeds a cert with a specific owner+team for reassignment.
func addOwnedCert(repo *mockCertRepo, id, ownerID, teamID string) {
cert := &domain.ManagedCertificate{
ID: id, CommonName: id, Status: domain.CertificateStatusActive,
OwnerID: ownerID, TeamID: teamID,
ExpiresAt: time.Now().AddDate(0, 1, 0),
}
repo.AddCert(cert)
}
func addOwner(repo *mockOwnerRepo, id string) {
repo.owners[id] = &domain.Owner{ID: id, Name: id}
}
// TestBulkReassign_HappyPath — N certs all reassigned successfully.
func TestBulkReassign_HappyPath(t *testing.T) {
svc, certRepo, ownerRepo, _ := newBulkReassignmentTestService()
addOwner(ownerRepo, "o-bob")
addOwnedCert(certRepo, "mc-1", "o-alice", "")
addOwnedCert(certRepo, "mc-2", "o-alice", "")
addOwnedCert(certRepo, "mc-3", "o-alice", "")
res, err := svc.BulkReassign(context.Background(),
domain.BulkReassignmentRequest{
CertificateIDs: []string{"mc-1", "mc-2", "mc-3"},
OwnerID: "o-bob",
}, "admin")
if err != nil {
t.Fatalf("BulkReassign failed: %v", err)
}
if res.TotalReassigned != 3 || res.TotalSkipped != 0 || res.TotalFailed != 0 {
t.Errorf("counts = reassigned:%d skipped:%d failed:%d, want 3/0/0",
res.TotalReassigned, res.TotalSkipped, res.TotalFailed)
}
for _, id := range []string{"mc-1", "mc-2", "mc-3"} {
if certRepo.Certs[id].OwnerID != "o-bob" {
t.Errorf("cert %s: owner_id = %s, want o-bob", id, certRepo.Certs[id].OwnerID)
}
}
}
// TestBulkReassign_SkipsAlreadyOwned — certs already owned by the
// target are no-op-skipped (not counted as reassigned, not surfaced as
// errors). Operator sees "5 of your 10 selections were no-ops because
// Bob already owned them" without triaging fake errors.
func TestBulkReassign_SkipsAlreadyOwned(t *testing.T) {
svc, certRepo, ownerRepo, _ := newBulkReassignmentTestService()
addOwner(ownerRepo, "o-bob")
addOwnedCert(certRepo, "mc-1", "o-bob", "") // already owned by target
addOwnedCert(certRepo, "mc-2", "o-alice", "") // needs reassign
res, err := svc.BulkReassign(context.Background(),
domain.BulkReassignmentRequest{
CertificateIDs: []string{"mc-1", "mc-2"},
OwnerID: "o-bob",
}, "admin")
if err != nil {
t.Fatalf("BulkReassign failed: %v", err)
}
if res.TotalReassigned != 1 || res.TotalSkipped != 1 {
t.Errorf("counts = reassigned:%d skipped:%d, want 1/1", res.TotalReassigned, res.TotalSkipped)
}
if len(res.Errors) != 0 {
t.Errorf("already-owned skip should NOT populate Errors; got %v", res.Errors)
}
}
// TestBulkReassign_OwnerIDRequired_Error — empty owner_id rejected.
func TestBulkReassign_OwnerIDRequired_Error(t *testing.T) {
svc, certRepo, _, _ := newBulkReassignmentTestService()
addOwnedCert(certRepo, "mc-1", "o-alice", "")
_, err := svc.BulkReassign(context.Background(),
domain.BulkReassignmentRequest{CertificateIDs: []string{"mc-1"}, OwnerID: ""}, "admin")
if err == nil {
t.Fatal("expected error for empty owner_id, got nil")
}
}
// TestBulkReassign_EmptyIDs_Error — empty IDs rejected.
func TestBulkReassign_EmptyIDs_Error(t *testing.T) {
svc, _, ownerRepo, _ := newBulkReassignmentTestService()
addOwner(ownerRepo, "o-bob")
_, err := svc.BulkReassign(context.Background(),
domain.BulkReassignmentRequest{CertificateIDs: []string{}, OwnerID: "o-bob"}, "admin")
if err == nil {
t.Fatal("expected error for empty IDs, got nil")
}
}
// TestBulkReassign_OwnerNotFound_TypedSentinel — non-existent OwnerID
// returns ErrBulkReassignOwnerNotFound. Handler maps this to 400 (the
// operator picked an owner that doesn't exist) rather than 500 (server
// error). Sentinel-error rather than substring-error matches the
// project's post-M-1 error-mapping convention.
func TestBulkReassign_OwnerNotFound_TypedSentinel(t *testing.T) {
svc, certRepo, _, _ := newBulkReassignmentTestService()
addOwnedCert(certRepo, "mc-1", "o-alice", "")
_, err := svc.BulkReassign(context.Background(),
domain.BulkReassignmentRequest{CertificateIDs: []string{"mc-1"}, OwnerID: "o-ghost"}, "admin")
if err == nil {
t.Fatal("expected ErrBulkReassignOwnerNotFound, got nil")
}
if !errors.Is(err, ErrBulkReassignOwnerNotFound) {
t.Errorf("err is not ErrBulkReassignOwnerNotFound; got: %v", err)
}
}
// TestBulkReassign_TeamIDOptional — happy path WITHOUT team_id leaves
// team_id unchanged. Empty team_id in request must not zero out the
// existing team_id on the cert.
func TestBulkReassign_TeamIDOptional(t *testing.T) {
svc, certRepo, ownerRepo, _ := newBulkReassignmentTestService()
addOwner(ownerRepo, "o-bob")
addOwnedCert(certRepo, "mc-1", "o-alice", "t-platform")
_, err := svc.BulkReassign(context.Background(),
domain.BulkReassignmentRequest{
CertificateIDs: []string{"mc-1"},
OwnerID: "o-bob",
// TeamID intentionally omitted
}, "admin")
if err != nil {
t.Fatalf("BulkReassign failed: %v", err)
}
if certRepo.Certs["mc-1"].TeamID != "t-platform" {
t.Errorf("team_id was zeroed out; want unchanged 't-platform', got %q", certRepo.Certs["mc-1"].TeamID)
}
}
// TestBulkReassign_TeamIDProvided_Updates — when TeamID is non-empty in
// the request, both owner_id and team_id update.
func TestBulkReassign_TeamIDProvided_Updates(t *testing.T) {
svc, certRepo, ownerRepo, _ := newBulkReassignmentTestService()
addOwner(ownerRepo, "o-bob")
addOwnedCert(certRepo, "mc-1", "o-alice", "t-platform")
_, err := svc.BulkReassign(context.Background(),
domain.BulkReassignmentRequest{
CertificateIDs: []string{"mc-1"},
OwnerID: "o-bob",
TeamID: "t-security",
}, "admin")
if err != nil {
t.Fatalf("BulkReassign failed: %v", err)
}
if certRepo.Certs["mc-1"].TeamID != "t-security" {
t.Errorf("team_id = %q, want t-security", certRepo.Certs["mc-1"].TeamID)
}
}
// TestBulkReassign_PartialFailure — N=3, one cert mid-batch hits an
// Update error. Rest of the batch continues; failure surfaced in
// Errors.
func TestBulkReassign_PartialFailure(t *testing.T) {
svc, certRepo, ownerRepo, _ := newBulkReassignmentTestService()
addOwner(ownerRepo, "o-bob")
addOwnedCert(certRepo, "mc-1", "o-alice", "")
addOwnedCert(certRepo, "mc-2", "o-alice", "")
addOwnedCert(certRepo, "mc-3", "o-alice", "")
// Force the next Update to fail uniformly. Mirrors how
// TestBulkRevoke_PartialFailure injects a downstream failure.
certRepo.UpdateErr = errors.New("simulated DB outage")
res, err := svc.BulkReassign(context.Background(),
domain.BulkReassignmentRequest{
CertificateIDs: []string{"mc-1", "mc-2", "mc-3"},
OwnerID: "o-bob",
}, "admin")
if err != nil {
t.Fatalf("BulkReassign should not propagate per-cert errors; got: %v", err)
}
if res.TotalFailed != 3 || res.TotalReassigned != 0 {
t.Errorf("counts = failed:%d reassigned:%d, want 3/0", res.TotalFailed, res.TotalReassigned)
}
}
// TestBulkReassign_AuditEventEmitted — single bulk audit event.
func TestBulkReassign_AuditEventEmitted(t *testing.T) {
svc, certRepo, ownerRepo, auditRepo := newBulkReassignmentTestService()
addOwner(ownerRepo, "o-bob")
addOwnedCert(certRepo, "mc-1", "o-alice", "")
addOwnedCert(certRepo, "mc-2", "o-alice", "")
_, err := svc.BulkReassign(context.Background(),
domain.BulkReassignmentRequest{
CertificateIDs: []string{"mc-1", "mc-2"},
OwnerID: "o-bob",
}, "admin")
if err != nil {
t.Fatalf("BulkReassign failed: %v", err)
}
if len(auditRepo.Events) != 1 {
t.Errorf("audit events count = %d, want exactly 1 (one bulk event, NOT N per-cert events)", len(auditRepo.Events))
}
if len(auditRepo.Events) > 0 && auditRepo.Events[0].Action != "bulk_reassignment_initiated" {
t.Errorf("audit action = %q, want 'bulk_reassignment_initiated'", auditRepo.Events[0].Action)
}
}