|
1 | | -import { beforeEach, describe, expect, it, vi } from 'vitest'; |
| 1 | +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; |
2 | 2 |
|
3 | 3 | import type { ProtectCheckResource } from '@/types'; |
4 | 4 |
|
5 | | -import { executeProtectCheck } from '../protectCheck'; |
| 5 | +import { DEFAULT_PROTECT_CHECK_LOAD_TIMEOUT_MS, executeProtectCheck } from '../protectCheck'; |
6 | 6 |
|
7 | 7 | const fakeContainer = (): HTMLDivElement => ({}) as HTMLDivElement; |
8 | 8 |
|
@@ -199,4 +199,131 @@ describe('executeProtectCheck', () => { |
199 | 199 | }); |
200 | 200 | }); |
201 | 201 | }); |
| 202 | + |
| 203 | + describe('the load bound covers the handoff and nothing after it', () => { |
| 204 | + beforeEach(() => { |
| 205 | + // Fake only the timer functions under test. Faking the whole clock also stalls the |
| 206 | + // machinery that settles a dynamic import, so the module would never finish loading. |
| 207 | + vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] }); |
| 208 | + }); |
| 209 | + afterEach(() => { |
| 210 | + vi.useRealTimers(); |
| 211 | + }); |
| 212 | + |
| 213 | + // A network that accepts the connection and then never answers is the one load failure that |
| 214 | + // cannot report itself — every other one rejects the import on its own. |
| 215 | + it('rejects as a load failure when the module never arrives', async () => { |
| 216 | + vi.doMock('https://protect.example.com/sdk-hangs.js', () => new Promise(() => {})); |
| 217 | + |
| 218 | + const running = executeProtectCheck( |
| 219 | + protectCheck({ sdkUrl: 'https://protect.example.com/sdk-hangs.js' }), |
| 220 | + fakeContainer(), |
| 221 | + ); |
| 222 | + const assertion = expect(running).rejects.toMatchObject({ code: 'protect_check_script_load_failed' }); |
| 223 | + |
| 224 | + await vi.advanceTimersByTimeAsync(DEFAULT_PROTECT_CHECK_LOAD_TIMEOUT_MS + 1); |
| 225 | + await assertion; |
| 226 | + }); |
| 227 | + |
| 228 | + it('honours a per-instance loadTimeoutMs override instead of the default', async () => { |
| 229 | + vi.doMock('https://protect.example.com/sdk-hangs-2.js', () => new Promise(() => {})); |
| 230 | + |
| 231 | + // Expressed relative to the default, and LONGER than it, so that outliving the default is |
| 232 | + // itself the proof the override replaced it rather than racing alongside it. |
| 233 | + const override = DEFAULT_PROTECT_CHECK_LOAD_TIMEOUT_MS * 2; |
| 234 | + const running = executeProtectCheck( |
| 235 | + protectCheck({ sdkUrl: 'https://protect.example.com/sdk-hangs-2.js' }), |
| 236 | + fakeContainer(), |
| 237 | + { loadTimeoutMs: override }, |
| 238 | + ); |
| 239 | + let settled = false; |
| 240 | + const watch = running.then( |
| 241 | + () => (settled = true), |
| 242 | + () => (settled = true), |
| 243 | + ); |
| 244 | + |
| 245 | + await vi.advanceTimersByTimeAsync(DEFAULT_PROTECT_CHECK_LOAD_TIMEOUT_MS + 1); |
| 246 | + expect(settled).toBe(false); |
| 247 | + |
| 248 | + await vi.advanceTimersByTimeAsync(override); |
| 249 | + await watch; |
| 250 | + expect(settled).toBe(true); |
| 251 | + }); |
| 252 | + |
| 253 | + // An abort mid-load has to settle the operation, not leave it pending for the whole bound |
| 254 | + // holding its closures and timer, and it is a cancellation rather than a load failure. |
| 255 | + it('settles as an abort when the caller aborts during a stalled load', async () => { |
| 256 | + vi.doMock('https://protect.example.com/sdk-hangs-3.js', () => new Promise(() => {})); |
| 257 | + |
| 258 | + const controller = new AbortController(); |
| 259 | + const running = executeProtectCheck( |
| 260 | + protectCheck({ sdkUrl: 'https://protect.example.com/sdk-hangs-3.js' }), |
| 261 | + fakeContainer(), |
| 262 | + { signal: controller.signal }, |
| 263 | + ); |
| 264 | + const assertion = expect(running).rejects.toMatchObject({ code: 'protect_check_aborted' }); |
| 265 | + |
| 266 | + controller.abort(); |
| 267 | + // No timer advanced: the abort alone must settle it, well before the load bound. |
| 268 | + await assertion; |
| 269 | + }); |
| 270 | + |
| 271 | + // setTimeout stores its delay in a signed 32-bit int, so an oversized value overflows and |
| 272 | + // fires immediately — failing every load instantly, the opposite of what was configured. |
| 273 | + it('clamps an oversized loadTimeoutMs instead of overflowing the timer', async () => { |
| 274 | + vi.doMock('https://protect.example.com/sdk-hangs-4.js', () => new Promise(() => {})); |
| 275 | + |
| 276 | + const running = executeProtectCheck( |
| 277 | + protectCheck({ sdkUrl: 'https://protect.example.com/sdk-hangs-4.js' }), |
| 278 | + fakeContainer(), |
| 279 | + { loadTimeoutMs: 2_147_483_648 }, |
| 280 | + ); |
| 281 | + let settled = false; |
| 282 | + const watch = running.then( |
| 283 | + () => (settled = true), |
| 284 | + () => (settled = true), |
| 285 | + ); |
| 286 | + |
| 287 | + // An overflowed timer would already have fired by now. |
| 288 | + await vi.advanceTimersByTimeAsync(1_000); |
| 289 | + expect(settled).toBe(false); |
| 290 | + |
| 291 | + await vi.advanceTimersByTimeAsync(600_000); |
| 292 | + await watch; |
| 293 | + expect(settled).toBe(true); |
| 294 | + }); |
| 295 | + |
| 296 | + // The point of the whole change: the challenge owns its own duration. A challenge running far |
| 297 | + // longer than any load bound must still resolve — proof-of-transfer moves a server-chosen |
| 298 | + // number of bytes, and a host-side wall would abort it as a "timeout". |
| 299 | + it('never bounds the challenge once the module has taken control', async () => { |
| 300 | + let running = false; |
| 301 | + let finish!: (proofToken: string) => void; |
| 302 | + vi.doMock('https://protect.example.com/sdk-slow.js', () => ({ |
| 303 | + default: () => { |
| 304 | + running = true; |
| 305 | + return new Promise<string>(resolve => (finish = resolve)); |
| 306 | + }, |
| 307 | + })); |
| 308 | + |
| 309 | + const loadTimeoutMs = 5_000; |
| 310 | + const execution = executeProtectCheck( |
| 311 | + protectCheck({ sdkUrl: 'https://protect.example.com/sdk-slow.js' }), |
| 312 | + fakeContainer(), |
| 313 | + { loadTimeoutMs }, |
| 314 | + ); |
| 315 | + const assertion = expect(execution).resolves.toBe('proof-after-ages'); |
| 316 | + |
| 317 | + // Wait on the real event loop until the module has taken control, so what follows is |
| 318 | + // unambiguously time spent in the CHALLENGE rather than in the load. |
| 319 | + while (!running) { |
| 320 | + await new Promise(resolve => setImmediate(resolve)); |
| 321 | + } |
| 322 | + |
| 323 | + // Burn far more time than the load bound. Nothing may abort the challenge for it. |
| 324 | + await vi.advanceTimersByTimeAsync(360 * loadTimeoutMs); |
| 325 | + finish('proof-after-ages'); |
| 326 | + await assertion; |
| 327 | + }); |
| 328 | + }); |
202 | 329 | }); |
0 commit comments