Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PYIC-7785: Add API tests to complete a strategic app journey #2833

Closed
wants to merge 13 commits into from
Closed
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
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
113 changes: 112 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,117 @@ 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 an 'check-mobile-app-result' page response
When I poll for async DCMAW credential receipt
Then the poll returns a '201'
MikeCollingwood marked this conversation as resolved.
Show resolved Hide resolved
And 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
When 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
Then I pass on the DCMAW callback in a separate session
# Mocking the time for the user to log back in
And I poll for async DCMAW credential receipt
When 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'
And 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'
And I submit the returned journey event
Then I get an 'pyi-no-match' page response

Scenario: MAM journey detected iphone
MikeCollingwood marked this conversation as resolved.
Show resolved Hide resolved
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}`,
);
}
});
123 changes: 122 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,122 @@ 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 (!this.oauthState) {
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
6 changes: 5 additions & 1 deletion api-tests/src/types/world.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { JourneyEngineResponse } from "./internal-api.js";
import { JourneyEngineResponse, JourneyResponse } from "./internal-api.js";
import { MfaResetResult, UserIdentity } from "./external-api.js";
import { World as CucumberWorld } from "@cucumber/cucumber";
import { VcJwtPayload } from "./external-api.js";
Expand Down Expand Up @@ -33,4 +33,8 @@ export interface World extends CucumberWorld {

// Latest error to assert against
error?: Error;

// Strategic app latent variables
oauthState?: string;
strategicAppPollResult?: JourneyResponse;
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ public APIGatewayProxyResponseEvent handleRequest(
}

// Frontend will continue polling
return ApiGatewayResponseGenerator.proxyResponse(HttpStatus.SC_NOT_FOUND);
return ApiGatewayResponseGenerator.proxyJsonResponse(HttpStatus.SC_NOT_FOUND, null);
} catch (HttpResponseExceptionWithErrorBody | VerifiableCredentialException e) {
return buildErrorResponse(e, HttpStatus.SC_BAD_REQUEST, e.getErrorResponse());
} catch (IpvSessionNotFoundException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.StringContains.containsString;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
Expand Down Expand Up @@ -237,7 +236,6 @@ void shouldReturn404WhenVcNotFound() throws Exception {

// Assert
assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode());
assertNull(response.getBody());
verify(sessionCredentialsService, never()).persistCredentials(any(), any(), anyBoolean());
}

Expand Down
Loading
Loading