141 lines
5.2 KiB
Go
141 lines
5.2 KiB
Go
package dbmigration
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.pcux.cn/xuans/Smail/config"
|
|
)
|
|
|
|
func TestIdentifierQuotationContainsUntrustedSyntax(t *testing.T) {
|
|
for _, value := range []string{
|
|
"ordinary", "with space", "mail`prod", `mail"prod`,
|
|
"quote`; DROP TABLE sentinel; --", `quote"; DROP TABLE sentinel; --`,
|
|
"comma,other", "comment--suffix", "中文表名", "back\\slash",
|
|
} {
|
|
for _, kind := range []config.DatabaseType{config.DatabaseTypeMySQL, config.DatabaseTypePostgreSQL} {
|
|
t.Run(string(kind)+"/"+value, func(t *testing.T) {
|
|
quoted := quoteDatabaseIdentifier(kind, value)
|
|
delimiter := byte('"')
|
|
if kind == config.DatabaseTypeMySQL {
|
|
delimiter = '`'
|
|
}
|
|
if len(quoted) < 2 || quoted[0] != delimiter || quoted[len(quoted)-1] != delimiter {
|
|
t.Fatal("标识符缺少完整引用边界")
|
|
}
|
|
var decoded strings.Builder
|
|
for index := 1; index < len(quoted)-1; index++ {
|
|
if quoted[index] == delimiter {
|
|
index++
|
|
if index >= len(quoted)-1 || quoted[index] != delimiter {
|
|
t.Fatal("标识符内部存在未成对的结束引用")
|
|
}
|
|
}
|
|
decoded.WriteByte(quoted[index])
|
|
}
|
|
if decoded.String() != value {
|
|
t.Fatal("标识符引用改变了原始名称")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMySQLReferenceRulesRejectInjectedSuffixes(t *testing.T) {
|
|
for _, rule := range []string{"CASCADE", "SET NULL", "SET DEFAULT", "RESTRICT", "NO ACTION"} {
|
|
for _, suffix := range []string{"; SELECT 1", " --", "/*comment*/", "\x00", ", CASCADE"} {
|
|
if validMySQLReferenceRule(rule + suffix) {
|
|
t.Fatal("外键动作接受了白名单以外的语法")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 只接受新建的隔离数据库;保留表和测试数据,不连接正式数据库或删除文件。
|
|
func TestSQLIdentifierLiveIntegration(t *testing.T) {
|
|
for _, kind := range []config.DatabaseType{config.DatabaseTypeMySQL, config.DatabaseTypePostgreSQL} {
|
|
t.Run(string(kind), func(t *testing.T) {
|
|
raw := os.Getenv("SMAIL_TEST_IDENTIFIER_" + strings.ToUpper(string(kind)))
|
|
if raw == "" {
|
|
t.Skip("未配置专用实机标识符测试数据库")
|
|
}
|
|
var input struct {
|
|
Name string `json:"name"`
|
|
User string `json:"user"`
|
|
Password string `json:"password"`
|
|
}
|
|
decoder := json.NewDecoder(strings.NewReader(raw))
|
|
decoder.DisallowUnknownFields()
|
|
if decoder.Decode(&input) != nil ||
|
|
!regexp.MustCompile(`^smail_runtime_test_[0-9a-f]{12}$`).MatchString(input.Name) ||
|
|
!regexp.MustCompile(`^smail_rt_[0-9a-f]{12}$`).MatchString(input.User) || input.Password == "" {
|
|
t.Fatal("实机测试连接必须使用专用隔离数据库和账号")
|
|
}
|
|
port := uint16(3306)
|
|
if kind == config.DatabaseTypePostgreSQL {
|
|
port = 5432
|
|
}
|
|
settings := config.DatabaseConfig{Type: kind, Host: "127.0.0.1", Port: port, Username: input.User}
|
|
driver, dsn, err := runtimeSQLDSN(settings, input.Password, input.Name)
|
|
if err != nil {
|
|
t.Fatal("无法构建隔离连接")
|
|
}
|
|
database, err := sql.Open(driver, dsn)
|
|
if err != nil {
|
|
t.Fatal("无法打开隔离连接")
|
|
}
|
|
defer database.Close()
|
|
database.SetMaxOpenConns(1)
|
|
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
|
|
defer cancel()
|
|
var count int
|
|
query := "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema=DATABASE()"
|
|
if kind == config.DatabaseTypePostgreSQL {
|
|
query = "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='public'"
|
|
}
|
|
if err := database.QueryRowContext(ctx, query).Scan(&count); err != nil {
|
|
var pathError *os.PathError
|
|
if errors.As(err, &pathError) {
|
|
t.Fatalf("测试连接的文件访问失败:操作=%s 文件=%s", pathError.Op, filepath.Base(pathError.Path))
|
|
}
|
|
t.Fatalf("测试库查询失败,错误类型:%T", err)
|
|
}
|
|
if count != 0 {
|
|
t.Fatal("测试数据库必须为空,拒绝复用现有业务数据")
|
|
}
|
|
if _, err := database.ExecContext(ctx, "CREATE TABLE sentinel (id INTEGER PRIMARY KEY)"); err != nil {
|
|
t.Fatal("隔离哨兵表创建失败")
|
|
}
|
|
if _, err := database.ExecContext(ctx, "INSERT INTO sentinel(id) VALUES (731)"); err != nil {
|
|
t.Fatal("隔离哨兵数据写入失败")
|
|
}
|
|
names := []string{"ordinary", "with space", "mail`prod", `mail"prod`,
|
|
"quote`; DROP TABLE sentinel; --", `quote"; DROP TABLE sentinel; --`, "中文表名"}
|
|
for _, name := range names {
|
|
quoted := quoteDatabaseIdentifier(kind, name)
|
|
if _, err := database.ExecContext(ctx, "CREATE TABLE "+quoted+" (id INTEGER PRIMARY KEY)"); err != nil {
|
|
t.Fatal("真实数据库未能按单个标识符创建测试表")
|
|
}
|
|
if _, err := database.ExecContext(ctx, "INSERT INTO "+quoted+" (id) VALUES (17)"); err != nil {
|
|
t.Fatal("真实数据库未能按单个标识符写入测试表")
|
|
}
|
|
var actual int
|
|
if database.QueryRowContext(ctx, "SELECT id FROM "+quoted).Scan(&actual) != nil || actual != 17 {
|
|
t.Fatal("真实数据库标识符引用未保留数据")
|
|
}
|
|
if database.QueryRowContext(ctx, "SELECT id FROM sentinel").Scan(&actual) != nil || actual != 731 {
|
|
t.Fatal("恶意标识符影响了隔离哨兵表")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|