Skip to content

Commit 9b3e35a

Browse files
committed
Better organization of unit & integration tests
1 parent efb1dc1 commit 9b3e35a

File tree

12 files changed

+300
-270
lines changed

12 files changed

+300
-270
lines changed

tests/integration/conftest.py

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@
88
import asyncio
99
import json
1010
import os
11+
import secrets
12+
import string
1113
import time
14+
from dataclasses import dataclass
1215
from typing import TYPE_CHECKING, Any, TypeVar, overload
1316

1417
import pytest
1518

16-
from .utils import DatasetFixture, KvsFixture, get_crypto_random_object_id
1719
from apify_client import ApifyClient, ApifyClientAsync
1820
from apify_client._utils import create_hmac_signature, create_storage_content_signature
1921

@@ -29,10 +31,54 @@
2931

3032

3133
# ============================================================================
32-
# Helper functions for sync/async test compatibility
34+
# Data classes for test fixtures
3335
# ============================================================================
3436

3537

38+
@dataclass
39+
class StorageFixture:
40+
"""Base storage fixture with ID and signature."""
41+
42+
id: str
43+
signature: str
44+
45+
46+
@dataclass
47+
class DatasetFixture(StorageFixture):
48+
"""Dataset fixture with expected content."""
49+
50+
expected_content: list
51+
52+
53+
@dataclass
54+
class KvsFixture(StorageFixture):
55+
"""Key-value store fixture with expected content and key signatures."""
56+
57+
expected_content: dict[str, Any]
58+
keys_signature: dict[str, str]
59+
60+
61+
# ============================================================================
62+
# Helper functions
63+
# ============================================================================
64+
65+
66+
def get_crypto_random_object_id(length: int = 17) -> str:
67+
"""Generate a cryptographically secure random object ID."""
68+
chars = 'abcdefghijklmnopqrstuvwxyzABCEDFGHIJKLMNOPQRSTUVWXYZ0123456789'
69+
return ''.join(secrets.choice(chars) for _ in range(length))
70+
71+
72+
def get_random_string(length: int = 10) -> str:
73+
"""Generate a random alphabetic string."""
74+
return ''.join(secrets.choice(string.ascii_letters) for _ in range(length))
75+
76+
77+
def get_random_resource_name(resource: str) -> str:
78+
"""Generate a random resource name for test resources."""
79+
return f'python-client-test-{resource}-{get_random_string(5)}'
80+
81+
3682
@overload
3783
async def maybe_await(value: Coroutine[Any, Any, T]) -> T: ...
3884

@@ -60,6 +106,24 @@ async def maybe_sleep(seconds: float, *, is_async: bool) -> None:
60106
time.sleep(seconds) # noqa: ASYNC251
61107

62108

109+
# ============================================================================
110+
# Pytest markers and parametrization
111+
# ============================================================================
112+
113+
parametrized_api_urls = pytest.mark.parametrize(
114+
('api_url', 'api_public_url'),
115+
[
116+
('https://api.apify.com', 'https://api.apify.com'),
117+
('https://api.apify.com', None),
118+
('https://api.apify.com', 'https://custom-public-url.com'),
119+
('https://api.apify.com', 'https://custom-public-url.com/with/custom/path'),
120+
('https://api.apify.com', 'https://custom-public-url.com/with/custom/path/'),
121+
('http://10.0.88.214:8010', 'https://api.apify.com'),
122+
('http://10.0.88.214:8010', None),
123+
],
124+
)
125+
"""Parametrize decorator for testing various API URL and public URL combinations."""
126+
63127
# ============================================================================
64128
# Session-scoped fixtures (created once per test session)
65129
# ============================================================================

tests/integration/test_actor.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44

55
from typing import TYPE_CHECKING, cast
66

7-
from .conftest import maybe_await
8-
from .utils import get_random_resource_name
7+
from .conftest import get_random_resource_name, maybe_await
98

109
if TYPE_CHECKING:
1110
from apify_client import ApifyClient, ApifyClientAsync

