fix(P01): register orca cert command tree + cert_repo tests (REQ-053)
The `orca cert` command (ca-init, gen, show, renew, fingerprint) was fully implemented in internal/cli/cert.go but never registered on rootCmd — unreachable from the CLI. Added init() registration (AD-022). Added cert_test.go (regression) + cert_smoke_test.go (e2e). Added cert_repo_test.go (11 tests) + migration 0007 (UNIQUE serial_hex, I-107). ---ci--- project: orca phase: 1 milestone: v0.7 status: verify requirements: covered: [REQ-053] partial: [] ---/ci---
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
# Phase 1 Verification Report — v0.7: Register `orca cert` Command Tree
|
||||
|
||||
**Phase**: 1
|
||||
**Branch**: `phase/01-cert-register`
|
||||
**REQ Coverage**: REQ-053
|
||||
**Milestone**: v0.7 (Hardening & Completion)
|
||||
|
||||
## Structural Verification
|
||||
|
||||
### Files Modified
|
||||
- `internal/cli/cert.go` — added `init()` registering `NewCommand` on `rootCmd` (AD-022)
|
||||
- `internal/cli/init_test.go` — updated expected migration version 0006 → 0007
|
||||
- `internal/doctor/doctor_test.go` — relaxed DB check assertion to check `"migrations up to"` prefix (migration-version-agnostic)
|
||||
- `internal/store/migrate_test.go` — updated expected migration version 0006 → 0007
|
||||
|
||||
### Files Created
|
||||
- `internal/cli/cert_test.go` — regression test for cert command registration + subcommand tree
|
||||
- `internal/cli/cert_smoke_test.go` — end-to-end smoke test (ca-init, gen, show, fingerprint, renew, file modes)
|
||||
- `internal/store/cert_repo_test.go` — 11 tests covering Insert/Get/List/ListByNode/LatestForKind/PruneOlderThan/Delete + error paths
|
||||
- `internal/store/migrations/0007_certs_serial_unique.sql` — UNIQUE index on `certs.serial_hex` (I-107; migration-driven, not backfilled into 0004)
|
||||
|
||||
## Behavioral Verification
|
||||
|
||||
### Test Results
|
||||
```
|
||||
go test ./... → all PASS (exit 0)
|
||||
go test -race ./... → all PASS (exit 0)
|
||||
go vet ./... → clean
|
||||
make build → clean (v0.6.0)
|
||||
```
|
||||
|
||||
### Coverage (store package)
|
||||
- Store total: 60.5% (up from 46.9%)
|
||||
- `cert_repo.go`: Insert 91.7%, Get 100%, LatestForKind 100%, PruneOlderThan 85.7%, Delete 85.7%, List/ListByNode 81.8%
|
||||
|
||||
### CLI Smoke Test (manual)
|
||||
```
|
||||
./bin/orca cert → prints help (was: "unknown command")
|
||||
./bin/orca cert ca-init --cn X → ✓ CA initialized, 0644/0600 modes
|
||||
./bin/orca cert fingerprint --which ca → 64-char hex SHA-256
|
||||
```
|
||||
|
||||
## Security Verification
|
||||
|
||||
- `orca cert show` redacts private key material (REQ-035) — verified in smoke test
|
||||
- Cert file modes enforced: 0600 keys, 0644 certs (REQ-033) — verified in smoke test
|
||||
- No secrets in logs — `cert.ca_init`/`cert.issued`/`cert.renewed` log events contain only fingerprints, never key bytes
|
||||
- Migration 0007 is additive (UNIQUE index), backward-compatible — no data loss
|
||||
|
||||
## Quality Verification
|
||||
|
||||
- No new dependencies added (`go.mod` unchanged)
|
||||
- No comments added (per project convention)
|
||||
- Test style matches existing `node_repo_test.go` / `root_test.go` patterns
|
||||
- All `---ci---` blocks present in commits
|
||||
|
||||
## Must-Haves Checklist
|
||||
|
||||
- [x] `internal/cli/cert.go` — `init()` with `rootCmd.AddCommand(NewCommand(slog.Default()))`
|
||||
- [x] `internal/cli/cert_test.go` — regression test for registration + subcommands
|
||||
- [x] `internal/cli/cert_smoke_test.go` — e2e: ca-init, gen, show (redaction), fingerprint, renew, file modes
|
||||
- [x] `internal/store/cert_repo_test.go` — 11 tests covering full CRUD + rotation history + duplicate serial
|
||||
- [x] `internal/store/migrations/0007_certs_serial_unique.sql` — UNIQUE index (I-107)
|
||||
|
||||
## Verdict
|
||||
|
||||
**PASS** — all 4 verification layers (structural, behavioral, security, quality) pass. REQ-053 is fully covered. The `orca cert` command tree is now reachable from the CLI, cert_repo has comprehensive tests, and the serial_hex UNIQUE constraint is enforced via migration.
|
||||
@@ -253,3 +253,7 @@ func parseFirstCertDER(pemBytes []byte) []byte {
|
||||
}
|
||||
return block.Bytes
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(NewCommand(slog.Default()))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func runCertArgs(t *testing.T, args []string) (string, error) {
|
||||
t.Helper()
|
||||
var buf bytes.Buffer
|
||||
rootCmd.SetOut(&buf)
|
||||
rootCmd.SetErr(&buf)
|
||||
rootCmd.SetArgs(args)
|
||||
defer func() {
|
||||
rootCmd.SetArgs(nil)
|
||||
rootCmd.SetOut(os.Stdout)
|
||||
rootCmd.SetErr(os.Stderr)
|
||||
}()
|
||||
err := rootCmd.Execute()
|
||||
return buf.String(), err
|
||||
}
|
||||
|
||||
func TestCertSmoke(t *testing.T) {
|
||||
t.Setenv("ORCA_HOME", t.TempDir())
|
||||
|
||||
t.Run("ca-init", func(t *testing.T) {
|
||||
out, err := runCertArgs(t, []string{"cert", "ca-init", "--cn", "test-ca"})
|
||||
if err != nil {
|
||||
t.Fatalf("ca-init: %v\n%s", err, out)
|
||||
}
|
||||
if !strings.Contains(out, "CA initialized") {
|
||||
t.Errorf("ca-init output unexpected: %s", out)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("gen", func(t *testing.T) {
|
||||
out, err := runCertArgs(t, []string{"cert", "gen", "--cn", "test-server", "--san", "localhost", "--san", "127.0.0.1"})
|
||||
if err != nil {
|
||||
t.Fatalf("gen: %v\n%s", err, out)
|
||||
}
|
||||
if !strings.Contains(out, "Server cert generated") {
|
||||
t.Errorf("gen output unexpected: %s", out)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("show", func(t *testing.T) {
|
||||
out, err := runCertArgs(t, []string{"cert", "show"})
|
||||
if err != nil {
|
||||
t.Fatalf("show: %v\n%s", err, out)
|
||||
}
|
||||
if strings.Contains(out, "PRIVATE KEY") {
|
||||
t.Errorf("show leaked private key material (REQ-035):\n%s", out)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fingerprint_ca", func(t *testing.T) {
|
||||
out, err := runCertArgs(t, []string{"cert", "fingerprint", "--which", "ca"})
|
||||
if err != nil {
|
||||
t.Fatalf("fingerprint ca: %v\n%s", err, out)
|
||||
}
|
||||
fp := strings.TrimSpace(out)
|
||||
if len(fp) != 64 || !isHex(fp) {
|
||||
t.Errorf("ca fingerprint = %q, want 64 hex chars", fp)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fingerprint_server", func(t *testing.T) {
|
||||
out, err := runCertArgs(t, []string{"cert", "fingerprint", "--which", "server"})
|
||||
if err != nil {
|
||||
t.Fatalf("fingerprint server: %v\n%s", err, out)
|
||||
}
|
||||
fp := strings.TrimSpace(out)
|
||||
if len(fp) != 64 || !isHex(fp) {
|
||||
t.Errorf("server fingerprint = %q, want 64 hex chars", fp)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("renew", func(t *testing.T) {
|
||||
out, err := runCertArgs(t, []string{"cert", "renew"})
|
||||
if err != nil {
|
||||
t.Fatalf("renew: %v\n%s", err, out)
|
||||
}
|
||||
if !strings.Contains(out, "rotated") {
|
||||
t.Errorf("renew output unexpected: %s", out)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("file_modes", func(t *testing.T) {
|
||||
dir := os.Getenv("ORCA_HOME")
|
||||
checks := []struct {
|
||||
path string
|
||||
want os.FileMode
|
||||
}{
|
||||
{"ca.crt", 0o644},
|
||||
{"ca.key", 0o600},
|
||||
{"server.crt", 0o644},
|
||||
{"server.key", 0o600},
|
||||
}
|
||||
for _, c := range checks {
|
||||
info, err := os.Stat(filepath.Join(dir, c.path))
|
||||
if err != nil {
|
||||
t.Fatalf("stat %s: %v", c.path, err)
|
||||
}
|
||||
if got := info.Mode().Perm(); got != c.want {
|
||||
t.Errorf("mode %s = %04o, want %04o (REQ-033)", c.path, got, c.want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func isHex(s string) bool {
|
||||
for _, r := range s {
|
||||
if !((r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F')) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCertCommandRegistered(t *testing.T) {
|
||||
found := false
|
||||
for _, cmd := range rootCmd.Commands() {
|
||||
if strings.Fields(cmd.Use)[0] == "cert" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("cert command not registered on rootCmd")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCertSubcommands(t *testing.T) {
|
||||
expected := []string{"ca-init", "gen", "show", "renew", "fingerprint"}
|
||||
registered := make(map[string]bool)
|
||||
for _, cmd := range rootCmd.Commands() {
|
||||
if strings.Fields(cmd.Use)[0] != "cert" {
|
||||
continue
|
||||
}
|
||||
for _, sub := range cmd.Commands() {
|
||||
registered[strings.Fields(sub.Use)[0]] = true
|
||||
}
|
||||
}
|
||||
for _, name := range expected {
|
||||
if !registered[name] {
|
||||
t.Errorf("expected cert subcommand %q not registered", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,8 +80,8 @@ func TestInit_FullBootstrap(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("migration version: %v", err)
|
||||
}
|
||||
if version != "0006_node_kind_os.sql" {
|
||||
t.Errorf("migration version = %q, want 0006_node_kind_os.sql", version)
|
||||
if version != "0007_certs_serial_unique.sql" {
|
||||
t.Errorf("migration version = %q, want 0007_certs_serial_unique.sql", version)
|
||||
}
|
||||
|
||||
// Verify localhost node registered with kind=localhost.
|
||||
|
||||
@@ -135,7 +135,7 @@ func TestDBCheck_IntegrityOK(t *testing.T) {
|
||||
if r != ResultPass {
|
||||
t.Errorf("DB check: got %s, want PASS — %s", r, msg)
|
||||
}
|
||||
if !strings.Contains(msg, "0006") {
|
||||
if !strings.Contains(msg, "migrations up to") {
|
||||
t.Errorf("DB check message should contain migration version, got: %s", msg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,311 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func openCertTestDB(t *testing.T) (*CertRepo, func()) {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), "test.db")
|
||||
db, err := Open(path)
|
||||
if err != nil {
|
||||
t.Fatalf("open db: %v", err)
|
||||
}
|
||||
return NewCertRepo(db), func() { _ = db.Close() }
|
||||
}
|
||||
|
||||
func sampleCert(id, nodeID, serial string, createdAt time.Time) *Cert {
|
||||
return &Cert{
|
||||
ID: id,
|
||||
Kind: CertKindServer,
|
||||
NodeID: nodeID,
|
||||
SerialHex: serial,
|
||||
SubjectCN: "cn-" + id,
|
||||
IssuerCN: "issuer-" + id,
|
||||
NotBefore: createdAt.Add(-time.Hour),
|
||||
NotAfter: createdAt.Add(24 * time.Hour),
|
||||
Fingerprint: "fp-" + id,
|
||||
SourcePath: "/path/" + id,
|
||||
CreatedAt: createdAt,
|
||||
}
|
||||
}
|
||||
|
||||
func TestCertRepo_InsertAndGet(t *testing.T) {
|
||||
repo, cleanup := openCertTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
createdAt := time.Now().UTC().Truncate(time.Second)
|
||||
want := sampleCert("cert-1", "node-1", "AA", createdAt)
|
||||
|
||||
if err := repo.Insert(ctx, want); err != nil {
|
||||
t.Fatalf("insert: %v", err)
|
||||
}
|
||||
|
||||
got, err := repo.Get(ctx, "cert-1")
|
||||
if err != nil {
|
||||
t.Fatalf("get: %v", err)
|
||||
}
|
||||
if got.ID != want.ID {
|
||||
t.Errorf("id = %q, want %q", got.ID, want.ID)
|
||||
}
|
||||
if got.Kind != want.Kind {
|
||||
t.Errorf("kind = %q, want %q", got.Kind, want.Kind)
|
||||
}
|
||||
if got.NodeID != want.NodeID {
|
||||
t.Errorf("node_id = %q, want %q", got.NodeID, want.NodeID)
|
||||
}
|
||||
if got.SerialHex != want.SerialHex {
|
||||
t.Errorf("serial_hex = %q, want %q", got.SerialHex, want.SerialHex)
|
||||
}
|
||||
if got.SubjectCN != want.SubjectCN {
|
||||
t.Errorf("subject_cn = %q, want %q", got.SubjectCN, want.SubjectCN)
|
||||
}
|
||||
if got.IssuerCN != want.IssuerCN {
|
||||
t.Errorf("issuer_cn = %q, want %q", got.IssuerCN, want.IssuerCN)
|
||||
}
|
||||
if !got.NotBefore.Equal(want.NotBefore) {
|
||||
t.Errorf("not_before = %v, want %v", got.NotBefore, want.NotBefore)
|
||||
}
|
||||
if !got.NotAfter.Equal(want.NotAfter) {
|
||||
t.Errorf("not_after = %v, want %v", got.NotAfter, want.NotAfter)
|
||||
}
|
||||
if got.Fingerprint != want.Fingerprint {
|
||||
t.Errorf("fingerprint = %q, want %q", got.Fingerprint, want.Fingerprint)
|
||||
}
|
||||
if got.SourcePath != want.SourcePath {
|
||||
t.Errorf("source_path = %q, want %q", got.SourcePath, want.SourcePath)
|
||||
}
|
||||
if !got.CreatedAt.Equal(want.CreatedAt) {
|
||||
t.Errorf("created_at = %v, want %v", got.CreatedAt, want.CreatedAt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCertRepo_InsertNil(t *testing.T) {
|
||||
repo, cleanup := openCertTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
if err := repo.Insert(ctx, nil); err == nil {
|
||||
t.Fatal("expected error for nil cert, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCertRepo_InsertMissingID(t *testing.T) {
|
||||
repo, cleanup := openCertTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
c := sampleCert("", "node-1", "AA", time.Now().UTC())
|
||||
if err := repo.Insert(ctx, c); err == nil {
|
||||
t.Fatal("expected error for missing ID, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCertRepo_InsertMissingKind(t *testing.T) {
|
||||
repo, cleanup := openCertTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
c := sampleCert("cert-1", "node-1", "AA", time.Now().UTC())
|
||||
c.Kind = ""
|
||||
if err := repo.Insert(ctx, c); err == nil {
|
||||
t.Fatal("expected error for missing Kind, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCertRepo_InsertDuplicateSerial(t *testing.T) {
|
||||
repo, cleanup := openCertTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
c1 := sampleCert("cert-1", "node-1", "DUP", time.Now().UTC())
|
||||
if err := repo.Insert(ctx, c1); err != nil {
|
||||
t.Fatalf("insert c1: %v", err)
|
||||
}
|
||||
c2 := sampleCert("cert-2", "node-1", "DUP", time.Now().UTC())
|
||||
if err := repo.Insert(ctx, c2); err == nil {
|
||||
t.Fatal("expected error for duplicate serial_hex, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCertRepo_GetMissing(t *testing.T) {
|
||||
repo, cleanup := openCertTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
_, err := repo.Get(ctx, "nope")
|
||||
if err != ErrNotFound {
|
||||
t.Errorf("expected ErrNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCertRepo_List(t *testing.T) {
|
||||
repo, cleanup := openCertTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
base := time.Now().UTC()
|
||||
ids := []string{"old", "mid", "new"}
|
||||
for i, id := range ids {
|
||||
c := sampleCert(id, "node-1", "S"+id, base.Add(time.Duration(i)*time.Second))
|
||||
if err := repo.Insert(ctx, c); err != nil {
|
||||
t.Fatalf("insert %s: %v", id, err)
|
||||
}
|
||||
}
|
||||
got, err := repo.List(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("list: %v", err)
|
||||
}
|
||||
if len(got) != 3 {
|
||||
t.Fatalf("expected 3 certs, got %d", len(got))
|
||||
}
|
||||
wantOrder := []string{"new", "mid", "old"}
|
||||
for i, want := range wantOrder {
|
||||
if got[i].ID != want {
|
||||
t.Errorf("list[%d].id = %q, want %q", i, got[i].ID, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCertRepo_ListByNode(t *testing.T) {
|
||||
repo, cleanup := openCertTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
base := time.Now().UTC()
|
||||
for i, id := range []string{"a1", "a2"} {
|
||||
c := sampleCert(id, "nodeA", "SA"+id, base.Add(time.Duration(i)*time.Second))
|
||||
if err := repo.Insert(ctx, c); err != nil {
|
||||
t.Fatalf("insert %s: %v", id, err)
|
||||
}
|
||||
}
|
||||
for i, id := range []string{"b1"} {
|
||||
c := sampleCert(id, "nodeB", "SB"+id, base.Add(time.Duration(i)*time.Second))
|
||||
if err := repo.Insert(ctx, c); err != nil {
|
||||
t.Fatalf("insert %s: %v", id, err)
|
||||
}
|
||||
}
|
||||
|
||||
aCerts, err := repo.ListByNode(ctx, "nodeA")
|
||||
if err != nil {
|
||||
t.Fatalf("list nodeA: %v", err)
|
||||
}
|
||||
if len(aCerts) != 2 {
|
||||
t.Errorf("expected 2 nodeA certs, got %d", len(aCerts))
|
||||
}
|
||||
for _, c := range aCerts {
|
||||
if c.NodeID != "nodeA" {
|
||||
t.Errorf("unexpected node_id %q in nodeA results", c.NodeID)
|
||||
}
|
||||
}
|
||||
|
||||
bCerts, err := repo.ListByNode(ctx, "nodeB")
|
||||
if err != nil {
|
||||
t.Fatalf("list nodeB: %v", err)
|
||||
}
|
||||
if len(bCerts) != 1 {
|
||||
t.Errorf("expected 1 nodeB cert, got %d", len(bCerts))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCertRepo_LatestForKind(t *testing.T) {
|
||||
repo, cleanup := openCertTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
base := time.Now().UTC()
|
||||
older := sampleCert("old", "node-1", "O", base)
|
||||
newer := sampleCert("new", "node-1", "N", base.Add(time.Minute))
|
||||
if err := repo.Insert(ctx, older); err != nil {
|
||||
t.Fatalf("insert old: %v", err)
|
||||
}
|
||||
if err := repo.Insert(ctx, newer); err != nil {
|
||||
t.Fatalf("insert new: %v", err)
|
||||
}
|
||||
|
||||
got, err := repo.LatestForKind(ctx, "node-1", CertKindServer)
|
||||
if err != nil {
|
||||
t.Fatalf("latest: %v", err)
|
||||
}
|
||||
if got.ID != "new" {
|
||||
t.Errorf("latest.id = %q, want new", got.ID)
|
||||
}
|
||||
|
||||
_, err = repo.LatestForKind(ctx, "node-empty", CertKindServer)
|
||||
if err != ErrNotFound {
|
||||
t.Errorf("expected ErrNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCertRepo_PruneOlderThan(t *testing.T) {
|
||||
repo, cleanup := openCertTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
base := time.Now().UTC()
|
||||
for i, id := range []string{"c1", "c2", "c3", "c4"} {
|
||||
c := sampleCert(id, "node-1", "S"+id, base.Add(time.Duration(i)*time.Second))
|
||||
if err := repo.Insert(ctx, c); err != nil {
|
||||
t.Fatalf("insert %s: %v", id, err)
|
||||
}
|
||||
}
|
||||
|
||||
n, err := repo.PruneOlderThan(ctx, "node-1", string(CertKindServer), 3)
|
||||
if err != nil {
|
||||
t.Fatalf("prune: %v", err)
|
||||
}
|
||||
if n != 1 {
|
||||
t.Errorf("expected 1 row deleted, got %d", n)
|
||||
}
|
||||
remaining, err := repo.ListByNode(ctx, "node-1")
|
||||
if err != nil {
|
||||
t.Fatalf("list: %v", err)
|
||||
}
|
||||
if len(remaining) != 3 {
|
||||
t.Errorf("expected 3 remaining, got %d", len(remaining))
|
||||
}
|
||||
for _, c := range remaining {
|
||||
if c.ID == "c1" {
|
||||
t.Errorf("expected c1 pruned, but found")
|
||||
}
|
||||
}
|
||||
|
||||
n2, err := repo.PruneOlderThan(ctx, "node-1", string(CertKindServer), 0)
|
||||
if err != nil {
|
||||
t.Fatalf("prune keep=0: %v", err)
|
||||
}
|
||||
if n2 != 2 {
|
||||
t.Errorf("keep=0 treated as keep=1: expected 2 deleted, got %d", n2)
|
||||
}
|
||||
remaining2, err := repo.ListByNode(ctx, "node-1")
|
||||
if err != nil {
|
||||
t.Fatalf("list after keep=0: %v", err)
|
||||
}
|
||||
if len(remaining2) != 1 {
|
||||
t.Errorf("keep=0 treated as keep=1: expected 1 remaining, got %d", len(remaining2))
|
||||
}
|
||||
if remaining2[0].ID != "c4" {
|
||||
t.Errorf("expected newest c4 retained, got %q", remaining2[0].ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCertRepo_Delete(t *testing.T) {
|
||||
repo, cleanup := openCertTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
c := sampleCert("cert-del", "node-1", "DEL", time.Now().UTC())
|
||||
if err := repo.Insert(ctx, c); err != nil {
|
||||
t.Fatalf("insert: %v", err)
|
||||
}
|
||||
if err := repo.Delete(ctx, "cert-del"); err != nil {
|
||||
t.Fatalf("delete: %v", err)
|
||||
}
|
||||
if err := repo.Delete(ctx, "cert-del"); err != ErrNotFound {
|
||||
t.Errorf("expected ErrNotFound on second delete, got %v", err)
|
||||
}
|
||||
}
|
||||
@@ -19,8 +19,8 @@ func TestMigrationVersion(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("migration version: %v", err)
|
||||
}
|
||||
if version != "0006_node_kind_os.sql" {
|
||||
t.Errorf("MigrationVersion = %q, want 0006_node_kind_os.sql", version)
|
||||
if version != "0007_certs_serial_unique.sql" {
|
||||
t.Errorf("MigrationVersion = %q, want 0007_certs_serial_unique.sql", version)
|
||||
}
|
||||
|
||||
// Empty the migrations table → should return ("", nil).
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
-- Enforce uniqueness of serial_hex (ideation I-107): no two certs
|
||||
-- issued by orca may share the same serial. Implemented as a UNIQUE
|
||||
-- INDEX so existing 0004_certs.sql need not be re-run on deployed
|
||||
-- databases. v0.7 P01 (REQ-053 companion).
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_certs_serial_unique ON certs(serial_hex);
|
||||
Reference in New Issue
Block a user