Skip to content

Commit 67e9305

Browse files
committed
feat(tests): enhance integration tests for DynamoDB Local and improve config store loading
1 parent b15b3ab commit 67e9305

20 files changed

Lines changed: 721 additions & 482 deletions

.github/workflows/stage-2-test.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,6 @@ jobs:
165165
- name: "Generate dependencies"
166166
run: |
167167
npm run generate-dependencies --workspaces --if-present
168-
- name: "Run workspace integration tests"
168+
- name: "Run integration tests"
169169
run: |
170-
npm run test:integration --workspaces --if-present
170+
npm run test:integration

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"generate-dependencies": "npm run generate-dependencies --workspaces --if-present",
5757
"lint": "npm run lint --workspaces",
5858
"lint:fix": "npm run lint:fix --workspaces",
59+
"test:integration": "npm run test:integration --workspaces --if-present",
5960
"test:unit": "npm run test:unit --workspaces",
6061
"typecheck": "npm run typecheck --workspaces --if-present"
6162
},

packages/ddb-publisher/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
`ddb-publisher` reads supplier config JSON files from a directory, validates them against the event schemas, audits existing records in DynamoDB, then publishes the records into the target table.
44

5+
It is used by the `publish-config` GitHub Action, and can also be run locally for testing and development.
6+
57
## Run the publisher
68

