internal/server/store/evolve_test.go
Ref: Size: 4.1 KiB History
package store
import (
"database/sql"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// ensureColumn must add a missing column exactly once and be a no-op after.
func TestEnsureColumnIdempotent(t *testing.T) {
s := newStore(t)
if err := ensureColumn(s.db, "tenants", "email", "TEXT NOT NULL DEFAULT ''"); err != nil {
t.Fatalf("first ensureColumn: %v", err)
}
if err := ensureColumn(s.db, "tenants", "email", "TEXT NOT NULL DEFAULT ''"); err != nil {
t.Fatalf("second ensureColumn: %v", err)
}
var n int
if err := s.db.QueryRow(`SELECT count(*) FROM pragma_table_info('tenants') WHERE name='email'`).Scan(&n); err != nil {
t.Fatal(err)
}
if n != 1 {
t.Fatalf("email column count = %d, want 1", n)
}
}
// TestRekeyRevokedSSHCertsCarriesRowsAndIsIdempotent covers the migration that
// re-keys revocation on (tenant, serial). A revocation the plane already holds
// must survive it — losing one silently un-revokes a certificate.
func TestRekeyRevokedSSHCertsCarriesRowsAndIsIdempotent(t *testing.T) {
db, err := sql.Open("sqlite", filepath.Join(t.TempDir(), "old.db"))
require.NoError(t, err)
defer db.Close()
// The table as it shipped: keyed on serial alone, tenant added later by
// ensureColumn.
_, err = db.Exec(`CREATE TABLE revoked_ssh_certs (
serial INTEGER PRIMARY KEY,
revoked_at DATETIME NOT NULL,
reason TEXT NOT NULL DEFAULT ''
)`)
require.NoError(t, err)
require.NoError(t, ensureColumn(db, "revoked_ssh_certs", "tenant", "TEXT NOT NULL DEFAULT 'default'"))
_, err = db.Exec(`INSERT INTO revoked_ssh_certs(serial, tenant, revoked_at, reason)
VALUES (7, 'alpha', '2026-01-01T00:00:00Z', 'leaked'), (9, 'beta', '2026-01-02T00:00:00Z', '')`)
require.NoError(t, err)
require.NoError(t, rekeyRevokedSSHCerts(db))
assert.Equal(t, []string{"tenant", "serial"}, mustPK(t, db), "key is now composite, in key order")
var n int
require.NoError(t, db.QueryRow(`SELECT COUNT(*) FROM revoked_ssh_certs`).Scan(&n))
assert.Equal(t, 2, n, "every revocation must survive the rebuild")
var reason string
require.NoError(t, db.QueryRow(`SELECT reason FROM revoked_ssh_certs WHERE tenant='alpha' AND serial=7`).Scan(&reason))
assert.Equal(t, "leaked", reason, "columns carry across, not just rows")
// Running again must be a no-op, since Open calls it on every boot.
require.NoError(t, rekeyRevokedSSHCerts(db))
require.NoError(t, db.QueryRow(`SELECT COUNT(*) FROM revoked_ssh_certs`).Scan(&n))
assert.Equal(t, 2, n)
// The point of the new key: two tenants may hold the same serial.
_, err = db.Exec(`INSERT INTO revoked_ssh_certs(tenant, serial, revoked_at, reason)
VALUES ('beta', 7, '2026-01-03T00:00:00Z', 'beta own cert')`)
require.NoError(t, err, "the old key would have rejected this and failed open")
}
func mustPK(t *testing.T, db *sql.DB) []string {
t.Helper()
cols, err := pkColumns(db, "revoked_ssh_certs")
require.NoError(t, err)
return cols
}
// TestRekeyWarnsOnRetiredDefaultTenantRows pins that the migration notices
// revocations it is about to render inert. Scoping enforcement means rows filed
// under the retired 'default' tenant stop denying anything, and a certificate
// quietly working again is the one outcome nobody would spot.
func TestRekeyWarnsOnRetiredDefaultTenantRows(t *testing.T) {
db, err := sql.Open("sqlite", filepath.Join(t.TempDir(), "legacy.db"))
require.NoError(t, err)
defer db.Close()
_, err = db.Exec(`CREATE TABLE revoked_ssh_certs (
serial INTEGER PRIMARY KEY, revoked_at DATETIME NOT NULL, reason TEXT NOT NULL DEFAULT '')`)
require.NoError(t, err)
require.NoError(t, ensureColumn(db, "revoked_ssh_certs", "tenant", "TEXT NOT NULL DEFAULT 'default'"))
_, err = db.Exec(`INSERT INTO revoked_ssh_certs(serial, revoked_at) VALUES (5, '2026-01-01T00:00:00Z')`)
require.NoError(t, err)
require.NoError(t, rekeyRevokedSSHCerts(db))
// The row is carried across rather than dropped — it is evidence, and an
// operator may want to re-file it under the owning tenant.
var tenant string
require.NoError(t, db.QueryRow(`SELECT tenant FROM revoked_ssh_certs WHERE serial=5`).Scan(&tenant))
assert.Equal(t, "default", tenant)
}