Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5197486
[GPCAPIM-351]: Add framework for contract tests for ProviderStub class
DWolfsNHS Apr 10, 2026
cf8f639
[GPCAPIM-351]: Enhance contract tests for ProviderStub class
DWolfsNHS Apr 10, 2026
6b8e9c5
[GPCAPIM-351]: Add validation error tests for structured record endpoint
DWolfsNHS Apr 10, 2026
3162986
[GPCAPIM-351]: Add header validation error tests for structured recor…
DWolfsNHS Apr 13, 2026
4aa5e29
[GPCAPIM-351]: Enhance diagnostics validation for authorization header
DWolfsNHS Apr 13, 2026
8a46f67
[GPCAPIM-351]: Update formatting guidelines and vocabulary
DWolfsNHS Apr 13, 2026
d8d7715
[GPCAPIM-351]: Update cSpell configuration to exclude generated artef…
DWolfsNHS Apr 13, 2026
09d0b54
[GPCAPIM-351]: Update cSpell dictionary and configuration
DWolfsNHS Apr 13, 2026
0b3b816
[GPCAPIM-351]: Refactor practitioner validation logic
DWolfsNHS Apr 13, 2026
b377bcc
[GPCAPIM-351]: Update .gitignore to include pact output
DWolfsNHS Apr 13, 2026
4b8d9cc
[GPCAPIM-351]: Remove local contract test pact output from .gitignore
DWolfsNHS Apr 13, 2026
2c2da40
[GPCAPIM-351]: Remove trace_id validation from access_record_structured
DWolfsNHS Apr 13, 2026
38b5940
[GPCAPIM-351]: Update validation logic for structured record requests
DWolfsNHS Apr 13, 2026
34129c0
[GPCAPIM-351]: address docker warning by removing empty line in the R…
DWolfsNHS Apr 14, 2026
3a0edbd
[GPCAPIM-351]: Refactor GpProviderStub to implement PostStub interface
DWolfsNHS Apr 14, 2026
d60d323
Merge branch 'main' into feature/GPCAPIM-351_tests_for_GPProvider_stub
DWolfsNHS Apr 14, 2026
0e96f61
[GPCAPIM-351]: Update PostStub documentation and add tests for GpProv…
DWolfsNHS Apr 14, 2026
7bece59
[GPCAPIM-351]: Add documentation for NHSE mock authorisation service
DWolfsNHS Apr 14, 2026
fbd6449
[GPCAPIM-351]: PR comments
DWolfsNHS Apr 15, 2026
8e7da7c
[GPCAPIM-351]: Refactor imports in stub and test files
DWolfsNHS Apr 15, 2026
760c731
Merge branch 'main' into feature/GPCAPIM-351_tests_for_GPProvider_stub
DWolfsNHS Apr 15, 2026
e942312
[GPCAPIM-358]: merge conflicts
DWolfsNHS Apr 15, 2026
4ef4775
[GPCAPIM-351]: Update documentation for NHSE mock authorisation service
DWolfsNHS Apr 15, 2026
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
12 changes: 12 additions & 0 deletions .github/instructions/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@ When reviewing code, ensure you compare the changes made to files to all README.
- Use docstrings on high-level functions and classes to explain their purpose, inputs, outputs, and any side effects
- Avoid comments that state the obvious or repeat what the code does; instead, focus on explaining the intent behind the code, the reasons for non-obvious decisions, and any important trade-offs or constraints

## Formatting

- For Python files, use 4-space indentation and keep line lengths within Ruff limits (default 88 chars unless configured otherwise)
- For Python changes, keep code compatible with both `ruff format` and `ruff check`
- Let Ruff manage import ordering (isort rules are enabled via Ruff)
- Follow `.editorconfig` basics for all files: UTF-8, LF line endings, final newline, and no trailing whitespace
- Use tabs (not spaces) in `Makefile` and `.mk` files, per `.editorconfig`
- When wrapping a long string value inside parentheses, do not add a trailing comma if the value must remain a string
- For Markdown changes, keep content compatible with markdownlint checks (rules in `scripts/config/.markdownlint.yaml`; enforced by `scripts/githooks/check-markdown-format.sh`)
- For Markdown prose, write content that passes Vale English usage checks (rules in `scripts/config/vale/vale.ini`; enforced by `scripts/githooks/check-english-usage.sh`)
- For Terraform changes, keep files compatible with `terraform fmt`

## Commits

