mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 19:11:30 +00:00
refactor(handler,repo): replace strings.Contains error dispatch with typed sentinels (S-2)
Closes one 2026-04-24 audit finding (P2):
- cat-s6-efc7f6f6bd50: 30 strings.Contains(err.Error(), ...) sites
in internal/api/handler/ — brittle to repository-layer message
changes, untyped against the actual failure mode.
Approach (Option B from prompt design notes):
- New typed sentinels in internal/repository/errors.go:
ErrNotFound, ErrForeignKeyConstraint
IsForeignKeyError(err) helper (the only place substring
matching at the lib/pq boundary is allowed; isolates the
DB-driver string knowledge to one function).
- New typed sentinel in internal/domain/errors.go:
ErrValidation (reserved for future per-entity validation
wrappers; not yet used by all handlers).
- 49 sites in internal/repository/postgres/*.go updated to wrap
sql.ErrNoRows-derived errors via fmt.Errorf("...: %w",
repository.ErrNotFound).
- 18 not-found handler sites + 2 FK-constraint handler sites
refactored to errors.Is(err, repository.ErrNotFound) /
repository.IsForeignKeyError(err).
- 23 inline `fmt.Errorf("X not found")` test fixtures across
handler tests rewrapped to wrap repository.ErrNotFound.
- test_utils.go::ErrMockNotFound rewrapped to wrap
repository.ErrNotFound; renewal_policy.go closure docblock
updated to reflect the new convention.
- integration test mockJobRepository.Get wraps repository.ErrNotFound.
CI regression guardrail:
- .github/workflows/ci.yml::"Forbidden strings.Contains(err.Error())
regression guard (S-2)" greps for the three patterns ("not found",
"violates foreign key", "RESTRICT") under internal/api/handler/
and fails the build on regression.
Verification:
- go build ./... — clean
- go vet ./... — clean
- go test ./... -short -count=1 — all packages pass (handler +
repository + service + integration)
- golangci-lint v2.11.4 run ./... — 0 issues
- S-2 guardrail dry-run on post-fix tree → empty (good)
- All sibling guardrails (S-1, G-3, D-1+D-2, B-1, L-1, H-1, C-1, F-1, P-1) pass
Audit findings closed:
- cat-s6-efc7f6f6bd50 (P2)
Deferred follow-ups:
- 6 domain-specific substring patterns still inline in handlers
("cannot approve", "cannot reject", "cannot be parsed",
"no certificates found", "challenge password", "invalid"/
"required" validation chains in profiles + agent_groups). Each
needs its own typed sentinel, scoped per service. Documented
by the S-2 CI guardrail's allowlist for closure-comments only.
- Per-entity not-found sentinels (Option A — ErrCertificateNotFound,
ErrAgentNotFound, etc.) deferred. Generic ErrNotFound covers the
current dispatch needs; per-entity precision would let handlers
return entity-aware error bodies without a domain.Type field,
but not blocking.
This commit is contained in:
@@ -522,7 +522,7 @@ func TestRevokeCertificate_AlreadyRevoked(t *testing.T) {
|
||||
func TestRevokeCertificate_NotFound(t *testing.T) {
|
||||
handler, mock := newCertHandlerWithMock()
|
||||
mock.RevokeCertificateFn = func(_ context.Context, id string, reason string, _ string) error {
|
||||
return fmt.Errorf("certificate not found")
|
||||
return fmt.Errorf("certificate not found: %w", ErrMockNotFound)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/certificates/mc-missing/revoke", strings.NewReader(`{"reason":"keyCompromise"}`))
|
||||
|
||||
@@ -33,7 +33,7 @@ func (m *MockAgentGroupService) GetAgentGroup(_ context.Context, id string) (*do
|
||||
if m.GetAgentGroupFn != nil {
|
||||
return m.GetAgentGroupFn(id)
|
||||
}
|
||||
return nil, fmt.Errorf("not found")
|
||||
return nil, fmt.Errorf("not found: %w", ErrMockNotFound)
|
||||
}
|
||||
|
||||
func (m *MockAgentGroupService) CreateAgentGroup(_ context.Context, group domain.AgentGroup) (*domain.AgentGroup, error) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/shankar0123/certctl/internal/repository"
|
||||
"errors"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
@@ -160,7 +162,7 @@ func (h AgentGroupHandler) UpdateAgentGroup(w http.ResponseWriter, r *http.Reque
|
||||
|
||||
updated, err := h.svc.UpdateAgentGroup(r.Context(), id, group)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
if errors.Is(err, repository.ErrNotFound) {
|
||||
ErrorWithRequestID(w, http.StatusNotFound, "Agent group not found", requestID)
|
||||
return
|
||||
}
|
||||
@@ -188,7 +190,7 @@ func (h AgentGroupHandler) DeleteAgentGroup(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
|
||||
if err := h.svc.DeleteAgentGroup(r.Context(), id); err != nil {
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
if errors.Is(err, repository.ErrNotFound) {
|
||||
ErrorWithRequestID(w, http.StatusNotFound, "Agent group not found", requestID)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package handler
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
@@ -142,7 +141,9 @@ func TestRetireAgentHandler_Sentinel_403(t *testing.T) {
|
||||
func TestRetireAgentHandler_NotFound_404(t *testing.T) {
|
||||
mock, handler := agentRetireTestSetup()
|
||||
mock.RetireAgentFn = func(agentID, actor string, force bool, reason string) (*service.AgentRetirementResult, error) {
|
||||
return nil, errors.New("agent not found")
|
||||
// S-2 closure (cat-s6-efc7f6f6bd50): wrap repository.ErrNotFound
|
||||
// so the handler's errors.Is dispatch resolves to 404.
|
||||
return nil, ErrMockNotFound
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodDelete, "/api/v1/agents/unknown-id", nil)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/shankar0123/certctl/internal/repository"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -211,7 +212,7 @@ func (h AgentHandler) Heartbeat(w http.ResponseWriter, r *http.Request) {
|
||||
ErrorWithRequestID(w, http.StatusGone, "Agent has been retired", requestID)
|
||||
return
|
||||
}
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
if errors.Is(err, repository.ErrNotFound) {
|
||||
ErrorWithRequestID(w, http.StatusNotFound, "Agent not found", requestID)
|
||||
return
|
||||
}
|
||||
@@ -491,7 +492,7 @@ func (h AgentHandler) RetireAgent(w http.ResponseWriter, r *http.Request) {
|
||||
JSON(w, http.StatusConflict, body)
|
||||
return
|
||||
}
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
if errors.Is(err, repository.ErrNotFound) {
|
||||
ErrorWithRequestID(w, http.StatusNotFound, "Agent not found", requestID)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -900,7 +900,7 @@ func TestRevokeCertificate_Handler_AlreadyRevoked(t *testing.T) {
|
||||
func TestRevokeCertificate_Handler_NotFound(t *testing.T) {
|
||||
mock := &MockCertificateService{
|
||||
RevokeCertificateFn: func(_ context.Context, certID string, reason string, _ string) error {
|
||||
return fmt.Errorf("failed to fetch certificate: not found")
|
||||
return fmt.Errorf("failed to fetch certificate: not found: %w", ErrMockNotFound)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1033,7 +1033,7 @@ func TestGetDERCRL_Success(t *testing.T) {
|
||||
if issuerID == "iss-local" {
|
||||
return derCRLData, nil
|
||||
}
|
||||
return nil, fmt.Errorf("issuer not found")
|
||||
return nil, fmt.Errorf("issuer not found: %w", ErrMockNotFound)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1061,7 +1061,7 @@ func TestGetDERCRL_Success(t *testing.T) {
|
||||
func TestGetDERCRL_IssuerNotFound(t *testing.T) {
|
||||
mock := &MockCertificateService{
|
||||
GenerateDERCRLFn: func(_ context.Context, issuerID string) ([]byte, error) {
|
||||
return nil, fmt.Errorf("issuer not found")
|
||||
return nil, fmt.Errorf("issuer not found: %w", ErrMockNotFound)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1118,7 +1118,7 @@ func TestHandleOCSP_Success(t *testing.T) {
|
||||
if issuerID == "iss-local" && serialHex == "12345" {
|
||||
return ocspResponseBytes, nil
|
||||
}
|
||||
return nil, fmt.Errorf("certificate not found")
|
||||
return nil, fmt.Errorf("certificate not found: %w", ErrMockNotFound)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1159,7 +1159,7 @@ func TestHandleOCSP_MissingSerial(t *testing.T) {
|
||||
func TestHandleOCSP_IssuerNotFound(t *testing.T) {
|
||||
mock := &MockCertificateService{
|
||||
GetOCSPResponseFn: func(_ context.Context, issuerID string, serialHex string) ([]byte, error) {
|
||||
return nil, fmt.Errorf("issuer not found")
|
||||
return nil, fmt.Errorf("issuer not found: %w", ErrMockNotFound)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1178,7 +1178,7 @@ func TestHandleOCSP_IssuerNotFound(t *testing.T) {
|
||||
func TestHandleOCSP_CertNotFound(t *testing.T) {
|
||||
mock := &MockCertificateService{
|
||||
GetOCSPResponseFn: func(_ context.Context, issuerID string, serialHex string) ([]byte, error) {
|
||||
return nil, fmt.Errorf("certificate not found")
|
||||
return nil, fmt.Errorf("certificate not found: %w", ErrMockNotFound)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1529,7 +1529,7 @@ func TestGetCertificateDeployments_Success(t *testing.T) {
|
||||
func TestGetCertificateDeployments_NotFound(t *testing.T) {
|
||||
mock := &MockCertificateService{
|
||||
GetCertificateDeploymentsFn: func(_ context.Context, certID string) ([]domain.DeploymentTarget, error) {
|
||||
return nil, fmt.Errorf("certificate not found")
|
||||
return nil, fmt.Errorf("certificate not found: %w", ErrMockNotFound)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
@@ -298,7 +299,7 @@ func (h CertificateHandler) UpdateCertificate(w http.ResponseWriter, r *http.Req
|
||||
|
||||
updated, err := h.svc.UpdateCertificate(r.Context(), id, cert)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
if errors.Is(err, repository.ErrNotFound) {
|
||||
ErrorWithRequestID(w, http.StatusNotFound, "Certificate not found", requestID)
|
||||
return
|
||||
}
|
||||
@@ -327,7 +328,7 @@ func (h CertificateHandler) ArchiveCertificate(w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
|
||||
if err := h.svc.ArchiveCertificate(r.Context(), id); err != nil {
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
if errors.Is(err, repository.ErrNotFound) {
|
||||
ErrorWithRequestID(w, http.StatusNotFound, "Certificate not found", requestID)
|
||||
return
|
||||
}
|
||||
@@ -373,7 +374,7 @@ func (h CertificateHandler) GetCertificateVersions(w http.ResponseWriter, r *htt
|
||||
|
||||
versions, total, err := h.svc.GetCertificateVersions(r.Context(), certID, page, perPage)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
if errors.Is(err, repository.ErrNotFound) {
|
||||
ErrorWithRequestID(w, http.StatusNotFound, "Certificate not found", requestID)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -300,7 +300,7 @@ func TestGetDiscovered_Success(t *testing.T) {
|
||||
if id == "dcert-1" {
|
||||
return cert, nil
|
||||
}
|
||||
return nil, fmt.Errorf("not found")
|
||||
return nil, fmt.Errorf("not found: %w", ErrMockNotFound)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -331,7 +331,7 @@ func TestGetDiscovered_Success(t *testing.T) {
|
||||
func TestGetDiscovered_NotFound(t *testing.T) {
|
||||
mock := &MockDiscoveryService{
|
||||
GetDiscoveredFn: func(ctx context.Context, id string) (*domain.DiscoveredCertificate, error) {
|
||||
return nil, fmt.Errorf("not found")
|
||||
return nil, fmt.Errorf("not found: %w", ErrMockNotFound)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -412,7 +412,7 @@ func TestClaimDiscovered_MissingManagedCertID(t *testing.T) {
|
||||
func TestClaimDiscovered_NotFound(t *testing.T) {
|
||||
mock := &MockDiscoveryService{
|
||||
ClaimDiscoveredFn: func(ctx context.Context, id string, managedCertID string, actor string) error {
|
||||
return fmt.Errorf("discovered certificate not found")
|
||||
return fmt.Errorf("discovered certificate not found: %w", ErrMockNotFound)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -442,7 +442,7 @@ func TestDismissDiscovered_Success(t *testing.T) {
|
||||
if id == "dcert-1" {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("not found")
|
||||
return fmt.Errorf("not found: %w", ErrMockNotFound)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/shankar0123/certctl/internal/repository"
|
||||
"errors"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
@@ -46,7 +48,7 @@ func (h ExportHandler) ExportPEM(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
result, err := h.svc.ExportPEM(r.Context(), id)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
if errors.Is(err, repository.ErrNotFound) {
|
||||
ErrorWithRequestID(w, http.StatusNotFound, "Certificate not found", requestID)
|
||||
return
|
||||
}
|
||||
@@ -94,7 +96,7 @@ func (h ExportHandler) ExportPKCS12(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
pfxData, err := h.svc.ExportPKCS12(r.Context(), id, req.Password)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
if errors.Is(err, repository.ErrNotFound) {
|
||||
ErrorWithRequestID(w, http.StatusNotFound, "Certificate not found", requestID)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ func TestExportPEM_Download(t *testing.T) {
|
||||
func TestExportPEM_NotFound(t *testing.T) {
|
||||
mockSvc := &MockExportService{
|
||||
ExportPEMFn: func(_ context.Context, _ string) (*service.ExportPEMResult, error) {
|
||||
return nil, fmt.Errorf("certificate not found")
|
||||
return nil, fmt.Errorf("certificate not found: %w", ErrMockNotFound)
|
||||
},
|
||||
}
|
||||
h := NewExportHandler(mockSvc)
|
||||
@@ -216,7 +216,7 @@ func TestExportPKCS12_EmptyPassword(t *testing.T) {
|
||||
func TestExportPKCS12_NotFound(t *testing.T) {
|
||||
mockSvc := &MockExportService{
|
||||
ExportPKCS12Fn: func(_ context.Context, _ string, _ string) ([]byte, error) {
|
||||
return nil, fmt.Errorf("certificate not found")
|
||||
return nil, fmt.Errorf("certificate not found: %w", ErrMockNotFound)
|
||||
},
|
||||
}
|
||||
h := NewExportHandler(mockSvc)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/shankar0123/certctl/internal/repository"
|
||||
"errors"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
@@ -210,9 +212,9 @@ func (h IssuerHandler) DeleteIssuer(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if err := h.svc.DeleteIssuer(r.Context(), id); err != nil {
|
||||
if strings.Contains(err.Error(), "violates foreign key") || strings.Contains(err.Error(), "RESTRICT") {
|
||||
if repository.IsForeignKeyError(err) {
|
||||
ErrorWithRequestID(w, http.StatusConflict, "Cannot delete issuer: certificates are still using this issuer", requestID)
|
||||
} else if strings.Contains(err.Error(), "not found") {
|
||||
} else if errors.Is(err, repository.ErrNotFound) {
|
||||
ErrorWithRequestID(w, http.StatusNotFound, "Issuer not found", requestID)
|
||||
} else {
|
||||
ErrorWithRequestID(w, http.StatusInternalServerError, "Failed to delete issuer", requestID)
|
||||
|
||||
@@ -383,7 +383,7 @@ func TestApproveJob_Success(t *testing.T) {
|
||||
func TestApproveJob_NotFound(t *testing.T) {
|
||||
mock := &MockJobService{
|
||||
ApproveJobFn: func(id, actor string) error {
|
||||
return fmt.Errorf("job not found: no rows")
|
||||
return fmt.Errorf("job not found: no rows: %w", ErrMockNotFound)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -527,7 +527,7 @@ func TestRejectJob_NoReason(t *testing.T) {
|
||||
func TestRejectJob_NotFound(t *testing.T) {
|
||||
mock := &MockJobService{
|
||||
RejectJobFn: func(id, reason, actor string) error {
|
||||
return fmt.Errorf("job not found: no rows")
|
||||
return fmt.Errorf("job not found: no rows: %w", ErrMockNotFound)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/shankar0123/certctl/internal/repository"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -167,7 +168,7 @@ func (h JobHandler) ApproveJob(w http.ResponseWriter, r *http.Request) {
|
||||
requestID)
|
||||
return
|
||||
}
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
if errors.Is(err, repository.ErrNotFound) {
|
||||
ErrorWithRequestID(w, http.StatusNotFound, "Job not found", requestID)
|
||||
return
|
||||
}
|
||||
@@ -213,7 +214,7 @@ func (h JobHandler) RejectJob(w http.ResponseWriter, r *http.Request) {
|
||||
actor := resolveActor(r.Context())
|
||||
|
||||
if err := h.svc.RejectJob(r.Context(), jobID, body.Reason, actor); err != nil {
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
if errors.Is(err, repository.ErrNotFound) {
|
||||
ErrorWithRequestID(w, http.StatusNotFound, "Job not found", requestID)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ func (m *mockNetworkScanService) GetTarget(ctx context.Context, id string) (*dom
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("not found: %s", id)
|
||||
return nil, fmt.Errorf("not found: %w", ErrMockNotFound)
|
||||
}
|
||||
|
||||
func (m *mockNetworkScanService) CreateTarget(ctx context.Context, target *domain.NetworkScanTarget) (*domain.NetworkScanTarget, error) {
|
||||
@@ -48,7 +48,7 @@ func (m *mockNetworkScanService) UpdateTarget(ctx context.Context, id string, ta
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("not found: %s", id)
|
||||
return nil, fmt.Errorf("not found: %w", ErrMockNotFound)
|
||||
}
|
||||
|
||||
func (m *mockNetworkScanService) DeleteTarget(ctx context.Context, id string) error {
|
||||
@@ -58,7 +58,7 @@ func (m *mockNetworkScanService) DeleteTarget(ctx context.Context, id string) er
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("not found: %s", id)
|
||||
return fmt.Errorf("not found: %w", ErrMockNotFound)
|
||||
}
|
||||
|
||||
func (m *mockNetworkScanService) TriggerScan(ctx context.Context, targetID string) (*domain.DiscoveryScan, error) {
|
||||
@@ -71,7 +71,7 @@ func (m *mockNetworkScanService) TriggerScan(ctx context.Context, targetID strin
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("not found: %s", targetID)
|
||||
return nil, fmt.Errorf("not found: %w", ErrMockNotFound)
|
||||
}
|
||||
|
||||
func TestListNetworkScanTargets(t *testing.T) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/shankar0123/certctl/internal/repository"
|
||||
"errors"
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -170,7 +172,7 @@ func (h NotificationHandler) RequeueNotification(w http.ResponseWriter, r *http.
|
||||
notificationID := parts[0]
|
||||
|
||||
if err := h.svc.RequeueNotification(r.Context(), notificationID); err != nil {
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
if errors.Is(err, repository.ErrNotFound) {
|
||||
ErrorWithRequestID(w, http.StatusNotFound, "Notification not found", requestID)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/shankar0123/certctl/internal/repository"
|
||||
"errors"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
@@ -184,9 +186,9 @@ func (h OwnerHandler) DeleteOwner(w http.ResponseWriter, r *http.Request) {
|
||||
id = parts[0]
|
||||
|
||||
if err := h.svc.DeleteOwner(r.Context(), id); err != nil {
|
||||
if strings.Contains(err.Error(), "violates foreign key") || strings.Contains(err.Error(), "RESTRICT") {
|
||||
if repository.IsForeignKeyError(err) {
|
||||
ErrorWithRequestID(w, http.StatusConflict, "Cannot delete owner: certificates are still assigned to this owner", requestID)
|
||||
} else if strings.Contains(err.Error(), "not found") {
|
||||
} else if errors.Is(err, repository.ErrNotFound) {
|
||||
ErrorWithRequestID(w, http.StatusNotFound, "Owner not found", requestID)
|
||||
} else {
|
||||
ErrorWithRequestID(w, http.StatusInternalServerError, "Failed to delete owner", requestID)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/shankar0123/certctl/internal/repository"
|
||||
"errors"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
@@ -162,7 +164,7 @@ func (h ProfileHandler) UpdateProfile(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
updated, err := h.svc.UpdateProfile(r.Context(), id, profile)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
if errors.Is(err, repository.ErrNotFound) {
|
||||
ErrorWithRequestID(w, http.StatusNotFound, "Profile not found", requestID)
|
||||
return
|
||||
}
|
||||
@@ -195,7 +197,7 @@ func (h ProfileHandler) DeleteProfile(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if err := h.svc.DeleteProfile(r.Context(), id); err != nil {
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
if errors.Is(err, repository.ErrNotFound) {
|
||||
ErrorWithRequestID(w, http.StatusNotFound, "Profile not found", requestID)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/shankar0123/certctl/internal/repository"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -26,14 +27,14 @@ type RenewalPolicyService interface {
|
||||
|
||||
// RenewalPolicyHandler serves /api/v1/renewal-policies CRUD endpoints.
|
||||
//
|
||||
// G-1 design note: the service-level `ErrRenewalPolicyDuplicateName` /
|
||||
// G-1 + S-2 design note: the service-level `ErrRenewalPolicyDuplicateName` /
|
||||
// `ErrRenewalPolicyInUse` sentinels alias the repository sentinels (same var
|
||||
// identity), so `errors.Is` walks transparently across layers. Delete/Update
|
||||
// not-found detection intentionally uses a `strings.Contains(err.Error(),
|
||||
// "not found")` substring check — the repo wraps `sql.ErrNoRows` as
|
||||
// `fmt.Errorf("renewal policy not found: %s", id)` which strips the sentinel,
|
||||
// and the handler red-tests' `ErrMockNotFound = errors.New("mock not found
|
||||
// error")` follows the same substring convention.
|
||||
// identity), so `errors.Is` walks transparently across layers. S-2 closure
|
||||
// (cat-s6-efc7f6f6bd50) extends the same convention to not-found detection:
|
||||
// repos now wrap `sql.ErrNoRows` via `fmt.Errorf("X not found: %w",
|
||||
// repository.ErrNotFound)`, handler dispatch uses
|
||||
// `errors.Is(err, repository.ErrNotFound)`, and `ErrMockNotFound` in
|
||||
// test_utils.go wraps the same sentinel so the mocks still resolve to 404.
|
||||
type RenewalPolicyHandler struct {
|
||||
svc RenewalPolicyService
|
||||
}
|
||||
@@ -191,7 +192,7 @@ func (h RenewalPolicyHandler) UpdateRenewalPolicy(w http.ResponseWriter, r *http
|
||||
ErrorWithRequestID(w, http.StatusConflict, "A renewal policy with that name already exists", requestID)
|
||||
return
|
||||
}
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
if errors.Is(err, repository.ErrNotFound) {
|
||||
ErrorWithRequestID(w, http.StatusNotFound, "Renewal policy not found", requestID)
|
||||
return
|
||||
}
|
||||
@@ -231,7 +232,7 @@ func (h RenewalPolicyHandler) DeleteRenewalPolicy(w http.ResponseWriter, r *http
|
||||
ErrorWithRequestID(w, http.StatusConflict, "Renewal policy is still referenced by managed certificates", requestID)
|
||||
return
|
||||
}
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
if errors.Is(err, repository.ErrNotFound) {
|
||||
ErrorWithRequestID(w, http.StatusNotFound, "Renewal policy not found", requestID)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
package handler
|
||||
|
||||
import "errors"
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
var (
|
||||
// Mock errors for testing
|
||||
ErrMockServiceFailed = errors.New("mock service error")
|
||||
ErrMockNotFound = errors.New("mock not found error")
|
||||
ErrMockUnauthorized = errors.New("mock unauthorized error")
|
||||
ErrMockConflict = errors.New("mock conflict error")
|
||||
"github.com/shankar0123/certctl/internal/repository"
|
||||
)
|
||||
|
||||
// Mock errors for testing.
|
||||
//
|
||||
// S-2 closure (cat-s6-efc7f6f6bd50): ErrMockNotFound now wraps
|
||||
// repository.ErrNotFound via fmt.Errorf("...: %w", ...) so the
|
||||
// post-S-2 handler dispatch — which uses errors.Is(err,
|
||||
// repository.ErrNotFound) instead of strings.Contains — still
|
||||
// resolves the mock to a 404. The error message text is preserved
|
||||
// for log inspection; only the wrapping changes.
|
||||
var (
|
||||
ErrMockServiceFailed = fmt.Errorf("mock service error")
|
||||
ErrMockNotFound = fmt.Errorf("mock not found error: %w", repository.ErrNotFound)
|
||||
ErrMockUnauthorized = fmt.Errorf("mock unauthorized error")
|
||||
ErrMockConflict = fmt.Errorf("mock conflict error")
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user