sefrone-api-e2e 1.3.4__tar.gz → 1.3.5__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.
- {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.5}/PKG-INFO +1 -1
- {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.5}/sefrone_api_e2e/__init__.py +2 -0
- {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.5}/sefrone_api_e2e/api_e2e_manager.py +17 -3
- sefrone_api_e2e-1.3.5/sefrone_api_e2e/e2e/Scenarios/Auth/9_project_account_access_control_flow.yaml +337 -0
- sefrone_api_e2e-1.3.5/sefrone_api_e2e/runner_csv.py +188 -0
- {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.5}/sefrone_api_e2e.egg-info/PKG-INFO +1 -1
- {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.5}/sefrone_api_e2e.egg-info/SOURCES.txt +2 -0
- {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.5}/setup.py +1 -1
- {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.5}/README.md +0 -0
- {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.5}/sefrone_api_e2e/e2e/Scenarios/Auth/0_check_features.yaml +0 -0
- {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.5}/sefrone_api_e2e/e2e/Scenarios/Auth/1_normal_auth_flow.yaml +0 -0
- {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.5}/sefrone_api_e2e/e2e/Scenarios/Auth/2_account_endpoints.yaml +0 -0
- {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.5}/sefrone_api_e2e/e2e/Scenarios/Auth/3_admin_account_endpoints.yaml +0 -0
- {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.5}/sefrone_api_e2e/e2e/Scenarios/Auth/4_workspace_endpoints.yaml +0 -0
- {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.5}/sefrone_api_e2e/e2e/Scenarios/Auth/5_apikey_endpoints.yaml +0 -0
- {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.5}/sefrone_api_e2e/e2e/Scenarios/Auth/6_project_endpoints.yaml +0 -0
- {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.5}/sefrone_api_e2e/e2e/Scenarios/Auth/7_email_2fa_flow.yaml +0 -0
- {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.5}/sefrone_api_e2e/e2e/Scenarios/Auth/8_temporary_passwords_flow.yaml +0 -0
- {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.5}/sefrone_api_e2e/e2e/Scenarios/Oidc/1_relying_party_admin.yaml +0 -0
- {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.5}/sefrone_api_e2e/e2e/Scenarios/Oidc/2_authorization_code_flow.yaml +0 -0
- {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.5}/sefrone_api_e2e/e2e/Scenarios/Oidc/3_disable_propagation_flow.yaml +0 -0
- {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.5}/sefrone_api_e2e/e2e/Scenarios/Oidc/4_multi_project_isolation.yaml +0 -0
- {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.5}/sefrone_api_e2e/e2e_scenarios_manager.py +0 -0
- {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.5}/sefrone_api_e2e/env_patch.py +0 -0
- {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.5}/sefrone_api_e2e/json_mock_server.py +0 -0
- {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.5}/sefrone_api_e2e/runner_assets.py +0 -0
- {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.5}/sefrone_api_e2e/runner_email_mock.py +0 -0
- {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.5}/sefrone_api_e2e/runner_rest_api.py +0 -0
- {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.5}/sefrone_api_e2e/runner_satim_mock.py +0 -0
- {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.5}/sefrone_api_e2e/runner_webhook_mock.py +0 -0
- {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.5}/sefrone_api_e2e/step_runner.py +0 -0
- {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.5}/sefrone_api_e2e.egg-info/dependency_links.txt +0 -0
- {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.5}/sefrone_api_e2e.egg-info/requires.txt +0 -0
- {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.5}/sefrone_api_e2e.egg-info/top_level.txt +0 -0
- {sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.5}/setup.cfg +0 -0
|
@@ -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
|
|
13
|
+
from .step_runner import StepRunner, RunContext
|
|
14
14
|
from .runner_rest_api import RestApiRunner
|
|
15
|
-
from .
|
|
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
|
-
|
|
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:]
|
sefrone_api_e2e-1.3.5/sefrone_api_e2e/e2e/Scenarios/Auth/9_project_account_access_control_flow.yaml
ADDED
|
@@ -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()
|
|
@@ -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.
|
|
8
|
+
version="1.3.5",
|
|
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",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sefrone_api_e2e-1.3.4 → sefrone_api_e2e-1.3.5}/sefrone_api_e2e.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|