diff --git a/.github/workflows/deno.yml b/.github/workflows/deno.yml index 854b8d9..87cc06b 100644 --- a/.github/workflows/deno.yml +++ b/.github/workflows/deno.yml @@ -15,7 +15,10 @@ jobs: matrix: # we test on both most recent stable version of deno (v1.x) as well as # the version of deno used by Run on Slack (as noted in https://api.slack.com/slackcli/metadata.json) - deno-version: [v1.x, v1.45.4] + deno-version: + - v1.x + - v1.46.2 + - v2.x permissions: contents: read steps: @@ -33,8 +36,9 @@ jobs: run: deno task generate-lcov - name: Upload coverage to CodeCov uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 + if: matrix.deno-version == 'v2.x' with: - file: ./lcov.info + files: ./lcov.info token: ${{ secrets.CODECOV_TOKEN }} health-score: diff --git a/.github/workflows/samples.yml b/.github/workflows/samples.yml index 5a4629a..4ba180d 100644 --- a/.github/workflows/samples.yml +++ b/.github/workflows/samples.yml @@ -13,6 +13,7 @@ jobs: samples: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: sample: - slack-samples/deno-issue-submission @@ -21,9 +22,12 @@ jobs: - slack-samples/deno-message-translator - slack-samples/deno-request-time-off - slack-samples/deno-simple-survey - # we test on both most recent stable version of deno (v1.x) as well as + # we test on both most recent stable version of deno (v1.x, v2.x) as well as # the version of deno used by Run on Slack (as noted in https://api.slack.com/slackcli/metadata.json) - deno-version: [v1.x, v1.45.4] + deno-version: + - v1.x + - v1.46.2 + - v2.x permissions: contents: read steps: @@ -44,14 +48,13 @@ jobs: path: ./sample persist-credentials: false - - name: Set imports.deno-slack-api/ to ../deno-slack-api/src/ in import_map.json + - name: Set imports.deno-slack-api/ to ../deno-slack-api/src/ in imports run: > deno run --allow-read --allow-write --allow-net - deno-slack-api/scripts/src/import_map/update.ts - --import-map "./sample/import_map.json" - --parent-import-map "./deno-slack-api/deno.jsonc" - --api "../deno-slack-api/src/" + deno-slack-api/scripts/src/imports/update.ts + --import-file "./sample/deno.jsonc" + --api "./deno-slack-api/" - name: Deno check **/*.ts working-directory: ./sample diff --git a/deno.jsonc b/deno.jsonc index d1d36e6..a3cbd80 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -7,17 +7,18 @@ "docs", "README.md", ".github/maintainers_guide.md", - ".github/CONTRIBUTING.md" + ".github/CONTRIBUTING.md", + "testing" ] }, "lint": { - "include": ["src", "scripts"] + "include": ["src", "scripts", "testing"] }, "publish": { "exclude": ["./README.md"] }, "test": { - "include": ["src", "scripts"] + "include": ["src", "scripts", "testing"] }, "tasks": { "test": "deno fmt --check && deno lint && deno test", @@ -36,7 +37,8 @@ "@std/assert": "jsr:@std/assert@^1", "@std/cli": "jsr:@std/cli@^1", "@std/fs": "jsr:@std/fs@^1", - "@std/http": "jsr:@std/http@^0.206", + "@std/http": "jsr:@std/http@^0.206.0", + "@std/path": "jsr:@std/path@^1.0.9", "@std/testing": "jsr:@std/testing@^1", "@std/text": "jsr:@std/text@^1" } diff --git a/scripts/src/import_map/update.ts b/scripts/src/import_map/update.ts deleted file mode 100644 index 75f7b55..0000000 --- a/scripts/src/import_map/update.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { parseArgs } from "@std/cli/parse-args"; -import { createHttpError } from "@std/http/http-errors"; - -// Regex for https://deno.land/x/deno_slack_api@x.x.x/ -const API_REGEX = - /(https:\/\/deno.land\/x\/deno_slack_api@[0-9]\.[0-9]+\.[0-9]+\/)/g; - -async function main() { - const flags = parseArgs(Deno.args, { - string: ["import-map", "api", "parent-import-map"], - default: { - "import-map": `${Deno.cwd()}/import_map.json`, - "api": "../deno-slack-api/src/", - "parent-import-map": undefined, - }, - }); - - const importMapJsonIn = await Deno.readTextFile(flags["import-map"]); - console.log("`import_map.json` in content:", importMapJsonIn); - - const importMap = JSON.parse(importMapJsonIn); - const denoSlackSdkValue = importMap["imports"]["deno-slack-sdk/"]; - - const apiDepsInSdk = await apiDepsIn(denoSlackSdkValue); - - importMap["imports"]["deno-slack-api/"] = flags.api; - importMap["scopes"] = { - [denoSlackSdkValue]: [...apiDepsInSdk].reduce( - (sdkScopes: Record, apiDep: string) => { - return { - ...sdkScopes, - [apiDep]: flags.api, - }; - }, - {}, - ), - }; - - if (flags["parent-import-map"]) { - const parentImportMapJsonIn = await Deno.readTextFile( - flags["parent-import-map"], - ); - console.log("parent `import_map.json` in content:", parentImportMapJsonIn); - const parentImportMap = JSON.parse(parentImportMapJsonIn); - for (const entry of Object.entries(parentImportMap["imports"])) { - importMap["imports"][entry[0]] = entry[1]; - } - } - - const importMapJsonOut = JSON.stringify(importMap, null, 2); - console.log("`import_map.json` out content:", importMapJsonOut); - - await Deno.writeTextFile(flags["import-map"], importMapJsonOut); -} - -export async function apiDepsIn(moduleUrl: string): Promise> { - const fileUrl = moduleUrl.endsWith("/") - ? `${moduleUrl}deps.ts?source,file` - : `${moduleUrl}/deps.ts?source,file`; - const response = await fetch(fileUrl); - - if (!response.ok) { - const err = createHttpError(response.status, await response.text(), { - expose: true, - headers: response.headers, - }); - console.error(err); - throw err; - } - - const depsTs = await response.text(); - return new Set(depsTs.match(API_REGEX)); -} - -if (import.meta.main) main(); diff --git a/scripts/src/imports/update.ts b/scripts/src/imports/update.ts new file mode 100644 index 0000000..e49ab4f --- /dev/null +++ b/scripts/src/imports/update.ts @@ -0,0 +1,82 @@ +import { parseArgs } from "@std/cli/parse-args"; +import { createHttpError } from "@std/http/http-errors"; +import { dirname, join, relative } from "@std/path"; + +// Regex for https://deno.land/x/deno_slack_api@x.x.x/ +const API_REGEX = + /(https:\/\/deno.land\/x\/deno_slack_api@[0-9]\.[0-9]+\.[0-9]+\/)/g; + +async function main() { + const flags = parseArgs(Deno.args, { + string: ["import-file", "api"], + default: { + "import-file": `${Deno.cwd()}/deno.jsonc`, + "api": "./deno-slack-api/", + }, + }); + + const importFilePath = await Deno.realPath(flags["import-file"]); + const importFileDir = dirname(importFilePath); + const apiDir = await Deno.realPath(flags.api); + + const importFileJsonIn = await Deno.readTextFile(importFilePath); + console.log(`content in ${flags["import-file"]}:`, importFileJsonIn); + + const importFile = JSON.parse(importFileJsonIn); + const denoSlackSdkValue = importFile["imports"]["deno-slack-sdk/"]; + + const apiDepsInSdk = await apiDepsIn(denoSlackSdkValue); + + const apiPackageSpecifier = join( + relative(importFileDir, apiDir), + "/src/", + ); + + importFile["imports"]["deno-slack-api/"] = apiPackageSpecifier; + importFile["scopes"] = { + [denoSlackSdkValue]: [...apiDepsInSdk].reduce( + (sdkScopes: Record, apiDep: string) => { + return { + ...sdkScopes, + [apiDep]: apiPackageSpecifier, + }; + }, + {}, + ), + }; + + const parentFileJsonIn = await Deno.readTextFile( + join(apiDir, "/deno.jsonc"), + ); + console.log("parent `import file` in content:", parentFileJsonIn); + const parentImportFile = JSON.parse(parentFileJsonIn); + for (const entry of Object.entries(parentImportFile["imports"])) { + importFile["imports"][entry[0]] = entry[1]; + } + + const importMapJsonOut = JSON.stringify(importFile, null, 2); + console.log("`import file` out content:", importMapJsonOut); + + await Deno.writeTextFile(flags["import-file"], importMapJsonOut); +} + +export async function apiDepsIn(moduleUrl: string): Promise> { + const fileUrl = moduleUrl.endsWith("/") + ? `${moduleUrl}deps.ts?source,file` + : `${moduleUrl}/deps.ts?source,file`; + const response = await fetch(fileUrl); + + if (!response.ok) { + const err = createHttpError(response.status, await response.text(), { + expose: true, + headers: response.headers, + }); + console.error(err); + throw err; + } + + const depsTs = await response.text(); + return new Set(depsTs.match(API_REGEX)); +} + +if (import.meta.main) main(); diff --git a/scripts/src/import_map/update_test.ts b/scripts/src/imports/update_test.ts similarity index 71% rename from scripts/src/import_map/update_test.ts rename to scripts/src/imports/update_test.ts index d5cd199..9849a68 100644 --- a/scripts/src/import_map/update_test.ts +++ b/scripts/src/imports/update_test.ts @@ -1,29 +1,19 @@ import { isHttpError } from "@std/http/http-errors"; -import { mf } from "../../../src/dev_deps.ts"; import { assertEquals, assertRejects } from "@std/assert"; -import { afterEach, beforeAll } from "@std/testing/bdd"; import { apiDepsIn } from "./update.ts"; +import { stubFetch } from "../../../testing/http.ts"; const depsTsMock = `export { SlackAPI } from "https://deno.land/x/deno_slack_api@2.1.0/mod.ts"; export type {SlackAPIClient, Trigger} from "https://deno.land/x/deno_slack_api@2.2.0/types.ts";`; -beforeAll(() => { - mf.install(); -}); - -afterEach(() => { - mf.reset(); -}); - Deno.test("apiDepsIn should return a list of the api module urls used by a module", async () => { - mf.mock("GET@/x/deno_slack_sdk@x.x.x/deps.ts", (req: Request) => { + using _fetchStub = stubFetch((req) => { assertEquals( req.url, "https://deno.land/x/deno_slack_sdk@x.x.x/deps.ts?source,file", ); - return new Response(depsTsMock); - }); + }, new Response(depsTsMock)); const apiDeps = await apiDepsIn( "https://deno.land/x/deno_slack_sdk@x.x.x/", @@ -39,9 +29,12 @@ Deno.test("apiDepsIn should return a list of the api module urls used by a modul }); Deno.test("apiDepsIn should throw http error on response not ok", async () => { - mf.mock("GET@/x/deno_slack_sdk@x.x.x/deps.ts", () => { - return new Response("error", { status: 500 }); - }); + using _fetchStub = stubFetch( + (req) => { + assertEquals(req.url, "https://deno.land/x/deno_slack_sdk@x.x.x/deps.ts"); + }, + new Response("error", { status: 500 }), + ); const error = await assertRejects(() => apiDepsIn("https://deno.land/x/deno_slack_sdk@x.x.x/") diff --git a/src/api_test.ts b/src/api_test.ts index 533b4e6..2f61e0b 100644 --- a/src/api_test.ts +++ b/src/api_test.ts @@ -1,4 +1,3 @@ -import { mf } from "./dev_deps.ts"; import { assertEquals, assertExists, @@ -7,10 +6,9 @@ import { } from "@std/assert"; import { SlackAPI } from "./mod.ts"; import { HttpError } from "@std/http/http-errors"; +import { stubFetch } from "../testing/http.ts"; Deno.test("SlackAPI class", async (t) => { - mf.install(); // mock out calls to `fetch` - await t.step( "instantiated with default API URL", async (t) => { @@ -24,53 +22,55 @@ Deno.test("SlackAPI class", async (t) => { await t.step("apiCall method", async (t) => { await t.step("should call the default API URL", async () => { - mf.mock("POST@/api/chat.postMessage", (req: Request) => { + using _fetchStub = stubFetch((req) => { assertEquals(req.url, "https://slack.com/api/chat.postMessage"); assertExists(req.headers.has("user-agent")); - return new Response('{"ok":true}'); - }); + }, new Response('{"ok":true}')); await client.apiCall("chat.postMessage", {}); - - mf.reset(); }); await t.step( "should prioritize calling provided token vs. token instantiated client with", async () => { - mf.mock("POST@/api/chat.postMessage", (req: Request) => { + let fetchStub = stubFetch((req) => { + assertEquals(req.method, "POST"); + assertEquals(req.url, "https://slack.com/api/chat.postMessage"); assertEquals(req.headers.get("authorization"), "Bearer override"); assertExists(req.headers.has("user-agent")); - return new Response('{"ok":true}'); - }); + }, new Response('{"ok":true}')); await client.apiCall("chat.postMessage", { token: "override" }); - mf.reset(); + fetchStub.restore(); - mf.mock("POST@/api/chat.postMessage", (req: Request) => { + fetchStub = stubFetch((req) => { + assertEquals(req.method, "POST"); + assertEquals(req.url, "https://slack.com/api/chat.postMessage"); assertEquals( req.headers.get("authorization"), "Bearer test-token", ); - return new Response('{"ok":true}'); - }); - + }, new Response('{"ok":true}')); await client.apiCall("chat.postMessage", {}); - mf.reset(); + fetchStub.restore(); }, ); await t.step( "should throw an HttpError if HTTP response status code >= 400", async () => { - mf.mock("POST@/api/chat.postMessage", () => { - return new Response("ratelimited", { + using _fetchStub = stubFetch( + (req) => { + assertEquals(req.method, "POST"); + assertEquals(req.url, "https://slack.com/api/chat.postMessage"); + }, + new Response("ratelimited", { status: 429, headers: { "Retry-After": "120" }, - }); - }); + }), + ); const error = await assertRejects( () => client.apiCall("chat.postMessage", {}), @@ -79,26 +79,26 @@ Deno.test("SlackAPI class", async (t) => { ); assertEquals(error.headers?.get("Retry-After"), "120"); assertEquals(error.status, 429); - - mf.reset(); }, ); await t.step( "should return successful response JSON", async () => { - mf.mock("POST@/api/chat.postMessage", () => { - return new Response( + using _fetchStub = stubFetch( + (req) => { + assertEquals(req.method, "POST"); + assertEquals(req.url, "https://slack.com/api/chat.postMessage"); + }, + new Response( '{"ok":true, "channel": "C123", "message": {}}', - ); - }); + ), + ); const res = await client.apiCall("chat.postMessage", {}); assertEquals(res.ok, true); assertEquals(res.channel, "C123"); assertEquals(res.message, {}); - - mf.reset(); }, ); @@ -106,19 +106,24 @@ Deno.test("SlackAPI class", async (t) => { await t.step( "should return usable Response with payload {'ok': false}", async () => { - mf.mock("POST@/api/chat.postMessage", () => { - return new Response('{"ok":false}', { + using _fetchStub = stubFetch( + (req) => { + assertEquals(req.method, "POST"); + assertEquals( + req.url, + "https://slack.com/api/chat.postMessage", + ); + }, + new Response('{"ok":false}', { headers: { "Retry-After": "120" }, - }); - }); + }), + ); const res = await client.apiCall("chat.postMessage", {}); assertEquals(res.ok, false); const fullRes = res.toFetchResponse(); assertInstanceOf(fullRes, Response); assertEquals(fullRes.headers?.get("Retry-After"), "120"); - - mf.reset(); }, ); }); @@ -128,12 +133,16 @@ Deno.test("SlackAPI class", async (t) => { await t.step( "should throw an HttpError if HTTP response status code >= 400", async () => { - mf.mock("POST@/api/chat.postMessage", () => { - return new Response("ratelimited", { + using _fetchStub = stubFetch( + (req) => { + assertEquals(req.method, "POST"); + assertEquals(req.url, "https://slack.com/api/chat.postMessage"); + }, + new Response("ratelimited", { status: 429, headers: { "Retry-After": "120" }, - }); - }); + }), + ); const error = await assertRejects( () => @@ -143,19 +152,21 @@ Deno.test("SlackAPI class", async (t) => { assertEquals(error.headers?.get("Retry-After"), "120"); assertEquals(error.status, 429); assertEquals(error.message, "429: ratelimited"); - - mf.reset(); }, ); await t.step( "should return successful response JSON", async () => { - mf.mock("POST@/api/chat.postMessage", () => { - return new Response( + using _fetchStub = stubFetch( + (req) => { + assertEquals(req.method, "POST"); + assertEquals(req.url, "https://slack.com/api/chat.postMessage"); + }, + new Response( '{"ok":true, "channel": "C123", "message": {}}', - ); - }); + ), + ); const res = await client.response( "https://slack.com/api/chat.postMessage", @@ -164,8 +175,6 @@ Deno.test("SlackAPI class", async (t) => { assertEquals(res.ok, true); assertEquals(res.channel, "C123"); assertEquals(res.message, {}); - - mf.reset(); }, ); @@ -173,11 +182,18 @@ Deno.test("SlackAPI class", async (t) => { await t.step( "should return usable Response with payload {'ok': false}", async () => { - mf.mock("POST@/api/chat.postMessage", () => { - return new Response('{"ok":false}', { + using _fetchStub = stubFetch( + (req) => { + assertEquals(req.method, "POST"); + assertEquals( + req.url, + "https://slack.com/api/chat.postMessage", + ); + }, + new Response('{"ok":false}', { headers: { "Retry-After": "120" }, - }); - }); + }), + ); const res = await client.response( "https://slack.com/api/chat.postMessage", @@ -187,8 +203,6 @@ Deno.test("SlackAPI class", async (t) => { const fullRes = res.toFetchResponse(); assertInstanceOf(fullRes, Response); assertEquals(fullRes.headers?.get("Retry-After"), "120"); - - mf.reset(); }, ); }); @@ -205,14 +219,14 @@ Deno.test("SlackAPI class", async (t) => { await t.step("apiCall method", async (t) => { await t.step("should call the custom API URL", async () => { - mf.mock("POST@/chat.postMessage", (req: Request) => { - assertEquals(req.url, "https://apitown.com/chat.postMessage"); - return new Response('{"ok":true}'); - }); + using _fetchStub = stubFetch( + (req) => { + assertEquals(req.url, "https://apitown.com/chat.postMessage"); + }, + new Response('{"ok":true}'), + ); await client.apiCall("chat.postMessage", {}); - - mf.reset(); }); }); }, @@ -227,14 +241,14 @@ Deno.test("SlackAPI class", async (t) => { await t.step("apiCall method", async (t) => { await t.step("should call the custom API URL", async () => { - mf.mock("POST@/chat.postMessage", (req: Request) => { - assertEquals(req.url, "https://apitown.com/chat.postMessage"); - return new Response('{"ok":true}'); - }); + using _fetchStub = stubFetch( + (req) => { + assertEquals(req.url, "https://apitown.com/chat.postMessage"); + }, + new Response('{"ok":true}'), + ); await client.apiCall("chat.postMessage", {}); - - mf.reset(); }); }); }, @@ -248,37 +262,48 @@ Deno.test("SlackAPI class", async (t) => { await t.step( "should provide single level deep api method functions", async () => { - mf.mock("POST@/api/chat.postMessage", () => { - return new Response('{"ok":true}'); - }); + using _fetchStub = stubFetch( + (req) => { + assertEquals(req.method, "POST"); + assertEquals(req.url, "https://slack.com/api/chat.postMessage"); + }, + new Response('{"ok":true}'), + ); const res = await client.chat.postMessage({ channel: "", text: "" }); assertEquals(res.ok, true); - - mf.reset(); }, ); await t.step( "should provide deeply nested api method functions", async () => { - mf.mock("POST@/api/admin.apps.approved.list", () => { - return new Response('{"ok":true}'); - }); + using _fetchStub = stubFetch( + (req) => { + assertEquals(req.method, "POST"); + assertEquals( + req.url, + "https://slack.com/api/admin.apps.approved.list", + ); + }, + new Response('{"ok":true}'), + ); const res = await client.admin.apps.approved.list(); assertEquals(res.ok, true); - - mf.reset(); }, ); await t.step( "should allow for typed method calls", async () => { - mf.mock("POST@/api/apps.datastore.put", () => { - return new Response('{"ok":true}'); - }); + using _fetchStub = stubFetch( + (req) => { + assertEquals(req.method, "POST"); + assertEquals(req.url, "https://slack.com/api/apps.datastore.put"); + }, + new Response('{"ok":true}'), + ); const TestDatastore = { name: "test", @@ -297,113 +322,121 @@ Deno.test("SlackAPI class", async (t) => { }, }); assertEquals(res.ok, true); - - mf.reset(); }, ); await t.step( "should allow for typed method calls for external auth", async () => { - mf.mock("POST@/api/apps.auth.external.get", () => { - return new Response('{"ok":true, "external_token": "abcd"}'); - }); + using _fetchStub = stubFetch( + (req) => { + assertEquals(req.method, "POST"); + assertEquals( + req.url, + "https://slack.com/api/apps.auth.external.get", + ); + }, + new Response('{"ok":true, "external_token": "abcd"}'), + ); const TestExternalAuthId = { external_token_id: "ET12345", }; const res = await client.apps.auth.external.get(TestExternalAuthId); assertEquals(res.ok, true); assertEquals(res.external_token, "abcd"); - mf.reset(); }, ); await t.step( "should allow for typed method calls for external auth with force_refresh", async () => { - mf.mock("POST@/api/apps.auth.external.get", () => { - return new Response('{"ok":true, "external_token": "abcd"}'); - }); + using _fetchStub = stubFetch( + (req) => { + assertEquals(req.method, "POST"); + assertEquals( + req.url, + "https://slack.com/api/apps.auth.external.get", + ); + }, + new Response('{"ok":true, "external_token": "abcd"}'), + ); const res = await client.apps.auth.external.get({ external_token_id: "ET12345", force_refresh: true, }); assertEquals(res.ok, true); assertEquals(res.external_token, "abcd"); - mf.reset(); }, ); await t.step( "should allow for typed method calls for external auth delete method", async () => { - mf.mock("POST@/api/apps.auth.external.delete", () => { - return new Response('{"ok":true}'); - }); + using _fetchStub = stubFetch( + (req) => { + assertEquals(req.method, "POST"); + assertEquals( + req.url, + "https://slack.com/api/apps.auth.external.delete", + ); + }, + new Response('{"ok":true}'), + ); const res = await client.apps.auth.external.delete({ external_token_id: "ET12345", }); assertEquals(res.ok, true); - mf.reset(); }, ); }, ); - - mf.uninstall(); }); Deno.test("SlackApi.setSlackApiUrl()", async (t) => { - mf.install(); const testClient = SlackAPI("test-token"); await t.step("override url", async () => { testClient.setSlackApiUrl("https://something.slack.com/api/"); - mf.mock("POST@/api/chat.postMessage", (req: Request) => { - assertEquals( - req.url, - "https://something.slack.com/api/chat.postMessage", - ); - return new Response('{"ok":true}'); - }); + using _fetchStub = stubFetch( + (req) => { + assertEquals( + req.url, + "https://something.slack.com/api/chat.postMessage", + ); + }, + new Response('{"ok":true}'), + ); await testClient.apiCall("chat.postMessage", {}); - - mf.reset(); }); await t.step("override url without trailing slash", async () => { testClient.setSlackApiUrl("https://something.slack.com/api"); - mf.mock("POST@/api/chat.postMessage", (req: Request) => { - assertEquals( - req.url, - "https://something.slack.com/api/chat.postMessage", - ); - return new Response('{"ok":true}'); - }); + using _fetchStub = stubFetch( + (req) => { + assertEquals( + req.url, + "https://something.slack.com/api/chat.postMessage", + ); + }, + new Response('{"ok":true}'), + ); await testClient.apiCall("chat.postMessage", {}); - - mf.reset(); }); await t.step("reset url", async () => { testClient.setSlackApiUrl("https://slack.com/api/"); - mf.mock("POST@/api/chat.postMessage", (req: Request) => { - assertEquals( - req.url, - "https://slack.com/api/chat.postMessage", - ); - return new Response('{"ok":true}'); - }); + using _fetchStub = stubFetch( + (req) => { + assertEquals(req.url, "https://slack.com/api/chat.postMessage"); + }, + new Response('{"ok":true}'), + ); await testClient.apiCall("chat.postMessage", {}); - - mf.reset(); }); - - mf.uninstall(); }); diff --git a/src/dev_deps.ts b/src/dev_deps.ts index 02ba0ea..e69de29 100644 --- a/src/dev_deps.ts +++ b/src/dev_deps.ts @@ -1 +0,0 @@ -export * as mf from "https://deno.land/x/mock_fetch@0.3.0/mod.ts"; diff --git a/src/typed-method-types/workflows/triggers/tests/crud_test.ts b/src/typed-method-types/workflows/triggers/tests/crud_test.ts index 47f67a5..13c6374 100644 --- a/src/typed-method-types/workflows/triggers/tests/crud_test.ts +++ b/src/typed-method-types/workflows/triggers/tests/crud_test.ts @@ -1,6 +1,6 @@ import { assertEquals, assertObjectMatch } from "@std/assert"; import { SlackAPI } from "../../../../mod.ts"; -import * as mf from "https://deno.land/x/mock_fetch@0.3.0/mod.ts"; +import { stubFetch } from "../../../../../testing/http.ts"; import { delete_response, shortcut_response, @@ -8,8 +8,6 @@ import { import { list_response } from "./fixtures/list_response.ts"; Deno.test("Mock CRUD call", async (t) => { - mf.install(); // mock out calls to `fetch` - await t.step("instantiated with default API URL", async (t) => { const client = SlackAPI("test-token"); @@ -22,13 +20,16 @@ Deno.test("Mock CRUD call", async (t) => { await t.step("create method", async (t) => { await t.step("should call the default API URL", async () => { - mf.mock("POST@/api/workflows.triggers.create", (req: Request) => { - assertEquals( - req.url, - "https://slack.com/api/workflows.triggers.create", - ); - return new Response('{"ok":true}'); - }); + using _fetchStub = stubFetch( + (req) => { + assertEquals(req.method, "POST"); + assertEquals( + req.url, + "https://slack.com/api/workflows.triggers.create", + ); + }, + new Response('{"ok":true}'), + ); await client.workflows.triggers.create({ name: "TEST", @@ -44,20 +45,21 @@ Deno.test("Mock CRUD call", async (t) => { channel_ids: ["C013ZG3K41Z"], }, }); - - mf.reset(); }); await t.step( "should return successful response JSON on create", async () => { - mf.mock("POST@/api/workflows.triggers.create", (req: Request) => { - assertEquals( - req.url, - "https://slack.com/api/workflows.triggers.create", - ); - return new Response(JSON.stringify(shortcut_response)); - }); + using _fetchStub = stubFetch( + (req) => { + assertEquals(req.method, "POST"); + assertEquals( + req.url, + "https://slack.com/api/workflows.triggers.create", + ); + }, + new Response(JSON.stringify(shortcut_response)), + ); const res = await client.workflows.triggers.create({ name: "TEST", @@ -77,8 +79,6 @@ Deno.test("Mock CRUD call", async (t) => { shortcut_response.trigger.shortcut_url, ); } - - mf.reset(); }, ); }); @@ -86,13 +86,16 @@ Deno.test("Mock CRUD call", async (t) => { await t.step( "should return successful response JSON on update", async () => { - mf.mock("POST@/api/workflows.triggers.update", (req: Request) => { - assertEquals( - req.url, - "https://slack.com/api/workflows.triggers.update", - ); - return new Response(JSON.stringify(shortcut_response)); - }); + using _fetchStub = stubFetch( + (req) => { + assertEquals(req.method, "POST"); + assertEquals( + req.url, + "https://slack.com/api/workflows.triggers.update", + ); + }, + new Response(JSON.stringify(shortcut_response)), + ); const res = await client.workflows.triggers.update({ name: "TEST", @@ -113,47 +116,47 @@ Deno.test("Mock CRUD call", async (t) => { shortcut_response.trigger.shortcut_url, ); } - - mf.reset(); }, ); await t.step( "should return successful response JSON on delete", async () => { - mf.mock("POST@/api/workflows.triggers.delete", (req: Request) => { - assertEquals( - req.url, - "https://slack.com/api/workflows.triggers.delete", - ); - return new Response(JSON.stringify(delete_response)); - }); + using _fetchStub = stubFetch( + (req) => { + assertEquals(req.method, "POST"); + assertEquals( + req.url, + "https://slack.com/api/workflows.triggers.delete", + ); + }, + new Response(JSON.stringify(delete_response)), + ); const res = await client.workflows.triggers.delete({ trigger_id: "123", }); assertEquals(res.ok, true); - - mf.reset(); }, ); await t.step( "should return successful response JSON on list", async () => { - mf.mock("POST@/api/workflows.triggers.list", (req: Request) => { - assertEquals( - req.url, - "https://slack.com/api/workflows.triggers.list", - ); - return new Response(JSON.stringify(list_response)); - }); + using _fetchStub = stubFetch( + (req) => { + assertEquals(req.method, "POST"); + assertEquals( + req.url, + "https://slack.com/api/workflows.triggers.list", + ); + }, + new Response(JSON.stringify(list_response)), + ); const res = await client.workflows.triggers.list(); assertEquals(res.ok, true); assertEquals(res.triggers?.length, list_response.triggers.length); - - mf.reset(); }, ); }); diff --git a/src/typed-method-types/workflows/triggers/tests/event_test.ts b/src/typed-method-types/workflows/triggers/tests/event_test.ts index cceceb5..d80f37e 100644 --- a/src/typed-method-types/workflows/triggers/tests/event_test.ts +++ b/src/typed-method-types/workflows/triggers/tests/event_test.ts @@ -1,6 +1,6 @@ import { assertEquals, assertObjectMatch } from "@std/assert"; import { SlackAPI, TriggerEventTypes, TriggerTypes } from "../../../../mod.ts"; -import * as mf from "https://deno.land/x/mock_fetch@0.3.0/mod.ts"; +import { stubFetch } from "../../../../../testing/http.ts"; import { event_response } from "./fixtures/sample_responses.ts"; import type { ExampleWorkflow } from "./fixtures/workflows.ts"; import type { EventTrigger } from "../event.ts"; @@ -164,8 +164,6 @@ Deno.test("Event trigger type tests", async (t) => { }); Deno.test("Mock call for event", async (t) => { - mf.install(); // mock out calls to `fetch` - await t.step("instantiated with default API URL", async (t) => { const client = SlackAPI("test-token"); @@ -178,13 +176,16 @@ Deno.test("Mock call for event", async (t) => { await t.step( "should return successful response JSON on create", async () => { - mf.mock("POST@/api/workflows.triggers.create", (req: Request) => { - assertEquals( - req.url, - "https://slack.com/api/workflows.triggers.create", - ); - return new Response(JSON.stringify(event_response)); - }); + using _fetchStub = stubFetch( + (req) => { + assertEquals(req.method, "POST"); + assertEquals( + req.url, + "https://slack.com/api/workflows.triggers.create", + ); + }, + new Response(JSON.stringify(event_response)), + ); const res = await client.workflows.triggers.create({ name: "TEST", @@ -208,8 +209,6 @@ Deno.test("Mock call for event", async (t) => { event_response.trigger.event_type, ); } - - mf.reset(); }, ); }); @@ -217,13 +216,16 @@ Deno.test("Mock call for event", async (t) => { await t.step( "should return successful response JSON on update", async () => { - mf.mock("POST@/api/workflows.triggers.update", (req: Request) => { - assertEquals( - req.url, - "https://slack.com/api/workflows.triggers.update", - ); - return new Response(JSON.stringify(event_response)); - }); + using _fetchStub = stubFetch( + (req) => { + assertEquals(req.method, "POST"); + assertEquals( + req.url, + "https://slack.com/api/workflows.triggers.update", + ); + }, + new Response(JSON.stringify(event_response)), + ); const res = await client.workflows.triggers.update({ name: "TEST", @@ -248,8 +250,6 @@ Deno.test("Mock call for event", async (t) => { event_response.trigger.event_type, ); } - - mf.reset(); }, ); }); diff --git a/src/typed-method-types/workflows/triggers/tests/scheduled_test.ts b/src/typed-method-types/workflows/triggers/tests/scheduled_test.ts index 9429631..9948e1f 100644 --- a/src/typed-method-types/workflows/triggers/tests/scheduled_test.ts +++ b/src/typed-method-types/workflows/triggers/tests/scheduled_test.ts @@ -1,8 +1,8 @@ -import { mf } from "../../../../dev_deps.ts"; import { assertEquals, assertObjectMatch } from "@std/assert"; import type { ScheduledTrigger } from "../scheduled.ts"; import { TriggerTypes } from "../mod.ts"; import { SlackAPI } from "../../../../mod.ts"; +import { stubFetch } from "../../../../../testing/http.ts"; import { scheduled_response } from "./fixtures/sample_responses.ts"; Deno.test("Scheduled triggers can be set with a string", () => { @@ -188,8 +188,6 @@ Deno.test("Scheduled triggers can be set to be reoccur yearly", () => { }); Deno.test("Mock call for schedule", async (t) => { - mf.install(); // mock out calls to `fetch` - await t.step("instantiated with default API URL", async (t) => { const client = SlackAPI("test-token"); @@ -202,15 +200,15 @@ Deno.test("Mock call for schedule", async (t) => { await t.step( "should return successful response JSON on create", async () => { - mf.mock( - "POST@/api/workflows.triggers.create", - (req: Request) => { + using _fetchStub = stubFetch( + (req) => { + assertEquals(req.method, "POST"); assertEquals( req.url, "https://slack.com/api/workflows.triggers.create", ); - return new Response(JSON.stringify(scheduled_response)); }, + new Response(JSON.stringify(scheduled_response)), ); const res = await client.workflows.triggers.create({ @@ -248,8 +246,6 @@ Deno.test("Mock call for schedule", async (t) => { scheduled_response.trigger.schedule.timezone, ); } - - mf.reset(); }, ); }); @@ -257,15 +253,15 @@ Deno.test("Mock call for schedule", async (t) => { await t.step( "should return successful response JSON on update", async () => { - mf.mock( - "POST@/api/workflows.triggers.update", - (req: Request) => { + using _fetchStub = stubFetch( + (req) => { + assertEquals(req.method, "POST"); assertEquals( req.url, "https://slack.com/api/workflows.triggers.update", ); - return new Response(JSON.stringify(scheduled_response)); }, + new Response(JSON.stringify(scheduled_response)), ); const res = await client.workflows.triggers.update({ @@ -304,8 +300,6 @@ Deno.test("Mock call for schedule", async (t) => { scheduled_response.trigger.schedule.timezone, ); } - - mf.reset(); }, ); }); diff --git a/src/typed-method-types/workflows/triggers/tests/shortcut_test.ts b/src/typed-method-types/workflows/triggers/tests/shortcut_test.ts index cce9b26..0e6bb8a 100644 --- a/src/typed-method-types/workflows/triggers/tests/shortcut_test.ts +++ b/src/typed-method-types/workflows/triggers/tests/shortcut_test.ts @@ -1,7 +1,7 @@ import { assertEquals, assertObjectMatch } from "@std/assert"; import type { ShortcutTrigger } from "../shortcut.ts"; import { TriggerTypes } from "../mod.ts"; -import * as mf from "https://deno.land/x/mock_fetch@0.3.0/mod.ts"; +import { stubFetch } from "../../../../../testing/http.ts"; import { SlackAPI } from "../../../../mod.ts"; import { shortcut_response } from "./fixtures/sample_responses.ts"; import type { @@ -29,8 +29,6 @@ Deno.test("Shortcut triggers can set the type using the TriggerTypes object", () }); Deno.test("Mock call for shortcut", async (t) => { - mf.install(); // mock out calls to `fetch` - await t.step("instantiated with default API URL", async (t) => { const client = SlackAPI("test-token"); @@ -43,13 +41,16 @@ Deno.test("Mock call for shortcut", async (t) => { await t.step( "should return successful response JSON on create", async () => { - mf.mock("POST@/api/workflows.triggers.create", (req: Request) => { - assertEquals( - req.url, - "https://slack.com/api/workflows.triggers.create", - ); - return new Response(JSON.stringify(shortcut_response)); - }); + using _fetchStub = stubFetch( + (req) => { + assertEquals(req.method, "POST"); + assertEquals( + req.url, + "https://slack.com/api/workflows.triggers.create", + ); + }, + new Response(JSON.stringify(shortcut_response)), + ); const res = await client.workflows.triggers.create({ name: "TEST", @@ -69,7 +70,6 @@ Deno.test("Mock call for shortcut", async (t) => { shortcut_response.trigger.shortcut_url, ); } - mf.reset(); }, ); }); @@ -77,13 +77,16 @@ Deno.test("Mock call for shortcut", async (t) => { await t.step( "should return successful response JSON on update", async () => { - mf.mock("POST@/api/workflows.triggers.update", (req: Request) => { - assertEquals( - req.url, - "https://slack.com/api/workflows.triggers.update", - ); - return new Response(JSON.stringify(shortcut_response)); - }); + using _fetchStub = stubFetch( + (req) => { + assertEquals(req.method, "POST"); + assertEquals( + req.url, + "https://slack.com/api/workflows.triggers.update", + ); + }, + new Response(JSON.stringify(shortcut_response)), + ); const res = await client.workflows.triggers.update({ name: "TEST", @@ -104,8 +107,6 @@ Deno.test("Mock call for shortcut", async (t) => { shortcut_response.trigger.shortcut_url, ); } - - mf.reset(); }, ); }); diff --git a/src/typed-method-types/workflows/triggers/tests/trigger-workflow_test.ts b/src/typed-method-types/workflows/triggers/tests/trigger-workflow_test.ts index 6ea33de..3804f09 100644 --- a/src/typed-method-types/workflows/triggers/tests/trigger-workflow_test.ts +++ b/src/typed-method-types/workflows/triggers/tests/trigger-workflow_test.ts @@ -9,30 +9,30 @@ import type { OptionalInputWorkflow, RequiredInputWorkflow, } from "./fixtures/workflows.ts"; -import * as mf from "https://deno.land/x/mock_fetch@0.3.0/mod.ts"; +import { stubFetch } from "../../../../../testing/http.ts"; import { shortcut_response } from "./fixtures/sample_responses.ts"; import type { Trigger } from "../../../../types.ts"; +import type { Stub } from "@std/testing/mock"; Deno.test("Trigger inputs are powered by generics", async (t) => { const client = SlackAPI(""); - mf.install(); - mf.mock("POST@/api/workflows.triggers.create", (req: Request) => { - assertEquals( - req.url, - "https://slack.com/api/workflows.triggers.create", - ); - return new Response(JSON.stringify(shortcut_response)); - }); - mf.mock("POST@/api/workflows.triggers.update", (req: Request) => { - assertEquals( - req.url, - "https://slack.com/api/workflows.triggers.update", + + function createStub(): Stub { + return stubFetch( + (req) => { + assertEquals(req.method, "POST"); + assertEquals( + req.url, + "https://slack.com/api/workflows.triggers.create", + ); + }, + new Response(JSON.stringify(shortcut_response)), ); - return new Response(JSON.stringify(shortcut_response)); - }); + } await t.step("no generics required", async (t) => { await t.step("catches invalid workflow strings", async () => { + using _createStub = createStub(); const _trigger: Trigger = { name: "TEST", type: "shortcut", @@ -49,6 +49,7 @@ Deno.test("Trigger inputs are powered by generics", async (t) => { }); await t.step("allows no inputs to be set", async () => { + using _createStub = createStub(); const _: Trigger = { name: "TEST", type: "shortcut", @@ -63,6 +64,7 @@ Deno.test("Trigger inputs are powered by generics", async (t) => { }); await t.step("allows empty inputs to be set", async () => { + using _createStub = createStub(); const _: Trigger = { name: "TEST", type: "shortcut", @@ -80,6 +82,7 @@ Deno.test("Trigger inputs are powered by generics", async (t) => { }); await t.step("allows populated inputs to be set", async () => { + using _createStub = createStub(); const _: Trigger = { name: "TEST", type: "shortcut", @@ -97,6 +100,7 @@ Deno.test("Trigger inputs are powered by generics", async (t) => { }); await t.step("catches error if inputs are wrong", async () => { + using _createStub = createStub(); const _: Trigger = { name: "TEST", type: "shortcut", @@ -116,6 +120,7 @@ Deno.test("Trigger inputs are powered by generics", async (t) => { }); await t.step("with trigger update", async () => { + using _createStub = createStub(); const _: Trigger = { name: "TEST", type: "shortcut", @@ -135,6 +140,7 @@ Deno.test("Trigger inputs are powered by generics", async (t) => { await t.step("with workflow generic", async (t) => { await t.step("catches invalid workflow string", async () => { + using _createStub = createStub(); const _: Trigger = { name: "TEST", type: "shortcut", @@ -153,6 +159,7 @@ Deno.test("Trigger inputs are powered by generics", async (t) => { await t.step("handles workflow with no defined input params", async (t) => { await t.step("allows no inputs to be set", async () => { + using _createStub = createStub(); const _: Trigger = { name: "TEST", type: "shortcut", @@ -168,6 +175,7 @@ Deno.test("Trigger inputs are powered by generics", async (t) => { }); await t.step("allows empty inputs to be set", async () => { + using _createStub = createStub(); const _: Trigger = { name: "TEST", type: "shortcut", @@ -185,6 +193,7 @@ Deno.test("Trigger inputs are powered by generics", async (t) => { }); await t.step("catches populated inputs being passed", async () => { + using _createStub = createStub(); const _: Trigger = { name: "TEST", type: "shortcut", @@ -206,6 +215,7 @@ Deno.test("Trigger inputs are powered by generics", async (t) => { await t.step("handles workflow with no defined properties", async (t) => { await t.step("allows no inputs to be set", async () => { + using _createStub = createStub(); const _: Trigger = { name: "TEST", type: "shortcut", @@ -221,6 +231,7 @@ Deno.test("Trigger inputs are powered by generics", async (t) => { }); await t.step("allows empty inputs to be set", async () => { + using _createStub = createStub(); const _: Trigger = { name: "TEST", type: "shortcut", @@ -238,6 +249,7 @@ Deno.test("Trigger inputs are powered by generics", async (t) => { }); await t.step("catches populated inputs being passed", async () => { + using _createStub = createStub(); const _: Trigger = { name: "TEST", type: "shortcut", @@ -259,6 +271,7 @@ Deno.test("Trigger inputs are powered by generics", async (t) => { await t.step("handles workflow with only optional inputs", async (t) => { await t.step("allows no inputs to be set", async () => { + using _createStub = createStub(); const _: Trigger = { name: "TEST", type: "shortcut", @@ -274,6 +287,7 @@ Deno.test("Trigger inputs are powered by generics", async (t) => { }); await t.step("allows empty inputs to be set", async () => { + using _createStub = createStub(); const _: Trigger = { name: "TEST", type: "shortcut", @@ -291,6 +305,7 @@ Deno.test("Trigger inputs are powered by generics", async (t) => { }); await t.step("allows optional inputs to be set", async () => { + using _createStub = createStub(); const _: Trigger = { name: "TEST", type: "shortcut", @@ -308,6 +323,7 @@ Deno.test("Trigger inputs are powered by generics", async (t) => { }); await t.step("catches error if inputs are wrong", async () => { + using _createStub = createStub(); const _: Trigger = { name: "TEST", type: "shortcut", @@ -329,6 +345,7 @@ Deno.test("Trigger inputs are powered by generics", async (t) => { await t.step("handles workflow with only required inputs", async (t) => { await t.step("requires inputs to be set", async () => { + using _createStub = createStub(); const _: Trigger = { name: "TEST", type: "shortcut", @@ -346,6 +363,7 @@ Deno.test("Trigger inputs are powered by generics", async (t) => { }); await t.step("catches empty inputs being set", async () => { + using _createStub = createStub(); const _: Trigger = { name: "TEST", type: "shortcut", @@ -365,6 +383,7 @@ Deno.test("Trigger inputs are powered by generics", async (t) => { }); await t.step("allows required inputs to be set", async () => { + using _createStub = createStub(); const _: Trigger = { name: "TEST", type: "shortcut", @@ -384,6 +403,7 @@ Deno.test("Trigger inputs are powered by generics", async (t) => { await t.step("handles workflow with customizable inputs", async (t) => { await t.step("allows customizable input to be set", async () => { + using _createStub = createStub(); const _: Trigger = { name: "TEST", type: "shortcut", @@ -400,6 +420,7 @@ Deno.test("Trigger inputs are powered by generics", async (t) => { }); await t.step("allows optional customizable input to be set", async () => { + using _createStub = createStub(); const _: Trigger = { name: "TEST", type: "shortcut", @@ -418,6 +439,7 @@ Deno.test("Trigger inputs are powered by generics", async (t) => { await t.step( "allows empty optional customizable inputs to be set", async () => { + using _createStub = createStub(); const _: Trigger = { name: "TEST", type: "shortcut", @@ -439,6 +461,7 @@ Deno.test("Trigger inputs are powered by generics", async (t) => { await t.step( "catches if customizable and value are set", async () => { + using _createStub = createStub(); const _: Trigger = { name: "TEST", type: "shortcut", @@ -464,6 +487,7 @@ Deno.test("Trigger inputs are powered by generics", async (t) => { await t.step( "catches if customizable is not a boolean", async () => { + using _createStub = createStub(); const _: Trigger = { name: "TEST", type: "shortcut", @@ -489,6 +513,7 @@ Deno.test("Trigger inputs are powered by generics", async (t) => { await t.step( "catches if customizable is false", async () => { + using _createStub = createStub(); const _: Trigger = { name: "TEST", type: "shortcut", @@ -514,6 +539,7 @@ Deno.test("Trigger inputs are powered by generics", async (t) => { await t.step("handles workflow with a mix of inputs", async (t) => { await t.step("requires inputs to be set", async () => { + using _createStub = createStub(); const _: Trigger = { name: "TEST", type: "shortcut", @@ -530,6 +556,7 @@ Deno.test("Trigger inputs are powered by generics", async (t) => { }); await t.step("catches empty inputs being set", async () => { + using _createStub = createStub(); const _: Trigger = { name: "TEST", type: "shortcut", @@ -549,6 +576,7 @@ Deno.test("Trigger inputs are powered by generics", async (t) => { }); await t.step("allows required inputs to be set", async () => { + using _createStub = createStub(); const _: Trigger = { name: "TEST", type: "shortcut", @@ -568,6 +596,7 @@ Deno.test("Trigger inputs are powered by generics", async (t) => { await t.step( "allows required and optional inputs to be set", async () => { + using _createStub = createStub(); const _: Trigger = { name: "TEST", type: "shortcut", @@ -592,6 +621,17 @@ Deno.test("Trigger inputs are powered by generics", async (t) => { }); await t.step("with trigger update", async () => { + using _updateStub = stubFetch( + (req) => { + assertEquals(req.method, "POST"); + assertEquals( + req.url, + "https://slack.com/api/workflows.triggers.update", + ); + }, + new Response(JSON.stringify(shortcut_response)), + ); + await client.workflows.triggers.update({ name: "TEST", type: "shortcut", @@ -601,5 +641,4 @@ Deno.test("Trigger inputs are powered by generics", async (t) => { assert(true); }); }); - mf.reset(); }); diff --git a/src/typed-method-types/workflows/triggers/tests/webhook_test.ts b/src/typed-method-types/workflows/triggers/tests/webhook_test.ts index ad3d69f..91815f1 100644 --- a/src/typed-method-types/workflows/triggers/tests/webhook_test.ts +++ b/src/typed-method-types/workflows/triggers/tests/webhook_test.ts @@ -1,8 +1,8 @@ -import { mf } from "../../../../dev_deps.ts"; import { assertEquals, assertObjectMatch } from "@std/assert"; import type { WebhookTrigger } from "../webhook.ts"; import { TriggerTypes } from "../mod.ts"; import { SlackAPI } from "../../../../mod.ts"; +import { stubFetch } from "../../../../../testing/http.ts"; import { webhook_response } from "./fixtures/sample_responses.ts"; Deno.test("Webhook triggers can set the type using the string", () => { @@ -47,8 +47,6 @@ Deno.test("Webhook triggers support an optional filter object", () => { }); Deno.test("Mock call for webhook", async (t) => { - mf.install(); // mock out calls to `fetch` - await t.step("instantiated with default API URL", async (t) => { const client = SlackAPI("test-token"); @@ -60,15 +58,15 @@ Deno.test("Mock call for webhook", async (t) => { await t.step( "should return successful response JSON on create", async () => { - mf.mock( - "POST@/api/workflows.triggers.create", - (req: Request) => { + using _fetchStub = stubFetch( + (req) => { + assertEquals(req.method, "POST"); assertEquals( req.url, "https://slack.com/api/workflows.triggers.create", ); - return new Response(JSON.stringify(webhook_response)); }, + new Response(JSON.stringify(webhook_response)), ); const res = await client.workflows.triggers.create({ @@ -89,7 +87,6 @@ Deno.test("Mock call for webhook", async (t) => { webhook_response.trigger.webhook_url, ); } - mf.reset(); }, ); }); @@ -97,15 +94,15 @@ Deno.test("Mock call for webhook", async (t) => { await t.step( "should return successful response JSON on update", async () => { - mf.mock( - "POST@/api/workflows.triggers.update", - (req: Request) => { + using _fetchStub = stubFetch( + (req) => { + assertEquals(req.method, "POST"); assertEquals( req.url, "https://slack.com/api/workflows.triggers.update", ); - return new Response(JSON.stringify(webhook_response)); }, + new Response(JSON.stringify(webhook_response)), ); const res = await client.workflows.triggers.update({ @@ -127,7 +124,6 @@ Deno.test("Mock call for webhook", async (t) => { webhook_response.trigger.webhook_url, ); } - mf.reset(); }, ); }); diff --git a/testing/http.ts b/testing/http.ts new file mode 100644 index 0000000..93ae4c0 --- /dev/null +++ b/testing/http.ts @@ -0,0 +1,41 @@ +import { type Stub, stub } from "@std/testing/mock"; + +/** + * Creates a simple fetch stub that replaces the global fetch implementation with a mock. + * + * @param matches - A function that validates the request object + * @param response - The Response object to return from the stubbed fetch call + * @returns A Stub object that can be used to restore the original fetch implementation + * + * @example With 'using' statement + * ```typescript + * { + * using fetchStub = stubFetch( + * (req) => { + * assertEquals(req.url, "https://api.example.com/data"); + * assertEquals(req.method, "POST"); + * }, + * new Response(JSON.stringify({ result: "success" })) + * ); + * + * // Test code - stub automatically cleaned up at end of block scope + * } + * ``` + */ +export function stubFetch( + matches: (req: Request) => void | Promise, + response: Response, +): Stub { + return stub( + globalThis, + "fetch", + async (url: string | URL | Request, options?: RequestInit) => { + const request = url instanceof Request ? url : new Request(url, options); + const matchesResult = matches(request.clone()); + if (matchesResult instanceof Promise) { + await matchesResult; + } + return Promise.resolve(response.clone()); + }, + ); +} diff --git a/testing/http_test.ts b/testing/http_test.ts new file mode 100644 index 0000000..de1a85a --- /dev/null +++ b/testing/http_test.ts @@ -0,0 +1,84 @@ +import { assertEquals, assertNotEquals } from "@std/assert"; +import { describe, it } from "@std/testing/bdd"; +import { assertSpyCalls, spy } from "@std/testing/mock"; + +import { stubFetch } from "./http.ts"; + +describe("stubFetch", () => { + const originalFetch = globalThis.fetch; + + it("should replace global fetch with a stub", async () => { + const matchesSpy = spy(() => {}); + + using _stub = stubFetch( + matchesSpy, + new Response(JSON.stringify({ success: true }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ); + + assertNotEquals(globalThis.fetch, originalFetch); + + const response = await fetch("https://example.com/api"); + + assertSpyCalls(matchesSpy, 1); + assertEquals(await response.json(), { success: true }); + assertEquals(response.status, 200); + assertEquals(response.headers.get("Content-Type"), "application/json"); + }); + + it("should handle Request objects directly", async () => { + const matchesSpy = spy((req: Request) => { + assertEquals(req.method, "POST"); + assertEquals(req.url, "https://example.com/data"); + }); + + using _stub = stubFetch(matchesSpy, new Response("Hello world")); + + const request = new Request("https://example.com/data", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ key: "value" }), + }); + + await fetch(request); + + assertSpyCalls(matchesSpy, 1); + }); + + it("should handle async matchers", async () => { + let asyncOperationCompleted = false; + + const asyncMatcher = async (req: Request) => { + await new Promise((resolve) => setTimeout(resolve, 10)); + asyncOperationCompleted = true; + assertEquals(req.url.includes("example.com"), true); + }; + + using _stub = stubFetch(asyncMatcher, new Response("Success")); + + await fetch("https://example.com/async"); + + assertEquals(asyncOperationCompleted, true); + }); + + it("should clone the response for each call", async () => { + const testBody = "test"; + using _stub = stubFetch(() => {}, new Response(testBody)); + + const response1 = await fetch("https://example.com/first"); + const response2 = await fetch("https://example.com/second"); + + assertEquals(await response1.text(), testBody); + assertEquals(await response2.text(), testBody); + }); + + it("should restore original fetch when stub is released", () => { + { + using _stub = stubFetch(() => {}, new Response("Test")); + assertEquals(globalThis.fetch !== originalFetch, true); + } + assertEquals(globalThis.fetch, originalFetch); + }); +});