Prepend `[AI-generated]` to the commit message when committing changes made by an AI agent.
Expand Down
111 changes: 110 additions & 1 deletion .vscode/cspell-dictionary.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,117 @@
ACMRT
addgroup
adduser
alexkrechik
anchore
apikey
apim
asid
buildx
cataloger
catalogers
cataloging
cfparse
charliermarsh
chdir
chsh
cicd
cloc
Codeable
Colima
conftest
cooldown
cpes
CRAINE
cucumberautocomplete
datetimez
davidanson
debugpy
directcare
doas
Dockerfiles
dotenv
dpkg
DSTU
eamodio
errstr
Farsley
fhir
getstructuredrecord
gocloc
GPCAPIM
gpcdemonstrator
gpconnect
searchset
gpgsign
gpprovider
HAPI
healthcheck
htmlcov
igorshubovych
isort
jdkato
jinja
koalaman
mikefarah
modifyitems
monkeypatching
mstruebing
musllinux
NCSC
nektos
nhsd
nhsdigital
NHSE
nhsnum
noaddr
noendpoint
nohup
nonexistent
nonroot
nopass
noqa
NOSONAR
NPFIT
ONESHELL
opencollection
pipefail
PIPX
pkce
PROV
proxygen
pycache
pycodestyle
pydantic
pyenv
pyjwt
pyproject
pytest
PYTHONPATH
pyupgrade
pyyaml
qualitygate
Qube
querytype
scannerwork
searchset
shellcheck
sonarcloud
sonarlint
sonarqube
sonarsource
sprintf
SYFT
teardown
tfstate
tfvars
toplevel
tput
UNINDEXED
urid
usecwd
usefixtures
usermod
vars
vendored
venv
virtualenv
zstd
23 changes: 22 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
"**/.venv",
"**/.mypy_cache",
"**/.pytest_cache",
"**/.ruff_cache"
"**/.ruff_cache",
"**/node_modules",
"**/.*"
Comment thread
ian-robinson-35 marked this conversation as resolved.
],
// Disable all telemetry.
"telemetry.telemetryLevel": "off",
Expand Down Expand Up @@ -72,8 +74,24 @@
// Disabling automatic port forwarding as the devcontainer should already have access to any required ports.
"remote.autoForwardPorts": false,

"github.copilot.chat.commitMessageGeneration.instructions": [
{
"file": ".commit_template"
}
],

// Code spell checker configuration
"cSpell.language": "en-GB",
// Exclude generated artefacts, lock files, and data fixtures that contain
// machine-generated identifiers, NHS codes, and FHIR field names which are
// not conventional English prose.
"cSpell.ignorePaths": [
"gateway-api/test-artefacts/**",
"gateway-api/poetry.lock",
"gateway-api/stubs/stubs/data/**/*.json",
"gateway-api/tests/data/**/*.json",
"scripts/config/vale/styles/config/vocabularies/words/accept.txt"
],
"cSpell.customDictionaries": {
"vale-accepted-words": {
"path": "${workspaceFolder}/scripts/config/vale/styles/config/vocabularies/words/accept.txt",
Expand All @@ -86,4 +104,7 @@
"description": "Accepted words for the code spell checker.",
}
},
"python.analysis.extraPaths": [
"./gateway-api/stubs"
],
}
2 changes: 1 addition & 1 deletion bruno/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Given the API is currently set up with CIS2 user-restricted access, and with the
The proxy base path defines to which proxy instance your request will be directed. For preview environments, the proxy base path has the GitHub PR number appended to it. As such you will need to add this to your `.env` file so that Bruno can correctly build the URL.

```plaintext
PR_NNUMBER=<pr number from GitHub>
PR_NUMBER=<pr number from GitHub>
```

### Environments
Expand Down
12 changes: 12 additions & 0 deletions bruno/gateway-api/collections/Steel_Thread/opencollection.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,17 @@ request:
settings:
autoFetchToken: true
autoRefreshToken: false

docs:
content: |-
# [Authentication with the NHSE mock authorisation service](https://digital.nhs.uk/developer/guides-and-documentation/security-and-authorisation/testing-apis-with-our-mock-authorisation-service)

| User | UID | National RBAC job roles | Authenticator assurance level |
|-------------|-------|------------------------------|------------------------------|
| 656005750108| R0260 | General Medical Practitioner | AAL3 |
| 656005750107| R8000 | Clinical Practitioner | AAL3 |
| 656005750111| R0260 | General Medical Practitioner | AAL2 |
| 656005750109| R8000 | Clinical Practitioner | AAL2 |
type: text/markdown
bundled: false
extensions: {}
32 changes: 17 additions & 15 deletions gateway-api/src/gateway_api/clinical_jwt/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,46 +173,48 @@ def _validate_practitioner_name(names: list[dict[str, Any]]) -> list[str]:
return errors

@staticmethod
def validate_practitioner(pract: dict[str, Any]) -> None:
def validate_practitioner(practitioner: dict[str, Any]) -> None:
"""
Validate JWT requesting_practitioner structure.

Raises:
JWTValidationError: If practitioner structure is invalid.
"""
if not hasattr(pract, "get"):
if not hasattr(practitioner, "get"):
raise JWTValidationError(
error_details="Invalid requesting_practitioner: must be a dict"
)

pract_errors = []
practitioner_errors = []

if pract.get("resourceType") != "Practitioner":
pract_errors.append("resourceType must be 'Practitioner'")
if practitioner.get("resourceType") != "Practitioner":
practitioner_errors.append("resourceType must be 'Practitioner'")

