diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb41109..2985dbd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,15 +10,39 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [18.x] + node-version: [20.x] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - # - run: yarn audit - - run: yarn - - run: yarn lint:check - - run: yarn style:check - - run: yarn test --coverage + - name: Install Dependencies + run: yarn + - name: Check Linting + run: yarn lint:check + - name: Check Coding Style + run: yarn style:check + - name: Test with Coverage + run: | + # Check coverage, if coverage is below 100% for any files exit & print a message + yarn test --experimental-test-coverage > test-output.txt + + cat test-output.txt + + grep "tests/compiled/src" test-output.txt | awk -F'|' ' + BEGIN { + status=0; + } + { + for (i=2; i { let app: express.Application | undefined; let connection: Server; - beforeAll((done: jest.DoneCallback) => { + before((_, done) => { app = express(); app.use(express.json()); app.use(HMAC(SECRET)); @@ -33,15 +36,15 @@ describe("e2e", () => { connection = app.listen(PORT, done); }); - afterAll(() => { + after(() => { app = undefined; connection.close(); }); - test("passes hmac", async () => { + it("passes hmac", async () => { const time: number = Date.now(); const body = { foo: "bar" }; - const url = new URL(`http://localhost:${PORT}/test`); + const url = new URL(`http://127.0.0.1:${PORT}/test`); const response: Response = await got.post(url, { json: body, hooks: { @@ -62,7 +65,7 @@ describe("e2e", () => { }, }); - expect(response.statusCode).toBe(200); + assert.strictEqual(response.statusCode, 200); }); }); @@ -70,7 +73,7 @@ describe("e2e", () => { let app: express.Application | undefined; let connection: Server; - beforeAll((done: jest.DoneCallback) => { + before((_, done) => { const dynamicSecret = (req: express.Request) => { if (req.path.includes("foo")) { return "firstsecret"; @@ -104,12 +107,12 @@ describe("e2e", () => { connection = app.listen(PORT, done); }); - afterAll(() => { + after(() => { app = undefined; connection.close(); }); - test("passes with foo url", async () => { + it("passes with foo url", async () => { const time: number = Date.now(); const body = { foo: "bar" }; const url = new URL(`http://localhost:${PORT}/foo`); @@ -133,10 +136,10 @@ describe("e2e", () => { }, }); - expect(response.statusCode).toBe(200); + assert.strictEqual(response.statusCode, 200); }); - test("passes with bar url", async () => { + it("passes with bar url", async () => { const time: number = Date.now(); const body = { foo: "bar" }; const url = new URL(`http://localhost:${PORT}/bar`); @@ -160,7 +163,7 @@ describe("e2e", () => { }, }); - expect(response.statusCode).toBe(201); + assert.strictEqual(response.statusCode, 201); }); }); }); diff --git a/tests/tsconfig.json b/tests/tsconfig.json new file mode 100644 index 0000000..b21de4d --- /dev/null +++ b/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "include": ["**/*"], + "compilerOptions": { + "outDir": "./compiled" + } +} diff --git a/tests/unit/index.test.ts b/tests/unit/index.test.ts index 5ecc327..758803d 100644 --- a/tests/unit/index.test.ts +++ b/tests/unit/index.test.ts @@ -1,9 +1,10 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ import { request, Request, Response } from "express"; -import crypto from "crypto"; -import { jest } from "@jest/globals"; +import { describe, it, mock, afterEach } from "node:test"; +import { strict as assert } from "node:assert"; +const crypto = await import("node:crypto"); -import { HMAC, AuthError, generate, order } from "../../src/index"; +import { HMAC, AuthError, generate, order } from "../../src/index.js"; interface MockRequest { headers?: { @@ -14,10 +15,6 @@ interface MockRequest { body?: Record | unknown[]; } -interface Spies { - next: jest.Mock; -} - const SECRET = "secret"; const TIME = 1573504737300; const METHOD = "POST"; @@ -50,15 +47,13 @@ function mockedRequest(override: MockRequest = {}): Partial { } describe("unit", () => { - const spies: Spies = { - next: jest.fn(), + const spies = { + next: mock.fn(), }; - afterEach(() => { - jest.clearAllMocks(); - }); + afterEach(() => spies.next.mock.resetCalls()); - test("passes hmac", async () => { + it("passes hmac", async () => { const originalDateNow = Date.now.bind(global.Date); global.Date.now = () => TIME; @@ -66,12 +61,13 @@ describe("unit", () => { await middleware(mockedRequest() as Request, {} as Response, spies.next); - expect(spies.next).toHaveBeenCalledWith(); + assert.strictEqual(spies.next.mock.calls.length, 1); + assert.deepStrictEqual(spies.next.mock.calls[0].arguments, []); global.Date.now = originalDateNow; }); - test("passes hmac with GET", async () => { + it("passes hmac with GET", async () => { const originalDateNow = Date.now.bind(global.Date); global.Date.now = () => TIME; @@ -96,12 +92,13 @@ describe("unit", () => { spies.next ); - expect(spies.next).toHaveBeenCalledWith(); + assert.strictEqual(spies.next.mock.calls.length, 1); + assert.deepStrictEqual(spies.next.mock.calls[0].arguments, []); global.Date.now = originalDateNow; }); - test("passes hamc with array as value", async () => { + it("passes hamc with array as value", async () => { const originalDateNow = Date.now.bind(global.Date); global.Date.now = () => TIME; @@ -127,55 +124,59 @@ describe("unit", () => { spies.next ); - expect(spies.next).toHaveBeenCalledWith(); + assert.strictEqual(spies.next.mock.calls.length, 1); + assert.deepStrictEqual(spies.next.mock.calls[0].arguments, []); global.Date.now = originalDateNow; }); - test("passes with every available algorithm", async () => { + it("passes with every available algorithm", async (t) => { const originalDateNow = Date.now.bind(global.Date); global.Date.now = () => TIME; for (const hash of crypto.getHashes()) { - try { - const middleware = HMAC(SECRET, { algorithm: hash }); - - await middleware( - mockedRequest({ - headers: { - authorization: `HMAC ${TIME}:${generate( - SECRET, - hash, - TIME, - METHOD, - URL, - BODY - ).digest("hex")}`, - }, - }) as Request, - {} as Response, - spies.next - ); - - expect(spies.next).toHaveBeenCalledWith(); - jest.clearAllMocks(); - } catch (e) { - // this error is for algos that are not supported by openssl - // this can change per platform so we can not have a fixed exclusion list - // instead we simply check if we get the error indicating it's not supported and skip over it - if ( - e instanceof Error && - e?.message !== "error:00000000:lib(0):func(0):reason(0)" - ) { - throw e; + await t.test(hash, async () => { + try { + const middleware = HMAC(SECRET, { algorithm: hash }); + + await middleware( + mockedRequest({ + headers: { + authorization: `HMAC ${TIME}:${generate( + SECRET, + hash, + TIME, + METHOD, + URL, + BODY + ).digest("hex")}`, + }, + }) as Request, + {} as Response, + spies.next + ); + + assert.strictEqual(spies.next.mock.calls.length, 1); + assert.deepStrictEqual(spies.next.mock.calls[0].arguments, []); + } catch (e) { + // this error is for algos that are not supported by openssl + // this can change per platform so we can not have a fixed exclusion list + // instead we simply check if we get the error indicating it's not supported and skip over it + if ( + e instanceof Error && + e?.message !== "error:00000000:lib(0):func(0):reason(0)" && + e?.message !== "error:00000000:lib(0)::reason(0)" + ) { + throw e; + } } - } + }); } global.Date.now = originalDateNow; }); - test("passes hmac with different algorithm", async () => { + it("passes hmac with different algorithm", async () => { const originalDateNow = Date.now.bind(global.Date); global.Date.now = () => TIME; @@ -198,12 +199,13 @@ describe("unit", () => { spies.next ); - expect(spies.next).toHaveBeenCalledWith(); + assert.strictEqual(spies.next.mock.calls.length, 1); + assert.deepStrictEqual(spies.next.mock.calls[0].arguments, []); global.Date.now = originalDateNow; }); - test("passes hmac without body", async () => { + it("passes hmac without body", async () => { const originalDateNow = Date.now.bind(global.Date); global.Date.now = () => TIME; @@ -226,12 +228,13 @@ describe("unit", () => { spies.next ); - expect(spies.next).toHaveBeenCalledWith(); + assert.strictEqual(spies.next.mock.calls.length, 1); + assert.deepStrictEqual(spies.next.mock.calls[0].arguments, []); global.Date.now = originalDateNow; }); - test("fails hmac not matching", async () => { + it("fails hmac not matching", async () => { const originalDateNow = Date.now.bind(global.Date); global.Date.now = () => TIME; @@ -240,16 +243,18 @@ describe("unit", () => { await middleware(mockedRequest() as Request, {} as Response, spies.next); const calledArgs = spies.next.mock.calls.pop(); - const calledArg = calledArgs?.[0]; - expect(calledArg).toBeInstanceOf(AuthError); + const calledArg = Array.isArray(calledArgs?.arguments) + ? calledArgs?.arguments[0] + : undefined; + assert(calledArg instanceof AuthError); if (checkArgMessage(calledArg)) { - expect(calledArg?.["message"]).toBe("HMAC's did not match"); + assert.strictEqual(calledArg?.["message"], "HMAC's did not match"); } global.Date.now = originalDateNow; }); - test("fails hmac on no header", async () => { + it("fails hmac on no header", async () => { const middleware = HMAC(SECRET); await middleware( @@ -259,16 +264,19 @@ describe("unit", () => { ); const calledArgs = spies.next.mock.calls.pop(); - const calledArg = calledArgs?.[0]; - expect(calledArg).toBeInstanceOf(AuthError); + const calledArg = Array.isArray(calledArgs?.arguments) + ? calledArgs?.arguments[0] + : undefined; + assert(calledArg instanceof AuthError); if (checkArgMessage(calledArg)) { - expect(calledArg?.["message"]).toBe( + assert.strictEqual( + calledArg?.["message"], "Header provided not in sent headers. Expected authorization but not found in request.headers" ); } }); - test("fails hmac on no header with custom header", async () => { + it("fails hmac on no header with custom header", async () => { const middleware = HMAC(SECRET, { header: "myhmac" }); await middleware( @@ -278,16 +286,19 @@ describe("unit", () => { ); const calledArgs = spies.next.mock.calls.pop(); - const calledArg = calledArgs?.[0]; - expect(calledArg).toBeInstanceOf(AuthError); + const calledArg = Array.isArray(calledArgs?.arguments) + ? calledArgs?.arguments[0] + : undefined; + assert(calledArg instanceof AuthError); if (checkArgMessage(calledArg)) { - expect(calledArg?.["message"]).toBe( + assert.strictEqual( + calledArg?.["message"], "Header provided not in sent headers. Expected myhmac but not found in request.headers" ); } }); - test("fails hmac on incorrect identifier", async () => { + it("fails hmac on incorrect identifier", async () => { const middleware = HMAC(SECRET); await middleware( @@ -297,16 +308,19 @@ describe("unit", () => { ); const calledArgs = spies.next.mock.calls.pop(); - const calledArg = calledArgs?.[0]; - expect(calledArg).toBeInstanceOf(AuthError); + const calledArg = Array.isArray(calledArgs?.arguments) + ? calledArgs?.arguments[0] + : undefined; + assert(calledArg instanceof AuthError); if (checkArgMessage(calledArg)) { - expect(calledArg?.["message"]).toBe( + assert.strictEqual( + calledArg?.["message"], "Header did not start with correct identifier. Expected HMAC but not found in options.header" ); } }); - test("fails hmac on incorrect identifier with custom identifier", async () => { + it("fails hmac on incorrect identifier with custom identifier", async () => { const middleware = HMAC(SECRET, { identifier: "BAR" }); await middleware( @@ -316,16 +330,19 @@ describe("unit", () => { ); const calledArgs = spies.next.mock.calls.pop(); - const calledArg = calledArgs?.[0]; - expect(calledArg).toBeInstanceOf(AuthError); + const calledArg = Array.isArray(calledArgs?.arguments) + ? calledArgs?.arguments[0] + : undefined; + assert(calledArg instanceof AuthError); if (checkArgMessage(calledArg)) { - expect(calledArg?.["message"]).toBe( + assert.strictEqual( + calledArg?.["message"], "Header did not start with correct identifier. Expected BAR but not found in options.header" ); } }); - test("fails if unix timestamp not found", async () => { + it("fails if unix timestamp not found", async () => { const middleware = HMAC(SECRET); await middleware( @@ -337,16 +354,19 @@ describe("unit", () => { ); const calledArgs = spies.next.mock.calls.pop(); - const calledArg = calledArgs?.[0]; - expect(calledArg).toBeInstanceOf(AuthError); + const calledArg = Array.isArray(calledArgs?.arguments) + ? calledArgs?.arguments[0] + : undefined; + assert(calledArg instanceof AuthError); if (checkArgMessage(calledArg)) { - expect(calledArg?.["message"]).toBe( + assert.strictEqual( + calledArg?.["message"], "Unix timestamp was not present in header" ); } }); - test("fails if time difference too great", async () => { + it("fails if time difference too great", async () => { const originalDateNow = Date.now.bind(global.Date); global.Date.now = () => 1573508732400; @@ -355,10 +375,13 @@ describe("unit", () => { await middleware(mockedRequest() as Request, {} as Response, spies.next); const calledArgs = spies.next.mock.calls.pop(); - const calledArg = calledArgs?.[0]; - expect(calledArg).toBeInstanceOf(AuthError); + const calledArg = Array.isArray(calledArgs?.arguments) + ? calledArgs?.arguments[0] + : undefined; + assert(calledArg instanceof AuthError); if (checkArgMessage(calledArg)) { - expect(calledArg?.["message"]).toBe( + assert.strictEqual( + calledArg?.["message"], "Time difference between generated and requested time is too great" ); } @@ -366,7 +389,7 @@ describe("unit", () => { global.Date.now = originalDateNow; }); - test("fails if time difference too great with custom time", async () => { + it("fails if time difference too great with custom time", async () => { const originalDateNow = Date.now.bind(global.Date); global.Date.now = () => 1573591800000; @@ -376,10 +399,13 @@ describe("unit", () => { await middleware(mockedRequest() as Request, {} as Response, spies.next); const calledArgs = spies.next.mock.calls.pop(); - const calledArg = calledArgs?.[0]; - expect(calledArg).toBeInstanceOf(AuthError); + const calledArg = Array.isArray(calledArgs?.arguments) + ? calledArgs?.arguments[0] + : undefined; + assert(calledArg instanceof AuthError); if (checkArgMessage(calledArg)) { - expect(calledArg?.["message"]).toBe( + assert.strictEqual( + calledArg?.["message"], "Time difference between generated and requested time is too great" ); } @@ -387,7 +413,7 @@ describe("unit", () => { global.Date.now = originalDateNow; }); - test("passes if within maxInterval", async () => { + it("passes if within maxInterval", async () => { const originalDateNow = Date.now.bind(global.Date); global.Date.now = () => 1573588200000; @@ -396,12 +422,12 @@ describe("unit", () => { await middleware(mockedRequest() as Request, {} as Response, spies.next); - expect(spies.next).toHaveBeenCalledWith(); + assert.strictEqual(spies.next.mock.calls.length, 1); global.Date.now = originalDateNow; }); - test("fails if time before timestamp in hmac", async () => { + it("fails if time before timestamp in hmac", async () => { const originalDateNow = Date.now.bind(global.Date); global.Date.now = () => 1542055800000; @@ -410,10 +436,13 @@ describe("unit", () => { await middleware(mockedRequest() as Request, {} as Response, spies.next); const calledArgs = spies.next.mock.calls.pop(); - const calledArg = calledArgs?.[0]; - expect(calledArg).toBeInstanceOf(AuthError); + const calledArg = Array.isArray(calledArgs?.arguments) + ? calledArgs?.arguments[0] + : undefined; + assert(calledArg instanceof AuthError); if (checkArgMessage(calledArg)) { - expect(calledArg?.["message"]).toBe( + assert.strictEqual( + calledArg?.["message"], "Time difference between generated and requested time is too great" ); } @@ -421,7 +450,7 @@ describe("unit", () => { global.Date.now = originalDateNow; }); - test("passes if time before timestamp in hmac but minInterval is configured", async () => { + it("passes if time before timestamp in hmac but minInterval is configured", async () => { const originalDateNow = Date.now.bind(global.Date); global.Date.now = () => 1573504736300; @@ -429,12 +458,12 @@ describe("unit", () => { await middleware(mockedRequest() as Request, {} as Response, spies.next); - expect(spies.next).toHaveBeenCalledWith(); + assert.strictEqual(spies.next.mock.calls.length, 1); global.Date.now = originalDateNow; }); - test("fails if missing hmac digest", async () => { + it("fails if missing hmac digest", async () => { const originalDateNow = Date.now.bind(global.Date); global.Date.now = () => TIME; @@ -449,10 +478,13 @@ describe("unit", () => { ); const calledArgs = spies.next.mock.calls.pop(); - const calledArg = calledArgs?.[0]; - expect(calledArg).toBeInstanceOf(AuthError); + const calledArg = Array.isArray(calledArgs?.arguments) + ? calledArgs?.arguments[0] + : undefined; + assert(calledArg instanceof AuthError); if (checkArgMessage(calledArg)) { - expect(calledArg?.["message"]).toBe( + assert.strictEqual( + calledArg?.["message"], "HMAC digest was not present in header" ); @@ -460,7 +492,7 @@ describe("unit", () => { } }); - test("passes hmac with empty object as body", async () => { + it("passes hmac with empty object as body", async () => { const originalDateNow = Date.now.bind(global.Date); global.Date.now = () => TIME; @@ -484,12 +516,12 @@ describe("unit", () => { spies.next ); - expect(spies.next).toHaveBeenCalledWith(); + assert.strictEqual(spies.next.mock.calls.length, 1); global.Date.now = originalDateNow; }); - test("passes hmac with basic object as body", async () => { + it("passes hmac with basic object as body", async () => { const originalDateNow = Date.now.bind(global.Date); global.Date.now = () => TIME; @@ -515,12 +547,12 @@ describe("unit", () => { spies.next ); - expect(spies.next).toHaveBeenCalledWith(); + assert.strictEqual(spies.next.mock.calls.length, 1); global.Date.now = originalDateNow; }); - test("passes hmac with complex object as body", async () => { + it("passes hmac with complex object as body", async () => { const originalDateNow = Date.now.bind(global.Date); global.Date.now = () => TIME; @@ -546,12 +578,12 @@ describe("unit", () => { spies.next ); - expect(spies.next).toHaveBeenCalledWith(); + assert.strictEqual(spies.next.mock.calls.length, 1); global.Date.now = originalDateNow; }); - test("passes hmac with empty array as body", async () => { + it("passes hmac with empty array as body", async () => { const originalDateNow = Date.now.bind(global.Date); global.Date.now = () => TIME; @@ -577,12 +609,12 @@ describe("unit", () => { spies.next ); - expect(spies.next).toHaveBeenCalledWith(); + assert.strictEqual(spies.next.mock.calls.length, 1); global.Date.now = originalDateNow; }); - test("passes hmac with array as body", async () => { + it("passes hmac with array as body", async () => { const originalDateNow = Date.now.bind(global.Date); global.Date.now = () => TIME; @@ -608,12 +640,12 @@ describe("unit", () => { spies.next ); - expect(spies.next).toHaveBeenCalledWith(); + assert.strictEqual(spies.next.mock.calls.length, 1); global.Date.now = originalDateNow; }); - test("hmac with async dynamic secret", async () => { + it("hmac with async dynamic secret", async () => { const originalDateNow = Date.now.bind(global.Date); global.Date.now = () => TIME; @@ -642,12 +674,12 @@ describe("unit", () => { spies.next ); - expect(spies.next).toHaveBeenCalledWith(); + assert.strictEqual(spies.next.mock.calls.length, 1); global.Date.now = originalDateNow; }); - test("hmac correctly throws error if dynamic secret function returns undefined", async () => { + it("hmac correctly throws error if dynamic secret function returns undefined", async () => { const originalDateNow = Date.now.bind(global.Date); global.Date.now = () => TIME; @@ -655,7 +687,8 @@ describe("unit", () => { await middleware(mockedRequest() as Request, {} as Response, spies.next); - expect(spies.next).toHaveBeenCalledWith( + assert.deepStrictEqual( + spies.next.mock.calls[0].arguments[0], new AuthError( "Invalid secret. Expected non-empty string but got 'undefined' (type: undefined)" ) @@ -664,7 +697,7 @@ describe("unit", () => { global.Date.now = originalDateNow; }); - test("hmac correctly throws error if dynamic secret function returns array", async () => { + it("hmac correctly throws error if dynamic secret function returns array", async () => { const originalDateNow = Date.now.bind(global.Date); global.Date.now = () => TIME; @@ -673,7 +706,8 @@ describe("unit", () => { await middleware(mockedRequest() as Request, {} as Response, spies.next); - expect(spies.next).toHaveBeenCalledWith( + assert.deepStrictEqual( + spies.next.mock.calls[0].arguments[0], new AuthError( "Invalid secret. Expected non-empty string but got '1,2' (type: object)" ) @@ -682,7 +716,7 @@ describe("unit", () => { global.Date.now = originalDateNow; }); - test("hmac correctly throws error if dynamic secret function returns empty string", async () => { + it("hmac correctly throws error if dynamic secret function returns empty string", async () => { const originalDateNow = Date.now.bind(global.Date); global.Date.now = () => TIME; @@ -690,7 +724,8 @@ describe("unit", () => { await middleware(mockedRequest() as Request, {} as Response, spies.next); - expect(spies.next).toHaveBeenCalledWith( + assert.deepStrictEqual( + spies.next.mock.calls[0].arguments[0], new AuthError( "Invalid secret. Expected non-empty string but got '' (type: string)" ) @@ -699,7 +734,7 @@ describe("unit", () => { global.Date.now = originalDateNow; }); - test("hmac correctly handles differently ordered json", async () => { + it("hmac correctly handles differently ordered json", async () => { const originalDateNow = Date.now.bind(global.Date); global.Date.now = () => TIME; @@ -725,74 +760,82 @@ describe("unit", () => { spies.next ); - expect(spies.next).toHaveBeenCalledWith(); + assert.strictEqual(spies.next.mock.calls.length, 1); global.Date.now = originalDateNow; }); // Some users aren't going to be using TS, we need to ensure these test still work even though you can't expose the error if you use TS - test("passing incorrect secret throws an error", () => { - expect(() => HMAC("")).toThrowError( + it("passing incorrect secret throws an error", () => { + assert.throws( + () => HMAC(""), new TypeError( `Invalid value provided for property secret. Expected non-empty string or function but got '' (type: string)` ) ); - // @ts-ignore - expect(() => HMAC(23)).toThrowError( + + assert.throws( + // @ts-ignore + () => HMAC(23), new TypeError( `Invalid value provided for property secret. Expected non-empty string or function but got '23' (type: number)` ) ); }); - test("passing incorrect algorithm throws an error", () => { - expect(() => HMAC(SECRET, { algorithm: "sha111" })).toThrowError( + it("passing incorrect algorithm throws an error", () => { + assert.throws( + () => HMAC(SECRET, { algorithm: "sha111" }), new TypeError( `Invalid value provided for property options.algorithm. Expected value from crypto.getHashes() but got sha111 (type: string)` ) ); }); - test("passing incorrect identifier throws an error", () => { - // @ts-ignore - expect(() => HMAC(SECRET, { identifier: 123 })).toThrowError( + it("passing incorrect identifier throws an error", () => { + assert.throws( + // @ts-ignore + () => HMAC(SECRET, { identifier: 123 }), new TypeError( `Invalid value provided for property options.identifier. Expected non-empty string but got '123' (type: number)` ) ); }); - test("passing incorrect header throws an error", () => { - // @ts-ignore - expect(() => HMAC(SECRET, { header: 123 })).toThrowError( + it("passing incorrect header throws an error", () => { + assert.throws( + // @ts-ignore + () => HMAC(SECRET, { header: 123 }), new TypeError( `Invalid value provided for property options.header. Expected non-empty string but got '123' (type: number)` ) ); }); - test("passing incorrect maxInterval throws an error", () => { - // @ts-ignore - expect(() => HMAC(SECRET, { maxInterval: "abc" })).toThrowError( + it("passing incorrect maxInterval throws an error", () => { + assert.throws( + // @ts-ignore + () => HMAC(SECRET, { maxInterval: "abc" }), new TypeError( `Invalid value provided for property options.maxInterval. Expected number but got 'abc' (type: string)` ) ); }); - test("passing incorrect minInterval throws an error", () => { - // @ts-ignore - expect(() => HMAC(SECRET, { minInterval: "abc" })).toThrowError( + it("passing incorrect minInterval throws an error", () => { + assert.throws( + // @ts-ignore + () => HMAC(SECRET, { minInterval: "abc" }), new TypeError( `Invalid value provided for optional property options.minInterval. Expected positive number but got 'abc' (type: string)` ) ); }); - test("passing negative number for minInterval throws an error", () => { - // @ts-ignore - expect(() => HMAC(SECRET, { minInterval: -1 })).toThrowError( + it("passing negative number for minInterval throws an error", () => { + assert.throws( + () => HMAC(SECRET, { minInterval: -1 }), new TypeError( `Invalid value provided for optional property options.minInterval. Expected positive number but got '-1' (type: number)` ) diff --git a/tests/unit/order.test.ts b/tests/unit/order.test.ts index 7043981..63330d8 100644 --- a/tests/unit/order.test.ts +++ b/tests/unit/order.test.ts @@ -1,8 +1,10 @@ -import order from "../../src/order"; +import order from "../../src/order.js"; +import { describe, it } from "node:test"; +import { strict as assert } from "node:assert"; describe("orderJson", () => { - test("should return json ordered alphabetically by property name", () => { - expect( + it("should return json ordered alphabetically by property name", () => { + assert.deepStrictEqual( order({ _id: "6223fbad35687c97fd227957", index: 0, @@ -43,47 +45,48 @@ describe("orderJson", () => { ], greeting: "Hello, Jacqueline Wooten! You have 4 unread messages.", favoriteFruit: "banana", - }) - ).toEqual({ - about: - "Nulla non eu laboris eu eu laboris duis ipsum. Dolore nostrud qui aliquip velit. Eu minim reprehenderit elit cillum sunt. Aliquip ut et fugiat consectetur veniam tempor eiusmod. Mollit officia laboris aute dolor incididunt id pariatur dolore non ut culpa ullamco enim. Ad ex ipsum irure fugiat laboris magna culpa.\r\n", - address: "436 Taylor Street, Manchester, Washington, 8727", - age: 35, - balance: "$1,141.86", - company: "TERRAGEN", - email: "jacquelinewooten@terragen.com", - eyeColor: "blue", - favoriteFruit: "banana", - friends: [ - { - age: 50, - id: 0, - name: "Tracy Harding", - }, - { - age: 24, - id: 1, - name: "Hodge Harrington", - }, - { - age: 74, - id: 2, - name: "Pierce Bailey", - }, - ], - gender: "female", - greeting: "Hello, Jacqueline Wooten! You have 4 unread messages.", - guid: "af604232-484d-4706-8867-d1a5789e8c76", - latitude: 0.848836, - _id: "6223fbad35687c97fd227957", - index: 0, - longitude: 70.415866, - isActive: true, - name: "Jacqueline Wooten", - phone: "+1 (875) 405-2646", - picture: "http://placehold.it/32x32", - registered: "2017-03-15T09:16:34 -00:00", - tags: ["esse", "ea", "dolore", "velit", "sint", "deserunt", "occaecat"], - }); + }), + { + about: + "Nulla non eu laboris eu eu laboris duis ipsum. Dolore nostrud qui aliquip velit. Eu minim reprehenderit elit cillum sunt. Aliquip ut et fugiat consectetur veniam tempor eiusmod. Mollit officia laboris aute dolor incididunt id pariatur dolore non ut culpa ullamco enim. Ad ex ipsum irure fugiat laboris magna culpa.\r\n", + address: "436 Taylor Street, Manchester, Washington, 8727", + age: 35, + balance: "$1,141.86", + company: "TERRAGEN", + email: "jacquelinewooten@terragen.com", + eyeColor: "blue", + favoriteFruit: "banana", + friends: [ + { + age: 50, + id: 0, + name: "Tracy Harding", + }, + { + age: 24, + id: 1, + name: "Hodge Harrington", + }, + { + age: 74, + id: 2, + name: "Pierce Bailey", + }, + ], + gender: "female", + greeting: "Hello, Jacqueline Wooten! You have 4 unread messages.", + guid: "af604232-484d-4706-8867-d1a5789e8c76", + latitude: 0.848836, + _id: "6223fbad35687c97fd227957", + index: 0, + longitude: 70.415866, + isActive: true, + name: "Jacqueline Wooten", + phone: "+1 (875) 405-2646", + picture: "http://placehold.it/32x32", + registered: "2017-03-15T09:16:34 -00:00", + tags: ["esse", "ea", "dolore", "velit", "sint", "deserunt", "occaecat"], + } + ); }); }); diff --git a/tsconfig.json b/tsconfig.json index c37dc43..e49b301 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,73 +1,110 @@ { "include": ["src/**/*"], "compilerOptions": { - /* Visit https://aka.ms/tsconfig.json to read more about this file */ + /* Visit https://aka.ms/tsconfig to read more about this file */ - /* Basic Options */ - // "incremental": true, /* Enable incremental compilation */ - "target": "ES2019" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */, - "module": "NodeNext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, - // "lib": [], /* Specify library files to be included in the compilation. */ - // "allowJs": true, /* Allow javascript files to be compiled. */ - // "checkJs": true, /* Report errors in .js files. */ - // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ - "declaration": true /* Generates corresponding '.d.ts' file. */, - // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ - // "sourceMap": true, /* Generates corresponding '.map' file. */ - // "outFile": "./", /* Concatenate and emit output to single file. */ - "outDir": "./dist" /* Redirect output structure to the directory. */, - // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ - // "composite": true, /* Enable project compilation */ - // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ - // "removeComments": true, /* Do not emit comments to output. */ - // "noEmit": true, /* Do not emit outputs. */ - // "importHelpers": true, /* Import emit helpers from 'tslib'. */ - // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ - // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - /* Strict Type-Checking Options */ - "strict": true /* Enable all strict type-checking options. */, - // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* Enable strict null checks. */ - // "strictFunctionTypes": true, /* Enable strict checking of function types. */ - // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ - // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ - // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ - // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + /* Language and Environment */ + "target": "es2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "nodenext" /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "nodenext" /* Specify how TypeScript looks up a file from a given module specifier. */, + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ - /* Additional Checks */ - "noUnusedLocals": true /* Report errors on unused locals. */, - "noUnusedParameters": true /* Report errors on unused parameters. */, - "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, - "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, - "noUncheckedIndexedAccess": true /* Include 'undefined' in index signature results */, - "noImplicitOverride": true /* Ensure overriding members in derived classes are marked with an 'override' modifier. */, - "noPropertyAccessFromIndexSignature": true /* Require undeclared properties from index signatures to use element accesses. */, + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ - /* Module Resolution Options */ - "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, - // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ - // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ - // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ - // "typeRoots": [], /* List of folders to include type definitions from. */ - // "types": [], /* Type declaration files to be included in compilation. */ - // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ - "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, - // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + /* Emit */ + "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./dist" /* Specify an output folder for all emitted files. */, + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ - /* Source Map Options */ - // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ - // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, - /* Experimental Options */ - // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ - // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - /* Advanced Options */ - "skipLibCheck": true /* Skip type checking of declaration files. */, - "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ } }