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>
152 lines
5.3 KiB
Go
152 lines
5.3 KiB
Go
//go:build dbtest
|
||
|
||
// redis_integration_test.go — RedisClient 對「真 Redis」的整合測試。
|
||
//
|
||
// owner: testing agent(internal/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.go(miniredis,預設可跑)互補: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() 可實際操作真 Redis(SET/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 隔離)")
|
||
}
|