karrio-server-core 2025.5__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.
Files changed (213) hide show
  1. karrio/server/conf.py +54 -0
  2. karrio/server/core/__init__.py +3 -0
  3. karrio/server/core/admin.py +1 -0
  4. karrio/server/core/apps.py +10 -0
  5. karrio/server/core/authentication.py +347 -0
  6. karrio/server/core/config.py +31 -0
  7. karrio/server/core/context_processors.py +12 -0
  8. karrio/server/core/datatypes.py +394 -0
  9. karrio/server/core/dataunits.py +187 -0
  10. karrio/server/core/exceptions.py +404 -0
  11. karrio/server/core/fields.py +12 -0
  12. karrio/server/core/filters.py +837 -0
  13. karrio/server/core/gateway.py +1011 -0
  14. karrio/server/core/logging.py +403 -0
  15. karrio/server/core/management/commands/cli.py +19 -0
  16. karrio/server/core/management/commands/create_oauth_client.py +41 -0
  17. karrio/server/core/management/commands/runserver.py +5 -0
  18. karrio/server/core/middleware.py +197 -0
  19. karrio/server/core/migrations/0001_initial.py +28 -0
  20. karrio/server/core/migrations/0002_apilogindex.py +69 -0
  21. karrio/server/core/migrations/0003_apilogindex_test_mode.py +62 -0
  22. karrio/server/core/migrations/0004_metafield.py +74 -0
  23. karrio/server/core/migrations/0005_alter_metafield_type_alter_metafield_value.py +23 -0
  24. karrio/server/core/migrations/0006_add_api_log_requested_at_index.py +22 -0
  25. karrio/server/core/migrations/__init__.py +0 -0
  26. karrio/server/core/models/__init__.py +48 -0
  27. karrio/server/core/models/base.py +103 -0
  28. karrio/server/core/models/entity.py +24 -0
  29. karrio/server/core/models/metafield.py +144 -0
  30. karrio/server/core/models/third_party.py +21 -0
  31. karrio/server/core/oauth_validators.py +170 -0
  32. karrio/server/core/permissions.py +36 -0
  33. karrio/server/core/renderers.py +11 -0
  34. karrio/server/core/router.py +3 -0
  35. karrio/server/core/serializers.py +1971 -0
  36. karrio/server/core/signals.py +55 -0
  37. karrio/server/core/telemetry.py +573 -0
  38. karrio/server/core/tests.py +99 -0
  39. karrio/server/core/tests_resource_token.py +411 -0
  40. karrio/server/core/urls.py +12 -0
  41. karrio/server/core/utils.py +1025 -0
  42. karrio/server/core/validators.py +264 -0
  43. karrio/server/core/views/__init__.py +2 -0
  44. karrio/server/core/views/api.py +133 -0
  45. karrio/server/core/views/metadata.py +44 -0
  46. karrio/server/core/views/oauth.py +75 -0
  47. karrio/server/core/views/references.py +82 -0
  48. karrio/server/core/views/schema.py +310 -0
  49. karrio/server/filters/__init__.py +2 -0
  50. karrio/server/filters/abstract.py +26 -0
  51. karrio/server/iam/__init__.py +0 -0
  52. karrio/server/iam/admin.py +3 -0
  53. karrio/server/iam/apps.py +21 -0
  54. karrio/server/iam/migrations/0001_initial.py +33 -0
  55. karrio/server/iam/migrations/__init__.py +0 -0
  56. karrio/server/iam/models.py +48 -0
  57. karrio/server/iam/permissions.py +155 -0
  58. karrio/server/iam/serializers.py +54 -0
  59. karrio/server/iam/signals.py +18 -0
  60. karrio/server/iam/tests.py +3 -0
  61. karrio/server/iam/views.py +3 -0
  62. karrio/server/openapi.py +75 -0
  63. karrio/server/providers/__init__.py +1 -0
  64. karrio/server/providers/admin.py +364 -0
  65. karrio/server/providers/apps.py +10 -0
  66. karrio/server/providers/management/commands/migrate_rate_sheets.py +101 -0
  67. karrio/server/providers/migrations/0001_initial.py +140 -0
  68. karrio/server/providers/migrations/0002_carrier_active.py +18 -0
  69. karrio/server/providers/migrations/0003_auto_20201230_0820.py +24 -0
  70. karrio/server/providers/migrations/0004_auto_20210212_0554.py +178 -0
  71. karrio/server/providers/migrations/0005_auto_20210212_0555.py +18 -0
  72. karrio/server/providers/migrations/0006_australiapostsettings.py +29 -0
  73. karrio/server/providers/migrations/0007_auto_20210213_0206.py +21 -0
  74. karrio/server/providers/migrations/0008_auto_20210214_0409.py +30 -0
  75. karrio/server/providers/migrations/0009_auto_20210308_0302.py +18 -0
  76. karrio/server/providers/migrations/0010_auto_20210409_0852.py +32 -0
  77. karrio/server/providers/migrations/0011_auto_20210409_0853.py +21 -0
  78. karrio/server/providers/migrations/0012_alter_carrier_options.py +17 -0
  79. karrio/server/providers/migrations/0013_tntsettings.py +30 -0
  80. karrio/server/providers/migrations/0014_auto_20210612_1608.py +46 -0
  81. karrio/server/providers/migrations/0015_auto_20210615_1601.py +28 -0
  82. karrio/server/providers/migrations/0016_alter_purolatorsettings_user_token.py +18 -0
  83. karrio/server/providers/migrations/0017_auto_20210805_0359.py +1293 -0
  84. karrio/server/providers/migrations/0018_alter_fedexsettings_user_key.py +18 -0
  85. karrio/server/providers/migrations/0019_dhlpolandsettings_servicelevel.py +65 -0
  86. karrio/server/providers/migrations/0020_genericsettings_labeltemplate.py +52 -0
  87. karrio/server/providers/migrations/0021_auto_20211231_2353.py +40 -0
  88. karrio/server/providers/migrations/0022_carrier_metadata.py +18 -0
  89. karrio/server/providers/migrations/0023_auto_20220124_1916.py +27 -0
  90. karrio/server/providers/migrations/0024_alter_genericsettings_custom_carrier_name.py +19 -0
  91. karrio/server/providers/migrations/0025_alter_servicelevel_service_code.py +19 -0
  92. karrio/server/providers/migrations/0026_auto_20220208_0132.py +59 -0
  93. karrio/server/providers/migrations/0027_auto_20220304_1340.py +29 -0
  94. karrio/server/providers/migrations/0028_auto_20220323_1500.py +33 -0
  95. karrio/server/providers/migrations/0029_easypostsettings.py +27 -0
  96. karrio/server/providers/migrations/0030_amazonmwssettings.py +29 -0
  97. karrio/server/providers/migrations/0031_delete_amazonmwssettings.py +18 -0
  98. karrio/server/providers/migrations/0032_alter_carrier_test.py +18 -0
  99. karrio/server/providers/migrations/0033_auto_20220708_1350.py +22 -0
  100. karrio/server/providers/migrations/0034_amazonmwssettings_dpdhlsettings.py +47 -0
  101. karrio/server/providers/migrations/0035_alter_carrier_capabilities.py +43 -0
  102. karrio/server/providers/migrations/0036_upsfreightsettings.py +31 -0
  103. karrio/server/providers/migrations/0037_chronopostsettings.py +29 -0
  104. karrio/server/providers/migrations/0038_alter_genericsettings_label_template.py +19 -0
  105. karrio/server/providers/migrations/0039_auto_20220906_0612.py +23 -0
  106. karrio/server/providers/migrations/0040_dpdhlsettings_services.py +18 -0
  107. karrio/server/providers/migrations/0041_auto_20221105_0705.py +38 -0
  108. karrio/server/providers/migrations/0042_auto_20221215_1642.py +23 -0
  109. karrio/server/providers/migrations/0043_alter_genericsettings_account_number_and_more.py +39 -0
  110. karrio/server/providers/migrations/0044_carrier_carrier_capabilities.py +64 -0
  111. karrio/server/providers/migrations/0045_alter_carrier_active_alter_carrier_carrier_id.py +31 -0
  112. karrio/server/providers/migrations/0046_remove_dpdhlsettings_signature_and_more.py +41 -0
  113. karrio/server/providers/migrations/0047_dpdsettings.py +286 -0
  114. karrio/server/providers/migrations/0048_servicelevel_min_weight_servicelevel_transit_days_and_more.py +64 -0
  115. karrio/server/providers/migrations/0049_boxknightsettings_geodissettings_lapostesettings_and_more.py +156 -0
  116. karrio/server/providers/migrations/0050_carrier_is_system_alter_carrier_metadata_and_more.py +106 -0
  117. karrio/server/providers/migrations/0051_rename_username_upssettings_client_id_and_more.py +31 -0
  118. karrio/server/providers/migrations/0052_alter_upssettings_account_number_and_more.py +20 -0
  119. karrio/server/providers/migrations/0053_locate2usettings.py +281 -0
  120. karrio/server/providers/migrations/0054_zoom2usettings.py +280 -0
  121. karrio/server/providers/migrations/0055_rename_amazonmwssettings_amazonshippingsettings_and_more.py +44 -0
  122. karrio/server/providers/migrations/0056_asendiaussettings_geodissettings_code_client_and_more.py +75 -0
  123. karrio/server/providers/migrations/0057_alter_servicelevel_weight_unit_belgianpostsettings.py +51 -0
  124. karrio/server/providers/migrations/0058_alliedexpresssettings.py +38 -0
  125. karrio/server/providers/migrations/0059_ratesheet.py +81 -0
  126. karrio/server/providers/migrations/0060_belgianpostsettings_rate_sheet_and_more.py +73 -0
  127. karrio/server/providers/migrations/0061_alliedexpresssettings_service_type.py +17 -0
  128. karrio/server/providers/migrations/0062_sendlesettings_account_country_code.py +257 -0
  129. karrio/server/providers/migrations/0063_servicelevel_metadata.py +25 -0
  130. karrio/server/providers/migrations/0064_alliedexpresslocalsettings.py +43 -0
  131. karrio/server/providers/migrations/0065_servicelevel_carrier_service_code_and_more.py +66 -0
  132. karrio/server/providers/migrations/0066_rename_fedexsettings_fedexwssettings_and_more.py +28 -0
  133. karrio/server/providers/migrations/0067_fedexsettings.py +283 -0
  134. karrio/server/providers/migrations/0068_fedexsettings_track_api_key_and_more.py +38 -0
  135. karrio/server/providers/migrations/0069_alter_canadapostsettings_contract_id_and_more.py +23 -0
  136. karrio/server/providers/migrations/0070_tgesettings_alter_carrier_capabilities.py +65 -0
  137. karrio/server/providers/migrations/0071_alter_tgesettings_my_toll_token.py +18 -0
  138. karrio/server/providers/migrations/0072_rename_eshippersettings_eshipperxmlsettings_and_more.py +28 -0
  139. karrio/server/providers/migrations/0073_delete_eshipperxmlsettings.py +41 -0
  140. karrio/server/providers/migrations/0074_eshippersettings.py +38 -0
  141. karrio/server/providers/migrations/0075_haypostsettings.py +40 -0
  142. karrio/server/providers/migrations/0076_rename_customer_registration_id_uspsinternationalsettings_account_number_and_more.py +125 -0
  143. karrio/server/providers/migrations/0077_uspswtinternationalsettings_uspswtsettings_and_more.py +165 -0
  144. karrio/server/providers/migrations/0078_auto_20240813_1552.py +120 -0
  145. karrio/server/providers/migrations/0079_alter_carrier_options_alter_ratesheet_created_by.py +31 -0
  146. karrio/server/providers/migrations/0080_alter_aramexsettings_account_country_code_and_more.py +3025 -0
  147. karrio/server/providers/migrations/0081_remove_alliedexpresssettings_carrier_ptr_and_more.py +338 -0
  148. karrio/server/providers/migrations/0082_add_zone_identifiers.py +50 -0
  149. karrio/server/providers/migrations/0083_add_optimized_rate_sheet_structure.py +33 -0
  150. karrio/server/providers/migrations/0084_alter_servicelevel_currency.py +168 -0
  151. karrio/server/providers/migrations/0085_populate_dhl_parcel_de_oauth_credentials.py +82 -0
  152. karrio/server/providers/migrations/0086_rename_dhl_parcel_de_customer_number_to_billing_number.py +71 -0
  153. karrio/server/providers/migrations/__init__.py +0 -0
  154. karrio/server/providers/models/__init__.py +16 -0
  155. karrio/server/providers/models/carrier.py +387 -0
  156. karrio/server/providers/models/config.py +30 -0
  157. karrio/server/providers/models/service.py +192 -0
  158. karrio/server/providers/models/sheet.py +287 -0
  159. karrio/server/providers/models/template.py +39 -0
  160. karrio/server/providers/models/utils.py +58 -0
  161. karrio/server/providers/router.py +3 -0
  162. karrio/server/providers/serializers/__init__.py +3 -0
  163. karrio/server/providers/serializers/base.py +538 -0
  164. karrio/server/providers/signals.py +25 -0
  165. karrio/server/providers/templates/providers/oauth_callback.html +105 -0
  166. karrio/server/providers/tests/__init__.py +5 -0
  167. karrio/server/providers/tests/test_connections.py +895 -0
  168. karrio/server/providers/urls.py +11 -0
  169. karrio/server/providers/views/__init__.py +0 -0
  170. karrio/server/providers/views/carriers.py +267 -0
  171. karrio/server/providers/views/connections.py +496 -0
  172. karrio/server/samples.py +352 -0
  173. karrio/server/serializers/__init__.py +2 -0
  174. karrio/server/serializers/abstract.py +602 -0
  175. karrio/server/tracing/__init__.py +0 -0
  176. karrio/server/tracing/admin.py +63 -0
  177. karrio/server/tracing/apps.py +8 -0
  178. karrio/server/tracing/migrations/0001_initial.py +41 -0
  179. karrio/server/tracing/migrations/0002_auto_20220710_1307.py +22 -0
  180. karrio/server/tracing/migrations/0003_auto_20221105_0317.py +43 -0
  181. karrio/server/tracing/migrations/0004_tracingrecord_carrier_account_idx.py +24 -0
  182. karrio/server/tracing/migrations/0005_optimise_tracingrecord_request_log_idx.py +25 -0
  183. karrio/server/tracing/migrations/0006_alter_tracingrecord_options_and_more.py +49 -0
  184. karrio/server/tracing/migrations/0007_tracingrecord_tracing_created_at_idx.py +19 -0
  185. karrio/server/tracing/migrations/__init__.py +0 -0
  186. karrio/server/tracing/models.py +82 -0
  187. karrio/server/tracing/tests.py +3 -0
  188. karrio/server/tracing/utils.py +109 -0
  189. karrio/server/user/__init__.py +0 -0
  190. karrio/server/user/admin.py +96 -0
  191. karrio/server/user/apps.py +7 -0
  192. karrio/server/user/forms.py +35 -0
  193. karrio/server/user/migrations/0001_initial.py +41 -0
  194. karrio/server/user/migrations/0002_token.py +29 -0
  195. karrio/server/user/migrations/0003_token_test_mode.py +20 -0
  196. karrio/server/user/migrations/0004_group.py +26 -0
  197. karrio/server/user/migrations/0005_token_label.py +21 -0
  198. karrio/server/user/migrations/0006_workspaceconfig.py +63 -0
  199. karrio/server/user/migrations/0007_user_metadata.py +25 -0
  200. karrio/server/user/migrations/__init__.py +0 -0
  201. karrio/server/user/models.py +218 -0
  202. karrio/server/user/serializers.py +47 -0
  203. karrio/server/user/templates/registration/login.html +108 -0
  204. karrio/server/user/templates/registration/registration_confirm_email.html +10 -0
  205. karrio/server/user/templates/registration/registration_confirm_email.txt +3 -0
  206. karrio/server/user/tests.py +3 -0
  207. karrio/server/user/urls.py +10 -0
  208. karrio/server/user/utils.py +60 -0
  209. karrio/server/user/views.py +9 -0
  210. karrio_server_core-2025.5.dist-info/METADATA +32 -0
  211. karrio_server_core-2025.5.dist-info/RECORD +213 -0
  212. karrio_server_core-2025.5.dist-info/WHEEL +5 -0
  213. karrio_server_core-2025.5.dist-info/top_level.txt +2 -0
