@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,557 @@
|
|
|
1
|
+
import type { PerformanceTracker } from '@luxexchange/sessions/src/performance/types'
|
|
2
|
+
import { createSessionInitializationService } from '@luxexchange/sessions/src/session-initialization/createSessionInitializationService'
|
|
3
|
+
import { ChallengeType } from '@luxexchange/sessions/src/session-service/types'
|
|
4
|
+
import {
|
|
5
|
+
createMockChallengeSolverService,
|
|
6
|
+
createMockSessionService,
|
|
7
|
+
TestScenarios,
|
|
8
|
+
} from '@luxexchange/sessions/src/test-utils/mocks'
|
|
9
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
10
|
+
|
|
11
|
+
// Mock performance tracker for testing
|
|
12
|
+
function createMockPerformanceTracker(): PerformanceTracker {
|
|
13
|
+
let time = 0
|
|
14
|
+
return {
|
|
15
|
+
now: (): number => {
|
|
16
|
+
time += 100
|
|
17
|
+
return time
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('createSessionInitializationService', () => {
|
|
23
|
+
let sessionService: ReturnType<typeof createMockSessionService>
|
|
24
|
+
let challengeSolverService: ReturnType<typeof createMockChallengeSolverService>
|
|
25
|
+
let mockPerformanceTracker: PerformanceTracker
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
sessionService = createMockSessionService()
|
|
29
|
+
challengeSolverService = createMockChallengeSolverService()
|
|
30
|
+
mockPerformanceTracker = createMockPerformanceTracker()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
describe('initialize()', () => {
|
|
34
|
+
describe('session initialization', () => {
|
|
35
|
+
it('initializes session without challenge when not required', async () => {
|
|
36
|
+
// Setup
|
|
37
|
+
TestScenarios.withNoChallenge(sessionService)
|
|
38
|
+
|
|
39
|
+
const service = createSessionInitializationService({
|
|
40
|
+
getSessionService: () => sessionService,
|
|
41
|
+
challengeSolverService,
|
|
42
|
+
performanceTracker: mockPerformanceTracker,
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
// Execute
|
|
46
|
+
const result = await service.initialize()
|
|
47
|
+
|
|
48
|
+
// Verify behavior
|
|
49
|
+
expect(result).toEqual({
|
|
50
|
+
sessionId: 'new-session-111',
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
// Verify correct flow
|
|
54
|
+
expect(sessionService.initSession).toHaveBeenCalled()
|
|
55
|
+
expect(sessionService.requestChallenge).not.toHaveBeenCalled()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('completes full challenge flow when required', async () => {
|
|
59
|
+
// Setup
|
|
60
|
+
TestScenarios.withChallengeRequired(sessionService, ChallengeType.TURNSTILE)
|
|
61
|
+
|
|
62
|
+
const service = createSessionInitializationService({
|
|
63
|
+
getSessionService: () => sessionService,
|
|
64
|
+
challengeSolverService,
|
|
65
|
+
performanceTracker: mockPerformanceTracker,
|
|
66
|
+
getIsSessionUpgradeAutoEnabled: () => true,
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// Execute
|
|
70
|
+
const result = await service.initialize()
|
|
71
|
+
|
|
72
|
+
// Verify behavior
|
|
73
|
+
expect(result).toEqual({
|
|
74
|
+
sessionId: 'new-session-222',
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
// Verify complete flow executed
|
|
78
|
+
expect(sessionService.initSession).toHaveBeenCalled()
|
|
79
|
+
expect(sessionService.requestChallenge).toHaveBeenCalled()
|
|
80
|
+
expect(sessionService.verifySession).toHaveBeenCalled()
|
|
81
|
+
|
|
82
|
+
// Verify solver was used
|
|
83
|
+
expect(challengeSolverService.getSolver).toHaveBeenCalledWith(ChallengeType.TURNSTILE)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('uses correct solver for challenge type', async () => {
|
|
87
|
+
// Setup with Hashcash challenge
|
|
88
|
+
TestScenarios.withChallengeRequired(sessionService, ChallengeType.HASHCASH)
|
|
89
|
+
|
|
90
|
+
const service = createSessionInitializationService({
|
|
91
|
+
getSessionService: () => sessionService,
|
|
92
|
+
challengeSolverService,
|
|
93
|
+
performanceTracker: mockPerformanceTracker,
|
|
94
|
+
getIsSessionUpgradeAutoEnabled: () => true,
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
// Execute
|
|
98
|
+
await service.initialize()
|
|
99
|
+
|
|
100
|
+
// Verify correct solver was requested
|
|
101
|
+
expect(challengeSolverService.getSolver).toHaveBeenCalledWith(ChallengeType.HASHCASH)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('passes solution to upgrade session', async () => {
|
|
105
|
+
// Setup
|
|
106
|
+
TestScenarios.withChallengeRequired(sessionService)
|
|
107
|
+
const expectedSolution = 'test-solution-xyz'
|
|
108
|
+
const mockSolver = {
|
|
109
|
+
solve: vi.fn().mockResolvedValue(expectedSolution),
|
|
110
|
+
}
|
|
111
|
+
challengeSolverService.getSolver = vi.fn().mockReturnValue(mockSolver)
|
|
112
|
+
|
|
113
|
+
const service = createSessionInitializationService({
|
|
114
|
+
getSessionService: () => sessionService,
|
|
115
|
+
challengeSolverService,
|
|
116
|
+
performanceTracker: mockPerformanceTracker,
|
|
117
|
+
getIsSessionUpgradeAutoEnabled: () => true,
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
// Execute
|
|
121
|
+
await service.initialize()
|
|
122
|
+
|
|
123
|
+
// Verify solution was passed correctly
|
|
124
|
+
expect(sessionService.verifySession).toHaveBeenCalledWith({
|
|
125
|
+
solution: expectedSolution,
|
|
126
|
+
challengeId: 'challenge-333',
|
|
127
|
+
challengeType: ChallengeType.TURNSTILE,
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
describe('when server requests retry', () => {
|
|
133
|
+
it('retries challenge when server requests', async () => {
|
|
134
|
+
// Setup
|
|
135
|
+
TestScenarios.withChallengeRequired(sessionService)
|
|
136
|
+
TestScenarios.withServerRetry(sessionService, 1) // Retry once then succeed
|
|
137
|
+
|
|
138
|
+
const service = createSessionInitializationService({
|
|
139
|
+
getSessionService: () => sessionService,
|
|
140
|
+
challengeSolverService,
|
|
141
|
+
performanceTracker: mockPerformanceTracker,
|
|
142
|
+
getIsSessionUpgradeAutoEnabled: () => true,
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
// Execute
|
|
146
|
+
const result = await service.initialize()
|
|
147
|
+
|
|
148
|
+
// Verify success
|
|
149
|
+
expect(result.sessionId).toBe('new-session-222')
|
|
150
|
+
|
|
151
|
+
// Verify retry happened (challenge requested twice)
|
|
152
|
+
expect(sessionService.requestChallenge).toHaveBeenCalledTimes(2)
|
|
153
|
+
expect(sessionService.verifySession).toHaveBeenCalledTimes(2)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('fails after maximum retry attempts', async () => {
|
|
157
|
+
// Setup - server always requests retry
|
|
158
|
+
TestScenarios.withChallengeRequired(sessionService)
|
|
159
|
+
vi.mocked(sessionService.verifySession).mockResolvedValue({ retry: true })
|
|
160
|
+
|
|
161
|
+
const service = createSessionInitializationService({
|
|
162
|
+
getSessionService: () => sessionService,
|
|
163
|
+
challengeSolverService,
|
|
164
|
+
performanceTracker: mockPerformanceTracker,
|
|
165
|
+
getIsSessionUpgradeAutoEnabled: () => true,
|
|
166
|
+
maxChallengeRetries: 2,
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
// Execute and verify failure
|
|
170
|
+
await expect(service.initialize()).rejects.toThrow(
|
|
171
|
+
'Maximum challenge retry attempts (2) exceeded after 3 attempts',
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
// Verify correct number of attempts
|
|
175
|
+
expect(sessionService.requestChallenge).toHaveBeenCalledTimes(3) // Initial + 2 retries
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('succeeds within retry limit', async () => {
|
|
179
|
+
// Setup - succeed on 3rd attempt
|
|
180
|
+
TestScenarios.withChallengeRequired(sessionService)
|
|
181
|
+
TestScenarios.withServerRetry(sessionService, 2)
|
|
182
|
+
|
|
183
|
+
const service = createSessionInitializationService({
|
|
184
|
+
getSessionService: () => sessionService,
|
|
185
|
+
challengeSolverService,
|
|
186
|
+
performanceTracker: mockPerformanceTracker,
|
|
187
|
+
getIsSessionUpgradeAutoEnabled: () => true,
|
|
188
|
+
maxChallengeRetries: 3,
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
// Execute
|
|
192
|
+
const result = await service.initialize()
|
|
193
|
+
|
|
194
|
+
// Verify success
|
|
195
|
+
expect(result.sessionId).toBe('new-session-222')
|
|
196
|
+
expect(sessionService.verifySession).toHaveBeenCalledTimes(3)
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
describe('error handling', () => {
|
|
201
|
+
it('throws when no solver available for challenge type', async () => {
|
|
202
|
+
// Setup
|
|
203
|
+
TestScenarios.withChallengeRequired(sessionService)
|
|
204
|
+
challengeSolverService.getSolver = vi.fn().mockReturnValue(null)
|
|
205
|
+
|
|
206
|
+
const service = createSessionInitializationService({
|
|
207
|
+
getSessionService: () => sessionService,
|
|
208
|
+
challengeSolverService,
|
|
209
|
+
performanceTracker: mockPerformanceTracker,
|
|
210
|
+
getIsSessionUpgradeAutoEnabled: () => true,
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
// Execute and verify
|
|
214
|
+
await expect(service.initialize()).rejects.toThrow('No solver available for challenge type: 1')
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('propagates sessionService errors', async () => {
|
|
218
|
+
// Setup
|
|
219
|
+
const error = new Error('Network error')
|
|
220
|
+
sessionService.initSession = vi.fn().mockRejectedValue(error)
|
|
221
|
+
|
|
222
|
+
const service = createSessionInitializationService({
|
|
223
|
+
getSessionService: () => sessionService,
|
|
224
|
+
challengeSolverService,
|
|
225
|
+
performanceTracker: mockPerformanceTracker,
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
// Execute and verify
|
|
229
|
+
await expect(service.initialize()).rejects.toThrow('Network error')
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('submits empty solution when solver throws, triggering verify-retry fallback', async () => {
|
|
233
|
+
// Setup: solver throws, but verifySession accepts empty solution (no retry)
|
|
234
|
+
TestScenarios.withChallengeRequired(sessionService)
|
|
235
|
+
const failingSolver = {
|
|
236
|
+
solve: vi.fn().mockRejectedValue(new Error('Solver failed')),
|
|
237
|
+
}
|
|
238
|
+
challengeSolverService.getSolver = vi.fn().mockReturnValue(failingSolver)
|
|
239
|
+
|
|
240
|
+
const service = createSessionInitializationService({
|
|
241
|
+
getSessionService: () => sessionService,
|
|
242
|
+
challengeSolverService,
|
|
243
|
+
performanceTracker: mockPerformanceTracker,
|
|
244
|
+
getIsSessionUpgradeAutoEnabled: () => true,
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
// Execute — should NOT throw; empty solution is submitted instead
|
|
248
|
+
await service.initialize()
|
|
249
|
+
|
|
250
|
+
// Verify empty solution was passed to verifySession
|
|
251
|
+
expect(sessionService.verifySession).toHaveBeenCalledWith({
|
|
252
|
+
solution: 'solver-failed',
|
|
253
|
+
challengeId: 'challenge-333',
|
|
254
|
+
challengeType: ChallengeType.TURNSTILE,
|
|
255
|
+
})
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('solver throws → empty verify triggers retry → different challenge type succeeds', async () => {
|
|
259
|
+
// Setup: solver throws on first challenge, verify says retry,
|
|
260
|
+
// second challenge uses a different solver that succeeds
|
|
261
|
+
TestScenarios.withChallengeRequired(sessionService)
|
|
262
|
+
|
|
263
|
+
// First call: solver throws; second call: solver succeeds
|
|
264
|
+
const failingSolver = { solve: vi.fn().mockRejectedValue(new Error('Turnstile domain error')) }
|
|
265
|
+
const successSolver = { solve: vi.fn().mockResolvedValue('hashcash-proof') }
|
|
266
|
+
challengeSolverService.getSolver = vi.fn().mockReturnValueOnce(failingSolver).mockReturnValueOnce(successSolver)
|
|
267
|
+
|
|
268
|
+
// First verify: retry; second verify: success
|
|
269
|
+
vi.mocked(sessionService.verifySession)
|
|
270
|
+
.mockResolvedValueOnce({ retry: true })
|
|
271
|
+
.mockResolvedValueOnce({ retry: false })
|
|
272
|
+
|
|
273
|
+
// Second challenge returns HASHCASH
|
|
274
|
+
vi.mocked(sessionService.requestChallenge)
|
|
275
|
+
.mockResolvedValueOnce({
|
|
276
|
+
challengeId: 'challenge-333',
|
|
277
|
+
challengeType: ChallengeType.TURNSTILE,
|
|
278
|
+
extra: {},
|
|
279
|
+
challengeData: { case: 'turnstile', value: { siteKey: 'test-sitekey', action: 'verify' } },
|
|
280
|
+
})
|
|
281
|
+
.mockResolvedValueOnce({
|
|
282
|
+
challengeId: 'challenge-444',
|
|
283
|
+
challengeType: ChallengeType.HASHCASH,
|
|
284
|
+
extra: {},
|
|
285
|
+
challengeData: { case: undefined },
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
const service = createSessionInitializationService({
|
|
289
|
+
getSessionService: () => sessionService,
|
|
290
|
+
challengeSolverService,
|
|
291
|
+
performanceTracker: mockPerformanceTracker,
|
|
292
|
+
getIsSessionUpgradeAutoEnabled: () => true,
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
await service.initialize()
|
|
296
|
+
|
|
297
|
+
// Verify flow: empty solution first, then real solution
|
|
298
|
+
expect(sessionService.verifySession).toHaveBeenCalledTimes(2)
|
|
299
|
+
expect(sessionService.verifySession).toHaveBeenNthCalledWith(1, {
|
|
300
|
+
solution: 'solver-failed',
|
|
301
|
+
challengeId: 'challenge-333',
|
|
302
|
+
challengeType: ChallengeType.TURNSTILE,
|
|
303
|
+
})
|
|
304
|
+
expect(sessionService.verifySession).toHaveBeenNthCalledWith(2, {
|
|
305
|
+
solution: 'hashcash-proof',
|
|
306
|
+
challengeId: 'challenge-444',
|
|
307
|
+
challengeType: ChallengeType.HASHCASH,
|
|
308
|
+
})
|
|
309
|
+
expect(sessionService.requestChallenge).toHaveBeenCalledTimes(2)
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it('solver keeps throwing + verify keeps returning retry → respects maxRetries', async () => {
|
|
313
|
+
// Setup: solver always throws, verify always says retry → should exhaust retries
|
|
314
|
+
TestScenarios.withChallengeRequired(sessionService)
|
|
315
|
+
const failingSolver = { solve: vi.fn().mockRejectedValue(new Error('Solver always fails')) }
|
|
316
|
+
challengeSolverService.getSolver = vi.fn().mockReturnValue(failingSolver)
|
|
317
|
+
vi.mocked(sessionService.verifySession).mockResolvedValue({ retry: true })
|
|
318
|
+
|
|
319
|
+
const service = createSessionInitializationService({
|
|
320
|
+
getSessionService: () => sessionService,
|
|
321
|
+
challengeSolverService,
|
|
322
|
+
performanceTracker: mockPerformanceTracker,
|
|
323
|
+
getIsSessionUpgradeAutoEnabled: () => true,
|
|
324
|
+
maxChallengeRetries: 2,
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
await expect(service.initialize()).rejects.toThrow(
|
|
328
|
+
'Maximum challenge retry attempts (2) exceeded after 3 attempts',
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
// Initial + 2 retries = 3 attempts
|
|
332
|
+
expect(sessionService.requestChallenge).toHaveBeenCalledTimes(3)
|
|
333
|
+
expect(sessionService.verifySession).toHaveBeenCalledTimes(3)
|
|
334
|
+
})
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
describe('analytics callbacks', () => {
|
|
338
|
+
it('fires onInitStarted when initialization begins', async () => {
|
|
339
|
+
const analytics = { onInitStarted: vi.fn() }
|
|
340
|
+
TestScenarios.withNoChallenge(sessionService)
|
|
341
|
+
|
|
342
|
+
const service = createSessionInitializationService({
|
|
343
|
+
getSessionService: () => sessionService,
|
|
344
|
+
challengeSolverService,
|
|
345
|
+
performanceTracker: mockPerformanceTracker,
|
|
346
|
+
analytics,
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
await service.initialize()
|
|
350
|
+
|
|
351
|
+
expect(analytics.onInitStarted).toHaveBeenCalledTimes(1)
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
it('reports needChallenge: false when no challenge required', async () => {
|
|
355
|
+
const analytics = { onInitCompleted: vi.fn() }
|
|
356
|
+
TestScenarios.withNoChallenge(sessionService)
|
|
357
|
+
|
|
358
|
+
const service = createSessionInitializationService({
|
|
359
|
+
getSessionService: () => sessionService,
|
|
360
|
+
challengeSolverService,
|
|
361
|
+
performanceTracker: mockPerformanceTracker,
|
|
362
|
+
analytics,
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
await service.initialize()
|
|
366
|
+
|
|
367
|
+
expect(analytics.onInitCompleted).toHaveBeenCalledWith(expect.objectContaining({ needChallenge: false }))
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
it('reports needChallenge: true when challenge required', async () => {
|
|
371
|
+
const analytics = { onInitCompleted: vi.fn() }
|
|
372
|
+
TestScenarios.withChallengeRequired(sessionService)
|
|
373
|
+
|
|
374
|
+
const service = createSessionInitializationService({
|
|
375
|
+
getSessionService: () => sessionService,
|
|
376
|
+
challengeSolverService,
|
|
377
|
+
performanceTracker: mockPerformanceTracker,
|
|
378
|
+
analytics,
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
await service.initialize()
|
|
382
|
+
|
|
383
|
+
expect(analytics.onInitCompleted).toHaveBeenCalledWith(expect.objectContaining({ needChallenge: true }))
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
it('fires onChallengeReceived with challenge details', async () => {
|
|
387
|
+
const analytics = { onChallengeReceived: vi.fn() }
|
|
388
|
+
TestScenarios.withChallengeRequired(sessionService, ChallengeType.HASHCASH)
|
|
389
|
+
|
|
390
|
+
const service = createSessionInitializationService({
|
|
391
|
+
getSessionService: () => sessionService,
|
|
392
|
+
challengeSolverService,
|
|
393
|
+
performanceTracker: mockPerformanceTracker,
|
|
394
|
+
getIsSessionUpgradeAutoEnabled: () => true,
|
|
395
|
+
analytics,
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
await service.initialize()
|
|
399
|
+
|
|
400
|
+
expect(analytics.onChallengeReceived).toHaveBeenCalledWith({
|
|
401
|
+
challengeType: String(ChallengeType.HASHCASH),
|
|
402
|
+
challengeId: 'challenge-333',
|
|
403
|
+
})
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
it('fires onVerifyCompleted on successful verification', async () => {
|
|
407
|
+
const analytics = { onVerifyCompleted: vi.fn() }
|
|
408
|
+
TestScenarios.withChallengeRequired(sessionService)
|
|
409
|
+
|
|
410
|
+
const service = createSessionInitializationService({
|
|
411
|
+
getSessionService: () => sessionService,
|
|
412
|
+
challengeSolverService,
|
|
413
|
+
performanceTracker: mockPerformanceTracker,
|
|
414
|
+
getIsSessionUpgradeAutoEnabled: () => true,
|
|
415
|
+
analytics,
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
await service.initialize()
|
|
419
|
+
|
|
420
|
+
expect(analytics.onVerifyCompleted).toHaveBeenCalledWith(
|
|
421
|
+
expect.objectContaining({ success: true, attemptNumber: 1 }),
|
|
422
|
+
)
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
it('tracks retry attempts through verification flow', async () => {
|
|
426
|
+
const analytics = { onVerifyCompleted: vi.fn() }
|
|
427
|
+
TestScenarios.withChallengeRequired(sessionService)
|
|
428
|
+
TestScenarios.withServerRetry(sessionService, 2) // Fail twice, succeed on 3rd
|
|
429
|
+
|
|
430
|
+
const service = createSessionInitializationService({
|
|
431
|
+
getSessionService: () => sessionService,
|
|
432
|
+
challengeSolverService,
|
|
433
|
+
performanceTracker: mockPerformanceTracker,
|
|
434
|
+
getIsSessionUpgradeAutoEnabled: () => true,
|
|
435
|
+
analytics,
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
await service.initialize()
|
|
439
|
+
|
|
440
|
+
// Should have been called 3 times (2 failures + 1 success)
|
|
441
|
+
expect(analytics.onVerifyCompleted).toHaveBeenCalledTimes(3)
|
|
442
|
+
expect(analytics.onVerifyCompleted).toHaveBeenNthCalledWith(
|
|
443
|
+
1,
|
|
444
|
+
expect.objectContaining({ success: false, attemptNumber: 1 }),
|
|
445
|
+
)
|
|
446
|
+
expect(analytics.onVerifyCompleted).toHaveBeenNthCalledWith(
|
|
447
|
+
2,
|
|
448
|
+
expect.objectContaining({ success: false, attemptNumber: 2 }),
|
|
449
|
+
)
|
|
450
|
+
expect(analytics.onVerifyCompleted).toHaveBeenNthCalledWith(
|
|
451
|
+
3,
|
|
452
|
+
expect.objectContaining({ success: true, attemptNumber: 3 }),
|
|
453
|
+
)
|
|
454
|
+
})
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
describe('edge cases', () => {
|
|
458
|
+
it('handles empty session ID from initSession', async () => {
|
|
459
|
+
// Setup
|
|
460
|
+
sessionService.initSession = vi.fn().mockResolvedValue({
|
|
461
|
+
sessionId: undefined,
|
|
462
|
+
needChallenge: false,
|
|
463
|
+
extra: {},
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
const service = createSessionInitializationService({
|
|
467
|
+
getSessionService: () => sessionService,
|
|
468
|
+
challengeSolverService,
|
|
469
|
+
performanceTracker: mockPerformanceTracker,
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
// Execute
|
|
473
|
+
const result = await service.initialize()
|
|
474
|
+
|
|
475
|
+
// Verify behavior - should return null when sessionId is undefined
|
|
476
|
+
expect(result.sessionId).toBeNull()
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
it('handles None bot detection type', async () => {
|
|
480
|
+
// Setup
|
|
481
|
+
TestScenarios.withChallengeRequired(sessionService, ChallengeType.UNSPECIFIED)
|
|
482
|
+
const noneSolver = {
|
|
483
|
+
solve: vi.fn().mockResolvedValue(''),
|
|
484
|
+
}
|
|
485
|
+
challengeSolverService.getSolver = vi
|
|
486
|
+
.fn()
|
|
487
|
+
.mockImplementation((type) => (type === ChallengeType.UNSPECIFIED ? noneSolver : null))
|
|
488
|
+
|
|
489
|
+
const service = createSessionInitializationService({
|
|
490
|
+
getSessionService: () => sessionService,
|
|
491
|
+
challengeSolverService,
|
|
492
|
+
performanceTracker: mockPerformanceTracker,
|
|
493
|
+
getIsSessionUpgradeAutoEnabled: () => true,
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
// Execute
|
|
497
|
+
await service.initialize()
|
|
498
|
+
|
|
499
|
+
// Verify None type was handled
|
|
500
|
+
expect(challengeSolverService.getSolver).toHaveBeenCalledWith(ChallengeType.UNSPECIFIED)
|
|
501
|
+
expect(noneSolver.solve).toHaveBeenCalled()
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
it('does not complete challenge flow when auto-upgrade is disabled', async () => {
|
|
505
|
+
// Setup
|
|
506
|
+
TestScenarios.withChallengeRequired(sessionService)
|
|
507
|
+
|
|
508
|
+
const service = createSessionInitializationService({
|
|
509
|
+
getSessionService: () => sessionService,
|
|
510
|
+
challengeSolverService,
|
|
511
|
+
performanceTracker: mockPerformanceTracker,
|
|
512
|
+
getIsSessionUpgradeAutoEnabled: () => false,
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
// Execute
|
|
516
|
+
const result = await service.initialize()
|
|
517
|
+
|
|
518
|
+
// Verify behavior - session initialized but challenge not handled
|
|
519
|
+
// sessionId is returned regardless of challenge status (null if not provided by backend)
|
|
520
|
+
expect(result).toEqual({
|
|
521
|
+
sessionId: 'new-session-222',
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
// Verify challenge flow was NOT executed
|
|
525
|
+
expect(sessionService.initSession).toHaveBeenCalled()
|
|
526
|
+
expect(sessionService.requestChallenge).not.toHaveBeenCalled()
|
|
527
|
+
expect(sessionService.verifySession).not.toHaveBeenCalled()
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
it('defaults to disabled when callback is not provided', async () => {
|
|
531
|
+
// Setup
|
|
532
|
+
TestScenarios.withChallengeRequired(sessionService)
|
|
533
|
+
|
|
534
|
+
const service = createSessionInitializationService({
|
|
535
|
+
getSessionService: () => sessionService,
|
|
536
|
+
challengeSolverService,
|
|
537
|
+
performanceTracker: mockPerformanceTracker,
|
|
538
|
+
// No getIsSessionUpgradeAutoEnabled callback provided
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
// Execute
|
|
542
|
+
const result = await service.initialize()
|
|
543
|
+
|
|
544
|
+
// Verify behavior - defaults to disabled (opt-in)
|
|
545
|
+
// sessionId is returned regardless of challenge status
|
|
546
|
+
expect(result).toEqual({
|
|
547
|
+
sessionId: 'new-session-222',
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
// Verify challenge flow was NOT executed (default disabled)
|
|
551
|
+
expect(sessionService.initSession).toHaveBeenCalled()
|
|
552
|
+
expect(sessionService.requestChallenge).not.toHaveBeenCalled()
|
|
553
|
+
expect(sessionService.verifySession).not.toHaveBeenCalled()
|
|
554
|
+
})
|
|
555
|
+
})
|
|
556
|
+
})
|
|
557
|
+
})
|