jim800121chen cabbdde495 feat(visionA-backend): DB 接入後續 — OIDC/pairing FK 收尾 + B4 metadata + nginx healthz + 補測試
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>
2026-06-21 06:36:35 +08:00

166 lines
6.3 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.

// seed_test.go — seedDemoData 的 in-memorydbBacked=falseunit test。
//
// Owner: testing agent補測任務 — cmd/api-server 可測部分)
//
// 對象seedDemoData。in-memory 分支不需 DB純 unit。DB-backeddbBacked=true分支
// 需真 Postgres → 拆到 seed_db_test.go//go:build dbtest130 docker 跑)。
//
// 行為對齊 seed.go docstring
// - dbBacked=false 用傳入 userIDStaticUserID 概念)當 ownermodel/device id 帶隨機後綴。
// - 不 upsert demo userin-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(),
}
}
// happyin-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()
// deviceowner = 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)
// modelowner = uidStorageKey 用 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 應設 UploadedAtready")
// pairing tokenowner = 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不需 FKuserStore 應維持空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-ishseedDemoData 每次呼叫產生獨立資料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 個 deviceid 帶隨機後綴、不去重)")
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 仍應回 nilfail-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 }