mirror of
https://github.com/black-ant/Ant-Browser.git
synced 2026-07-14 18:48:55 +08:00
channel: master version: 1.0.0 source-ref: master published-at-utc: 2026-03-13T15:19:28Z
200 lines
5.6 KiB
Go
200 lines
5.6 KiB
Go
package launchcode_test
|
||
|
||
// Feature: instance-launch-code, Property 2: persistence round-trip
|
||
// Validates: Requirements 1.3, 5.1, 5.3
|
||
|
||
import (
|
||
"database/sql"
|
||
"fmt"
|
||
"os"
|
||
"testing"
|
||
|
||
"ant-chrome/backend/internal/launchcode"
|
||
|
||
"github.com/leanovate/gopter"
|
||
"github.com/leanovate/gopter/gen"
|
||
"github.com/leanovate/gopter/prop"
|
||
_ "modernc.org/sqlite"
|
||
)
|
||
|
||
// newTestDB 创建内存 SQLite 数据库并执行建表迁移
|
||
func newTestDB(t *testing.T) *sql.DB {
|
||
t.Helper()
|
||
db, err := sql.Open("sqlite", "file::memory:?cache=shared&_journal_mode=WAL")
|
||
if err != nil {
|
||
t.Fatalf("打开测试数据库失败: %v", err)
|
||
}
|
||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS launch_codes (
|
||
profile_id TEXT PRIMARY KEY,
|
||
code TEXT NOT NULL UNIQUE,
|
||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||
)`)
|
||
if err != nil {
|
||
t.Fatalf("建表失败: %v", err)
|
||
}
|
||
_, err = db.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_launch_codes_code ON launch_codes(code)`)
|
||
if err != nil {
|
||
t.Fatalf("建索引失败: %v", err)
|
||
}
|
||
t.Cleanup(func() { db.Close() })
|
||
return db
|
||
}
|
||
|
||
// newFileTestDB 创建基于文件的 SQLite 数据库(用于需要独立隔离的测试)
|
||
func newFileTestDB(t *testing.T) *sql.DB {
|
||
t.Helper()
|
||
f, err := os.CreateTemp("", "launchcode_test_*.db")
|
||
if err != nil {
|
||
t.Fatalf("创建临时数据库文件失败: %v", err)
|
||
}
|
||
f.Close()
|
||
dbPath := f.Name()
|
||
|
||
db, err := sql.Open("sqlite", dbPath)
|
||
if err != nil {
|
||
t.Fatalf("打开测试数据库失败: %v", err)
|
||
}
|
||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS launch_codes (
|
||
profile_id TEXT PRIMARY KEY,
|
||
code TEXT NOT NULL UNIQUE,
|
||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||
)`)
|
||
if err != nil {
|
||
t.Fatalf("建表失败: %v", err)
|
||
}
|
||
_, err = db.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_launch_codes_code ON launch_codes(code)`)
|
||
if err != nil {
|
||
t.Fatalf("建索引失败: %v", err)
|
||
}
|
||
t.Cleanup(func() {
|
||
db.Close()
|
||
os.Remove(dbPath)
|
||
})
|
||
return db
|
||
}
|
||
|
||
// TestProperty2_PersistenceRoundTrip
|
||
// Property 2: 持久化 Round-Trip
|
||
// 对于任意 ProfileId 和 LaunchCode,Upsert 后:
|
||
// - FindProfileId(code) 返回相同的 profileId
|
||
// - FindCode(profileId) 返回相同的 code
|
||
func TestProperty2_PersistenceRoundTrip(t *testing.T) {
|
||
properties := gopter.NewProperties(gopter.DefaultTestParameters())
|
||
|
||
properties.Property("Upsert 后 FindProfileId 返回正确 profileId", prop.ForAll(
|
||
func(profileId, code string) bool {
|
||
db := newFileTestDB(t)
|
||
dao := launchcode.NewSQLiteLaunchCodeDAO(db)
|
||
|
||
if err := dao.Upsert(profileId, code); err != nil {
|
||
return false
|
||
}
|
||
got, err := dao.FindProfileId(code)
|
||
return err == nil && got == profileId
|
||
},
|
||
gen.AlphaString().SuchThat(func(s string) bool { return len(s) > 0 && len(s) <= 64 }),
|
||
gen.RegexMatch(`[A-Z0-9]{6}`),
|
||
))
|
||
|
||
properties.Property("Upsert 后 FindCode 返回正确 code", prop.ForAll(
|
||
func(profileId, code string) bool {
|
||
db := newFileTestDB(t)
|
||
dao := launchcode.NewSQLiteLaunchCodeDAO(db)
|
||
|
||
if err := dao.Upsert(profileId, code); err != nil {
|
||
return false
|
||
}
|
||
got, err := dao.FindCode(profileId)
|
||
return err == nil && got == code
|
||
},
|
||
gen.AlphaString().SuchThat(func(s string) bool { return len(s) > 0 && len(s) <= 64 }),
|
||
gen.RegexMatch(`[A-Z0-9]{6}`),
|
||
))
|
||
|
||
properties.Property("Upsert 幂等:相同 profileId 更新 code 后查询返回新 code", prop.ForAll(
|
||
func(profileId, code1, code2 string) bool {
|
||
if code1 == code2 {
|
||
return true // 跳过相同 code 的情况
|
||
}
|
||
db := newFileTestDB(t)
|
||
dao := launchcode.NewSQLiteLaunchCodeDAO(db)
|
||
|
||
if err := dao.Upsert(profileId, code1); err != nil {
|
||
return false
|
||
}
|
||
if err := dao.Upsert(profileId, code2); err != nil {
|
||
return false
|
||
}
|
||
got, err := dao.FindCode(profileId)
|
||
return err == nil && got == code2
|
||
},
|
||
gen.AlphaString().SuchThat(func(s string) bool { return len(s) > 0 && len(s) <= 64 }),
|
||
gen.RegexMatch(`[A-Z0-9]{6}`),
|
||
gen.RegexMatch(`[A-Z0-9]{6}`),
|
||
))
|
||
|
||
properties.TestingRun(t)
|
||
}
|
||
|
||
// TestProperty2_DeleteRemovesMapping
|
||
// Property 2 补充:Delete 后查询应返回 not found
|
||
func TestProperty2_DeleteRemovesMapping(t *testing.T) {
|
||
properties := gopter.NewProperties(gopter.DefaultTestParameters())
|
||
|
||
properties.Property("Delete 后 FindCode 返回错误", prop.ForAll(
|
||
func(profileId, code string) bool {
|
||
db := newFileTestDB(t)
|
||
dao := launchcode.NewSQLiteLaunchCodeDAO(db)
|
||
|
||
if err := dao.Upsert(profileId, code); err != nil {
|
||
return false
|
||
}
|
||
if err := dao.Delete(profileId); err != nil {
|
||
return false
|
||
}
|
||
_, err := dao.FindCode(profileId)
|
||
return err != nil
|
||
},
|
||
gen.AlphaString().SuchThat(func(s string) bool { return len(s) > 0 && len(s) <= 64 }),
|
||
gen.RegexMatch(`[A-Z0-9]{6}`),
|
||
))
|
||
|
||
properties.TestingRun(t)
|
||
}
|
||
|
||
// TestProperty2_LoadAllRoundTrip
|
||
// Property 2 补充:LoadAll 返回所有已写入的映射
|
||
func TestProperty2_LoadAllRoundTrip(t *testing.T) {
|
||
db := newTestDB(t)
|
||
dao := launchcode.NewSQLiteLaunchCodeDAO(db)
|
||
|
||
// 写入一批映射
|
||
entries := map[string]string{}
|
||
for i := 0; i < 10; i++ {
|
||
profileId := fmt.Sprintf("profile-%02d", i)
|
||
code := fmt.Sprintf("CODE%02d", i)
|
||
entries[profileId] = code
|
||
if err := dao.Upsert(profileId, code); err != nil {
|
||
t.Fatalf("Upsert 失败: %v", err)
|
||
}
|
||
}
|
||
|
||
loaded, err := dao.LoadAll()
|
||
if err != nil {
|
||
t.Fatalf("LoadAll 失败: %v", err)
|
||
}
|
||
|
||
for profileId, code := range entries {
|
||
got, ok := loaded[profileId]
|
||
if !ok {
|
||
t.Errorf("LoadAll 缺少 profileId=%s", profileId)
|
||
continue
|
||
}
|
||
if got != code {
|
||
t.Errorf("LoadAll profileId=%s: 期望 code=%s,实际=%s", profileId, code, got)
|
||
}
|
||
}
|
||
}
|