Repository: govuk-one-login/performance-testing
Pull request #1255: [QA-954]: ID check async V2 script development
Author: login: abhinaysrivastavaperf
Labels: name: k6
description: Pull requests that make changes to k6 scripts or the k6 application
Created: 2025-04-23T10:53:02Z | Merged: 2025-05-20T16:49:33Z

## QA-954 <!--Jira Ticket Number-->

### What?
ID check async V2 script development

#### Changes:
- Blank script added for V2 script development.

---

### Why?
For mobile team to add inputs on this script. Description will be updated once the script is ready for review.


Diff:
diff --git a/deploy/scripts/src/mobile-id-check-async/test.ts b/deploy/scripts/src/mobile-id-check-async/test.ts
new file mode 100644
index 000000000..54dc61705
--- /dev/null
+++ b/deploy/scripts/src/mobile-id-check-async/test.ts
@@ -0,0 +1,86 @@
+import { type Options } from 'k6/options'
+import {
+  describeProfile,
+  type ProfileList,
+  selectProfile,
+  createScenario,
+  LoadProfile,
+  createI4PeakTestSignUpScenario
+} from '../common/utils/config/load-profiles'
+import { getThresholds } from '../common/utils/config/thresholds'
+import { iterationsCompleted, iterationsStarted } from '../common/utils/custom_metric/counter'
+import { createSession } from './testSteps/createSession'
+import { getActiveSession } from './testSteps/getActiveSession'
+import { postBiometricToken } from './testSteps/postBiometricToken'
+import { sleepBetween } from '../common/utils/sleep/sleepBetween'
+import { getWellknownJwks } from './testSteps/getWellKnownJwks'
+import { postTxmaEvent } from './testSteps/postTxmaEvent'
+import { postSetupVendorResponse } from './testSteps/postSetupVendorResponse'
+import { uuidv4 } from '../common/utils/jslib'
+import { postFinishBiometricSession } from './testSteps/postFinishBiometricSession'
+import { postAbortSession } from './testSteps/postAbortSession'
+
+const profiles: ProfileList = {
+  smoke: {
+    ...createScenario('idCheckAsync', LoadProfile.smoke)
+  },
+  perf006Iteration4PeakTest: {
+    ...createI4PeakTestSignUpScenario('idCheckAsync', 450, 27, 451)
+  }
+}
+
+const loadProfile = selectProfile(profiles)
+export const groupMap = {
+  idCheckAsync: [
+    'B01_IDCheckV2_00_POST_/async/token',
+    'B01_IDCheckV2_01_POST_/async/credential', //pragma: allowlist secret
+    'B01_IDCheckV2_02_POST_sts-mock /token',
+    'B01_IDCheckV2_03_GET_/async/activeSession', //pragma: allowlist secret
+    'B01_IDCheckV2_04_POST_/async/biometricToken', //pragma: allowlist secret
+    'B01_IDCheckV2_05_GET_/async/.well-known/jwks.json',
+    'B01_IDCheckV2_06_POST_/async/txmaEvent', //pragma: allowlist secret
+    'B01_IDCheckV2_07_POST_readid-mock /setupVendorResponse',
+    'B01_IDCheckV2_08_POST_async/finishBiometricSession', //pragma: allowlist secret
+    'B01_IDCheckV2_09_POST async/abortSession'
+  ]
+} as const
+
+export const options: Options = {
+  scenarios: loadProfile.scenarios,
+  thresholds: getThresholds(groupMap),
+  tags: { name: '' }
+}
+
+export function setup(): void {
+  describeProfile(loadProfile)
+}
+
+export function idCheckAsync(): void {
+  iterationsStarted.add(1)
+  const sub = createSession()
+  const sessionId = getActiveSession(sub)
+  sleepBetween(0.5, 1)
+  getWellknownJwks()
+  sleepBetween(0.5, 1)
+  const opaqueId = postBiometricToken(sessionId)
+  sleepBetween(0.5, 1)
+  postTxmaEvent(sessionId)
+  sleepBetween(0.5, 1)
+  postTxmaEvent(sessionId)
+  sleepBetween(0.5, 1)
+  postTxmaEvent(sessionId)
+  sleepBetween(0.5, 1)
+
+  if (Math.random() <= 0.8) {
+    // Approximately 80% of users complete journey successfully
+    const biometricSessionId = uuidv4()
+    postSetupVendorResponse({ biometricSessionId, opaqueId })
+    sleepBetween(0.5, 1)
+    postFinishBiometricSession({ biometricSessionId, sessionId })
+    sleepBetween(0.5, 1)
+  } else {
+    // Approximately 20% of users abort journey
+    postAbortSession(sessionId)
+  }
+  iterationsCompleted.add(1)
+}
diff --git a/deploy/scripts/src/mobile-id-check-async/testSteps/createSession.ts b/deploy/scripts/src/mobile-id-check-async/testSteps/createSession.ts
new file mode 100644
index 000000000..6342a8ba1
--- /dev/null
+++ b/deploy/scripts/src/mobile-id-check-async/testSteps/createSession.ts
@@ -0,0 +1,99 @@
+import http from 'k6/http'
+import { timeGroup } from '../../common/utils/request/timing'
+import { groupMap } from '../test'
+import { config } from '../utils/config'
+import { b64encode } from 'k6/encoding'
+import { isStatusCode200, isStatusCode201 } from '../../common/utils/checks/assertions'
+import { sleepBetween } from '../../common/utils/sleep/sleepBetween'
+
+import { URL } from '../../common/utils/jslib/url'
+import { uuidv4 } from '../../common/utils/jslib'
+import { apiSignaturev4Signer } from '../utils/apiSignatureV4Signer'
+
+export function createSession(): string {
+  const asyncTokenRequestBody = 'grant_type=client_credentials'
+  const signedAsyncTokenRequest = apiSignaturev4Signer.sign({
+    method: 'POST',
+    protocol: 'https',
+    hostname: new URL(getUrl()).hostname,
+    path: '/async/token',
+    headers: getTokenRequestHeader(),
+    body: asyncTokenRequestBody
+  })
+
+  const asyncTokenResponse = timeGroup(
+    groupMap.idCheckAsync[0],
+    () =>
+      http.post(signedAsyncTokenRequest.url, asyncTokenRequestBody, {
+        headers: { ...signedAsyncTokenRequest.headers, ...getTokenRequestHeader() }
+      }),
+    {
+      isStatusCode200
+    }
+  )
+
+  sleepBetween(0.5, 1)
+
+  const sub = uuidv4()
+
+  const asyncCredentialRequestBody = {
+    state: 'mockState',
+    sub,
+    client_id: config.clientId,
+    govuk_signin_journey_id: 'performanceTest'
+  }
+
+  const accessToken = asyncTokenResponse.json('access_token') as string
+
+  const signedAsyncCredentialRequest = apiSignaturev4Signer.sign({
+    method: 'POST',
+    protocol: 'https',
+    hostname: new URL(getUrl()).hostname,
+    path: '/async/credential',
+    headers: getAsyncCredentialRequestHeader(accessToken),
+    body: JSON.stringify(asyncCredentialRequestBody)
+  })
+
+  timeGroup(
+    groupMap.idCheckAsync[1],
+    () =>
+      http.post(signedAsyncCredentialRequest.url, JSON.stringify(asyncCredentialRequestBody), {
+        headers: { ...signedAsyncCredentialRequest.headers, ...getAsyncCredentialRequestHeader(accessToken) }
+      }),
+    {
+      isStatusCode201
+    }
+  )
+  return sub
+}
+
+function getUrl(): string {
+  if (useProxyApi()) {
+    return config.proxyApiUrl
+  }
+  return config.privateApiUrl
+}
+
+function useProxyApi(): boolean {
+  return config.useProxyApi === 'true'
+}
+
+function getTokenRequestHeader(): Record<string, string> {
+  const encodedClientCredentials = b64encode(`${config.clientId}:${config.clientSecret}`)
+  if (useProxyApi()) {
+    return {
+      'X-Custom-Auth': `Basic ${encodedClientCredentials}`
+    }
+  }
+  return { Authorization: `Basic ${encodedClientCredentials}` }
+}
+
+function getAsyncCredentialRequestHeader(accessToken: string): Record<string, string> {
+  const authHeader = `Bearer ${accessToken}`
+  if (useProxyApi()) {
+    return {
+      'X-Custom-Auth': authHeader
+    }
+  }
+  return { Authorization: authHeader }
+}
diff --git a/deploy/scripts/src/mobile-id-check-async/testSteps/getActiveSession.ts b/deploy/scripts/src/mobile-id-check-async/testSteps/getActiveSession.ts
new file mode 100644
index 000000000..2c2f1bc3e
--- /dev/null
+++ b/deploy/scripts/src/mobile-id-check-async/testSteps/getActiveSession.ts
@@ -0,0 +1,52 @@
+import http from 'k6/http'
+import { timeGroup } from '../../common/utils/request/timing'
+import { groupMap } from '../test'
+import { config } from '../utils/config'
+import { isStatusCode200 } from '../../common/utils/checks/assertions'
+import { sleepBetween } from '../../common/utils/sleep/sleepBetween'
+
+export function getActiveSession(sub: string): string {
+  const stsMockRequestBody = new URLSearchParams({
+    subject_token: sub,
+    scope: 'idCheck.activeSession.read',
+    grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
+    subject_token_type: 'urn:ietf:params:oauth:token-type:access_token'
+  })
+
+  const stsMockResponse = timeGroup(
+    groupMap.idCheckAsync[2],
+    () => http.post(getStsTokenUrl(), stsMockRequestBody.toString()),
+    {
+      isStatusCode200
+    }
+  )
+
+  const accessToken = stsMockResponse.json('access_token') as string
+
+  sleepBetween(0.5, 1)
+
+  const asyncActiveSessionResponse = timeGroup(
+    groupMap.idCheckAsync[3],
+    () =>
+      http.get(getAsyncActiveSessionUrl(), {
+        headers: { Authorization: getAsyncActiveSessionAuthorizationHeader(accessToken) }
+      }),
+    {
+      isStatusCode200
+    }
+  )
+
+  return asyncActiveSessionResponse.json('sessionId') as string
+}
+
+function getStsTokenUrl() {
+  return `${config.stsMockApiUrl}/token`
+}
+
+function getAsyncActiveSessionUrl() {
+  return `${config.sessionsApiUrl}/async/activeSession`
+}
+
+function getAsyncActiveSessionAuthorizationHeader(accessToken: string) {
+  return `Bearer ${accessToken}`
+}
diff --git a/deploy/scripts/src/mobile-id-check-async/testSteps/getWellKnownJwks.ts b/deploy/scripts/src/mobile-id-check-async/testSteps/getWellKnownJwks.ts
new file mode 100644
index 000000000..9161192f3
--- /dev/null
+++ b/deploy/scripts/src/mobile-id-check-async/testSteps/getWellKnownJwks.ts
@@ -0,0 +1,15 @@
+import http from 'k6/http'
+import { timeGroup } from '../../common/utils/request/timing'
+import { groupMap } from '../test'
+import { config } from '../utils/config'
+import { isStatusCode200 } from '../../common/utils/checks/assertions'
+
+export function getWellknownJwks(): void {
+  timeGroup(groupMap.idCheckAsync[5], () => http.get(getWellKnownJwksUrl()), {
+    isStatusCode200
+  })
+}
+
+function getWellKnownJwksUrl() {
+  return `${config.sessionsApiUrl}/.well-known/jwks.json`
+}
diff --git a/deploy/scripts/src/mobile-id-check-async/testSteps/postAbortSession.ts b/deploy/scripts/src/mobile-id-check-async/testSteps/postAbortSession.ts
new file mode 100644
index 000000000..1dd096559
--- /dev/null
+++ b/deploy/scripts/src/mobile-id-check-async/testSteps/postAbortSession.ts
@@ -0,0 +1,23 @@
+import http from 'k6/http'
+import { timeGroup } from '../../common/utils/request/timing'
+import { groupMap } from '../test'
+import { config } from '../utils/config'
+import { isStatusCode200 } from '../../common/utils/checks/assertions'
+
+export function postAbortSession(sessionId: string): void {
+  const asyncAbortSessionBody = {
+    sessionId
+  }
+
+  timeGroup(
+    groupMap.idCheckAsync[9],
+    () => http.post(getAsyncAbortSessionUrl(), JSON.stringify(asyncAbortSessionBody)),
+    {
+      isStatusCode200
+    }
+  )
+}
+
+function getAsyncAbortSessionUrl() {
+  return `${config.sessionsApiUrl}/async/abortSession`
+}
diff --git a/deploy/scripts/src/mobile-id-check-async/testSteps/postBiometricToken.ts b/deploy/scripts/src/mobile-id-check-async/testSteps/postBiometricToken.ts
new file mode 100644
index 000000000..20aa22913
--- /dev/null
+++ b/deploy/scripts/src/mobile-id-check-async/testSteps/postBiometricToken.ts
@@ -0,0 +1,26 @@
+import http from 'k6/http'
+import { timeGroup } from '../../common/utils/request/timing'
+import { groupMap } from '../test'
+import { config } from '../utils/config'
+import { isStatusCode200 } from '../../common/utils/checks/assertions'
+
+export function postBiometricToken(sessionId: string): string {
+  const asyncBiometricTokenBody = {
+    sessionId,
+    documentType: 'NFC_PASSPORT'
+  }
+
+  const response = timeGroup(
+    groupMap.idCheckAsync[4],
+    () => http.post(getAsyncBiometricTokenUrl(), JSON.stringify(asyncBiometricTokenBody)),
+    {
+      isStatusCode200
+    }
+  )
+
+  return response.json('opaqueId') as string
+}
+
+function getAsyncBiometricTokenUrl() {
+  return `${config.sessionsApiUrl}/async/biometricToken`
+}
diff --git a/deploy/scripts/src/mobile-id-check-async/testSteps/postFinishBiometricSession.ts b/deploy/scripts/src/mobile-id-check-async/testSteps/postFinishBiometricSession.ts
new file mode 100644
index 000000000..fe0b15772
--- /dev/null
+++ b/deploy/scripts/src/mobile-id-check-async/testSteps/postFinishBiometricSession.ts
@@ -0,0 +1,24 @@
+import http from 'k6/http'
+import { timeGroup } from '../../common/utils/request/timing'
+import { groupMap } from '../test'
+import { config } from '../utils/config'
+import { isStatusCode200 } from '../../common/utils/checks/assertions'
+
+export function postFinishBiometricSession(testData: { biometricSessionId: string; sessionId: string }): void {
+  const asyncFinishBiometricSessionBody = {
+    sessionId: testData.sessionId,
+    biometricSessionId: testData.biometricSessionId
+  }
+
+  timeGroup(
+    groupMap.idCheckAsync[8],
+    () => http.post(getAsyncFinishBiometricSessionUrl(), JSON.stringify(asyncFinishBiometricSessionBody)),
+    {
+      isStatusCode200
+    }
+  )
+}
+
+function getAsyncFinishBiometricSessionUrl() {
+  return `${config.sessionsApiUrl}/async/finishBiometricSession`
+}
diff --git a/deploy/scripts/src/mobile-id-check-async/testSteps/postSetupVendorResponse.ts b/deploy/scripts/src/mobile-id-check-async/testSteps/postSetupVendorResponse.ts
new file mode 100644
index 000000000..d0b84e4f5
--- /dev/null
+++ b/deploy/scripts/src/mobile-id-check-async/testSteps/postSetupVendorResponse.ts
@@ -0,0 +1,37 @@
+import http from 'k6/http'
+import { timeGroup } from '../../common/utils/request/timing'
+import { groupMap } from '../test'
+import { config } from '../utils/config'
+import { isStatusCode201 } from '../../common/utils/checks/assertions'
+import { apiSignaturev4Signer } from '../utils/apiSignatureV4Signer'
+
+export function postSetupVendorResponse(testData: { biometricSessionId: string; opaqueId: string }): void {
+  const biometricSession = JSON.parse(config.biometricSessionTestData)
+
+  const setupVendorResponseBody = {
+    ...biometricSession,
+    opaqueId: testData.opaqueId,
+    creationDate: new Date().toISOString()
+  }
+
+  const signedRequest = apiSignaturev4Signer.sign({
+    method: 'POST',
+    protocol: 'https',
+    hostname: new URL(config.readidMockApiUrl).hostname,
+    path: getSetupVendorResponsePath(testData.biometricSessionId),
+    body: JSON.stringify(setupVendorResponseBody),
+    headers: {}
+  })
+
+  timeGroup(
+    groupMap.idCheckAsync[7],
+    () => http.post(signedRequest.url, JSON.stringify(setupVendorResponseBody), { headers: signedRequest.headers }),
+    {
+      isStatusCode201
+    }
+  )
+}
+
+function getSetupVendorResponsePath(biometricSessionId: string): string {
+  return `/v2/setupVendorResponse/${biometricSessionId}`
+}
diff --git a/deploy/scripts/src/mobile-id-check-async/testSteps/postTxmaEvent.ts b/deploy/scripts/src/mobile-id-check-async/testSteps/postTxmaEvent.ts
new file mode 100644
index 000000000..5acac5d20
--- /dev/null
+++ b/deploy/scripts/src/mobile-id-check-async/testSteps/postTxmaEvent.ts
@@ -0,0 +1,20 @@
+import http from 'k6/http'
+import { timeGroup } from '../../common/utils/request/timing'
+import { groupMap } from '../test'
+import { config } from '../utils/config'
+import { isStatusCode200 } from '../../common/utils/checks/assertions'
+
+export function postTxmaEvent(sessionId: string): void {
+  const asyncTxmaEventBody = {
+    sessionId,
+    eventName: 'DCMAW_ASYNC_HYBRID_BILLING_STARTED'
+  }
+
+  timeGroup(groupMap.idCheckAsync[6], () => http.post(getAsyncTxmaEventUrl(), JSON.stringify(asyncTxmaEventBody)), {
+    isStatusCode200
+  })
+}
+
+function getAsyncTxmaEventUrl() {
+  return `${config.sessionsApiUrl}/async/txmaEvent`
+}
diff --git a/deploy/scripts/src/mobile-id-check-async/utils/apiSignatureV4Signer.ts b/deploy/scripts/src/mobile-id-check-async/utils/apiSignatureV4Signer.ts
new file mode 100644
index 000000000..d4ef521e9
--- /dev/null
+++ b/deploy/scripts/src/mobile-id-check-async/utils/apiSignatureV4Signer.ts
@@ -0,0 +1,16 @@
+import { SignatureV4 } from '../../common/utils/jslib/aws-signature'
+import { AssumeRoleOutput } from '../../common/utils/aws/types'
+import { getEnv } from '../../common/utils/config/environment-variables'
+
+const credentials = (JSON.parse(getEnv('EXECUTION_CREDENTIALS')) as AssumeRoleOutput).Credentials
+export const apiSignaturev4Signer = new SignatureV4({
+  service: 'execute-api',
+  region: getEnv('AWS_REGION'),
+  credentials: {
+    accessKeyId: credentials.AccessKeyId,
+    secretAccessKey: credentials.SecretAccessKey,
+    sessionToken: credentials.SessionToken
+  },
+  uriEscapePath: false,
+  applyChecksum: false
+})
diff --git a/deploy/scripts/src/mobile-id-check-async/utils/config.ts b/deploy/scripts/src/mobile-id-check-async/utils/config.ts
new file mode 100644
index 000000000..93d7b2dae
--- /dev/null
+++ b/deploy/scripts/src/mobile-id-check-async/utils/config.ts
@@ -0,0 +1,19 @@
+import { getEnv } from '../../common/utils/config/environment-variables'
+
+// Refer to deploy/scripts/README.md for guidance on how to set environment variables
+export const environment = getEnv('ENVIRONMENT').toLocaleUpperCase()
+const validEnvironments = ['BUILD', 'DEV']
+if (!validEnvironments.includes(environment))
+  throw new Error(`Environment '${environment}' not in [${validEnvironments.toString()}]`)
+
+export const config = {
+  clientId: getEnv(`MOBILE_ID_CHECK_ASYNC_${environment}_CLIENT_ID`),
+  clientSecret: getEnv(`MOBILE_ID_CHECK_ASYNC_${environment}_CLIENT_SECRET`),
+  privateApiUrl: getEnv(`MOBILE_ID_CHECK_ASYNC_${environment}_PRIVATE_API_URL`),
+  proxyApiUrl: getEnv(`MOBILE_ID_CHECK_ASYNC_${environment}_PROXY_API_URL`),
+  readidMockApiUrl: getEnv(`MOBILE_ID_CHECK_ASYNC_${environment}_READ_ID_MOCK_API_URL`),
+  sessionsApiUrl: getEnv(`MOBILE_ID_CHECK_ASYNC_${environment}_SESSIONS_API_URL`),
+  stsMockApiUrl: getEnv(`MOBILE_ID_CHECK_ASYNC_${environment}_STS_MOCK_API_URL`),
+  useProxyApi: getEnv(`MOBILE_ID_CHECK_ASYNC_${environment}_USE_PROXY_API`),
+  biometricSessionTestData: getEnv(`MOBILE_ID_CHECK_ASYNC_${environment}_BIOMETRIC_SESSION_TEST_DATA`)
+}
diff --git a/deploy/template.yaml b/deploy/template.yaml
index 2f8e94f65..c1d9df243 100644
--- a/deploy/template.yaml
+++ b/deploy/template.yaml
@@ -535,6 +535,15 @@ Resources:
               MOBILE_DEV_FRONTEND_URL: "/perfTest/mobile/dev/frontendUrl"
               MOBILE_DEV_REQ_BODY: "/perfTest/mobile/dev/reqBody"
               MOBILE_DEV_TEST_CLIENT_URL: "/perfTest/mobile/dev/testClientUrl"
