96 lines
4.6 KiB
Go
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")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|