mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-27 02:34:52 -06:00
91d98c7205
This commit adds a postprocessor to the docs build that enables examples to be edited and have the results show up live. See the additions to `docs/README.md` for more about what writing these blocks looks like or the netlify PR preview (e.g. at `docs/edge/how-do-i-write-policies/`) to see them in action (you'll need to disable CORS for live output). Signed-off-by: David Boles <me@davidbol.es>
373 lines
12 KiB
JavaScript
373 lines
12 KiB
JavaScript
import chai, {expect} from 'chai'
|
|
import chaiAsPromised from 'chai-as-promised'
|
|
import sinon from 'sinon'
|
|
import {describe, it} from 'mocha'
|
|
|
|
import {batchMapFold, batchProcess, delay, expectedErrorTags, getAllGroupModules, getGroupData, getGroupField, handleLater, infoFromLabel, toArray} from '../src/helpers'
|
|
import {BLOCK_TYPES, TAG_TYPES} from '../src/constants'
|
|
|
|
chai.use(chaiAsPromised)
|
|
|
|
describe('infoFromLabel', () => {
|
|
it('parses a label', () => {
|
|
const res = infoFromLabel('live:eg/pi:output:merge_down,expect_undefined')
|
|
expect(res).has.keys(['group', 'type', 'tags'])
|
|
expect(res.group).equals('eg/pi')
|
|
expect(res.type).equals(BLOCK_TYPES.OUTPUT)
|
|
expect(res.tags).lengthOf(2).and.contains('merge_down').and.contains('expect_undefined')
|
|
})
|
|
|
|
it('parses a label without tags', () => {
|
|
const res = infoFromLabel('live:eg/pi:output')
|
|
expect(res).has.keys(['group', 'type', 'tags'])
|
|
expect(res.group).equals('eg/pi')
|
|
expect(res.type).equals('output')
|
|
expect(res.tags).lengthOf(0)
|
|
})
|
|
|
|
it('errors on non-live block labels', () => {
|
|
expect(() => infoFromLabel('json')).to.throw()
|
|
})
|
|
|
|
it('errors on malformed block labels', () => {
|
|
expect(() => infoFromLabel('live:eg:modle')).to.throw()
|
|
expect(() => infoFromLabel('live::module')).to.throw()
|
|
expect(() => infoFromLabel('live:eg:module:')).to.throw()
|
|
expect(() => infoFromLabel('live:eg:module:foobar')).to.throw()
|
|
expect(() => infoFromLabel('live:eg:module:expect_undefined')).to.throw()
|
|
})
|
|
})
|
|
|
|
describe('expectedErrorTags', () => {
|
|
it('accepts an empty array', () => {
|
|
expect(expectedErrorTags([])).deep.equals([])
|
|
})
|
|
|
|
it('can consume only non-expectations', () => {
|
|
expect(expectedErrorTags([TAG_TYPES.HIDDEN, TAG_TYPES.MERGE_DOWN])).deep.equals([])
|
|
})
|
|
|
|
it('can consume only expectations', () => {
|
|
expect(expectedErrorTags([TAG_TYPES.EXPECT_BUILTIN_ERROR, TAG_TYPES.EXPECT_ERROR]))
|
|
.has.lengthOf(2)
|
|
.and.contains(TAG_TYPES.EXPECT_BUILTIN_ERROR)
|
|
.and.contains(TAG_TYPES.EXPECT_ERROR)
|
|
})
|
|
|
|
it('can consume a mixture', () => {
|
|
expect(expectedErrorTags([TAG_TYPES.HIDDEN, TAG_TYPES.EXPECT_BUILTIN_ERROR, TAG_TYPES.MERGE_DOWN, TAG_TYPES.EXPECT_ERROR]))
|
|
.has.lengthOf(2)
|
|
.and.contains(TAG_TYPES.EXPECT_BUILTIN_ERROR)
|
|
.and.contains(TAG_TYPES.EXPECT_ERROR)
|
|
})
|
|
|
|
it('doesn\'t mutate argument', () => {
|
|
const tags = [TAG_TYPES.HIDDEN, TAG_TYPES.EXPECT_BUILTIN_ERROR, TAG_TYPES.MERGE_DOWN, TAG_TYPES.EXPECT_ERROR]
|
|
const args = tags.slice()
|
|
|
|
expectedErrorTags([TAG_TYPES.HIDDEN, TAG_TYPES.EXPECT_BUILTIN_ERROR, TAG_TYPES.MERGE_DOWN, TAG_TYPES.EXPECT_ERROR])
|
|
expect(args).to.deep.equal(tags)
|
|
})
|
|
})
|
|
|
|
describe('getGroupField', () => {
|
|
it('gets field in group if there', () => {
|
|
expect(getGroupField({foo: {bar: 1}, 'foo/two': {bar: 2}}, 'foo/two', 'bar')).to.equal(2)
|
|
})
|
|
|
|
it('gets parents\' fields when field is missing', () => {
|
|
expect(getGroupField({foo: {bar: 7}, 'foo/two': {baz: 42}}, 'foo/two', 'bar')).to.equal(7)
|
|
})
|
|
|
|
it('gets parents\' fields when group is missing', () => {
|
|
expect(getGroupField({foo: {bar: 7}}, 'foo/two', 'bar')).to.equal(7)
|
|
})
|
|
|
|
it('gets field from deepest parent', () => {
|
|
const groups = {foo: {bar: 1}, 'foo/two': {bar: 2}, 'foo/two/child': {}}
|
|
expect(getGroupField(groups, 'foo/two/child', 'bar')).to.equal(2)
|
|
})
|
|
|
|
it('skips missing parents', () => {
|
|
expect(getGroupField({foo: {bar: 7}, 'foo/two/three': {baz: 42}}, 'foo/two/three', 'bar')).to.equal(7)
|
|
})
|
|
|
|
it('returns undefined when hierarchy does not exist', () => {
|
|
expect(getGroupField({foo: {bar: 7}, 'foo/two/three': {baz: 42}}, 'a/b/c', 'bar')).to.equal(undefined)
|
|
})
|
|
|
|
it('returns undefined when out of parents', () => {
|
|
expect(getGroupField({foo: {bar: 7}, 'foo/two': {baz: 42}}, 'foo/two', 'unset')).to.equal(undefined)
|
|
})
|
|
|
|
it('doesn\'t mutate groups', () => {
|
|
const groups = {foo: {bar: 7}, 'foo/two': {baz: 42}}
|
|
const arg = JSON.parse(JSON.stringify(groups))
|
|
getGroupField(arg, 'foo/two', 'bar')
|
|
expect(arg).to.deep.equal(groups)
|
|
getGroupField(arg, 'foo/two', 'unset')
|
|
expect(arg).to.deep.equal(groups)
|
|
})
|
|
})
|
|
|
|
describe('getAllGroupModules', () => {
|
|
it('can get no modules', () => {
|
|
expect(getAllGroupModules({}, 'foo')).to.deep.equal([])
|
|
expect(getAllGroupModules({foo: {bar: 7}, 'foo/two': {baz: 42}}, 'foo/two')).to.deep.equal([])
|
|
})
|
|
|
|
it('gets all the present modules from shallowest to deepest', () => {
|
|
expect(getAllGroupModules({
|
|
'a': {module: 1},
|
|
'a/b/c': {module: 2}
|
|
}, 'a/b/c/d')).to.deep.equal([1, 2])
|
|
})
|
|
|
|
it('doesn\'t mutate groups', () => {
|
|
const groups = {
|
|
'a': {module: 1},
|
|
'a/b/c': {module: 2}
|
|
}
|
|
const arg = JSON.parse(JSON.stringify(groups))
|
|
getAllGroupModules(arg, 'a/b/c/d')
|
|
expect(arg).to.deep.equal(groups)
|
|
})
|
|
})
|
|
|
|
describe('getGroupData', () => {
|
|
it('concatenates module blocks', () => {
|
|
expect(getGroupData({
|
|
'a': {module: {get: () => 'a'}},
|
|
'a/b/c': {module: {get: () => 'c'}}
|
|
}, 'a/b/c/d').module).to.equal('a\n\nc')
|
|
})
|
|
|
|
it('errors when there are no module blocks', () => {
|
|
expect(() => getGroupData({
|
|
'a': {query: {get: () => 'a'}},
|
|
'a/b/c': {query: {get: () => 'c'}}
|
|
}, 'a/b/c/d')).to.throw('no module')
|
|
})
|
|
|
|
const egInput = ['foo', {bar: 'baz'}]
|
|
const strInput = JSON.stringify(egInput)
|
|
|
|
it('parses input from block', () => {
|
|
expect(getGroupData({
|
|
'a': {input: {get: () => 'foobar'}, module: {get: () => 'a'}}, // Will error if gets from this parent
|
|
'a/b/c': {input: {get: () => strInput}, module: {get: () => 'c'}}
|
|
}, 'a/b/c').input).to.deep.equal(egInput)
|
|
})
|
|
|
|
it('can get input from parents', () => {
|
|
expect(getGroupData({
|
|
'a': {input: {get: () => 'foobar'}, module: {get: () => 'a'}}, // Will error if gets from this parent
|
|
'a/b/c': {input: {get: () => strInput}, module: {get: () => 'c'}}
|
|
}, 'a/b/c/d').input).to.deep.equal(egInput)
|
|
})
|
|
|
|
it('errors when input cannot be parsed', () => {
|
|
expect(() => getGroupData({
|
|
'a': {input: {get: () => 'foobar'}, module: {get: () => 'a'}},
|
|
}, 'a')).to.throw('can\'t parse input')
|
|
})
|
|
|
|
it('gets the query', () => {
|
|
expect(getGroupData({
|
|
'a': {query: {get: () => {
|
|
throw new Error()
|
|
}}, module: {get: () => 'a'}}, // Will error if gets from this parent
|
|
'a/b/c': {query: {get: () => 'foobar'}, module: {get: () => 'c'}}
|
|
}, 'a/b/c').query).to.equal('foobar')
|
|
})
|
|
|
|
it('can get the query from parents', () => {
|
|
expect(getGroupData({
|
|
'a': {query: {get: () => {
|
|
throw new Error()
|
|
}}, module: {get: () => 'a'}}, // Will error if gets from this parent
|
|
'a/b/c': {query: {get: () => 'foobar'}, module: {get: () => 'c'}}
|
|
}, 'a/b/c/d').query).to.equal('foobar')
|
|
})
|
|
|
|
it('doesn\'t mutate groups', () => {
|
|
const groups = {
|
|
'a': {input: {get: () => '{}'}, module: {get: () => 'a'}}, // Will error if gets from this parent
|
|
'a/b/c': {query: {get: () => 'foobar'}, module: {get: () => 'c'}}
|
|
}
|
|
const arg = Object.fromEntries(Object.entries(groups).map(([key, value]) => [key, Object.assign({}, value)])) // depth 2 object clone
|
|
|
|
getGroupData(arg, 'a/b/c/d')
|
|
expect(arg).to.deep.equal(groups)
|
|
})
|
|
})
|
|
|
|
describe('handleLater', () => {
|
|
it('resolves with the resolved value', async () => {
|
|
expect(await handleLater(() => Promise.resolve('foo'))).to.deep.equal(['foo', undefined])
|
|
})
|
|
|
|
it('resolves with the rejected value', async () => {
|
|
expect(await handleLater(() => Promise.reject('foo'))).to.deep.equal([undefined, 'foo'])
|
|
})
|
|
|
|
it('resolves with resolved undefined', async () => {
|
|
expect(await handleLater(() => Promise.resolve(undefined))).to.deep.equal([undefined, undefined])
|
|
})
|
|
|
|
it('resolves with rejected undefined', async () => {
|
|
expect(await handleLater(() => Promise.reject(undefined))).to.deep.equal([undefined, undefined])
|
|
})
|
|
})
|
|
|
|
describe('batchProcess', () => {
|
|
it('can run no jobs', async () => {
|
|
expect(await batchProcess(0, [])).to.deep.equal([])
|
|
})
|
|
|
|
it('runs only maxC jobs at a time', async () => {
|
|
expect(await time(() =>
|
|
batchProcess(1, [
|
|
async () => await delay(100),
|
|
async () => await delay(100),
|
|
])
|
|
)).to.equal(200)
|
|
|
|
expect(await time(() =>
|
|
batchProcess(2, [
|
|
async () => await delay(100),
|
|
async () => await delay(100),
|
|
async () => await delay(100),
|
|
async () => await delay(100),
|
|
])
|
|
)).to.equal(200)
|
|
})
|
|
|
|
it('runs all of them simultaneously with maxC = 0', async () => {
|
|
expect(await time(() =>
|
|
batchProcess(0, [
|
|
async () => await delay(100),
|
|
async () => await delay(100),
|
|
])
|
|
)).to.equal(100)
|
|
})
|
|
|
|
it('resolves to an array of the results', async () => {
|
|
expect(await batchProcess(0, [() => 1, () => 2])).to.have.a.lengthOf(2).and.contain(1).and.contain(2)
|
|
})
|
|
|
|
it('rejects once all are run with a flat array of errors', async () => {
|
|
const [_, e] = await handleLater(() => batchProcess(0, [() => {
|
|
throw [1, 2]
|
|
}, () => {
|
|
throw 3
|
|
}]))
|
|
expect(e).to.have.a.lengthOf(3).and.contain(1).and.contain(2).and.contain(3)
|
|
})
|
|
|
|
it('doesn\'t mutate jobs', async () => {
|
|
const jobs = [() => 1, () => 2]
|
|
const arg = jobs.slice()
|
|
await batchProcess(0, arg)
|
|
expect(arg).to.deep.equal(jobs)
|
|
})
|
|
})
|
|
|
|
describe('batchMapFold', () => {
|
|
it('can handle no input', async () => {
|
|
expect(await batchMapFold(0, [], () => {
|
|
throw ''
|
|
}, () => {
|
|
throw ''
|
|
}, 'foo')).to.equal('foo')
|
|
})
|
|
|
|
it('handles only maxC inputs at a time', async () => {
|
|
expect(await time(() =>
|
|
batchMapFold(1, [0, 0], () => delay(100), () => {}, 0)
|
|
)).to.equal(200)
|
|
|
|
expect(await time(() =>
|
|
batchMapFold(2, [0, 0, 0, 0], () => delay(100), () => {}, 0)
|
|
)).to.equal(200)
|
|
})
|
|
|
|
it('handles all of them simultaneously with maxC = 0', async () => {
|
|
expect(await time(() =>
|
|
batchMapFold(0, [0, 0, 0, 0], () => delay(100), () => {}, 0)
|
|
)).to.equal(100)
|
|
})
|
|
|
|
it('resolves to the folded results', async () => {
|
|
expect(await batchMapFold(0, [1, 2, 4, 8], (n) => n, (o, {output: [n]}) => o + n, 0)).to.equal(15)
|
|
})
|
|
|
|
it('reports map errors to the folder', async () => {
|
|
expect(await batchMapFold(0, [1, 2, 4, 8], (n) => {
|
|
throw n
|
|
}, (o, {output: [, n]}) => o + n, 0)).to.equal(15)
|
|
})
|
|
|
|
it('folds synchronously', async () => {
|
|
expect(await batchMapFold(0, [1, 2, 4, 8], (n) => n, async (o, {output: [n]}) => o + n, 0)).to.equal('[object Promise]1')
|
|
})
|
|
|
|
it('doesn\'t mutate inputs', async () => {
|
|
const inputs = [1, 2, 4, 8]
|
|
const arg = inputs.slice()
|
|
await batchMapFold(0, arg, (n) => n, (o, {output: [n]}) => o + n, 0)
|
|
expect(arg).to.deep.equal(inputs)
|
|
})
|
|
|
|
it('can mutate the out argument', async () => {
|
|
const out = {val: 0}
|
|
await batchMapFold(0, [1, 2, 4, 8], (n) => n, (o, {output: [n]}) => {
|
|
o.val += n; return o
|
|
}, out)
|
|
expect(out.val).to.equal(15)
|
|
})
|
|
})
|
|
|
|
describe('delay', () => {
|
|
it('delays the given amount', async () => {
|
|
expect(await time(() => delay(100))).to.equal(100)
|
|
})
|
|
})
|
|
|
|
describe('toArray', () => {
|
|
it('returns arrays', () => {
|
|
const empty = []
|
|
expect(toArray(empty)).to.equal(empty) // Not just deep; actually equal
|
|
const full = [1, 2, 3]
|
|
expect(toArray(full)).to.equal(full)
|
|
})
|
|
|
|
it('wraps non-arrays', () => {
|
|
expect(toArray(undefined)).to.deep.equal([undefined])
|
|
expect(toArray('a')).to.deep.equal(['a'])
|
|
expect(toArray({a: 'a'})).to.deep.equal([{a: 'a'}])
|
|
})
|
|
})
|
|
|
|
// Time how long it takes an async function to complete without actually waiting that long. Returns a promise that resolves to the # of ms.
|
|
async function time(f) {
|
|
const clock = sinon.useFakeTimers() // Will only advance automatically if runAll() doesn't work.
|
|
|
|
// Run the function
|
|
let done = false;
|
|
(async () => {
|
|
await handleLater(f) // await f to either resolve or reject
|
|
done = true
|
|
})()
|
|
|
|
// Run the clock, allowing the function's delays to complete
|
|
while (!done) {
|
|
clock.runAll() // Even though it's called 'runAll', calling it multiple times is necessary
|
|
await Promise.resolve() // Yield to the function that's running
|
|
}
|
|
|
|
// Finish up
|
|
const end = new Date().getTime()
|
|
clock.restore()
|
|
return end
|
|
} |