From 648783928f775325ac28da2b5a03679f0dbd7bb9 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Wed, 4 Feb 2026 00:03:56 -0800 Subject: [PATCH 1/3] build: use default node test runner for unit tests --- package-lock.json | 119 ----- package.json | 2 - test/client.spec.js | 529 ++++++++++++-------- test/config.spec.js | 148 ++++-- test/content.spec.js | 252 +++++----- test/fixtures/payload-invalid.json | 2 + test/fixtures/payload-invalid.yaml | 1 + test/fixtures/payload-with-env-vars.json | 56 +++ test/fixtures/payload-with-github-vars.json | 4 + test/fixtures/payload-with-missing-var.json | 3 + test/fixtures/payload.json | 4 + test/fixtures/payload.yaml | 2 + test/fixtures/payload.yaml.md | 2 + test/fixtures/payload.yml | 2 + test/index.spec.js | 116 +++-- test/logger.spec.js | 4 +- test/send.spec.js | 78 +-- test/webhook.spec.js | 162 +++--- 18 files changed, 811 insertions(+), 675 deletions(-) create mode 100644 test/fixtures/payload-invalid.json create mode 100644 test/fixtures/payload-invalid.yaml create mode 100644 test/fixtures/payload-with-env-vars.json create mode 100644 test/fixtures/payload-with-github-vars.json create mode 100644 test/fixtures/payload-with-missing-var.json create mode 100644 test/fixtures/payload.json create mode 100644 test/fixtures/payload.yaml create mode 100644 test/fixtures/payload.yaml.md create mode 100644 test/fixtures/payload.yml diff --git a/package-lock.json b/package-lock.json index e58d4545..f3131294 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,9 +26,7 @@ "@types/js-yaml": "^4.0.9", "@types/markup-js": "^1.5.0", "@types/node": "^20.19.30", - "@types/sinon": "^21.0.0", "@vercel/ncc": "^0.38.4", - "sinon": "^21.0.1", "typescript": "^5.9.3" }, "engines": { @@ -386,47 +384,6 @@ "@octokit/openapi-types": "^27.0.0" } }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.1.0.tgz", - "integrity": "sha512-cqfapCxwTGsrR80FEgOoPsTonoefMBY7dnUEbQ+GRcved0jvkJLzvX6F4WtN+HBqbPX/SiFsIRUp+IrCW/2I2w==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1" - } - }, - "node_modules/@sinonjs/samsam": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.3.tgz", - "integrity": "sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1", - "type-detect": "^4.1.0" - } - }, - "node_modules/@sinonjs/samsam/node_modules/type-detect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", - "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/@slack/logger": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@slack/logger/-/logger-4.0.0.tgz", @@ -510,23 +467,6 @@ "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", "license": "MIT" }, - "node_modules/@types/sinon": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-21.0.0.tgz", - "integrity": "sha512-+oHKZ0lTI+WVLxx1IbJDNmReQaIsQJjN2e7UUrJHEeByG7bFeKJYsv1E75JxTQ9QKJDp21bAa/0W2Xo4srsDnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/sinonjs__fake-timers": "*" - } - }, - "node_modules/@types/sinonjs__fake-timers": { - "version": "8.1.5", - "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", - "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@vercel/ncc": { "version": "0.38.4", "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.38.4.tgz", @@ -634,16 +574,6 @@ "node": ">=0.4.0" } }, - "node_modules/diff": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", - "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -830,15 +760,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -1043,36 +964,6 @@ "node": ">= 4" } }, - "node_modules/sinon": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-21.0.1.tgz", - "integrity": "sha512-Z0NVCW45W8Mg5oC/27/+fCqIHFnW8kpkFOq0j9XJIev4Ld0mKmERaZv5DMLAb9fGCevjKwaEeIQz5+MBXfZcDw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1", - "@sinonjs/fake-timers": "^15.1.0", - "@sinonjs/samsam": "^8.0.3", - "diff": "^8.0.2", - "supports-color": "^7.2.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/sinon" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/tunnel": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", @@ -1082,16 +973,6 @@ "node": ">=0.6.11 <=0.7.0 || >=0.7.3" } }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", diff --git a/package.json b/package.json index f650e191..70b6ee5b 100644 --- a/package.json +++ b/package.json @@ -51,9 +51,7 @@ "@types/js-yaml": "^4.0.9", "@types/markup-js": "^1.5.0", "@types/node": "^20.19.30", - "@types/sinon": "^21.0.0", "@vercel/ncc": "^0.38.4", - "sinon": "^21.0.1", "typescript": "^5.9.3" } } diff --git a/test/client.spec.js b/test/client.spec.js index 26e613b9..9e486f67 100644 --- a/test/client.spec.js +++ b/test/client.spec.js @@ -1,8 +1,7 @@ import assert from "node:assert"; -import { beforeEach, describe, it } from "node:test"; +import { beforeEach, describe, it, mock } from "node:test"; import webapi from "@slack/web-api"; import errors from "@slack/web-api/dist/errors.js"; -import sinon from "sinon"; import Client from "../src/client.js"; import Config from "../src/config.js"; import SlackError from "../src/errors.js"; @@ -48,7 +47,10 @@ describe("client", () => { method: "chat.postMessage", }, }; - mocks.core.getInput.withArgs("token").returns("xoxb-example-001"); + mocks.inputs = { + ...mocks.inputs, + token: "xoxb-example-001", + }; try { await new Client().post(config); assert.fail("Failed to throw for missing input"); @@ -64,10 +66,13 @@ describe("client", () => { describe("api", async () => { it("uses arguments to send to a slack api method", async () => { - const apis = sinon.stub().resolves({ ok: true }); - const constructors = sinon - .stub(mocks.webapi, "WebClient") - .returns({ apiCall: apis }); + const apis = mock.fn(() => Promise.resolve({ ok: true })); + const constructorCalls = []; + // Use a regular function as constructor since mock.fn() doesn't work with `new` + mocks.webapi.WebClient = function (...args) { + constructorCalls.push(args); + this.apiCall = apis; + }; /** * @type {Config} */ @@ -87,31 +92,40 @@ describe("client", () => { webapi: mocks.webapi, }; await new Client().post(config); - assert.ok(constructors.calledWithNew()); - assert.ok( - constructors.calledWith("xoxb-example-002", { - agent: undefined, - allowAbsoluteUrls: false, - logger: config.logger, - retryConfig: webapi.retryPolicies.fiveRetriesInFiveMinutes, - slackApiUrl: undefined, - }), - ); - assert.ok(apis.calledOnce); + assert.equal(constructorCalls.length, 1); + const [token, options] = constructorCalls[0]; + assert.equal(token, "xoxb-example-002"); + assert.deepEqual(options, { + agent: undefined, + allowAbsoluteUrls: false, + logger: config.logger, + retryConfig: webapi.retryPolicies.fiveRetriesInFiveMinutes, + slackApiUrl: undefined, + }); + assert.equal(apis.mock.callCount(), 1); + const [method, args] = apis.mock.calls[0].arguments; + assert.equal(method, "pins.add"); + assert.deepEqual(args, { + channel: "CHANNELHERE", + timestamp: "1234567890.000000", + }); assert.ok( - apis.calledWith("pins.add", { - channel: "CHANNELHERE", - timestamp: "1234567890.000000", - }), + mocks.core.setOutput.mock.calls.some( + (c) => c.arguments[0] === "ok" && c.arguments[1] === true, + ), ); - assert.ok(config.core.setOutput.calledWith("ok", true)); }); it("uses arguments to send to a custom api method", async () => { - const apis = sinon.stub().resolves({ done: true, response: "Infinite" }); - const constructors = sinon - .stub(mocks.webapi, "WebClient") - .returns({ apiCall: apis }); + const apis = mock.fn(() => + Promise.resolve({ done: true, response: "Infinite" }), + ); + const constructorCalls = []; + // Use a regular function as constructor since mock.fn() doesn't work with `new` + mocks.webapi.WebClient = function (...args) { + constructorCalls.push(args); + this.apiCall = apis; + }; /** * @type {Config} */ @@ -134,29 +148,35 @@ describe("client", () => { webapi: mocks.webapi, }; await new Client().post(config); - assert.ok(constructors.calledWithNew()); + assert.equal(constructorCalls.length, 1); + const [token, options] = constructorCalls[0]; + assert.equal(token, "ollamapassword"); + assert.deepEqual(options, { + agent: undefined, + allowAbsoluteUrls: false, + logger: config.logger, + retryConfig: webapi.retryPolicies.tenRetriesInAboutThirtyMinutes, + slackApiUrl: "http://localhost:11434/api/", + }); + assert.equal(apis.mock.callCount(), 1); + const [method, args] = apis.mock.calls[0].arguments; + assert.equal(method, "generate"); + assert.deepEqual(args, { + model: "llama3.2", + prompt: "How many sides does a circle have?", + stream: false, + }); assert.ok( - constructors.calledWith("ollamapassword", { - agent: undefined, - allowAbsoluteUrls: false, - logger: config.logger, - retryConfig: webapi.retryPolicies.tenRetriesInAboutThirtyMinutes, - slackApiUrl: "http://localhost:11434/api/", - }), - ); - assert.ok(apis.calledOnce); - assert.ok( - apis.calledWith("generate", { - model: "llama3.2", - prompt: "How many sides does a circle have?", - stream: false, - }), + mocks.core.setOutput.mock.calls.some( + (c) => c.arguments[0] === "ok" && c.arguments[1] === undefined, + ), ); - assert.ok(config.core.setOutput.calledWith("ok", undefined)); assert.ok( - config.core.setOutput.calledWith( - "response", - JSON.stringify({ done: true, response: "Infinite" }), + mocks.core.setOutput.mock.calls.some( + (c) => + c.arguments[0] === "response" && + c.arguments[1] === + JSON.stringify({ done: true, response: "Infinite" }), ), ); }); @@ -164,123 +184,150 @@ describe("client", () => { describe("success", () => { it("calls 'chat.postMessage' with the given token and content", async () => { - try { - const args = { - channel: "C0123456789", - text: "hello", + const args = { + channel: "C0123456789", + text: "hello", + thread_ts: "1234567890.000001", + }; + const response = { + ok: true, + channel: "C0123456789", + ts: "1234567890.000002", + message: { thread_ts: "1234567890.000001", - }; - const response = { - ok: true, - channel: "C0123456789", - ts: "1234567890.000002", - message: { - thread_ts: "1234567890.000001", - }, - }; - mocks.core.getInput.withArgs("method").returns("chat.postMessage"); - mocks.core.getInput.withArgs("token").returns("xoxb-example"); - mocks.core.getInput.withArgs("payload").returns(JSON.stringify(args)); - mocks.calls.resolves(response); - await send(mocks.core); - assert.deepEqual(mocks.calls.getCall(0).firstArg, "chat.postMessage"); - assert.deepEqual(mocks.calls.getCall(0).lastArg, args); - assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); - assert.equal(mocks.core.setOutput.getCall(0).lastArg, true); - assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); - assert.equal( - mocks.core.setOutput.getCall(1).lastArg, - JSON.stringify(response), - ); - assert.equal(mocks.core.setOutput.getCall(2).firstArg, "channel_id"); - assert.equal(mocks.core.setOutput.getCall(2).lastArg, "C0123456789"); - assert.equal(mocks.core.setOutput.getCall(3).firstArg, "thread_ts"); - assert.equal( - mocks.core.setOutput.getCall(3).lastArg, - "1234567890.000001", - ); - assert.equal(mocks.core.setOutput.getCall(4).firstArg, "ts"); - assert.equal( - mocks.core.setOutput.getCall(4).lastArg, - "1234567890.000002", - ); - assert.equal(mocks.core.setOutput.getCall(5).firstArg, "time"); - assert.equal(mocks.core.setOutput.getCalls().length, 6); - } catch (err) { - console.error(err); - assert.fail("Unexpected error when calling the method"); - } + }, + }; + const payload = JSON.stringify(args); + mocks.inputs = { + ...mocks.inputs, + method: "chat.postMessage", + token: "xoxb-example", + payload, + }; + mocks.calls._resolvesWith = response; + await send(mocks.core); + assert.deepEqual( + mocks.calls.mock.calls[0].arguments[0], + "chat.postMessage", + ); + assert.deepEqual(mocks.calls.mock.calls[0].arguments[1], args); + assert.equal(mocks.core.setOutput.mock.calls[0].arguments[0], "ok"); + assert.equal(mocks.core.setOutput.mock.calls[0].arguments[1], true); + assert.equal( + mocks.core.setOutput.mock.calls[1].arguments[0], + "response", + ); + assert.equal( + mocks.core.setOutput.mock.calls[1].arguments[1], + JSON.stringify(response), + ); + assert.equal( + mocks.core.setOutput.mock.calls[2].arguments[0], + "channel_id", + ); + assert.equal( + mocks.core.setOutput.mock.calls[2].arguments[1], + "C0123456789", + ); + assert.equal( + mocks.core.setOutput.mock.calls[3].arguments[0], + "thread_ts", + ); + assert.equal( + mocks.core.setOutput.mock.calls[3].arguments[1], + "1234567890.000001", + ); + assert.equal(mocks.core.setOutput.mock.calls[4].arguments[0], "ts"); + assert.equal( + mocks.core.setOutput.mock.calls[4].arguments[1], + "1234567890.000002", + ); + assert.equal(mocks.core.setOutput.mock.calls[5].arguments[0], "time"); + assert.equal(mocks.core.setOutput.mock.calls.length, 6); }); it("calls 'conversations.create' with the given token and content", async () => { - try { - const args = { + const args = { + name: "pull-request-review-010101", + }; + const response = { + ok: true, + channel: { + id: "C0101010101", name: "pull-request-review-010101", - }; - const response = { - ok: true, - channel: { - id: "C0101010101", - name: "pull-request-review-010101", - is_channel: true, - created: 1730425428, - }, - }; - mocks.core.getInput.withArgs("method").returns("chat.postMessage"); - mocks.core.getInput.withArgs("token").returns("xoxb-example"); - mocks.core.getInput.withArgs("payload").returns(JSON.stringify(args)); - mocks.calls.resolves(response); - await send(mocks.core); - assert.deepEqual(mocks.calls.getCall(0).firstArg, "chat.postMessage"); - assert.deepEqual(mocks.calls.getCall(0).lastArg, args); - assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); - assert.equal(mocks.core.setOutput.getCall(0).lastArg, true); - assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); - assert.equal( - mocks.core.setOutput.getCall(1).lastArg, - JSON.stringify(response), - ); - assert.equal(mocks.core.setOutput.getCall(2).firstArg, "channel_id"); - assert.equal(mocks.core.setOutput.getCall(2).lastArg, "C0101010101"); - assert.equal(mocks.core.setOutput.getCall(3).firstArg, "time"); - assert.equal(mocks.core.setOutput.getCalls().length, 4); - } catch (err) { - console.error(err); - assert.fail("Unexpected error when calling the method"); - } + is_channel: true, + created: 1730425428, + }, + }; + const payload = JSON.stringify(args); + mocks.inputs = { + ...mocks.inputs, + method: "chat.postMessage", + token: "xoxb-example", + payload, + }; + mocks.calls._resolvesWith = response; + await send(mocks.core); + assert.deepEqual( + mocks.calls.mock.calls[0].arguments[0], + "chat.postMessage", + ); + assert.deepEqual(mocks.calls.mock.calls[0].arguments[1], args); + assert.equal(mocks.core.setOutput.mock.calls[0].arguments[0], "ok"); + assert.equal(mocks.core.setOutput.mock.calls[0].arguments[1], true); + assert.equal( + mocks.core.setOutput.mock.calls[1].arguments[0], + "response", + ); + assert.equal( + mocks.core.setOutput.mock.calls[1].arguments[1], + JSON.stringify(response), + ); + assert.equal( + mocks.core.setOutput.mock.calls[2].arguments[0], + "channel_id", + ); + assert.equal( + mocks.core.setOutput.mock.calls[2].arguments[1], + "C0101010101", + ); + assert.equal(mocks.core.setOutput.mock.calls[3].arguments[0], "time"); + assert.equal(mocks.core.setOutput.mock.calls.length, 4); }); it("calls 'files.uploadV2' with the provided token and content", async () => { - try { - const args = { - channel: "C0000000001", - initial_comment: "the results are in!", - file: "results.out", - filename: "results-888888.out", - }; - const response = { - ok: true, - files: [{ id: "F0000000001", created: 1234567890 }], - }; - mocks.core.getInput.withArgs("method").returns("files.uploadV2"); - mocks.core.getInput.withArgs("token").returns("xoxp-example"); - mocks.core.getInput.withArgs("payload").returns(JSON.stringify(args)); - mocks.calls.resolves(response); - await send(mocks.core); - assert.deepEqual(mocks.calls.getCall(0).lastArg, args); - assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); - assert.equal(mocks.core.setOutput.getCall(0).lastArg, true); - assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); - assert.equal( - mocks.core.setOutput.getCall(1).lastArg, - JSON.stringify(response), - ); - assert.equal(mocks.core.setOutput.getCall(2).firstArg, "time"); - assert.equal(mocks.core.setOutput.getCalls().length, 3); - } catch (err) { - console.error(err); - assert.fail("Unexpected error when calling the method"); - } + const args = { + channel: "C0000000001", + initial_comment: "the results are in!", + file: "results.out", + filename: "results-888888.out", + }; + const response = { + ok: true, + files: [{ id: "F0000000001", created: 1234567890 }], + }; + const payload = JSON.stringify(args); + mocks.inputs = { + ...mocks.inputs, + method: "files.uploadV2", + token: "xoxp-example", + payload, + }; + mocks.calls._resolvesWith = response; + await send(mocks.core); + assert.deepEqual(mocks.calls.mock.calls[0].arguments[1], args); + assert.equal(mocks.core.setOutput.mock.calls[0].arguments[0], "ok"); + assert.equal(mocks.core.setOutput.mock.calls[0].arguments[1], true); + assert.equal( + mocks.core.setOutput.mock.calls[1].arguments[0], + "response", + ); + assert.equal( + mocks.core.setOutput.mock.calls[1].arguments[1], + JSON.stringify(response), + ); + assert.equal(mocks.core.setOutput.mock.calls[2].arguments[0], "time"); + assert.equal(mocks.core.setOutput.mock.calls.length, 3); }); }); @@ -296,26 +343,34 @@ describe("client", () => { message: "Something bad happened!", }, }; + mocks.booleanInputs = { + ...mocks.booleanInputs, + errors: true, + }; + mocks.inputs = { + ...mocks.inputs, + method: "chat.postMessage", + token: "xoxb-example", + payload: `"text": "hello"`, + }; + mocks.calls._rejectsWith = errors.requestErrorWithOriginal(response, true); try { - mocks.core.getInput.reset(); - mocks.core.getBooleanInput.withArgs("errors").returns(true); - mocks.core.getInput.withArgs("method").returns("chat.postMessage"); - mocks.core.getInput.withArgs("token").returns("xoxb-example"); - mocks.core.getInput.withArgs("payload").returns(`"text": "hello"`); - mocks.calls.rejects(errors.requestErrorWithOriginal(response, true)); await send(mocks.core); assert.fail("Expected an error but none was found"); } catch (_err) { - assert.ok(mocks.core.setFailed.called); - assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); - assert.equal(mocks.core.setOutput.getCall(0).lastArg, false); - assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); + assert.ok(mocks.core.setFailed.mock.callCount() > 0); + assert.equal(mocks.core.setOutput.mock.calls[0].arguments[0], "ok"); + assert.equal(mocks.core.setOutput.mock.calls[0].arguments[1], false); + assert.equal( + mocks.core.setOutput.mock.calls[1].arguments[0], + "response", + ); assert.deepEqual( - mocks.core.setOutput.getCall(1).lastArg, + mocks.core.setOutput.mock.calls[1].arguments[1], JSON.stringify(response), ); - assert.equal(mocks.core.setOutput.getCall(2).firstArg, "time"); - assert.equal(mocks.core.setOutput.getCalls().length, 3); + assert.equal(mocks.core.setOutput.mock.calls[2].arguments[0], "time"); + assert.equal(mocks.core.setOutput.mock.calls.length, 3); } }); @@ -333,26 +388,32 @@ describe("client", () => { error: "unknown_http_method", }, }; + mocks.inputs = { + ...mocks.inputs, + method: "chat.postMessage", + token: "xoxb-example", + payload: `"text": "hello"`, + }; + mocks.calls._rejectsWith = errors.httpErrorFromResponse(response); try { - mocks.core.getInput.withArgs("method").returns("chat.postMessage"); - mocks.core.getInput.withArgs("token").returns("xoxb-example"); - mocks.core.getInput.withArgs("payload").returns(`"text": "hello"`); - mocks.calls.rejects(errors.httpErrorFromResponse(response)); await send(mocks.core); assert.fail("Expected an error but none was found"); } catch (_err) { - assert.strictEqual(mocks.core.setFailed.called, false); - assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); - assert.equal(mocks.core.setOutput.getCall(0).lastArg, false); - assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); + assert.strictEqual(mocks.core.setFailed.mock.callCount(), 0); + assert.equal(mocks.core.setOutput.mock.calls[0].arguments[0], "ok"); + assert.equal(mocks.core.setOutput.mock.calls[0].arguments[1], false); + assert.equal( + mocks.core.setOutput.mock.calls[1].arguments[0], + "response", + ); response.body = response.data; response.data = undefined; assert.deepEqual( - mocks.core.setOutput.getCall(1).lastArg, + mocks.core.setOutput.mock.calls[1].arguments[1], JSON.stringify(response), ); - assert.equal(mocks.core.setOutput.getCall(2).firstArg, "time"); - assert.equal(mocks.core.setOutput.getCalls().length, 3); + assert.equal(mocks.core.setOutput.mock.calls[2].arguments[0], "time"); + assert.equal(mocks.core.setOutput.mock.calls.length, 3); } }); @@ -367,26 +428,34 @@ describe("client", () => { error: "missing_channel", }, }; + mocks.booleanInputs = { + ...mocks.booleanInputs, + errors: true, + }; + mocks.inputs = { + ...mocks.inputs, + method: "chat.postMessage", + token: "xoxb-example", + payload: `"text": "hello"`, + }; + mocks.calls._rejectsWith = errors.platformErrorFromResult(response); try { - mocks.core.getInput.reset(); - mocks.core.getBooleanInput.withArgs("errors").returns(true); - mocks.core.getInput.withArgs("method").returns("chat.postMessage"); - mocks.core.getInput.withArgs("token").returns("xoxb-example"); - mocks.core.getInput.withArgs("payload").returns(`"text": "hello"`); - mocks.calls.rejects(errors.platformErrorFromResult(response)); await send(mocks.core); assert.fail("Expected an error but none was found"); } catch (_err) { - assert.ok(mocks.core.setFailed.called); - assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); - assert.equal(mocks.core.setOutput.getCall(0).lastArg, false); - assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); + assert.ok(mocks.core.setFailed.mock.callCount() > 0); + assert.equal(mocks.core.setOutput.mock.calls[0].arguments[0], "ok"); + assert.equal(mocks.core.setOutput.mock.calls[0].arguments[1], false); + assert.equal( + mocks.core.setOutput.mock.calls[1].arguments[0], + "response", + ); assert.deepEqual( - mocks.core.setOutput.getCall(1).lastArg, + mocks.core.setOutput.mock.calls[1].arguments[1], JSON.stringify(response), ); - assert.equal(mocks.core.setOutput.getCall(2).firstArg, "time"); - assert.equal(mocks.core.setOutput.getCalls().length, 3); + assert.equal(mocks.core.setOutput.mock.calls[2].arguments[0], "time"); + assert.equal(mocks.core.setOutput.mock.calls.length, 3); } }); @@ -398,24 +467,30 @@ describe("client", () => { error: "missing_channel", }, }; + mocks.inputs = { + ...mocks.inputs, + method: "chat.postMessage", + token: "xoxb-example", + payload: `"text": "hello"`, + }; + mocks.calls._rejectsWith = errors.platformErrorFromResult(response); try { - mocks.core.getInput.withArgs("method").returns("chat.postMessage"); - mocks.core.getInput.withArgs("token").returns("xoxb-example"); - mocks.core.getInput.withArgs("payload").returns(`"text": "hello"`); - mocks.calls.rejects(errors.platformErrorFromResult(response)); await send(mocks.core); assert.fail("Expected an error but none was found"); } catch (_err) { - assert.strictEqual(mocks.core.setFailed.called, false); - assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); - assert.equal(mocks.core.setOutput.getCall(0).lastArg, false); - assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); + assert.strictEqual(mocks.core.setFailed.mock.callCount(), 0); + assert.equal(mocks.core.setOutput.mock.calls[0].arguments[0], "ok"); + assert.equal(mocks.core.setOutput.mock.calls[0].arguments[1], false); + assert.equal( + mocks.core.setOutput.mock.calls[1].arguments[0], + "response", + ); assert.deepEqual( - mocks.core.setOutput.getCall(1).lastArg, + mocks.core.setOutput.mock.calls[1].arguments[1], JSON.stringify(response), ); - assert.equal(mocks.core.setOutput.getCall(2).firstArg, "time"); - assert.equal(mocks.core.setOutput.getCalls().length, 3); + assert.equal(mocks.core.setOutput.mock.calls[2].arguments[0], "time"); + assert.equal(mocks.core.setOutput.mock.calls.length, 3); } }); @@ -424,24 +499,30 @@ describe("client", () => { code: "slack_webapi_rate_limited_error", retryAfter: 12, }; + mocks.inputs = { + ...mocks.inputs, + method: "chat.postMessage", + token: "xoxb-example", + payload: `"text": "hello"`, + }; + mocks.calls._rejectsWith = errors.rateLimitedErrorWithDelay(12); try { - mocks.core.getInput.withArgs("method").returns("chat.postMessage"); - mocks.core.getInput.withArgs("token").returns("xoxb-example"); - mocks.core.getInput.withArgs("payload").returns(`"text": "hello"`); - mocks.calls.rejects(errors.rateLimitedErrorWithDelay(12)); await send(mocks.core); assert.fail("Expected an error but none was found"); } catch (_err) { - assert.strictEqual(mocks.core.setFailed.called, false); - assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); - assert.equal(mocks.core.setOutput.getCall(0).lastArg, false); - assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); + assert.strictEqual(mocks.core.setFailed.mock.callCount(), 0); + assert.equal(mocks.core.setOutput.mock.calls[0].arguments[0], "ok"); + assert.equal(mocks.core.setOutput.mock.calls[0].arguments[1], false); + assert.equal( + mocks.core.setOutput.mock.calls[1].arguments[0], + "response", + ); assert.deepEqual( - mocks.core.setOutput.getCall(1).lastArg, + mocks.core.setOutput.mock.calls[1].arguments[1], JSON.stringify(response), ); - assert.equal(mocks.core.setOutput.getCall(2).firstArg, "time"); - assert.equal(mocks.core.setOutput.getCalls().length, 3); + assert.equal(mocks.core.setOutput.mock.calls[2].arguments[0], "time"); + assert.equal(mocks.core.setOutput.mock.calls.length, 3); } }); }); @@ -449,9 +530,12 @@ describe("client", () => { describe("proxies", () => { it("sets up the proxy agent for the provided https proxy", async () => { const proxy = "https://example.com"; - mocks.core.getInput.withArgs("method").returns("chat.postMessage"); - mocks.core.getInput.withArgs("proxy").returns(proxy); - mocks.core.getInput.withArgs("token").returns("xoxb-example"); + mocks.inputs = { + ...mocks.inputs, + method: "chat.postMessage", + proxy, + token: "xoxb-example", + }; const config = new Config(mocks.core); const client = new Client(); const { httpsAgent, proxy: proxying } = client.proxies(config); @@ -461,9 +545,12 @@ describe("client", () => { it("fails to configure proxies with an invalid proxied url", async () => { const proxy = "https://"; - mocks.core.getInput.withArgs("method").returns("chat.postMessage"); - mocks.core.getInput.withArgs("proxy").returns(proxy); - mocks.core.getInput.withArgs("token").returns("xoxb-example"); + mocks.inputs = { + ...mocks.inputs, + method: "chat.postMessage", + proxy, + token: "xoxb-example", + }; try { const config = new Config(mocks.core); const client = new Client(); diff --git a/test/config.spec.js b/test/config.spec.js index db105c52..5382af00 100644 --- a/test/config.spec.js +++ b/test/config.spec.js @@ -20,13 +20,19 @@ describe("config", () => { describe("inputs", () => { it("valid values are collected from the action inputs", async () => { - mocks.core.getInput.withArgs("api").returns("http://localhost:8080"); - mocks.core.getBooleanInput.withArgs("errors").returns(true); - mocks.core.getInput.withArgs("method").returns("chat.postMessage"); - mocks.core.getInput.withArgs("payload").returns('"hello": "world"'); - mocks.core.getInput.withArgs("proxy").returns("https://example.com"); - mocks.core.getInput.withArgs("retries").returns("0"); - mocks.core.getInput.withArgs("token").returns("xoxb-example"); + mocks.inputs = { + ...mocks.inputs, + api: "http://localhost:8080", + method: "chat.postMessage", + payload: '"hello": "world"', + proxy: "https://example.com", + retries: "0", + token: "xoxb-example", + }; + mocks.booleanInputs = { + ...mocks.booleanInputs, + errors: true, + }; const config = new Config(mocks.core); assert.equal(config.inputs.api, "http://localhost:8080"); assert.equal(config.inputs.errors, true); @@ -35,36 +41,65 @@ describe("config", () => { assert.equal(config.inputs.proxy, "https://example.com"); assert.equal(config.inputs.retries, config.Retries.ZERO); assert.equal(config.inputs.token, "xoxb-example"); - assert.ok(mocks.core.setSecret.withArgs("xoxb-example").called); + assert.ok( + mocks.core.setSecret.mock.calls.some( + (c) => c.arguments[0] === "xoxb-example", + ), + ); }); it("allows token environment variables with a webhook", async () => { process.env.SLACK_TOKEN = "xoxb-example"; - mocks.core.getInput.withArgs("webhook").returns("https://example.com"); - mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); + mocks.inputs = { + ...mocks.inputs, + webhook: "https://example.com", + "webhook-type": "incoming-webhook", + }; const config = new Config(mocks.core); assert.equal(config.inputs.token, "xoxb-example"); assert.equal(config.inputs.webhook, "https://example.com"); assert.equal(config.inputs.webhookType, "incoming-webhook"); - assert.ok(mocks.core.setSecret.withArgs("xoxb-example").called); - assert.ok(mocks.core.setSecret.withArgs("https://example.com").called); + assert.ok( + mocks.core.setSecret.mock.calls.some( + (c) => c.arguments[0] === "xoxb-example", + ), + ); + assert.ok( + mocks.core.setSecret.mock.calls.some( + (c) => c.arguments[0] === "https://example.com", + ), + ); }); it("allows webhook environment variables with a token", async () => { process.env.SLACK_WEBHOOK_URL = "https://example.com"; - mocks.core.getInput.withArgs("method").returns("chat.postMessage"); - mocks.core.getInput.withArgs("token").returns("xoxb-example"); + mocks.inputs = { + ...mocks.inputs, + method: "chat.postMessage", + token: "xoxb-example", + }; const config = new Config(mocks.core); assert.equal(config.inputs.method, "chat.postMessage"); assert.equal(config.inputs.token, "xoxb-example"); assert.equal(config.inputs.webhook, "https://example.com"); - assert.ok(mocks.core.setSecret.withArgs("xoxb-example").called); - assert.ok(mocks.core.setSecret.withArgs("https://example.com").called); + assert.ok( + mocks.core.setSecret.mock.calls.some( + (c) => c.arguments[0] === "xoxb-example", + ), + ); + assert.ok( + mocks.core.setSecret.mock.calls.some( + (c) => c.arguments[0] === "https://example.com", + ), + ); }); it("errors when both the token and webhook is provided", async () => { - mocks.core.getInput.withArgs("token").returns("xoxb-example"); - mocks.core.getInput.withArgs("webhook").returns("https://example.com"); + mocks.inputs = { + ...mocks.inputs, + token: "xoxb-example", + webhook: "https://example.com", + }; try { new Config(mocks.core); assert.fail("Failed to error when invalid inputs are provided"); @@ -75,9 +110,15 @@ describe("config", () => { "Invalid input! Either the token or webhook is required - not both.", ), ); - assert.ok(mocks.core.setSecret.withArgs("xoxb-example").called); assert.ok( - mocks.core.setSecret.withArgs("https://example.com").called, + mocks.core.setSecret.mock.calls.some( + (c) => c.arguments[0] === "xoxb-example", + ), + ); + assert.ok( + mocks.core.setSecret.mock.calls.some( + (c) => c.arguments[0] === "https://example.com", + ), ); } else { assert.fail(err); @@ -86,7 +127,10 @@ describe("config", () => { }); it("errors if the method is provided without a token", async () => { - mocks.core.getInput.withArgs("method").returns("chat.postMessage"); + mocks.inputs = { + ...mocks.inputs, + method: "chat.postMessage", + }; try { new Config(mocks.core); assert.fail("Failed to error when invalid inputs are provided"); @@ -121,7 +165,10 @@ describe("config", () => { }); it("errors if a webhook is provided without the type", async () => { - mocks.core.getInput.withArgs("webhook").returns("https://example.com"); + mocks.inputs = { + ...mocks.inputs, + webhook: "https://example.com", + }; try { new Config(mocks.core); assert.fail("Failed to error when invalid inputs are provided"); @@ -139,8 +186,11 @@ describe("config", () => { }); it("errors if the webhook type does not match techniques", async () => { - mocks.core.getInput.withArgs("webhook").returns("https://example.com"); - mocks.core.getInput.withArgs("webhook-type").returns("post"); + mocks.inputs = { + ...mocks.inputs, + webhook: "https://example.com", + "webhook-type": "post", + }; try { new Config(mocks.core); assert.fail("Failed to error when invalid inputs are provided"); @@ -160,35 +210,50 @@ describe("config", () => { describe("mask", async () => { it("treats the provided token as a secret", async () => { - mocks.core.getInput.withArgs("token").returns("xoxb-example"); + mocks.inputs = { + ...mocks.inputs, + token: "xoxb-example", + }; try { await send(mocks.core); assert.fail("Failed to error for incomplete inputs while testing"); } catch { - assert.ok(mocks.core.setSecret.withArgs("xoxb-example").called); + assert.ok( + mocks.core.setSecret.mock.calls.some( + (c) => c.arguments[0] === "xoxb-example", + ), + ); } }); it("treats the provided webhook as a secret", async () => { - mocks.core.getInput.withArgs("webhook").returns("https://slack.com"); - mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); + mocks.inputs = { + ...mocks.inputs, + webhook: "https://slack.com", + "webhook-type": "incoming-webhook", + }; try { await send(mocks.core); assert.fail("Failed to error for incomplete inputs while testing"); } catch { - assert.ok(mocks.core.setSecret.withArgs("https://slack.com").called); + assert.ok( + mocks.core.setSecret.mock.calls.some( + (c) => c.arguments[0] === "https://slack.com", + ), + ); } }); }); describe("validate", () => { it('allow the "retries" option with lowercased space', async () => { - mocks.axios.post.returns(Promise.resolve("LGTM")); - mocks.core.getInput.withArgs("retries").returns(" rapid "); - mocks.core.getInput - .withArgs("webhook") - .returns("https://hooks.slack.com"); - mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); + mocks.axios.post._promise = Promise.resolve("LGTM"); + mocks.inputs = { + ...mocks.inputs, + retries: " rapid ", + webhook: "https://hooks.slack.com", + "webhook-type": "incoming-webhook", + }; try { await send(mocks.core); } catch (err) { @@ -205,12 +270,13 @@ describe("config", () => { }); it("errors if an invalid retries option is provided", async () => { - mocks.axios.post.returns(Promise.resolve("LGTM")); - mocks.core.getInput.withArgs("retries").returns("FOREVER"); - mocks.core.getInput - .withArgs("webhook") - .returns("https://hooks.slack.com"); - mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); + mocks.axios.post._promise = Promise.resolve("LGTM"); + mocks.inputs = { + ...mocks.inputs, + retries: "FOREVER", + webhook: "https://hooks.slack.com", + "webhook-type": "incoming-webhook", + }; try { await send(mocks.core); } catch (err) { diff --git a/test/content.spec.js b/test/content.spec.js index 5288d61b..13ba512a 100644 --- a/test/content.spec.js +++ b/test/content.spec.js @@ -1,6 +1,7 @@ import assert from "node:assert"; -import path from "node:path"; +import { dirname, join } from "node:path"; import { beforeEach, describe, it } from "node:test"; +import { fileURLToPath } from "node:url"; import { YAMLException } from "js-yaml"; import Config from "../src/config.js"; import Content from "../src/content.js"; @@ -8,25 +9,36 @@ import SlackError from "../src/errors.js"; import send from "../src/send.js"; import { mocks } from "./index.spec.js"; +const __dirname = dirname(fileURLToPath(import.meta.url)); +const FIXTURES = join(__dirname, "fixtures"); + /** * Confirm values from the action input or environment variables are gathered */ describe("content", () => { beforeEach(() => { mocks.reset(); - mocks.core.getInput.withArgs("method").returns("chat.postMessage"); - mocks.core.getInput.withArgs("token").returns("xoxb-example"); + mocks.inputs = { + ...mocks.inputs, + method: "chat.postMessage", + token: "xoxb-example", + }; + // Set up default apiCall mock for tests that use send() + mocks.calls._resolvesWith = { ok: true }; }); describe("flatten", () => { it("flattens nested payloads provided with delimiter", async () => { - mocks.core.getInput.withArgs("payload").returns(` + mocks.inputs = { + ...mocks.inputs, + payload: ` "apples": "tree", "bananas": { "truthiness": true } - `); - mocks.core.getInput.withArgs("payload-delimiter").returns("_"); + `, + "payload-delimiter": "_", + }; const config = new Config(mocks.core); const expected = { apples: "tree", @@ -38,8 +50,11 @@ describe("content", () => { describe("get", () => { it("errors if both a payload and file path are provided", async () => { - mocks.core.getInput.withArgs("payload").returns(`"message"="hello"`); - mocks.core.getInput.withArgs("payload-file-path").returns("example.json"); + mocks.inputs = { + ...mocks.inputs, + payload: `"message"="hello"`, + "payload-file-path": "example.json", + }; try { await send(mocks.core); assert.fail("Failed to throw for invalid input"); @@ -59,10 +74,13 @@ describe("content", () => { describe("payload", async () => { it("parses complete YAML from the input payload", async () => { - mocks.core.getInput.withArgs("payload").returns(` + mocks.inputs = { + ...mocks.inputs, + payload: ` message: "this is wrapped" channel: "C0123456789" - `); + `, + }; const config = new Config(mocks.core); const expected = { message: "this is wrapped", @@ -72,11 +90,14 @@ describe("content", () => { }); it("parses complete JSON from the input payload", async () => { - mocks.core.getInput.withArgs("payload").returns(`{ + mocks.inputs = { + ...mocks.inputs, + payload: `{ "message": "this is wrapped", "channel": "C0123456789" } - `); + `, + }; const config = new Config(mocks.core); const expected = { message: "this is wrapped", @@ -86,11 +107,14 @@ describe("content", () => { }); it("templatizes variables requires configuration", async () => { - mocks.core.getInput.withArgs("payload").returns(`{ + mocks.inputs = { + ...mocks.inputs, + payload: `{ "message": "this matches an existing variable: \${{ github.apiUrl }}", "channel": "C0123456789" } - `); + `, + }; const config = new Config(mocks.core); // biome-ignore-start lint/suspicious/noTemplateCurlyInString: GitHub Action YAML variable syntax const expected = { @@ -102,7 +126,9 @@ describe("content", () => { }); it("templatizes variables with matching variables", async () => { - mocks.core.getInput.withArgs("payload").returns(` + mocks.inputs = { + ...mocks.inputs, + payload: ` channel: C0123456789 reply_broadcast: false message: Served \${{ env.NUMBER }} items @@ -136,8 +162,12 @@ describe("content", () => { type: plain_text text: "\${{ github.graphqlUrl }}" value: graphql - `); - mocks.core.getBooleanInput.withArgs("payload-templated").returns(true); + `, + }; + mocks.booleanInputs = { + ...mocks.booleanInputs, + "payload-templated": true, + }; process.env.DETAILS = ` -fri -sat @@ -210,11 +240,15 @@ describe("content", () => { */ it("templatizes variables with missing variables", async () => { // biome-ignore-start lint/suspicious/noTemplateCurlyInString: GitHub Action YAML variable syntax - mocks.core.getInput - .withArgs("payload") - .returns("message: What makes ${{ env.TREASURE }} a secret"); + mocks.inputs = { + ...mocks.inputs, + payload: "message: What makes ${{ env.TREASURE }} a secret", + }; // biome-ignore-end lint/suspicious/noTemplateCurlyInString: https://docs.github.com/en/actions/how-tos/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#using-contexts-to-access-variable-values - mocks.core.getBooleanInput.withArgs("payload-templated").returns(true); + mocks.booleanInputs = { + ...mocks.booleanInputs, + "payload-templated": true, + }; const config = new Config(mocks.core); const expected = { message: "What makes ??? a secret", @@ -223,10 +257,13 @@ describe("content", () => { }); it("trims last comma JSON with the input payload", async () => { - mocks.core.getInput.withArgs("payload").returns(` + mocks.inputs = { + ...mocks.inputs, + payload: ` "message": "LGTM!", "channel": "C0123456789", - `); + `, + }; const config = new Config(mocks.core); const expected = { message: "LGTM!", @@ -236,7 +273,9 @@ describe("content", () => { }); it("wraps incomplete JSON from the input payload", async () => { - mocks.core.getInput.withArgs("payload").returns(` + mocks.inputs = { + ...mocks.inputs, + payload: ` "message": "LGTM!", "channel": "C0123456789", "blocks": [ @@ -247,7 +286,8 @@ describe("content", () => { } } ] - `); + `, + }; const config = new Config(mocks.core); const expected = { message: "LGTM!", @@ -291,7 +331,10 @@ describe("content", () => { }); it("fails if invalid JSON exists in the input payload", async () => { - mocks.core.getInput.withArgs("payload").returns("{"); + mocks.inputs = { + ...mocks.inputs, + payload: "{", + }; try { await send(mocks.core); assert.fail("Failed to throw for invalid JSON"); @@ -316,13 +359,10 @@ describe("content", () => { describe("payload file", async () => { it("parses complete YAML from the input payload file", async () => { - mocks.core.getInput.withArgs("payload-file-path").returns("example.yaml"); - mocks.fs.readFileSync - .withArgs(path.resolve("example.yaml"), "utf-8") - .returns(` - message: "drink water" - channel: "C6H12O6H2O2" - `); + mocks.inputs = { + ...mocks.inputs, + "payload-file-path": join(FIXTURES, "payload.yaml"), + }; const config = new Config(mocks.core); const expected = { message: "drink water", @@ -332,13 +372,10 @@ describe("content", () => { }); it("parses complete YML from the input payload file", async () => { - mocks.core.getInput.withArgs("payload-file-path").returns("example.yml"); - mocks.fs.readFileSync - .withArgs(path.resolve("example.yml"), "utf-8") - .returns(` - message: "drink coffee" - channel: "C0FFEEEEEEEE" - `); + mocks.inputs = { + ...mocks.inputs, + "payload-file-path": join(FIXTURES, "payload.yml"), + }; const config = new Config(mocks.core); const expected = { message: "drink coffee", @@ -348,13 +385,10 @@ describe("content", () => { }); it("parses complete JSON from the input payload file", async () => { - mocks.core.getInput.withArgs("payload-file-path").returns("example.json"); - mocks.fs.readFileSync - .withArgs(path.resolve("example.json"), "utf-8") - .returns(`{ - "message": "drink water", - "channel": "C6H12O6H2O2" - }`); + mocks.inputs = { + ...mocks.inputs, + "payload-file-path": join(FIXTURES, "payload.json"), + }; const config = new Config(mocks.core); const expected = { message: "drink water", @@ -364,14 +398,10 @@ describe("content", () => { }); it("templatizes variables requires configuration", async () => { - mocks.core.getInput.withArgs("payload-file-path").returns("example.json"); - mocks.fs.readFileSync - .withArgs(path.resolve("example.json"), "utf-8") - .returns(`{ - "message": "this matches an existing variable: \${{ github.apiUrl }}", - "channel": "C0123456789" - } - `); + mocks.inputs = { + ...mocks.inputs, + "payload-file-path": join(FIXTURES, "payload-with-github-vars.json"), + }; const config = new Config(mocks.core); // biome-ignore-start lint/suspicious/noTemplateCurlyInString: GitHub Action YAML variable syntax const expected = { @@ -383,66 +413,14 @@ describe("content", () => { }); it("templatizes variables with matching variables", async () => { - mocks.core.getInput.withArgs("payload-file-path").returns("example.json"); - mocks.fs.readFileSync - .withArgs(path.resolve("example.json"), "utf-8") - .returns(`{ - "channel": "C0123456789", - "reply_broadcast": false, - "message": "Served \${{ env.NUMBER }} items", - "blocks": [ - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": "Served \${{ env.NUMBER }} items on: \${{ env.DETAILS }}" - } - }, - { - "type": "divider" - }, - { - "type": "section", - "block_id": "selector", - "text": { - "type": "mrkdwn", - "text": "Send feedback" - }, - "accessory": { - "action_id": "response", - "type": "multi_static_select", - "placeholder": { - "type": "plain_text", - "text": "Select URL" - }, - "options": [ - { - "text": { - "type": "plain_text", - "text": "\${{ github.apiUrl }}" - }, - "value": "api" - }, - { - "text": { - "type": "plain_text", - "text": "\${{ github.serverUrl }}" - }, - "value": "server" - }, - { - "text": { - "type": "plain_text", - "text": "\${{ github.graphqlUrl }}" - }, - "value": "graphql" - } - ] - } - } - ] - }`); - mocks.core.getBooleanInput.withArgs("payload-templated").returns(true); + mocks.inputs = { + ...mocks.inputs, + "payload-file-path": join(FIXTURES, "payload-with-env-vars.json"), + }; + mocks.booleanInputs = { + ...mocks.booleanInputs, + "payload-templated": true, + }; process.env.DETAILS = ` -fri -sat @@ -514,13 +492,14 @@ describe("content", () => { * @see {@link https://github.com/slackapi/slack-github-action/issues/203} */ it("templatizes variables with missing variables", async () => { - mocks.core.getInput.withArgs("payload-file-path").returns("example.json"); - mocks.fs.readFileSync - .withArgs(path.resolve("example.json"), "utf-8") - .returns(`{ - "message": "What makes $\{{ env.TREASURE }} a secret" - }`); - mocks.core.getBooleanInput.withArgs("payload-templated").returns(true); + mocks.inputs = { + ...mocks.inputs, + "payload-file-path": join(FIXTURES, "payload-with-missing-var.json"), + }; + mocks.booleanInputs = { + ...mocks.booleanInputs, + "payload-templated": true, + }; const config = new Config(mocks.core); const expected = { message: "What makes ??? a secret", @@ -553,7 +532,10 @@ describe("content", () => { }); it("fails to parse a file path that does not exist", async () => { - mocks.core.getInput.withArgs("payload-file-path").returns("unknown.json"); + mocks.inputs = { + ...mocks.inputs, + "payload-file-path": join(FIXTURES, "nonexistent.json"), + }; try { await send(mocks.core); assert.fail("Failed to throw for nonexistent files"); @@ -571,7 +553,10 @@ describe("content", () => { }); it("fails to parse a file with an unknown extension", async () => { - mocks.core.getInput.withArgs("payload-file-path").returns("unknown.md"); + mocks.inputs = { + ...mocks.inputs, + "payload-file-path": join(FIXTURES, "payload.yaml.md"), + }; try { await send(mocks.core); assert.fail("Failed to throw for an unknown extension"); @@ -586,7 +571,7 @@ describe("content", () => { assert.equal(err.cause.values.length, 1); assert.ok( err.cause.values[0].message.includes( - "Invalid input! Failed to parse file extension unknown.md", + "Invalid input! Failed to parse file extension", ), ); } else { @@ -596,11 +581,10 @@ describe("content", () => { }); it("fails if invalid JSON exists in the input payload", async () => { - mocks.core.getInput.withArgs("payload-file-path").returns("example.json"); - mocks.fs.readFileSync - .withArgs(path.resolve("example.json"), "utf-8") - .returns(`{ - "message": "a truncated file without an end`); + mocks.inputs = { + ...mocks.inputs, + "payload-file-path": join(FIXTURES, "payload-invalid.json"), + }; try { await send(mocks.core); assert.fail("Failed to throw for invalid JSON"); @@ -621,10 +605,10 @@ describe("content", () => { }); it("fails if invalid YAML exists in the input payload", async () => { - mocks.core.getInput.withArgs("payload-file-path").returns("example.yaml"); - mocks.fs.readFileSync - .withArgs(path.resolve("example.yaml"), "utf-8") - .returns(`- "message": "assigned": "values"`); + mocks.inputs = { + ...mocks.inputs, + "payload-file-path": join(FIXTURES, "payload-invalid.yaml"), + }; try { await send(mocks.core); assert.fail("Failed to throw for invalid YAML"); diff --git a/test/fixtures/payload-invalid.json b/test/fixtures/payload-invalid.json new file mode 100644 index 00000000..59181a69 --- /dev/null +++ b/test/fixtures/payload-invalid.json @@ -0,0 +1,2 @@ +{ + "message": "a truncated file without an end \ No newline at end of file diff --git a/test/fixtures/payload-invalid.yaml b/test/fixtures/payload-invalid.yaml new file mode 100644 index 00000000..9faeb299 --- /dev/null +++ b/test/fixtures/payload-invalid.yaml @@ -0,0 +1 @@ +- "message": "assigned": "values" \ No newline at end of file diff --git a/test/fixtures/payload-with-env-vars.json b/test/fixtures/payload-with-env-vars.json new file mode 100644 index 00000000..1d83090d --- /dev/null +++ b/test/fixtures/payload-with-env-vars.json @@ -0,0 +1,56 @@ +{ + "channel": "C0123456789", + "reply_broadcast": false, + "message": "Served ${{ env.NUMBER }} items", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Served ${{ env.NUMBER }} items on: ${{ env.DETAILS }}" + } + }, + { + "type": "divider" + }, + { + "type": "section", + "block_id": "selector", + "text": { + "type": "mrkdwn", + "text": "Send feedback" + }, + "accessory": { + "action_id": "response", + "type": "multi_static_select", + "placeholder": { + "type": "plain_text", + "text": "Select URL" + }, + "options": [ + { + "text": { + "type": "plain_text", + "text": "${{ github.apiUrl }}" + }, + "value": "api" + }, + { + "text": { + "type": "plain_text", + "text": "${{ github.serverUrl }}" + }, + "value": "server" + }, + { + "text": { + "type": "plain_text", + "text": "${{ github.graphqlUrl }}" + }, + "value": "graphql" + } + ] + } + } + ] +} diff --git a/test/fixtures/payload-with-github-vars.json b/test/fixtures/payload-with-github-vars.json new file mode 100644 index 00000000..c75eaf1a --- /dev/null +++ b/test/fixtures/payload-with-github-vars.json @@ -0,0 +1,4 @@ +{ + "message": "this matches an existing variable: ${{ github.apiUrl }}", + "channel": "C0123456789" +} diff --git a/test/fixtures/payload-with-missing-var.json b/test/fixtures/payload-with-missing-var.json new file mode 100644 index 00000000..ee59f884 --- /dev/null +++ b/test/fixtures/payload-with-missing-var.json @@ -0,0 +1,3 @@ +{ + "message": "What makes ${{ env.TREASURE }} a secret" +} diff --git a/test/fixtures/payload.json b/test/fixtures/payload.json new file mode 100644 index 00000000..dfbe48ce --- /dev/null +++ b/test/fixtures/payload.json @@ -0,0 +1,4 @@ +{ + "message": "drink water", + "channel": "C6H12O6H2O2" +} diff --git a/test/fixtures/payload.yaml b/test/fixtures/payload.yaml new file mode 100644 index 00000000..fa343a0d --- /dev/null +++ b/test/fixtures/payload.yaml @@ -0,0 +1,2 @@ +message: "drink water" +channel: "C6H12O6H2O2" diff --git a/test/fixtures/payload.yaml.md b/test/fixtures/payload.yaml.md new file mode 100644 index 00000000..9dc98d0e --- /dev/null +++ b/test/fixtures/payload.yaml.md @@ -0,0 +1,2 @@ +# This file has an unknown extension +message: "test" diff --git a/test/fixtures/payload.yml b/test/fixtures/payload.yml new file mode 100644 index 00000000..17299e2a --- /dev/null +++ b/test/fixtures/payload.yml @@ -0,0 +1,2 @@ +message: "drink coffee" +channel: "C0FFEEEEEEEE" diff --git a/test/index.spec.js b/test/index.spec.js index 728102be..9fa55e65 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -1,7 +1,6 @@ -import fs from "node:fs"; +import { mock } from "node:test"; import webapi from "@slack/web-api"; import axios, { AxiosError } from "axios"; -import sinon from "sinon"; /** * Hello experimenter! These tests are here to confirm that the happy paths keep @@ -35,65 +34,84 @@ export class Mock { }; /** - * Setup stubbed dependencies and configure default input arguments for all + * Lookup tables for input values. + */ + inputs = {}; + booleanInputs = {}; + + /** + * Setup mocked dependencies and configure default input arguments for all * tests. * * @see {@link ../action.yml} */ constructor() { - this.sandbox = sinon.createSandbox(); - this.axios = this.sandbox.stub(axios); - this.calls = this.sandbox.stub(webapi.WebClient.prototype, "apiCall"); - this.core = { - debug: this.sandbox.stub(), - error: this.sandbox.stub(), - getInput: this.sandbox.stub(), - getBooleanInput: this.sandbox.stub(), - info: this.sandbox.stub(), - isDebug: this.sandbox.stub(), - setFailed: this.sandbox.stub(), - setOutput: this.sandbox.stub(), - setSecret: this.sandbox.stub(), - warning: this.sandbox.stub(), + this.axios = { + post: mock.fn((..._args) => { + if (this.axios.post._promise !== undefined) { + return this.axios.post._promise; + } + throw new Error( + "Test error: axios.post was called but no promise was configured. " + + "Set mocks.axios.post._promise = Promise.resolve(...) or Promise.reject(...)", + ); + }), }; - this.fs = this.sandbox.stub(fs); - this.webapi = { - WebClient: function () { - this.apiCall = () => ({ - ok: true, - }); - }, + axios.post = this.axios.post; + this.calls = mock.fn((..._args) => { + if (this.calls._resolvesWith !== undefined) { + return Promise.resolve(this.calls._resolvesWith); + } + if (this.calls._rejectsWith !== undefined) { + return Promise.reject(this.calls._rejectsWith); + } + throw new Error( + "Test error: apiCall was called but no promise was configured. " + + "Set mocks.calls._resolvesWith = {...} or mocks.calls._rejectsWith = {...}", + ); + }); + webapi.WebClient.prototype.apiCall = this.calls; + this.core = { + debug: mock.fn(), + error: mock.fn(), + getInput: mock.fn((key) => this.inputs[key] ?? ""), + getBooleanInput: mock.fn((key) => this.booleanInputs[key] ?? false), + info: mock.fn(), + isDebug: mock.fn(() => this._isDebug ?? false), + setFailed: mock.fn(), + setOutput: mock.fn(), + setSecret: mock.fn(), + warning: mock.fn(), }; - this.core.getInput.withArgs("errors").returns("false"); - this.core.getInput.withArgs("retries").returns("5"); } /** - * Testing interface that removes internal state from existing stubs. + * Testing interface that removes internal state from existing mocks. */ reset() { - this.sandbox.reset(); - this.axios.post.resetHistory(); - this.calls.resetHistory(); - this.core.debug.reset(); - this.core.error.reset(); - this.core.getInput.reset(); - this.core.getBooleanInput.reset(); - this.core.info.reset(); - this.core.isDebug.reset(); - this.core.setFailed.reset(); - this.core.setOutput.reset(); - this.core.setSecret.reset(); - this.core.warning.reset(); - this.webapi = { - WebClient: function () { - this.apiCall = () => ({ - ok: true, - }); - }, - }; - this.core.getInput.withArgs("errors").returns("false"); - this.core.getInput.withArgs("retries").returns("5"); + // Clear lookup tables + this.inputs = {}; + this.booleanInputs = {}; + + // Reset axios mock + this.axios.post.mock.resetCalls(); + this.axios.post._promise = undefined; + + // Reset apiCall mock + this.calls.mock.resetCalls(); + this.calls._resolvesWith = undefined; + this.calls._rejectsWith = undefined; + + // Reset core mocks + for (const fn of Object.values(this.core)) { + fn.mock?.resetCalls(); + } + this._isDebug = false; + + // Reset webapi + this.webapi = {}; + + // Clear environment variables process.env.SLACK_TOKEN = ""; process.env.SLACK_WEBHOOK_URL = ""; } diff --git a/test/logger.spec.js b/test/logger.spec.js index a246f4fb..469cb932 100644 --- a/test/logger.spec.js +++ b/test/logger.spec.js @@ -11,7 +11,7 @@ describe("logger", () => { describe("level", () => { it("debug", () => { - mocks.core.isDebug.returns(true); + mocks._isDebug = true; const { logger } = new Logger(mocks.core); const actual = logger.getLevel(); const expected = LogLevel.DEBUG; @@ -19,7 +19,7 @@ describe("logger", () => { }); it("info", () => { - mocks.core.isDebug.returns(false); + mocks._isDebug = false; const { logger } = new Logger(mocks.core); const actual = logger.getLevel(); const expected = LogLevel.INFO; diff --git a/test/send.spec.js b/test/send.spec.js index 9ab906ee..56d85de0 100644 --- a/test/send.spec.js +++ b/test/send.spec.js @@ -19,62 +19,68 @@ describe("send", () => { describe("techniques", async () => { it("webhook trigger", async () => { - mocks.core.getInput - .withArgs("webhook") - .returns("https://hooks.slack.com"); - mocks.core.getInput.withArgs("webhook-type").returns("webhook-trigger"); - mocks.core.getInput.withArgs("payload").returns('"greetings": "hello"'); - mocks.axios.post.returns( - Promise.resolve({ status: 200, data: { ok: true } }), - ); + mocks.inputs = { + ...mocks.inputs, + webhook: "https://hooks.slack.com", + "webhook-type": "webhook-trigger", + payload: '"greetings": "hello"', + }; + mocks.axios.post._promise = Promise.resolve({ + status: 200, + data: { ok: true }, + }); await send(mocks.core); - assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); - assert.equal(mocks.core.setOutput.getCall(0).lastArg, true); - assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); + assert.equal(mocks.core.setOutput.mock.calls[0].arguments[0], "ok"); + assert.equal(mocks.core.setOutput.mock.calls[0].arguments[1], true); + assert.equal(mocks.core.setOutput.mock.calls[1].arguments[0], "response"); assert.equal( - mocks.core.setOutput.getCall(1).lastArg, + mocks.core.setOutput.mock.calls[1].arguments[1], JSON.stringify({ ok: true }), ); - assert.equal(mocks.core.setOutput.getCall(2).firstArg, "time"); - assert.ok(mocks.core.setOutput.getCall(2).lastArg >= 0); + assert.equal(mocks.core.setOutput.mock.calls[2].arguments[0], "time"); + assert.ok(mocks.core.setOutput.mock.calls[2].arguments[1] >= 0); }); it("token", async () => { process.env.SLACK_WEBHOOK_URL = "https://example.com"; // https://github.com/slackapi/slack-github-action/issues/373 - mocks.calls.resolves({ ok: true }); - mocks.core.getInput.withArgs("method").returns("chat.postMessage"); - mocks.core.getInput.withArgs("token").returns("xoxb-example"); - mocks.core.getInput.withArgs("payload").returns('"text": "hello"'); + mocks.calls._resolvesWith = { ok: true }; + mocks.inputs = { + ...mocks.inputs, + method: "chat.postMessage", + token: "xoxb-example", + payload: '"text": "hello"', + }; await send(mocks.core); - assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); - assert.equal(mocks.core.setOutput.getCall(0).lastArg, true); - assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); + assert.equal(mocks.core.setOutput.mock.calls[0].arguments[0], "ok"); + assert.equal(mocks.core.setOutput.mock.calls[0].arguments[1], true); + assert.equal(mocks.core.setOutput.mock.calls[1].arguments[0], "response"); assert.equal( - mocks.core.setOutput.getCall(1).lastArg, + mocks.core.setOutput.mock.calls[1].arguments[1], JSON.stringify({ ok: true }), ); - assert.equal(mocks.core.setOutput.getCall(2).firstArg, "time"); - assert.ok(mocks.core.setOutput.getCall(2).lastArg >= 0); + assert.equal(mocks.core.setOutput.mock.calls[2].arguments[0], "time"); + assert.ok(mocks.core.setOutput.mock.calls[2].arguments[1] >= 0); }); it("incoming webhook", async () => { process.env.SLACK_TOKEN = "xoxb-example"; // https://github.com/slackapi/slack-github-action/issues/373 - mocks.core.getInput - .withArgs("webhook") - .returns("https://hooks.slack.com"); - mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); - mocks.core.getInput.withArgs("payload").returns('"text": "hello"'); - mocks.axios.post.returns(Promise.resolve({ status: 200, data: "ok" })); + mocks.inputs = { + ...mocks.inputs, + webhook: "https://hooks.slack.com", + "webhook-type": "incoming-webhook", + payload: '"text": "hello"', + }; + mocks.axios.post._promise = Promise.resolve({ status: 200, data: "ok" }); await send(mocks.core); - assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); - assert.equal(mocks.core.setOutput.getCall(0).lastArg, true); - assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); + assert.equal(mocks.core.setOutput.mock.calls[0].arguments[0], "ok"); + assert.equal(mocks.core.setOutput.mock.calls[0].arguments[1], true); + assert.equal(mocks.core.setOutput.mock.calls[1].arguments[0], "response"); assert.equal( - mocks.core.setOutput.getCall(1).lastArg, + mocks.core.setOutput.mock.calls[1].arguments[1], JSON.stringify("ok"), ); - assert.equal(mocks.core.setOutput.getCall(2).firstArg, "time"); - assert.ok(mocks.core.setOutput.getCall(2).lastArg >= 0); + assert.equal(mocks.core.setOutput.mock.calls[2].arguments[0], "time"); + assert.ok(mocks.core.setOutput.mock.calls[2].arguments[1] >= 0); }); }); }); diff --git a/test/webhook.spec.js b/test/webhook.spec.js index 09867e68..6d2a1cae 100644 --- a/test/webhook.spec.js +++ b/test/webhook.spec.js @@ -14,26 +14,32 @@ describe("webhook", () => { describe("success", () => { it("sends the parsed payload to the provided webhook trigger", async () => { - mocks.core.getInput - .withArgs("webhook") - .returns("https://hooks.slack.com"); - mocks.core.getInput.withArgs("webhook-type").returns("webhook-trigger"); - mocks.core.getInput.withArgs("payload").returns("drinks: coffee"); - mocks.axios.post.returns( - Promise.resolve({ status: 200, data: { ok: true } }), - ); + mocks.inputs = { + ...mocks.inputs, + webhook: "https://hooks.slack.com", + "webhook-type": "webhook-trigger", + payload: "drinks: coffee", + }; + mocks.axios.post._promise = Promise.resolve({ + status: 200, + data: { ok: true }, + }); try { await send(mocks.core); - assert.equal(mocks.axios.post.getCalls().length, 1); - const [url, payload, options] = mocks.axios.post.getCall(0).args; + assert.equal(mocks.axios.post.mock.calls.length, 1); + const [url, payload, options] = + mocks.axios.post.mock.calls[0].arguments; assert.equal(url, "https://hooks.slack.com"); assert.deepEqual(payload, { drinks: "coffee" }); assert.deepEqual(options, {}); - assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); - assert.equal(mocks.core.setOutput.getCall(0).lastArg, true); - assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); + assert.equal(mocks.core.setOutput.mock.calls[0].arguments[0], "ok"); + assert.equal(mocks.core.setOutput.mock.calls[0].arguments[1], true); + assert.equal( + mocks.core.setOutput.mock.calls[1].arguments[0], + "response", + ); assert.equal( - mocks.core.setOutput.getCall(1).lastArg, + mocks.core.setOutput.mock.calls[1].arguments[1], JSON.stringify({ ok: true }), ); } catch (err) { @@ -43,24 +49,29 @@ describe("webhook", () => { }); it("sends the parsed payload to the provided incoming webhook", async () => { - mocks.core.getInput - .withArgs("webhook") - .returns("https://hooks.slack.com"); - mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); - mocks.core.getInput.withArgs("payload").returns("text: greetings"); - mocks.axios.post.returns(Promise.resolve({ status: 200, data: "ok" })); + mocks.inputs = { + ...mocks.inputs, + webhook: "https://hooks.slack.com", + "webhook-type": "incoming-webhook", + payload: "text: greetings", + }; + mocks.axios.post._promise = Promise.resolve({ status: 200, data: "ok" }); try { await send(mocks.core); - assert.equal(mocks.axios.post.getCalls().length, 1); - const [url, payload, options] = mocks.axios.post.getCall(0).args; + assert.equal(mocks.axios.post.mock.calls.length, 1); + const [url, payload, options] = + mocks.axios.post.mock.calls[0].arguments; assert.equal(url, "https://hooks.slack.com"); assert.deepEqual(payload, { text: "greetings" }); assert.deepEqual(options, {}); - assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); - assert.equal(mocks.core.setOutput.getCall(0).lastArg, true); - assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); + assert.equal(mocks.core.setOutput.mock.calls[0].arguments[0], "ok"); + assert.equal(mocks.core.setOutput.mock.calls[0].arguments[1], true); + assert.equal( + mocks.core.setOutput.mock.calls[1].arguments[0], + "response", + ); assert.equal( - mocks.core.setOutput.getCall(1).lastArg, + mocks.core.setOutput.mock.calls[1].arguments[1], JSON.stringify("ok"), ); } catch (err) { @@ -92,11 +103,12 @@ describe("webhook", () => { }); it("returns the failures from a webhook trigger", async () => { - mocks.core.getInput - .withArgs("webhook") - .returns("https://hooks.slack.com"); - mocks.core.getInput.withArgs("webhook-type").returns("webhook-trigger"); - mocks.core.getInput.withArgs("payload").returns("drinks: coffee"); + mocks.inputs = { + ...mocks.inputs, + webhook: "https://hooks.slack.com", + "webhook-type": "webhook-trigger", + payload: "drinks: coffee", + }; const response = new AxiosError( "Request failed with status code 400", "ERR_BAD_REQUEST", @@ -104,7 +116,7 @@ describe("webhook", () => { {}, { status: 400 }, ); - mocks.axios.post.resolves(Promise.reject(response)); + mocks.axios.post._promise = Promise.reject(response); try { await send(mocks.core); } catch (err) { @@ -116,22 +128,23 @@ describe("webhook", () => { assert.fail(err); } } - assert.equal(mocks.axios.post.getCalls().length, 1); - const [url, payload, options] = mocks.axios.post.getCall(0).args; + assert.equal(mocks.axios.post.mock.calls.length, 1); + const [url, payload, options] = mocks.axios.post.mock.calls[0].arguments; assert.equal(url, "https://hooks.slack.com"); assert.deepEqual(payload, { drinks: "coffee" }); assert.deepEqual(options, {}); - assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); - assert.equal(mocks.core.setOutput.getCall(0).lastArg, false); - assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); + assert.equal(mocks.core.setOutput.mock.calls[0].arguments[0], "ok"); + assert.equal(mocks.core.setOutput.mock.calls[0].arguments[1], false); + assert.equal(mocks.core.setOutput.mock.calls[1].arguments[0], "response"); }); it("returns the failures from an incoming webhook", async () => { - mocks.core.getInput - .withArgs("webhook") - .returns("https://hooks.slack.com"); - mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); - mocks.core.getInput.withArgs("payload").returns("textt: oops"); + mocks.inputs = { + ...mocks.inputs, + webhook: "https://hooks.slack.com", + "webhook-type": "incoming-webhook", + payload: "textt: oops", + }; const response = new AxiosError( "Request failed with status code 400", "ERR_BAD_REQUEST", @@ -139,7 +152,7 @@ describe("webhook", () => { {}, { status: 400 }, ); - mocks.axios.post.resolves(Promise.reject(response)); + mocks.axios.post._promise = Promise.reject(response); try { await send(mocks.core); } catch (err) { @@ -151,14 +164,14 @@ describe("webhook", () => { assert.fail(err); } } - assert.equal(mocks.axios.post.getCalls().length, 1); - const [url, payload, options] = mocks.axios.post.getCall(0).args; + assert.equal(mocks.axios.post.mock.calls.length, 1); + const [url, payload, options] = mocks.axios.post.mock.calls[0].arguments; assert.equal(url, "https://hooks.slack.com"); assert.deepEqual(payload, { textt: "oops" }); assert.deepEqual(options, {}); - assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); - assert.equal(mocks.core.setOutput.getCall(0).lastArg, false); - assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); + assert.equal(mocks.core.setOutput.mock.calls[0].arguments[0], "ok"); + assert.equal(mocks.core.setOutput.mock.calls[0].arguments[1], false); + assert.equal(mocks.core.setOutput.mock.calls[1].arguments[0], "response"); }); }); @@ -186,9 +199,12 @@ describe("webhook", () => { }); it("skips proxying an http webhook url altogether", async () => { - mocks.core.getInput.withArgs("webhook").returns("http://hooks.slack.com"); - mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); - mocks.core.getInput.withArgs("proxy").returns("https://example.com"); + mocks.inputs = { + ...mocks.inputs, + webhook: "http://hooks.slack.com", + "webhook-type": "incoming-webhook", + proxy: "https://example.com", + }; const config = new Config(mocks.core); const webhook = new Webhook(); const request = webhook.proxies(config); @@ -197,11 +213,12 @@ describe("webhook", () => { it("sets up the proxy agent for the provided https proxy", async () => { const proxy = "https://example.com"; - mocks.core.getInput - .withArgs("webhook") - .returns("https://hooks.slack.com"); - mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); - mocks.core.getInput.withArgs("proxy").returns(proxy); + mocks.inputs = { + ...mocks.inputs, + webhook: "https://hooks.slack.com", + "webhook-type": "incoming-webhook", + proxy, + }; const config = new Config(mocks.core); const webhook = new Webhook(); const { httpsAgent, proxy: proxying } = webhook.proxies(config); @@ -211,11 +228,12 @@ describe("webhook", () => { it("sets up the agent without proxy for http proxies", async () => { const proxy = "http://example.com"; - mocks.core.getInput - .withArgs("webhook") - .returns("https://hooks.slack.com"); - mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); - mocks.core.getInput.withArgs("proxy").returns(proxy); + mocks.inputs = { + ...mocks.inputs, + webhook: "https://hooks.slack.com", + "webhook-type": "incoming-webhook", + proxy, + }; const config = new Config(mocks.core); const webhook = new Webhook(); const { httpsAgent, proxy: proxying } = webhook.proxies(config); @@ -225,11 +243,12 @@ describe("webhook", () => { it("fails to configure proxies with an invalid proxied url", async () => { const proxy = "https://"; - mocks.core.getInput - .withArgs("webhook") - .returns("https://hooks.slack.com"); - mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); - mocks.core.getInput.withArgs("proxy").returns(proxy); + mocks.inputs = { + ...mocks.inputs, + webhook: "https://hooks.slack.com", + "webhook-type": "incoming-webhook", + proxy, + }; try { const config = new Config(mocks.core); const webhook = new Webhook(); @@ -248,11 +267,12 @@ describe("webhook", () => { it("fails to configure proxies with an unknown url protocol", async () => { const proxy = "ssh://"; - mocks.core.getInput - .withArgs("webhook") - .returns("https://hooks.slack.com"); - mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); - mocks.core.getInput.withArgs("proxy").returns(proxy); + mocks.inputs = { + ...mocks.inputs, + webhook: "https://hooks.slack.com", + "webhook-type": "incoming-webhook", + proxy, + }; try { const config = new Config(mocks.core); const webhook = new Webhook(); From c0f43820e15e8fcfb7a3d5fa527248763b45d3f7 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Wed, 4 Feb 2026 00:18:35 -0800 Subject: [PATCH 2/3] chore: lint --- test/client.spec.js | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/test/client.spec.js b/test/client.spec.js index 9e486f67..330cda3a 100644 --- a/test/client.spec.js +++ b/test/client.spec.js @@ -213,10 +213,7 @@ describe("client", () => { assert.deepEqual(mocks.calls.mock.calls[0].arguments[1], args); assert.equal(mocks.core.setOutput.mock.calls[0].arguments[0], "ok"); assert.equal(mocks.core.setOutput.mock.calls[0].arguments[1], true); - assert.equal( - mocks.core.setOutput.mock.calls[1].arguments[0], - "response", - ); + assert.equal(mocks.core.setOutput.mock.calls[1].arguments[0], "response"); assert.equal( mocks.core.setOutput.mock.calls[1].arguments[1], JSON.stringify(response), @@ -275,10 +272,7 @@ describe("client", () => { assert.deepEqual(mocks.calls.mock.calls[0].arguments[1], args); assert.equal(mocks.core.setOutput.mock.calls[0].arguments[0], "ok"); assert.equal(mocks.core.setOutput.mock.calls[0].arguments[1], true); - assert.equal( - mocks.core.setOutput.mock.calls[1].arguments[0], - "response", - ); + assert.equal(mocks.core.setOutput.mock.calls[1].arguments[0], "response"); assert.equal( mocks.core.setOutput.mock.calls[1].arguments[1], JSON.stringify(response), @@ -318,10 +312,7 @@ describe("client", () => { assert.deepEqual(mocks.calls.mock.calls[0].arguments[1], args); assert.equal(mocks.core.setOutput.mock.calls[0].arguments[0], "ok"); assert.equal(mocks.core.setOutput.mock.calls[0].arguments[1], true); - assert.equal( - mocks.core.setOutput.mock.calls[1].arguments[0], - "response", - ); + assert.equal(mocks.core.setOutput.mock.calls[1].arguments[0], "response"); assert.equal( mocks.core.setOutput.mock.calls[1].arguments[1], JSON.stringify(response), @@ -353,7 +344,10 @@ describe("client", () => { token: "xoxb-example", payload: `"text": "hello"`, }; - mocks.calls._rejectsWith = errors.requestErrorWithOriginal(response, true); + mocks.calls._rejectsWith = errors.requestErrorWithOriginal( + response, + true, + ); try { await send(mocks.core); assert.fail("Expected an error but none was found"); From 4760897243f7f4ffd1a37adceb8e336ccf5f7b75 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Wed, 4 Feb 2026 00:29:08 -0800 Subject: [PATCH 3/3] refactor: make test resets more explicit --- test/index.spec.js | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/test/index.spec.js b/test/index.spec.js index 9fa55e65..8e60110b 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -89,29 +89,25 @@ export class Mock { * Testing interface that removes internal state from existing mocks. */ reset() { - // Clear lookup tables - this.inputs = {}; - this.booleanInputs = {}; - - // Reset axios mock + this._isDebug = false; this.axios.post.mock.resetCalls(); this.axios.post._promise = undefined; - - // Reset apiCall mock + this.booleanInputs = {}; this.calls.mock.resetCalls(); this.calls._resolvesWith = undefined; this.calls._rejectsWith = undefined; - - // Reset core mocks - for (const fn of Object.values(this.core)) { - fn.mock?.resetCalls(); - } - this._isDebug = false; - - // Reset webapi + this.core.debug.mock.resetCalls(); + this.core.error.mock.resetCalls(); + this.core.getBooleanInput.mock.resetCalls(); + this.core.getInput.mock.resetCalls(); + this.core.info.mock.resetCalls(); + this.core.isDebug.mock.resetCalls(); + this.core.setFailed.mock.resetCalls(); + this.core.setOutput.mock.resetCalls(); + this.core.setSecret.mock.resetCalls(); + this.core.warning.mock.resetCalls(); + this.inputs = {}; this.webapi = {}; - - // Clear environment variables process.env.SLACK_TOKEN = ""; process.env.SLACK_WEBHOOK_URL = ""; }