Smail/backend/controllers/database_migration_test.go
smail f68c367042
Some checks are pending
Security Checks / security (push) Waiting to run
实现三种数据库迁移并完善认证与迁移写入保护
2026-09-06 01:31:29 +08:00

96 lines
4.6 KiB
Go

package controllers
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"git.pcux.cn/xuans/Smail/config"
"git.pcux.cn/xuans/Smail/services/access"
"git.pcux.cn/xuans/Smail/services/dbmigration"
appcontext "git.pcux.cn/xuans/Smail/utils/context"
)
// 这里只验证控制器契约;真实数据复制由迁移服务集成测试覆盖。
type migrationContractRunner struct{}
func (migrationContractRunner) Run(context.Context, dbmigration.Plan, func(dbmigration.Status) error) error {
return nil
}
func TestDatabaseMigrationPreviewContract(t *testing.T) {
root := t.TempDir()
if err := os.Mkdir(filepath.Join(root, "config"), 0700); err != nil {
t.Fatal(err)
}
legacy := []byte(`{"domain":"mail.example.com","webDomain":"mail.example.com","dbType":"sqlite","dbDSN":"./config/source.db","isInit":true,"httpPort":80,"httpsPort":443,"httpsEnabled":1}`)
if err := os.WriteFile(filepath.Join(root, "config", "config.json"), legacy, 0600); err != nil {
t.Fatal(err)
}
validation := config.RuntimeValidationOptions{HasSuperAdmin: true, Production: true}
if _, err := config.MigrateLegacyConfig(root, validation); err != nil {
t.Fatal(err)
}
manager, err := config.NewConfigManager(root, config.ConfigLoaderOptions{Validation: validation})
if err != nil {
t.Fatal(err)
}
previousRoot, previousManager, previousService := config.ROOT_PATH, config.CurrentManager(), dbmigration.Current()
config.ROOT_PATH = root
config.SetRuntimeManager(manager)
dbmigration.SetCurrent(dbmigration.NewService(migrationContractRunner{}))
t.Cleanup(func() {
config.ROOT_PATH = previousRoot
config.SetRuntimeManager(previousManager)
dbmigration.SetCurrent(previousService)
})
admin := &appcontext.Context{UserID: 1, Role: access.RoleSuperAdmin, RecentAuth: true}
for _, test := range []struct {
name, body string
ctx *appcontext.Context
code int
}{
{"sqlite", `{"target":{"type":"sqlite","localRef":"next.db"},"expectedGeneration":1}`, admin, 200},
{"mysql", `{"target":{"type":"mysql","host":"localhost","username":"review","name":"next","password":{"plaintext":"private-test-value"}},"expectedGeneration":1}`, admin, 200},
{"postgresql", `{"target":{"type":"postgresql","host":"localhost","username":"review","name":"next","password":{"plaintext":"private-test-value"}},"expectedGeneration":1}`, admin, 200},
{"legacy", `{"targetName":"next.db","expectedGeneration":1}`, admin, 200},
{"both targets", `{"targetName":"next.db","target":{"type":"sqlite","localRef":"next.db"},"expectedGeneration":1}`, admin, 422},
{"raw dsn", `{"target":{"type":"mysql","dsn":"forbidden"},"expectedGeneration":1}`, admin, 422},
{"unsafe path", `{"target":{"type":"sqlite","localRef":"../next.db"},"expectedGeneration":1}`, admin, 422},
{"sqlite password", `{"target":{"type":"sqlite","localRef":"next.db","password":{"plaintext":"private-test-value"}},"expectedGeneration":1}`, admin, 422},
{"missing password", `{"target":{"type":"mysql","host":"localhost","username":"review","name":"next"},"expectedGeneration":1}`, admin, 422},
{"generation conflict", `{"targetName":"next.db","expectedGeneration":9}`, admin, 409},
{"no recent auth", `{"targetName":"next.db","expectedGeneration":1}`, &appcontext.Context{UserID: 1, Role: access.RoleSuperAdmin}, 403},
{"ordinary admin", `{"targetName":"next.db","expectedGeneration":1}`, &appcontext.Context{UserID: 2, Role: access.RoleAdmin, RecentAuth: true}, 403},
{"ordinary user", `{"targetName":"next.db","expectedGeneration":1}`, &appcontext.Context{UserID: 3, Role: access.RoleUser, RecentAuth: true}, 403},
} {
t.Run(test.name, func(t *testing.T) {
recorder := httptest.NewRecorder()
PreviewDatabaseMigration(test.ctx, recorder, httptest.NewRequest(http.MethodPost, "/api/v1/database/migrations/preview", strings.NewReader(test.body)))
if recorder.Code != test.code {
t.Fatalf("unexpected status: %d", recorder.Code)
}
if strings.Contains(recorder.Body.String(), "private-test-value") || strings.Contains(recorder.Body.String(), "ciphertext") {
t.Fatal("migration response disclosed secret")
}
if test.code == 200 {
var payload struct {
ErrorNo int `json:"errorNo"`
Data struct {
Migration dbmigration.Record `json:"migration"`
Token string `json:"token"`
} `json:"data"`
}
if err := json.Unmarshal(recorder.Body.Bytes(), &payload); err != nil || payload.ErrorNo != 0 || payload.Data.Token == "" || payload.Data.Migration.Status != dbmigration.StatusPreviewed {
t.Fatal("invalid preview response")
}
}
})
}
}