visionA/visionA-backend/internal/db/db_integration_test.go
jim800121chen 8369cab85c feat(db): migration 0005 agents 模型(個人設備管理 A' 走向第二階段地基)
- 新增 agents 表 + devices 加 4 欄(agent_id/agent_local_device_id/
  registered_at/is_representative)+ 2 index + 純 SQL data migration
- data migration 採 R-A:agent.id := device.id 決定性推導、冪等、
  soft-deleted device 排除(WHERE deleted_at IS NULL)
- 守住 ADR-018 走向 A':不碰 session_tokens FK、不改現有 partial
  unique index uq_devices_owner_serial_active、對 0001-0003 零破壞
- migrate_0005_db_test.go 6 個 dbtest case(apply/data migration/
  rollback 對稱/re-apply 冪等/session_tokens 零影響/既有 device 讀寫回歸)
- 修 TestMigrate_UpDownUp 編號 gap 假設(0001/2/3/5 無 0004):
  assert.Equal(topVer-1) → assert.Less(downVer, topVer)
- .gitignore 加 .logs/ + **/.logs/(本機執行 log per-branch 不進 git)

Reviewer 通過(0 Critical/0 Major/3 Minor/4 Sug)。全 db package 41
dbtest 130 綠、build/vet/test 綠、gitleaks 0。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 04:05:53 +08:00

152 lines
5.8 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.

//go:build dbtest
// DB 接入塊 00.8):連線池 / migration 本身的整合測試,走 testcontainers真 Postgres 14.23)。
//
// build tag `dbtest`:需要 Docker daemon。預設 `go test ./...` 不編譯本檔。
// 執行go test -tags=dbtest ./internal/db/...
package db_test
import (
"context"
"io"
"log/slog"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"visiona-backend/internal/config"
"visiona-backend/internal/db"
"visiona-backend/internal/db/testsupport"
)
func discardLog() *slog.Logger { return slog.New(slog.NewTextHandler(io.Discard, nil)) }
// TestPool_PingAfterSetup 驗證 setupTestDB 後連線池可 ping 通。
func TestPool_PingAfterSetup(t *testing.T) {
tdb := testsupport.SetupTestDB(t)
require.NoError(t, tdb.Pool.Ping(context.Background()))
}
// TestMigrate_Idempotent 驗證 migrate up 冪等:跑第二次為 no-change、版本不變、not dirty。
//
// 不寫死版本號——冪等的本質是「再 up 一次 version 不變」不是「version == 某固定數字」。
// 以 SetupTestDB 跑完第一次 up 後的版本當基準,驗第二次 up 後版本與其一致。
// 如此未來再加 0004/0005 也不會壞。
func TestMigrate_Idempotent(t *testing.T) {
tdb := testsupport.SetupTestDB(t)
// SetupTestDB 已跑過一次 up記下當下版本當基準= 最新可用 migration 版本)。
mg, err := db.NewMigrator(tdb.Cfg, discardLog())
require.NoError(t, err)
defer mg.Close()
baseVer, dirty, err := mg.Version()
require.NoError(t, err)
require.False(t, dirty, "第一次 up 後不應 dirty")
require.Greater(t, baseVer, uint(0), "第一次 up 後版本應 > 0")
// 再跑一次 up 應為 no-change不報錯
require.NoError(t, db.RunMigrations(tdb.Cfg, discardLog()))
ver, dirty, err := mg.Version()
require.NoError(t, err)
assert.False(t, dirty, "schema 不應處於 dirty 狀態")
assert.Equal(t, baseVer, ver, "再 up 一次版本不應改變(冪等)")
}
// TestMigrate_UpDownUp 驗證 down 後 up 仍可回到原狀態(雙向 migration 正確)。
//
// 注意Migrator.Down() 實作為 m.Steps(-1),是「回退一個 migration」而非「回滾全部」。
// 因此本測試驗的是版本層級的可逆性down 後版本變小、再 up 後版本回到原值且 not dirty。
//
// 不假設 migration 連續編號golang-migrate 的 version = migration 檔的數字前綴,
// Steps(-1) 是回退「前一個實際存在的 migration」不是 version-1。編號有 gap
// (如 0001/0002/0003/0005無 0004從 5 down 一步會回到 3 而非 4。
// 故 down 後只斷言「版本嚴格變小且 not dirty」不寫死差值。
func TestMigrate_UpDownUp(t *testing.T) {
tdb := testsupport.SetupTestDB(t)
mg, err := db.NewMigrator(tdb.Cfg, discardLog())
require.NoError(t, err)
defer mg.Close()
// SetupTestDB 已 up 到最新,記下原始版本。
topVer, dirty, err := mg.Version()
require.NoError(t, err)
require.False(t, dirty, "起始不應 dirty")
require.Greater(t, topVer, uint(0), "起始版本應 > 0")
// down 一步Steps(-1):只回退最新一個 migration
require.NoError(t, mg.Down())
downVer, dirty, err := mg.Version()
require.NoError(t, err)
assert.False(t, dirty, "down 後不應 dirty")
assert.Less(t, downVer, topVer, "down 一步後版本應嚴格變小(不假設連續編號,回退到前一個實際存在的 migration")
// 再 up 一次應回到原始最新版本(雙向可逆)。
require.NoError(t, db.RunMigrations(tdb.Cfg, discardLog()))
upVer, dirty, err := mg.Version()
require.NoError(t, err)
assert.False(t, dirty, "重新 up 後不應 dirty")
assert.Equal(t, topVer, upVer, "重新 up 後版本應回到原始最新版本")
}
// TestMigrate_SchemaShape 抽查 0001 的關鍵 schema 特性gen_random_uuid 預設、FK、partial index
func TestMigrate_SchemaShape(t *testing.T) {
tdb := testsupport.SetupTestDB(t)
ctx := context.Background()
// users.id 預設應能用 gen_random_uuid()PG14 內建)插入不指定 id。
var uid string
err := tdb.Pool.QueryRow(ctx,
`INSERT INTO users (email) VALUES ($1) RETURNING id`, "shape@test.local").Scan(&uid)
require.NoError(t, err, "insert user with default uuid")
assert.NotEmpty(t, uid)
// models.owner_user_id FK插入不存在的 owner 應失敗。
_, err = tdb.Pool.Exec(ctx,
`INSERT INTO models (owner_user_id, name, storage_key, file_size, source)
VALUES ($1, $2, $3, $4, $5)`,
"99999999-9999-9999-9999-999999999999", "m", "k", 1, "uploaded")
assert.Error(t, err, "FK 違反應報錯")
// 用合法 owner 插入應成功array 欄位 round-trip。
var mid string
err = tdb.Pool.QueryRow(ctx,
`INSERT INTO models (owner_user_id, name, storage_key, file_size, source, classes, input_shape)
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`,
uid, "m1", "models/x.nef", 123, "uploaded", []string{"cat", "dog"}, []int32{1, 224, 224, 3}).Scan(&mid)
require.NoError(t, err)
assert.NotEmpty(t, mid)
}
// TestPool_FailFast 驗證連到不存在的 DB 會 fail-fast建池/ping 回 error不 hang
func TestPool_FailFast(t *testing.T) {
cfg := config.DatabaseConfig{
Host: "127.0.0.1",
Port: 1, // 不可能有 postgres 監聽的 port
User: "nobody",
Password: "nopw",
DBName: "nodb",
SSLMode: "disable",
ConnTimeout: 2 * time.Second,
}
start := time.Now()
_, err := db.NewPool(context.Background(), cfg, discardLog())
require.Error(t, err, "連不上 DB 應回 error")
assert.Less(t, time.Since(start), 10*time.Second, "應在 ConnTimeout 內 fail-fast不 hang")
}
// TestTruncate 驗證 truncate helper 清空資料。
func TestTruncate(t *testing.T) {
tdb := testsupport.SetupTestDB(t)
tdb.EnsureDemoUser(t)
assert.Equal(t, 1, tdb.CountRows(t, "users"))
tdb.Truncate(t, "models", "users")
assert.Equal(t, 0, tdb.CountRows(t, "users"))
}