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) }