79
From this package directory:
@@ -14,6 +16,28 @@ npm run cli -- \
1416
--dry-run
1517
```
1618

19+
## Run the integration test
20+
21+
From the repo root:
22+
23+
```bash
24+
npm run test:integration --workspace @supplier-config/ddb-publisher
25+
```
26+
27+
The test starts DynamoDB Local with Testcontainers, creates the `supplier-config-it` table, bundles the CLI, and publishes the shared example config store from `tests/example-config-store`.
28+
29+
If you are using Colima and the test cannot start Docker correctly, retry with:
30+
31+
```bash
32+
TESTCONTAINERS_RYUK_DISABLED=true npm run test:integration --workspace @supplier-config/ddb-publisher
33+
```
34+
35+
If needed, you can also try:
36+
37+
```bash
38+
TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE=/var/run/docker.sock npm run test:integration --workspace @supplier-config/ddb-publisher
39+
```
40+
1741
## Local DynamoDB
1842

1943
Start DynamoDB Local in Docker:
Lines changed: 160 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
21
import { execFile } from "node:child_process";
3-
import { tmpdir } from "node:os";
4-
import { join, resolve } from "node:path";
2+
import { resolve } from "node:path";
53
import { promisify } from "node:util";
64

7-
import { ScanCommand } from "@aws-sdk/client-dynamodb";
5+
import { PutItemCommand, ScanCommand } from "@aws-sdk/client-dynamodb";
86

97
import {
108
createPublisherTable,
@@ -16,7 +14,56 @@ import type { DbTestContext } from "./dynamodb-local";
1614
const execFileAsync = promisify(execFile);
1715

1816
const repoRoot = resolve(__dirname, "../../../..");
17+
const exampleConfigStoreRoot = resolve(repoRoot, "tests/example-config-store");
18+
const bundlePath = resolve(
19+
repoRoot,
20+
"packages/ddb-publisher/artifacts/ddb-publish/index.cjs",
21+
);
1922
const tableName = "supplier-config-it";
23+
const expectedRecords = [
24+
{
25+
pk: "ENTITY#channel",
26+
sk: "ID#letter",
27+
entity: "channel",
28+
id: "letter",
29+
},
30+
{
31+
pk: "ENTITY#volume-group",
32+
sk: "ID#vg-1",
33+
entity: "volume-group",
34+
id: "vg-1",
35+
},
36+
{
37+
pk: "ENTITY#letter-variant",
38+
sk: "ID#lv-1",
39+
entity: "letter-variant",
40+
id: "lv-1",
41+
},
42+
{
43+
pk: "ENTITY#pack-specification",
44+
sk: "ID#pack-spec-1",
45+
entity: "pack-specification",
46+
id: "pack-spec-1",
47+
},
48+
{
49+
pk: "ENTITY#supplier",
50+
sk: "ID#sup-1",
51+
entity: "supplier",
52+
id: "sup-1",
53+
},
54+
{
55+
pk: "ENTITY#supplier-allocation",
56+
sk: "ID#alloc-1",
57+
entity: "supplier-allocation",
58+
id: "alloc-1",
59+
},
60+
{
61+
pk: "ENTITY#supplier-pack",
62+
sk: "ID#sp-1",
63+
entity: "supplier-pack",
64+
id: "sp-1",
65+
},
66+
] as const;
2067

2168
let context: DbTestContext | null = null;
2269

@@ -29,38 +76,72 @@ async function run(
2976
return execFileAsync(command, args, { cwd, env });
3077
}
3178

32-
function mockPackSpecification(): Record<string, unknown> {
33-
return {
34-
id: "bau-standard-c5",
35-
name: "BAU Standard Letter C5",
36-
status: "PROD",
37-
version: 1,
38-
createdAt: "2023-01-01T00:00:00Z",
39-
updatedAt: "2023-01-01T00:00:00Z",
40-
billingId: "BILLING-BAU-C5-001",
41-
constraints: {
42-
sheets: {
43-
value: 5,
44-
operator: "LESS_THAN_OR_EQUAL",
45-
},
46-
},
47-
postage: {
48-
id: "ECONOMY",
49-
size: "STANDARD",
50-
deliveryDays: 3,
51-
},
52-
assembly: {
53-
envelopeId: "envelope-nhs-c5-economy",
54-
printColour: "BLACK",
79+
function activeContext(): DbTestContext {
80+
if (!context) {
81+
throw new Error("DynamoDB test context was not initialised.");
82+
}
83+
84+
return context;
85+
}
86+
87+
async function runBundle(extraArgs: string[] = []): Promise<{
88+
stdout: string;
89+
stderr: string;
90+
}> {
91+
return run(
92+
"node",
93+
[
94+
bundlePath,
95+
"--source",
96+
exampleConfigStoreRoot,
97+
"--env",
98+
"draft",
99+
"--table",
100+
tableName,
101+
...extraArgs,
102+
],
103+
repoRoot,
104+
{
105+
...process.env,
106+
SUPPLIER_CONFIG_DDB_ENDPOINT_URL: activeContext().endpoint,
55107
},
56-
};
108+
);
109+
}
110+
111+
async function scanTable() {
112+
return activeContext().ddbClient.send(new ScanCommand({ TableName: tableName }));
113+
}
114+
115+
function expectExpectedRecords(
116+
items: NonNullable<Awaited<ReturnType<typeof scanTable>>["Items"]>,
117+
): void {
118+
expect(items).toHaveLength(expectedRecords.length);
119+
120+
for (const expectedRecord of expectedRecords) {
121+
const actual = items.find(
122+
(item) =>
123+
item.pk?.S === expectedRecord.pk
124+
&& item.sk?.S === expectedRecord.sk,
125+
);
126+
127+
expect(actual).toBeDefined();
128+
expect(actual?.env?.S).toBe("draft");
129+
expect(actual?.entity?.S).toBe(expectedRecord.entity);
130+
expect(actual?.id?.S).toBe(expectedRecord.id);
131+
}
57132
}
58133

59134
describe("publish action integration (DynamoDB Local)", () => {
60135
beforeAll(async () => {
61136
try {
62137
context = await setupDynamoDBContainer();
63138
await createPublisherTable(context, tableName);
139+
await run("npm", [
140+
"run",
141+
"bundle:release",
142+
"--workspace",
143+
"@supplier-config/ddb-publisher",
144+
]);
64145
} catch (error) {
65146
const reason = error instanceof Error ? error.message : String(error);
66147
throw new Error(
@@ -91,68 +172,65 @@ describe("publish action integration (DynamoDB Local)", () => {
91172
});
92173

93174
it("publishes records via the bundled action runtime into local DynamoDB", async () => {
94-
if (!context) {
95-
throw new Error("DynamoDB test context was not initialised.");
96-
}
175+
await runBundle();
176+
177+
const scanned = await scanTable();
178+
179+
expect(scanned.Items).toBeDefined();
180+
expectExpectedRecords(scanned.Items ?? []);
181+
});
97182

98-
const tempRoot = await mkdtemp(join(tmpdir(), "ddb-publish-config-"));
183+
it("blocks a reload when extra active records exist, then allows it with --force", async () => {
184+
await activeContext().ddbClient.send(
185+
new PutItemCommand({
186+
TableName: tableName,
187+
Item: {
188+
pk: { S: "ENTITY#supplier" },
189+
sk: { S: "ID#stale-supplier" },
190+
entity: { S: "supplier" },
191+
id: { S: "stale-supplier" },
192+
env: { S: "draft" },
193+
status: { S: "DRAFT" },
194+
name: { S: "Stale Supplier" },
195+
channelType: { S: "LETTER" },
196+
dailyCapacity: { N: "1" },
197+
},
198+
}),
199+
);
200+
201+
let blocked = false;
99202

100203
try {
101-
const entityDir = join(tempRoot, "pack-specification");
102-
await mkdir(entityDir, { recursive: true });
103-
await writeFile(
104-
join(entityDir, "bau-standard-c5.json"),
105-
JSON.stringify(mockPackSpecification(), null, 2),
106-
"utf8",
107-
);
204+
await runBundle();
205+
} catch (error) {
206+
blocked = true;
207+
const stdout = String((error as { stdout?: string }).stdout ?? "");
208+
const stderr = String((error as { stderr?: string }).stderr ?? "");
209+
const combined = `${stdout}\n${stderr}`;
108210

109-
await run("npm", [
110-
"run",
111-
"bundle:release",
112-
"--workspace",
113-
"@supplier-config/ddb-publisher",
114-
]);
211+
expect(combined).toContain("Upload blocked");
212+
expect(combined).toContain("ID#stale-supplier");
213+
}
115214

116-
const bundlePath = join(
117-
repoRoot,
118-
"packages/ddb-publisher/artifacts/ddb-publish/index.cjs",
119-
);
215+
expect(blocked).toBe(true);
120216

121-
await run(
122-
"node",
123-
[
124-
bundlePath,
125-
"--source",
126-
tempRoot,
127-
"--env",
128-
"draft",
129-
"--table",
130-
tableName,
131-
],
132-
repoRoot,
133-
{
134-
...process.env,
135-
SUPPLIER_CONFIG_DDB_ENDPOINT_URL: context.endpoint,
136-
AWS_ACCESS_KEY_ID: "fakeMyKeyId",
137-
AWS_SECRET_ACCESS_KEY: "fakeSecretAccessKey",
138-
AWS_REGION: "eu-west-2",
139-
},
140-
);
217+
await runBundle(["--force"]);
141218

142-
const scanned = await context.ddbClient.send(
143-
new ScanCommand({ TableName: tableName }),
144-
);
219+
const scanned = await scanTable();
145220

146-
expect(scanned.Items).toBeDefined();
147-
expect(scanned.Items).toHaveLength(1);
221+
expect(scanned.Items).toBeDefined();
222+
expect(scanned.Items).toHaveLength(expectedRecords.length + 1);
223+
expectExpectedRecords(
224+
(scanned.Items ?? []).filter((item) => item.sk?.S !== "ID#stale-supplier"),
225+
);
148226

149-
const [item] = scanned.Items ?? [];
150-
expect(item?.pk?.S).toBe("ENTITY#pack-specification");
151-
expect(item?.sk?.S).toBe("ID#bau-standard-c5");
152-
expect(item?.env?.S).toBe("draft");
153-
expect(item?.entity?.S).toBe("pack-specification");
154-
} finally {
155-
await rm(tempRoot, { recursive: true, force: true });
156-
}
227+
const stale = scanned.Items?.find(
228+
(item) =>
229+
item.pk?.S === "ENTITY#supplier"
230+
&& item.sk?.S === "ID#stale-supplier",
231+
);
232+
233+
expect(stale).toBeDefined();
234+
expect(stale?.status?.S).toBe("DRAFT");
157235
});
158236
});

packages/ddb-publisher/src/__tests__/audit-branches.test.ts

Lines changed: 0 additions & 43 deletions
This file was deleted.

0 commit comments

Comments
 (0)