visionA/visionA-backend/internal/db/redis_integration_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

152 lines
5.3 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.

//go:build dbtest
// redis_integration_test.go — RedisClient 對「真 Redis」的整合測試。
//
// owner: testing agentinternal/db redis 補測)
//
// build tag `dbtest`:預設 `go test ./...` 不編譯本檔。
// 執行方式(二擇一,對齊 internal/usersession/redis_integration_test.go 既有 pattern
//
// 1. 連既有 Redis例如 130 上的 visiona-redis
// VISIONA_TEST_REDIS_ADDR=visiona-redis:6379 go test -tags=dbtest ./internal/db/...
//
// 2. testcontainers 自動起一次性 Redis需 Docker daemon本機無 docker 時用
// DOCKER_HOST=tcp://192.168.0.130:2375 TESTCONTAINERS_RYUK_DISABLED=true 指向 130
// go test -tags=dbtest ./internal/db/...
//
// 本檔與 redis_test.gominiredis預設可跑互補miniredis 驗連線/ping/close 程式邏輯,
// 本檔驗「NewRedisClient 對真 Redis 的 ping / graceful close / Client() 真的可用」。
package db
import (
"context"
"net"
"os"
"strconv"
"testing"
"time"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
"visiona-backend/internal/config"
)
// realRedisAddr 回傳一個可連的真 Redis host:port
// - 若設了 VISIONA_TEST_REDIS_ADDR → 直接用130 補跑路徑)。
// - 否則 → testcontainers 起一次性 redis:7-alpine。
func realRedisAddr(t *testing.T) string {
t.Helper()
if addr := os.Getenv("VISIONA_TEST_REDIS_ADDR"); addr != "" {
return addr
}
ctx := context.Background()
req := testcontainers.ContainerRequest{
Image: "redis:7-alpine",
ExposedPorts: []string{"6379/tcp"},
WaitingFor: wait.ForListeningPort("6379/tcp").WithStartupTimeout(60 * time.Second),
}
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
if err != nil {
t.Fatalf("start redis container: %v", err)
}
t.Cleanup(func() { _ = container.Terminate(ctx) })
host, err := container.Host(ctx)
if err != nil {
t.Fatalf("container host: %v", err)
}
port, err := container.MappedPort(ctx, "6379/tcp")
if err != nil {
t.Fatalf("container port: %v", err)
}
return host + ":" + port.Port()
}
// realRedisConfig 把 host:port 拆回 RedisConfig 餵 NewRedisClient。
func realRedisConfig(t *testing.T, addr string) config.RedisConfig {
t.Helper()
host, portStr, err := net.SplitHostPort(addr)
require.NoError(t, err, "解析 real redis addr=%s", addr)
port, err := strconv.Atoi(portStr)
require.NoError(t, err)
return config.RedisConfig{
Host: host,
Port: port,
Password: os.Getenv("VISIONA_TEST_REDIS_PASSWORD"),
DB: 0,
ConnTimeout: 5 * time.Second,
}
}
// TestRealRedis_NewClientConnectsAndPings 驗證 NewRedisClient 對真 Redis
// 建連成功、啟動 ping 通過、Client() 可實際操作、Ping() 可重複呼叫。
func TestRealRedis_NewClientConnectsAndPings(t *testing.T) {
cfg := realRedisConfig(t, realRedisAddr(t))
rc, err := NewRedisClient(context.Background(), cfg, nil)
require.NoError(t, err, "真 Redis 可達時應成功建連")
require.NotNil(t, rc)
t.Cleanup(rc.Close)
// Client() 可實際操作真 RedisSET/GET round-trip
ctx := context.Background()
require.NoError(t, rc.Client().Set(ctx, "db_redis_it_key", "v1", time.Minute).Err())
got, err := rc.Client().Get(ctx, "db_redis_it_key").Result()
require.NoError(t, err)
assert.Equal(t, "v1", got)
t.Cleanup(func() { _ = rc.Client().Del(context.Background(), "db_redis_it_key").Err() })
// Ping 可重複呼叫health check 場景)。
require.NoError(t, rc.Ping(ctx))
require.NoError(t, rc.Ping(ctx))
}
// TestRealRedis_GracefulClose 驗證 Close 後底層 client 不可再用graceful close 生效)。
func TestRealRedis_GracefulClose(t *testing.T) {
cfg := realRedisConfig(t, realRedisAddr(t))
rc, err := NewRedisClient(context.Background(), cfg, nil)
require.NoError(t, err)
rc.Close()
// Close 後 Ping 應失敗go-redis 回 "client is closed")。
assert.Error(t, rc.Ping(context.Background()), "graceful close 後 Ping 應失敗")
}
// TestRealRedis_WrongDBStillConnects 驗證非 0 的 DB index 也能正常建連DB 欄位有被套用)。
func TestRealRedis_WrongDBStillConnects(t *testing.T) {
cfg := realRedisConfig(t, realRedisAddr(t))
cfg.DB = 1 // 切到 db1
rc, err := NewRedisClient(context.Background(), cfg, nil)
require.NoError(t, err)
t.Cleanup(rc.Close)
// 在 db1 寫一筆,確認 SELECT 1 真的生效(不會誤落 db0
ctx := context.Background()
require.NoError(t, rc.Client().Set(ctx, "db1_only_key", "x", time.Minute).Err())
n, err := rc.Client().Exists(ctx, "db1_only_key").Result()
require.NoError(t, err)
assert.Equal(t, int64(1), n)
t.Cleanup(func() { _ = rc.Client().Del(context.Background(), "db1_only_key").Err() })
// 另開一個 db0 client確認 db1 的 key 不可見DB 隔離正確)。
db0 := redis.NewClient(&redis.Options{
Addr: net.JoinHostPort(cfg.Host, strconv.Itoa(cfg.Port)),
Password: cfg.Password,
DB: 0,
})
t.Cleanup(func() { _ = db0.Close() })
n0, err := db0.Exists(ctx, "db1_only_key").Result()
require.NoError(t, err)
assert.Equal(t, int64(0), n0, "db1 的 key 不應在 db0 可見DB 隔離)")
}