- 新增 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>
152 lines
5.8 KiB
Go
152 lines
5.8 KiB
Go
//go:build dbtest
|
||
|
||
// DB 接入塊 0(0.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"))
|
||
}
|