import { processStringSync, encodeJSBinding, defaultJSSetup, } from "../src/index" import { UUID_REGEX } from "./constants" import tk from "timekeeper" const DATE = "2021-01-21T12:00:00" tk.freeze(DATE) const processJS = (js: string, context?: object): any => { return processStringSync(encodeJSBinding(js), context) } describe("Javascript", () => { beforeAll(() => { defaultJSSetup() }) describe("Test the JavaScript helper", () => { it("should execute a simple expression", () => { const output = processJS(`return 1 + 2`) expect(output).toBe(3) }) it("should be able to use primitive bindings", () => { const output = processJS(`return $("foo")`, { foo: "bar", }) expect(output).toBe("bar") }) it("should be able to use an object binding", () => { const output = processJS(`return $("foo").bar`, { foo: { bar: "baz", }, }) expect(output).toBe("baz") }) it("should be able to use a complex object binding", () => { const output = processJS(`return $("foo").bar[0].baz`, { foo: { bar: [ { baz: "shazbat", }, ], }, }) expect(output).toBe("shazbat") }) it("should be able to use a deep binding", () => { const output = processJS(`return $("foo.bar.baz")`, { foo: { bar: { baz: "shazbat", }, }, }) expect(output).toBe("shazbat") }) it("should be able to return an object", () => { const output = processJS(`return $("foo")`, { foo: { bar: { baz: "shazbat", }, }, }) expect(output.bar.baz).toBe("shazbat") }) it("should be able to return an array", () => { const output = processJS(`return $("foo")`, { foo: ["a", "b", "c"], }) expect(output[2]).toBe("c") }) it("should be able to return null", () => { const output = processJS(`return $("foo")`, { foo: null, }) expect(output).toBe(null) }) it("should be able to return undefined", () => { const output = processJS(`return $("foo")`, { foo: undefined, }) expect(output).toBe(undefined) }) it("should be able to return 0", () => { const output = processJS(`return $("foo")`, { foo: 0, }) expect(output).toBe(0) }) it("should be able to return an empty string", () => { const output = processJS(`return $("foo")`, { foo: "", }) expect(output).toBe("") }) it("should be able to use a deep array binding", () => { const output = processJS(`return $("foo.0.bar")`, { foo: [ { bar: "baz", }, ], }) expect(output).toBe("baz") }) it("should handle errors", () => { expect(processJS(`throw "Error"`)).toEqual("Error") }) it("should timeout after one second", () => { const output = processJS(`while (true) {}`) expect(output).toBe("Timed out while executing JS") }) it("should prevent access to the process global", async () => { expect(processJS(`return process`)).toEqual( "ReferenceError: process is not defined" ) }) }) describe("check JS helpers", () => { it("should error if using the format helper. not helpers.", () => { expect(processJS(`return helper.toInt(4.3)`)).toEqual( "ReferenceError: helper is not defined" ) }) it("should be able to use toInt", () => { const output = processJS(`return helpers.toInt(4.3)`) expect(output).toBe(4) }) it("should be able to use uuid", () => { const output = processJS(`return helpers.uuid()`) expect(output).toMatch(UUID_REGEX) }) }) describe("JS literal strings", () => { it("should be able to handle a literal string that is quoted (like role IDs)", () => { const output = processJS(`return $("'Custom'")`) expect(output).toBe("Custom") }) }) describe("mutability", () => { it("should not allow the context to be mutated", async () => { const context = { array: [1] } const result = await processJS( ` const array = $("array"); array.push(2); return array[1] `, context ) expect(result).toEqual(2) expect(context.array).toEqual([1]) }) }) describe("malice", () => { it("should not be able to call JS functions", () => { expect(processJS(`return alert("hello")`)).toEqual( "ReferenceError: alert is not defined" ) expect(processJS(`return prompt("hello")`)).toEqual( "ReferenceError: prompt is not defined" ) expect(processJS(`return confirm("hello")`)).toEqual( "ReferenceError: confirm is not defined" ) expect(processJS(`return setTimeout(() => {}, 1000)`)).toEqual( "ReferenceError: setTimeout is not defined" ) expect(processJS(`return setInterval(() => {}, 1000)`)).toEqual( "ReferenceError: setInterval is not defined" ) }) }) // the test cases here were extracted from templates/real world examples of JS in Budibase describe("real test cases from Budicloud", () => { const context = { "Unit Value": 2, Quantity: 1, } it("handle test case 1", async () => { const result = await processJS( ` var Gross = $("[Unit Value]") * $("[Quantity]") return Gross.toFixed(2)`, context ) expect(result).toBeDefined() expect(result).toBe("2.00") }) it("handle test case 2", async () => { const todayDate = new Date() // add a year and a month todayDate.setMonth(new Date().getMonth() + 1) todayDate.setFullYear(todayDate.getFullYear() + 1) const context = { "Purchase Date": DATE, today: todayDate.toISOString(), } const result = await processJS( ` var purchase = new Date($("[Purchase Date]")); let purchaseyear = purchase.getFullYear(); let purchasemonth = purchase.getMonth(); var today = new Date($("today")); let todayyear = today.getFullYear(); let todaymonth = today.getMonth(); var age = todayyear - purchaseyear if (((todaymonth - purchasemonth) < 6) == true){ return age } `, context ) expect(result).toBeDefined() expect(result).toBe(1) }) it("should handle test case 3", async () => { const context = { Escalate: true, "Budget ($)": 1100, } const result = await processJS( ` if ($("[Escalate]") == true) { if ($("Budget ($)") <= 1000) {return 2;} if ($("Budget ($)") > 1000) {return 3;} } else { if ($("Budget ($)") <= 1000) {return 1;} if ($("Budget ($)") > 1000) if ($("Budget ($)") < 10000) {return 2;} else {return 3} } `, context ) expect(result).toBeDefined() expect(result).toBe(3) }) it("should handle test case 4", async () => { const context = { "Time Sheets": ["a", "b"], } const result = await processJS( ` let hours = 0 if (($("[Time Sheets]") != null) == true){ for (i = 0; i < $("[Time Sheets]").length; i++){ let hoursLogged = "Time Sheets." + i + ".Hours" hours += $(hoursLogged) } return hours } if (($("[Time Sheets]") != null) == false){ return hours } `, context ) expect(result).toBeDefined() expect(result).toBe("0ab") }) it("should handle test case 5", async () => { const context = { change: JSON.stringify({ a: 1, primaryDisplay: "a" }), previous: JSON.stringify({ a: 2, primaryDisplay: "b" }), } const result = await processJS( ` let change = $("[change]") ? JSON.parse($("[change]")) : {} let previous = $("[previous]") ? JSON.parse($("[previous]")) : {} function simplifyLink(originalKey, value, parent) { if (Array.isArray(value)) { if (value.filter(item => Object.keys(item || {}).includes("primaryDisplay")).length > 0) { parent[originalKey] = value.map(link => link.primaryDisplay) } } } for (let entry of Object.entries(change)) { simplifyLink(entry[0], entry[1], change) } for (let entry of Object.entries(previous)) { simplifyLink(entry[0], entry[1], previous) } let diff = Object.fromEntries(Object.entries(change).filter(([k, v]) => previous[k]?.toString() !== v?.toString())) delete diff.audit_change delete diff.audit_previous delete diff._id delete diff._rev delete diff.tableId delete diff.audit for (let entry of Object.entries(diff)) { simplifyLink(entry[0], entry[1], diff) } return JSON.stringify(change)?.replaceAll(",\\"", ",\\n\\t\\"").replaceAll("{\\"", "{\\n\\t\\"").replaceAll("}", "\\n}") `, context ) expect(result).toBe(`{\n\t"a":1,\n\t"primaryDisplay":"a"\n}`) }) it("should handle test case 6", async () => { const context = { "Join Date": DATE, } const result = await processJS( ` var rate = 5; var today = new Date(); // comment function monthDiff(dateFrom, dateTo) { return dateTo.getMonth() - dateFrom.getMonth() + (12 * (dateTo.getFullYear() - dateFrom.getFullYear())) } var serviceMonths = monthDiff( new Date($("[Join Date]")), today); var serviceYears = serviceMonths / 12; if (serviceYears >= 1 && serviceYears < 5){ rate = 10; } if (serviceYears >= 5 && serviceYears < 10){ rate = 15; } if (serviceYears >= 10){ rate = 15; rate += 0.5 * (Number(serviceYears.toFixed(0)) - 10); } return rate; `, context ) expect(result).toBe(10) }) it("should handle test case 7", async () => { const context = { "P I": "Pass", "PA I": "Pass", "F I": "Fail", "V I": "Pass", } const result = await processJS( `if (($("[P I]") == "Pass") == true) if (($("[ P I]") == "Pass") == true) if (($("[F I]") == "Pass") == true) if (($("[V I]") == "Pass") == true) {return "Pass"} if (($("[PA I]") == "Fail") == true) {return "Fail"} if (($("[ P I]") == "Fail") == true) {return "Fail"} if (($("[F I]") == "Fail") == true) {return "Fail"} if (($("[V I]") == "Fail") == true) {return "Fail"} else {return ""}`, context ) expect(result).toBe("Fail") }) it("should handle test case 8", async () => { const context = { "T L": [{ Hours: 10 }], "B H": 50, } const result = await processJS( `var totalHours = 0; if (($("[T L]") != null) == true){ for (let i = 0; i < ($("[T L]").length); i++){ var individualHours = "T L." + i + ".Hours"; var hoursNum = Number($(individualHours)); totalHours += hoursNum; } return totalHours.toFixed(2); } if (($("[T L]") != null) == false) { return totalHours.toFixed(2); } `, context ) expect(result).toBe("10.00") }) it("should handle test case 9", async () => { const context = { "T L": [{ Hours: 10 }], "B H": 50, } const result = await processJS( `var totalHours = 0; if (($("[T L]") != null) == true){ for (let i = 0; i < ($("[T L]").length); i++){ var individualHours = "T L." + i + ".Hours"; var hoursNum = Number($(individualHours)); totalHours += hoursNum; } return ($("[B H]") - totalHours).toFixed(2); } if (($("[T L]") != null) == false) { return ($("[B H]") - totalHours).toFixed(2); }`, context ) expect(result).toBe("40.00") }) it("should handle test case 10", async () => { const context = { "F F": [{ "F S": 10 }], } const result = await processJS( `var rating = 0; if ($("[F F]") != null){ for (i = 0; i < $("[F F]").length; i++){ var individualRating = $("F F." + i + ".F S"); rating += individualRating; } rating = (rating / $("[F F]").length); } return rating; `, context ) expect(result).toBe(10) }) }) })