|
1 | 1 | from __future__ import annotations
|
2 | 2 |
|
| 3 | +import json |
3 | 4 | from unittest import mock
|
4 | 5 | from unittest.mock import Mock
|
5 | 6 |
|
6 | 7 | import impit
|
| 8 | +import pytest |
| 9 | +from apify_shared.utils import create_hmac_signature, create_storage_content_signature |
7 | 10 |
|
8 | 11 | from integration.integration_test_utils import parametrized_api_urls, random_resource_name
|
9 | 12 |
|
10 | 13 | from apify_client import ApifyClient, ApifyClientAsync
|
11 | 14 | from apify_client.client import DEFAULT_API_URL
|
12 | 15 |
|
13 |
| -MOCKED_API_KVS_RESPONSE = """{ |
14 |
| - "data": { |
15 |
| - "id": "someID", |
16 |
| - "name": "name", |
17 |
| - "userId": "userId", |
18 |
| - "createdAt": "2025-09-11T08:48:51.806Z", |
19 |
| - "modifiedAt": "2025-09-11T08:48:51.806Z", |
20 |
| - "accessedAt": "2025-09-11T08:48:51.806Z", |
21 |
| - "actId": null, |
22 |
| - "actRunId": null, |
23 |
| - "schema": null, |
24 |
| - "stats": { |
25 |
| - "readCount": 0, |
26 |
| - "writeCount": 0, |
27 |
| - "deleteCount": 0, |
28 |
| - "listCount": 0, |
29 |
| - "storageBytes": 0 |
30 |
| - }, |
31 |
| - "consoleUrl": "https://console.apify.com/storage/key-value-stores/someID", |
32 |
| - "keysPublicUrl": "https://api.apify.com/v2/key-value-stores/someID/keys", |
33 |
| - "generalAccess": "FOLLOW_USER_SETTING", |
34 |
| - "urlSigningSecretKey": "urlSigningSecretKey" |
35 |
| - } |
36 |
| -}""" |
| 16 | +MOCKED_ID = 'someID' |
| 17 | + |
| 18 | + |
| 19 | +def _get_mocked_api_kvs_response(signing_key: str | None = None) -> str: |
| 20 | + response_data = { |
| 21 | + 'data': { |
| 22 | + 'id': MOCKED_ID, |
| 23 | + 'name': 'name', |
| 24 | + 'userId': 'userId', |
| 25 | + 'createdAt': '2025-09-11T08:48:51.806Z', |
| 26 | + 'modifiedAt': '2025-09-11T08:48:51.806Z', |
| 27 | + 'accessedAt': '2025-09-11T08:48:51.806Z', |
| 28 | + 'actId': None, |
| 29 | + 'actRunId': None, |
| 30 | + 'schema': None, |
| 31 | + 'stats': {'readCount': 0, 'writeCount': 0, 'deleteCount': 0, 'listCount': 0, 'storageBytes': 0}, |
| 32 | + 'consoleUrl': 'https://console.apify.com/storage/key-value-stores/someID', |
| 33 | + 'keysPublicUrl': 'https://api.apify.com/v2/key-value-stores/someID/keys', |
| 34 | + 'generalAccess': 'FOLLOW_USER_SETTING', |
| 35 | + } |
| 36 | + } |
| 37 | + if signing_key: |
| 38 | + response_data['data']['urlSigningSecretKey'] = signing_key |
| 39 | + |
| 40 | + return json.dumps(response_data) |
37 | 41 |
|
38 | 42 |
|
39 | 43 | class TestKeyValueStoreSync:
|
@@ -73,17 +77,48 @@ def test_key_value_store_should_create_public_keys_non_expiring_url(self, apify_
|
73 | 77 | store.delete()
|
74 | 78 | assert apify_client.key_value_store(created_store['id']).get() is None
|
75 | 79 |
|
| 80 | + @pytest.mark.parametrize('signing_key', [None, 'custom-signing-key']) |
76 | 81 | @parametrized_api_urls
|
77 |
| - def test_public_url(self, api_token: str, api_url: str, api_public_url: str) -> None: |
| 82 | + def test_public_url(self, api_token: str, api_url: str, api_public_url: str, signing_key: str) -> None: |
78 | 83 | apify_client = ApifyClient(token=api_token, api_url=api_url, api_public_url=api_public_url)
|
79 |
| - kvs = apify_client.key_value_store('someID') |
| 84 | + kvs = apify_client.key_value_store(MOCKED_ID) |
80 | 85 |
|
81 | 86 | # Mock the API call to return predefined response
|
82 |
| - with mock.patch.object(apify_client.http_client, 'call', return_value=Mock(text=MOCKED_API_KVS_RESPONSE)): |
| 87 | + with mock.patch.object( |
| 88 | + apify_client.http_client, |
| 89 | + 'call', |
| 90 | + return_value=Mock(text=_get_mocked_api_kvs_response(signing_key=signing_key)), |
| 91 | + ): |
83 | 92 | public_url = kvs.create_keys_public_url()
|
| 93 | + if signing_key: |
| 94 | + signature_value = create_storage_content_signature( |
| 95 | + resource_id=MOCKED_ID, url_signing_secret_key=signing_key |
| 96 | + ) |
| 97 | + expected_signature = f'?signature={signature_value}' |
| 98 | + else: |
| 99 | + expected_signature = '' |
84 | 100 | assert public_url == (
|
85 |
| - f'{(api_public_url or DEFAULT_API_URL).strip("/")}/v2/key-value-stores/' |
86 |
| - f'someID/keys?signature={public_url.split("signature=")[1]}' |
| 101 | + f'{(api_public_url or DEFAULT_API_URL).strip("/")}/v2/key-value-stores/someID/keys{expected_signature}' |
| 102 | + ) |
| 103 | + |
| 104 | + @pytest.mark.parametrize('signing_key', [None, 'custom-signing-key']) |
| 105 | + @parametrized_api_urls |
| 106 | + def test_record_public_url(self, api_token: str, api_url: str, api_public_url: str, signing_key: str) -> None: |
| 107 | + apify_client = ApifyClient(token=api_token, api_url=api_url, api_public_url=api_public_url) |
| 108 | + key = 'some_key' |
| 109 | + kvs = apify_client.key_value_store(MOCKED_ID) |
| 110 | + |
| 111 | + # Mock the API call to return predefined response |
| 112 | + with mock.patch.object( |
| 113 | + apify_client.http_client, |
| 114 | + 'call', |
| 115 | + return_value=Mock(text=_get_mocked_api_kvs_response(signing_key=signing_key)), |
| 116 | + ): |
| 117 | + public_url = kvs.get_record_public_url(key=key) |
| 118 | + expected_signature = f'?signature={create_hmac_signature(signing_key, key)}' if signing_key else '' |
| 119 | + assert public_url == ( |
| 120 | + f'{(api_public_url or DEFAULT_API_URL).strip("/")}/v2/key-value-stores/someID/' |
| 121 | + f'records/{key}{expected_signature}' |
87 | 122 | )
|
88 | 123 |
|
89 | 124 |
|
@@ -130,15 +165,47 @@ async def test_key_value_store_should_create_public_keys_non_expiring_url(
|
130 | 165 | await store.delete()
|
131 | 166 | assert await apify_client_async.key_value_store(created_store['id']).get() is None
|
132 | 167 |
|
| 168 | + @pytest.mark.parametrize('signing_key', [None, 'custom-signing-key']) |
133 | 169 | @parametrized_api_urls
|
134 |
| - async def test_public_url(self, api_token: str, api_url: str, api_public_url: str) -> None: |
| 170 | + async def test_public_url(self, api_token: str, api_url: str, api_public_url: str, signing_key: str) -> None: |
135 | 171 | apify_client = ApifyClientAsync(token=api_token, api_url=api_url, api_public_url=api_public_url)
|
136 |
| - kvs = apify_client.key_value_store('someID') |
| 172 | + kvs = apify_client.key_value_store(MOCKED_ID) |
| 173 | + mocked_response = _get_mocked_api_kvs_response(signing_key=signing_key) |
137 | 174 |
|
138 | 175 | # Mock the API call to return predefined response
|
139 |
| - with mock.patch.object(apify_client.http_client, 'call', return_value=Mock(text=MOCKED_API_KVS_RESPONSE)): |
| 176 | + with mock.patch.object( |
| 177 | + apify_client.http_client, |
| 178 | + 'call', |
| 179 | + return_value=Mock(text=mocked_response), |
| 180 | + ): |
140 | 181 | public_url = await kvs.create_keys_public_url()
|
| 182 | + if signing_key: |
| 183 | + signature_value = create_storage_content_signature( |
| 184 | + resource_id=MOCKED_ID, url_signing_secret_key=signing_key |
| 185 | + ) |
| 186 | + expected_signature = f'?signature={signature_value}' |
| 187 | + else: |
| 188 | + expected_signature = '' |
| 189 | + assert public_url == ( |
| 190 | + f'{(api_public_url or DEFAULT_API_URL).strip("/")}/v2/key-value-stores/someID/keys{expected_signature}' |
| 191 | + ) |
| 192 | + |
| 193 | + @pytest.mark.parametrize('signing_key', [None, 'custom-signing-key']) |
| 194 | + @parametrized_api_urls |
| 195 | + async def test_record_public_url(self, api_token: str, api_url: str, api_public_url: str, signing_key: str) -> None: |
| 196 | + apify_client = ApifyClientAsync(token=api_token, api_url=api_url, api_public_url=api_public_url) |
| 197 | + key = 'some_key' |
| 198 | + kvs = apify_client.key_value_store(MOCKED_ID) |
| 199 | + |
| 200 | + # Mock the API call to return predefined response |
| 201 | + with mock.patch.object( |
| 202 | + apify_client.http_client, |
| 203 | + 'call', |
| 204 | + return_value=Mock(text=_get_mocked_api_kvs_response(signing_key=signing_key)), |
| 205 | + ): |
| 206 | + public_url = await kvs.get_record_public_url(key=key) |
| 207 | + expected_signature = f'?signature={create_hmac_signature(signing_key, key)}' if signing_key else '' |
141 | 208 | assert public_url == (
|
142 |
| - f'{(api_public_url or DEFAULT_API_URL).strip("/")}/v2/key-value-stores/' |
143 |
| - f'someID/keys?signature={public_url.split("signature=")[1]}' |
| 209 | + f'{(api_public_url or DEFAULT_API_URL).strip("/")}/v2/key-value-stores/someID/' |
| 210 | + f'records/{key}{expected_signature}' |
144 | 211 | )
|
0 commit comments