visionA/visionA-backend/internal/user/postgres_store_db_test.go
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

139 lines
5.0 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.

//go:build dbtest
// PostgresStoreuser的真 DB 整合測試DB-on FK 收尾,問題 #1
//
// build tag `dbtest`:只在帶 `-tags=dbtest` 時編譯/執行(需要 Docker / testcontainers
// 預設 `go test ./...`(無 Docker不會觸碰本檔維持綠燈。
//
// 執行:
//
// go test -tags=dbtest ./internal/user/...
// # 無本機 Docker 時Orchestrator 在 130 補跑:
// DOCKER_HOST=tcp://192.168.0.130:2375 TESTCONTAINERS_RYUK_DISABLED=true \
// go test -tags=dbtest ./internal/user/...
//
// 涵蓋:
// - Upsert 新建 + Get round-tripid=sub UUID、email、name、roles
// - Upsert 同 id 再寫:更新 email/name/roles、保留 created_at
// - email 大小寫不敏感唯一uq_users_email_lower兩個不同 sub 共用同 email差大小寫→ 撞 unique
// - Get 不存在 → ErrNotFound
// - roles nil → 寫空陣列NOT NULL DEFAULT '{}'round-trip 回空 slice
package user
import (
"context"
"testing"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgconn"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"visiona-backend/internal/db/testsupport"
)
// newPGUserStore 啟動一次性測試 DB、truncate users回傳 store。
func newPGUserStore(t *testing.T) (*PostgresStore, *testsupport.TestDB) {
t.Helper()
tdb := testsupport.SetupTestDB(t)
tdb.Truncate(t, "users")
return NewPostgresStore(tdb.Pool), tdb
}
func TestPGUser_UpsertAndGet(t *testing.T) {
ctx := context.Background()
s, _ := newPGUserStore(t)
id := uuid.NewString() // 模擬 OIDC subUUID 格式D1-B
in := &User{ID: id, Email: "Alice@Example.com", Name: "Alice", Roles: []string{"admin", "user"}}
require.NoError(t, s.Upsert(ctx, in))
got, err := s.Get(ctx, id)
require.NoError(t, err)
assert.Equal(t, id, got.ID)
assert.Equal(t, "Alice@Example.com", got.Email)
assert.Equal(t, "Alice", got.Name)
assert.Equal(t, []string{"admin", "user"}, got.Roles)
assert.False(t, got.CreatedAt.IsZero())
assert.False(t, got.UpdatedAt.IsZero())
}
func TestPGUser_Upsert_UpdatesAndPreservesCreatedAt(t *testing.T) {
ctx := context.Background()
s, _ := newPGUserStore(t)
id := uuid.NewString()
require.NoError(t, s.Upsert(ctx, &User{ID: id, Email: "old@example.com", Name: "Old"}))
first, err := s.Get(ctx, id)
require.NoError(t, err)
origCreated := first.CreatedAt
require.NoError(t, s.Upsert(ctx, &User{ID: id, Email: "new@example.com", Name: "New", Roles: []string{"r"}}))
second, err := s.Get(ctx, id)
require.NoError(t, err)
assert.Equal(t, "new@example.com", second.Email, "email 應更新")
assert.Equal(t, "New", second.Name, "name 應更新")
assert.Equal(t, []string{"r"}, second.Roles, "roles 應更新")
assert.Equal(t, origCreated, second.CreatedAt, "created_at 應保留ON CONFLICT 不動 created_at")
assert.True(t, second.UpdatedAt.After(origCreated) || second.UpdatedAt.Equal(origCreated),
"updated_at 應 >= created_at")
}
// TestPGUser_Upsert_EmailUniqueCaseInsensitive 驗證 uq_users_email_lower
// 兩個不同 sub 共用同 email差大小寫→ 第二筆撞 lower(email) unique23505
//
// 這是 D1-B 下的已知資料異常邊界(雛形原樣回錯、不靜默吞)。
func TestPGUser_Upsert_EmailUniqueCaseInsensitive(t *testing.T) {
ctx := context.Background()
s, _ := newPGUserStore(t)
require.NoError(t, s.Upsert(ctx, &User{ID: uuid.NewString(), Email: "dup@example.com"}))
err := s.Upsert(ctx, &User{ID: uuid.NewString(), Email: "DUP@example.com"})
require.Error(t, err, "不同 sub 共用同 email差大小寫應撞 uq_users_email_lower")
var pgErr *pgconn.PgError
if assert.ErrorAs(t, err, &pgErr) {
assert.Equal(t, "23505", pgErr.Code, "應為 unique_violation")
}
}
func TestPGUser_Get_NotFound(t *testing.T) {
ctx := context.Background()
s, _ := newPGUserStore(t)
_, err := s.Get(ctx, uuid.NewString())
assert.ErrorIs(t, err, ErrNotFound)
}
// TestPGUser_Upsert_NilRolesEmptyArray 驗證 roles nil → 寫空陣列 round-trip 回空/ nil。
func TestPGUser_Upsert_NilRolesEmptyArray(t *testing.T) {
ctx := context.Background()
s, _ := newPGUserStore(t)
id := uuid.NewString()
require.NoError(t, s.Upsert(ctx, &User{ID: id, Email: "noroles@example.com"}))
got, err := s.Get(ctx, id)
require.NoError(t, err)
assert.Empty(t, got.Roles, "roles 預設應為空NOT NULL DEFAULT '{}'")
}
// TestPGUser_Upsert_NameNullable 驗證 name 空字串 → 寫 NULL → 掃回空字串。
func TestPGUser_Upsert_NameNullable(t *testing.T) {
ctx := context.Background()
s, tdb := newPGUserStore(t)
id := uuid.NewString()
require.NoError(t, s.Upsert(ctx, &User{ID: id, Email: "noname@example.com"}))
// 直接查 DB 確認 name 欄位是 NULL非空字串
var nameIsNull bool
require.NoError(t, tdb.Pool.QueryRow(ctx,
`SELECT name IS NULL FROM users WHERE id = $1`, id).Scan(&nameIsNull))
assert.True(t, nameIsNull, "空 name 應寫成 NULL")
got, err := s.Get(ctx, id)
require.NoError(t, err)
assert.Equal(t, "", got.Name, "NULL name 應掃回空字串")
}