從 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>
91 lines
3.4 KiB
Go
91 lines
3.4 KiB
Go
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 涵蓋)
|
||
}
|