Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[pytest]
pythonpath =
tests/e2e-tests
1 change: 1 addition & 0 deletions scripts/init.mk
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ _install-dependency: # Install asdf dependency - mandatory: name=[listed in the
_install-dependencies: # Install all the dependencies listed in .tool-versions
for plugin in $$(grep '^[a-z]' .tool-versions | cut -f1 -d' '); do \
echo "Installing $${plugin}..."; \
$(MAKE) _install-dependency name=$${plugin}; \
done

clean:: # Remove all generated and temporary files (common) @Operations
Expand Down
3 changes: 3 additions & 0 deletions tests/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ node_modules/
/target
/playwright/.auth/
/allure-report
*/__pycache__/
**/__pycache__/
*.pyc
1 change: 1 addition & 0 deletions tests/e2e-tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# E2E Tests
Empty file.
33 changes: 33 additions & 0 deletions tests/e2e-tests/api/authentication/test-401-errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import requests
import pytest
from lib.constants import METHODS, VALID_ENDPOINTS
from lib.fixtures import *

@pytest.mark.test
@pytest.mark.devtest
@pytest.mark.inttest
@pytest.mark.prodtest
@pytest.mark.parametrize("method", METHODS)
@pytest.mark.parametrize("endpoints", VALID_ENDPOINTS)
def test_401_invalid(url, method, endpoints):

resp = getattr(requests, method)(f"{url}{endpoints}", headers={
"Authorization": "invalid",
})

assert(resp.status_code == 401)

@pytest.mark.test
@pytest.mark.nhsd_apim_authorization({"access": "application", "level": "level0"})
@pytest.mark.parametrize("method", METHODS)
@pytest.mark.parametrize("endpoints", VALID_ENDPOINTS)
def test_401_invalid_level(nhsd_apim_proxy_url, nhsd_apim_auth_headers, method, endpoints):
print(nhsd_apim_proxy_url)

resp = getattr(requests, method)(f"{nhsd_apim_proxy_url}{endpoints}", headers={
**nhsd_apim_auth_headers
})

print("Status:", resp.status_code)

assert(resp.status_code == 401)
22 changes: 22 additions & 0 deletions tests/e2e-tests/api/authentication/test-403-errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import requests
import pytest
from lib.constants import VALID_ENDPOINTS

METHODS = ["get", "post"]
CORRELATION_IDS = [None, "76491414-d0cf-4655-ae20-a4d1368472f3"]


@pytest.mark.test
@pytest.mark.nhsd_apim_authorization({"access": "application", "level": "level0"})
@pytest.mark.parametrize("method", METHODS)
@pytest.mark.parametrize("endpoints", VALID_ENDPOINTS)
def test_user_token_get(nhsd_apim_proxy_url, nhsd_apim_auth_headers, method, endpoints):
print(nhsd_apim_proxy_url)

resp = getattr(requests, method)(f"{nhsd_apim_proxy_url}{endpoints}", headers={
**nhsd_apim_auth_headers
})

print("Status:", resp.status_code)

assert(resp.status_code == 401)
39 changes: 39 additions & 0 deletions tests/e2e-tests/api/endpoint_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@

import pytest
import requests

def _get(url, headers=None, timeout=10):
return requests.get(url, headers=headers or {}, timeout=timeout)

@pytest.mark.smoketest
def test_ping(nhsd_apim_proxy_url):
resp = requests.get(nhsd_apim_proxy_url + "/_ping")
assert resp.status_code == 200

@pytest.mark.smoketest
def test_401_status_without_api_key(nhsd_apim_proxy_url):
resp = requests.get(
f"{nhsd_apim_proxy_url}/_status"
)
assert resp.status_code == 401

@pytest.mark.smoketest
@pytest.mark.nhsd_apim_authorization(access="application", level="level3")
def test_invalid_jwt_rejected(nhsd_apim_proxy_url, nhsd_apim_auth_headers):
"""
Best-effort: if gateway validates JWTs, an invalid token should be rejected.
If JWT not used in this env, test is skipped.
"""
headers = {
**nhsd_apim_auth_headers,
"x-request-id": "123456"
}

# If no Authorization configured in project headers, skip
if "Authorization" not in headers:
pytest.skip("JWT auth not configured for this environment")

