|
5 | 5 |
|
6 | 6 | from ksef_client.config import KsefClientOptions |
7 | 7 | from ksef_client.exceptions import KsefApiError, KsefHttpError, KsefRateLimitError |
8 | | -from ksef_client.http import AsyncBaseHttpClient, BaseHttpClient, HttpResponse, _merge_headers |
| 8 | +from ksef_client.http import ( |
| 9 | + AsyncBaseHttpClient, |
| 10 | + BaseHttpClient, |
| 11 | + HttpResponse, |
| 12 | + _host_allowed, |
| 13 | + _merge_headers, |
| 14 | + _validate_presigned_url_security, |
| 15 | +) |
9 | 16 |
|
10 | 17 |
|
11 | 18 | class HttpTests(unittest.TestCase): |
@@ -119,6 +126,93 @@ def test_default_status_error(self): |
119 | 126 | ): |
120 | 127 | client.request("GET", "/path") |
121 | 128 |
|
| 129 | + def test_skip_auth_presigned_url_accepts_valid_https(self): |
| 130 | + options = KsefClientOptions(base_url="https://api-test.ksef.mf.gov.pl") |
| 131 | + client = BaseHttpClient(options) |
| 132 | + response = httpx.Response(200, json={"ok": True}) |
| 133 | + with patch.object(client._client, "request", Mock(return_value=response)) as request_mock: |
| 134 | + client.request("GET", "https://files.example.com/upload", skip_auth=True) |
| 135 | + _, kwargs = request_mock.call_args |
| 136 | + self.assertEqual(kwargs["url"], "https://files.example.com/upload") |
| 137 | + self.assertNotIn("Authorization", kwargs["headers"]) |
| 138 | + |
| 139 | + def test_skip_auth_presigned_url_rejects_http_when_strict(self): |
| 140 | + options = KsefClientOptions(base_url="https://api-test.ksef.mf.gov.pl") |
| 141 | + client = BaseHttpClient(options) |
| 142 | + with self.assertRaisesRegex(ValueError, "https is required"): |
| 143 | + client.request("GET", "http://files.example.com/upload", skip_auth=True) |
| 144 | + |
| 145 | + def test_skip_auth_presigned_url_allows_http_when_not_strict(self): |
| 146 | + options = KsefClientOptions( |
| 147 | + base_url="https://api-test.ksef.mf.gov.pl", |
| 148 | + strict_presigned_url_validation=False, |
| 149 | + ) |
| 150 | + client = BaseHttpClient(options) |
| 151 | + response = httpx.Response(200, json={"ok": True}) |
| 152 | + with patch.object(client._client, "request", Mock(return_value=response)) as request_mock: |
| 153 | + client.request("GET", "http://files.example.com/upload", skip_auth=True) |
| 154 | + _, kwargs = request_mock.call_args |
| 155 | + self.assertEqual(kwargs["url"], "http://files.example.com/upload") |
| 156 | + |
| 157 | + def test_skip_auth_presigned_url_rejects_localhost_and_loopback(self): |
| 158 | + options = KsefClientOptions(base_url="https://api-test.ksef.mf.gov.pl") |
| 159 | + client = BaseHttpClient(options) |
| 160 | + with self.assertRaisesRegex(ValueError, "localhost"): |
| 161 | + client.request("GET", "https://localhost/upload", skip_auth=True) |
| 162 | + with self.assertRaisesRegex(ValueError, "loopback"): |
| 163 | + client.request("GET", "https://127.0.0.1/upload", skip_auth=True) |
| 164 | + |
| 165 | + def test_skip_auth_presigned_url_rejects_private_ip_by_default(self): |
| 166 | + options = KsefClientOptions(base_url="https://api-test.ksef.mf.gov.pl") |
| 167 | + client = BaseHttpClient(options) |
| 168 | + with self.assertRaisesRegex(ValueError, "private, link-local, and reserved IP"): |
| 169 | + client.request("GET", "https://10.1.2.3/upload", skip_auth=True) |
| 170 | + |
| 171 | + def test_skip_auth_presigned_url_allows_private_ip_when_opted_in(self): |
| 172 | + options = KsefClientOptions( |
| 173 | + base_url="https://api-test.ksef.mf.gov.pl", |
| 174 | + allow_private_network_presigned_urls=True, |
| 175 | + ) |
| 176 | + client = BaseHttpClient(options) |
| 177 | + response = httpx.Response(200, json={"ok": True}) |
| 178 | + with patch.object(client._client, "request", Mock(return_value=response)) as request_mock: |
| 179 | + client.request("GET", "https://10.1.2.3/upload", skip_auth=True) |
| 180 | + _, kwargs = request_mock.call_args |
| 181 | + self.assertEqual(kwargs["url"], "https://10.1.2.3/upload") |
| 182 | + |
| 183 | + def test_skip_auth_presigned_url_allowlist_exact_and_subdomain(self): |
| 184 | + options = KsefClientOptions( |
| 185 | + base_url="https://api-test.ksef.mf.gov.pl", |
| 186 | + allowed_presigned_hosts=["uploads.example.com"], |
| 187 | + ) |
| 188 | + client = BaseHttpClient(options) |
| 189 | + response = httpx.Response(200, json={"ok": True}) |
| 190 | + with patch.object(client._client, "request", Mock(return_value=response)): |
| 191 | + client.request("GET", "https://uploads.example.com/path", skip_auth=True) |
| 192 | + client.request("GET", "https://sub.uploads.example.com/path", skip_auth=True) |
| 193 | + |
| 194 | + def test_skip_auth_presigned_url_allowlist_rejects_other_hosts(self): |
| 195 | + options = KsefClientOptions( |
| 196 | + base_url="https://api-test.ksef.mf.gov.pl", |
| 197 | + allowed_presigned_hosts=["uploads.example.com"], |
| 198 | + ) |
| 199 | + client = BaseHttpClient(options) |
| 200 | + with self.assertRaisesRegex(ValueError, "allowed_presigned_hosts"): |
| 201 | + client.request("GET", "https://other.example.com/path", skip_auth=True) |
| 202 | + |
| 203 | + def test_host_allowed_skips_empty_and_ip_allowlist_entries(self): |
| 204 | + self.assertTrue( |
| 205 | + _host_allowed( |
| 206 | + "sub.uploads.example.com", |
| 207 | + ["", "10.0.0.1", "uploads.example.com"], |
| 208 | + ) |
| 209 | + ) |
| 210 | + |
| 211 | + def test_validate_presigned_url_security_rejects_missing_host(self): |
| 212 | + options = KsefClientOptions(base_url="https://api-test.ksef.mf.gov.pl") |
| 213 | + with self.assertRaisesRegex(ValueError, "host is missing"): |
| 214 | + _validate_presigned_url_security(options, "https:///no-host") |
| 215 | + |
122 | 216 |
|
123 | 217 | class AsyncHttpTests(unittest.IsolatedAsyncioTestCase): |
124 | 218 | async def test_async_request(self): |
@@ -193,6 +287,12 @@ async def test_async_raise_for_status_paths(self): |
193 | 287 | with self.assertRaises(KsefHttpError): |
194 | 288 | client._raise_for_status(response_http) |
195 | 289 |
|
| 290 | + async def test_async_skip_auth_presigned_validation_rejects_localhost(self): |
| 291 | + options = KsefClientOptions(base_url="https://api-test.ksef.mf.gov.pl") |
| 292 | + client = AsyncBaseHttpClient(options) |
| 293 | + with self.assertRaisesRegex(ValueError, "localhost"): |
| 294 | + await client.request("GET", "https://localhost/upload", skip_auth=True) |
| 295 | + |
196 | 296 |
|
197 | 297 | if __name__ == "__main__": |
198 | 298 | unittest.main() |
0 commit comments