|
1 | 1 | import { describe, expect, mock, test } from 'bun:test' |
2 | | -import { calculateRetryDelay, getRandomFrom, withRetry } from './strategy.js' |
| 2 | +import type { RequestOptions, ScrapeConfig } from '../types/index.js' |
| 3 | +import type { ValidateResponseContext } from '../types/validate.js' |
| 4 | +import { calculateRetryDelay, executeCustomRequest, getRandomFrom, withRetry } from './strategy.js' |
3 | 5 |
|
4 | 6 | describe('calculateRetryDelay', () => { |
5 | 7 | describe('exponential backoff', () => { |
@@ -284,24 +286,183 @@ describe('executeBrowserRequest', () => { |
284 | 286 |
|
285 | 287 | describe('executeCustomRequest', () => { |
286 | 288 | describe('custom fetch function', () => { |
287 | | - // TODO: should throw error when custom fetch not provided |
288 | | - // TODO: should execute custom fetch function |
289 | | - // TODO: should pass url to custom fetch |
290 | | - // TODO: should pass options to custom fetch |
291 | | - // TODO: should return custom response |
| 289 | + test('should throw error when custom fetch not provided', async () => { |
| 290 | + const config: ScrapeConfig = {} |
| 291 | + const options: RequestOptions = {} |
| 292 | + |
| 293 | + const resultFn = () => executeCustomRequest('https://example.com', config, options) |
| 294 | + |
| 295 | + expect(resultFn()).rejects.toThrow('Custom fetch function not provided') |
| 296 | + }) |
| 297 | + |
| 298 | + test('should execute custom fetch function', async () => { |
| 299 | + const mockFn = mock(async () => ({ data: 'test' })) |
| 300 | + const config: ScrapeConfig = { custom: { fn: mockFn } } |
| 301 | + const options: RequestOptions = {} |
| 302 | + |
| 303 | + await executeCustomRequest('https://example.com', config, options) |
| 304 | + |
| 305 | + expect(mockFn).toHaveBeenCalledTimes(1) |
| 306 | + }) |
| 307 | + |
| 308 | + test('should pass url to custom fetch', async () => { |
| 309 | + let capturedUrl: string | undefined |
| 310 | + const config: ScrapeConfig = { |
| 311 | + custom: { |
| 312 | + fn: async (url) => { |
| 313 | + capturedUrl = url |
| 314 | + return { data: 'test' } |
| 315 | + }, |
| 316 | + }, |
| 317 | + } |
| 318 | + const options: RequestOptions = {} |
| 319 | + |
| 320 | + await executeCustomRequest('https://example.com/test', config, options) |
| 321 | + |
| 322 | + expect(capturedUrl).toBe('https://example.com/test') |
| 323 | + }) |
| 324 | + |
| 325 | + test('should pass options to custom fetch', async () => { |
| 326 | + let capturedOptions: RequestOptions | undefined |
| 327 | + const config: ScrapeConfig = { |
| 328 | + custom: { |
| 329 | + fn: async (_url, options) => { |
| 330 | + capturedOptions = options |
| 331 | + return { data: 'test' } |
| 332 | + }, |
| 333 | + }, |
| 334 | + } |
| 335 | + const options: RequestOptions = { |
| 336 | + headers: { 'X-Test': 'value' }, |
| 337 | + timeout: 5000, |
| 338 | + proxy: 'http://proxy.com:8080', |
| 339 | + } |
| 340 | + |
| 341 | + await executeCustomRequest('https://example.com', config, options) |
| 342 | + |
| 343 | + expect(capturedOptions).toEqual({ |
| 344 | + headers: { 'X-Test': 'value' }, |
| 345 | + timeout: 5000, |
| 346 | + proxy: 'http://proxy.com:8080', |
| 347 | + }) |
| 348 | + }) |
| 349 | + |
| 350 | + test('should return custom response', async () => { |
| 351 | + const customResponse = { data: 'test', count: 42 } |
| 352 | + const config: ScrapeConfig = { |
| 353 | + custom: { fn: async () => customResponse }, |
| 354 | + } |
| 355 | + const options: RequestOptions = {} |
| 356 | + |
| 357 | + const result = await executeCustomRequest('https://example.com', config, options) |
| 358 | + |
| 359 | + expect(result.mechanism).toBe('custom') |
| 360 | + expect(result.response).toEqual(customResponse) |
| 361 | + }) |
292 | 362 | }) |
293 | 363 |
|
294 | 364 | describe('validation', () => { |
295 | | - // TODO: should validate response when validator provided |
296 | | - // TODO: should pass mechanism and response to validator |
297 | | - // TODO: should throw error when validation fails |
298 | | - // TODO: should skip validation when validator not provided |
| 365 | + test('should validate response when validator provided', async () => { |
| 366 | + const mockValidator = mock(() => true) |
| 367 | + const config: ScrapeConfig = { |
| 368 | + custom: { fn: async () => ({ status: 'ok' }) }, |
| 369 | + options: { validateResponse: mockValidator }, |
| 370 | + } |
| 371 | + const options: RequestOptions = {} |
| 372 | + |
| 373 | + await executeCustomRequest('https://example.com', config, options) |
| 374 | + |
| 375 | + expect(mockValidator).toHaveBeenCalledTimes(1) |
| 376 | + }) |
| 377 | + |
| 378 | + test('should pass mechanism and response to validator', async () => { |
| 379 | + let capturedContext: ValidateResponseContext | undefined |
| 380 | + const customResponse = { status: 'ok' } |
| 381 | + const config: ScrapeConfig = { |
| 382 | + custom: { fn: async () => customResponse }, |
| 383 | + options: { |
| 384 | + validateResponse: (context) => { |
| 385 | + capturedContext = context |
| 386 | + return true |
| 387 | + }, |
| 388 | + }, |
| 389 | + } |
| 390 | + const options: RequestOptions = {} |
| 391 | + |
| 392 | + await executeCustomRequest('https://example.com', config, options) |
| 393 | + |
| 394 | + expect(capturedContext?.mechanism).toBe('custom') |
| 395 | + expect(capturedContext?.response).toEqual(customResponse) |
| 396 | + }) |
| 397 | + |
| 398 | + test('should throw error when validation fails', async () => { |
| 399 | + const config: ScrapeConfig = { |
| 400 | + custom: { fn: async () => ({ status: 'error' }) }, |
| 401 | + options: { |
| 402 | + validateResponse: () => false, |
| 403 | + }, |
| 404 | + } |
| 405 | + const options: RequestOptions = {} |
| 406 | + |
| 407 | + const resultFn = () => executeCustomRequest('https://example.com', config, options) |
| 408 | + |
| 409 | + expect(resultFn()).rejects.toThrow('Response validation failed') |
| 410 | + }) |
| 411 | + |
| 412 | + test('should skip validation when validator not provided', async () => { |
| 413 | + const config: ScrapeConfig = { |
| 414 | + custom: { fn: async () => ({ data: 'test' }) }, |
| 415 | + } |
| 416 | + const options: RequestOptions = {} |
| 417 | + |
| 418 | + const result = await executeCustomRequest('https://example.com', config, options) |
| 419 | + |
| 420 | + expect(result.mechanism).toBe('custom') |
| 421 | + expect(result.response).toEqual({ data: 'test' }) |
| 422 | + }) |
299 | 423 | }) |
300 | 424 |
|
301 | 425 | describe('error handling', () => { |
302 | | - // TODO: should throw error when response is null |
303 | | - // TODO: should propagate custom fetch errors |
304 | | - // TODO: should handle validation errors |
| 426 | + test('should throw error when response is null', async () => { |
| 427 | + const config: ScrapeConfig = { |
| 428 | + custom: { fn: async () => null }, |
| 429 | + } |
| 430 | + const options: RequestOptions = {} |
| 431 | + const resultFn = () => executeCustomRequest('https://example.com', config, options) |
| 432 | + |
| 433 | + expect(resultFn()).rejects.toThrow('No response received') |
| 434 | + }) |
| 435 | + |
| 436 | + test('should propagate custom fetch errors', async () => { |
| 437 | + const config: ScrapeConfig = { |
| 438 | + custom: { |
| 439 | + fn: async () => { |
| 440 | + throw new Error('Custom fetch failed') |
| 441 | + }, |
| 442 | + }, |
| 443 | + } |
| 444 | + const options: RequestOptions = {} |
| 445 | + |
| 446 | + const resultFn = () => executeCustomRequest('https://example.com', config, options) |
| 447 | + |
| 448 | + expect(resultFn()).rejects.toThrow('Custom fetch failed') |
| 449 | + }) |
| 450 | + |
| 451 | + test('should handle validation errors', async () => { |
| 452 | + const config: ScrapeConfig = { |
| 453 | + custom: { fn: async () => ({ data: 'test' }) }, |
| 454 | + options: { |
| 455 | + validateResponse: () => { |
| 456 | + throw new Error('Validation error') |
| 457 | + }, |
| 458 | + }, |
| 459 | + } |
| 460 | + const options: RequestOptions = {} |
| 461 | + |
| 462 | + const resultFn = () => executeCustomRequest('https://example.com', config, options) |
| 463 | + |
| 464 | + expect(resultFn()).rejects.toThrow('Validation error') |
| 465 | + }) |
305 | 466 | }) |
306 | 467 | }) |
307 | 468 |
|
|
0 commit comments