@@ -0,0 +1,99 @@
1
+ from django.contrib.auth import get_user_model
2
+ from rest_framework.test import APITestCase as BaseAPITestCase, APIClient
3
+ from karrio.server.core.logging import logger
4
+
5
+ from karrio.server.user.models import Token
6
+ import karrio.server.iam.permissions as iam
7
+ import karrio.server.providers.models as providers
8
+
9
+ iam.setup_groups()
10
+
11
+
12
+ class APITestCase(BaseAPITestCase):
13
+ def setUp(self) -> None:
14
+ self.maxDiff = None
15
+ # Loguru is already configured globally in settings
16
+
17
+ # Setup user and API Token.
18
+ self.user = get_user_model().objects.create_superuser(
19
+ "admin@example.com", "test"
20
+ )
21
+ self.token = Token.objects.create(user=self.user, test_mode=True)
22
+
23
+ # Setup API client.
24
+ self.client = APIClient()
25
+ self.client.credentials(HTTP_AUTHORIZATION="Token " + self.token.key)
26
+
27
+ # Setup test carrier connections.
28
+ self.carrier = providers.Carrier.objects.create(
29
+ carrier_code="canadapost",
30
+ carrier_id="canadapost",
31
+ test_mode=True,
32
+ active=True,
33
+ created_by=self.user,
34
+ credentials=dict(
35
+ username="6e93d53968881714",
36
+ customer_number="2004381",
37
+ contract_id="42708517",
38
+ password="0bfa9fcb9853d1f51ee57a",
39
+ ),
40
+ )
41
+ self.ups_carrier = providers.Carrier.objects.create(
42
+ carrier_code="ups",
43
+ carrier_id="ups_package",
44
+ test_mode=True,
45
+ active=True,
46
+ created_by=self.user,
47
+ credentials=dict(
48
+ client_id="test",
49
+ client_secret="test",
50
+ account_number="000000",
51
+ ),
52
+ )
53
+ self.fedex_carrier = providers.Carrier.objects.create(
54
+ carrier_code="fedex",
55
+ carrier_id="fedex_express",
56
+ test_mode=True,
57
+ active=True,
58
+ created_by=self.user,
59
+ credentials=dict(
60
+ api_key="test",
61
+ secret_key="password",
62
+ account_number="000000",
63
+ track_api_key="test",
64
+ track_secret_key="password",
65
+ ),
66
+ )
67
+ self.dhl_carrier = providers.Carrier.objects.create(
68
+ carrier_code="dhl_express",
69
+ carrier_id="dhl_express",
70
+ test_mode=True,
71
+ active=True,
72
+ created_by=self.user,
73
+ credentials=dict(
74
+ site_id="test",
75
+ password="password",
76
+ account_number="000000",
77
+ )
78
+ )
79
+
80
+ def getJWTToken(self, email: str, password: str) -> str:
81
+ url = reverse("jwt-obtain-pair")
82
+ data = dict(
83
+ email=email,
84
+ password=password,
85
+ )
86
+ response = self.client.post(url, data)
87
+
88
+ return response.data.get("access")
89
+
90
+ def assertResponseNoErrors(self, response):
91
+ is_ok = f"{response.status_code}".startswith("2")
92
+
93
+ if is_ok is False or response.data.get("errors") is not None:
94
+ logger.error("Response has errors",
95
+ status_code=response.status_code,
96
+ response_data=response.data)
97
+
98
+ self.assertTrue(is_ok)
99
+ assert response.data.get("errors") is None
@@ -0,0 +1,411 @@
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
+
11
+
12
+ class TestResourceAccessTokenUnit(TestCase):
13
+ """Unit tests for ResourceAccessToken class."""
14
+
15
+ def setUp(self):
16
+ self.user = get_user_model().objects.create_user(
17
+ email="test@example.com", password="testpass123"
18
+ )
19
+
20
+ def test_create_token_for_single_resource(self):
21
+ """Test creating a token for a single resource."""
22
+ token = ResourceAccessToken.for_resource(
23
+ user=self.user,
24
+ resource_type="shipment",
25
+ resource_ids=["shp_123"],
26
+ access=["label"],
27
+ format="pdf",
28
+ )
29
+
30
+ self.assertDictEqual(
31
+ {
32
+ "resource_type": token["resource_type"],
33
+ "resource_ids": token["resource_ids"],
34
+ "access": token["access"],
35
+ "format": token["format"],
36
+ },
37
+ {
38
+ "resource_type": "shipment",
39
+ "resource_ids": ["shp_123"],
40
+ "access": ["label"],
41
+ "format": "pdf",
42
+ },
43
+ )
44
+
45
+ def test_create_token_for_multiple_resources(self):
46
+ """Test creating a token for multiple resources."""
47
+ token = ResourceAccessToken.for_resource(
48
+ user=self.user,
49
+ resource_type="document",
50
+ resource_ids=["shp_1", "shp_2", "shp_3"],
51
+ access=["batch_labels"],
52
+ format="pdf",
53
+ )
54
+
55
+ self.assertDictEqual(
56
+ {
57
+ "resource_type": token["resource_type"],
58
+ "resource_ids": sorted(token["resource_ids"]),
59
+ "access": token["access"],
60
+ },
61
+ {
62
+ "resource_type": "document",
63
+ "resource_ids": ["shp_1", "shp_2", "shp_3"],
64
+ "access": ["batch_labels"],
65
+ },
66
+ )
67
+
68
+ def test_decode_valid_token(self):
69
+ """Test decoding a valid token."""
70
+ token = ResourceAccessToken.for_resource(
71
+ user=self.user,
72
+ resource_type="manifest",
73
+ resource_ids=["mnf_456"],
74
+ access=["manifest"],
75
+ )
76
+
77
+ claims = ResourceAccessToken.decode(str(token))
78
+
79
+ self.assertDictEqual(
80
+ {
81
+ "resource_type": claims["resource_type"],
82
+ "resource_ids": claims["resource_ids"],
83
+ "access": claims["access"],
84
+ },
85
+ {
86
+ "resource_type": "manifest",
87
+ "resource_ids": ["mnf_456"],
88
+ "access": ["manifest"],
89
+ },
90
+ )
91
+
92
+ def test_validate_access_success(self):
93
+ """Test successful access validation."""
94
+ token = ResourceAccessToken.for_resource(
95
+ user=self.user,
96
+ resource_type="shipment",
97
+ resource_ids=["shp_123"],
98
+ access=["label", "invoice"],
99
+ )
100
+
101
+ claims = ResourceAccessToken.validate_access(
102
+ token_string=str(token),
103
+ resource_type="shipment",
104
+ resource_id="shp_123",
105
+ access="label",
106
+ )
107
+
108
+ self.assertIsNotNone(claims)
109
+ self.assertEqual(claims["resource_type"], "shipment")
110
+
111
+ def test_validate_access_wrong_resource_type(self):
112
+ """Test validation fails for wrong resource type."""
113
+ token = ResourceAccessToken.for_resource(
114
+ user=self.user,
115
+ resource_type="shipment",
116
+ resource_ids=["shp_123"],
117
+ access=["label"],
118
+ )
119
+
120
+ with self.assertRaises(PermissionError) as context:
121
+ ResourceAccessToken.validate_access(
122
+ token_string=str(token),
123
+ resource_type="manifest",
124
+ resource_id="shp_123",
125
+ access="label",
126
+ )
127
+
128
+ self.assertIn("resource type", str(context.exception).lower())
129
+
130
+ def test_validate_access_wrong_resource_id(self):
131
+ """Test validation fails for wrong resource ID."""
132
+ token = ResourceAccessToken.for_resource(
133
+ user=self.user,
134
+ resource_type="shipment",
135
+ resource_ids=["shp_123"],
136
+ access=["label"],
137
+ )
138
+
139
+ with self.assertRaises(PermissionError) as context:
140
+ ResourceAccessToken.validate_access(
141
+ token_string=str(token),
142
+ resource_type="shipment",
143
+ resource_id="shp_999",
144
+ access="label",
145
+ )
146
+
147
+ self.assertIn("resource", str(context.exception).lower())
148
+
149
+ def test_validate_access_wrong_permission(self):
150
+ """Test validation fails for wrong access permission."""
151
+ token = ResourceAccessToken.for_resource(
152
+ user=self.user,
153
+ resource_type="shipment",
154
+ resource_ids=["shp_123"],
155
+ access=["label"],
156
+ )
157
+
158
+ with self.assertRaises(PermissionError) as context:
159
+ ResourceAccessToken.validate_access(
160
+ token_string=str(token),
161
+ resource_type="shipment",
162
+ resource_id="shp_123",
163
+ access="invoice",
164
+ )
165
+
166
+ self.assertIn("access", str(context.exception).lower())
167
+
168
+ def test_validate_batch_access_success(self):
169
+ """Test successful batch access validation."""
170
+ token = ResourceAccessToken.for_resource(
171
+ user=self.user,
172
+ resource_type="document",
173
+ resource_ids=["shp_1", "shp_2", "shp_3"],
174
+ access=["batch_labels"],
175
+ )
176
+
177
+ claims = ResourceAccessToken.validate_batch_access(
178
+ token_string=str(token),
179
+ resource_type="document",
180
+ resource_ids=["shp_1", "shp_2"],
181
+ access="batch_labels",
182
+ )
183
+
184
+ self.assertIsNotNone(claims)
185
+ self.assertEqual(claims["resource_type"], "document")
186
+
187
+ def test_validate_batch_access_missing_id(self):
188
+ """Test batch validation fails when requesting ID not in token."""
189
+ token = ResourceAccessToken.for_resource(
190
+ user=self.user,
191
+ resource_type="document",
192
+ resource_ids=["shp_1", "shp_2"],
193
+ access=["batch_labels"],
194
+ )
195
+
196
+ with self.assertRaises(PermissionError) as context:
197
+ ResourceAccessToken.validate_batch_access(
198
+ token_string=str(token),
199
+ resource_type="document",
200
+ resource_ids=["shp_1", "shp_2", "shp_3"],
201
+ access="batch_labels",
202
+ )
203
+
204
+ self.assertIn("shp_3", str(context.exception))
205
+
206
+ def test_token_with_org_id_and_test_mode(self):
207
+ """Test token includes org_id and test_mode when provided."""
208
+ token = ResourceAccessToken.for_resource(
209
+ user=self.user,
210
+ resource_type="shipment",
211
+ resource_ids=["shp_123"],
212
+ access=["label"],
213
+ org_id="org_abc",
214
+ test_mode=True,
215
+ )
216
+
217
+ claims = ResourceAccessToken.decode(str(token))
218
+
219
+ self.assertDictEqual(
220
+ {
221
+ "org_id": claims["org_id"],
222
+ "test_mode": claims["test_mode"],
223
+ },
224
+ {
225
+ "org_id": "org_abc",
226
+ "test_mode": True,
227
+ },
228
+ )
229
+
230
+
231
+ class TestResourceTokenAPI(APITestCase):
232
+ """API tests for /api/tokens endpoint."""
233
+
234
+ def setUp(self):
235
+ self.user = get_user_model().objects.create_user(
236
+ email="api_test@example.com", password="testpass123"
237
+ )
238
+ self.token = Token.objects.create(user=self.user, test_mode=True)
239
+ self.client = APIClient()
240
+ self.client.credentials(HTTP_AUTHORIZATION=f"Token {self.token.key}")
241
+
242
+ def test_generate_shipment_label_token(self):
243
+ """Test generating a token for shipment label access."""
244
+ response = self.client.post(
245
+ "/api/tokens",
246
+ {
247
+ "resource_type": "shipment",
248
+ "resource_ids": ["shp_123"],
249
+ "access": ["label"],
250
+ "format": "pdf",
251
+ },
252
+ format="json",
253
+ )
254
+
255
+ self.assertEqual(response.status_code, 201)
256
+ self.assertDictEqual(
257
+ {
258
+ "has_token": "token" in response.data,
259
+ "has_expires_at": "expires_at" in response.data,
260
+ "has_resource_urls": "resource_urls" in response.data,
261
+ "has_shp_123_url": "shp_123" in response.data.get("resource_urls", {}),
262
+ },
263
+ {
264
+ "has_token": True,
265
+ "has_expires_at": True,
266
+ "has_resource_urls": True,
267
+ "has_shp_123_url": True,
268
+ },
269
+ )
270
+
271
+ def test_generate_batch_labels_token(self):
272
+ """Test generating a token for batch labels."""
273
+ response = self.client.post(
274
+ "/api/tokens",
275
+ {
276
+ "resource_type": "document",
277
+ "resource_ids": ["shp_1", "shp_2", "shp_3"],
278
+ "access": ["batch_labels"],
279
+ "format": "pdf",
280
+ },
281
+ format="json",
282
+ )
283
+
284
+ self.assertEqual(response.status_code, 201)
285
+ self.assertIn("batch", response.data["resource_urls"])
286
+
287
+ def test_generate_manifest_token(self):
288
+ """Test generating a token for manifest access."""
289
+ response = self.client.post(
290
+ "/api/tokens",
291
+ {
292
+ "resource_type": "manifest",
293
+ "resource_ids": ["mnf_456"],
294
+ "access": ["manifest"],
295
+ },
296
+ format="json",
297
+ )
298
+
299
+ self.assertEqual(response.status_code, 201)
300
+ self.assertIn("mnf_456", response.data["resource_urls"])
301
+
302
+ def test_generate_template_token(self):
303
+ """Test generating a token for template access."""
304
+ response = self.client.post(
305
+ "/api/tokens",
306
+ {
307
+ "resource_type": "template",
308
+ "resource_ids": ["tpl_789"],
309
+ "access": ["render"],
310
+ },
311
+ format="json",
312
+ )
313
+
314
+ self.assertEqual(response.status_code, 201)
315
+ self.assertIn("tpl_789", response.data["resource_urls"])
316
+
317
+ def test_unauthenticated_request_fails(self):
318
+ """Test that unauthenticated requests are rejected."""
319
+ self.client.credentials()
320
+ response = self.client.post(
321
+ "/api/tokens",
322
+ {
323
+ "resource_type": "shipment",
324
+ "resource_ids": ["shp_123"],
325
+ "access": ["label"],
326
+ },
327
+ format="json",
328
+ )
329
+
330
+ self.assertEqual(response.status_code, 401)
331
+
332
+ def test_invalid_resource_type_fails(self):
333
+ """Test that invalid resource type is rejected."""
334
+ response = self.client.post(
335
+ "/api/tokens",
336
+ {
337
+ "resource_type": "invalid_type",
338
+ "resource_ids": ["shp_123"],
339
+ "access": ["label"],
340
+ },
341
+ format="json",
342
+ )
343
+
344
+ self.assertEqual(response.status_code, 400)
345
+
346
+ def test_invalid_access_type_fails(self):
347
+ """Test that invalid access type is rejected."""
348
+ response = self.client.post(
349
+ "/api/tokens",
350
+ {
351
+ "resource_type": "shipment",
352
+ "resource_ids": ["shp_123"],
353
+ "access": ["invalid_access"],
354
+ },
355
+ format="json",
356
+ )
357
+
358
+ self.assertEqual(response.status_code, 400)
359
+
360
+ def test_empty_resource_ids_fails(self):
361
+ """Test that empty resource_ids is rejected."""
362
+ response = self.client.post(
363
+ "/api/tokens",
364
+ {
365
+ "resource_type": "shipment",
366
+ "resource_ids": [],
367
+ "access": ["label"],
368
+ },
369
+ format="json",
370
+ )
371
+
372
+ self.assertEqual(response.status_code, 400)
373
+
374
+ def test_custom_expiration_success(self):
375
+ """Test custom token expiration."""
376
+ response = self.client.post(
377
+ "/api/tokens",
378
+ {
379
+ "resource_type": "shipment",
380
+ "resource_ids": ["shp_123"],
381
+ "access": ["label"],
382
+ "expires_in": 600,
383
+ },
384
+ format="json",
385
+ )
386
+
387
+ self.assertEqual(response.status_code, 201)
388
+
389
+ def test_response_has_no_cache_headers(self):
390
+ """Test that token response includes cache prevention headers."""
391
+ response = self.client.post(
392
+ "/api/tokens",
393
+ {
394
+ "resource_type": "shipment",
395
+ "resource_ids": ["shp_123"],
396
+ "access": ["label"],
397
+ },
398
+ format="json",
399
+ )
400
+
401
+ self.assertEqual(response.status_code, 201)
402
+ self.assertDictEqual(
403
+ {
404
+ "cache_control": response.get("Cache-Control"),
405
+ "cdn_cache_control": response.get("CDN-Cache-Control"),
406
+ },
407
+ {
408
+ "cache_control": "no-store",
409
+ "cdn_cache_control": "no-store",
410
+ },
411
+ )
@@ -0,0 +1,12 @@
1
+ """
2
+ karrio server core module urls
3
+ """
4
+ from django.urls import include, path
5
+ from karrio.server.core.views import metadata, router
6
+
7
+ app_name = "karrio.server.core"
8
+ urlpatterns = [
9
+ path("", metadata.view, name="metadata"),
10
+ path("v1/", include(router.urls), name="references"),
11
+ path("status/", include("health_check.urls")),
12
+ ]