karrio-server-manager 2026.1__py3-none-any.whl → 2026.1.3__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/manager/migrations/0070_add_meta_and_product_fields.py +98 -0
- karrio/server/manager/migrations/0071_product_proxy.py +25 -0
- karrio/server/manager/migrations/0072_populate_json_fields.py +267 -0
- karrio/server/manager/migrations/0073_make_shipment_fk_nullable.py +36 -0
- karrio/server/manager/migrations/0074_clean_model_refactoring.py +207 -0
- karrio/server/manager/migrations/0075_populate_template_meta.py +69 -0
- karrio/server/manager/migrations/0076_remove_customs_model.py +66 -0
- karrio/server/manager/migrations/0077_add_carrier_snapshot_fields.py +83 -0
- karrio/server/manager/migrations/0078_populate_carrier_snapshots.py +112 -0
- karrio/server/manager/migrations/0079_remove_carrier_fk_fields.py +56 -0
- karrio/server/manager/migrations/0080_add_carrier_json_indexes.py +137 -0
- karrio/server/manager/migrations/0081_cleanup.py +62 -0
- karrio/server/manager/migrations/0082_shipment_fees.py +26 -0
- karrio/server/manager/models.py +421 -321
- karrio/server/manager/serializers/__init__.py +5 -4
- karrio/server/manager/serializers/address.py +8 -2
- karrio/server/manager/serializers/commodity.py +11 -4
- karrio/server/manager/serializers/document.py +29 -15
- karrio/server/manager/serializers/manifest.py +6 -3
- karrio/server/manager/serializers/parcel.py +5 -2
- karrio/server/manager/serializers/pickup.py +194 -67
- karrio/server/manager/serializers/shipment.py +232 -152
- karrio/server/manager/serializers/tracking.py +53 -12
- karrio/server/manager/tests/__init__.py +0 -1
- karrio/server/manager/tests/test_addresses.py +53 -0
- karrio/server/manager/tests/test_parcels.py +50 -0
- karrio/server/manager/tests/test_pickups.py +286 -50
- karrio/server/manager/tests/test_products.py +597 -0
- karrio/server/manager/tests/test_shipments.py +237 -92
- karrio/server/manager/tests/test_trackers.py +65 -1
- karrio/server/manager/views/__init__.py +1 -1
- karrio/server/manager/views/addresses.py +38 -2
- karrio/server/manager/views/documents.py +1 -1
- karrio/server/manager/views/parcels.py +25 -2
- karrio/server/manager/views/products.py +239 -0
- karrio/server/manager/views/trackers.py +69 -1
- {karrio_server_manager-2026.1.dist-info → karrio_server_manager-2026.1.3.dist-info}/METADATA +1 -1
- {karrio_server_manager-2026.1.dist-info → karrio_server_manager-2026.1.3.dist-info}/RECORD +40 -28
- {karrio_server_manager-2026.1.dist-info → karrio_server_manager-2026.1.3.dist-info}/WHEEL +1 -1
- karrio/server/manager/serializers/customs.py +0 -84
- karrio/server/manager/tests/test_custom_infos.py +0 -101
- karrio/server/manager/views/customs.py +0 -159
- {karrio_server_manager-2026.1.dist-info → karrio_server_manager-2026.1.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Product REST API Tests
|
|
3
|
+
|
|
4
|
+
Comprehensive tests for the full CRUD operations on product templates.
|
|
5
|
+
Covers all endpoints, edge cases, validation, and access control.
|
|
6
|
+
"""
|
|
7
|
+
import json
|
|
8
|
+
from unittest.mock import ANY
|
|
9
|
+
from django.urls import reverse
|
|
10
|
+
from rest_framework import status
|
|
11
|
+
from karrio.server.core.tests import APITestCase
|
|
12
|
+
from karrio.server.manager.models import Commodity
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestProductsList(APITestCase):
|
|
16
|
+
"""Tests for GET /v1/products and POST /v1/products"""
|
|
17
|
+
|
|
18
|
+
def test_create_product(self):
|
|
19
|
+
"""Test creating a new product template."""
|
|
20
|
+
url = reverse("karrio.server.manager:product-list")
|
|
21
|
+
data = PRODUCT_DATA
|
|
22
|
+
|
|
23
|
+
response = self.client.post(url, data)
|
|
24
|
+
response_data = json.loads(response.content)
|
|
25
|
+
|
|
26
|
+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
|
27
|
+
self.assertDictEqual(response_data, PRODUCT_RESPONSE)
|
|
28
|
+
|
|
29
|
+
def test_create_product_minimal(self):
|
|
30
|
+
"""Test creating a product with only required fields."""
|
|
31
|
+
url = reverse("karrio.server.manager:product-list")
|
|
32
|
+
data = {
|
|
33
|
+
"weight": 0.5,
|
|
34
|
+
"weight_unit": "KG",
|
|
35
|
+
"meta": {"label": "Minimal Product"},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
response = self.client.post(url, data)
|
|
39
|
+
response_data = json.loads(response.content)
|
|
40
|
+
|
|
41
|
+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
|
42
|
+
self.assertEqual(response_data["weight"], 0.5)
|
|
43
|
+
self.assertEqual(response_data["weight_unit"], "KG")
|
|
44
|
+
self.assertEqual(response_data["meta"]["label"], "Minimal Product")
|
|
45
|
+
|
|
46
|
+
def test_create_product_with_default_flag(self):
|
|
47
|
+
"""Test creating a product marked as default."""
|
|
48
|
+
url = reverse("karrio.server.manager:product-list")
|
|
49
|
+
data = {
|
|
50
|
+
"weight": 1.0,
|
|
51
|
+
"weight_unit": "LB",
|
|
52
|
+
"title": "Default Product",
|
|
53
|
+
"meta": {"label": "Default Product", "is_default": True},
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
response = self.client.post(url, data)
|
|
57
|
+
response_data = json.loads(response.content)
|
|
58
|
+
|
|
59
|
+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
|
60
|
+
self.assertTrue(response_data["meta"]["is_default"])
|
|
61
|
+
|
|
62
|
+
def test_create_product_without_label_fails(self):
|
|
63
|
+
"""Test that creating a product without meta.label fails."""
|
|
64
|
+
url = reverse("karrio.server.manager:product-list")
|
|
65
|
+
data = {
|
|
66
|
+
"weight": 1.5,
|
|
67
|
+
"weight_unit": "KG",
|
|
68
|
+
"title": "Test Product",
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
response = self.client.post(url, data)
|
|
72
|
+
|
|
73
|
+
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
|
74
|
+
|
|
75
|
+
def test_create_product_with_empty_label_fails(self):
|
|
76
|
+
"""Test that creating a product with empty meta.label fails."""
|
|
77
|
+
url = reverse("karrio.server.manager:product-list")
|
|
78
|
+
data = {
|
|
79
|
+
"weight": 1.5,
|
|
80
|
+
"weight_unit": "KG",
|
|
81
|
+
"meta": {"label": ""},
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
response = self.client.post(url, data)
|
|
85
|
+
|
|
86
|
+
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
|
87
|
+
|
|
88
|
+
def test_create_product_with_null_label_fails(self):
|
|
89
|
+
"""Test that creating a product with null meta.label fails."""
|
|
90
|
+
url = reverse("karrio.server.manager:product-list")
|
|
91
|
+
data = {
|
|
92
|
+
"weight": 1.5,
|
|
93
|
+
"weight_unit": "KG",
|
|
94
|
+
"meta": {"label": None},
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
response = self.client.post(url, data)
|
|
98
|
+
|
|
99
|
+
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
|
100
|
+
|
|
101
|
+
def test_create_product_without_weight_fails(self):
|
|
102
|
+
"""Test that creating a product without weight fails validation."""
|
|
103
|
+
url = reverse("karrio.server.manager:product-list")
|
|
104
|
+
data = {
|
|
105
|
+
"weight_unit": "KG",
|
|
106
|
+
"title": "No Weight Product",
|
|
107
|
+
"meta": {"label": "No Weight"},
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
response = self.client.post(url, data)
|
|
111
|
+
|
|
112
|
+
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
|
113
|
+
|
|
114
|
+
def test_create_product_without_weight_unit_fails(self):
|
|
115
|
+
"""Test that creating a product without weight_unit fails validation."""
|
|
116
|
+
url = reverse("karrio.server.manager:product-list")
|
|
117
|
+
data = {
|
|
118
|
+
"weight": 1.5,
|
|
119
|
+
"title": "No Weight Unit Product",
|
|
120
|
+
"meta": {"label": "No Weight Unit"},
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
response = self.client.post(url, data)
|
|
124
|
+
|
|
125
|
+
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
|
126
|
+
|
|
127
|
+
def test_create_product_with_all_fields(self):
|
|
128
|
+
"""Test creating a product with all available fields."""
|
|
129
|
+
url = reverse("karrio.server.manager:product-list")
|
|
130
|
+
data = {
|
|
131
|
+
"weight": 2.5,
|
|
132
|
+
"weight_unit": "LB",
|
|
133
|
+
"quantity": 10,
|
|
134
|
+
"sku": "FULL-SKU-001",
|
|
135
|
+
"title": "Full Product",
|
|
136
|
+
"hs_code": "9876.54",
|
|
137
|
+
"description": "A full product with all fields",
|
|
138
|
+
"value_amount": 149.99,
|
|
139
|
+
"value_currency": "EUR",
|
|
140
|
+
"origin_country": "DE",
|
|
141
|
+
"product_url": "https://example.com/product",
|
|
142
|
+
"image_url": "https://example.com/image.jpg",
|
|
143
|
+
"product_id": "prod_123",
|
|
144
|
+
"variant_id": "var_456",
|
|
145
|
+
"metadata": {"custom_field": "custom_value"},
|
|
146
|
+
"meta": {"label": "Full Product", "is_default": True},
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
response = self.client.post(url, data)
|
|
150
|
+
response_data = json.loads(response.content)
|
|
151
|
+
|
|
152
|
+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
|
153
|
+
self.assertEqual(response_data["weight"], 2.5)
|
|
154
|
+
self.assertEqual(response_data["weight_unit"], "LB")
|
|
155
|
+
self.assertEqual(response_data["quantity"], 10)
|
|
156
|
+
self.assertEqual(response_data["sku"], "FULL-SKU-001")
|
|
157
|
+
self.assertEqual(response_data["value_currency"], "EUR")
|
|
158
|
+
self.assertEqual(response_data["origin_country"], "DE")
|
|
159
|
+
self.assertEqual(response_data["metadata"]["custom_field"], "custom_value")
|
|
160
|
+
|
|
161
|
+
def test_list_products(self):
|
|
162
|
+
"""Test listing all product templates."""
|
|
163
|
+
# Create a product first
|
|
164
|
+
Commodity.objects.create(
|
|
165
|
+
**{
|
|
166
|
+
"weight": 1.0,
|
|
167
|
+
"weight_unit": "KG",
|
|
168
|
+
"title": "Existing Product",
|
|
169
|
+
"quantity": 1,
|
|
170
|
+
"meta": {"label": "Existing Product", "is_default": False},
|
|
171
|
+
"created_by": self.user,
|
|
172
|
+
}
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
url = reverse("karrio.server.manager:product-list")
|
|
176
|
+
response = self.client.get(url)
|
|
177
|
+
response_data = json.loads(response.content)
|
|
178
|
+
|
|
179
|
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
180
|
+
self.assertIn("results", response_data)
|
|
181
|
+
self.assertGreaterEqual(len(response_data["results"]), 1)
|
|
182
|
+
|
|
183
|
+
def test_list_products_empty(self):
|
|
184
|
+
"""Test listing products when none exist."""
|
|
185
|
+
url = reverse("karrio.server.manager:product-list")
|
|
186
|
+
response = self.client.get(url)
|
|
187
|
+
response_data = json.loads(response.content)
|
|
188
|
+
|
|
189
|
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
190
|
+
self.assertEqual(response_data["results"], [])
|
|
191
|
+
|
|
192
|
+
def test_list_products_excludes_non_templates(self):
|
|
193
|
+
"""Test that listing products excludes commodities without meta.label."""
|
|
194
|
+
# Create a commodity without meta.label (not a template)
|
|
195
|
+
Commodity.objects.create(
|
|
196
|
+
**{
|
|
197
|
+
"weight": 1.0,
|
|
198
|
+
"weight_unit": "KG",
|
|
199
|
+
"title": "Non-template Commodity",
|
|
200
|
+
"quantity": 1,
|
|
201
|
+
"meta": {}, # No label
|
|
202
|
+
"created_by": self.user,
|
|
203
|
+
}
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Create a product template
|
|
207
|
+
Commodity.objects.create(
|
|
208
|
+
**{
|
|
209
|
+
"weight": 2.0,
|
|
210
|
+
"weight_unit": "KG",
|
|
211
|
+
"title": "Template Product",
|
|
212
|
+
"quantity": 1,
|
|
213
|
+
"meta": {"label": "Template Product", "is_default": False},
|
|
214
|
+
"created_by": self.user,
|
|
215
|
+
}
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
url = reverse("karrio.server.manager:product-list")
|
|
219
|
+
response = self.client.get(url)
|
|
220
|
+
response_data = json.loads(response.content)
|
|
221
|
+
|
|
222
|
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
223
|
+
# Should only return the template, not the non-template commodity
|
|
224
|
+
self.assertEqual(len(response_data["results"]), 1)
|
|
225
|
+
self.assertEqual(
|
|
226
|
+
response_data["results"][0]["meta"]["label"], "Template Product"
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
def test_list_products_pagination(self):
|
|
230
|
+
"""Test that product listing supports pagination."""
|
|
231
|
+
# Create multiple products
|
|
232
|
+
for i in range(25):
|
|
233
|
+
Commodity.objects.create(
|
|
234
|
+
**{
|
|
235
|
+
"weight": float(i + 1),
|
|
236
|
+
"weight_unit": "KG",
|
|
237
|
+
"title": f"Product {i}",
|
|
238
|
+
"quantity": 1,
|
|
239
|
+
"meta": {"label": f"Product {i}", "is_default": False},
|
|
240
|
+
"created_by": self.user,
|
|
241
|
+
}
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
url = reverse("karrio.server.manager:product-list")
|
|
245
|
+
response = self.client.get(url)
|
|
246
|
+
response_data = json.loads(response.content)
|
|
247
|
+
|
|
248
|
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
249
|
+
# Default limit is 20
|
|
250
|
+
self.assertEqual(len(response_data["results"]), 20)
|
|
251
|
+
self.assertIn("next", response_data)
|
|
252
|
+
|
|
253
|
+
def test_list_products_with_limit(self):
|
|
254
|
+
"""Test listing products with custom limit."""
|
|
255
|
+
# Create multiple products
|
|
256
|
+
for i in range(10):
|
|
257
|
+
Commodity.objects.create(
|
|
258
|
+
**{
|
|
259
|
+
"weight": float(i + 1),
|
|
260
|
+
"weight_unit": "KG",
|
|
261
|
+
"title": f"Product {i}",
|
|
262
|
+
"quantity": 1,
|
|
263
|
+
"meta": {"label": f"Product {i}", "is_default": False},
|
|
264
|
+
"created_by": self.user,
|
|
265
|
+
}
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
url = reverse("karrio.server.manager:product-list") + "?limit=5"
|
|
269
|
+
response = self.client.get(url)
|
|
270
|
+
response_data = json.loads(response.content)
|
|
271
|
+
|
|
272
|
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
273
|
+
self.assertEqual(len(response_data["results"]), 5)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class TestProductDetails(APITestCase):
|
|
277
|
+
"""Tests for GET, PATCH, DELETE /v1/products/<pk>"""
|
|
278
|
+
|
|
279
|
+
def setUp(self) -> None:
|
|
280
|
+
super().setUp()
|
|
281
|
+
self.product: Commodity = Commodity.objects.create(
|
|
282
|
+
**{
|
|
283
|
+
"weight": 1.5,
|
|
284
|
+
"weight_unit": "KG",
|
|
285
|
+
"quantity": 1,
|
|
286
|
+
"sku": "TEST-SKU-001",
|
|
287
|
+
"title": "Test Product Title",
|
|
288
|
+
"hs_code": "123456",
|
|
289
|
+
"description": "A test product",
|
|
290
|
+
"value_amount": 99.99,
|
|
291
|
+
"value_currency": "USD",
|
|
292
|
+
"origin_country": "US",
|
|
293
|
+
"meta": {"label": "Test Product", "is_default": False},
|
|
294
|
+
"created_by": self.user,
|
|
295
|
+
}
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
def test_retrieve_product(self):
|
|
299
|
+
"""Test retrieving a product by ID."""
|
|
300
|
+
url = reverse(
|
|
301
|
+
"karrio.server.manager:product-details", kwargs=dict(pk=self.product.pk)
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
response = self.client.get(url)
|
|
305
|
+
response_data = json.loads(response.content)
|
|
306
|
+
|
|
307
|
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
308
|
+
self.assertEqual(response_data["id"], self.product.pk)
|
|
309
|
+
self.assertEqual(response_data["object_type"], "commodity")
|
|
310
|
+
self.assertEqual(response_data["meta"]["label"], "Test Product")
|
|
311
|
+
|
|
312
|
+
def test_retrieve_product_not_found(self):
|
|
313
|
+
"""Test retrieving a non-existent product returns 404."""
|
|
314
|
+
url = reverse(
|
|
315
|
+
"karrio.server.manager:product-details", kwargs=dict(pk="nonexistent_id")
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
response = self.client.get(url)
|
|
319
|
+
|
|
320
|
+
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
|
321
|
+
|
|
322
|
+
def test_update_product(self):
|
|
323
|
+
"""Test updating a product."""
|
|
324
|
+
url = reverse(
|
|
325
|
+
"karrio.server.manager:product-details", kwargs=dict(pk=self.product.pk)
|
|
326
|
+
)
|
|
327
|
+
data = PRODUCT_UPDATE_DATA
|
|
328
|
+
|
|
329
|
+
response = self.client.patch(url, data)
|
|
330
|
+
response_data = json.loads(response.content)
|
|
331
|
+
|
|
332
|
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
333
|
+
self.assertDictEqual(response_data, PRODUCT_UPDATE_RESPONSE)
|
|
334
|
+
|
|
335
|
+
def test_update_product_label(self):
|
|
336
|
+
"""Test updating a product's label."""
|
|
337
|
+
url = reverse(
|
|
338
|
+
"karrio.server.manager:product-details", kwargs=dict(pk=self.product.pk)
|
|
339
|
+
)
|
|
340
|
+
data = {"meta": {"label": "Updated Label"}}
|
|
341
|
+
|
|
342
|
+
response = self.client.patch(url, data)
|
|
343
|
+
response_data = json.loads(response.content)
|
|
344
|
+
|
|
345
|
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
346
|
+
self.assertEqual(response_data["meta"]["label"], "Updated Label")
|
|
347
|
+
|
|
348
|
+
def test_update_product_default_flag(self):
|
|
349
|
+
"""Test updating a product's default flag."""
|
|
350
|
+
url = reverse(
|
|
351
|
+
"karrio.server.manager:product-details", kwargs=dict(pk=self.product.pk)
|
|
352
|
+
)
|
|
353
|
+
data = {"meta": {"is_default": True}}
|
|
354
|
+
|
|
355
|
+
response = self.client.patch(url, data)
|
|
356
|
+
response_data = json.loads(response.content)
|
|
357
|
+
|
|
358
|
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
359
|
+
self.assertTrue(response_data["meta"]["is_default"])
|
|
360
|
+
|
|
361
|
+
def test_update_product_single_field(self):
|
|
362
|
+
"""Test updating a single field on a product."""
|
|
363
|
+
url = reverse(
|
|
364
|
+
"karrio.server.manager:product-details", kwargs=dict(pk=self.product.pk)
|
|
365
|
+
)
|
|
366
|
+
data = {"weight": 3.0}
|
|
367
|
+
|
|
368
|
+
response = self.client.patch(url, data)
|
|
369
|
+
response_data = json.loads(response.content)
|
|
370
|
+
|
|
371
|
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
372
|
+
self.assertEqual(response_data["weight"], 3.0)
|
|
373
|
+
# Verify other fields unchanged
|
|
374
|
+
self.assertEqual(response_data["title"], "Test Product Title")
|
|
375
|
+
|
|
376
|
+
def test_update_product_sku(self):
|
|
377
|
+
"""Test updating a product's SKU."""
|
|
378
|
+
url = reverse(
|
|
379
|
+
"karrio.server.manager:product-details", kwargs=dict(pk=self.product.pk)
|
|
380
|
+
)
|
|
381
|
+
data = {"sku": "NEW-SKU-002"}
|
|
382
|
+
|
|
383
|
+
response = self.client.patch(url, data)
|
|
384
|
+
response_data = json.loads(response.content)
|
|
385
|
+
|
|
386
|
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
387
|
+
self.assertEqual(response_data["sku"], "NEW-SKU-002")
|
|
388
|
+
|
|
389
|
+
def test_update_product_value_and_currency(self):
|
|
390
|
+
"""Test updating a product's value and currency."""
|
|
391
|
+
url = reverse(
|
|
392
|
+
"karrio.server.manager:product-details", kwargs=dict(pk=self.product.pk)
|
|
393
|
+
)
|
|
394
|
+
data = {"value_amount": 199.99, "value_currency": "CAD"}
|
|
395
|
+
|
|
396
|
+
response = self.client.patch(url, data)
|
|
397
|
+
response_data = json.loads(response.content)
|
|
398
|
+
|
|
399
|
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
400
|
+
self.assertEqual(response_data["value_amount"], 199.99)
|
|
401
|
+
self.assertEqual(response_data["value_currency"], "CAD")
|
|
402
|
+
|
|
403
|
+
def test_update_product_metadata(self):
|
|
404
|
+
"""Test updating a product's metadata."""
|
|
405
|
+
url = reverse(
|
|
406
|
+
"karrio.server.manager:product-details", kwargs=dict(pk=self.product.pk)
|
|
407
|
+
)
|
|
408
|
+
data = {"metadata": {"custom_key": "custom_value"}}
|
|
409
|
+
|
|
410
|
+
response = self.client.patch(url, data)
|
|
411
|
+
response_data = json.loads(response.content)
|
|
412
|
+
|
|
413
|
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
414
|
+
self.assertEqual(response_data["metadata"]["custom_key"], "custom_value")
|
|
415
|
+
|
|
416
|
+
def test_update_product_not_found(self):
|
|
417
|
+
"""Test updating a non-existent product returns 404."""
|
|
418
|
+
url = reverse(
|
|
419
|
+
"karrio.server.manager:product-details", kwargs=dict(pk="nonexistent_id")
|
|
420
|
+
)
|
|
421
|
+
data = {"weight": 2.0}
|
|
422
|
+
|
|
423
|
+
response = self.client.patch(url, data)
|
|
424
|
+
|
|
425
|
+
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
|
426
|
+
|
|
427
|
+
def test_delete_product(self):
|
|
428
|
+
"""Test deleting a product."""
|
|
429
|
+
product_pk = self.product.pk
|
|
430
|
+
url = reverse(
|
|
431
|
+
"karrio.server.manager:product-details", kwargs=dict(pk=product_pk)
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
response = self.client.delete(url)
|
|
435
|
+
response_data = json.loads(response.content)
|
|
436
|
+
|
|
437
|
+
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
438
|
+
self.assertEqual(response_data["object_type"], "commodity")
|
|
439
|
+
self.assertFalse(Commodity.objects.filter(pk=product_pk).exists())
|
|
440
|
+
|
|
441
|
+
def test_delete_product_not_found(self):
|
|
442
|
+
"""Test deleting a non-existent product returns 404."""
|
|
443
|
+
url = reverse(
|
|
444
|
+
"karrio.server.manager:product-details", kwargs=dict(pk="nonexistent_id")
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
response = self.client.delete(url)
|
|
448
|
+
|
|
449
|
+
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
class TestProductValidation(APITestCase):
|
|
453
|
+
"""Tests for product validation rules."""
|
|
454
|
+
|
|
455
|
+
def test_invalid_weight_unit(self):
|
|
456
|
+
"""Test that invalid weight unit fails validation."""
|
|
457
|
+
url = reverse("karrio.server.manager:product-list")
|
|
458
|
+
data = {
|
|
459
|
+
"weight": 1.5,
|
|
460
|
+
"weight_unit": "INVALID",
|
|
461
|
+
"meta": {"label": "Invalid Weight Unit"},
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
response = self.client.post(url, data)
|
|
465
|
+
|
|
466
|
+
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
|
467
|
+
|
|
468
|
+
def test_invalid_value_currency(self):
|
|
469
|
+
"""Test that invalid currency code fails validation."""
|
|
470
|
+
url = reverse("karrio.server.manager:product-list")
|
|
471
|
+
data = {
|
|
472
|
+
"weight": 1.5,
|
|
473
|
+
"weight_unit": "KG",
|
|
474
|
+
"value_amount": 100,
|
|
475
|
+
"value_currency": "INVALID",
|
|
476
|
+
"meta": {"label": "Invalid Currency"},
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
response = self.client.post(url, data)
|
|
480
|
+
|
|
481
|
+
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
|
482
|
+
|
|
483
|
+
def test_invalid_origin_country(self):
|
|
484
|
+
"""Test that invalid country code fails validation."""
|
|
485
|
+
url = reverse("karrio.server.manager:product-list")
|
|
486
|
+
data = {
|
|
487
|
+
"weight": 1.5,
|
|
488
|
+
"weight_unit": "KG",
|
|
489
|
+
"origin_country": "INVALID",
|
|
490
|
+
"meta": {"label": "Invalid Country"},
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
response = self.client.post(url, data)
|
|
494
|
+
|
|
495
|
+
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
|
496
|
+
|
|
497
|
+
def test_negative_weight(self):
|
|
498
|
+
"""Test that negative weight is handled."""
|
|
499
|
+
url = reverse("karrio.server.manager:product-list")
|
|
500
|
+
data = {
|
|
501
|
+
"weight": -1.5,
|
|
502
|
+
"weight_unit": "KG",
|
|
503
|
+
"meta": {"label": "Negative Weight"},
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
response = self.client.post(url, data)
|
|
507
|
+
# The API may accept negative weight (floats aren't validated for min)
|
|
508
|
+
# This tests the current behavior
|
|
509
|
+
|
|
510
|
+
def test_zero_weight(self):
|
|
511
|
+
"""Test that zero weight is handled."""
|
|
512
|
+
url = reverse("karrio.server.manager:product-list")
|
|
513
|
+
data = {
|
|
514
|
+
"weight": 0,
|
|
515
|
+
"weight_unit": "KG",
|
|
516
|
+
"meta": {"label": "Zero Weight"},
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
response = self.client.post(url, data)
|
|
520
|
+
# Zero weight might be allowed for some use cases
|
|
521
|
+
|
|
522
|
+
def test_negative_quantity(self):
|
|
523
|
+
"""Test that negative quantity is handled."""
|
|
524
|
+
url = reverse("karrio.server.manager:product-list")
|
|
525
|
+
data = {
|
|
526
|
+
"weight": 1.5,
|
|
527
|
+
"weight_unit": "KG",
|
|
528
|
+
"quantity": -1,
|
|
529
|
+
"meta": {"label": "Negative Quantity"},
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
response = self.client.post(url, data)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
# Test data
|
|
536
|
+
PRODUCT_DATA = {
|
|
537
|
+
"weight": 1.5,
|
|
538
|
+
"weight_unit": "KG",
|
|
539
|
+
"quantity": 1,
|
|
540
|
+
"sku": "TEST-SKU-001",
|
|
541
|
+
"title": "Test Product Title",
|
|
542
|
+
"hs_code": "123456",
|
|
543
|
+
"description": "A test product",
|
|
544
|
+
"value_amount": 99.99,
|
|
545
|
+
"value_currency": "USD",
|
|
546
|
+
"origin_country": "US",
|
|
547
|
+
"meta": {"label": "Test Product", "is_default": False},
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
PRODUCT_RESPONSE = {
|
|
551
|
+
"id": ANY,
|
|
552
|
+
"object_type": "commodity",
|
|
553
|
+
"weight": 1.5,
|
|
554
|
+
"weight_unit": "KG",
|
|
555
|
+
"title": "Test Product Title",
|
|
556
|
+
"description": "A test product",
|
|
557
|
+
"quantity": 1,
|
|
558
|
+
"sku": "TEST-SKU-001",
|
|
559
|
+
"hs_code": "123456",
|
|
560
|
+
"value_amount": 99.99,
|
|
561
|
+
"value_currency": "USD",
|
|
562
|
+
"origin_country": "US",
|
|
563
|
+
"product_url": None,
|
|
564
|
+
"image_url": None,
|
|
565
|
+
"product_id": None,
|
|
566
|
+
"variant_id": None,
|
|
567
|
+
"parent_id": None,
|
|
568
|
+
"metadata": {},
|
|
569
|
+
"meta": {"label": "Test Product", "is_default": False},
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
PRODUCT_UPDATE_DATA = {
|
|
573
|
+
"weight": 2.0,
|
|
574
|
+
"title": "Updated Title",
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
PRODUCT_UPDATE_RESPONSE = {
|
|
578
|
+
"id": ANY,
|
|
579
|
+
"object_type": "commodity",
|
|
580
|
+
"weight": 2.0,
|
|
581
|
+
"weight_unit": "KG",
|
|
582
|
+
"title": "Updated Title",
|
|
583
|
+
"description": "A test product",
|
|
584
|
+
"quantity": 1,
|
|
585
|
+
"sku": "TEST-SKU-001",
|
|
586
|
+
"hs_code": "123456",
|
|
587
|
+
"value_amount": 99.99,
|
|
588
|
+
"value_currency": "USD",
|
|
589
|
+
"origin_country": "US",
|
|
590
|
+
"product_url": None,
|
|
591
|
+
"image_url": None,
|
|
592
|
+
"product_id": None,
|
|
593
|
+
"variant_id": None,
|
|
594
|
+
"parent_id": None,
|
|
595
|
+
"metadata": {},
|
|
596
|
+
"meta": {"label": "Test Product", "is_default": False},
|
|
597
|
+
}
|