Merge branch 'master' of github.com:budibase/budibase into test-oracle

This commit is contained in:
Sam Rose 2024-08-02 11:20:19 +01:00
commit 84020be98e
No known key found for this signature in database
24 changed files with 679 additions and 918 deletions

View File

@ -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:
'<!doctype html><html itemscope="" itemtype="http://schema.org/WebPage" lang="en-GB"></html>',
})
} 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
}

View File

@ -1,6 +1,13 @@
import { npmUpload, urlUpload, githubUpload } from "./uploaders" import { npmUpload, urlUpload, githubUpload } from "./uploaders"
import { plugins as pluginCore } from "@budibase/backend-core" 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 env from "../../../environment"
import { clientAppSocket } from "../../../websockets" import { clientAppSocket } from "../../../websockets"
import sdk from "../../../sdk" 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<CreatePluginRequest, CreatePluginResponse>
) {
const { source, url, headers, githubToken } = ctx.request.body const { source, url, headers, githubToken } = ctx.request.body
try { try {
@ -75,14 +84,9 @@ export async function create(ctx: any) {
const doc = await pro.plugins.storePlugin(metadata, directory, source) const doc = await pro.plugins.storePlugin(metadata, directory, source)
clientAppSocket?.emit("plugins-update", { name, hash: doc.hash }) clientAppSocket?.emit("plugins-update", { name, hash: doc.hash })
ctx.body = {
message: "Plugin uploaded successfully",
plugins: [doc],
}
ctx.body = { plugin: doc } ctx.body = { plugin: doc }
} catch (err: any) { } catch (err: any) {
const errMsg = err?.message ? err?.message : err const errMsg = err?.message ? err?.message : err
ctx.throw(400, `Failed to import plugin: ${errMsg}`) ctx.throw(400, `Failed to import plugin: ${errMsg}`)
} }
} }

View File

