Scert/backend/internal/bootstrap/database_test.go

307 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package bootstrap
import (
"context"
"encoding/json"
"path/filepath"
"strings"
"sync"
"testing"
"time"
"git.pcux.cn/xuans/Scert/backend/internal/config"
)
type memoryDatabaseStore struct {
mu sync.Mutex
values map[string]map[string]string
}
func (s *memoryDatabaseStore) Put(_ context.Context, reference string, value map[string]string) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.values == nil {
s.values = make(map[string]map[string]string)
}
s.values[reference] = map[string]string{"username": value["username"], "password": value["password"]}
return nil
}
func (s *memoryDatabaseStore) Get(_ context.Context, reference string) (map[string]string, error) {
s.mu.Lock()
defer s.mu.Unlock()
return s.values[reference], nil
}
func (s *memoryDatabaseStore) Delete(_ context.Context, reference string) error {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.values, reference)
return nil
}
func TestBuildDatabaseConfigSupportsThreeStorageModesWithoutPersistingCredentials(t *testing.T) {
base := config.Default(t.TempDir())
base.Paths.Data = filepath.Join(t.TempDir(), "data")
tests := []struct {
name string
input DatabaseInput
}{
{name: "SQLite", input: DatabaseInput{Driver: "sqlite"}},
{name: "MySQL", input: DatabaseInput{Driver: "mysql", Host: "db.example.test", Database: "scert", Username: "scert_user", Password: "mysql-secret", TLSMode: "true"}},
{name: "PostgreSQL", input: DatabaseInput{Driver: "postgres", Host: "db.example.test", Database: "scert", Username: "scert_user", Password: "postgres-secret", TLSMode: "verify-full"}},
}
for _, item := range tests {
t.Run(item.name, func(t *testing.T) {
persisted, credentials, err := buildDatabaseConfig(base, item.input)
if err != nil {
t.Fatalf("构造数据库配置失败:%v", err)
}
if (item.input.Username != "" && strings.Contains(persisted.DSN, item.input.Username)) ||
(item.input.Password != "" && strings.Contains(persisted.DSN, item.input.Password)) {
t.Fatalf("持久化数据库配置泄露账号或密码:%s", persisted.DSN)
}
if item.input.Driver == "sqlite" {
if len(credentials) != 0 || persisted.DSN != filepath.Join(base.Paths.Data, "scert.db") {
t.Fatalf("SQLite 配置不正确:%+v credentials=%v", persisted, credentials)
}
return
}
if credentials["username"] != item.input.Username || credentials["password"] != item.input.Password {
t.Fatal("外部数据库凭据未单独返回")
}
})
}
}
func TestDatabaseVerificationStoresCredentialsAndConsumesTokenOnce(t *testing.T) {
base := config.Default(t.TempDir())
store := &memoryDatabaseStore{}
manager := NewDatabaseVerificationManager(base, store)
manager.verify = func(_ context.Context, runtime config.DatabaseConfig) error {
if !strings.Contains(runtime.DSN, "database-password") {
t.Fatalf("运行时连接信息未注入内存密码:%s", runtime.DSN)
}
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.Fatalf("数据库验证失败:%v", err)
}
if result.CredentialRef == "" || result.VerificationToken == "" {
t.Fatal("数据库验证结果缺少加密凭据引用或令牌")
}
if strings.Contains(result.Config.DSN, "scert_user") || strings.Contains(result.Config.DSN, "database-password") {
t.Fatalf("数据库验证响应泄露凭据:%+v", result.Config)
}
consumed, err := manager.Consume(result.VerificationToken, result.CredentialRef)
if err != nil || consumed.CredentialRef != result.CredentialRef {
t.Fatalf("消费数据库验证令牌失败:%v config=%+v", err, consumed)
}
if _, err := manager.Consume(result.VerificationToken, result.CredentialRef); err == nil {
t.Fatal("数据库验证令牌被重复消费")
}
store.mu.Lock()
_, retained := store.values[result.CredentialRef]
store.mu.Unlock()
if !retained {
t.Fatal("已消费的正式数据库凭据不应被验证令牌管理器删除")
}
}
func TestDatabaseVerificationFailureAndExpiryDeleteTemporaryCredentials(t *testing.T) {
base := config.Default(t.TempDir())
store := &memoryDatabaseStore{}
manager := NewDatabaseVerificationManager(base, store)
manager.verify = func(context.Context, config.DatabaseConfig) error { return context.DeadlineExceeded }
if _, err := manager.Verify(t.Context(), DatabaseInput{
Driver: "postgres", Host: "db.example.test", Database: "scert",
Username: "scert_user", Password: "database-password", TLSMode: "verify-full",
}); err == nil {
t.Fatal("数据库验证失败未返回错误")
}
store.mu.Lock()
count := len(store.values)
store.mu.Unlock()
if count != 0 {
t.Fatalf("失败的数据库验证遗留临时凭据:%d", count)
}
manager.verify = func(context.Context, config.DatabaseConfig) error { return nil }
now := time.Now().UTC()
manager.now = func() time.Time { return now }
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)
}
manager.now = func() time.Time { return now.Add(databaseVerificationLifetime + time.Second) }
if _, err := manager.Check(result.VerificationToken, result.CredentialRef); err == nil {
t.Fatal("过期数据库验证令牌未被拒绝")
}
store.mu.Lock()
_, exists := store.values[result.CredentialRef]
store.mu.Unlock()
if exists {
t.Fatal("过期数据库验证令牌的临时凭据未被删除")
}
}
func TestSQLiteRejectsUserControlledPathAndExternalFields(t *testing.T) {
base := config.Default(t.TempDir())
if _, _, err := buildDatabaseConfig(base, DatabaseInput{Driver: "sqlite", Database: "other.db"}); err == nil {
t.Fatal("SQLite 用户自定义路径未被拒绝")
}
}
func TestDatabaseVerificationDiscardOnlyRemovesSelectedToken(t *testing.T) {
base := config.Default(t.TempDir())
store := &memoryDatabaseStore{}
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_user", Password: "first-password", TLSMode: "true",
})
if err != nil {
t.Fatal(err)
}
second, err := manager.Verify(t.Context(), DatabaseInput{
Driver: "postgres", Host: "db.example.test", Database: "scert",
Username: "second_user", Password: "second-password", TLSMode: "verify-full",
})
if err != nil {
t.Fatal(err)
}
if err := manager.Discard(first.VerificationToken); err != nil {
t.Fatalf("精确回收数据库验证令牌失败:%v", err)
}
if _, err := manager.Check(first.VerificationToken, first.CredentialRef); err == nil {
t.Fatal("已回收的数据库验证令牌仍然有效")
}
if _, err := manager.Check(second.VerificationToken, second.CredentialRef); err != nil {
t.Fatalf("精确回收影响了其他数据库验证令牌:%v", err)
}
store.mu.Lock()
_, firstExists := store.values[first.CredentialRef]
_, secondExists := store.values[second.CredentialRef]
store.mu.Unlock()
if firstExists || !secondExists {
t.Fatalf("数据库凭据回收范围错误:first=%v second=%v", firstExists, secondExists)
}
if err := manager.Discard(first.VerificationToken); err == nil {
t.Fatal("数据库验证令牌被重复回收")
}
}
func TestDatabaseVerificationDiscardAllExceptProtectsFormalCredential(t *testing.T) {
base := config.Default(t.TempDir())
store := &memoryDatabaseStore{}
manager := NewDatabaseVerificationManager(base, store)
manager.verify = func(context.Context, config.DatabaseConfig) error { return nil }
protected, err := manager.Verify(t.Context(), DatabaseInput{
Driver: "mysql", Host: "db.example.test", Database: "scert",
Username: "formal_user", Password: "formal-password", TLSMode: "true",
})
if err != nil {
t.Fatal(err)
}
pending, err := manager.Verify(t.Context(), DatabaseInput{
Driver: "postgres", Host: "db.example.test", Database: "scert",
Username: "pending_user", Password: "pending-password", TLSMode: "verify-full",
})
if err != nil {
t.Fatal(err)
}
manager.DiscardAllExcept(" " + protected.CredentialRef + " ")
if _, err := manager.Resolve(protected.VerificationToken); err == nil {
t.Fatal("批量清理后受保护凭据对应的验证令牌仍然有效")
}
if _, err := manager.Resolve(pending.VerificationToken); err == nil {
t.Fatal("批量清理后待处理凭据对应的验证令牌仍然有效")
}
store.mu.Lock()
_, protectedExists := store.values[protected.CredentialRef]
_, pendingExists := store.values[pending.CredentialRef]
store.mu.Unlock()
if !protectedExists || pendingExists {
t.Fatalf("数据库凭据保护范围错误:protected=%v pending=%v", protectedExists, pendingExists)
}
}
func TestDatabaseVerificationResolveIsServerOnlyAndDoesNotConsumeToken(t *testing.T) {
base := config.Default(t.TempDir())
store := &memoryDatabaseStore{}
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)
}
resolved, err := manager.Resolve(result.VerificationToken)
if err != nil {
t.Fatalf("按令牌解析数据库验证结果失败:%v", err)
}
if resolved.Config.CredentialRef != result.CredentialRef || resolved.CredentialRef != result.CredentialRef || resolved.ExpiresAt != result.ExpiresAt {
t.Fatalf("解析的数据库验证状态不完整:%+v", resolved)
}
encoded, err := json.Marshal(resolved)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(encoded), result.CredentialRef) || strings.Contains(string(encoded), result.Config.DSN) {
t.Fatalf("服务端数据库验证状态可被意外序列化:%s", encoded)
}
if _, err := manager.Consume(result.VerificationToken, result.CredentialRef); err != nil {
t.Fatalf("Resolve 不应消费数据库验证令牌:%v", err)
}
if _, err := manager.Resolve(result.VerificationToken); err == nil {
t.Fatal("已消费的数据库验证令牌仍可被解析")
}
}
func TestDatabaseVerificationConsumesTokenOnlyOnceUnderConcurrency(t *testing.T) {
base := config.Default(t.TempDir())
store := &memoryDatabaseStore{}
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)
}
start := make(chan struct{})
results := make(chan error, 2)
for range 2 {
go func() {
<-start
_, consumeErr := manager.Consume(result.VerificationToken, result.CredentialRef)
results <- consumeErr
}()
}
close(start)
successes := 0
for range 2 {
if <-results == nil {
successes++
}
}
if successes != 1 {
t.Fatalf("并发消费数据库验证令牌成功次数为 %d,预期为 1", successes)
}
}