| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127 |
- package auth
- import (
- "context"
- "crypto/rand"
- "database/sql"
- "encoding/hex"
- "fmt"
- "net/url"
- "os"
- "strings"
- "testing"
- "github.com/kairo/haven/internal/db"
- )
- // 集成测试的隔离方式:每个用例在自己的 PostgreSQL schema 里跑。
- //
- // 平铺的表名加上 search_path 切换,既能复用 0001_init.up.sql,又不会让并发的
- // TRUNCATE 互相踩踏(`go test ./...` 会并行跑不同包,同包内也可能并发)。
- //
- // HAVEN_TEST_DB_DSN='postgres://haven:...@127.0.0.1:5432/haven_dev?sslmode=disable' \
- // go test ./... -v
- // newSchemaService 在独占 schema 里建表,返回已切到该 schema 的 Service 与连接。
- func newSchemaService(t *testing.T) *Service {
- t.Helper()
- admin, dsn := openTestDB(t)
- schema := fmt.Sprintf("test_%s", sanitize(t.Name()))
- ctx := context.Background()
- if _, err := admin.ExecContext(ctx, `DROP SCHEMA IF EXISTS `+schema+` CASCADE`); err != nil {
- t.Fatalf("清理测试 schema 失败: %v", err)
- }
- if _, err := admin.ExecContext(ctx, `CREATE SCHEMA `+schema); err != nil {
- t.Fatalf("创建测试 schema 失败: %v", err)
- }
- // 在该 schema 内重建全部表:search_path 切到新 schema,
- // pg_trgm 建在 public 并由 search_path 继承。
- conn, err := db.Open(ctx, dsnWithSchema(dsn, schema+",public"))
- if err != nil {
- t.Fatalf("连接测试 schema 失败: %v", err)
- }
- t.Cleanup(func() {
- conn.Close()
- admin.ExecContext(context.Background(), `DROP SCHEMA IF EXISTS `+schema+` CASCADE`)
- admin.Close()
- })
- migration, err := os.ReadFile("./migrations/0001_init.up.sql")
- if err == nil {
- // 从包目录跑时可用
- if _, err := conn.ExecContext(ctx, string(migration)); err != nil {
- t.Fatalf("在测试 schema 内建表失败: %v", err)
- }
- } else {
- // 在 internal/auth 下跑时,迁移文件在 ../../migrations
- migration, err = os.ReadFile("../../migrations/0001_init.up.sql")
- if err != nil {
- t.Fatalf("读取 migration 失败: %v", err)
- }
- if _, err := conn.ExecContext(ctx, string(migration)); err != nil {
- t.Fatalf("在测试 schema 内建表失败: %v", err)
- }
- }
- return NewService(conn)
- }
- // openTestDB 打开与测试库的连接,未设置 HAVEN_TEST_DB_DSN 时跳过。
- func openTestDB(t *testing.T) (*sql.DB, string) {
- t.Helper()
- dsn := os.Getenv("HAVEN_TEST_DB_DSN")
- if dsn == "" {
- t.Skip("未设置 HAVEN_TEST_DB_DSN,跳过数据库集成测试")
- }
- conn, err := db.Open(context.Background(), dsn)
- if err != nil {
- t.Fatalf("连接测试库失败: %v", err)
- }
- t.Cleanup(func() { conn.Close() })
- return conn, dsn
- }
- // dsnWithSchema 在 DSN 上追加 search_path。
- func dsnWithSchema(dsn, searchPath string) string {
- u, err := url.Parse(dsn)
- if err != nil {
- sep := "?"
- if strings.Contains(dsn, "?") {
- sep = "&"
- }
- return dsn + sep + "search_path=" + url.QueryEscape(searchPath)
- }
- q := u.Query()
- q.Set("search_path", searchPath)
- u.RawQuery = q.Encode()
- return u.String()
- }
- // randomSuffix 给 schema 名加一个短随机后缀,避免 -count>1 时重名。
- func randomSuffix() string {
- buf := make([]byte, 4)
- _, _ = rand.Read(buf)
- return hex.EncodeToString(buf)
- }
- // sanitize 把测试名变成合法的 schema 片段(只留小写字母数字下划线,限长 40)。
- func sanitize(name string) string {
- var b strings.Builder
- for _, r := range strings.ToLower(name) {
- switch {
- case r >= 'a' && r <= 'z', r >= '0' && r <= '9', r == '_':
- b.WriteRune(r)
- default:
- b.WriteRune('_')
- }
- }
- out := b.String()
- if len(out) > 40 {
- out = out[:40]
- }
- return out
- }
|