From 3d20d4ccb7760ccff61da9e3f12b3b4f2dde1aed Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 1 Aug 2024 10:37:54 +0100 Subject: [PATCH 01/13] Fix application.spec.ts's reliance on the node-fetch mock. --- .../{node-fetch.ts => _node-fetch.ts} | 0 .../src/api/routes/tests/application.spec.ts | 19 ++++++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) rename packages/server/__mocks__/{node-fetch.ts => _node-fetch.ts} (100%) diff --git a/packages/server/__mocks__/node-fetch.ts b/packages/server/__mocks__/_node-fetch.ts similarity index 100% rename from packages/server/__mocks__/node-fetch.ts rename to packages/server/__mocks__/_node-fetch.ts diff --git a/packages/server/src/api/routes/tests/application.spec.ts b/packages/server/src/api/routes/tests/application.spec.ts index 13b7451a7e..6ae598ee84 100644 --- a/packages/server/src/api/routes/tests/application.spec.ts +++ b/packages/server/src/api/routes/tests/application.spec.ts @@ -20,6 +20,7 @@ import { type App } from "@budibase/types" import tk from "timekeeper" import * as uuid from "uuid" import { structures } from "@budibase/backend-core/tests" +import nock from "nock" describe("/applications", () => { let config = setup.getConfig() @@ -35,6 +36,7 @@ describe("/applications", () => { throw new Error("Failed to publish app") } jest.clearAllMocks() + nock.cleanAll() }) // These need to go first for the app totals to make sense @@ -324,18 +326,33 @@ describe("/applications", () => { describe("delete", () => { it("should delete published app and dev apps with dev app ID", async () => { + const prodAppId = app.appId.replace("_dev", "") + nock("http://localhost:10000") + .delete(`/api/global/roles/${prodAppId}`) + .reply(200, {}) + await config.api.application.delete(app.appId) expect(events.app.deleted).toHaveBeenCalledTimes(1) expect(events.app.unpublished).toHaveBeenCalledTimes(1) }) it("should delete published app and dev app with prod app ID", async () => { - await config.api.application.delete(app.appId.replace("_dev", "")) + const prodAppId = app.appId.replace("_dev", "") + nock("http://localhost:10000") + .delete(`/api/global/roles/${prodAppId}`) + .reply(200, {}) + + await config.api.application.delete(prodAppId) expect(events.app.deleted).toHaveBeenCalledTimes(1) expect(events.app.unpublished).toHaveBeenCalledTimes(1) }) it("should be able to delete an app after SQS_SEARCH_ENABLE has been set but app hasn't been migrated", async () => { + const prodAppId = app.appId.replace("_dev", "") + nock("http://localhost:10000") + .delete(`/api/global/roles/${prodAppId}`) + .reply(200, {}) + await config.withCoreEnv({ SQS_SEARCH_ENABLE: "true" }, async () => { await config.api.application.delete(app.appId) }) From 9a2e8031bcc40808d61a7a147155faf3695116c9 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 1 Aug 2024 11:43:37 +0100 Subject: [PATCH 02/13] Fix plugin.spec.ts's reliance on the node-fetch mock. --- .../src/api/controllers/plugin/index.ts | 18 +-- .../src/api/routes/tests/plugin.spec.ts | 116 +++++++++++------- .../integrations/tests/googlesheets.spec.ts | 22 ++++ .../server/src/tests/utilities/api/index.ts | 3 + .../server/src/tests/utilities/api/plugin.ts | 11 ++ packages/types/src/api/web/index.ts | 1 + packages/types/src/api/web/plugins.ts | 12 ++ 7 files changed, 129 insertions(+), 54 deletions(-) create mode 100644 packages/server/src/tests/utilities/api/plugin.ts create mode 100644 packages/types/src/api/web/plugins.ts diff --git a/packages/server/src/api/controllers/plugin/index.ts b/packages/server/src/api/controllers/plugin/index.ts index c7d4912db3..e1c51f0219 100644 --- a/packages/server/src/api/controllers/plugin/index.ts +++ b/packages/server/src/api/controllers/plugin/index.ts @@ -1,6 +1,13 @@ import { npmUpload, urlUpload, githubUpload } from "./uploaders" import { plugins as pluginCore } from "@budibase/backend-core" -import { PluginType, FileType, PluginSource } from "@budibase/types" +import { + PluginType, + FileType, + PluginSource, + Ctx, + CreatePluginRequest, + CreatePluginResponse, +} from "@budibase/types" import env from "../../../environment" import { clientAppSocket } from "../../../websockets" import sdk from "../../../sdk" @@ -29,7 +36,9 @@ export async function upload(ctx: any) { } } -export async function create(ctx: any) { +export async function create( + ctx: Ctx +) { const { source, url, headers, githubToken } = ctx.request.body try { @@ -75,14 +84,9 @@ export async function create(ctx: any) { const doc = await pro.plugins.storePlugin(metadata, directory, source) clientAppSocket?.emit("plugins-update", { name, hash: doc.hash }) - ctx.body = { - message: "Plugin uploaded successfully", - plugins: [doc], - } ctx.body = { plugin: doc } } catch (err: any) { const errMsg = err?.message ? err?.message : err - ctx.throw(400, `Failed to import plugin: ${errMsg}`) } } diff --git a/packages/server/src/api/routes/tests/plugin.spec.ts b/packages/server/src/api/routes/tests/plugin.spec.ts index 788d3cf349..e592772909 100644 --- a/packages/server/src/api/routes/tests/plugin.spec.ts +++ b/packages/server/src/api/routes/tests/plugin.spec.ts @@ -15,6 +15,8 @@ jest.mock("@budibase/backend-core", () => { import { events, objectStore } from "@budibase/backend-core" import * as setup from "./utilities" +import nock from "nock" +import { PluginSource } from "@budibase/types" const mockUploadDirectory = objectStore.uploadDirectory as jest.Mock const mockDeleteFolder = objectStore.deleteFolder as jest.Mock @@ -28,6 +30,7 @@ describe("/plugins", () => { beforeEach(async () => { await config.init() jest.clearAllMocks() + nock.cleanAll() }) const createPlugin = async (status?: number) => { @@ -112,67 +115,86 @@ describe("/plugins", () => { }) describe("github", () => { - const createGithubPlugin = async (status?: number, url?: string) => { - return await request - .post(`/api/plugin`) - .send({ - source: "Github", - url, - githubToken: "token", + beforeEach(async () => { + nock("https://api.github.com") + .get("/repos/my-repo/budibase-comment-box") + .reply(200, { + name: "budibase-comment-box", + releases_url: + "https://api.github.com/repos/my-repo/budibase-comment-box{/id}", }) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(status ? status : 200) - } - it("should be able to create a plugin from github", async () => { - const res = await createGithubPlugin( - 200, - "https://github.com/my-repo/budibase-comment-box.git" - ) - expect(res.body).toBeDefined() - expect(res.body.plugin).toBeDefined() - expect(res.body.plugin._id).toEqual("plg_comment-box") + .get("/repos/my-repo/budibase-comment-box/latest") + .reply(200, { + assets: [ + { + content_type: "application/gzip", + browser_download_url: + "https://github.com/my-repo/budibase-comment-box/releases/download/v1.0.2/comment-box-1.0.2.tar.gz", + }, + ], + }) + + nock("https://github.com") + .get( + "/my-repo/budibase-comment-box/releases/download/v1.0.2/comment-box-1.0.2.tar.gz" + ) + .replyWithFile( + 200, + "src/api/routes/tests/data/comment-box-1.0.2.tar.gz" + ) }) + + it("should be able to create a plugin from github", async () => { + const { plugin } = await config.api.plugin.create({ + source: PluginSource.GITHUB, + url: "https://github.com/my-repo/budibase-comment-box.git", + githubToken: "token", + }) + expect(plugin._id).toEqual("plg_comment-box") + }) + it("should fail if the url is not from github", async () => { - const res = await createGithubPlugin( - 400, - "https://notgithub.com/my-repo/budibase-comment-box" - ) - expect(res.body.message).toEqual( - "Failed to import plugin: The plugin origin must be from Github" + await config.api.plugin.create( + { + source: PluginSource.GITHUB, + url: "https://notgithub.com/my-repo/budibase-comment-box", + githubToken: "token", + }, + { + status: 400, + body: { + message: + "Failed to import plugin: The plugin origin must be from Github", + }, + } ) }) }) describe("npm", () => { it("should be able to create a plugin from npm", async () => { - const res = await request - .post(`/api/plugin`) - .send({ - source: "NPM", - url: "https://www.npmjs.com/package/budibase-component", - }) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) - expect(res.body).toBeDefined() - expect(res.body.plugin._id).toEqual("plg_budibase-component") + const { plugin } = await config.api.plugin.create({ + source: PluginSource.NPM, + url: "https://www.npmjs.com/package/budibase-component", + }) + expect(plugin._id).toEqual("plg_budibase-component") expect(events.plugin.imported).toHaveBeenCalled() }) }) describe("url", () => { it("should be able to create a plugin from a URL", async () => { - const res = await request - .post(`/api/plugin`) - .send({ - source: "URL", - url: "https://www.someurl.com/comment-box/comment-box-1.0.2.tar.gz", - }) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(200) - expect(res.body).toBeDefined() - expect(res.body.plugin._id).toEqual("plg_comment-box") + nock("https://www.someurl.com") + .get("/comment-box/comment-box-1.0.2.tar.gz") + .replyWithFile( + 200, + "src/api/routes/tests/data/comment-box-1.0.2.tar.gz" + ) + + const { plugin } = await config.api.plugin.create({ + source: PluginSource.URL, + url: "https://www.someurl.com/comment-box/comment-box-1.0.2.tar.gz", + }) + expect(plugin._id).toEqual("plg_comment-box") expect(events.plugin.imported).toHaveBeenCalledTimes(1) }) }) diff --git a/packages/server/src/integrations/tests/googlesheets.spec.ts b/packages/server/src/integrations/tests/googlesheets.spec.ts index 97ac35787d..2252a8bb9b 100644 --- a/packages/server/src/integrations/tests/googlesheets.spec.ts +++ b/packages/server/src/integrations/tests/googlesheets.spec.ts @@ -64,6 +64,28 @@ describe("Google Sheets Integration", () => { jest.clearAllMocks() }) + // nock("https://www.googleapis.com/").post("/oauth2/v4/token").reply(200, { + // grant_type: "client_credentials", + // client_id: "your-client-id", + // client_secret: "your-client-secret", + // }) + + // nock("https://oauth2.googleapis.com").post("/token").reply(200, { + // access_token: "your-access-token", + // }) + + // nock("https://sheets.googleapis.com") + // .get("/v4/spreadsheets/randomId/") + // .reply(200, { + // sheets: [ + // { + // properties: { + // title: "test", + // }, + // }, + // ], + // }) + function createBasicTable(name: string, columns: string[]): Table { return { type: "table", diff --git a/packages/server/src/tests/utilities/api/index.ts b/packages/server/src/tests/utilities/api/index.ts index 36a6ed0222..79514d4ece 100644 --- a/packages/server/src/tests/utilities/api/index.ts +++ b/packages/server/src/tests/utilities/api/index.ts @@ -15,6 +15,7 @@ import { RoleAPI } from "./role" import { TemplateAPI } from "./template" import { RowActionAPI } from "./rowAction" import { AutomationAPI } from "./automation" +import { PluginAPI } from "./plugin" export default class API { table: TableAPI @@ -33,6 +34,7 @@ export default class API { templates: TemplateAPI rowAction: RowActionAPI automation: AutomationAPI + plugin: PluginAPI constructor(config: TestConfiguration) { this.table = new TableAPI(config) @@ -51,5 +53,6 @@ export default class API { this.templates = new TemplateAPI(config) this.rowAction = new RowActionAPI(config) this.automation = new AutomationAPI(config) + this.plugin = new PluginAPI(config) } } diff --git a/packages/server/src/tests/utilities/api/plugin.ts b/packages/server/src/tests/utilities/api/plugin.ts new file mode 100644 index 0000000000..c2b3a3269d --- /dev/null +++ b/packages/server/src/tests/utilities/api/plugin.ts @@ -0,0 +1,11 @@ +import { Expectations, TestAPI } from "./base" +import { CreatePluginRequest, CreatePluginResponse } from "@budibase/types" + +export class PluginAPI extends TestAPI { + create = async (body: CreatePluginRequest, expectations?: Expectations) => { + return await this._post(`/api/plugin`, { + body, + expectations, + }) + } +} diff --git a/packages/types/src/api/web/index.ts b/packages/types/src/api/web/index.ts index 8a091afdba..27d51ce1b7 100644 --- a/packages/types/src/api/web/index.ts +++ b/packages/types/src/api/web/index.ts @@ -15,3 +15,4 @@ export * from "./automation" export * from "./layout" export * from "./query" export * from "./role" +export * from "./plugins" diff --git a/packages/types/src/api/web/plugins.ts b/packages/types/src/api/web/plugins.ts new file mode 100644 index 0000000000..458ad3f6ce --- /dev/null +++ b/packages/types/src/api/web/plugins.ts @@ -0,0 +1,12 @@ +import { PluginSource } from "../../documents" + +export interface CreatePluginRequest { + source: PluginSource + url: string + githubToken?: string + headers?: { [key: string]: string } +} + +export interface CreatePluginResponse { + plugin: any +} From 3657067337cef5c66e5ded487d4d17a60d5c910c Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 1 Aug 2024 11:44:58 +0100 Subject: [PATCH 03/13] Fix googlesheets.spec.ts's reliance on the node-fetch mock. --- .../integrations/tests/googlesheets.spec.ts | 30 +++++-------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/packages/server/src/integrations/tests/googlesheets.spec.ts b/packages/server/src/integrations/tests/googlesheets.spec.ts index 2252a8bb9b..9b1ea815f5 100644 --- a/packages/server/src/integrations/tests/googlesheets.spec.ts +++ b/packages/server/src/integrations/tests/googlesheets.spec.ts @@ -1,4 +1,5 @@ import type { GoogleSpreadsheetWorksheet } from "google-spreadsheet" +import nock from "nock" jest.mock("google-auth-library") const { OAuth2Client } = require("google-auth-library") @@ -62,30 +63,15 @@ describe("Google Sheets Integration", () => { await config.init() jest.clearAllMocks() + + nock.cleanAll() + nock("https://www.googleapis.com/").post("/oauth2/v4/token").reply(200, { + grant_type: "client_credentials", + client_id: "your-client-id", + client_secret: "your-client-secret", + }) }) - // nock("https://www.googleapis.com/").post("/oauth2/v4/token").reply(200, { - // grant_type: "client_credentials", - // client_id: "your-client-id", - // client_secret: "your-client-secret", - // }) - - // nock("https://oauth2.googleapis.com").post("/token").reply(200, { - // access_token: "your-access-token", - // }) - - // nock("https://sheets.googleapis.com") - // .get("/v4/spreadsheets/randomId/") - // .reply(200, { - // sheets: [ - // { - // properties: { - // title: "test", - // }, - // }, - // ], - // }) - function createBasicTable(name: string, columns: string[]): Table { return { type: "table", From e530400f461c996636d6bd4ec88d9103cb178ca2 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 1 Aug 2024 11:57:04 +0100 Subject: [PATCH 04/13] Fix n8n.spec.ts's reliance on the node-fetch mock. --- .../server/src/automations/tests/n8n.spec.ts | 25 +++++++++++-------- packages/server/src/tests/jestSetup.ts | 10 ++++++++ 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/packages/server/src/automations/tests/n8n.spec.ts b/packages/server/src/automations/tests/n8n.spec.ts index d60a08b53b..0c18f313b1 100644 --- a/packages/server/src/automations/tests/n8n.spec.ts +++ b/packages/server/src/automations/tests/n8n.spec.ts @@ -1,4 +1,5 @@ import { getConfig, afterAll, runStep, actions } from "./utilities" +import nock from "nock" describe("test the outgoing webhook action", () => { let config = getConfig() @@ -9,31 +10,33 @@ describe("test the outgoing webhook action", () => { afterAll() + beforeEach(() => { + nock.cleanAll() + }) + it("should be able to run the action and default to 'get'", async () => { + nock("http://www.example.com/").get("/").reply(200, { foo: "bar" }) const res = await runStep(actions.n8n.stepId, { url: "http://www.example.com", body: { test: "IGNORE_ME", }, }) - expect(res.response.url).toEqual("http://www.example.com") - expect(res.response.method).toEqual("GET") - expect(res.response.body).toBeUndefined() + expect(res.response.foo).toEqual("bar") expect(res.success).toEqual(true) }) it("should add the payload props when a JSON string is provided", async () => { - const payload = `{ "name": "Adam", "age": 9 }` + nock("http://www.example.com/") + .post("/", { name: "Adam", age: 9 }) + .reply(200) const res = await runStep(actions.n8n.stepId, { body: { - value: payload, + value: JSON.stringify({ name: "Adam", age: 9 }), }, method: "POST", url: "http://www.example.com", }) - expect(res.response.url).toEqual("http://www.example.com") - expect(res.response.method).toEqual("POST") - expect(res.response.body).toEqual(`{"name":"Adam","age":9}`) expect(res.success).toEqual(true) }) @@ -53,6 +56,9 @@ describe("test the outgoing webhook action", () => { }) it("should not append the body if the method is HEAD", async () => { + nock("http://www.example.com/") + .head("/", body => body === "") + .reply(200) const res = await runStep(actions.n8n.stepId, { url: "http://www.example.com", method: "HEAD", @@ -60,9 +66,6 @@ describe("test the outgoing webhook action", () => { test: "IGNORE_ME", }, }) - expect(res.response.url).toEqual("http://www.example.com") - expect(res.response.method).toEqual("HEAD") - expect(res.response.body).toBeUndefined() expect(res.success).toEqual(true) }) }) diff --git a/packages/server/src/tests/jestSetup.ts b/packages/server/src/tests/jestSetup.ts index c01f415f9e..bc6384e4cd 100644 --- a/packages/server/src/tests/jestSetup.ts +++ b/packages/server/src/tests/jestSetup.ts @@ -1,6 +1,7 @@ import env from "../environment" import { env as coreEnv, timers } from "@budibase/backend-core" import { testContainerUtils } from "@budibase/backend-core/tests" +import nock from "nock" if (!process.env.CI) { // set a longer timeout in dev for debugging 100 seconds @@ -9,6 +10,15 @@ if (!process.env.CI) { jest.setTimeout(30 * 1000) } +nock.disableNetConnect() +nock.enableNetConnect(host => { + return ( + host.includes("localhost") || + host.includes("127.0.0.1") || + host.includes("::1") + ) +}) + testContainerUtils.setupEnv(env, coreEnv) afterAll(() => { From f16f1fb7bad1477a862526a9c13cdb14e8912712 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 1 Aug 2024 12:03:29 +0100 Subject: [PATCH 05/13] Fix zapier.spec.ts's reliance on the node-fetch mock. --- .../src/automations/tests/zapier.spec.ts | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/packages/server/src/automations/tests/zapier.spec.ts b/packages/server/src/automations/tests/zapier.spec.ts index 994df3dc99..a1406e4818 100644 --- a/packages/server/src/automations/tests/zapier.spec.ts +++ b/packages/server/src/automations/tests/zapier.spec.ts @@ -1,4 +1,5 @@ import { getConfig, afterAll, runStep, actions } from "./utilities" +import nock from "nock" describe("test the outgoing webhook action", () => { let config = getConfig() @@ -9,34 +10,39 @@ describe("test the outgoing webhook action", () => { afterAll() + beforeEach(() => { + nock.cleanAll() + }) + it("should be able to run the action", async () => { + nock("http://www.example.com/").post("/").reply(200, { foo: "bar" }) const res = await runStep(actions.zapier.stepId, { - value1: "test", url: "http://www.example.com", }) - expect(res.response.url).toEqual("http://www.example.com") - expect(res.response.method).toEqual("post") + expect(res.response.foo).toEqual("bar") expect(res.success).toEqual(true) }) it("should add the payload props when a JSON string is provided", async () => { - const payload = `{ "value1": 1, "value2": 2, "value3": 3, "value4": 4, "value5": 5, "name": "Adam", "age": 9 }` + const payload = { + value1: 1, + value2: 2, + value3: 3, + value4: 4, + value5: 5, + name: "Adam", + age: 9, + } + + nock("http://www.example.com/") + .post("/", { ...payload, platform: "budibase" }) + .reply(200, { foo: "bar" }) + const res = await runStep(actions.zapier.stepId, { - value1: "ONE", - value2: "TWO", - value3: "THREE", - value4: "FOUR", - value5: "FIVE", - body: { - value: payload, - }, + body: { value: JSON.stringify(payload) }, url: "http://www.example.com", }) - expect(res.response.url).toEqual("http://www.example.com") - expect(res.response.method).toEqual("post") - expect(res.response.body).toEqual( - `{"platform":"budibase","value1":1,"value2":2,"value3":3,"value4":4,"value5":5,"name":"Adam","age":9}` - ) + expect(res.response.foo).toEqual("bar") expect(res.success).toEqual(true) }) From b39875fb4b6495f816ffa270458f6f3d61f8282d Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 1 Aug 2024 12:07:01 +0100 Subject: [PATCH 06/13] Fix make.spec.ts's reliance on the node-fetch mock. --- .../server/src/automations/tests/make.spec.ts | 44 ++++++++++--------- .../src/automations/tests/zapier.spec.ts | 6 +-- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/server/src/automations/tests/make.spec.ts b/packages/server/src/automations/tests/make.spec.ts index 62474ae2c0..388b197c7f 100644 --- a/packages/server/src/automations/tests/make.spec.ts +++ b/packages/server/src/automations/tests/make.spec.ts @@ -1,4 +1,5 @@ import { getConfig, afterAll, runStep, actions } from "./utilities" +import nock from "nock" describe("test the outgoing webhook action", () => { let config = getConfig() @@ -9,42 +10,45 @@ describe("test the outgoing webhook action", () => { afterAll() + beforeEach(() => { + nock.cleanAll() + }) + it("should be able to run the action", async () => { + nock("http://www.example.com/").post("/").reply(200, { foo: "bar" }) const res = await runStep(actions.integromat.stepId, { - value1: "test", url: "http://www.example.com", }) - expect(res.response.url).toEqual("http://www.example.com") - expect(res.response.method).toEqual("post") + expect(res.response.foo).toEqual("bar") expect(res.success).toEqual(true) }) it("should add the payload props when a JSON string is provided", async () => { - const payload = `{"value1":1,"value2":2,"value3":3,"value4":4,"value5":5,"name":"Adam","age":9}` + const payload = { + value1: 1, + value2: 2, + value3: 3, + value4: 4, + value5: 5, + name: "Adam", + age: 9, + } + + nock("http://www.example.com/") + .post("/", payload) + .reply(200, { foo: "bar" }) + const res = await runStep(actions.integromat.stepId, { - value1: "ONE", - value2: "TWO", - value3: "THREE", - value4: "FOUR", - value5: "FIVE", - body: { - value: payload, - }, + body: { value: JSON.stringify(payload) }, url: "http://www.example.com", }) - expect(res.response.url).toEqual("http://www.example.com") - expect(res.response.method).toEqual("post") - expect(res.response.body).toEqual(payload) + expect(res.response.foo).toEqual("bar") expect(res.success).toEqual(true) }) it("should return a 400 if the JSON payload string is malformed", async () => { - const payload = `{ value1 1 }` const res = await runStep(actions.integromat.stepId, { - value1: "ONE", - body: { - value: payload, - }, + body: { value: "{ invalid json }" }, url: "http://www.example.com", }) expect(res.httpStatus).toEqual(400) diff --git a/packages/server/src/automations/tests/zapier.spec.ts b/packages/server/src/automations/tests/zapier.spec.ts index a1406e4818..a7dc7d3eae 100644 --- a/packages/server/src/automations/tests/zapier.spec.ts +++ b/packages/server/src/automations/tests/zapier.spec.ts @@ -47,12 +47,8 @@ describe("test the outgoing webhook action", () => { }) it("should return a 400 if the JSON payload string is malformed", async () => { - const payload = `{ value1 1 }` const res = await runStep(actions.zapier.stepId, { - value1: "ONE", - body: { - value: payload, - }, + body: { value: "{ invalid json }" }, url: "http://www.example.com", }) expect(res.httpStatus).toEqual(400) From 661e1f241d335b7ce3285ebbf223c19c0ee66658 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 1 Aug 2024 12:09:11 +0100 Subject: [PATCH 07/13] Fix startup.spec.ts's reliance on the node-fetch mock. --- packages/server/src/startup/tests/startup.spec.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/server/src/startup/tests/startup.spec.ts b/packages/server/src/startup/tests/startup.spec.ts index fef9270bb5..fd2e0df61a 100644 --- a/packages/server/src/startup/tests/startup.spec.ts +++ b/packages/server/src/startup/tests/startup.spec.ts @@ -1,6 +1,7 @@ import TestConfiguration from "../../tests/utilities/TestConfiguration" import { startup } from "../index" import { users, utils, tenancy } from "@budibase/backend-core" +import nock from "nock" describe("check BB_ADMIN environment variables", () => { const config = new TestConfiguration() @@ -8,7 +9,17 @@ describe("check BB_ADMIN environment variables", () => { await config.init() }) + beforeEach(() => { + nock.cleanAll() + }) + it("should be able to create a user with the BB_ADMIN environment variables", async () => { + nock("http://localhost:10000") + .get("/api/global/configs/checklist") + .reply(200, {}) + .get("/api/global/self/api_key") + .reply(200, {}) + const EMAIL = "budibase@budibase.com", PASSWORD = "budibase" await tenancy.doInTenant(tenancy.DEFAULT_TENANT_ID, async () => { From 6d70dd1924b6e9e2539e035834c2942f20dd48c0 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 1 Aug 2024 12:19:44 +0100 Subject: [PATCH 08/13] Fix outgoingWebhook.spec.ts's reliance on the node-fetch mock. --- .../automations/tests/outgoingWebhook.spec.js | 41 ------------------- .../automations/tests/outgoingWebhook.spec.ts | 37 +++++++++++++++++ 2 files changed, 37 insertions(+), 41 deletions(-) delete mode 100644 packages/server/src/automations/tests/outgoingWebhook.spec.js create mode 100644 packages/server/src/automations/tests/outgoingWebhook.spec.ts diff --git a/packages/server/src/automations/tests/outgoingWebhook.spec.js b/packages/server/src/automations/tests/outgoingWebhook.spec.js deleted file mode 100644 index 06fe2e0a38..0000000000 --- a/packages/server/src/automations/tests/outgoingWebhook.spec.js +++ /dev/null @@ -1,41 +0,0 @@ -const setup = require("./utilities") -const fetch = require("node-fetch") - -jest.mock("node-fetch") - -describe("test the outgoing webhook action", () => { - let inputs - let config = setup.getConfig() - - beforeAll(async () => { - await config.init() - inputs = { - requestMethod: "POST", - url: "www.example.com", - requestBody: JSON.stringify({ - a: 1, - }), - } - }) - - afterAll(setup.afterAll) - - it("should be able to run the action", async () => { - const res = await setup.runStep( - setup.actions.OUTGOING_WEBHOOK.stepId, - inputs - ) - expect(res.success).toEqual(true) - expect(res.response.url).toEqual("http://www.example.com") - expect(res.response.method).toEqual("POST") - expect(JSON.parse(res.response.body).a).toEqual(1) - }) - - it("should return an error if something goes wrong in fetch", async () => { - const res = await setup.runStep(setup.actions.OUTGOING_WEBHOOK.stepId, { - requestMethod: "GET", - url: "www.invalid.com", - }) - expect(res.success).toEqual(false) - }) -}) diff --git a/packages/server/src/automations/tests/outgoingWebhook.spec.ts b/packages/server/src/automations/tests/outgoingWebhook.spec.ts new file mode 100644 index 0000000000..0e26927c55 --- /dev/null +++ b/packages/server/src/automations/tests/outgoingWebhook.spec.ts @@ -0,0 +1,37 @@ +import { getConfig, afterAll as _afterAll, runStep, actions } from "./utilities" +import nock from "nock" + +describe("test the outgoing webhook action", () => { + const config = getConfig() + + beforeAll(async () => { + await config.init() + }) + + afterAll(_afterAll) + + beforeEach(() => { + nock.cleanAll() + }) + + it("should be able to run the action", async () => { + nock("http://www.example.com") + .post("/", { a: 1 }) + .reply(200, { foo: "bar" }) + const res = await runStep(actions.OUTGOING_WEBHOOK.stepId, { + requestMethod: "POST", + url: "www.example.com", + requestBody: JSON.stringify({ a: 1 }), + }) + expect(res.success).toEqual(true) + expect(res.response.foo).toEqual("bar") + }) + + it("should return an error if something goes wrong in fetch", async () => { + const res = await runStep(actions.OUTGOING_WEBHOOK.stepId, { + requestMethod: "GET", + url: "www.invalid.com", + }) + expect(res.success).toEqual(false) + }) +}) From c8fadc33d9e67d73a49ed607140483deb9a13584 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 1 Aug 2024 12:21:42 +0100 Subject: [PATCH 09/13] Fix discorfd.spec.ts's reliance on the node-fetch mock. --- .../src/automations/tests/discord.spec.js | 26 ------------------- .../src/automations/tests/discord.spec.ts | 26 +++++++++++++++++++ 2 files changed, 26 insertions(+), 26 deletions(-) delete mode 100644 packages/server/src/automations/tests/discord.spec.js create mode 100644 packages/server/src/automations/tests/discord.spec.ts diff --git a/packages/server/src/automations/tests/discord.spec.js b/packages/server/src/automations/tests/discord.spec.js deleted file mode 100644 index 84c7e6f46e..0000000000 --- a/packages/server/src/automations/tests/discord.spec.js +++ /dev/null @@ -1,26 +0,0 @@ -const setup = require("./utilities") -const fetch = require("node-fetch") - -jest.mock("node-fetch") - -describe("test the outgoing webhook action", () => { - let inputs - let config = setup.getConfig() - - beforeAll(async () => { - await config.init() - inputs = { - username: "joe_bloggs", - url: "http://www.example.com", - } - }) - - afterAll(setup.afterAll) - - it("should be able to run the action", async () => { - const res = await setup.runStep(setup.actions.discord.stepId, inputs) - expect(res.response.url).toEqual("http://www.example.com") - expect(res.response.method).toEqual("post") - expect(res.success).toEqual(true) - }) -}) diff --git a/packages/server/src/automations/tests/discord.spec.ts b/packages/server/src/automations/tests/discord.spec.ts new file mode 100644 index 0000000000..07eab7205c --- /dev/null +++ b/packages/server/src/automations/tests/discord.spec.ts @@ -0,0 +1,26 @@ +import { getConfig, afterAll as _afterAll, runStep, actions } from "./utilities" +import nock from "nock" + +describe("test the outgoing webhook action", () => { + let config = getConfig() + + beforeAll(async () => { + await config.init() + }) + + afterAll(_afterAll) + + beforeEach(() => { + nock.cleanAll() + }) + + it("should be able to run the action", async () => { + nock("http://www.example.com/").post("/").reply(200, { foo: "bar" }) + const res = await runStep(actions.discord.stepId, { + url: "http://www.example.com", + username: "joe_bloggs", + }) + expect(res.response.foo).toEqual("bar") + expect(res.success).toEqual(true) + }) +}) From a973b65a7231dfc6462d825b77301448f53db7d3 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 1 Aug 2024 12:36:32 +0100 Subject: [PATCH 10/13] Fix plugni.spec.ts's reliance on the node-fetch mock (again?). --- .../src/api/routes/tests/plugin.spec.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/server/src/api/routes/tests/plugin.spec.ts b/packages/server/src/api/routes/tests/plugin.spec.ts index e592772909..70bbfd3cea 100644 --- a/packages/server/src/api/routes/tests/plugin.spec.ts +++ b/packages/server/src/api/routes/tests/plugin.spec.ts @@ -172,6 +172,28 @@ describe("/plugins", () => { }) describe("npm", () => { it("should be able to create a plugin from npm", async () => { + nock("https://registry.npmjs.org") + .get("/budibase-component") + .reply(200, { + name: "budibase-component", + "dist-tags": { + latest: "1.0.0", + }, + versions: { + "1.0.0": { + dist: { + tarball: + "https://registry.npmjs.org/budibase-component/-/budibase-component-1.0.1.tgz", + }, + }, + }, + }) + .get("/budibase-component/-/budibase-component-1.0.1.tgz") + .replyWithFile( + 200, + "src/api/routes/tests/data/budibase-component-1.0.1.tgz" + ) + const { plugin } = await config.api.plugin.create({ source: PluginSource.NPM, url: "https://www.npmjs.com/package/budibase-component", From a38dc3d16356e2b4113f3d3371622df49789e2c5 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 1 Aug 2024 12:45:37 +0100 Subject: [PATCH 11/13] Fix datasource.spec.ts's reliance on the node-fetch mock. --- .../server/src/api/routes/tests/datasource.spec.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/server/src/api/routes/tests/datasource.spec.ts b/packages/server/src/api/routes/tests/datasource.spec.ts index 255e46167f..4ca766247b 100644 --- a/packages/server/src/api/routes/tests/datasource.spec.ts +++ b/packages/server/src/api/routes/tests/datasource.spec.ts @@ -19,6 +19,7 @@ import { } from "@budibase/types" import { DatabaseName, getDatasource } from "../../../integrations/tests/utils" import { tableForDatasource } from "../../../tests/utilities/structures" +import nock from "nock" describe("/datasources", () => { const config = setup.getConfig() @@ -37,6 +38,7 @@ describe("/datasources", () => { config: {}, }) jest.clearAllMocks() + nock.cleanAll() }) describe("create", () => { @@ -71,6 +73,12 @@ describe("/datasources", () => { describe("dynamic variables", () => { it("should invalidate changed or removed variables", async () => { + nock("http://www.example.com/") + .get("/") + .reply(200, [{ value: "test" }]) + .get("/?test=test") + .reply(200, [{ value: 1 }]) + let datasource = await config.api.datasource.create({ type: "datasource", name: "Rest", @@ -81,7 +89,7 @@ describe("/datasources", () => { const query = await config.api.query.save({ datasourceId: datasource._id!, fields: { - path: "www.google.com", + path: "www.example.com", }, parameters: [], transformer: null, From 97e142a1d8132a06dcb895c26360abe07548d517 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 1 Aug 2024 12:46:01 +0100 Subject: [PATCH 12/13] Delete node-fetch.ts mock. --- packages/server/__mocks__/_node-fetch.ts | 206 ----------------------- 1 file changed, 206 deletions(-) delete mode 100644 packages/server/__mocks__/_node-fetch.ts diff --git a/packages/server/__mocks__/_node-fetch.ts b/packages/server/__mocks__/_node-fetch.ts deleted file mode 100644 index c556d0f2e9..0000000000 --- a/packages/server/__mocks__/_node-fetch.ts +++ /dev/null @@ -1,206 +0,0 @@ -// @ts-ignore -import fs from "fs" - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -module FetchMock { - // @ts-ignore - const fetch = jest.requireActual("node-fetch") - let failCount = 0 - let mockSearch = false - - const func = async (url: any, opts: any) => { - const { host, pathname } = new URL(url) - function json(body: any, status = 200) { - return { - status, - headers: { - raw: () => { - return { "content-type": ["application/json"] } - }, - get: (name: string) => { - if (name.toLowerCase() === "content-type") { - return ["application/json"] - } - }, - }, - json: async () => { - //x-www-form-encoded body is a URLSearchParams - //The call to stringify it leaves it blank - if (body?.opts?.body instanceof URLSearchParams) { - const paramArray = Array.from(body.opts.body.entries()) - body.opts.body = paramArray.reduce((acc: any, pair: any) => { - acc[pair[0]] = pair[1] - return acc - }, {}) - } - return body - }, - } - } - - if (pathname.includes("/api/global")) { - const user = { - email: "test@example.com", - _id: "us_test@example.com", - status: "active", - roles: {}, - builder: { - global: false, - }, - admin: { - global: false, - }, - } - return pathname.endsWith("/users") && opts.method === "GET" - ? json([user]) - : json(user) - } - // mocked data based on url - else if (pathname.includes("api/apps")) { - return json({ - app1: { - url: "/app1", - }, - }) - } else if (host.includes("example.com")) { - return json({ - body: opts.body, - url, - method: opts.method, - }) - } else if (host.includes("invalid.com")) { - return json( - { - invalid: true, - }, - 404 - ) - } else if (mockSearch && pathname.includes("_search")) { - const body = opts.body - const parts = body.split("tableId:") - let tableId - if (parts && parts[1]) { - tableId = parts[1].split('"')[0] - } - return json({ - rows: [ - { - doc: { - _id: "test", - tableId: tableId, - query: opts.body, - }, - }, - ], - bookmark: "test", - }) - } else if (host.includes("google.com")) { - return json({ - url, - opts, - value: - '', - }) - } else if ( - url === "https://api.github.com/repos/my-repo/budibase-comment-box" - ) { - return Promise.resolve({ - json: () => { - return { - name: "budibase-comment-box", - releases_url: - "https://api.github.com/repos/my-repo/budibase-comment-box{/id}", - } - }, - }) - } else if ( - url === "https://api.github.com/repos/my-repo/budibase-comment-box/latest" - ) { - return Promise.resolve({ - json: () => { - return { - assets: [ - { - content_type: "application/gzip", - browser_download_url: - "https://github.com/my-repo/budibase-comment-box/releases/download/v1.0.2/comment-box-1.0.2.tar.gz", - }, - ], - } - }, - }) - } else if ( - url === - "https://github.com/my-repo/budibase-comment-box/releases/download/v1.0.2/comment-box-1.0.2.tar.gz" - ) { - return Promise.resolve({ - body: fs.createReadStream( - "src/api/routes/tests/data/comment-box-1.0.2.tar.gz" - ), - ok: true, - }) - } else if (url === "https://www.npmjs.com/package/budibase-component") { - return Promise.resolve({ - status: 200, - json: () => { - return { - name: "budibase-component", - "dist-tags": { - latest: "1.0.0", - }, - versions: { - "1.0.0": { - dist: { - tarball: - "https://registry.npmjs.org/budibase-component/-/budibase-component-1.0.2.tgz", - }, - }, - }, - } - }, - }) - } else if ( - url === - "https://registry.npmjs.org/budibase-component/-/budibase-component-1.0.2.tgz" - ) { - return Promise.resolve({ - body: fs.createReadStream( - "src/api/routes/tests/data/budibase-component-1.0.2.tgz" - ), - ok: true, - }) - } else if ( - url === "https://www.someurl.com/comment-box/comment-box-1.0.2.tar.gz" - ) { - return Promise.resolve({ - body: fs.createReadStream( - "src/api/routes/tests/data/comment-box-1.0.2.tar.gz" - ), - ok: true, - }) - } else if (url === "https://www.googleapis.com/oauth2/v4/token") { - // any valid response - return json({}) - } else if (host.includes("failonce.com")) { - failCount++ - if (failCount === 1) { - return json({ message: "error" }, 500) - } else { - return json({ - fails: failCount - 1, - url, - opts, - }) - } - } - return fetch(url, opts) - } - - func.Headers = fetch.Headers - - func.mockSearch = () => { - mockSearch = true - } - - module.exports = func -} From 01ad68f843821301da1da4ed1b6fad49fa98c072 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 1 Aug 2024 17:57:33 +0100 Subject: [PATCH 13/13] Remove all mocking from rest.spec.ts. --- .../src/api/routes/tests/queries/rest.spec.ts | 2 - packages/server/src/integrations/rest.ts | 62 +- .../src/integrations/tests/rest.spec.ts | 795 +++++++----------- .../types/src/documents/app/datasource.ts | 12 +- packages/types/src/documents/app/query.ts | 36 +- yarn.lock | 192 ++++- 6 files changed, 544 insertions(+), 555 deletions(-) diff --git a/packages/server/src/api/routes/tests/queries/rest.spec.ts b/packages/server/src/api/routes/tests/queries/rest.spec.ts index 29bbbf3a61..6f489bc723 100644 --- a/packages/server/src/api/routes/tests/queries/rest.spec.ts +++ b/packages/server/src/api/routes/tests/queries/rest.spec.ts @@ -5,8 +5,6 @@ import { getCachedVariable } from "../../../../threads/utils" import nock from "nock" import { generator } from "@budibase/backend-core/tests" -jest.unmock("node-fetch") - describe("rest", () => { let config: TestConfiguration let datasource: Datasource diff --git a/packages/server/src/integrations/rest.ts b/packages/server/src/integrations/rest.ts index 86c059bc82..ce2ec7d545 100644 --- a/packages/server/src/integrations/rest.ts +++ b/packages/server/src/integrations/rest.ts @@ -1,4 +1,5 @@ import { + BodyType, DatasourceFieldType, HttpMethod, Integration, @@ -15,7 +16,7 @@ import { import get from "lodash/get" import * as https from "https" import qs from "querystring" -import type { Response } from "node-fetch" +import type { Response, RequestInit } from "node-fetch" import fetch from "node-fetch" import { formatBytes } from "../utilities" import { performance } from "perf_hooks" @@ -28,15 +29,6 @@ import path from "path" import { Builder as XmlBuilder } from "xml2js" import { getAttachmentHeaders } from "./utils/restUtils" -enum BodyType { - NONE = "none", - FORM_DATA = "form", - XML = "xml", - ENCODED = "encoded", - JSON = "json", - TEXT = "text", -} - const coreFields = { path: { type: DatasourceFieldType.STRING, @@ -127,7 +119,23 @@ const SCHEMA: Integration = { }, } -class RestIntegration implements IntegrationBase { +interface ParsedResponse { + data: any + info: { + code: number + size: string + time: string + } + extra?: { + raw: string | undefined + headers: Record + } + pagination?: { + cursor: any + } +} + +export class RestIntegration implements IntegrationBase { private config: RestConfig private headers: { [key: string]: string @@ -138,7 +146,10 @@ class RestIntegration implements IntegrationBase { this.config = config } - async parseResponse(response: Response, pagination: PaginationConfig | null) { + async parseResponse( + response: Response, + pagination?: PaginationConfig + ): Promise { let data: any[] | string | undefined, raw: string | undefined, headers: Record = {}, @@ -235,8 +246,8 @@ class RestIntegration implements IntegrationBase { getUrl( path: string, queryString: string, - pagination: PaginationConfig | null, - paginationValues: PaginationValues | null + pagination?: PaginationConfig, + paginationValues?: PaginationValues ): string { // Add pagination params to query string if required if (pagination?.location === "query" && paginationValues) { @@ -279,10 +290,10 @@ class RestIntegration implements IntegrationBase { addBody( bodyType: string, body: string | any, - input: any, - pagination: PaginationConfig | null, - paginationValues: PaginationValues | null - ) { + input: RequestInit, + pagination?: PaginationConfig, + paginationValues?: PaginationValues + ): RequestInit { if (!input.headers) { input.headers = {} } @@ -345,6 +356,7 @@ class RestIntegration implements IntegrationBase { string = new XmlBuilder().buildObject(object) } input.body = string + // @ts-ignore input.headers["Content-Type"] = "application/xml" break case BodyType.JSON: @@ -356,13 +368,14 @@ class RestIntegration implements IntegrationBase { object[key] = value }) input.body = JSON.stringify(object) + // @ts-ignore input.headers["Content-Type"] = "application/json" break } return input } - getAuthHeaders(authConfigId: string): { [key: string]: any } { + getAuthHeaders(authConfigId?: string): { [key: string]: any } { let headers: any = {} if (this.config.authConfigs && authConfigId) { @@ -398,7 +411,7 @@ class RestIntegration implements IntegrationBase { headers = {}, method = HttpMethod.GET, disabledHeaders, - bodyType, + bodyType = BodyType.NONE, requestBody, authConfigId, pagination, @@ -407,7 +420,7 @@ class RestIntegration implements IntegrationBase { const authHeaders = this.getAuthHeaders(authConfigId) this.headers = { - ...this.config.defaultHeaders, + ...(this.config.defaultHeaders || {}), ...headers, ...authHeaders, } @@ -420,7 +433,7 @@ class RestIntegration implements IntegrationBase { } } - let input: any = { method, headers: this.headers } + let input: RequestInit = { method, headers: this.headers } input = this.addBody( bodyType, requestBody, @@ -437,7 +450,12 @@ class RestIntegration implements IntegrationBase { // Deprecated by rejectUnauthorized if (this.config.legacyHttpParser) { + // NOTE(samwho): it seems like this code doesn't actually work because it requires + // node-fetch >=3, and we're not on that because upgrading to it produces errors to + // do with ESM that are above my pay grade. + // https://github.com/nodejs/node/issues/43798 + // @ts-ignore input.extraHttpOptions = { insecureHTTPParser: true } } diff --git a/packages/server/src/integrations/tests/rest.spec.ts b/packages/server/src/integrations/tests/rest.spec.ts index dee17a5497..e869c58875 100644 --- a/packages/server/src/integrations/tests/rest.spec.ts +++ b/packages/server/src/integrations/tests/rest.spec.ts @@ -1,281 +1,222 @@ -jest.mock("node-fetch", () => { - const obj = { - my_next_cursor: 123, - } - const str = JSON.stringify(obj) - return jest.fn(() => ({ - headers: { - raw: () => { - return { - "content-type": ["application/json"], - "content-length": str.length, - } - }, - get: (name: string) => { - const lcName = name.toLowerCase() - if (lcName === "content-type") { - return ["application/json"] - } else if (lcName === "content-length") { - return str.length - } - }, - }, - json: jest.fn(() => obj), - text: jest.fn(() => str), - })) -}) - -jest.mock("@budibase/backend-core", () => { - const core = jest.requireActual("@budibase/backend-core") - return { - ...core, - context: { - ...core.context, - getProdAppId: jest.fn(() => "app-id"), - }, - } -}) -jest.mock("uuid", () => ({ v4: () => "00000000-0000-0000-0000-000000000000" })) - -import { default as RestIntegration } from "../rest" -import { RestAuthType } from "@budibase/types" -import fetch from "node-fetch" -import { Readable } from "stream" - -const FormData = require("form-data") -const { URLSearchParams } = require("url") +import nock from "nock" +import { RestIntegration } from "../rest" +import { BodyType, RestAuthType } from "@budibase/types" +import { Response } from "node-fetch" +import TestConfiguration from "../../../src/tests/utilities/TestConfiguration" +const UUID_REGEX = + "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}" const HEADERS = { Accept: "application/json", "Content-Type": "application/json", } -class TestConfiguration { - integration: any - - constructor(config: any = {}) { - this.integration = new RestIntegration.integration(config) - } -} - describe("REST Integration", () => { - const BASE_URL = "https://myapi.com" - let config: any + let integration: RestIntegration + const config = new TestConfiguration() + + beforeAll(async () => { + await config.init() + }) + + afterAll(async () => { + config.end() + }) beforeEach(() => { - config = new TestConfiguration({ - url: BASE_URL, - }) - jest.clearAllMocks() + integration = new RestIntegration({ url: "https://example.com" }) + nock.cleanAll() }) it("calls the create method with the correct params", async () => { - const query = { + const body = { name: "test" } + nock("https://example.com", { reqheaders: HEADERS }) + .post("/api?test=1", JSON.stringify(body)) + .reply(200, { foo: "bar" }) + + const { data } = await integration.create({ path: "api", queryString: "test=1", headers: HEADERS, - bodyType: "json", - requestBody: JSON.stringify({ - name: "test", - }), - } - await config.integration.create(query) - expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api?test=1`, { - method: "POST", - body: '{"name":"test"}', - headers: HEADERS, + bodyType: BodyType.JSON, + requestBody: JSON.stringify(body), }) + expect(data).toEqual({ foo: "bar" }) }) it("calls the read method with the correct params", async () => { - const query = { + nock("https://example.com") + .get("/api?test=1") + .matchHeader("Accept", "text/html") + .reply(200, { foo: "bar" }) + + const { data } = await integration.read({ path: "api", queryString: "test=1", headers: { Accept: "text/html", }, - } - await config.integration.read(query) - expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api?test=1`, { - headers: { - Accept: "text/html", - }, - method: "GET", }) + expect(data).toEqual({ foo: "bar" }) }) it("calls the update method with the correct params", async () => { - const query = { + nock("https://example.com") + .put("/api?test=1", { name: "test" }) + .matchHeader("Accept", "application/json") + .reply(200, { foo: "bar" }) + + const { data } = await integration.update({ path: "api", queryString: "test=1", headers: { Accept: "application/json", }, - bodyType: "json", + bodyType: BodyType.JSON, requestBody: JSON.stringify({ name: "test", }), - } - await config.integration.update(query) - expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api?test=1`, { - method: "PUT", - body: '{"name":"test"}', - headers: HEADERS, }) + expect(data).toEqual({ foo: "bar" }) }) it("calls the delete method with the correct params", async () => { - const query = { + nock("https://example.com") + .delete("/api?test=1", { name: "test" }) + .matchHeader("Accept", "application/json") + .reply(200, { foo: "bar" }) + + const { data } = await integration.delete({ path: "api", queryString: "test=1", headers: { Accept: "application/json", }, - bodyType: "json", + bodyType: BodyType.JSON, requestBody: JSON.stringify({ name: "test", }), - } - await config.integration.delete(query) - expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api?test=1`, { - method: "DELETE", - headers: HEADERS, - body: '{"name":"test"}', }) + expect(data).toEqual({ foo: "bar" }) }) describe("request body", () => { const input = { a: 1, b: 2 } it("should allow no body", () => { - const output = config.integration.addBody("none", null, {}) + const output = integration.addBody("none", null, {}) expect(output.body).toBeUndefined() - expect(Object.keys(output.headers).length).toEqual(0) + expect(Object.keys(output.headers!).length).toEqual(0) }) it("should allow text body", () => { - const output = config.integration.addBody("text", "hello world", {}) + const output = integration.addBody("text", "hello world", {}) expect(output.body).toEqual("hello world") // gets added by fetch - expect(Object.keys(output.headers).length).toEqual(0) + expect(Object.keys(output.headers!).length).toEqual(0) }) it("should allow form data", () => { const FormData = require("form-data") - const output = config.integration.addBody("form", input, {}) + const output = integration.addBody("form", input, {}) expect(output.body instanceof FormData).toEqual(true) - expect(output.body._valueLength).toEqual(2) + expect((output.body! as any)._valueLength).toEqual(2) // gets added by fetch - expect(Object.keys(output.headers).length).toEqual(0) + expect(Object.keys(output.headers!).length).toEqual(0) }) it("should allow encoded form data", () => { const { URLSearchParams } = require("url") - const output = config.integration.addBody("encoded", input, {}) + const output = integration.addBody("encoded", input, {}) expect(output.body instanceof URLSearchParams).toEqual(true) - expect(output.body.toString()).toEqual("a=1&b=2") + expect(output.body!.toString()).toEqual("a=1&b=2") // gets added by fetch - expect(Object.keys(output.headers).length).toEqual(0) + expect(Object.keys(output.headers!).length).toEqual(0) }) it("should allow JSON", () => { - const output = config.integration.addBody("json", input, {}) + const output = integration.addBody("json", input, {}) expect(output.body).toEqual(JSON.stringify(input)) - expect(output.headers["Content-Type"]).toEqual("application/json") + expect((output.headers! as any)["Content-Type"]).toEqual( + "application/json" + ) }) it("should allow raw XML", () => { - const output = config.integration.addBody("xml", "12", {}) - expect(output.body.includes("1")).toEqual(true) - expect(output.body.includes("2")).toEqual(true) - expect(output.headers["Content-Type"]).toEqual("application/xml") + const output = integration.addBody("xml", "12", {}) + const body = output.body?.toString() + expect(body!.includes("1")).toEqual(true) + expect(body!.includes("2")).toEqual(true) + expect((output.headers! as any)["Content-Type"]).toEqual( + "application/xml" + ) }) it("should allow a valid js object and parse the contents to xml", () => { - const output = config.integration.addBody("xml", input, {}) - expect(output.body.includes("1")).toEqual(true) - expect(output.body.includes("2")).toEqual(true) - expect(output.headers["Content-Type"]).toEqual("application/xml") + const output = integration.addBody("xml", input, {}) + const body = output.body?.toString() + expect(body!.includes("1")).toEqual(true) + expect(body!.includes("2")).toEqual(true) + expect((output.headers! as any)["Content-Type"]).toEqual( + "application/xml" + ) }) it("should allow a valid json string and parse the contents to xml", () => { - const output = config.integration.addBody( - "xml", - JSON.stringify(input), - {} + const output = integration.addBody("xml", JSON.stringify(input), {}) + const body = output.body?.toString() + expect(body!.includes("1")).toEqual(true) + expect(body!.includes("2")).toEqual(true) + expect((output.headers! as any)["Content-Type"]).toEqual( + "application/xml" ) - expect(output.body.includes("1")).toEqual(true) - expect(output.body.includes("2")).toEqual(true) - expect(output.headers["Content-Type"]).toEqual("application/xml") }) }) describe("response", () => { - const contentTypes = ["application/json", "text/plain", "application/xml"] - function buildInput( - json: any, - text: any, - header: any, - status: number = 200 - ) { - return { - status, - json: json ? async () => json : undefined, - text: text ? async () => text : undefined, - headers: { - get: (key: string) => { - switch (key.toLowerCase()) { - case "content-length": - return 100 - case "content-type": - return header - default: - return "" - } - }, - raw: () => ({ "content-type": header }), - }, - } - } - it("should be able to parse JSON response", async () => { const obj = { a: 1 } - const input = buildInput(obj, JSON.stringify(obj), "application/json") - const output = await config.integration.parseResponse(input) - expect(output.data).toEqual({ a: 1 }) + const output = await integration.parseResponse( + new Response(JSON.stringify(obj), { + headers: { "content-type": "application/json" }, + }) + ) + expect(output.data).toEqual(obj) expect(output.info.code).toEqual(200) - expect(output.info.size).toEqual("100B") - expect(output.extra.raw).toEqual(JSON.stringify({ a: 1 })) - expect(output.extra.headers["content-type"]).toEqual("application/json") + expect(output.info.size).toEqual("7B") }) it("should be able to parse text response", async () => { const text = "hello world" - const input = buildInput(null, text, "text/plain") - const output = await config.integration.parseResponse(input) + const output = await integration.parseResponse( + new Response(text, { + headers: { "content-type": "text/plain" }, + }) + ) expect(output.data).toEqual(text) - expect(output.extra.raw).toEqual(text) - expect(output.extra.headers["content-type"]).toEqual("text/plain") }) it("should be able to parse XML response", async () => { const text = "12" - const input = buildInput(null, text, "application/xml") - const output = await config.integration.parseResponse(input) + const output = await integration.parseResponse( + new Response(text, { + headers: { "content-type": "application/xml" }, + }) + ) expect(output.data).toEqual({ a: "1", b: "2" }) - expect(output.extra.raw).toEqual(text) - expect(output.extra.headers["content-type"]).toEqual("application/xml") }) - test.each([...contentTypes, undefined])( - "should not throw an error on 204 no content", + test.each(["application/json", "text/plain", "application/xml", undefined])( + "should not throw an error on 204 no content for content type: %s", async contentType => { - const input = buildInput(undefined, "", contentType, 204) - const output = await config.integration.parseResponse(input) + const output = await integration.parseResponse( + new Response(undefined, { + headers: { "content-type": contentType! }, + status: 204, + }) + ) expect(output.data).toEqual([]) - expect(output.extra.raw).toEqual("") expect(output.info.code).toEqual(204) - expect(output.extra.headers["content-type"]).toEqual(contentType) } ) }) @@ -301,443 +242,311 @@ describe("REST Integration", () => { } beforeEach(() => { - config = new TestConfiguration({ - url: BASE_URL, + integration = new RestIntegration({ + url: "https://example.com", authConfigs: [basicAuth, bearerAuth], }) }) it("adds basic auth", async () => { - const query = { - authConfigId: "c59c14bd1898a43baa08da68959b24686", - } - await config.integration.read(query) - expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/`, { - method: "GET", - headers: { - Authorization: "Basic dXNlcjpwYXNzd29yZA==", - }, - }) + const auth = `Basic ${Buffer.from("user:password").toString("base64")}` + nock("https://example.com", { reqheaders: { Authorization: auth } }) + .get("/") + .reply(200, { foo: "bar" }) + + const { data } = await integration.read({ authConfigId: basicAuth._id }) + expect(data).toEqual({ foo: "bar" }) }) it("adds bearer auth", async () => { - const query = { - authConfigId: "0d91d732f34e4befabeff50b392a8ff3", - } - await config.integration.read(query) - expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/`, { - method: "GET", - headers: { - Authorization: "Bearer mytoken", - }, + nock("https://example.com", { + reqheaders: { Authorization: "Bearer mytoken" }, }) + .get("/") + .reply(200, { foo: "bar" }) + const { data } = await integration.read({ authConfigId: bearerAuth._id }) + expect(data).toEqual({ foo: "bar" }) }) }) describe("page based pagination", () => { it("can paginate using query params", async () => { - const pageParam = "my_page_param" - const sizeParam = "my_size_param" - const pageValue = 3 - const sizeValue = 10 - const query = { + nock("https://example.com") + .get("/api?page=3&size=10") + .reply(200, { foo: "bar" }) + const { data } = await integration.read({ path: "api", pagination: { type: "page", location: "query", - pageParam, - sizeParam, + pageParam: "page", + sizeParam: "size", }, - paginationValues: { - page: pageValue, - limit: sizeValue, - }, - } - await config.integration.read(query) - expect(fetch).toHaveBeenCalledWith( - `${BASE_URL}/api?${pageParam}=${pageValue}&${sizeParam}=${sizeValue}`, - { - headers: {}, - method: "GET", - } - ) + paginationValues: { page: 3, limit: 10 }, + }) + expect(data).toEqual({ foo: "bar" }) }) it("can paginate using JSON request body", async () => { - const pageParam = "my_page_param" - const sizeParam = "my_size_param" - const pageValue = 3 - const sizeValue = 10 - const query = { - bodyType: "json", + nock("https://example.com") + .post("/api", JSON.stringify({ page: 3, size: 10 })) + .reply(200, { foo: "bar" }) + const { data } = await integration.create({ + bodyType: BodyType.JSON, path: "api", pagination: { type: "page", location: "body", - pageParam, - sizeParam, + pageParam: "page", + sizeParam: "size", }, - paginationValues: { - page: pageValue, - limit: sizeValue, - }, - } - await config.integration.create(query) - expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api`, { - body: JSON.stringify({ - [pageParam]: pageValue, - [sizeParam]: sizeValue, - }), - headers: { - "Content-Type": "application/json", - }, - method: "POST", + paginationValues: { page: 3, limit: 10 }, }) + expect(data).toEqual({ foo: "bar" }) }) it("can paginate using form-data request body", async () => { - const pageParam = "my_page_param" - const sizeParam = "my_size_param" - const pageValue = 3 - const sizeValue = 10 - const query = { - bodyType: "form", + nock("https://example.com") + .post("/api", body => { + return ( + body.includes(`name="page"\r\n\r\n3\r\n`) && + body.includes(`name="size"\r\n\r\n10\r\n`) + ) + }) + .reply(200, { foo: "bar" }) + + const { data } = await integration.create({ + bodyType: BodyType.FORM_DATA, path: "api", pagination: { type: "page", location: "body", - pageParam, - sizeParam, + pageParam: "page", + sizeParam: "size", }, - paginationValues: { - page: pageValue, - limit: sizeValue, - }, - } - await config.integration.create(query) - expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api`, { - body: expect.any(FormData), - headers: {}, - method: "POST", + paginationValues: { page: 3, limit: 10 }, }) - // @ts-ignore - const sentData = JSON.stringify(fetch.mock.calls[0][1].body) - expect(sentData).toContain(pageParam) - expect(sentData).toContain(sizeParam) + expect(data).toEqual({ foo: "bar" }) }) it("can paginate using form-encoded request body", async () => { - const pageParam = "my_page_param" - const sizeParam = "my_size_param" - const pageValue = 3 - const sizeValue = 10 - const query = { - bodyType: "encoded", + nock("https://example.com") + .post("/api", { page: "3", size: "10" }) + .reply(200, { foo: "bar" }) + + const { data } = await integration.create({ + bodyType: BodyType.ENCODED, path: "api", pagination: { type: "page", location: "body", - pageParam, - sizeParam, + pageParam: "page", + sizeParam: "size", }, - paginationValues: { - page: pageValue, - limit: sizeValue, - }, - } - await config.integration.create(query) - expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api`, { - body: expect.any(URLSearchParams), - headers: {}, - method: "POST", + paginationValues: { page: 3, limit: 10 }, }) - // @ts-ignore - const sentData = fetch.mock.calls[0][1].body - expect(sentData.has(pageParam)).toEqual(true) - expect(sentData.get(pageParam)).toEqual(pageValue.toString()) - expect(sentData.has(pageParam)).toEqual(true) - expect(sentData.get(sizeParam)).toEqual(sizeValue.toString()) + expect(data).toEqual({ foo: "bar" }) }) }) describe("cursor based pagination", () => { it("can paginate using query params", async () => { - const pageParam = "my_page_param" - const sizeParam = "my_size_param" - const pageValue = 3 - const sizeValue = 10 - const query = { + nock("https://example.com") + .get("/api?page=3&size=10") + .reply(200, { cursor: 123, foo: "bar" }) + const { data, pagination } = await integration.read({ path: "api", pagination: { type: "cursor", location: "query", - pageParam, - sizeParam, - responseParam: "my_next_cursor", + pageParam: "page", + sizeParam: "size", + responseParam: "cursor", }, - paginationValues: { - page: pageValue, - limit: sizeValue, - }, - } - const res = await config.integration.read(query) - expect(fetch).toHaveBeenCalledWith( - `${BASE_URL}/api?${pageParam}=${pageValue}&${sizeParam}=${sizeValue}`, - { - headers: {}, - method: "GET", - } - ) - expect(res.pagination.cursor).toEqual(123) + paginationValues: { page: 3, limit: 10 }, + }) + expect(pagination?.cursor).toEqual(123) + expect(data).toEqual({ cursor: 123, foo: "bar" }) }) it("can paginate using JSON request body", async () => { - const pageParam = "my_page_param" - const sizeParam = "my_size_param" - const pageValue = 3 - const sizeValue = 10 - const query = { - bodyType: "json", + nock("https://example.com") + .post("/api", JSON.stringify({ page: 3, size: 10 })) + .reply(200, { cursor: 123, foo: "bar" }) + const { data, pagination } = await integration.create({ + bodyType: BodyType.JSON, path: "api", pagination: { type: "page", location: "body", - pageParam, - sizeParam, - responseParam: "my_next_cursor", + pageParam: "page", + sizeParam: "size", + responseParam: "cursor", }, - paginationValues: { - page: pageValue, - limit: sizeValue, - }, - } - const res = await config.integration.create(query) - expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api`, { - body: JSON.stringify({ - [pageParam]: pageValue, - [sizeParam]: sizeValue, - }), - headers: { - "Content-Type": "application/json", - }, - method: "POST", + paginationValues: { page: 3, limit: 10 }, }) - expect(res.pagination.cursor).toEqual(123) + expect(data).toEqual({ cursor: 123, foo: "bar" }) + expect(pagination?.cursor).toEqual(123) }) it("can paginate using form-data request body", async () => { - const pageParam = "my_page_param" - const sizeParam = "my_size_param" - const pageValue = 3 - const sizeValue = 10 - const query = { - bodyType: "form", + nock("https://example.com") + .post("/api", body => { + return ( + body.includes(`name="page"\r\n\r\n3\r\n`) && + body.includes(`name="size"\r\n\r\n10\r\n`) + ) + }) + .reply(200, { cursor: 123, foo: "bar" }) + const { data, pagination } = await integration.create({ + bodyType: BodyType.FORM_DATA, path: "api", pagination: { type: "page", location: "body", - pageParam, - sizeParam, - responseParam: "my_next_cursor", + pageParam: "page", + sizeParam: "size", + responseParam: "cursor", }, - paginationValues: { - page: pageValue, - limit: sizeValue, - }, - } - const res = await config.integration.create(query) - expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api`, { - body: expect.any(FormData), - headers: {}, - method: "POST", + paginationValues: { page: 3, limit: 10 }, }) - // @ts-ignore - const sentData = JSON.stringify(fetch.mock.calls[0][1].body) - expect(sentData).toContain(pageParam) - expect(sentData).toContain(sizeParam) - expect(res.pagination.cursor).toEqual(123) + expect(data).toEqual({ cursor: 123, foo: "bar" }) + expect(pagination?.cursor).toEqual(123) }) it("can paginate using form-encoded request body", async () => { - const pageParam = "my_page_param" - const sizeParam = "my_size_param" - const pageValue = 3 - const sizeValue = 10 - const query = { - bodyType: "encoded", + nock("https://example.com") + .post("/api", { page: "3", size: "10" }) + .reply(200, { cursor: 123, foo: "bar" }) + const { data, pagination } = await integration.create({ + bodyType: BodyType.ENCODED, path: "api", pagination: { type: "page", location: "body", - pageParam, - sizeParam, - responseParam: "my_next_cursor", + pageParam: "page", + sizeParam: "size", + responseParam: "cursor", }, - paginationValues: { - page: pageValue, - limit: sizeValue, - }, - } - const res = await config.integration.create(query) - expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api`, { - body: expect.any(URLSearchParams), - headers: {}, - method: "POST", + paginationValues: { page: 3, limit: 10 }, }) - // @ts-ignore - const sentData = fetch.mock.calls[0][1].body - expect(sentData.has(pageParam)).toEqual(true) - expect(sentData.get(pageParam)).toEqual(pageValue.toString()) - expect(sentData.has(pageParam)).toEqual(true) - expect(sentData.get(sizeParam)).toEqual(sizeValue.toString()) - expect(res.pagination.cursor).toEqual(123) + expect(data).toEqual({ cursor: 123, foo: "bar" }) + expect(pagination?.cursor).toEqual(123) }) it("should encode query string correctly", async () => { - const query = { + nock("https://example.com", { reqheaders: HEADERS }) + .post("/api?test=1%202", JSON.stringify({ name: "test" })) + .reply(200, { foo: "bar" }) + const { data } = await integration.create({ path: "api", queryString: "test=1 2", headers: HEADERS, - bodyType: "json", + bodyType: BodyType.JSON, requestBody: JSON.stringify({ name: "test", }), - } - await config.integration.create(query) - expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api?test=1%202`, { - method: "POST", - body: '{"name":"test"}', - headers: HEADERS, }) + expect(data).toEqual({ foo: "bar" }) }) }) describe("Configuration options", () => { - it("Attaches insecureHttpParams when legacy HTTP Parser option is set", async () => { - config = new TestConfiguration({ - url: BASE_URL, + // NOTE(samwho): it seems like this code doesn't actually work because it requires + // node-fetch >=3, and we're not on that because upgrading to it produces errors to + // do with ESM that are above my pay grade. + + // eslint-disable-next-line jest/no-commented-out-tests + // it("doesn't fail when legacyHttpParser is set", async () => { + // const server = createServer((req, res) => { + // res.writeHead(200, { + // "Transfer-Encoding": "chunked", + // "Content-Length": "10", + // }) + // res.end(JSON.stringify({ foo: "bar" })) + // }) + + // server.listen() + // await new Promise(resolve => server.once("listening", resolve)) + + // const address = server.address() as AddressInfo + + // const integration = new RestIntegration({ + // url: `http://localhost:${address.port}`, + // legacyHttpParser: true, + // }) + // const { data } = await integration.read({}) + // expect(data).toEqual({ foo: "bar" }) + // }) + + it("doesn't fail when legacyHttpParser is true", async () => { + nock("https://example.com").get("/").reply(200, { foo: "bar" }) + const integration = new RestIntegration({ + url: "https://example.com", legacyHttpParser: true, }) - await config.integration.read({}) - expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/`, { - method: "GET", - headers: {}, - extraHttpOptions: { - insecureHTTPParser: true, - }, + const { data } = await integration.read({}) + expect(data).toEqual({ foo: "bar" }) + }) + + it("doesn't fail when rejectUnauthorized is false", async () => { + nock("https://example.com").get("/").reply(200, { foo: "bar" }) + const integration = new RestIntegration({ + url: "https://example.com", + rejectUnauthorized: false, }) + const { data } = await integration.read({}) + expect(data).toEqual({ foo: "bar" }) }) }) - it("Attaches custom agent when Reject Unauthorized option is false", async () => { - config = new TestConfiguration({ - url: BASE_URL, - rejectUnauthorized: false, - }) - await config.integration.read({}) - - // @ts-ignore - const calls: any = fetch.mock.calls[0] - const url = calls[0] - expect(url).toBe(`${BASE_URL}/`) - - const calledConfig = calls[1] - expect(calledConfig.method).toBe("GET") - expect(calledConfig.headers).toEqual({}) - expect(calledConfig.agent.options.rejectUnauthorized).toBe(false) - }) - describe("File Handling", () => { it("uploads file to object store and returns signed URL", async () => { - const responseData = Buffer.from("teest file contnt") - const filename = "test.tar.gz" - const contentType = "application/gzip" - const mockReadable = new Readable() - mockReadable.push(responseData) - mockReadable.push(null) - ;(fetch as unknown as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ - status: 200, - headers: { - raw: () => ({ - "content-type": [contentType], - "content-disposition": [`attachment; filename="${filename}"`], - }), - get: (header: any) => { - if (header === "content-type") return contentType - if (header === "content-length") return responseData.byteLength - if (header === "content-disposition") - return `attachment; filename="${filename}"` - }, - }, - body: mockReadable, + await config.doInContext(config.getAppId(), async () => { + const content = "test file content" + nock("https://example.com").get("/api").reply(200, content, { + "content-disposition": `attachment; filename="testfile.tar.gz"`, + "content-type": "text/plain", }) - ) - const query = { - path: "api", - } - - const response = await config.integration.read(query) - - expect(response.data).toEqual({ - size: responseData.byteLength, - name: "00000000-0000-0000-0000-000000000000.tar.gz", - url: expect.stringContaining( - "/files/signed/tmp-file-attachments/app-id/00000000-0000-0000-0000-000000000000.tar.gz" - ), - extension: "tar.gz", - key: expect.stringContaining( - "app-id/00000000-0000-0000-0000-000000000000.tar.gz" - ), + const { data } = await integration.read({ path: "api" }) + expect(data).toEqual({ + size: content.length, + name: expect.stringMatching(new RegExp(`^${UUID_REGEX}.tar.gz$`)), + url: expect.stringMatching( + new RegExp( + `^/files/signed/tmp-file-attachments/app.*?/${UUID_REGEX}.tar.gz.*$` + ) + ), + extension: "tar.gz", + key: expect.stringMatching( + new RegExp(`^app.*?/${UUID_REGEX}.tar.gz$`) + ), + }) }) }) it("uploads file with non ascii filename to object store and returns signed URL", async () => { - const responseData = Buffer.from("teest file contnt") - const contentType = "text/plain" - const mockReadable = new Readable() - mockReadable.push(responseData) - mockReadable.push(null) - ;(fetch as unknown as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ - status: 200, - headers: { - raw: () => ({ - "content-type": [contentType], - "content-disposition": [ - // eslint-disable-next-line no-useless-escape - `attachment; filename="£ and ? rates.pdf"; filename*=UTF-8'\'%C2%A3%20and%20%E2%82%AC%20rates.pdf`, - ], - }), - get: (header: any) => { - if (header === "content-type") return contentType - if (header === "content-length") return responseData.byteLength - if (header === "content-disposition") - // eslint-disable-next-line no-useless-escape - return `attachment; filename="£ and ? rates.pdf"; filename*=UTF-8'\'%C2%A3%20and%20%E2%82%AC%20rates.pdf` - }, - }, - body: mockReadable, + await config.doInContext(config.getAppId(), async () => { + const content = "test file content" + nock("https://example.com").get("/api").reply(200, content, { + // eslint-disable-next-line no-useless-escape + "content-disposition": `attachment; filename="£ and ? rates.pdf"; filename*=UTF-8'\'%C2%A3%20and%20%E2%82%AC%20rates.pdf`, + "content-type": "text/plain", }) - ) - const query = { - path: "api", - } - - const response = await config.integration.read(query) - - expect(response.data).toEqual({ - size: responseData.byteLength, - name: "00000000-0000-0000-0000-000000000000.pdf", - url: expect.stringContaining( - "/files/signed/tmp-file-attachments/app-id/00000000-0000-0000-0000-000000000000.pdf" - ), - extension: "pdf", - key: expect.stringContaining( - "app-id/00000000-0000-0000-0000-000000000000.pdf" - ), + const { data } = await integration.read({ path: "api" }) + expect(data).toEqual({ + size: content.length, + name: expect.stringMatching(new RegExp(`^${UUID_REGEX}.pdf$`)), + url: expect.stringMatching( + new RegExp( + `^/files/signed/tmp-file-attachments/app.*?/${UUID_REGEX}.pdf.*$` + ) + ), + extension: "pdf", + key: expect.stringMatching(new RegExp(`^app.*?/${UUID_REGEX}.pdf$`)), + }) }) }) }) diff --git a/packages/types/src/documents/app/datasource.ts b/packages/types/src/documents/app/datasource.ts index e52019fc18..a0be7bd80d 100644 --- a/packages/types/src/documents/app/datasource.ts +++ b/packages/types/src/documents/app/datasource.ts @@ -45,15 +45,15 @@ export interface DynamicVariable { export interface RestConfig { url: string - rejectUnauthorized: boolean + rejectUnauthorized?: boolean downloadImages?: boolean - defaultHeaders: { + defaultHeaders?: { [key: string]: any } - legacyHttpParser: boolean - authConfigs: RestAuthConfig[] - staticVariables: { + legacyHttpParser?: boolean + authConfigs?: RestAuthConfig[] + staticVariables?: { [key: string]: string } - dynamicVariables: DynamicVariable[] + dynamicVariables?: DynamicVariable[] } diff --git a/packages/types/src/documents/app/query.ts b/packages/types/src/documents/app/query.ts index baba4def95..a545ca144e 100644 --- a/packages/types/src/documents/app/query.ts +++ b/packages/types/src/documents/app/query.ts @@ -36,31 +36,39 @@ export interface QueryResponse { pagination: any } +export enum BodyType { + NONE = "none", + FORM_DATA = "form", + XML = "xml", + ENCODED = "encoded", + JSON = "json", + TEXT = "text", +} + export interface RestQueryFields { - path: string + path?: string queryString?: string - headers: { [key: string]: any } - disabledHeaders: { [key: string]: any } - requestBody: any - bodyType: string - json: object - method: string - authConfigId: string - pagination: PaginationConfig | null - paginationValues: PaginationValues | null + headers?: { [key: string]: any } + disabledHeaders?: { [key: string]: any } + requestBody?: any + bodyType?: BodyType + method?: string + authConfigId?: string + pagination?: PaginationConfig + paginationValues?: PaginationValues } export interface PaginationConfig { type: string location: string pageParam: string - sizeParam: string | null - responseParam: string | null + sizeParam?: string + responseParam?: string } export interface PaginationValues { - page: string | number | null - limit: number | null + page?: string | number + limit?: number } export enum HttpMethod { diff --git a/yarn.lock b/yarn.lock index 2d69b37cc6..607db0b7bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6707,22 +6707,39 @@ acorn-import-assertions@^1.9.0: resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== -acorn-jsx@^5.3.2: +acorn-jsx-walk@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/acorn-jsx-walk/-/acorn-jsx-walk-2.0.0.tgz#a5ed648264e68282d7c2aead80216bfdf232573a" + integrity sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA== + +acorn-jsx@5.3.2, acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== +acorn-loose@8.4.0: + version "8.4.0" + resolved "https://registry.yarnpkg.com/acorn-loose/-/acorn-loose-8.4.0.tgz#26d3e219756d1e180d006f5bcc8d261a28530f55" + integrity sha512-M0EUka6rb+QC4l9Z3T0nJEzNOO7JcoJlYMrBlyBCiFSXRyxjLKayd4TbQs2FDRWQU1h9FR7QVNHt+PEaoNL5rQ== + dependencies: + acorn "^8.11.0" + +acorn-walk@8.3.3, acorn-walk@^8.0.2, acorn-walk@^8.1.1, acorn-walk@^8.2.0, acorn-walk@^8.3.2: + version "8.3.3" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.3.tgz#9caeac29eefaa0c41e3d4c65137de4d6f34df43e" + integrity sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw== + dependencies: + acorn "^8.11.0" + acorn-walk@^7.1.1: version "7.2.0" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== -acorn-walk@^8.0.2, acorn-walk@^8.1.1, acorn-walk@^8.2.0, acorn-walk@^8.3.2: - version "8.3.3" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.3.tgz#9caeac29eefaa0c41e3d4c65137de4d6f34df43e" - integrity sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw== - dependencies: - acorn "^8.11.0" +acorn@8.12.1: + version "8.12.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" + integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== acorn@^5.2.1, acorn@^5.7.3: version "5.7.4" @@ -6791,6 +6808,16 @@ ajv-formats@^2.0.2: dependencies: ajv "^8.0.0" +ajv@8.17.1: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" + integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== + dependencies: + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + ajv@^6.12.3, ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -8484,6 +8511,11 @@ combos@^0.2.0: resolved "https://registry.yarnpkg.com/combos/-/combos-0.2.0.tgz#dc31c5a899b42293d55fe19c064d3e6e207ba4f7" integrity sha512-Z6YfvgiTCERWJTj3wQiXamFhssdvz1n4ok447rS330lw3uL72WAx8IvrLU7xiE71uyb5WF8JEP+BWB5KhOoGeg== +commander@12.1.0: + version "12.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3" + integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== + commander@6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.0.tgz#b990bfb8ac030aedc6d11bc04d1488ffef56db75" @@ -9551,6 +9583,34 @@ depd@^1.1.0, depd@~1.1.2: resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== +dependency-cruiser@^16.3.7: + version "16.3.10" + resolved "https://registry.yarnpkg.com/dependency-cruiser/-/dependency-cruiser-16.3.10.tgz#fe26a50d5e10a4496bc2b70d027fca6ded48814f" + integrity sha512-WkCnibHBfvaiaQ+S46LZ6h4AR6oj42Vsf5/0Vgtrwdwn7ZekMJdZ/ALoTwNp/RaGlKW+MbV/fhSZOvmhAWVWzQ== + dependencies: + acorn "8.12.1" + acorn-jsx "5.3.2" + acorn-jsx-walk "2.0.0" + acorn-loose "8.4.0" + acorn-walk "8.3.3" + ajv "8.17.1" + commander "12.1.0" + enhanced-resolve "5.17.1" + ignore "5.3.1" + interpret "^3.1.1" + is-installed-globally "1.0.0" + json5 "2.2.3" + memoize "10.0.0" + picocolors "1.0.1" + picomatch "4.0.2" + prompts "2.4.2" + rechoir "^0.8.0" + safe-regex "2.1.1" + semver "^7.6.3" + teamcity-service-messages "0.1.14" + tsconfig-paths-webpack-plugin "4.1.0" + watskeburt "4.1.0" + dependency-tree@^9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/dependency-tree/-/dependency-tree-9.0.0.tgz#9288dd6daf35f6510c1ea30d9894b75369aa50a2" @@ -10221,6 +10281,14 @@ engine.io@~6.5.2: engine.io-parser "~5.2.1" ws "~8.17.1" +enhanced-resolve@5.17.1, enhanced-resolve@^5.7.0: + version "5.17.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" + integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + enhanced-resolve@^5.8.3: version "5.14.1" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.14.1.tgz#de684b6803724477a4af5d74ccae5de52c25f6b3" @@ -11016,6 +11084,11 @@ fast-text-encoding@^1.0.0: resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz#0aa25f7f638222e3396d72bf936afcf1d42d6867" integrity sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w== +fast-uri@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.1.tgz#cddd2eecfc83a71c1be2cc2ef2061331be8a7134" + integrity sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw== + fast-url-parser@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/fast-url-parser/-/fast-url-parser-1.1.3.tgz#f4af3ea9f34d8a271cf58ad2b3759f431f0b318d" @@ -11877,6 +11950,13 @@ global-agent@3.0.0: semver "^7.3.2" serialize-error "^7.0.1" +global-directory@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/global-directory/-/global-directory-4.0.1.tgz#4d7ac7cfd2cb73f304c53b8810891748df5e361e" + integrity sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q== + dependencies: + ini "4.1.1" + global-dirs@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.1.tgz#0c488971f066baceda21447aecb1a8b911d22485" @@ -12541,6 +12621,11 @@ ignore-walk@^6.0.0: dependencies: minimatch "^7.4.2" +ignore@5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" + integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== + ignore@^5.0.4, ignore@^5.2.0, ignore@^5.2.4: version "5.3.0" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78" @@ -12666,6 +12751,11 @@ ini@2.0.0: resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== +ini@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ini/-/ini-4.1.1.tgz#d95b3d843b1e906e56d6747d5447904ff50ce7a1" + integrity sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g== + ini@^1.3.2, ini@^1.3.4, ini@^1.3.8, ini@~1.3.0: version "1.3.8" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" @@ -12743,6 +12833,11 @@ interpret@^2.2.0: resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== +interpret@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4" + integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ== + into-stream@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/into-stream/-/into-stream-3.1.0.tgz#96fb0a936c12babd6ff1752a17d05616abd094c6" @@ -12973,6 +13068,14 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-installed-globally@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-1.0.0.tgz#08952c43758c33d815692392f7f8437b9e436d5a" + integrity sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ== + dependencies: + global-directory "^4.0.1" + is-path-inside "^4.0.0" + is-installed-globally@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520" @@ -13060,6 +13163,11 @@ is-path-inside@^3.0.2, is-path-inside@^3.0.3: resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== +is-path-inside@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-4.0.0.tgz#805aeb62c47c1b12fc3fd13bfb3ed1e7430071db" + integrity sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA== + is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" @@ -14084,6 +14192,11 @@ json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== +json5@2.2.3, json5@^2.2.1, json5@^2.2.2, json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + json5@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" @@ -14091,11 +14204,6 @@ json5@^1.0.2: dependencies: minimist "^1.2.0" -json5@^2.2.1, json5@^2.2.2, json5@^2.2.3: - version "2.2.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" - integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== - jsonc-parser@3.2.0, jsonc-parser@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76" @@ -15441,6 +15549,13 @@ memdown@^5.1.0: ltgt "~2.2.0" safe-buffer "~5.2.0" +memoize@10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/memoize/-/memoize-10.0.0.tgz#43fa66b2022363c7c50cf5dfab732a808a3d7147" + integrity sha512-H6cBLgsi6vMWOcCpvVCdFFnl3kerEXbrYh9q+lY6VXvQSmM6CkmV08VOwT+WE2tzIEqRPFfAq3fm4v/UIW6mSA== + dependencies: + mimic-function "^5.0.0" + memory-pager@^1.0.2: version "1.5.0" resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5" @@ -15549,6 +15664,11 @@ mimic-fn@^4.0.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== +mimic-function@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/mimic-function/-/mimic-function-5.0.1.tgz#acbe2b3349f99b9deaca7fb70e48b83e94e67076" + integrity sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA== + mimic-response@^1.0.0, mimic-response@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" @@ -17412,15 +17532,20 @@ phin@^2.9.1: resolved "https://registry.yarnpkg.com/phin/-/phin-2.9.3.tgz#f9b6ac10a035636fb65dfc576aaaa17b8743125c" integrity sha512-CzFr90qM24ju5f88quFC/6qohjC144rehe5n6DH900lgXmUe86+xCKc10ev56gRKC4/BkHUoG4uSiQgBiIXwDA== +picocolors@1.0.1, picocolors@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" + integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== + picocolors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== -picocolors@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" - integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== +picomatch@4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.2.tgz#77c742931e8f3b8820946c76cd0c1f13730d1dab" + integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg== picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" @@ -18385,7 +18510,7 @@ promise.series@^0.2.0: resolved "https://registry.yarnpkg.com/promise.series/-/promise.series-0.2.0.tgz#2cc7ebe959fc3a6619c04ab4dbdc9e452d864bbd" integrity sha512-VWQJyU2bcDTgZw8kpfBpB/ejZASlCrzwz5f2hjb/zlujOEB4oeiAhHygAWq8ubsX2GVkD4kCU5V2dwOTaCY5EQ== -prompts@^2.0.1: +prompts@2.4.2, prompts@^2.0.1: version "2.4.2" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== @@ -18952,6 +19077,11 @@ regenerator-transform@^0.15.1: dependencies: "@babel/runtime" "^7.8.4" +regexp-tree@~0.1.1: + version "0.1.27" + resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.27.tgz#2198f0ef54518ffa743fe74d983b56ffd631b6cd" + integrity sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA== + regexp.prototype.flags@^1.4.3, regexp.prototype.flags@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" @@ -19492,6 +19622,13 @@ safe-regex-test@^1.0.3: es-errors "^1.3.0" is-regex "^1.1.4" +safe-regex@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-2.1.1.tgz#f7128f00d056e2fe5c11e81a1324dd974aadced2" + integrity sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A== + dependencies: + regexp-tree "~0.1.1" + safe-stable-stringify@^2.1.0, safe-stable-stringify@^2.3.1: version "2.4.3" resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz#138c84b6f6edb3db5f8ef3ef7115b8f55ccbf886" @@ -19603,7 +19740,7 @@ semver-diff@^3.1.1: dependencies: semver "^6.3.0" -"semver@2 || 3 || 4 || 5", semver@7.5.3, semver@^5.5.0, semver@^5.6.0, semver@^5.7.1, semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0, semver@^6.3.1, semver@^7.0.0, semver@^7.1.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4, semver@~2.3.1: +"semver@2 || 3 || 4 || 5", semver@7.5.3, semver@^5.5.0, semver@^5.6.0, semver@^5.7.1, semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0, semver@^6.3.1, semver@^7.0.0, semver@^7.1.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4, semver@^7.6.3, semver@~2.3.1: version "7.5.3" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.3.tgz#161ce8c2c6b4b3bdca6caadc9fa3317a4c4fe88e" integrity sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ== @@ -20867,6 +21004,11 @@ tarn@^3.0.1, tarn@^3.0.2: resolved "https://registry.yarnpkg.com/tarn/-/tarn-3.0.2.tgz#73b6140fbb881b71559c4f8bfde3d9a4b3d27693" integrity sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ== +teamcity-service-messages@0.1.14: + version "0.1.14" + resolved "https://registry.yarnpkg.com/teamcity-service-messages/-/teamcity-service-messages-0.1.14.tgz#193d420a5e4aef8e5e50b8c39e7865e08fbb5d8a" + integrity sha512-29aQwaHqm8RMX74u2o/h1KbMLP89FjNiMxD9wbF2BbWOnbM+q+d1sCEC+MqCc4QW3NJykn77OMpTFw/xTHIc0w== + tedious@^16.4.0: version "16.7.1" resolved "https://registry.yarnpkg.com/tedious/-/tedious-16.7.1.tgz#1190f30fd99a413f1dc9250dee4835cf0788b650" @@ -21258,6 +21400,15 @@ ts-node@10.8.1: v8-compile-cache-lib "^3.0.1" yn "3.1.1" +tsconfig-paths-webpack-plugin@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.1.0.tgz#3c6892c5e7319c146eee1e7302ed9e6f2be4f763" + integrity sha512-xWFISjviPydmtmgeUAuXp4N1fky+VCtfhOkDUFIv5ea7p4wuTomI4QTrXvFBX2S4jZsmyTSrStQl+E+4w+RzxA== + dependencies: + chalk "^4.1.0" + enhanced-resolve "^5.7.0" + tsconfig-paths "^4.1.2" + tsconfig-paths@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.0.0.tgz#1082f5d99fd127b72397eef4809e4dd06d229b64" @@ -22037,6 +22188,11 @@ walker@^1.0.8: dependencies: makeerror "1.0.12" +watskeburt@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/watskeburt/-/watskeburt-4.1.0.tgz#3c0227669be646a97424b631164b1afe3d4d5344" + integrity sha512-KkY5H51ajqy9HYYI+u9SIURcWnqeVVhdH0I+ab6aXPGHfZYxgRCwnR6Lm3+TYB6jJVt5jFqw4GAKmwf1zHmGQw== + wcwidth@^1.0.0, wcwidth@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"