sefrone-api-e2e 1.3.4__tar.gz → 1.3.6__tar.gz

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 (35) hide show
  1. {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.6}/PKG-INFO +1 -1
  2. {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.6}/sefrone_api_e2e/__init__.py +2 -0
  3. {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.6}/sefrone_api_e2e/api_e2e_manager.py +18 -3
  4. sefrone_api_e2e-1.3.6/sefrone_api_e2e/e2e/Scenarios/Auth/9_project_account_access_control_flow.yaml +337 -0
  5. sefrone_api_e2e-1.3.6/sefrone_api_e2e/runner_csv.py +188 -0
  6. {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.6}/sefrone_api_e2e.egg-info/PKG-INFO +1 -1
  7. {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.6}/sefrone_api_e2e.egg-info/SOURCES.txt +2 -0
  8. {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.6}/setup.py +1 -1
  9. {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.6}/README.md +0 -0
  10. {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.6}/sefrone_api_e2e/e2e/Scenarios/Auth/0_check_features.yaml +0 -0
  11. {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.6}/sefrone_api_e2e/e2e/Scenarios/Auth/1_normal_auth_flow.yaml +0 -0
  12. {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.6}/sefrone_api_e2e/e2e/Scenarios/Auth/2_account_endpoints.yaml +0 -0
  13. {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.6}/sefrone_api_e2e/e2e/Scenarios/Auth/3_admin_account_endpoints.yaml +0 -0
  14. {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.6}/sefrone_api_e2e/e2e/Scenarios/Auth/4_workspace_endpoints.yaml +0 -0
  15. {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.6}/sefrone_api_e2e/e2e/Scenarios/Auth/5_apikey_endpoints.yaml +0 -0
  16. {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.6}/sefrone_api_e2e/e2e/Scenarios/Auth/6_project_endpoints.yaml +0 -0
  17. {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.6}/sefrone_api_e2e/e2e/Scenarios/Auth/7_email_2fa_flow.yaml +0 -0
  18. {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.6}/sefrone_api_e2e/e2e/Scenarios/Auth/8_temporary_passwords_flow.yaml +0 -0
  19. {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.6}/sefrone_api_e2e/e2e/Scenarios/Oidc/1_relying_party_admin.yaml +0 -0
  20. {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.6}/sefrone_api_e2e/e2e/Scenarios/Oidc/2_authorization_code_flow.yaml +0 -0
  21. {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.6}/sefrone_api_e2e/e2e/Scenarios/Oidc/3_disable_propagation_flow.yaml +0 -0
  22. {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.6}/sefrone_api_e2e/e2e/Scenarios/Oidc/4_multi_project_isolation.yaml +0 -0
  23. {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.6}/sefrone_api_e2e/e2e_scenarios_manager.py +0 -0
  24. {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.6}/sefrone_api_e2e/env_patch.py +0 -0
  25. {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.6}/sefrone_api_e2e/json_mock_server.py +0 -0
  26. {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.6}/sefrone_api_e2e/runner_assets.py +0 -0
  27. {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.6}/sefrone_api_e2e/runner_email_mock.py +0 -0
  28. {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.6}/sefrone_api_e2e/runner_rest_api.py +0 -0
  29. {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.6}/sefrone_api_e2e/runner_satim_mock.py +0 -0
  30. {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.6}/sefrone_api_e2e/runner_webhook_mock.py +0 -0
  31. {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.6}/sefrone_api_e2e/step_runner.py +0 -0
  32. {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.6}/sefrone_api_e2e.egg-info/dependency_links.txt +0 -0
  33. {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.6}/sefrone_api_e2e.egg-info/requires.txt +0 -0
  34. {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.6}/sefrone_api_e2e.egg-info/top_level.txt +0 -0
  35. {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sefrone_api_e2e
3
- Version: 1.3.4
3
+ Version: 1.3.6
4
4
  Summary: A Python package to provide e2e testing helpers for sefrone API projects
5
5
  Home-page: https://bitbucket.org/sefrone/sefrone_pypi
6
6
  Author: Sefrone
@@ -5,6 +5,7 @@ from .runner_email_mock import EmailMockRunner
5
5
  from .runner_webhook_mock import WebhookMockRunner
6
6
  from .runner_assets import AssetsRunner
7
7
  from .runner_satim_mock import SatimMockRunner
8
+ from .runner_csv import CsvRunner
8
9
  from .json_mock_server import JsonMockServer
9
10
  from .e2e_scenarios_manager import E2EScenariosManager
10
11
 
@@ -19,6 +20,7 @@ __all__ = [
19
20
  "WebhookMockRunner",
20
21
  "AssetsRunner",
21
22
  "SatimMockRunner",
23
+ "CsvRunner",
22
24
  "JsonMockServer",
23
25
  "E2EScenariosManager",
24
26
  ]
@@ -10,9 +10,10 @@ import requests
10
10
  from pathlib import Path
11
11
  from typing import Any, Dict, List, Optional
12
12
 
13
- from .step_runner import StepRunner, RunContext, StepResult
13
+ from .step_runner import StepRunner, RunContext
14
14
  from .runner_rest_api import RestApiRunner
15
- from .runner_email_mock import EmailMockRunner
15
+ from .runner_assets import AssetsRunner
16
+ from .runner_csv import CsvRunner
16
17
 
17
18
 
18
19
  class ScenarioSkippedException(Exception):
@@ -41,9 +42,22 @@ class ApiE2ETestsManager:
41
42
  ApiE2ETestsManager.global_store = {}
42
43
  ApiE2ETestsManager.scenario_results = []
43
44
 
44
- active_runners: List[StepRunner] = [RestApiRunner()]
45
+ # RestApiRunner (default), AssetsRunner and CsvRunner are stateless/self-contained
46
+ # (no external config needed), so they're always available without callers having to
47
+ # construct and pass them in. Runners that need caller-supplied config (mail/webhook
48
+ # mocks, etc.) still come in through the `runners` param.
49
+ active_runners: List[StepRunner] = [RestApiRunner(), AssetsRunner(), CsvRunner()]
45
50
  if runners:
51
+ # make sure to distinct on runner name
46
52
  active_runners.extend(runners)
53
+ # distinct on runner name
54
+ seen = set()
55
+ unique_runners = []
56
+ for r in active_runners:
57
+ if r.name not in seen:
58
+ seen.add(r.name)
59
+ unique_runners.append(r)
60
+ active_runners = unique_runners
47
61
 
48
62
  default_runner = active_runners[0]
49
63
  extra_runners = active_runners[1:]
@@ -309,6 +323,7 @@ class ApiE2ETestsManager:
309
323
  type_map = {
310
324
  "string": str, "number": (int, float),
311
325
  "boolean": bool, "datetime": str,
326
+ "object": dict,
312
327
  }
313
328
  if base_type not in type_map:
314
329
  raise ValueError(f"Unknown type in YAML: {expected_type}")
@@ -0,0 +1,337 @@
1
+ name: "Project & Account Access Control Flow"
2
+ skip_if:
3
+ - "Projects not in {$stored.0_check_features.enabled_features}"
4
+ - "Accounts not in {$stored.0_check_features.enabled_features}"
5
+ base_url: "http://localhost:$var(API_PUBLIC_PORT)"
6
+ steps:
7
+ - name: "Login root admin"
8
+ retry:
9
+ attempts: 10
10
+ delay_seconds: 5
11
+ method: "POST"
12
+ endpoint: "/api/v1/apiAuth/account/$var(AUTH_PROJECT_NAME)/login"
13
+ send_as: "json"
14
+ body:
15
+ device: "e2e_test_device"
16
+ pseudo: "$var(DEFAULT_USERNAME)"
17
+ password: "$var(DEFAULT_PASSWORD)"
18
+ expect:
19
+ status: 200
20
+ body:
21
+ response:
22
+ accountId: string
23
+ accountType: string
24
+ token: string
25
+ save:
26
+ root_token: "{{body.response.token}}"
27
+ root_account_id: "{{body.response.accountId}}"
28
+ assertions:
29
+ - "{{body.response.accountType}} == 'root'"
30
+
31
+ - name: "Generate unique test suffix"
32
+ randomize:
33
+ seed_id: 8
34
+
35
+ # --- Only root can create projects ---
36
+
37
+ - name: "Root creates second project (project B)"
38
+ method: "POST"
39
+ endpoint: "/api/v1/apiAuth/project"
40
+ request_headers:
41
+ - x-api-token: "{$stored.root_token}"
42
+ send_as: "json"
43
+ body:
44
+ name: "e2e_access_project_{$stored.seed_id}"
45
+ description: "E2E cross-project access control project"
46
+ expect:
47
+ status: 200
48
+ body:
49
+ errorCode: number
50
+ errorCodeStr: string
51
+ message: string
52
+ response:
53
+ id: string
54
+ name: string
55
+ save:
56
+ project_b_id: "{{body.response.id}}"
57
+ project_b_name: "{{body.response.name}}"
58
+ assertions:
59
+ - "{{body.response.name}} == 'e2e_access_project_{$stored.seed_id}'"
60
+
61
+ # --- Setup: one admin in the default project (A), one admin in project B, and one standard account in project A ---
62
+
63
+ - name: "Root creates admin in project A"
64
+ method: "POST"
65
+ endpoint: "/api/v1/apiAuth/account/$var(AUTH_PROJECT_NAME)/create?accountType=admin"
66
+ request_headers:
67
+ - x-api-token: "{$stored.root_token}"
68
+ send_as: "json"
69
+ body:
70
+ username: "e2e_admin_a_{$stored.seed_id}"
71
+ password: "e2e_admin_a_pwd_v1"
72
+ expect:
73
+ status: 200
74
+ body:
75
+ response:
76
+ accountId: string
77
+ accountType: string
78
+ save:
79
+ admin_a_account_id: "{{body.response.accountId}}"
80
+ assertions:
81
+ - "{{body.response.accountType}} == 'admin'"
82
+
83
+ - name: "Root creates admin in project B"
84
+ method: "POST"
85
+ endpoint: "/api/v1/apiAuth/account/{$stored.project_b_name}/create?accountType=admin"
86
+ request_headers:
87
+ - x-api-token: "{$stored.root_token}"
88
+ send_as: "json"
89
+ body:
90
+ username: "e2e_admin_b_{$stored.seed_id}"
91
+ password: "e2e_admin_b_pwd_v1"
92
+ expect:
93
+ status: 200
94
+ body:
95
+ response:
96
+ accountId: string
97
+ accountType: string
98
+ save:
99
+ admin_b_account_id: "{{body.response.accountId}}"
100
+ assertions:
101
+ - "{{body.response.accountType}} == 'admin'"
102
+
103
+ - name: "Root creates standard account in project A"
104
+ method: "POST"
105
+ endpoint: "/api/v1/apiAuth/account/$var(AUTH_PROJECT_NAME)/create?accountType=standard"
106
+ request_headers:
107
+ - x-api-token: "{$stored.root_token}"
108
+ send_as: "json"
109
+ body:
110
+ username: "e2e_standard_{$stored.seed_id}"
111
+ password: "e2e_standard_pwd_v1"
112
+ expect:
113
+ status: 200
114
+ body:
115
+ response:
116
+ accountId: string
117
+ accountType: string
118
+ save:
119
+ standard_account_id: "{{body.response.accountId}}"
120
+ assertions:
121
+ - "{{body.response.accountType}} == 'standard'"
122
+
123
+ - name: "Login admin of project A"
124
+ method: "POST"
125
+ endpoint: "/api/v1/apiAuth/account/$var(AUTH_PROJECT_NAME)/login"
126
+ send_as: "json"
127
+ body:
128
+ device: "e2e_test_device"
129
+ pseudo: "e2e_admin_a_{$stored.seed_id}"
130
+ password: "e2e_admin_a_pwd_v1"
131
+ expect:
132
+ status: 200
133
+ body:
134
+ response:
135
+ token: string
136
+ save:
137
+ admin_a_token: "{{body.response.token}}"
138
+
139
+ - name: "Login admin of project B"
140
+ method: "POST"
141
+ endpoint: "/api/v1/apiAuth/account/{$stored.project_b_name}/login"
142
+ send_as: "json"
143
+ body:
144
+ device: "e2e_test_device"
145
+ pseudo: "e2e_admin_b_{$stored.seed_id}"
146
+ password: "e2e_admin_b_pwd_v1"
147
+ expect:
148
+ status: 200
149
+ body:
150
+ response:
151
+ token: string
152
+ save:
153
+ admin_b_token: "{{body.response.token}}"
154
+
155
+ - name: "Login standard account of project A"
156
+ method: "POST"
157
+ endpoint: "/api/v1/apiAuth/account/$var(AUTH_PROJECT_NAME)/login"
158
+ send_as: "json"
159
+ body:
160
+ device: "e2e_test_device"
161
+ pseudo: "e2e_standard_{$stored.seed_id}"
162
+ password: "e2e_standard_pwd_v1"
163
+ expect:
164
+ status: 200
165
+ body:
166
+ response:
167
+ token: string
168
+ save:
169
+ standard_token: "{{body.response.token}}"
170
+
171
+ # --- Only root can create projects: admin and standard accounts are rejected ---
172
+
173
+ - name: "Admin cannot create a project"
174
+ method: "POST"
175
+ endpoint: "/api/v1/apiAuth/project"
176
+ request_headers:
177
+ - x-api-token: "{$stored.admin_a_token}"
178
+ send_as: "json"
179
+ body:
180
+ name: "e2e_forbidden_project_admin_{$stored.seed_id}"
181
+ description: "Should not be created"
182
+ expect:
183
+ status: 403
184
+ body:
185
+ errorCode: number
186
+ errorCodeStr: string
187
+ message: string
188
+
189
+ - name: "Standard account cannot create a project"
190
+ method: "POST"
191
+ endpoint: "/api/v1/apiAuth/project"
192
+ request_headers:
193
+ - x-api-token: "{$stored.standard_token}"
194
+ send_as: "json"
195
+ body:
196
+ name: "e2e_forbidden_project_std_{$stored.seed_id}"
197
+ description: "Should not be created"
198
+ expect:
199
+ status: 403
200
+ body:
201
+ errorCode: number
202
+ errorCodeStr: string
203
+ message: string
204
+
205
+ # --- An admin cannot manage accounts outside of its own project ---
206
+
207
+ - name: "Admin of project A cannot create an account in project B"
208
+ method: "POST"
209
+ endpoint: "/api/v1/apiAuth/account/{$stored.project_b_name}/create?accountType=standard"
210
+ request_headers:
211
+ - x-api-token: "{$stored.admin_a_token}"
212
+ send_as: "json"
213
+ body:
214
+ username: "e2e_cross_project_a_to_b_{$stored.seed_id}"
215
+ password: "e2e_cross_project_pwd"
216
+ expect:
217
+ status: 422
218
+ body:
219
+ errorCode: number
220
+ errorCodeStr: string
221
+ message: string
222
+
223
+ - name: "Admin of project B cannot create an account in project A"
224
+ method: "POST"
225
+ endpoint: "/api/v1/apiAuth/account/$var(AUTH_PROJECT_NAME)/create?accountType=standard"
226
+ request_headers:
227
+ - x-api-token: "{$stored.admin_b_token}"
228
+ send_as: "json"
229
+ body:
230
+ username: "e2e_cross_project_b_to_a_{$stored.seed_id}"
231
+ password: "e2e_cross_project_pwd"
232
+ expect:
233
+ status: 422
234
+ body:
235
+ errorCode: number
236
+ errorCodeStr: string
237
+ message: string
238
+
239
+ - name: "Admin of project A can still create an account within its own project"
240
+ method: "POST"
241
+ endpoint: "/api/v1/apiAuth/account/$var(AUTH_PROJECT_NAME)/create?accountType=standard"
242
+ request_headers:
243
+ - x-api-token: "{$stored.admin_a_token}"
244
+ send_as: "json"
245
+ body:
246
+ username: "e2e_own_project_{$stored.seed_id}"
247
+ password: "e2e_own_project_pwd"
248
+ expect:
249
+ status: 200
250
+ body:
251
+ response:
252
+ accountId: string
253
+ accountType: string
254
+ save:
255
+ admin_a_created_account_id: "{{body.response.accountId}}"
256
+ assertions:
257
+ - "{{body.response.accountType}} == 'standard'"
258
+
259
+ # --- A standard account cannot create an admin account ---
260
+
261
+ - name: "Standard account cannot create an admin account"
262
+ method: "POST"
263
+ endpoint: "/api/v1/apiAuth/account/$var(AUTH_PROJECT_NAME)/create?accountType=admin"
264
+ request_headers:
265
+ - x-api-token: "{$stored.standard_token}"
266
+ send_as: "json"
267
+ body:
268
+ username: "e2e_std_forbidden_admin_{$stored.seed_id}"
269
+ password: "e2e_std_forbidden_admin_pwd"
270
+ expect:
271
+ status: 403
272
+ body:
273
+ errorCode: number
274
+ errorCodeStr: string
275
+ message: string
276
+
277
+ # --- Cleanup ---
278
+
279
+ - name: "Cleanup: Delete account created by admin of project A"
280
+ method: "DELETE"
281
+ endpoint: "/api/v1/apiAuth/account/$var(AUTH_PROJECT_NAME)/delete/{$stored.admin_a_created_account_id}"
282
+ request_headers:
283
+ - x-api-token: "{$stored.root_token}"
284
+ expect:
285
+ status: 200
286
+ body:
287
+ errorCode: number
288
+ errorCodeStr: string
289
+ message: string
290
+
291
+ - name: "Cleanup: Delete standard account"
292
+ method: "DELETE"
293
+ endpoint: "/api/v1/apiAuth/account/$var(AUTH_PROJECT_NAME)/delete/{$stored.standard_account_id}"
294
+ request_headers:
295
+ - x-api-token: "{$stored.root_token}"
296
+ expect:
297
+ status: 200
298
+ body:
299
+ errorCode: number
300
+ errorCodeStr: string
301
+ message: string
302
+
303
+ - name: "Cleanup: Delete admin of project A"
304
+ method: "DELETE"
305
+ endpoint: "/api/v1/apiAuth/account/$var(AUTH_PROJECT_NAME)/delete/{$stored.admin_a_account_id}"
306
+ request_headers:
307
+ - x-api-token: "{$stored.root_token}"
308
+ expect:
309
+ status: 200
310
+ body:
311
+ errorCode: number
312
+ errorCodeStr: string
313
+ message: string
314
+
315
+ - name: "Cleanup: Delete admin of project B"
316
+ method: "DELETE"
317
+ endpoint: "/api/v1/apiAuth/account/{$stored.project_b_name}/delete/{$stored.admin_b_account_id}"
318
+ request_headers:
319
+ - x-api-token: "{$stored.root_token}"
320
+ expect:
321
+ status: 200
322
+ body:
323
+ errorCode: number
324
+ errorCodeStr: string
325
+ message: string
326
+
327
+ - name: "Cleanup: Delete project B"
328
+ method: "DELETE"
329
+ endpoint: "/api/v1/apiAuth/project/{$stored.project_b_id}"
330
+ request_headers:
331
+ - x-api-token: "{$stored.root_token}"
332
+ expect:
333
+ status: 200
334
+ body:
335
+ errorCode: number
336
+ errorCodeStr: string
337
+ message: string
@@ -0,0 +1,188 @@
1
+ import csv
2
+ import os
3
+ from typing import Any, Dict, List, Set
4
+
5
+ from .step_runner import StepRunner, RunContext, StepResult
6
+
7
+
8
+ class CsvRunner(StepRunner):
9
+ """
10
+ Runner for generating and reading CSV files during scenario execution, so a
11
+ downloaded CSV (e.g. a settlement export) can be asserted on by structured
12
+ row/column content instead of only a checksum.
13
+
14
+ This runner never makes HTTP requests itself -- pair a normal HTTP step's
15
+ ``save_file:`` (already supported by the default rest runner) with a
16
+ ``read`` step to assert on a downloaded file's content.
17
+
18
+ Step syntax — read (parse an existing/just-downloaded CSV file)::
19
+
20
+ - name: "Download daily settlement file"
21
+ method: "GET"
22
+ endpoint: "/api/v1/SettlementLedger/file/{$stored.acquirer_id}/{$stored.file_id}"
23
+ request_headers:
24
+ - x-api-token: "{$stored.token}"
25
+ expect:
26
+ status: 200
27
+ save_file: "./e2e/tmp/daily_withholding.csv"
28
+
29
+ - name: "Verify settlement file content"
30
+ runner: csv
31
+ csv:
32
+ action: read # optional, default: read
33
+ path: "./e2e/tmp/daily_withholding.csv"
34
+ assertions:
35
+ - "{{csv.row_count}} == 3"
36
+ - "{{csv.headers}} include 'LineType'"
37
+ - "{{csv.rows[0].LineType}} == 'charge'"
38
+ - "{{csv.rows[0].CurrencyCode}} == 'JOD'"
39
+ save:
40
+ first_line_type: "{{csv.rows[0].LineType}}"
41
+
42
+ Step syntax — generate (write a CSV fixture with specific content)::
43
+
44
+ - name: "Generate CSV fixture"
45
+ runner: csv
46
+ csv:
47
+ action: generate
48
+ path: "./e2e/assets/test_import.csv"
49
+ headers: ["Name", "Amount"]
50
+ rows:
51
+ - ["Alice", "100.00"]
52
+ - ["Bob", "200.50"]
53
+
54
+ ``csv.rows`` in the read context is a list of dicts keyed by header name
55
+ (e.g. ``{{csv.rows[0].Amount}}``); ``csv.raw_rows`` is the same data as
56
+ plain lists (positional access) for files with duplicate/blank headers.
57
+ Runner-generated files are removed during teardown, matching AssetsRunner;
58
+ files a ``read`` step merely reads (e.g. ones saved by an HTTP step) are
59
+ left alone since this runner doesn't own them.
60
+ """
61
+
62
+ def __init__(self):
63
+ self._generated_paths: Set[str] = set()
64
+
65
+ @property
66
+ def name(self) -> str:
67
+ return "csv"
68
+
69
+ def handles(self, step: dict) -> bool:
70
+ return "csv" in step and "method" not in step
71
+
72
+ def execute(self, step: dict, ctx: RunContext) -> StepResult:
73
+ try:
74
+ spec = step.get("csv")
75
+ if not isinstance(spec, dict):
76
+ return StepResult.failed("csv step requires a 'csv' object")
77
+
78
+ action = str(spec.get("action", "read")).strip().lower()
79
+
80
+ if action == "read":
81
+ return self._read(step, spec, ctx)
82
+ if action == "generate":
83
+ return self._generate(spec, ctx)
84
+
85
+ return StepResult.failed(f"Unsupported csv action: '{action}'. Supported: 'read', 'generate'")
86
+ except AssertionError as e:
87
+ return StepResult.failed(str(e))
88
+ except Exception as e:
89
+ return StepResult.failed(str(e))
90
+
91
+ def teardown(self) -> None:
92
+ for path in list(self._generated_paths):
93
+ try:
94
+ os.remove(path)
95
+ print(f"[csv] Removed: {path}")
96
+ except FileNotFoundError:
97
+ pass
98
+ except Exception as e:
99
+ print(f"[csv] Failed removing '{path}': {e}")
100
+ self._generated_paths.clear()
101
+
102
+ # ------------------------------------------------------------------ #
103
+ # read
104
+ # ------------------------------------------------------------------ #
105
+
106
+ def _read(self, step: dict, spec: Dict[str, Any], ctx: RunContext) -> StepResult:
107
+ path = spec.get("path")
108
+ if not isinstance(path, str) or not path.strip():
109
+ return StepResult.failed("csv.path is required and must be a non-empty string")
110
+
111
+ resolved_path = os.path.normpath(ctx.substitute_stored(path))
112
+
113
+ if not os.path.exists(resolved_path):
114
+ return StepResult.failed(f"csv file not found: '{resolved_path}'")
115
+
116
+ # utf-8-sig transparently strips a BOM if the file has one, and behaves
117
+ # like plain utf-8 otherwise -- ClosedXML/Excel-adjacent tooling
118
+ # sometimes writes a BOM, our own CsvWriter does not.
119
+ encoding = str(spec.get("encoding", "utf-8-sig"))
120
+ with open(resolved_path, "r", encoding=encoding, newline="") as f:
121
+ all_rows = list(csv.reader(f))
122
+
123
+ if not all_rows:
124
+ headers: List[str] = []
125
+ raw_rows: List[List[str]] = []
126
+ else:
127
+ headers, raw_rows = all_rows[0], all_rows[1:]
128
+
129
+ row_dicts = [dict(zip(headers, row)) for row in raw_rows]
130
+
131
+ template_context: Dict[str, Any] = {
132
+ "csv": {
133
+ "headers": headers,
134
+ "rows": row_dicts,
135
+ "raw_rows": raw_rows,
136
+ "row_count": len(row_dicts),
137
+ }
138
+ }
139
+
140
+ if "assertions" in step:
141
+ for expr in step["assertions"]:
142
+ try:
143
+ rendered_expr = ctx.substitute_stored(expr)
144
+ rendered = ctx.render_template(rendered_expr, template_context, add_quotes=True)
145
+ ctx.try_eval(rendered, rendered_expr)
146
+ except Exception as e:
147
+ raise AssertionError(f"Assertion failed for expression\n -->'{expr}'\n -->{e}")
148
+
149
+ if "save" in step:
150
+ for key, path_expr in step["save"].items():
151
+ rendered_path = ctx.substitute_stored(str(path_expr))
152
+ rendered = ctx.render_template(rendered_path, template_context)
153
+ ctx.store_value(key, rendered)
154
+
155
+ return StepResult.passed()
156
+
157
+ # ------------------------------------------------------------------ #
158
+ # generate
159
+ # ------------------------------------------------------------------ #
160
+
161
+ def _generate(self, spec: Dict[str, Any], ctx: RunContext) -> StepResult:
162
+ path = spec.get("path")
163
+ headers = spec.get("headers")
164
+ rows = spec.get("rows", [])
165
+
166
+ if not isinstance(path, str) or not path.strip():
167
+ return StepResult.failed("csv.path is required and must be a non-empty string")
168
+ if not isinstance(headers, list) or not headers:
169
+ return StepResult.failed("csv.headers is required and must be a non-empty list")
170
+ if not isinstance(rows, list):
171
+ return StepResult.failed("csv.rows must be a list of row lists")
172
+
173
+ resolved_path = os.path.normpath(ctx.substitute_stored(path))
174
+ parent_dir = os.path.dirname(resolved_path)
175
+ if parent_dir:
176
+ os.makedirs(parent_dir, exist_ok=True)
177
+
178
+ resolved_headers = [ctx.substitute_stored(str(h)) for h in headers]
179
+ resolved_rows = [[ctx.substitute_stored(str(cell)) for cell in row] for row in rows]
180
+
181
+ with open(resolved_path, "w", encoding="utf-8", newline="") as f:
182
+ writer = csv.writer(f)
183
+ writer.writerow(resolved_headers)
184
+ writer.writerows(resolved_rows)
185
+
186
+ self._generated_paths.add(resolved_path)
187
+ print(f"[csv] Generated: {resolved_path} ({len(resolved_rows)} row(s))")
188
+ return StepResult.passed()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sefrone_api_e2e
3
- Version: 1.3.4
3
+ Version: 1.3.6
4
4
  Summary: A Python package to provide e2e testing helpers for sefrone API projects
5
5
  Home-page: https://bitbucket.org/sefrone/sefrone_pypi
6
6
  Author: Sefrone
@@ -6,6 +6,7 @@ sefrone_api_e2e/e2e_scenarios_manager.py
6
6
  sefrone_api_e2e/env_patch.py
7
7
  sefrone_api_e2e/json_mock_server.py
8
8
  sefrone_api_e2e/runner_assets.py
9
+ sefrone_api_e2e/runner_csv.py
9
10
  sefrone_api_e2e/runner_email_mock.py
10
11
  sefrone_api_e2e/runner_rest_api.py
11
12
  sefrone_api_e2e/runner_satim_mock.py
@@ -25,6 +26,7 @@ sefrone_api_e2e/e2e/Scenarios/Auth/5_apikey_endpoints.yaml
25
26
  sefrone_api_e2e/e2e/Scenarios/Auth/6_project_endpoints.yaml
26
27
  sefrone_api_e2e/e2e/Scenarios/Auth/7_email_2fa_flow.yaml
27
28
  sefrone_api_e2e/e2e/Scenarios/Auth/8_temporary_passwords_flow.yaml
29
+ sefrone_api_e2e/e2e/Scenarios/Auth/9_project_account_access_control_flow.yaml
28
30
  sefrone_api_e2e/e2e/Scenarios/Oidc/1_relying_party_admin.yaml
29
31
  sefrone_api_e2e/e2e/Scenarios/Oidc/2_authorization_code_flow.yaml
30
32
  sefrone_api_e2e/e2e/Scenarios/Oidc/3_disable_propagation_flow.yaml
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
5
5
 
6
6
  setup(
7
7
  name="sefrone_api_e2e",
8
- version="1.3.4",
8
+ version="1.3.6",
9
9
  author="Sefrone",
10
10
  author_email="contact@sefrone.com",
11
11
  description="A Python package to provide e2e testing helpers for sefrone API projects",