jim800121chen 22f0837ba8 feat(visionA-backend): Phase 0 → 0.7 雲端後端(雙 binary + OIDC BFF + stage 部署)
從 edge-ai-platform POC 轉為正式產品的雲端後端,含以下整合階段:

- Phase 0:雛形骨架 — `cmd/api-server` (REST :3721) + `cmd/remote-proxy`
  (tunnel :3800 / internal :3801) 雙 binary 共用 internal/,沿用 POC 的
  WebSocket+yamux tunnel 協定但解耦 relay 與 API
- Phase 0.6:OIDC BFF 接 Innovedus Member Center
  - internal/oidc package(coreos/go-oidc + PKCE S256 + state + nonce)
  - internal/usersession package(HMAC-SHA256 cookie + RotateSessionID
    防 session fixation, OWASP ASVS V3.2.1)
  - 4 個 OIDC handler(/api/auth/login|callback|me|logout)+ AuthMiddleware
  - 完全拔除 StaticAuthProvider,OIDC 是唯一認證路徑
  - 9 個 ADR(含 ADR-010 BFF / ADR-011 取代 static auth /
    ADR-012 pending session shared cookie / ADR-013 PKCE-only public client)
- Phase 0.7:A1 改造 + security audit 修復
  - OIDC ClientSecret 變選填,支援 stage MC 的 public PKCE-only client
    (AuthStyleInParams 強制 token endpoint 不送 client_secret)
  - 預留 ServiceClient* 欄位給未來 client_credentials grant
  - 移除 13+ 處 resolveUserID(uc, StaticUserID) fallback 改 strict mode
    (Audit C1:multi-tenant 隔離破口)
  - Pairing exchange MarkUsed 失敗 abort + revoke session token(Audit M3)
  - 新增 all_endpoints_require_auth_test 整合測試(51 endpoint × 401)

驗證:go test -race -count=3 ./... 17 packages 全綠 / go vet 0 warning

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 11:21:20 +08:00

91 lines
3.4 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.

package session
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
// TestForwarder_OpenStream_NoProxyHost 驗證 baseURL 為空時直接拒絕。
func TestForwarder_OpenStream_NoProxyHost(t *testing.T) {
f := NewForwarder("", nil)
_, err := f.OpenStream(context.Background(), "vAc_x")
assert.Error(t, err)
}
// TestForwarder_OpenStream_EmptyToken 驗證空 token 拒絕。
func TestForwarder_OpenStream_EmptyToken(t *testing.T) {
f := NewForwarder("http://localhost:9999", nil)
_, err := f.OpenStream(context.Background(), "")
assert.Error(t, err)
}
// TestForwarder_ForwardWebSocket_NotImplemented 驗證 ForwardWebSocket 仍是 stub。
func TestForwarder_ForwardWebSocket_NotImplemented(t *testing.T) {
f := NewForwarder("http://localhost:9999", nil)
req, _ := http.NewRequest(http.MethodGet, "/ws", nil)
_, err := f.ForwardWebSocket(context.Background(), "vAc_x", req)
assert.Error(t, err)
}
// TestForwarder_OpenStream_502_TreatedAsNotFound 驗證當 remote-proxy 回 502
// session 不存在時的雛形行為)→ 包裝成 ErrSessionNotFound。
//
// 用 httptest 起一個假的 internal endpoint回 502 JSON。
func TestForwarder_OpenStream_502_TreatedAsNotFound(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadGateway)
_, _ = w.Write([]byte(`{"error":{"code":"TUNNEL_DISCONNECTED","message":"session not connected"}}`))
}))
defer ts.Close()
f := NewForwarder(ts.URL, nil)
_, err := f.OpenStream(context.Background(), "vAc_dead")
if !errors.Is(err, ErrSessionNotFound) {
t.Fatalf("expected ErrSessionNotFound, got %v", err)
}
}
// TestForwarder_OpenStream_HandshakeRead 驗證能正確讀「HTTP/1.1 200 Connected\r\n\r\n」
// 握手;用一個假 server 回正確握手後立刻 close — 期望我們的 OpenStream 成功,
// 後續 Read 拿 EOF這對 forwarder 而言是合法情境,由 caller 處理)。
//
// 此 case 直接驗證 happy-path 握手解析;真正的端對端轉發由 integration test 涵蓋。
func TestForwarder_OpenStream_HandshakeRead(t *testing.T) {
// 為了保證 server 端在 200 Connected 後不再寫 body讓 forwarder 結束 header 讀
// 不被預讀干擾),用一個 raw TCP listener 而非 httptest.NewServer。
// 但 raw listener 會增加測試複雜度;在 unit test 用 httptest 已足以驗證
// 「能 parse 200 Connected + 兩個 \r\n」的路徑——讀 body 結束會回 EOF
// 後續 caller 用該 conn 才會發現問題,這裡僅驗證 OpenStream 不 error。
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 不能直接寫 raw "HTTP/1.1 200 Connected\r\n\r\n" — httptest 會額外加
// content-length 等 header。改用 hijack 模擬真實 raw forward 行為。
hj, ok := w.(http.Hijacker)
if !ok {
http.Error(w, "no hijacker", 500)
return
}
conn, _, err := hj.Hijack()
if err != nil {
return
}
defer conn.Close()
_, _ = conn.Write([]byte("HTTP/1.1 200 Connected\r\n\r\n"))
// 不再寫;讓 forwarder 拿到 conn 後若 read 會 EOF
}))
defer ts.Close()
f := NewForwarder(ts.URL, nil)
conn, err := f.OpenStream(context.Background(), "vAc_x")
if err != nil {
t.Fatalf("OpenStream should succeed: %v", err)
}
defer conn.Close()
// 不再做 read 驗證(行為由 integration test 涵蓋)
}