karrio-server-core 2025.5rc31__py3-none-any.whl → 2026.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- karrio/server/core/authentication.py +38 -20
- karrio/server/core/config.py +31 -0
- karrio/server/core/datatypes.py +30 -4
- karrio/server/core/dataunits.py +26 -7
- karrio/server/core/exceptions.py +287 -17
- karrio/server/core/filters.py +14 -0
- karrio/server/core/gateway.py +284 -11
- karrio/server/core/logging.py +403 -0
- karrio/server/core/middleware.py +104 -2
- karrio/server/core/models/base.py +34 -1
- karrio/server/core/oauth_validators.py +2 -3
- karrio/server/core/permissions.py +1 -2
- karrio/server/core/serializers.py +154 -7
- karrio/server/core/signals.py +22 -28
- karrio/server/core/telemetry.py +573 -0
- karrio/server/core/tests/__init__.py +27 -0
- karrio/server/core/{tests.py → tests/base.py} +6 -7
- karrio/server/core/tests/test_exception_level.py +159 -0
- karrio/server/core/tests/test_resource_token.py +593 -0
- karrio/server/core/utils.py +688 -38
- karrio/server/core/validators.py +144 -222
- karrio/server/core/views/oauth.py +13 -12
- karrio/server/core/views/references.py +2 -2
- karrio/server/iam/apps.py +1 -4
- karrio/server/iam/migrations/0002_setup_carrier_permission_groups.py +103 -0
- karrio/server/iam/migrations/0003_remove_permission_groups.py +91 -0
- karrio/server/iam/permissions.py +7 -134
- karrio/server/iam/serializers.py +9 -3
- karrio/server/iam/signals.py +2 -4
- karrio/server/providers/admin.py +1 -1
- karrio/server/providers/migrations/0085_populate_dhl_parcel_de_oauth_credentials.py +82 -0
- karrio/server/providers/migrations/0086_rename_dhl_parcel_de_customer_number_to_billing_number.py +71 -0
- karrio/server/providers/migrations/0087_alter_carrier_capabilities.py +38 -0
- karrio/server/providers/migrations/0088_servicelevel_surcharges.py +24 -0
- karrio/server/providers/migrations/0089_servicelevel_cost_max_volume.py +31 -0
- karrio/server/providers/migrations/0090_ratesheet_surcharges_servicelevel_zone_surcharge_ids.py +47 -0
- karrio/server/providers/migrations/0091_migrate_legacy_zones_surcharges.py +154 -0
- karrio/server/providers/models/carrier.py +101 -29
- karrio/server/providers/models/service.py +182 -125
- karrio/server/providers/models/sheet.py +342 -198
- karrio/server/providers/serializers/base.py +263 -2
- karrio/server/providers/signals.py +2 -4
- karrio/server/providers/templates/providers/oauth_callback.html +105 -0
- karrio/server/providers/tests/__init__.py +5 -0
- karrio/server/providers/tests/test_connections.py +895 -0
- karrio/server/providers/views/carriers.py +1 -3
- karrio/server/providers/views/connections.py +322 -2
- karrio/server/serializers/abstract.py +112 -21
- karrio/server/tracing/utils.py +5 -8
- karrio/server/user/models.py +36 -34
- karrio/server/user/serializers.py +1 -0
- {karrio_server_core-2025.5rc31.dist-info → karrio_server_core-2026.1.1.dist-info}/METADATA +2 -2
- {karrio_server_core-2025.5rc31.dist-info → karrio_server_core-2026.1.1.dist-info}/RECORD +55 -38
- karrio/server/providers/tests.py +0 -3
- {karrio_server_core-2025.5rc31.dist-info → karrio_server_core-2026.1.1.dist-info}/WHEEL +0 -0
- {karrio_server_core-2025.5rc31.dist-info → karrio_server_core-2026.1.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
"""Tests for ResourceAccessToken and /api/tokens endpoint."""
|
|
2
|
+
|
|
3
|
+
from unittest import mock
|
|
4
|
+
from django.test import TestCase
|
|
5
|
+
from django.contrib.auth import get_user_model
|
|
6
|
+
from rest_framework.test import APITestCase, APIClient
|
|
7
|
+
|
|
8
|
+
from karrio.server.user.models import Token
|
|
9
|
+
from karrio.server.core.utils import ResourceAccessToken
|
|
10
|
+
from karrio.server.core.tests.base import APITestCase as KarrioAPITestCase
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestResourceAccessTokenUnit(TestCase):
|
|
14
|
+
"""Unit tests for ResourceAccessToken class."""
|
|
15
|
+
|
|
16
|
+
def setUp(self):
|
|
17
|
+
self.user = get_user_model().objects.create_user(
|
|
18
|
+
email="test@example.com", password="testpass123"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
def test_create_token_for_single_resource(self):
|
|
22
|
+
"""Test creating a token for a single resource."""
|
|
23
|
+
token = ResourceAccessToken.for_resource(
|
|
24
|
+
user=self.user,
|
|
25
|
+
resource_type="shipment",
|
|
26
|
+
resource_ids=["shp_123"],
|
|
27
|
+
access=["label"],
|
|
28
|
+
format="pdf",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
self.assertDictEqual(
|
|
32
|
+
{
|
|
33
|
+
"resource_type": token["resource_type"],
|
|
34
|
+
"resource_ids": token["resource_ids"],
|
|
35
|
+
"access": token["access"],
|
|
36
|
+
"format": token["format"],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"resource_type": "shipment",
|
|
40
|
+
"resource_ids": ["shp_123"],
|
|
41
|
+
"access": ["label"],
|
|
42
|
+
"format": "pdf",
|
|
43
|
+
},
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def test_create_token_for_multiple_resources(self):
|
|
47
|
+
"""Test creating a token for multiple resources."""
|
|
48
|
+
token = ResourceAccessToken.for_resource(
|
|
49
|
+
user=self.user,
|
|
50
|
+
resource_type="document",
|
|
51
|
+
resource_ids=["shp_1", "shp_2", "shp_3"],
|
|
52
|
+
access=["batch_labels"],
|
|
53
|
+
format="pdf",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
self.assertDictEqual(
|
|
57
|
+
{
|
|
58
|
+
"resource_type": token["resource_type"],
|
|
59
|
+
"resource_ids": sorted(token["resource_ids"]),
|
|
60
|
+
"access": token["access"],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"resource_type": "document",
|
|
64
|
+
"resource_ids": ["shp_1", "shp_2", "shp_3"],
|
|
65
|
+
"access": ["batch_labels"],
|
|
66
|
+
},
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def test_decode_valid_token(self):
|
|
70
|
+
"""Test decoding a valid token."""
|
|
71
|
+
token = ResourceAccessToken.for_resource(
|
|
72
|
+
user=self.user,
|
|
73
|
+
resource_type="manifest",
|
|
74
|
+
resource_ids=["mnf_456"],
|
|
75
|
+
access=["manifest"],
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
claims = ResourceAccessToken.decode(str(token))
|
|
79
|
+
|
|
80
|
+
self.assertDictEqual(
|
|
81
|
+
{
|
|
82
|
+
"resource_type": claims["resource_type"],
|
|
83
|
+
"resource_ids": claims["resource_ids"],
|
|
84
|
+
"access": claims["access"],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"resource_type": "manifest",
|
|
88
|
+
"resource_ids": ["mnf_456"],
|
|
89
|
+
"access": ["manifest"],
|
|
90
|
+
},
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def test_validate_access_success(self):
|
|
94
|
+
"""Test successful access validation."""
|
|
95
|
+
token = ResourceAccessToken.for_resource(
|
|
96
|
+
user=self.user,
|
|
97
|
+
resource_type="shipment",
|
|
98
|
+
resource_ids=["shp_123"],
|
|
99
|
+
access=["label", "invoice"],
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
claims = ResourceAccessToken.validate_access(
|
|
103
|
+
token_string=str(token),
|
|
104
|
+
resource_type="shipment",
|
|
105
|
+
resource_id="shp_123",
|
|
106
|
+
access="label",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
self.assertIsNotNone(claims)
|
|
110
|
+
self.assertEqual(claims["resource_type"], "shipment")
|
|
111
|
+
|
|
112
|
+
def test_validate_access_wrong_resource_type(self):
|
|
113
|
+
"""Test validation fails for wrong resource type."""
|
|
114
|
+
token = ResourceAccessToken.for_resource(
|
|
115
|
+
user=self.user,
|
|
116
|
+
resource_type="shipment",
|
|
117
|
+
resource_ids=["shp_123"],
|
|
118
|
+
access=["label"],
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
with self.assertRaises(PermissionError) as context:
|
|
122
|
+
ResourceAccessToken.validate_access(
|
|
123
|
+
token_string=str(token),
|
|
124
|
+
resource_type="manifest",
|
|
125
|
+
resource_id="shp_123",
|
|
126
|
+
access="label",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
self.assertIn("resource type", str(context.exception).lower())
|
|
130
|
+
|
|
131
|
+
def test_validate_access_wrong_resource_id(self):
|
|
132
|
+
"""Test validation fails for wrong resource ID."""
|
|
133
|
+
token = ResourceAccessToken.for_resource(
|
|
134
|
+
user=self.user,
|
|
135
|
+
resource_type="shipment",
|
|
136
|
+
resource_ids=["shp_123"],
|
|
137
|
+
access=["label"],
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
with self.assertRaises(PermissionError) as context:
|
|
141
|
+
ResourceAccessToken.validate_access(
|
|
142
|
+
token_string=str(token),
|
|
143
|
+
resource_type="shipment",
|
|
144
|
+
resource_id="shp_999",
|
|
145
|
+
access="label",
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
self.assertIn("resource", str(context.exception).lower())
|
|
149
|
+
|
|
150
|
+
def test_validate_access_wrong_permission(self):
|
|
151
|
+
"""Test validation fails for wrong access permission."""
|
|
152
|
+
token = ResourceAccessToken.for_resource(
|
|
153
|
+
user=self.user,
|
|
154
|
+
resource_type="shipment",
|
|
155
|
+
resource_ids=["shp_123"],
|
|
156
|
+
access=["label"],
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
with self.assertRaises(PermissionError) as context:
|
|
160
|
+
ResourceAccessToken.validate_access(
|
|
161
|
+
token_string=str(token),
|
|
162
|
+
resource_type="shipment",
|
|
163
|
+
resource_id="shp_123",
|
|
164
|
+
access="invoice",
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
self.assertIn("access", str(context.exception).lower())
|
|
168
|
+
|
|
169
|
+
def test_validate_batch_access_success(self):
|
|
170
|
+
"""Test successful batch access validation."""
|
|
171
|
+
token = ResourceAccessToken.for_resource(
|
|
172
|
+
user=self.user,
|
|
173
|
+
resource_type="document",
|
|
174
|
+
resource_ids=["shp_1", "shp_2", "shp_3"],
|
|
175
|
+
access=["batch_labels"],
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
claims = ResourceAccessToken.validate_batch_access(
|
|
179
|
+
token_string=str(token),
|
|
180
|
+
resource_type="document",
|
|
181
|
+
resource_ids=["shp_1", "shp_2"],
|
|
182
|
+
access="batch_labels",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
self.assertIsNotNone(claims)
|
|
186
|
+
self.assertEqual(claims["resource_type"], "document")
|
|
187
|
+
|
|
188
|
+
def test_validate_batch_access_missing_id(self):
|
|
189
|
+
"""Test batch validation fails when requesting ID not in token."""
|
|
190
|
+
token = ResourceAccessToken.for_resource(
|
|
191
|
+
user=self.user,
|
|
192
|
+
resource_type="document",
|
|
193
|
+
resource_ids=["shp_1", "shp_2"],
|
|
194
|
+
access=["batch_labels"],
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
with self.assertRaises(PermissionError) as context:
|
|
198
|
+
ResourceAccessToken.validate_batch_access(
|
|
199
|
+
token_string=str(token),
|
|
200
|
+
resource_type="document",
|
|
201
|
+
resource_ids=["shp_1", "shp_2", "shp_3"],
|
|
202
|
+
access="batch_labels",
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
self.assertIn("shp_3", str(context.exception))
|
|
206
|
+
|
|
207
|
+
def test_token_with_org_id_and_test_mode(self):
|
|
208
|
+
"""Test token includes org_id and test_mode when provided."""
|
|
209
|
+
token = ResourceAccessToken.for_resource(
|
|
210
|
+
user=self.user,
|
|
211
|
+
resource_type="shipment",
|
|
212
|
+
resource_ids=["shp_123"],
|
|
213
|
+
access=["label"],
|
|
214
|
+
org_id="org_abc",
|
|
215
|
+
test_mode=True,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
claims = ResourceAccessToken.decode(str(token))
|
|
219
|
+
|
|
220
|
+
self.assertDictEqual(
|
|
221
|
+
{
|
|
222
|
+
"org_id": claims["org_id"],
|
|
223
|
+
"test_mode": claims["test_mode"],
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"org_id": "org_abc",
|
|
227
|
+
"test_mode": True,
|
|
228
|
+
},
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class TestResourceTokenAPI(APITestCase):
|
|
233
|
+
"""API tests for /api/tokens endpoint."""
|
|
234
|
+
|
|
235
|
+
def setUp(self):
|
|
236
|
+
self.user = get_user_model().objects.create_user(
|
|
237
|
+
email="api_test@example.com", password="testpass123"
|
|
238
|
+
)
|
|
239
|
+
self.token = Token.objects.create(user=self.user, test_mode=True)
|
|
240
|
+
self.client = APIClient()
|
|
241
|
+
self.client.credentials(HTTP_AUTHORIZATION=f"Token {self.token.key}")
|
|
242
|
+
|
|
243
|
+
def test_generate_shipment_label_token(self):
|
|
244
|
+
"""Test generating a token for shipment label access."""
|
|
245
|
+
response = self.client.post(
|
|
246
|
+
"/api/tokens",
|
|
247
|
+
{
|
|
248
|
+
"resource_type": "shipment",
|
|
249
|
+
"resource_ids": ["shp_123"],
|
|
250
|
+
"access": ["label"],
|
|
251
|
+
"format": "pdf",
|
|
252
|
+
},
|
|
253
|
+
format="json",
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
self.assertEqual(response.status_code, 201)
|
|
257
|
+
self.assertDictEqual(
|
|
258
|
+
{
|
|
259
|
+
"has_token": "token" in response.data,
|
|
260
|
+
"has_expires_at": "expires_at" in response.data,
|
|
261
|
+
"has_resource_urls": "resource_urls" in response.data,
|
|
262
|
+
"has_shp_123_url": "shp_123" in response.data.get("resource_urls", {}),
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
"has_token": True,
|
|
266
|
+
"has_expires_at": True,
|
|
267
|
+
"has_resource_urls": True,
|
|
268
|
+
"has_shp_123_url": True,
|
|
269
|
+
},
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
def test_generate_batch_labels_token(self):
|
|
273
|
+
"""Test generating a token for batch labels."""
|
|
274
|
+
response = self.client.post(
|
|
275
|
+
"/api/tokens",
|
|
276
|
+
{
|
|
277
|
+
"resource_type": "document",
|
|
278
|
+
"resource_ids": ["shp_1", "shp_2", "shp_3"],
|
|
279
|
+
"access": ["batch_labels"],
|
|
280
|
+
"format": "pdf",
|
|
281
|
+
},
|
|
282
|
+
format="json",
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
self.assertEqual(response.status_code, 201)
|
|
286
|
+
self.assertIn("batch", response.data["resource_urls"])
|
|
287
|
+
|
|
288
|
+
def test_generate_manifest_token(self):
|
|
289
|
+
"""Test generating a token for manifest access."""
|
|
290
|
+
response = self.client.post(
|
|
291
|
+
"/api/tokens",
|
|
292
|
+
{
|
|
293
|
+
"resource_type": "manifest",
|
|
294
|
+
"resource_ids": ["mnf_456"],
|
|
295
|
+
"access": ["manifest"],
|
|
296
|
+
},
|
|
297
|
+
format="json",
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
self.assertEqual(response.status_code, 201)
|
|
301
|
+
self.assertIn("mnf_456", response.data["resource_urls"])
|
|
302
|
+
|
|
303
|
+
def test_generate_template_token(self):
|
|
304
|
+
"""Test generating a token for template access."""
|
|
305
|
+
response = self.client.post(
|
|
306
|
+
"/api/tokens",
|
|
307
|
+
{
|
|
308
|
+
"resource_type": "template",
|
|
309
|
+
"resource_ids": ["tpl_789"],
|
|
310
|
+
"access": ["render"],
|
|
311
|
+
},
|
|
312
|
+
format="json",
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
self.assertEqual(response.status_code, 201)
|
|
316
|
+
self.assertIn("tpl_789", response.data["resource_urls"])
|
|
317
|
+
|
|
318
|
+
def test_unauthenticated_request_fails(self):
|
|
319
|
+
"""Test that unauthenticated requests are rejected."""
|
|
320
|
+
self.client.credentials()
|
|
321
|
+
response = self.client.post(
|
|
322
|
+
"/api/tokens",
|
|
323
|
+
{
|
|
324
|
+
"resource_type": "shipment",
|
|
325
|
+
"resource_ids": ["shp_123"],
|
|
326
|
+
"access": ["label"],
|
|
327
|
+
},
|
|
328
|
+
format="json",
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
self.assertEqual(response.status_code, 401)
|
|
332
|
+
|
|
333
|
+
def test_invalid_resource_type_fails(self):
|
|
334
|
+
"""Test that invalid resource type is rejected."""
|
|
335
|
+
response = self.client.post(
|
|
336
|
+
"/api/tokens",
|
|
337
|
+
{
|
|
338
|
+
"resource_type": "invalid_type",
|
|
339
|
+
"resource_ids": ["shp_123"],
|
|
340
|
+
"access": ["label"],
|
|
341
|
+
},
|
|
342
|
+
format="json",
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
self.assertEqual(response.status_code, 400)
|
|
346
|
+
|
|
347
|
+
def test_invalid_access_type_fails(self):
|
|
348
|
+
"""Test that invalid access type is rejected."""
|
|
349
|
+
response = self.client.post(
|
|
350
|
+
"/api/tokens",
|
|
351
|
+
{
|
|
352
|
+
"resource_type": "shipment",
|
|
353
|
+
"resource_ids": ["shp_123"],
|
|
354
|
+
"access": ["invalid_access"],
|
|
355
|
+
},
|
|
356
|
+
format="json",
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
self.assertEqual(response.status_code, 400)
|
|
360
|
+
|
|
361
|
+
def test_empty_resource_ids_fails(self):
|
|
362
|
+
"""Test that empty resource_ids is rejected."""
|
|
363
|
+
response = self.client.post(
|
|
364
|
+
"/api/tokens",
|
|
365
|
+
{
|
|
366
|
+
"resource_type": "shipment",
|
|
367
|
+
"resource_ids": [],
|
|
368
|
+
"access": ["label"],
|
|
369
|
+
},
|
|
370
|
+
format="json",
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
self.assertEqual(response.status_code, 400)
|
|
374
|
+
|
|
375
|
+
def test_custom_expiration_success(self):
|
|
376
|
+
"""Test custom token expiration."""
|
|
377
|
+
response = self.client.post(
|
|
378
|
+
"/api/tokens",
|
|
379
|
+
{
|
|
380
|
+
"resource_type": "shipment",
|
|
381
|
+
"resource_ids": ["shp_123"],
|
|
382
|
+
"access": ["label"],
|
|
383
|
+
"expires_in": 600,
|
|
384
|
+
},
|
|
385
|
+
format="json",
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
self.assertEqual(response.status_code, 201)
|
|
389
|
+
|
|
390
|
+
def test_response_has_no_cache_headers(self):
|
|
391
|
+
"""Test that token response includes cache prevention headers."""
|
|
392
|
+
response = self.client.post(
|
|
393
|
+
"/api/tokens",
|
|
394
|
+
{
|
|
395
|
+
"resource_type": "shipment",
|
|
396
|
+
"resource_ids": ["shp_123"],
|
|
397
|
+
"access": ["label"],
|
|
398
|
+
},
|
|
399
|
+
format="json",
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
self.assertEqual(response.status_code, 201)
|
|
403
|
+
self.assertDictEqual(
|
|
404
|
+
{
|
|
405
|
+
"cache_control": response.get("Cache-Control"),
|
|
406
|
+
"cdn_cache_control": response.get("CDN-Cache-Control"),
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
"cache_control": "no-store",
|
|
410
|
+
"cdn_cache_control": "no-store",
|
|
411
|
+
},
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
class TestDocumentDownloadWithAPIToken(KarrioAPITestCase):
|
|
416
|
+
"""Test document download endpoints with API Token authentication.
|
|
417
|
+
|
|
418
|
+
These tests verify that document download endpoints accept API Token
|
|
419
|
+
authentication as an alternative to resource access tokens.
|
|
420
|
+
"""
|
|
421
|
+
|
|
422
|
+
def setUp(self):
|
|
423
|
+
super().setUp()
|
|
424
|
+
from karrio.server.manager.models import Shipment, Address, Parcel, Manifest
|
|
425
|
+
|
|
426
|
+
# Create test addresses
|
|
427
|
+
self.shipper = Address.objects.create(
|
|
428
|
+
postal_code="E1C4Z8",
|
|
429
|
+
city="Moncton",
|
|
430
|
+
person_name="John Doe",
|
|
431
|
+
country_code="CA",
|
|
432
|
+
state_code="NB",
|
|
433
|
+
address_line1="125 Church St",
|
|
434
|
+
created_by=self.user,
|
|
435
|
+
)
|
|
436
|
+
self.recipient = Address.objects.create(
|
|
437
|
+
postal_code="V6M2V9",
|
|
438
|
+
city="Vancouver",
|
|
439
|
+
person_name="Jane Doe",
|
|
440
|
+
country_code="CA",
|
|
441
|
+
state_code="BC",
|
|
442
|
+
address_line1="5840 Oak St",
|
|
443
|
+
created_by=self.user,
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
# Create test parcel
|
|
447
|
+
self.parcel = Parcel.objects.create(
|
|
448
|
+
weight=1.0,
|
|
449
|
+
weight_unit="KG",
|
|
450
|
+
created_by=self.user,
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
# Create test shipment with label
|
|
454
|
+
self.shipment = Shipment.objects.create(
|
|
455
|
+
shipper=self.shipper,
|
|
456
|
+
recipient=self.recipient,
|
|
457
|
+
created_by=self.user,
|
|
458
|
+
test_mode=True,
|
|
459
|
+
status="purchased",
|
|
460
|
+
tracking_number="TEST123456",
|
|
461
|
+
label="JVBERi0xLjQKMSAwIG9iago8PAovVGl0bGUgKP7/AFQAZQBzAHQpCj4+CmVuZG9iagoyIDAgb2JqCjw8Cj4+CmVuZG9iagozIDAgb2JqCjw8Cj4+CmVuZG9iagp4cmVmCjAgNAowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMTUgMDAwMDAgbiAKMDAwMDAwMDA2OCAwMDAwMCBuIAowMDAwMDAwMDg5IDAwMDAwIG4gCnRyYWlsZXIKPDwKL1NpemUgNAo+PgpzdGFydHhyZWYKMTEwCiUlRU9GCg==", # Base64 encoded minimal PDF
|
|
462
|
+
label_type="PDF",
|
|
463
|
+
)
|
|
464
|
+
self.shipment.parcels.set([self.parcel])
|
|
465
|
+
|
|
466
|
+
# Create address for manifest
|
|
467
|
+
self.manifest_address = Address.objects.create(
|
|
468
|
+
postal_code="E1C4Z8",
|
|
469
|
+
city="Moncton",
|
|
470
|
+
person_name="Manifest Address",
|
|
471
|
+
country_code="CA",
|
|
472
|
+
state_code="NB",
|
|
473
|
+
address_line1="125 Church St",
|
|
474
|
+
created_by=self.user,
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
# Create test manifest with document
|
|
478
|
+
self.manifest = Manifest.objects.create(
|
|
479
|
+
created_by=self.user,
|
|
480
|
+
test_mode=True,
|
|
481
|
+
address=self.manifest_address,
|
|
482
|
+
manifest="JVBERi0xLjQKMSAwIG9iago8PAovVGl0bGUgKP7/AFQAZQBzAHQpCj4+CmVuZG9iagoyIDAgb2JqCjw8Cj4+CmVuZG9iagozIDAgb2JqCjw8Cj4+CmVuZG9iagp4cmVmCjAgNAowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMTUgMDAwMDAgbiAKMDAwMDAwMDA2OCAwMDAwMCBuIAowMDAwMDAwMDg5IDAwMDAwIG4gCnRyYWlsZXIKPDwKL1NpemUgNAo+PgpzdGFydHhyZWYKMTEwCiUlRU9GCg==", # Base64 encoded minimal PDF
|
|
483
|
+
manifest_carrier=self.carrier,
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
def test_shipment_label_download_with_api_token(self):
|
|
487
|
+
"""Test that shipment label can be downloaded with API Token auth."""
|
|
488
|
+
# Request with API Token (no resource token needed)
|
|
489
|
+
response = self.client.get(
|
|
490
|
+
f"/v1/shipments/{self.shipment.pk}/label.pdf",
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
self.assertEqual(response.status_code, 200)
|
|
494
|
+
self.assertTrue(response.get("Content-Type", "").startswith("application/pdf"))
|
|
495
|
+
|
|
496
|
+
def test_shipment_label_download_without_auth_fails(self):
|
|
497
|
+
"""Test that shipment label download fails without any authentication."""
|
|
498
|
+
# Clear credentials
|
|
499
|
+
self.client.credentials()
|
|
500
|
+
|
|
501
|
+
response = self.client.get(
|
|
502
|
+
f"/v1/shipments/{self.shipment.pk}/label.pdf",
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
self.assertEqual(response.status_code, 403)
|
|
506
|
+
|
|
507
|
+
def test_shipment_label_download_with_resource_token(self):
|
|
508
|
+
"""Test that shipment label download still works with resource token."""
|
|
509
|
+
# Clear API credentials
|
|
510
|
+
self.client.credentials()
|
|
511
|
+
|
|
512
|
+
# Generate resource token first (re-authenticate for this call)
|
|
513
|
+
self.client.credentials(HTTP_AUTHORIZATION=f"Token {self.token.key}")
|
|
514
|
+
token_response = self.client.post(
|
|
515
|
+
"/api/tokens",
|
|
516
|
+
{
|
|
517
|
+
"resource_type": "shipment",
|
|
518
|
+
"resource_ids": [self.shipment.pk],
|
|
519
|
+
"access": ["label"],
|
|
520
|
+
"format": "pdf",
|
|
521
|
+
},
|
|
522
|
+
format="json",
|
|
523
|
+
)
|
|
524
|
+
self.assertEqual(token_response.status_code, 201)
|
|
525
|
+
resource_token = token_response.data["token"]
|
|
526
|
+
|
|
527
|
+
# Clear credentials again and use resource token
|
|
528
|
+
self.client.credentials()
|
|
529
|
+
response = self.client.get(
|
|
530
|
+
f"/v1/shipments/{self.shipment.pk}/label.pdf?token={resource_token}",
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
self.assertEqual(response.status_code, 200)
|
|
534
|
+
self.assertTrue(response.get("Content-Type", "").startswith("application/pdf"))
|
|
535
|
+
|
|
536
|
+
def test_manifest_download_with_api_token(self):
|
|
537
|
+
"""Test that manifest document can be downloaded with API Token auth."""
|
|
538
|
+
response = self.client.get(
|
|
539
|
+
f"/v1/manifests/{self.manifest.pk}/manifest.pdf",
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
self.assertEqual(response.status_code, 200)
|
|
543
|
+
self.assertTrue(response.get("Content-Type", "").startswith("application/pdf"))
|
|
544
|
+
|
|
545
|
+
def test_manifest_download_without_auth_fails(self):
|
|
546
|
+
"""Test that manifest download fails without any authentication."""
|
|
547
|
+
self.client.credentials()
|
|
548
|
+
|
|
549
|
+
response = self.client.get(
|
|
550
|
+
f"/v1/manifests/{self.manifest.pk}/manifest.pdf",
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
self.assertEqual(response.status_code, 403)
|
|
554
|
+
|
|
555
|
+
def test_manifest_download_with_resource_token(self):
|
|
556
|
+
"""Test that manifest download still works with resource token."""
|
|
557
|
+
# Clear API credentials
|
|
558
|
+
self.client.credentials()
|
|
559
|
+
|
|
560
|
+
# Generate resource token first
|
|
561
|
+
self.client.credentials(HTTP_AUTHORIZATION=f"Token {self.token.key}")
|
|
562
|
+
token_response = self.client.post(
|
|
563
|
+
"/api/tokens",
|
|
564
|
+
{
|
|
565
|
+
"resource_type": "manifest",
|
|
566
|
+
"resource_ids": [self.manifest.pk],
|
|
567
|
+
"access": ["manifest"],
|
|
568
|
+
},
|
|
569
|
+
format="json",
|
|
570
|
+
)
|
|
571
|
+
self.assertEqual(token_response.status_code, 201)
|
|
572
|
+
resource_token = token_response.data["token"]
|
|
573
|
+
|
|
574
|
+
# Clear credentials and use resource token
|
|
575
|
+
self.client.credentials()
|
|
576
|
+
response = self.client.get(
|
|
577
|
+
f"/v1/manifests/{self.manifest.pk}/manifest.pdf?token={resource_token}",
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
self.assertEqual(response.status_code, 200)
|
|
581
|
+
self.assertTrue(response.get("Content-Type", "").startswith("application/pdf"))
|
|
582
|
+
|
|
583
|
+
def test_batch_labels_without_auth_fails(self):
|
|
584
|
+
"""Test that batch labels endpoint requires authentication."""
|
|
585
|
+
# Clear credentials to test unauthenticated access
|
|
586
|
+
self.client.credentials()
|
|
587
|
+
|
|
588
|
+
response = self.client.get(
|
|
589
|
+
f"/documents/shipments/label.pdf?shipments={self.shipment.pk}",
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
# Should return 403 (Forbidden) when no auth is provided
|
|
593
|
+
self.assertEqual(response.status_code, 403)
|