Merge branch 'fix/s2-handler-error-mapping-typed-sentinels' (S-2 standalone, 1 audit finding)

This commit is contained in:
certctl-bot
2026-04-25 17:54:14 +00:00
39 changed files with 265 additions and 107 deletions
+36
View File
@@ -518,6 +518,42 @@ jobs:
fi fi
echo "B-1 orphan-CRUD client function guardrail: all 8 functions have page consumers." echo "B-1 orphan-CRUD client function guardrail: all 8 functions have page consumers."
- name: Forbidden strings.Contains(err.Error()) regression guard (S-2)
# S-2 closure (cat-s6-efc7f6f6bd50): replaced 30 brittle
# substring-match error-dispatch sites in internal/api/handler/
# with errors.Is + typed sentinels (repository.ErrNotFound,
# repository.ErrForeignKeyConstraint via the
# repository.IsForeignKeyError helper). This step grep-fails
# the build if any new strings.Contains(err.Error(), "not found")
# or strings.Contains(err.Error(), "violates foreign key")
# site appears under internal/api/handler/.
#
# Allowed: closure-comments documenting the convention (e.g.
# bulk_reassignment.go's "post-M-1 errToStatus convention"
# docblock); domain-specific substring patterns that are
# legitimately one-off ("cannot approve", "cannot reject",
# "cannot be parsed", "challenge password") — flagged as
# deferred follow-ups in the S-2 commit message.
#
# See coverage-gap-audit-2026-04-24-v5/unified-audit.md
# cat-s6-efc7f6f6bd50 for closure rationale.
run: |
set -e
BAD=$(grep -rnE 'strings\.Contains\(err\.Error\(\),\s*"(not found|violates foreign key|RESTRICT)"' internal/api/handler/ 2>/dev/null \
| grep -vE '^\s*[^:]+:[0-9]+:\s*//' \
|| true)
if [ -n "$BAD" ]; then
echo "S-2 regression: brittle substring-match error-dispatch reappeared:"
echo "$BAD"
echo ""
echo "Use errors.Is(err, repository.ErrNotFound) for not-found dispatch,"
echo "or repository.IsForeignKeyError(err) for FK violations."
echo "See coverage-gap-audit-2026-04-24-v5/unified-audit.md"
echo "cat-s6-efc7f6f6bd50 for closure rationale."
exit 1
fi
echo "S-2 typed-sentinel error-dispatch guardrail: clean."
- name: Race Detection - name: Race Detection
run: go test -race ./internal/service/... ./internal/api/handler/... ./internal/api/middleware/... ./internal/scheduler/... ./internal/connector/... ./internal/crypto/... ./internal/domain/... ./internal/validation/... ./internal/tlsprobe/... -count=1 -timeout 300s run: go test -race ./internal/service/... ./internal/api/handler/... ./internal/api/middleware/... ./internal/scheduler/... ./internal/connector/... ./internal/crypto/... ./internal/domain/... ./internal/validation/... ./internal/tlsprobe/... -count=1 -timeout 300s
@@ -522,7 +522,7 @@ func TestRevokeCertificate_AlreadyRevoked(t *testing.T) {
func TestRevokeCertificate_NotFound(t *testing.T) { func TestRevokeCertificate_NotFound(t *testing.T) {
handler, mock := newCertHandlerWithMock() handler, mock := newCertHandlerWithMock()
mock.RevokeCertificateFn = func(_ context.Context, id string, reason string, _ string) error { 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"}`)) 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 { if m.GetAgentGroupFn != nil {
return m.GetAgentGroupFn(id) 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) { func (m *MockAgentGroupService) CreateAgentGroup(_ context.Context, group domain.AgentGroup) (*domain.AgentGroup, error) {
+4 -2
View File
@@ -1,6 +1,8 @@
package handler package handler
import ( import (
"github.com/shankar0123/certctl/internal/repository"
"errors"
"context" "context"
"encoding/json" "encoding/json"
"net/http" "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) updated, err := h.svc.UpdateAgentGroup(r.Context(), id, group)
if err != nil { 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) ErrorWithRequestID(w, http.StatusNotFound, "Agent group not found", requestID)
return 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 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) ErrorWithRequestID(w, http.StatusNotFound, "Agent group not found", requestID)
return return
} }
@@ -3,7 +3,6 @@ package handler
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
@@ -142,7 +141,9 @@ func TestRetireAgentHandler_Sentinel_403(t *testing.T) {
func TestRetireAgentHandler_NotFound_404(t *testing.T) { func TestRetireAgentHandler_NotFound_404(t *testing.T) {
mock, handler := agentRetireTestSetup() mock, handler := agentRetireTestSetup()
mock.RetireAgentFn = func(agentID, actor string, force bool, reason string) (*service.AgentRetirementResult, error) { 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) req := httptest.NewRequest(http.MethodDelete, "/api/v1/agents/unknown-id", nil)
+3 -2
View File
@@ -1,6 +1,7 @@
package handler package handler
import ( import (
"github.com/shankar0123/certctl/internal/repository"
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
@@ -211,7 +212,7 @@ func (h AgentHandler) Heartbeat(w http.ResponseWriter, r *http.Request) {
ErrorWithRequestID(w, http.StatusGone, "Agent has been retired", requestID) ErrorWithRequestID(w, http.StatusGone, "Agent has been retired", requestID)
return return
} }
if strings.Contains(err.Error(), "not found") { if errors.Is(err, repository.ErrNotFound) {
ErrorWithRequestID(w, http.StatusNotFound, "Agent not found", requestID) ErrorWithRequestID(w, http.StatusNotFound, "Agent not found", requestID)
return return
} }
@@ -491,7 +492,7 @@ func (h AgentHandler) RetireAgent(w http.ResponseWriter, r *http.Request) {
JSON(w, http.StatusConflict, body) JSON(w, http.StatusConflict, body)
return return
} }
if strings.Contains(err.Error(), "not found") { if errors.Is(err, repository.ErrNotFound) {
ErrorWithRequestID(w, http.StatusNotFound, "Agent not found", requestID) ErrorWithRequestID(w, http.StatusNotFound, "Agent not found", requestID)
return return
} }
@@ -900,7 +900,7 @@ func TestRevokeCertificate_Handler_AlreadyRevoked(t *testing.T) {
func TestRevokeCertificate_Handler_NotFound(t *testing.T) { func TestRevokeCertificate_Handler_NotFound(t *testing.T) {
mock := &MockCertificateService{ mock := &MockCertificateService{
RevokeCertificateFn: func(_ context.Context, certID string, reason string, _ string) error { 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" { if issuerID == "iss-local" {
return derCRLData, nil 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) { func TestGetDERCRL_IssuerNotFound(t *testing.T) {
mock := &MockCertificateService{ mock := &MockCertificateService{
GenerateDERCRLFn: func(_ context.Context, issuerID string) ([]byte, error) { 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" { if issuerID == "iss-local" && serialHex == "12345" {
return ocspResponseBytes, nil 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) { func TestHandleOCSP_IssuerNotFound(t *testing.T) {
mock := &MockCertificateService{ mock := &MockCertificateService{
GetOCSPResponseFn: func(_ context.Context, issuerID string, serialHex string) ([]byte, error) { 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) { func TestHandleOCSP_CertNotFound(t *testing.T) {
mock := &MockCertificateService{ mock := &MockCertificateService{
GetOCSPResponseFn: func(_ context.Context, issuerID string, serialHex string) ([]byte, error) { 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) { func TestGetCertificateDeployments_NotFound(t *testing.T) {
mock := &MockCertificateService{ mock := &MockCertificateService{
GetCertificateDeploymentsFn: func(_ context.Context, certID string) ([]domain.DeploymentTarget, error) { 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)
}, },
} }
+4 -3
View File
@@ -1,6 +1,7 @@
package handler package handler
import ( import (
"errors"
"context" "context"
"encoding/json" "encoding/json"
"log/slog" "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) updated, err := h.svc.UpdateCertificate(r.Context(), id, cert)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "not found") { if errors.Is(err, repository.ErrNotFound) {
ErrorWithRequestID(w, http.StatusNotFound, "Certificate not found", requestID) ErrorWithRequestID(w, http.StatusNotFound, "Certificate not found", requestID)
return 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 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) ErrorWithRequestID(w, http.StatusNotFound, "Certificate not found", requestID)
return 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) versions, total, err := h.svc.GetCertificateVersions(r.Context(), certID, page, perPage)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "not found") { if errors.Is(err, repository.ErrNotFound) {
ErrorWithRequestID(w, http.StatusNotFound, "Certificate not found", requestID) ErrorWithRequestID(w, http.StatusNotFound, "Certificate not found", requestID)
return return
} }
@@ -300,7 +300,7 @@ func TestGetDiscovered_Success(t *testing.T) {
if id == "dcert-1" { if id == "dcert-1" {
return cert, nil 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) { func TestGetDiscovered_NotFound(t *testing.T) {
mock := &MockDiscoveryService{ mock := &MockDiscoveryService{
GetDiscoveredFn: func(ctx context.Context, id string) (*domain.DiscoveredCertificate, error) { 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) { func TestClaimDiscovered_NotFound(t *testing.T) {
mock := &MockDiscoveryService{ mock := &MockDiscoveryService{
ClaimDiscoveredFn: func(ctx context.Context, id string, managedCertID string, actor string) error { 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" { if id == "dcert-1" {
return nil return nil
} }
return fmt.Errorf("not found") return fmt.Errorf("not found: %w", ErrMockNotFound)
}, },
} }
+4 -2
View File
@@ -1,6 +1,8 @@
package handler package handler
import ( import (
"github.com/shankar0123/certctl/internal/repository"
"errors"
"context" "context"
"encoding/json" "encoding/json"
"log/slog" "log/slog"
@@ -46,7 +48,7 @@ func (h ExportHandler) ExportPEM(w http.ResponseWriter, r *http.Request) {
result, err := h.svc.ExportPEM(r.Context(), id) result, err := h.svc.ExportPEM(r.Context(), id)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "not found") { if errors.Is(err, repository.ErrNotFound) {
ErrorWithRequestID(w, http.StatusNotFound, "Certificate not found", requestID) ErrorWithRequestID(w, http.StatusNotFound, "Certificate not found", requestID)
return 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) pfxData, err := h.svc.ExportPKCS12(r.Context(), id, req.Password)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "not found") { if errors.Is(err, repository.ErrNotFound) {
ErrorWithRequestID(w, http.StatusNotFound, "Certificate not found", requestID) ErrorWithRequestID(w, http.StatusNotFound, "Certificate not found", requestID)
return return
} }
+2 -2
View File
@@ -110,7 +110,7 @@ func TestExportPEM_Download(t *testing.T) {
func TestExportPEM_NotFound(t *testing.T) { func TestExportPEM_NotFound(t *testing.T) {
mockSvc := &MockExportService{ mockSvc := &MockExportService{
ExportPEMFn: func(_ context.Context, _ string) (*service.ExportPEMResult, error) { 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) h := NewExportHandler(mockSvc)
@@ -216,7 +216,7 @@ func TestExportPKCS12_EmptyPassword(t *testing.T) {
func TestExportPKCS12_NotFound(t *testing.T) { func TestExportPKCS12_NotFound(t *testing.T) {
mockSvc := &MockExportService{ mockSvc := &MockExportService{
ExportPKCS12Fn: func(_ context.Context, _ string, _ string) ([]byte, error) { 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) h := NewExportHandler(mockSvc)
+4 -2
View File
@@ -1,6 +1,8 @@
package handler package handler
import ( import (
"github.com/shankar0123/certctl/internal/repository"
"errors"
"context" "context"
"encoding/json" "encoding/json"
"log/slog" "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 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) 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) ErrorWithRequestID(w, http.StatusNotFound, "Issuer not found", requestID)
} else { } else {
ErrorWithRequestID(w, http.StatusInternalServerError, "Failed to delete issuer", requestID) ErrorWithRequestID(w, http.StatusInternalServerError, "Failed to delete issuer", requestID)
+2 -2
View File
@@ -383,7 +383,7 @@ func TestApproveJob_Success(t *testing.T) {
func TestApproveJob_NotFound(t *testing.T) { func TestApproveJob_NotFound(t *testing.T) {
mock := &MockJobService{ mock := &MockJobService{
ApproveJobFn: func(id, actor string) error { 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) { func TestRejectJob_NotFound(t *testing.T) {
mock := &MockJobService{ mock := &MockJobService{
RejectJobFn: func(id, reason, actor string) error { 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)
}, },
} }
+3 -2
View File
@@ -1,6 +1,7 @@
package handler package handler
import ( import (
"github.com/shankar0123/certctl/internal/repository"
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
@@ -167,7 +168,7 @@ func (h JobHandler) ApproveJob(w http.ResponseWriter, r *http.Request) {
requestID) requestID)
return return
} }
if strings.Contains(err.Error(), "not found") { if errors.Is(err, repository.ErrNotFound) {
ErrorWithRequestID(w, http.StatusNotFound, "Job not found", requestID) ErrorWithRequestID(w, http.StatusNotFound, "Job not found", requestID)
return return
} }
@@ -213,7 +214,7 @@ func (h JobHandler) RejectJob(w http.ResponseWriter, r *http.Request) {
actor := resolveActor(r.Context()) actor := resolveActor(r.Context())
if err := h.svc.RejectJob(r.Context(), jobID, body.Reason, actor); err != nil { 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) ErrorWithRequestID(w, http.StatusNotFound, "Job not found", requestID)
return return
} }
@@ -27,7 +27,7 @@ func (m *mockNetworkScanService) GetTarget(ctx context.Context, id string) (*dom
return t, nil 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) { 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 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 { 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 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) { 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 }, nil
} }
} }
return nil, fmt.Errorf("not found: %s", targetID) return nil, fmt.Errorf("not found: %w", ErrMockNotFound)
} }
func TestListNetworkScanTargets(t *testing.T) { func TestListNetworkScanTargets(t *testing.T) {
+3 -1
View File
@@ -1,6 +1,8 @@
package handler package handler
import ( import (
"github.com/shankar0123/certctl/internal/repository"
"errors"
"context" "context"
"net/http" "net/http"
"strconv" "strconv"
@@ -170,7 +172,7 @@ func (h NotificationHandler) RequeueNotification(w http.ResponseWriter, r *http.
notificationID := parts[0] notificationID := parts[0]
if err := h.svc.RequeueNotification(r.Context(), notificationID); err != nil { 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) ErrorWithRequestID(w, http.StatusNotFound, "Notification not found", requestID)
return return
} }
+4 -2
View File
@@ -1,6 +1,8 @@
package handler package handler
import ( import (
"github.com/shankar0123/certctl/internal/repository"
"errors"
"context" "context"
"encoding/json" "encoding/json"
"net/http" "net/http"
@@ -184,9 +186,9 @@ func (h OwnerHandler) DeleteOwner(w http.ResponseWriter, r *http.Request) {
id = parts[0] id = parts[0]
if err := h.svc.DeleteOwner(r.Context(), id); err != nil { 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) 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) ErrorWithRequestID(w, http.StatusNotFound, "Owner not found", requestID)
} else { } else {
ErrorWithRequestID(w, http.StatusInternalServerError, "Failed to delete owner", requestID) ErrorWithRequestID(w, http.StatusInternalServerError, "Failed to delete owner", requestID)
+4 -2
View File
@@ -1,6 +1,8 @@
package handler package handler
import ( import (
"github.com/shankar0123/certctl/internal/repository"
"errors"
"context" "context"
"encoding/json" "encoding/json"
"net/http" "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) updated, err := h.svc.UpdateProfile(r.Context(), id, profile)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "not found") { if errors.Is(err, repository.ErrNotFound) {
ErrorWithRequestID(w, http.StatusNotFound, "Profile not found", requestID) ErrorWithRequestID(w, http.StatusNotFound, "Profile not found", requestID)
return 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 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) ErrorWithRequestID(w, http.StatusNotFound, "Profile not found", requestID)
return return
} }
+10 -9
View File
@@ -1,6 +1,7 @@
package handler package handler
import ( import (
"github.com/shankar0123/certctl/internal/repository"
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
@@ -26,14 +27,14 @@ type RenewalPolicyService interface {
// RenewalPolicyHandler serves /api/v1/renewal-policies CRUD endpoints. // 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 // `ErrRenewalPolicyInUse` sentinels alias the repository sentinels (same var
// identity), so `errors.Is` walks transparently across layers. Delete/Update // identity), so `errors.Is` walks transparently across layers. S-2 closure
// not-found detection intentionally uses a `strings.Contains(err.Error(), // (cat-s6-efc7f6f6bd50) extends the same convention to not-found detection:
// "not found")` substring check — the repo wraps `sql.ErrNoRows` as // repos now wrap `sql.ErrNoRows` via `fmt.Errorf("X not found: %w",
// `fmt.Errorf("renewal policy not found: %s", id)` which strips the sentinel, // repository.ErrNotFound)`, handler dispatch uses
// and the handler red-tests' `ErrMockNotFound = errors.New("mock not found // `errors.Is(err, repository.ErrNotFound)`, and `ErrMockNotFound` in
// error")` follows the same substring convention. // test_utils.go wraps the same sentinel so the mocks still resolve to 404.
type RenewalPolicyHandler struct { type RenewalPolicyHandler struct {
svc RenewalPolicyService 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) ErrorWithRequestID(w, http.StatusConflict, "A renewal policy with that name already exists", requestID)
return return
} }
if strings.Contains(err.Error(), "not found") { if errors.Is(err, repository.ErrNotFound) {
ErrorWithRequestID(w, http.StatusNotFound, "Renewal policy not found", requestID) ErrorWithRequestID(w, http.StatusNotFound, "Renewal policy not found", requestID)
return 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) ErrorWithRequestID(w, http.StatusConflict, "Renewal policy is still referenced by managed certificates", requestID)
return return
} }
if strings.Contains(err.Error(), "not found") { if errors.Is(err, repository.ErrNotFound) {
ErrorWithRequestID(w, http.StatusNotFound, "Renewal policy not found", requestID) ErrorWithRequestID(w, http.StatusNotFound, "Renewal policy not found", requestID)
return return
} }
+18 -7
View File
@@ -1,11 +1,22 @@
package handler package handler
import "errors" import (
"fmt"
var ( "github.com/shankar0123/certctl/internal/repository"
// Mock errors for testing )
ErrMockServiceFailed = errors.New("mock service error")
ErrMockNotFound = errors.New("mock not found error") // Mock errors for testing.
ErrMockUnauthorized = errors.New("mock unauthorized error") //
ErrMockConflict = errors.New("mock conflict error") // 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")
) )
+19
View File
@@ -0,0 +1,19 @@
// Package domain — error sentinels.
//
// S-2 closure (cat-s6-efc7f6f6bd50): pre-S-2 every handler-side
// validation-failure dispatch was a `strings.Contains(err.Error(),
// "invalid")` or `"required"` site, brittle to any domain-layer
// message change. Post-S-2 domain validators that surface a
// 400 Bad Request wrap their per-field errors via fmt.Errorf("...: %w",
// domain.ErrValidation) so handlers can dispatch via errors.Is.
package domain
import "errors"
// ErrValidation is the canonical sentinel for input-validation
// failures surfaced by domain-layer Validate() methods. Handlers that
// surface a 400 Bad Request should `errors.Is(err, domain.ErrValidation)`.
// Per-field error messages are still preserved via fmt.Errorf wrapping
// so the response body retains the actionable detail; the sentinel
// only drives the HTTP status code dispatch.
var ErrValidation = errors.New("domain: validation failed")
+4 -1
View File
@@ -626,7 +626,10 @@ func (m *mockJobRepository) List(ctx context.Context) ([]*domain.Job, error) {
func (m *mockJobRepository) Get(ctx context.Context, id string) (*domain.Job, error) { func (m *mockJobRepository) Get(ctx context.Context, id string) (*domain.Job, error) {
job, ok := m.jobs[id] job, ok := m.jobs[id]
if !ok { if !ok {
return nil, fmt.Errorf("job not found") // S-2 closure: wrap repository.ErrNotFound so the handler's
// errors.Is dispatch resolves to 404 (matches the Postgres
// repo's post-S-2 wrapping).
return nil, fmt.Errorf("job not found: %w", repository.ErrNotFound)
} }
return job, nil return job, nil
} }
+62
View File
@@ -0,0 +1,62 @@
// Package repository defines the repository-layer error sentinels that
// handlers map to HTTP status codes via errors.Is.
//
// S-2 closure (cat-s6-efc7f6f6bd50): pre-S-2 every handler-side
// not-found dispatch was a `strings.Contains(err.Error(), "not found")`
// site (30+ across internal/api/handler/*.go), brittle to any
// repository-layer message change and untyped against the actual
// failure mode. Post-S-2 the dispatch is type-checked: repositories
// wrap sql.ErrNoRows via fmt.Errorf("...: %w", repository.ErrNotFound)
// and FK constraint violations via repository.ErrForeignKeyConstraint;
// handlers consume via errors.Is. The substring matching is preserved
// at the lib/pq boundary inside `errors.go::isFKError` because the
// PostgreSQL driver returns un-typed *pq.Error values whose codes are
// the canonical signal — but it's confined to one helper rather than
// scattered across every handler file. See unified-audit.md
// cat-s6-efc7f6f6bd50 for the closure rationale.
package repository
import (
"errors"
"strings"
)
// ErrNotFound is the canonical sentinel for repository methods that
// return after sql.ErrNoRows (or its wrapped form). Handlers that
// surface a 404 should `errors.Is(err, repository.ErrNotFound)`
// rather than substring-match.
var ErrNotFound = errors.New("repository: row not found")
// ErrForeignKeyConstraint is the canonical sentinel for PostgreSQL
// FK / RESTRICT violations bubbling up from a DELETE or UPDATE.
// Handlers that surface a 409 Conflict should
// `errors.Is(err, repository.ErrForeignKeyConstraint)`.
//
// The B-1 closure introduced ErrRenewalPolicyInUse as the per-entity
// FK sentinel for renewal_policies; future per-entity FK sentinels
// (ErrIssuerInUse, ErrTeamInUse, ErrOwnerInUse) can wrap this generic
// one via fmt.Errorf("...: %w", ErrForeignKeyConstraint) so handlers
// can choose between generic-409 and entity-specific 409 dispatch.
var ErrForeignKeyConstraint = errors.New("repository: foreign key constraint violation")
// IsForeignKeyError detects PostgreSQL FK violation errors from the
// lib/pq driver via the canonical error-text patterns it emits. The
// substring matching is intentionally confined to this helper —
// callers should use this once at the repo layer to wrap into the
// typed ErrForeignKeyConstraint sentinel, then handlers consume via
// errors.Is.
//
// Patterns recognised:
// - "violates foreign key constraint" (the standard PG message)
// - "violates restrict" / "RESTRICT" (DELETE blocked by ON DELETE RESTRICT)
//
// Returns false for nil err so callers can defensively chain it.
func IsForeignKeyError(err error) bool {
if err == nil {
return false
}
msg := err.Error()
return strings.Contains(msg, "violates foreign key") ||
strings.Contains(msg, "RESTRICT") ||
strings.Contains(msg, "violates restrict")
}
+6 -5
View File
@@ -1,6 +1,7 @@
package postgres package postgres
import ( import (
"github.com/shankar0123/certctl/internal/repository"
"context" "context"
"database/sql" "database/sql"
"fmt" "fmt"
@@ -73,7 +74,7 @@ func (r *AgentRepository) Get(ctx context.Context, id string) (*domain.Agent, er
agent, err := scanAgent(row) agent, err := scanAgent(row)
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return nil, fmt.Errorf("agent not found") return nil, fmt.Errorf("agent not found: %w", repository.ErrNotFound)
} }
return nil, fmt.Errorf("failed to query agent: %w", err) return nil, fmt.Errorf("failed to query agent: %w", err)
} }
@@ -170,7 +171,7 @@ func (r *AgentRepository) Update(ctx context.Context, agent *domain.Agent) error
} }
if rows == 0 { if rows == 0 {
return fmt.Errorf("agent not found") return fmt.Errorf("agent not found: %w", repository.ErrNotFound)
} }
return nil return nil
@@ -190,7 +191,7 @@ func (r *AgentRepository) Delete(ctx context.Context, id string) error {
} }
if rows == 0 { if rows == 0 {
return fmt.Errorf("agent not found") return fmt.Errorf("agent not found: %w", repository.ErrNotFound)
} }
return nil return nil
@@ -237,7 +238,7 @@ func (r *AgentRepository) UpdateHeartbeat(ctx context.Context, id string, metada
} }
if rows == 0 { if rows == 0 {
return fmt.Errorf("agent not found") return fmt.Errorf("agent not found: %w", repository.ErrNotFound)
} }
return nil return nil
@@ -259,7 +260,7 @@ func (r *AgentRepository) GetByAPIKey(ctx context.Context, keyHash string) (*dom
agent, err := scanAgent(row) agent, err := scanAgent(row)
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return nil, fmt.Errorf("agent not found") return nil, fmt.Errorf("agent not found: %w", repository.ErrNotFound)
} }
return nil, fmt.Errorf("failed to query agent: %w", err) return nil, fmt.Errorf("failed to query agent: %w", err)
} }
+4 -3
View File
@@ -1,6 +1,7 @@
package postgres package postgres
import ( import (
"github.com/shankar0123/certctl/internal/repository"
"context" "context"
"database/sql" "database/sql"
"fmt" "fmt"
@@ -50,7 +51,7 @@ func (r *AgentGroupRepository) Get(ctx context.Context, id string) (*domain.Agen
err := row.Scan(&g.ID, &g.Name, &g.Description, &g.MatchOS, &g.MatchArchitecture, err := row.Scan(&g.ID, &g.Name, &g.Description, &g.MatchOS, &g.MatchArchitecture,
&g.MatchIPCIDR, &g.MatchVersion, &g.Enabled, &g.CreatedAt, &g.UpdatedAt) &g.MatchIPCIDR, &g.MatchVersion, &g.Enabled, &g.CreatedAt, &g.UpdatedAt)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return nil, fmt.Errorf("agent group not found: %s", id) return nil, fmt.Errorf("agent group not found: %w", repository.ErrNotFound)
} }
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get agent group: %w", err) return nil, fmt.Errorf("failed to get agent group: %w", err)
@@ -84,7 +85,7 @@ func (r *AgentGroupRepository) Update(ctx context.Context, group *domain.AgentGr
} }
rows, _ := result.RowsAffected() rows, _ := result.RowsAffected()
if rows == 0 { if rows == 0 {
return fmt.Errorf("agent group not found: %s", group.ID) return fmt.Errorf("agent group not found: %w", repository.ErrNotFound)
} }
return nil return nil
} }
@@ -97,7 +98,7 @@ func (r *AgentGroupRepository) Delete(ctx context.Context, id string) error {
} }
rows, _ := result.RowsAffected() rows, _ := result.RowsAffected()
if rows == 0 { if rows == 0 {
return fmt.Errorf("agent group not found: %s", id) return fmt.Errorf("agent group not found: %w", repository.ErrNotFound)
} }
return nil return nil
} }
+3 -3
View File
@@ -265,7 +265,7 @@ func (r *CertificateRepository) Get(ctx context.Context, id string) (*domain.Man
cert, err := r.scanCertificate(ctx, row) cert, err := r.scanCertificate(ctx, row)
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return nil, fmt.Errorf("certificate not found") return nil, fmt.Errorf("certificate not found: %w", repository.ErrNotFound)
} }
return nil, fmt.Errorf("failed to query certificate: %w", err) return nil, fmt.Errorf("failed to query certificate: %w", err)
} }
@@ -397,7 +397,7 @@ func (r *CertificateRepository) Update(ctx context.Context, cert *domain.Managed
} }
if rows == 0 { if rows == 0 {
return fmt.Errorf("certificate not found") return fmt.Errorf("certificate not found: %w", repository.ErrNotFound)
} }
return nil return nil
@@ -419,7 +419,7 @@ func (r *CertificateRepository) Archive(ctx context.Context, id string) error {
} }
if rows == 0 { if rows == 0 {
return fmt.Errorf("certificate not found") return fmt.Errorf("certificate not found: %w", repository.ErrNotFound)
} }
return nil return nil
+3 -3
View File
@@ -62,7 +62,7 @@ func (r *DiscoveryRepository) GetScan(ctx context.Context, id string) (*domain.D
&scan.ScanDurationMs, &scan.StartedAt, &scan.CompletedAt, &scan.ScanDurationMs, &scan.StartedAt, &scan.CompletedAt,
) )
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return nil, fmt.Errorf("discovery scan not found: %s", id) return nil, fmt.Errorf("discovery scan not found: %w", repository.ErrNotFound)
} }
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get discovery scan: %w", err) return nil, fmt.Errorf("failed to get discovery scan: %w", err)
@@ -190,7 +190,7 @@ func (r *DiscoveryRepository) GetDiscovered(ctx context.Context, id string) (*do
&cert.CreatedAt, &cert.UpdatedAt, &cert.CreatedAt, &cert.UpdatedAt,
) )
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return nil, fmt.Errorf("discovered certificate not found: %s", id) return nil, fmt.Errorf("discovered certificate not found: %w", repository.ErrNotFound)
} }
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get discovered certificate: %w", err) return nil, fmt.Errorf("failed to get discovered certificate: %w", err)
@@ -317,7 +317,7 @@ func (r *DiscoveryRepository) UpdateDiscoveredStatus(ctx context.Context, id str
} }
rowsAffected, _ := result.RowsAffected() rowsAffected, _ := result.RowsAffected()
if rowsAffected == 0 { if rowsAffected == 0 {
return fmt.Errorf("discovered certificate not found: %s", id) return fmt.Errorf("discovered certificate not found: %w", repository.ErrNotFound)
} }
return nil return nil
} }
+2 -2
View File
@@ -113,7 +113,7 @@ func (r *HealthCheckRepository) Get(ctx context.Context, id string) (*domain.End
&check.CreatedAt, &check.UpdatedAt, &check.CreatedAt, &check.UpdatedAt,
) )
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return nil, fmt.Errorf("health check not found: %s", id) return nil, fmt.Errorf("health check not found: %w", repository.ErrNotFound)
} }
if err != nil { if err != nil {
return nil, fmt.Errorf("get health check: %w", err) return nil, fmt.Errorf("get health check: %w", err)
@@ -299,7 +299,7 @@ func (r *HealthCheckRepository) GetByEndpoint(ctx context.Context, endpoint stri
&check.CreatedAt, &check.UpdatedAt, &check.CreatedAt, &check.UpdatedAt,
) )
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return nil, fmt.Errorf("health check not found for endpoint: %s", endpoint) return nil, fmt.Errorf("health check not found for endpoint: %w", repository.ErrNotFound)
} }
if err != nil { if err != nil {
return nil, fmt.Errorf("get health check by endpoint: %w", err) return nil, fmt.Errorf("get health check by endpoint: %w", err)
+4 -3
View File
@@ -1,6 +1,7 @@
package postgres package postgres
import ( import (
"github.com/shankar0123/certctl/internal/repository"
"context" "context"
"database/sql" "database/sql"
"fmt" "fmt"
@@ -69,7 +70,7 @@ func (r *IssuerRepository) Get(ctx context.Context, id string) (*domain.Issuer,
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return nil, fmt.Errorf("issuer not found") return nil, fmt.Errorf("issuer not found: %w", repository.ErrNotFound)
} }
return nil, fmt.Errorf("failed to query issuer: %w", err) return nil, fmt.Errorf("failed to query issuer: %w", err)
} }
@@ -169,7 +170,7 @@ func (r *IssuerRepository) Update(ctx context.Context, issuer *domain.Issuer) er
} }
if rows == 0 { if rows == 0 {
return fmt.Errorf("issuer not found") return fmt.Errorf("issuer not found: %w", repository.ErrNotFound)
} }
return nil return nil
@@ -189,7 +190,7 @@ func (r *IssuerRepository) Delete(ctx context.Context, id string) error {
} }
if rows == 0 { if rows == 0 {
return fmt.Errorf("issuer not found") return fmt.Errorf("issuer not found: %w", repository.ErrNotFound)
} }
return nil return nil
+5 -4
View File
@@ -1,6 +1,7 @@
package postgres package postgres
import ( import (
"github.com/shankar0123/certctl/internal/repository"
"context" "context"
"database/sql" "database/sql"
"fmt" "fmt"
@@ -62,7 +63,7 @@ func (r *JobRepository) Get(ctx context.Context, id string) (*domain.Job, error)
job, err := scanJob(row) job, err := scanJob(row)
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return nil, fmt.Errorf("job not found") return nil, fmt.Errorf("job not found: %w", repository.ErrNotFound)
} }
return nil, fmt.Errorf("failed to query job: %w", err) return nil, fmt.Errorf("failed to query job: %w", err)
} }
@@ -123,7 +124,7 @@ func (r *JobRepository) Update(ctx context.Context, job *domain.Job) error {
} }
if rows == 0 { if rows == 0 {
return fmt.Errorf("job not found") return fmt.Errorf("job not found: %w", repository.ErrNotFound)
} }
return nil return nil
@@ -143,7 +144,7 @@ func (r *JobRepository) Delete(ctx context.Context, id string) error {
} }
if rows == 0 { if rows == 0 {
return fmt.Errorf("job not found") return fmt.Errorf("job not found: %w", repository.ErrNotFound)
} }
return nil return nil
@@ -232,7 +233,7 @@ func (r *JobRepository) UpdateStatus(ctx context.Context, id string, status doma
} }
if rows == 0 { if rows == 0 {
return fmt.Errorf("job not found") return fmt.Errorf("job not found: %w", repository.ErrNotFound)
} }
return nil return nil
+4 -3
View File
@@ -1,6 +1,7 @@
package postgres package postgres
import ( import (
"github.com/shankar0123/certctl/internal/repository"
"context" "context"
"database/sql" "database/sql"
"fmt" "fmt"
@@ -68,7 +69,7 @@ func (r *NetworkScanRepository) Get(ctx context.Context, id string) (*domain.Net
&target.CreatedAt, &target.UpdatedAt, &target.CreatedAt, &target.UpdatedAt,
) )
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return nil, fmt.Errorf("network scan target not found: %s", id) return nil, fmt.Errorf("network scan target not found: %w", repository.ErrNotFound)
} }
if err != nil { if err != nil {
return nil, fmt.Errorf("get network scan target: %w", err) return nil, fmt.Errorf("get network scan target: %w", err)
@@ -117,7 +118,7 @@ func (r *NetworkScanRepository) Update(ctx context.Context, target *domain.Netwo
} }
rows, _ := result.RowsAffected() rows, _ := result.RowsAffected()
if rows == 0 { if rows == 0 {
return fmt.Errorf("network scan target not found: %s", target.ID) return fmt.Errorf("network scan target not found: %w", repository.ErrNotFound)
} }
return nil return nil
} }
@@ -130,7 +131,7 @@ func (r *NetworkScanRepository) Delete(ctx context.Context, id string) error {
} }
rows, _ := result.RowsAffected() rows, _ := result.RowsAffected()
if rows == 0 { if rows == 0 {
return fmt.Errorf("network scan target not found: %s", id) return fmt.Errorf("network scan target not found: %w", repository.ErrNotFound)
} }
return nil return nil
} }
+4 -4
View File
@@ -174,7 +174,7 @@ func (r *NotificationRepository) UpdateStatus(ctx context.Context, id string, st
} }
if rows == 0 { if rows == 0 {
return fmt.Errorf("notification not found") return fmt.Errorf("notification not found: %w", repository.ErrNotFound)
} }
return nil return nil
@@ -336,7 +336,7 @@ func (r *NotificationRepository) RecordFailedAttempt(ctx context.Context, id str
// Same "not found" error shape as UpdateStatus above. The scheduler // Same "not found" error shape as UpdateStatus above. The scheduler
// logs-and-continues on this so a concurrently-deleted row doesn't // logs-and-continues on this so a concurrently-deleted row doesn't
// break the sweep. // break the sweep.
return fmt.Errorf("notification not found") return fmt.Errorf("notification not found: %w", repository.ErrNotFound)
} }
return nil return nil
} }
@@ -368,7 +368,7 @@ func (r *NotificationRepository) MarkAsDead(ctx context.Context, id string, last
return fmt.Errorf("failed to get rows affected: %w", err) return fmt.Errorf("failed to get rows affected: %w", err)
} }
if rows == 0 { if rows == 0 {
return fmt.Errorf("notification not found") return fmt.Errorf("notification not found: %w", repository.ErrNotFound)
} }
return nil return nil
} }
@@ -405,7 +405,7 @@ func (r *NotificationRepository) Requeue(ctx context.Context, id string) error {
return fmt.Errorf("failed to get rows affected: %w", err) return fmt.Errorf("failed to get rows affected: %w", err)
} }
if rows == 0 { if rows == 0 {
return fmt.Errorf("notification not found") return fmt.Errorf("notification not found: %w", repository.ErrNotFound)
} }
return nil return nil
} }
+4 -3
View File
@@ -1,6 +1,7 @@
package postgres package postgres
import ( import (
"github.com/shankar0123/certctl/internal/repository"
"context" "context"
"database/sql" "database/sql"
"fmt" "fmt"
@@ -61,7 +62,7 @@ func (r *OwnerRepository) Get(ctx context.Context, id string) (*domain.Owner, er
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return nil, fmt.Errorf("owner not found") return nil, fmt.Errorf("owner not found: %w", repository.ErrNotFound)
} }
return nil, fmt.Errorf("failed to query owner: %w", err) return nil, fmt.Errorf("failed to query owner: %w", err)
} }
@@ -110,7 +111,7 @@ func (r *OwnerRepository) Update(ctx context.Context, owner *domain.Owner) error
} }
if rows == 0 { if rows == 0 {
return fmt.Errorf("owner not found") return fmt.Errorf("owner not found: %w", repository.ErrNotFound)
} }
return nil return nil
@@ -130,7 +131,7 @@ func (r *OwnerRepository) Delete(ctx context.Context, id string) error {
} }
if rows == 0 { if rows == 0 {
return fmt.Errorf("owner not found") return fmt.Errorf("owner not found: %w", repository.ErrNotFound)
} }
return nil return nil
+3 -3
View File
@@ -63,7 +63,7 @@ func (r *PolicyRepository) GetRule(ctx context.Context, id string) (*domain.Poli
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return nil, fmt.Errorf("policy rule not found") return nil, fmt.Errorf("policy rule not found: %w", repository.ErrNotFound)
} }
return nil, fmt.Errorf("failed to query policy rule: %w", err) return nil, fmt.Errorf("failed to query policy rule: %w", err)
} }
@@ -114,7 +114,7 @@ func (r *PolicyRepository) UpdateRule(ctx context.Context, rule *domain.PolicyRu
} }
if rows == 0 { if rows == 0 {
return fmt.Errorf("policy rule not found") return fmt.Errorf("policy rule not found: %w", repository.ErrNotFound)
} }
return nil return nil
@@ -134,7 +134,7 @@ func (r *PolicyRepository) DeleteRule(ctx context.Context, id string) error {
} }
if rows == 0 { if rows == 0 {
return fmt.Errorf("policy rule not found") return fmt.Errorf("policy rule not found: %w", repository.ErrNotFound)
} }
return nil return nil
+4 -3
View File
@@ -1,6 +1,7 @@
package postgres package postgres
import ( import (
"github.com/shankar0123/certctl/internal/repository"
"context" "context"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
@@ -64,7 +65,7 @@ func (r *ProfileRepository) Get(ctx context.Context, id string) (*domain.Certifi
p, err := scanProfile(row) p, err := scanProfile(row)
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return nil, fmt.Errorf("profile not found") return nil, fmt.Errorf("profile not found: %w", repository.ErrNotFound)
} }
return nil, fmt.Errorf("failed to query profile: %w", err) return nil, fmt.Errorf("failed to query profile: %w", err)
} }
@@ -159,7 +160,7 @@ func (r *ProfileRepository) Update(ctx context.Context, profile *domain.Certific
} }
if rows == 0 { if rows == 0 {
return fmt.Errorf("profile not found") return fmt.Errorf("profile not found: %w", repository.ErrNotFound)
} }
return nil return nil
@@ -178,7 +179,7 @@ func (r *ProfileRepository) Delete(ctx context.Context, id string) error {
} }
if rows == 0 { if rows == 0 {
return fmt.Errorf("profile not found") return fmt.Errorf("profile not found: %w", repository.ErrNotFound)
} }
return nil return nil
@@ -72,7 +72,7 @@ func (r *RenewalPolicyRepository) Get(ctx context.Context, id string) (*domain.R
policy, err := scanRenewalPolicy(row) policy, err := scanRenewalPolicy(row)
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("renewal policy not found: %s", id) return nil, fmt.Errorf("renewal policy not found: %w", repository.ErrNotFound)
} }
return nil, fmt.Errorf("failed to query renewal policy: %w", err) return nil, fmt.Errorf("failed to query renewal policy: %w", err)
} }
@@ -252,7 +252,7 @@ func (r *RenewalPolicyRepository) Update(ctx context.Context, id string, policy
updated, err := scanRenewalPolicy(row) updated, err := scanRenewalPolicy(row)
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return fmt.Errorf("renewal policy not found: %s", id) return fmt.Errorf("renewal policy not found: %w", repository.ErrNotFound)
} }
if isUniqueViolation(err) { if isUniqueViolation(err) {
return repository.ErrRenewalPolicyDuplicateName return repository.ErrRenewalPolicyDuplicateName
@@ -283,7 +283,7 @@ func (r *RenewalPolicyRepository) Delete(ctx context.Context, id string) error {
return fmt.Errorf("failed to read RowsAffected for delete: %w", err) return fmt.Errorf("failed to read RowsAffected for delete: %w", err)
} }
if rows == 0 { if rows == 0 {
return fmt.Errorf("renewal policy not found: %s", id) return fmt.Errorf("renewal policy not found: %w", repository.ErrNotFound)
} }
return nil return nil
} }
+2 -1
View File
@@ -1,6 +1,7 @@
package postgres package postgres
import ( import (
"github.com/shankar0123/certctl/internal/repository"
"context" "context"
"database/sql" "database/sql"
"fmt" "fmt"
@@ -136,7 +137,7 @@ func (r *RevocationRepository) MarkIssuerNotified(ctx context.Context, id string
} }
if rows == 0 { if rows == 0 {
return fmt.Errorf("revocation not found") return fmt.Errorf("revocation not found: %w", repository.ErrNotFound)
} }
return nil return nil
+4 -3
View File
@@ -1,6 +1,7 @@
package postgres package postgres
import ( import (
"github.com/shankar0123/certctl/internal/repository"
"context" "context"
"database/sql" "database/sql"
"fmt" "fmt"
@@ -89,7 +90,7 @@ func (r *TargetRepository) Get(ctx context.Context, id string) (*domain.Deployme
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return nil, fmt.Errorf("target not found") return nil, fmt.Errorf("target not found: %w", repository.ErrNotFound)
} }
return nil, fmt.Errorf("failed to query target: %w", err) return nil, fmt.Errorf("failed to query target: %w", err)
} }
@@ -174,7 +175,7 @@ func (r *TargetRepository) Update(ctx context.Context, target *domain.Deployment
} }
if rows == 0 { if rows == 0 {
return fmt.Errorf("target not found") return fmt.Errorf("target not found: %w", repository.ErrNotFound)
} }
return nil return nil
@@ -194,7 +195,7 @@ func (r *TargetRepository) Delete(ctx context.Context, id string) error {
} }
if rows == 0 { if rows == 0 {
return fmt.Errorf("target not found") return fmt.Errorf("target not found: %w", repository.ErrNotFound)
} }
return nil return nil
+4 -3
View File
@@ -1,6 +1,7 @@
package postgres package postgres
import ( import (
"github.com/shankar0123/certctl/internal/repository"
"context" "context"
"database/sql" "database/sql"
"fmt" "fmt"
@@ -61,7 +62,7 @@ func (r *TeamRepository) Get(ctx context.Context, id string) (*domain.Team, erro
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return nil, fmt.Errorf("team not found") return nil, fmt.Errorf("team not found: %w", repository.ErrNotFound)
} }
return nil, fmt.Errorf("failed to query team: %w", err) return nil, fmt.Errorf("failed to query team: %w", err)
} }
@@ -108,7 +109,7 @@ func (r *TeamRepository) Update(ctx context.Context, team *domain.Team) error {
} }
if rows == 0 { if rows == 0 {
return fmt.Errorf("team not found") return fmt.Errorf("team not found: %w", repository.ErrNotFound)
} }
return nil return nil
@@ -128,7 +129,7 @@ func (r *TeamRepository) Delete(ctx context.Context, id string) error {
} }
if rows == 0 { if rows == 0 {
return fmt.Errorf("team not found") return fmt.Errorf("team not found: %w", repository.ErrNotFound)
} }
return nil return nil