tests/integration/test_actor_env_var.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@
99
from apify_client._models import Actor, EnvVar, ListOfEnvVars
1010

1111

12-
from .conftest import maybe_await
13-
from .utils import get_random_resource_name
12+
from .conftest import get_random_resource_name, maybe_await
1413

1514

1615
async def test_actor_env_var_list(client: ApifyClient | ApifyClientAsync) -> None:

tests/integration/test_actor_version.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@
99
from apify_client._models import Actor, ListOfVersions, Version
1010

1111

12-
from .conftest import maybe_await
13-
from .utils import get_random_resource_name
12+
from .conftest import get_random_resource_name, maybe_await
1413
from apify_client._models import VersionSourceType
1514

1615

tests/integration/test_build.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@
99
from apify_client._models import Actor, Build, ListOfBuilds
1010

1111

12-
from .conftest import maybe_await
13-
from .utils import get_random_resource_name
12+
from .conftest import get_random_resource_name, maybe_await
1413

1514
# Use a public actor that has builds available
1615
HELLO_WORLD_ACTOR = 'apify/hello-world'

tests/integration/test_dataset.py

Lines changed: 2 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -10,54 +10,18 @@
1010

1111
from impit import Response
1212

13+
from apify_client import ApifyClient, ApifyClientAsync
1314
from apify_client._models import Dataset
1415
from apify_client._resource_clients.dataset import DatasetItemsPage
1516

1617
import json
17-
from unittest import mock
18-
from unittest.mock import Mock
1918

2019
import impit
2120
import pytest
2221

23-
from .conftest import maybe_await, maybe_sleep
24-
from .utils import DatasetFixture, get_random_resource_name, parametrized_api_urls
25-
from apify_client import ApifyClient, ApifyClientAsync
26-
from apify_client._config import DEFAULT_API_URL
22+
from .conftest import DatasetFixture, get_random_resource_name, maybe_await, maybe_sleep
2723
from apify_client.errors import ApifyApiError
2824

29-
##################################################
30-
# OLD TESTS - Tests with mocks and signed URLs
31-
##################################################
32-
33-
MOCKED_API_DATASET_RESPONSE = """{
34-
"data": {
35-
"id": "someID",
36-
"name": "name",
37-
"userId": "userId",
38-
"createdAt": "2025-09-11T08:48:51.806Z",
39-
"modifiedAt": "2025-09-11T08:48:51.806Z",
40-
"accessedAt": "2025-09-11T08:48:51.806Z",
41-
"itemCount": 0,
42-
"cleanItemCount": 0,
43-
"actId": null,
44-
"actRunId": null,
45-
"schema": null,
46-
"stats": {
47-
"readCount": 0,
48-
"writeCount": 0,
49-
"deleteCount": 0,
50-
"listCount": 0,
51-
"storageBytes": 0
52-
},
53-
"fields": [],
54-
"consoleUrl": "https://console.apify.com/storage/datasets/someID",
55-
"itemsPublicUrl": "https://api.apify.com/v2/datasets/someID/items",
56-
"generalAccess": "FOLLOW_USER_SETTING",
57-
"urlSigningSecretKey": "urlSigningSecretKey"
58-
}
59-
}"""
60-
6125