bad_headers = dict(headers)
bad_headers["Authorization"] = "Bearer invalid.invalid.invalid"
status = _get(f"{nhsd_apim_proxy_url}/_status", headers=bad_headers).status_code
assert status in (401, 403), "Expected gateway to reject invalid JWT"
31 changes: 31 additions & 0 deletions tests/e2e-tests/api/get_letters_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

import pytest
import requests
from lib import generators as Generators

from pytest_nhsd_apim.apigee_apis import (
DeveloperAppsAPI,
ApigeeClient,
ApigeeNonProdCredentials,
)

@pytest.fixture()
def client():
config = ApigeeNonProdCredentials()
return ApigeeClient(config=config)

@pytest.mark.nhsd_apim_authorization(access="application", level="level3")
def test_app_level0_access_post(nhsd_apim_proxy_url, nhsd_apim_auth_headers, _create_test_app, client: ApigeeClient ):
headers = {
**nhsd_apim_auth_headers,
"x-request-id": "123456"
}

data = Generators.generate_valid_create_message_body("sandbox")
print(data);

resp = requests.post(
f"{nhsd_apim_proxy_url + "/letters/:id"}", headers=headers,
json = data
)
assert resp.status_code == 200
Empty file.
2 changes: 2 additions & 0 deletions tests/e2e-tests/lib/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
VALID_ENDPOINTS = ["/letters"]
METHODS = ["get", "post"]
42 changes: 42 additions & 0 deletions tests/e2e-tests/lib/fixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import pytest
import os
import re
# for now this is the same as PROXY_NAME
# this is here to illustrate how these can be decoupled
@pytest.fixture(scope='session')
def api_product_name():
api_proxy = os.environ.get("API_PROXY")
proxy_name = os.environ.get("PROXY_NAME")

if api_proxy:
return api_proxy

if proxy_name:
return proxy_name

raise RuntimeError(
"Neither API_PROXY nor PROXY_NAME is set in the environment.\n"
"You must set at least one of them.\n"
"Example:\n"
"export API_PROXY=nhs-notify-supplier--internal-dev--nhs-notify-supplier")

@pytest.fixture(scope='session')
def url(api_product_name):
# Extract the last part after "--"
# Examples:
# nhs-notify-supplier--internal-dev--nhs-notify-supplier → nhs-notify-supplier
# nhs-notify-supplier--internal-dev--nhs-notify-supplier-PR-277 → nhs-notify-supplier-PR-277
suffix = api_product_name.split("--")[-1]

environment = os.environ["API_ENVIRONMENT"]
# Production uses the standard live URL pattern
if environment == "prod":
return f"https://api.service.nhs.uk/{suffix}"

# REF / REF2 share internal-dev gateway
elif environment in ["ref", "ref2"]:
return f"https://internal-dev.api.service.nhs.uk/{suffix}"

# Everything else (dev, test, pr environments, internal-dev)
else:
return f"https://{environment}.api.service.nhs.uk/{suffix}"
12 changes: 12 additions & 0 deletions tests/e2e-tests/lib/generators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class Generators:
@staticmethod
def generate_valid_create_message_body(environment="sandbox"):
return {
"data": {
"attributes": {
"status": "PENDING"
},
"id": "2WL5eYSWGzCHlGmzNxuqVusPxDg",
"type": "Letter"
}
}
13 changes: 13 additions & 0 deletions tests/e2e-tests/partials/authentication/test_401_invalid.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Scenario: An API consumer submitting a request with an invalid authorization token receives a 401 'Access Denied' response
==========================================================================================================================

| **Given** the API consumer provides an invalid authorization token
| **When** the request is submitted
| **Then** the response is a 401 access denied error


**Asserts**
- Response returns a 401 'Access Denied' error
- Response returns the expected error message body

.. include:: /partials/methods.rst
16 changes: 16 additions & 0 deletions tests/e2e-tests/partials/methods.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
**Methods**

This test makes use of different HTTP methods, if the method is either HEAD or OPTIONS the test will not assert against the body of the response as none is returned.

.. list-table::
:widths: 50
:header-rows: 7

* - Value
* - GET
* - POST
* - PUT
* - PATCH
* - DELETE
* - HEAD
* - OPTIONS
Loading
Loading