156 lines
4.6 KiB
Go
156 lines
4.6 KiB
Go
package dbmigration
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"testing"
|
||
"time"
|
||
|
||
"git.pcux.cn/xuans/Smail/config"
|
||
)
|
||
|
||
// 审查专用:只使用临时数据库,不连接正在运行的服务。
|
||
func TestAuditCustomUniqueIndexMustNotDisappear(t *testing.T) {
|
||
root, source := portableFixture(t)
|
||
switched := false
|
||
executor := &PortableExecutor{
|
||
root: root,
|
||
switchConfig: func(context.Context, uint64, config.DatabaseConfig, config.AuditActor) error {
|
||
switched = true
|
||
return nil
|
||
},
|
||
}
|
||
defer executor.Close()
|
||
database, err := executor.connect(context.Background(), source)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := database.Exec(`CREATE UNIQUE INDEX audit_unique_user_name ON "user"(name)`); err != nil {
|
||
_ = database.Close()
|
||
t.Fatal(err)
|
||
}
|
||
_ = database.Close()
|
||
plan, err := buildTargetPlan(source, config.DatabaseConfig{Type: config.DatabaseTypeSQLite, LocalRef: "target.db"})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
err = executor.Run(context.Background(), plan, func(Status) error { return nil })
|
||
if errors.Is(err, ErrSQLUnsupportedObject) && !switched {
|
||
return
|
||
}
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
target, err := executor.connect(context.Background(), plan.targetConfig)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer target.Close()
|
||
var count int
|
||
if err := target.QueryRow(`SELECT COUNT(*) FROM sqlite_master WHERE type='index' AND name='audit_unique_user_name'`).Scan(&count); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
_, duplicateErr := target.Exec(`INSERT INTO "user" (account,name,password) SELECT 'audit-duplicate',name,'hash' FROM "user" LIMIT 1`)
|
||
t.Logf("switched=%v custom_index_count=%d duplicate_insert_error=%v", switched, count, duplicateErr)
|
||
if count == 0 || duplicateErr == nil {
|
||
t.Fatal("迁移成功切库,但自定义唯一索引丢失且重复数据能够写入")
|
||
}
|
||
}
|
||
|
||
func TestAuditRequestDrainMustRespectCancellation(t *testing.T) {
|
||
service := NewService(nil)
|
||
service.requests.RLock()
|
||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Millisecond)
|
||
defer cancel()
|
||
done := make(chan struct{})
|
||
go func() {
|
||
release, err := service.drainRequests(ctx)
|
||
if err != nil {
|
||
close(done)
|
||
return
|
||
}
|
||
release()
|
||
close(done)
|
||
}()
|
||
select {
|
||
case <-done:
|
||
service.requests.RUnlock()
|
||
case <-time.After(200 * time.Millisecond):
|
||
service.requests.RUnlock()
|
||
<-done
|
||
t.Fatalf("请求上下文已结束,但排空仍等待在途请求释放锁: %v", ctx.Err())
|
||
}
|
||
}
|
||
|
||
func TestAuditMigrationMustPreserveAllocatedMailboxUIDs(t *testing.T) {
|
||
root, source := portableFixture(t)
|
||
switched := false
|
||
executor := &PortableExecutor{
|
||
root: root,
|
||
switchConfig: func(context.Context, uint64, config.DatabaseConfig, config.AuditActor) error {
|
||
switched = true
|
||
return nil
|
||
},
|
||
}
|
||
defer executor.Close()
|
||
database, err := executor.connect(context.Background(), source)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
// 正常分配两次关系 ID,再模拟清除最后一封邮件;不手工修改序列表。
|
||
var allocated int64
|
||
for range 2 {
|
||
result, err := database.Exec(`INSERT INTO user_email (user_id,email_id) VALUES (40,82)`)
|
||
if err != nil {
|
||
_ = database.Close()
|
||
t.Fatal(err)
|
||
}
|
||
allocated, err = result.LastInsertId()
|
||
if err != nil {
|
||
_ = database.Close()
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
if _, err := database.Exec(`DELETE FROM user_email WHERE id=?`, allocated); err != nil {
|
||
_ = database.Close()
|
||
t.Fatal(err)
|
||
}
|
||
var sourceSequence int64
|
||
if err := database.QueryRow(`SELECT seq FROM sqlite_sequence WHERE name='user_email'`).Scan(&sourceSequence); err != nil {
|
||
_ = database.Close()
|
||
t.Fatal(err)
|
||
}
|
||
_ = database.Close()
|
||
plan, err := buildTargetPlan(source, config.DatabaseConfig{Type: config.DatabaseTypeSQLite, LocalRef: "target.db"})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := executor.Run(context.Background(), plan, func(Status) error { return nil }); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
target, err := executor.connect(context.Background(), plan.targetConfig)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer target.Close()
|
||
var targetSequence int64
|
||
if err := target.QueryRow(`SELECT seq FROM sqlite_sequence WHERE name='user_email'`).Scan(&targetSequence); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
result, err := target.Exec(`INSERT INTO user_email (user_id,email_id) VALUES (40,82)`)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
next, err := result.LastInsertId()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
t.Logf("switched=%v removed_uid=%d source_sequence=%d target_sequence=%d newly_allocated_uid=%d", switched, allocated, sourceSequence, targetSequence, next)
|
||
if !switched || sourceSequence < allocated {
|
||
t.Fatal("迁移或源库序列前提未成立")
|
||
}
|
||
if next <= allocated {
|
||
t.Fatal("迁移丢失已分配序号的高水位,新关系复用已删除邮件的 UID")
|
||
}
|
||
}
|