@ -20,6 +20,7 @@ import { type App } from "@budibase/types"
import tk from "timekeeper" import tk from "timekeeper"
import * as uuid from "uuid" import * as uuid from "uuid"
import { structures } from "@budibase/backend-core/tests" import { structures } from "@budibase/backend-core/tests"
import nock from "nock"
describe("/applications", () => { describe("/applications", () => {
let config = setup.getConfig() let config = setup.getConfig()
@ -35,6 +36,7 @@ describe("/applications", () => {
throw new Error("Failed to publish app") throw new Error("Failed to publish app")
} }
jest.clearAllMocks() jest.clearAllMocks()
nock.cleanAll()
}) })
// These need to go first for the app totals to make sense // These need to go first for the app totals to make sense
@ -324,18 +326,33 @@ describe("/applications", () => {
describe("delete", () => { describe("delete", () => {
it("should delete published app and dev apps with dev app ID", async () => { 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) await config.api.application.delete(app.appId)
expect(events.app.deleted).toHaveBeenCalledTimes(1) expect(events.app.deleted).toHaveBeenCalledTimes(1)
expect(events.app.unpublished).toHaveBeenCalledTimes(1) expect(events.app.unpublished).toHaveBeenCalledTimes(1)
}) })
it("should delete published app and dev app with prod app ID", async () => { 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.deleted).toHaveBeenCalledTimes(1)
expect(events.app.unpublished).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 () => { 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.withCoreEnv({ SQS_SEARCH_ENABLE: "true" }, async () => {
await config.api.application.delete(app.appId) await config.api.application.delete(app.appId)
}) })

View File

@ -19,6 +19,7 @@ import {
} from "@budibase/types" } from "@budibase/types"
import { DatabaseName, getDatasource } from "../../../integrations/tests/utils" import { DatabaseName, getDatasource } from "../../../integrations/tests/utils"
import { tableForDatasource } from "../../../tests/utilities/structures" import { tableForDatasource } from "../../../tests/utilities/structures"
import nock from "nock"
describe("/datasources", () => { describe("/datasources", () => {
const config = setup.getConfig() const config = setup.getConfig()
@ -37,6 +38,7 @@ describe("/datasources", () => {
config: {}, config: {},
}) })
jest.clearAllMocks() jest.clearAllMocks()
nock.cleanAll()
}) })
describe("create", () => { describe("create", () => {
@ -71,6 +73,12 @@ describe("/datasources", () => {
describe("dynamic variables", () => { describe("dynamic variables", () => {
it("should invalidate changed or removed variables", async () => { 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({ let datasource = await config.api.datasource.create({
type: "datasource", type: "datasource",
name: "Rest", name: "Rest",
@ -81,7 +89,7 @@ describe("/datasources", () => {
const query = await config.api.query.save({ const query = await config.api.query.save({
datasourceId: datasource._id!, datasourceId: datasource._id!,
fields: { fields: {
path: "www.google.com", path: "www.example.com",
}, },
parameters: [], parameters: [],
transformer: null, transformer: null,

View File

@ -15,6 +15,8 @@ jest.mock("@budibase/backend-core", () => {
import { events, objectStore } from "@budibase/backend-core" import { events, objectStore } from "@budibase/backend-core"
import * as setup from "./utilities" import * as setup from "./utilities"
import nock from "nock"
import { PluginSource } from "@budibase/types"
const mockUploadDirectory = objectStore.uploadDirectory as jest.Mock const mockUploadDirectory = objectStore.uploadDirectory as jest.Mock
const mockDeleteFolder = objectStore.deleteFolder as jest.Mock const mockDeleteFolder = objectStore.deleteFolder as jest.Mock
@ -28,6 +30,7 @@ describe("/plugins", () => {
beforeEach(async () => { beforeEach(async () => {
await config.init() await config.init()
jest.clearAllMocks() jest.clearAllMocks()
nock.cleanAll()
}) })
const createPlugin = async (status?: number) => { const createPlugin = async (status?: number) => {
@ -112,67 +115,108 @@ describe("/plugins", () => {
}) })
describe("github", () => { describe("github", () => {
const createGithubPlugin = async (status?: number, url?: string) => { beforeEach(async () => {
return await request nock("https://api.github.com")
.post(`/api/plugin`) .get("/repos/my-repo/budibase-comment-box")
.send({ .reply(200, {
source: "Github", name: "budibase-comment-box",
url, releases_url:
githubToken: "token", "https://api.github.com/repos/my-repo/budibase-comment-box{/id}",
}) })
.set(config.defaultHeaders()) .get("/repos/my-repo/budibase-comment-box/latest")
.expect("Content-Type", /json/) .reply(200, {
.expect(status ? status : 200) assets: [
} {
it("should be able to create a plugin from github", async () => { content_type: "application/gzip",
const res = await createGithubPlugin( browser_download_url:
200, "https://github.com/my-repo/budibase-comment-box/releases/download/v1.0.2/comment-box-1.0.2.tar.gz",
"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") 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 () => { it("should fail if the url is not from github", async () => {
const res = await createGithubPlugin( await config.api.plugin.create(
400, {
"https://notgithub.com/my-repo/budibase-comment-box" source: PluginSource.GITHUB,
) url: "https://notgithub.com/my-repo/budibase-comment-box",
expect(res.body.message).toEqual( githubToken: "token",
"Failed to import plugin: The plugin origin must be from Github" },
{
status: 400,
body: {
message:
"Failed to import plugin: The plugin origin must be from Github",
},
}
) )
}) })
}) })
describe("npm", () => { describe("npm", () => {
it("should be able to create a plugin from npm", async () => { it("should be able to create a plugin from npm", async () => {
const res = await request nock("https://registry.npmjs.org")
.post(`/api/plugin`) .get("/budibase-component")
.send({ .reply(200, {
source: "NPM", name: "budibase-component",
url: "https://www.npmjs.com/package/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",
},
},
},
}) })
.set(config.defaultHeaders()) .get("/budibase-component/-/budibase-component-1.0.1.tgz")
.expect("Content-Type", /json/) .replyWithFile(
.expect(200) 200,
expect(res.body).toBeDefined() "src/api/routes/tests/data/budibase-component-1.0.1.tgz"
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() expect(events.plugin.imported).toHaveBeenCalled()
}) })
}) })
describe("url", () => { describe("url", () => {
it("should be able to create a plugin from a URL", async () => { it("should be able to create a plugin from a URL", async () => {
const res = await request nock("https://www.someurl.com")
.post(`/api/plugin`) .get("/comment-box/comment-box-1.0.2.tar.gz")
.send({ .replyWithFile(
source: "URL", 200,
url: "https://www.someurl.com/comment-box/comment-box-1.0.2.tar.gz", "src/api/routes/tests/data/comment-box-1.0.2.tar.gz"
}) )
.set(config.defaultHeaders())
.expect("Content-Type", /json/) const { plugin } = await config.api.plugin.create({
.expect(200) source: PluginSource.URL,
expect(res.body).toBeDefined() url: "https://www.someurl.com/comment-box/comment-box-1.0.2.tar.gz",
expect(res.body.plugin._id).toEqual("plg_comment-box") })
expect(plugin._id).toEqual("plg_comment-box")
expect(events.plugin.imported).toHaveBeenCalledTimes(1) expect(events.plugin.imported).toHaveBeenCalledTimes(1)
}) })
}) })

View File

@ -5,8 +5,6 @@ import { getCachedVariable } from "../../../../threads/utils"
import nock from "nock" import nock from "nock"
import { generator } from "@budibase/backend-core/tests" import { generator } from "@budibase/backend-core/tests"
jest.unmock("node-fetch")
describe("rest", () => { describe("rest", () => {
let config: TestConfiguration let config: TestConfiguration
let datasource: Datasource let datasource: Datasource

View File

@ -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)
})
})

View File

@ -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)
})
})

View File

@ -1,4 +1,5 @@
import { getConfig, afterAll, runStep, actions } from "./utilities" import { getConfig, afterAll, runStep, actions } from "./utilities"
import nock from "nock"
describe("test the outgoing webhook action", () => { describe("test the outgoing webhook action", () => {
let config = getConfig() let config = getConfig()
@ -9,42 +10,45 @@ describe("test the outgoing webhook action", () => {
afterAll() afterAll()
beforeEach(() => {
nock.cleanAll()
})
it("should be able to run the action", async () => { 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, { const res = await runStep(actions.integromat.stepId, {
value1: "test",
url: "http://www.example.com", url: "http://www.example.com",
}) })
expect(res.response.url).toEqual("http://www.example.com") expect(res.response.foo).toEqual("bar")
expect(res.response.method).toEqual("post")
expect(res.success).toEqual(true) expect(res.success).toEqual(true)
}) })
it("should add the payload props when a JSON string is provided", async () => { 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, { const res = await runStep(actions.integromat.stepId, {
value1: "ONE", body: { value: JSON.stringify(payload) },
value2: "TWO",
value3: "THREE",
value4: "FOUR",
value5: "FIVE",
body: {
value: payload,
},
url: "http://www.example.com", url: "http://www.example.com",
}) })
expect(res.response.url).toEqual("http://www.example.com") expect(res.response.foo).toEqual("bar")
expect(res.response.method).toEqual("post")
expect(res.response.body).toEqual(payload)
expect(res.success).toEqual(true) expect(res.success).toEqual(true)
}) })
it("should return a 400 if the JSON payload string is malformed", async () => { it("should return a 400 if the JSON payload string is malformed", async () => {
const payload = `{ value1 1 }`
const res = await runStep(actions.integromat.stepId, { const res = await runStep(actions.integromat.stepId, {
value1: "ONE", body: { value: "{ invalid json }" },
body: {
value: payload,
},
url: "http://www.example.com", url: "http://www.example.com",
}) })
expect(res.httpStatus).toEqual(400) expect(res.httpStatus).toEqual(400)

View File

@ -1,4 +1,5 @@
import { getConfig, afterAll, runStep, actions } from "./utilities" import { getConfig, afterAll, runStep, actions } from "./utilities"
import nock from "nock"
describe("test the outgoing webhook action", () => { describe("test the outgoing webhook action", () => {
let config = getConfig() let config = getConfig()
@ -9,31 +10,33 @@ describe("test the outgoing webhook action", () => {
afterAll() afterAll()
beforeEach(() => {
nock.cleanAll()
})
it("should be able to run the action and default to 'get'", async () => { 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, { const res = await runStep(actions.n8n.stepId, {
url: "http://www.example.com", url: "http://www.example.com",
body: { body: {
test: "IGNORE_ME", test: "IGNORE_ME",
}, },
}) })
expect(res.response.url).toEqual("http://www.example.com") expect(res.response.foo).toEqual("bar")
expect(res.response.method).toEqual("GET")
expect(res.response.body).toBeUndefined()
expect(res.success).toEqual(true) expect(res.success).toEqual(true)
}) })
it("should add the payload props when a JSON string is provided", async () => { 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, { const res = await runStep(actions.n8n.stepId, {
body: { body: {
value: payload, value: JSON.stringify({ name: "Adam", age: 9 }),
}, },
method: "POST", method: "POST",
url: "http://www.example.com", 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) 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 () => { 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, { const res = await runStep(actions.n8n.stepId, {
url: "http://www.example.com", url: "http://www.example.com",
method: "HEAD", method: "HEAD",
@ -60,9 +66,6 @@ describe("test the outgoing webhook action", () => {
test: "IGNORE_ME", 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) expect(res.success).toEqual(true)
}) })
}) })

View File

@ -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)
})
})

View File

@ -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)
})
})

View File

@ -1,4 +1,5 @@
import { getConfig, afterAll, runStep, actions } from "./utilities" import { getConfig, afterAll, runStep, actions } from "./utilities"
import nock from "nock"
describe("test the outgoing webhook action", () => { describe("test the outgoing webhook action", () => {
let config = getConfig() let config = getConfig()
@ -9,44 +10,45 @@ describe("test the outgoing webhook action", () => {
afterAll() afterAll()
beforeEach(() => {
nock.cleanAll()
})
it("should be able to run the action", async () => { 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, { const res = await runStep(actions.zapier.stepId, {
value1: "test",
url: "http://www.example.com", url: "http://www.example.com",
}) })
expect(res.response.url).toEqual("http://www.example.com") expect(res.response.foo).toEqual("bar")
expect(res.response.method).toEqual("post")
expect(res.success).toEqual(true) expect(res.success).toEqual(true)
}) })
it("should add the payload props when a JSON string is provided", async () => { 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, { const res = await runStep(actions.zapier.stepId, {
value1: "ONE", body: { value: JSON.stringify(payload) },
value2: "TWO",
value3: "THREE",
value4: "FOUR",
value5: "FIVE",
body: {
value: payload,
},
url: "http://www.example.com", url: "http://www.example.com",
}) })
expect(res.response.url).toEqual("http://www.example.com") expect(res.response.foo).toEqual("bar")
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.success).toEqual(true) expect(res.success).toEqual(true)
}) })
it("should return a 400 if the JSON payload string is malformed", async () => { it("should return a 400 if the JSON payload string is malformed", async () => {
const payload = `{ value1 1 }`
const res = await runStep(actions.zapier.stepId, { const res = await runStep(actions.zapier.stepId, {
value1: "ONE", body: { value: "{ invalid json }" },
body: {
value: payload,
},
url: "http://www.example.com", url: "http://www.example.com",
}) })
expect(res.httpStatus).toEqual(400) expect(res.httpStatus).toEqual(400)

View File

@ -1,4 +1,5 @@
import { import {
BodyType,
DatasourceFieldType, DatasourceFieldType,
HttpMethod, HttpMethod,
Integration, Integration,
@ -15,7 +16,7 @@ import {
import get from "lodash/get" import get from "lodash/get"
import * as https from "https" import * as https from "https"
import qs from "querystring" import qs from "querystring"
import type { Response } from "node-fetch" import type { Response, RequestInit } from "node-fetch"
import fetch from "node-fetch" import fetch from "node-fetch"
import { formatBytes } from "../utilities" import { formatBytes } from "../utilities"
import { performance } from "perf_hooks" import { performance } from "perf_hooks"
@ -28,15 +29,6 @@ import path from "path"
import { Builder as XmlBuilder } from "xml2js" import { Builder as XmlBuilder } from "xml2js"
import { getAttachmentHeaders } from "./utils/restUtils" import { getAttachmentHeaders } from "./utils/restUtils"
enum BodyType {
NONE = "none",
FORM_DATA = "form",
XML = "xml",
ENCODED = "encoded",
JSON = "json",
TEXT = "text",
}
const coreFields = { const coreFields = {
path: { path: {
type: DatasourceFieldType.STRING, 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<string, string[] | string>
}
pagination?: {
cursor: any
}
}
export class RestIntegration implements IntegrationBase {
private config: RestConfig private config: RestConfig
private headers: { private headers: {
[key: string]: string [key: string]: string
@ -138,7 +146,10 @@ class RestIntegration implements IntegrationBase {
this.config = config this.config = config
} }
async parseResponse(response: Response, pagination: PaginationConfig | null) { async parseResponse(
response: Response,
pagination?: PaginationConfig
): Promise<ParsedResponse> {
let data: any[] | string | undefined, let data: any[] | string | undefined,
raw: string | undefined, raw: string | undefined,
headers: Record<string, string[] | string> = {}, headers: Record<string, string[] | string> = {},
@ -235,8 +246,8 @@ class RestIntegration implements IntegrationBase {
getUrl( getUrl(
path: string, path: string,
queryString: string, queryString: string,
pagination: PaginationConfig | null, pagination?: PaginationConfig,
paginationValues: PaginationValues | null paginationValues?: PaginationValues
): string { ): string {
// Add pagination params to query string if required // Add pagination params to query string if required
if (pagination?.location === "query" && paginationValues) { if (pagination?.location === "query" && paginationValues) {
@ -279,10 +290,10 @@ class RestIntegration implements IntegrationBase {
addBody( addBody(
bodyType: string, bodyType: string,
body: string | any, body: string | any,
input: any, input: RequestInit,
pagination: PaginationConfig | null, pagination?: PaginationConfig,
paginationValues: PaginationValues | null paginationValues?: PaginationValues
) { ): RequestInit {
if (!input.headers) { if (!input.headers) {
input.headers = {} input.headers = {}
} }
@ -345,6 +356,7 @@ class RestIntegration implements IntegrationBase {
string = new XmlBuilder().buildObject(object) string = new XmlBuilder().buildObject(object)
} }
input.body = string input.body = string
// @ts-ignore
input.headers["Content-Type"] = "application/xml" input.headers["Content-Type"] = "application/xml"
break break
case BodyType.JSON: case BodyType.JSON:
@ -356,13 +368,14 @@ class RestIntegration implements IntegrationBase {
object[key] = value object[key] = value
}) })
input.body = JSON.stringify(object) input.body = JSON.stringify(object)
// @ts-ignore
input.headers["Content-Type"] = "application/json" input.headers["Content-Type"] = "application/json"
break break
} }
return input return input
} }
getAuthHeaders(authConfigId: string): { [key: string]: any } { getAuthHeaders(authConfigId?: string): { [key: string]: any } {
let headers: any = {} let headers: any = {}
if (this.config.authConfigs && authConfigId) { if (this.config.authConfigs && authConfigId) {
@ -398,7 +411,7 @@ class RestIntegration implements IntegrationBase {
headers = {}, headers = {},
method = HttpMethod.GET, method = HttpMethod.GET,
disabledHeaders, disabledHeaders,
bodyType, bodyType = BodyType.NONE,
requestBody, requestBody,
authConfigId, authConfigId,
pagination, pagination,
@ -407,7 +420,7 @@ class RestIntegration implements IntegrationBase {
const authHeaders = this.getAuthHeaders(authConfigId) const authHeaders = this.getAuthHeaders(authConfigId)
this.headers = { this.headers = {
...this.config.defaultHeaders, ...(this.config.defaultHeaders || {}),
...headers, ...headers,
...authHeaders, ...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( input = this.addBody(
bodyType, bodyType,
requestBody, requestBody,
@ -437,7 +450,12 @@ class RestIntegration implements IntegrationBase {
// Deprecated by rejectUnauthorized // Deprecated by rejectUnauthorized
if (this.config.legacyHttpParser) { 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 // https://github.com/nodejs/node/issues/43798
// @ts-ignore
input.extraHttpOptions = { insecureHTTPParser: true } input.extraHttpOptions = { insecureHTTPParser: true }
} }

View File

@ -1,4 +1,5 @@
import type { GoogleSpreadsheetWorksheet } from "google-spreadsheet" import type { GoogleSpreadsheetWorksheet } from "google-spreadsheet"
import nock from "nock"
jest.mock("google-auth-library") jest.mock("google-auth-library")
const { OAuth2Client } = require("google-auth-library") const { OAuth2Client } = require("google-auth-library")
@ -62,6 +63,13 @@ describe("Google Sheets Integration", () => {
await config.init() await config.init()
jest.clearAllMocks() 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",
})
}) })
function createBasicTable(name: string, columns: string[]): Table { function createBasicTable(name: string, columns: string[]): Table {

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,7 @@
import TestConfiguration from "../../tests/utilities/TestConfiguration" import TestConfiguration from "../../tests/utilities/TestConfiguration"
import { startup } from "../index" import { startup } from "../index"
import { users, utils, tenancy } from "@budibase/backend-core" import { users, utils, tenancy } from "@budibase/backend-core"
import nock from "nock"
describe("check BB_ADMIN environment variables", () => { describe("check BB_ADMIN environment variables", () => {
const config = new TestConfiguration() const config = new TestConfiguration()
@ -8,7 +9,17 @@ describe("check BB_ADMIN environment variables", () => {
await config.init() await config.init()
}) })
beforeEach(() => {
nock.cleanAll()
})
it("should be able to create a user with the BB_ADMIN environment variables", async () => { 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", const EMAIL = "budibase@budibase.com",
PASSWORD = "budibase" PASSWORD = "budibase"
await tenancy.doInTenant(tenancy.DEFAULT_TENANT_ID, async () => { await tenancy.doInTenant(tenancy.DEFAULT_TENANT_ID, async () => {

View File

@ -1,6 +1,7 @@
import env from "../environment" import env from "../environment"
import { env as coreEnv, timers } from "@budibase/backend-core" import { env as coreEnv, timers } from "@budibase/backend-core"
import { testContainerUtils } from "@budibase/backend-core/tests" import { testContainerUtils } from "@budibase/backend-core/tests"
import nock from "nock"
if (!process.env.CI) { if (!process.env.CI) {
// set a longer timeout in dev for debugging 100 seconds // set a longer timeout in dev for debugging 100 seconds
@ -9,6 +10,15 @@ if (!process.env.CI) {
jest.setTimeout(30 * 1000) 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) testContainerUtils.setupEnv(env, coreEnv)
afterAll(() => { afterAll(() => {

View File

@ -15,6 +15,7 @@ import { RoleAPI } from "./role"
import { TemplateAPI } from "./template" import { TemplateAPI } from "./template"
import { RowActionAPI } from "./rowAction" import { RowActionAPI } from "./rowAction"
import { AutomationAPI } from "./automation" import { AutomationAPI } from "./automation"
import { PluginAPI } from "./plugin"
export default class API { export default class API {
table: TableAPI table: TableAPI
@ -33,6 +34,7 @@ export default class API {
templates: TemplateAPI templates: TemplateAPI
rowAction: RowActionAPI rowAction: RowActionAPI
automation: AutomationAPI automation: AutomationAPI
plugin: PluginAPI
constructor(config: TestConfiguration) { constructor(config: TestConfiguration) {
this.table = new TableAPI(config) this.table = new TableAPI(config)
@ -51,5 +53,6 @@ export default class API {
this.templates = new TemplateAPI(config) this.templates = new TemplateAPI(config)
this.rowAction = new RowActionAPI(config) this.rowAction = new RowActionAPI(config)
this.automation = new AutomationAPI(config) this.automation = new AutomationAPI(config)
this.plugin = new PluginAPI(config)
} }
} }

View File

@ -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<CreatePluginResponse>(`/api/plugin`, {
body,
expectations,
})
}
}

View File

@ -15,3 +15,4 @@ export * from "./automation"
export * from "./layout" export * from "./layout"
export * from "./query" export * from "./query"
export * from "./role" export * from "./role"
export * from "./plugins"

View File

@ -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
}

View File

@ -45,15 +45,15 @@ export interface DynamicVariable {
export interface RestConfig { export interface RestConfig {
url: string url: string
rejectUnauthorized: boolean rejectUnauthorized?: boolean
downloadImages?: boolean downloadImages?: boolean
defaultHeaders: { defaultHeaders?: {
[key: string]: any [key: string]: any
} }
legacyHttpParser: boolean legacyHttpParser?: boolean
authConfigs: RestAuthConfig[] authConfigs?: RestAuthConfig[]
staticVariables: { staticVariables?: {
[key: string]: string [key: string]: string
} }
dynamicVariables: DynamicVariable[] dynamicVariables?: DynamicVariable[]
} }

View File

@ -36,31 +36,39 @@ export interface QueryResponse {
pagination: any pagination: any
} }
export enum BodyType {
NONE = "none",
FORM_DATA = "form",
XML = "xml",
ENCODED = "encoded",
JSON = "json",
TEXT = "text",
}
export interface RestQueryFields { export interface RestQueryFields {
path: string path?: string
queryString?: string queryString?: string
headers: { [key: string]: any } headers?: { [key: string]: any }
disabledHeaders: { [key: string]: any } disabledHeaders?: { [key: string]: any }
requestBody: any requestBody?: any
bodyType: string bodyType?: BodyType
json: object method?: string
method: string authConfigId?: string
authConfigId: string pagination?: PaginationConfig
pagination: PaginationConfig | null paginationValues?: PaginationValues
paginationValues: PaginationValues | null
} }
export interface PaginationConfig { export interface PaginationConfig {
type: string type: string
location: string location: string
pageParam: string pageParam: string
sizeParam: string | null sizeParam?: string
responseParam: string | null responseParam?: string
} }
export interface PaginationValues { export interface PaginationValues {
page: string | number | null page?: string | number
limit: number | null limit?: number
} }
export enum HttpMethod { export enum HttpMethod {