From 42e03242a9a4bba17f9e9ca97cbcee929f3ee663 Mon Sep 17 00:00:00 2001 From: xuans Date: Wed, 30 Sep 2026 12:57:40 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E7=94=9F=E5=91=BD=E5=91=A8=E6=9C=9F=E5=9B=9E=E5=BD=92=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 加入保存失败、单个回收失败、过期回收失败和批量回收失败恢复的回归覆盖,当前实现仍待修正测试发现的状态顺序问题。 --- backend/internal/bootstrap/database.go | 2 - backend/internal/bootstrap/database_test.go | 136 ++++++++++++++++++++ 2 files changed, 136 insertions(+), 2 deletions(-) diff --git a/backend/internal/bootstrap/database.go b/backend/internal/bootstrap/database.go index da26b28..c72e90b 100644 --- a/backend/internal/bootstrap/database.go +++ b/backend/internal/bootstrap/database.go @@ -379,7 +379,6 @@ func (m *DatabaseVerificationManager) deleteExpiredSessions(entries []expiredDat reference := strings.TrimSpace(entry.Session.CredentialRef) if err := m.deleteCredential(reference); err != nil { m.restoreExpiredSession(entry) - m.rememberPendingCleanup(reference) continue } m.removeExpiredSession(entry) @@ -392,7 +391,6 @@ func (m *DatabaseVerificationManager) deleteDiscardedSessions(entries []expiredD reference := strings.TrimSpace(entry.Session.CredentialRef) if err := m.deleteCredential(reference); err != nil { m.restoreSession(entry.Token, entry.Session) - m.rememberPendingCleanup(reference) continue } m.forgetPendingCleanup(reference) diff --git a/backend/internal/bootstrap/database_test.go b/backend/internal/bootstrap/database_test.go index 5d0578d..7c09d23 100644 --- a/backend/internal/bootstrap/database_test.go +++ b/backend/internal/bootstrap/database_test.go @@ -314,3 +314,139 @@ func TestDatabaseVerificationConsumesTokenOnlyOnceUnderConcurrency(t *testing.T) t.Fatalf("并发消费数据库验证令牌成功次数为 %d,预期为 1", successes) } } + +func TestDatabaseVerificationPutFailureQueuesCredentialCleanup(t *testing.T) { + base := config.Default(t.TempDir()) + store := &memoryDatabaseStore{putFailure: true, deleteFailures: 1} + manager := NewDatabaseVerificationManager(base, store) + manager.verify = func(context.Context, config.DatabaseConfig) error { return nil } + _, err := manager.Verify(t.Context(), DatabaseInput{ + Driver: "mysql", Host: "db.example.test", Database: "scert", + Username: "scert_user", Password: "database-password", TLSMode: "true", + }) + if err == nil { + t.Fatal("expected credential persistence failure") + } + store.mu.Lock() + retainedAfterFailure := len(store.values) + store.mu.Unlock() + if retainedAfterFailure != 1 { + t.Fatalf("expected failed persistence cleanup to retain one credential temporarily, got %d", retainedAfterFailure) + } + manager.retryPendingCredentialCleanup() + store.mu.Lock() + remaining := len(store.values) + store.mu.Unlock() + if remaining != 0 { + t.Fatalf("expected pending credential cleanup to succeed on retry, got %d remaining", remaining) + } +} + +func TestDatabaseVerificationDiscardFailureRestoresTokenForRetry(t *testing.T) { + base := config.Default(t.TempDir()) + store := &memoryDatabaseStore{deleteFailures: 1} + manager := NewDatabaseVerificationManager(base, store) + manager.verify = func(context.Context, config.DatabaseConfig) error { return nil } + result, err := manager.Verify(t.Context(), DatabaseInput{ + Driver: "mysql", Host: "db.example.test", Database: "scert", + Username: "scert_user", Password: "database-password", TLSMode: "true", + }) + if err != nil { + t.Fatal(err) + } + if err := manager.Discard(result.VerificationToken); err == nil { + t.Fatal("expected first credential discard to fail") + } + if _, err := manager.Resolve(result.VerificationToken); err != nil { + t.Fatalf("expected failed discard to restore the verification token: %v", err) + } + store.mu.Lock() + _, retained := store.values[result.CredentialRef] + store.mu.Unlock() + if !retained { + t.Fatal("credential must remain available while the token is restored") + } + if err := manager.Discard(result.VerificationToken); err != nil { + t.Fatalf("expected retry discard to succeed: %v", err) + } + if _, err := manager.Resolve(result.VerificationToken); err == nil { + t.Fatal("discarded token must not remain usable") + } + store.mu.Lock() + _, retained = store.values[result.CredentialRef] + store.mu.Unlock() + if retained { + t.Fatal("successfully discarded credential must be deleted") + } +} + +func TestDatabaseVerificationExpiredCleanupFailureRestoresTokenForRetry(t *testing.T) { + base := config.Default(t.TempDir()) + store := &memoryDatabaseStore{deleteFailures: 1} + manager := NewDatabaseVerificationManager(base, store) + manager.verify = func(context.Context, config.DatabaseConfig) error { return nil } + result, err := manager.Verify(t.Context(), DatabaseInput{ + Driver: "postgres", Host: "db.example.test", Database: "scert", + Username: "scert_user", Password: "database-password", TLSMode: "verify-full", + }) + if err != nil { + t.Fatal(err) + } + manager.now = func() time.Time { return result.ExpiresAt } + if _, err := manager.Check(result.VerificationToken, result.CredentialRef); err != nil { + t.Fatalf("expected failed expiry cleanup to preserve the session for retry: %v", err) + } + store.mu.Lock() + _, retained := store.values[result.CredentialRef] + store.mu.Unlock() + if !retained { + t.Fatal("credential must remain available after failed expiry cleanup") + } + if _, err := manager.Check(result.VerificationToken, result.CredentialRef); err == nil { + t.Fatal("expired token must be invalid after retry cleanup succeeds") + } + store.mu.Lock() + _, retained = store.values[result.CredentialRef] + store.mu.Unlock() + if retained { + t.Fatal("successfully cleaned expired credential must be deleted") + } +} + +func TestDatabaseVerificationDiscardAllRestoresOnlyFailedSession(t *testing.T) { + base := config.Default(t.TempDir()) + store := &memoryDatabaseStore{deleteFailures: 1} + manager := NewDatabaseVerificationManager(base, store) + manager.verify = func(context.Context, config.DatabaseConfig) error { return nil } + first, err := manager.Verify(t.Context(), DatabaseInput{ + Driver: "mysql", Host: "db.example.test", Database: "scert", + Username: "first", Password: "first-password", TLSMode: "true", + }) + if err != nil { + t.Fatal(err) + } + second, err := manager.Verify(t.Context(), DatabaseInput{ + Driver: "mysql", Host: "db.example.test", Database: "scert", + Username: "second", Password: "second-password", TLSMode: "true", + }) + if err != nil { + t.Fatal(err) + } + manager.DiscardAll() + valid := 0 + for _, token := range []string{first.VerificationToken, second.VerificationToken} { + if _, err := manager.Resolve(token); err == nil { + valid++ + } + } + if valid != 1 { + t.Fatalf("expected exactly one failed batch cleanup to restore its token, got %d", valid) + } + manager.DiscardAll() + store.mu.Lock() + remaining := len(store.values) + store.mu.Unlock() + if remaining != 0 { + t.Fatalf("expected second batch cleanup to remove the restored credential, got %d remaining", remaining) + } +}