visionA/visionA-backend/cmd/api-server/dbon_fk_fix_test.go
jim800121chen 59c57fa481 feat(device): WP-B repository 接 agents 模型 + exchange 重塑(A' 走向第二階段 Go 層)
- Device struct 加 4 欄(agent_id/agent_local_device_id/registered_at/
  is_representative)+ deviceColumns 13→17 + scanDevice/SaveTx 讀寫新欄
- 新增 internal/agent package(domain + interface + in-memory + PG repo):
  GetOrCreateAgentTx/GetByOwnerTx,advisory lock 序列化同 owner get-or-create
- exchange 重塑:建/復用 agent → representative device(綁 session_tokens、
  serial=NULL)→ loop 建 N 顆真 USB device(R1 完整 N 顆非只第一顆)
- List filter is_representative=false + DeviceListItem 回傳 agent_id/registered_at
- 併入 WP-0/0005 follow-up Minor:Mi#2 lost-update 收斂(tx 內查詢+局部更新)
  / Mi#3 過時註解 / Mi#4 空 serial 回 ErrNotFound / Mi#5 serial 白名單
  ^0x[0-9A-Fa-f]{8}$ + 去重 / S-1 firmware forward-compat / S-2 device Name 衍生

守 ADR-018 A'(一 owner N agents、session_tokens FK 物理不動、不加 owner
unique 為多機器留路)。Reviewer 通過(0C/0M)。5 套件 dbtest 130 全綠
(db 19/device 37/agent 13/api 172/cmd 60)、build/vet/test 綠。

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

103 lines
4.6 KiB
Go
Raw Permalink 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.

// dbon_fk_fix_test.go — DB-on FK 收尾兩個問題的 integration 驗證in-memory wiring
//
// 不帶 build tag屬於預設 `go test ./...` 範圍。用 fixture 的 in-memory store + in-memory
// exchanger 走真實 OIDC login flow / pairing exchange flow驗證
// - 問題 #1OIDC callback 成功後 user 被 provision 進 UserStore。
// - 問題 #2pairing exchange 成功後自建一筆 device且該 device owner = 登入 user。
//
// DB-on 行為(真 FK / 交易)由 internal/user/postgres_store_db_test.go 與
// internal/api/pairing_exchange_db_test.go 的 dbtest 覆蓋(待 130 補跑)。
package main
import (
"bytes"
"context"
"encoding/json"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"visiona-backend/internal/api"
"visiona-backend/internal/auth"
)
// TestDBOnFix_OIDCCallbackProvisionsUser 驗證問題 #1OIDC callback 成功後 user 進 UserStore。
func TestDBOnFix_OIDCCallbackProvisionsUser(t *testing.T) {
f := setupFixture(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
defer f.Close()
// 登入前user 不存在
_, err := f.userStore.Get(context.Background(), "alice-sub")
require.Error(t, err, "登入前 user 不應存在")
// 走完整 OIDC login flowcallback 會 Upsert user
_ = f.AuthenticatedClient(t, "alice-sub", "alice@example.com")
// 登入後user 已被 provision
got, err := f.userStore.Get(context.Background(), "alice-sub")
require.NoError(t, err, "OIDC callback 後 user 應被 provision 進 UserStore")
assert.Equal(t, "alice-sub", got.ID)
assert.Equal(t, "alice@example.com", got.Email)
}
// TestDBOnFix_ExchangeProvisionsDevice 驗證問題 #2 + A' 重塑WP-B B3/B4pairing exchange
// 後建 agent 的 representative device 供 session token 綁定。
//
// A' 語意變更exchange 無 USB 上報時只建 representative deviceis_representative=true
// 而 ListB4會 filter 掉 representative只列真 USB——故 exchange 前後 List 都為空。
// 「device 有被建」改由「representative device 存在且綁到 session」驗證。
func TestDBOnFix_ExchangeProvisionsDevice(t *testing.T) {
f := setupFixture(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
defer f.Close()
const sub = "bob-sub"
client := f.AuthenticatedClient(t, sub, "bob@example.com")
// 登入後但 exchange 前:該 user 名下無(真 USBdevice
before, err := f.deviceRepo.List(context.Background(), sub)
require.NoError(t, err)
require.Empty(t, before, "exchange 前不應有 device")
// 1) 產 pairing token
tokResp, err := client.Post(f.apiServer.URL+"/api/pairing/token", "", nil)
require.NoError(t, err)
defer tokResp.Body.Close()
require.Equal(t, http.StatusOK, tokResp.StatusCode)
var tokBody map[string]any
require.NoError(t, json.NewDecoder(tokResp.Body).Decode(&tokBody))
pairingTok := tokBody["data"].(map[string]any)["token"].(string)
// 2) exchange不走 AuthMiddleware→ 建 agent + representative device + session token
reqBody, _ := json.Marshal(api.PairingExchangeRequest{PairingToken: pairingTok})
exchResp, err := http.Post(f.apiServer.URL+"/api/pairing/exchange",
"application/json", bytes.NewReader(reqBody))
require.NoError(t, err)
defer exchResp.Body.Close()
require.Equal(t, http.StatusOK, exchResp.StatusCode)
var exchBody map[string]any
require.NoError(t, json.NewDecoder(exchResp.Body).Decode(&exchBody))
sessionTok := exchBody["data"].(map[string]any)["session_token"].(string)
require.True(t, auth.IsValidSessionToken(sessionTok))
// 3) exchange 後List真 USB仍為空——A' 下無 USB 上報只建 representativeB4 filter
after, err := f.deviceRepo.List(context.Background(), sub)
require.NoError(t, err)
require.Empty(t, after, "無 USB 上報 → List真 USB仍為空representative 被 filter")
// 4) session token 綁到一顆存在的 representative deviceis_representative=true、owner 對齊)。
tok, err := f.sessionTokenStore.Get(context.Background(), sessionTok)
require.NoError(t, err)
require.NotEmpty(t, tok.DeviceID, "session token 應綁一個 device")
rep, err := f.deviceRepo.Get(context.Background(), tok.DeviceID)
require.NoError(t, err, "session 綁的 representative device 應存在")
assert.True(t, rep.IsRepresentative, "exchange 建的應為 representative device")
assert.Equal(t, sub, rep.OwnerUserID, "device owner 應為登入 user")
assert.NotNil(t, rep.PairedAt, "representative device 應設 paired_at")
}