/ packages / string-templates / test / javascript.spec.ts
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  })