pytest-nhsd-apim 4.0.1__py3-none-any.whl → 5.0.2__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.
Potentially problematic release.
This version of pytest-nhsd-apim might be problematic. Click here for more details.
- pytest_nhsd_apim/apigee_edge.py +18 -9
- pytest_nhsd_apim/identity_service.py +17 -30
- {pytest_nhsd_apim-4.0.1.dist-info → pytest_nhsd_apim-5.0.2.dist-info}/METADATA +2 -2
- {pytest_nhsd_apim-4.0.1.dist-info → pytest_nhsd_apim-5.0.2.dist-info}/RECORD +7 -7
- {pytest_nhsd_apim-4.0.1.dist-info → pytest_nhsd_apim-5.0.2.dist-info}/WHEEL +1 -1
- {pytest_nhsd_apim-4.0.1.dist-info → pytest_nhsd_apim-5.0.2.dist-info}/entry_points.txt +0 -0
- {pytest_nhsd_apim-4.0.1.dist-info → pytest_nhsd_apim-5.0.2.dist-info}/top_level.txt +0 -0
pytest_nhsd_apim/apigee_edge.py
CHANGED
|
@@ -5,6 +5,7 @@ This includes app setup/teardown, getting proxy info (proxy-under-test
|
|
|
5
5
|
+ identity-service of choice), getting products, registering them with
|
|
6
6
|
the test app.
|
|
7
7
|
"""
|
|
8
|
+
|
|
8
9
|
import functools
|
|
9
10
|
import warnings
|
|
10
11
|
from datetime import datetime
|
|
@@ -22,7 +23,7 @@ from .apigee_apis import (
|
|
|
22
23
|
DebugSessionsAPI,
|
|
23
24
|
AccessTokensAPI,
|
|
24
25
|
ApiProductsAPI,
|
|
25
|
-
DeveloperAppsAPI
|
|
26
|
+
DeveloperAppsAPI,
|
|
26
27
|
)
|
|
27
28
|
|
|
28
29
|
APIGEE_BASE_URL = "https://api.enterprise.apigee.com/v1/"
|
|
@@ -48,6 +49,7 @@ def _apigee_app_base_url(nhsd_apim_config):
|
|
|
48
49
|
url = APIGEE_BASE_URL + f"organizations/{org}/developers/{dev}/apps"
|
|
49
50
|
return url
|
|
50
51
|
|
|
52
|
+
|
|
51
53
|
@pytest.fixture(scope="session")
|
|
52
54
|
@log_method
|
|
53
55
|
def _apigee_app_base_url_no_dev(nhsd_apim_config):
|
|
@@ -55,6 +57,7 @@ def _apigee_app_base_url_no_dev(nhsd_apim_config):
|
|
|
55
57
|
url = APIGEE_BASE_URL + f"organizations/{org}/apps"
|
|
56
58
|
return url
|
|
57
59
|
|
|
60
|
+
|
|
58
61
|
@functools.lru_cache(maxsize=None)
|
|
59
62
|
@log_method
|
|
60
63
|
def _get_proxy_json(session, nhsd_apim_proxy_url):
|
|
@@ -62,8 +65,8 @@ def _get_proxy_json(session, nhsd_apim_proxy_url):
|
|
|
62
65
|
Query the apigee edge API to get data about the desired proxy, in particular its current deployment.
|
|
63
66
|
"""
|
|
64
67
|
deployment_err_msg = (
|
|
65
|
-
"\n\tFailed to retrieve the proxy deployment data. "
|
|
66
|
-
"Please check the validity of the APIGEE credentials and token as well as any headers."
|
|
68
|
+
"\n\tFailed to retrieve the proxy deployment data. "
|
|
69
|
+
+ "Please check the validity of the APIGEE credentials and token as well as any headers."
|
|
67
70
|
)
|
|
68
71
|
deployment_resp = session.get(f"{nhsd_apim_proxy_url}/deployments")
|
|
69
72
|
assert deployment_resp.status_code == 200, deployment_err_msg.format(deployment_resp.content)
|
|
@@ -283,7 +286,9 @@ _TEST_APP = None
|
|
|
283
286
|
|
|
284
287
|
@pytest.fixture(scope="session")
|
|
285
288
|
@log_method
|
|
286
|
-
def nhsd_apim_test_app(
|
|
289
|
+
def nhsd_apim_test_app(
|
|
290
|
+
_create_test_app, _apigee_edge_session, _apigee_app_base_url, _apigee_app_base_url_no_dev, _test_app_id
|
|
291
|
+
) -> Callable:
|
|
287
292
|
"""
|
|
288
293
|
A Callable that gets you the current state of the test app.
|
|
289
294
|
"""
|
|
@@ -314,7 +319,7 @@ def nhsd_apim_test_app(_create_test_app, _apigee_edge_session, _apigee_app_base_
|
|
|
314
319
|
return _TEST_APP
|
|
315
320
|
if _test_app_id:
|
|
316
321
|
resp = _apigee_edge_session.get(_apigee_app_base_url_no_dev + "/" + _test_app_id)
|
|
317
|
-
else:
|
|
322
|
+
else:
|
|
318
323
|
resp = _apigee_edge_session.get(_apigee_app_base_url + "/" + _create_test_app["name"])
|
|
319
324
|
_TEST_APP = resp.json()
|
|
320
325
|
return _TEST_APP
|
|
@@ -468,7 +473,7 @@ def _create_test_app(
|
|
|
468
473
|
else:
|
|
469
474
|
app = {
|
|
470
475
|
"name": f"apim-auto-{uuid4()}",
|
|
471
|
-
"callbackUrl": "https://
|
|
476
|
+
"callbackUrl": "https://google.com/callback",
|
|
472
477
|
"attributes": [{"name": "jwks-resource-url", "value": jwt_public_key_url}],
|
|
473
478
|
}
|
|
474
479
|
create_resp = _apigee_edge_session.post(_apigee_app_base_url, json=app)
|
|
@@ -512,7 +517,7 @@ def _create_function_scoped_test_app(
|
|
|
512
517
|
else:
|
|
513
518
|
app = {
|
|
514
519
|
"name": f"apim-auto-{uuid4()}",
|
|
515
|
-
"callbackUrl": "https://
|
|
520
|
+
"callbackUrl": "https://google.com/callback",
|
|
516
521
|
"attributes": [{"name": "jwks-resource-url", "value": jwt_public_key_url}],
|
|
517
522
|
}
|
|
518
523
|
create_resp = _apigee_edge_session.post(_apigee_app_base_url, json=app)
|
|
@@ -538,6 +543,7 @@ def _test_app_callback_url(_create_test_app):
|
|
|
538
543
|
def _test_app_id(nhsd_apim_config):
|
|
539
544
|
return nhsd_apim_config["APIGEE_APP_ID"]
|
|
540
545
|
|
|
546
|
+
|
|
541
547
|
@pytest.fixture()
|
|
542
548
|
@log_method
|
|
543
549
|
def trace(_apigee_proxy):
|
|
@@ -545,15 +551,16 @@ def trace(_apigee_proxy):
|
|
|
545
551
|
Authenticated wrapper around the DebugSessionsAPI class
|
|
546
552
|
"""
|
|
547
553
|
config = ApigeeNonProdCredentials()
|
|
548
|
-
client =
|
|
554
|
+
client = ApigeeClient(config=config)
|
|
549
555
|
debug = DebugSessionsAPI(
|
|
550
556
|
client=client,
|
|
551
557
|
env_name=_apigee_proxy["environment"],
|
|
552
558
|
api_name=_apigee_proxy["name"],
|
|
553
|
-
revision_number=_apigee_proxy["revision"]
|
|
559
|
+
revision_number=_apigee_proxy["revision"],
|
|
554
560
|
)
|
|
555
561
|
return debug
|
|
556
562
|
|
|
563
|
+
|
|
557
564
|
@pytest.fixture(scope="session")
|
|
558
565
|
@log_method
|
|
559
566
|
def access_token_api():
|
|
@@ -575,6 +582,7 @@ def products_api():
|
|
|
575
582
|
client = ApigeeClient(config=config)
|
|
576
583
|
return ApiProductsAPI(client=client)
|
|
577
584
|
|
|
585
|
+
|
|
578
586
|
@pytest.fixture(scope="session")
|
|
579
587
|
@log_method
|
|
580
588
|
def developer_apps_api():
|
|
@@ -585,6 +593,7 @@ def developer_apps_api():
|
|
|
585
593
|
client = ApigeeClient(config=config)
|
|
586
594
|
return DeveloperAppsAPI(client=client)
|
|
587
595
|
|
|
596
|
+
|
|
588
597
|
@pytest.fixture(scope="session")
|
|
589
598
|
@log_method
|
|
590
599
|
def developer_app_keys_api():
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""
|
|
2
2
|
This is a self-contained wrapper for a bunch of authentication
|
|
3
|
-
methods in APIM. NOT ONLY the identity service is taken into
|
|
4
|
-
account in here, you will also find authenticators for keycloak
|
|
5
|
-
and more... Feel free to keep adding authenticators here and
|
|
3
|
+
methods in APIM. NOT ONLY the identity service is taken into
|
|
4
|
+
account in here, you will also find authenticators for keycloak
|
|
5
|
+
and more... Feel free to keep adding authenticators here and
|
|
6
6
|
maybe move this file to its own library.
|
|
7
7
|
"""
|
|
8
8
|
|
|
@@ -20,6 +20,7 @@ from typing_extensions import Annotated
|
|
|
20
20
|
|
|
21
21
|
HttpUrlString = Annotated[HttpUrl, AfterValidator(lambda v: str(v))]
|
|
22
22
|
|
|
23
|
+
|
|
23
24
|
#### Config models
|
|
24
25
|
class KeycloakConfig(BaseModel):
|
|
25
26
|
"""Basic Keycloak config"""
|
|
@@ -55,7 +56,7 @@ class KeycloakConfig(BaseModel):
|
|
|
55
56
|
class KeycloakUserConfig(KeycloakConfig):
|
|
56
57
|
client_id: str
|
|
57
58
|
client_secret: str
|
|
58
|
-
redirect_uri: HttpUrlString = "https://
|
|
59
|
+
redirect_uri: HttpUrlString = "https://google.com"
|
|
59
60
|
login_form: dict
|
|
60
61
|
|
|
61
62
|
|
|
@@ -92,9 +93,7 @@ class AuthorizationCodeConfig(BaseModel):
|
|
|
92
93
|
@validator("environment")
|
|
93
94
|
def validate_environment(cls, environment):
|
|
94
95
|
if environment == "prod":
|
|
95
|
-
raise ValueError(
|
|
96
|
-
f"We dont support testing in the production environment"
|
|
97
|
-
)
|
|
96
|
+
raise ValueError(f"We dont support testing in the production environment")
|
|
98
97
|
return environment
|
|
99
98
|
|
|
100
99
|
|
|
@@ -136,9 +135,7 @@ class ClientCredentialsConfig(BaseModel):
|
|
|
136
135
|
"exp": int(time()) + 300, # 5 minutes in the future
|
|
137
136
|
}
|
|
138
137
|
additional_headers = {"kid": self.jwt_kid}
|
|
139
|
-
client_assertion = jwt.encode(
|
|
140
|
-
claims, self.jwt_private_key, algorithm="RS512", headers=additional_headers
|
|
141
|
-
)
|
|
138
|
+
client_assertion = jwt.encode(claims, self.jwt_private_key, algorithm="RS512", headers=additional_headers)
|
|
142
139
|
return client_assertion
|
|
143
140
|
|
|
144
141
|
|
|
@@ -150,6 +147,7 @@ class TokenExchangeConfig(ClientCredentialsConfig):
|
|
|
150
147
|
class KeycloakSignedJWTConfig:
|
|
151
148
|
pass
|
|
152
149
|
|
|
150
|
+
|
|
153
151
|
# currently only targets AOS environment
|
|
154
152
|
class NHSLoginConfig(BaseModel):
|
|
155
153
|
"""Config needed to authenticate using NHS Login"""
|
|
@@ -159,7 +157,7 @@ class NHSLoginConfig(BaseModel):
|
|
|
159
157
|
self.nhs_login_base_url = openid_config["issuer"]
|
|
160
158
|
|
|
161
159
|
well_known_jwks: list = requests.get(openid_config["jwks_uri"]).json()
|
|
162
|
-
well_known_key = well_known_jwks[
|
|
160
|
+
well_known_key = well_known_jwks["keys"].pop()
|
|
163
161
|
self.jwt_kid = well_known_key["kid"]
|
|
164
162
|
self.alg = well_known_key["alg"]
|
|
165
163
|
|
|
@@ -184,16 +182,14 @@ class NHSLoginConfig(BaseModel):
|
|
|
184
182
|
"exp": int(time()) + 300, # 5 minutes in the future
|
|
185
183
|
}
|
|
186
184
|
additional_headers = {"kid": self.jwt_kid}
|
|
187
|
-
client_assertion = jwt.encode(
|
|
188
|
-
claims, self.jwt_private_key, algorithm=self.alg, headers=additional_headers
|
|
189
|
-
)
|
|
185
|
+
client_assertion = jwt.encode(claims, self.jwt_private_key, algorithm=self.alg, headers=additional_headers)
|
|
190
186
|
return client_assertion
|
|
191
|
-
|
|
192
187
|
|
|
193
188
|
|
|
194
189
|
class BananaAuthenticatorConfig: # Placeholder
|
|
195
190
|
pass
|
|
196
191
|
|
|
192
|
+
|
|
197
193
|
### Authenticators
|
|
198
194
|
class Authenticator(ABC):
|
|
199
195
|
"""Defines the interface"""
|
|
@@ -263,9 +259,7 @@ class AuthorizationCodeAuthenticator(Authenticator):
|
|
|
263
259
|
},
|
|
264
260
|
)
|
|
265
261
|
if resp.status_code != 200:
|
|
266
|
-
raise RuntimeError(
|
|
267
|
-
f"{auth_url} request returned {resp.status_code}: {resp.text}"
|
|
268
|
-
)
|
|
262
|
+
raise RuntimeError(f"{auth_url} request returned {resp.status_code}: {resp.text}")
|
|
269
263
|
return resp
|
|
270
264
|
|
|
271
265
|
@staticmethod
|
|
@@ -299,9 +293,7 @@ class AuthorizationCodeAuthenticator(Authenticator):
|
|
|
299
293
|
form_submission_data,
|
|
300
294
|
):
|
|
301
295
|
form_submit_url = authorize_form.action or authorize_response.request.url
|
|
302
|
-
resp = session.request(
|
|
303
|
-
authorize_form.method, form_submit_url, data=form_submission_data
|
|
304
|
-
)
|
|
296
|
+
resp = session.request(authorize_form.method, form_submit_url, data=form_submission_data)
|
|
305
297
|
# TODO: Investigate why when using the fixtures it returns 404 and when
|
|
306
298
|
# using with external credentials returns 200
|
|
307
299
|
# if resp.status_code != 200:
|
|
@@ -312,9 +304,7 @@ class AuthorizationCodeAuthenticator(Authenticator):
|
|
|
312
304
|
|
|
313
305
|
@staticmethod
|
|
314
306
|
def _get_auth_code_from_mock_auth(response_identity_service_login):
|
|
315
|
-
qs = urlparse(
|
|
316
|
-
response_identity_service_login.history[-1].headers["Location"]
|
|
317
|
-
).query
|
|
307
|
+
qs = urlparse(response_identity_service_login.history[-1].headers["Location"]).query
|
|
318
308
|
auth_code = parse_qs(qs)["code"]
|
|
319
309
|
if isinstance(auth_code, list):
|
|
320
310
|
# in case there's multiple, this was a bug at one stage
|
|
@@ -336,16 +326,12 @@ class AuthorizationCodeAuthenticator(Authenticator):
|
|
|
336
326
|
self.config.scope,
|
|
337
327
|
)
|
|
338
328
|
|
|
339
|
-
authorize_form = self._get_authorization_form(
|
|
340
|
-
authorize_response.content.decode()
|
|
341
|
-
)
|
|
329
|
+
authorize_form = self._get_authorization_form(authorize_response.content.decode())
|
|
342
330
|
# 2. Parse the login page. For keycloak this presents an
|
|
343
331
|
# HTML form, which must be filled in with valid data. The tester
|
|
344
332
|
# can submits their login data with the `login_form` field.
|
|
345
333
|
|
|
346
|
-
form_submission_data = self._get_authorize_form_submission_data(
|
|
347
|
-
authorize_form, self.config.login_form
|
|
348
|
-
)
|
|
334
|
+
form_submission_data = self._get_authorize_form_submission_data(authorize_form, self.config.login_form)
|
|
349
335
|
|
|
350
336
|
# form_submission_data["username"] = 656005750104
|
|
351
337
|
# # And here we inject a valid mock username for keycloak.
|
|
@@ -508,6 +494,7 @@ class NHSLoginSandpitAuthenticator(Authenticator):
|
|
|
508
494
|
|
|
509
495
|
class NHSLoginAosAuthenticator(Authenticator):
|
|
510
496
|
"""Authenticates you against NHS-Login aos environment"""
|
|
497
|
+
|
|
511
498
|
# This is only partially implemented. See below for usage:
|
|
512
499
|
# https://nhsd-confluence.digital.nhs.uk/display/APM/KOP-085+Generating+NHS+login+ID+tokens
|
|
513
500
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pytest-nhsd-apim
|
|
3
|
-
Version:
|
|
3
|
+
Version: 5.0.2
|
|
4
4
|
Summary: Pytest plugin accessing NHSDigital's APIM proxies
|
|
5
5
|
Home-page: https://github.com/NHSDigital/pytest-nhsd-apim
|
|
6
6
|
Author: Adrian Ciobanita
|
|
@@ -13,7 +13,7 @@ Requires-Python: >=3.8
|
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
Requires-Dist: Authlib (==1.3.1)
|
|
15
15
|
Requires-Dist: cryptography (==42.0.0)
|
|
16
|
-
Requires-Dist: lxml (==
|
|
16
|
+
Requires-Dist: lxml (==5.3.1)
|
|
17
17
|
Requires-Dist: pycryptodome (==3.20.0)
|
|
18
18
|
Requires-Dist: PyJWT (==2.8.0)
|
|
19
19
|
Requires-Dist: pyotp (==2.9.0)
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
pytest_nhsd_apim/__init__.py,sha256=CRwQfUDrbXIqvJX6OfTR4jGdhlU9kjGmBAptJasRg7E,72
|
|
2
2
|
pytest_nhsd_apim/apigee_apis.py,sha256=u_sHjISj4XKgidVje1Uzq4LblMGnX4lrrrMWZM-SEF4,67289
|
|
3
|
-
pytest_nhsd_apim/apigee_edge.py,sha256=
|
|
3
|
+
pytest_nhsd_apim/apigee_edge.py,sha256=STfwfXpdGLURCfZ3AoXwyAhfP2vIyVDY1U_W0350SE8,19076
|
|
4
4
|
pytest_nhsd_apim/auth_journey.py,sha256=UovbLXhokUnikrMOaXIhjV8t5aRrcxinAbS96nfZWcY,5154
|
|
5
5
|
pytest_nhsd_apim/config.py,sha256=ScKfV8iURqDXX2ajgGsRDcVn9RZy2DxLoEU2QQt9lmA,4246
|
|
6
|
-
pytest_nhsd_apim/identity_service.py,sha256=
|
|
6
|
+
pytest_nhsd_apim/identity_service.py,sha256=naVMtBJR7cRYLbP1mJyyGGrit9YqWF50CpELM2vgp4g,19702
|
|
7
7
|
pytest_nhsd_apim/log.py,sha256=8gYHqzlQM546FB2XvFmLTk1YfZRNeNhIwLmOy0GScr8,2685
|
|
8
8
|
pytest_nhsd_apim/nhsd_apim_authorization.py,sha256=GR8GfbIZyqBC4jsSZMYNifDH52E3VWoIa7lrpuvIbaM,3513
|
|
9
9
|
pytest_nhsd_apim/pytest_nhsd_apim.py,sha256=ZCItUqcM23CCmcRyGU8bEwI3vJnNcGdoOlbSfvYILR8,5242
|
|
10
10
|
pytest_nhsd_apim/secrets.py,sha256=yIYwOZwpliIomtqSJGIYRbAE2HYvLvQU4W2kOa9TnXo,2354
|
|
11
11
|
pytest_nhsd_apim/token_cache.py,sha256=6L08taTlSyRsx2NCb0LSGsHdWx_wmqwlFtcF7pZMhUg,3540
|
|
12
|
-
pytest_nhsd_apim-
|
|
13
|
-
pytest_nhsd_apim-
|
|
14
|
-
pytest_nhsd_apim-
|
|
15
|
-
pytest_nhsd_apim-
|
|
16
|
-
pytest_nhsd_apim-
|
|
12
|
+
pytest_nhsd_apim-5.0.2.dist-info/METADATA,sha256=eovd0I30Nv0B0-RFskDi1FFdxuOQiK_VPfEpP93Y7eU,4704
|
|
13
|
+
pytest_nhsd_apim-5.0.2.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
|
|
14
|
+
pytest_nhsd_apim-5.0.2.dist-info/entry_points.txt,sha256=XWicT3meTpqLXnZcXNoAd2IfXspFPlNgMgLBMy4nqwQ,57
|
|
15
|
+
pytest_nhsd_apim-5.0.2.dist-info/top_level.txt,sha256=ZK5GZP-g_K8gNfm4a58T9JCRb0i1X267ngvNelCGgfQ,17
|
|
16
|
+
pytest_nhsd_apim-5.0.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|