把 visionA-backend 6 個 in-memory store 接到資料庫持久化,範圍=完整 (PG 全接 + session 接 Redis + 交易韌性)。interface / handler 不動, 只加 DB 實作 + 換 wiring,config 未設 DB 時保留 in-memory fallback。 - 塊 0 基礎建設:pgx/v5 連線池 + DatabaseConfig/RedisConfig + golang-migrate runner(embed)+ cmd/migrate + testcontainers 測試基礎建設 - 塊 1 model → Postgres:array 映射、upsert 保留 CreatedAt、faa_object_key、 三維 filter(owner/chip/source)、soft-delete partial index - 塊 2 device → Postgres:partial unique(已刪 serial 可重註冊)、雙狀態欄位 - 塊 3 token → Postgres:pairing_tokens + session_tokens 分表、token_hash 當 PK - 塊 4 userSession → Redis:idle + absolute 雙 TTL 取代 cleanup goroutine (tunnel session 維持 in-memory,yamux handle 不可序列化) - 塊 5 交易/韌性:WithTx helper + 刪 device cascade 撤銷 token(同 tx 原子) + /healthz ping PG/Redis(fail-fast 503)+ pgx error 統一映射(不洩漏 raw error) 降級策略(fail-fast):PG 掉 → 持久資料 API 回 503;Redis 掉 → session 失敗 不自動 fallback in-memory(避免多機 session 不同步)。 DB:PostgreSQL 14.23(gen_random_uuid 內建、無 citext → email 用 lower() unique index)。每塊經 Reviewer 審查 + 真 PG/Redis testcontainers 全量 dbtest 綠燈, in-memory fallback 未受影響。 docs: 同步更新 database.md(schema/config/migration 清單)+ api-spec.md (409/503 錯誤碼、/healthz 新行為、device unpair cascade)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
72 lines
2.4 KiB
Go
72 lines
2.4 KiB
Go
//go:build dbtest
|
||
|
||
package testsupport
|
||
|
||
import (
|
||
"context"
|
||
"testing"
|
||
|
||
"github.com/google/uuid"
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
// DemoUserID 是測試共用的固定 user id,對齊雛形 demo-user 概念。
|
||
// 因 schema 的 users.id 是 UUID,這裡用一個固定 UUID 當測試 owner。
|
||
const DemoUserID = "00000000-0000-0000-0000-000000000001"
|
||
|
||
// InsertUser 直接寫入一筆 users(測試 fixture)。
|
||
//
|
||
// 塊 1+ 的 model / device / token repository 測試需要一個存在的 owner_user_id 來滿足 FK。
|
||
// 回傳寫入的 user id(傳入 id 為空時自動產生 UUID)。
|
||
func (tdb *TestDB) InsertUser(t *testing.T, id, email string) string {
|
||
t.Helper()
|
||
if id == "" {
|
||
id = uuid.NewString()
|
||
}
|
||
if email == "" {
|
||
email = id + "@test.local"
|
||
}
|
||
_, err := tdb.Pool.Exec(context.Background(),
|
||
`INSERT INTO users (id, email) VALUES ($1, $2)
|
||
ON CONFLICT (id) DO NOTHING`,
|
||
id, email)
|
||
require.NoError(t, err, "insert user fixture")
|
||
return id
|
||
}
|
||
|
||
// EnsureDemoUser 確保 DemoUserID 這筆 user 存在,回傳其 id。
|
||
// 供「只需要一個合法 owner、不在意是誰」的 model / device 測試使用。
|
||
func (tdb *TestDB) EnsureDemoUser(t *testing.T) string {
|
||
t.Helper()
|
||
return tdb.InsertUser(t, DemoUserID, "demo@visiona.local")
|
||
}
|
||
|
||
// InsertDevice 直接寫入一筆 devices(測試 fixture)。
|
||
//
|
||
// 塊 3 的 session_tokens 測試需要一個存在的 device_id(NOT NULL FK → devices(id))。
|
||
// ownerUserID 須先存在於 users。回傳寫入的 device id(傳入 id 為空時自動產生 UUID)。
|
||
func (tdb *TestDB) InsertDevice(t *testing.T, id, ownerUserID string) string {
|
||
t.Helper()
|
||
if id == "" {
|
||
id = uuid.NewString()
|
||
}
|
||
_, err := tdb.Pool.Exec(context.Background(),
|
||
`INSERT INTO devices (id, owner_user_id, name, serial_number)
|
||
VALUES ($1, $2, $3, $4)
|
||
ON CONFLICT (id) DO NOTHING`,
|
||
id, ownerUserID, "fixture-device", "SN-"+id[:8])
|
||
require.NoError(t, err, "insert device fixture")
|
||
return id
|
||
}
|
||
|
||
// CountRows 回傳指定 table 的列數(含或不含 soft-deleted 由呼叫端用 where 決定,此處為全表)。
|
||
// 供測試斷言「插入幾筆 / truncate 後是否歸零」。
|
||
func (tdb *TestDB) CountRows(t *testing.T, table string) int {
|
||
t.Helper()
|
||
var n int
|
||
err := tdb.Pool.QueryRow(context.Background(),
|
||
"SELECT count(*) FROM "+pgIdent(table)).Scan(&n)
|
||
require.NoError(t, err, "count rows in %s", table)
|
||
return n
|
||
}
|