if not pract.get("id"):
pract_errors.append("id is required")
if not practitioner.get("id"):
practitioner_errors.append("id is required")

# Validate identifiers
identifiers = pract.get("identifier")
identifiers = practitioner.get("identifier")
if not identifiers or not isinstance(identifiers, list):
pract_errors.append("Practitioner identifier must be a non-empty list")
practitioner_errors.append(
"Practitioner identifier must be a non-empty list"
)
else:
pract_errors.extend(
practitioner_errors.extend(
JWTValidator._validate_practitioner_identifiers(identifiers)
)

# Validate name
names = pract.get("name")
names = practitioner.get("name")
if not names or not isinstance(names, list):
pract_errors.append("name must be a non-empty list")
practitioner_errors.append("name must be a non-empty list")
else:
pract_errors.extend(JWTValidator._validate_practitioner_name(names))
practitioner_errors.extend(JWTValidator._validate_practitioner_name(names))

if pract_errors:
if practitioner_errors:
raise JWTValidationError(
error_details=(
f"Invalid requesting_practitioner: {', '.join(pract_errors)}"
f"Invalid requesting_practitioner: {', '.join(practitioner_errors)}"
)
)

Expand Down
3 changes: 1 addition & 2 deletions gateway-api/stubs/stubs/base_stub.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,7 @@ class PostStub(Protocol):
def post(
self,
url: str,
data: bytes | dict[str, Any] | None = None,
json: dict[str, Any] | None = None,
data: str,
**kwargs: Any,
) -> Response:
"""
Expand Down
69 changes: 36 additions & 33 deletions gateway-api/stubs/stubs/provider/stub.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,16 @@

from gateway_api.clinical_jwt import JWT, JWTValidator
from gateway_api.common.error import JWTValidationError
from gateway_api.get_structured_record import ACCESS_RECORD_STRUCTURED_INTERACTION_ID
from gateway_api.get_structured_record import (
ACCESS_RECORD_STRUCTURED_INTERACTION_ID,
)
from requests import Response

from stubs.base_stub import StubBase
from stubs.base_stub import PostStub, StubBase
from stubs.data.bundles import Bundles


class GpProviderStub(StubBase):
class GpProviderStub(StubBase, PostStub):
"""
A minimal in-memory stub for a Provider GP System FHIR API,
implementing only accessRecordStructured to read basic
Expand All @@ -44,6 +46,28 @@ class GpProviderStub(StubBase):
# https://simplifier.net/guide/gp-connect-access-record-structured/Home/Examples/Allergy-examples?version=1.6.2
"""

def __init__(self) -> None:
self._post_url: str = ""
self._post_headers: dict[str, str] = {}
self._post_data: str = ""
self._post_timeout: int | None = None

@property
def post_url(self) -> str:
return self._post_url

@property
def post_headers(self) -> dict[str, str]:
return self._post_headers

@property
def post_data(self) -> str:
return self._post_data

@property
def post_timeout(self) -> int | None:
return self._post_timeout

def _validate_headers(self, headers: dict[str, Any]) -> Response | None:
"""
Validate required headers for GPConnect FHIR API request.
Expand Down Expand Up @@ -131,7 +155,7 @@ def _validate_headers(self, headers: dict[str, Any]) -> Response | None:
"severity": "error",
"code": "invalid",
"diagnostics": (
"Authorization header must start with 'Bearer '",
"Authorization header must start with 'Bearer '"
),
}
],
Expand Down Expand Up @@ -191,11 +215,9 @@ def access_record_structured(
"""
# Validate that all required parameters are provided
missing_params: list[str] = []
if trace_id is None:
missing_params.append("trace_id")
if body is None:
if not body:
missing_params.append("body")
if headers is None:
if not headers:
missing_params.append("headers")

if missing_params:
Expand Down Expand Up @@ -273,38 +295,19 @@ def access_record_structured(

def post(
self,
_url: str,
url: str,
data: str,
_json: dict[str, Any] | None = None,
**kwargs: Any,
) -> Response:
"""
Handle HTTP POST requests for the stub.

:param url: Request URL.
:param headers: Request headers.
:param data: Request body data.
:param timeout: Request timeout in seconds.
:return: A :class:`requests.Response` instance.
"""
headers = kwargs.get("headers", {})
trace_id = headers.get("Ssp-TraceID", "no-trace-id")
return self.access_record_structured(trace_id, data, headers)

def get(
self,
url: str,
headers: dict[str, str],
params: dict[str, Any],
timeout: int,
) -> Response:
"""
Handle HTTP GET requests for the stub.
self._post_url = url
self._post_headers = headers
self._post_data = data
self._post_timeout = kwargs.get("timeout")

:param url: Request URL.
:param headers: Request headers.
:param params: Query parameters.
:param timeout: Request timeout in seconds.
:raises NotImplementedError: GET requests are not supported by this stub.
"""
raise NotImplementedError("GET requests are not supported by GpProviderStub")
return self.access_record_structured(trace_id, data, headers)
Comment thread
DWolfsNHS marked this conversation as resolved.
Loading
Loading