+              MOBILE_ID_CHECK_ASYNC_BUILD_PROXY_API_URL: "/perfTest/mobile/idCheckAsync/build/proxyApiUrl"
+              MOBILE_ID_CHECK_ASYNC_BUILD_CLIENT_ID: "/perfTest/mobile/idCheckAsync/build/clientId"
+              MOBILE_ID_CHECK_ASYNC_BUILD_CLIENT_SECRET: "/perfTest/mobile/idCheckAsync/build/clientSecret"
+              MOBILE_ID_CHECK_ASYNC_BUILD_PRIVATE_API_URL: "/perfTest/mobile/idCheckAsync/build/privateApiUrl"
+              MOBILE_ID_CHECK_ASYNC_BUILD_USE_PROXY_API: "/perfTest/mobile/idCheckAsync/build/useProxyApi"
+              MOBILE_ID_CHECK_ASYNC_BUILD_STS_MOCK_API_URL: "/perfTest/mobile/idCheckAsync/build/stsMockApiUrl"
+              MOBILE_ID_CHECK_ASYNC_BUILD_SESSIONS_API_URL: "/perfTest/mobile/idCheckAsync/build/sessionsApiUrl"
+              MOBILE_ID_CHECK_ASYNC_BUILD_READ_ID_MOCK_API_URL: "/perfTest/mobile/idCheckAsync/build/readIdMockApiUrl"
+              MOBILE_ID_CHECK_ASYNC_BUILD_BIOMETRIC_SESSION_TEST_DATA: "/perfTest/mobile/idCheckAsync/build/biometricSessionTestData"
               MOBILE_STS_BUILD_STS_BASE_URL: "/perfTest/mobile/sts/build/baseUrl"
               MOBILE_STS_BUILD_STS_MOCK_CLIENT_BASE_URL: "/perfTest/mobile/sts/build/mockClientBaseUrl"
               MOBILE_STS_BUILD_ORCHESTRATION_BASE_URL: "/perfTest/mobile/sts/build/orchBaseUrl"


