//go:build dbtest // WithTx 交易 helper 的真 DB 整合測試(DB 接入塊 5.1 / 5.5)。 // // build tag `dbtest`:只在帶 `-tags=dbtest`(需要 Docker / testcontainers)時編譯/執行。 // 預設 `go test ./...`(無 Docker)不觸碰本檔,維持綠燈。 // // 用 package db_test(外部測試包)避免 testsupport → db 的 import cycle(對齊 db_integration_test.go)。 // // 執行: // // go test -tags=dbtest ./internal/db/... // # 無本機 Docker 時,Orchestrator 在 130 補跑: // DOCKER_HOST=tcp://192.168.0.130:2375 TESTCONTAINERS_RYUK_DISABLED=true \ // go test -tags=dbtest ./internal/db/... // // 涵蓋:commit 落地、fn 回 error 整筆 rollback、DB 錯誤 rollback、panic rollback、context 取消、nil pool。 package db_test import ( "context" "errors" "testing" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "visiona-backend/internal/db" "visiona-backend/internal/db/testsupport" ) func countUsersTx(t *testing.T, tdb *testsupport.TestDB) int { t.Helper() var n int require.NoError(t, tdb.Pool.QueryRow(context.Background(), `SELECT count(*) FROM users`).Scan(&n)) return n } func insertUserTx(ctx context.Context, q db.Querier, id, email string) error { _, err := q.Exec(ctx, `INSERT INTO users (id, email) VALUES ($1, $2)`, id, email) return err } // TestWithTx_CommitsOnSuccess 驗證 fn 成功時變更落地(commit)。 func TestWithTx_CommitsOnSuccess(t *testing.T) { tdb := testsupport.SetupTestDB(t) tdb.Truncate(t, "users") id := uuid.NewString() err := db.WithTx(context.Background(), tdb.Pool, func(q db.Querier) error { return insertUserTx(context.Background(), q, id, id+"@t.local") }) require.NoError(t, err) assert.Equal(t, 1, countUsersTx(t, tdb), "commit 後該列應存在") } // TestWithTx_RollbackOnError 驗證 fn 回 error 時整筆 rollback(中途已寫入的列也回滾)。 func TestWithTx_RollbackOnError(t *testing.T) { tdb := testsupport.SetupTestDB(t) tdb.Truncate(t, "users") sentinel := errors.New("boom") err := db.WithTx(context.Background(), tdb.Pool, func(q db.Querier) error { if e := insertUserTx(context.Background(), q, uuid.NewString(), "a@t.local"); e != nil { return e } return sentinel }) require.ErrorIs(t, err, sentinel, "WithTx 應原樣回傳 fn 的 error") assert.Equal(t, 0, countUsersTx(t, tdb), "rollback 後第一列也不該存在(原子性)") } // TestWithTx_RollbackOnDBError 驗證 fn 內 DB 錯誤(unique violation)→ rollback。 func TestWithTx_RollbackOnDBError(t *testing.T) { tdb := testsupport.SetupTestDB(t) tdb.Truncate(t, "users") email := "dup@t.local" _, err := tdb.Pool.Exec(context.Background(), `INSERT INTO users (id, email) VALUES ($1, $2)`, uuid.NewString(), email) require.NoError(t, err) err = db.WithTx(context.Background(), tdb.Pool, func(q db.Querier) error { if e := insertUserTx(context.Background(), q, uuid.NewString(), "ok@t.local"); e != nil { return e } return insertUserTx(context.Background(), q, uuid.NewString(), email) // 撞 unique }) require.Error(t, err) assert.Equal(t, 1, countUsersTx(t, tdb), "交易外那筆仍在,交易內兩筆 rollback") var okCount int require.NoError(t, tdb.Pool.QueryRow(context.Background(), `SELECT count(*) FROM users WHERE email = 'ok@t.local'`).Scan(&okCount)) assert.Equal(t, 0, okCount, "交易內第一列也應 rollback") } // TestWithTx_PanicRollsBackAndRepanics 驗證 fn panic 時 rollback 並重新 panic(不吞)。 func TestWithTx_PanicRollsBackAndRepanics(t *testing.T) { tdb := testsupport.SetupTestDB(t) tdb.Truncate(t, "users") assert.Panics(t, func() { _ = db.WithTx(context.Background(), tdb.Pool, func(q db.Querier) error { _ = insertUserTx(context.Background(), q, uuid.NewString(), "panic@t.local") panic("kaboom") }) }) assert.Equal(t, 0, countUsersTx(t, tdb), "panic 後該列應 rollback") } // TestWithTx_ContextCanceled 驗證已取消的 context → Begin / 查詢失敗、無半開交易。 func TestWithTx_ContextCanceled(t *testing.T) { tdb := testsupport.SetupTestDB(t) tdb.Truncate(t, "users") ctx, cancel := context.WithCancel(context.Background()) cancel() err := db.WithTx(ctx, tdb.Pool, func(q db.Querier) error { return insertUserTx(ctx, q, uuid.NewString(), "cancel@t.local") }) require.Error(t, err) assert.Equal(t, 0, countUsersTx(t, tdb)) } // TestWithTx_NilPool 驗證 nil pool 回 error(不 panic)。此測試不需真 DB,但置於 dbtest 檔 // 共用 build tag;純驗 nil 防護。 func TestWithTx_NilPool(t *testing.T) { err := db.WithTx(context.Background(), nil, func(q db.Querier) error { return nil }) require.Error(t, err) }