mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 19:51:33 +00:00
8b75e0311b
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.
186 lines
6.5 KiB
Go
186 lines
6.5 KiB
Go
// Copyright (c) certctl
|
|
// SPDX-License-Identifier: BSL-1.1
|
|
//
|
|
// Closes the #3 acquisition-readiness blocker from the 2026-05-01
|
|
// issuer coverage audit by pinning the atomic-audit-row contract on
|
|
// the issuance, renewal, and revocation paths.
|
|
//
|
|
// Pre-fix: cert insert / version insert / revocation insert ran on a
|
|
// *sql.DB connection while the audit row INSERT ran on a separate
|
|
// *sql.DB connection. A failed audit INSERT was logged but did not
|
|
// fail the operation — silently incomplete audit trail.
|
|
//
|
|
// Post-fix: when SetTransactor is wired (production via
|
|
// cmd/server/main.go), the operation runs inside Transactor.WithinTx
|
|
// and any audit-insert failure rolls back the entire transaction.
|
|
//
|
|
// These tests use mockTransactor + mockAuditRepo with CreateErr to
|
|
// simulate audit-insert failure. The mock repos share state in memory
|
|
// (no real rollback), so the test asserts the contract via the
|
|
// returned error and the auditService side effect, not by inspecting
|
|
// post-rollback row counts. The testcontainers-backed sibling test in
|
|
// the postgres package exercises real-Postgres rollback semantics
|
|
// against a real audit_events table.
|
|
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/certctl-io/certctl/internal/domain"
|
|
"github.com/certctl-io/certctl/internal/repository"
|
|
)
|
|
|
|
// TestCertificateService_Create_AtomicWithTx asserts the issuance path
|
|
// runs inside Transactor.WithinTx when the transactor is wired. Without
|
|
// the wrapping, an audit-insert failure would silently log; with it,
|
|
// the failure surfaces as the operation's error.
|
|
func TestCertificateService_Create_AtomicWithTx(t *testing.T) {
|
|
auditRepo := newMockAuditRepository()
|
|
auditRepo.CreateErr = errors.New("simulated audit insert failure")
|
|
auditService := NewAuditService(auditRepo)
|
|
|
|
certRepo := newMockCertificateRepository()
|
|
policyService := NewPolicyService(newMockPolicyRepository(), auditService)
|
|
|
|
svc := NewCertificateService(certRepo, policyService, auditService)
|
|
svc.SetTransactor(newMockTransactor())
|
|
|
|
cert := &domain.ManagedCertificate{
|
|
ID: "mc-test-atomic",
|
|
Name: "atomic-test",
|
|
CommonName: "atomic.example.com",
|
|
IssuerID: "iss-test",
|
|
}
|
|
|
|
err := svc.Create(context.Background(), cert, "test-actor")
|
|
if err == nil {
|
|
t.Fatal("Create should fail when audit insert fails inside the transaction")
|
|
}
|
|
if !errIncludes(err, "audit") {
|
|
t.Errorf("expected error to mention audit, got: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestCertificateService_Create_LegacyPathLogs asserts the pre-fix
|
|
// behavior is preserved when SetTransactor is NOT wired: audit failure
|
|
// is logged but the operation succeeds (returns nil). This documents
|
|
// the backward-compat fallback so callers that haven't migrated to the
|
|
// atomic path still build and run.
|
|
func TestCertificateService_Create_LegacyPathLogs(t *testing.T) {
|
|
auditRepo := newMockAuditRepository()
|
|
auditRepo.CreateErr = errors.New("simulated audit insert failure")
|
|
auditService := NewAuditService(auditRepo)
|
|
|
|
certRepo := newMockCertificateRepository()
|
|
policyService := NewPolicyService(newMockPolicyRepository(), auditService)
|
|
|
|
svc := NewCertificateService(certRepo, policyService, auditService)
|
|
// Intentionally NOT calling SetTransactor — exercise the legacy
|
|
// path.
|
|
|
|
cert := &domain.ManagedCertificate{
|
|
ID: "mc-test-legacy",
|
|
Name: "legacy-test",
|
|
CommonName: "legacy.example.com",
|
|
IssuerID: "iss-test",
|
|
}
|
|
|
|
err := svc.Create(context.Background(), cert, "test-actor")
|
|
if err != nil {
|
|
t.Fatalf("legacy path should swallow audit failure, got: %v", err)
|
|
}
|
|
// The cert insert still landed in the mock — the audit failure
|
|
// did not roll it back (because there's no transaction). This is
|
|
// the audit's blocker behavior; it remains for callers that
|
|
// haven't wired SetTransactor.
|
|
if _, ok := certRepo.Certs["mc-test-legacy"]; !ok {
|
|
t.Fatal("cert insert should land in legacy path even when audit fails")
|
|
}
|
|
}
|
|
|
|
// TestCertificateService_Create_TransactorBeginFailure asserts that
|
|
// when Transactor.WithinTx itself fails (BeginTx error path), the
|
|
// operation surfaces the error and no cert insert happens.
|
|
func TestCertificateService_Create_TransactorBeginFailure(t *testing.T) {
|
|
auditRepo := newMockAuditRepository()
|
|
auditService := NewAuditService(auditRepo)
|
|
|
|
certRepo := newMockCertificateRepository()
|
|
policyService := NewPolicyService(newMockPolicyRepository(), auditService)
|
|
|
|
tx := newMockTransactor()
|
|
tx.BeginTxErr = errors.New("simulated begin tx failure")
|
|
|
|
svc := NewCertificateService(certRepo, policyService, auditService)
|
|
svc.SetTransactor(tx)
|
|
|
|
cert := &domain.ManagedCertificate{
|
|
ID: "mc-test-begin-fail",
|
|
Name: "begin-fail",
|
|
CommonName: "begin-fail.example.com",
|
|
IssuerID: "iss-test",
|
|
}
|
|
|
|
err := svc.Create(context.Background(), cert, "test-actor")
|
|
if err == nil {
|
|
t.Fatal("Create should fail when BeginTx fails")
|
|
}
|
|
if _, ok := certRepo.Certs["mc-test-begin-fail"]; ok {
|
|
t.Fatal("cert insert must NOT happen when BeginTx fails — fn never ran")
|
|
}
|
|
if len(auditRepo.Events) > 0 {
|
|
t.Fatal("audit insert must NOT happen when BeginTx fails")
|
|
}
|
|
}
|
|
|
|
// TestCertificateService_Create_TransactorCommitFailure asserts that
|
|
// a Commit failure after successful in-fn writes surfaces as the
|
|
// operation's error. Real Postgres can fail Commit on serialization
|
|
// conflicts; the service must report this rather than swallowing it.
|
|
func TestCertificateService_Create_TransactorCommitFailure(t *testing.T) {
|
|
auditRepo := newMockAuditRepository()
|
|
auditService := NewAuditService(auditRepo)
|
|
|
|
certRepo := newMockCertificateRepository()
|
|
policyService := NewPolicyService(newMockPolicyRepository(), auditService)
|
|
|
|
tx := newMockTransactor()
|
|
tx.CommitErr = errors.New("simulated commit failure")
|
|
|
|
svc := NewCertificateService(certRepo, policyService, auditService)
|
|
svc.SetTransactor(tx)
|
|
|
|
cert := &domain.ManagedCertificate{
|
|
ID: "mc-test-commit-fail",
|
|
Name: "commit-fail",
|
|
CommonName: "commit-fail.example.com",
|
|
IssuerID: "iss-test",
|
|
}
|
|
|
|
err := svc.Create(context.Background(), cert, "test-actor")
|
|
if err == nil {
|
|
t.Fatal("Create should fail when Commit fails")
|
|
}
|
|
}
|
|
|
|
// Compile-time guard: ensure mockTransactor satisfies repository.Transactor.
|
|
var _ repository.Transactor = (*mockTransactor)(nil)
|
|
|
|
// errIncludes is a tiny strings.Contains alias for use in error-message
|
|
// assertions — keeps the test file dependency-light.
|
|
func errIncludes(err error, sub string) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
s := err.Error()
|
|
for i := 0; i+len(sub) <= len(s); i++ {
|
|
if s[i:i+len(sub)] == sub {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|