從 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>
181 lines
5.9 KiB
Go
181 lines
5.9 KiB
Go
package session
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/stretchr/testify/assert"
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
// fakeProxyClient 是測試用 ProxyClient mock;以 lambda 注入行為。
|
||
type fakeProxyClient struct {
|
||
getSessionFn func(ctx context.Context, token string) (*Summary, error)
|
||
listFn func(ctx context.Context) ([]*Summary, error)
|
||
closeSessionFn func(ctx context.Context, token string) error
|
||
}
|
||
|
||
func (f *fakeProxyClient) GetSession(ctx context.Context, token string) (*Summary, error) {
|
||
return f.getSessionFn(ctx, token)
|
||
}
|
||
func (f *fakeProxyClient) ListSessions(ctx context.Context) ([]*Summary, error) {
|
||
return f.listFn(ctx)
|
||
}
|
||
func (f *fakeProxyClient) CloseSession(ctx context.Context, token string) error {
|
||
return f.closeSessionFn(ctx, token)
|
||
}
|
||
|
||
// TestProxyClientStore_WriteOps_Unsupported 驗證所有寫入類操作回 ErrNotSupported。
|
||
func TestProxyClientStore_WriteOps_Unsupported(t *testing.T) {
|
||
store := NewProxyClientStore(&fakeProxyClient{}, nil)
|
||
ctx := context.Background()
|
||
|
||
err := store.Register(ctx, "vAc_x", nil)
|
||
assert.ErrorIs(t, err, ErrNotSupported, "Register 必須回 ErrNotSupported")
|
||
|
||
err = store.Heartbeat(ctx, "vAc_x")
|
||
assert.ErrorIs(t, err, ErrNotSupported, "Heartbeat 必須回 ErrNotSupported")
|
||
|
||
_, err = store.CleanupExpired(ctx, time.Minute)
|
||
assert.ErrorIs(t, err, ErrNotSupported, "CleanupExpired 必須回 ErrNotSupported")
|
||
}
|
||
|
||
// TestProxyClientStore_Lookup_OK 驗證 Lookup 走 client.GetSession 並回 RemoteHandle。
|
||
func TestProxyClientStore_Lookup_OK(t *testing.T) {
|
||
now := time.Now().UTC()
|
||
client := &fakeProxyClient{
|
||
getSessionFn: func(ctx context.Context, token string) (*Summary, error) {
|
||
assert.Equal(t, "vAc_x", token)
|
||
return &Summary{Token: token, ConnectedAt: now, LastHeartbeat: now}, nil
|
||
},
|
||
}
|
||
store := NewProxyClientStore(client, nil)
|
||
|
||
h, err := store.Lookup(context.Background(), "vAc_x")
|
||
require.NoError(t, err)
|
||
require.NotNil(t, h)
|
||
assert.False(t, h.IsClosed())
|
||
assert.Equal(t, "vAc_x", h.Summary().Token)
|
||
}
|
||
|
||
// TestProxyClientStore_Lookup_NotFound 驗證 ErrSessionNotFound 透傳。
|
||
func TestProxyClientStore_Lookup_NotFound(t *testing.T) {
|
||
client := &fakeProxyClient{
|
||
getSessionFn: func(ctx context.Context, token string) (*Summary, error) {
|
||
return nil, ErrSessionNotFound
|
||
},
|
||
}
|
||
store := NewProxyClientStore(client, nil)
|
||
_, err := store.Lookup(context.Background(), "vAc_x")
|
||
assert.ErrorIs(t, err, ErrSessionNotFound)
|
||
}
|
||
|
||
// TestProxyClientStore_Exists 驗證 Exists 的兩種狀態。
|
||
func TestProxyClientStore_Exists(t *testing.T) {
|
||
t.Run("exists", func(t *testing.T) {
|
||
client := &fakeProxyClient{
|
||
getSessionFn: func(ctx context.Context, token string) (*Summary, error) {
|
||
return &Summary{Token: token}, nil
|
||
},
|
||
}
|
||
store := NewProxyClientStore(client, nil)
|
||
ok, err := store.Exists(context.Background(), "vAc_x")
|
||
require.NoError(t, err)
|
||
assert.True(t, ok)
|
||
})
|
||
|
||
t.Run("not_found_returns_false_no_error", func(t *testing.T) {
|
||
client := &fakeProxyClient{
|
||
getSessionFn: func(ctx context.Context, token string) (*Summary, error) {
|
||
return nil, ErrSessionNotFound
|
||
},
|
||
}
|
||
store := NewProxyClientStore(client, nil)
|
||
ok, err := store.Exists(context.Background(), "vAc_x")
|
||
require.NoError(t, err)
|
||
assert.False(t, ok)
|
||
})
|
||
|
||
t.Run("other_error_propagates", func(t *testing.T) {
|
||
boom := errors.New("network down")
|
||
client := &fakeProxyClient{
|
||
getSessionFn: func(ctx context.Context, token string) (*Summary, error) {
|
||
return nil, boom
|
||
},
|
||
}
|
||
store := NewProxyClientStore(client, nil)
|
||
ok, err := store.Exists(context.Background(), "vAc_x")
|
||
assert.False(t, ok)
|
||
require.Error(t, err)
|
||
// Store 直接 propagate(不 wrap),所以 errors.Is 對 sentinel 應為 true。
|
||
assert.ErrorIs(t, err, boom)
|
||
})
|
||
}
|
||
|
||
// TestProxyClientStore_Unregister_DelegatesToClose 驗證 Unregister 走 CloseSession,
|
||
// 並且 NotFound 視為 no-op。
|
||
func TestProxyClientStore_Unregister_DelegatesToClose(t *testing.T) {
|
||
t.Run("delegates", func(t *testing.T) {
|
||
var called bool
|
||
client := &fakeProxyClient{
|
||
closeSessionFn: func(ctx context.Context, token string) error {
|
||
called = true
|
||
return nil
|
||
},
|
||
}
|
||
store := NewProxyClientStore(client, nil)
|
||
err := store.Unregister(context.Background(), "vAc_x")
|
||
require.NoError(t, err)
|
||
assert.True(t, called, "CloseSession 應被呼叫")
|
||
})
|
||
|
||
t.Run("not_found_is_noop", func(t *testing.T) {
|
||
client := &fakeProxyClient{
|
||
closeSessionFn: func(ctx context.Context, token string) error {
|
||
return ErrSessionNotFound
|
||
},
|
||
}
|
||
store := NewProxyClientStore(client, nil)
|
||
err := store.Unregister(context.Background(), "vAc_x")
|
||
assert.NoError(t, err, "NotFound 應視為 no-op")
|
||
})
|
||
}
|
||
|
||
// TestRemoteHandle_OpenStream_NoForwarder 驗證沒注入 forwarder 時回 ErrNotSupported。
|
||
func TestRemoteHandle_OpenStream_NoForwarder(t *testing.T) {
|
||
h := newRemoteHandle(&fakeProxyClient{}, nil, &Summary{Token: "vAc_x"})
|
||
_, err := h.OpenStream(context.Background())
|
||
assert.ErrorIs(t, err, ErrNotSupported)
|
||
}
|
||
|
||
// TestRemoteHandle_Close_Idempotent 驗證 Close 多次只 trigger 一次 CloseSession。
|
||
func TestRemoteHandle_Close_Idempotent(t *testing.T) {
|
||
var calls int
|
||
client := &fakeProxyClient{
|
||
closeSessionFn: func(ctx context.Context, token string) error {
|
||
calls++
|
||
return nil
|
||
},
|
||
}
|
||
h := newRemoteHandle(client, nil, &Summary{Token: "vAc_x"})
|
||
|
||
require.NoError(t, h.Close())
|
||
require.NoError(t, h.Close())
|
||
require.NoError(t, h.Close())
|
||
assert.Equal(t, 1, calls, "Close 多次應冪等:CloseSession 只被呼叫一次")
|
||
assert.True(t, h.IsClosed())
|
||
}
|
||
|
||
// TestRemoteHandle_OpenStream_AfterClose 驗證 close 後 OpenStream 回 ErrSessionClosed。
|
||
func TestRemoteHandle_OpenStream_AfterClose(t *testing.T) {
|
||
client := &fakeProxyClient{
|
||
closeSessionFn: func(ctx context.Context, token string) error { return nil },
|
||
}
|
||
h := newRemoteHandle(client, nil, &Summary{Token: "vAc_x"})
|
||
_ = h.Close()
|
||
_, err := h.OpenStream(context.Background())
|
||
assert.ErrorIs(t, err, ErrSessionClosed)
|
||
}
|