From 1bc1eb509166e6dfcb24c59a90eb05f5dc16de78 Mon Sep 17 00:00:00 2001 From: Arseniy Klempner Date: Tue, 7 Nov 2023 20:06:44 -0800 Subject: [PATCH] feat: add function to validate autoshard content topic --- .cspell.json | 2 + packages/utils/.mocha.reporters.json | 6 ++ packages/utils/.mocharc.cjs | 26 +++++++ packages/utils/package.json | 4 +- packages/utils/src/common/sharding.spec.ts | 88 ++++++++++++++++++++++ packages/utils/src/common/sharding.ts | 39 ++++++++++ 6 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 packages/utils/.mocha.reporters.json create mode 100644 packages/utils/.mocharc.cjs create mode 100644 packages/utils/src/common/sharding.spec.ts diff --git a/.cspell.json b/.cspell.json index 674f55faa7..a6c0a9724c 100644 --- a/.cspell.json +++ b/.cspell.json @@ -7,6 +7,8 @@ "ahadns", "Alives", "asym", + "autoshard", + "autosharding", "backoff", "backoffs", "bitauth", diff --git a/packages/utils/.mocha.reporters.json b/packages/utils/.mocha.reporters.json new file mode 100644 index 0000000000..8c00e441fc --- /dev/null +++ b/packages/utils/.mocha.reporters.json @@ -0,0 +1,6 @@ +{ + "reporterEnabled": "spec, allure-mocha", + "allureMochaReporter": { + "outputDir": "allure-results" + } +} diff --git a/packages/utils/.mocharc.cjs b/packages/utils/.mocharc.cjs new file mode 100644 index 0000000000..423c0517bd --- /dev/null +++ b/packages/utils/.mocharc.cjs @@ -0,0 +1,26 @@ +const config = { + extension: ['ts'], + spec: 'src/**/*.spec.ts', + require: ['ts-node/register', 'isomorphic-fetch'], + loader: 'ts-node/esm', + 'node-option': [ + 'experimental-specifier-resolution=node', + 'loader=ts-node/esm' + ], + exit: true +}; + +if (process.env.CI) { + console.log("Running tests in parallel"); + config.parallel = true; + config.jobs = 6; + console.log("Activating allure reporting"); + config.reporter = 'mocha-multi-reporters'; + config.reporterOptions = { + configFile: '.mocha.reporters.json' + }; +} else { + console.log("Running tests serially. To enable parallel execution update mocha config"); +} + +module.exports = config; diff --git a/packages/utils/package.json b/packages/utils/package.json index 77d5a3de92..ed2c44adca 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -59,7 +59,9 @@ "check:spelling": "cspell \"{README.md,src/**/*.ts}\"", "check:tsc": "tsc -p tsconfig.dev.json", "prepublish": "npm run build", - "reset-hard": "git clean -dfx -e .idea && git reset --hard && npm i && npm run build" + "reset-hard": "git clean -dfx -e .idea && git reset --hard && npm i && npm run build", + "test": "run-s test:*", + "test:node": "TS_NODE_PROJECT=./tsconfig.dev.json mocha" }, "engines": { "node": ">=18" diff --git a/packages/utils/src/common/sharding.spec.ts b/packages/utils/src/common/sharding.spec.ts new file mode 100644 index 0000000000..f2963e6b09 --- /dev/null +++ b/packages/utils/src/common/sharding.spec.ts @@ -0,0 +1,88 @@ +import { expect } from "chai"; + +import { ensureValidContentTopic } from "./sharding"; + +const testInvalidCases = ( + contentTopics: string[], + expectedError: string +): void => { + for (const invalidTopic of contentTopics) { + expect(() => ensureValidContentTopic(invalidTopic)).to.throw(expectedError); + } +}; + +describe("ensureValidContentTopic", () => { + it("does not throw on valid cases", () => { + const validTopics = [ + "/0/myapp/1/mytopic/cbor", + "/myapp/1/mytopic/cbor", + "/myapp/v1.1/mytopic/cbor" + ]; + for (const validTopic of validTopics) { + expect(() => ensureValidContentTopic(validTopic)).to.not.throw; + } + }); + it("throws on empty content topic", () => { + testInvalidCases(["", " ", " "], "Content topic format is invalid"); + }); + + it("throws on content topic with too few or too many fields", () => { + testInvalidCases( + [ + "myContentTopic", + "myapp1mytopiccbor/", + " /myapp/1/mytopic", + "/myapp/1/mytopic", + "/0/myapp/1/mytopic/cbor/extra" + ], + "Content topic format is invalid" + ); + }); + + it("throws on content topic with non-number generation field", () => { + testInvalidCases( + [ + "/a/myapp/1/mytopic/cbor", + "/ /myapp/1/mytopic/cbor", + "/_/myapp/1/mytopic/cbor", + "/$/myapp/1/mytopic/cbor" + ], + "Invalid generation field in content topic" + ); + }); + + // Note that this test case should be removed once Waku supports other generations + it("throws on content topic with generation field greater than 0", () => { + testInvalidCases( + [ + "/1/myapp/1/mytopic/cbor", + "/2/myapp/1/mytopic/cbor", + "/3/myapp/1/mytopic/cbor", + "/1000/myapp/1/mytopic/cbor" + ], + "Generation greater than 0 is not supported" + ); + }); + + it("throws on content topic with empty application field", () => { + testInvalidCases( + ["/0//1/mytopic/cbor"], + "Application field cannot be empty" + ); + }); + + it("throws on content topic with empty version field", () => { + testInvalidCases( + ["/0/myapp//mytopic/cbor"], + "Version field cannot be empty" + ); + }); + + it("throws on content topic with empty topic name field", () => { + testInvalidCases(["/0/myapp/1//cbor"], "Topic name field cannot be empty"); + }); + + it("throws on content topic with empty encoding field", () => { + testInvalidCases(["/0/myapp/1/mytopic/"], "Encoding field cannot be empty"); + }); +}); diff --git a/packages/utils/src/common/sharding.ts b/packages/utils/src/common/sharding.ts index fcbfe0a0b6..302229b4e8 100644 --- a/packages/utils/src/common/sharding.ts +++ b/packages/utils/src/common/sharding.ts @@ -18,3 +18,42 @@ export function ensurePubsubTopicIsConfigured( ); } } + +/** + * Given a string, will throw an error if it is not formatted as a valid content topic for autosharding based on https://rfc.vac.dev/spec/51/ + * @param contentTopic String to validate + */ +export function ensureValidContentTopic(contentTopic: string): void { + const parts = contentTopic.split("/"); + if (parts.length < 5 || parts.length > 6) { + throw Error("Content topic format is invalid"); + } + // Validate generation field if present + if (parts.length == 6) { + const generation = parseInt(parts[1]); + if (isNaN(generation)) { + throw new Error("Invalid generation field in content topic"); + } + if (generation > 0) { + throw new Error("Generation greater than 0 is not supported"); + } + } + // Validate remaining fields + const fields = parts.splice(-4); + // Validate application field + if (fields[0].length == 0) { + throw new Error("Application field cannot be empty"); + } + // Validate version field + if (fields[1].length == 0) { + throw new Error("Version field cannot be empty"); + } + // Validate topic name field + if (fields[2].length == 0) { + throw new Error("Topic name field cannot be empty"); + } + // Validate encoding field + if (fields[3].length == 0) { + throw new Error("Encoding field cannot be empty"); + } +}