Skip to content

Commit

Permalink
Pyic 7785: Add API tests to complete a strategic app journey (#2870)
Browse files Browse the repository at this point in the history
  • Loading branch information
DanCorderIPV authored Jan 15, 2025
2 parents 86889a8 + e704325 commit 1176155
Show file tree
Hide file tree
Showing 15 changed files with 360 additions and 16 deletions.
2 changes: 1 addition & 1 deletion api-tests/cucumber.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const base = {
publish: false,
retry: 0,
loader: ["ts-node/esm"],
import: ["src/steps/**/*.ts", "src/config/**/*.ts"],
import: ["src/steps/**/*.ts", "src/config/**/*.ts", "src/hooks.ts"],
};

export default base;
Expand Down
115 changes: 114 additions & 1 deletion api-tests/features/p2-strategic-app.feature
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@Build
@Build @InitialisesDCMAWSessionState
Feature: M2B Strategic App Journeys

Scenario: MAM journey declared iphone
Expand All @@ -13,6 +13,119 @@ Feature: M2B Strategic App Journeys
Then I get a 'pyi-triage-select-smartphone' page response with context 'mam'
When I submit an 'iphone' event
Then I get a 'pyi-triage-mobile-download-app' page response with context 'iphone'
When the DCMAW CRI produces a 'kennethD' 'ukChippedPassport' 'success' VC
# And the user returns from the app to core-front
And I pass on the DCMAW callback
Then I get a 'check-mobile-app-result' page response
When I poll for async DCMAW credential receipt
Then the poll returns a '201'
When I submit the returned journey event
Then I get a 'page-dcmaw-success' page response
When I submit a 'next' event
Then I get an 'address' CRI response
When I submit 'kenneth-current' details to the CRI stub
Then I get a 'fraud' CRI response
When I submit 'kenneth-score-2' details to the CRI stub
Then I get a 'page-ipv-success' page response
When I submit a 'next' event
Then I get an OAuth response
When I use the OAuth response to get my identity
Then I get a 'P2' identity

Scenario: MAM journey pending credential
Given I activate the 'strategicApp' feature set
When I start a new 'medium-confidence' journey
Then I get a 'page-ipv-identity-document-start' page response
When I submit an 'appTriage' event
Then I get a 'identify-device' page response
When I submit an 'appTriage' event
Then I get a 'pyi-triage-select-device' page response
When I submit a 'smartphone' event
Then I get a 'pyi-triage-select-smartphone' page response with context 'mam'
When I submit an 'iphone' event
Then I get a 'pyi-triage-mobile-download-app' page response with context 'iphone'
# And the user returns from the app to core-front
When I pass on the DCMAW callback
Then I get an 'check-mobile-app-result' page response
When I poll for async DCMAW credential receipt
Then the poll returns a '404'
When the DCMAW CRI produces a 'kennethD' 'ukChippedPassport' 'success' VC
And I poll for async DCMAW credential receipt
Then the poll returns a '201'

Scenario: MAM journey cross-browser scenario
Given I activate the 'strategicApp' feature set
When I start a new 'medium-confidence' journey
Then I get a 'page-ipv-identity-document-start' page response
When I submit an 'appTriage' event
Then I get a 'identify-device' page response
When I submit an 'appTriage' event
Then I get a 'pyi-triage-select-device' page response
When I submit a 'smartphone' event
Then I get a 'pyi-triage-select-smartphone' page response with context 'mam'
When I submit an 'iphone' event
Then I get a 'pyi-triage-mobile-download-app' page response with context 'iphone'
When the DCMAW CRI produces a 'kennethD' 'ukChippedPassport' 'success' VC
# And the user returns from the app to core-front
And I pass on the DCMAW callback in a separate session
Then I get an error response with message 'Missing ipv session id header' and status code '400'
# Wait for the VC to be received before continuing. In the usual case the VC will be received well before the user
# has managed to log back in to the site.
When I poll for async DCMAW credential receipt
And I start a new 'medium-confidence' journey
Then I get a 'page-dcmaw-success' page response
When I submit a 'next' event
Then I get an 'address' CRI response
When I submit 'kenneth-current' details to the CRI stub
Then I get a 'fraud' CRI response
When I submit 'kenneth-score-2' details to the CRI stub
Then I get a 'page-ipv-success' page response
When I submit a 'next' event
Then I get an OAuth response
When I use the OAuth response to get my identity
Then I get a 'P2' identity

Scenario: MAM journey credential fails with no ci
Given I activate the 'strategicApp' feature set
When I start a new 'medium-confidence' journey
Then I get a 'page-ipv-identity-document-start' page response
When I submit an 'appTriage' event
Then I get a 'identify-device' page response
When I submit an 'appTriage' event
Then I get a 'pyi-triage-select-device' page response
When I submit a 'smartphone' event
Then I get a 'pyi-triage-select-smartphone' page response with context 'mam'
When I submit an 'iphone' event
Then I get a 'pyi-triage-mobile-download-app' page response with context 'iphone'
When the DCMAW CRI produces a 'kennethD' 'ukChippedPassport' 'fail' VC
# And the user returns from the app to core-front
And I pass on the DCMAW callback
Then I get an 'check-mobile-app-result' page response
When I poll for async DCMAW credential receipt
Then the poll returns a '201'
When I submit the returned journey event
Then I get an 'page-multiple-doc-check' page response

Scenario: MAM journey credential fails with ci
Given I activate the 'strategicApp' feature set
When I start a new 'medium-confidence' journey
Then I get a 'page-ipv-identity-document-start' page response
When I submit an 'appTriage' event
Then I get a 'identify-device' page response
When I submit an 'appTriage' event
Then I get a 'pyi-triage-select-device' page response
When I submit a 'smartphone' event
Then I get a 'pyi-triage-select-smartphone' page response with context 'mam'
When I submit an 'iphone' event
Then I get a 'pyi-triage-mobile-download-app' page response with context 'iphone'
When the DCMAW CRI produces a 'kennethD' 'ukChippedPassport' 'fail' VC with a CI
# And the user returns from the app to core-front
And I pass on the DCMAW callback
Then I get an 'check-mobile-app-result' page response
When I poll for async DCMAW credential receipt
Then the poll returns a '201'
When I submit the returned journey event
Then I get an 'pyi-no-match' page response

Scenario: MAM journey detected iphone
Given I activate the 'strategicApp' feature set
Expand Down
52 changes: 52 additions & 0 deletions api-tests/src/clients/core-back-internal-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,58 @@ export const sendJourneyEvent = async (
return (await response.json()) as JourneyEngineResponse;
};

export const callbackFromStrategicApp = async (
oauthState: string,
ipvSessionId: string | undefined,
featureSet: string | undefined,
): Promise<JourneyEngineResponse> => {
const url = `${config.core.internalApiUrl}/app/callback`;
const response = await fetch(url, {
method: POST,
headers: {
"Content-Type": "application/json",
...(featureSet ? { "feature-set": featureSet } : {}),
...(ipvSessionId ? { "ipv-session-id": ipvSessionId } : {}),
},
body: JSON.stringify({ state: oauthState }),
});

if (!response.ok) {
throw new Error(
`callbackFromStrategicApp request failed: ${response.statusText}`,
);
}

const body = await response.json();

return await sendJourneyEvent(body?.journey, ipvSessionId, featureSet);
};

export const pollAsyncDcmaw = async (
ipvSessionId: string | undefined,
featureSet: string | undefined,
): Promise<JourneyResponse | undefined> => {
const url = `${config.core.internalApiUrl}/app/check-vc-receipt`;
const response = await fetch(url, {
method: POST,
headers: {
"Content-Type": "application/json",
...(featureSet ? { "feature-set": featureSet } : {}),
...(ipvSessionId ? { "ipv-session-id": ipvSessionId } : {}),
},
});

if (response.ok) {
return response.json();
}

if (response.status === 404) {
return;
}

throw new Error(`pollAsyncDcmaw request failed: ${response.statusText}`);
};

export const processCriCallback = async (
requestBody: ProcessCriCallbackRequest,
ipvSessionId: string | undefined,
Expand Down
20 changes: 20 additions & 0 deletions api-tests/src/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { After } from "@cucumber/cucumber";

After({ tags: "@InitialisesDCMAWSessionState" }, async function () {
const response = await fetch(
`https://dcmaw-async.stubs.account.gov.uk/management/cleanupDcmawState`,
{
method: "POST",
body: JSON.stringify({
user_id: this.userId,
}),
redirect: "manual",
},
);

if (response.status !== 200) {
throw new Error(
`DCMAW session state cleanup request failed: ${response.statusText}`,
);
}
});
125 changes: 124 additions & 1 deletion api-tests/src/steps/cri-steps.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DataTable, When } from "@cucumber/cucumber";
import { DataTable, Then, When } from "@cucumber/cucumber";
import { World } from "../types/world.js";
import * as internalClient from "../clients/core-back-internal-client.js";
import * as criStubClient from "../clients/cri-stub-client.js";
Expand All @@ -25,6 +25,11 @@ import {
} from "../types/cri-stub.js";
import { getRandomString } from "../utils/random-string-generator.js";
import assert from "assert";
import {
callbackFromStrategicApp,
pollAsyncDcmaw,
} from "../clients/core-back-internal-client.js";
import config from "../config/config.js";

const EXPIRED_NBF = 1658829758; // 26/07/2022 in epoch seconds
const STANDARD_JAR_VALUES = [
Expand Down Expand Up @@ -437,6 +442,124 @@ When(
},
);

const postToEnqueue = async (body: object) => {
const response = await fetch(
`https://dcmaw-async.stubs.account.gov.uk/management/enqueueVc`,
{
method: "POST",
body: JSON.stringify(body),
redirect: "manual",
},
);

if (response.status !== 201) {
throw new Error(`DCMAW enqueue request failed: ${response.statusText}`);
}

const responsePayload = await response.json();
if (
!responsePayload.oauthState ||
typeof responsePayload.oauthState !== "string"
) {
throw new Error(
`DCMAW enqueue request did not return a string oauthState: ${responsePayload.oauthState}`,
);
}
return responsePayload.oauthState;
};

When(
/^the DCMAW CRI produces a '([\w-]+)' '([\w-]+)' '([\w-]+)' VC( with a CI)?$/,
async function (
this: World,
testUser: string,
documentType: string,
evidenceType: string,
hasCi: " with a CI" | undefined,
): Promise<void> {
this.oauthState = await postToEnqueue({
user_id: this.userId,
test_user: testUser,
document_type: documentType,
evidence_type: evidenceType,
queue_name: config.asyncQueue.name,
ci: hasCi && ["BREACHING"],
});
},
);

When(
/^I pass on the DCMAW callback( in a separate session)?$/,
async function (
this: World,
separateSession: " in a separate session" | undefined,
): Promise<void> {
// If we've asked the stub to create a VC for us, we will already have the OAuth state.
if (!this.oauthState) {
// If we post to the stub's Enqueue endpoint without specifying VC details it just returns the OAuth state to us.
this.oauthState = await postToEnqueue({
user_id: this.userId,
});
}

if (!this.oauthState) {
throw new Error("Oauth state must not be undefined");
}

this.lastJourneyEngineResponse = await callbackFromStrategicApp(
this.oauthState,
separateSession ? undefined : this.ipvSessionId,
this.featureSet,
);
},
);

When(
"I poll for async DCMAW credential receipt",
async function (this: World): Promise<void> {
let numberOfAttempts = 0;
while (numberOfAttempts < 10 && !this.strategicAppPollResult) {
this.strategicAppPollResult = await pollAsyncDcmaw(
this.ipvSessionId,
this.featureSet,
);
numberOfAttempts++;

await new Promise((resolve) => setTimeout(resolve, 1000));
}
},
);

When(
"I submit the returned journey event",
async function (this: World): Promise<void> {
if (!this.strategicAppPollResult?.journey) {
throw new Error("Poll result must have a journey event.");
}

this.lastJourneyEngineResponse = await internalClient.sendJourneyEvent(
this.strategicAppPollResult.journey,
this.ipvSessionId,
this.featureSet,
this.clientOAuthSessionId,
);
},
);

Then(
/^the poll returns a '(\d+)'$/,
async function (this: World, statusCode: number): Promise<void> {
// Assuming the poll fails whenever the status is not OK or Not Found.
// These cases are distinguished by whether a body was returned or not.
if (statusCode === 201 && !this.strategicAppPollResult?.journey) {
throw new Error("Poll should returned a journey.");
}
if (statusCode === 404 && this.strategicAppPollResult?.journey) {
throw new Error("Poll should have not returned a journey.");
}
},
);

const assertNoUnexpectedJarProperties = (
jarPayload: CriStubResponseJarPayload,
expectedNonStandardValues: string[] | undefined = undefined,
Expand Down
17 changes: 17 additions & 0 deletions api-tests/src/steps/ipv-steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { getRandomString } from "../utils/random-string-generator.js";
import {
isClientResponse,
isCriResponse,
isErrorResponse,
isJourneyResponse,
isPageResponse,
JourneyEngineResponse,
Expand Down Expand Up @@ -164,6 +165,22 @@ Then(
},
);

Then(
/I get an error response with message '([\w,: ]+)' and status code '(\d{3})'/,
function (this: World, expectedMessage: string, expectedStatusCode: number) {
if (!this.lastJourneyEngineResponse) {
throw new Error("No last journey engine response found.");
}

assert.ok(
isErrorResponse(this.lastJourneyEngineResponse),
`got a ${describeResponse(this.lastJourneyEngineResponse)}`,
);
assert.equal(this.lastJourneyEngineResponse.message, expectedMessage);
assert.equal(this.lastJourneyEngineResponse.statusCode, expectedStatusCode);
},
);

When(
"I start a new {string} inherited identity journey with an invalid inherited identity JWT",
async function (this: World, journeyType: string): Promise<void> {
Expand Down
Loading

0 comments on commit 1176155

Please sign in to comment.