從 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>
146 lines
4.9 KiB
Go
146 lines
4.9 KiB
Go
package session
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/stretchr/testify/assert"
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
// TestHTTPProxyClient_GetSession_OK 驗證能正確解析 remote-proxy 的
|
||
// /internal/session/:token 回應 → Summary。
|
||
func TestHTTPProxyClient_GetSession_OK(t *testing.T) {
|
||
now := time.Now().UTC().Truncate(time.Second)
|
||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
assert.Equal(t, "/internal/session/vAc_abc", r.URL.Path)
|
||
w.Header().Set("Content-Type", "application/json")
|
||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||
"token": "vAc_abc",
|
||
"connected": true,
|
||
"connected_at": now,
|
||
"last_heartbeat": now,
|
||
"remote_addr": "1.2.3.4:5678",
|
||
"user_id": "demo-user",
|
||
"device_id": "dev-1",
|
||
})
|
||
}))
|
||
defer ts.Close()
|
||
|
||
c := NewHTTPProxyClient(ts.URL, nil)
|
||
sum, err := c.GetSession(context.Background(), "vAc_abc")
|
||
require.NoError(t, err)
|
||
require.NotNil(t, sum)
|
||
assert.Equal(t, "vAc_abc", sum.Token)
|
||
assert.Equal(t, "demo-user", sum.UserID)
|
||
assert.Equal(t, "dev-1", sum.DeviceID)
|
||
assert.Equal(t, "1.2.3.4:5678", sum.RemoteAddr)
|
||
assert.True(t, sum.LastHeartbeat.Equal(now))
|
||
}
|
||
|
||
// TestHTTPProxyClient_GetSession_NotFound 驗證 404 → ErrSessionNotFound。
|
||
func TestHTTPProxyClient_GetSession_NotFound(t *testing.T) {
|
||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
http.Error(w, `{"error":"NOT_FOUND"}`, http.StatusNotFound)
|
||
}))
|
||
defer ts.Close()
|
||
|
||
c := NewHTTPProxyClient(ts.URL, nil)
|
||
_, err := c.GetSession(context.Background(), "vAc_xxx")
|
||
assert.ErrorIs(t, err, ErrSessionNotFound)
|
||
}
|
||
|
||
// TestHTTPProxyClient_GetSession_ConnectedFalse_TreatedAsNotFound
|
||
// 驗證 remote-proxy 回 connected=false(session 已被排隊清除)→ NotFound。
|
||
func TestHTTPProxyClient_GetSession_ConnectedFalse_TreatedAsNotFound(t *testing.T) {
|
||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||
"token": "vAc_dead",
|
||
"connected": false,
|
||
})
|
||
}))
|
||
defer ts.Close()
|
||
|
||
c := NewHTTPProxyClient(ts.URL, nil)
|
||
_, err := c.GetSession(context.Background(), "vAc_dead")
|
||
assert.ErrorIs(t, err, ErrSessionNotFound)
|
||
}
|
||
|
||
// TestHTTPProxyClient_GetSession_EmptyToken 驗證空 token 直接被本地拒絕。
|
||
func TestHTTPProxyClient_GetSession_EmptyToken(t *testing.T) {
|
||
c := NewHTTPProxyClient("http://localhost:9999", nil)
|
||
_, err := c.GetSession(context.Background(), "")
|
||
assert.Error(t, err)
|
||
}
|
||
|
||
// TestHTTPProxyClient_ListSessions_OK 驗證能正確 parse sessions array。
|
||
func TestHTTPProxyClient_ListSessions_OK(t *testing.T) {
|
||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
assert.Equal(t, "/internal/sessions", r.URL.Path)
|
||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||
"sessions": []map[string]any{
|
||
{"token": "vAc_a", "userId": "u1"},
|
||
{"token": "vAc_b", "userId": "u2"},
|
||
},
|
||
"total": 2,
|
||
})
|
||
}))
|
||
defer ts.Close()
|
||
|
||
c := NewHTTPProxyClient(ts.URL, nil)
|
||
sums, err := c.ListSessions(context.Background())
|
||
require.NoError(t, err)
|
||
require.Len(t, sums, 2)
|
||
assert.Equal(t, "vAc_a", sums[0].Token)
|
||
}
|
||
|
||
// TestHTTPProxyClient_ListSessions_Empty 驗證空 sessions 回 non-nil empty slice。
|
||
func TestHTTPProxyClient_ListSessions_Empty(t *testing.T) {
|
||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
_ = json.NewEncoder(w).Encode(map[string]any{"sessions": nil, "total": 0})
|
||
}))
|
||
defer ts.Close()
|
||
|
||
c := NewHTTPProxyClient(ts.URL, nil)
|
||
sums, err := c.ListSessions(context.Background())
|
||
require.NoError(t, err)
|
||
assert.NotNil(t, sums)
|
||
assert.Empty(t, sums)
|
||
}
|
||
|
||
// TestHTTPProxyClient_CloseSession_OK 驗證 200 → nil error。
|
||
func TestHTTPProxyClient_CloseSession_OK(t *testing.T) {
|
||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
assert.Equal(t, http.MethodPost, r.Method)
|
||
assert.Equal(t, "/internal/session/vAc_x/close", r.URL.Path)
|
||
_, _ = w.Write([]byte(`{"closed":true}`))
|
||
}))
|
||
defer ts.Close()
|
||
|
||
c := NewHTTPProxyClient(ts.URL, nil)
|
||
err := c.CloseSession(context.Background(), "vAc_x")
|
||
assert.NoError(t, err)
|
||
}
|
||
|
||
// TestHTTPProxyClient_CloseSession_NotFound 驗證 404 → ErrSessionNotFound。
|
||
func TestHTTPProxyClient_CloseSession_NotFound(t *testing.T) {
|
||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
w.WriteHeader(http.StatusNotFound)
|
||
}))
|
||
defer ts.Close()
|
||
|
||
c := NewHTTPProxyClient(ts.URL, nil)
|
||
err := c.CloseSession(context.Background(), "vAc_x")
|
||
assert.ErrorIs(t, err, ErrSessionNotFound)
|
||
}
|
||
|
||
// TestHTTPProxyClient_BaseURL_TrimsTrailingSlash 驗證 baseURL 結尾的 / 會被移除。
|
||
func TestHTTPProxyClient_BaseURL_TrimsTrailingSlash(t *testing.T) {
|
||
c := NewHTTPProxyClient("http://localhost:3801/", nil)
|
||
assert.Equal(t, "http://localhost:3801", c.BaseURL())
|
||
}
|