diff --git a/package-lock.json b/package-lock.json index 22d8478..cc92ea6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "dependencies": { "@18f/us-federal-holidays": "^4.0.0", "@slack/bolt": "^4.1.0", - "axios": "^1.7.7", "cfenv": "^1.2.3", "cheerio": "^1.0.0-rc.12", "csv-parse": "^5.0.4", @@ -1707,6 +1706,7 @@ "version": "1.7.7", "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", diff --git a/package.json b/package.json index 279255d..1a61d74 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,6 @@ "dependencies": { "@18f/us-federal-holidays": "^4.0.0", "@slack/bolt": "^4.1.0", - "axios": "^1.7.7", "cfenv": "^1.2.3", "cheerio": "^1.0.0-rc.12", "csv-parse": "^5.0.4", diff --git a/src/scripts/define.js b/src/scripts/define.js index cc07b2f..225a582 100644 --- a/src/scripts/define.js +++ b/src/scripts/define.js @@ -12,7 +12,6 @@ // @bot define contracting officer const { directMention } = require("@slack/bolt"); -const axios = require("axios"); const he = require("he"); const yaml = require("js-yaml"); const { @@ -114,9 +113,9 @@ module.exports = (app) => { // Cache the glossary for 1 minute const glossary = await cache("glossary get", 60, async () => { - const { data } = await axios.get( + const data = await fetch( "https://raw.githubusercontent.com/18F/the-glossary/main/glossary.yml", - ); + ).then((r) => r.text()); return yaml.load(data, { json: true }).entries; }); diff --git a/src/scripts/define.test.js b/src/scripts/define.test.js index d1f7435..243a3da 100644 --- a/src/scripts/define.test.js +++ b/src/scripts/define.test.js @@ -1,5 +1,4 @@ const { - axios, getApp, utils: { cache }, } = require("../utils/test"); @@ -228,8 +227,11 @@ describe("glossary", () => { const fetcher = cache.mock.calls[0].pop(); - axios.get.mockResolvedValue({ - data: ` + fetch.mockResolvedValue({ + // The data being pulled across the network is expected to be YAML, so + // we need to be sure we format it correctly here. Hence the funky-looking + // string. + text: async () => ` entries: term 1: ATO term 2: diff --git a/src/scripts/dot-gov.js b/src/scripts/dot-gov.js index 624425e..47f82be 100644 --- a/src/scripts/dot-gov.js +++ b/src/scripts/dot-gov.js @@ -35,7 +35,6 @@ */ const { directMention } = require("@slack/bolt"); -const axios = require("axios"); const https = require("https"); /* eslint-disable import/no-unresolved */ @@ -91,10 +90,10 @@ const DOMAIN_TYPES = { /** Current list of fields available in the CSV file. */ const DATA_FIELDS = { - NAME: "Domain Name", - TYPE: "Domain Type", + NAME: "Domain name", + TYPE: "Domain type", AGENCY: "Agency", - ORG: "Organization", + ORG: "Organization name", CITY: "City", STATE: "State", }; @@ -186,15 +185,13 @@ const selectDomainsAtRandom = (domainsArr, numToSelect) => { * Returns a human-readable response status. */ const checkDomainStatus = async (domainObj, timeout = 2000) => - axios - .head(`https://${domainObj[DATA_FIELDS.NAME]}`, { - timeout, - httpsAgent: new https.Agent({ - rejectUnauthorized: false, - }), - }) + fetch(`https://${domainObj[DATA_FIELDS.NAME]}`, { + method: "HEAD", + agent: new https.Agent({ rejectUnauthorized: false }), + signal: AbortSignal.timeout(timeout), + }) .then((response) => { - if (response && response.status) { + if (response?.ok && response.status) { return response.statusText; } return "Unknown Status"; @@ -203,7 +200,7 @@ const checkDomainStatus = async (domainObj, timeout = 2000) => if (error.response) { return error.response.statusText; } - if (error.code === "CERT_HAS_EXPIRED") { + if (error.cause?.code === "CERT_HAS_EXPIRED") { return "Cert Expired"; } return "Unknown Status"; @@ -240,14 +237,9 @@ module.exports = (app) => { module.exports.dotGov = async ({ entity, searchTerm, say, thread }) => { // fetch the domain list const domains = await cache("dotgov domains", 1440, async () => - axios - .get(CISA_DOTGOV_DATA_URL) - .then((response) => { - if (response && response.data) { - return parse(response.data, { columns: true }); - } - return []; - }) + fetch(CISA_DOTGOV_DATA_URL) + .then((response) => response.text()) + .then((data) => parse(data, { columns: true })) .catch(() => []), ); diff --git a/src/scripts/dot-gov.test.js b/src/scripts/dot-gov.test.js index 1d5d9c7..01d0f42 100644 --- a/src/scripts/dot-gov.test.js +++ b/src/scripts/dot-gov.test.js @@ -1,5 +1,4 @@ const { - axios, getApp, utils: { cache }, } = require("../utils/test"); @@ -12,81 +11,81 @@ describe("dot-gov domains", () => { { Agency: "Non-Federal Agency", City: "Albany", - "Domain Name": "ALBANYCA.GOV", - "Domain Type": "City", - Organization: "City of Albany", + "Domain name": "ALBANYCA.GOV", + "Domain type": "City", + "Organization name": "City of Albany", "Security Contact Email": "(blank)", State: "CA", }, { Agency: "Non-Federal Agency", City: "Belle Plaine", - "Domain Name": "BELLEPLAINEIOWA.GOV", - "Domain Type": "City", - Organization: "City of Belle Plaine", + "Domain name": "BELLEPLAINEIOWA.GOV", + "Domain type": "City", + "Organization name": "City of Belle Plaine", "Security Contact Email": "(blank)", State: "IA", }, { Agency: "U.S. Department of Agriculture", City: "Washington", - "Domain Name": "RURAL.GOV", - "Domain Type": "Federal - Executive", - Organization: "Rural Development", + "Domain name": "RURAL.GOV", + "Domain type": "Federal - Executive", + "Organization name": "Rural Development", "Security Contact Email": "cyber.(blank)", State: "DC", }, { Agency: "The Supreme Court", City: "Washington", - "Domain Name": "SUPREMECOURTUS.GOV", - "Domain Type": "Federal - Judicial", - Organization: "Supreme Court of the United Statest", + "Domain name": "SUPREMECOURTUS.GOV", + "Domain type": "Federal - Judicial", + "Organization name": "Supreme Court of the United Statest", "Security Contact Email": "(blank)", State: "DC", }, { Agency: "Government Publishing Office", City: "Washington", - "Domain Name": "USCODE.GOV", - "Domain Type": "Federal - Legislative", - Organization: "United States Government Publishing Office", + "Domain name": "USCODE.GOV", + "Domain type": "Federal - Legislative", + "Organization name": "United States Government Publishing Office", "Security Contact Email": "(blank)", State: "DC", }, { Agency: "Non-Federal Agency", City: "Arizona City", - "Domain Name": "ACSD-AZ.GOV", - "Domain Type": "Independent Intrastate", - Organization: "Arizona City Sanitary District ", + "Domain name": "ACSD-AZ.GOV", + "Domain type": "Independent Intrastate", + "Organization name": "Arizona City Sanitary District ", "Security Contact Email": "(blank)", State: "AZ", }, { Agency: "Non-Federal Agency", City: "Mechanicsburg", - "Domain Name": "EMSCOMPACT.GOV", - "Domain Type": "Interstate", - Organization: "Interstate Commission for EMS Personnel Practice", + "Domain name": "EMSCOMPACT.GOV", + "Domain type": "Interstate", + "Organization name": "Interstate Commission for EMS Personnel Practice", "Security Contact Email": "(blank)", State: "PA", }, { Agency: "Non-Federal Agency", City: "St Croix", - "Domain Name": "VIVOTE.GOV", - "Domain Type": "State", - Organization: "Election System of the Virgin Islands", + "Domain name": "VIVOTE.GOV", + "Domain type": "State", + "Organization name": "Election System of the Virgin Islands", "Security Contact Email": "(blank)", State: "VI", }, { Agency: "Non-Federal Agency", City: "Ada", - "Domain Name": "CHICKASAW-NSN.GOV", - "Domain Type": "Tribal", - Organization: "the Chickasaw Nation", + "Domain name": "CHICKASAW-NSN.GOV", + "Domain type": "Tribal", + "Organization name": "the Chickasaw Nation", "Security Contact Email": "(blank)", State: "OK", }, @@ -99,10 +98,16 @@ describe("dot-gov domains", () => { username: ".Gov", }; + const fetchResponse = { + json: jest.fn(), + text: jest.fn(), + }; + beforeAll(() => {}); beforeEach(() => { jest.resetAllMocks(); + fetch.mockResolvedValue(fetchResponse); }); afterAll(() => {}); @@ -138,38 +143,39 @@ describe("dot-gov domains", () => { }); describe("gets domains from github if the cache is expired or whatever", () => { - let fetch; + let fetcher; + beforeEach(async () => { cache.mockResolvedValue([]); await handler(message); - fetch = cache.mock.calls[0][2]; + fetcher = cache.mock.calls[0][2]; }); it("if the API throws an error", async () => { - axios.get.mockRejectedValue("error"); - const out = await fetch(); + fetch.mockRejectedValue("error"); + const out = await fetcher(); expect(out).toEqual([]); }); it("if the API returns a malformed object", async () => { - axios.get.mockResolvedValue({ data: { data_is_missing: [] } }); - const out = await fetch(); + fetchResponse.json.mockResolvedValue({ data: { data_is_missing: [] } }); + const out = await fetcher(); expect(out).toEqual([]); }); it("if the API doesn't return any domains", async () => { - axios.get.mockResolvedValue({ data: { data: [] } }); - const out = await fetch(); + fetchResponse.json.mockResolvedValue({ data: { data: [] } }); + const out = await fetcher(); expect(out).toEqual([]); }); it("if the API does return some domains", async () => { - axios.get.mockResolvedValue({ - data: [ - "Domain Name,Domain Type,Agency,Organization,City,State,Security Contact Email", + fetchResponse.text.mockResolvedValue( + [ + "Domain name,Domain type,Agency,Organization name,City,State,Security Contact Email", "ALBANYCA.GOV,City,Non-Federal Agency,City of Albany,Albany,CA,(blank)", "BELLEPLAINEIOWA.GOV,City,Non-Federal Agency,City of Belle Plaine,Belle Plaine,IA,(blank)", "RURAL.GOV,Federal - Executive,U.S. Department of Agriculture,Rural Development,Washington,DC,cyber.(blank)", @@ -180,18 +186,14 @@ describe("dot-gov domains", () => { "VIVOTE.GOV,State,Non-Federal Agency,Election System of the Virgin Islands,St Croix,VI,(blank)", "CHICKASAW-NSN.GOV,Tribal,Non-Federal Agency,the Chickasaw Nation,Ada,OK,(blank)", ].join("\n"), - }); - const out = await fetch(); + ); + const out = await fetcher(); expect(out).toEqual(mockCache); }); }); describe("responds with a domain", () => { - beforeEach(() => { - axios.head.mockImplementation(() => Promise.resolve("Ok")); - }); - it("unless there aren't any domains", async () => { cache.mockResolvedValue([]); await handler(message); @@ -216,7 +218,7 @@ describe("dot-gov domains", () => { }); it("even if checking the status of the domain fails", async () => { - axios.head.mockImplementation(() => Promise.reject(new Error())); + fetch.mockRejectedValue(new Error()); cache.mockResolvedValue(mockCache); await handler(message); @@ -281,10 +283,6 @@ describe("dot-gov domains", () => { const message = { message: { thread_ts: "thread id" }, say: jest.fn() }; describe("responds with results", () => { - beforeEach(() => { - axios.head.mockImplementation(() => Promise.resolve("Ok")); - }); - it("if there aren't any results", async () => { cache.mockResolvedValue(mockCache); message.context = { @@ -341,9 +339,6 @@ describe("dot-gov domains", () => { }); }); describe("filters correctly by entity", () => { - beforeEach(() => { - axios.head.mockImplementation(() => Promise.resolve("Ok")); - }); it("if entity is executive", async () => { cache.mockResolvedValue(mockCache); message.context = { diff --git a/src/scripts/handbook.js b/src/scripts/handbook.js index 24109b6..e6958b0 100644 --- a/src/scripts/handbook.js +++ b/src/scripts/handbook.js @@ -1,4 +1,3 @@ -const axios = require("axios"); const { helpMessage, slack: { postEphemeralResponse }, @@ -42,19 +41,21 @@ module.exports = (app) => { const { context: { - matches: [, , search], + matches: [, search], }, event: { thread_ts: thread, ts }, say, } = msg; + console.log(msg.context.matches); + const searchString = search - .replace(/[”“]/g, '"') // replace smart quotes - .replace(/[’‘]/g, "'"); // more smart quotes + ?.replace(/[”“]/g, '"') // replace smart quotes + ?.replace(/[’‘]/g, "'"); // more smart quotes const url = `${baseUrl}${encodeURIComponent(searchString)}`; try { - const { data } = await axios.get(url); + const data = await fetch(url).then((r) => r.json()); const results = data.results.slice(0, 3); if (results.length === 0) { diff --git a/src/scripts/handbook.test.js b/src/scripts/handbook.test.js index dfbef68..45c2b64 100644 --- a/src/scripts/handbook.test.js +++ b/src/scripts/handbook.test.js @@ -1,5 +1,4 @@ const { - axios, getApp, utils: { slack: { postEphemeralResponse }, @@ -10,8 +9,13 @@ const handbook = require("./handbook"); describe("TTS Handbook search", () => { const app = getApp(); + const fetchResponse = { + json: jest.fn(), + }; + beforeEach(() => { jest.resetAllMocks(); + fetch.mockResolvedValue(fetchResponse); }); it("subscribes to the right message", () => { @@ -24,7 +28,7 @@ describe("TTS Handbook search", () => { describe("it handles bot triggers", () => { const message = { - context: { matches: [null, null, "search string"] }, + context: { matches: [null, "search string"] }, event: { thread_ts: undefined, ts: 150 }, say: jest.fn(), }; @@ -34,11 +38,11 @@ describe("TTS Handbook search", () => { handbook(app); handler = app.getHandler(); - message.context.matches[2] = "search string"; + message.context.matches[1] = "search string"; }); it("gracefully responds if there is an error", async () => { - axios.get.mockRejectedValue(); + fetch.mockRejectedValue(); await handler(message); expect(postEphemeralResponse).toHaveBeenCalledWith(message, { @@ -49,10 +53,10 @@ describe("TTS Handbook search", () => { }); it("converts characters accordingly before putting them in the search URL", async () => { - message.context.matches[2] = "”some ’search‘ goes here“"; + message.context.matches[1] = "”some ’search‘ goes here“"; await handler(message); - expect(axios.get).toHaveBeenCalledWith( + expect(fetch).toHaveBeenCalledWith( "https://search.usa.gov/search/?utf8=no&affiliate=tts-handbook&format=json&query=%22some%20'search'%20goes%20here%22", ); }); @@ -64,7 +68,7 @@ describe("TTS Handbook search", () => { }); it("and there are no search results", async () => { - axios.get.mockResolvedValue({ data: { results: [] } }); + fetchResponse.json.mockResolvedValue({ results: [] }); await handler(message); expect(message.say).toHaveBeenCalledWith({ @@ -76,16 +80,14 @@ describe("TTS Handbook search", () => { }); it("and there are some results", async () => { - axios.get.mockResolvedValue({ - data: { - results: [ - { body: "this is result #1", link: "link1", title: "result 1" }, - { body: "this is result #2", link: "link2", title: "result 2" }, - { body: "this is result #3", link: "link3", title: "result 3" }, - { body: "this is result #4", link: "link4", title: "result 4" }, - { body: "this is result #5", link: "link5", title: "result 5" }, - ], - }, + fetchResponse.json.mockResolvedValue({ + results: [ + { body: "this is result #1", link: "link1", title: "result 1" }, + { body: "this is result #2", link: "link2", title: "result 2" }, + { body: "this is result #3", link: "link3", title: "result 3" }, + { body: "this is result #4", link: "link4", title: "result 4" }, + { body: "this is result #5", link: "link5", title: "result 5" }, + ], }); await handler(message); @@ -148,7 +150,7 @@ describe("TTS Handbook search", () => { }); it("and there are no search results", async () => { - axios.get.mockResolvedValue({ data: { results: [] } }); + fetchResponse.json.mockResolvedValue({ results: [] }); await handler(message); expect(message.say).toHaveBeenCalledWith({ @@ -160,16 +162,14 @@ describe("TTS Handbook search", () => { }); it("and there are some results", async () => { - axios.get.mockResolvedValue({ - data: { - results: [ - { body: "this is result #1", link: "link1", title: "result 1" }, - { body: "this is result #2", link: "link2", title: "result 2" }, - { body: "this is result #3", link: "link3", title: "result 3" }, - { body: "this is result #4", link: "link4", title: "result 4" }, - { body: "this is result #5", link: "link5", title: "result 5" }, - ], - }, + fetchResponse.json.mockResolvedValue({ + results: [ + { body: "this is result #1", link: "link1", title: "result 1" }, + { body: "this is result #2", link: "link2", title: "result 2" }, + { body: "this is result #3", link: "link3", title: "result 3" }, + { body: "this is result #4", link: "link4", title: "result 4" }, + { body: "this is result #5", link: "link5", title: "result 5" }, + ], }); await handler(message); diff --git a/src/scripts/opm_status.js b/src/scripts/opm_status.js index cb74bde..c84981a 100644 --- a/src/scripts/opm_status.js +++ b/src/scripts/opm_status.js @@ -11,7 +11,6 @@ // lauraggit const { directMention } = require("@slack/bolt"); -const axios = require("axios"); const { helpMessage, stats: { incrementStats }, @@ -38,12 +37,10 @@ module.exports = (robot) => { incrementStats("OPM status"); try { - const { data, status } = await axios.get( + const data = await fetch( "https://www.opm.gov/json/operatingstatus.json", - ); - if (status !== 200) { - throw new Error("Invalid status"); - } + ).then((r) => r.json()); + say({ icon_emoji: icons[data.Icon], text: `${data.StatusSummary} for ${data.AppliesTo}. (<${data.Url}|Read more>)`, diff --git a/src/scripts/opm_status.test.js b/src/scripts/opm_status.test.js index d3ee160..2117025 100644 --- a/src/scripts/opm_status.test.js +++ b/src/scripts/opm_status.test.js @@ -1,4 +1,4 @@ -const { axios, getApp } = require("../utils/test"); +const { getApp } = require("../utils/test"); const opm = require("./opm_status"); describe("OPM status for DC-area offices", () => { @@ -9,8 +9,11 @@ describe("OPM status for DC-area offices", () => { say: jest.fn(), }; + const json = jest.fn(); + beforeEach(() => { jest.resetAllMocks(); + fetch.mockResolvedValue({ json }); }); it("registers a listener", () => { @@ -27,23 +30,7 @@ describe("OPM status for DC-area offices", () => { opm(app); const handler = app.getHandler(); - axios.get.mockRejectedValue("error"); - - await handler(message); - - expect(message.say).toHaveBeenCalledWith({ - text: "I didn't get a response from OPM, so... what does say?", - thread_ts: "thread id", - unfurl_links: false, - unfurl_media: false, - }); - }); - - it("rejects on a non-200 status code", async () => { - opm(app); - const handler = app.getHandler(); - - axios.get.mockResolvedValue({ data: null, status: 400 }); + fetch.mockRejectedValue("error"); await handler(message); @@ -59,14 +46,11 @@ describe("OPM status for DC-area offices", () => { opm(app); const handler = app.getHandler(); - axios.get.mockResolvedValue({ - data: { - AppliesTo: "applies to", - Icon: "Open", - StatusSummary: "summary", - Url: "http://url", - }, - status: 200, + json.mockResolvedValue({ + AppliesTo: "applies to", + Icon: "Open", + StatusSummary: "summary", + Url: "http://url", }); await handler(message); diff --git a/src/scripts/random-responses.js b/src/scripts/random-responses.js index e76d8ca..3dee16f 100644 --- a/src/scripts/random-responses.js +++ b/src/scripts/random-responses.js @@ -1,4 +1,3 @@ -const axios = require("axios"); const fs = require("fs"); const plural = require("plural"); const { @@ -31,10 +30,7 @@ const getResponses = async (config, searchTerm = false, negate = false) => { responses = await cache( `random response from ${config.responseUrl}`, 5, - async () => { - const { data } = await axios.get(config.responseUrl); - return data; - }, + async () => fetch(config.responseUrl).then((r) => r.json()), ); } diff --git a/src/scripts/random-responses.test.js b/src/scripts/random-responses.test.js index e911e89..9e47389 100644 --- a/src/scripts/random-responses.test.js +++ b/src/scripts/random-responses.test.js @@ -1,6 +1,5 @@ const fs = require("fs"); const { - axios, getApp, utils: { cache }, } = require("../utils/test"); @@ -10,8 +9,12 @@ const script = require("./random-responses"); jest.mock("fs"); describe("random responder", () => { + const json = jest.fn(); + beforeEach(() => { jest.resetAllMocks(); + + fetch.mockResolvedValue({ json }); }); describe("response builder", () => { @@ -203,7 +206,7 @@ describe("random responder", () => { }); it("gets responses from a url", async () => { - axios.get.mockResolvedValue({ data: "response data" }); + json.mockResolvedValue("response data"); cache.mockResolvedValue("cached data"); const responses = await script.getResponses({ @@ -217,7 +220,7 @@ describe("random responder", () => { expect.any(Number), expect.any(Function), ); - expect(axios.get).toHaveBeenCalledWith("over there"); + expect(fetch).toHaveBeenCalledWith("over there"); expect(data).toEqual("response data"); expect(responses).toEqual("cached data"); }); diff --git a/src/scripts/us-code.js b/src/scripts/us-code.js index 2debfe5..6ef930e 100644 --- a/src/scripts/us-code.js +++ b/src/scripts/us-code.js @@ -1,4 +1,3 @@ -const axios = require("axios"); const cheerio = require("cheerio"); const { @@ -77,7 +76,7 @@ module.exports = (app) => { const pieces = citation.split(" "); const url = `https://www.law.cornell.edu/uscode/text/${pieces[0]}/${pieces[1]}`; - const { data } = await axios.get(url); + const data = await fetch(url).then((r) => r.text()); const dom = cheerio.load(data); const sectionDom = dom(".tab-pane.active div.section"); @@ -269,7 +268,7 @@ module.exports = (app) => { const response = { blocks: [], text: "", thread_ts: thread ?? ts }; try { - const { data } = await axios.get(url); + const data = await fetch(url).then((r) => r.text()); const dom = cheerio.load(data); const pageTitle = dom("h1#page_title") diff --git a/src/scripts/us-code.test.js b/src/scripts/us-code.test.js index 682501e..62e7888 100644 --- a/src/scripts/us-code.test.js +++ b/src/scripts/us-code.test.js @@ -1,12 +1,14 @@ -const { getApp, axios } = require("../utils/test"); +const { getApp } = require("../utils/test"); const usc = require("./us-code"); describe("U.S. Code bot", () => { const app = getApp(); + const text = jest.fn(); beforeEach(() => { jest.resetAllMocks(); + fetch.mockResolvedValue({ text }); }); it("registers a handler", () => { @@ -35,7 +37,7 @@ describe("U.S. Code bot", () => { describe("if there is any kind of unexpected error", () => { beforeEach(() => { - axios.get.mockRejectedValue({}); + fetch.mockRejectedValue({}); }); it("does not say anything", async () => { @@ -46,7 +48,7 @@ describe("U.S. Code bot", () => { describe("for a title or subsection that does not exist", () => { beforeEach(() => { - axios.get.mockRejectedValue({ response: { status: 404 } }); + fetch.mockRejectedValue({ response: { status: 404 } }); }); it("tells us it doesn't exist", async () => { @@ -100,7 +102,7 @@ describe("U.S. Code bot", () => { .map((v) => v.trim()), ); - axios.get.mockImplementation(async () => ({ data: html.join("") })); + text.mockImplementation(async () => html.join("")); }); it("sends the user a message with the title and a button for the full text", async () => { @@ -251,9 +253,10 @@ describe("U.S. Code bot", () => { .map((_, i) => `${(i + 1) % 10}`) .join(" "); - axios.get.mockImplementation(async () => ({ - data: `

XX USC YYY - Section Name

${longMessage}
`, - })); + text.mockImplementation( + async () => + `

XX USC YYY - Section Name

${longMessage}
`, + ); await requestModal(request); diff --git a/src/scripts/zen.js b/src/scripts/zen.js index 0fb74f2..5903aad 100644 --- a/src/scripts/zen.js +++ b/src/scripts/zen.js @@ -14,7 +14,7 @@ // anildigital // const { directMention } = require("@slack/bolt"); -const axios = require("axios"); + const { helpMessage, stats: { incrementStats }, @@ -33,8 +33,10 @@ module.exports = (app) => { /\bzen\b/i, async ({ event: { thread_ts: thread }, say }) => { incrementStats("zen"); - const { data } = await axios.get("https://api.github.com/zen"); - say({ text: data, thread_ts: thread }); + const text = await fetch("https://api.github.com/zen").then((r) => + r.text(), + ); + say({ text, thread_ts: thread }); }, ); }; diff --git a/src/scripts/zen.test.js b/src/scripts/zen.test.js index 1f41eb5..26378cb 100644 --- a/src/scripts/zen.test.js +++ b/src/scripts/zen.test.js @@ -1,8 +1,14 @@ -const { axios, getApp } = require("../utils/test"); +const { getApp } = require("../utils/test"); const zen = require("./zen"); describe("zen bot", () => { const app = getApp(); + const text = jest.fn(); + + beforeEach(() => { + jest.resetAllMocks(); + fetch.mockResolvedValue({ text }); + }); it("subscribes to direct mentions that include the word 'zen'", () => { zen(app); @@ -15,6 +21,7 @@ describe("zen bot", () => { }); it("fetches a zen message from the GitHub API when triggered", async () => { + zen(app); const handler = app.getHandler(); const message = { @@ -24,7 +31,7 @@ describe("zen bot", () => { say: jest.fn(), }; - axios.get.mockResolvedValue({ data: "zen message" }); + text.mockResolvedValue("zen message"); await handler(message); diff --git a/src/utils/test.js b/src/utils/test.js index 1c9dcfa..2e76540 100644 --- a/src/utils/test.js +++ b/src/utils/test.js @@ -1,4 +1,3 @@ -const axios = require("axios"); const brain = require("../brain"); const { cache, @@ -11,11 +10,12 @@ const { tock, } = require("./index"); -// Mock axios and the utility functions, to make it easier for tests to use. -jest.mock("axios"); +// Mock fetch and the utility functions, to make it easier for tests to use. jest.mock("../brain"); jest.mock("./index"); +global.fetch = jest.fn(); + module.exports = { getApp: () => { const action = jest.fn(); @@ -85,7 +85,6 @@ module.exports = { }, }; }, - axios, brain, utils: { cache, diff --git a/src/utils/tock.js b/src/utils/tock.js index 08aa588..7066c98 100644 --- a/src/utils/tock.js +++ b/src/utils/tock.js @@ -1,16 +1,15 @@ -const axios = require("axios"); +const path = require("node:path"); const { cache } = require("./cache"); const { getSlackUsers } = require("./slack"); -const tockAPI = axios.create({ - baseURL: process.env.TOCK_API, - headers: { Authorization: `Token ${process.env.TOCK_TOKEN}` }, -}); - const getFromTock = async (url) => cache(`tock fetch: ${url}`, 10, async () => { - const { data } = await tockAPI.get(url); - return data; + const absoluteURL = new URL(process.env.TOCK_API); + absoluteURL.pathname = path.join(absoluteURL.pathname, url); + + return fetch(absoluteURL, { + headers: { Authorization: `Token ${process.env.TOCK_TOKEN}` }, + }).then((r) => r.json()); }); /** diff --git a/src/utils/tock.test.js b/src/utils/tock.test.js index a81e704..4c9adde 100644 --- a/src/utils/tock.test.js +++ b/src/utils/tock.test.js @@ -1,16 +1,11 @@ const moment = require("moment"); +require("./test"); describe("utils / tock", () => { - const create = jest.fn(); - jest.doMock("axios", () => ({ create })); - - const get = jest.fn(); - create.mockReturnValue({ get }); - const getSlackUsers = jest.fn(); jest.doMock("./slack", () => ({ getSlackUsers })); - process.env.TOCK_API = "tock api"; + process.env.TOCK_API = "https://tock"; process.env.TOCK_TOKEN = "tock token"; const { @@ -67,87 +62,80 @@ describe("utils / tock", () => { }, ]); - get.mockImplementation(async (url) => { - switch (url) { - case "/user_data.json": - return { - data: [ - { - is_18f_employee: true, - is_active: true, - current_employee: true, - user: "user 1", - }, - { - is_18f_employee: false, - is_active: true, - current_employee: true, - user: "user 2", - }, - { - is_18f_employee: true, - is_active: false, - current_employee: true, - user: "user 3", - }, - { - is_18f_employee: true, - is_active: true, - current_employee: false, - user: "user 4", - }, - { - is_18f_employee: true, - is_active: true, - current_employee: true, - user: "user 5", - }, - ], - }; - - case "/users.json": - return { - data: [ - { email: "email 1", id: 1, username: "user 1" }, - { email: "email 2", id: 2, username: "user 2" }, - { email: "email 3", id: 3, username: "user 3" }, - { email: "email 4", id: 4, username: "user 4" }, - { email: "email 5", id: 5, username: "user 5" }, - ], - }; - - case "/reporting_period_audit/2020-10-04.json": - return { - data: [ - { - id: 1, - username: "user 1", - email: "email 1", - }, - ], - }; - - case "/reporting_period_audit/2020-10-11.json": - return { - data: [ - { - id: 5, - username: "user 5", - email: "email 5", - }, - ], - }; + fetch.mockImplementation(async (url) => { + const json = jest.fn(); + + switch (url.toString()) { + case "https://tock/user_data.json": + json.mockResolvedValue([ + { + is_18f_employee: true, + is_active: true, + current_employee: true, + user: "user 1", + }, + { + is_18f_employee: false, + is_active: true, + current_employee: true, + user: "user 2", + }, + { + is_18f_employee: true, + is_active: false, + current_employee: true, + user: "user 3", + }, + { + is_18f_employee: true, + is_active: true, + current_employee: false, + user: "user 4", + }, + { + is_18f_employee: true, + is_active: true, + current_employee: true, + user: "user 5", + }, + ]); + break; + + case "https://tock/users.json": + json.mockResolvedValue([ + { email: "email 1", id: 1, username: "user 1" }, + { email: "email 2", id: 2, username: "user 2" }, + { email: "email 3", id: 3, username: "user 3" }, + { email: "email 4", id: 4, username: "user 4" }, + { email: "email 5", id: 5, username: "user 5" }, + ]); + break; + + case "https://tock/reporting_period_audit/2020-10-04.json": + json.mockResolvedValue([ + { + id: 1, + username: "user 1", + email: "email 1", + }, + ]); + break; + + case "https://tock/reporting_period_audit/2020-10-11.json": + json.mockResolvedValue([ + { + id: 5, + username: "user 5", + email: "email 5", + }, + ]); + break; default: - throw new Error("unmocked url"); + throw new Error(`unmocked url: ${url}`); } - }); - it("sets up the default Axios client correctly", () => { - expect(create).toHaveBeenCalledWith({ - baseURL: "tock api", - headers: { Authorization: "Token tock token" }, - }); + return { json }; }); it("gets a list of current 18F Tock users", async () => {