DB 接入塊 0-5 上主幹後的收尾工作,讓 DB-on 模式可真人使用 + 補齊功能與測試。 OIDC / pairing FK 修復(接 DB 上線必要): - 新建 internal/user package(User + Store + InMemory + Postgres);OIDC callback 驗證 id_token 成功後 fail-closed upsert users(sub 直接當 users.id,MC sub 為 UUID) - pairing exchange 雲端自建 device(不動 local-tool)+ 同 tx 綁 session token; 自建 device 空 serial 寫 NULL(避免撞 partial unique) - device.SaveTx / session.CreateTx 新增 tx-aware 版本 B4 model metadata: - 轉檔 result 的 analysis_info(input_shape/classes/framework)串進 model: converter_client → flow → adapter → model.Model → PG → ModelResponse DTO - input_shape 優先用陣列、後備四維組 NCHW、缺一不亂組;全 optional 防禦性 - 前端詳細頁顯示(另 repo);轉檔端串接交接檔 b4-converter-handoff.md nginx healthz(部署層): - 新增 /healthz/deep 轉發 backend(ping PG+Redis、down 回 503)給 LB - 修掉 default_server return 444 短路 bug(docker healthcheck 長期 unhealthy 真因) storage error 統一映射(不洩漏 storage 後端細節)。 測試:補 internal/api(storage/errors handler)、cmd/api-server(seed/adapter)、 internal/db(redis)、relay/session 弱處,含 testcontainers integration。 DB 接入相關 package 真環境覆蓋達 88-94%。全程 Reviewer 審查 + 130 真 PG/Redis dbtest 綠。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
166 lines
6.3 KiB
Go
166 lines
6.3 KiB
Go
// seed_test.go — seedDemoData 的 in-memory(dbBacked=false)unit test。
|
||
//
|
||
// Owner: testing agent(補測任務 — cmd/api-server 可測部分)
|
||
//
|
||
// 對象:seedDemoData。in-memory 分支不需 DB,純 unit。DB-backed(dbBacked=true)分支
|
||
// 需真 Postgres → 拆到 seed_db_test.go(//go:build dbtest,130 docker 跑)。
|
||
//
|
||
// 行為對齊 seed.go docstring:
|
||
// - dbBacked=false 用傳入 userID(StaticUserID 概念)當 owner,model/device id 帶隨機後綴。
|
||
// - 不 upsert demo user(in-memory 不需 FK)。
|
||
// - seedDemoData 本身「每次呼叫都產新資料」(id 帶 uuid 後綴 / pairing 每次新建)——
|
||
// 「重啟不重複 seed」是 main.go 只呼叫一次保證的,不是本函式的冪等性;故本檔不誤測冪等。
|
||
package main
|
||
|
||
import (
|
||
"context"
|
||
"io"
|
||
"log/slog"
|
||
"testing"
|
||
|
||
"github.com/stretchr/testify/assert"
|
||
"github.com/stretchr/testify/require"
|
||
|
||
"visiona-backend/internal/auth"
|
||
"visiona-backend/internal/device"
|
||
"visiona-backend/internal/model"
|
||
"visiona-backend/internal/user"
|
||
)
|
||
|
||
// discardLog 丟棄 seed 的 log 噪音。
|
||
func discardLog() *slog.Logger {
|
||
return slog.New(slog.NewTextHandler(io.Discard, nil))
|
||
}
|
||
|
||
// seedInMemoryDeps 建立一組全 in-memory 的 seed 依賴。
|
||
type seedDeps struct {
|
||
devRepo *device.InMemoryRepository
|
||
mdlRepo *model.InMemoryRepository
|
||
pairings *auth.InMemoryPairingStore
|
||
userStore *user.InMemoryStore
|
||
}
|
||
|
||
func newSeedDeps() seedDeps {
|
||
return seedDeps{
|
||
devRepo: device.NewInMemoryRepository(),
|
||
mdlRepo: model.NewInMemoryRepository(),
|
||
pairings: auth.NewInMemoryPairingStore(),
|
||
userStore: user.NewInMemoryStore(),
|
||
}
|
||
}
|
||
|
||
// happy:in-memory seed 應建出 1 device + 1 model + 1 pairing token,全掛在傳入的 userID 下。
|
||
func TestSeedDemoData_InMemory_SeedsAllEntities(t *testing.T) {
|
||
t.Parallel()
|
||
d := newSeedDeps()
|
||
const uid = "demo-user" // 雛形 StaticUserID 概念(in-memory 不需是合法 UUID)
|
||
|
||
require.NoError(t, seedDemoData(
|
||
d.devRepo, d.mdlRepo, d.pairings, d.userStore,
|
||
uid, false /* dbBacked */, discardLog(),
|
||
))
|
||
|
||
ctx := context.Background()
|
||
|
||
// device:owner = uid
|
||
devs, err := d.devRepo.List(ctx, uid)
|
||
require.NoError(t, err)
|
||
require.Len(t, devs, 1, "in-memory seed 應建 1 個 demo device")
|
||
assert.Equal(t, uid, devs[0].OwnerUserID)
|
||
assert.Equal(t, "kl520", devs[0].DeviceType)
|
||
assert.Equal(t, device.RemoteStatusOffline, devs[0].RemoteStatus)
|
||
|
||
// model:owner = uid,StorageKey 用 owner 拼路徑
|
||
mdls, err := d.mdlRepo.List(ctx, model.ListFilter{OwnerUserID: uid})
|
||
require.NoError(t, err)
|
||
require.Len(t, mdls, 1, "in-memory seed 應建 1 個 demo model")
|
||
assert.Equal(t, uid, mdls[0].OwnerUserID)
|
||
assert.Equal(t, "kl520", mdls[0].TargetChip)
|
||
assert.Equal(t, model.SourceUploaded, mdls[0].Source)
|
||
assert.Equal(t, "models/"+uid+"/demo.nef", mdls[0].StorageKey)
|
||
require.NotNil(t, mdls[0].UploadedAt, "seed model 應設 UploadedAt(ready)")
|
||
|
||
// pairing token:owner = uid
|
||
tokens, err := d.pairings.List(ctx, uid)
|
||
require.NoError(t, err)
|
||
require.Len(t, tokens, 1, "in-memory seed 應建 1 個 demo pairing token")
|
||
assert.Equal(t, uid, tokens[0].UserID)
|
||
}
|
||
|
||
// in-memory 不 upsert demo user(不需 FK);userStore 應維持空(demoSeedUserID 不存在)。
|
||
func TestSeedDemoData_InMemory_DoesNotUpsertDemoUser(t *testing.T) {
|
||
t.Parallel()
|
||
d := newSeedDeps()
|
||
|
||
require.NoError(t, seedDemoData(
|
||
d.devRepo, d.mdlRepo, d.pairings, d.userStore,
|
||
"demo-user", false, discardLog(),
|
||
))
|
||
|
||
// dbBacked=false 不應走 userStore.Upsert(demoSeedUserID)
|
||
_, err := d.userStore.Get(context.Background(), demoSeedUserID)
|
||
assert.Error(t, err, "in-memory seed 不應 upsert demoSeedUserID(無 FK 需求)")
|
||
}
|
||
|
||
// boundary / concurrency-ish:seedDemoData 每次呼叫產生獨立資料(id 帶隨機後綴)——
|
||
// 連呼叫兩次同一 userID 應累積 2 device / 2 model / 2 token(對齊 docstring:本函式非冪等,
|
||
// 「不重複 seed」由 main 只呼叫一次保證)。此測試把該行為釘住,避免有人誤改成「靜默去重」。
|
||
func TestSeedDemoData_InMemory_CalledTwice_Accumulates(t *testing.T) {
|
||
t.Parallel()
|
||
d := newSeedDeps()
|
||
const uid = "demo-user"
|
||
log := discardLog()
|
||
|
||
require.NoError(t, seedDemoData(d.devRepo, d.mdlRepo, d.pairings, d.userStore, uid, false, log))
|
||
require.NoError(t, seedDemoData(d.devRepo, d.mdlRepo, d.pairings, d.userStore, uid, false, log))
|
||
|
||
ctx := context.Background()
|
||
devs, err := d.devRepo.List(ctx, uid)
|
||
require.NoError(t, err)
|
||
assert.Len(t, devs, 2, "兩次呼叫應產生 2 個 device(id 帶隨機後綴、不去重)")
|
||
|
||
mdls, err := d.mdlRepo.List(ctx, model.ListFilter{OwnerUserID: uid})
|
||
require.NoError(t, err)
|
||
assert.Len(t, mdls, 2, "兩次呼叫應產生 2 個 model")
|
||
|
||
tokens, err := d.pairings.List(ctx, uid)
|
||
require.NoError(t, err)
|
||
assert.Len(t, tokens, 2, "兩次呼叫應產生 2 個 pairing token")
|
||
}
|
||
|
||
// error path:底層 repo Save 失敗時 seedDemoData 只 log warning、不回 error、不中斷(fail-soft)。
|
||
// 用一個 Save 永遠失敗的 device repo 驗「seed 失敗不阻擋啟動」契約。
|
||
func TestSeedDemoData_InMemory_DeviceSaveFails_StillReturnsNil(t *testing.T) {
|
||
t.Parallel()
|
||
d := newSeedDeps()
|
||
|
||
err := seedDemoData(
|
||
failingDeviceRepo{}, // device.Save 永遠失敗
|
||
d.mdlRepo, d.pairings, d.userStore,
|
||
"demo-user", false, discardLog(),
|
||
)
|
||
require.NoError(t, err, "device save 失敗時 seedDemoData 仍應回 nil(fail-soft,不阻擋啟動)")
|
||
|
||
// model / pairing 仍應照常建立(device 失敗不影響後續步驟)
|
||
mdls, err := d.mdlRepo.List(context.Background(), model.ListFilter{OwnerUserID: "demo-user"})
|
||
require.NoError(t, err)
|
||
assert.Len(t, mdls, 1, "device save 失敗不應中斷 model 建立")
|
||
}
|
||
|
||
// failingDeviceRepo 是 device.Save 永遠回錯的 device.Repository(其餘方法委派給空行為)。
|
||
type failingDeviceRepo struct{}
|
||
|
||
func (failingDeviceRepo) Get(ctx context.Context, id string) (*device.Device, error) {
|
||
return nil, device.ErrNotFound
|
||
}
|
||
func (failingDeviceRepo) GetBySerial(ctx context.Context, ownerUserID, serial string) (*device.Device, error) {
|
||
return nil, device.ErrNotFound
|
||
}
|
||
func (failingDeviceRepo) List(ctx context.Context, ownerUserID string) ([]*device.Device, error) {
|
||
return nil, nil
|
||
}
|
||
func (failingDeviceRepo) Save(ctx context.Context, dev *device.Device) error {
|
||
return assert.AnError
|
||
}
|
||
func (failingDeviceRepo) Delete(ctx context.Context, id string) error { return nil }
|