Files
cmautobuy/admin/repository/sqlite_to_mysql_test.go
T

171 lines
6.5 KiB
Go

package repository
import (
"crypto/sha256"
"database/sql"
"errors"
"os"
"path/filepath"
"testing"
"cmautobuy/admin/model"
)
func TestMigrateSQLiteToMySQL_演练迁移核对和重复保护(t *testing.T) {
sourcePath := newLegacyMigrationSource(t)
before := fileDigest(t, sourcePath)
source, err := OpenLegacySQLiteReadOnly(sourcePath)
if err != nil {
t.Fatal(err)
}
defer source.Close()
target := newSybTestDB(t)
dryRun, err := MigrateSQLiteToMySQL(source, target, true)
if err != nil {
t.Fatalf("演练失败: %v", err)
}
if !dryRun.DryRun || summaryCount(dryRun, "users") != 1 || summaryCount(dryRun, "tasks") != 1 {
t.Fatalf("演练摘要不对: %+v", dryRun)
}
assertBusinessTablesEmpty(t, target)
result, err := MigrateSQLiteToMySQL(source, target, false)
if err != nil {
t.Fatalf("迁移失败: %v", err)
}
if result.DryRun {
t.Fatal("正式迁移摘要不应标记为 dry-run")
}
if _, err := VerifySQLiteToMySQL(source, target); err != nil {
t.Fatalf("迁移后核对失败: %v", err)
}
if got := fileDigest(t, sourcePath); got != before {
t.Fatal("迁移修改了源 SQLite 文件")
}
if _, err := MigrateSQLiteToMySQL(source, target, false); err == nil {
t.Fatal("重复迁移到非空目标应该被拒绝")
}
}
func TestMigrateSQLiteToMySQL_中途失败整体回滚(t *testing.T) {
sourcePath := newLegacyMigrationSource(t)
source, err := OpenLegacySQLiteReadOnly(sourcePath)
if err != nil {
t.Fatal(err)
}
defer source.Close()
target := newSybTestDB(t)
if _, err := target.Exec(`CREATE TRIGGER reject_tasks BEFORE INSERT ON tasks
FOR EACH ROW SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'fixture rejection'`); err != nil {
t.Fatal(err)
}
_, err = MigrateSQLiteToMySQL(source, target, false)
var migrationErr *SQLiteMigrationError
if !errors.As(err, &migrationErr) || migrationErr.Table != "tasks" {
t.Fatalf("应该返回不含业务数据的 tasks 迁移错误,实际 %v", err)
}
assertBusinessTablesEmpty(t, target)
}
func TestMigrateSQLiteToMySQL_拒绝错误SQLite版本(t *testing.T) {
dir := t.TempDir()
db, err := Open(dir)
if err != nil {
t.Fatal(err)
}
if err := Migrate(db); err != nil {
t.Fatal(err)
}
if _, err := db.Exec(`PRAGMA user_version = 7`); err != nil {
t.Fatal(err)
}
db.Close()
source, err := OpenLegacySQLiteReadOnly(filepath.Join(dir, "admin.db"))
if err != nil {
t.Fatal(err)
}
defer source.Close()
target := newSybTestDB(t)
if _, err := MigrateSQLiteToMySQL(source, target, true); err == nil {
t.Fatal("错误 SQLite 版本应该被拒绝")
}
}
func newLegacyMigrationSource(t *testing.T) string {
t.Helper()
dir := t.TempDir()
db, err := Open(dir)
if err != nil {
t.Fatal(err)
}
if err := Migrate(db); err != nil {
db.Close()
t.Fatal(err)
}
now := model.NowISO()
statements := []struct {
query string
args []any
}{
{`INSERT INTO users (user_id,username,password_hash,role,status,password_changed_at,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?)`, []any{"USR-1", "admin", "bcrypt-fixture", "admin", "active", now, now, now}},
{`INSERT INTO clients (client_id,name,last_seen_at,created_at,updated_at) VALUES (?,?,?,?,?)`, []any{"CLIENT-1", "测试客户端", now, now, now}},
{`INSERT INTO shopee_products (goods_id,title,pdd_goods_url,pdd_goods_id,created_at,updated_at) VALUES (?,?,?,?,?,?)`, []any{"SP-1", "测试商品", "https://example.invalid/pdd", "PDD-1", now, now}},
{`INSERT INTO pdd_products (id,goods_id,url,title,skus_json,collect_status,created_at,updated_at) VALUES (?,?,?,?,?,'collected',?,?)`, []any{7, "PDD-1", "https://example.invalid/pdd", "PDD 商品", `{"skus":[]}`, now, now}},
{`INSERT INTO shopee_skus (sku_id,goods_id,spec_raw,color,size,parse_ok,created_at,updated_at) VALUES (?,?,?,?,?,1,?,?)`, []any{"SKU-1", "SP-1", "黑色,M", "黑色", "M", now, now}},
{`INSERT INTO syb_orders (syb_id,order_no,title,shopee_goods_id,shopee_sku_id,product_spec,quantity,syb_data,created_at,updated_at) VALUES (?,?,?,?,?,?,1,'{}',?,?)`, []any{"SYB-1", "ORDER-1", "测试货运单", "SP-1", "SKU-1", "黑色,M", now, now}},
{`INSERT INTO sku_mappings (shopee_sku_id,pdd_goods_id,pdd_option_key,pdd_options,goods_id,mapped_at,mapped_by) VALUES (?,?,?,?,?,?,?)`, []any{"SKU-1", "PDD-1", "color=黑色&size=M", `{"color":"黑色","size":"M"}`, "SP-1", now, "USR-1"}},
{`INSERT INTO tasks (task_id,task_type,status,assigned_client,syb_id,pdd_goods_url,pdd_goods_id,created_at,updated_at) VALUES (?,'collect','claimed',?,?,?,?,?,?)`, []any{"TASK-1", "CLIENT-1", "SYB-1", "https://example.invalid/pdd", "PDD-1", now, now}},
{`INSERT INTO task_claims (task_id,client_id,claimed_at) VALUES (?,?,?)`, []any{"TASK-1", "CLIENT-1", now}},
{`INSERT INTO idempotency_keys (key,request_hash,response_body,created_at) VALUES (?,?,?,?)`, []any{"IDEM-1", "hash", `{"accepted":true}`, now}},
{`INSERT INTO syb_session (username,cookies,expires_at,updated_at) VALUES (?,?,?,?)`, []any{"fixture", `[{"name":"session"}]`, now, now}},
{`INSERT INTO syb_sync_state (id,last_synced_at,updated_at) VALUES (1,?,?)`, []any{now, now}},
{`INSERT INTO web_sessions (session_hash,user_id,expires_at,created_at,last_seen_at) VALUES (?,?,?,?,?)`, []any{"SESSION-HASH", "USR-1", now, now, now}},
{`INSERT INTO client_user_assignments (assignment_id,client_id,user_id,started_at,assigned_by_user_id) VALUES (?,?,?,?,?)`, []any{"ASSIGN-1", "CLIENT-1", "USR-1", now, "USR-1"}},
{`INSERT INTO syb_sync_runs (run_id,user_id,date_from,date_to,status,started_at,finished_at) VALUES (?,?,?,?,'succeeded',?,?)`, []any{"RUN-1", "USR-1", "2026-08-09", "2026-08-09", now, now}},
}
for _, statement := range statements {
if _, err := db.Exec(statement.query, statement.args...); err != nil {
db.Close()
t.Fatalf("准备迁移 fixture 失败: %v", err)
}
}
if err := db.Close(); err != nil {
t.Fatal(err)
}
return filepath.Join(dir, "admin.db")
}
func assertBusinessTablesEmpty(t *testing.T, db *sql.DB) {
t.Helper()
for _, spec := range sqliteMigrationTables {
var count int
if err := db.QueryRow("SELECT COUNT(*) FROM " + quoteMySQLIdentifier(spec.name)).Scan(&count); err != nil {
t.Fatal(err)
}
if count != 0 {
t.Fatalf("表 %s 应为空,实际 %d", spec.name, count)
}
}
}
func summaryCount(summary *SQLiteMigrationSummary, name string) int64 {
for _, table := range summary.Tables {
if table.Name == name {
return table.Count
}
}
return -1
}
func fileDigest(t *testing.T, path string) [32]byte {
t.Helper()
content, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
return sha256.Sum256(content)
}