Reviews:
user: login: sandymay-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T09:14:27Z
user: login: abhinaysrivastavaperf
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-29T16:04:07Z
user: login: abhinaysrivastavaperf
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-29T17:50:46Z
user: login: sandymay-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T19:55:14Z
user: login: abhinaysrivastavaperf
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-20T14:43:02Z
user: login: abhinaysrivastavaperf
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-20T15:43:09Z
user: login: abhinaysrivastavaperf
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-20T15:53:31Z
user: login: abhinaysrivastavaperf
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-20T15:54:12Z
user: login: maddiemorris
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T16:03:20Z
user: login: sandymay-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T16:29:09Z
user: login: sandymay-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T16:29:59Z
user: login: sandymay-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T16:31:40Z
user: login: abhinaysrivastavaperf
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-20T16:32:48Z
user: login: abhinaysrivastavaperf
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-20T17:24:37Z

Review comments:
path: deploy/scripts/src/mobile-id-check-async/test.ts
user: login: sandymay-dd
body: @abhinaysrivastavaperf I had an error doing an `npm install` and therefore I haven't been able to run the linting. I have a suspicion I might need a PAT to install the data vocab package.
created_at: 2025-04-29T09:14:27Z
author_association: CONTRIBUTOR
path: deploy/scripts/src/mobile-id-check-async/test.ts
user: login: abhinaysrivastavaperf
body: This package has now been removed from the repo. We have replaced it with custom interfaces. Hence, this should not be a problem anymore
created_at: 2025-04-29T16:04:07Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,15 @@
+import { SignatureV4 } from "../../common/utils/jslib/aws-signature"
+import { config } from "./config"
+
+  const credentials = JSON.parse(config.awsExecutionCredentials)
path: deploy/scripts/src/mobile-id-check-async/utils/apiSignatureV4Signer.ts
user: login: abhinaysrivastavaperf
body: ```suggestion
const credentials = (JSON.parse(getEnv('EXECUTION_CREDENTIALS')) as AssumeRoleOutput).Credentials
```
created_at: 2025-04-29T17:48:36Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,15 @@
+import { SignatureV4 } from "../../common/utils/jslib/aws-signature"
+import { config } from "./config"
+
+  const credentials = JSON.parse(config.awsExecutionCredentials)
path: deploy/scripts/src/mobile-id-check-async/utils/apiSignatureV4Signer.ts
user: login: abhinaysrivastavaperf
body: You will need to import the relevant type as well

