@luxexchange/sessions 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.depcheckrc +20 -0
- package/.eslintrc.js +21 -0
- package/README.md +1 -0
- package/env.d.ts +12 -0
- package/package.json +50 -0
- package/project.json +42 -0
- package/src/challenge-solvers/createChallengeSolverService.ts +64 -0
- package/src/challenge-solvers/createHashcashMockSolver.ts +39 -0
- package/src/challenge-solvers/createHashcashSolver.test.ts +385 -0
- package/src/challenge-solvers/createHashcashSolver.ts +255 -0
- package/src/challenge-solvers/createNoneMockSolver.ts +11 -0
- package/src/challenge-solvers/createTurnstileMockSolver.ts +30 -0
- package/src/challenge-solvers/createTurnstileSolver.ts +353 -0
- package/src/challenge-solvers/hashcash/core.native.ts +34 -0
- package/src/challenge-solvers/hashcash/core.test.ts +314 -0
- package/src/challenge-solvers/hashcash/core.ts +35 -0
- package/src/challenge-solvers/hashcash/core.web.ts +123 -0
- package/src/challenge-solvers/hashcash/createWorkerHashcashSolver.test.ts +195 -0
- package/src/challenge-solvers/hashcash/createWorkerHashcashSolver.ts +120 -0
- package/src/challenge-solvers/hashcash/shared.ts +70 -0
- package/src/challenge-solvers/hashcash/worker/createHashcashMultiWorkerChannel.native.ts +22 -0
- package/src/challenge-solvers/hashcash/worker/createHashcashMultiWorkerChannel.ts +22 -0
- package/src/challenge-solvers/hashcash/worker/createHashcashMultiWorkerChannel.web.ts +212 -0
- package/src/challenge-solvers/hashcash/worker/createHashcashWorkerChannel.native.ts +16 -0
- package/src/challenge-solvers/hashcash/worker/createHashcashWorkerChannel.ts +16 -0
- package/src/challenge-solvers/hashcash/worker/createHashcashWorkerChannel.web.ts +97 -0
- package/src/challenge-solvers/hashcash/worker/hashcash.worker.ts +91 -0
- package/src/challenge-solvers/hashcash/worker/types.ts +69 -0
- package/src/challenge-solvers/turnstileErrors.ts +49 -0
- package/src/challenge-solvers/turnstileScriptLoader.ts +325 -0
- package/src/challenge-solvers/turnstileSolver.integration.test.ts +331 -0
- package/src/challenge-solvers/types.ts +55 -0
- package/src/challengeFlow.integration.test.ts +627 -0
- package/src/device-id/createDeviceIdService.ts +19 -0
- package/src/device-id/types.ts +11 -0
- package/src/index.ts +137 -0
- package/src/lux-identifier/createLuxIdentifierService.ts +19 -0
- package/src/lux-identifier/luxIdentifierQuery.ts +20 -0
- package/src/lux-identifier/types.ts +11 -0
- package/src/oauth-service/createOAuthService.ts +125 -0
- package/src/oauth-service/types.ts +104 -0
- package/src/performance/createNoopPerformanceTracker.ts +12 -0
- package/src/performance/createPerformanceTracker.ts +43 -0
- package/src/performance/index.ts +7 -0
- package/src/performance/types.ts +11 -0
- package/src/session-initialization/createSessionInitializationService.test.ts +557 -0
- package/src/session-initialization/createSessionInitializationService.ts +193 -0
- package/src/session-initialization/sessionErrors.ts +32 -0
- package/src/session-repository/createSessionClient.ts +10 -0
- package/src/session-repository/createSessionRepository.test.ts +313 -0
- package/src/session-repository/createSessionRepository.ts +242 -0
- package/src/session-repository/errors.ts +22 -0
- package/src/session-repository/types.ts +289 -0
- package/src/session-service/createNoopSessionService.ts +24 -0
- package/src/session-service/createSessionService.test.ts +388 -0
- package/src/session-service/createSessionService.ts +61 -0
- package/src/session-service/types.ts +59 -0
- package/src/session-storage/createSessionStorage.ts +28 -0
- package/src/session-storage/types.ts +15 -0
- package/src/session.integration.test.ts +480 -0
- package/src/sessionLifecycle.integration.test.ts +264 -0
- package/src/test-utils/createLocalCookieTransport.ts +52 -0
- package/src/test-utils/createLocalHeaderTransport.ts +45 -0
- package/src/test-utils/mocks.ts +122 -0
- package/src/test-utils.ts +200 -0
- package/tsconfig.json +19 -0
- package/tsconfig.lint.json +8 -0
- package/tsconfig.spec.json +8 -0
- package/vitest.config.ts +20 -0
- package/vitest.integration.config.ts +14 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChallengeResponse,
|
|
3
|
+
ChallengeType,
|
|
4
|
+
DeleteSessionResponse,
|
|
5
|
+
GetChallengeTypesResponse,
|
|
6
|
+
InitSessionResponse,
|
|
7
|
+
IntrospectSessionResponse,
|
|
8
|
+
SignoutResponse,
|
|
9
|
+
UpdateSessionResponse,
|
|
10
|
+
VerifyResponse,
|
|
11
|
+
} from '@luxdex/client-platform-service/dist/uniswap/platformservice/v1/sessionService_pb'
|
|
12
|
+
import { createSessionRepository } from '@luxfi/sessions/src/session-repository/createSessionRepository'
|
|
13
|
+
import { createSessionService } from '@luxfi/sessions/src/session-service/createSessionService'
|
|
14
|
+
import type { SessionService } from '@luxfi/sessions/src/session-service/types'
|
|
15
|
+
import {
|
|
16
|
+
createMockSessionClient,
|
|
17
|
+
InMemoryDeviceIdService,
|
|
18
|
+
InMemorySessionStorage,
|
|
19
|
+
InMemoryLuxIdentifierService,
|
|
20
|
+
type MockEndpoints,
|
|
21
|
+
} from '@luxfi/sessions/src/test-utils'
|
|
22
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
23
|
+
|
|
24
|
+
describe('Session Lifecycle Integration Tests', () => {
|
|
25
|
+
let sessionStorage: InMemorySessionStorage
|
|
26
|
+
let deviceIdService: InMemoryDeviceIdService
|
|
27
|
+
let luxIdentifierService: InMemoryLuxIdentifierService
|
|
28
|
+
let sessionService: SessionService
|
|
29
|
+
let mockEndpoints: MockEndpoints
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
// Initialize in-memory storage
|
|
33
|
+
sessionStorage = new InMemorySessionStorage()
|
|
34
|
+
deviceIdService = new InMemoryDeviceIdService()
|
|
35
|
+
luxIdentifierService = new InMemoryLuxIdentifierService()
|
|
36
|
+
|
|
37
|
+
// Set up mock endpoints with default responses
|
|
38
|
+
mockEndpoints = {
|
|
39
|
+
'/lux.platformservice.v1.SessionService/InitSession': async (): Promise<InitSessionResponse> => {
|
|
40
|
+
return new InitSessionResponse({
|
|
41
|
+
sessionId: 'test-session-123',
|
|
42
|
+
needChallenge: false,
|
|
43
|
+
extra: {},
|
|
44
|
+
})
|
|
45
|
+
},
|
|
46
|
+
'/lux.platformservice.v1.SessionService/Challenge': async (): Promise<ChallengeResponse> => {
|
|
47
|
+
return new ChallengeResponse({
|
|
48
|
+
challengeId: 'challenge-123',
|
|
49
|
+
challengeType: ChallengeType.TURNSTILE,
|
|
50
|
+
extra: { sitekey: 'test-key' },
|
|
51
|
+
})
|
|
52
|
+
},
|
|
53
|
+
'/lux.platformservice.v1.SessionService/Verify': async (): Promise<VerifyResponse> => {
|
|
54
|
+
return new VerifyResponse({
|
|
55
|
+
retry: false,
|
|
56
|
+
})
|
|
57
|
+
},
|
|
58
|
+
'/lux.platformservice.v1.SessionService/DeleteSession': async (): Promise<DeleteSessionResponse> => {
|
|
59
|
+
return new DeleteSessionResponse({})
|
|
60
|
+
},
|
|
61
|
+
'/lux.platformservice.v1.SessionService/IntrospectSession': async (): Promise<IntrospectSessionResponse> => {
|
|
62
|
+
return new IntrospectSessionResponse({})
|
|
63
|
+
},
|
|
64
|
+
'/lux.platformservice.v1.SessionService/UpdateSession': async (): Promise<UpdateSessionResponse> => {
|
|
65
|
+
return new UpdateSessionResponse({})
|
|
66
|
+
},
|
|
67
|
+
'/lux.platformservice.v1.SessionService/GetChallengeTypes': async (): Promise<GetChallengeTypesResponse> => {
|
|
68
|
+
return new GetChallengeTypesResponse({ challengeTypes: [] })
|
|
69
|
+
},
|
|
70
|
+
'/lux.platformservice.v1.SessionService/Signout': async (): Promise<SignoutResponse> => {
|
|
71
|
+
return new SignoutResponse({})
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Create session client with test transport
|
|
76
|
+
const sessionClient = createMockSessionClient(mockEndpoints, sessionStorage, deviceIdService)
|
|
77
|
+
|
|
78
|
+
// Create repository
|
|
79
|
+
const sessionRepository = createSessionRepository({
|
|
80
|
+
client: sessionClient as any,
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
// Create session service
|
|
84
|
+
sessionService = createSessionService({
|
|
85
|
+
sessionStorage,
|
|
86
|
+
deviceIdService,
|
|
87
|
+
luxIdentifierService,
|
|
88
|
+
sessionRepository,
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
afterEach(async () => {
|
|
93
|
+
// Clean up any stored data
|
|
94
|
+
await sessionStorage.clear()
|
|
95
|
+
await deviceIdService.removeDeviceId()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('initializes and stores a session', async () => {
|
|
99
|
+
const response = await sessionService.initSession()
|
|
100
|
+
|
|
101
|
+
expect(response).toEqual({
|
|
102
|
+
sessionId: 'test-session-123',
|
|
103
|
+
needChallenge: false,
|
|
104
|
+
extra: {},
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
// Verify session was stored
|
|
108
|
+
const storedSession = await sessionStorage.get()
|
|
109
|
+
expect(storedSession?.sessionId).toBe('test-session-123')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('handles session initialization without challenge requirement', async () => {
|
|
113
|
+
// Override default to return needChallenge: false
|
|
114
|
+
mockEndpoints['/lux.platformservice.v1.SessionService/InitSession'] =
|
|
115
|
+
async (): Promise<InitSessionResponse> => {
|
|
116
|
+
return new InitSessionResponse({
|
|
117
|
+
sessionId: 'simple-session-123',
|
|
118
|
+
needChallenge: false,
|
|
119
|
+
extra: {},
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
await deviceIdService.setDeviceId('simple-device-123')
|
|
124
|
+
|
|
125
|
+
// Initialize
|
|
126
|
+
const response = await sessionService.initSession()
|
|
127
|
+
|
|
128
|
+
// Verify response
|
|
129
|
+
expect(response.sessionId).toBe('simple-session-123')
|
|
130
|
+
expect(response.needChallenge).toBe(false)
|
|
131
|
+
|
|
132
|
+
// Session should be stored
|
|
133
|
+
const storedSession = await sessionStorage.get()
|
|
134
|
+
expect(storedSession?.sessionId).toBe('simple-session-123')
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('properly clears session on deletion', async () => {
|
|
138
|
+
// Initialize a session first
|
|
139
|
+
await sessionService.initSession()
|
|
140
|
+
|
|
141
|
+
// Verify session exists
|
|
142
|
+
let storedSession = await sessionStorage.get()
|
|
143
|
+
expect(storedSession?.sessionId).toBe('test-session-123')
|
|
144
|
+
|
|
145
|
+
// Delete session
|
|
146
|
+
await sessionService.removeSession()
|
|
147
|
+
|
|
148
|
+
// Verify session is cleared
|
|
149
|
+
storedSession = await sessionStorage.get()
|
|
150
|
+
expect(storedSession).toBeNull()
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('stores session ID when provided (mobile/extension behavior)', async () => {
|
|
154
|
+
mockEndpoints['/lux.platformservice.v1.SessionService/InitSession'] =
|
|
155
|
+
async (): Promise<InitSessionResponse> => {
|
|
156
|
+
return new InitSessionResponse({
|
|
157
|
+
sessionId: 'mobile-session-123',
|
|
158
|
+
needChallenge: false,
|
|
159
|
+
extra: {},
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const response = await sessionService.initSession()
|
|
164
|
+
|
|
165
|
+
expect(response.sessionId).toBe('mobile-session-123')
|
|
166
|
+
|
|
167
|
+
// Session should be stored
|
|
168
|
+
const storedSession = await sessionStorage.get()
|
|
169
|
+
expect(storedSession?.sessionId).toBe('mobile-session-123')
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('handles web sessions without storing session ID when undefined', async () => {
|
|
173
|
+
mockEndpoints['/lux.platformservice.v1.SessionService/InitSession'] =
|
|
174
|
+
async (): Promise<InitSessionResponse> => {
|
|
175
|
+
return new InitSessionResponse({
|
|
176
|
+
sessionId: undefined,
|
|
177
|
+
needChallenge: true,
|
|
178
|
+
extra: { sitekey: 'turnstile-key' },
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const response = await sessionService.initSession()
|
|
183
|
+
|
|
184
|
+
expect(response.sessionId).toBeUndefined()
|
|
185
|
+
expect(response.needChallenge).toBe(true)
|
|
186
|
+
|
|
187
|
+
// Session should not be stored
|
|
188
|
+
const storedSession = await sessionStorage.get()
|
|
189
|
+
expect(storedSession).toBeNull()
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('retrieves existing session state', async () => {
|
|
193
|
+
// Initialize a session
|
|
194
|
+
await sessionService.initSession()
|
|
195
|
+
|
|
196
|
+
// Get session state
|
|
197
|
+
const sessionState = await sessionService.getSessionState()
|
|
198
|
+
|
|
199
|
+
expect(sessionState).toEqual({
|
|
200
|
+
sessionId: 'test-session-123',
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('returns null when no session exists', async () => {
|
|
205
|
+
// Don't initialize any session
|
|
206
|
+
const sessionState = await sessionService.getSessionState()
|
|
207
|
+
|
|
208
|
+
expect(sessionState).toBeNull()
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('handles multiple initialization attempts', async () => {
|
|
212
|
+
// First initialization
|
|
213
|
+
await sessionService.initSession()
|
|
214
|
+
|
|
215
|
+
const firstSession = await sessionStorage.get()
|
|
216
|
+
expect(firstSession?.sessionId).toBe('test-session-123')
|
|
217
|
+
|
|
218
|
+
// Update mock to return different session
|
|
219
|
+
mockEndpoints['/lux.platformservice.v1.SessionService/InitSession'] =
|
|
220
|
+
async (): Promise<InitSessionResponse> => {
|
|
221
|
+
return new InitSessionResponse({
|
|
222
|
+
sessionId: 'new-session-789',
|
|
223
|
+
needChallenge: false,
|
|
224
|
+
extra: {},
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Second initialization should replace the first
|
|
229
|
+
await sessionService.initSession()
|
|
230
|
+
|
|
231
|
+
const secondSession = await sessionStorage.get()
|
|
232
|
+
expect(secondSession?.sessionId).toBe('new-session-789')
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('sets and retrieves device ID through init response', async () => {
|
|
236
|
+
mockEndpoints['/lux.platformservice.v1.SessionService/InitSession'] =
|
|
237
|
+
async (): Promise<InitSessionResponse> => {
|
|
238
|
+
return new InitSessionResponse({
|
|
239
|
+
sessionId: 'device-test-session',
|
|
240
|
+
deviceId: 'new-device-123',
|
|
241
|
+
needChallenge: false,
|
|
242
|
+
extra: {},
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
await sessionService.initSession()
|
|
247
|
+
|
|
248
|
+
// Device ID should be stored
|
|
249
|
+
const deviceId = await deviceIdService.getDeviceId()
|
|
250
|
+
expect(deviceId).toBe('new-device-123')
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it('preserves existing device ID when not provided in response', async () => {
|
|
254
|
+
// Set device ID first
|
|
255
|
+
await deviceIdService.setDeviceId('existing-device-456')
|
|
256
|
+
|
|
257
|
+
// Init without device_id in response
|
|
258
|
+
await sessionService.initSession()
|
|
259
|
+
|
|
260
|
+
// Device ID should remain unchanged
|
|
261
|
+
const deviceId = await deviceIdService.getDeviceId()
|
|
262
|
+
expect(deviceId).toBe('existing-device-456')
|
|
263
|
+
})
|
|
264
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Transport } from '@connectrpc/connect'
|
|
2
|
+
import { createConnectTransport } from '@connectrpc/connect-web'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a test transport that simulates browser cookie behavior
|
|
6
|
+
* for integration testing with real backends.
|
|
7
|
+
*/
|
|
8
|
+
export function createLocalCookieTransport(options: { baseUrl: string; cookieJar: Map<string, string> }): Transport {
|
|
9
|
+
const { baseUrl, cookieJar } = options
|
|
10
|
+
|
|
11
|
+
return createConnectTransport({
|
|
12
|
+
baseUrl,
|
|
13
|
+
credentials: 'include', // Tell backend we want cookies
|
|
14
|
+
interceptors: [
|
|
15
|
+
(next) => async (request) => {
|
|
16
|
+
// Add required headers that backend expects
|
|
17
|
+
request.header.set('x-request-source', 'lux-web')
|
|
18
|
+
|
|
19
|
+
// Simulate browser cookie behavior: add stored cookies to request
|
|
20
|
+
if (cookieJar.size > 0) {
|
|
21
|
+
const cookieHeader = Array.from(cookieJar.entries())
|
|
22
|
+
.map(([name, value]) => `${name}=${value}`)
|
|
23
|
+
.join('; ')
|
|
24
|
+
request.header.set('cookie', cookieHeader)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Make the request
|
|
28
|
+
const response = await next(request)
|
|
29
|
+
|
|
30
|
+
// Simulate browser cookie behavior: store cookies from response
|
|
31
|
+
for (const cookie of response.header.getSetCookie()) {
|
|
32
|
+
const [nameValue] = cookie.split(';')
|
|
33
|
+
if (nameValue) {
|
|
34
|
+
const [name, value] = nameValue.split('=')
|
|
35
|
+
if (name && value) {
|
|
36
|
+
cookieJar.set(name.trim(), value.trim())
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return response
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Creates a cookie jar for managing cookies in tests
|
|
49
|
+
*/
|
|
50
|
+
export function createCookieJar(): Map<string, string> {
|
|
51
|
+
return new Map<string, string>()
|
|
52
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Transport } from '@connectrpc/connect'
|
|
2
|
+
import { createConnectTransport } from '@connectrpc/connect-web'
|
|
3
|
+
|
|
4
|
+
interface CreateLocalHeaderTransportOptions {
|
|
5
|
+
baseUrl: string
|
|
6
|
+
requestSource: 'lux-ios' | 'lux-android' | 'lux-extension'
|
|
7
|
+
getSessionId: () => Promise<string | null>
|
|
8
|
+
getDeviceId: () => Promise<string | null>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Creates a test transport that simulates native app behavior
|
|
13
|
+
* for integration testing with real backends.
|
|
14
|
+
*
|
|
15
|
+
* Unlike web which uses cookies, native platforms send session
|
|
16
|
+
* and device IDs via headers on every request.
|
|
17
|
+
*/
|
|
18
|
+
export function createLocalHeaderTransport(options: CreateLocalHeaderTransportOptions): Transport {
|
|
19
|
+
const { baseUrl, requestSource, getSessionId, getDeviceId } = options
|
|
20
|
+
|
|
21
|
+
return createConnectTransport({
|
|
22
|
+
baseUrl,
|
|
23
|
+
// NO credentials: 'include' - native platforms don't use cookies
|
|
24
|
+
interceptors: [
|
|
25
|
+
(next) => async (request) => {
|
|
26
|
+
// Add platform-specific request source header
|
|
27
|
+
request.header.set('x-request-source', requestSource)
|
|
28
|
+
|
|
29
|
+
// Add session ID header if available
|
|
30
|
+
const sessionId = await getSessionId()
|
|
31
|
+
if (sessionId) {
|
|
32
|
+
request.header.set('X-Session-ID', sessionId)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Add device ID header if available
|
|
36
|
+
const deviceId = await getDeviceId()
|
|
37
|
+
if (deviceId) {
|
|
38
|
+
request.header.set('X-Device-ID', deviceId)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return next(request)
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
})
|
|
45
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { ChallengeSolver, ChallengeSolverService } from '@luxfi/sessions/src/challenge-solvers/types'
|
|
2
|
+
import type { SessionService } from '@luxfi/sessions/src/session-service/types'
|
|
3
|
+
import { ChallengeType } from '@luxfi/sessions/src/session-service/types'
|
|
4
|
+
import { vi } from 'vitest'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Creates a mock SessionService with sensible defaults
|
|
8
|
+
* All methods are vi.fn() mocks that can be overridden
|
|
9
|
+
*/
|
|
10
|
+
export function createMockSessionService(overrides: Partial<SessionService> = {}): SessionService {
|
|
11
|
+
return {
|
|
12
|
+
getSessionState: vi.fn().mockResolvedValue(null),
|
|
13
|
+
initSession: vi.fn().mockResolvedValue({
|
|
14
|
+
sessionId: 'mock-session-123',
|
|
15
|
+
needChallenge: false,
|
|
16
|
+
extra: {},
|
|
17
|
+
}),
|
|
18
|
+
requestChallenge: vi.fn().mockResolvedValue({
|
|
19
|
+
challengeId: 'mock-challenge-456',
|
|
20
|
+
challengeType: ChallengeType.TURNSTILE,
|
|
21
|
+
extra: {},
|
|
22
|
+
challengeData: { case: 'turnstile', value: { siteKey: 'mock-sitekey', action: 'verify' } },
|
|
23
|
+
}),
|
|
24
|
+
verifySession: vi.fn().mockResolvedValue({
|
|
25
|
+
retry: false,
|
|
26
|
+
}),
|
|
27
|
+
removeSession: vi.fn().mockResolvedValue(undefined),
|
|
28
|
+
...overrides,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Creates a mock ChallengeSolver
|
|
34
|
+
*/
|
|
35
|
+
export function createMockChallengeSolver(
|
|
36
|
+
solveFn: () => Promise<string> = async (): Promise<string> => 'mock-solution',
|
|
37
|
+
): ChallengeSolver {
|
|
38
|
+
return {
|
|
39
|
+
solve: vi.fn().mockImplementation(solveFn),
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Creates a mock ChallengeSolverService
|
|
45
|
+
*/
|
|
46
|
+
export function createMockChallengeSolverService(
|
|
47
|
+
solvers: Map<ChallengeType, ChallengeSolver> = new Map(),
|
|
48
|
+
): ChallengeSolverService {
|
|
49
|
+
// Default solvers if not provided
|
|
50
|
+
if (solvers.size === 0) {
|
|
51
|
+
solvers.set(
|
|
52
|
+
ChallengeType.TURNSTILE,
|
|
53
|
+
createMockChallengeSolver(async () => 'mock-turnstile-token'),
|
|
54
|
+
)
|
|
55
|
+
solvers.set(
|
|
56
|
+
ChallengeType.HASHCASH,
|
|
57
|
+
createMockChallengeSolver(async () => 'mock-hashcash-proof'),
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
getSolver: vi.fn().mockImplementation((type: ChallengeType) => solvers.get(type) || null),
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Test scenario helpers for common setups
|
|
68
|
+
*/
|
|
69
|
+
export const TestScenarios = {
|
|
70
|
+
/**
|
|
71
|
+
* Setup for when a session already exists
|
|
72
|
+
*/
|
|
73
|
+
withExistingSession(service: SessionService, sessionId = 'existing-session-789'): void {
|
|
74
|
+
vi.mocked(service.getSessionState).mockResolvedValue({ sessionId })
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Setup for when initialization requires no challenge
|
|
79
|
+
*/
|
|
80
|
+
withNoChallenge(service: SessionService): void {
|
|
81
|
+
vi.mocked(service.getSessionState).mockResolvedValue(null)
|
|
82
|
+
vi.mocked(service.initSession).mockResolvedValue({
|
|
83
|
+
sessionId: 'new-session-111',
|
|
84
|
+
needChallenge: false,
|
|
85
|
+
extra: {},
|
|
86
|
+
})
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Setup for when initialization requires a challenge
|
|
91
|
+
*/
|
|
92
|
+
withChallengeRequired(service: SessionService, challengeType = ChallengeType.TURNSTILE): void {
|
|
93
|
+
vi.mocked(service.getSessionState).mockResolvedValue(null)
|
|
94
|
+
vi.mocked(service.initSession).mockResolvedValue({
|
|
95
|
+
sessionId: 'new-session-222',
|
|
96
|
+
needChallenge: true,
|
|
97
|
+
extra: {},
|
|
98
|
+
})
|
|
99
|
+
vi.mocked(service.requestChallenge).mockResolvedValue({
|
|
100
|
+
challengeId: 'challenge-333',
|
|
101
|
+
challengeType,
|
|
102
|
+
extra: {},
|
|
103
|
+
challengeData: { case: 'turnstile', value: { siteKey: 'test-sitekey', action: 'verify' } },
|
|
104
|
+
})
|
|
105
|
+
vi.mocked(service.verifySession).mockResolvedValue({
|
|
106
|
+
retry: false,
|
|
107
|
+
})
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Setup for when server requests challenge retry
|
|
112
|
+
*/
|
|
113
|
+
withServerRetry(service: SessionService, retriesBeforeSuccess = 1): void {
|
|
114
|
+
let attemptCount = 0
|
|
115
|
+
vi.mocked(service.verifySession).mockImplementation(async () => {
|
|
116
|
+
attemptCount++
|
|
117
|
+
return {
|
|
118
|
+
retry: attemptCount <= retriesBeforeSuccess,
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
},
|
|
122
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/** biome-ignore-all lint/suspicious/noExplicitAny: mock handlers */
|
|
2
|
+
import type { PartialMessage } from '@bufbuild/protobuf'
|
|
3
|
+
import type { CallOptions } from '@connectrpc/connect'
|
|
4
|
+
import { createConnectTransport } from '@connectrpc/connect-web'
|
|
5
|
+
import {
|
|
6
|
+
type ChallengeRequest,
|
|
7
|
+
type ChallengeResponse,
|
|
8
|
+
type GetChallengeTypesRequest,
|
|
9
|
+
type GetChallengeTypesResponse,
|
|
10
|
+
type InitSessionRequest,
|
|
11
|
+
type InitSessionResponse,
|
|
12
|
+
type IntrospectSessionRequest,
|
|
13
|
+
type IntrospectSessionResponse,
|
|
14
|
+
type SignoutRequest,
|
|
15
|
+
type SignoutResponse,
|
|
16
|
+
type UpdateSessionRequest,
|
|
17
|
+
type UpdateSessionResponse,
|
|
18
|
+
type VerifyRequest,
|
|
19
|
+
type VerifyResponse,
|
|
20
|
+
} from '@luxdex/client-platform-service/dist/uniswap/platformservice/v1/sessionService_pb'
|
|
21
|
+
import type { DeviceIdService } from '@luxfi/sessions/src/device-id/types'
|
|
22
|
+
import type { SessionServiceClient } from '@luxfi/sessions/src/session-repository/createSessionClient'
|
|
23
|
+
import type { SessionState, SessionStorage } from '@luxfi/sessions/src/session-storage/types'
|
|
24
|
+
import type { LuxIdentifierService } from '@luxfi/sessions/src/lux-identifier/types'
|
|
25
|
+
// Types for our test transport
|
|
26
|
+
export interface MockEndpointHandler {
|
|
27
|
+
(request: any, headers: Record<string, string>): Promise<any>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface MockEndpoints {
|
|
31
|
+
'/lux.platformservice.v1.SessionService/InitSession': MockEndpointHandler
|
|
32
|
+
'/lux.platformservice.v1.SessionService/Challenge': MockEndpointHandler
|
|
33
|
+
'/lux.platformservice.v1.SessionService/Verify': MockEndpointHandler
|
|
34
|
+
'/lux.platformservice.v1.SessionService/IntrospectSession': MockEndpointHandler
|
|
35
|
+
'/lux.platformservice.v1.SessionService/UpdateSession': MockEndpointHandler
|
|
36
|
+
'/lux.platformservice.v1.SessionService/GetChallengeTypes': MockEndpointHandler
|
|
37
|
+
'/lux.platformservice.v1.SessionService/Signout': MockEndpointHandler
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Test transport that intercepts requests and returns mock responses
|
|
41
|
+
export function createTestTransport(mockEndpoints: MockEndpoints): ReturnType<typeof createConnectTransport> {
|
|
42
|
+
return createConnectTransport({
|
|
43
|
+
baseUrl: 'https://test.api.lux.org',
|
|
44
|
+
interceptors: [
|
|
45
|
+
(_next) => async (request) => {
|
|
46
|
+
const url = request.url
|
|
47
|
+
const path = new URL(url).pathname
|
|
48
|
+
const handler = mockEndpoints[path as keyof MockEndpoints]
|
|
49
|
+
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
51
|
+
if (!handler) {
|
|
52
|
+
throw new Error(`No mock handler for ${path}`)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Extract headers
|
|
56
|
+
const headers: Record<string, string> = {}
|
|
57
|
+
request.header.forEach((value, key) => {
|
|
58
|
+
headers[key] = value
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const requestData = request.message
|
|
62
|
+
|
|
63
|
+
const responseData = await handler(requestData, headers)
|
|
64
|
+
|
|
65
|
+
// Return properly typed response
|
|
66
|
+
return {
|
|
67
|
+
stream: false as const,
|
|
68
|
+
service: request.service,
|
|
69
|
+
method: request.method,
|
|
70
|
+
header: new Headers(),
|
|
71
|
+
message: responseData,
|
|
72
|
+
trailer: new Headers(),
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// In-memory storage implementations
|
|
80
|
+
export class InMemorySessionStorage implements SessionStorage {
|
|
81
|
+
private state: SessionState | null = null
|
|
82
|
+
|
|
83
|
+
async get(): Promise<SessionState | null> {
|
|
84
|
+
return this.state
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async set(session: SessionState): Promise<void> {
|
|
88
|
+
this.state = session
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async clear(): Promise<void> {
|
|
92
|
+
this.state = null
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export class InMemoryDeviceIdService implements DeviceIdService {
|
|
97
|
+
private deviceId: string | null = null
|
|
98
|
+
|
|
99
|
+
async getDeviceId(): Promise<string | null> {
|
|
100
|
+
return this.deviceId
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async setDeviceId(id: string): Promise<void> {
|
|
104
|
+
this.deviceId = id
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async removeDeviceId(): Promise<void> {
|
|
108
|
+
this.deviceId = null
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export class InMemoryLuxIdentifierService implements LuxIdentifierService {
|
|
113
|
+
private identifier: string | null = null
|
|
114
|
+
|
|
115
|
+
async getLuxIdentifier(): Promise<string | null> {
|
|
116
|
+
return this.identifier
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async setLuxIdentifier(id: string): Promise<void> {
|
|
120
|
+
this.identifier = id
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async removeLuxIdentifier(): Promise<void> {
|
|
124
|
+
this.identifier = null
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Create a mock session client for testing
|
|
129
|
+
// eslint-disable-next-line max-params
|
|
130
|
+
export function createMockSessionClient(
|
|
131
|
+
mockEndpoints: MockEndpoints,
|
|
132
|
+
sessionStorage: SessionStorage,
|
|
133
|
+
deviceIdService: DeviceIdService,
|
|
134
|
+
): SessionServiceClient {
|
|
135
|
+
return {
|
|
136
|
+
initSession: async (
|
|
137
|
+
request: PartialMessage<InitSessionRequest>,
|
|
138
|
+
_options?: CallOptions,
|
|
139
|
+
): Promise<InitSessionResponse> => {
|
|
140
|
+
const response = await mockEndpoints['/lux.platformservice.v1.SessionService/InitSession'](request, {})
|
|
141
|
+
return response as InitSessionResponse
|
|
142
|
+
},
|
|
143
|
+
challenge: async (
|
|
144
|
+
request: PartialMessage<ChallengeRequest>,
|
|
145
|
+
_options?: CallOptions,
|
|
146
|
+
): Promise<ChallengeResponse> => {
|
|
147
|
+
const sessionId = await sessionStorage.get()
|
|
148
|
+
const deviceId = await deviceIdService.getDeviceId()
|
|
149
|
+
const headers: Record<string, string> = {}
|
|
150
|
+
if (sessionId?.sessionId) {
|
|
151
|
+
headers['X-Session-ID'] = sessionId.sessionId
|
|
152
|
+
}
|
|
153
|
+
if (deviceId) {
|
|
154
|
+
headers['X-Device-ID'] = deviceId
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const response = await mockEndpoints['/lux.platformservice.v1.SessionService/Challenge'](request, headers)
|
|
158
|
+
return response as ChallengeResponse
|
|
159
|
+
},
|
|
160
|
+
verify: async (request: PartialMessage<VerifyRequest>, _options?: CallOptions): Promise<VerifyResponse> => {
|
|
161
|
+
const sessionId = await sessionStorage.get()
|
|
162
|
+
const deviceId = await deviceIdService.getDeviceId()
|
|
163
|
+
const headers: Record<string, string> = {}
|
|
164
|
+
if (sessionId?.sessionId) {
|
|
165
|
+
headers['X-Session-ID'] = sessionId.sessionId
|
|
166
|
+
}
|
|
167
|
+
if (deviceId) {
|
|
168
|
+
headers['X-Device-ID'] = deviceId
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const response = await mockEndpoints['/lux.platformservice.v1.SessionService/Verify'](request, headers)
|
|
172
|
+
return response as VerifyResponse
|
|
173
|
+
},
|
|
174
|
+
introspectSession: async (
|
|
175
|
+
request: PartialMessage<IntrospectSessionRequest>,
|
|
176
|
+
_options?: CallOptions,
|
|
177
|
+
): Promise<IntrospectSessionResponse> => {
|
|
178
|
+
const response = await mockEndpoints['/lux.platformservice.v1.SessionService/IntrospectSession'](request, {})
|
|
179
|
+
return response as IntrospectSessionResponse
|
|
180
|
+
},
|
|
181
|
+
updateSession: async (
|
|
182
|
+
request: PartialMessage<UpdateSessionRequest>,
|
|
183
|
+
_options?: CallOptions,
|
|
184
|
+
): Promise<UpdateSessionResponse> => {
|
|
185
|
+
const response = await mockEndpoints['/lux.platformservice.v1.SessionService/UpdateSession'](request, {})
|
|
186
|
+
return response as UpdateSessionResponse
|
|
187
|
+
},
|
|
188
|
+
getChallengeTypes: async (
|
|
189
|
+
request: PartialMessage<GetChallengeTypesRequest>,
|
|
190
|
+
_options?: CallOptions,
|
|
191
|
+
): Promise<GetChallengeTypesResponse> => {
|
|
192
|
+
const response = await mockEndpoints['/lux.platformservice.v1.SessionService/GetChallengeTypes'](request, {})
|
|
193
|
+
return response as GetChallengeTypesResponse
|
|
194
|
+
},
|
|
195
|
+
signout: async (request: PartialMessage<SignoutRequest>, _options?: CallOptions): Promise<SignoutResponse> => {
|
|
196
|
+
const response = await mockEndpoints['/lux.platformservice.v1.SessionService/Signout'](request, {})
|
|
197
|
+
return response as SignoutResponse
|
|
198
|
+
},
|
|
199
|
+
}
|
|
200
|
+
}
|