Files
Ant-Browser/backend/test/launchcode/dao_prop_test.go
T
Ant Browser Release Bot 6f58a6c19a publish: 1.0.0 snapshot (bad2ec1)
channel: master
version: 1.0.0
source-ref: master
published-at-utc: 2026-03-13T15:19:28Z
2026-03-13 23:19:29 +08:00

200 lines
5.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 和 LaunchCodeUpsert 后:
// - 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)
}
}
}