apostrophe
Version:
The Apostrophe Content Management System.
262 lines (252 loc) • 7.9 kB
JavaScript
const t = require('../test-lib/test.js');
const assert = require('assert');
const { createId } = require('@paralleldrive/cuid2');
describe('Recursion Guard', function() {
this.timeout(t.timeout);
let apos;
after(function() {
return t.destroy(apos);
});
it('should exist on the apos.util object', async function() {
apos = await t.create({
root: module,
modules: {
product: {
options: {
alias: 'product'
},
extend: '@apostrophecms/piece-type',
fields: {
add: {
_articles: {
type: 'relationshipReverse',
withType: 'article'
}
}
}
},
article: {
options: {
alias: 'article'
},
extend: '@apostrophecms/piece-type',
fields: {
add: {
_products: {
type: 'relationship',
withType: 'product'
},
main: {
type: 'area',
widgets: {
article: {}
}
}
}
}
},
'article-widget': {
extend: '@apostrophecms/widget-type',
options: {
label: 'Article'
},
fields: {
add: {
_articles: {
type: 'relationship',
withType: 'article'
}
}
}
},
'scary-article-widget': {
extend: '@apostrophecms/widget-type',
options: {
label: 'Scary Article',
neverLoadSelf: false
},
fields: {
add: {
_articles: {
type: 'relationship',
withType: 'article'
}
}
}
},
'product-widget': {
extend: '@apostrophecms/widget-type',
options: {
label: 'Product'
},
fields: {
add: {
_products: {
type: 'relationship',
withType: 'product'
}
}
}
},
'@apostrophecms/page': {
options: {
park: [
{
slug: '/recursion-test-page',
type: 'recursion-test-page',
title: 'Recursion Test Page',
parkedId: 'recursion-test-page'
}
],
types: [
{
name: '@apostrophecms/home-page',
label: 'Home'
},
{
name: 'recursion-test-page',
label: 'Recursion Test Page'
}
]
}
},
'recursion-test-page': {
extend: '@apostrophecms/page-type'
}
}
});
assert(apos.util.recursionGuard);
});
it('should create a stack as it goes and stop without executing depth 50', async function() {
let depth = 0;
let depth49First = true;
const req = apos.task.getReq();
let warnings = '';
// Capture output of util.warn so we can verify there was a warning
apos.util.warn = function(...args) {
warnings += args.join('\n');
};
const result = await load();
assert(warnings.match(/review the stack to find the problem/));
assert(warnings.match(/test/));
// Guarded functions not directly blocked by the depth rule should
// return their own result
assert(result === 'result');
assert(depth === 49);
assert(!req.aposStack.length);
async function load() {
return apos.util.recursionGuard(req, 'test', async () => {
depth++;
assert(req.aposStack);
assert(req.aposStack.length === depth);
const nestedResult = await load();
// Careful, "depth" stays at 49 as we return up the stack
if ((depth === 49) && (depth49First === true)) {
// Guarded functions directly blocked by the depth rule
// should return undefined
assert(nestedResult === undefined);
depth49First = false;
} else {
// Other invocations should return the result of the inner function
assert(nestedResult === 'result');
}
return 'result';
});
}
});
it('should immediately stop runaway self-references among widgets by default', async function() {
const req = apos.task.getReq();
const product = await apos.product.insert(req, {
title: 'Test Product'
});
const selfRefId = createId();
await apos.article.insert(req, {
aposDocId: selfRefId,
title: 'Self Referential Article',
main: {
metaType: 'area',
_id: createId(),
items: [
{
metaType: 'widget',
type: 'article',
articlesIds: [ selfRefId ]
},
{
metaType: 'widget',
type: 'product',
productsIds: [ product._id ]
}
]
}
});
const article = await apos.article.find(req, {
aposDocId: selfRefId
}).toObject();
assert(article);
// Sanity check that we didn't kill all widget loaders: check
// the product widget
assert(article.main.items[1]._products[0]);
// Now dig into the recursive article widget
assert(article.main.items[0]);
// We do go down one level, because it's not recursing within the
// widget loader yet on the first go
assert(article.main.items[0]._articles);
assert(article.main.items[0]._articles[0]);
// However there should be no second go!
assert(article.main.items[0]._articles[0].main);
assert(article.main.items[0]._articles[0].main.items[0]);
assert(!article.main.items[0]._articles[0].main.items[0]._articles);
});
it('should eventually stop runaway self-references among widgets that use neverLoadSelf: false', async function() {
const req = apos.task.getReq();
const selfRefId = createId();
await apos.article.insert(req, {
aposDocId: selfRefId,
title: 'Very Self Referential Article',
main: {
metaType: 'area',
_id: createId(),
items: [
{
metaType: 'widget',
type: 'scary-article',
articlesIds: [ selfRefId ]
}
]
}
});
let warnings = '';
// Capture output of util.warn so we can verify there was a warning
apos.util.warn = function(...args) {
warnings += args.join('\n');
};
const article = await apos.article.find(req, {
aposDocId: selfRefId
}).toObject();
// Verify the stack shown in the warning references the right bits
assert(warnings.match(/widget:scary-article/));
assert(warnings.match(/relationship:article/));
// If we get this far we successfully stopped the recursion at some depth
assert(article);
// ... But verify some recursion did take place
assert(article.main.items[0]);
assert(article.main.items[0]._articles[0]);
assert(article.main.items[0]._articles[0].main.items[0]._articles[0]);
});
it('should eventually stop runaway self-references in async components', async function() {
let warnings = '';
// Capture output of util.warn so we can verify there was a warning
apos.util.warn = function(...args) {
warnings += args.join('\n');
};
const html = await apos.http.get('/recursion-test-page');
assert(warnings.match(/component:recursion-test-page:test/));
// If we got this far the recursion was eventually stopped
assert(html.match(/Sing to me, Oh Muse\./));
assert(html.match(/At depth 0/));
assert(html.match(/At depth 48/));
assert(!html.match(/At depth 49/));
assert(!html.match(/Object/));
});
});