visionA/visionA-backend/internal/relay/local_handle_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

165 lines
5.4 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.

package relay
import (
"context"
"net"
"testing"
"time"
"github.com/hashicorp/yamux"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"visiona-backend/internal/session"
)
// owner: testing agentinternal/relay local_handle 補測)
//
// 測試策略LocalHandle 包住 *yamux.Session。我們不需要真實 WebSocket / 真實 tunnel —
// yamux 可直接跑在 net.Pipe() 之上in-memory、純 Go、不需 docker
// 這已足以驗證 LocalHandle 的「session 開 stream / 關閉 / IsClosed」邏輯。
// 真正端對端的 yamux-over-WebSocket 由 relay 的 integration_test.go 涵蓋。
// newYamuxPair 用 net.Pipe 建一對 yamux sessionserver 給 LocalHandleclient 端配合)。
// 回傳 server sessionLocalHandle 用)+ client session驅動 Accept避免 Open 卡住)。
func newYamuxPair(t *testing.T) (server, client *yamux.Session) {
t.Helper()
c1, c2 := net.Pipe()
srv, err := yamux.Server(c1, yamux.DefaultConfig())
require.NoError(t, err, "yamux.Server")
cli, err := yamux.Client(c2, yamux.DefaultConfig())
require.NoError(t, err, "yamux.Client")
t.Cleanup(func() {
_ = srv.Close()
_ = cli.Close()
_ = c1.Close()
_ = c2.Close()
})
return srv, cli
}
// ---------------------------------------------------------------------------
// Summary / RecordHeartbeat — 純邏輯mutex-protected不碰 yamux
// ---------------------------------------------------------------------------
func TestLocalHandle_Summary_ReflectsConstructorArgs(t *testing.T) {
srv, _ := newYamuxPair(t)
h := NewLocalHandle(srv, "vAc_tok", "203.0.113.7:54321")
sum := h.Summary()
require.NotNil(t, sum)
assert.Equal(t, "vAc_tok", sum.Token)
assert.Equal(t, "203.0.113.7:54321", sum.RemoteAddr)
assert.False(t, sum.ConnectedAt.IsZero(), "ConnectedAt 應已設定")
assert.False(t, sum.LastHeartbeat.IsZero(), "LastHeartbeat 應已設定")
// ConnectedAt 與 LastHeartbeat 初始相同
assert.Equal(t, sum.ConnectedAt, sum.LastHeartbeat)
}
func TestLocalHandle_Summary_ReturnsCopy(t *testing.T) {
srv, _ := newYamuxPair(t)
h := NewLocalHandle(srv, "vAc_tok", "addr")
// caller 改 snapshot 不應影響內部狀態(防中間態觀察 / 並發寫)。
s1 := h.Summary()
s1.Token = "MUTATED"
s1.LastHeartbeat = time.Unix(0, 0)
s2 := h.Summary()
assert.Equal(t, "vAc_tok", s2.Token, "Summary() 必須回副本caller 修改不得污染內部")
assert.NotEqual(t, time.Unix(0, 0), s2.LastHeartbeat)
}
func TestLocalHandle_RecordHeartbeat_UpdatesLastHeartbeat(t *testing.T) {
srv, _ := newYamuxPair(t)
h := NewLocalHandle(srv, "vAc_tok", "addr")
before := h.Summary().LastHeartbeat
newTime := before.Add(30 * time.Second)
h.RecordHeartbeat(newTime)
after := h.Summary().LastHeartbeat
assert.True(t, after.Equal(newTime), "RecordHeartbeat 應更新 LastHeartbeatwant %v got %v", newTime, after)
// ConnectedAt 不應被動到
assert.Equal(t, before, h.Summary().ConnectedAt)
}
// ---------------------------------------------------------------------------
// OpenStream / IsClosed / Close — 跑在 net.Pipe 上的真 yamuxin-memory
// ---------------------------------------------------------------------------
func TestLocalHandle_OpenStream_Succeeds(t *testing.T) {
srv, cli := newYamuxPair(t)
h := NewLocalHandle(srv, "vAc_tok", "addr")
// client 端需要 Accept 才能讓 server 端 Open 完成 handshake。
acceptDone := make(chan net.Conn, 1)
go func() {
stream, err := cli.Accept()
if err == nil {
acceptDone <- stream
} else {
acceptDone <- nil
}
}()
conn, err := h.OpenStream(context.Background())
require.NoError(t, err, "session 健在時 OpenStream 應成功")
require.NotNil(t, conn)
t.Cleanup(func() { _ = conn.Close() })
select {
case s := <-acceptDone:
require.NotNil(t, s, "client 端應 Accept 到對應 stream")
_ = s.Close()
case <-time.After(2 * time.Second):
t.Fatal("client 端未在 2s 內 Accept 到 stream")
}
}
func TestLocalHandle_IsClosed_FalseWhenLive(t *testing.T) {
srv, _ := newYamuxPair(t)
h := NewLocalHandle(srv, "vAc_tok", "addr")
assert.False(t, h.IsClosed(), "活的 session IsClosed 應為 false")
}
func TestLocalHandle_Close_MarksClosed(t *testing.T) {
srv, _ := newYamuxPair(t)
h := NewLocalHandle(srv, "vAc_tok", "addr")
require.NoError(t, h.Close())
assert.True(t, h.IsClosed(), "Close 後 IsClosed 應為 true")
}
// TestLocalHandle_OpenStream_AfterClose 驗證 close 後 OpenStream 回 ErrSessionClosed。
func TestLocalHandle_OpenStream_AfterClose(t *testing.T) {
srv, _ := newYamuxPair(t)
h := NewLocalHandle(srv, "vAc_tok", "addr")
require.NoError(t, h.Close())
_, err := h.OpenStream(context.Background())
assert.ErrorIs(t, err, session.ErrSessionClosed, "已關閉的 session OpenStream 應回 ErrSessionClosed")
}
// TestLocalHandle_OpenStream_CtxCancelled 驗證 ctx 已取消時 OpenStream 早退回 ctx.Err()。
func TestLocalHandle_OpenStream_CtxCancelled(t *testing.T) {
srv, _ := newYamuxPair(t)
h := NewLocalHandle(srv, "vAc_tok", "addr")
ctx, cancel := context.WithCancel(context.Background())
cancel() // 立即取消
_, err := h.OpenStream(ctx)
require.Error(t, err)
assert.ErrorIs(t, err, context.Canceled, "ctx 取消時 OpenStream 應回 ctx.Err()")
}
// TestLocalHandle_ImplementsHandle 編譯期 + 執行期確認介面一致。
func TestLocalHandle_ImplementsHandle(t *testing.T) {
srv, _ := newYamuxPair(t)
var _ session.Handle = NewLocalHandle(srv, "t", "a")
}