From 0068e4772b32b1978c128c5ab73a585cbb3165a6 Mon Sep 17 00:00:00 2001 From: "Thomas.G" Date: Sat, 20 Jul 2024 14:14:35 +0200 Subject: [PATCH] refactor(config): rename files & fix UT (#144) --- src/config/README.md | 10 +- ...cConfig.ts => AsynchronousConfig.class.ts} | 121 ++--- src/config/src/index.ts | 6 +- ...fig.test.ts => AsynchronousConfig.test.ts} | 458 ++++++++++-------- .../fixtures/{.dummyConfig => .dotconfig} | 0 src/config/test/fixtures/.empty | 1 + src/config/test/fixtures/config.toml | 2 +- 7 files changed, 335 insertions(+), 263 deletions(-) rename src/config/src/{asyncConfig.ts => AsynchronousConfig.class.ts} (85%) rename src/config/test/{asyncConfig.test.ts => AsynchronousConfig.test.ts} (51%) rename src/config/test/fixtures/{.dummyConfig => .dotconfig} (100%) diff --git a/src/config/README.md b/src/config/README.md index 2feed41..9430e10 100644 --- a/src/config/README.md +++ b/src/config/README.md @@ -43,7 +43,7 @@ Create a simple json file for your project Now, create a new Configuration instance and read it ```js -import { AsynchronousConfig } from "@openally/config"; +import { AsynchronousConfig } from "@openally/config"; const config = new AsynchronousConfig("./path/to/config.json"); await config.read(); @@ -98,7 +98,7 @@ Available options are: Will read the local configuration on disk. A default `payload` value can be provided in case the file doesn't exist ! > [!CAUTION] -> When the file doesn't exist, the configuration is written at the next loop iteration (with `lazyWriteOnDisk`). +> When the file doesn't exist, the configuration is written at the next loop iteration ### `AsynchronousConfig.setupHotReload(): void` @@ -156,11 +156,5 @@ Return a deep clone of the configuration payload. Write the configuration payload on the local disk. -### `AsynchronousConfig.lazyWriteOnDisk(): void` - -Write the configuration payload on the local disk at the next loop iteration. - -Use `configWritten` event to know when the configuration has been written on the disk. - ## License MIT diff --git a/src/config/src/asyncConfig.ts b/src/config/src/AsynchronousConfig.class.ts similarity index 85% rename from src/config/src/asyncConfig.ts rename to src/config/src/AsynchronousConfig.class.ts index d768ac9..5d86e30 100644 --- a/src/config/src/asyncConfig.ts +++ b/src/config/src/AsynchronousConfig.class.ts @@ -24,8 +24,6 @@ const kDefaultSchema = { additionalProperties: true }; -type WithRequired = T & Required> - export interface ConfigOptions { createOnNoEntry?: boolean; autoReload?: boolean; @@ -46,15 +44,19 @@ export class AsynchronousConfig = Record])[] = []; - #jsonSchema?: object; + #jsonSchema?: JSONSchemaType; #cleanupTimeout: NodeJS.Timeout; - #watcherSignal = new AbortController(); - #fs!: WithRequired, "fs">["fs"]; + #watcher: nodeFs.FSWatcher; + #fs: Required>["fs"]; - constructor(configFilePath: string, options: ConfigOptions = Object.create(null)) { + constructor( + configFilePath: string, + options: ConfigOptions = Object.create(null) + ) { super(); if (typeof configFilePath !== "string") { throw new TypeError("The configPath must be a string"); @@ -131,15 +133,16 @@ export class AsynchronousConfig = Record ${ajvErrors}`; throw new Error(errorMessage); } - this[kPayloadIdentifier] = newPayload; + this[kPayloadIdentifier] = tempPayload; for (const [fieldPath, subscriptionObservers] of this.#subscriptionObservers) { subscriptionObservers.next(this.get(fieldPath)); } @@ -185,8 +188,7 @@ export class AsynchronousConfig = Record = Record { this.#subscriptionObservers = this.#subscriptionObservers.filter( ([, subscriptionObservers]) => !subscriptionObservers.closed @@ -207,7 +212,14 @@ export class AsynchronousConfig = Record = Record = Record { - try { - if (!this.#configHasBeenRead) { - return; + this.#watcher = this.#fs.watch( + this.#configFilePath, + { persistent: false }, + async() => { + try { + if (!this.#configHasBeenRead) { + return; + } + await this.read(); + this.emit("reload"); + } + catch (err) { + this.emit("error", err); } - await this.read(); - this.emit("reload"); - } - catch (err) { - this.emit("error", err); } - }); + ); this.#autoReloadActivated = true; this.emit("watcherInitialized"); @@ -254,6 +270,15 @@ export class AsynchronousConfig = Record { + observer.next(fieldValue); + this.#subscriptionObservers.push([fieldPath, observer]); + }); + } + get(fieldPath: string, depth = Infinity): Y | null { if (!this.#configHasBeenRead) { throw new Error("You must read config first before getting a field!"); @@ -273,15 +298,6 @@ export class AsynchronousConfig = Record { - observer.next(fieldValue); - this.#subscriptionObservers.push([fieldPath, observer]); - }); - } - set(fieldPath: string, fieldValue: any) { if (!this.#configHasBeenRead) { throw new Error("You must read config first before setting a field!"); @@ -291,10 +307,8 @@ export class AsynchronousConfig = Record(this.payload, fieldPath, fieldValue); - Object.assign(this.payload, { [fieldPath]: fieldValue }); - if (this.#writeOnSet) { - this.lazyWriteOnDisk(); + this.#lazyWriteOnDisk(); } return this; @@ -305,46 +319,43 @@ export class AsynchronousConfig = Record { - try { - await this.writeOnDisk(); - } - catch (error) { - this.emit("error", error); - } - }); + this.#scheduledLazyWrite = setImmediate( + () => this.writeOnDisk().catch((error) => this.emit("error", error)) + ); } - async close() { + + async close(): Promise { if (!this.#configHasBeenRead) { - throw new Error("Cannot close unreaded configuration"); + return; } + clearImmediate(this.#scheduledLazyWrite); if (this.#autoReloadActivated) { - this.#watcherSignal.abort(); + this.#watcher.close(); this.#autoReloadActivated = false; } - await this.writeOnDisk(); - this.#configHasBeenRead = false; - for (const [, subscriptionObservers] of this.#subscriptionObservers) { subscriptionObservers.complete(); } this.#subscriptionObservers = []; - clearInterval(this.#cleanupTimeout); + await this.writeOnDisk(); + this.#configHasBeenRead = false; + this.emit("close"); } } diff --git a/src/config/src/index.ts b/src/config/src/index.ts index 1960f3c..6301d71 100644 --- a/src/config/src/index.ts +++ b/src/config/src/index.ts @@ -1,6 +1,2 @@ // Import Internal Dependencies -import { AsynchronousConfig } from "./asyncConfig.js"; - -export { - AsynchronousConfig -}; +export * from "./AsynchronousConfig.class.js"; diff --git a/src/config/test/asyncConfig.test.ts b/src/config/test/AsynchronousConfig.test.ts similarity index 51% rename from src/config/test/asyncConfig.test.ts rename to src/config/test/AsynchronousConfig.test.ts index 2557530..15393a0 100644 --- a/src/config/test/asyncConfig.test.ts +++ b/src/config/test/AsynchronousConfig.test.ts @@ -1,15 +1,13 @@ // Import Node.js Dependencies -import { describe, before, after, it } from "node:test"; -import assert from "assert"; +import { describe, before, after, it, test } from "node:test"; +import assert from "node:assert"; import path from "node:path"; import url from "node:url"; import fs from "node:fs"; import os from "node:os"; +import crypto from "node:crypto"; import { once } from "node:events"; -// Import Third-party Dependencies -import { TomlDate } from "smol-toml"; - // Import Internal Dependencies import { AsynchronousConfig } from "../src/index.js"; @@ -17,7 +15,21 @@ const __dirname = url.fileURLToPath(new URL(".", import.meta.url)); type FooConfig = { foo: string }; -describe("AsynchronousConfig", { concurrency: 1 }, () => { +describe("AsynchronousConfig", () => { + // Keep the event-loop alive while running tests + const keepAliveTimer: NodeJS.Timeout = setInterval(() => void 0, 100_000); + + let tempDir: string; + + before(() => { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openally-config-")); + }); + + after(() => { + fs.rmSync(tempDir, { force: true, recursive: true }); + clearInterval(keepAliveTimer); + }); + describe("given bad parameters", () => { it("should throw when path is not a string", () => { assert.throws(() => { @@ -109,33 +121,31 @@ describe("AsynchronousConfig", { concurrency: 1 }, () => { }); }); - describe("JSON configuration", { concurrency: 1 }, () => { - const configPath = path.join(__dirname, "fixtures", "codeMirror.json"); - let config: AsynchronousConfig; - - before(() => { - const configToCreate = { - addons: { - cpu: { - active: false, - standalone: false - } + describe("JSON configuration", () => { + const configToCreate = { + addons: { + cpu: { + active: false, + standalone: false } - }; - fs.writeFileSync(configPath, JSON.stringify(configToCreate, null, 2)); - - config = new AsynchronousConfig(configPath, { - createOnNoEntry: true, - writeOnSet: true, - autoReload: true - }); - }); - - after(() => { - fs.rmSync(configPath); - }); + } + }; + const configOptions = { + createOnNoEntry: true, + writeOnSet: true, + autoReload: true + }; + + it("should read and observe config", async(t) => { + const configPath = path.join( + tempDir, + randomFileName() + ); + fs.writeFileSync(configPath, JSON.stringify(configToCreate)); + + const config = new AsynchronousConfig(configPath, configOptions); + t.after(() => config.close()); - it("should read and observe config", async() => { await config.read({ hostname: os.hostname(), platform: os.platform(), @@ -151,60 +161,77 @@ describe("AsynchronousConfig", { concurrency: 1 }, () => { config.set("addons.cpu.active", true); await once(config, "configWritten"); assert.strictEqual(observableResults.length, 2); - assert.deepEqual(observableResults[0], { active: false, standalone: false }); - assert.deepEqual(observableResults[1], { active: true, standalone: false }); - await config.close(); + assert.deepStrictEqual(observableResults[0], { active: false, standalone: false }); + assert.deepStrictEqual(observableResults[1], { active: true, standalone: false }); }); - it("should update multiple fields", async() => { + it("should update multiple fields", async(t) => { + const configPath = path.join( + tempDir, + randomFileName() + ); + fs.writeFileSync(configPath, JSON.stringify(configToCreate)); + + const config = new AsynchronousConfig(configPath, configOptions); + t.after(() => config.close()); + await config.read(); config.set("newField", "value"); - await once(config, "configWritten"); config.set("addons.foo", "bar"); - await once(config, "configWritten"); config.set("hostname", "localhost"); await once(config, "configWritten"); assert.strictEqual(config.get("newField"), "value"); assert.strictEqual(config.get("addons.foo"), "bar"); assert.strictEqual(config.get("hostname"), "localhost"); - - await config.close(); }); it("should get empty payload without calling read", () => { - const config = new AsynchronousConfig(path.join(__dirname, "fixtures", "withSchema.json")); - assert.deepEqual(config.payload, {}); + const config = new AsynchronousConfig( + path.join(__dirname, "fixtures", "withSchema.json") + ); + assert.deepStrictEqual(config.payload, {}); }); - it("should find withSchema.json when withSchema given (without extension)", async() => { - const config = new AsynchronousConfig(path.join(__dirname, "fixtures", "withSchema")); + it("should find withSchema.json when withSchema given (without extension)", async(t) => { + const config = new AsynchronousConfig( + path.join(__dirname, "fixtures", "withSchema") + ); + t.after(() => config.close()); + await config.read(); - assert.deepEqual(config.payload, { foo: "bar", name: 42 }); - await config.close(); + assert.deepStrictEqual(config.payload, { foo: "bar", name: 42 }); }); - it("should create an empty config object when file is empty", async() => { - const config = new AsynchronousConfig(path.join(__dirname, "fixtures", ".empty")); + it("should create an empty config object when file is empty", async(t) => { + const config = new AsynchronousConfig( + path.join(__dirname, "fixtures", ".empty") + ); + t.after(() => config.close()); + await config.read(); - assert.deepEqual(config.payload, {}); - await config.close(); - // reset file - fs.writeFileSync(path.join(__dirname, "fixtures", ".empty"), ""); + assert.deepStrictEqual(config.payload, {}); }); - it("should return null when field does not exists", async() => { - const config = new AsynchronousConfig(path.join(__dirname, "fixtures", "withSchema.json")); + it("should return null when field does not exists", async(t) => { + const config = new AsynchronousConfig( + path.join(__dirname, "fixtures", "withSchema.json") + ); + t.after(() => config.close()); + await config.read(); assert.strictEqual(config.get("doesNotExists"), null); - await config.close(); }); - it("should get keys when using depth", async() => { - const config = new AsynchronousConfig(path.join(__dirname, "fixtures", "nested.json")); + it("should get keys when using depth", async(t) => { + const config = new AsynchronousConfig( + path.join(__dirname, "fixtures", "nested.json") + ); + t.after(() => config.close()); + await config.read(); - assert.deepEqual(config.get("user"), { + assert.deepStrictEqual(config.get("user"), { name: "John Doe", nested: { deep: { @@ -212,73 +239,101 @@ describe("AsynchronousConfig", { concurrency: 1 }, () => { } } }); - assert.deepEqual(config.get("user", 1), { name: "John Doe", nested: ["deep"] }); - await config.close(); + assert.deepStrictEqual( + config.get("user", 1), + { name: "John Doe", nested: ["deep"] } + ); }); }); - describe("JSON configuration autoreload", () => { + describe("autoReload", () => { it("payload should be updated when file is updated", async() => { - const configPath = path.join(__dirname, "fixtures", ".autoreload"); - const config = new AsynchronousConfig(configPath, { autoReload: true, createOnNoEntry: true, writeOnSet: true }); - await config.read({ foo: "bar" }); - await once(config, "configWritten"); - - const observableResults: string[] = []; - config.observableOf("foo").subscribe((value: any) => { - observableResults.push(value); + const configPath = path.join(tempDir, ".autoreload"); + const config = new AsynchronousConfig(configPath, { + autoReload: true, + createOnNoEntry: true, + writeOnSet: true }); - fs.writeFileSync(configPath, JSON.stringify({ foo: "foo" }, null, 2)); - await once(config, "reload"); - - assert.strictEqual(observableResults.length, 2); - assert.strictEqual(observableResults[0], "bar"); - assert.strictEqual(observableResults[1], "foo"); - - await config.close(); - fs.rmSync(configPath); + try { + await config.read({ foo: "bar" }); + await once(config, "configWritten", { + signal: AbortSignal.timeout(1000) + }); + + const observableResults: string[] = []; + config.observableOf("foo").subscribe((value: any) => { + observableResults.push(value); + }); + + fs.writeFileSync( + configPath, + JSON.stringify({ foo: "foo" }, null, 2) + ); + await once(config, "reload", { + signal: AbortSignal.timeout(1000) + }); + + assert.strictEqual(observableResults.length, 2); + assert.strictEqual(observableResults[0], "bar"); + assert.strictEqual(observableResults[1], "foo"); + } + finally { + await config.close(); + } }); it("should observe the updated field multiple times", async() => { - const configPath = path.join(__dirname, "fixtures", ".autoreload"); - const config = new AsynchronousConfig(configPath, { autoReload: true, createOnNoEntry: true, writeOnSet: true }); - await config.read({ foo: "bar" }); - await once(config, "configWritten"); - - const observableResults: string[] = []; - config.observableOf("foo").subscribe((value: any) => { - observableResults.push(value); - }); - config.observableOf("foo").subscribe((value: any) => { - observableResults.push(value); - }); - config.observableOf("foo").subscribe((value: any) => { - observableResults.push(value); + const configPath = path.join(tempDir, ".autoreload2"); + const config = new AsynchronousConfig(configPath, { + autoReload: true, + createOnNoEntry: true, + writeOnSet: true }); - fs.writeFileSync(configPath, JSON.stringify({ foo: "foo" }, null, 2)); - await once(config, "reload"); - - assert.strictEqual(observableResults.length, 6); - const uniquesResults = [...new Set(observableResults)]; - assert.strictEqual(uniquesResults[0], "bar"); - assert.strictEqual(uniquesResults[1], "foo"); - - await config.close(); - fs.rmSync(configPath); + try { + await config.read({ foo: "bar" }); + await once(config, "configWritten", { + signal: AbortSignal.timeout(1000) + }); + + const observableResults: string[] = []; + config.observableOf("foo").subscribe((value: any) => { + observableResults.push(value); + }); + config.observableOf("foo").subscribe((value: any) => { + observableResults.push(value); + }); + config.observableOf("foo").subscribe((value: any) => { + observableResults.push(value); + }); + + fs.writeFileSync( + configPath, + JSON.stringify({ foo: "foo" }, null, 2) + ); + await once(config, "reload", { + signal: AbortSignal.timeout(1000) + }); + + assert.strictEqual(observableResults.length, 6); + const uniquesResults = [...new Set(observableResults)]; + assert.strictEqual(uniquesResults[0], "bar"); + assert.strictEqual(uniquesResults[1], "foo"); + } + finally { + await config.close(); + } }); }); - describe("JSON configuration with JSON Schema", () => { - after(() => { - fs.rmSync(path.join(__dirname, "fixtures", ".doesNotExists")); - }); - - it("should throw when set invalid value", async() => { + describe("JSON Schema", () => { + it("should throw when set invalid value", async(t) => { const config = new AsynchronousConfig(path.join(__dirname, "fixtures", "withSchema.json"), { writeOnSet: true }); + t.after(() => config.close()); + await config.read(); assert.throws(() => { config.set("foo", 42); @@ -286,7 +341,6 @@ describe("AsynchronousConfig", { concurrency: 1 }, () => { name: "Error", message: "Config.payload (setter) - AJV Validation failed with error(s) => property /foo must be string\n" }); - await config.close(); }); it("should throw with default payload", async() => { @@ -301,18 +355,16 @@ describe("AsynchronousConfig", { concurrency: 1 }, () => { } } as any }); + await assert.rejects(async() => { await config.read(); }, { name: "Error", message: "Config.payload (setter) - AJV Validation failed with error(s) => property /foo must be number\n" }); - await config.close(); - // reset the file - fs.writeFileSync(path.join(__dirname, "fixtures", ".config"), JSON.stringify({ foo: "bar" }, null, 2)); }); - it("should have a valid config once read", async() => { + it("should have a valid config once read", async(t) => { const config = new AsynchronousConfig(path.join(__dirname, "fixtures", ".config"), { writeOnSet: false, jsonSchema: { @@ -326,128 +378,112 @@ describe("AsynchronousConfig", { concurrency: 1 }, () => { additionalProperties: false } }); + t.after(() => config.close()); + await config.read(); - assert.deepEqual(config.payload, { foo: "bar" }); + assert.deepStrictEqual(config.payload, { foo: "bar" }); assert.strictEqual(config.get("foo"), "bar"); - await config.close(); }); - it("should create file with default payload when file does not exists and createOnNoEntry is true", async() => { - const configPath = path.join(__dirname, "fixtures", ".doesNotExists"); + it("should create file with default payload when file does not exists and createOnNoEntry is true", async(t) => { + const configPath = path.join(tempDir, ".doesNotExists"); assert(fs.existsSync(configPath) === false); const config = new AsynchronousConfig(configPath, { createOnNoEntry: true }); - await config.read({ boo: "boom" }); + t.after(() => config.close()); - assert.deepEqual(config.payload, { boo: "boom" }); - await config.close(); + await config.read({ boo: "boom" }); + assert.deepStrictEqual(config.payload, { boo: "boom" }); }); - it("should throw when file does not exists and createOnNoEntry is false", async() => { - const configPath = path.join(__dirname, "fixtures", ".doesNotExists2"); + it("should throw when file does not exists and createOnNoEntry is false", async(t) => { + const configPath = path.join(tempDir, ".doesNotExists2"); assert(fs.existsSync(configPath) === false); const config = new AsynchronousConfig(configPath); + t.after(() => config.close()); + await assert.rejects(async() => await config.read({ boo: "boom" }), { name: "Error", message: /ENOENT: no such file or directory/ }); }); - it("should recreate file on read when file does not exists and createOnNoEntry is true", async() => { - const configPath = path.join(__dirname, "fixtures", ".doesNotExists3"); + it("should recreate file on read when file does not exists and createOnNoEntry is true", async(t) => { + const configPath = path.join(tempDir, ".doesNotExists3"); assert(fs.existsSync(configPath) === false); const config = new AsynchronousConfig(configPath, { createOnNoEntry: true }); + t.after(() => config.close()); + await config.read({ boo: "boom" }); - await once(config, "configWritten"); + await once(config, "configWritten", { + signal: AbortSignal.timeout(1000) + }); - assert(fs.existsSync(configPath) === true); - assert.deepEqual(config.payload, { boo: "boom" }); + assert.ok(fs.existsSync(configPath)); + assert.deepStrictEqual(config.payload, { boo: "boom" }); fs.rmSync(configPath); assert(fs.existsSync(configPath) === false); await config.read(); - await once(config, "configWritten"); - assert(fs.existsSync(configPath) === true); - assert.deepEqual(config.payload, { boo: "boom" }); + await once(config, "configWritten", { + signal: AbortSignal.timeout(1000) + }); + assert.ok(fs.existsSync(configPath)); + assert.deepStrictEqual(config.payload, { boo: "boom" }); - await config.close(); fs.rmSync(configPath); }); }); - describe("TOML configuration", () => { - it("should read config", async() => { - const config = new AsynchronousConfig(path.join(__dirname, "fixtures", "config.toml")); - await config.read(); - - assert.deepEqual(config.payload, { - title: "TOML Example", - owner: { - dob: new TomlDate("1979-05-27T15:32:00.000Z"), - name: "Tom Preston-Werner" + describe("read() formats", () => { + test("Given TOML configuration files with and without extensions, it must successfully read their contents", async() => { + const cases = [ + path.join(__dirname, "fixtures", "config.toml"), + path.join(__dirname, "fixtures", "config") + ]; + + for (const configPath of cases) { + const config = new AsynchronousConfig(configPath); + + try { + await config.read(); + assert.deepStrictEqual(config.payload, { + title: "TOML Example", + owner: { + dob: "1979-05-27T15:32:00.000Z", + name: "Tom Preston-Werner" + } + }); } - }); - await config.close(); - }); - - it("should find config.toml when config given (without extension)", async() => { - const config = new AsynchronousConfig(path.join(__dirname, "fixtures", "config")); - await config.read(); - - assert.deepEqual(config.payload, { - title: "TOML Example", - owner: { - dob: new TomlDate("1979-05-27T15:32:00.000Z"), - name: "Tom Preston-Werner" + finally { + await config.close(); } - }); - await config.close(); + } }); - }); - describe("Dotfile configuration", () => { - it("should read config", async() => { - const config = new AsynchronousConfig(path.join(__dirname, "fixtures", ".dummyConfig")); + test("Given a configuration file no extension (starting with a dot), it must read it with no error", async(t) => { + const config = new AsynchronousConfig( + path.join(__dirname, "fixtures", ".dotconfig") + ); + t.after(async() => await config.close()); + await config.read(); - assert.deepEqual(config.payload, { + assert.deepStrictEqual(config.payload, { foo: "bar" }); assert.strictEqual(config.get("foo"), "bar"); - await config.close(); - }); - }); - - describe("JSON configuration with syntax error payload", () => { - it("should throw an error", async() => { - const config = new AsynchronousConfig(path.join(__dirname, "fixtures", "syntaxError.json")); - await assert.rejects(async() => { - await config.read(); - }, { - name: "SyntaxError" - }); }); }); describe("When config has not been read", () => { - let config: AsynchronousConfig; - - before(() => { - config = new AsynchronousConfig(path.join(__dirname, "fixtures", ".dummyConfig")); - }); - - it("should throw when lazyWriteOnDisk is called", () => { - assert.throws(() => { - config.lazyWriteOnDisk(); - }, { - name: "Error", - message: "You must read config first before writing it on the disk!" - }); - }); - it("should throw when writeOnDisk is called", async() => { + const config = new AsynchronousConfig( + path.join(__dirname, "fixtures", ".dotconfig") + ); + await assert.rejects(async() => { await config.writeOnDisk(); }, { @@ -456,16 +492,26 @@ describe("AsynchronousConfig", { concurrency: 1 }, () => { }); }); - it("should throw when close is called", async() => { - await assert.rejects(async() => { - await config.close(); - }, { - name: "Error", - message: "Cannot close unreaded configuration" - }); + it("should not throw and return without emitting close event", async() => { + const config = new AsynchronousConfig( + path.join(__dirname, "fixtures", ".dotconfig") + ); + let closeEmit = 0; + config.on("close", () => closeEmit++); + + await config.close(); + + assert.strictEqual( + closeEmit, + 0 + ); }); it("should throw when set is called", () => { + const config = new AsynchronousConfig( + path.join(__dirname, "fixtures", ".dotconfig") + ); + assert.throws(() => { config.set("foo", "bar"); }, { @@ -475,6 +521,10 @@ describe("AsynchronousConfig", { concurrency: 1 }, () => { }); it("should throw when get is called", () => { + const config = new AsynchronousConfig( + path.join(__dirname, "fixtures", ".dotconfig") + ); + assert.throws(() => { config.get("foo"); }, { @@ -484,6 +534,10 @@ describe("AsynchronousConfig", { concurrency: 1 }, () => { }); it("should throw when setup autoReload", () => { + const config = new AsynchronousConfig( + path.join(__dirname, "fixtures", ".dotconfig") + ); + assert.throws(() => { config.setupAutoReload(); }, { @@ -492,4 +546,20 @@ describe("AsynchronousConfig", { concurrency: 1 }, () => { }); }); }); + + test("Given a JSON configuration file with a SyntaxError, read() method must return it", async() => { + const config = new AsynchronousConfig( + path.join(__dirname, "fixtures", "syntaxError.json") + ); + + await assert.rejects(async() => { + await config.read(); + }, { + name: "SyntaxError" + }); + }); }); + +function randomFileName(ext = ".json"): string { + return crypto.randomBytes(8).toString("hex") + ext; +} diff --git a/src/config/test/fixtures/.dummyConfig b/src/config/test/fixtures/.dotconfig similarity index 100% rename from src/config/test/fixtures/.dummyConfig rename to src/config/test/fixtures/.dotconfig diff --git a/src/config/test/fixtures/.empty b/src/config/test/fixtures/.empty index e69de29..9e26dfe 100644 --- a/src/config/test/fixtures/.empty +++ b/src/config/test/fixtures/.empty @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/config/test/fixtures/config.toml b/src/config/test/fixtures/config.toml index 78308b0..2fb5365 100644 --- a/src/config/test/fixtures/config.toml +++ b/src/config/test/fixtures/config.toml @@ -2,4 +2,4 @@ title = "TOML Example" [owner] name = "Tom Preston-Werner" -dob = 1979-05-27T07:32:00.000-08:00 \ No newline at end of file +dob = "1979-05-27T15:32:00.000Z" \ No newline at end of file