@@ -16,9 +16,11 @@ permissions:
1616env :
1717 AWS_REGION : eu-west-2
1818 PREVIEW_PREFIX : pr-
19+ MOCK_PREFIX : mock-
1920 PYTHON_VERSION : 3.14
2021 LAMBDA_RUNTIME : python3.14
2122 LAMBDA_HANDLER : lambda_handler.handler
23+ MOCK_LAMBDA_HANDLER : handler.handler
2224 MTLS_SECRET_NAME : ${{ vars.PREVIEW_ENV_MTLS_SECRET_NAME }}
2325 PROXYGEN_KEY_ID : ${{ vars.PREVIEW_ENV_PROXYGEN_KEY_ID }}
2426 PROXYGEN_CLIENT_ID : ${{ vars.PREVIEW_ENV_PROXYGEN_CLIENT_ID }}
5052 run : |
5153 make build
5254
55+ # Place holder mock artifact packaging to allow testing of mock API in preview environment;
56+ # can be extended to build a real mock Lambda if needed
57+ - name : Package mock artifact
58+ run : |
59+ cd infrastructure/environments/preview
60+ rm -f mock_artifact.zip
61+ zip -r mock_artifact.zip .
62+
5363 - name : Select AWS role inputs
5464 id : role-select
5565 env :
@@ -88,25 +98,51 @@ jobs:
8898 run : |
8999 SAFE=${{ steps.branch.outputs.safe }}
90100 PREFIX=${{ env.PREVIEW_PREFIX }}
91- MAX_FN_LEN=64
92- MAX_SAFE_LEN=$((MAX_FN_LEN - ${#PREFIX}))
101+ MOCK_PREFIX=${{ env.MOCK_PREFIX }}
102+ MAX_FN_LEN=62
103+ MAX_PREFIX_LEN=${#PREFIX}
104+ if [ ${#MOCK_PREFIX} -gt "$MAX_PREFIX_LEN" ]; then
105+ MAX_PREFIX_LEN=${#MOCK_PREFIX}
106+ fi
107+ MAX_SAFE_LEN=$((MAX_FN_LEN - MAX_PREFIX_LEN))
93108 if [ ${#SAFE} -gt "$MAX_SAFE_LEN" ]; then
94109 SAFE=${SAFE:0:MAX_SAFE_LEN}
95110 fi
96111 FN="${PREFIX}${SAFE}"
112+ MFN="${MOCK_PREFIX}${SAFE}"
97113 echo "function_name=$FN" >> "$GITHUB_OUTPUT"
114+ echo "mock_function_name=$MFN" >> "$GITHUB_OUTPUT"
98115 URL="https://${SAFE}.dev.endpoints.${{ env.PROXYGEN_API_NAME }}.national.nhs.uk"
116+ MOCK_URL="https://${SAFE}.m.dev.endpoints.${{ env.PROXYGEN_API_NAME }}.national.nhs.uk"
99117 echo "preview_url=$URL" >> "$GITHUB_OUTPUT"
118+ echo "mock_preview_url=$MOCK_URL" >> "$GITHUB_OUTPUT"
100119
120+ # ---------- Handle application ----------
101121 - name : Create or update preview Lambda (on open/sync/reopen)
102122 if : github.event.action != 'closed'
123+ env :
124+ MOCK_URL : ${{ steps.names.outputs.mock_preview_url }}
125+ EXPIRY_THRESHOLD : ${{ secrets.APIM_TOKEN_EXPIRY_THRESHOLD }}
126+ JWKS_SECRET : ${{ secrets.JWKS_SECRET }}
127+ APIM_PRIVATE_KEY : ${{ secrets.APIM_PRIVATE_KEY }}
128+ APIM_APIKEY : ${{ secrets.APIM_APIKEY }}
129+ API_MTLS_CERT : ${{ secrets.API_MTLS_CERT }}
130+ API_MTLS_KEY : ${{ secrets.API_MTLS_KEY }}
103131 run : |
104132 cd pathology-api/target/
105133 FN="${{ steps.names.outputs.function_name }}"
134+ EXPIRY_THRESHOLD="${TOKEN_EXPIRY_THRESHOLD:-30s}"
135+ JWKS_SECRET="${JWKS_SECRET_NAME:-/cds/pathology/dev/jwks/secret}"
136+ PRIVATE_KEY="${APIM_PRIVATE_KEY:-/cds/pathology/dev/apim/private-key}"
137+ API_KEY="${APIM_APIKEY:-/cds/pathology/dev/apim/api-key}"
138+ MTLS_CERT="${API_MTLS_CERT:-/cds/pathology/dev/mtls/client1-key-public}"
139+ MTLS_KEY="${API_MTLS_KEY:-/cds/pathology/dev/mtls/client1-key-secret}"
106140 echo "Deploying preview function: $FN"
107141 wait_for_lambda_ready() {
108142 while true; do
109- status=$(aws lambda get-function-configuration --function-name "$FN" --query 'LastUpdateStatus' --output text 2>/dev/null || echo "Unknown")
143+ status=$(aws lambda get-function-configuration --function-name "$FN" \
144+ --query 'LastUpdateStatus' \
145+ --output text 2>/dev/null || echo "Unknown")
110146 if [ "$status" = "Successful" ] || [ "$status" = "Unknown" ]; then
111147 break
112148 fi
@@ -120,15 +156,36 @@ jobs:
120156 }
121157 if aws lambda get-function --function-name "$FN" >/dev/null 2>&1; then
122158 wait_for_lambda_ready
123- aws lambda update-function-configuration --function-name "$FN" --handler "${{ env.LAMBDA_HANDLER }}" || true
159+ aws lambda update-function-configuration --function-name "$FN" \
160+ --handler "${{ env.LAMBDA_HANDLER }}" \
161+ --environment "Variables={APIM_TOKEN_EXPIRY_THRESHOLD=$EXPIRY_THRESHOLD, \
162+ APIM_PRIVATE_KEY_NAME=$PRIVATE_KEY, \
163+ APIM_API_KEY_NAME=$API_KEY, \
164+ APIM_MTLS_CERT_NAME=$MTLS_CERT, \
165+ APIM_MTLS_KEY_NAME=$MTLS_KEY, \
166+ APIM_TOKEN_URL=$MOCK_URL/apim, \
167+ PDM_BUNDLE_URL=$MOCK_URL/pdm, \
168+ MNS_EVENT_URL=$MOCK_URL/mns, \
169+ JWKS_SECRET_NAME=$JWKS_SECRET}" || true
124170 wait_for_lambda_ready
125- aws lambda update-function-code --function-name "$FN" --zip-file "fileb://artifact.zip" --publish
171+ aws lambda update-function-code --function-name "$FN" \
172+ --zip-file "fileb://artifact.zip" \
173+ --publish
126174 else
127175 aws lambda create-function --function-name "$FN" \
128176 --runtime "${{ env.LAMBDA_RUNTIME }}" \
129177 --handler "${{ env.LAMBDA_HANDLER }}" \
130178 --zip-file "fileb://artifact.zip" \
131179 --role "${{ steps.role-select.outputs.lambda_role }}" \
180+ --environment "Variables={APIM_TOKEN_EXPIRY_THRESHOLD=$EXPIRY_THRESHOLD, \
181+ APIM_PRIVATE_KEY_NAME=$PRIVATE_KEY, \
182+ APIM_API_KEY_NAME=$API_KEY, \
183+ APIM_MTLS_CERT_NAME=$MTLS_CERT, \
184+ APIM_MTLS_KEY_NAME=$MTLS_KEY, \
185+ APIM_TOKEN_URL=$MOCK_URL/apim, \
186+ PDM_BUNDLE_URL=$MOCK_URL/pdm, \
187+ MNS_EVENT_URL=$MOCK_URL/mns, \
188+ JWKS_SECRET_NAME=$JWKS_SECRET}" \
132189 --publish
133190 wait_for_lambda_ready
134191 fi
@@ -145,6 +202,65 @@ jobs:
145202 echo "function = ${{ steps.names.outputs.function_name }}"
146203 echo "url = ${{ steps.names.outputs.preview_url }}"
147204
205+ # ---------- Handle mock endpoints ----------
206+ - name : Create or update mock Lambda (on open/sync/reopen)
207+ if : github.event.action != 'closed'
208+ env :
209+ TOKEN_EXPIRY_TIME : ${{ secrets.TOKEN_LIFETIME }}
210+ run : |
211+ cd infrastructure/environments/preview
212+ MFN="${{ steps.names.outputs.mock_function_name }}"
213+ SAFE="${{ steps.branch.outputs.safe }}"
214+ TOKEN_LIFETIME="${TOKEN_EXPIRY_TIME:-15m}"
215+ echo "Deploying mock function: $MFN"
216+ wait_for_lambda_ready() {
217+ while true; do
218+ status=$(aws lambda get-function-configuration --function-name "$MFN" --query 'LastUpdateStatus' --output text 2>/dev/null || echo "Unknown")
219+ if [ "$status" = "Successful" ] || [ "$status" = "Unknown" ]; then
220+ break
221+ fi
222+ if [ "$status" = "Failed" ]; then
223+ echo "Lambda is in Failed state; check logs." >&2
224+ exit 1
225+ fi
226+ echo "Lambda update status: $status — waiting..."
227+ sleep 5
228+ done
229+ }
230+ if aws lambda get-function --function-name "$MFN" >/dev/null 2>&1; then
231+ wait_for_lambda_ready
232+ aws lambda update-function-configuration --function-name "$MFN" \
233+ --handler "${{ env.MOCK_LAMBDA_HANDLER }}" \
234+ --environment "Variables={CLIENT_PUBLIC_KEY_ARN=mock, \
235+ DDB_INDEX_TAG=$SAFE, \
236+ TOKEN_LIFETIME=$TOKEN_LIFETIME}" || true
237+ wait_for_lambda_ready
238+ aws lambda update-function-code --function-name "$MFN" --zip-file "fileb://mock_artifact.zip" --publish
239+ else
240+ aws lambda create-function --function-name "$MFN" \
241+ --runtime "${{ env.LAMBDA_RUNTIME }}" \
242+ --handler "${{ env.MOCK_LAMBDA_HANDLER }}" \
243+ --zip-file "fileb://mock_artifact.zip" \
244+ --role "${{ steps.role-select.outputs.lambda_role }}" \
245+ --environment "Variables={CLIENT_PUBLIC_KEY_ARN=mock, \
246+ DDB_INDEX_TAG=$SAFE, \
247+ TOKEN_LIFETIME=$TOKEN_LIFETIME}" \
248+ --publish
249+ wait_for_lambda_ready
250+ fi
251+
252+ - name : Delete mock Lambda (on PR closed)
253+ if : github.event.action == 'closed'
254+ run : |
255+ MFN="${{ steps.names.outputs.mock_function_name }}"
256+ echo "Deleting mock function: $MFN"
257+ aws lambda delete-function --function-name "$MFN" || true
258+
259+ - name : Output mock function name
260+ run : |
261+ echo "mock_function = ${{ steps.names.outputs.mock_function_name }}"
262+ echo "mock_url = ${{ steps.names.outputs.mock_preview_url }}"
263+
148264 # ---------- Wait on AWS tasks and notify ----------
149265 - name : Get mTLS certs for testing
150266 if : github.event.action != 'closed'
@@ -207,6 +323,57 @@ jobs:
207323 echo "http_result=unexpected-status" >> "$GITHUB_OUTPUT"
208324 exit 0
209325
326+ - name : Smoke test mock URL
327+ if : github.event.action != 'closed'
328+ id : smoke-mock
329+ env :
330+ PREVIEW_URL : ${{ steps.names.outputs.mock_preview_url }}
331+ run : |
332+ if [ -z "$PREVIEW_URL" ] || [ "$PREVIEW_URL" = "null" ]; then
333+ echo "Mock URL missing"
334+ echo "http_status=missing" >> "$GITHUB_OUTPUT"
335+ echo "http_result=missing-url" >> "$GITHUB_OUTPUT"
336+ exit 0
337+ fi
338+
339+ # Reachability check: allow 404 (app routes might not exist yet) but fail otherwise
340+ printf '%s' "$_cds_pathology_dev_mtls_client1_key_secret" > /tmp/client1-key.pem
341+ printf '%s' "$_cds_pathology_dev_mtls_client1_key_public" > /tmp/client1-cert.pem
342+ STATUS=$(curl \
343+ --cert /tmp/client1-cert.pem \
344+ --key /tmp/client1-key.pem \
345+ --silent \
346+ --output /tmp/preview.headers \
347+ --write-out '%{http_code}' \
348+ --head \
349+ --max-time 30 \
350+ -X GET "$PREVIEW_URL"/_status || true)
351+ rm -f /tmp/client1-key.pem
352+ rm -f /tmp/client1-cert.pem
353+
354+ if [ "$STATUS" = "404" ]; then
355+ echo "Mock responded with expected 404"
356+ echo "http_status=404" >> "$GITHUB_OUTPUT"
357+ echo "http_result=allowed-404" >> "$GITHUB_OUTPUT"
358+ exit 0
359+ fi
360+
361+ if [[ "$STATUS" =~ ^[0-9]{3}$ ]] && [ "$STATUS" -ge 200 ] && [ "$STATUS" -lt 400 ]; then
362+ echo "Mock responded with status $STATUS"
363+ echo "http_status=$STATUS" >> "$GITHUB_OUTPUT"
364+ echo "http_result=success" >> "$GITHUB_OUTPUT"
365+ exit 0
366+ fi
367+
368+ echo "Mock responded with unexpected status $STATUS"
369+ if [ -f /tmp/preview.headers ]; then
370+ echo "Response headers:"
371+ cat /tmp/preview.headers
372+ fi
373+ echo "http_status=$STATUS" >> "$GITHUB_OUTPUT"
374+ echo "http_result=unexpected-status" >> "$GITHUB_OUTPUT"
375+ exit 0
376+
210377 - name : Get proxygen machine user details
211378 id : proxygen-machine-user
212379 uses : aws-actions/aws-secretsmanager-get-secrets@a9a7eb4e2f2871d30dc5b892576fde60a2ecc802
@@ -220,7 +387,7 @@ jobs:
220387 with :
221388 mtls-secret-name : ${{ env.MTLS_SECRET_NAME }}
222389 target-url : ${{ steps.names.outputs.preview_url }}
223- proxy-base-path : ' ${{ env.PROXYGEN_API_NAME }}-pr-${{ github.event.pull_request.number }}'
390+ proxy-base-path : " ${{ env.PROXYGEN_API_NAME }}-pr-${{ github.event.pull_request.number }}"
224391 proxygen-key-secret : ${{ env._cds_pathology_dev_proxygen_proxygen_key_secret }}
225392 proxygen-key-id : ${{ env.PROXYGEN_KEY_ID }}
226393 proxygen-client-id : ${{ env.PROXYGEN_CLIENT_ID }}
@@ -230,7 +397,7 @@ jobs:
230397 if : github.event.action == 'closed'
231398 uses : ./.github/actions/proxy/tear-down-proxy
232399 with :
233- proxy-base-path : ' ${{ env.PROXYGEN_API_NAME }}-pr-${{ github.event.pull_request.number }}'
400+ proxy-base-path : " ${{ env.PROXYGEN_API_NAME }}-pr-${{ github.event.pull_request.number }}"
234401 proxygen-key-secret : ${{ env._cds_pathology_dev_proxygen_proxygen_key_secret }}
235402 proxygen-key-id : ${{ env.PROXYGEN_KEY_ID }}
236403 proxygen-client-id : ${{ env.PROXYGEN_CLIENT_ID }}
@@ -242,13 +409,17 @@ jobs:
242409 with :
243410 script : |
244411 const fn = '${{ steps.names.outputs.function_name }}';
412+ const mock_fn = '${{ steps.names.outputs.mock_function_name }}';
245413 const url = '${{ steps.names.outputs.preview_url }}';
414+ const mock_url = '${{ steps.names.outputs.mock_preview_url }}';
246415 const proxy_url = 'https://internal-dev.api.service.nhs.uk/${{ env.PROXYGEN_API_NAME }}-pr-${{ github.event.pull_request.number }}';
247416 const owner = context.repo.owner;
248417 const repo = context.repo.repo;
249418 const issueNumber = context.issue.number;
250419 const smokeStatus = '${{ steps.smoke-test.outputs.http_status }}' || 'n/a';
251420 const smokeResult = '${{ steps.smoke-test.outputs.http_result }}' || 'not-run';
421+ const smokeMockStatus = '${{ steps.smoke-mock.outputs.http_status }}' || 'n/a';
422+ const smokeMockResult = '${{ steps.smoke-mock.outputs.http_result }}' || 'not-run';
252423
253424 const smokeLabels = {
254425 success: ':white_check_mark: Passed',
@@ -258,7 +429,7 @@ jobs:
258429 };
259430
260431 const smokeReadable = smokeLabels[smokeResult] ?? smokeResult;
261-
432+ const smokeMockReadable = smokeLabels[smokeMockResult] ?? smokeMockResult;
262433 const { data: comments } = await github.rest.issues.listComments({
263434 owner,
264435 repo,
@@ -281,10 +452,13 @@ jobs:
281452
282453 const lines = [
283454 '**Deployment Complete**',
284- `- Preview URL: [${url}](${url}) — [Status endpoint](${url}/_status)`,
285- `- Smoke Test: ${smokeReadable} (HTTP ${smokeStatus})`,
455+ `- Preview URL: [${url}](${url}) — [Status](${url}/_status)`,
456+ ` - Smoke Test: ${smokeReadable} (HTTP ${smokeStatus})`,
457+ `- Mock URL: [${mock_url}](${mock_url})`,
458+ ` - Smoke Mock Test: ${smokeMockReadable} (HTTP ${smokeMockStatus})`,
286459 `- Proxy URL: [${proxy_url}](${proxy_url})`,
287460 `- Lambda Function: ${fn}`,
461+ `- Mock Lambda Function: ${mock_fn}`,
288462 ];
289463
290464 await github.rest.issues.createComment({
0 commit comments