1+ """Pytest configuration and fixtures for integration tests.
2+
3+ Provides sync/async client parametrization and shared test fixtures.
4+ """
5+
6+ from __future__ import annotations
7+
8+ import asyncio
19import json
210import os
3- from collections .abc import Generator
11+ import time
12+ from typing import TYPE_CHECKING , Any , TypeVar , overload
413
514import pytest
615
716from .utils import DatasetFixture , KvsFixture , get_crypto_random_object_id
817from apify_client import ApifyClient , ApifyClientAsync
918from apify_client ._utils import create_hmac_signature , create_storage_content_signature
1019
20+ if TYPE_CHECKING :
21+ from collections .abc import Coroutine , Generator
22+
23+ # Environment variable names for test configuration
1124TOKEN_ENV_VAR = 'APIFY_TEST_USER_API_TOKEN'
1225TOKEN_ENV_VAR_2 = 'APIFY_TEST_USER_2_API_TOKEN'
1326API_URL_ENV_VAR = 'APIFY_INTEGRATION_TESTS_API_URL'
1427
28+ T = TypeVar ('T' )
29+
30+
31+ # ============================================================================
32+ # Helper functions for sync/async test compatibility
33+ # ============================================================================
34+
35+
36+ @overload
37+ async def maybe_await (value : Coroutine [Any , Any , T ]) -> T : ...
38+
39+
40+ @overload
41+ async def maybe_await (value : T ) -> T : ...
42+
43+
44+ async def maybe_await (value : T | Coroutine [Any , Any , T ]) -> T :
45+ """Await coroutines, pass through other values.
46+
47+ Enables unified test code for both sync and async clients:
48+ result = await maybe_await(client.datasets().list())
49+ """
50+ if hasattr (value , '__await__' ):
51+ return await value # type: ignore[misc,return-value]
52+ return value
53+
54+
55+ async def maybe_sleep (seconds : float , * , is_async : bool ) -> None :
56+ """Sleep using asyncio or time.sleep based on client type."""
57+ if is_async :
58+ await asyncio .sleep (seconds )
59+ else :
60+ time .sleep (seconds ) # noqa: ASYNC251
61+
62+
63+ # ============================================================================
64+ # Session-scoped fixtures (created once per test session)
65+ # ============================================================================
66+
1567
1668@pytest .fixture (scope = 'session' )
1769def api_token () -> str :
70+ """Primary test user API token."""
1871 token = os .getenv (TOKEN_ENV_VAR )
1972 if not token :
2073 raise RuntimeError (f'{ TOKEN_ENV_VAR } environment variable is missing, cannot run tests!' )
@@ -23,44 +76,27 @@ def api_token() -> str:
2376
2477@pytest .fixture (scope = 'session' )
2578def api_token_2 () -> str :
26- """API token for the second test user for storage permission tests."""
79+ """Secondary test user API token for permission tests."""
2780 token = os .getenv (TOKEN_ENV_VAR_2 )
2881 if not token :
2982 raise RuntimeError (f'{ TOKEN_ENV_VAR_2 } environment variable is missing, cannot run permission tests!' )
3083 return token
3184
3285
33- @pytest .fixture
34- def apify_client (api_token : str ) -> ApifyClient :
35- return ApifyClient (api_token , api_url = os .getenv (API_URL_ENV_VAR ))
36-
37-
38- # This fixture can't be session-scoped,
39- # because then you start getting `RuntimeError: Event loop is closed` errors,
40- # because `impit.AsyncClient` in `ApifyClientAsync` tries to reuse the same event loop across requests,
41- # but `pytest-asyncio` closes the event loop after each test,
42- # and uses a new one for the next test.
43- @pytest .fixture
44- def apify_client_async (api_token : str ) -> ApifyClientAsync :
45- return ApifyClientAsync (api_token , api_url = os .getenv (API_URL_ENV_VAR ))
46-
47-
4886@pytest .fixture (scope = 'session' )
4987def test_dataset_of_another_user (api_token_2 : str ) -> Generator [DatasetFixture ]:
50- """Pre-existing named dataset of another test user with restricted access."""
88+ """Dataset owned by secondary user for testing cross-user access restrictions ."""
5189 client = ApifyClient (api_token_2 , api_url = os .getenv (API_URL_ENV_VAR ))
5290
91+ # Create dataset with test data
5392 dataset_name = f'API-test-permissions-{ get_crypto_random_object_id ()} '
5493 dataset = client .datasets ().get_or_create (name = dataset_name )
5594 dataset_client = client .dataset (dataset_id = dataset .id )
5695 expected_content = [{'item1' : 1 , 'item2' : 2 , 'item3' : 3 }, {'item1' : 4 , 'item2' : 5 , 'item3' : 6 }]
57-
58- # Push data to dataset
5996 dataset_client .push_items (json .dumps (expected_content ))
6097
98+ # Generate signature for authenticated access
6199 assert dataset .url_signing_secret_key is not None
62-
63- # Generate signature for the test
64100 signature = create_storage_content_signature (
65101 resource_id = dataset .id ,
66102 url_signing_secret_key = dataset .url_signing_secret_key ,
@@ -69,29 +105,29 @@ def test_dataset_of_another_user(api_token_2: str) -> Generator[DatasetFixture]:
69105 yield DatasetFixture (
70106 id = dataset .id ,
71107 signature = signature ,
72- expected_content = [{ 'item1' : 1 , 'item2' : 2 , 'item3' : 3 }, { 'item1' : 4 , 'item2' : 5 , 'item3' : 6 }] ,
108+ expected_content = expected_content ,
73109 )
74110
75111 dataset_client .delete ()
76112
77113
78114@pytest .fixture (scope = 'session' )
79115def test_kvs_of_another_user (api_token_2 : str ) -> Generator [KvsFixture ]:
80- """Pre-existing named key value store of another test user with restricted access."""
116+ """Key- value store owned by secondary user for testing cross-user access restrictions ."""
81117 client = ApifyClient (api_token_2 , api_url = os .getenv (API_URL_ENV_VAR ))
82118
119+ # Create key-value store with test data
83120 kvs_name = f'API-test-permissions-{ get_crypto_random_object_id ()} '
84121 kvs = client .key_value_stores ().get_or_create (name = kvs_name )
85122 kvs_client = client .key_value_store (key_value_store_id = kvs .id )
86123 expected_content = {'key1' : 1 , 'key2' : 2 , 'key3' : 3 }
87-
88- # Push data to kvs
89124 for key , value in expected_content .items ():
90125 kvs_client .set_record (key , value )
91126
92- # Generate signature for the test
127+ # Generate signatures for authenticated access
93128 signature = create_storage_content_signature (
94- resource_id = kvs .id , url_signing_secret_key = kvs .url_signing_secret_key or ''
129+ resource_id = kvs .id ,
130+ url_signing_secret_key = kvs .url_signing_secret_key or '' ,
95131 )
96132
97133 yield KvsFixture (
@@ -102,3 +138,42 @@ def test_kvs_of_another_user(api_token_2: str) -> Generator[KvsFixture]:
102138 )
103139
104140 kvs_client .delete ()
141+
142+
143+ # ============================================================================
144+ # Function-scoped fixtures (created for each test)
145+ # ============================================================================
146+
147+
148+ @pytest .fixture
149+ def apify_client (api_token : str ) -> ApifyClient :
150+ """Sync Apify client instance."""
151+ return ApifyClient (api_token , api_url = os .getenv (API_URL_ENV_VAR ))
152+
153+
154+ @pytest .fixture
155+ def apify_client_async (api_token : str ) -> ApifyClientAsync :
156+ """Async Apify client instance."""
157+ return ApifyClientAsync (api_token , api_url = os .getenv (API_URL_ENV_VAR ))
158+
159+
160+ @pytest .fixture (params = ['sync' , 'async' ])
161+ def client_type (request : pytest .FixtureRequest ) -> str :
162+ """Parametrize tests to run with both sync and async clients."""
163+ return request .param
164+
165+
166+ @pytest .fixture
167+ def client (
168+ client_type : str ,
169+ apify_client : ApifyClient ,
170+ apify_client_async : ApifyClientAsync ,
171+ ) -> ApifyClient | ApifyClientAsync :
172+ """Return sync or async client based on parametrization."""
173+ return apify_client if client_type == 'sync' else apify_client_async
174+
175+
176+ @pytest .fixture
177+ def is_async (client_type : str ) -> bool :
178+ """True if current test is using async client."""
179+ return client_type == 'async'
0 commit comments