Files
jsonata-js-jsonata/test/implementation-tests.js
T

1207 lines
44 KiB
JavaScript

/**
* © Copyright IBM Corp. 2016 All Rights Reserved
* Project name: JSONata
* This project is licensed under the MIT License, see LICENSE
*
* The files in this directory are tests that aren't really portable
* to other implementations for various reasons but they are included
* in order to achieve 100% coverage for this implementation.
*/
"use strict";
var jsonata = require("../src/jsonata");
var chai = require("chai");
var chaiAsPromised = require("chai-as-promised");
chai.use(chaiAsPromised);
var expect = chai.expect;
var testdata1 = {
"foo": {
"bar": 42,
"blah": [{"baz": {"fud": "hello"}}, {"baz": {"fud": "world"}}, {"bazz": "gotcha"}],
"blah.baz": "here"
}, "bar": 98
};
var testdata2 = {
Account: {
"Account Name": "Firefly",
Order: [
{
OrderID: "order103",
Product: [
{
"Product Name": "Bowler Hat",
ProductID: 858383,
SKU: "0406654608",
Description: {
Colour: "Purple",
Width: 300,
Height: 200,
Depth: 210,
Weight: 0.75
},
Price: 34.45,
Quantity: 2
},
{
"Product Name": "Trilby hat",
ProductID: 858236,
SKU: "0406634348",
Description: {
Colour: "Orange",
Width: 300,
Height: 200,
Depth: 210,
Weight: 0.6
},
Price: 21.67,
Quantity: 1
}
]
},
{
OrderID: "order104",
Product: [
{
"Product Name": "Bowler Hat",
ProductID: 858383,
SKU: "040657863",
Description: {
Colour: "Purple",
Width: 300,
Height: 200,
Depth: 210,
Weight: 0.75
},
Price: 34.45,
Quantity: 4
},
{
ProductID: 345664,
SKU: "0406654603",
"Product Name": "Cloak",
Description: {
Colour: "Black",
Width: 30,
Height: 20,
Depth: 210,
Weight: 2.0
},
Price: 107.99,
Quantity: 1
}
]
}
]
}
};
describe("Functions with side-effects", () => {
describe("Evaluator - function: millis", function() {
describe("$millis() returns milliseconds since the epoch", function() {
it("should return result object", function() {
var expr = jsonata("$millis()");
// 27 Sep 2016, first commit to JSONata
expect(expr.evaluate(testdata2)).to.eventually.be.above(
1474934400
);
});
});
describe("$millis() always returns same value within an expression", function() {
it("should return result object", function() {
var expr = jsonata(
'{"now": $millis(), "delay": $sum([1..10000]), "later": $millis()}.(now = later)'
);
expect(expr.evaluate(testdata2)).to.eventually.equal(true);
});
});
describe("$millis() returns different timestamp for subsequent evaluate() calls", function() {
it("should return result object", async function() {
var expr = jsonata("($sum([1..10000]); $millis())");
var result = await expr.evaluate(testdata2);
var result2 = await expr.evaluate(testdata2);
expect(result).to.not.equal(result2);
});
});
});
describe("$now() returns timestamp", function() {
it("should return result object", function() {
var expr = jsonata("$now()");
var result = expr.evaluate(testdata2);
// follows this pattern - "2017-05-09T10:10:16.918Z"
expect(result).to.eventually.match(
/^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d.\d\d\dZ$/
);
});
});
describe("$now() returns timestamp with defined format", function() {
it("should return result object", function() {
var expr = jsonata("$now('[h]:[M01][P] [z]')");
var result = expr.evaluate(testdata2);
// follows this pattern - "10:23am GMT+00:00"
expect(result).to.eventually.match(/^\d?\d:\d\d[ap]m GMT\+00:00$/);
});
});
describe("$now() returns timestamp with defined format and timezone", function() {
it("should return result object", function() {
var expr = jsonata("$now('[h]:[M01][P] [z]', '-0500')");
var result = expr.evaluate(testdata2);
// follows this pattern - "10:23am GMT-05:00"
expect(result).to.eventually.match(/^\d?\d:\d\d[ap]m GMT-05:00$/);
});
});
describe("$now() always returns same value within an expression", function() {
it("should return result object", async function() {
var expr = jsonata('{"now": $now(), "delay": $sum([1..10000]), "later": $now()}.(now = later)');
var result = await expr.evaluate(testdata2);
var expected = true;
expect(result).to.deep.equal(expected);
});
});
describe("$now() returns different timestamp for subsequent evaluate() calls", function() {
it("should return result object", async function() {
var expr = jsonata("($sum([1..100000]); $now())");
var result = await expr.evaluate(testdata2);
var result2 = await expr.evaluate(testdata2);
expect(result).to.not.equal(result2);
});
});
describe("$millis() returns milliseconds since the epoch", function() {
it("should return result object", function() {
var expr = jsonata("$millis()");
// 27 Sep 2016, first commit to JSONata
expect(expr.evaluate(testdata2)).to.eventually.be.above(1474934400);
});
});
describe("Evaluator - functions: random", function() {
describe('random number")', function() {
it("should return result object", async function() {
var expr = jsonata("$random()");
var result = await expr.evaluate();
var expected = result >= 0 && result < 1;
expect(expected).to.equal(true);
});
});
describe('consequetive random numbers should be different")', function() {
it("should return result object", function() {
var expr = jsonata("$random() = $random()");
var expected = false;
expect(expr.evaluate()).to.eventually.deep.equal(expected);
});
});
});
});
describe("Tests that rely on JavaScript-style object traversal", () => {
// A JSON object is an unordered list of key-value pairs.
// When traversing an object, the entries may be returned
// in a non-deterministic order (depending on the language).
// The following tests assume a traversal order which works
// in JavaScript but may not apply to other languages.
// See https://github.com/jsonata-js/jsonata/issues/179.
describe('foo.*[0]', function () {
it('should return result object', async function () {
var expr = jsonata("foo.*[0]");
var result = await expr.evaluate(testdata1);
var expected = 42;
expect(result).to.deep.equal(expected);
});
});
describe('**[2]', function () {
it('should return result object', async function () {
var expr = jsonata("**[2]");
var result = await expr.evaluate(testdata2);
var expected = "Firefly";
expect(result).to.deep.equal(expected);
});
});
});
describe("Tests that use the $clone() function", () => {
// $clone() allows jsonata-js to play nicely with Node-RED.
// It's not part of the JSONata standard.
// See https://github.com/jsonata-js/jsonata/issues/207.
describe('clone undefined', function () {
it('should return undefined', async function () {
var expr = jsonata('$clone(foo)');
var result = await expr.evaluate(testdata2);
var expected = undefined;
expect(result).to.deep.equal(expected);
});
});
describe('clone empty object', function () {
it('should return empty object', async function () {
var expr = jsonata('$clone({})');
var result = await expr.evaluate(testdata2);
var expected = {};
expect(result).to.deep.equal(expected);
});
});
describe('clone object', function () {
it('should return same object', async function () {
var expr = jsonata('$clone({"a": 1})');
var result = await expr.evaluate(testdata2);
var expected = {"a": 1};
expect(result).to.deep.equal(expected);
});
});
describe("transform expression with overridden $clone function", function() {
it("should return result object", async function() {
var expr = jsonata('Account ~> |Order|{"Product":"blah"},nomatch|');
var count = 0;
expr.registerFunction("clone", function(arg) {
count++;
return JSON.parse(JSON.stringify(arg));
});
var result = await expr.evaluate(testdata2);
var expected = {
"Account Name": "Firefly",
Order: [
{
OrderID: "order103",
Product: "blah"
},
{
OrderID: "order104",
Product: "blah"
}
]
};
expect(result).to.deep.equal(expected);
expect(count).to.equal(1);
});
});
describe('transform expression with overridden $clone value', function () {
it('should throw error', async function () {
var expr = jsonata('( $clone := 5; $ ~> |Account.Order.Product|{"blah":"foo"}| )');
expect(
expr.evaluate(testdata2)
).to.eventually.be.rejected.to.deep.contain({ code: "T2013" });
});
});
});
describe("Tests that bind Javascript functions", () => {
// These involve binding of functions
describe("Override implementation of $now()", function() {
it("should return result object", async function() {
var expr = jsonata("$now()");
expr.registerFunction("now", function() {
return "time for tea";
});
var result = await expr.evaluate(testdata2);
expect(result).to.equal("time for tea");
});
});
// Issue #261. Previously we would attempt to assign to the read-only `message` property,
// causing an unrelated `TypeError` to be thrown instead
describe("function throws a `DOMException` with a read-only `message` property", function() {
/**
* `DOMException` is not available in our testing environment. Additionally, we can't
* just import the `domexception` module since it doesn't work on Node.js v4, which
* we still support. So, here's a fake skeleton implementation which has the relevant
* qualities we need to reproduce the bug, most importantly a read-only `message`
* property
* @param {string} message - Error message
* @constructor
*/
function DOMException (message) {
Object.defineProperty(this, "message", {
get() {
return message;
},
enumerable: true,
configurable: true
});
}
Object.setPrototypeOf(DOMException.prototype, Error.prototype);
it("rethrows correctly", function() {
var expr = jsonata("$throwDomEx()");
expr.registerFunction("throwDomEx", function() {
throw new DOMException('Here is my message');
});
expect(expr.evaluate({}))
.to.eventually.be.rejectedWith(DOMException)
.to.deep.contain({
message: "Here is my message",
position: 12,
token: "throwDomEx",
});
});
});
describe("map a user-defined Javascript function with signature", function() {
it("should return result object", async function() {
var expr = jsonata("$map([1,4,9,16], $squareroot)");
expr.registerFunction(
"squareroot",
function(num) {
return Math.sqrt(num);
},
"<n:n>"
);
var result = await expr.evaluate(testdata2);
var expected = [1, 2, 3, 4];
expect(result).to.deep.equal(expected);
});
});
describe("map a user-defined Javascript function with undefined signature", function() {
it("should return result object", async function() {
var expr = jsonata("$map([1,4,9,16], $squareroot)");
expr.registerFunction("squareroot", function(num) {
return Math.sqrt(num);
});
var result = await expr.evaluate(testdata2);
var expected = [1, 2, 3, 4];
expect(result).to.deep.equal(expected);
});
});
describe("map a user-defined Javascript function", function() {
it("should return result object", async function() {
var expr = jsonata("$map([1,4,9,16], $squareroot)");
expr.assign("squareroot", function(num) {
return Math.sqrt(num);
});
var result = await expr.evaluate(testdata2);
var expected = [1, 2, 3, 4];
expect(result).to.deep.equal(expected);
});
});
describe("$filter with a user-defined Javascript function", function() {
it("should return result object", async function() {
var expr = jsonata("$filter([1,4,9,16], $even)");
expr.assign("even", function(num) {
return num % 2 === 0;
});
var result = await expr.evaluate(testdata2);
var expected = [4, 16];
expect(result).to.deep.equal(expected);
});
});
describe("$sift with a user-defined Javascript function", function() {
it("should return result object", async function() {
var expr = jsonata("$sift({'one': 1, 'four': 4, 'nine': 9, 'sixteen': 16}, $even)");
expr.assign("even", function(num) {
return num % 2 === 0;
});
var result = await expr.evaluate(testdata2);
var expected = {'four': 4, 'sixteen': 16};
expect(result).to.deep.equal(expected);
});
});
describe("$each with a user-defined Javascript function", function() {
it("should return result object", async function() {
var expr = jsonata("$each({'one': 1, 'four': 4, 'nine': 9, 'sixteen': 16}, $squareroot)");
expr.assign("squareroot", function(num) {
return Math.sqrt(num);
});
var result = await expr.evaluate(testdata2);
var expected = [1, 2, 3, 4];
expect(result).to.deep.equal(expected);
});
});
describe("Partially apply user-defined Javascript function", function() {
it("should return result object", async function() {
var expr = jsonata(
"(" +
" $firstn := $substr(?, 0, ?);" +
" $first5 := $firstn(?, 5);" +
' $first5("Hello World")' +
")"
);
expr.assign("substr", function(str, start, len) {
return str.substr(start, len);
});
var result = await expr.evaluate(testdata2);
var expected = "Hello";
expect(result).to.deep.equal(expected);
});
});
describe("User defined matchers", function() {
var repeatingLetters = function(char, repeat) {
// custom matcher to match `repeat` contiguous occurrences of `char`
var chars = char.repeat(repeat);
var match = function(str, offset) {
var pos = str.indexOf(chars, (offset || 0));
if (pos === -1) {
return;
} else {
return {
match: chars,
start: pos,
end: pos + chars.length,
groups: [],
next: function () {
return match(str, pos + chars.length);
}
};
}
};
return match;
};
it("should match using a custom matcher", async function() {
var expr = jsonata("$match('LLANFAIRPWLLGWYNGYLLGOGERYCHWYRNDROBWLLLLANTYSILIOGOGOGOCH', $repeatingLetters('L', 2))");
expr.registerFunction("repeatingLetters", repeatingLetters);
var result = await expr.evaluate();
var expected = [
{"match": "LL", "index": 0, "groups": []},
{"match": "LL", "index": 10, "groups": []},
{"match": "LL", "index": 18, "groups": []},
{"match": "LL", "index": 37, "groups": []},
{"match": "LL", "index": 39, "groups": []}
];
expect(result).to.deep.equal(expected);
});
it("should split using a custom matcher", async function() {
var expr = jsonata("$split('LLANFAIRPWLLGWYNGYLLGOGERYCHWYRNDROBWLLLLANTYSILIOGOGOGOCH', $repeatingLetters('L', 2))");
expr.registerFunction("repeatingLetters", repeatingLetters);
var result = await expr.evaluate();
var expected = ["","ANFAIRPW","GWYNGY","GOGERYCHWYRNDROBW","","ANTYSILIOGOGOGOCH"];
expect(result).to.deep.equal(expected);
});
it("should replace using a custom matcher", async function() {
var expr = jsonata("$replace('LLANFAIRPWLLGWYNGYLLGOGERYCHWYRNDROBWLLLLANTYSILIOGOGOGOCH', $repeatingLetters('L', 2), 'Ỻ')");
expr.registerFunction("repeatingLetters", repeatingLetters);
var result = await expr.evaluate();
var expected = "ỺANFAIRPWỺGWYNGYỺGOGERYCHWYRNDROBWỺỺANTYSILIOGOGOGOCH";
expect(result).to.deep.equal(expected);
});
it("should test inclusion using a custom matcher", async function() {
var expr = jsonata("$contains('LLANFAIRPWLLGWYNGYLLGOGERYCHWYRNDROBWLLLLANTYSILIOGOGOGOCH', $repeatingLetters('L', 4))");
expr.registerFunction("repeatingLetters", repeatingLetters);
var result = await expr.evaluate();
var expected = true;
expect(result).to.deep.equal(expected);
});
});
describe('User defined higher-order functions', () => {
var myfunc = async (arr, fn) => 2 * (await fn(arr));
var startsWith = function(str) {
// returns a function that returns true if its argument starts with the string `str`
return (arg) => {
return arg.startsWith(str);
};
};
it('should be able to invoke a built-in function passed as an argument', async () => {
var expr = jsonata("$myfunc([1,2,3], $sum)");
expr.registerFunction('myfunc', myfunc);
var result = await expr.evaluate();
var expected = 12;
expect(result).to.deep.equal(expected);
});
it('should be able to invoke a lambda function passed as an argument', async () => {
var expr = jsonata("$myfunc([1,2,3], λ($arr) { $arr[1] + $arr[2] })");
expr.registerFunction('myfunc', myfunc);
var result = await expr.evaluate();
var expected = 10;
expect(result).to.deep.equal(expected);
});
it('should be able to invoke a user-defined function passed as an argument', async () => {
var expr = jsonata("$myfunc([1,2,3], $myfunc2)");
expr.registerFunction('myfunc', myfunc);
expr.registerFunction('myfunc2', (arr) => {
return 2 * arr[1];
});
var result = await expr.evaluate();
var expected = 8;
expect(result).to.deep.equal(expected);
});
it('should be able to return a function from a user-defined function', async () => {
var expr = jsonata(`
(
$startsWithHello := $startsWith("Hello");
[$startsWithHello("Hello, Bob"), $startsWithHello("Goodbye, Bill")]
)`);
expr.registerFunction('startsWith', startsWith);
var result = await expr.evaluate();
var expected = [true, false];
expect(result).to.deep.equal(expected);
});
});
describe('User defined generator function', () => {
var myAddFunc = function*(val) {
yield val + 10;
};
var myArrayFunc = function *() {
yield [1,2,3];
};
var myObjectFunc = function* () {
yield {
downloads: [
{
downloads: 1,
day: "2016-09-01",
},
{
downloads: 2,
day: "2016-09-02",
},
{
downloads: 3,
day: "2016-09-03",
},
{
downloads: 1453,
day: "2017-03-10",
},
{
downloads: 1194,
day: "2017-03-11",
},
{
downloads: 988,
day: "2017-03-12",
},
],
};
};
it('should be able to invoke a generator function returning a simple value', async () => {
var expr = jsonata("$myAddFunc(1)");
expr.registerFunction('myAddFunc', myAddFunc);
var result = await expr.evaluate();
expect(result).to.equal(11);
});
it('should be able to invoke a generator function and map over its return array value', async () => {
var expr = jsonata("$myArrayFunc().{\"foo\": \"bar\"}");
expr.registerFunction('myArrayFunc', myArrayFunc);
var result = await expr.evaluate();
expect(result).to.deep.equal([
{
"foo": "bar"
},
{
"foo": "bar"
},
{
"foo": "bar"
}
]);
});
it('should be able to invoke a generator function and map over its return object value', async () => {
var expr = jsonata("$myObjectFunc().downloads{ $substring(day, 0, 7): $sum(downloads) }");
expr.registerFunction('myObjectFunc', myObjectFunc);
var result = await expr.evaluate();
expect(result).to.deep.equal({ '2016-09': 6, '2017-03': 3635 });
});
});
describe('User defined higher-order generator functions', () => {
var myfunc = function*(arr, fn) {
const val = yield* fn(arr);
return 2 * val;
};
// FIXME:
it('a higher-order generator function will not work', async () => {
var expr = jsonata("$myfunc([1,2,3], $sum)");
expr.registerFunction('myfunc', myfunc);
try {
await expr.evaluate();
} catch (e) {
expect(e.message).to.equal("yield* (intermediate value)(intermediate value) is not iterable");
}
});
});
});
describe("Tests that are specific to a Javascript runtime", () => {
// Javascript specific
describe('/ab/ ("ab")', function() {
it("should return result object", async function() {
var expr = jsonata('/ab/ ("ab")');
var result = await expr.evaluate();
var expected = { match: "ab", start: 0, end: 2, groups: [] };
expect(JSON.stringify(result)).to.equal(JSON.stringify(expected));
});
});
describe("/ab/ ()", function() {
it("should return result object", async function() {
var expr = jsonata("/ab/ ()");
var result = await expr.evaluate();
var expected = undefined;
expect(JSON.stringify(result)).to.equal(JSON.stringify(expected));
});
});
describe('/ab+/ ("ababbabbcc")', function() {
it("should return result object", async function() {
var expr = jsonata('/ab+/ ("ababbabbcc")');
var result = await expr.evaluate();
var expected = { match: "ab", start: 0, end: 2, groups: [] };
expect(JSON.stringify(result)).to.equal(JSON.stringify(expected));
});
});
describe('/a(b+)/ ("ababbabbcc")', function() {
it("should return result object", async function() {
var expr = jsonata('/a(b+)/ ("ababbabbcc")');
var result = await expr.evaluate();
var expected = { match: "ab", start: 0, end: 2, groups: ["b"] };
expect(JSON.stringify(result)).to.equal(JSON.stringify(expected));
});
});
describe('/a(b+)/ ("ababbabbcc").next()', function() {
it("should return result object", async function() {
var expr = jsonata('/a(b+)/ ("ababbabbcc").next()');
var result = await expr.evaluate();
var expected = { match: "abb", start: 2, end: 5, groups: ["bb"] };
expect(JSON.stringify(result)).to.equal(JSON.stringify(expected));
});
});
describe('/a(b+)/ ("ababbabbcc").next().next()', function() {
it("should return result object", async function() {
var expr = jsonata('/a(b+)/ ("ababbabbcc").next().next()');
var result = await expr.evaluate();
var expected = { match: "abb", start: 5, end: 8, groups: ["bb"] };
expect(JSON.stringify(result)).to.equal(JSON.stringify(expected));
});
});
describe('/a(b+)/ ("ababbabbcc").next().next().next()', function() {
it("should return result object", async function() {
var expr = jsonata('/a(b+)/ ("ababbabbcc").next().next().next()');
var result = await expr.evaluate();
var expected = undefined;
expect(JSON.stringify(result)).to.equal(JSON.stringify(expected));
});
});
describe('/a(b+)/i ("Ababbabbcc")', function() {
it("should return result object", async function() {
var expr = jsonata('/a(b+)/i ("Ababbabbcc")');
var result = await expr.evaluate();
var expected = { match: "Ab", start: 0, end: 2, groups: ["b"] };
expect(JSON.stringify(result)).to.equal(JSON.stringify(expected));
});
});
describe("empty regex", function() {
it("should throw error", function() {
expect(function() {
var expr = jsonata("//");
expr.evaluate();
})
.to.throw()
.to.deep.contain({ position: 1, code: "S0301" });
});
});
describe("empty regex", function() {
it("should throw error", function() {
expect(function() {
var expr = jsonata("/");
expr.evaluate();
})
.to.throw()
.to.deep.contain({ position: 1, code: "S0302" });
});
});
describe("empty regex: Escaped termination", function() {
it("should throw error", function() {
expect(function() {
var expr = jsonata("/\\/");
expr.evaluate();
})
.to.throw()
.to.deep.contain({ position: 3, code: "S0302" });
});
});
describe("empty regex: Escaped termination", function() {
it("should throw error", function() {
expect(function() {
var expr = jsonata("/\\\\\\/");
expr.evaluate();
})
.to.throw()
.to.deep.contain({ position: 5, code: "S0302" });
});
});
describe("Functions - $match", function() {
describe('$match("test escape \\\\", /\\\\/)', function() {
it("should find \\", async function() {
var expr = jsonata('$match("test escape \\\\", /\\\\/)');
var result = await expr.evaluate();
var expected = { match: "\\", index: 12, groups: []};
expect(result).to.deep.equal(expected);
});
});
describe('$match("ababbabbcc",/ab/)', function() {
it("should return result object", async function() {
var expr = jsonata('$match("ababbabbcc",/ab/)');
var result = await expr.evaluate();
var expected = [
{ match: "ab", index: 0, groups: [] },
{
match: "ab",
index: 2,
groups: []
},
{ match: "ab", index: 5, groups: [] }
];
expect(result).to.deep.equal(expected);
});
});
describe('$match("ababbabbcc",/a(b+)/)', function() {
it("should return result object", async function() {
var expr = jsonata('$match("ababbabbcc",/a(b+)/)');
var result = await expr.evaluate();
var expected = [
{ match: "ab", index: 0, groups: ["b"] },
{
match: "abb",
index: 2,
groups: ["bb"]
},
{ match: "abb", index: 5, groups: ["bb"] }
];
expect(result).to.deep.equal(expected);
});
});
describe('$match("ababbabbcc",/a(b+)/, 1)', function() {
it("should return result object", async function() {
var expr = jsonata('$match("ababbabbcc",/a(b+)/, 1)');
var result = await expr.evaluate();
var expected = { match: "ab", index: 0, groups: ["b"] };
expect(result).to.deep.equal(expected);
});
});
describe('$match("ababbabbcc",/a(b+)/, 0)', function() {
it("should return result object", async function() {
var expr = jsonata('$match("ababbabbcc",/a(b+)/, 0)');
var result = await expr.evaluate();
var expected = undefined;
expect(result).to.deep.equal(expected);
});
});
describe("$match(nothing,/a(xb+)/)", function() {
it("should return result object", async function() {
var expr = jsonata("$match(nothing,/a(xb+)/)");
var result = await expr.evaluate();
var expected = undefined;
expect(result).to.deep.equal(expected);
});
});
describe('$match("ababbabbcc",/a(xb+)/)', function() {
it("should return result object", async function() {
var expr = jsonata('$match("ababbabbcc",/a(xb+)/)');
var result = await expr.evaluate();
var expected = undefined;
expect(result).to.deep.equal(expected);
});
});
describe('$match("a, b, c, d", /ab/, -3)', function() {
it("should throw error", function() {
var expr = jsonata('$match("a, b, c, d", /ab/, -3)');
expect(
expr.evaluate()
).to.eventually.be.rejected.to.deep.contain({
position: 7,
code: "D3040",
token: "match",
index: 3,
value: -3,
});
});
});
describe('$match("a, b, c, d", /ab/, null)', function() {
it("should throw error", function() {
var expr = jsonata('$match("a, b, c, d", /ab/, null)');
expect(
expr.evaluate()
).to.eventually.be.rejected.to.deep.contain({
position: 7,
code: "T0410",
token: "match",
index: 3,
value: null,
});
});
});
describe('$match("a, b, c, d", /ab/, "2")', function() {
it("should throw error", function() {
var expr = jsonata('$match("a, b, c, d", /ab/, "2")');
expect(
expr.evaluate()
).to.eventually.be.rejected.to.deep.contain({
position: 7,
code: "T0410",
token: "match",
index: 3,
value: "2",
});
});
});
describe('$match("a, b, c, d", "ab")', function() {
it("should throw error", function() {
var expr = jsonata('$match("a, b, c, d", "ab")');
expect(
expr.evaluate()
).to.eventually.be.rejected.to.deep.contain({
position: 7,
code: "T0410",
token: "match",
index: 2,
value: "ab",
});
});
});
describe('$match("a, b, c, d", true)', function() {
it("should throw error", function() {
var expr = jsonata('$match("a, b, c, d", true)');
expect(
expr.evaluate()
).to.eventually.be.rejected.to.deep.contain({
position: 7,
code: "T0410",
token: "match",
index: 2,
value: true,
});
});
});
describe("$match(12345, 3)", function() {
it("should throw error", function() {
var expr = jsonata("$match(12345, 3)");
expect(
expr.evaluate()
).to.eventually.be.rejected.to.deep.contain({
position: 7,
code: "T0410",
token: "match",
index: 1,
value: 12345,
});
});
});
describe("$match(12345)", function() {
it("should throw error", function() {
var expr = jsonata("$match(12345)");
expect(
expr.evaluate()
).to.eventually.be.rejected.to.deep.contain({
position: 7,
code: "T0410",
token: "match",
index: 1,
});
});
});
});
describe("Expressions that attempt to access the object prototype", function() {
const data = {"foo": {"bar": "baz"}};
it("should ignore __proto__", async function() {
const expr = jsonata('foo.__proto__');
const result = await expr.evaluate(data);
expect(result).to.deep.equal(undefined);
});
it("should throw an error trying to invoke toString()", async function() {
const expr = jsonata('foo.toString()');
expect(
expr.evaluate(data)
).to.eventually.be.rejected.to.deep.contain({
position: 13,
code: "T1006",
});
});
});
describe("Expressions that attempt to pollute the object prototype", function() {
it("should ignore __proto__", async function() {
const expr = jsonata('{} ~> | __proto__ | {"is_admin": true} |');
const result = await expr.evaluate();
expect(result).to.deep.equal({});
});
it("should throw an error with __lookupGetter__", async function() {
const expr = jsonata('{} ~> | __lookupGetter__("__proto__")() | {"is_admin": true} |');
expect(
expr.evaluate()
).to.eventually.be.rejected.to.deep.contain({
position: 25,
code: "T1006",
});
});
it("should ignore constructor", async function() {
const expr = jsonata('{} ~> | constructor | {"is_admin": true} |');
const result = await expr.evaluate();
expect(result).to.deep.equal({});
});
});
});
describe("Test that yield platform specific results", () => {
// Platform specific
describe("$sqrt(10) * $sqrt(10)", function() {
it("should return result object", async function() {
var expr = jsonata("$sqrt(10) * $sqrt(10)");
var result = await expr.evaluate();
var expected = 10;
expect(result).to.be.closeTo(expected, 1e-13);
});
});
});
describe("Tests that include infinite recursion", () => {
describe("stack overflow - infinite recursive function - non-tail call", function() {
it("should throw error", function() {
const options = {
'timeout': 1000,
'stack': 300
}
const expr = jsonata("($inf := function($n){$n+$inf($n-1)}; $inf(5))", options);
expect(expr.evaluate()).to.eventually.be.rejected.to.deep.contain({
token: "inf",
position: 30,
code: "D1011",
});
});
});
describe("stack overflow - infinite recursive function - tail call", function() {
this.timeout(5000);
it("should throw error", function() {
const options = {
'timeout': 1000,
'stack': 500
}
const expr = jsonata("( $inf := function(){$inf()}; $inf())", options);
expect(expr.evaluate()).to.eventually.be.rejected.to.deep.contain({
token: "inf",
code: "D1012",
});
});
});
describe("stack overflow - infinite recursive function - tail call (no stack guardrail)", function() {
this.timeout(5000);
it("should throw error", function() {
const options = {
'timeout': 1000
}
const expr = jsonata("( $inf := function(){$inf()}; $inf())", options);
expect(expr.evaluate()).to.eventually.be.rejected.to.deep.contain({
token: "inf",
code: "D1012",
});
});
});
describe("guardrails on Ackermann function", function() {
this.timeout(5000);
const ackermann = (m, n) => `
(
$ack := function($m, $n) {
$m = 0 ? $n + 1 :
$n = 0 ? $ack($m - 1, 1) :
$ack($m - 1, $ack($m, $n - 1))
};
$ack(${m}, ${n})
)`;
it("should complete for small parameters", async function() {
const options = {
'timeout': 1000,
'stack': 500
}
const expr = jsonata(ackermann(3, 4), options);
const result = await expr.evaluate();
expect(result).to.equal(125);
});
it("larger inputs cause stack overflow", function() {
const options = {
'stack': 500
}
const expr = jsonata(ackermann(4, 4), options);
expect(expr.evaluate()).to.eventually.be.rejected.to.deep.contain({
token: "ack",
code: "D1011",
});
});
it("larger inputs cause stack overflow", function() {
const options = {
'stack': 500
}
const expr = jsonata(ackermann(4, 4), options);
expect(expr.evaluate()).to.eventually.be.rejected.to.deep.contain({
token: "ack",
code: "D1011",
});
});
});
describe("guardrails on sequence length", function() {
it("prevents large ranges", function() {
const options = {
'sequence': 1000
}
const expr = jsonata('[0..1001]', options);
expect(expr.evaluate()).to.eventually.be.rejected.to.deep.contain({
code: "D2015",
});
});
it("prevents large intermediate sequences", function() {
const options = {
'sequence': 1000
}
const expr = jsonata('[0..100].([0..100]) ~> count()', options);
expect(expr.evaluate()).to.eventually.be.rejected.to.deep.contain({
code: "D2015",
});
});
it("prevents appending large sequences", function() {
const options = {
'sequence': 1000
}
const expr = jsonata('$append([0..600], [0..600]) ~> $count()', options);
expect(expr.evaluate()).to.eventually.be.rejected.to.deep.contain({
code: "D2015",
});
});
});
});
describe("Tests invalid object creation", () => {
it("prevents creating an object mimicking a lambda", () => {
const expr = jsonata('($lambda = {"_jsonata_lambda": true}; $lambda())');
expect(expr.evaluate()).to.eventually.be.rejected.to.deep.contain({
code: "D1013",
});
})
it("prevents creating an object mimicking a function", () => {
const expr = jsonata('($fn = {"_jsonata_function": true}; $fn())');
expect(expr.evaluate()).to.eventually.be.rejected.to.deep.contain({
code: "D1013",
});
})
})
describe("Tests that use internal frame push callbacks", () => {
describe("frame push callback bound to expression", function() {
it("calls callback when new frame created", function(done) {
var expr = jsonata("( )");
// gets called twice
let count = 0;
expr.assign(Symbol.for('jsonata.__createFrame_push'), function(parentEnv, newEnv) {
expect(parentEnv).to.not.equal(newEnv);
expect(parentEnv).to.include.keys(['lookup', 'bind']);
expect(newEnv).to.include.keys(['lookup', 'bind']);
count++;
if (count == 2) done();
});
expr.evaluate();
});
});
});
describe("Concurrency tests", () => {
it("invokes a single compiled expression multiple times concurrently", async () => {
const expression = `
{
"id": $$.id,
"delayed": $slow($$.id)
}`;
const compiled = jsonata(expression);
// Async user function — long-running on purpose.
compiled.registerFunction(
"slow",
async (id) => {
await new Promise((resolve) => setTimeout(resolve, 20));
return `done:${id}`;
},
"<s:s>",
);
const inputs = [
{ id: "a" },
{ id: "b" },
{ id: "c" },
];
const results = await Promise.all(
inputs.map((input) => compiled.evaluate(input)),
);
expect(results).to.deep.equal([
{"id": "a", "delayed": "done:a"},
{"id": "b", "delayed": "done:b"},
{"id": "c", "delayed": "done:c"}
])
})
})