`import { type AssumeRoleOutput } from '../common/utils/aws/types'`
created_at: 2025-04-29T17:49:48Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,89 @@
+import { type Options } from 'k6/options'
+import {
+  describeProfile,
+  type ProfileList,
+  selectProfile,
+  createScenario,
+  LoadProfile
+} from '../common/utils/config/load-profiles'
+import { getThresholds } from '../common/utils/config/thresholds'
+import { iterationsCompleted, iterationsStarted } from '../common/utils/custom_metric/counter'
+import { createSession } from './testSteps/createSession'
+import { getActiveSession } from './testSteps/getActiveSession'
+import { postBiometricToken } from './testSteps/postBiometricToken'
+import { sleepBetween } from '../common/utils/sleep/sleepBetween'
+import { getWellknownJwks } from './testSteps/getWellKnownJwks'
+import { postTxmaEvent } from './testSteps/postTxmaEvent'
+import { postSetupVendorResponse } from './testSteps/postSetupVendorResponse'
+import { uuidv4 } from '../common/utils/jslib'
+import { postFinishBiometricSession } from './testSteps/postFinishBiometricSession'
+import { postAbortSession } from './testSteps/postAbortSession'
+
+const profiles: ProfileList = {
+  smoke: {
+    ...createScenario('idCheckAsync', LoadProfile.smoke)
+  }
+}
+
+const loadProfile = selectProfile(profiles)
+export const groupMap = {
+  idCheckAsync: [
+    '00 POST /async/token',
+    '01 POST /async/credential',
+    '02 POST sts-mock /token',
+    '03 GET /async/activeSession',
+    '04 POST /async/biometricToken',
+    '05 GET /async/.well-known/jwks.json',
+    '06 POST /async/txmaEvent',
+    '07 POST readid-mock /setupVendorResponse',
+    '08 POST async/finishBiometricSession',
+    '09 POST async/abortSession',
+  ]
+} as const
+
+export const options: Options = {
+  scenarios: loadProfile.scenarios,
+  thresholds: getThresholds(groupMap),
+  tags: { name: '' }
+}
+
+export function setup(): void {
+  describeProfile(loadProfile)
+}
+
+export function idCheckAsync(): void {
+  iterationsStarted.add(1)
+  const sub = createSession()
+  const sessionId = getActiveSession(sub)
+  sleepBetween(0.5, 1)
+  getWellknownJwks()
+  sleepBetween(0.5, 1)
+  const opaqueId = postBiometricToken(sessionId)
+  sleepBetween(0.5, 1)
+  postTxmaEvent(sessionId)
+  sleepBetween(0.5, 1)
+  postTxmaEvent(sessionId)
+  sleepBetween(0.5, 1)
+  postTxmaEvent(sessionId)
+  sleepBetween(0.5, 1)
+  const biometricSessionId = uuidv4()
+  postSetupVendorResponse({ biometricSessionId, opaqueId })
+  sleepBetween(0.5, 1)
+  postFinishBiometricSession({ biometricSessionId, sessionId })
+  sleepBetween(0.5, 1)
+  // postAbortSession(sessionId)
+  iterationsCompleted.add(1)
+}
+
+/*
+At this point we either call:
path: deploy/scripts/src/mobile-id-check-async/test.ts
user: login: sandymay-dd
body: @abhinaysrivastavaperf - would appreciate some guidance on how we should do this - multiple scenarios and split the load?
created_at: 2025-04-29T19:55:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,89 @@
+import { type Options } from 'k6/options'
+import {
+  describeProfile,
+  type ProfileList,
+  selectProfile,
+  createScenario,
+  LoadProfile
+} from '../common/utils/config/load-profiles'
+import { getThresholds } from '../common/utils/config/thresholds'
+import { iterationsCompleted, iterationsStarted } from '../common/utils/custom_metric/counter'
+import { createSession } from './testSteps/createSession'
+import { getActiveSession } from './testSteps/getActiveSession'
+import { postBiometricToken } from './testSteps/postBiometricToken'
+import { sleepBetween } from '../common/utils/sleep/sleepBetween'
+import { getWellknownJwks } from './testSteps/getWellKnownJwks'
+import { postTxmaEvent } from './testSteps/postTxmaEvent'
+import { postSetupVendorResponse } from './testSteps/postSetupVendorResponse'
+import { uuidv4 } from '../common/utils/jslib'
+import { postFinishBiometricSession } from './testSteps/postFinishBiometricSession'
+import { postAbortSession } from './testSteps/postAbortSession'
+
+const profiles: ProfileList = {
+  smoke: {
+    ...createScenario('idCheckAsync', LoadProfile.smoke)
+  }
+}
+
+const loadProfile = selectProfile(profiles)
+export const groupMap = {
+  idCheckAsync: [
+    '00 POST /async/token',
+    '01 POST /async/credential',
+    '02 POST sts-mock /token',
+    '03 GET /async/activeSession',
+    '04 POST /async/biometricToken',
+    '05 GET /async/.well-known/jwks.json',
+    '06 POST /async/txmaEvent',
+    '07 POST readid-mock /setupVendorResponse',
+    '08 POST async/finishBiometricSession',
+    '09 POST async/abortSession',
+  ]
+} as const
+
+export const options: Options = {
+  scenarios: loadProfile.scenarios,
+  thresholds: getThresholds(groupMap),
+  tags: { name: '' }
+}
+
+export function setup(): void {
+  describeProfile(loadProfile)
+}
+
+export function idCheckAsync(): void {
+  iterationsStarted.add(1)
+  const sub = createSession()
+  const sessionId = getActiveSession(sub)
+  sleepBetween(0.5, 1)
+  getWellknownJwks()
+  sleepBetween(0.5, 1)
+  const opaqueId = postBiometricToken(sessionId)
+  sleepBetween(0.5, 1)
+  postTxmaEvent(sessionId)
+  sleepBetween(0.5, 1)
+  postTxmaEvent(sessionId)
+  sleepBetween(0.5, 1)
+  postTxmaEvent(sessionId)
+  sleepBetween(0.5, 1)
+  const biometricSessionId = uuidv4()
+  postSetupVendorResponse({ biometricSessionId, opaqueId })
+  sleepBetween(0.5, 1)
+  postFinishBiometricSession({ biometricSessionId, sessionId })
+  sleepBetween(0.5, 1)
+  // postAbortSession(sessionId)
+  iterationsCompleted.add(1)
+}
+
+/*
+At this point we either call:
path: deploy/scripts/src/mobile-id-check-async/test.ts
user: login: abhinaysrivastavaperf
body: Updated the script to split the traffic 80-20 with 20% of users aborting the journey.
created_at: 2025-05-20T14:43:01Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,89 @@
+import { type Options } from 'k6/options'
+import {
+  describeProfile,
+  type ProfileList,
+  selectProfile,
+  createScenario,
+  LoadProfile
+} from '../common/utils/config/load-profiles'
+import { getThresholds } from '../common/utils/config/thresholds'
+import { iterationsCompleted, iterationsStarted } from '../common/utils/custom_metric/counter'
+import { createSession } from './testSteps/createSession'
+import { getActiveSession } from './testSteps/getActiveSession'
+import { postBiometricToken } from './testSteps/postBiometricToken'
+import { sleepBetween } from '../common/utils/sleep/sleepBetween'
+import { getWellknownJwks } from './testSteps/getWellKnownJwks'
+import { postTxmaEvent } from './testSteps/postTxmaEvent'
+import { postSetupVendorResponse } from './testSteps/postSetupVendorResponse'
+import { uuidv4 } from '../common/utils/jslib'
+import { postFinishBiometricSession } from './testSteps/postFinishBiometricSession'
+import { postAbortSession } from './testSteps/postAbortSession'
+
+const profiles: ProfileList = {
+  smoke: {
+    ...createScenario('idCheckAsync', LoadProfile.smoke)
+  }
+}
+
+const loadProfile = selectProfile(profiles)
+export const groupMap = {
+  idCheckAsync: [
+    '00 POST /async/token',
+    '01 POST /async/credential',
+    '02 POST sts-mock /token',
+    '03 GET /async/activeSession',
+    '04 POST /async/biometricToken',
+    '05 GET /async/.well-known/jwks.json',
+    '06 POST /async/txmaEvent',
+    '07 POST readid-mock /setupVendorResponse',
+    '08 POST async/finishBiometricSession',
+    '09 POST async/abortSession',
+  ]
+} as const
+
+export const options: Options = {
+  scenarios: loadProfile.scenarios,
+  thresholds: getThresholds(groupMap),
+  tags: { name: '' }
+}
+
+export function setup(): void {
+  describeProfile(loadProfile)
+}
+
+export function idCheckAsync(): void {
+  iterationsStarted.add(1)
+  const sub = createSession()
+  const sessionId = getActiveSession(sub)
+  sleepBetween(0.5, 1)
+  getWellknownJwks()
+  sleepBetween(0.5, 1)
+  const opaqueId = postBiometricToken(sessionId)
+  sleepBetween(0.5, 1)
+  postTxmaEvent(sessionId)
+  sleepBetween(0.5, 1)
+  postTxmaEvent(sessionId)
+  sleepBetween(0.5, 1)
+  postTxmaEvent(sessionId)
+  sleepBetween(0.5, 1)
+  const biometricSessionId = uuidv4()
+  postSetupVendorResponse({ biometricSessionId, opaqueId })
+  sleepBetween(0.5, 1)
+  postFinishBiometricSession({ biometricSessionId, sessionId })
+  sleepBetween(0.5, 1)
+  // postAbortSession(sessionId)
+  iterationsCompleted.add(1)
+}
+
+/*
+At this point we either call:
path: deploy/scripts/src/mobile-id-check-async/test.ts
user: login: abhinaysrivastavaperf
body: Resolving the conversation and deleting the commented bit.
created_at: 2025-05-20T15:43:08Z
author_association: COLLABORATOR
path: deploy/scripts/src/mobile-id-check-async/test.ts
user: login: abhinaysrivastavaperf
body: I have run linting on all the files and pushed a commit to fix the errors
created_at: 2025-05-20T15:53:31Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,15 @@
+import { SignatureV4 } from "../../common/utils/jslib/aws-signature"
+import { config } from "./config"
+
+  const credentials = JSON.parse(config.awsExecutionCredentials)
path: deploy/scripts/src/mobile-id-check-async/utils/apiSignatureV4Signer.ts
user: login: abhinaysrivastavaperf
body: Have updated the script to use `AssumeRoleOutput` in `apiSignatureV4Signer.ts`
created_at: 2025-05-20T15:54:12Z
author_association: COLLABORATOR
diff_hunk: @@ -535,6 +535,15 @@ Resources:
               MOBILE_DEV_FRONTEND_URL: "/perfTest/mobile/dev/frontendUrl"
               MOBILE_DEV_REQ_BODY: "/perfTest/mobile/dev/reqBody"
               MOBILE_DEV_TEST_CLIENT_URL: "/perfTest/mobile/dev/testClientUrl"
+              MOBILE_ID_CHECK_ASYNC_BUILD_PROXY_API_URL: "/perfTest/mobile/idChekcAsync/build/proxyApiUrl"
path: deploy/template.yaml
user: login: sandymay-dd
body: type in the parameter path (`idChekcAsync`)
created_at: 2025-05-20T16:29:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -535,6 +535,15 @@ Resources:
               MOBILE_DEV_FRONTEND_URL: "/perfTest/mobile/dev/frontendUrl"
               MOBILE_DEV_REQ_BODY: "/perfTest/mobile/dev/reqBody"
               MOBILE_DEV_TEST_CLIENT_URL: "/perfTest/mobile/dev/testClientUrl"
+              MOBILE_ID_CHECK_ASYNC_BUILD_PROXY_API_URL: "/perfTest/mobile/idChekcAsync/build/proxyApiUrl"
path: deploy/template.yaml
user: login: sandymay-dd
body: (and the ones below)
created_at: 2025-05-20T16:29:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -535,6 +535,15 @@ Resources:
               MOBILE_DEV_FRONTEND_URL: "/perfTest/mobile/dev/frontendUrl"
               MOBILE_DEV_REQ_BODY: "/perfTest/mobile/dev/reqBody"
               MOBILE_DEV_TEST_CLIENT_URL: "/perfTest/mobile/dev/testClientUrl"
+              MOBILE_ID_CHECK_ASYNC_BUILD_PROXY_API_URL: "/perfTest/mobile/idChekcAsync/build/proxyApiUrl"
+              MOBILE_ID_CHECK_ASYNC_BUILD_CLIENT_ID: "/perfTest/mobile/idChekcAsync/build/clientId"
+              MOBILE_ID_CHECK_ASYNC_BUILD_CLIENT_SECRET: "/perfTest/mobile/idChekcAsync/build/clientSecret"
+              MOBILE_ID_CHECK_ASYNC_BUILD_PRIVATE_API_URL: "/perfTest/mobile/idChekcAsync/build/privateApiUrl"
+              MOBILE_ID_CHECK_ASYNC_BUILD_USE_PROXY_API: "/perfTest/mobile/idChekcAsync/build/proxyApi"
path: deploy/template.yaml
user: login: sandymay-dd
body: nit: it might be clearer for the parameter to be called `/perfTest/mobile/idChekcAsync/build/useProxyApi`

as a reminder: this needs to be set to `false` (or any string that is not `true`) in SSM to ensure use the Private API
created_at: 2025-05-20T16:31:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -535,6 +535,15 @@ Resources:
               MOBILE_DEV_FRONTEND_URL: "/perfTest/mobile/dev/frontendUrl"
               MOBILE_DEV_REQ_BODY: "/perfTest/mobile/dev/reqBody"
               MOBILE_DEV_TEST_CLIENT_URL: "/perfTest/mobile/dev/testClientUrl"
+              MOBILE_ID_CHECK_ASYNC_BUILD_PROXY_API_URL: "/perfTest/mobile/idChekcAsync/build/proxyApiUrl"
path: deploy/template.yaml
user: login: abhinaysrivastavaperf
body: Fixed
created_at: 2025-05-20T16:32:47Z
author_association: COLLABORATOR
diff_hunk: @@ -535,6 +535,15 @@ Resources:
               MOBILE_DEV_FRONTEND_URL: "/perfTest/mobile/dev/frontendUrl"
               MOBILE_DEV_REQ_BODY: "/perfTest/mobile/dev/reqBody"
               MOBILE_DEV_TEST_CLIENT_URL: "/perfTest/mobile/dev/testClientUrl"
+              MOBILE_ID_CHECK_ASYNC_BUILD_PROXY_API_URL: "/perfTest/mobile/idChekcAsync/build/proxyApiUrl"
+              MOBILE_ID_CHECK_ASYNC_BUILD_CLIENT_ID: "/perfTest/mobile/idChekcAsync/build/clientId"
+              MOBILE_ID_CHECK_ASYNC_BUILD_CLIENT_SECRET: "/perfTest/mobile/idChekcAsync/build/clientSecret"
+              MOBILE_ID_CHECK_ASYNC_BUILD_PRIVATE_API_URL: "/perfTest/mobile/idChekcAsync/build/privateApiUrl"
+              MOBILE_ID_CHECK_ASYNC_BUILD_USE_PROXY_API: "/perfTest/mobile/idChekcAsync/build/proxyApi"
path: deploy/template.yaml
user: login: abhinaysrivastavaperf
body: This has been done
created_at: 2025-05-20T17:24:37Z
author_association: COLLABORATOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-05-20T16:38:35Z
body: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=govuk-one-login_performance-testing&pullRequest=1255) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [1 Security Hotspot](https://sonarcloud.io/project/security_hotspots?id=govuk-one-login_performance-testing&pullRequest=1255&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=govuk-one-login_performance-testing&pullRequest=1255)


author_association: NONE