Merge branch 'master' into detect-iframe-clicks

This commit is contained in:
Andrew Kingston 2025-02-05 11:51:42 +00:00 committed by GitHub
commit eef2ecd4f2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 1221 additions and 1262 deletions

View File

@ -1,5 +1,12 @@
import { Feature, License, Quotas } from "@budibase/types" import {
Feature,
License,
MonthlyQuotaName,
QuotaType,
QuotaUsageType,
} from "@budibase/types"
import cloneDeep from "lodash/cloneDeep" import cloneDeep from "lodash/cloneDeep"
import merge from "lodash/merge"
let CLOUD_FREE_LICENSE: License let CLOUD_FREE_LICENSE: License
let UNLIMITED_LICENSE: License let UNLIMITED_LICENSE: License
@ -27,18 +34,19 @@ export function initInternal(opts: {
export interface UseLicenseOpts { export interface UseLicenseOpts {
features?: Feature[] features?: Feature[]
quotas?: Quotas monthlyQuotas?: [MonthlyQuotaName, number][]
} }
// LICENSES // LICENSES
export const useLicense = (license: License, opts?: UseLicenseOpts) => { export const useLicense = (license: License, opts?: UseLicenseOpts) => {
if (opts) { if (opts?.features) {
if (opts.features) { license.features.push(...opts.features)
license.features.push(...opts.features) }
} if (opts?.monthlyQuotas) {
if (opts.quotas) { for (const [name, value] of opts.monthlyQuotas) {
license.quotas = opts.quotas license.quotas[QuotaType.USAGE][QuotaUsageType.MONTHLY][name].value =
value
} }
} }
@ -57,12 +65,9 @@ export const useCloudFree = () => {
// FEATURES // FEATURES
const useFeature = (feature: Feature) => { const useFeature = (feature: Feature, extra?: Partial<UseLicenseOpts>) => {
const license = cloneDeep(getCachedLicense() || UNLIMITED_LICENSE) const license = cloneDeep(getCachedLicense() || UNLIMITED_LICENSE)
const opts: UseLicenseOpts = { const opts: UseLicenseOpts = merge({ features: [feature] }, extra)
features: [feature],
}
return useLicense(license, opts) return useLicense(license, opts)
} }
@ -102,8 +107,12 @@ export const useAppBuilders = () => {
return useFeature(Feature.APP_BUILDERS) return useFeature(Feature.APP_BUILDERS)
} }
export const useBudibaseAI = () => { export const useBudibaseAI = (opts?: { monthlyQuota?: number }) => {
return useFeature(Feature.BUDIBASE_AI) return useFeature(Feature.BUDIBASE_AI, {
monthlyQuotas: [
[MonthlyQuotaName.BUDIBASE_AI_CREDITS, opts?.monthlyQuota || 1000],
],
})
} }
export const useAICustomConfigs = () => { export const useAICustomConfigs = () => {

View File

@ -169,6 +169,18 @@ if (descriptions.length) {
) )
} }
const resetRowUsage = async () => {
await config.doInContext(
undefined,
async () =>
await quotas.setUsage(
0,
StaticQuotaName.ROWS,
QuotaUsageType.STATIC
)
)
}
const getRowUsage = async () => { const getRowUsage = async () => {
const { total } = await config.doInContext(undefined, () => const { total } = await config.doInContext(undefined, () =>
quotas.getCurrentUsageValues( quotas.getCurrentUsageValues(
@ -206,6 +218,10 @@ if (descriptions.length) {
table = await config.api.table.save(defaultTable()) table = await config.api.table.save(defaultTable())
}) })
beforeEach(async () => {
await resetRowUsage()
})
describe("create", () => { describe("create", () => {
it("creates a new row successfully", async () => { it("creates a new row successfully", async () => {
const rowUsage = await getRowUsage() const rowUsage = await getRowUsage()
@ -3317,6 +3333,7 @@ if (descriptions.length) {
beforeAll(async () => { beforeAll(async () => {
mocks.licenses.useBudibaseAI() mocks.licenses.useBudibaseAI()
mocks.licenses.useAICustomConfigs() mocks.licenses.useAICustomConfigs()
envCleanup = setEnv({ envCleanup = setEnv({
OPENAI_API_KEY: "sk-abcdefghijklmnopqrstuvwxyz1234567890abcd", OPENAI_API_KEY: "sk-abcdefghijklmnopqrstuvwxyz1234567890abcd",
}) })

View File

@ -17,11 +17,11 @@ import { basicAutomation } from "../../tests/utilities/structures"
import { wait } from "../../utilities" import { wait } from "../../utilities"
import { makePartial } from "../../tests/utilities" import { makePartial } from "../../tests/utilities"
import { cleanInputValues } from "../automationUtils" import { cleanInputValues } from "../automationUtils"
import * as setup from "./utilities"
import { Automation } from "@budibase/types" import { Automation } from "@budibase/types"
import TestConfiguration from "../../tests/utilities/TestConfiguration"
describe("Run through some parts of the automations system", () => { describe("Run through some parts of the automations system", () => {
let config = setup.getConfig() const config = new TestConfiguration()
beforeAll(async () => { beforeAll(async () => {
await automation.init() await automation.init()
@ -30,7 +30,7 @@ describe("Run through some parts of the automations system", () => {
afterAll(async () => { afterAll(async () => {
await automation.shutdown() await automation.shutdown()
setup.afterAll() config.end()
}) })
it("should be able to init in builder", async () => { it("should be able to init in builder", async () => {

View File

@ -1,11 +1,11 @@
import * as automation from "../../index" import * as automation from "../index"
import * as setup from "../utilities"
import { Table, AutomationStatus } from "@budibase/types" import { Table, AutomationStatus } from "@budibase/types"
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder" import { createAutomationBuilder } from "./utilities/AutomationTestBuilder"
import TestConfiguration from "../../tests/utilities/TestConfiguration"
describe("Branching automations", () => { describe("Branching automations", () => {
let config = setup.getConfig(), const config = new TestConfiguration()
table: Table let table: Table
beforeEach(async () => { beforeEach(async () => {
await automation.init() await automation.init()
@ -14,7 +14,9 @@ describe("Branching automations", () => {
await config.createRow() await config.createRow()
}) })
afterAll(setup.afterAll) afterAll(() => {
config.end()
})
it("should run a multiple nested branching automation", async () => { it("should run a multiple nested branching automation", async () => {
const firstLogId = "11111111-1111-1111-1111-111111111111" const firstLogId = "11111111-1111-1111-1111-111111111111"
@ -22,11 +24,7 @@ describe("Branching automations", () => {
const branch2LogId = "33333333-3333-3333-3333-333333333333" const branch2LogId = "33333333-3333-3333-3333-333333333333"
const branch2Id = "44444444-4444-4444-4444-444444444444" const branch2Id = "44444444-4444-4444-4444-444444444444"
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "Test Trigger with Loop and Create Row",
})
const results = await builder
.appAction({ fields: {} }) .appAction({ fields: {} })
.serverLog( .serverLog(
{ text: "Starting automation" }, { text: "Starting automation" },
@ -85,11 +83,7 @@ describe("Branching automations", () => {
}) })
it("should execute correct branch based on string equality", async () => { it("should execute correct branch based on string equality", async () => {
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "String Equality Branching",
})
const results = await builder
.appAction({ fields: { status: "active" } }) .appAction({ fields: { status: "active" } })
.branch({ .branch({
activeBranch: { activeBranch: {
@ -114,11 +108,7 @@ describe("Branching automations", () => {
}) })
it("should handle multiple conditions with AND operator", async () => { it("should handle multiple conditions with AND operator", async () => {
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "Multiple AND Conditions Branching",
})
const results = await builder
.appAction({ fields: { status: "active", role: "admin" } }) .appAction({ fields: { status: "active", role: "admin" } })
.branch({ .branch({
activeAdminBranch: { activeAdminBranch: {
@ -146,11 +136,7 @@ describe("Branching automations", () => {
}) })
it("should handle multiple conditions with OR operator", async () => { it("should handle multiple conditions with OR operator", async () => {
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "Multiple OR Conditions Branching",
})
const results = await builder
.appAction({ fields: { status: "test", role: "user" } }) .appAction({ fields: { status: "test", role: "user" } })
.branch({ .branch({
specialBranch: { specialBranch: {
@ -182,11 +168,7 @@ describe("Branching automations", () => {
}) })
it("should stop the branch automation when no conditions are met", async () => { it("should stop the branch automation when no conditions are met", async () => {
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "Multiple OR Conditions Branching",
})
const results = await builder
.appAction({ fields: { status: "test", role: "user" } }) .appAction({ fields: { status: "test", role: "user" } })
.createRow({ row: { name: "Test", tableId: table._id } }) .createRow({ row: { name: "Test", tableId: table._id } })
.branch({ .branch({
@ -213,7 +195,6 @@ describe("Branching automations", () => {
}, },
}, },
}) })
.serverLog({ text: "Test" })
.run() .run()
expect(results.steps[1].outputs.status).toEqual( expect(results.steps[1].outputs.status).toEqual(
@ -223,11 +204,7 @@ describe("Branching automations", () => {
}) })
it("evaluate multiple conditions", async () => { it("evaluate multiple conditions", async () => {
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "evaluate multiple conditions",
})
const results = await builder
.appAction({ fields: { test_trigger: true } }) .appAction({ fields: { test_trigger: true } })
.serverLog({ text: "Starting automation" }, { stepId: "aN6znRYHG" }) .serverLog({ text: "Starting automation" }, { stepId: "aN6znRYHG" })
.branch({ .branch({
@ -268,11 +245,7 @@ describe("Branching automations", () => {
}) })
it("evaluate multiple conditions with interpolated text", async () => { it("evaluate multiple conditions with interpolated text", async () => {
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "evaluate multiple conditions",
})
const results = await builder
.appAction({ fields: { test_trigger: true } }) .appAction({ fields: { test_trigger: true } })
.serverLog({ text: "Starting automation" }, { stepId: "aN6znRYHG" }) .serverLog({ text: "Starting automation" }, { stepId: "aN6znRYHG" })
.branch({ .branch({

View File

@ -1,22 +0,0 @@
import { runStep, actions, getConfig } from "./utilities"
import { reset } from "timekeeper"
// need real Date for this test
reset()
describe("test the delay logic", () => {
const config = getConfig()
beforeAll(async () => {
await config.init()
})
it("should be able to run the delay", async () => {
const time = 100
const before = Date.now()
await runStep(config, actions.DELAY.stepId, { time: time })
const now = Date.now()
// divide by two just so that test will always pass as long as there was some sort of delay
expect(now - before).toBeGreaterThanOrEqual(time / 2)
})
})

View File

@ -1,26 +0,0 @@
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(config, 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,64 +0,0 @@
import * as setup from "./utilities"
import { automations } from "@budibase/shared-core"
const FilterConditions = automations.steps.filter.FilterConditions
describe("test the filter logic", () => {
const config = setup.getConfig()
beforeAll(async () => {
await config.init()
})
async function checkFilter(
field: any,
condition: string,
value: any,
pass = true
) {
let res = await setup.runStep(config, setup.actions.FILTER.stepId, {
field,
condition,
value,
})
expect(res.result).toEqual(pass)
expect(res.success).toEqual(true)
}
it("should be able test equality", async () => {
await checkFilter("hello", FilterConditions.EQUAL, "hello", true)
await checkFilter("hello", FilterConditions.EQUAL, "no", false)
})
it("should be able to test greater than", async () => {
await checkFilter(10, FilterConditions.GREATER_THAN, 5, true)
await checkFilter(10, FilterConditions.GREATER_THAN, 15, false)
})
it("should be able to test less than", async () => {
await checkFilter(5, FilterConditions.LESS_THAN, 10, true)
await checkFilter(15, FilterConditions.LESS_THAN, 10, false)
})
it("should be able to in-equality", async () => {
await checkFilter("hello", FilterConditions.NOT_EQUAL, "no", true)
await checkFilter(10, FilterConditions.NOT_EQUAL, 10, false)
})
it("check number coercion", async () => {
await checkFilter("10", FilterConditions.GREATER_THAN, "5", true)
})
it("check date coercion", async () => {
await checkFilter(
new Date().toISOString(),
FilterConditions.GREATER_THAN,
new Date(-10000).toISOString(),
true
)
})
it("check objects always false", async () => {
await checkFilter({}, FilterConditions.EQUAL, {}, false)
})
})

View File

@ -1,153 +0,0 @@
import * as automation from "../index"
import * as triggers from "../triggers"
import { loopAutomation } from "../../tests/utilities/structures"
import { context } from "@budibase/backend-core"
import * as setup from "./utilities"
import { Table, LoopStepType, AutomationResults } from "@budibase/types"
import * as loopUtils from "../loopUtils"
import { LoopInput } from "../../definitions/automations"
describe("Attempt to run a basic loop automation", () => {
let config = setup.getConfig(),
table: Table
beforeEach(async () => {
await automation.init()
await config.init()
table = await config.createTable()
await config.createRow()
})
afterAll(setup.afterAll)
async function runLoop(loopOpts?: LoopInput): Promise<AutomationResults> {
const appId = config.getAppId()
return await context.doInAppContext(appId, async () => {
const params = { fields: { appId } }
const result = await triggers.externalTrigger(
loopAutomation(table._id!, loopOpts),
params,
{ getResponses: true }
)
if ("outputs" in result && !result.outputs.success) {
throw new Error("Unable to proceed - failed to return anything.")
}
return result as AutomationResults
})
}
it("attempt to run a basic loop", async () => {
const resp = await runLoop()
expect(resp.steps[2].outputs.iterations).toBe(1)
})
it("test a loop with a string", async () => {
const resp = await runLoop({
option: LoopStepType.STRING,
binding: "a,b,c",
})
expect(resp.steps[2].outputs.iterations).toBe(3)
})
it("test a loop with a binding that returns an integer", async () => {
const resp = await runLoop({
option: LoopStepType.ARRAY,
binding: "{{ 1 }}",
})
expect(resp.steps[2].outputs.iterations).toBe(1)
})
describe("replaceFakeBindings", () => {
it("should replace loop bindings in nested objects", () => {
const originalStepInput = {
schema: {
name: {
type: "string",
constraints: {
type: "string",
length: { maximum: null },
presence: false,
},
name: "name",
display: { type: "Text" },
},
},
row: {
tableId: "ta_aaad4296e9f74b12b1b90ef7a84afcad",
name: "{{ loop.currentItem.pokemon }}",
},
}
const loopStepNumber = 3
const result = loopUtils.replaceFakeBindings(
originalStepInput,
loopStepNumber
)
expect(result).toEqual({
schema: {
name: {
type: "string",
constraints: {
type: "string",
length: { maximum: null },
presence: false,
},
name: "name",
display: { type: "Text" },
},
},
row: {
tableId: "ta_aaad4296e9f74b12b1b90ef7a84afcad",
name: "{{ steps.3.currentItem.pokemon }}",
},
})
})
it("should handle null values in nested objects", () => {
const originalStepInput = {
nullValue: null,
nestedNull: {
someKey: null,
},
validValue: "{{ loop.someValue }}",
}
const loopStepNumber = 2
const result = loopUtils.replaceFakeBindings(
originalStepInput,
loopStepNumber
)
expect(result).toEqual({
nullValue: null,
nestedNull: {
someKey: null,
},
validValue: "{{ steps.2.someValue }}",
})
})
it("should handle empty objects and arrays", () => {
const originalStepInput = {
emptyObject: {},
emptyArray: [],
nestedEmpty: {
emptyObj: {},
emptyArr: [],
},
}
const loopStepNumber = 1
const result = loopUtils.replaceFakeBindings(
originalStepInput,
loopStepNumber
)
expect(result).toEqual(originalStepInput)
})
})
})

View File

@ -1,58 +0,0 @@
import { getConfig, afterAll, runStep, actions } from "./utilities"
import nock from "nock"
describe("test the outgoing webhook action", () => {
let config = getConfig()
beforeAll(async () => {
await config.init()
})
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(config, actions.integromat.stepId, {
url: "http://www.example.com",
})
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,
}
nock("http://www.example.com/")
.post("/", payload)
.reply(200, { foo: "bar" })
const res = await runStep(config, actions.integromat.stepId, {
body: { value: JSON.stringify(payload) },
url: "http://www.example.com",
})
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 res = await runStep(config, actions.integromat.stepId, {
body: { value: "{ invalid json }" },
url: "http://www.example.com",
})
expect(res.httpStatus).toEqual(400)
expect(res.response).toEqual("Invalid payload JSON")
expect(res.success).toEqual(false)
})
})

View File

@ -1,71 +0,0 @@
import { getConfig, afterAll, runStep, actions } from "./utilities"
import nock from "nock"
describe("test the outgoing webhook action", () => {
let config = getConfig()
beforeAll(async () => {
await config.init()
})
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(config, actions.n8n.stepId, {
url: "http://www.example.com",
body: {
test: "IGNORE_ME",
},
})
expect(res.response.foo).toEqual("bar")
expect(res.success).toEqual(true)
})
it("should add the payload props when a JSON string is provided", async () => {
nock("http://www.example.com/")
.post("/", { name: "Adam", age: 9 })
.reply(200)
const res = await runStep(config, actions.n8n.stepId, {
body: {
value: JSON.stringify({ name: "Adam", age: 9 }),
},
method: "POST",
url: "http://www.example.com",
})
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(config, actions.n8n.stepId, {
value1: "ONE",
body: {
value: payload,
},
method: "POST",
url: "http://www.example.com",
})
expect(res.httpStatus).toEqual(400)
expect(res.response).toEqual("Invalid payload JSON")
expect(res.success).toEqual(false)
})
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(config, actions.n8n.stepId, {
url: "http://www.example.com",
method: "HEAD",
body: {
test: "IGNORE_ME",
},
})
expect(res.success).toEqual(true)
})
})

View File

@ -1,165 +0,0 @@
import { getConfig, afterAll as _afterAll } from "./utilities"
import { createAutomationBuilder } from "./utilities/AutomationTestBuilder"
import { OpenAI } from "openai"
import { setEnv as setCoreEnv } from "@budibase/backend-core"
import * as pro from "@budibase/pro"
import { Model } from "@budibase/types"
jest.mock("openai", () => ({
OpenAI: jest.fn().mockImplementation(() => ({
chat: {
completions: {
create: jest.fn(() => ({
choices: [
{
message: {
content: "This is a test",
},
},
],
})),
},
},
})),
}))
jest.mock("@budibase/pro", () => ({
...jest.requireActual("@budibase/pro"),
ai: {
LargeLanguageModel: {
forCurrentTenant: jest.fn().mockImplementation(() => ({
llm: {},
init: jest.fn(),
run: jest.fn(),
})),
},
},
features: {
isAICustomConfigsEnabled: jest.fn(),
isBudibaseAIEnabled: jest.fn(),
},
}))
const mockedPro = jest.mocked(pro)
const mockedOpenAI = OpenAI as jest.MockedClass<typeof OpenAI>
const OPENAI_PROMPT = "What is the meaning of life?"
describe("test the openai action", () => {
let config = getConfig()
let resetEnv: () => void | undefined
beforeAll(async () => {
setCoreEnv({ SELF_HOSTED: true })
await config.init()
})
beforeEach(() => {
resetEnv = setCoreEnv({ OPENAI_API_KEY: "abc123" })
})
afterEach(() => {
resetEnv()
jest.clearAllMocks()
})
afterAll(_afterAll)
it("should be able to receive a response from ChatGPT given a prompt", async () => {
setCoreEnv({ SELF_HOSTED: true })
const result = await createAutomationBuilder({
name: "Test OpenAI Response",
config,
})
.appAction({ fields: {} })
.openai(
{ prompt: OPENAI_PROMPT, model: Model.GPT_4O_MINI },
{ stepName: "Basic OpenAI Query" }
)
.run()
expect(result.steps[0].outputs.response).toEqual("This is a test")
expect(result.steps[0].outputs.success).toBeTruthy()
})
it("should present the correct error message when a prompt is not provided", async () => {
const result = await createAutomationBuilder({
name: "Test OpenAI No Prompt",
config,
})
.appAction({ fields: {} })
.openai(
{ prompt: "", model: Model.GPT_4O_MINI },
{ stepName: "Empty Prompt Query" }
)
.run()
expect(result.steps[0].outputs.response).toEqual(
"Budibase OpenAI Automation Failed: No prompt supplied"
)
expect(result.steps[0].outputs.success).toBeFalsy()
})
it("should present the correct error message when an error is thrown from the createChatCompletion call", async () => {
mockedOpenAI.mockImplementation(
() =>
({
chat: {
completions: {
create: jest.fn(() => {
throw new Error(
"An error occurred while calling createChatCompletion"
)
}),
},
},
} as any)
)
const result = await createAutomationBuilder({
name: "Test OpenAI Error",
config,
})
.appAction({ fields: {} })
.openai(
{ prompt: OPENAI_PROMPT, model: Model.GPT_4O_MINI },
{ stepName: "Error Producing Query" }
)
.run()
expect(result.steps[0].outputs.response).toEqual(
"Error: An error occurred while calling createChatCompletion"
)
expect(result.steps[0].outputs.success).toBeFalsy()
})
it("should ensure that the pro AI module is called when the budibase AI features are enabled", async () => {
jest.spyOn(pro.features, "isBudibaseAIEnabled").mockResolvedValue(true)
jest.spyOn(pro.features, "isAICustomConfigsEnabled").mockResolvedValue(true)
const prompt = "What is the meaning of life?"
await createAutomationBuilder({
name: "Test OpenAI Pro Features",
config,
})
.appAction({ fields: {} })
.openai(
{
model: Model.GPT_4O_MINI,
prompt,
},
{ stepName: "Pro Features Query" }
)
.run()
expect(pro.ai.LargeLanguageModel.forCurrentTenant).toHaveBeenCalledWith(
"gpt-4o-mini"
)
const llmInstance =
mockedPro.ai.LargeLanguageModel.forCurrentTenant.mock.results[0].value
// init does not appear to be called currently
// expect(llmInstance.init).toHaveBeenCalled()
expect(llmInstance.run).toHaveBeenCalledWith(prompt)
})
})

View File

@ -1,37 +0,0 @@
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(config, 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(config, actions.OUTGOING_WEBHOOK.stepId, {
requestMethod: "GET",
url: "www.invalid.com",
})
expect(res.success).toEqual(false)
})
})

View File

@ -1,36 +1,35 @@
import * as automation from "../../index" import * as automation from "../index"
import * as setup from "../utilities"
import { LoopStepType, FieldType, Table, Datasource } from "@budibase/types" import { LoopStepType, FieldType, Table, Datasource } from "@budibase/types"
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder" import { createAutomationBuilder } from "./utilities/AutomationTestBuilder"
import { import {
DatabaseName, DatabaseName,
datasourceDescribe, datasourceDescribe,
} from "../../../integrations/tests/utils" } from "../../integrations/tests/utils"
import { Knex } from "knex" import { Knex } from "knex"
import { generator } from "@budibase/backend-core/tests" import { generator } from "@budibase/backend-core/tests"
import { automations } from "@budibase/shared-core" import { automations } from "@budibase/shared-core"
import TestConfiguration from "../../tests/utilities/TestConfiguration"
import { basicTable } from "../../tests/utilities/structures"
const FilterConditions = automations.steps.filter.FilterConditions const FilterConditions = automations.steps.filter.FilterConditions
describe("Automation Scenarios", () => { describe("Automation Scenarios", () => {
let config = setup.getConfig() const config = new TestConfiguration()
beforeEach(async () => { beforeEach(async () => {
await automation.init() await automation.init()
await config.init() await config.init()
}) })
afterAll(setup.afterAll) afterAll(() => {
config.end()
})
describe("Row Automations", () => { describe("Row Automations", () => {
it("should trigger an automation which then creates a row", async () => { it("should trigger an automation which then creates a row", async () => {
const table = await config.createTable() const table = await config.api.table.save(basicTable())
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "Test Row Save and Create",
})
const results = await builder
.rowUpdated( .rowUpdated(
{ tableId: table._id! }, { tableId: table._id! },
{ {
@ -58,20 +57,15 @@ describe("Automation Scenarios", () => {
}) })
}) })
it("should trigger an automation which querys the database", async () => { it("should trigger an automation which queries the database", async () => {
const table = await config.createTable() const table = await config.api.table.save(basicTable())
const row = { const row = {
name: "Test Row", name: "Test Row",
description: "original description", description: "original description",
tableId: table._id,
} }
await config.createRow(row) await config.api.row.save(table._id!, row)
await config.createRow(row) await config.api.row.save(table._id!, row)
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "Test Row Save and Create",
})
const results = await builder
.appAction({ fields: {} }) .appAction({ fields: {} })
.queryRows({ .queryRows({
tableId: table._id!, tableId: table._id!,
@ -82,20 +76,15 @@ describe("Automation Scenarios", () => {
expect(results.steps[0].outputs.rows).toHaveLength(2) expect(results.steps[0].outputs.rows).toHaveLength(2)
}) })
it("should trigger an automation which querys the database then deletes a row", async () => { it("should trigger an automation which queries the database then deletes a row", async () => {
const table = await config.createTable() const table = await config.api.table.save(basicTable())
const row = { const row = {
name: "DFN", name: "DFN",
description: "original description", description: "original description",
tableId: table._id,
} }
await config.createRow(row) await config.api.row.save(table._id!, row)
await config.createRow(row) await config.api.row.save(table._id!, row)
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "Test Row Save and Create",
})
const results = await builder
.appAction({ fields: {} }) .appAction({ fields: {} })
.queryRows({ .queryRows({
tableId: table._id!, tableId: table._id!,
@ -115,7 +104,8 @@ describe("Automation Scenarios", () => {
}) })
it("should trigger an automation which creates and then updates a row", async () => { it("should trigger an automation which creates and then updates a row", async () => {
const table = await config.createTable({ const table = await config.api.table.save({
...basicTable(),
name: "TestTable", name: "TestTable",
type: "table", type: "table",
schema: { schema: {
@ -136,11 +126,7 @@ describe("Automation Scenarios", () => {
}, },
}) })
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "Test Create and Update Row",
})
const results = await builder
.appAction({ fields: {} }) .appAction({ fields: {} })
.createRow( .createRow(
{ {
@ -202,19 +188,14 @@ describe("Automation Scenarios", () => {
describe("Name Based Automations", () => { describe("Name Based Automations", () => {
it("should fetch and delete a rpw using automation naming", async () => { it("should fetch and delete a rpw using automation naming", async () => {
const table = await config.createTable() const table = await config.api.table.save(basicTable())
const row = { const row = {
name: "DFN", name: "DFN",
description: "original description", description: "original description",
tableId: table._id,
} }
await config.createRow(row) await config.api.row.save(table._id!, row)
await config.createRow(row) await config.api.row.save(table._id!, row)
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "Test Query and Delete Row",
})
const results = await builder
.appAction({ fields: {} }) .appAction({ fields: {} })
.queryRows( .queryRows(
{ {
@ -240,7 +221,8 @@ describe("Automation Scenarios", () => {
let table: Table let table: Table
beforeEach(async () => { beforeEach(async () => {
table = await config.createTable({ table = await config.api.table.save({
...basicTable(),
name: "TestTable", name: "TestTable",
type: "table", type: "table",
schema: { schema: {
@ -263,11 +245,7 @@ describe("Automation Scenarios", () => {
}) })
it("should stop an automation if the condition is not met", async () => { it("should stop an automation if the condition is not met", async () => {
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "Test Equal",
})
const results = await builder
.appAction({ fields: {} }) .appAction({ fields: {} })
.createRow({ .createRow({
row: { row: {
@ -293,11 +271,7 @@ describe("Automation Scenarios", () => {
}) })
it("should continue the automation if the condition is met", async () => { it("should continue the automation if the condition is met", async () => {
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "Test Not Equal",
})
const results = await builder
.appAction({ fields: {} }) .appAction({ fields: {} })
.createRow({ .createRow({
row: { row: {
@ -364,11 +338,7 @@ describe("Automation Scenarios", () => {
it.each(testCases)( it.each(testCases)(
"should pass the filter when condition is $condition", "should pass the filter when condition is $condition",
async ({ condition, value, rowValue, expectPass }) => { async ({ condition, value, rowValue, expectPass }) => {
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: `Test ${condition}`,
})
const results = await builder
.appAction({ fields: {} }) .appAction({ fields: {} })
.createRow({ .createRow({
row: { row: {
@ -401,13 +371,9 @@ describe("Automation Scenarios", () => {
}) })
it("Check user is passed through from row trigger", async () => { it("Check user is passed through from row trigger", async () => {
const table = await config.createTable() const table = await config.api.table.save(basicTable())
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "Test a user is successfully passed from the trigger",
})
const results = await builder
.rowUpdated( .rowUpdated(
{ tableId: table._id! }, { tableId: table._id! },
{ {
@ -422,11 +388,7 @@ describe("Automation Scenarios", () => {
}) })
it("Check user is passed through from app trigger", async () => { it("Check user is passed through from app trigger", async () => {
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "Test a user is successfully passed from the trigger",
})
const results = await builder
.appAction({ fields: {} }) .appAction({ fields: {} })
.serverLog({ text: "{{ [user].[email] }}" }) .serverLog({ text: "{{ [user].[email] }}" })
.run() .run()
@ -449,7 +411,8 @@ if (descriptions.length) {
}) })
it("should query an external database for some data then insert than into an internal table", async () => { it("should query an external database for some data then insert than into an internal table", async () => {
const newTable = await config.createTable({ const newTable = await config.api.table.save({
...basicTable(),
name: "table", name: "table",
type: "table", type: "table",
schema: { schema: {
@ -484,19 +447,20 @@ if (descriptions.length) {
await client(tableName).insert(rows) await client(tableName).insert(rows)
const query = await setup.saveTestQuery( const query = await config.api.query.save({
config, name: "test query",
client, datasourceId: datasource._id!,
tableName, parameters: [],
datasource fields: {
) sql: client(tableName).select("*").toSQL().toNative().sql,
},
const builder = createAutomationBuilder({ transformer: "",
name: "Test external query and save", schema: {},
config, readable: true,
queryVerb: "read",
}) })
const results = await builder const results = await createAutomationBuilder({ config })
.appAction({ .appAction({
fields: {}, fields: {},
}) })

View File

@ -1,20 +0,0 @@
import { getConfig, afterAll as _afterAll, runStep, actions } from "./utilities"
describe("test the server log action", () => {
let config = getConfig()
let inputs: any
beforeAll(async () => {
await config.init()
inputs = {
text: "log message",
}
})
afterAll(_afterAll)
it("should be able to log the text", async () => {
let res = await runStep(config, actions.SERVER_LOG.stepId, inputs)
expect(res.message).toEqual(`App ${config.getAppId()} - ${inputs.text}`)
expect(res.success).toEqual(true)
})
})

View File

@ -1,30 +1,30 @@
import { createAutomationBuilder } from "./utilities/AutomationTestBuilder" import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
import * as automation from "../index" import * as automation from "../../index"
import * as setup from "./utilities"
import { Table } from "@budibase/types" import { Table } from "@budibase/types"
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
import { basicTable } from "../../../tests/utilities/structures"
describe("Execute Bash Automations", () => { describe("Execute Bash Automations", () => {
let config = setup.getConfig(), const config = new TestConfiguration()
table: Table let table: Table
beforeAll(async () => { beforeAll(async () => {
await automation.init() await automation.init()
await config.init() await config.init()
table = await config.createTable() table = await config.api.table.save(basicTable())
await config.createRow({ await config.api.row.save(table._id!, {
name: "test row", name: "test row",
description: "test description", description: "test description",
tableId: table._id!,
}) })
}) })
afterAll(setup.afterAll) afterAll(() => {
automation.shutdown()
config.end()
})
it("should use trigger data in bash command and pass output to subsequent steps", async () => { it("should use trigger data in bash command and pass output to subsequent steps", async () => {
const result = await createAutomationBuilder({ const result = await createAutomationBuilder({ config })
name: "Bash with Trigger Data",
config,
})
.appAction({ fields: { command: "hello world" } }) .appAction({ fields: { command: "hello world" } })
.bash( .bash(
{ code: "echo '{{ trigger.fields.command }}'" }, { code: "echo '{{ trigger.fields.command }}'" },
@ -43,10 +43,7 @@ describe("Execute Bash Automations", () => {
}) })
it("should chain multiple bash commands using previous outputs", async () => { it("should chain multiple bash commands using previous outputs", async () => {
const result = await createAutomationBuilder({ const result = await createAutomationBuilder({ config })
name: "Chained Bash Commands",
config,
})
.appAction({ fields: { filename: "testfile.txt" } }) .appAction({ fields: { filename: "testfile.txt" } })
.bash( .bash(
{ code: "echo 'initial content' > {{ trigger.fields.filename }}" }, { code: "echo 'initial content' > {{ trigger.fields.filename }}" },
@ -67,10 +64,7 @@ describe("Execute Bash Automations", () => {
}) })
it("should integrate bash output with row operations", async () => { it("should integrate bash output with row operations", async () => {
const result = await createAutomationBuilder({ const result = await createAutomationBuilder({ config })
name: "Bash with Row Operations",
config,
})
.appAction({ fields: {} }) .appAction({ fields: {} })
.queryRows( .queryRows(
{ {
@ -100,10 +94,7 @@ describe("Execute Bash Automations", () => {
}) })
it("should handle bash output in conditional logic", async () => { it("should handle bash output in conditional logic", async () => {
const result = await createAutomationBuilder({ const result = await createAutomationBuilder({ config })
name: "Bash with Conditional",
config,
})
.appAction({ fields: { threshold: "5" } }) .appAction({ fields: { threshold: "5" } })
.bash( .bash(
{ code: "echo $(( {{ trigger.fields.threshold }} + 5 ))" }, { code: "echo $(( {{ trigger.fields.threshold }} + 5 ))" },
@ -130,13 +121,10 @@ describe("Execute Bash Automations", () => {
}) })
it("should handle null values gracefully", async () => { it("should handle null values gracefully", async () => {
const result = await createAutomationBuilder({ const result = await createAutomationBuilder({ config })
name: "Null Bash Input",
config,
})
.appAction({ fields: {} }) .appAction({ fields: {} })
.bash( .bash(
//@ts-ignore // @ts-expect-error - testing null input
{ code: null }, { code: null },
{ stepName: "Null Command" } { stepName: "Null Command" }
) )

View File

@ -1,7 +1,11 @@
import * as setup from "./utilities" import {
import { basicTableWithAttachmentField } from "../../tests/utilities/structures" basicTable,
basicTableWithAttachmentField,
} from "../../../tests/utilities/structures"
import { objectStore } from "@budibase/backend-core" import { objectStore } from "@budibase/backend-core"
import { createAutomationBuilder } from "./utilities/AutomationTestBuilder" import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
import { Row, Table } from "@budibase/types"
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
async function uploadTestFile(filename: string) { async function uploadTestFile(filename: string) {
let bucket = "testbucket" let bucket = "testbucket"
@ -10,19 +14,20 @@ async function uploadTestFile(filename: string) {
filename, filename,
body: Buffer.from("test data"), body: Buffer.from("test data"),
}) })
let presignedUrl = await objectStore.getPresignedUrl(bucket, filename, 60000) let presignedUrl = objectStore.getPresignedUrl(bucket, filename, 60000)
return presignedUrl return presignedUrl
} }
describe("test the create row action", () => { describe("test the create row action", () => {
let table: any const config = new TestConfiguration()
let row: any
let config = setup.getConfig() let table: Table
let row: Row
beforeEach(async () => { beforeEach(async () => {
await config.init() await config.init()
table = await config.createTable() table = await config.api.table.save(basicTable())
row = { row = {
tableId: table._id, tableId: table._id,
name: "test", name: "test",
@ -30,14 +35,12 @@ describe("test the create row action", () => {
} }
}) })
afterAll(setup.afterAll) afterAll(() => {
config.end()
})
it("should be able to run the action", async () => { it("should be able to run the action", async () => {
const result = await createAutomationBuilder({ const result = await createAutomationBuilder({ config })
name: "Test Create Row Flow",
appId: config.getAppId(),
config,
})
.appAction({ fields: { status: "new" } }) .appAction({ fields: { status: "new" } })
.serverLog({ text: "Starting create row flow" }, { stepName: "StartLog" }) .serverLog({ text: "Starting create row flow" }, { stepName: "StartLog" })
.createRow({ row }, { stepName: "CreateRow" }) .createRow({ row }, { stepName: "CreateRow" })
@ -50,8 +53,9 @@ describe("test the create row action", () => {
expect(result.steps[1].outputs.success).toBeDefined() expect(result.steps[1].outputs.success).toBeDefined()
expect(result.steps[1].outputs.id).toBeDefined() expect(result.steps[1].outputs.id).toBeDefined()
expect(result.steps[1].outputs.revision).toBeDefined() expect(result.steps[1].outputs.revision).toBeDefined()
const gottenRow = await config.api.row.get( const gottenRow = await config.api.row.get(
table._id, table._id!,
result.steps[1].outputs.id result.steps[1].outputs.id
) )
expect(gottenRow.name).toEqual("test") expect(gottenRow.name).toEqual("test")
@ -62,11 +66,7 @@ describe("test the create row action", () => {
}) })
it("should return an error (not throw) when bad info provided", async () => { it("should return an error (not throw) when bad info provided", async () => {
const result = await createAutomationBuilder({ const result = await createAutomationBuilder({ config })
name: "Test Create Row Error Flow",
appId: config.getAppId(),
config,
})
.appAction({ fields: { status: "error" } }) .appAction({ fields: { status: "error" } })
.serverLog({ text: "Starting error test flow" }, { stepName: "StartLog" }) .serverLog({ text: "Starting error test flow" }, { stepName: "StartLog" })
.createRow( .createRow(
@ -84,11 +84,7 @@ describe("test the create row action", () => {
}) })
it("should check invalid inputs return an error", async () => { it("should check invalid inputs return an error", async () => {
const result = await createAutomationBuilder({ const result = await createAutomationBuilder({ config })
name: "Test Create Row Invalid Flow",
appId: config.getAppId(),
config,
})
.appAction({ fields: { status: "invalid" } }) .appAction({ fields: { status: "invalid" } })
.serverLog({ text: "Testing invalid input" }, { stepName: "StartLog" }) .serverLog({ text: "Testing invalid input" }, { stepName: "StartLog" })
.createRow({ row: {} }, { stepName: "CreateRow" }) .createRow({ row: {} }, { stepName: "CreateRow" })
@ -108,11 +104,11 @@ describe("test the create row action", () => {
}) })
it("should check that an attachment field is sent to storage and parsed", async () => { it("should check that an attachment field is sent to storage and parsed", async () => {
let attachmentTable = await config.createTable( let attachmentTable = await config.api.table.save(
basicTableWithAttachmentField() basicTableWithAttachmentField()
) )
let attachmentRow: any = { let attachmentRow: Row = {
tableId: attachmentTable._id, tableId: attachmentTable._id,
} }
@ -126,11 +122,7 @@ describe("test the create row action", () => {
] ]
attachmentRow.file_attachment = attachmentObject attachmentRow.file_attachment = attachmentObject
const result = await createAutomationBuilder({ const result = await createAutomationBuilder({ config })
name: "Test Create Row Attachment Flow",
appId: config.getAppId(),
config,
})
.appAction({ fields: { type: "attachment" } }) .appAction({ fields: { type: "attachment" } })
.serverLog( .serverLog(
{ text: "Processing attachment upload" }, { text: "Processing attachment upload" },
@ -165,11 +157,11 @@ describe("test the create row action", () => {
}) })
it("should check that an single attachment field is sent to storage and parsed", async () => { it("should check that an single attachment field is sent to storage and parsed", async () => {
let attachmentTable = await config.createTable( let attachmentTable = await config.api.table.save(
basicTableWithAttachmentField() basicTableWithAttachmentField()
) )
let attachmentRow: any = { let attachmentRow: Row = {
tableId: attachmentTable._id, tableId: attachmentTable._id,
} }
@ -181,11 +173,7 @@ describe("test the create row action", () => {
} }
attachmentRow.single_file_attachment = attachmentObject attachmentRow.single_file_attachment = attachmentObject
const result = await createAutomationBuilder({ const result = await createAutomationBuilder({ config })
name: "Test Create Row Single Attachment Flow",
appId: config.getAppId(),
config,
})
.appAction({ fields: { type: "single-attachment" } }) .appAction({ fields: { type: "single-attachment" } })
.serverLog( .serverLog(
{ text: "Processing single attachment" }, { text: "Processing single attachment" },
@ -240,11 +228,11 @@ describe("test the create row action", () => {
}) })
it("should check that attachment without the correct keys throws an error", async () => { it("should check that attachment without the correct keys throws an error", async () => {
let attachmentTable = await config.createTable( let attachmentTable = await config.api.table.save(
basicTableWithAttachmentField() basicTableWithAttachmentField()
) )
let attachmentRow: any = { let attachmentRow: Row = {
tableId: attachmentTable._id, tableId: attachmentTable._id,
} }
@ -256,11 +244,7 @@ describe("test the create row action", () => {
} }
attachmentRow.single_file_attachment = attachmentObject attachmentRow.single_file_attachment = attachmentObject
const result = await createAutomationBuilder({ const result = await createAutomationBuilder({ config })
name: "Test Create Row Invalid Attachment Flow",
appId: config.getAppId(),
config,
})
.appAction({ fields: { type: "invalid-attachment" } }) .appAction({ fields: { type: "invalid-attachment" } })
.serverLog( .serverLog(
{ text: "Testing invalid attachment keys" }, { text: "Testing invalid attachment keys" },

View File

@ -1,8 +1,8 @@
import tk from "timekeeper" import tk from "timekeeper"
import "../../environment" import "../../../environment"
import * as automations from "../index" import * as automations from "../../index"
import * as setup from "./utilities" import TestConfiguration from "../../../tests/utilities/TestConfiguration"
import { basicCronAutomation } from "../../tests/utilities/structures" import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
const initialTime = Date.now() const initialTime = Date.now()
tk.freeze(initialTime) tk.freeze(initialTime)
@ -10,7 +10,7 @@ tk.freeze(initialTime)
const oneMinuteInMs = 60 * 1000 const oneMinuteInMs = 60 * 1000
describe("cron automations", () => { describe("cron automations", () => {
let config = setup.getConfig() const config = new TestConfiguration()
beforeAll(async () => { beforeAll(async () => {
await automations.init() await automations.init()
@ -19,26 +19,22 @@ describe("cron automations", () => {
afterAll(async () => { afterAll(async () => {
await automations.shutdown() await automations.shutdown()
setup.afterAll() config.end()
}) })
beforeEach(() => { beforeEach(() => {
tk.freeze(initialTime) tk.freeze(initialTime)
}) })
async function travel(ms: number) {
tk.travel(Date.now() + ms)
}
it("should initialise the automation timestamp", async () => { it("should initialise the automation timestamp", async () => {
const automation = basicCronAutomation(config.appId!, "* * * * *") await createAutomationBuilder({ config }).cron({ cron: "* * * * *" }).save()
await config.api.automation.post(automation)
await travel(oneMinuteInMs) tk.travel(Date.now() + oneMinuteInMs)
await config.publish() await config.publish()
const automationLogs = await config.getAutomationLogs() const { data } = await config.getAutomationLogs()
expect(automationLogs.data).toHaveLength(1) expect(data).toHaveLength(1)
expect(automationLogs.data).toEqual([ expect(data).toEqual([
expect.objectContaining({ expect.objectContaining({
trigger: expect.objectContaining({ trigger: expect.objectContaining({
outputs: { timestamp: initialTime + oneMinuteInMs }, outputs: { timestamp: initialTime + oneMinuteInMs },

View File

@ -0,0 +1,29 @@
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
describe("test the delay logic", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.init()
})
afterAll(() => {
config.end()
})
it("should be able to run the delay", async () => {
const time = 100
const before = performance.now()
await createAutomationBuilder({ config })
.appAction({ fields: {} })
.delay({ time })
.run()
const now = performance.now()
// divide by two just so that test will always pass as long as there was some sort of delay
expect(now - before).toBeGreaterThanOrEqual(time / 2)
})
})

View File

@ -1,44 +1,41 @@
import { createAutomationBuilder } from "./utilities/AutomationTestBuilder" import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
import * as setup from "./utilities" import { Row, Table } from "@budibase/types"
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
import { basicTable } from "../../../tests/utilities/structures"
describe("test the delete row action", () => { describe("test the delete row action", () => {
let table: any, const config = new TestConfiguration()
row: any,
config = setup.getConfig() let table: Table
let row: Row
beforeAll(async () => { beforeAll(async () => {
await config.init() await config.init()
table = await config.createTable() table = await config.api.table.save(basicTable())
row = await config.createRow() row = await config.api.row.save(table._id!, {})
}) })
afterAll(setup.afterAll) afterAll(() => {
config.end()
})
it("should be able to run the delete row action", async () => { it("should be able to run the delete row action", async () => {
const builder = createAutomationBuilder({ await createAutomationBuilder({ config })
name: "Delete Row Automation",
})
await builder
.appAction({ fields: {} }) .appAction({ fields: {} })
.deleteRow({ .deleteRow({
tableId: table._id, tableId: table._id!,
id: row._id, id: row._id!,
revision: row._rev, revision: row._rev,
}) })
.run() .run()
await config.api.row.get(table._id, row._id, { await config.api.row.get(table._id!, row._id!, {
status: 404, status: 404,
}) })
}) })
it("should check invalid inputs return an error", async () => { it("should check invalid inputs return an error", async () => {
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "Invalid Inputs Automation",
})
const results = await builder
.appAction({ fields: {} }) .appAction({ fields: {} })
.deleteRow({ tableId: "", id: "", revision: "" }) .deleteRow({ tableId: "", id: "", revision: "" })
.run() .run()
@ -47,11 +44,7 @@ describe("test the delete row action", () => {
}) })
it("should return an error when table doesn't exist", async () => { it("should return an error when table doesn't exist", async () => {
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "Nonexistent Table Automation",
})
const results = await builder
.appAction({ fields: {} }) .appAction({ fields: {} })
.deleteRow({ .deleteRow({
tableId: "invalid", tableId: "invalid",

View File

@ -0,0 +1,33 @@
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
import nock from "nock"
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
describe("test the outgoing webhook action", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.init()
})
afterAll(() => {
config.end()
})
beforeEach(() => {
nock.cleanAll()
})
it("should be able to run the action", async () => {
nock("http://www.example.com/").post("/").reply(200, { foo: "bar" })
const result = await createAutomationBuilder({ config })
.appAction({ fields: {} })
.discord({
url: "http://www.example.com",
username: "joe_bloggs",
content: "Hello, world",
})
.run()
expect(result.steps[0].outputs.response.foo).toEqual("bar")
expect(result.steps[0].outputs.success).toEqual(true)
})
})

View File

@ -1,9 +1,9 @@
import { Datasource, Query } from "@budibase/types" import { Datasource, Query } from "@budibase/types"
import * as setup from "./utilities" import * as setup from "../utilities"
import { import {
DatabaseName, DatabaseName,
datasourceDescribe, datasourceDescribe,
} from "../../integrations/tests/utils" } from "../../../integrations/tests/utils"
import { Knex } from "knex" import { Knex } from "knex"
import { generator } from "@budibase/backend-core/tests" import { generator } from "@budibase/backend-core/tests"

View File

@ -1,27 +1,26 @@
import { createAutomationBuilder } from "./utilities/AutomationTestBuilder" import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
import * as automation from "../index" import * as automation from "../../index"
import * as setup from "./utilities"
import { Table } from "@budibase/types" import { Table } from "@budibase/types"
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
import { basicTable } from "../../../tests/utilities/structures"
describe("Execute Script Automations", () => { describe("Execute Script Automations", () => {
let config = setup.getConfig(), const config = new TestConfiguration()
table: Table let table: Table
beforeEach(async () => { beforeAll(async () => {
await automation.init() await automation.init()
await config.init() await config.init()
table = await config.createTable() table = await config.api.table.save(basicTable())
await config.createRow() await config.api.row.save(table._id!, {})
}) })
afterAll(setup.afterAll) afterAll(() => {
config.end()
})
it("should execute a basic script and return the result", async () => { it("should execute a basic script and return the result", async () => {
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "Basic Script Execution",
})
const results = await builder
.appAction({ fields: {} }) .appAction({ fields: {} })
.executeScript({ code: "return 2 + 2" }) .executeScript({ code: "return 2 + 2" })
.run() .run()
@ -30,11 +29,7 @@ describe("Execute Script Automations", () => {
}) })
it("should access bindings from previous steps", async () => { it("should access bindings from previous steps", async () => {
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "Access Bindings",
})
const results = await builder
.appAction({ fields: { data: [1, 2, 3] } }) .appAction({ fields: { data: [1, 2, 3] } })
.executeScript( .executeScript(
{ {
@ -48,11 +43,7 @@ describe("Execute Script Automations", () => {
}) })
it("should handle script execution errors gracefully", async () => { it("should handle script execution errors gracefully", async () => {
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "Handle Script Errors",
})
const results = await builder
.appAction({ fields: {} }) .appAction({ fields: {} })
.executeScript({ code: "return nonexistentVariable.map(x => x)" }) .executeScript({ code: "return nonexistentVariable.map(x => x)" })
.run() .run()
@ -64,11 +55,7 @@ describe("Execute Script Automations", () => {
}) })
it("should handle conditional logic in scripts", async () => { it("should handle conditional logic in scripts", async () => {
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "Conditional Script Logic",
})
const results = await builder
.appAction({ fields: { value: 10 } }) .appAction({ fields: { value: 10 } })
.executeScript({ .executeScript({
code: ` code: `
@ -85,11 +72,7 @@ describe("Execute Script Automations", () => {
}) })
it("should use multiple steps and validate script execution", async () => { it("should use multiple steps and validate script execution", async () => {
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "Multi-Step Script Execution",
})
const results = await builder
.appAction({ fields: {} }) .appAction({ fields: {} })
.serverLog( .serverLog(
{ text: "Starting multi-step automation" }, { text: "Starting multi-step automation" },

View File

@ -0,0 +1,71 @@
import { automations } from "@budibase/shared-core"
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
const FilterConditions = automations.steps.filter.FilterConditions
function stringToFilterCondition(condition: "==" | "!=" | ">" | "<"): string {
switch (condition) {
case "==":
return FilterConditions.EQUAL
case "!=":
return FilterConditions.NOT_EQUAL
case ">":
return FilterConditions.GREATER_THAN
case "<":
return FilterConditions.LESS_THAN
}
}
type TestCase = [any, "==" | "!=" | ">" | "<", any]
describe("test the filter logic", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.init()
})
afterAll(() => {
config.end()
})
const pass: TestCase[] = [
[10, ">", 5],
["10", ">", 5],
[10, ">", "5"],
["10", ">", "5"],
[10, "==", 10],
[10, "<", 15],
["hello", "==", "hello"],
["hello", "!=", "no"],
[new Date().toISOString(), ">", new Date(-10000).toISOString()],
]
it.each(pass)("should pass %p %p %p", async (field, condition, value) => {
const result = await createAutomationBuilder({ config })
.appAction({ fields: {} })
.filter({ field, condition: stringToFilterCondition(condition), value })
.run()
expect(result.steps[0].outputs.result).toEqual(true)
expect(result.steps[0].outputs.success).toEqual(true)
})
const fail: TestCase[] = [
[10, ">", 15],
[10, "<", 5],
[10, "==", 5],
["hello", "==", "no"],
["hello", "!=", "hello"],
[{}, "==", {}],
]
it.each(fail)("should fail %p %p %p", async (field, condition, value) => {
const result = await createAutomationBuilder({ config })
.appAction({ fields: {} })
.filter({ field, condition: stringToFilterCondition(condition), value })
.run()
expect(result.steps[0].outputs.result).toEqual(false)
expect(result.steps[0].outputs.success).toEqual(true)
})
})

View File

@ -1,33 +1,78 @@
import * as automation from "../../index" import * as automation from "../../index"
import * as setup from "../utilities" import * as triggers from "../../triggers"
import { basicTable, loopAutomation } from "../../../tests/utilities/structures"
import { context } from "@budibase/backend-core"
import { import {
Table, Table,
LoopStepType, LoopStepType,
CreateRowStepOutputs, AutomationResults,
ServerLogStepOutputs, ServerLogStepOutputs,
CreateRowStepOutputs,
FieldType, FieldType,
} from "@budibase/types" } from "@budibase/types"
import * as loopUtils from "../../loopUtils"
import { LoopInput } from "../../../definitions/automations"
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder" import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
describe("Loop automations", () => { describe("Attempt to run a basic loop automation", () => {
let config = setup.getConfig(), const config = new TestConfiguration()
table: Table let table: Table
beforeEach(async () => { beforeAll(async () => {
await automation.init()
await config.init() await config.init()
table = await config.createTable() await automation.init()
await config.createRow()
}) })
afterAll(setup.afterAll) beforeEach(async () => {
table = await config.api.table.save(basicTable())
await config.api.row.save(table._id!, {})
})
afterAll(() => {
automation.shutdown()
config.end()
})
async function runLoop(loopOpts?: LoopInput): Promise<AutomationResults> {
const appId = config.getAppId()
return await context.doInAppContext(appId, async () => {
const params = { fields: { appId } }
const result = await triggers.externalTrigger(
loopAutomation(table._id!, loopOpts),
params,
{ getResponses: true }
)
if ("outputs" in result && !result.outputs.success) {
throw new Error("Unable to proceed - failed to return anything.")
}
return result as AutomationResults
})
}
it("attempt to run a basic loop", async () => {
const resp = await runLoop()
expect(resp.steps[2].outputs.iterations).toBe(1)
})
it("test a loop with a string", async () => {
const resp = await runLoop({
option: LoopStepType.STRING,
binding: "a,b,c",
})
expect(resp.steps[2].outputs.iterations).toBe(3)
})
it("test a loop with a binding that returns an integer", async () => {
const resp = await runLoop({
option: LoopStepType.ARRAY,
binding: "{{ 1 }}",
})
expect(resp.steps[2].outputs.iterations).toBe(1)
})
it("should run an automation with a trigger, loop, and create row step", async () => { it("should run an automation with a trigger, loop, and create row step", async () => {
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "Test Trigger with Loop and Create Row",
})
const results = await builder
.rowSaved( .rowSaved(
{ tableId: table._id! }, { tableId: table._id! },
{ {
@ -70,11 +115,7 @@ describe("Loop automations", () => {
}) })
it("should run an automation where a loop step is between two normal steps to ensure context correctness", async () => { it("should run an automation where a loop step is between two normal steps to ensure context correctness", async () => {
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "Test Trigger with Loop and Create Row",
})
const results = await builder
.rowSaved( .rowSaved(
{ tableId: table._id! }, { tableId: table._id! },
{ {
@ -110,11 +151,7 @@ describe("Loop automations", () => {
}) })
it("if an incorrect type is passed to the loop it should return an error", async () => { it("if an incorrect type is passed to the loop it should return an error", async () => {
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "Test Loop error",
})
const results = await builder
.appAction({ fields: {} }) .appAction({ fields: {} })
.loop({ .loop({
option: LoopStepType.ARRAY, option: LoopStepType.ARRAY,
@ -130,11 +167,7 @@ describe("Loop automations", () => {
}) })
it("ensure the loop stops if the failure condition is reached", async () => { it("ensure the loop stops if the failure condition is reached", async () => {
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "Test Loop error",
})
const results = await builder
.appAction({ fields: {} }) .appAction({ fields: {} })
.loop({ .loop({
option: LoopStepType.ARRAY, option: LoopStepType.ARRAY,
@ -153,11 +186,7 @@ describe("Loop automations", () => {
}) })
it("ensure the loop stops if the max iterations are reached", async () => { it("ensure the loop stops if the max iterations are reached", async () => {
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "Test Loop max iterations",
})
const results = await builder
.appAction({ fields: {} }) .appAction({ fields: {} })
.loop({ .loop({
option: LoopStepType.ARRAY, option: LoopStepType.ARRAY,
@ -172,11 +201,7 @@ describe("Loop automations", () => {
}) })
it("should run an automation with loop and max iterations to ensure context correctness further down the tree", async () => { it("should run an automation with loop and max iterations to ensure context correctness further down the tree", async () => {
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "Test context down tree with Loop and max iterations",
})
const results = await builder
.appAction({ fields: {} }) .appAction({ fields: {} })
.loop({ .loop({
option: LoopStepType.ARRAY, option: LoopStepType.ARRAY,
@ -191,11 +216,7 @@ describe("Loop automations", () => {
}) })
it("should run an automation where a loop is successfully run twice", async () => { it("should run an automation where a loop is successfully run twice", async () => {
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "Test Trigger with Loop and Create Row",
})
const results = await builder
.rowSaved( .rowSaved(
{ tableId: table._id! }, { tableId: table._id! },
{ {
@ -257,11 +278,7 @@ describe("Loop automations", () => {
}) })
it("should run an automation where a loop is used twice to ensure context correctness further down the tree", async () => { it("should run an automation where a loop is used twice to ensure context correctness further down the tree", async () => {
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "Test Trigger with Loop and Create Row",
})
const results = await builder
.appAction({ fields: {} }) .appAction({ fields: {} })
.loop({ .loop({
option: LoopStepType.ARRAY, option: LoopStepType.ARRAY,
@ -283,11 +300,7 @@ describe("Loop automations", () => {
}) })
it("should use automation names to loop with", async () => { it("should use automation names to loop with", async () => {
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "Test Trigger with Loop and Create Row",
})
const results = await builder
.appAction({ fields: {} }) .appAction({ fields: {} })
.loop( .loop(
{ {
@ -339,11 +352,7 @@ describe("Loop automations", () => {
await config.api.row.bulkImport(table._id!, { rows }) await config.api.row.bulkImport(table._id!, { rows })
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "Test Loop and Update Row",
})
const results = await builder
.appAction({ fields: {} }) .appAction({ fields: {} })
.queryRows({ .queryRows({
tableId: table._id!, tableId: table._id!,
@ -423,11 +432,7 @@ describe("Loop automations", () => {
await config.api.row.bulkImport(table._id!, { rows }) await config.api.row.bulkImport(table._id!, { rows })
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "Test Loop and Update Row",
})
const results = await builder
.appAction({ fields: {} }) .appAction({ fields: {} })
.queryRows( .queryRows(
{ {
@ -510,11 +515,7 @@ describe("Loop automations", () => {
await config.api.row.bulkImport(table._id!, { rows }) await config.api.row.bulkImport(table._id!, { rows })
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "Test Loop and Delete Row",
})
const results = await builder
.appAction({ fields: {} }) .appAction({ fields: {} })
.queryRows({ .queryRows({
tableId: table._id!, tableId: table._id!,
@ -536,4 +537,98 @@ describe("Loop automations", () => {
expect(results.steps[2].outputs.rows).toHaveLength(0) expect(results.steps[2].outputs.rows).toHaveLength(0)
}) })
describe("replaceFakeBindings", () => {
it("should replace loop bindings in nested objects", () => {
const originalStepInput = {
schema: {
name: {
type: "string",
constraints: {
type: "string",
length: { maximum: null },
presence: false,
},
name: "name",
display: { type: "Text" },
},
},
row: {
tableId: "ta_aaad4296e9f74b12b1b90ef7a84afcad",
name: "{{ loop.currentItem.pokemon }}",
},
}
const loopStepNumber = 3
const result = loopUtils.replaceFakeBindings(
originalStepInput,
loopStepNumber
)
expect(result).toEqual({
schema: {
name: {
type: "string",
constraints: {
type: "string",
length: { maximum: null },
presence: false,
},
name: "name",
display: { type: "Text" },
},
},
row: {
tableId: "ta_aaad4296e9f74b12b1b90ef7a84afcad",
name: "{{ steps.3.currentItem.pokemon }}",
},
})
})
it("should handle null values in nested objects", () => {
const originalStepInput = {
nullValue: null,
nestedNull: {
someKey: null,
},
validValue: "{{ loop.someValue }}",
}
const loopStepNumber = 2
const result = loopUtils.replaceFakeBindings(
originalStepInput,
loopStepNumber
)
expect(result).toEqual({
nullValue: null,
nestedNull: {
someKey: null,
},
validValue: "{{ steps.2.someValue }}",
})
})
it("should handle empty objects and arrays", () => {
const originalStepInput = {
emptyObject: {},
emptyArray: [],
nestedEmpty: {
emptyObj: {},
emptyArr: [],
},
}
const loopStepNumber = 1
const result = loopUtils.replaceFakeBindings(
originalStepInput,
loopStepNumber
)
expect(result).toEqual(originalStepInput)
})
})
}) })

View File

@ -0,0 +1,74 @@
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
import nock from "nock"
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
describe("test the outgoing webhook action", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.init()
})
afterAll(() => {
config.end()
})
beforeEach(() => {
nock.cleanAll()
})
it("should be able to run the action", async () => {
nock("http://www.example.com/").post("/").reply(200, { foo: "bar" })
const result = await createAutomationBuilder({ config })
.appAction({ fields: {} })
.make({
url: "http://www.example.com",
body: null,
})
.run()
expect(result.steps[0].outputs.response.foo).toEqual("bar")
expect(result.steps[0].outputs.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,
}
nock("http://www.example.com/")
.post("/", payload)
.reply(200, { foo: "bar" })
const result = await createAutomationBuilder({ config })
.appAction({ fields: {} })
.make({
body: { value: JSON.stringify(payload) },
url: "http://www.example.com",
})
.run()
expect(result.steps[0].outputs.response.foo).toEqual("bar")
expect(result.steps[0].outputs.success).toEqual(true)
})
it("should return a 400 if the JSON payload string is malformed", async () => {
const result = await createAutomationBuilder({ config })
.appAction({ fields: {} })
.make({
body: { value: "{ invalid json }" },
url: "http://www.example.com",
})
.run()
expect(result.steps[0].outputs.httpStatus).toEqual(400)
expect(result.steps[0].outputs.response).toEqual("Invalid payload JSON")
expect(result.steps[0].outputs.success).toEqual(false)
})
})

View File

@ -0,0 +1,88 @@
import TestConfiguration from "../../..//tests/utilities/TestConfiguration"
import nock from "nock"
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
import { HttpMethod } from "@budibase/types"
describe("test the outgoing webhook action", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.init()
})
afterAll(() => {
config.end()
})
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 result = await createAutomationBuilder({ config })
.appAction({ fields: {} })
.n8n({
url: "http://www.example.com",
body: { test: "IGNORE_ME" },
authorization: "",
})
.run()
expect(result.steps[0].outputs.response).toEqual({ foo: "bar" })
expect(result.steps[0].outputs.httpStatus).toEqual(200)
expect(result.steps[0].outputs.success).toEqual(true)
})
it("should add the payload props when a JSON string is provided", async () => {
nock("http://www.example.com/")
.post("/", { name: "Adam", age: 9 })
.reply(200)
const result = await createAutomationBuilder({ config })
.appAction({ fields: {} })
.n8n({
url: "http://www.example.com",
body: { value: JSON.stringify({ name: "Adam", age: 9 }) },
method: HttpMethod.POST,
authorization: "",
})
.run()
expect(result.steps[0].outputs.success).toEqual(true)
})
it("should return a 400 if the JSON payload string is malformed", async () => {
const result = await createAutomationBuilder({ config })
.appAction({ fields: {} })
.n8n({
url: "http://www.example.com",
body: { value: "{ value1 1 }" },
method: HttpMethod.POST,
authorization: "",
})
.run()
expect(result.steps[0].outputs.httpStatus).toEqual(400)
expect(result.steps[0].outputs.response).toEqual("Invalid payload JSON")
expect(result.steps[0].outputs.success).toEqual(false)
})
it("should not append the body if the method is HEAD", async () => {
nock("http://www.example.com/")
.head("/", body => body === "")
.reply(200)
const result = await createAutomationBuilder({ config })
.appAction({ fields: {} })
.n8n({
url: "http://www.example.com",
method: HttpMethod.HEAD,
body: { test: "IGNORE_ME" },
authorization: "",
})
.run()
expect(result.steps[0].outputs.success).toEqual(true)
})
})

View File

@ -0,0 +1,119 @@
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
import { setEnv as setCoreEnv } from "@budibase/backend-core"
import { Model, MonthlyQuotaName, QuotaUsageType } from "@budibase/types"
import TestConfiguration from "../../..//tests/utilities/TestConfiguration"
import {
mockChatGPTError,
mockChatGPTResponse,
} from "../../../tests/utilities/mocks/openai"
import nock from "nock"
import { mocks } from "@budibase/backend-core/tests"
import { quotas } from "@budibase/pro"
describe("test the openai action", () => {
const config = new TestConfiguration()
let resetEnv: () => void | undefined
beforeAll(async () => {
await config.init()
})
beforeEach(() => {
resetEnv = setCoreEnv({ SELF_HOSTED: true, OPENAI_API_KEY: "abc123" })
})
afterEach(() => {
resetEnv()
jest.clearAllMocks()
nock.cleanAll()
})
afterAll(() => {
config.end()
})
const getAIUsage = async () => {
const { total } = await config.doInContext(config.getProdAppId(), () =>
quotas.getCurrentUsageValues(
QuotaUsageType.MONTHLY,
MonthlyQuotaName.BUDIBASE_AI_CREDITS
)
)
return total
}
const expectAIUsage = async <T>(expected: number, f: () => Promise<T>) => {
const before = await getAIUsage()
const result = await f()
const after = await getAIUsage()
expect(after - before).toEqual(expected)
return result
}
it("should be able to receive a response from ChatGPT given a prompt", async () => {
mockChatGPTResponse("This is a test")
// The AI usage is 0 because the AI feature is disabled by default, which
// means it goes through the "legacy" path which requires you to set your
// own API key. We don't count this against your quota.
const result = await expectAIUsage(0, () =>
createAutomationBuilder({ config })
.appAction({ fields: {} })
.openai({ prompt: "Hello, world", model: Model.GPT_4O_MINI })
.run()
)
expect(result.steps[0].outputs.response).toEqual("This is a test")
expect(result.steps[0].outputs.success).toBeTruthy()
})
it("should present the correct error message when a prompt is not provided", async () => {
const result = await expectAIUsage(0, () =>
createAutomationBuilder({ config })
.appAction({ fields: {} })
.openai({ prompt: "", model: Model.GPT_4O_MINI })
.run()
)
expect(result.steps[0].outputs.response).toEqual(
"Budibase OpenAI Automation Failed: No prompt supplied"
)
expect(result.steps[0].outputs.success).toBeFalsy()
})
it("should present the correct error message when an error is thrown from the createChatCompletion call", async () => {
mockChatGPTError()
const result = await expectAIUsage(0, () =>
createAutomationBuilder({ config })
.appAction({ fields: {} })
.openai({ prompt: "Hello, world", model: Model.GPT_4O_MINI })
.run()
)
expect(result.steps[0].outputs.response).toEqual(
"Error: 500 Internal Server Error"
)
expect(result.steps[0].outputs.success).toBeFalsy()
})
it("should ensure that the pro AI module is called when the budibase AI features are enabled", async () => {
mocks.licenses.useBudibaseAI()
mocks.licenses.useAICustomConfigs()
mockChatGPTResponse("This is a test")
// We expect a non-0 AI usage here because it goes through the @budibase/pro
// path, because we've enabled Budibase AI. The exact value depends on a
// calculation we use to approximate cost. This uses Budibase's OpenAI API
// key, so we charge users for it.
const result = await expectAIUsage(14, () =>
createAutomationBuilder({ config })
.appAction({ fields: {} })
.openai({ model: Model.GPT_4O_MINI, prompt: "Hello, world" })
.run()
)
expect(result.steps[0].outputs.response).toEqual("This is a test")
})
})

View File

@ -0,0 +1,53 @@
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
import nock from "nock"
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
import { RequestType } from "@budibase/types"
describe("test the outgoing webhook action", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.init()
})
afterAll(() => {
config.end()
})
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 result = await createAutomationBuilder({ config })
.appAction({ fields: {} })
.outgoingWebhook({
requestMethod: RequestType.POST,
url: "http://www.example.com",
requestBody: JSON.stringify({ a: 1 }),
headers: {},
})
.run()
expect(result.steps[0].outputs.success).toEqual(true)
expect(result.steps[0].outputs.httpStatus).toEqual(200)
expect(result.steps[0].outputs.response.foo).toEqual("bar")
})
it("should return an error if something goes wrong in fetch", async () => {
const result = await createAutomationBuilder({ config })
.appAction({ fields: {} })
.outgoingWebhook({
requestMethod: RequestType.GET,
url: "www.invalid.com",
requestBody: "",
headers: {},
})
.run()
expect(result.steps[0].outputs.success).toEqual(false)
})
})

View File

@ -1,30 +1,31 @@
import { EmptyFilterOption, SortOrder, Table } from "@budibase/types" import { EmptyFilterOption, SortOrder, Table } from "@budibase/types"
import * as setup from "./utilities" import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
import { createAutomationBuilder } from "./utilities/AutomationTestBuilder" import * as automation from "../../index"
import * as automation from "../index" import { basicTable } from "../../../tests/utilities/structures"
import { basicTable } from "../../tests/utilities/structures" import TestConfiguration from "../../../tests/utilities/TestConfiguration"
const NAME = "Test" const NAME = "Test"
describe("Test a query step automation", () => { describe("Test a query step automation", () => {
const config = new TestConfiguration()
let table: Table let table: Table
let config = setup.getConfig()
beforeAll(async () => { beforeAll(async () => {
await automation.init() await automation.init()
await config.init() await config.init()
table = await config.createTable() table = await config.api.table.save(basicTable())
const row = { const row = {
name: NAME, name: NAME,
description: "original description", description: "original description",
tableId: table._id,
} }
await config.createRow(row) await config.api.row.save(table._id!, row)
await config.createRow(row) await config.api.row.save(table._id!, row)
}) })
afterAll(setup.afterAll) afterAll(() => {
config.end()
})
it("should be able to run the query step", async () => { it("should be able to run the query step", async () => {
const result = await createAutomationBuilder({ const result = await createAutomationBuilder({
@ -157,13 +158,12 @@ describe("Test a query step automation", () => {
}) })
it("return rows when querying a table with a space in the name", async () => { it("return rows when querying a table with a space in the name", async () => {
const tableWithSpaces = await config.createTable({ const tableWithSpaces = await config.api.table.save({
...basicTable(), ...basicTable(),
name: "table with spaces", name: "table with spaces",
}) })
await config.createRow({ await config.api.row.save(tableWithSpaces._id!, {
name: NAME, name: NAME,
tableId: tableWithSpaces._id,
}) })
const result = await createAutomationBuilder({ const result = await createAutomationBuilder({
name: "Return All Test", name: "Return All Test",

View File

@ -1,6 +1,7 @@
import * as workerRequests from "../../utilities/workerRequests" import TestConfiguration from "../../../tests/utilities/TestConfiguration"
import * as workerRequests from "../../../utilities/workerRequests"
jest.mock("../../utilities/workerRequests", () => ({ jest.mock("../../../utilities/workerRequests", () => ({
sendSmtpEmail: jest.fn(), sendSmtpEmail: jest.fn(),
})) }))
@ -18,16 +19,18 @@ function generateResponse(to: string, from: string) {
} }
} }
import * as setup from "./utilities" import * as setup from "../utilities"
describe("test the outgoing webhook action", () => { describe("test the outgoing webhook action", () => {
let inputs const config = new TestConfiguration()
let config = setup.getConfig()
beforeAll(async () => { beforeAll(async () => {
await config.init() await config.init()
}) })
afterAll(setup.afterAll) afterAll(() => {
config.end()
})
it("should be able to run the action", async () => { it("should be able to run the action", async () => {
jest jest
@ -42,7 +45,7 @@ describe("test the outgoing webhook action", () => {
location: "location", location: "location",
url: "url", url: "url",
} }
inputs = { const inputs = {
to: "user1@example.com", to: "user1@example.com",
from: "admin@example.com", from: "admin@example.com",
subject: "hello", subject: "hello",

View File

@ -0,0 +1,25 @@
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
describe("test the server log action", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.init()
})
afterAll(() => {
config.end()
})
it("should be able to log the text", async () => {
const result = await createAutomationBuilder({ config })
.appAction({ fields: {} })
.serverLog({ text: "Hello World" })
.run()
expect(result.steps[0].outputs.message).toEqual(
`App ${config.getAppId()} - Hello World`
)
expect(result.steps[0].outputs.success).toEqual(true)
})
})

View File

@ -0,0 +1,52 @@
import * as automation from "../../index"
import env from "../../../environment"
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
describe("Test triggering an automation from another automation", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await automation.init()
await config.init()
})
afterAll(async () => {
await automation.shutdown()
config.end()
})
it("should trigger an other server log automation", async () => {
const automation = await createAutomationBuilder({ config })
.appAction({ fields: {} })
.serverLog({ text: "Hello World" })
.save()
const result = await createAutomationBuilder({ config })
.appAction({ fields: {} })
.triggerAutomationRun({
automation: {
automationId: automation._id!,
},
timeout: env.getDefaults().AUTOMATION_THREAD_TIMEOUT,
})
.run()
expect(result.steps[0].outputs.success).toBe(true)
})
it("should fail gracefully if the automation id is incorrect", async () => {
const result = await createAutomationBuilder({ config })
.appAction({ fields: {} })
.triggerAutomationRun({
automation: {
// @ts-expect-error - incorrect on purpose
automationId: null,
},
timeout: env.getDefaults().AUTOMATION_THREAD_TIMEOUT,
})
.run()
expect(result.steps[0].outputs.success).toBe(false)
})
})

View File

@ -8,15 +8,16 @@ import {
Table, Table,
TableSourceType, TableSourceType,
} from "@budibase/types" } from "@budibase/types"
import { createAutomationBuilder } from "./utilities/AutomationTestBuilder" import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
import * as setup from "./utilities"
import * as uuid from "uuid" import * as uuid from "uuid"
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
describe("test the update row action", () => { describe("test the update row action", () => {
let table: Table, const config = new TestConfiguration()
row: Row,
config = setup.getConfig() let table: Table
let row: Row
beforeAll(async () => { beforeAll(async () => {
await config.init() await config.init()
@ -24,14 +25,12 @@ describe("test the update row action", () => {
row = await config.createRow() row = await config.createRow()
}) })
afterAll(setup.afterAll) afterAll(() => {
config.end()
})
it("should be able to run the update row action", async () => { it("should be able to run the update row action", async () => {
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "Update Row Automation",
})
const results = await builder
.appAction({ fields: {} }) .appAction({ fields: {} })
.updateRow({ .updateRow({
rowId: row._id!, rowId: row._id!,
@ -54,11 +53,7 @@ describe("test the update row action", () => {
}) })
it("should check invalid inputs return an error", async () => { it("should check invalid inputs return an error", async () => {
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "Invalid Inputs Automation",
})
const results = await builder
.appAction({ fields: {} }) .appAction({ fields: {} })
.updateRow({ meta: {}, row: {}, rowId: "" }) .updateRow({ meta: {}, row: {}, rowId: "" })
.run() .run()
@ -67,11 +62,7 @@ describe("test the update row action", () => {
}) })
it("should return an error when table doesn't exist", async () => { it("should return an error when table doesn't exist", async () => {
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "Nonexistent Table Automation",
})
const results = await builder
.appAction({ fields: {} }) .appAction({ fields: {} })
.updateRow({ .updateRow({
row: { _id: "invalid" }, row: { _id: "invalid" },
@ -115,11 +106,7 @@ describe("test the update row action", () => {
user2: [{ _id: user2._id }], user2: [{ _id: user2._id }],
}) })
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "Link Preservation Automation",
})
const results = await builder
.appAction({ fields: {} }) .appAction({ fields: {} })
.updateRow({ .updateRow({
rowId: row._id!, rowId: row._id!,
@ -173,11 +160,7 @@ describe("test the update row action", () => {
user2: [{ _id: user2._id }], user2: [{ _id: user2._id }],
}) })
const builder = createAutomationBuilder({ const results = await createAutomationBuilder({ config })
name: "Link Overwrite Automation",
})
const results = await builder
.appAction({ fields: {} }) .appAction({ fields: {} })
.updateRow({ .updateRow({
rowId: row._id!, rowId: row._id!,

View File

@ -0,0 +1,72 @@
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
import nock from "nock"
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
describe("test the outgoing webhook action", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.init()
})
afterAll(() => {
config.end()
})
beforeEach(() => {
nock.cleanAll()
})
it("should be able to run the action", async () => {
nock("http://www.example.com/").post("/").reply(200, { foo: "bar" })
const result = await createAutomationBuilder({ config })
.appAction({ fields: {} })
.zapier({ url: "http://www.example.com", body: null })
.run()
expect(result.steps[0].outputs.response.foo).toEqual("bar")
expect(result.steps[0].outputs.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,
}
nock("http://www.example.com/")
.post("/", { ...payload, platform: "budibase" })
.reply(200, { foo: "bar" })
const result = await createAutomationBuilder({ config })
.appAction({ fields: {} })
.zapier({
url: "http://www.example.com",
body: { value: JSON.stringify(payload) },
})
.run()
expect(result.steps[0].outputs.response.foo).toEqual("bar")
expect(result.steps[0].outputs.success).toEqual(true)
})
it("should return a 400 if the JSON payload string is malformed", async () => {
const result = await createAutomationBuilder({ config })
.appAction({ fields: {} })
.zapier({
url: "http://www.example.com",
body: { value: "{ invalid json }" },
})
.run()
expect(result.steps[0].outputs.success).toEqual(false)
expect(result.steps[0].outputs.response).toEqual("Invalid payload JSON")
expect(result.steps[0].outputs.httpStatus).toEqual(400)
})
})

View File

@ -1,54 +0,0 @@
jest.spyOn(global.console, "error")
import * as setup from "./utilities"
import * as automation from "../index"
import { serverLogAutomation } from "../../tests/utilities/structures"
import env from "../../environment"
describe("Test triggering an automation from another automation", () => {
let config = setup.getConfig()
beforeAll(async () => {
await automation.init()
await config.init()
})
afterAll(async () => {
await automation.shutdown()
setup.afterAll()
})
it("should trigger an other server log automation", async () => {
let automation = serverLogAutomation()
let newAutomation = await config.createAutomation(automation)
const inputs: any = {
automation: {
automationId: newAutomation._id,
timeout: env.getDefaults().AUTOMATION_THREAD_TIMEOUT,
},
}
const res = await setup.runStep(
config,
setup.actions.TRIGGER_AUTOMATION_RUN.stepId,
inputs
)
// Check if the SERVER_LOG step was successful
expect(res.value[1].outputs.success).toBe(true)
})
it("should fail gracefully if the automation id is incorrect", async () => {
const inputs: any = {
automation: {
automationId: null,
timeout: env.getDefaults().AUTOMATION_THREAD_TIMEOUT,
},
}
const res = await setup.runStep(
config,
setup.actions.TRIGGER_AUTOMATION_RUN.stepId,
inputs
)
expect(res.success).toBe(false)
})
})

View File

@ -1,21 +1,18 @@
import * as automation from "../../index" import * as automation from "../../index"
import * as setup from "../utilities"
import { Table, Webhook, WebhookActionType } from "@budibase/types" import { Table, Webhook, WebhookActionType } from "@budibase/types"
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder" import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
import { mocks } from "@budibase/backend-core/tests" import { mocks } from "@budibase/backend-core/tests"
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
mocks.licenses.useSyncAutomations() mocks.licenses.useSyncAutomations()
describe("Branching automations", () => { describe("Branching automations", () => {
let config = setup.getConfig(), const config = new TestConfiguration()
table: Table, let table: Table
webhook: Webhook let webhook: Webhook
async function createWebhookAutomation(testName: string) { async function createWebhookAutomation() {
const builder = createAutomationBuilder({ const automation = await createAutomationBuilder({ config })
name: testName,
})
const automation = await builder
.webhook({ fields: { parameter: "string" } }) .webhook({ fields: { parameter: "string" } })
.createRow({ .createRow({
row: { tableId: table._id!, name: "{{ trigger.parameter }}" }, row: { tableId: table._id!, name: "{{ trigger.parameter }}" },
@ -45,12 +42,12 @@ describe("Branching automations", () => {
table = await config.createTable() table = await config.createTable()
}) })
afterAll(setup.afterAll) afterAll(() => {
config.end()
})
it("should run the webhook automation - checking for parameters", async () => { it("should run the webhook automation - checking for parameters", async () => {
const { webhook } = await createWebhookAutomation( const { webhook } = await createWebhookAutomation()
"Check a basic webhook works as expected"
)
const res = await config.api.webhook.trigger( const res = await config.api.webhook.trigger(
config.getProdAppId(), config.getProdAppId(),
webhook._id!, webhook._id!,

View File

@ -17,14 +17,20 @@ import {
BranchStepInputs, BranchStepInputs,
CollectStepInputs, CollectStepInputs,
CreateRowStepInputs, CreateRowStepInputs,
CronTriggerInputs,
CronTriggerOutputs, CronTriggerOutputs,
DelayStepInputs,
DeleteRowStepInputs, DeleteRowStepInputs,
DiscordStepInputs,
ExecuteQueryStepInputs, ExecuteQueryStepInputs,
ExecuteScriptStepInputs, ExecuteScriptStepInputs,
FilterStepInputs, FilterStepInputs,
isDidNotTriggerResponse, isDidNotTriggerResponse,
LoopStepInputs, LoopStepInputs,
MakeIntegrationInputs,
n8nStepInputs,
OpenAIStepInputs, OpenAIStepInputs,
OutgoingWebhookStepInputs,
QueryRowsStepInputs, QueryRowsStepInputs,
RowCreatedTriggerInputs, RowCreatedTriggerInputs,
RowCreatedTriggerOutputs, RowCreatedTriggerOutputs,
@ -36,9 +42,11 @@ import {
ServerLogStepInputs, ServerLogStepInputs,
SmtpEmailStepInputs, SmtpEmailStepInputs,
TestAutomationRequest, TestAutomationRequest,
TriggerAutomationStepInputs,
UpdateRowStepInputs, UpdateRowStepInputs,
WebhookTriggerInputs, WebhookTriggerInputs,
WebhookTriggerOutputs, WebhookTriggerOutputs,
ZapierStepInputs,
} from "@budibase/types" } from "@budibase/types"
import TestConfiguration from "../../../tests/utilities/TestConfiguration" import TestConfiguration from "../../../tests/utilities/TestConfiguration"
import * as setup from "../utilities" import * as setup from "../utilities"
@ -263,6 +271,90 @@ class BaseStepBuilder {
opts opts
) )
} }
zapier(
input: ZapierStepInputs,
opts?: { stepName?: string; stepId?: string }
): this {
return this.step(
AutomationActionStepId.zapier,
BUILTIN_ACTION_DEFINITIONS.zapier,
input,
opts
)
}
triggerAutomationRun(
input: TriggerAutomationStepInputs,
opts?: { stepName?: string; stepId?: string }
): this {
return this.step(
AutomationActionStepId.TRIGGER_AUTOMATION_RUN,
BUILTIN_ACTION_DEFINITIONS.TRIGGER_AUTOMATION_RUN,
input,
opts
)
}
outgoingWebhook(
input: OutgoingWebhookStepInputs,
opts?: { stepName?: string; stepId?: string }
): this {
return this.step(
AutomationActionStepId.OUTGOING_WEBHOOK,
BUILTIN_ACTION_DEFINITIONS.OUTGOING_WEBHOOK,
input,
opts
)
}
n8n(
input: n8nStepInputs,
opts?: { stepName?: string; stepId?: string }
): this {
return this.step(
AutomationActionStepId.n8n,
BUILTIN_ACTION_DEFINITIONS.n8n,
input,
opts
)
}
make(
input: MakeIntegrationInputs,
opts?: { stepName?: string; stepId?: string }
): this {
return this.step(
AutomationActionStepId.integromat,
BUILTIN_ACTION_DEFINITIONS.integromat,
input,
opts
)
}
discord(
input: DiscordStepInputs,
opts?: { stepName?: string; stepId?: string }
): this {
return this.step(
AutomationActionStepId.discord,
BUILTIN_ACTION_DEFINITIONS.discord,
input,
opts
)
}
delay(
input: DelayStepInputs,
opts?: { stepName?: string; stepId?: string }
): this {
return this.step(
AutomationActionStepId.DELAY,
BUILTIN_ACTION_DEFINITIONS.DELAY,
input,
opts
)
}
} }
class StepBuilder extends BaseStepBuilder { class StepBuilder extends BaseStepBuilder {
@ -286,6 +378,7 @@ class AutomationBuilder extends BaseStepBuilder {
options: { name?: string; appId?: string; config?: TestConfiguration } = {} options: { name?: string; appId?: string; config?: TestConfiguration } = {}
) { ) {
super() super()
this.config = options.config || setup.getConfig()
this.automationConfig = { this.automationConfig = {
name: options.name || `Test Automation ${uuidv4()}`, name: options.name || `Test Automation ${uuidv4()}`,
definition: { definition: {
@ -294,9 +387,8 @@ class AutomationBuilder extends BaseStepBuilder {
stepNames: {}, stepNames: {},
}, },
type: "automation", type: "automation",
appId: options.appId ?? setup.getConfig().getAppId(), appId: options.appId ?? this.config.getAppId(),
} }
this.config = options.config || setup.getConfig()
} }
// TRIGGERS // TRIGGERS
@ -356,6 +448,16 @@ class AutomationBuilder extends BaseStepBuilder {
) )
} }
cron(inputs: CronTriggerInputs, outputs?: CronTriggerOutputs) {
this.triggerOutputs = outputs
return this.trigger(
TRIGGER_DEFINITIONS.CRON,
AutomationTriggerStepId.CRON,
inputs,
outputs
)
}
private trigger<TStep extends AutomationTriggerStepId>( private trigger<TStep extends AutomationTriggerStepId>(
triggerSchema: AutomationTriggerDefinition, triggerSchema: AutomationTriggerDefinition,
stepId: TStep, stepId: TStep,
@ -393,7 +495,8 @@ class AutomationBuilder extends BaseStepBuilder {
throw new Error("Please add a trigger to this automation test") throw new Error("Please add a trigger to this automation test")
} }
this.automationConfig.definition.steps = this.steps this.automationConfig.definition.steps = this.steps
return await this.config.createAutomation(this.build()) const { automation } = await this.config.api.automation.post(this.build())
return automation
} }
async run() { async run() {

View File

@ -1,58 +0,0 @@
import { getConfig, afterAll, runStep, actions } from "./utilities"
import nock from "nock"
describe("test the outgoing webhook action", () => {
let config = getConfig()
beforeAll(async () => {
await config.init()
})
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(config, actions.zapier.stepId, {
url: "http://www.example.com",
})
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,
}
nock("http://www.example.com/")
.post("/", { ...payload, platform: "budibase" })
.reply(200, { foo: "bar" })
const res = await runStep(config, actions.zapier.stepId, {
body: { value: JSON.stringify(payload) },
url: "http://www.example.com",
})
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 res = await runStep(config, actions.zapier.stepId, {
body: { value: "{ invalid json }" },
url: "http://www.example.com",
})
expect(res.httpStatus).toEqual(400)
expect(res.response).toEqual("Invalid payload JSON")
expect(res.success).toEqual(false)
})
})

View File

@ -1,26 +1,82 @@
import nock from "nock" import nock from "nock"
let chatID = 1 let chatID = 1
const SPACE_REGEX = /\s+/g
interface MockChatGPTResponseOpts {
host?: string
}
interface Message {
role: string
content: string
}
interface Choice {
index: number
message: Message
logprobs: null
finish_reason: string
}
interface CompletionTokensDetails {
reasoning_tokens: number
accepted_prediction_tokens: number
rejected_prediction_tokens: number
}
interface Usage {
prompt_tokens: number
completion_tokens: number
total_tokens: number
completion_tokens_details: CompletionTokensDetails
}
interface ChatCompletionRequest {
messages: Message[]
model: string
}
interface ChatCompletionResponse {
id: string
object: string
created: number
model: string
system_fingerprint: string
choices: Choice[]
usage: Usage
}
export function mockChatGPTResponse( export function mockChatGPTResponse(
response: string | ((prompt: string) => string) answer: string | ((prompt: string) => string),
opts?: MockChatGPTResponseOpts
) { ) {
return nock("https://api.openai.com") return nock(opts?.host || "https://api.openai.com")
.post("/v1/chat/completions") .post("/v1/chat/completions")
.reply(200, (uri, requestBody) => { .reply(200, (uri: string, requestBody: ChatCompletionRequest) => {
let content = response const messages = requestBody.messages
if (typeof response === "function") { const prompt = messages[0].content
const messages = (requestBody as any).messages
content = response(messages[0].content) let content
if (typeof answer === "function") {
content = answer(prompt)
} else {
content = answer
} }
chatID++ chatID++
return { // We mock token usage because we use it to calculate Budibase AI quota
// usage when Budibase AI is enabled, and some tests assert against quota
// usage to make sure we're tracking correctly.
const prompt_tokens = messages[0].content.split(SPACE_REGEX).length
const completion_tokens = content.split(SPACE_REGEX).length
const response: ChatCompletionResponse = {
id: `chatcmpl-${chatID}`, id: `chatcmpl-${chatID}`,
object: "chat.completion", object: "chat.completion",
created: Math.floor(Date.now() / 1000), created: Math.floor(Date.now() / 1000),
model: "gpt-4o-mini", model: requestBody.model,
system_fingerprint: `fp_${chatID}`, system_fingerprint: `fp_${chatID}`,
choices: [ choices: [
{ {
@ -31,9 +87,9 @@ export function mockChatGPTResponse(
}, },
], ],
usage: { usage: {
prompt_tokens: 0, prompt_tokens,
completion_tokens: 0, completion_tokens,
total_tokens: 0, total_tokens: prompt_tokens + completion_tokens,
completion_tokens_details: { completion_tokens_details: {
reasoning_tokens: 0, reasoning_tokens: 0,
accepted_prediction_tokens: 0, accepted_prediction_tokens: 0,
@ -41,6 +97,14 @@ export function mockChatGPTResponse(
}, },
}, },
} }
return response
}) })
.persist() .persist()
} }
export function mockChatGPTError() {
return nock("https://api.openai.com")
.post("/v1/chat/completions")
.reply(500, "Internal Server Error")
.persist()
}

View File

@ -238,88 +238,6 @@ export function basicAutomation(opts?: DeepPartial<Automation>): Automation {
return merge(baseAutomation, opts) return merge(baseAutomation, opts)
} }
export function basicCronAutomation(appId: string, cron: string): Automation {
const automation: Automation = {
name: `Automation ${generator.guid()}`,
definition: {
trigger: {
stepId: AutomationTriggerStepId.CRON,
name: "test",
tagline: "test",
icon: "test",
description: "test",
type: AutomationStepType.TRIGGER,
id: "test",
inputs: {
cron,
},
schema: {
inputs: {
properties: {},
},
outputs: {
properties: {},
},
},
},
steps: [],
},
type: "automation",
appId,
}
return automation
}
export function serverLogAutomation(appId?: string): Automation {
return {
name: "My Automation",
screenId: "kasdkfldsafkl",
live: true,
uiTree: {},
definition: {
trigger: {
stepId: AutomationTriggerStepId.APP,
name: "test",
tagline: "test",
icon: "test",
description: "test",
type: AutomationStepType.TRIGGER,
id: "test",
inputs: { fields: {} },
schema: {
inputs: {
properties: {},
},
outputs: {
properties: {},
},
},
},
steps: [
{
stepId: AutomationActionStepId.SERVER_LOG,
name: "Backend log",
tagline: "Console log a value in the backend",
icon: "Monitoring",
description: "Logs the given text to the server (using console.log)",
internal: true,
features: {
LOOPING: true,
},
inputs: {
text: "log statement",
},
schema: BUILTIN_ACTION_DEFINITIONS.SERVER_LOG.schema,
id: "y8lkZbeSe",
type: AutomationStepType.ACTION,
},
],
},
type: "automation",
appId: appId!,
}
}
export function loopAutomation( export function loopAutomation(
tableId: string, tableId: string,
loopOpts?: LoopInput loopOpts?: LoopInput

View File

@ -141,7 +141,7 @@ export type MakeIntegrationInputs = {
export type n8nStepInputs = { export type n8nStepInputs = {
url: string url: string
method: HttpMethod method?: HttpMethod
authorization: string authorization: string
body: any body: any
} }
@ -237,7 +237,8 @@ export type ZapierStepInputs = {
export type ZapierStepOutputs = Omit<ExternalAppStepOutputs, "response"> & { export type ZapierStepOutputs = Omit<ExternalAppStepOutputs, "response"> & {
response: string response: string
} }
enum RequestType {
export enum RequestType {
POST = "POST", POST = "POST",
GET = "GET", GET = "GET",
PUT = "PUT", PUT = "PUT",
@ -249,7 +250,7 @@ export type OutgoingWebhookStepInputs = {
requestMethod: RequestType requestMethod: RequestType
url: string url: string
requestBody: string requestBody: string
headers: string headers: string | Record<string, string>
} }
export type AppActionTriggerInputs = { export type AppActionTriggerInputs = {