pytest-nhsd-apim 3.3.6__py3-none-any.whl → 5.0.15__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.
- pytest_nhsd_apim/apigee_apis.py +233 -32
- pytest_nhsd_apim/apigee_edge.py +33 -10
- pytest_nhsd_apim/identity_service.py +44 -57
- pytest_nhsd_apim/nhsd_apim_authorization.py +1 -1
- pytest_nhsd_apim/pytest_nhsd_apim.py +2 -1
- pytest_nhsd_apim/secrets.py +14 -3
- pytest_nhsd_apim/token_cache.py +18 -5
- {pytest_nhsd_apim-3.3.6.dist-info → pytest_nhsd_apim-5.0.15.dist-info}/METADATA +30 -14
- pytest_nhsd_apim-5.0.15.dist-info/RECORD +16 -0
- {pytest_nhsd_apim-3.3.6.dist-info → pytest_nhsd_apim-5.0.15.dist-info}/WHEEL +1 -1
- pytest_nhsd_apim-3.3.6.dist-info/RECORD +0 -16
- {pytest_nhsd_apim-3.3.6.dist-info → pytest_nhsd_apim-5.0.15.dist-info}/entry_points.txt +0 -0
- {pytest_nhsd_apim-3.3.6.dist-info → pytest_nhsd_apim-5.0.15.dist-info}/top_level.txt +0 -0
pytest_nhsd_apim/apigee_apis.py
CHANGED
|
@@ -5,7 +5,8 @@ import jwt
|
|
|
5
5
|
import pyotp
|
|
6
6
|
import requests
|
|
7
7
|
from jwt import ExpiredSignatureError
|
|
8
|
-
from pydantic import
|
|
8
|
+
from pydantic import model_validator
|
|
9
|
+
from pydantic_settings import BaseSettings
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
class ApigeeProdCredentials(BaseSettings):
|
|
@@ -38,29 +39,27 @@ class ApigeeProdCredentials(BaseSettings):
|
|
|
38
39
|
apigee_nhsd_prod_password: Optional[str] = None
|
|
39
40
|
apigee_nhsd_prod_passcode: Optional[str] = None
|
|
40
41
|
apigee_access_token: Optional[str] = None
|
|
42
|
+
auth_method: Optional[str] = None
|
|
41
43
|
|
|
42
|
-
@
|
|
44
|
+
@model_validator(mode="before")
|
|
43
45
|
def check_credentials_config(cls, values):
|
|
44
46
|
print(values)
|
|
45
47
|
"""Checks for the right set of credentials"""
|
|
46
48
|
if all(
|
|
47
49
|
[
|
|
48
|
-
values.get(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"apigee_nhsd_prod_password",
|
|
52
|
-
"auth_server",
|
|
53
|
-
]
|
|
50
|
+
values.get("apigee_nhsd_prod_username"),
|
|
51
|
+
values.get("apigee_nhsd_prod_password"),
|
|
52
|
+
values.get("auth_server"),
|
|
54
53
|
]
|
|
55
54
|
):
|
|
56
55
|
values["auth_method"] = "saml"
|
|
57
56
|
return values
|
|
58
57
|
elif all(
|
|
59
|
-
[values.get(
|
|
58
|
+
[values.get("auth_server"), values.get("apigee_nhsd_prod_passcode")]
|
|
60
59
|
):
|
|
61
60
|
values["auth_method"] = "saml"
|
|
62
61
|
return values
|
|
63
|
-
elif values
|
|
62
|
+
elif values.get("apigee_access_token"):
|
|
64
63
|
values["auth_method"] = "access_token"
|
|
65
64
|
return values
|
|
66
65
|
else:
|
|
@@ -101,39 +100,34 @@ class ApigeeProdCredentials(BaseSettings):
|
|
|
101
100
|
|
|
102
101
|
class ApigeeNonProdCredentials(BaseSettings):
|
|
103
102
|
auth_server: str = "login.apigee.com"
|
|
104
|
-
apigee_nhsd_nonprod_username: Optional[str]
|
|
105
|
-
apigee_nhsd_nonprod_password: Optional[str]
|
|
106
|
-
apigee_nhsd_nonprod_otp_key: Optional[str]
|
|
107
|
-
apigee_access_token: Optional[str]
|
|
103
|
+
apigee_nhsd_nonprod_username: Optional[str] = None
|
|
104
|
+
apigee_nhsd_nonprod_password: Optional[str] = None
|
|
105
|
+
apigee_nhsd_nonprod_otp_key: Optional[str] = None
|
|
106
|
+
apigee_access_token: Optional[str] = None
|
|
107
|
+
auth_method: Optional[str] = None
|
|
108
108
|
|
|
109
|
-
@
|
|
109
|
+
@model_validator(mode='before')
|
|
110
110
|
def check_credentials_config(cls, values):
|
|
111
111
|
"""Checks for the right set of credentials"""
|
|
112
112
|
if all(
|
|
113
113
|
[
|
|
114
|
-
values.get(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
"apigee_nhsd_nonprod_password",
|
|
118
|
-
"apigee_nhsd_nonprod_otp_key",
|
|
119
|
-
]
|
|
114
|
+
values.get("apigee_nhsd_nonprod_username"),
|
|
115
|
+
values.get("apigee_nhsd_nonprod_password"),
|
|
116
|
+
values.get("apigee_nhsd_nonprod_otp_key"),
|
|
120
117
|
]
|
|
121
118
|
):
|
|
122
119
|
values["auth_method"] = "saml"
|
|
123
120
|
return values
|
|
124
121
|
elif all(
|
|
125
122
|
[
|
|
126
|
-
values.get(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
"apigee_nhsd_nonprod_password",
|
|
130
|
-
"apigee_nhsd_nonprod_username",
|
|
131
|
-
]
|
|
123
|
+
values.get("auth_server"),
|
|
124
|
+
values.get("apigee_nhsd_nonprod_password"),
|
|
125
|
+
values.get("apigee_nhsd_nonprod_username"),
|
|
132
126
|
]
|
|
133
127
|
):
|
|
134
128
|
values["auth_method"] = "saml"
|
|
135
129
|
return values
|
|
136
|
-
elif values
|
|
130
|
+
elif values.get("apigee_access_token"):
|
|
137
131
|
values["auth_method"] = "access_token"
|
|
138
132
|
return values
|
|
139
133
|
else:
|
|
@@ -476,7 +470,7 @@ class DeveloperAppsAPI:
|
|
|
476
470
|
resp = self.client.delete(url=url)
|
|
477
471
|
if resp.status_code != 200:
|
|
478
472
|
raise Exception(
|
|
479
|
-
f"
|
|
473
|
+
f"DELETE request to {resp.url} failed with status_code: {resp.status_code}, Reason: {resp.reason} and Content: {resp.text}"
|
|
480
474
|
)
|
|
481
475
|
return resp.json()
|
|
482
476
|
|
|
@@ -1241,11 +1235,218 @@ class UserRolesAPI:
|
|
|
1241
1235
|
|
|
1242
1236
|
|
|
1243
1237
|
class AppKeysAPI:
|
|
1238
|
+
"""
|
|
1239
|
+
Manage consumer credentials for apps associated with individual developers.
|
|
1240
|
+
|
|
1241
|
+
Credential pairs consisting of consumer key and consumer secret are provisioned
|
|
1242
|
+
by Apigee Edge to apps for specific API products. Apigee Edge maintains the
|
|
1243
|
+
relationship between consumer keys and API products, enabling API products to be
|
|
1244
|
+
added to and removed from consumer keys. A single consumer key can be used to
|
|
1245
|
+
access multiple API products. Keys may be manually or automatically approved for
|
|
1246
|
+
API products--how they are issued depends on the API product configuration. A key
|
|
1247
|
+
must approved and approved for an API product to be capable of accessing any of
|
|
1248
|
+
the URIs defined in the API product.
|
|
1249
|
+
"""
|
|
1250
|
+
|
|
1244
1251
|
def __init__(self, client: RestClient) -> None:
|
|
1245
1252
|
self.client = client
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1253
|
+
|
|
1254
|
+
def create_app_key(self, email: str, app_name: str, body: dict) -> "dict":
|
|
1255
|
+
"""
|
|
1256
|
+
Creates a custom consumer key and secret for a developer app.
|
|
1257
|
+
This is particularly useful if you want to migrate existing consumer
|
|
1258
|
+
keys/secrets to Edge from another system.
|
|
1259
|
+
|
|
1260
|
+
After creating the consumer key and secret, associate the key with an
|
|
1261
|
+
API product, as described in Add API Product to Key.
|
|
1262
|
+
|
|
1263
|
+
Consumer keys and secrets can contain letters, numbers, underscores,
|
|
1264
|
+
and hyphens. No other special characters are allowed.
|
|
1265
|
+
|
|
1266
|
+
Note: Be aware of the following size limits on API keys. By staying
|
|
1267
|
+
within these limits, you help avoid service disruptions.
|
|
1268
|
+
|
|
1269
|
+
- Consumer key (API key) size: 2 KB
|
|
1270
|
+
|
|
1271
|
+
- Consumer secret size: 2 KB
|
|
1272
|
+
|
|
1273
|
+
If a consumer key and secret already exist, you can either keep them
|
|
1274
|
+
or delete them, as described in Delete Key for a Developer App.
|
|
1275
|
+
|
|
1276
|
+
In addition, you can use this API if you have existing API keys and
|
|
1277
|
+
secrets that you want to copy into Edge from another system. For more
|
|
1278
|
+
information, see Import existing consumer keys and secrets.
|
|
1279
|
+
"""
|
|
1280
|
+
|
|
1281
|
+
resource = f"/developers/{email}/apps/{app_name}/keys/create"
|
|
1282
|
+
url = f"{self.client.base_url}{resource}"
|
|
1283
|
+
resp = self.client.post(url=url, json=body)
|
|
1284
|
+
if resp.status_code != 201:
|
|
1285
|
+
raise Exception(
|
|
1286
|
+
f"POST request to {resp.url} failed with status_code: {resp.status_code}, Reason: {resp.reason} and Content: {resp.text}"
|
|
1287
|
+
)
|
|
1288
|
+
return resp.json()
|
|
1289
|
+
|
|
1290
|
+
def delete_app_key(self, email: str, app_name: str, app_key: str) -> None:
|
|
1291
|
+
"""
|
|
1292
|
+
Deletes a consumer key that belongs to an app, and removes all API products
|
|
1293
|
+
associated with the app. Once deleted, the consumer key cannot be used
|
|
1294
|
+
to access any APIs.
|
|
1295
|
+
|
|
1296
|
+
After you delete a consumer key, you may want to:
|
|
1297
|
+
|
|
1298
|
+
- Create a new consumer key and secret for the developer app, and
|
|
1299
|
+
subsequently add an API product to the key.
|
|
1300
|
+
|
|
1301
|
+
- Delete the developer app, if it is no longer required.
|
|
1302
|
+
"""
|
|
1303
|
+
|
|
1304
|
+
resource = f"/developers/{email}/apps/{app_name}/keys/{app_key}"
|
|
1305
|
+
url = f"{self.client.base_url}{resource}"
|
|
1306
|
+
resp = self.client.delete(url=url)
|
|
1307
|
+
if resp.status_code != 200:
|
|
1308
|
+
raise Exception(
|
|
1309
|
+
f"DELETE request to {resp.url} failed with status_code: {resp.status_code}, Reason: {resp.reason} and Content: {resp.text}"
|
|
1310
|
+
)
|
|
1311
|
+
return resp.json()
|
|
1312
|
+
|
|
1313
|
+
def get_app_key(self, email: str, app_name: str, key: str, **query_params) -> "list[str]":
|
|
1314
|
+
"""
|
|
1315
|
+
Gets details for a consumer key for a developer app, including the key
|
|
1316
|
+
and secret value, associated API products, and other information.
|
|
1317
|
+
"""
|
|
1318
|
+
|
|
1319
|
+
params = query_params
|
|
1320
|
+
resource = f"/developers/{email}/apps/{app_name}/keys/{key}"
|
|
1321
|
+
url = f"{self.client.base_url}{resource}"
|
|
1322
|
+
resp = self.client.get(url=url, params=params)
|
|
1323
|
+
if resp.status_code != 200:
|
|
1324
|
+
raise Exception(
|
|
1325
|
+
f"GET request to {resp.url} failed with status_code: {resp.status_code}, Reason: {resp.reason} and Content: {resp.text}"
|
|
1326
|
+
)
|
|
1327
|
+
return resp.json()
|
|
1328
|
+
|
|
1329
|
+
def post_app_key(self, email: str, app_name: str, key: str, body: dict, **query_params) -> "dict":
|
|
1330
|
+
"""
|
|
1331
|
+
Enables you to perform one of the following tasks:
|
|
1332
|
+
|
|
1333
|
+
- Add an API product to a developer app key, enabling the app that holds
|
|
1334
|
+
the key to access the API resources bundled in the API product. You can
|
|
1335
|
+
also use this API to add attributes to the key. You must include all existing
|
|
1336
|
+
attributes, whether or not you are updating them, as well as any new attributes
|
|
1337
|
+
that you are adding. After adding the API product, you can use the same key
|
|
1338
|
+
to access all API products associated with the app.
|
|
1339
|
+
|
|
1340
|
+
- Approve or revoke a specific consumer key for an app. Call the API with the
|
|
1341
|
+
action query parameter set to approve or revoke (with no request body) and
|
|
1342
|
+
set the Content-type header to application/octet-stream. If successful, the HTTP
|
|
1343
|
+
status code for success is: 204 No Content
|
|
1344
|
+
|
|
1345
|
+
- You can approve a consumer key that is currently revoked or pending. Once
|
|
1346
|
+
approved, the app can use the consumer key to access APIs. Revoking a consumer
|
|
1347
|
+
key renders it unusable for the app to use to access an API.
|
|
1348
|
+
|
|
1349
|
+
- Note: Any access tokens associated with a revoked app key will remain active.
|
|
1350
|
+
However, Apigee Edge checks the status of the app key and if set to revoked it
|
|
1351
|
+
will not allow API calls to go through.
|
|
1352
|
+
"""
|
|
1353
|
+
|
|
1354
|
+
resource = f"/developers/{email}/apps/{app_name}/keys/{key}"
|
|
1355
|
+
url = f"{self.client.base_url}{resource}"
|
|
1356
|
+
resp = self.client.post(url=url, json=body, params=query_params)
|
|
1357
|
+
if resp.status_code != 200 and resp.status_code != 204:
|
|
1358
|
+
raise Exception(
|
|
1359
|
+
f"POST request to {resp.url} failed with status_code: {resp.status_code}, Reason: {resp.reason} and Content: {resp.text}"
|
|
1360
|
+
)
|
|
1361
|
+
if resp.status_code == 204:
|
|
1362
|
+
return resp
|
|
1363
|
+
else:
|
|
1364
|
+
return resp.json()
|
|
1365
|
+
|
|
1366
|
+
def put_app_key(self, email: str, app_name: str, key: str, body: dict) -> "dict":
|
|
1367
|
+
"""
|
|
1368
|
+
Updates the allowed OAuth scopes associated with an app.
|
|
1369
|
+
|
|
1370
|
+
Note: Specify the complete list of scopes to apply. The specified list replaces
|
|
1371
|
+
the existing scopes on the app. Therefore, to add a scope, you must specify all
|
|
1372
|
+
of the existing scopes along with the added scope.
|
|
1373
|
+
|
|
1374
|
+
This API does not change the list of scopes in the API product(s) included in
|
|
1375
|
+
the app; rather, it sets allowed list of scopes in the scopes element under the
|
|
1376
|
+
apiProducts element in the attributes of the app.
|
|
1377
|
+
|
|
1378
|
+
Important: The specified scopes must already exist on the API product(s)
|
|
1379
|
+
associated with the app. You can't arbitrarily add a scope that does not already
|
|
1380
|
+
exist in an API product. For example, if the app has one API product with these
|
|
1381
|
+
scopes: READ, WRITE. You can't use this API to add a new scope, such as DELETE
|
|
1382
|
+
(unless the app has another product with that scope). If you do this, you'll get
|
|
1383
|
+
a 400 Bad Request error. For example:
|
|
1384
|
+
|
|
1385
|
+
{
|
|
1386
|
+
"code": "keymanagement.service.InvalidScopes",
|
|
1387
|
+
"message": "Invalid scopes. Scopes must be contained in [READ, WRITE]",
|
|
1388
|
+
"contexts": []
|
|
1389
|
+
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
It would be allowed to remove one or both of the existing scopes, and later add
|
|
1393
|
+
one or both back.
|
|
1394
|
+
"""
|
|
1395
|
+
|
|
1396
|
+
resource = f"/developers/{email}/apps/{app_name}/keys/{key}"
|
|
1397
|
+
url = f"{self.client.base_url}{resource}"
|
|
1398
|
+
resp = self.client.put(url=url, json=body)
|
|
1399
|
+
if resp.status_code != 200:
|
|
1400
|
+
raise Exception(
|
|
1401
|
+
f"PUT request to {resp.url} failed with status_code: {resp.status_code}, Reason: {resp.reason} and Content: {resp.text}"
|
|
1402
|
+
)
|
|
1403
|
+
return resp.json()
|
|
1404
|
+
|
|
1405
|
+
def delete_product_app_key_association(self, email: str, app_name: str, app_key: str, apiproduct_name: str) -> None:
|
|
1406
|
+
"""
|
|
1407
|
+
Removes an API product from an app's consumer key, and thereby renders the app
|
|
1408
|
+
unable to access the API resources defined in that API product.
|
|
1409
|
+
|
|
1410
|
+
Note that the consumer key itself still exists after this call. Only the
|
|
1411
|
+
association of the key with the API product is removed.
|
|
1412
|
+
"""
|
|
1413
|
+
|
|
1414
|
+
resource = f"/developers/{email}/apps/{app_name}/keys/{app_key}/apiproducts/{apiproduct_name}"
|
|
1415
|
+
url = f"{self.client.base_url}{resource}"
|
|
1416
|
+
resp = self.client.delete(url=url)
|
|
1417
|
+
if resp.status_code != 200:
|
|
1418
|
+
raise Exception(
|
|
1419
|
+
f"DELETE request to {resp.url} failed with status_code: {resp.status_code}, Reason: {resp.reason} and Content: {resp.text}"
|
|
1420
|
+
)
|
|
1421
|
+
return resp.json()
|
|
1422
|
+
|
|
1423
|
+
def post_product_app_key_association(self, email: str, app_name: str, key: str, apiproduct_name: str, **query_params) -> "dict":
|
|
1424
|
+
"""
|
|
1425
|
+
Approves or revokes an API product for an API key. Call the API with the action
|
|
1426
|
+
query parameter set to approve or revoke (with no request body) and set the
|
|
1427
|
+
Content-type header to application/octet-stream. If successful, the HTTP status
|
|
1428
|
+
code for success is: 204 No Content
|
|
1429
|
+
|
|
1430
|
+
To consume API resources defined in an API product, an app's consumer key must
|
|
1431
|
+
be approved and it must also be approved for that specific API product.
|
|
1432
|
+
|
|
1433
|
+
Notes:
|
|
1434
|
+
|
|
1435
|
+
- The API product must already be associated with the app.
|
|
1436
|
+
|
|
1437
|
+
- Any access tokens associated with a revoked app key will remain active. However,
|
|
1438
|
+
Apigee Edge checks the status of the app key and if set to revoked it will not
|
|
1439
|
+
allow API calls to go through.
|
|
1440
|
+
"""
|
|
1441
|
+
|
|
1442
|
+
resource = f"/developers/{email}/apps/{app_name}/keys/{key}/apiproducts/{apiproduct_name}"
|
|
1443
|
+
url = f"{self.client.base_url}{resource}"
|
|
1444
|
+
resp = self.client.post(url=url, params=query_params)
|
|
1445
|
+
if resp.status_code != 204:
|
|
1446
|
+
raise Exception(
|
|
1447
|
+
f"POST request to {resp.url} failed with status_code: {resp.status_code}, Reason: {resp.reason} and Content: {resp.text}"
|
|
1448
|
+
)
|
|
1449
|
+
return resp
|
|
1249
1450
|
|
|
1250
1451
|
|
|
1251
1452
|
class UsersAPI:
|
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
|
|
@@ -18,10 +19,11 @@ from .log import log, log_method
|
|
|
18
19
|
from .apigee_apis import (
|
|
19
20
|
ApigeeNonProdCredentials,
|
|
20
21
|
ApigeeClient,
|
|
22
|
+
AppKeysAPI,
|
|
21
23
|
DebugSessionsAPI,
|
|
22
24
|
AccessTokensAPI,
|
|
23
25
|
ApiProductsAPI,
|
|
24
|
-
DeveloperAppsAPI
|
|
26
|
+
DeveloperAppsAPI,
|
|
25
27
|
)
|
|
26
28
|
|
|
27
29
|
APIGEE_BASE_URL = "https://api.enterprise.apigee.com/v1/"
|
|
@@ -47,6 +49,7 @@ def _apigee_app_base_url(nhsd_apim_config):
|
|
|
47
49
|
url = APIGEE_BASE_URL + f"organizations/{org}/developers/{dev}/apps"
|
|
48
50
|
return url
|
|
49
51
|
|
|
52
|
+
|
|
50
53
|
@pytest.fixture(scope="session")
|
|
51
54
|
@log_method
|
|
52
55
|
def _apigee_app_base_url_no_dev(nhsd_apim_config):
|
|
@@ -54,14 +57,18 @@ def _apigee_app_base_url_no_dev(nhsd_apim_config):
|
|
|
54
57
|
url = APIGEE_BASE_URL + f"organizations/{org}/apps"
|
|
55
58
|
return url
|
|
56
59
|
|
|
60
|
+
|
|
57
61
|
@functools.lru_cache(maxsize=None)
|
|
58
62
|
@log_method
|
|
59
63
|
def _get_proxy_json(session, nhsd_apim_proxy_url):
|
|
60
64
|
"""
|
|
61
65
|
Query the apigee edge API to get data about the desired proxy, in particular its current deployment.
|
|
62
66
|
"""
|
|
63
|
-
deployment_err_msg =
|
|
64
|
-
|
|
67
|
+
deployment_err_msg = (
|
|
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."
|
|
70
|
+
)
|
|
71
|
+
deployment_resp = session.get(f"{nhsd_apim_proxy_url}/deployments")
|
|
65
72
|
assert deployment_resp.status_code == 200, deployment_err_msg.format(deployment_resp.content)
|
|
66
73
|
deployment_json = deployment_resp.json()
|
|
67
74
|
|
|
@@ -279,7 +286,9 @@ _TEST_APP = None
|
|
|
279
286
|
|
|
280
287
|
@pytest.fixture(scope="session")
|
|
281
288
|
@log_method
|
|
282
|
-
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:
|
|
283
292
|
"""
|
|
284
293
|
A Callable that gets you the current state of the test app.
|
|
285
294
|
"""
|
|
@@ -310,7 +319,7 @@ def nhsd_apim_test_app(_create_test_app, _apigee_edge_session, _apigee_app_base_
|
|
|
310
319
|
return _TEST_APP
|
|
311
320
|
if _test_app_id:
|
|
312
321
|
resp = _apigee_edge_session.get(_apigee_app_base_url_no_dev + "/" + _test_app_id)
|
|
313
|
-
else:
|
|
322
|
+
else:
|
|
314
323
|
resp = _apigee_edge_session.get(_apigee_app_base_url + "/" + _create_test_app["name"])
|
|
315
324
|
_TEST_APP = resp.json()
|
|
316
325
|
return _TEST_APP
|
|
@@ -318,8 +327,8 @@ def nhsd_apim_test_app(_create_test_app, _apigee_edge_session, _apigee_app_base_
|
|
|
318
327
|
return app
|
|
319
328
|
|
|
320
329
|
|
|
321
|
-
@log_method
|
|
322
330
|
@pytest.fixture(scope="session")
|
|
331
|
+
@log_method
|
|
323
332
|
def nhsd_apim_unsubscribe_test_app_from_all_products(
|
|
324
333
|
nhsd_apim_test_app, _apigee_edge_session, _apigee_app_base_url, _test_app_id
|
|
325
334
|
):
|
|
@@ -464,7 +473,7 @@ def _create_test_app(
|
|
|
464
473
|
else:
|
|
465
474
|
app = {
|
|
466
475
|
"name": f"apim-auto-{uuid4()}",
|
|
467
|
-
"callbackUrl": "https://
|
|
476
|
+
"callbackUrl": "https://google.com/callback",
|
|
468
477
|
"attributes": [{"name": "jwks-resource-url", "value": jwt_public_key_url}],
|
|
469
478
|
}
|
|
470
479
|
create_resp = _apigee_edge_session.post(_apigee_app_base_url, json=app)
|
|
@@ -508,7 +517,7 @@ def _create_function_scoped_test_app(
|
|
|
508
517
|
else:
|
|
509
518
|
app = {
|
|
510
519
|
"name": f"apim-auto-{uuid4()}",
|
|
511
|
-
"callbackUrl": "https://
|
|
520
|
+
"callbackUrl": "https://google.com/callback",
|
|
512
521
|
"attributes": [{"name": "jwks-resource-url", "value": jwt_public_key_url}],
|
|
513
522
|
}
|
|
514
523
|
create_resp = _apigee_edge_session.post(_apigee_app_base_url, json=app)
|
|
@@ -534,6 +543,7 @@ def _test_app_callback_url(_create_test_app):
|
|
|
534
543
|
def _test_app_id(nhsd_apim_config):
|
|
535
544
|
return nhsd_apim_config["APIGEE_APP_ID"]
|
|
536
545
|
|
|
546
|
+
|
|
537
547
|
@pytest.fixture()
|
|
538
548
|
@log_method
|
|
539
549
|
def trace(_apigee_proxy):
|
|
@@ -541,15 +551,16 @@ def trace(_apigee_proxy):
|
|
|
541
551
|
Authenticated wrapper around the DebugSessionsAPI class
|
|
542
552
|
"""
|
|
543
553
|
config = ApigeeNonProdCredentials()
|
|
544
|
-
client =
|
|
554
|
+
client = ApigeeClient(config=config)
|
|
545
555
|
debug = DebugSessionsAPI(
|
|
546
556
|
client=client,
|
|
547
557
|
env_name=_apigee_proxy["environment"],
|
|
548
558
|
api_name=_apigee_proxy["name"],
|
|
549
|
-
revision_number=_apigee_proxy["revision"]
|
|
559
|
+
revision_number=_apigee_proxy["revision"],
|
|
550
560
|
)
|
|
551
561
|
return debug
|
|
552
562
|
|
|
563
|
+
|
|
553
564
|
@pytest.fixture(scope="session")
|
|
554
565
|
@log_method
|
|
555
566
|
def access_token_api():
|
|
@@ -571,6 +582,7 @@ def products_api():
|
|
|
571
582
|
client = ApigeeClient(config=config)
|
|
572
583
|
return ApiProductsAPI(client=client)
|
|
573
584
|
|
|
585
|
+
|
|
574
586
|
@pytest.fixture(scope="session")
|
|
575
587
|
@log_method
|
|
576
588
|
def developer_apps_api():
|
|
@@ -580,3 +592,14 @@ def developer_apps_api():
|
|
|
580
592
|
config = ApigeeNonProdCredentials()
|
|
581
593
|
client = ApigeeClient(config=config)
|
|
582
594
|
return DeveloperAppsAPI(client=client)
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
@pytest.fixture(scope="session")
|
|
598
|
+
@log_method
|
|
599
|
+
def developer_app_keys_api():
|
|
600
|
+
"""
|
|
601
|
+
Authenitcated wrapper for Apigee's developer app keys API
|
|
602
|
+
"""
|
|
603
|
+
config = ApigeeNonProdCredentials()
|
|
604
|
+
client = ApigeeClient(config=config)
|
|
605
|
+
return AppKeysAPI(client=client)
|
|
@@ -1,23 +1,24 @@
|
|
|
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
|
|
|
9
9
|
import uuid
|
|
10
|
-
from time import time
|
|
11
|
-
from typing import Literal
|
|
12
|
-
|
|
13
|
-
import jwt
|
|
14
|
-
from pydantic import BaseModel, HttpUrl, validator
|
|
15
10
|
from abc import ABC, abstractmethod
|
|
11
|
+
from time import time
|
|
16
12
|
from typing import Literal
|
|
17
13
|
from urllib.parse import parse_qs, urlparse
|
|
18
14
|
|
|
15
|
+
import jwt
|
|
19
16
|
import requests
|
|
20
17
|
from lxml import html
|
|
18
|
+
from pydantic import BaseModel, HttpUrl, validator, AfterValidator
|
|
19
|
+
from typing_extensions import Annotated
|
|
20
|
+
|
|
21
|
+
HttpUrlString = Annotated[HttpUrl, AfterValidator(lambda v: str(v).rstrip("/"))]
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
#### Config models
|
|
@@ -26,14 +27,22 @@ class KeycloakConfig(BaseModel):
|
|
|
26
27
|
|
|
27
28
|
realm: Literal[
|
|
28
29
|
"Cis2-mock-internal-dev",
|
|
30
|
+
"Cis2-mock-internal-dev-sandbox",
|
|
29
31
|
"Cis2-mock-internal-qa",
|
|
32
|
+
"Cis2-mock-internal-qa-sandbox",
|
|
33
|
+
"Cis2-mock-ref",
|
|
34
|
+
"Cis2-mock-dev",
|
|
30
35
|
"Cis2-mock-sandbox",
|
|
31
36
|
"Cis2-mock-int",
|
|
32
37
|
"NHS-Login-mock-internal-dev",
|
|
38
|
+
"NHS-Login-mock-internal-dev-sandbox",
|
|
33
39
|
"NHS-Login-mock-internal-qa",
|
|
40
|
+
"NHS-Login-mock-internal-qa-sandbox",
|
|
41
|
+
"NHS-Login-mock-ref",
|
|
42
|
+
"NHS-Login-mock-dev",
|
|
34
43
|
"NHS-Login-mock-sandbox",
|
|
35
44
|
"NHS-Login-mock-int",
|
|
36
|
-
"
|
|
45
|
+
"api-producers", # just in case u want to get a cheeky token for proxygen ;)
|
|
37
46
|
]
|
|
38
47
|
|
|
39
48
|
@property
|
|
@@ -47,7 +56,8 @@ class KeycloakConfig(BaseModel):
|
|
|
47
56
|
class KeycloakUserConfig(KeycloakConfig):
|
|
48
57
|
client_id: str
|
|
49
58
|
client_secret: str
|
|
50
|
-
|
|
59
|
+
scope: str = "openid"
|
|
60
|
+
redirect_uri: HttpUrlString = "https://google.com"
|
|
51
61
|
login_form: dict
|
|
52
62
|
|
|
53
63
|
|
|
@@ -57,7 +67,7 @@ class AuthorizationCodeConfig(BaseModel):
|
|
|
57
67
|
def _identity_service_base_url(env):
|
|
58
68
|
prefix = "https://"
|
|
59
69
|
host = "api.service.nhs.uk"
|
|
60
|
-
path = "/oauth2-mock"
|
|
70
|
+
path = "/oauth2-mock"
|
|
61
71
|
if env != "prod":
|
|
62
72
|
prefix += f"{env}."
|
|
63
73
|
return f"{prefix}{host}{path}"
|
|
@@ -68,36 +78,23 @@ class AuthorizationCodeConfig(BaseModel):
|
|
|
68
78
|
"internal-dev-sandbox",
|
|
69
79
|
"internal-qa-sandbox",
|
|
70
80
|
"ref",
|
|
81
|
+
"sandbox",
|
|
82
|
+
"dev",
|
|
71
83
|
"int",
|
|
72
84
|
"prod",
|
|
73
85
|
] = "internal-dev"
|
|
74
86
|
org: Literal["nhsd-nonprod", "nhsd-prod"] = "nhsd-nonprod"
|
|
75
|
-
callback_url:
|
|
76
|
-
identity_service_base_url:
|
|
87
|
+
callback_url: HttpUrlString
|
|
88
|
+
identity_service_base_url: HttpUrlString = _identity_service_base_url(environment)
|
|
77
89
|
client_id: str
|
|
78
90
|
client_secret: str
|
|
79
91
|
scope: Literal["nhs-login", "nhs-cis2"]
|
|
80
92
|
login_form: dict
|
|
81
93
|
|
|
82
94
|
@validator("environment")
|
|
83
|
-
# The dream is to suport all the auth methods in all the environments
|
|
84
|
-
# however this is only true for client_credentials at the time of writing
|
|
85
|
-
# this library, we dont have at the moment a complete map between identity
|
|
86
|
-
# service deployment environments and keycloak realms so authorization_code
|
|
87
|
-
# and token_exchange are only supported in internal-dev, internal-qa and
|
|
88
|
-
# int. We use validators to handle this and when the moment comes we can
|
|
89
|
-
# just delete them.
|
|
90
95
|
def validate_environment(cls, environment):
|
|
91
|
-
if environment
|
|
92
|
-
"
|
|
93
|
-
"internal-qa-sandbox",
|
|
94
|
-
"sandbox",
|
|
95
|
-
"ref",
|
|
96
|
-
"prod",
|
|
97
|
-
]:
|
|
98
|
-
raise ValueError(
|
|
99
|
-
f"This is awkward.. we dont support auth_code flow for the {environment} just yet"
|
|
100
|
-
)
|
|
96
|
+
if environment == "prod":
|
|
97
|
+
raise ValueError(f"We dont support testing in the production environment")
|
|
101
98
|
return environment
|
|
102
99
|
|
|
103
100
|
|
|
@@ -118,6 +115,8 @@ class ClientCredentialsConfig(BaseModel):
|
|
|
118
115
|
"internal-dev-sandbox",
|
|
119
116
|
"internal-qa-sandbox",
|
|
120
117
|
"ref",
|
|
118
|
+
"dev",
|
|
119
|
+
"sandbox",
|
|
121
120
|
"int",
|
|
122
121
|
"prod",
|
|
123
122
|
] = "internal-dev"
|
|
@@ -125,7 +124,7 @@ class ClientCredentialsConfig(BaseModel):
|
|
|
125
124
|
client_id: str
|
|
126
125
|
jwt_private_key: str
|
|
127
126
|
jwt_kid: str
|
|
128
|
-
identity_service_base_url:
|
|
127
|
+
identity_service_base_url: HttpUrlString = _identity_service_base_url(environment)
|
|
129
128
|
|
|
130
129
|
def encode_jwt(self):
|
|
131
130
|
url = f"{self.identity_service_base_url}/token"
|
|
@@ -137,9 +136,7 @@ class ClientCredentialsConfig(BaseModel):
|
|
|
137
136
|
"exp": int(time()) + 300, # 5 minutes in the future
|
|
138
137
|
}
|
|
139
138
|
additional_headers = {"kid": self.jwt_kid}
|
|
140
|
-
client_assertion = jwt.encode(
|
|
141
|
-
claims, self.jwt_private_key, algorithm="RS512", headers=additional_headers
|
|
142
|
-
)
|
|
139
|
+
client_assertion = jwt.encode(claims, self.jwt_private_key, algorithm="RS512", headers=additional_headers)
|
|
143
140
|
return client_assertion
|
|
144
141
|
|
|
145
142
|
|
|
@@ -151,6 +148,7 @@ class TokenExchangeConfig(ClientCredentialsConfig):
|
|
|
151
148
|
class KeycloakSignedJWTConfig:
|
|
152
149
|
pass
|
|
153
150
|
|
|
151
|
+
|
|
154
152
|
# currently only targets AOS environment
|
|
155
153
|
class NHSLoginConfig(BaseModel):
|
|
156
154
|
"""Config needed to authenticate using NHS Login"""
|
|
@@ -160,14 +158,14 @@ class NHSLoginConfig(BaseModel):
|
|
|
160
158
|
self.nhs_login_base_url = openid_config["issuer"]
|
|
161
159
|
|
|
162
160
|
well_known_jwks: list = requests.get(openid_config["jwks_uri"]).json()
|
|
163
|
-
well_known_key = well_known_jwks[
|
|
161
|
+
well_known_key = well_known_jwks["keys"].pop()
|
|
164
162
|
self.jwt_kid = well_known_key["kid"]
|
|
165
163
|
self.alg = well_known_key["alg"]
|
|
166
164
|
|
|
167
165
|
super().__init__(**kwargs)
|
|
168
166
|
|
|
169
|
-
callback_url:
|
|
170
|
-
nhs_login_base_url:
|
|
167
|
+
callback_url: HttpUrlString = "https://nhsd-apim-testing-int-ns.herokuapp.com/nhslogin/callback"
|
|
168
|
+
nhs_login_base_url: HttpUrlString
|
|
171
169
|
client_id: str = "APIM-1"
|
|
172
170
|
jwt_private_key: str
|
|
173
171
|
jwt_kid: str
|
|
@@ -185,16 +183,14 @@ class NHSLoginConfig(BaseModel):
|
|
|
185
183
|
"exp": int(time()) + 300, # 5 minutes in the future
|
|
186
184
|
}
|
|
187
185
|
additional_headers = {"kid": self.jwt_kid}
|
|
188
|
-
client_assertion = jwt.encode(
|
|
189
|
-
claims, self.jwt_private_key, algorithm=self.alg, headers=additional_headers
|
|
190
|
-
)
|
|
186
|
+
client_assertion = jwt.encode(claims, self.jwt_private_key, algorithm=self.alg, headers=additional_headers)
|
|
191
187
|
return client_assertion
|
|
192
|
-
|
|
193
188
|
|
|
194
189
|
|
|
195
190
|
class BananaAuthenticatorConfig: # Placeholder
|
|
196
191
|
pass
|
|
197
192
|
|
|
193
|
+
|
|
198
194
|
### Authenticators
|
|
199
195
|
class Authenticator(ABC):
|
|
200
196
|
"""Defines the interface"""
|
|
@@ -264,9 +260,7 @@ class AuthorizationCodeAuthenticator(Authenticator):
|
|
|
264
260
|
},
|
|
265
261
|
)
|
|
266
262
|
if resp.status_code != 200:
|
|
267
|
-
raise RuntimeError(
|
|
268
|
-
f"{auth_url} request returned {resp.status_code}: {resp.text}"
|
|
269
|
-
)
|
|
263
|
+
raise RuntimeError(f"{auth_url} request returned {resp.status_code}: {resp.text}")
|
|
270
264
|
return resp
|
|
271
265
|
|
|
272
266
|
@staticmethod
|
|
@@ -300,9 +294,7 @@ class AuthorizationCodeAuthenticator(Authenticator):
|
|
|
300
294
|
form_submission_data,
|
|
301
295
|
):
|
|
302
296
|
form_submit_url = authorize_form.action or authorize_response.request.url
|
|
303
|
-
resp = session.request(
|
|
304
|
-
authorize_form.method, form_submit_url, data=form_submission_data
|
|
305
|
-
)
|
|
297
|
+
resp = session.request(authorize_form.method, form_submit_url, data=form_submission_data)
|
|
306
298
|
# TODO: Investigate why when using the fixtures it returns 404 and when
|
|
307
299
|
# using with external credentials returns 200
|
|
308
300
|
# if resp.status_code != 200:
|
|
@@ -313,9 +305,7 @@ class AuthorizationCodeAuthenticator(Authenticator):
|
|
|
313
305
|
|
|
314
306
|
@staticmethod
|
|
315
307
|
def _get_auth_code_from_mock_auth(response_identity_service_login):
|
|
316
|
-
qs = urlparse(
|
|
317
|
-
response_identity_service_login.history[-1].headers["Location"]
|
|
318
|
-
).query
|
|
308
|
+
qs = urlparse(response_identity_service_login.history[-1].headers["Location"]).query
|
|
319
309
|
auth_code = parse_qs(qs)["code"]
|
|
320
310
|
if isinstance(auth_code, list):
|
|
321
311
|
# in case there's multiple, this was a bug at one stage
|
|
@@ -337,16 +327,12 @@ class AuthorizationCodeAuthenticator(Authenticator):
|
|
|
337
327
|
self.config.scope,
|
|
338
328
|
)
|
|
339
329
|
|
|
340
|
-
authorize_form = self._get_authorization_form(
|
|
341
|
-
authorize_response.content.decode()
|
|
342
|
-
)
|
|
330
|
+
authorize_form = self._get_authorization_form(authorize_response.content.decode())
|
|
343
331
|
# 2. Parse the login page. For keycloak this presents an
|
|
344
332
|
# HTML form, which must be filled in with valid data. The tester
|
|
345
333
|
# can submits their login data with the `login_form` field.
|
|
346
334
|
|
|
347
|
-
form_submission_data = self._get_authorize_form_submission_data(
|
|
348
|
-
authorize_form, self.config.login_form
|
|
349
|
-
)
|
|
335
|
+
form_submission_data = self._get_authorize_form_submission_data(authorize_form, self.config.login_form)
|
|
350
336
|
|
|
351
337
|
# form_submission_data["username"] = 656005750104
|
|
352
338
|
# # And here we inject a valid mock username for keycloak.
|
|
@@ -423,7 +409,7 @@ class KeycloakUserAuthenticator(Authenticator):
|
|
|
423
409
|
params={
|
|
424
410
|
"response_type": "code",
|
|
425
411
|
"client_id": self.config.client_id,
|
|
426
|
-
"scope":
|
|
412
|
+
"scope": self.config.scope,
|
|
427
413
|
"redirect_uri": self.config.redirect_uri,
|
|
428
414
|
},
|
|
429
415
|
)
|
|
@@ -509,6 +495,7 @@ class NHSLoginSandpitAuthenticator(Authenticator):
|
|
|
509
495
|
|
|
510
496
|
class NHSLoginAosAuthenticator(Authenticator):
|
|
511
497
|
"""Authenticates you against NHS-Login aos environment"""
|
|
498
|
+
|
|
512
499
|
# This is only partially implemented. See below for usage:
|
|
513
500
|
# https://nhsd-confluence.digital.nhs.uk/display/APM/KOP-085+Generating+NHS+login+ID+tokens
|
|
514
501
|
|
pytest_nhsd_apim/secrets.py
CHANGED
|
@@ -22,12 +22,23 @@ _SESSION = requests.session()
|
|
|
22
22
|
|
|
23
23
|
@pytest.fixture()
|
|
24
24
|
@log_method
|
|
25
|
-
def _mock_jwks_api_key(
|
|
25
|
+
def _mock_jwks_api_key(
|
|
26
|
+
_apigee_app_base_url,
|
|
27
|
+
_apigee_edge_session,
|
|
28
|
+
test_app,
|
|
29
|
+
apigee_environment,
|
|
30
|
+
_test_app_id,
|
|
31
|
+
):
|
|
26
32
|
# Apps in prod Apigee shouldn't rely on mock-jwks for their api key
|
|
27
|
-
if apigee_environment in ["int", "prod"]:
|
|
33
|
+
if apigee_environment in ["dev", "sandbox", "int", "prod"]:
|
|
34
|
+
return ""
|
|
28
35
|
|
|
29
36
|
creds = get_app_credentials_for_product(
|
|
30
|
-
_apigee_app_base_url,
|
|
37
|
+
_apigee_app_base_url,
|
|
38
|
+
_apigee_edge_session,
|
|
39
|
+
test_app(),
|
|
40
|
+
f"mock-jwks-{apigee_environment}",
|
|
41
|
+
_test_app_id,
|
|
31
42
|
)
|
|
32
43
|
return creds["consumerKey"]
|
|
33
44
|
|
pytest_nhsd_apim/token_cache.py
CHANGED
|
@@ -20,7 +20,14 @@ class _TokenCache:
|
|
|
20
20
|
# only present on app-restricted tokens. we can inject
|
|
21
21
|
# this ourselves. Assume 5 seconds ago, probably was more
|
|
22
22
|
# recently
|
|
23
|
-
token_data["issued_at"] = time() - 5000
|
|
23
|
+
token_data["issued_at"] = (int(time()) * 1000) - 5000
|
|
24
|
+
|
|
25
|
+
if not self.is_time_in_milliseconds(token_data["issued_at"]):
|
|
26
|
+
token_data["issued_at"] = int(token_data["issued_at"]) * 1000
|
|
27
|
+
|
|
28
|
+
if not self.is_expiry_in_milliseconds(token_data["expires_in"]):
|
|
29
|
+
token_data["expires_in"] = int(token_data["expires_in"]) * 1000
|
|
30
|
+
|
|
24
31
|
self._cache[key] = token_data
|
|
25
32
|
|
|
26
33
|
@log_method
|
|
@@ -33,18 +40,24 @@ class _TokenCache:
|
|
|
33
40
|
|
|
34
41
|
old_token_data = self._cache[key]
|
|
35
42
|
grace_period_seconds = 5
|
|
36
|
-
now_ish = int(time()) + grace_period_seconds
|
|
43
|
+
now_ish = (int(time()) + grace_period_seconds) * 1000
|
|
37
44
|
|
|
38
45
|
# issued_at is epoch_time in milliseconds
|
|
39
46
|
# but expires_in is in seconds
|
|
40
47
|
# => need factor of 1000 in this sum.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
)
|
|
48
|
+
|
|
49
|
+
expiry_time = int(old_token_data["issued_at"]) + int(old_token_data["expires_in"])
|
|
44
50
|
if now_ish > expiry_time:
|
|
45
51
|
self._cache.pop(key)
|
|
46
52
|
return None
|
|
47
53
|
return old_token_data
|
|
54
|
+
|
|
55
|
+
def is_time_in_milliseconds(self, time_value):
|
|
56
|
+
return int(time_value) > 10**11 #Current time in secs not go over 11 digits
|
|
57
|
+
|
|
58
|
+
def is_expiry_in_milliseconds(self, expiry_value):
|
|
59
|
+
return int(expiry_value) > 86400 #Assuming seconds not go higher than 24hrs
|
|
60
|
+
|
|
48
61
|
|
|
49
62
|
|
|
50
63
|
_CACHES = {}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-nhsd-apim
|
|
3
|
-
Version:
|
|
3
|
+
Version: 5.0.15
|
|
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
|
|
@@ -11,18 +11,33 @@ License: MIT
|
|
|
11
11
|
Classifier: Framework :: Pytest
|
|
12
12
|
Requires-Python: >=3.8
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
|
-
Requires-Dist: Authlib
|
|
15
|
-
Requires-Dist: cryptography
|
|
16
|
-
Requires-Dist: lxml
|
|
17
|
-
Requires-Dist: pycryptodome
|
|
18
|
-
Requires-Dist: PyJWT
|
|
19
|
-
Requires-Dist: pyotp
|
|
20
|
-
Requires-Dist: pytest
|
|
21
|
-
Requires-Dist: requests
|
|
22
|
-
Requires-Dist: toml
|
|
23
|
-
Requires-Dist: typing-extensions
|
|
24
|
-
Requires-Dist: pydantic
|
|
25
|
-
Requires-Dist: wheel
|
|
14
|
+
Requires-Dist: Authlib==1.6.1
|
|
15
|
+
Requires-Dist: cryptography==44.0.1
|
|
16
|
+
Requires-Dist: lxml==5.3.1
|
|
17
|
+
Requires-Dist: pycryptodome==3.20.0
|
|
18
|
+
Requires-Dist: PyJWT==2.8.0
|
|
19
|
+
Requires-Dist: pyotp==2.9.0
|
|
20
|
+
Requires-Dist: pytest==8.2.0
|
|
21
|
+
Requires-Dist: requests==2.32.0
|
|
22
|
+
Requires-Dist: toml==0.10.2
|
|
23
|
+
Requires-Dist: typing-extensions==4.12.2
|
|
24
|
+
Requires-Dist: pydantic==2.9.2
|
|
25
|
+
Requires-Dist: wheel<0.45.0,===0.37.1
|
|
26
|
+
Requires-Dist: pydantic-settings==2.2.1
|
|
27
|
+
Requires-Dist: setuptools==80.0.1
|
|
28
|
+
Requires-Dist: urllib3==2.6.1
|
|
29
|
+
Dynamic: author
|
|
30
|
+
Dynamic: author-email
|
|
31
|
+
Dynamic: classifier
|
|
32
|
+
Dynamic: description
|
|
33
|
+
Dynamic: description-content-type
|
|
34
|
+
Dynamic: home-page
|
|
35
|
+
Dynamic: license
|
|
36
|
+
Dynamic: maintainer
|
|
37
|
+
Dynamic: maintainer-email
|
|
38
|
+
Dynamic: requires-dist
|
|
39
|
+
Dynamic: requires-python
|
|
40
|
+
Dynamic: summary
|
|
26
41
|
|
|
27
42
|
# pytest-nhsd-apim
|
|
28
43
|
|
|
@@ -99,6 +114,7 @@ The APIs we offer at the moment are:
|
|
|
99
114
|
| ApiProductsAPI | [here](/src/pytest_nhsd_apim/apigee_apis.py#L575) |[Overview](https://apidocs.apigee.com/docs/api-products/1/overview)|
|
|
100
115
|
| DebugSessionsAPI | [here](/src/pytest_nhsd_apim/apigee_apis.py#L844) |[Overview](https://apidocs.apigee.com/docs/debug-sessions/1/overview)|
|
|
101
116
|
| AccessTokensAPI | [here](/src/pytest_nhsd_apim/apigee_apis.py#L983) |[Overview](https://apidocs.apigee.com/docs/oauth-20-access-tokens/1/overview)|
|
|
117
|
+
| AppKeysAPI | [here](/src/pytest_nhsd_apim/apigee_apis.py#L1243) |[Overview](https://apidocs.apigee.com/docs/developer-app-keys/1/overview)|
|
|
102
118
|
|
|
103
119
|
For a more detailed implementation of the available APIs please refer to the tests [here](/tests/test_apigee_apis.py).
|
|
104
120
|
We will keep adding APIs with time, if you are looking for a particular APIs not listed above please feel free to open a pull request and send it to us.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
pytest_nhsd_apim/__init__.py,sha256=CRwQfUDrbXIqvJX6OfTR4jGdhlU9kjGmBAptJasRg7E,72
|
|
2
|
+
pytest_nhsd_apim/apigee_apis.py,sha256=u_sHjISj4XKgidVje1Uzq4LblMGnX4lrrrMWZM-SEF4,67289
|
|
3
|
+
pytest_nhsd_apim/apigee_edge.py,sha256=9Vtad7LGyNAnRoSj9I-1T6mylgRlDF88ISanRAjpU5c,19076
|
|
4
|
+
pytest_nhsd_apim/auth_journey.py,sha256=UovbLXhokUnikrMOaXIhjV8t5aRrcxinAbS96nfZWcY,5154
|
|
5
|
+
pytest_nhsd_apim/config.py,sha256=ScKfV8iURqDXX2ajgGsRDcVn9RZy2DxLoEU2QQt9lmA,4246
|
|
6
|
+
pytest_nhsd_apim/identity_service.py,sha256=1SYR8yY66XTygF6jr3dUD22lAMnz6IImMaDQtm5YwXg,19749
|
|
7
|
+
pytest_nhsd_apim/log.py,sha256=8gYHqzlQM546FB2XvFmLTk1YfZRNeNhIwLmOy0GScr8,2685
|
|
8
|
+
pytest_nhsd_apim/nhsd_apim_authorization.py,sha256=GR8GfbIZyqBC4jsSZMYNifDH52E3VWoIa7lrpuvIbaM,3513
|
|
9
|
+
pytest_nhsd_apim/pytest_nhsd_apim.py,sha256=ZCItUqcM23CCmcRyGU8bEwI3vJnNcGdoOlbSfvYILR8,5242
|
|
10
|
+
pytest_nhsd_apim/secrets.py,sha256=yIYwOZwpliIomtqSJGIYRbAE2HYvLvQU4W2kOa9TnXo,2354
|
|
11
|
+
pytest_nhsd_apim/token_cache.py,sha256=u22VEoHxGkpOzPjsCOHFSW-5aM5zlj2uejwV5mB5Lbo,4140
|
|
12
|
+
pytest_nhsd_apim-5.0.15.dist-info/METADATA,sha256=VnNWHfWkNLapZA3g4RZWmxocZ82v3buj4MRY0nYaB_4,4990
|
|
13
|
+
pytest_nhsd_apim-5.0.15.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
14
|
+
pytest_nhsd_apim-5.0.15.dist-info/entry_points.txt,sha256=XWicT3meTpqLXnZcXNoAd2IfXspFPlNgMgLBMy4nqwQ,57
|
|
15
|
+
pytest_nhsd_apim-5.0.15.dist-info/top_level.txt,sha256=ZK5GZP-g_K8gNfm4a58T9JCRb0i1X267ngvNelCGgfQ,17
|
|
16
|
+
pytest_nhsd_apim-5.0.15.dist-info/RECORD,,
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
pytest_nhsd_apim/__init__.py,sha256=CRwQfUDrbXIqvJX6OfTR4jGdhlU9kjGmBAptJasRg7E,72
|
|
2
|
-
pytest_nhsd_apim/apigee_apis.py,sha256=b1Ay1MBCdlcy3-QoQhfeFwnGkY2bmYAXJhEzCO1ZK54,57430
|
|
3
|
-
pytest_nhsd_apim/apigee_edge.py,sha256=a7hXBgm4hmUyyzrxJkWapsbWVBVGQAB1r9Mdnyw9-GU,18675
|
|
4
|
-
pytest_nhsd_apim/auth_journey.py,sha256=UovbLXhokUnikrMOaXIhjV8t5aRrcxinAbS96nfZWcY,5154
|
|
5
|
-
pytest_nhsd_apim/config.py,sha256=ScKfV8iURqDXX2ajgGsRDcVn9RZy2DxLoEU2QQt9lmA,4246
|
|
6
|
-
pytest_nhsd_apim/identity_service.py,sha256=tFpu20oiOVj2Nb78mGKfwDchbc-mUX6x0DLgUozy190,20102
|
|
7
|
-
pytest_nhsd_apim/log.py,sha256=8gYHqzlQM546FB2XvFmLTk1YfZRNeNhIwLmOy0GScr8,2685
|
|
8
|
-
pytest_nhsd_apim/nhsd_apim_authorization.py,sha256=TE_6YnoM7kpRReiZP6-Z52rRs9bUuax1mpXqkbzg8zQ,3505
|
|
9
|
-
pytest_nhsd_apim/pytest_nhsd_apim.py,sha256=_snJGTUVsHA70FGrKNNXjx9yNc_UjO1Ffhw0SLQtYUs,5214
|
|
10
|
-
pytest_nhsd_apim/secrets.py,sha256=ZoTMRQGElAq4NNOx6FelINKo_YVNBWdgDY8x3bs_Zr0,2272
|
|
11
|
-
pytest_nhsd_apim/token_cache.py,sha256=6L08taTlSyRsx2NCb0LSGsHdWx_wmqwlFtcF7pZMhUg,3540
|
|
12
|
-
pytest_nhsd_apim-3.3.6.dist-info/METADATA,sha256=mKf7s7BQ4TuHv-imklQwzzYNuA7TqCdJ4Lu2QzoZmgw,4526
|
|
13
|
-
pytest_nhsd_apim-3.3.6.dist-info/WHEEL,sha256=2wepM1nk4DS4eFpYrW1TTqPcoGNfHhhO_i5m4cOimbo,92
|
|
14
|
-
pytest_nhsd_apim-3.3.6.dist-info/entry_points.txt,sha256=XWicT3meTpqLXnZcXNoAd2IfXspFPlNgMgLBMy4nqwQ,57
|
|
15
|
-
pytest_nhsd_apim-3.3.6.dist-info/top_level.txt,sha256=ZK5GZP-g_K8gNfm4a58T9JCRb0i1X267ngvNelCGgfQ,17
|
|
16
|
-
pytest_nhsd_apim-3.3.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|