6226
async def test_dataset_should_create_public_items_expiring_url_with_params(
6327
client: ApifyClient | ApifyClientAsync,
@@ -111,38 +75,6 @@ async def test_dataset_should_create_public_items_non_expiring_url(
11175
assert result is None
11276

11377

114-
# Parametrized test remains sync-only as it tests URL generation with mocks
115-
@parametrized_api_urls
116-
async def test_public_url(
117-
client: ApifyClient | ApifyClientAsync,
118-
api_token: str,
119-
api_url: str,
120-
api_public_url: str,
121-
) -> None:
122-
"""Test public URL generation for datasets (runs for both sync and async clients)."""
123-
# Create a fresh client with the parametrized URL settings
124-
test_client = (
125-
ApifyClientAsync(token=api_token, api_url=api_url, api_public_url=api_public_url)
126-
if isinstance(client, ApifyClientAsync)
127-
else ApifyClient(token=api_token, api_url=api_url, api_public_url=api_public_url)
128-
)
129-
130-
dataset = test_client.dataset('someID')
131-
132-
# Mock the API call to return predefined response
133-
mock_response = Mock()
134-
mock_response.json.return_value = json.loads(MOCKED_API_DATASET_RESPONSE)
135-
136-
with mock.patch.object(test_client._http_client, 'call', return_value=mock_response):
137-
result = await maybe_await(dataset.create_items_public_url())
138-
public_url = cast('str', result)
139-
140-
assert public_url == (
141-
f'{(api_public_url or DEFAULT_API_URL).strip("/")}/v2/datasets/'
142-
f'someID/items?signature={public_url.split("signature=")[1]}'
143-
)
144-
145-
14678
async def test_list_items_signature(
14779
client: ApifyClient | ApifyClientAsync, test_dataset_of_another_user: DatasetFixture
14880
) -> None:
@@ -214,11 +146,6 @@ async def test_get_items_as_bytes_signature(
214146
assert test_dataset_of_another_user.expected_content == json.loads(raw_data.decode('utf-8'))
215147

216148

217-
#############
218-
# NEW TESTS #
219-
#############
220-
221-
222149
async def test_dataset_get_or_create_and_get(client: ApifyClient | ApifyClientAsync) -> None:
223150
"""Test creating a dataset and retrieving it."""
224151
dataset_name = get_random_resource_name('dataset')

tests/integration/test_key_value_store.py

Lines changed: 2 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -5,54 +5,17 @@
55
from typing import TYPE_CHECKING, cast
66

77
if TYPE_CHECKING:
8+
from apify_client import ApifyClient, ApifyClientAsync
89
from apify_client._models import KeyValueStore, ListOfKeys
910

1011
import json
11-
from unittest import mock
12-
from unittest.mock import Mock
1312

1413
import impit
1514
import pytest
1615

17-
from .conftest import maybe_await, maybe_sleep
18-
from .utils import KvsFixture, get_random_resource_name, parametrized_api_urls
19-
from apify_client import ApifyClient, ApifyClientAsync
20-
from apify_client._config import DEFAULT_API_URL
21-
from apify_client._utils import create_hmac_signature, create_storage_content_signature
16+
from .conftest import KvsFixture, get_random_resource_name, maybe_await, maybe_sleep
2217
from apify_client.errors import ApifyApiError
2318

24-
##################################################
25-
# OLD TESTS - Tests with mocks and signed URLs
26-
##################################################
27-
28-
MOCKED_ID = 'someID'
29-
30-
31-
def _get_mocked_api_kvs_response(signing_key: str | None = None) -> Mock:
32-
response_data = {
33-
'data': {
34-
'id': MOCKED_ID,
35-
'name': 'name',
36-
'userId': 'userId',
37-
'createdAt': '2025-09-11T08:48:51.806Z',
38-
'modifiedAt': '2025-09-11T08:48:51.806Z',
39-
'accessedAt': '2025-09-11T08:48:51.806Z',
40-
'actId': None,
41-
'actRunId': None,
42-
'schema': None,
43-
'stats': {'readCount': 0, 'writeCount': 0, 'deleteCount': 0, 'listCount': 0, 'storageBytes': 0},
44-
'consoleUrl': 'https://console.apify.com/storage/key-value-stores/someID',
45-
'keysPublicUrl': 'https://api.apify.com/v2/key-value-stores/someID/keys',
46-
'generalAccess': 'FOLLOW_USER_SETTING',
47-
}
48-
}
49-
if signing_key:
50-
response_data['data']['urlSigningSecretKey'] = signing_key
51-
52-
mock_response = Mock()
53-
mock_response.json.return_value = response_data
54-
return mock_response
55-
5619

5720
async def test_key_value_store_should_create_expiring_keys_public_url_with_params(
5821
client: ApifyClient | ApifyClientAsync,
@@ -104,72 +67,6 @@ async def test_key_value_store_should_create_public_keys_non_expiring_url(
10467
assert result is None
10568

10669

107-
@pytest.mark.parametrize('signing_key', [None, 'custom-signing-key'])
108-
@parametrized_api_urls
109-
async def test_public_url(
110-
client: ApifyClient | ApifyClientAsync, api_token: str, api_url: str, api_public_url: str, signing_key: str
111-
) -> None:
112-
"""Test public URL generation for key-value stores (runs for both sync and async clients)."""
113-
# Create a fresh client with the parametrized URL settings
114-
if isinstance(client, ApifyClientAsync):
115-
test_client: ApifyClient | ApifyClientAsync = ApifyClientAsync(
116-
token=api_token, api_url=api_url, api_public_url=api_public_url
117-
)
118-
else:
119-
test_client = ApifyClient(token=api_token, api_url=api_url, api_public_url=api_public_url)
120-
121-
kvs = test_client.key_value_store(MOCKED_ID)
122-
123-
# Mock the API call to return predefined response
124-
with mock.patch.object(
125-
test_client._http_client,
126-
'call',
127-
return_value=_get_mocked_api_kvs_response(signing_key=signing_key),
128-
):
129-
public_url = await maybe_await(kvs.create_keys_public_url())
130-
if signing_key:
131-
signature_value = create_storage_content_signature(
132-
resource_id=MOCKED_ID, url_signing_secret_key=signing_key
133-
)
134-
expected_signature = f'?signature={signature_value}'
135-
else:
136-
expected_signature = ''
137-
assert public_url == (
138-
f'{(api_public_url or DEFAULT_API_URL).strip("/")}/v2/key-value-stores/someID/keys{expected_signature}'
139-
)
140-
141-
142-
@pytest.mark.parametrize('signing_key', [None, 'custom-signing-key'])
143-
@parametrized_api_urls
144-
async def test_record_public_url(
145-
client: ApifyClient | ApifyClientAsync, api_token: str, api_url: str, api_public_url: str, signing_key: str
146-
) -> None:
147-
"""Test record public URL generation for key-value stores (runs for both sync and async clients)."""
148-
# Create a fresh client with the parametrized URL settings
149-
if isinstance(client, ApifyClientAsync):
150-
test_client: ApifyClient | ApifyClientAsync = ApifyClientAsync(
151-
token=api_token, api_url=api_url, api_public_url=api_public_url
152-
)
153-
else:
154-
test_client = ApifyClient(token=api_token, api_url=api_url, api_public_url=api_public_url)
155-
156-
key = 'some_key'
157-
kvs = test_client.key_value_store(MOCKED_ID)
158-
159-
# Mock the API call to return predefined response
160-
with mock.patch.object(
161-
test_client._http_client,
162-
'call',
163-
return_value=_get_mocked_api_kvs_response(signing_key=signing_key),
164-
):
165-
public_url = await maybe_await(kvs.get_record_public_url(key=key))
166-
expected_signature = f'?signature={create_hmac_signature(signing_key, key)}' if signing_key else ''
167-
assert public_url == (
168-
f'{(api_public_url or DEFAULT_API_URL).strip("/")}/v2/key-value-stores/someID/'
169-
f'records/{key}{expected_signature}'
170-
)
171-
172-
17370
async def test_list_keys_signature(
17471
client: ApifyClient | ApifyClientAsync, test_kvs_of_another_user: KvsFixture
17572
) -> None:
@@ -271,11 +168,6 @@ async def test_stream_record_signature(
271168
assert test_kvs_of_another_user.expected_content[key] == value
272169

273170

274-
#############
275-
# NEW TESTS #
276-
#############
277-
278-
279171
async def test_key_value_store_get_or_create_and_get(client: ApifyClient | ApifyClientAsync) -> None:
280172
"""Test creating a key-value store and retrieving it."""
281173
store_name = get_random_resource_name('kvs')

0 commit comments

Comments
 (0)