|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
| 3 | +import json as jsonlib |
3 | 4 | from dataclasses import dataclass, field |
4 | 5 | from typing import TYPE_CHECKING, Any |
5 | 6 |
|
| 7 | +import impit |
6 | 8 | import pytest |
| 9 | +from werkzeug import Request, Response |
7 | 10 |
|
8 | 11 | import apify_client as apify_client_module |
9 | 12 | import apify_client.http_clients as http_clients_module |
@@ -423,3 +426,125 @@ async def call(self, *, method: str, url: str, **kwargs: Any) -> HttpResponse: |
423 | 426 |
|
424 | 427 | assert result is not None |
425 | 428 | assert result['data']['id'] == 'test-dataset' |
| 429 | + |
| 430 | + |
| 431 | +class PreparingHttpClient(HttpClient): |
| 432 | + """A custom sync HTTP client that sends requests prepared by the base-class helpers.""" |
| 433 | + |
| 434 | + def __init__(self, token: str | None = None) -> None: |
| 435 | + super().__init__(token=token) |
| 436 | + self._impit_client = impit.Client() |
| 437 | + |
| 438 | + def call( |
| 439 | + self, |
| 440 | + *, |
| 441 | + method: str, |
| 442 | + url: str, |
| 443 | + headers: dict[str, str] | None = None, |
| 444 | + params: dict[str, Any] | None = None, |
| 445 | + data: str | bytes | bytearray | None = None, |
| 446 | + json: Any = None, |
| 447 | + **_kwargs: Any, |
| 448 | + ) -> HttpResponse: |
| 449 | + headers, params, content = self._prepare_request_call(headers=headers, params=params, data=data, json=json) |
| 450 | + url = self._build_url_with_params(url, params=params) |
| 451 | + return self._impit_client.request(method=method, url=url, headers=headers, content=content) |
| 452 | + |
| 453 | + |
| 454 | +class PreparingHttpClientAsync(HttpClientAsync): |
| 455 | + """A custom async HTTP client that sends requests prepared by the base-class helpers.""" |
| 456 | + |
| 457 | + def __init__(self, token: str | None = None) -> None: |
| 458 | + super().__init__(token=token) |
| 459 | + self._impit_client = impit.AsyncClient() |
| 460 | + |
| 461 | + async def call( |
| 462 | + self, |
| 463 | + *, |
| 464 | + method: str, |
| 465 | + url: str, |
| 466 | + headers: dict[str, str] | None = None, |
| 467 | + params: dict[str, Any] | None = None, |
| 468 | + data: str | bytes | bytearray | None = None, |
| 469 | + json: Any = None, |
| 470 | + **_kwargs: Any, |
| 471 | + ) -> HttpResponse: |
| 472 | + headers, params, content = self._prepare_request_call(headers=headers, params=params, data=data, json=json) |
| 473 | + url = self._build_url_with_params(url, params=params) |
| 474 | + return await self._impit_client.request(method=method, url=url, headers=headers, content=content) |
| 475 | + |
| 476 | + |
| 477 | +def _echo_headers(request: Request) -> Response: |
| 478 | + """Respond with the received request headers so tests can assert on them.""" |
| 479 | + return Response( |
| 480 | + response=jsonlib.dumps({'received_headers': dict(request.headers)}), |
| 481 | + status=200, |
| 482 | + content_type='application/json', |
| 483 | + ) |
| 484 | + |
| 485 | + |
| 486 | +def test_custom_http_client_sends_token_from_classmethod(httpserver: HTTPServer) -> None: |
| 487 | + """Token passed to with_custom_http_client is sent as the Authorization header by the custom client.""" |
| 488 | + httpserver.expect_request('/v2/datasets/test-dataset').respond_with_handler(_echo_headers) |
| 489 | + |
| 490 | + api_url = httpserver.url_for('/').removesuffix('/') |
| 491 | + client = ApifyClient.with_custom_http_client( |
| 492 | + token='test_token', |
| 493 | + api_url=api_url, |
| 494 | + http_client=PreparingHttpClient(), |
| 495 | + ) |
| 496 | + |
| 497 | + result = client.dataset('test-dataset')._get(timeout='short') |
| 498 | + |
| 499 | + assert result is not None |
| 500 | + assert result['received_headers']['Authorization'] == 'Bearer test_token' |
| 501 | + |
| 502 | + |
| 503 | +async def test_custom_http_client_async_sends_token_from_classmethod(httpserver: HTTPServer) -> None: |
| 504 | + """Token passed to async with_custom_http_client is sent as the Authorization header by the custom client.""" |
| 505 | + httpserver.expect_request('/v2/datasets/test-dataset').respond_with_handler(_echo_headers) |
| 506 | + |
| 507 | + api_url = httpserver.url_for('/').removesuffix('/') |
| 508 | + client = ApifyClientAsync.with_custom_http_client( |
| 509 | + token='test_token', |
| 510 | + api_url=api_url, |
| 511 | + http_client=PreparingHttpClientAsync(), |
| 512 | + ) |
| 513 | + |
| 514 | + result = await client.dataset('test-dataset')._get(timeout='short') |
| 515 | + |
| 516 | + assert result is not None |
| 517 | + assert result['received_headers']['Authorization'] == 'Bearer test_token' |
| 518 | + |
| 519 | + |
| 520 | +def test_custom_http_client_impit_instance_sends_token(httpserver: HTTPServer) -> None: |
| 521 | + """Token from with_custom_http_client reaches the wire even for a pre-built tokenless ImpitHttpClient.""" |
| 522 | + httpserver.expect_request('/v2/datasets/test-dataset').respond_with_handler(_echo_headers) |
| 523 | + |
| 524 | + api_url = httpserver.url_for('/').removesuffix('/') |
| 525 | + client = ApifyClient.with_custom_http_client(token='test_token', api_url=api_url, http_client=ImpitHttpClient()) |
| 526 | + |
| 527 | + result = client.dataset('test-dataset')._get(timeout='short') |
| 528 | + |
| 529 | + assert result is not None |
| 530 | + assert result['received_headers']['Authorization'] == 'Bearer test_token' |
| 531 | + |
| 532 | + |
| 533 | +def test_custom_http_client_keeps_own_token() -> None: |
| 534 | + """An Authorization header configured on the custom client itself is not overridden by with_custom_http_client.""" |
| 535 | + http_client = PreparingHttpClient(token='client_token') |
| 536 | + |
| 537 | + ApifyClient.with_custom_http_client(token='outer_token', http_client=http_client) |
| 538 | + |
| 539 | + assert http_client._headers['Authorization'] == 'Bearer client_token' |
| 540 | + |
| 541 | + |
| 542 | +def test_custom_http_client_keeps_differently_cased_authorization() -> None: |
| 543 | + """A lowercase 'authorization' header on the custom client is not duplicated by the token injection.""" |
| 544 | + http_client = PreparingHttpClient() |
| 545 | + http_client._headers['authorization'] = 'Bearer client_token' |
| 546 | + |
| 547 | + ApifyClient.with_custom_http_client(token='outer_token', http_client=http_client) |
| 548 | + |
| 549 | + assert 'Authorization' not in http_client._headers |
| 550 | + assert http_client._headers['authorization'] == 'Bearer client_token' |
0 commit comments