chore(merge): 整合最新 main 与统一认证变更
ci / verify (pull_request) Successful in 12m18s
ci / verify (pull_request) Successful in 12m18s
合并远端生产 CI、依赖与 API 文档改动,保留统一认证和 SSF 能力。由于远端生产基线已占用 0062,将尚未发布的身份迁移整理为 0063 至 0068 的最终增量 Schema,移除仅用于开发期草稿升级的破坏性迁移步骤。 验证:go test ./...。其余生产门禁在合并提交后继续执行。
This commit is contained in:
+29
-181
@@ -10,149 +10,45 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
const identityPairingStartReservationUpgradeMigration = "../../migrations/0073_identity_pairing_start_reservation_upgrade.sql"
|
||||
const identityPairingStartReservationMigration = "../../migrations/0068_identity_pairing_start_reservation.sql"
|
||||
|
||||
func TestIdentityPairingStartReservationUpgradeMigrationDefinesCurrentLifecycle(t *testing.T) {
|
||||
payload, err := os.ReadFile(identityPairingStartReservationUpgradeMigration)
|
||||
func TestIdentityPairingStartReservationMigrationDefinesCurrentLifecycle(t *testing.T) {
|
||||
payload, err := os.ReadFile(identityPairingStartReservationMigration)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
content := strings.ToLower(string(payload))
|
||||
for _, required := range []string{
|
||||
"add column if not exists state text",
|
||||
"add column if not exists revision_id uuid",
|
||||
"add column if not exists updated_at timestamptz",
|
||||
"alter column state set not null",
|
||||
"state text not null default 'starting'",
|
||||
"revision_id uuid unique references gateway_identity_configuration_revisions",
|
||||
"updated_at timestamptz not null default now()",
|
||||
"gateway_identity_pairing_start_state_check",
|
||||
"foreign key (revision_id)",
|
||||
"idx_gateway_identity_pairing_start_revision_unique",
|
||||
"idx_gateway_identity_pairing_start_expiry",
|
||||
"canonical_pairing",
|
||||
"on conflict (singleton) do update",
|
||||
"on conflict do nothing",
|
||||
} {
|
||||
if !strings.Contains(content, required) {
|
||||
t.Fatalf("identity pairing start reservation upgrade migration is missing %q", required)
|
||||
t.Fatalf("identity pairing start reservation migration is missing %q", required)
|
||||
}
|
||||
}
|
||||
for _, forbidden := range []string{"exchange_token text", "client_secret", "machine_secret", "secret_value"} {
|
||||
for _, forbidden := range []string{"exchange_token text", "client_secret", "machine_secret", "secret_value", "drop ", "do $"} {
|
||||
if strings.Contains(content, forbidden) {
|
||||
t.Fatalf("identity pairing start reservation upgrade migration stores forbidden value field %q", forbidden)
|
||||
t.Fatalf("identity pairing start reservation migration contains forbidden content %q", forbidden)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentityPairingStartReservationUpgradeMigratesLegacyShapeAndIsIdempotent(t *testing.T) {
|
||||
func TestIdentityPairingStartReservationMigrationSeedsOutstandingPairing(t *testing.T) {
|
||||
pool := newIdentityMigrationPostgresTestSchema(t)
|
||||
ctx := context.Background()
|
||||
applyIdentityPairingStartReservationPrerequisites(t, ctx, pool)
|
||||
|
||||
revisionID, pairingID, pairingExpiresAt := seedOutstandingIdentityPairingForReservationMigration(t, ctx, pool)
|
||||
legacyAttemptID := uuid.NewString()
|
||||
if _, err := pool.Exec(ctx, `
|
||||
CREATE TABLE gateway_identity_pairing_start_reservation (
|
||||
singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton),
|
||||
attempt_id uuid NOT NULL UNIQUE,
|
||||
expires_at timestamptz NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
)`); err != nil {
|
||||
t.Fatalf("create legacy pairing start reservation: %v", err)
|
||||
}
|
||||
if _, err := pool.Exec(ctx, `
|
||||
INSERT INTO gateway_identity_pairing_start_reservation(singleton,attempt_id,expires_at)
|
||||
VALUES(true,$1::uuid,now()+interval '2 minutes')`, legacyAttemptID); err != nil {
|
||||
t.Fatalf("seed legacy pairing start reservation: %v", err)
|
||||
}
|
||||
|
||||
applyIdentityMigrationTestFile(t, ctx, pool, identityPairingStartReservationUpgradeMigration)
|
||||
assertCurrentIdentityPairingStartReservation(t, ctx, pool, pairingID, revisionID, pairingExpiresAt)
|
||||
assertIdentityPairingStartReservationSchema(t, ctx, pool)
|
||||
|
||||
// Migration runners apply each version once, but a repeat execution proves
|
||||
// that recovery from an interrupted/manual rollout does not duplicate state.
|
||||
applyIdentityMigrationTestFile(t, ctx, pool, identityPairingStartReservationUpgradeMigration)
|
||||
assertCurrentIdentityPairingStartReservation(t, ctx, pool, pairingID, revisionID, pairingExpiresAt)
|
||||
assertIdentityPairingStartReservationSchema(t, ctx, pool)
|
||||
}
|
||||
|
||||
func TestIdentityPairingStartReservationUpgradePreservesLegacyStartingLease(t *testing.T) {
|
||||
pool := newIdentityMigrationPostgresTestSchema(t)
|
||||
ctx := context.Background()
|
||||
applyIdentityPairingStartReservationPrerequisites(t, ctx, pool)
|
||||
|
||||
attemptID := uuid.NewString()
|
||||
expiresAt := time.Now().UTC().Add(2 * time.Minute).Truncate(time.Microsecond)
|
||||
if _, err := pool.Exec(ctx, `
|
||||
CREATE TABLE gateway_identity_pairing_start_reservation (
|
||||
singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton),
|
||||
attempt_id uuid NOT NULL UNIQUE,
|
||||
expires_at timestamptz NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
)`); err != nil {
|
||||
t.Fatalf("create legacy pairing start reservation: %v", err)
|
||||
}
|
||||
if _, err := pool.Exec(ctx, `
|
||||
INSERT INTO gateway_identity_pairing_start_reservation(singleton,attempt_id,expires_at)
|
||||
VALUES(true,$1::uuid,$2)`, attemptID, expiresAt); err != nil {
|
||||
t.Fatalf("seed legacy starting reservation: %v", err)
|
||||
}
|
||||
|
||||
applyIdentityMigrationTestFile(t, ctx, pool, identityPairingStartReservationUpgradeMigration)
|
||||
applyIdentityMigrationTestFile(t, ctx, pool, identityPairingStartReservationUpgradeMigration)
|
||||
|
||||
var gotAttemptID, state string
|
||||
var revisionID *string
|
||||
var gotExpiresAt, updatedAt time.Time
|
||||
if err := pool.QueryRow(ctx, `
|
||||
SELECT attempt_id::text,state,revision_id::text,expires_at,updated_at
|
||||
FROM gateway_identity_pairing_start_reservation`).
|
||||
Scan(&gotAttemptID, &state, &revisionID, &gotExpiresAt, &updatedAt); err != nil {
|
||||
t.Fatalf("read upgraded legacy starting reservation: %v", err)
|
||||
}
|
||||
if gotAttemptID != attemptID || state != "starting" || revisionID != nil ||
|
||||
!gotExpiresAt.Equal(expiresAt) || updatedAt.IsZero() {
|
||||
t.Fatalf("unexpected upgraded starting reservation attempt=%q state=%q revision=%v expires=%v updated=%v",
|
||||
gotAttemptID, state, revisionID, gotExpiresAt, updatedAt)
|
||||
}
|
||||
|
||||
_, err := pool.Exec(ctx, `UPDATE gateway_identity_pairing_start_reservation SET state='paired'`)
|
||||
requireIdentityPairingStartReservationMigrationCode(t, err, "23514", "paired reservation without revision")
|
||||
}
|
||||
|
||||
func TestIdentityPairingStartReservationUpgradeAcceptsFresh0071Schema(t *testing.T) {
|
||||
pool := newIdentityMigrationPostgresTestSchema(t)
|
||||
ctx := context.Background()
|
||||
applyIdentityPairingStartReservationPrerequisites(t, ctx, pool)
|
||||
|
||||
revisionID, pairingID, pairingExpiresAt := seedOutstandingIdentityPairingForReservationMigration(t, ctx, pool)
|
||||
applyIdentityMigrationTestFile(t, ctx, pool, "../../migrations/0071_identity_pairing_start_reservation.sql")
|
||||
assertCurrentIdentityPairingStartReservation(t, ctx, pool, pairingID, revisionID, pairingExpiresAt)
|
||||
|
||||
applyIdentityMigrationTestFile(t, ctx, pool, identityPairingStartReservationUpgradeMigration)
|
||||
applyIdentityMigrationTestFile(t, ctx, pool, identityPairingStartReservationUpgradeMigration)
|
||||
assertCurrentIdentityPairingStartReservation(t, ctx, pool, pairingID, revisionID, pairingExpiresAt)
|
||||
assertIdentityPairingStartReservationSchema(t, ctx, pool)
|
||||
}
|
||||
|
||||
func applyIdentityPairingStartReservationPrerequisites(t *testing.T, ctx context.Context, pool *pgxpool.Pool) {
|
||||
t.Helper()
|
||||
for _, migration := range []string{
|
||||
"../../migrations/0067_identity_configuration_revisions.sql",
|
||||
"../../migrations/0068_identity_onboarding_exchanges.sql",
|
||||
"../../migrations/0069_identity_pairing_cancellation.sql",
|
||||
"../../migrations/0065_identity_configuration_revisions.sql",
|
||||
"../../migrations/0066_identity_onboarding_exchanges.sql",
|
||||
} {
|
||||
applyIdentityMigrationTestFile(t, ctx, pool, migration)
|
||||
}
|
||||
}
|
||||
|
||||
func seedOutstandingIdentityPairingForReservationMigration(
|
||||
t *testing.T,
|
||||
ctx context.Context,
|
||||
pool *pgxpool.Pool,
|
||||
) (string, string, time.Time) {
|
||||
t.Helper()
|
||||
revisionID := uuid.NewString()
|
||||
pairingID := uuid.NewString()
|
||||
expiresAt := time.Now().UTC().Add(30 * time.Minute).Truncate(time.Microsecond)
|
||||
@@ -170,79 +66,31 @@ INSERT INTO gateway_identity_onboarding_exchanges (
|
||||
pairingID, revisionID, uuid.NewString(), "identity-exchange-"+pairingID, expiresAt); err != nil {
|
||||
t.Fatalf("seed identity exchange for pairing reservation migration: %v", err)
|
||||
}
|
||||
return revisionID, pairingID, expiresAt
|
||||
}
|
||||
|
||||
func assertCurrentIdentityPairingStartReservation(
|
||||
t *testing.T,
|
||||
ctx context.Context,
|
||||
pool *pgxpool.Pool,
|
||||
wantAttemptID string,
|
||||
wantRevisionID string,
|
||||
wantExpiresAt time.Time,
|
||||
) {
|
||||
t.Helper()
|
||||
var count int
|
||||
var attemptID, state, revisionID string
|
||||
var expiresAt, updatedAt time.Time
|
||||
applyIdentityMigrationTestFile(t, ctx, pool, identityPairingStartReservationMigration)
|
||||
|
||||
var attemptID, state, reservedRevisionID string
|
||||
var gotExpiresAt, updatedAt time.Time
|
||||
if err := pool.QueryRow(ctx, `
|
||||
SELECT count(*) OVER (),attempt_id::text,state,revision_id::text,expires_at,updated_at
|
||||
FROM gateway_identity_pairing_start_reservation`).
|
||||
Scan(&count, &attemptID, &state, &revisionID, &expiresAt, &updatedAt); err != nil {
|
||||
t.Fatalf("read upgraded pairing start reservation: %v", err)
|
||||
SELECT attempt_id::text,state,revision_id::text,expires_at,updated_at
|
||||
FROM gateway_identity_pairing_start_reservation`).Scan(
|
||||
&attemptID, &state, &reservedRevisionID, &gotExpiresAt, &updatedAt,
|
||||
); err != nil {
|
||||
t.Fatalf("read pairing start reservation: %v", err)
|
||||
}
|
||||
if count != 1 || attemptID != wantAttemptID || state != "paired" || revisionID != wantRevisionID ||
|
||||
!expiresAt.Equal(wantExpiresAt) || updatedAt.IsZero() {
|
||||
t.Fatalf("unexpected upgraded pairing reservation count=%d attempt=%q state=%q revision=%q expires=%v updated=%v",
|
||||
count, attemptID, state, revisionID, expiresAt, updatedAt)
|
||||
}
|
||||
}
|
||||
|
||||
func assertIdentityPairingStartReservationSchema(t *testing.T, ctx context.Context, pool *pgxpool.Pool) {
|
||||
t.Helper()
|
||||
for _, column := range []struct {
|
||||
name string
|
||||
wantDefaultFragment string
|
||||
}{
|
||||
{name: "state", wantDefaultFragment: "starting"},
|
||||
{name: "updated_at", wantDefaultFragment: "now()"},
|
||||
} {
|
||||
var nullable, defaultExpression string
|
||||
if err := pool.QueryRow(ctx, `
|
||||
SELECT is_nullable,COALESCE(column_default,'')
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema=current_schema()
|
||||
AND table_name='gateway_identity_pairing_start_reservation'
|
||||
AND column_name=$1`, column.name).Scan(&nullable, &defaultExpression); err != nil {
|
||||
t.Fatalf("read upgraded pairing reservation column %s: %v", column.name, err)
|
||||
}
|
||||
if nullable != "NO" || !strings.Contains(defaultExpression, column.wantDefaultFragment) {
|
||||
t.Fatalf("pairing reservation column %s nullable=%q default=%q", column.name, nullable, defaultExpression)
|
||||
}
|
||||
}
|
||||
|
||||
var expiryIndex, revisionIndex string
|
||||
if err := pool.QueryRow(ctx, `SELECT pg_get_indexdef('idx_gateway_identity_pairing_start_expiry'::regclass)`).
|
||||
Scan(&expiryIndex); err != nil {
|
||||
t.Fatalf("read pairing reservation expiry index: %v", err)
|
||||
}
|
||||
if err := pool.QueryRow(ctx, `SELECT pg_get_indexdef('idx_gateway_identity_pairing_start_revision_unique'::regclass)`).
|
||||
Scan(&revisionIndex); err != nil {
|
||||
t.Fatalf("read pairing reservation revision index: %v", err)
|
||||
}
|
||||
if !strings.Contains(strings.ToLower(expiryIndex), "(state, expires_at)") ||
|
||||
!strings.Contains(strings.ToLower(revisionIndex), "unique") ||
|
||||
!strings.Contains(strings.ToLower(revisionIndex), "(revision_id)") {
|
||||
t.Fatalf("unexpected pairing reservation indexes expiry=%q revision=%q", expiryIndex, revisionIndex)
|
||||
if attemptID != pairingID || state != "paired" || reservedRevisionID != revisionID ||
|
||||
!gotExpiresAt.Equal(expiresAt) || updatedAt.IsZero() {
|
||||
t.Fatalf("unexpected pairing reservation attempt=%q state=%q revision=%q expires=%v updated=%v",
|
||||
attemptID, state, reservedRevisionID, gotExpiresAt, updatedAt)
|
||||
}
|
||||
|
||||
_, err := pool.Exec(ctx, `UPDATE gateway_identity_pairing_start_reservation SET state='starting'`)
|
||||
requireIdentityPairingStartReservationMigrationCode(t, err, "23514", "starting reservation with revision")
|
||||
requirePairingReservationMigrationCode(t, err, "23514", "starting reservation with revision")
|
||||
_, err = pool.Exec(ctx, `UPDATE gateway_identity_pairing_start_reservation SET revision_id=$1::uuid`, uuid.NewString())
|
||||
requireIdentityPairingStartReservationMigrationCode(t, err, "23503", "reservation with unknown revision")
|
||||
requirePairingReservationMigrationCode(t, err, "23503", "reservation with unknown revision")
|
||||
}
|
||||
|
||||
func requireIdentityPairingStartReservationMigrationCode(t *testing.T, err error, wantCode, operation string) {
|
||||
func requirePairingReservationMigrationCode(t *testing.T, err error, wantCode, operation string) {
|
||||
t.Helper()
|
||||
if err == nil {
|
||||
t.Fatalf("%s unexpectedly satisfied migration constraints", operation)
|
||||
|
||||
@@ -24,6 +24,10 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
defer conn.Close(ctx)
|
||||
if _, err := conn.Exec(ctx, "SET standard_conforming_strings = on"); err != nil {
|
||||
logger.Error("enforce standard SQL string semantics failed", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if _, err := conn.Exec(ctx, `
|
||||
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
@@ -64,6 +68,14 @@ CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
logger.Error("begin migration failed", "version", version, "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
// Pin string parsing semantics inside every migration transaction. A
|
||||
// previous migration may have changed the session GUC, while each file is
|
||||
// parsed and executed independently.
|
||||
if _, err := tx.Exec(ctx, "SET LOCAL standard_conforming_strings = on"); err != nil {
|
||||
_ = tx.Rollback(ctx)
|
||||
logger.Error("enforce migration SQL string semantics failed", "version", version, "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, string(sqlBytes)); err != nil {
|
||||
_ = tx.Rollback(ctx)
|
||||
logger.Error("execute migration failed", "version", version, "error", err)
|
||||
|
||||
@@ -14,42 +14,41 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
func TestSecurityEventIdempotencyRepairMigrationExists(t *testing.T) {
|
||||
payload, err := os.ReadFile("../../migrations/0064_security_event_connection_idempotency_repair.sql")
|
||||
func TestSecurityEventSchemaMigrationsDefineCurrentLifecycle(t *testing.T) {
|
||||
streamPayload, err := os.ReadFile("../../migrations/0063_oidc_security_events.sql")
|
||||
if err != nil {
|
||||
t.Fatalf("read repair migration: %v", err)
|
||||
t.Fatalf("read security event stream migration: %v", err)
|
||||
}
|
||||
sql := string(payload)
|
||||
streamSQL := string(streamPayload)
|
||||
for _, statement := range []string{
|
||||
"CREATE TABLE IF NOT EXISTS gateway_security_event_connection_idempotency",
|
||||
"CREATE INDEX IF NOT EXISTS idx_gateway_security_event_connection_idempotency_created",
|
||||
} {
|
||||
if !strings.Contains(sql, statement) {
|
||||
t.Fatalf("repair migration is missing %q", statement)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecurityEventVerificationStateRepairMigrationExists(t *testing.T) {
|
||||
payload, err := os.ReadFile("../../migrations/0065_security_event_verification_state_repair.sql")
|
||||
if err != nil {
|
||||
t.Fatalf("read verification state repair migration: %v", err)
|
||||
}
|
||||
sql := string(payload)
|
||||
for _, statement := range []string{
|
||||
"ADD COLUMN IF NOT EXISTS stream_status",
|
||||
"ADD COLUMN IF NOT EXISTS created_at",
|
||||
"CREATE TABLE IF NOT EXISTS gateway_security_event_stream_state",
|
||||
"CREATE TABLE IF NOT EXISTS gateway_security_event_verification_challenges",
|
||||
"CREATE INDEX IF NOT EXISTS idx_gateway_security_event_challenges_expiry",
|
||||
} {
|
||||
if !strings.Contains(sql, statement) {
|
||||
t.Fatalf("verification state repair migration is missing %q", statement)
|
||||
if !strings.Contains(streamSQL, statement) {
|
||||
t.Fatalf("security event stream migration is missing %q", statement)
|
||||
}
|
||||
}
|
||||
|
||||
connectionPayload, err := os.ReadFile("../../migrations/0064_security_event_connections.sql")
|
||||
if err != nil {
|
||||
t.Fatalf("read security event connection migration: %v", err)
|
||||
}
|
||||
connectionSQL := string(connectionPayload)
|
||||
for _, statement := range []string{
|
||||
"CREATE TABLE IF NOT EXISTS gateway_security_event_connections",
|
||||
"management_client_id text",
|
||||
"management_credential_ref text",
|
||||
"CREATE TABLE IF NOT EXISTS gateway_security_event_connection_idempotency",
|
||||
} {
|
||||
if !strings.Contains(connectionSQL, statement) {
|
||||
t.Fatalf("security event connection migration is missing %q", statement)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentityConfigurationRevisionMigrationDefinesSecretReferencesAndSingleActiveRevision(t *testing.T) {
|
||||
payload, err := os.ReadFile("../../migrations/0067_identity_configuration_revisions.sql")
|
||||
payload, err := os.ReadFile("../../migrations/0065_identity_configuration_revisions.sql")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -58,6 +57,7 @@ func TestIdentityConfigurationRevisionMigrationDefinesSecretReferencesAndSingleA
|
||||
"CREATE TABLE IF NOT EXISTS gateway_identity_configuration_revisions",
|
||||
"machine_credential_ref text",
|
||||
"session_encryption_key_ref text",
|
||||
"security_event_configuration_url text",
|
||||
"WHERE state = 'active'",
|
||||
"CHECK (state IN ('draft','validated','active','superseded','failed'))",
|
||||
} {
|
||||
@@ -73,7 +73,7 @@ func TestIdentityConfigurationRevisionMigrationDefinesSecretReferencesAndSingleA
|
||||
}
|
||||
|
||||
func TestIdentityOnboardingExchangeMigrationStoresOnlySecretReferences(t *testing.T) {
|
||||
payload, err := os.ReadFile("../../migrations/0068_identity_onboarding_exchanges.sql")
|
||||
payload, err := os.ReadFile("../../migrations/0066_identity_onboarding_exchanges.sql")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -81,7 +81,8 @@ func TestIdentityOnboardingExchangeMigrationStoresOnlySecretReferences(t *testin
|
||||
for _, required := range []string{
|
||||
"create table if not exists gateway_identity_onboarding_exchanges",
|
||||
"exchange_token_ref text not null",
|
||||
"security_event_configuration_url text",
|
||||
"cleanup_status text not null default 'none'",
|
||||
"security_event_credential_handoff_unsafe",
|
||||
} {
|
||||
if !strings.Contains(content, required) {
|
||||
t.Fatalf("identity onboarding migration is missing %q", required)
|
||||
@@ -94,8 +95,8 @@ func TestIdentityOnboardingExchangeMigrationStoresOnlySecretReferences(t *testin
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentityPairingCancellationMigrationSeparatesCleanupLifecycle(t *testing.T) {
|
||||
payload, err := os.ReadFile("../../migrations/0069_identity_pairing_cancellation.sql")
|
||||
func TestIdentityOnboardingMigrationDefinesCancellationLifecycle(t *testing.T) {
|
||||
payload, err := os.ReadFile("../../migrations/0066_identity_onboarding_exchanges.sql")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -106,9 +107,6 @@ func TestIdentityPairingCancellationMigrationSeparatesCleanupLifecycle(t *testin
|
||||
"cleanup_completed_at",
|
||||
"idx_gateway_identity_onboarding_cleanup",
|
||||
"last_error_category",
|
||||
"set last_error_category = 'pairing_step_failed'",
|
||||
"set local lock_timeout = '10s'",
|
||||
"set local statement_timeout = '60s'",
|
||||
} {
|
||||
if !strings.Contains(content, required) {
|
||||
t.Fatalf("identity pairing cancellation migration is missing %q", required)
|
||||
@@ -121,14 +119,14 @@ func TestIdentityPairingCancellationMigrationSeparatesCleanupLifecycle(t *testin
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentityPairingErrorCategoryUpgradeMigrationExists(t *testing.T) {
|
||||
payload, err := os.ReadFile("../../migrations/0074_identity_pairing_error_categories.sql")
|
||||
func TestIdentityOnboardingMigrationDefinesFinalErrorCategories(t *testing.T) {
|
||||
payload, err := os.ReadFile("../../migrations/0066_identity_onboarding_exchanges.sql")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
content := strings.ToLower(string(payload))
|
||||
for _, required := range []string{
|
||||
"drop constraint if exists gateway_identity_onboarding_error_category_check",
|
||||
"gateway_identity_onboarding_error_category_check",
|
||||
"security_event_credential_handoff_unsafe",
|
||||
"security_event_connection_binding_missing",
|
||||
"security_event_connection_binding_unavailable",
|
||||
@@ -142,18 +140,14 @@ func TestIdentityPairingErrorCategoryUpgradeMigrationExists(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentityPairingErrorCategoryUpgradeMigrationExecutesAgainstCurrentSchema(t *testing.T) {
|
||||
func TestIdentityPairingErrorCategoriesExecuteAgainstCurrentSchema(t *testing.T) {
|
||||
pool := newIdentityMigrationPostgresTestSchema(t)
|
||||
ctx := context.Background()
|
||||
for _, migration := range []string{
|
||||
"../../migrations/0067_identity_configuration_revisions.sql",
|
||||
"../../migrations/0068_identity_onboarding_exchanges.sql",
|
||||
"../../migrations/0069_identity_pairing_cancellation.sql",
|
||||
"../../migrations/0070_identity_secret_cleanup_queue.sql",
|
||||
"../../migrations/0071_identity_pairing_start_reservation.sql",
|
||||
"../../migrations/0072_identity_secret_cleanup_claim_lifecycle.sql",
|
||||
"../../migrations/0073_identity_pairing_start_reservation_upgrade.sql",
|
||||
"../../migrations/0074_identity_pairing_error_categories.sql",
|
||||
"../../migrations/0065_identity_configuration_revisions.sql",
|
||||
"../../migrations/0066_identity_onboarding_exchanges.sql",
|
||||
"../../migrations/0067_identity_secret_cleanup_queue.sql",
|
||||
"../../migrations/0068_identity_pairing_start_reservation.sql",
|
||||
} {
|
||||
applyIdentityMigrationTestFile(t, ctx, pool, migration)
|
||||
}
|
||||
@@ -193,7 +187,7 @@ SET last_error_category='credential_secret_leaked' WHERE id=$1::uuid`, pairingID
|
||||
}
|
||||
|
||||
func TestIdentitySecretCleanupQueueMigrationStoresOnlyReferences(t *testing.T) {
|
||||
payload, err := os.ReadFile("../../migrations/0070_identity_secret_cleanup_queue.sql")
|
||||
payload, err := os.ReadFile("../../migrations/0067_identity_secret_cleanup_queue.sql")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -219,11 +213,11 @@ func TestIdentitySecretCleanupQueueMigrationStoresOnlyReferences(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentityPairingCancellationMigrationExecutesAgainstLegacyData(t *testing.T) {
|
||||
func TestIdentityPairingCancellationLifecycleRejectsPartialStates(t *testing.T) {
|
||||
pool := newIdentityMigrationPostgresTestSchema(t)
|
||||
ctx := context.Background()
|
||||
applyIdentityMigrationTestFile(t, ctx, pool, "../../migrations/0067_identity_configuration_revisions.sql")
|
||||
applyIdentityMigrationTestFile(t, ctx, pool, "../../migrations/0068_identity_onboarding_exchanges.sql")
|
||||
applyIdentityMigrationTestFile(t, ctx, pool, "../../migrations/0065_identity_configuration_revisions.sql")
|
||||
applyIdentityMigrationTestFile(t, ctx, pool, "../../migrations/0066_identity_onboarding_exchanges.sql")
|
||||
|
||||
revisionID, pairingID := uuid.NewString(), uuid.NewString()
|
||||
if _, err := pool.Exec(ctx, `
|
||||
@@ -236,13 +230,11 @@ INSERT INTO gateway_identity_configuration_revisions (
|
||||
if _, err := pool.Exec(ctx, `
|
||||
INSERT INTO gateway_identity_onboarding_exchanges (
|
||||
id,revision_id,remote_exchange_id,exchange_token_ref,status,remote_version,expires_at,last_error_category
|
||||
) VALUES ($1::uuid,$2::uuid,$3::uuid,$4,'credentials_saved',4,now() + interval '30 minutes','secret_token_abcdef')`,
|
||||
) VALUES ($1::uuid,$2::uuid,$3::uuid,$4,'credentials_saved',4,now() + interval '30 minutes','pairing_step_failed')`,
|
||||
pairingID, revisionID, uuid.NewString(), "identity-exchange-"+pairingID); err != nil {
|
||||
t.Fatalf("seed legacy onboarding exchange: %v", err)
|
||||
}
|
||||
|
||||
applyIdentityMigrationTestFile(t, ctx, pool, "../../migrations/0069_identity_pairing_cancellation.sql")
|
||||
|
||||
var status, cleanupStatus, errorCategory string
|
||||
var cancelledAt, cleanupCompletedAt *time.Time
|
||||
if err := pool.QueryRow(ctx, `
|
||||
|
||||
Reference in New Issue
Block a user