javascript.spec.ts
1 import { 2 processStringSync, 3 encodeJSBinding, 4 defaultJSSetup, 5 } from "../src/index" 6 import { UUID_REGEX } from "./constants" 7 import tk from "timekeeper" 8 9 const DATE = "2021-01-21T12:00:00" 10 tk.freeze(DATE) 11 12 const processJS = (js: string, context?: object): any => { 13 return processStringSync(encodeJSBinding(js), context) 14 } 15 16 describe("Javascript", () => { 17 beforeAll(() => { 18 defaultJSSetup() 19 }) 20 21 describe("Test the JavaScript helper", () => { 22 it("should execute a simple expression", () => { 23 const output = processJS(`return 1 + 2`) 24 expect(output).toBe(3) 25 }) 26 27 it("should be able to use primitive bindings", () => { 28 const output = processJS(`return $("foo")`, { 29 foo: "bar", 30 }) 31 expect(output).toBe("bar") 32 }) 33 34 it("should be able to use an object binding", () => { 35 const output = processJS(`return $("foo").bar`, { 36 foo: { 37 bar: "baz", 38 }, 39 }) 40 expect(output).toBe("baz") 41 }) 42 43 it("should be able to use a complex object binding", () => { 44 const output = processJS(`return $("foo").bar[0].baz`, { 45 foo: { 46 bar: [ 47 { 48 baz: "shazbat", 49 }, 50 ], 51 }, 52 }) 53 expect(output).toBe("shazbat") 54 }) 55 56 it("should be able to use a deep binding", () => { 57 const output = processJS(`return $("foo.bar.baz")`, { 58 foo: { 59 bar: { 60 baz: "shazbat", 61 }, 62 }, 63 }) 64 expect(output).toBe("shazbat") 65 }) 66 67 it("should be able to return an object", () => { 68 const output = processJS(`return $("foo")`, { 69 foo: { 70 bar: { 71 baz: "shazbat", 72 }, 73 }, 74 }) 75 expect(output.bar.baz).toBe("shazbat") 76 }) 77 78 it("should be able to return an array", () => { 79 const output = processJS(`return $("foo")`, { 80 foo: ["a", "b", "c"], 81 }) 82 expect(output[2]).toBe("c") 83 }) 84 85 it("should be able to return null", () => { 86 const output = processJS(`return $("foo")`, { 87 foo: null, 88 }) 89 expect(output).toBe(null) 90 }) 91 92 it("should be able to return undefined", () => { 93 const output = processJS(`return $("foo")`, { 94 foo: undefined, 95 }) 96 expect(output).toBe(undefined) 97 }) 98 99 it("should be able to return 0", () => { 100 const output = processJS(`return $("foo")`, { 101 foo: 0, 102 }) 103 expect(output).toBe(0) 104 }) 105 106 it("should be able to return an empty string", () => { 107 const output = processJS(`return $("foo")`, { 108 foo: "", 109 }) 110 expect(output).toBe("") 111 }) 112 113 it("should be able to use a deep array binding", () => { 114 const output = processJS(`return $("foo.0.bar")`, { 115 foo: [ 116 { 117 bar: "baz", 118 }, 119 ], 120 }) 121 expect(output).toBe("baz") 122 }) 123 124 it("should handle errors", () => { 125 expect(processJS(`throw "Error"`)).toEqual("Error") 126 }) 127 128 it("should prevent access to the process global", async () => { 129 expect(processJS(`return process`)).toEqual( 130 "ReferenceError: process is not defined" 131 ) 132 }) 133 }) 134 135 describe("check JS helpers", () => { 136 it("should error if using the format helper. not helpers.", () => { 137 expect(processJS(`return helper.toInt(4.3)`)).toEqual( 138 "ReferenceError: helper is not defined" 139 ) 140 }) 141 142 it("should be able to use toInt", () => { 143 const output = processJS(`return helpers.toInt(4.3)`) 144 expect(output).toBe(4) 145 }) 146 147 it("should be able to use uuid", () => { 148 const output = processJS(`return helpers.uuid()`) 149 expect(output).toMatch(UUID_REGEX) 150 }) 151 }) 152 153 describe("JS literal strings", () => { 154 it("should be able to handle a literal string that is quoted (like role IDs)", () => { 155 const output = processJS(`return $("'Custom'")`) 156 expect(output).toBe("Custom") 157 }) 158 }) 159 160 describe("mutability", () => { 161 it("should not allow the context to be mutated", async () => { 162 const context = { array: [1] } 163 const result = await processJS( 164 ` 165 const array = $("array"); 166 array.push(2); 167 return array[1] 168 `, 169 context 170 ) 171 expect(result).toEqual(2) 172 expect(context.array).toEqual([1]) 173 }) 174 }) 175 176 describe("malice", () => { 177 it("should not be able to call JS functions", () => { 178 expect(processJS(`return alert("hello")`)).toEqual( 179 "ReferenceError: alert is not defined" 180 ) 181 182 expect(processJS(`return prompt("hello")`)).toEqual( 183 "ReferenceError: prompt is not defined" 184 ) 185 186 expect(processJS(`return confirm("hello")`)).toEqual( 187 "ReferenceError: confirm is not defined" 188 ) 189 190 expect(processJS(`return setTimeout(() => {}, 1000)`)).toEqual( 191 "ReferenceError: setTimeout is not defined" 192 ) 193 194 expect(processJS(`return setInterval(() => {}, 1000)`)).toEqual( 195 "ReferenceError: setInterval is not defined" 196 ) 197 }) 198 }) 199 200 // the test cases here were extracted from templates/real world examples of JS in Budibase 201 describe("real test cases from Budicloud", () => { 202 const context = { 203 "Unit Value": 2, 204 Quantity: 1, 205 } 206 it("handle test case 1", async () => { 207 const result = await processJS( 208 ` 209 var Gross = $("[Unit Value]") * $("[Quantity]") 210 return Gross.toFixed(2)`, 211 context 212 ) 213 expect(result).toBeDefined() 214 expect(result).toBe("2.00") 215 }) 216 217 it("handle test case 2", async () => { 218 const todayDate = new Date() 219 // add a year and a month 220 todayDate.setMonth(new Date().getMonth() + 1) 221 todayDate.setFullYear(todayDate.getFullYear() + 1) 222 const context = { 223 "Purchase Date": DATE, 224 today: todayDate.toISOString(), 225 } 226 const result = await processJS( 227 ` 228 var purchase = new Date($("[Purchase Date]")); 229 let purchaseyear = purchase.getFullYear(); 230 let purchasemonth = purchase.getMonth(); 231 232 var today = new Date($("today")); 233 let todayyear = today.getFullYear(); 234 let todaymonth = today.getMonth(); 235 236 var age = todayyear - purchaseyear 237 238 if (((todaymonth - purchasemonth) < 6) == true){ 239 return age 240 } 241 `, 242 context 243 ) 244 expect(result).toBeDefined() 245 expect(result).toBe(1) 246 }) 247 248 it("should handle test case 3", async () => { 249 const context = { 250 Escalate: true, 251 "Budget ($)": 1100, 252 } 253 const result = await processJS( 254 ` 255 if ($("[Escalate]") == true) { 256 if ($("Budget ($)") <= 1000) 257 {return 2;} 258 if ($("Budget ($)") > 1000) 259 {return 3;} 260 } 261 else { 262 if ($("Budget ($)") <= 1000) 263 {return 1;} 264 if ($("Budget ($)") > 1000) 265 if ($("Budget ($)") < 10000) 266 {return 2;} 267 else 268 {return 3} 269 } 270 `, 271 context 272 ) 273 expect(result).toBeDefined() 274 expect(result).toBe(3) 275 }) 276 277 it("should handle test case 4", async () => { 278 const context = { 279 "Time Sheets": ["a", "b"], 280 } 281 const result = await processJS( 282 ` 283 let hours = 0 284 if (($("[Time Sheets]") != null) == true){ 285 for (i = 0; i < $("[Time Sheets]").length; i++){ 286 let hoursLogged = "Time Sheets." + i + ".Hours" 287 hours += $(hoursLogged) 288 } 289 return hours 290 } 291 if (($("[Time Sheets]") != null) == false){ 292 return hours 293 } 294 `, 295 context 296 ) 297 expect(result).toBeDefined() 298 expect(result).toBe("0ab") 299 }) 300 301 it("should handle test case 5", async () => { 302 const context = { 303 change: JSON.stringify({ a: 1, primaryDisplay: "a" }), 304 previous: JSON.stringify({ a: 2, primaryDisplay: "b" }), 305 } 306 const result = await processJS( 307 ` 308 let change = $("[change]") ? JSON.parse($("[change]")) : {} 309 let previous = $("[previous]") ? JSON.parse($("[previous]")) : {} 310 311 function simplifyLink(originalKey, value, parent) { 312 if (Array.isArray(value)) { 313 if (value.filter(item => Object.keys(item || {}).includes("primaryDisplay")).length > 0) { 314 parent[originalKey] = value.map(link => link.primaryDisplay) 315 } 316 } 317 } 318 319 for (let entry of Object.entries(change)) { 320 simplifyLink(entry[0], entry[1], change) 321 } 322 for (let entry of Object.entries(previous)) { 323 simplifyLink(entry[0], entry[1], previous) 324 } 325 326 let diff = Object.fromEntries(Object.entries(change).filter(([k, v]) => previous[k]?.toString() !== v?.toString())) 327 328 delete diff.audit_change 329 delete diff.audit_previous 330 delete diff._id 331 delete diff._rev 332 delete diff.tableId 333 delete diff.audit 334 335 for (let entry of Object.entries(diff)) { 336 simplifyLink(entry[0], entry[1], diff) 337 } 338 339 return JSON.stringify(change)?.replaceAll(",\\"", ",\\n\\t\\"").replaceAll("{\\"", "{\\n\\t\\"").replaceAll("}", "\\n}") 340 `, 341 context 342 ) 343 expect(result).toBe(`{\n\t"a":1,\n\t"primaryDisplay":"a"\n}`) 344 }) 345 346 it("should handle test case 6", async () => { 347 const todayDate = new Date() 348 // add a year and a month 349 todayDate.setMonth(new Date().getMonth() + 1) 350 todayDate.setFullYear(todayDate.getFullYear() + 1) 351 const context = { 352 "Join Date": DATE, 353 today: todayDate.toISOString(), 354 } 355 const result = await processJS( 356 ` 357 var rate = 5; 358 var today = new Date($("today")); 359 360 // comment 361 function monthDiff(dateFrom, dateTo) { 362 return dateTo.getMonth() - dateFrom.getMonth() + 363 (12 * (dateTo.getFullYear() - dateFrom.getFullYear())) 364 } 365 var serviceMonths = monthDiff( new Date($("[Join Date]")), today); 366 var serviceYears = serviceMonths / 12; 367 368 if (serviceYears >= 1 && serviceYears < 5){ 369 rate = 10; 370 } 371 if (serviceYears >= 5 && serviceYears < 10){ 372 rate = 15; 373 } 374 if (serviceYears >= 10){ 375 rate = 15; 376 rate += 0.5 * (Number(serviceYears.toFixed(0)) - 10); 377 } 378 return rate; 379 `, 380 context 381 ) 382 expect(result).toBe(10) 383 }) 384 385 it("should handle test case 7", async () => { 386 const context = { 387 "P I": "Pass", 388 "PA I": "Pass", 389 "F I": "Fail", 390 "V I": "Pass", 391 } 392 const result = await processJS( 393 `if (($("[P I]") == "Pass") == true) 394 if (($("[ P I]") == "Pass") == true) 395 if (($("[F I]") == "Pass") == true) 396 if (($("[V I]") == "Pass") == true) 397 {return "Pass"} 398 399 if (($("[PA I]") == "Fail") == true) 400 {return "Fail"} 401 if (($("[ P I]") == "Fail") == true) 402 {return "Fail"} 403 if (($("[F I]") == "Fail") == true) 404 {return "Fail"} 405 if (($("[V I]") == "Fail") == true) 406 {return "Fail"} 407 408 else 409 {return ""}`, 410 context 411 ) 412 expect(result).toBe("Fail") 413 }) 414 415 it("should handle test case 8", async () => { 416 const context = { 417 "T L": [{ Hours: 10 }], 418 "B H": 50, 419 } 420 const result = await processJS( 421 `var totalHours = 0; 422 if (($("[T L]") != null) == true){ 423 for (let i = 0; i < ($("[T L]").length); i++){ 424 var individualHours = "T L." + i + ".Hours"; 425 var hoursNum = Number($(individualHours)); 426 totalHours += hoursNum; 427 } 428 return totalHours.toFixed(2); 429 } 430 if (($("[T L]") != null) == false) { 431 return totalHours.toFixed(2); 432 } 433 `, 434 context 435 ) 436 expect(result).toBe("10.00") 437 }) 438 439 it("should handle test case 9", async () => { 440 const context = { 441 "T L": [{ Hours: 10 }], 442 "B H": 50, 443 } 444 const result = await processJS( 445 `var totalHours = 0; 446 if (($("[T L]") != null) == true){ 447 for (let i = 0; i < ($("[T L]").length); i++){ 448 var individualHours = "T L." + i + ".Hours"; 449 var hoursNum = Number($(individualHours)); 450 totalHours += hoursNum; 451 } 452 return ($("[B H]") - totalHours).toFixed(2); 453 } 454 if (($("[T L]") != null) == false) { 455 return ($("[B H]") - totalHours).toFixed(2); 456 }`, 457 context 458 ) 459 expect(result).toBe("40.00") 460 }) 461 462 it("should handle test case 10", async () => { 463 const context = { 464 "F F": [{ "F S": 10 }], 465 } 466 const result = await processJS( 467 `var rating = 0; 468 469 if ($("[F F]") != null){ 470 for (i = 0; i < $("[F F]").length; i++){ 471 var individualRating = $("F F." + i + ".F S"); 472 rating += individualRating; 473 } 474 rating = (rating / $("[F F]").length); 475 } 476 return rating; 477 `, 478 context 479 ) 480 expect(result).toBe(10) 481 }) 482 }) 483 })