Skip to content

Commit 2236170

Browse files
authored
Add RP onboarding E2E test (#1390)
1 parent 700328f commit 2236170

16 files changed

Lines changed: 361 additions & 8 deletions

README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,44 @@ These tests are split into two projects based on their execution patterns. The m
187187

188188
Running `npm run test:integration` will run all tests concurrently in two projects. The Step Function executions are isolated between the different test suites. The full test suite typically takes approximately 14 minutes to complete. Tests automatically clean up all test data from S3 and Athena after completion.
189189

190+
#### E2E tests
191+
192+
E2E tests validate the full data pipeline from event ingestion through to the conform layer in the _staging_ environment. They send events through event-processing and verify the data arrives correctly in the conform layer, ensuring it has been processed successfully by both self serve and DAP.
193+
194+
There are two types of BAU E2E tests:
195+
* **RP Onboarding** — verifies that a new relying party's events are correctly reflected in the Redshift dim and ref tables
196+
* **Event Onboarding** — verifies that a new event type is processed and lands in the conform layer correctly (coming soon)
197+
198+
A daily E2E test to validate the full end-to-end flow will also be added in the future.
199+
200+
###### RP Onboarding
201+
202+
The RP onboarding test verifies that when a new relying party's events flow through the full pipeline, the correct entries appear in the Redshift `dim_relying_party_refactored` and `ref_relying_parties_refactored` tables.
203+
204+
Prerequisites:
205+
* Set up ~/.aws/config file with the correct AWS credentials
206+
* Login to AWS:
207+
```sh
208+
export AWS_PROFILE=data-dap-staging
209+
aws sso login
210+
```
211+
212+
Test configuration is in [tests/e2e-tests/config/rp-onboarding.config.ts](tests/e2e-tests/config/rp-onboarding.config.ts). Each entry specifies a `clientId` and the expected values for the dim and ref tables. To test a new RP, add an entry to the `rpOnboardingTestEvents` array — see the comments in that file for examples.
213+
214+
There are two ways to run the test:
215+
216+
```sh
217+
# Full run — sends events to SQS, waits for events to be processed by event-processing and self serve and arrive in the DAP raw layer, runs the step function and checks the tables in the DAP conform layer
218+
# Takes ~25 minutes due to event processing and ETL execution
219+
npm run test:e2e:rp-onboarding
220+
221+
# Check only — skips event sending and step function execution, just queries Redshift
222+
# Use this to verify tables are already populated (e.g. after a previous full run)
223+
npm run test:e2e:rp-onboarding:check-only
224+
```
225+
226+
The test output includes a comparison table showing expected vs actual values for each client_id. Mismatched columns are highlighted in red with a summary below the table.
227+
190228
#### Test reports
191229

192230
After running unit tests, a test report called `index.html` will be available in the [test-report](test-report) directory.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,9 @@
8383
"test:integration": "NODE_OPTIONS='--experimental-vm-modules' jest -c ./tests/integration-tests/jest.config.unified.ts --runInBand",
8484
"test:integration:ci": "NODE_OPTIONS='--experimental-vm-modules' jest -c ./tests/integration-tests/jest.config.unified.ts --runInBand",
8585
"test:integration:main": "NODE_OPTIONS='--experimental-vm-modules' jest -c ./tests/integration-tests/jest.config.main-test-suite.ts --runInBand",
86-
"test:integration:raw-to-stage-unhappy": "NODE_OPTIONS='--experimental-vm-modules' jest -c ./tests/integration-tests/jest.config.raw-to-stage-unhappy-path.ts --runInBand"
86+
"test:integration:raw-to-stage-unhappy": "NODE_OPTIONS='--experimental-vm-modules' jest -c ./tests/integration-tests/jest.config.raw-to-stage-unhappy-path.ts --runInBand",
87+
"test:e2e:rp-onboarding": "NODE_OPTIONS='--experimental-vm-modules' jest -c ./tests/e2e-tests/jest.config.rp-onboarding.ts --runInBand",
88+
"test:e2e:rp-onboarding:check-only": "NODE_OPTIONS='--experimental-vm-modules' jest -c ./tests/e2e-tests/jest.config.rp-onboarding-check-only.ts --runInBand"
8789
},
8890
"lint-staged": {
8991
"*": "prettier --write",
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* RP Onboarding E2E Test Configuration
3+
*
4+
* TO TEST A SINGLE RP:
5+
* Update the clientId and expected values in the existing entry below.
6+
*
7+
* TO TEST MULTIPLE RPs AT ONCE:
8+
* Add another entry to the rpOnboardingTestEvents array before the closing ].
9+
* Copy and paste the example below:
10+
*
11+
* {
12+
* clientId: 'your-client-id',
13+
* expectedDimRelyingParty: {
14+
* department_name: '...',
15+
* relying_party_name: '...',
16+
* agency_name: '...',
17+
* display_name: '...',
18+
* },
19+
* expectedRefRelyingParties: {
20+
* department_name: '...',
21+
* client_name: '...',
22+
* agency_name: '...',
23+
* display_name: '...',
24+
* },
25+
* },
26+
*/
27+
28+
export interface RpOnboardingTestEvent {
29+
clientId: string;
30+
expectedDimRelyingParty: {
31+
department_name: string;
32+
relying_party_name: string;
33+
agency_name: string;
34+
display_name: string;
35+
};
36+
expectedRefRelyingParties: {
37+
department_name: string;
38+
client_name: string;
39+
agency_name: string;
40+
display_name: string;
41+
};
42+
}
43+
44+
export const rpOnboardingTestEvents: RpOnboardingTestEvent[] = [
45+
// ──── Event 1 ────
46+
{
47+
clientId: 'sXr5F6w5QytPPJN-Dtsgbl6hegQ',
48+
expectedDimRelyingParty: {
49+
department_name: 'HO',
50+
relying_party_name: 'Foreign Influence Registration Scheme',
51+
agency_name: 'HSG',
52+
display_name: 'HO - Foreign Influence Registration Scheme',
53+
},
54+
expectedRefRelyingParties: {
55+
department_name: 'HO',
56+
client_name: 'Foreign Influence Registration Scheme',
57+
agency_name: 'HSG',
58+
display_name: 'HO - Foreign Influence Registration Scheme',
59+
},
60+
},
61+
// ──── Add new events below this line ────
62+
];
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import {
2+
sharedSsmMappings,
3+
setEnvVarsFromSsm,
4+
formatTestStackSsmParam,
5+
} from '../../../shared-test-code/config/ssm-config';
6+
7+
const e2eSsmMappings = {
8+
...sharedSsmMappings,
9+
DAP_E2E_TEST_PRODUCER_QUEUE_URL: formatTestStackSsmParam('dapE2ETestProducerQueueUrl'),
10+
};
11+
12+
export const setE2EEnvVarsFromSsm = async () => setEnvVarsFromSsm(e2eSsmMappings);
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/* eslint-disable no-console */
2+
3+
const red = (s: string) => `\x1b[31m${s}\x1b[0m`;
4+
const green = (s: string) => `\x1b[32m${s}\x1b[0m`;
5+
6+
export const printResults = (
7+
tableName: string,
8+
expected: Record<string, string>,
9+
actual: Record<string, string | number>[],
10+
) => {
11+
const columns = Object.keys(expected);
12+
const allRows = [expected, ...actual];
13+
const widths = columns.map(col => Math.max(col.length, ...allRows.map(row => String(row[col] ?? '').length)));
14+
const separator = '─'.repeat(10) + '┼' + widths.map(w => '─'.repeat(w + 2)).join('┼');
15+
const header = ' '.repeat(10) + '│' + columns.map((col, i) => ` ${col.padEnd(widths[i])} `).join('│');
16+
const formatRow = (row: Record<string, string | number>) =>
17+
columns.map((col, i) => ` ${String(row[col] ?? '').padEnd(widths[i])} `).join('│');
18+
19+
const lines = [
20+
`\n📋 ${tableName}`,
21+
` ${header}`,
22+
` ${separator}`,
23+
` ${'Expected'.padEnd(10)}${formatRow(expected)}`,
24+
];
25+
26+
if (actual.length === 0) {
27+
lines.push(
28+
` ${'Actual'.padEnd(10)}${columns.map((_, i) => ' '.repeat(widths[i] + 2)).join('│')} (no rows found) ❌`,
29+
);
30+
} else {
31+
for (const row of actual) {
32+
const mismatches: string[] = [];
33+
const cells = columns.map((col, i) => {
34+
const expectedVal = String(expected[col] ?? '');
35+
const actualVal = String(row[col] ?? '');
36+
const padded = ` ${actualVal.padEnd(widths[i])} `;
37+
if (expectedVal !== actualVal) {
38+
mismatches.push(col);
39+
return red(padded);
40+
}
41+
return green(padded);
42+
});
43+
const match = mismatches.length === 0;
44+
lines.push(` ${'Actual'.padEnd(10)}${cells.join('│')} ${match ? '✅' : '❌'}`);
45+
if (!match) {
46+
lines.push(`\n ⚠️ Mismatched columns: ${mismatches.join(', ')}`);
47+
for (const col of mismatches) {
48+
lines.push(
49+
` ${col}: expected ${red(String(expected[col] ?? ''))} but got ${green(String(row[col] ?? ''))}`,
50+
);
51+
}
52+
}
53+
}
54+
}
55+
56+
lines.push('');
57+
console.log(lines.join('\n'));
58+
};
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { getTestEnv } from '../../../shared-test-code/utils/get-test-env';
2+
import { E2ETestEnvName } from '../../types/e2e-test-env';
3+
4+
export const getE2ETestEnv = (name: E2ETestEnvName): string => getTestEnv(name);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { JestConfigWithTsJest } from 'ts-jest';
2+
3+
const config: JestConfigWithTsJest = {
4+
preset: 'ts-jest',
5+
verbose: true,
6+
testMatch: ['**/tests/e2e-tests/test-suites/rp-onboarding/**/*.spec.ts'],
7+
globalSetup: '<rootDir>/setup-rp-onboarding-check-only.ts',
8+
testTimeout: 600000,
9+
reporters: [
10+
'default',
11+
[
12+
'jest-junit',
13+
{
14+
suiteName: 'RP Onboarding e2e tests (check only)',
15+
outputDirectory: '<rootDir>/reports',
16+
ancestorSeparator: ',',
17+
includeConsoleOutput: true,
18+
},
19+
],
20+
],
21+
};
22+
23+
export default config;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { JestConfigWithTsJest } from 'ts-jest';
2+
3+
const config: JestConfigWithTsJest = {
4+
preset: 'ts-jest',
5+
verbose: true,
6+
testMatch: ['**/tests/e2e-tests/test-suites/rp-onboarding/**/*.spec.ts'],
7+
globalSetup: '<rootDir>/setup-rp-onboarding.ts',
8+
testTimeout: 600000,
9+
reporters: [
10+
'default',
11+
[
12+
'jest-junit',
13+
{
14+
suiteName: 'RP Onboarding e2e tests',
15+
outputDirectory: '<rootDir>/reports',
16+
ancestorSeparator: ',',
17+
includeConsoleOutput: true,
18+
},
19+
],
20+
],
21+
};
22+
23+
export default config;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { AWS_REGION, STACK_NAME } from '../shared-test-code/constants';
2+
import { grantRedshiftAccess } from '../shared-test-code/aws/redshift/grant-access';
3+
import { setE2EEnvVarsFromSsm } from './helpers/config/ssm-config';
4+
import { getE2ETestEnv } from './helpers/utils/utils';
5+
6+
export default async function globalSetup() {
7+
process.env.STACK_NAME = process.env.STACK_NAME ?? STACK_NAME;
8+
process.env.AWS_REGION = process.env.AWS_REGION ?? AWS_REGION;
9+
10+
await setE2EEnvVarsFromSsm();
11+
await grantRedshiftAccess(getE2ETestEnv('REDSHIFT_WORKGROUP_NAME'));
12+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/* eslint-disable no-console */
2+
import { AWS_REGION, STACK_NAME } from '../shared-test-code/constants';
3+
import { addMessageToQueue } from '../shared-test-code/aws/sqs/add-message-to-queue';
4+
import { executeStepFunction } from '../shared-test-code/aws/step-function/execute-step-function';
5+
import { generateTimestamp, generateTimestampFormatted } from '../shared-test-code/utils';
6+
import { pollForRawLayerData, pollForStageLayerData } from '../shared-test-code/poll-for-athena-data';
7+
import { pollForFactJourneyData } from '../shared-test-code/poll-for-redshift-data';
8+
import { grantRedshiftAccess } from '../shared-test-code/aws/redshift/grant-access';
9+
import { setE2EEnvVarsFromSsm } from './helpers/config/ssm-config';
10+
import { constructTestEvent } from './test-events/rp-onboarding-test-event';
11+
import { rpOnboardingTestEvents } from './config/rp-onboarding.config';
12+
import { getE2ETestEnv } from './helpers/utils/utils';
13+
import { AuditEvent } from '../../common/types/event';
14+
15+
export default async function globalSetup() {
16+
const setupStartTime = Date.now();
17+
18+
try {
19+
process.env.STACK_NAME = process.env.STACK_NAME ?? STACK_NAME;
20+
process.env.AWS_REGION = process.env.AWS_REGION ?? AWS_REGION;
21+
22+
await setE2EEnvVarsFromSsm();
23+
await grantRedshiftAccess(getE2ETestEnv('REDSHIFT_WORKGROUP_NAME'));
24+
25+
const queueUrl = getE2ETestEnv('DAP_E2E_TEST_PRODUCER_QUEUE_URL');
26+
const timestamp = generateTimestamp();
27+
const timestampFormatted = generateTimestampFormatted();
28+
29+
const sentEvents: AuditEvent[] = [];
30+
for (const testEvent of rpOnboardingTestEvents) {
31+
const event = constructTestEvent(testEvent.clientId, timestamp, timestampFormatted);
32+
await addMessageToQueue(event, queueUrl);
33+
sentEvents.push(event);
34+
console.log(`📤 Sent event:`, JSON.stringify(event, null, 2));
35+
}
36+
37+
const eventIds = sentEvents.map(e => e.event_id);
38+
39+
console.log(
40+
'⏳ Waiting 10 mins before polling (event-processing transit + DAP batching can take up to 15 mins)...',
41+
);
42+
const rawPollStart = Date.now();
43+
await new Promise(resolve => setTimeout(resolve, 10 * 60 * 1000));
44+
45+
console.log('⏳ Now polling for events in raw layer (up to 15 mins)...');
46+
await pollForRawLayerData(eventIds, {
47+
maxWaitTimeMs: 15 * 60 * 1000,
48+
pollIntervalMs: 10000,
49+
});
50+
console.log(`✅ Events found in raw layer after ${Math.round((Date.now() - rawPollStart) / 1000)}s`);
51+
52+
const rawToStageStepFunction = getE2ETestEnv('RAW_TO_STAGE_STEP_FUNCTION');
53+
await executeStepFunction(rawToStageStepFunction, undefined, 'rp-onboarding-e2e', 25 * 60 * 1000);
54+
await pollForStageLayerData(eventIds, {
55+
maxWaitTimeMs: 10 * 60 * 1000,
56+
pollIntervalMs: 10000,
57+
});
58+
await pollForFactJourneyData(eventIds, { maxWaitTimeMs: 5 * 60 * 1000, pollIntervalMs: 5000 });
59+
60+
const totalSetupDuration = Date.now() - setupStartTime;
61+
console.log(`🎉 RP Onboarding e2e setup completed in ${Math.round(totalSetupDuration / 1000)}s`);
62+
} catch (error) {
63+
const setupDuration = Date.now() - setupStartTime;
64+
console.error(`❌ RP Onboarding e2e setup failed after ${Math.round(setupDuration / 1000)}s:`, error);
65+
throw error;
66+
}
67+
}

0 commit comments

Comments
 (0)