async def test_api_slow_supplies_content_location( api_client: APISessionClient): async with api_client.get("slow?complete_in=0.01&final_status=418") as r: assert 'application/json' in r.headers.get('Content-Type') poll_location = r.headers.get('Content-Location') assert r.status == 202, (r.status, r.reason, (await r.text())[:2000]) async with api_client.get(poll_location) as r: poll_count = r.cookies.get('poll-count') assert poll_count.value == '1' assert r.status == 418, (r.status, r.reason, (await r.text())[:2000])
async def test_token_exchange_happy_path(test_app, api_client: APISessionClient): subject_token_claims = { 'identity_proofing_level': test_app.request_params.get('identity_proofing_level') } token_response = await conftest.get_token_nhs_login_token_exchange( test_app, subject_token_claims=subject_token_claims ) token = token_response["access_token"] correlation_id = str(uuid4()) headers = { "Authorization": f"Bearer {token}", "X-Correlation-ID": correlation_id } async with api_client.get( _base_valid_uri("9999999990"), headers=headers, allow_retries=True ) as resp: assert resp.status == 200, 'failed getting backend data' body = await resp.json() assert "x-correlation-id" in resp.headers, resp.headers assert resp.headers["x-correlation-id"] == correlation_id assert_body(body)
async def api_client(api_test_config: APITestSessionConfig): session_client = APISessionClient(api_test_config.base_uri) yield session_client await session_client.close()
async def set_custom_attributes(self, attributes: dict) -> dict: """ Replaces the current list of attributes with the attributes specified """ custom_attributes = [{"name": "DisplayName", "value": self.name}] for key, value in attributes.items(): custom_attributes.append({"name": key, "value": value}) params = self.default_params.copy() params['name'] = self.name async with APISessionClient(self.app_base_uri) as session: async with session.post(f"apps/{self.name}/attributes", params=params, headers=self.headers, json={"attribute": custom_attributes}) as resp: body = await resp.json() if resp.status != 200: headers = dict(resp.headers.items()) throw_friendly_error( message= f"unable to add custom attributes {attributes} to app: " f"{self.name}", url=resp.url, status_code=resp.status, response=body, headers=headers) return body['attribute']
async def test_wait_for_status(api_client: APISessionClient, api_test_config: APITestSessionConfig): async def is_deployed(resp: ClientResponse): if resp.status != 200: return False body = await resp.json() if body.get("commitId") != api_test_config.commit_id: return False backend = dict_path(body, ["checks", "healthcheck", "outcome", "version"]) if not backend: return True return backend.get("commitId") == api_test_config.commit_id deploy_timeout = 120 if api_test_config.api_environment.endswith( "sandbox") else 30 await poll_until( make_request=lambda: api_client.get( "_status", headers={"apikey": env.status_endpoint_api_key()}), until=is_deployed, timeout=deploy_timeout, )
async def test_token_exchange_both_header_and_exchange(api_client: APISessionClient, test_product_and_app): test_product, test_app = test_product_and_app subject_token_claims = { 'identity_proofing_level': test_app.request_params.get('identity_proofing_level') } authorised_headers = {} correlation_id = str(uuid4()) authorised_headers["X-Correlation-ID"] = correlation_id authorised_headers["NHSD-User-Identity"] = conftest.nhs_login_id_token(test_app) # Use token exchange token in conjunction with JWT header token_response = await conftest.get_token_nhs_login_token_exchange( test_app, subject_token_claims=subject_token_claims ) token = token_response["access_token"] authorised_headers["Authorization"] = f"Bearer {token}" async with api_client.get( _base_valid_uri("9999999990"), headers=authorised_headers, allow_retries=True ) as resp: assert resp.status == 200 body = await resp.json() assert "x-correlation-id" in resp.headers, resp.headers assert resp.headers["x-correlation-id"] == correlation_id assert_body(body)
async def test_user_restricted_access_not_permitted( api_client: APISessionClient, test_product_and_app): await asyncio.sleep(1 ) # Add delay to tests to avoid 429 on service callout test_product, test_app = test_product_and_app await test_product.update_scopes( ["urn:nhsd:apim:user-nhs-id:aal3:immunisation-history"]) await test_app.add_api_product([test_product.name]) token_response = await conftest.get_token(test_app) authorised_headers = { "Authorization": f"Bearer {token_response['access_token']}", "NHSD-User-Identity": conftest.nhs_login_id_token(test_app) } async with api_client.get(_valid_uri("9912003888", "90640007"), headers=authorised_headers, allow_retries=True) as resp: assert resp.status == 401 body = await resp.json() assert body["resourceType"] == "OperationOutcome" assert body["issue"][0]["severity"] == "error" assert body["issue"][0][ "diagnostics"] == "Provided access token is invalid" assert body["issue"][0]["code"] == "forbidden"
async def _get_state(self, request_state: str) -> str: """Send an authorize request and retrieve the state""" params = { "client_id": self.client_id, "redirect_uri": self.redirect_uri, "response_type": "code", "state": request_state, } async with APISessionClient(self.base_uri) as session: async with session.get("authorize", params=params) as resp: body = await resp.read() if resp.status != 200: headers = dict(resp.headers.items()) throw_friendly_error( message= "unexpected response, unable to authenticate with simulated oauth", url=resp.url, status_code=resp.status, response=body, headers=headers, ) state = dict(resp.url.query)["state"] # Confirm state is converted to a cryptographic value assert state != request_state return state
async def test_retry_request_varying_responses(status_codes, max_retries, expected_response): async with APISessionClient("https://httpbin.org") as session: mock_status_list = map(mock_response, status_codes) requester = MockRequest(mock_status_list) resp = await session._retry_requests(requester, max_retries) # pylint: disable=W0212 assert resp.status == expected_response
async def test_token_exchange_both_header_and_exchange( api_client: APISessionClient, test_product_and_app, authorised_headers): test_product, test_app = test_product_and_app correlation_id = str(uuid4()) authorised_headers["X-Correlation-ID"] = correlation_id authorised_headers["NHSD-User-Identity"] = conftest.nhs_login_id_token( test_app) # Use token exchange token in conjunction with JWT header token_response = await conftest.get_token_nhs_login_token_exchange(test_app ) token = token_response["access_token"] authorised_headers["Authorization"] = f"Bearer {token}" async with api_client.get(_valid_uri("9912003888", "90640007"), headers=authorised_headers, allow_retries=True) as resp: assert resp.status == 200 body = await resp.json() assert "x-correlation-id" in resp.headers, resp.headers assert resp.headers["x-correlation-id"] == correlation_id assert body["resourceType"] == "Bundle", body # no data for this nhs number ... assert len(body["entry"]) == 0, body
async def add_api_product(self, api_products: list) -> dict: """ Add a number of API Products to the app """ params = self.default_params.copy() params['name'] = self.name data = { "apiProducts": api_products, "name": self.name, "status": "approved" } async with APISessionClient(self.app_base_uri) as session: async with session.put(f"apps/{self.name}/keys/{self.client_id}", params=params, headers=self.headers, json=data) as resp: body = await resp.json() if resp.status != 200: headers = dict(resp.headers.items()) throw_friendly_error( message= f"unable to add api products {api_products} to app: " f"{self.name}", url=resp.url, status_code=resp.status, response=body, headers=headers) return body['apiProducts']
async def test_p5_token_exchange_with_allowed_proofing_level( api_client: APISessionClient, test_product_and_app): test_product, test_app = test_product_and_app await _set_app_allowed_proofing_level(test_app, 'P5') token_response = await conftest.get_token_nhs_login_token_exchange( test_app, subject_token_claims={"identity_proofing_level": "P5"}) token = token_response["access_token"] correlation_id = str(uuid4()) headers = { "Authorization": f"Bearer {token}", "X-Correlation-ID": correlation_id, } async with api_client.get(_valid_uri("9912003888", "90640007"), headers=headers, allow_retries=True) as resp: assert resp.status == 200 body = await resp.json() assert body["resourceType"] == "Bundle", body # no data for this nhs number ... assert len(body["entry"]) == 0, body
async def test_immunisation_id_token_error_scenarios( test_app, api_client: APISessionClient, authorised_headers, request_data: dict): await asyncio.sleep(1 ) # Add delay to tests to avoid 429 on service callout id_token = conftest.nhs_login_id_token( test_app=test_app, id_token_claims=request_data.get("claims"), id_token_headers=request_data.get("headers")) if request_data.get("id_token") is not None: authorised_headers["NHSD-User-Identity"] = request_data.get("id_token") else: authorised_headers["NHSD-User-Identity"] = id_token async with api_client.get(_valid_uri("9912003888", "90640007"), headers=authorised_headers, allow_retries=True) as resp: assert resp.status == request_data["expected_status_code"] body = await resp.json() assert body["resourceType"] == "OperationOutcome" assert body["issue"][0]["severity"] == request_data[ "expected_response"]["severity"] assert body["issue"][0]["diagnostics"] == request_data[ "expected_response"]["error_diagnostics"] assert body["issue"][0]["code"] == request_data["expected_response"][ "error_code"]
async def update_custom_attribute(self, attribute_name: str, attribute_value: str) -> dict: """ Update an existing custom attribute """ params = self.default_params.copy() params["name"] = self.name params["attribute_name"] = attribute_name data = {"value": attribute_value} async with APISessionClient(self.app_base_uri) as session: async with session.post( f"apps/{self.name}/attributes/{attribute_name}", params=params, headers=self.headers, json=data) as resp: body = await resp.json() if resp.status != 200: headers = dict(resp.headers.items()) throw_friendly_error( message= f"unable to add custom attribute for app: {self.name}", url=resp.url, status_code=resp.status, response=body, headers=headers) return body
async def hit_oauth_endpoint(self, method: str, endpoint: str, **kwargs) -> dict: """Send a request to a OAuth endpoint""" async with APISessionClient(self.base_uri) as session: request_method = (session.post, session.get)[method.lower().strip() == 'get'] resp = await self._retry_requests( lambda: request_method(endpoint, **kwargs), 5) try: body = await resp.json() _ = body.pop( 'message_id', None ) # Remove the unique message id if the response is na error except ContentTypeError: # Might be html or text response body = await resp.read() if isinstance(body, bytes): # Convert into a string body = str(body, "UTF-8") try: # In case json response was of type bytes body = literal_eval(body) except SyntaxError: # Continue pass return { 'method': resp.method, 'url': resp.url, 'status_code': resp.status, 'body': body, 'headers': dict(resp.headers.items()), 'history': resp.history }
async def get_trace_data(self) -> dict or None: if not self.revision: raise RuntimeError( "You must run start_trace() before you can run get_raw_trace()" ) await self._set_transaction_id() if not self.transaction_id: return None async with APISessionClient(self.base_uri) as session: async with session.post( f"environments/{self.env}/apis/{self.proxy}/revisions/{self.revision}/" f"debugsessions/{self.name}/data/{self.transaction_id}", headers=self.headers) as resp: body = await resp.read() if resp.status != 201: headers = dict(resp.headers.items()) throw_friendly_error( message= f"unable to get trace data for session {self.name} " f"on proxy {self.proxy}", url=resp.url, status_code=resp.status, response=body, headers=headers) return body
async def test_retry_request_varying_error(): async with APISessionClient("https://httpbin.org") as session: mock_status_list = map(mock_response, [429, 429, 503]) requester = MockRequest(mock_status_list) with pytest.raises(TimeoutError) as excinfo: await session._retry_requests(requester, max_retries=3) # pylint: disable=W0212 error = excinfo.value assert error == "Maximum retry limit hit."
async def test_api_slow_supplies_content_location( api_client: APISessionClient, api_test_config: APITestSessionConfig): async with api_client.get("slow") as r: assert r.status == 202, (r.status, r.reason, (await r.text())[:2000]) assert r.headers.get('Content-Type') == 'application/json' assert r.headers.get('Content-Location').startswith( api_test_config.base_uri + '/poll?') assert r.cookies.get('poll-count') == '0'
async def test_max_retries_limit(endpoint, should_retry, expected_error): async with APISessionClient("https://httpbin.org") as session: with pytest.raises(TimeoutError) as excinfo: await session.get(endpoint, allow_retries=should_retry, max_retries=3) error = excinfo.value assert expected_error in str(error)
async def test_postman_echo_send_multivalue_headers(): async with APISessionClient("http://postman-echo.com") as session: async with session.get("headers", headers=[("foo1", "bar1"), ("foo1", "bar2")]) as resp: assert resp.status == 200 body = await resp.json() assert body["headers"]["foo1"] == "bar1, bar2"
async def test_fixture_postman_echo_send_multivalue_headers( api_client: APISessionClient): async with api_client.get("headers", headers=[("foo1", "bar1"), ("foo1", "bar2")]) as resp: assert resp.status == 200 body = await resp.json() assert body["headers"]["foo1"] == "bar1, bar2"
async def test_wait_for_poll_does_timeout(api_client: APISessionClient): with pytest.raises(PollTimeoutError) as exec_info: await poll_until(lambda: api_client.get('status/404'), timeout=1, sleep_for=0.3) error = exec_info.value # type: PollTimeoutError assert len(error.responses) > 0 assert error.responses[0][0] == 404
async def test_wait_for_200_json_deflate(api_client: APISessionClient): responses = await poll_until(lambda: api_client.get('deflate'), timeout=5) assert len(responses) == 1 status, headers, body = responses[0] assert status == 200 assert headers.get('Content-Type').split(';')[0] == 'application/json' assert body['deflated'] is True
async def authenticate(self, request_state: str = str(uuid4())) -> str: """Authenticate and retrieve the code value""" state = await self._get_state(request_state) params = { "response_type": "code", "client_id": self.client_id, "redirect_uri": self.redirect_uri, "scope": "openid", "state": state } headers = {"Content-Type": "application/x-www-form-urlencoded"} payload = {"state": state} async with APISessionClient(self.base_uri) as session: async with session.post("simulated_auth", params=params, data=payload, headers=headers, allow_redirects=False) as resp: if resp.status != 302: body = await resp.json() headers = dict(resp.headers.items()) throw_friendly_error( message= "unexpected response, unable to authenticate with simulated oauth", url=resp.url, status_code=resp.status, response=body, headers=headers) redirect_uri = resp.headers['Location'][ resp.headers['Location'].index('callback'):] async with session.get(redirect_uri, allow_redirects=False) as callback_resp: headers = dict(callback_resp.headers.items()) # Confirm request was successful if callback_resp.status != 302: body = await resp.read() throw_friendly_error( message= "unexpected response, unable to authenticate with simulated oauth", url=resp.url, status_code=resp.status, response=body, headers=headers) # Get code value from location parameters query = headers['Location'].split("?")[1] params = { x[0]: x[1] for x in [x.split("=") for x in query.split("&")] } return params['code']
async def test_wait_for_200_json(api_client: APISessionClient): responses = await poll_until(lambda: api_client.get('json'), timeout=5) assert len(responses) == 1 status, headers, body = responses[0] assert status == 200 assert headers.get('Content-Type').split(';')[0] == 'application/json' assert body['slideshow']['title'] == 'Sample Slide Show'
async def test_wait_for_200_html(api_client: APISessionClient): responses = await poll_until(lambda: api_client.get('html'), timeout=5) assert len(responses) == 1 status, headers, body = responses[0] assert status == 200 assert headers.get('Content-Type').split(';')[0] == 'text/html' assert isinstance(body, str) assert body.startswith('<!DOCTYPE html>')
async def test_wait_for_ping(api_client: APISessionClient, api_test_config: APITestSessionConfig): async def _is_complete(resp: ClientResponse): if resp.status != 200: return False body = await resp.json() return body.get("commitId") == api_test_config.commit_id await poll_until(make_request=lambda: api_client.get('_ping'), until=_is_complete, timeout=120)
async def test_wait_for_ping(api_client: APISessionClient, api_test_config: APITestSessionConfig): async def apigee_deployed(resp: ClientResponse): if resp.status != 200: return False body = await resp.json() return body.get("commitId") == api_test_config.commit_id await poll_until(make_request=lambda: api_client.get("_ping"), until=apigee_deployed, timeout=30)
async def test_fixture_override_http_bin_post(api_client: APISessionClient): data = {'test': 'data'} async with api_client.post("post", json=data) as resp: assert resp.status == 200 body = await resp.json() assert body['headers'].get('Host') == 'httpbin.org' assert body['headers'].get('Content-Type') == 'application/json' assert body['data'] == json.dumps(data)
async def test_wait_for_ping(api_client: APISessionClient, api_test_config: APITestSessionConfig): """ test for _ping .. this uses poll_until to wait until the correct SOURCE_COMMIT_ID ( from env var ) is available """ is_deployed = partial(_is_ping_deployed, api_test_config=api_test_config) await poll_until(make_request=lambda: api_client.get('_ping'), until=is_deployed, timeout=120)