Skip to content

Commit 7b879cd

Browse files
committed
test(bootstrap): enhance tests for ABI artifact loading and output verification
- Added tests to verify the loading of ABI artifacts when a directory is provided. - Improved assertions to check the captured payload and ABI path during the bootstrap process. - Ensured that output results include the expected ABI artifacts and their configurations. - Updated existing tests to reflect changes in the handling of allocations and ABI loading.
1 parent bd9cedd commit 7b879cd

File tree

11 files changed

+642
-4
lines changed

11 files changed

+642
-4
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ Commands:
1919
emit a Besu genesis.
2020
compile-genesis [options] Merge per-account allocation ConfigMaps into a Besu
2121
genesis file.
22+
download-abi [options] Download ABI ConfigMaps annotated with
23+
settlemint.com/artifact=abi into a local directory.
2224
help [command] display help for command
2325
```
2426

@@ -39,6 +41,7 @@ Options:
3941
--faucet-artifact-prefix <prefix> Prefix applied to faucet ConfigMaps and Secrets.
4042
-v, --validators <count> Number of validator nodes to generate. (default: 4)
4143
-a, --allocations <file> Path to a genesis allocations JSON file. (default: none)
44+
--abi-directory <path> Directory containing ABI JSON files to publish as ConfigMaps.
4245
-o, --outputType <type> Output target (screen, file, kubernetes). (default: "screen")
4346
--static-node-port <number> P2P port used for static-nodes enode URIs. (default: 30303)
4447
--static-node-discovery-port <number> Discovery port used for static-nodes enode URIs. (default: 30303)
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3+
import { tmpdir } from "node:os";
4+
import { join } from "node:path";
5+
6+
import { loadAbis } from "./bootstrap.abis.ts";
7+
8+
let workingDirectory: string;
9+
10+
const MISSING_DIRECTORY_REGEX = /ABI directory not found/u;
11+
const BROKEN_FILE_REGEX = /ABI file Broken\.json is not valid JSON/u;
12+
13+
beforeEach(async () => {
14+
workingDirectory = await mkdtemp(join(tmpdir(), "abis-"));
15+
});
16+
17+
afterEach(async () => {
18+
await rm(workingDirectory, { recursive: true, force: true });
19+
});
20+
21+
describe("loadAbis", () => {
22+
test("reads json files and normalizes configmap names", async () => {
23+
const firstAbi = join(workingDirectory, "Token.json");
24+
const secondAbi = join(workingDirectory, "vault.ABI.JSON");
25+
await writeFile(firstAbi, JSON.stringify({ name: "Token" }));
26+
await writeFile(secondAbi, JSON.stringify({ name: "Vault" }));
27+
28+
const abis = await loadAbis(workingDirectory);
29+
30+
expect(abis).toEqual([
31+
{
32+
configMapName: "abi-token",
33+
fileName: "Token.json",
34+
contents: `${JSON.stringify({ name: "Token" }, null, 2)}\n`,
35+
},
36+
{
37+
configMapName: "abi-vault.abi",
38+
fileName: "vault.ABI.JSON",
39+
contents: `${JSON.stringify({ name: "Vault" }, null, 2)}\n`,
40+
},
41+
]);
42+
});
43+
44+
test("recursively reads json files", async () => {
45+
const nestedDirectory = join(workingDirectory, "nested");
46+
await mkdir(nestedDirectory, { recursive: true });
47+
await writeFile(
48+
join(nestedDirectory, "Nested.json"),
49+
JSON.stringify({ name: "Nested" })
50+
);
51+
52+
const abis = await loadAbis(workingDirectory);
53+
54+
expect(abis).toContainEqual({
55+
configMapName: "abi-nested",
56+
fileName: "Nested.json",
57+
contents: `${JSON.stringify({ name: "Nested" }, null, 2)}\n`,
58+
});
59+
});
60+
61+
test("ignores non-json files", async () => {
62+
await writeFile(join(workingDirectory, "README.md"), "# readme");
63+
64+
const abis = await loadAbis(workingDirectory);
65+
expect(abis).toHaveLength(0);
66+
});
67+
68+
test("throws when directory is missing", async () => {
69+
await expect(loadAbis(join(workingDirectory, "missing"))).rejects.toThrow(
70+
MISSING_DIRECTORY_REGEX
71+
);
72+
});
73+
74+
test("throws when file payload is invalid json", async () => {
75+
await writeFile(join(workingDirectory, "Broken.json"), "not json");
76+
77+
await expect(loadAbis(workingDirectory)).rejects.toThrow(BROKEN_FILE_REGEX);
78+
});
79+
});
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { stat } from "node:fs/promises";
2+
import { parse } from "node:path";
3+
import { Glob } from "bun";
4+
5+
const JSON_EXTENSION = ".json";
6+
const ABI_CONFIGMAP_PREFIX = "abi-";
7+
const ABI_GLOB = new Glob("**/*");
8+
9+
type AbiArtifact = {
10+
configMapName: string;
11+
fileName: string;
12+
contents: string;
13+
};
14+
15+
const normalizeConfigMapName = (fileName: string): string => {
16+
const { name } = parse(fileName);
17+
const normalized = name.trim().toLowerCase();
18+
if (normalized.length === 0) {
19+
throw new Error(
20+
"ABI filenames must contain at least one alphanumeric character."
21+
);
22+
}
23+
return `${ABI_CONFIGMAP_PREFIX}${normalized}`;
24+
};
25+
26+
const loadAbis = async (directory: string): Promise<AbiArtifact[]> => {
27+
const trimmedDirectory = directory.trim();
28+
if (trimmedDirectory.length === 0) {
29+
throw new Error("ABI directory must be provided.");
30+
}
31+
32+
const directoryStats = await stat(trimmedDirectory).catch(() => null);
33+
if (!directoryStats) {
34+
throw new Error(`ABI directory not found at ${directory}`);
35+
}
36+
if (!directoryStats.isDirectory()) {
37+
throw new Error(`ABI path must be a directory. Received ${directory}`);
38+
}
39+
40+
const matchedFiles: string[] = [];
41+
for await (const filePath of ABI_GLOB.scan({
42+
cwd: trimmedDirectory,
43+
absolute: true,
44+
onlyFiles: true,
45+
})) {
46+
if (!filePath.toLowerCase().endsWith(JSON_EXTENSION)) {
47+
continue;
48+
}
49+
matchedFiles.push(filePath);
50+
}
51+
52+
if (matchedFiles.length === 0) {
53+
return [];
54+
}
55+
56+
const artifacts: AbiArtifact[] = [];
57+
for (const absolutePath of matchedFiles) {
58+
const fileName = parse(absolutePath).base;
59+
const configMapName = normalizeConfigMapName(fileName);
60+
61+
try {
62+
const parsed = await Bun.file(absolutePath).json();
63+
artifacts.push({
64+
configMapName,
65+
fileName,
66+
contents: `${JSON.stringify(parsed, null, 2)}\n`,
67+
});
68+
} catch (error) {
69+
throw new Error(
70+
`ABI file ${fileName} is not valid JSON: ${(error as Error).message}`
71+
);
72+
}
73+
}
74+
75+
artifacts.sort((left, right) =>
76+
left.configMapName.localeCompare(right.configMapName)
77+
);
78+
79+
return artifacts;
80+
};
81+
82+
export type { AbiArtifact };
83+
export { loadAbis };

src/cli/commands/bootstrap/bootstrap.command.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ describe("CLI command bootstrap", () => {
136136
const promptCalls: [string, number | undefined, number][] = [];
137137
const textPromptCalls: [string, string][] = [];
138138
let loadAllocationsPath: string | undefined;
139+
let loadAbisPath: string | undefined;
139140
let outputInvocation:
140141
| {
141142
type: OutputType;
@@ -199,6 +200,10 @@ describe("CLI command bootstrap", () => {
199200
[expectedAddress(FAUCET_INDEX)]: { balance: "0x01" as const },
200201
} satisfies Record<string, BesuAllocAccount>);
201202
},
203+
loadAbis: (path: string) => {
204+
loadAbisPath = path;
205+
return Promise.resolve([]);
206+
},
202207
outputResult: async (type, payload) => {
203208
outputInvocation = { type, payload };
204209
await realOutputResult(type, payload);
@@ -225,11 +230,13 @@ describe("CLI command bootstrap", () => {
225230
expect(output).toContain("Static Nodes");
226231
expect(output).toContain(GENESIS_MARKER);
227232
expect(loadAllocationsPath).toBe("/tmp/alloc.json");
233+
expect(loadAbisPath).toBeUndefined();
228234
expect(outputInvocation?.type).toBe("screen");
229235
expect(outputInvocation?.payload.staticNodes).toEqual([
230236
expectedStaticNodeUri(FIRST_VALIDATOR_INDEX),
231237
expectedStaticNodeUri(SECOND_VALIDATOR_INDEX),
232238
]);
239+
expect(outputInvocation?.payload.abiArtifacts).toEqual([]);
233240
expect(outputInvocation?.payload.artifactNames).toEqual({
234241
faucetPrefix: DEFAULT_FAUCET_PREFIX,
235242
validatorPrefix: DEFAULT_POD_PREFIX,
@@ -238,6 +245,57 @@ describe("CLI command bootstrap", () => {
238245
});
239246
});
240247

248+
test("runBootstrap loads ABI artifacts when directory provided", async () => {
249+
const factory = createFactoryStub();
250+
const abiArtifacts = [
251+
{
252+
configMapName: "abi-demo",
253+
fileName: "Demo.json",
254+
contents: `${JSON.stringify({ name: "Demo" }, null, 2)}\n`,
255+
},
256+
];
257+
let capturedPayload: OutputPayload | undefined;
258+
let capturedAbiPath: string | undefined;
259+
260+
const deps: BootstrapDependencies = {
261+
factory,
262+
promptForCount: () => Promise.resolve(EXPECTED_DEFAULT_VALIDATOR),
263+
promptForGenesis: async (_service, { faucetAddress }) => ({
264+
algorithm: ALGORITHM.QBFT,
265+
config: {
266+
chainId: 1,
267+
faucetWalletAddress: faucetAddress,
268+
gasLimit: "0x1",
269+
secondsPerBlock: 2,
270+
},
271+
genesis: { config: {}, extraData: "0x" } as any,
272+
}),
273+
promptForText: passthroughTextPrompt,
274+
service: {} as any,
275+
loadAllocations: () =>
276+
Promise.resolve({} as Record<string, BesuAllocAccount>),
277+
loadAbis: (path: string) => {
278+
capturedAbiPath = path;
279+
return Promise.resolve(abiArtifacts);
280+
},
281+
outputResult: (_type, payload) => {
282+
capturedPayload = payload;
283+
return Promise.resolve();
284+
},
285+
};
286+
287+
await runBootstrap(
288+
{
289+
abiDirectory: " /opt/abis ",
290+
acceptDefaults: true,
291+
},
292+
deps
293+
);
294+
295+
expect(capturedAbiPath).toBe("/opt/abis");
296+
expect(capturedPayload?.abiArtifacts).toEqual(abiArtifacts);
297+
});
298+
241299
test("createCliCommand wires metadata", () => {
242300
const command = createCliCommand();
243301
expect(command.name()).toBe("network-bootstrapper");
@@ -278,6 +336,7 @@ describe("CLI command bootstrap", () => {
278336
service: {} as any,
279337
loadAllocations: () =>
280338
Promise.resolve({} satisfies Record<string, BesuAllocAccount>),
339+
loadAbis: () => Promise.resolve([]),
281340
outputResult: async (type, payload) => {
282341
await realOutputResult(type, payload);
283342
},
@@ -345,6 +404,7 @@ describe("CLI command bootstrap", () => {
345404
service: {} as any,
346405
loadAllocations: () =>
347406
Promise.resolve({} satisfies Record<string, BesuAllocAccount>),
407+
loadAbis: () => Promise.resolve([]),
348408
outputResult: (_type, payload) => {
349409
capturedPayload = payload;
350410
return Promise.resolve();
@@ -426,6 +486,7 @@ describe("CLI command bootstrap", () => {
426486
service: {} as any,
427487
loadAllocations: () =>
428488
Promise.resolve({} satisfies Record<string, BesuAllocAccount>),
489+
loadAbis: () => Promise.resolve([]),
429490
outputResult: (_type, payload) => {
430491
capturedPayload = payload;
431492
return Promise.resolve();
@@ -494,6 +555,7 @@ describe("CLI command bootstrap", () => {
494555
service: {} as any,
495556
loadAllocations: () =>
496557
Promise.resolve({} satisfies Record<string, BesuAllocAccount>),
558+
loadAbis: () => Promise.resolve([]),
497559
outputResult: async () => {
498560
// no-op for test
499561
},
@@ -576,6 +638,7 @@ describe("CLI command bootstrap", () => {
576638
service: {} as any,
577639
loadAllocations: () =>
578640
Promise.resolve({} satisfies Record<string, BesuAllocAccount>),
641+
loadAbis: () => Promise.resolve([]),
579642
outputResult: (type) => {
580643
capturedOutputType = type;
581644
return Promise.resolve();
@@ -624,6 +687,7 @@ describe("CLI command bootstrap", () => {
624687
const factory = createFactoryStub();
625688
let promptCountInvocations = 0;
626689
let loadAllocationsInvoked = false;
690+
let loadAbisInvoked = false;
627691

628692
const deps: BootstrapDependencies = {
629693
factory,
@@ -662,6 +726,10 @@ describe("CLI command bootstrap", () => {
662726
loadAllocationsInvoked = true;
663727
return Promise.resolve({} as Record<string, BesuAllocAccount>);
664728
},
729+
loadAbis: () => {
730+
loadAbisInvoked = true;
731+
return Promise.resolve([]);
732+
},
665733
outputResult: (_type, payload) => {
666734
expect(payload.validators).toHaveLength(EXPECTED_DEFAULT_VALIDATOR);
667735
expect(payload.artifactNames).toEqual({
@@ -683,5 +751,6 @@ describe("CLI command bootstrap", () => {
683751

684752
expect(promptCountInvocations).toBe(0);
685753
expect(loadAllocationsInvoked).toBe(false);
754+
expect(loadAbisInvoked).toBe(false);
686755
});
687756
});

0 commit comments

Comments
 (0)