From 2cbfe58654266a4aedab8f7acac00182d9f8ab9e Mon Sep 17 00:00:00 2001 From: Karolin Varner Date: Fri, 19 Apr 2019 13:50:37 +0200 Subject: [PATCH] feat(pipe): Simplify pipeline step executor BREAKING CHANGE: return value from pipeline functions are no longer merged into the context NOTE: Most of the functional changes live in src/pipeline.js; most other changes are just refactoring the rest of the code base to utilize the changes there. feat(pipe): Drop write protection in pipeline fixes #228 feat(pipe): Get rid of parameter merging fixes #223 This specific change necessitated the bulk of the changes to the code bases; most of the code that was refactored relied on parameter merging before. feat(pipe): Fuller error reporting in pipeline Specifically, all errors are now reported by printing an error message the stack trace with additional information identifying the specific pipeline step. On the http level a 500 status code is transmitted. Co-authored-by: Tobias Bocanegra --- .eslintrc.js | 8 +- src/defaults/html.pipe.js | 5 +- src/defaults/json.pipe.js | 1 - src/defaults/xml.pipe.js | 5 +- src/helper.js | 25 ----- src/html/fetch-markdown.js | 49 ++++---- src/html/find-embeds.js | 3 - src/html/flag-esi.js | 11 +- src/html/get-metadata.js | 62 +++++------ src/html/html-to-hast.js | 11 +- src/html/make-html.js | 5 +- src/html/output-debug.js | 35 +++--- src/html/parse-frontmatter.js | 10 +- src/html/parse-markdown.js | 10 +- src/html/responsify-images.js | 3 - src/html/set-status.js | 70 +++++------- src/html/set-surrogate-key.js | 35 +++--- src/html/shared-cache.js | 15 +-- src/html/smartypants.js | 11 +- src/html/split-sections.js | 18 ++- src/html/static-asset-links.js | 16 +-- src/html/stringify-hast.js | 10 +- src/json/emit-json.js | 29 +++-- src/pipeline.js | 136 +++++++++++++---------- src/utils/coerce-secrets.js | 1 - src/utils/conditional-sections.js | 107 ++++++------------ src/utils/dump-context.js | 2 - src/utils/is-production.js | 2 +- src/utils/set-content-type.js | 18 +-- src/xml/check-xml.js | 20 ++-- src/xml/emit-xml.js | 33 +++--- src/xml/set-xml-status.js | 66 +++++------ test/testCheckXML.js | 4 +- test/testConditionalSections.js | 55 +++++---- test/testDebugTmp.js | 11 +- test/testDefault.js | 6 +- test/testEmbedHandler.js | 8 +- test/testEmitJSON.js | 54 +++------ test/testEmitXML.js | 29 ++--- test/testFetchMarkdown.js | 90 ++++++++------- test/testFindEmbeds.js | 12 +- test/testFrontmatter.js | 11 +- test/testGetMetadata.js | 101 ++++++++--------- test/testHTML.js | 135 +++++++++++----------- test/testHTMLProduction.js | 10 +- test/testHelper.js | 31 ------ test/testJSON.js | 31 +++--- test/testOutputDebug.js | 10 +- test/testParseMarkdown.js | 7 +- test/testPipeline.js | 159 ++++++++++++++------------ test/testRewriteStatic.js | 44 ++++---- test/testSetContentType.js | 17 +-- test/testSetStatus.js | 62 ++++------- test/testSetXMLStatus.js | 78 ++++++------- test/testSmartypants.js | 14 ++- test/testSplitSections.js | 8 +- test/testStringifyHast.js | 166 ++++++++++++++-------------- test/testToHast.js | 178 +++++++++++++++--------------- test/testXML.js | 78 +++++++------ 59 files changed, 1038 insertions(+), 1203 deletions(-) delete mode 100644 src/helper.js delete mode 100644 test/testHelper.js diff --git a/.eslintrc.js b/.eslintrc.js index fb5237fd5..6312a379b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -42,8 +42,12 @@ module.exports = { // Allow return before else & redundant else statements 'no-else-return': 'off', - // allow dangling underscores for 'fields' - 'no-underscore-dangle': ['error', {'allowAfterThis': true}], + // Quite useful to mark values as unused + 'no-underscore-dangle': 'off', + + // We have quite a lot of use cases where assignment to function + // parameters is definitely desirable + 'no-param-reassign': 'off', // enforce license header 'header/header': [2, 'block', ['', diff --git a/src/defaults/html.pipe.js b/src/defaults/html.pipe.js index 738c036f9..e70ac9014 100644 --- a/src/defaults/html.pipe.js +++ b/src/defaults/html.pipe.js @@ -18,7 +18,7 @@ const meta = require('../html/get-metadata.js'); const html = require('../html/make-html.js'); const responsive = require('../html/responsify-images.js'); const type = require('../utils/set-content-type.js'); -const { selectStatus } = require('../html/set-status.js'); +const selectStatus = require('../html/set-status.js'); const smartypants = require('../html/smartypants'); const sections = require('../html/split-sections'); const { selectstrain, selecttest } = require('../utils/conditional-sections'); @@ -36,7 +36,6 @@ const tohast = require('../html/html-to-hast'); const tohtml = require('../html/stringify-hast'); const addHeaders = require('../html/add-headers'); -/* eslint no-param-reassign: off */ /* eslint newline-per-chained-call: off */ function hascontent({ content }) { @@ -71,7 +70,7 @@ const htmlpipe = (cont, payload, action) => { .after(addHeaders) .after(tohtml) // end HTML post-processing .after(flag).expose('esi').when(esi) // flag ESI when there is ESI in the response - .error(selectStatus(production())); + .error(selectStatus); action.logger.log('debug', 'Running HTML pipeline'); return pipe.run(payload); diff --git a/src/defaults/json.pipe.js b/src/defaults/json.pipe.js index eda48b3de..1bab113df 100644 --- a/src/defaults/json.pipe.js +++ b/src/defaults/json.pipe.js @@ -25,7 +25,6 @@ const emit = require('../json/emit-json.js'); const { selectStatus } = require('../json/set-json-status.js'); const parseFrontmatter = require('../html/parse-frontmatter.js'); -/* eslint no-param-reassign: off */ /* eslint newline-per-chained-call: off */ const jsonpipe = (cont, payload, action) => { diff --git a/src/defaults/xml.pipe.js b/src/defaults/xml.pipe.js index 1947b4c63..ab638c76b 100644 --- a/src/defaults/xml.pipe.js +++ b/src/defaults/xml.pipe.js @@ -25,11 +25,10 @@ const dump = require('../utils/dump-context.js'); const validate = require('../utils/validate'); const type = require('../utils/set-content-type.js'); const emit = require('../xml/emit-xml.js'); -const { selectStatus } = require('../xml/set-xml-status.js'); +const selectStatus = require('../xml/set-xml-status.js'); const check = require('../xml/check-xml'); const parseFrontmatter = require('../html/parse-frontmatter'); -/* eslint no-param-reassign: off */ /* eslint newline-per-chained-call: off */ const xmlpipe = (cont, payload, action) => { @@ -53,7 +52,7 @@ const xmlpipe = (cont, payload, action) => { .when(uncached) .after(key) .after(flag).expose('esi').when(esi) // flag ESI when there is ESI in the response - .error(selectStatus(production())); + .error(selectStatus); action.logger.log('debug', 'Running XML pipeline'); return pipe.run(payload); diff --git a/src/helper.js b/src/helper.js deleted file mode 100644 index 413994ba9..000000000 --- a/src/helper.js +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2018 Adobe. All rights reserved. - * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. You may obtain a copy - * of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - * OF ANY KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ - -function bail(logger, message, error, status = 500) { - logger.error(message); - return { - error: Object.assign({ message }, error), - response: { - status, - }, - }; -} - -module.exports = { - bail, -}; diff --git a/src/html/fetch-markdown.js b/src/html/fetch-markdown.js index 9db56ba9d..438a4ca80 100644 --- a/src/html/fetch-markdown.js +++ b/src/html/fetch-markdown.js @@ -9,9 +9,10 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ +const { inspect } = require('util'); const client = require('request-promise-native'); const URI = require('uri-js'); -const { bail } = require('../helper'); +const { setdefault } = require('@adobe/helix-shared').types; function uri(root, owner, repo, ref, path) { const rootURI = URI.parse(root); @@ -36,16 +37,11 @@ function uri(root, owner, repo, ref, path) { * @param {import("../context").Context} ctx some param * @param {import("../context").Action} action some other param */ -async function fetch( - { content: { sources = [] } = {} }, - { - secrets = {}, - request, - logger, - }, -) { +async function fetch(context, { secrets = {}, request, logger }) { + const content = setdefault(context, 'content', {}); + if (!request || !request.params) { - return bail(logger, 'Request parameters are missing'); + throw new Error('Request parameters missing'); } let timeout; @@ -64,13 +60,13 @@ async function fetch( // bail if a required parameter cannot be found if (!owner) { - return bail(logger, 'Unknown owner, cannot fetch content'); + throw new Error('Unknown owner, cannot fetch content'); } if (!repo) { - return bail(logger, 'Unknown repo, cannot fetch content'); + throw new Error('Unknown repo, cannot fetch content'); } if (!path) { - return bail(logger, 'Unknown path, cannot fetch content'); + throw new Error('Unknown path, cannot fetch content'); } if (!ref) { logger.warn(`Recoverable error: no ref given for ${repo}/${owner}.git${path}, falling back to master`); @@ -86,24 +82,25 @@ async function fetch( timeout, time: true, }; + logger.debug(`fetching Markdown from ${options.uri}`); try { - const response = await client(options); - return { - content: { - body: response, - sources: [...sources, options.uri], - }, - }; + content.body = await client(options); + setdefault(content, 'sources', []).push(options.uri); } catch (e) { - if (e && e.statusCode && e.statusCode === 404) { - return bail(logger, `Could not find Markdown at ${options.uri}`, e, 404); - } else if ((e && e.response && e.response.elapsedTime && e.response.elapsedTime > timeout) || (e && e.cause && e.cause.code && (e.cause.code === 'ESOCKETTIMEDOUT' || e.cause.code === 'ETIMEDOUT'))) { + if (e.statusCode === 404) { + logger.error(`Could not find Markdown at ${options.uri}`); + setdefault(context, 'response', {}).status = 404; + } else if ((e.response && e.response.elapsedTime && e.response.elapsedTime > timeout) || (e.cause && e.cause.code && (e.cause.code === 'ESOCKETTIMEDOUT' || e.cause.code === 'ETIMEDOUT'))) { // return gateway timeout - return bail(logger, `Gateway timout of ${timeout} milliseconds exceeded for ${options.uri}`, e, 504); + logger.error(`Gateway timout of ${timeout} milliseconds exceeded for ${options.uri}`); + setdefault(context, 'response', {}).status = 504; + } else { + logger.error(`Error while fetching Markdown from ${options.uri} with the following ` + + `options:\n${inspect(options, { depth: null })}`); + setdefault(context, 'response', {}).status = 502; } - // return a bad gateway for all other errors - return bail(logger, `Could not fetch Markdown from ${options.uri}`, e, 502); + context.error = e; } } diff --git a/src/html/find-embeds.js b/src/html/find-embeds.js index 41e97de40..93b27d8d8 100644 --- a/src/html/find-embeds.js +++ b/src/html/find-embeds.js @@ -9,7 +9,6 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -/* eslint-disable no-param-reassign */ const map = require('unist-util-map'); const URI = require('uri-js'); const mm = require('micromatch'); @@ -135,8 +134,6 @@ function find({ content: { mdast }, request: { extension, url } }, internalembed(internalImgEmbed(node, url, contentext, resourceext), node, `.${EMBED_SELECTOR}.${extension}`); } }); - - return { content: { mdast } }; } module.exports = find; diff --git a/src/html/flag-esi.js b/src/html/flag-esi.js index adeb250ce..455b7122b 100644 --- a/src/html/flag-esi.js +++ b/src/html/flag-esi.js @@ -9,26 +9,29 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ + +const { merge } = require('lodash'); + /** * Detects if ESI tags are used in the repose body. Intended to be used as * a predicate in the pipeline construction. * @param {Context} param0 the pipeline payload */ function esi({ response }) { - return response && response.body && / payload))); return section; } function title(section) { const header = select('heading', section); - return header ? Object.assign({ title: plain(header) }, section) : section; + section.title = header ? plain(header) : ''; } function intro(section) { @@ -34,14 +36,16 @@ function intro(section) { } return true; })[0]; - return para ? Object.assign({ intro: plain(para) }, section) : section; + section.intro = para ? plain(para) : ''; } function image(section) { // selects the most prominent image of the section // TODO: get a better measure of prominence than "first" const img = select('image', section); - return img ? Object.assign({ image: img.url }, section) : section; + if (img) { + section.image = img.url; + } } /** @@ -77,7 +81,6 @@ function sectiontype(section) { function reducer(counter, node) { const { type, children: pChildren } = node; - // eslint-disable-next-line no-param-reassign node.data = Object.assign({ types: [] }, node.data); if (type === 'yaml') { @@ -129,43 +132,36 @@ function sectiontype(section) { } const typecounter = children.reduce(reducer, {}); - - const types = constructTypes(typecounter); - - return Object.assign({ types }, section); + section.types = constructTypes(typecounter); } function fallback(section) { if (section.intro && !section.title) { - return Object.assign({ title: section.intro }, section); + section.title = section.intro; } else if (section.title && !section.intro) { - return Object.assign({ intro: section.title }, section); + section.intro = section.title; } - return section; } -function getmetadata({ content: { sections = [] } }, { logger }) { +function getmetadata({ content }, { logger }) { + const { sections } = content; + if (!sections) { + content.meta = {}; + return; + } + logger.debug(`Parsing Markdown Metadata from ${sections.length} sections`); - const retsections = sections - .map(yaml) - .map(title) - .map(intro) - .map(image) - .map(sectiontype) - .map(fallback); - const img = retsections.filter(section => section.image)[0]; - if (retsections[0]) { - const retcontent = { - sections: retsections, - meta: retsections[0].meta, - title: retsections[0].title, - intro: retsections[0].intro, - image: img ? img.image : undefined, - }; - return { content: retcontent }; - } - return { content: { meta: {} } }; + each([yaml, title, intro, image, sectiontype, fallback], (fn) => { + each(sections, fn); + }); + + const img = sections.filter(section => section.image)[0]; + + content.meta = empty(sections) ? {} : sections[0].meta; + content.title = empty(sections) ? '' : sections[0].title; + content.intro = empty(sections) ? '' : sections[0].intro; + content.image = img ? img.image : undefined; } module.exports = getmetadata; diff --git a/src/html/html-to-hast.js b/src/html/html-to-hast.js index b15e90ac3..083a70d45 100644 --- a/src/html/html-to-hast.js +++ b/src/html/html-to-hast.js @@ -12,14 +12,9 @@ const unified = require('unified'); const parse = require('rehype-parse'); -function tohast({ response: { body } }) { - const fragment = !body.match(/'; +const DEBUG_TEMPLATE = ''; -function debug(payload, { logger }) { - const isDebug = payload.request && payload.request.params && (payload.request.params.debug === true || payload.request.params.debug === 'true'); - const hasBody = payload.response && payload.response.body; - if (isDebug && hasBody) { - logger.debug('Adding debug script'); - const p = payload; - // backup body - const { body } = p.response; - // remove body because that would be the response content - // and causes rendering issues of the script - delete p.response.body; +function debug(context, { logger }) { + const isDebug = context.request && context.request.params && (context.request.params.debug === true || context.request.params.debug === 'true'); + const hasBody = context.response && context.response.body; - const debugScript = DEBUG_TEMPLATE.replace(/PAYLOAD_JSON/, JSON.stringify(p)); - // inject debug script before the closing body tag - p.response.body = body.replace(/<\/body>/i, `${debugScript}`); - return p; + if (!isDebug || !hasBody) { + return; } - return payload; + + logger.debug('Adding debug script'); + // backup body + const { body } = context.response; + + // remove body because that would be the response content + // and causes rendering issues of the script + delete context.response.body; + + const debugScript = DEBUG_TEMPLATE.replace(/PAYLOAD_JSON/, JSON.stringify(context)); + // inject debug script before the closing body tag + context.response.body = body.replace(/<\/body>/i, `${debugScript}`); } module.exports = debug; module.exports.DEBUG_TEMPLATE = DEBUG_TEMPLATE; diff --git a/src/html/parse-frontmatter.js b/src/html/parse-frontmatter.js index edd6d33bf..5eae62b8d 100644 --- a/src/html/parse-frontmatter.js +++ b/src/html/parse-frontmatter.js @@ -259,7 +259,13 @@ const findFrontmatter = (mdast, str) => { class FrontmatterParsingError extends Error {} -const parseFrontmatter = ({ content: { mdast, body } }) => { +const parseFrontmatter = ({ content = {} }) => { + const { mdast, body } = content; + + if (!mdast || !body) { + return; + } + // We splice the mdast. let off = 0; @@ -301,7 +307,5 @@ const parseFrontmatter = ({ content: { mdast, body } }) => { } }; -parseFrontmatter.does_mutate = true; assign(parseFrontmatter, { findFrontmatter, FrontmatterParsingError }); - module.exports = parseFrontmatter; diff --git a/src/html/parse-markdown.js b/src/html/parse-markdown.js index 672daa58e..2eb0f1581 100644 --- a/src/html/parse-markdown.js +++ b/src/html/parse-markdown.js @@ -11,12 +11,14 @@ */ const unified = require('unified'); const remark = require('remark-parse'); +const { setdefault } = require('@adobe/helix-shared').types; + +function parseMarkdown(context, { logger }) { + const content = setdefault(context, 'content', {}); + const body = setdefault(content, 'body', ''); -function parseMarkdown({ content: { body = '' } = {} }, { logger }) { logger.debug(`Parsing markdown from request body starting with ${body.split('\n')[0]}`); - const preprocessor = unified().use(remark); - const mdast = preprocessor.parse(body); - return { content: { mdast } }; + content.mdast = unified().use(remark).parse(body); } module.exports = parseMarkdown; diff --git a/src/html/responsify-images.js b/src/html/responsify-images.js index 63fc461a5..bd6d0d4a4 100644 --- a/src/html/responsify-images.js +++ b/src/html/responsify-images.js @@ -29,7 +29,6 @@ const RESOLUTION_SWITCHING = [ ]; /* Parameter Reassignment is the standard design pattern for Unified */ -/* eslint no-param-reassign: "off" */ function transformer( { content: { htast } }, @@ -69,8 +68,6 @@ function transformer( // the visit function is modifying its argument in place. visit(htast); - - return { content: { htast } }; } module.exports = transformer; diff --git a/src/html/set-status.js b/src/html/set-status.js index 2adbd26fc..477e7c7e7 100644 --- a/src/html/set-status.js +++ b/src/html/set-status.js @@ -10,49 +10,33 @@ * governing permissions and limitations under the License. */ -function setVerboseError(error) { - const res = { - response: { - status: 500, - body: `

500

${error}
`, - headers: { - 'Content-Type': 'text/html', - }, - }, - }; - return res; -} +const { setdefault } = require('@adobe/helix-shared').types; +const isProduction = require('../utils/is-production'); -function selectStatus(prod) { - return ({ response = {}, error }, { logger }) => { - // if a status is already default, keep it. - if (response.status) { - return {}; - } - if (!error) { - return { - response: { - status: 200, - }, - }; - } - // error handling - logger.debug('payload.error -> 500'); - if (prod) { - return { - response: { - status: 500, - body: '', - }, - }; - } - return setVerboseError(error); - }; -} +const selectStatus = (context, { logger }) => { + const res = setdefault(context, 'response', {}); + const headers = setdefault(res, 'headers', {}); + const err = context.error; -function setStatus({ response = {}, error }, { logger }) { - return selectStatus(false)({ response, error }, { logger }); -} + // if a status is already default, keep it. + if (res.status) { + return; + } -module.exports = setStatus; -module.exports.selectStatus = selectStatus; + if (!err) { + res.status = 200; + return; + } + + // error handling + logger.debug('payload.error -> 500'); + res.status = 500; + res.body = ''; + + if (!isProduction()) { + res.body = `

500

${err}
`; + headers['Content-Type'] = 'text/html'; + } +}; + +module.exports = selectStatus; diff --git a/src/html/set-surrogate-key.js b/src/html/set-surrogate-key.js index ebfe0d976..595a37990 100644 --- a/src/html/set-surrogate-key.js +++ b/src/html/set-surrogate-key.js @@ -10,24 +10,27 @@ * governing permissions and limitations under the License. */ const crypto = require('crypto'); +const { type } = require('@adobe/helix-shared').types; +const { map, join } = require('@adobe/helix-shared').sequence; +const { setdefault } = require('@adobe/helix-shared').types; + +function key(context, { logger }) { + const cont = setdefault(context, 'content', {}); + const res = setdefault(context, 'response', {}); + const headers = setdefault(res, 'headers', {}); -function key({ content, response }, { logger }) { // somebody already set a surrogate key - if (!(response && response.headers && response.headers['Surrogate-Key']) && (content && content.sources && Array.isArray(content.sources))) { - logger.debug('Setting Surrogate-Key header'); - return { - response: { - headers: { - 'Surrogate-Key': content.sources.map((uri) => { - const hash = crypto.createHash('sha256'); - hash.update(uri); - return hash.digest('base64'); - }).join(' '), - }, - }, - }; + if (headers['Surrogate-Key'] || !(type(cont.sources) === Array)) { + logger.debug('Keeping existing Surrogate-Key header'); + return; } - logger.debug('Keeping existing Surrogate-Key header'); - return {}; + + logger.debug('Setting Surrogate-Key header'); + headers['Surrogate-Key'] = join(' ')(map(cont.sources, (uri) => { + const hash = crypto.createHash('sha256'); + hash.update(uri); + return hash.digest('base64'); + })); } + module.exports = key; diff --git a/src/html/shared-cache.js b/src/html/shared-cache.js index 6b3734038..639c66dac 100644 --- a/src/html/shared-cache.js +++ b/src/html/shared-cache.js @@ -10,19 +10,16 @@ * governing permissions and limitations under the License. */ +const { setdefault } = require('@adobe/helix-shared').types; + function uncached({ response }) { return !(response && response.headers && response.headers['Cache-Control']); } -function cache() { - return { - response: { - headers: { - // cache for up to one week in CDN - 'Cache-Control': 's-maxage=604800', - }, - }, - }; +function cache(context) { + const res = setdefault(context, 'response', {}); + const headers = setdefault(res, 'headers', {}); + headers['Cache-Control'] = 's-maxage=604800'; } module.exports = { uncached, cache }; diff --git a/src/html/smartypants.js b/src/html/smartypants.js index fee15b689..e2e46baab 100644 --- a/src/html/smartypants.js +++ b/src/html/smartypants.js @@ -13,17 +13,16 @@ const retext = require('retext'); const map = require('unist-util-map'); const smartypants = require('retext-smartypants'); -function reformat({ content: { mdast } }) { +function reformat({ content }) { + if (!content.mdast) { + return; + } const smart = retext().use(smartypants); - - map(mdast, (node) => { + map(content.mdast, (node) => { if (node.type === 'text') { - // eslint-disable-next-line no-param-reassign node.value = smart.processSync(node.value).contents; } }); - - return { content: { mdast } }; } module.exports = reformat; diff --git a/src/html/split-sections.js b/src/html/split-sections.js index 43745738c..7a0f3a06a 100644 --- a/src/html/split-sections.js +++ b/src/html/split-sections.js @@ -13,14 +13,9 @@ const between = require('unist-util-find-all-between'); const _ = require('lodash/fp'); -function section(children) { - return { - type: 'root', - children, - }; -} +function split({ content }) { + const { mdast } = content; -function split({ content: { mdast = { children: [] } } }) { // filter all children that are either yaml or break blocks const dividers = mdast.children.filter(node => node.type === 'yaml' || node.type === 'thematicBreak') // then get their index in the list of children @@ -30,17 +25,18 @@ function split({ content: { mdast = { children: [] } } }) { // include the very start and end of the document const starts = [0, ...dividers]; const ends = [...dividers, mdast.children.length]; - const sections = _.zip(starts, ends) + content.sections = _.zip(starts, ends) // but filter out empty section .filter(([start, end]) => start !== end) // then return all nodes that are in between .map(([start, end]) => { // skip 'thematicBreak' nodes const index = mdast.children[start].type === 'thematicBreak' ? start + 1 : start; - return section(between(mdast, index, end)); + return { + type: 'root', + children: between(mdast, index, end), + }; }); - - return { content: { sections } }; } module.exports = split; diff --git a/src/html/static-asset-links.js b/src/html/static-asset-links.js index 8e9e265bc..0385a32e8 100644 --- a/src/html/static-asset-links.js +++ b/src/html/static-asset-links.js @@ -48,18 +48,12 @@ function links() { }; } -function rewrite({ response: { hast, headers } }) { - if (headers && headers['Content-Type'] && headers['Content-Type'].match(/html/) && hast) { - links()(hast); - scripts()(hast); - - return { - response: { - hast, - }, - }; +function rewrite(context) { + const res = context.response; + if ((res.headers['Content-Type'] || '').match(/html/) && res.hast) { + links()(res.hast); + scripts()(res.hast); } - return {}; } module.exports = rewrite; diff --git a/src/html/stringify-hast.js b/src/html/stringify-hast.js index 07d29bfdf..ef2eb4840 100644 --- a/src/html/stringify-hast.js +++ b/src/html/stringify-hast.js @@ -11,8 +11,8 @@ */ const serialize = require('hast-util-to-html'); -function stringify({ response: { hast } }) { - const body = serialize(hast, { +function stringify(context) { + context.response.body = serialize(context.response.hast, { allowParseErrors: true, allowDangerousHTML: true, allowDangerousCharacters: true, @@ -21,12 +21,6 @@ function stringify({ response: { hast } }) { useNamedReferences: true, }, }); - - return { - response: { - body, - }, - }; } module.exports = stringify; diff --git a/src/json/emit-json.js b/src/json/emit-json.js index 4bd8351cb..819c575a3 100644 --- a/src/json/emit-json.js +++ b/src/json/emit-json.js @@ -10,26 +10,23 @@ * governing permissions and limitations under the License. */ -function emit({ content, response = {} }, { logger }) { +const { setdefault } = require('@adobe/helix-shared').types; + +function emit(context, { logger }) { + const content = setdefault(context, 'content', {}); + const response = setdefault(context, 'response', {}); + if (response.body) { logger.debug('Response body already exists'); - return {}; + return; } - if (content && content.json) { - try { - logger.debug(`Emitting JSON from ${typeof content.json}`); - return { - response: { - body: JSON.parse(JSON.stringify(content.json)), - }, - }; - } catch (e) { - logger.error(`Error building JSON: ${e}`); - return {}; - } + if (!content.json) { + logger.debug('No JSON to emit'); + return; } - logger.debug('No JSON to emit'); - return {}; + + logger.debug(`Emitting JSON from ${typeof content.json}`); + response.body = JSON.parse(JSON.stringify(content.json)); } module.exports = emit; diff --git a/src/pipeline.js b/src/pipeline.js index 9f7e5c796..fec7c4e09 100644 --- a/src/pipeline.js +++ b/src/pipeline.js @@ -10,8 +10,11 @@ * governing permissions and limitations under the License. */ +/* eslint-disable no-await-in-loop */ + const _ = require('lodash/fp'); const callsites = require('callsites'); +const { enumerate, iter } = require('@adobe/helix-shared').sequence; const coerce = require('./utils/coerce-secrets'); const noOp = () => {}; @@ -262,14 +265,12 @@ class Pipeline { } /** - * Sets an error function. The error function is executed if the `context` contains an `error` - * object. + * Sets an error function. The error function is executed when an error is encountered. * @param {pipelineFunction} f the error function. * @return {Pipeline} this; */ error(f) { this.describe(f); - const wrapped = errorWrapper(f); // ensure proper alias wrapped.alias = f.alias; @@ -290,17 +291,11 @@ class Pipeline { if (f.alias) { return f.alias; } - if (!f.alias && f.name) { - // eslint-disable-next-line no-param-reassign - f.alias = f.name; - } else if (!f.name && !f.alias) { - // eslint-disable-next-line no-param-reassign - f.alias = 'anonymous'; - } + + f.alias = f.name || f.ext || 'anonymous'; const [current, injector, caller] = callsites(); if (current.getFunctionName() === 'describe') { - // eslint-disable-next-line no-param-reassign f.alias = `${injector.getFunctionName()}:${f.alias} from ${caller.getFileName()}:${caller.getLineNumber()}`; } @@ -314,65 +309,88 @@ class Pipeline { * context. */ async run(context = {}) { + const { logger } = this._action; + // register all custom attachers to the pipeline this.attach(this._oncef); + + const getident = (fn, classifier, idx) => `${classifier}-#${idx}/${this.describe(fn)}`; + /** - * Reduction function used to process the pipeline functions and merge the context parameters. - * @param {Object} currContext Accumulated context - * @param {pipelineFunction} currFunction Function that is currently "reduced" - * @param {number} index index of the function in the given array - * @returns {Promise} Promise resolving to the new value of the accumulator + * Executes the taps of the current function. + * @param {Function[]} taps the taps + * @param {string} fnIdent the name of the function + * @param {number} fnIdx the current idx of the function */ - const merge = async (currContext, currFunction, index) => { - const skip = (!currContext.error) === (!!currFunction.errorHandler); - - // log the function that is being called and the parameters of the function - this._action.logger.silly(skip ? 'skipping ' : 'processing ', { - function: this.describe(currFunction), - index, - params: currContext, - }); - - if (skip) { - return currContext; + const execTaps = async (taps, fnIdent, fnIdx) => { + for (const [idx, t] of iter(taps)) { + const ident = getident(t, 'tap', idx); + logger.silly(`exec ${ident} before ${fnIdent}`); + try { + await t(context, this._action, fnIdx); + } catch (e) { + logger.error(`Exception during ${ident}:\n${e.stack}`); + throw e; + } } + }; + + /** + * Executes the pipeline functions + * @param {Function[]} fns the functions + * @param {number} startIdx offset of the function's index in the entire pipeline. + * @param {string} classifier type of function (for logging) + */ + const execFns = async (fns, startIdx, classifier) => { + for (const [i, f] of enumerate(fns)) { + const idx = i + startIdx; + const ident = getident(f, classifier, idx); + + // skip if error and no error handler (or no error and error handler) + if ((!context.error) === (!!f.errorHandler)) { + logger.silly(`skip ${ident}`, { + function: this.describe(f), + }); + // eslint-disable-next-line no-continue + continue; + } - // copy the pipeline payload into a new object to avoid modifications - // TODO: Better way of write-protecting the args for tapped functions; this may lead - // to weird bugs in user's code because modification seems to work but in - // fact the modification results are ignored; this one also has the draw - // TODO: This is also inefficient - // TODO: This also only works for objects and arrays; any data of custom type (e.g. DOM) - // is overwritten - const mergedargs = _.merge({}, currContext); - const tapresults = this._taps.map((f) => { try { - return f(mergedargs, this._action, index); + await execTaps(enumerate(this._taps), ident, idx); + } catch (e) { + if (!context.error) { + context.error = e; + } + } + if (context.error && !f.errorHandler) { + // eslint-disable-next-line no-continue + continue; + } + try { + logger.silly(`exec ${ident}`, { + function: this.describe(f), + }); + await f(context, this._action); } catch (e) { - return Promise.reject(e); + logger.error(`Exception during ${ident}:\n${e.stack}`); + if (!context.error) { + context.error = e; + } } - }); - - return Promise.all(tapresults) - .then(() => Promise.resolve(currFunction(mergedargs, this._action)) - .then((value) => { - const result = currFunction.does_mutate ? mergedargs : _.merge(currContext, value); - this._action.logger.silly('received ', { function: this.describe(currFunction), result }); - return result; - })).catch((e) => { - // tapping failed - this._action.logger.error(`tapping failed: ${e.stack}`); - return { - error: `${currContext.error || ''}\n${e.stack || ''}`, - }; - }); + } }; - // go over inner.pres (those that run before), inner.oncef (the function that runs once) - // and inner.posts (those that run after) – reduce using the merge function and return - // the resolved value - return [...this._pres, this._oncef, ...this._posts] - .reduce(async (ctx, fn, index) => merge(await ctx, fn, index), context); + try { + await execFns(this._pres, 0, 'pre'); + await execFns([this._oncef], this._pres.length, 'once'); + await execFns(this._posts, this._pres.length + 1, 'post'); + } catch (e) { + logger.error(`Unexpected error during pipeline execution: \n${e.stack}`); + if (!context.error) { + context.error = e; + } + } + return context; } } diff --git a/src/utils/coerce-secrets.js b/src/utils/coerce-secrets.js index ab0818cb9..871658131 100644 --- a/src/utils/coerce-secrets.js +++ b/src/utils/coerce-secrets.js @@ -14,7 +14,6 @@ const ajv = require('./validator'); function coerce(action) { const defaultsetter = ajv(action.logger, { useDefaults: true, coerceTypes: true }); if (!action.secrets) { - /* eslint-disable no-param-reassign */ action.secrets = {}; } action.logger.debug('Coercing secrets'); diff --git a/src/utils/conditional-sections.js b/src/utils/conditional-sections.js index c637328a9..f371c7a97 100644 --- a/src/utils/conditional-sections.js +++ b/src/utils/conditional-sections.js @@ -9,66 +9,38 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ + const hash = require('object-hash'); +const { setdefault, type } = require('@adobe/helix-shared').types; +const { each } = require('@adobe/helix-shared').sequence; -function selectstrain({ content }, { request, logger }) { - // this only works when there are multiple sections and a strain has been chosen - if (request.params - && request.params.strain - && content - && content.sections - && content.sections.length) { - const { strain } = request.params; - const { sections } = content; - logger.debug(`Filtering sections not intended for strain ${strain}`); - const remaining = sections.map((section) => { - if (section.meta && section.meta.strain && Array.isArray(section.meta.strain)) { - // this is a list of strains - // return true if the selected strain is somewhere in the array - return { - meta: { - type: 'array', - hidden: !section.meta.strain.includes(strain), - }, - }; - } else if (section.meta && section.meta.strain) { - // we treat it as a string - // return true if the selected strain is in the metadata - return { - meta: { - type: 'string', - hidden: !(section.meta.strain === strain), - }, - }; - } - // if there is no metadata, or no strain selection, just include it - return { - meta: { - hidden: false, - }, - }; - }); - logger.debug(`${remaining.length} Sections remaining`); - return { - content: { - sections: remaining, - }, - }; +function selectstrain(context, { request, logger }) { + const cont = setdefault(context, 'content', {}); + const params = request.params || {}; + const { strain } = params; + const { sections } = cont; + + if (!strain || !sections) { + return; } - return {}; + + logger.debug(`Filtering sections not intended for strain ${strain}`); + each(sections, (section) => { + const meta = setdefault(section, 'meta', {}); + const strainList = type(meta.strain) === Array ? meta.strain : [meta.strain]; + meta.hidden = Boolean(meta.strain && meta.strain !== [] && !strainList.includes(strain)); + }); } function testgroups(sections = []) { return sections .filter(({ meta = {} } = {}) => !!meta.test) .reduce((groups, section) => { - /* eslint-disable no-param-reassign */ if (groups[section.meta.test]) { groups[section.meta.test].push(section); } else { groups[section.meta.test] = [section]; } - /* eslint-enable no-param-reassign */ return groups; }, {}); } @@ -91,41 +63,30 @@ function pick(groups = {}, strain = 'default') { const candidates = groups[group]; candidates.sort(strainhashsort(strain)); if (candidates.length) { - /* eslint-disable-next-line no-param-reassign */ [selected[group]] = candidates; // eslint prefers array destructing here } return selected; }, {}); } -function selecttest({ content }, { request, logger }) { - if (request.params - && request.params.strain - && content - && content.sections - && content.sections.length) { - const groups = testgroups(content.sections); - const selected = pick(groups, request.params.strain); - const remaining = content.sections.map((section) => { - if (section.meta && section.meta.test) { - return { - meta: { - type: 'string', - hidden: !(section === selected[section.meta.test]), - }, - }; - } - return section; - }); +function selecttest(context, { request }) { + const cont = setdefault(context, 'content', {}); + const params = request.params || {}; + const { strain } = params; + const { sections } = cont; - logger.debug(`${remaining.length} Sections remaining`); - return { - content: { - sections: remaining, - }, - }; + if (!strain || !sections) { + return; } - return {}; + + const selected = pick(testgroups(sections), strain); + each(sections, (section) => { + if (!section.meta || !section.meta.test) { + return; + } + + section.meta.hidden = !(section === selected[section.meta.test]); + }); } module.exports = { diff --git a/src/utils/dump-context.js b/src/utils/dump-context.js index 4f26b2be8..323326cfe 100644 --- a/src/utils/dump-context.js +++ b/src/utils/dump-context.js @@ -34,13 +34,11 @@ function tstamp() { async function dump(context, action, index) { const nowStr = tstamp(); - // eslint-disable-next-line no-param-reassign action.debug = action.debug || {}; if (!action.debug.dumpDir) { const id = (context.request && context.request.headers && context.request.headers['x-openwhisk-activation-id']) || ''; const pathStr = context.request && context.request.path ? context.request.path.replace(/\//g, '-').substring(1) : ''; const dirName = `context_dump_${nowStr}_${pathStr}_${id}`; - // eslint-disable-next-line no-param-reassign action.debug.dumpDir = path.resolve(process.cwd(), 'logs', 'debug', dirName); await fs.ensureDir(action.debug.dumpDir); } diff --git a/src/utils/is-production.js b/src/utils/is-production.js index 9c4b09d30..a09527492 100644 --- a/src/utils/is-production.js +++ b/src/utils/is-production.js @@ -15,7 +15,7 @@ function production() { try { // eslint-disable-next-line no-underscore-dangle - return process && process.env && !!process.env.__OW_ACTIVATION_ID; + return Boolean(process.env.__OW_ACTIVATION_ID); } catch (e) { // this error only occurs when running inside OpenWhisk return true; diff --git a/src/utils/set-content-type.js b/src/utils/set-content-type.js index 867bbc2ba..8a3e6fdfc 100644 --- a/src/utils/set-content-type.js +++ b/src/utils/set-content-type.js @@ -9,21 +9,13 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ +const { setdefault } = require('@adobe/helix-shared').types; function type(mime) { - return function setmime({ response }, { logger }) { - if (!(response && response.headers && response.headers['Content-Type'])) { - logger.debug(`Setting content type header to ${mime}`); - return { - response: { - headers: { - 'Content-Type': mime, - }, - }, - }; - } - logger.debug('Keeping existing content type header'); - return { response }; + return function setmime(context) { + const res = setdefault(context, 'response', {}); + const head = setdefault(res, 'headers', {}); + setdefault(head, 'Content-Type', mime); }; } diff --git a/src/xml/check-xml.js b/src/xml/check-xml.js index c41f6a7b5..94e58f6d9 100644 --- a/src/xml/check-xml.js +++ b/src/xml/check-xml.js @@ -12,17 +12,17 @@ const parse = require('xml2js').parseString; function check({ response }, { logger }) { - if (response.body) { - logger.debug('Validating XML'); - parse(response.body, { trim: true }, (err) => { - if (err) { - throw new Error(`Error parsing XML: ${err}`); - } - return { }; - }); + if (!response.body) { + logger.debug('No XML to validate'); + return; } - logger.debug('No XML to validate'); - return { }; + + logger.debug('Validating XML'); + parse(response.body, { trim: true }, (err) => { + if (err) { + throw new Error(`Error parsing XML: ${err}`); + } + }); } module.exports = check; diff --git a/src/xml/emit-xml.js b/src/xml/emit-xml.js index fcaf9820d..2c9bb3100 100644 --- a/src/xml/emit-xml.js +++ b/src/xml/emit-xml.js @@ -11,28 +11,25 @@ */ const builder = require('xmlbuilder'); +const { setdefault } = require('@adobe/helix-shared').types; -function emit({ content, response = {} }, { secrets, logger }) { - if (response.body) { +function emit(context, { secrets, logger }) { + const cont = setdefault(context, 'content', {}); + const res = setdefault(context, 'response', {}); + + if (res.body) { logger.debug('Response body already exists'); - return {}; + return; } - if (content.xml) { - try { - logger.debug(`Emitting XML from ${typeof content.xml}`); - const xml = builder.create(content.xml, { encoding: 'utf-8' }); - return { - response: { - body: xml.end({ pretty: !!secrets.XML_PRETTY }), - }, - }; - } catch (e) { - logger.error(`Error building XML: ${e}`); - return {}; - } + + if (!cont.xml) { + logger.debug('No XML to emit'); + return; } - logger.debug('No XML to emit'); - return {}; + + logger.debug(`Emitting XML from ${typeof cont.xml}`); + const xml = builder.create(cont.xml, { encoding: 'utf-8' }); + res.body = xml.end({ pretty: !!secrets.XML_PRETTY }); } module.exports = emit; diff --git a/src/xml/set-xml-status.js b/src/xml/set-xml-status.js index 867e0bb9a..23ff1d4b4 100644 --- a/src/xml/set-xml-status.js +++ b/src/xml/set-xml-status.js @@ -10,49 +10,33 @@ * governing permissions and limitations under the License. */ -function setVerboseError(error) { - const res = { - response: { - status: 500, - body: `500${error.trim()}`, - headers: { - 'Content-Type': 'application/xml', - }, - }, - }; - return res; -} +const { setdefault } = require('@adobe/helix-shared').types; +const isProduction = require('../utils/is-production'); -function selectStatus(prod) { - return ({ response = {}, error }, { logger }) => { - // if a status is already default, keep it. - if (response.status) { - return {}; - } - if (!error) { - return { - response: { - status: 200, - }, - }; - } - // error handling - logger.debug('payload.error -> 500'); - if (prod) { - return { - response: { - status: 500, - body: '', - }, - }; - } - return setVerboseError(error); - }; -} +function setStatus(context, { logger }) { + const res = setdefault(context, 'response', {}); + const headers = setdefault(res, 'headers', {}); + const err = context.error; + + + // if a status is already default, keep it. + if (res.status) { + return; + } + + if (!err) { + res.status = 200; + return; + } + + logger.debug('payload.error -> 500'); + res.status = 500; + res.body = ''; -function setStatus({ response = {}, error }, { logger }) { - return selectStatus(false)({ response, error }, { logger }); + if (!isProduction()) { + res.body = `500${err.trim()}`; + headers['Content-Type'] = 'application/xml'; + } } module.exports = setStatus; -module.exports.selectStatus = selectStatus; diff --git a/test/testCheckXML.js b/test/testCheckXML.js index f402b3c4e..becee2044 100644 --- a/test/testCheckXML.js +++ b/test/testCheckXML.js @@ -30,7 +30,7 @@ const payload = { describe('Test check-xml', () => { it('validates proper XML', () => { - assert.deepEqual(check(payload, { logger }), {}); + check(payload, { logger }); }); it('throws error on improper XML', () => { @@ -44,6 +44,6 @@ describe('Test check-xml', () => { it('does nothing with empty response body', () => { payload.response.body = ''; - assert.deepEqual(check(payload, { logger }), {}); + check(payload, { logger }); }); }); diff --git a/test/testConditionalSections.js b/test/testConditionalSections.js index a0f94c86c..820662367 100644 --- a/test/testConditionalSections.js +++ b/test/testConditionalSections.js @@ -13,6 +13,7 @@ const assert = require('assert'); const { Logger } = require('@adobe/helix-shared'); +const { setdefault } = require('@adobe/helix-shared').types; const { pipe } = require('../src/defaults/html.pipe.js'); const { selectstrain, testgroups, pick } = require('../src/utils/conditional-sections'); @@ -72,23 +73,19 @@ const crequest = { }; // return only sections that are not hidden -function nonhidden(section) { - if (section.meta && section.meta) { - return !section.meta.hidden; - } - return true; -} +const nonhidden = section => !section.meta.hidden; describe('Integration Test Section Strain Filtering', () => { it('html.pipe sees only selected section', async () => { const myparams = Object.assign({ strain: 'a' }, params); const result = await pipe( - ({ content }) => { + (context) => { // this is the main function (normally it would be the template function) // but we use it to assert that pre-processing has happened + const { content } = context; logger.debug(`Found ${content.sections.filter(nonhidden).length} nonhidden sections`); assert.equal(content.sections.filter(nonhidden).length, 3); - return { response: { body: content.document.body.innerHTML } }; + setdefault(context, 'response', {}).body = content.document.body.innerHTML; }, { request: crequest, @@ -137,14 +134,14 @@ describe('Unit Test Section Strain Filtering', () => { }, }; const action = { + logger, request: { params: { strain: 'empty', }, }, }; - const result = selectstrain(context, action); - assert.deepStrictEqual(result, {}); + selectstrain(context, action); }); it('Filters sections based on strain', () => { @@ -165,8 +162,8 @@ describe('Unit Test Section Strain Filtering', () => { }, logger, }; - const result = selectstrain(context, action); - assert.equal(result.content.sections.filter(nonhidden).length, 2); + selectstrain(context, action); + assert.equal(context.content.sections.filter(nonhidden).length, 2); }); it('Filters sections based on strain (array)', () => { @@ -187,8 +184,8 @@ describe('Unit Test Section Strain Filtering', () => { }, logger, }; - const result = selectstrain(context, action); - assert.equal(result.content.sections.filter(nonhidden).length, 2); + selectstrain(context, action); + assert.equal(context.content.sections.filter(nonhidden).length, 2); }); it('Keeps sections without a strain', () => { @@ -209,8 +206,8 @@ describe('Unit Test Section Strain Filtering', () => { }, logger, }; - const result = selectstrain(context, action); - assert.equal(result.content.sections.filter(nonhidden).length, 2); + selectstrain(context, action); + assert.equal(context.content.sections.filter(nonhidden).length, 2); }); it('Keeps sections without metadata', () => { @@ -231,8 +228,8 @@ describe('Unit Test Section Strain Filtering', () => { }, logger, }; - const result = selectstrain(context, action); - assert.equal(result.content.sections.filter(nonhidden).length, 2); + selectstrain(context, action); + assert.equal(context.content.sections.filter(nonhidden).length, 2); }); @@ -281,11 +278,9 @@ describe('Unit Test Section Strain Filtering', () => { }, logger, }; - const result = selectstrain(context, action); - assert.equal(result.content.sections.filter(nonhidden).length, 3); - assert.deepEqual(result.content.sections.filter(nonhidden)[0], { - meta: { hidden: false }, - }); + selectstrain(context, action); + assert.equal(context.content.sections.filter(nonhidden).length, 3); + assert.equal(context.content.sections.filter(nonhidden)[0].meta.hidden, false); }); }); @@ -340,13 +335,14 @@ describe('Integration Test A/B Testing', () => { it('html.pipe sees only one variant', async () => { const myparams = Object.assign({ strain: 'default' }, params); const result = await pipe( - ({ content }) => { + (context) => { + const { content } = context; // this is the main function (normally it would be the template function) // but we use it to assert that pre-processing has happened logger.debug(`Found ${content.sections.filter(nonhidden).length} nonhidden sections`); assert.equal(content.sections.filter(nonhidden).length, 3); assert.equal(content.sections.filter(nonhidden)[2].meta.test, 'a'); - return { response: { body: content.document.body.innerHTML } }; + setdefault(context, 'response', {}).body = content.document.body.innerHTML; }, { request: crequest, @@ -390,15 +386,16 @@ Or this one at the same time. let selected = {}; const myparams = Object.assign({ strain }, params); const result = await pipe( - ({ content }) => { - // this is the main function (normally it would be the template function) - // but we use it to assert that pre-processing has happened + (context) => { + const { content } = context; + // this is the main function (normally it would be the template function) + // but we use it to assert that pre-processing has happened logger.debug(`Found ${content.sections.filter(nonhidden).length} nonhidden sections`); assert.equal(content.sections.filter(nonhidden).length, 3); // remember what was selected /* eslint-disable-next-line prefer-destructuring */ selected = content.sections.filter(nonhidden)[2]; - return { response: { body: content.document.body.innerHTML } }; + setdefault(context, 'response', {}).body = content.document.body.innerHTML; }, { request: crequest, diff --git a/test/testDebugTmp.js b/test/testDebugTmp.js index 8d35f6416..1463a18f5 100644 --- a/test/testDebugTmp.js +++ b/test/testDebugTmp.js @@ -15,11 +15,8 @@ const assert = require('assert'); const dumper = require('../src/utils/dump-context.js'); describe('Test Temp Context Dumper', () => { - it('Creates a temp directory', (done) => { - dumper({ foo: 'bar' }, {}, 0).then((file) => { - assert.ok(fs.existsSync(file)); - assert.deepEqual(fs.readJSONSync(file), { foo: 'bar' }); - done(); - }); - }); + it('Creates a temp directory', () => dumper({ foo: 'bar' }, {}, 0).then((file) => { + assert.ok(fs.existsSync(file)); + assert.deepEqual(fs.readJSONSync(file), { foo: 'bar' }); + })); }); diff --git a/test/testDefault.js b/test/testDefault.js index 7e39959cc..bc074873c 100644 --- a/test/testDefault.js +++ b/test/testDefault.js @@ -27,9 +27,9 @@ describe('Testing Default Pipeline', () => { }); it('creates and runs the default pipeline', async () => { - const out = await pipe((payload, action) => ({ - body: `test. payload: ${payload.title} action: ${action.title}`, - }), { + const out = await pipe((payload, action) => { + payload.body = `test. payload: ${payload.title} action: ${action.title}`; + }, { title: 'my payload', }, { title: 'my action', diff --git a/test/testEmbedHandler.js b/test/testEmbedHandler.js index d2528389c..7639b1e5d 100644 --- a/test/testEmbedHandler.js +++ b/test/testEmbedHandler.js @@ -97,7 +97,9 @@ describe('Test Embed Handler', () => { describe('Integration Test with Embeds', () => { it('html.pipe processes embeds', async () => { const result = await pipe( - ({ content }) => ({ response: { status: 201, body: content.document.body.innerHTML } }), + (context) => { + context.response = { status: 201, body: context.content.document.body.innerHTML }; + }, { request: crequest, content: { @@ -128,7 +130,9 @@ Here comes an embed.

it('html.pipe processes internal embeds', async () => { const result = await pipe( - ({ content }) => ({ response: { status: 201, body: content.document.body.innerHTML } }), + (context) => { + context.response = { status: 201, body: context.content.document.body.innerHTML }; + }, { request: crequest, content: { diff --git a/test/testEmitJSON.js b/test/testEmitJSON.js index eef5af9a3..0d41429f5 100644 --- a/test/testEmitJSON.js +++ b/test/testEmitJSON.js @@ -12,6 +12,7 @@ /* eslint-env mocha */ const assert = require('assert'); const { Logger } = require('@adobe/helix-shared'); +const { deepclone } = require('@adobe/helix-shared').types; const emit = require('../src/json/emit-json'); const logger = Logger.getTestLogger({ @@ -35,57 +36,34 @@ const expectedJSON = Object.assign({}, content.json); describe('Test emit-json', () => { it('builds JSON from object', () => { - const output = emit({ content, response }, action); - assert.deepEqual(output.response.body, expectedJSON); + const dat = deepclone({ content, response }); + emit(dat, action); + assert.deepEqual(dat.response.body, expectedJSON); }); it('does nothing if no JSON object specified', () => { - assert.deepEqual(emit({ content: {}, response }, action), {}); - }); - - it('fails gracefully in case of invalid object', () => { - // exclude element with illegal value - assert.deepEqual(emit({ - content: { - json: { - with: { - illegal: function elementValue() {}, - }, - }, - }, - response, - }, action), { - response: { - body: { - with: {}, - }, - }, - }); - // JSON.stringify does not like circular structures - const obj = {}; - obj.a = { b: obj }; - assert.deepEqual(emit({ - content: { - json: { - foo: obj, - }, - }, - response, - }, action), { }); + const dat = deepclone({ content: {}, response }); + emit(dat, action); + assert(dat, { content: {}, response }); }); it('keeps existing response body', () => { response.body = expectedJSON; - assert.deepEqual(emit({ content, response }, action), {}); + const dat = deepclone({ content, response }); + emit(dat, action); + assert.deepEqual(dat, { content, response }); }); it('handles missing response object', () => { - const output = emit({ content }, action); - assert.deepEqual(output.response.body, expectedJSON); + const dat = deepclone({ content }); + emit(dat, action); + assert.deepEqual(dat.response.body, expectedJSON); }); it('handles missing content object', () => { // no content object at all - assert.deepEqual(emit({ response }, action), { }); + const dat = deepclone({ response }); + emit(dat, action); + assert.deepEqual(dat, { response, content: {} }); }); }); diff --git a/test/testEmitXML.js b/test/testEmitXML.js index 0ff034810..b2db17482 100644 --- a/test/testEmitXML.js +++ b/test/testEmitXML.js @@ -12,6 +12,7 @@ /* eslint-env mocha */ const assert = require('assert'); const { Logger } = require('@adobe/helix-shared'); +const { deepclone } = require('@adobe/helix-shared').types; const emit = require('../src/xml/emit-xml'); const logger = Logger.getTestLogger({ @@ -45,33 +46,35 @@ const expectedPrettyXML = '\n\n describe('Test emit-xml', () => { it('builds XML from object', () => { - const output = emit(payload, action); - assert.deepEqual(output.response.body, expectedXML); + const data = deepclone(payload); + emit(data, action); + assert.deepEqual(data.response.body, expectedXML); }); it('builds pretty XML from object', () => { + const data = deepclone(payload); action.secrets.XML_PRETTY = true; - const output = emit(payload, action); - assert.deepEqual(output.response.body, expectedPrettyXML); + emit(data, action); + assert.deepEqual(data.response.body, expectedPrettyXML); action.secrets.XML_PRETTY = false; }); it('does nothing if no XML object specified', () => { - assert.deepEqual(emit({ content: {}, response: {} }, action), {}); - }); - - it('fails gracefully in case of invalid object', () => { - // xml contains unexpected value which will break xmlbuilder-js - assert.deepEqual(emit({ content: { xml: function bla() {} }, response: {} }, action), {}); + const data = { content: {}, response: {} }; + emit(data, action); + assert.deepEqual(data, { content: {}, response: {} }); }); it('keeps existing response body', () => { payload.response.body = expectedXML; - assert.deepEqual(emit(payload, action), {}); + const data = deepclone(payload); + emit(data, action); + assert.deepEqual(data, payload); }); it('handles missing response object', () => { - const output = emit({ content }, action); - assert.deepEqual(output.response.body, expectedXML); + const data = { content }; + emit(data, action); + assert.deepEqual(data.response.body, expectedXML); }); }); diff --git a/test/testFetchMarkdown.js b/test/testFetchMarkdown.js index 1d37fb405..40cc18ace 100644 --- a/test/testFetchMarkdown.js +++ b/test/testFetchMarkdown.js @@ -122,59 +122,65 @@ describe('Test URI parsing and construction', () => { describe('Test invalid input', () => { it('Test for missing owner', async () => { - assert.ok((await fetch( + await assert.rejects(() => (fetch( {}, { request: { params: { repo: 'xdm', ref: 'master', path: 'README.md' } }, logger, }, - )).error); + )), { + name: 'Error', + message: 'Unknown owner, cannot fetch content', + }); }); it('Test for missing repo', async () => { - assert.ok((await fetch( + await assert.rejects(() => (fetch( {}, { request: { params: { ref: 'master', path: 'README.md', owner: 'adobe' } }, logger, }, - )).error); + )), { + name: 'Error', + message: 'Unknown repo, cannot fetch content', + }); }); it('Test for missing path', async () => { - assert.ok((await fetch( + await assert.rejects(() => (fetch( {}, { request: { params: { repo: 'xdm', ref: 'master', owner: 'adobe' } }, logger, }, - )).error); + )), { + name: 'Error', + message: 'Unknown path, cannot fetch content', + }); }); it('Test for missing params', async () => { - assert.ok((await fetch( + await assert.rejects(() => (fetch( {}, { request: {}, logger, }, - )).error); + ))); + }, { + name: 'Error', + message: 'Unknown path, cannot fetch content', }); it('Test for missing request', async () => { - assert.ok((await fetch( + await assert.rejects(() => (fetch( {}, { logger }, - )).error); - }); - - it('Test for error pass-through', () => { - const err = { message: 'this error is mine.' }; - - assert.deepEqual(fetch( - { error: err }, - { logger }, - ), {}); + )), { + name: 'Error', + message: 'Request parameters missing', + }); }); }); @@ -202,8 +208,9 @@ describe('Test non-existing content', () => { }; await coerce(myaction); - - assert.ok((await fetch({}, myaction)).error); + const context = {}; + await fetch(context, myaction); + assert.ok(context.error); }); it('Getting XDM README (with missing ref)', async () => { @@ -217,9 +224,9 @@ describe('Test non-existing content', () => { }; await coerce(myaction); - - const result = await fetch({}, myaction); - assert.ok(result.content.body); + const context = {}; + await fetch(context, myaction); + assert.ok(context.content.body); }); }); @@ -247,10 +254,10 @@ describe('Test requests', () => { }; await coerce(myaction); - - const result = await fetch({}, myaction); - assert.ok(result.content.body); - assert.equal(result.content.body.split('\n')[0], '# Foo Data Model (XDM) Schema'); + const context = {}; + await fetch(context, myaction); + assert.ok(context.content.body); + assert.equal(context.content.body.split('\n')[0], '# Foo Data Model (XDM) Schema'); }); }); @@ -259,6 +266,7 @@ describe('Test misbehaved HTTP Responses', () => { setupPolly({ logging: false, recordFailedRequests: false, + recordIfMissing: false, adapters: [NodeHttpAdapter], persister: FSPersister, persisterOptions: { @@ -285,10 +293,10 @@ describe('Test misbehaved HTTP Responses', () => { }; await coerce(myaction); - - const result = await fetch({}, myaction); - assert.ok(result.error); - assert.equal(result.response.status, 502); + const context = {}; + await fetch(context, myaction); + assert.ok(context.error); + assert.equal(context.response.status, 502); }); it('Getting XDM README with ultra-short Timeout', async function shortTimeout() { @@ -314,16 +322,14 @@ describe('Test misbehaved HTTP Responses', () => { }; await coerce(myaction); - - const result = await fetch({}, myaction); - assert.ok(result.error); - assert.equal(result.response.status, 504); + const context = {}; + await fetch(context, myaction); + assert.ok(context.error); + assert.equal(context.response.status, 504); }); it('Getting XDM README with Backend Timeout', async function badTimeout() { const { server } = this.polly; - - server .get('https://raw.githubusercontent.com/adobe/xdm/master/README.md') .intercept(async (_, res) => { @@ -341,9 +347,9 @@ describe('Test misbehaved HTTP Responses', () => { }; await coerce(myaction); - - const result = await fetch({}, myaction); - assert.ok(result.error); - assert.equal(result.response.status, 504); + const context = {}; + await fetch(context, myaction); + assert.ok(context.error); + assert.equal(context.response.status, 504); }).timeout(3000); }); diff --git a/test/testFindEmbeds.js b/test/testFindEmbeds.js index 571d35471..6b58129dc 100644 --- a/test/testFindEmbeds.js +++ b/test/testFindEmbeds.js @@ -34,13 +34,17 @@ const action = { }; function mdast(body) { - const parsed = parse({ content: { body } }, { logger }); - return embeds({ content: parsed.content, request: { extension: 'html', url: '/docs/index.html' } }, action).content.mdast; + const dat = { content: { body }, request: { extension: 'html', url: '/docs/index.html' } }; + parse(dat, { logger }); + embeds(dat, action); + return dat.content.mdast; } function context(body) { - const parsed = parse({ content: { body } }, { logger }); - return embeds({ content: parsed.content, request: { extension: 'html', url: '/docs/index.html' } }, action); + const dat = { content: { body }, request: { extension: 'html', url: '/docs/index.html' } }; + parse(dat, { logger }); + embeds(dat, action); + return dat; } describe('Test Embed Detection Processing', () => { diff --git a/test/testFrontmatter.js b/test/testFrontmatter.js index 8eb6fe8d5..955242904 100644 --- a/test/testFrontmatter.js +++ b/test/testFrontmatter.js @@ -32,11 +32,11 @@ const logger = winston.createLogger({ }); const procMd = (md) => { - const body = multiline(md); - const { mdast: orig } = parseMd({ content: { body } }, { logger }).content; - const proc = cloneDeep(orig); - parseFront({ content: { mdast: proc, body } }); - return { orig, proc }; + const dat = { content: { body: multiline(md) } }; + parseMd(dat, { logger }); + const orig = cloneDeep(dat.content.mdast); + parseFront(dat); + return { orig, proc: dat.content.mdast }; }; const ck = (wat, md, ast) => { @@ -46,7 +46,6 @@ const ck = (wat, md, ast) => { const nodes = flattenTree(proc, (node, recurse) => concat([node], recurse(node.children || []))); each(nodes, (node) => { - /* eslint-disable-next-line no-param-reassign */ delete node.position; }); assert.deepStrictEqual(proc.children, yaml.safeLoad(ast)); diff --git a/test/testGetMetadata.js b/test/testGetMetadata.js index ca9392e6b..b25fa9fee 100644 --- a/test/testGetMetadata.js +++ b/test/testGetMetadata.js @@ -24,10 +24,12 @@ const logger = Logger.getTestLogger({ }); function callback(body) { - const parsed = parse({ content: { body } }, { logger }); - parseFront({ content: { mdast: parsed.content.mdast, body } }); - const splitted = split(parsed, { logger }); - return getmetadata(splitted, { logger }).content.sections; + const dat = { content: { body } }; + parse(dat, { logger }); + parseFront(dat); + split(dat, { logger }); + getmetadata(dat, { logger }); + return dat.content.sections; } const SECTIONS_BLOCS = [ @@ -51,63 +53,62 @@ describe('Test getMetadata', () => { }); it('getmetadata does not fail with "empty" mdast', () => { - assert.deepEqual( - getmetadata({ - content: { - sections: [], - mdast: { - children: [], - position: {}, - type: '', - }, + const dat = { + content: { + sections: [], + mdast: { + children: [], + position: {}, + type: '', }, - }, { logger }), - { - content: { meta: {} }, }, - ); + }; + getmetadata(dat, { logger }); + assert.deepEqual(dat.content.meta, {}); }); it('getmetadata does not fail with missing sections', () => { - assert.deepEqual( - getmetadata({ - content: { - mdast: { - children: [], - position: {}, - type: '', - }, + const dat = { + content: { + mdast: { + children: [], + position: {}, + type: '', }, - }, { logger }), - { - content: { meta: {} }, }, - ); + }; + getmetadata(dat, { logger }); + assert.deepEqual(dat.content.meta, {}); }); it('getmetadata does not fail with empty sections', () => { - assert.deepEqual( - getmetadata({ - content: { - sections: [{}], - mdast: { - children: [], - position: {}, - type: '', - }, + const dat = { + content: { + sections: [{}], + mdast: { + children: [], + position: {}, + type: '', }, - }, { logger }), - - { - content: - { - sections: [{ meta: {}, types: [] }], - meta: {}, - title: undefined, - intro: undefined, - image: undefined, - }, }, - ); + }; + getmetadata(dat, { logger }); + assert.deepEqual(dat.content, { + sections: [{ + meta: {}, + title: '', + intro: '', + types: [], + }], + meta: {}, + title: '', + intro: '', + image: undefined, + mdast: { + children: [], + position: {}, + type: '', + }, + }); }); }); diff --git a/test/testHTML.js b/test/testHTML.js index 21f9e17e4..2c936ef2d 100644 --- a/test/testHTML.js +++ b/test/testHTML.js @@ -121,6 +121,7 @@ describe('Testing HTML Pipeline', () => { setupPolly({ logging: false, recordFailedRequests: true, + recordIfMissing: false, adapters: [NodeHttpAdapter], persister: FSPersister, persisterOptions: { @@ -137,12 +138,12 @@ describe('Testing HTML Pipeline', () => { it('html.pipe does not make HTTP requests if body is provided', async () => { const result = await pipe( - ({ content }) => { + (context) => { // this is the main function (normally it would be the template function) // but we use it to assert that pre-processing has happened - assert.equal(content.body, 'Hello World'); + assert.equal(context.content.body, 'Hello World'); // and return a different status code - return { response: { status: 201, body: content.document.body.innerHTML } }; + context.response = { status: 201, body: context.content.document.body.innerHTML }; }, { request: crequest, @@ -165,14 +166,14 @@ describe('Testing HTML Pipeline', () => { it('html.pipe keeps proper ESI tags', async () => { const result = await pipe( - ({ content }) => { + (context) => { // this is the main function (normally it would be the template function) // but we use it to assert that pre-processing has happened - assert.equal(content.body, 'Hello World'); + assert.equal(context.content.body, 'Hello World'); // and return a different status code - return { - response: { - status: 201, body: ` + context.response = { + status: 201, + body: ` ESI-Test
@@ -180,7 +181,6 @@ describe('Testing HTML Pipeline', () => {
`, - }, }; }, { @@ -206,14 +206,14 @@ describe('Testing HTML Pipeline', () => { it('html.pipe keeps self-closing ESI tags', async () => { const result = await pipe( - ({ content }) => { + (context) => { // this is the main function (normally it would be the template function) // but we use it to assert that pre-processing has happened - assert.equal(content.body, 'Hello World'); + assert.equal(context.content.body, 'Hello World'); // and return a different status code - return { - response: { - status: 201, body: ` + context.response = { + status: 201, + body: ` ESI-Test
@@ -221,7 +221,6 @@ describe('Testing HTML Pipeline', () => {
`, - }, }; }, { @@ -248,14 +247,14 @@ describe('Testing HTML Pipeline', () => { it('html.pipe keeps double ESI tags', async () => { const result = await pipe( - ({ content }) => { + (context) => { // this is the main function (normally it would be the template function) // but we use it to assert that pre-processing has happened - assert.equal(content.body, 'Hello World'); + assert.equal(context.content.body, 'Hello World'); // and return a different status code - return { - response: { - status: 201, body: ` + context.response = { + status: 201, + body: ` ESI-Test
@@ -264,7 +263,6 @@ describe('Testing HTML Pipeline', () => {
`, - }, }; }, { @@ -290,12 +288,12 @@ describe('Testing HTML Pipeline', () => { }); it('html.pipe can be extended', async () => { - const myfunc = ({ content }) => ({ - response: { - body: `

${content.title}

-${content.document.body.innerHTML}`, - }, - }); + const myfunc = (context) => { + context.response = { + body: `

${context.content.title}

+${context.content.document.body.innerHTML}`, + }; + }; let calledfoo = false; let calledbar = false; @@ -317,11 +315,7 @@ ${content.document.body.innerHTML}`, } function shouttitle(p) { - return { - content: { - title: `${p.content.title.toUpperCase()}!!!`, - }, - }; + p.content.title = `${p.content.title.toUpperCase()}!!!`; } myfunc.before = { @@ -360,14 +354,15 @@ ${content.document.body.innerHTML}`, it('html.pipe renders index.md from helix-cli correctly', async () => { const result = await pipe( - ({ content }) => { + (context) => { + const { content } = context; // this is the main function (normally it would be the template function) // but we use it to assert that pre-processing has happened assert.equal(content.title, 'Helix - {{project.name}}'); assert.equal(content.image, './helix_logo.png'); assert.equal(content.intro, 'It works! {{project.name}} is up and running.'); // and return a different status code - return { response: { status: 201, body: content.document.body.innerHTML } }; + context.response = { status: 201, body: content.document.body.innerHTML }; }, { request: crequest, @@ -387,14 +382,15 @@ ${content.document.body.innerHTML}`, it('html.pipe renders index.md from project-helix.io correctly', async () => { const result = await pipe( - ({ content }) => { + (context) => { + const { content } = context; // this is the main function (normally it would be the template function) // but we use it to assert that pre-processing has happened assert.equal(content.title, 'Welcome to Project Helix'); assert.equal(content.image, 'assets/browser.png'); assert.equal(content.intro, 'Helix is the new experience management service to create, manage, and deliver great digital experiences.'); // and return a different status code - return { response: { status: 201, body: content.document.body.innerHTML } }; + context.response = { status: 201, body: content.document.body.innerHTML }; }, { request: crequest, @@ -414,14 +410,15 @@ ${content.document.body.innerHTML}`, it('html.pipe renders modified index.md from helix-cli correctly', async () => { const result = await pipe( - ({ content }) => { + (context) => { + const { content } = context; // this is the main function (normally it would be the template function) // but we use it to assert that pre-processing has happened assert.equal(content.title, 'Helix - {{project.name}}'); assert.equal(content.image, undefined); assert.equal(content.intro, 'It works! {{project.name}} is up and running.'); // and return a different status code - return { response: { status: 201, body: content.document.body.innerHTML } }; + context.response = { status: 201, body: content.document.body.innerHTML }; }, { request: crequest, @@ -441,7 +438,9 @@ ${content.document.body.innerHTML}`, it('html.pipe complains when context is invalid', async () => { const result = await pipe( - ({ content }) => ({ response: { status: 201, body: content.document.body.innerHTML } }), + (context) => { + context.response = { status: 201, body: context.content.document.body.innerHTML }; + }, { request: crequest, content: { @@ -454,9 +453,9 @@ ${content.document.body.innerHTML}`, logger, }, ); - assert.ok(result.error, 'no error reported'); - assert.equal(result.error.split('\n')[1], 'Error: Invalid Context at step 0: '); - assert.equal(result.error.split('\n')[2], '#/additionalProperties should NOT have additional properties - params: "{ additionalProperty: \'foo\' }" - path: .content'); + assert.ok(result.error); + assert(result.error.stack.includes('Error: Invalid Context')); + assert(result.error.stack.includes('additionalProperties should NOT have additional properties')); }); it('html.pipe complains with a specific message for mdast nodes when context is invalid', async () => { @@ -475,14 +474,15 @@ ${content.document.body.innerHTML}`, }, ); assert.ok(result.error); - assert.equal(result.error.split('\n')[1], 'Error: Invalid Context at step 0: '); - assert.equal(result.error.split('\n')[2], '#/properties/type/const should be equal to constant - params: "{ allowedValue: \'root\' }" - value: notroot - path: .content.sections[0].type'); + assert(result.error.stack.includes('Error: Invalid Context')); + assert(result.error.stack.includes('should be equal to constant')); }); it('html.pipe complains with a specific message for mdast nodes wih extra properties when context is invalid', async () => { const result = await pipe( - ({ content }) => ({ response: { status: 201, body: content.document.body.innerHTML } }), - { + (context) => { + context.response = { status: 201, body: context.content.document.body.innerHTML }; + }, { request: crequest, content: { sections: [{ type: 'root', custom: 'notallowed' }], @@ -495,14 +495,15 @@ ${content.document.body.innerHTML}`, }, ); assert.ok(result.error); - assert.equal(result.error.split('\n')[1], 'Error: Invalid Context at step 0: '); - assert.equal(result.error.split('\n')[2], 'root#/additionalProperties should NOT have additional properties - path: .content.sections[0]'); + assert(result.error.stack.includes('Error: Invalid Context')); + assert(result.error.stack.includes('additionalProperties should NOT have additional properties')); }); it('html.pipe complains when action is invalid', async () => { const result = await pipe( - ({ content }) => ({ response: { status: 201, body: content.document.body.innerHTML } }), - { + (context) => { + context.response = { status: 201, body: context.content.document.body.innerHTML }; + }, { request: crequest, content: { body: 'Hello World', @@ -516,13 +517,14 @@ ${content.document.body.innerHTML}`, }, ); assert.ok(result.error); - assert.equal(result.error.split('\n')[1], 'Error: Invalid Action at step 0: '); - assert.equal(result.error.split('\n')[2], '#/additionalProperties should NOT have additional properties - params: "{ additionalProperty: \'break\' }" - path: '); + assert(result.error.stack.includes('Error: Invalid Action')); + assert(result.error.stack.includes('additionalProperties should NOT have additional properties')); }); it('html.pipe makes HTTP requests', async () => { const result = await pipe( - ({ content }) => { + (context) => { + const { content } = context; // this is the main function (normally it would be the template function) // but we use it to assert that pre-processing has happened assert.ok(content.body); @@ -537,7 +539,7 @@ ${content.document.body.innerHTML}`, assert.equal(content.title, 'Bill, Welcome to the future'); assert.deepEqual(content.sources, ['https://raw.githubusercontent.com/trieloff/soupdemo/master/hello.md']); // and return a different status code - return { response: { status: 201, body: content.document.body.innerHTML } }; + context.response = { status: 201, body: content.document.body.innerHTML }; }, { request: { @@ -566,7 +568,8 @@ ${content.document.body.innerHTML}`, delete myparams.ref; const result = await pipe( - ({ content }) => { + (context) => { + const { content } = context; // this is the main function (normally it would be the template function) // but we use it to assert that pre-processing has happened assert.ok(content.body); @@ -581,7 +584,7 @@ ${content.document.body.innerHTML}`, assert.equal(content.title, 'Bill, Welcome to the future'); assert.deepEqual(content.sources, ['https://raw.githubusercontent.com/trieloff/soupdemo/master/hello.md']); // and return a different status code - return { response: { status: 201, body: content.document.body.innerHTML } }; + context.response = { status: 201, body: content.document.body.innerHTML }; }, { request: { @@ -628,17 +631,17 @@ ${content.document.body.innerHTML}`, it('html.pipe keeps existing headers', async () => { const result = await pipe( - ({ content }) => ({ - response: { + (context) => { + context.response = { status: 201, - body: content.document.body.innerHTML, + body: context.content.document.body.innerHTML, headers: { 'Content-Type': 'text/plain', 'Surrogate-Key': 'foobar', 'Cache-Control': 'max-age=0', }, - }, - }), + }; + }, { request: crequest }, { request: { params }, @@ -655,16 +658,16 @@ ${content.document.body.innerHTML}`, it('html.pipe produces debug dumps', async () => { const result = await pipe( - ({ content }) => ({ - response: { + (context) => { + context.response = { status: 201, - body: content.document.body.innerHTML, + body: context.content.document.body.innerHTML, headers: { 'Content-Type': 'text/plain', 'Surrogate-Key': 'foobar', }, - }, - }), + }; + }, { request: crequest }, { request: { params }, diff --git a/test/testHTMLProduction.js b/test/testHTMLProduction.js index 15cd8329b..54ca5b1e3 100644 --- a/test/testHTMLProduction.js +++ b/test/testHTMLProduction.js @@ -82,8 +82,8 @@ describe('Testing HTML Pipeline in Production', () => { it('html.pipe adds headers from meta and link tags', async () => { const result = await pipe( - ({ content }) => ({ - response: { + (context) => { + context.response = { status: 201, headers: { Foo: 'bar', @@ -100,11 +100,11 @@ describe('Testing HTML Pipeline in Production', () => { - ${content.document.body.innerHTML} + ${context.content.document.body.innerHTML} `, - }, - }), + }; + }, { request: crequest, content: { diff --git a/test/testHelper.js b/test/testHelper.js deleted file mode 100644 index a1dfb4cba..000000000 --- a/test/testHelper.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2018 Adobe. All rights reserved. - * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. You may obtain a copy - * of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - * OF ANY KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ -/* eslint-env mocha */ - -const assert = require('assert'); -const { Logger } = require('@adobe/helix-shared'); -const helper = require('../src/helper'); - -const logger = Logger.getTestLogger({ - // tune this for debugging - level: 'info', -}); - -describe('Test bail', () => { - it('Bail returns an error', () => { - assert.equal(helper.bail(logger, 'This is bad').error.message, 'This is bad'); - }); - - it('Bail logs something', (done) => { - helper.bail({ error: () => done() }, 'This is bad'); - }); -}); diff --git a/test/testJSON.js b/test/testJSON.js index b641d0fd5..62336f0af 100644 --- a/test/testJSON.js +++ b/test/testJSON.js @@ -12,6 +12,7 @@ /* eslint-env mocha */ const assert = require('assert'); const { Logger } = require('@adobe/helix-shared'); +const { setdefault } = require('@adobe/helix-shared').types; const NodeHttpAdapter = require('@pollyjs/adapter-node-http'); const FSPersister = require('@pollyjs/persister-fs'); const setupPolly = require('@pollyjs/core').setupMocha; @@ -88,7 +89,9 @@ describe('Testing JSON Pipeline', () => { it('json.pipe makes HTTP requests', async () => { const result = await pipe( - ({ content }) => { + (context) => { + const { content } = context; + const res = setdefault(context, 'response', {}); // this is the main function (normally it would be the template function) // but we use it to assert that pre-processing has happened assert.ok(content.body); @@ -98,7 +101,8 @@ describe('Testing JSON Pipeline', () => { assert.equal(content.intro, 'Project Helix'); assert.equal(content.title, 'Bill, Welcome to the future'); // and return a different status code - return { response: { status: 201, body: { foo: 'bar' } } }; + res.status = 201; + res.body = { foo: 'bar' }; }, {}, { @@ -115,7 +119,9 @@ describe('Testing JSON Pipeline', () => { it('json.pipe keeps Mime-Type', async () => { const result = await pipe( - ({ content }) => { + (context) => { + const { content } = context; + const res = setdefault(context, 'response', {}); // this is the main function (normally it would be the template function) // but we use it to assert that pre-processing has happened assert.ok(content.body); @@ -125,7 +131,9 @@ describe('Testing JSON Pipeline', () => { assert.equal(content.intro, 'Project Helix'); assert.equal(content.title, 'Bill, Welcome to the future'); // and return a different status code - return { response: { status: 201, body: { foo: 'bar' }, headers: { 'Content-Type': 'text/plain+json' } } }; + res.status = 201; + res.body = { foo: 'bar' }; + setdefault(res, 'headers', {})['Content-Type'] = 'text/plain+json'; }, {}, { @@ -141,15 +149,13 @@ describe('Testing JSON Pipeline', () => { }); it('json.pipe can be extended', async () => { - const myfunc = ({ content }) => ({ - content: { - json: { - root: { - title: content.title, - }, + const myfunc = (context) => { + context.content.json = { + root: { + title: context.content.title, }, - }, - }); + }; + }; let calledfoo = false; let calledbar = false; @@ -174,7 +180,6 @@ describe('Testing JSON Pipeline', () => { const { content } = p; const { title } = content.json.root; content.json.root.title = `${title.toUpperCase()}!!!`; - return { content }; } myfunc.before = { diff --git a/test/testOutputDebug.js b/test/testOutputDebug.js index 19d719391..6ce755b1c 100644 --- a/test/testOutputDebug.js +++ b/test/testOutputDebug.js @@ -12,6 +12,7 @@ /* eslint-env mocha */ const assert = require('assert'); const { Logger } = require('@adobe/helix-shared'); +const { deepclone } = require('@adobe/helix-shared').types; const _ = require('lodash'); const debug = require('../src/html/output-debug'); @@ -46,19 +47,22 @@ describe('Test outputDebug', () => { it('Testing no debug', () => { const payload = getPayload(); payload.request.params.debug = false; - assert.deepEqual(debug(payload, { logger }), payload); + debug(payload, { logger }); + assert.deepEqual(payload, deepclone(payload)); }); it('Testing simple payload', () => { const payload = getPayload(); const expected = computeExpectedOutput(payload); - assert.deepEqual(debug(payload, { logger }), expected); + debug(payload, { logger }); + assert.deepEqual(payload, expected); }); it('Testing upper case body tag', () => { const payload = getPayload(); payload.response.body = payload.response.body.toUpperCase(); const expected = computeExpectedOutput(payload); - assert.deepEqual(debug(payload, { logger }), expected); + debug(payload, { logger }); + assert.deepEqual(payload, expected); }); }); diff --git a/test/testParseMarkdown.js b/test/testParseMarkdown.js index 5542ef758..68b2e59a1 100644 --- a/test/testParseMarkdown.js +++ b/test/testParseMarkdown.js @@ -21,9 +21,10 @@ const logger = Logger.getTestLogger({ }); function callback(body) { - const { mdast } = parse({ content: { body } }, { logger }).content; - parseFront({ content: { mdast, body } }); - return mdast; + const dat = { content: { body } }; + parse(dat, { logger }); + parseFront(dat); + return dat.content.mdast; } describe('Test Markdown Parsing', () => { diff --git a/test/testPipeline.js b/test/testPipeline.js index 8c608543d..05d440754 100644 --- a/test/testPipeline.js +++ b/test/testPipeline.js @@ -14,30 +14,31 @@ const assert = require('assert'); const { Logger } = require('@adobe/helix-shared'); const { Pipeline } = require('../index.js'); -const logger = Logger.getTestLogger({ - // tune this for debugging - level: 'info', -}); +/* eslint-disable array-callback-return */ +let logger; describe('Testing Pipeline', () => { - it('Executes without logger', (done) => { - new Pipeline().run().then(() => done()).catch(done); + beforeEach(() => { + logger = Logger.getTestLogger({ + // tune this for debugging + level: 'info', + }); + }); + + it('Executes without logger', async () => { + await new Pipeline().once(() => {}).run({}); }); - it('Executes correct order', (done) => { + it('Executes correct order', async () => { const order = []; - new Pipeline({ logger }) + await new Pipeline({ logger }) .before(() => { order.push('pre0'); }) .after(() => { order.push('post0'); }) .before(() => { order.push('pre1'); }) .after(() => { order.push('post1'); }) .once(() => { order.push('once'); }) - .run() - .then(() => { - assert.deepEqual(order, ['pre0', 'pre1', 'once', 'post0', 'post1']); - done(); - }) - .catch(done); + .run(); + assert.deepEqual(order, ['pre0', 'pre1', 'once', 'post0', 'post1']); }); it('Can be run twice', async () => { @@ -77,7 +78,6 @@ describe('Testing Pipeline', () => { // inject explicit extension points [first, second, third, fourth].forEach((f) => { - // eslint-disable-next-line no-param-reassign f.ext = f.name; }); @@ -116,7 +116,6 @@ describe('Testing Pipeline', () => { // inject explicit extension points [first, second, third, fourth].forEach((f) => { - // eslint-disable-next-line no-param-reassign f.ext = f.name; }); @@ -141,18 +140,13 @@ describe('Testing Pipeline', () => { assert.deepStrictEqual(order, ['one', 'two', 'middle', 'three', 'four']); }); - it('Logs correct names', (done) => { + it('Logs correct names', async () => { const order = []; - const pre0 = () => order.push('pre0'); - const post0 = function post0() { - order.push('post0'); - }; - + const post0 = () => order.push('post0'); function noOp() {} let counter = 0; - const validatinglogger = { error: noOp, warn: noOp, @@ -167,33 +161,20 @@ describe('Testing Pipeline', () => { assert.ok(obj.function.match(/^before:pre0/)); } if (counter === 2) { - assert.ok(obj.function.match(/^before:pre0/)); - } - if (counter === 3) { assert.ok(obj.function.match(/^once:anonymous/)); } - if (counter === 4) { - assert.ok(obj.function.match(/^once:anonymous/)); - } - if (counter === 5) { - assert.ok(obj.function.match(/^after:post0/)); - } - if (counter === 6) { + if (counter === 3) { assert.ok(obj.function.match(/^after:post0/)); } }, }; - new Pipeline({ logger: validatinglogger }) + await new Pipeline({ logger: validatinglogger }) .before(pre0) .after(post0) .once(() => { order.push('once'); }) - .run() - .then(() => { - assert.deepEqual(order, ['pre0', 'once', 'post0']); - done(); - }) - .catch(done); + .run(); + assert.deepEqual(order, ['pre0', 'once', 'post0']); }); it('Disables pre before when', (done) => { @@ -377,50 +358,47 @@ describe('Testing Pipeline', () => { .catch(done); }); - it('Executes promises', (done) => { - const retval = new Pipeline({ logger }) - .after(() => Promise.resolve({ foo: 'bar' })) - .after((v) => { + it('Executes promises', async () => { + await new Pipeline({ logger }) + .once(v => new Promise((resolve) => { + setTimeout(() => { + v.foo = 'bar'; + resolve(); + }, 0.05); + })).after((v) => { assert.equal(v.foo, 'bar'); }).run(); - retval.then((r) => { - assert.equal(r.foo, 'bar'); - done(); - }); }); - it('Executes taps', (done) => { - new Pipeline({ logger }) - .before(() => ({ foo: 'bar' })) - .after(() => ({ bar: 'baz' })) - .every((c, a, i) => { - if (i === 1) { - done(); - } - return true; + it('Executes taps', async () => { + let cnt = 0; + await new Pipeline({ logger }) + .before(() => {}) + .once(() => {}) + .after(() => {}) + .every(() => { + cnt += 1; }) .run(); + assert.strictEqual(cnt, 3); }); - it('Does not executes taps when conditions fail', (done) => { - new Pipeline({ logger }) + it('Does not executes taps when conditions fail', async () => { + let cnt = 0; + await new Pipeline({ logger }) .before(() => ({ foo: 'bar' })) + .once(() => {}) .after(() => ({ bar: 'baz' })) - .every((c, a, i) => { - if (i === 1) { - done(new Error('this is a trap')); - } - return true; + .every(() => { + assert.fail('this should not be invoked'); }) .when(() => false) - .every((c, a, i) => { - if (i === 1) { - done(); - } - return true; + .every(() => { + cnt += 1; }) .when(() => true) .run(); + assert.strictEqual(cnt, 3); }); it('Ignore error if no error', (done) => { @@ -443,9 +421,9 @@ describe('Testing Pipeline', () => { it('skip functions if context.error', (done) => { const order = []; new Pipeline({ logger }) - .before(() => { + .before((ctx) => { order.push('pre0'); - return { error: 'stop' }; + ctx.error = new Error(); }) .before(() => { order.push('pre1'); }) .once(() => { order.push('once'); }) @@ -488,9 +466,9 @@ describe('Testing Pipeline', () => { throw new Error('stop'); }) .before(() => { order.push('pre1'); }) - .error(() => { + .error((ctx) => { order.push('error0'); - return { error: null }; + ctx.error = null; }) .once(() => { order.push('once'); }) .after(() => { order.push('post0'); }) @@ -503,4 +481,39 @@ describe('Testing Pipeline', () => { }) .catch(done); }); + + it('handles error in error', async () => { + const order = []; + await new Pipeline({ logger }) + .before(() => { + order.push('pre0'); + throw new Error('stop'); + }) + .before(() => { order.push('pre1'); }) + .once(() => { order.push('once'); }) + .after(() => { order.push('post0'); }) + .after(() => { order.push('post1'); }) + .error(() => { throw Error('in error handler'); }) + .run(); + const output = await logger.getOutput(); + assert.deepEqual(order, ['pre0']); + assert.ok(output.indexOf('Exception during post-#5/error:anonymous') > 0); + }); + + it('handles generic pipeline error', async () => { + const order = []; + await new Pipeline({ logger }) + .before(() => { order.push('pre1'); }) + .once({ + get errorHandler() { + throw new Error('generic error'); + }, + }) + .error(() => { order.push('error'); }) + .run(); + + const output = await logger.getOutput(); + assert.deepEqual(order, ['pre1']); + assert.ok(output.indexOf('Error: generic error') > 0); + }); }); diff --git a/test/testRewriteStatic.js b/test/testRewriteStatic.js index b2617a4de..db156a43d 100644 --- a/test/testRewriteStatic.js +++ b/test/testRewriteStatic.js @@ -12,6 +12,7 @@ /* eslint-env mocha */ const assert = require('assert'); const { Logger } = require('@adobe/helix-shared'); +const { deepclone, assertEquals } = require('@adobe/helix-shared').types; const rewrite = require('../src/html/static-asset-links'); const tohast = require('../src/html/html-to-hast'); const stringify = require('../src/html/stringify-hast'); @@ -23,26 +24,18 @@ const logger = Logger.getTestLogger({ }); function rw(content) { - const hastcontext = tohast({ + const ctx = { response: { body: content, headers: { 'Content-Type': 'text/html', }, }, - }); - - const rewritecontext = rewrite({ - response: { - body: content, - hast: hastcontext.response.hast, - headers: { - 'Content-Type': 'text/html', - }, - }, - }); - - return stringify(rewritecontext).response.body; + }; + tohast(ctx); + rewrite(ctx); + stringify(ctx); + return ctx.response.body; } describe('Integration Test Static Asset Rewriting', () => { @@ -73,28 +66,28 @@ describe('Integration Test Static Asset Rewriting', () => { }, logger, }; - const once = ({ content }) => ({ - response: { + const once = (ctx) => { + ctx.response = { status: 200, body: ` - ${content.document.body.textContent} + ${ctx.content.document.body.textContent} - ${content.document.body.innerHTML} + ${ctx.content.document.body.innerHTML} `, - }, - }); + }; + }; - const res = await pipe(once, context, action); + await pipe(once, context, action); - assert.equal(res.response.body, ` + assert.equal(context.response.body, ` Hello World @@ -116,14 +109,17 @@ describe('Integration Test Static Asset Rewriting', () => { describe('Test Static Asset Rewriting', () => { it('Ignores non-HTML', () => { - assert.deepEqual(rewrite({ + const dat = { response: { body: '{}', headers: { 'Content-Type': 'application/json', }, }, - }), {}); + }; + const dat2 = deepclone(dat); + rewrite(dat); + assertEquals(dat, dat2); }); it('Load simple HTML', async () => { diff --git a/test/testSetContentType.js b/test/testSetContentType.js index b6f2baceb..4a7083100 100644 --- a/test/testSetContentType.js +++ b/test/testSetContentType.js @@ -33,18 +33,13 @@ describe('Test set-content-type', () => { }); it('sets a content type', () => { - assert.deepEqual( - type('text/html')({}, { logger }), - { - response: { - headers: { - 'Content-Type': 'text/html', - }, - }, - }, - ); + const data = {}; + type('text/html')(data, { logger }); + assert.strictEqual(data.response.headers['Content-Type'], 'text/html'); }); + it('keeps existing content type', () => { - assert.deepEqual(type('text/html')(payload, { logger }), payload); + type('text/html')(payload, { logger }); + assert.strictEqual(payload.response.headers['Content-Type'], 'text/plain'); }); }); diff --git a/test/testSetStatus.js b/test/testSetStatus.js index 13f00f9ec..0147ead44 100644 --- a/test/testSetStatus.js +++ b/test/testSetStatus.js @@ -12,7 +12,19 @@ /* eslint-env mocha */ const assert = require('assert'); const { Logger } = require('@adobe/helix-shared'); -const status = require('../src/html/set-status.js'); +const selectStatus_ = require('../src/html/set-status.js'); + +const selectStatus = inProduction => (context, env) => { + // Mocking whether we are in production or not + const old = process.env.__OW_ACTIVATION_ID; + try { + process.env.__OW_ACTIVATION_ID = inProduction ? 'mocked' : ''; + selectStatus_(context, env); + return context; + } finally { + process.env.__OW_ACTIVATION_ID = old; + } +}; const logger = Logger.getTestLogger({ // tune this for debugging @@ -23,51 +35,25 @@ describe('Test set-status', () => { const error = 'oh, no!'; it('sets a verbose 500 for an error in dev', () => { - assert.deepEqual( - status.selectStatus(false)({ content: { html: '' }, error }, { logger }), - { - response: { - status: 500, - headers: { - 'Content-Type': 'text/html', - }, - body: `

500

${error}
`, - }, - }, - ); + const data = selectStatus(false)({ content: { html: '' }, error }, { logger }); + assert.strictEqual(data.response.status, 500); + assert.strictEqual(data.response.body, `

500

${error}
`); + assert.strictEqual(data.response.headers['Content-Type'], 'text/html'); }); it('sets a terse 500 for an error in production', () => { - assert.deepEqual( - status.selectStatus(true)({ content: { html: '' }, error }, { logger }), - { - response: { - status: 500, - body: '', - }, - }, - ); + const data = selectStatus(true)({ content: { html: '' }, error }, { logger }); + assert.strictEqual(data.response.status, 500); + assert.strictEqual(data.response.body, ''); }); it('keeps an existing status', () => { - assert.deepEqual( - status({ - response: { - status: 201, - }, - }, { logger }), - {}, - ); + const data = selectStatus(true)({ response: { status: 201 } }, { logger }); + assert.strictEqual(data.response.status, 201); }); it('sets a 200 if all good', () => { - assert.deepEqual( - status({ content: { html: '' } }, { logger }), - { - response: { - status: 200, - }, - }, - ); + const data = selectStatus(true)({ content: { html: '' } }, { logger }); + assert.strictEqual(data.response.status, 200); }); }); diff --git a/test/testSetXMLStatus.js b/test/testSetXMLStatus.js index c10fbf7f9..fc22e5e97 100644 --- a/test/testSetXMLStatus.js +++ b/test/testSetXMLStatus.js @@ -10,9 +10,21 @@ * governing permissions and limitations under the License. */ /* eslint-env mocha */ -const assert = require('assert'); const { Logger } = require('@adobe/helix-shared'); -const status = require('../src/xml/set-xml-status.js'); +const { assertEquals } = require('@adobe/helix-shared').types; +const selectStatus_ = require('../src/xml/set-xml-status.js'); + +const selectStatus = inProduction => (context, env) => { + // Mocking whether we are in production or not + const old = process.env.__OW_ACTIVATION_ID; + try { + process.env.__OW_ACTIVATION_ID = inProduction ? 'mocked' : ''; + selectStatus_(context, env); + return context; + } finally { + process.env.__OW_ACTIVATION_ID = old; + } +}; const logger = Logger.getTestLogger({ // tune this for debugging @@ -23,58 +35,36 @@ describe('Test set-xml-status', () => { const error = 'oh, no!'; it('sets a verbose 500 for an error in dev', () => { - assert.deepEqual( - status.selectStatus(false)({ content: { }, error }, { logger }), - { - response: { - status: 500, - body: `500${error}`, - headers: { - 'Content-Type': 'application/xml', - }, - }, - }, - ); + const ctx = selectStatus(false)({ content: { }, error }, { logger }); + assertEquals(ctx.response.status, 500); + assertEquals(ctx.response.body, `500${error}`); + assertEquals(ctx.response.headers['Content-Type'], 'application/xml'); }); it('sets a terse 500 for an error in production', () => { - assert.deepEqual( - status.selectStatus(true)({ content: { }, error }, { logger }), - { - response: { - status: 500, - body: '', - }, - }, - ); + const ctx = selectStatus(true)({ content: { }, error }, { logger }); + assertEquals(ctx.response.status, 500); + assertEquals(ctx.response.body, ''); }); it('keeps an existing status', () => { - assert.deepEqual( - status({ - response: { - status: 201, - }, - }, { logger }), - {}, - ); + const ctx = selectStatus(false)({ + response: { + status: 201, + }, + }, { logger }); + assertEquals(ctx.response.status, 201); }); it('sets a 200 if all good', () => { - assert.deepEqual( - status({ - content: { - xml: { - root: {}, - }, - }, - }, - { logger }), - { - response: { - status: 200, + const ctx = selectStatus(false)({ + content: { + xml: { + root: {}, }, }, - ); + }, + { logger }); + assertEquals(ctx.response.status, 200); }); }); diff --git a/test/testSmartypants.js b/test/testSmartypants.js index a66958efd..c566b38f3 100644 --- a/test/testSmartypants.js +++ b/test/testSmartypants.js @@ -21,12 +21,22 @@ const logger = Logger.getTestLogger({ }); function callback(body) { - const parsed = parse({ content: { body } }, { logger }); - return smartypants(parsed, { logger }).content.mdast; + const data = { content: { body } }; + parse(data, { logger }); + smartypants(data, { logger }); + return data.content.mdast; } describe('Test Smartypants Processing', () => { it('Parses markdown with formatting', () => { assertMatch('smartypants', callback); }); + + it('does not throw error if mdast is missing', () => { + smartypants({ + content: { + html: '', + }, + }); + }); }); diff --git a/test/testSplitSections.js b/test/testSplitSections.js index 9b878b096..077022005 100644 --- a/test/testSplitSections.js +++ b/test/testSplitSections.js @@ -22,9 +22,11 @@ const logger = Logger.getTestLogger({ }); function callback(body) { - const parsed = parse({ content: { body } }, { logger }); - parseFront({ content: { mdast: parsed.content.mdast, body } }); - return split(parsed, { logger }).content.sections; + const data = { content: { body } }; + parse(data, { logger }); + parseFront(data); + split(data, { logger }); + return data.content.sections; } describe('Test Section Splitting', () => { diff --git a/test/testStringifyHast.js b/test/testStringifyHast.js index f8502bdde..80a7e30a4 100644 --- a/test/testStringifyHast.js +++ b/test/testStringifyHast.js @@ -15,106 +15,106 @@ const stringify = require('../src/html/stringify-hast'); describe('Testing stringify pipeline step', () => { it('Simple HTML can be transformed', () => { - assert.deepEqual( - stringify({ - response: { - hast: { - type: 'root', - children: [ - { - type: 'element', - tagName: 'html', - properties: {}, - children: [ - { - type: 'element', - tagName: 'head', - properties: {}, - children: [ - { - type: 'text', - value: '\n ', - position: { - start: { line: 2, column: 9, offset: 15 }, - end: { line: 3, column: 5, offset: 20 }, - }, + const data = { + response: { + hast: { + type: 'root', + children: [ + { + type: 'element', + tagName: 'html', + properties: {}, + children: [ + { + type: 'element', + tagName: 'head', + properties: {}, + children: [ + { + type: 'text', + value: '\n ', + position: { + start: { line: 2, column: 9, offset: 15 }, + end: { line: 3, column: 5, offset: 20 }, }, - { - type: 'element', - tagName: 'title', - properties: {}, - children: [ - { - type: 'text', - value: 'Foo', - position: { - start: { line: 3, column: 12, offset: 27 }, - end: { line: 3, column: 15, offset: 30 }, - }, + }, + { + type: 'element', + tagName: 'title', + properties: {}, + children: [ + { + type: 'text', + value: 'Foo', + position: { + start: { line: 3, column: 12, offset: 27 }, + end: { line: 3, column: 15, offset: 30 }, }, - ], - position: { - start: { line: 3, column: 5, offset: 20 }, - end: { line: 3, column: 23, offset: 38 }, }, + ], + position: { + start: { line: 3, column: 5, offset: 20 }, + end: { line: 3, column: 23, offset: 38 }, }, - { - type: 'text', - value: '\n ', - position: { - start: { line: 3, column: 23, offset: 38 }, - end: { line: 4, column: 3, offset: 41 }, - }, + }, + { + type: 'text', + value: '\n ', + position: { + start: { line: 3, column: 23, offset: 38 }, + end: { line: 4, column: 3, offset: 41 }, }, - ], - position: { - start: { line: 2, column: 3, offset: 9 }, - end: { line: 4, column: 10, offset: 48 }, }, + ], + position: { + start: { line: 2, column: 3, offset: 9 }, + end: { line: 4, column: 10, offset: 48 }, }, - { - type: 'text', - value: '\n ', - position: { - start: { line: 4, column: 10, offset: 48 }, - end: { line: 5, column: 3, offset: 51 }, - }, + }, + { + type: 'text', + value: '\n ', + position: { + start: { line: 4, column: 10, offset: 48 }, + end: { line: 5, column: 3, offset: 51 }, }, - { - type: 'element', - tagName: 'body', - properties: {}, - children: [ - { - type: 'text', - value: 'bar\n', - position: { - start: { line: 5, column: 10, offset: 58 }, - end: { line: 6, column: 1, offset: 69 }, - }, + }, + { + type: 'element', + tagName: 'body', + properties: {}, + children: [ + { + type: 'text', + value: 'bar\n', + position: { + start: { line: 5, column: 10, offset: 58 }, + end: { line: 6, column: 1, offset: 69 }, }, - ], - }, - ], - position: { - start: { line: 1, column: 1, offset: 0 }, - end: { line: 6, column: 8, offset: 76 }, + }, + ], }, + ], + position: { + start: { line: 1, column: 1, offset: 0 }, + end: { line: 6, column: 8, offset: 76 }, }, - ], - data: { quirksMode: true }, - position: { - start: { line: 1, column: 1, offset: 0 }, - end: { line: 6, column: 8, offset: 76 }, }, + ], + data: { quirksMode: true }, + position: { + start: { line: 1, column: 1, offset: 0 }, + end: { line: 6, column: 8, offset: 76 }, }, }, - }).response.body, + }, + }; + stringify(data); + assert.deepEqual(data.response.body, ` Foo bar -`, - ); +`); }); }); diff --git a/test/testToHast.js b/test/testToHast.js index a15092b31..5293cdf03 100644 --- a/test/testToHast.js +++ b/test/testToHast.js @@ -15,111 +15,107 @@ const tohast = require('../src/html/html-to-hast'); describe('Testing tohast pipeline step', () => { it('Simple HTML can be transformed', () => { - assert.deepEqual( - tohast({ - response: { - body: ` + const dat = { + response: { + body: ` Foo bar `, - }, - }), - { - response: { - hast: { - type: 'root', - children: [ - { - type: 'element', - tagName: 'html', - properties: {}, - children: [ - { - type: 'element', - tagName: 'head', - properties: {}, - children: [ - { - type: 'text', - value: '\n ', - position: { - start: { line: 2, column: 9, offset: 15 }, - end: { line: 3, column: 5, offset: 20 }, - }, - }, - { - type: 'element', - tagName: 'title', - properties: {}, - children: [ - { - type: 'text', - value: 'Foo', - position: { - start: { line: 3, column: 12, offset: 27 }, - end: { line: 3, column: 15, offset: 30 }, - }, - }, - ], - position: { - start: { line: 3, column: 5, offset: 20 }, - end: { line: 3, column: 23, offset: 38 }, - }, - }, - { - type: 'text', - value: '\n ', - position: { - start: { line: 3, column: 23, offset: 38 }, - end: { line: 4, column: 3, offset: 41 }, - }, - }, - ], - position: { - start: { line: 2, column: 3, offset: 9 }, - end: { line: 4, column: 10, offset: 48 }, - }, + }, + }; + tohast(dat); + + assert.deepEqual(dat.response.hast, { + type: 'root', + children: [ + { + type: 'element', + tagName: 'html', + properties: {}, + children: [ + { + type: 'element', + tagName: 'head', + properties: {}, + children: [ + { + type: 'text', + value: '\n ', + position: { + start: { line: 2, column: 9, offset: 15 }, + end: { line: 3, column: 5, offset: 20 }, }, - { - type: 'text', - value: '\n ', - position: { - start: { line: 4, column: 10, offset: 48 }, - end: { line: 5, column: 3, offset: 51 }, + }, + { + type: 'element', + tagName: 'title', + properties: {}, + children: [ + { + type: 'text', + value: 'Foo', + position: { + start: { line: 3, column: 12, offset: 27 }, + end: { line: 3, column: 15, offset: 30 }, + }, }, + ], + position: { + start: { line: 3, column: 5, offset: 20 }, + end: { line: 3, column: 23, offset: 38 }, }, - { - type: 'element', - tagName: 'body', - properties: {}, - children: [ - { - type: 'text', - value: 'bar\n', - position: { - start: { line: 5, column: 10, offset: 58 }, - end: { line: 6, column: 1, offset: 69 }, - }, - }, - ], + }, + { + type: 'text', + value: '\n ', + position: { + start: { line: 3, column: 23, offset: 38 }, + end: { line: 4, column: 3, offset: 41 }, }, - ], - position: { - start: { line: 1, column: 1, offset: 0 }, - end: { line: 6, column: 8, offset: 76 }, }, + ], + position: { + start: { line: 2, column: 3, offset: 9 }, + end: { line: 4, column: 10, offset: 48 }, + }, + }, + { + type: 'text', + value: '\n ', + position: { + start: { line: 4, column: 10, offset: 48 }, + end: { line: 5, column: 3, offset: 51 }, }, - ], - data: { quirksMode: true }, - position: { - start: { line: 1, column: 1, offset: 0 }, - end: { line: 6, column: 8, offset: 76 }, }, + { + type: 'element', + tagName: 'body', + properties: {}, + children: [ + { + type: 'text', + value: 'bar\n', + position: { + start: { line: 5, column: 10, offset: 58 }, + end: { line: 6, column: 1, offset: 69 }, + }, + }, + ], + }, + ], + position: { + start: { line: 1, column: 1, offset: 0 }, + end: { line: 6, column: 8, offset: 76 }, }, }, + ], + data: { quirksMode: true }, + position: { + start: { line: 1, column: 1, offset: 0 }, + end: { line: 6, column: 8, offset: 76 }, }, - ); + }); }); }); diff --git a/test/testXML.js b/test/testXML.js index 2f037860e..ffd10444f 100644 --- a/test/testXML.js +++ b/test/testXML.js @@ -12,6 +12,7 @@ /* eslint-env mocha */ const assert = require('assert'); const { Logger } = require('@adobe/helix-shared'); +const { setdefault } = require('@adobe/helix-shared').types; const NodeHttpAdapter = require('@pollyjs/adapter-node-http'); const FSPersister = require('@pollyjs/persister-fs'); const setupPolly = require('@pollyjs/core').setupMocha; @@ -138,26 +139,27 @@ describe('Testing XML Pipeline', () => { it('xml.pipe makes HTTP requests', async () => { const result = await pipe( - ({ content }) => { + (context) => { + const cont = context.content; // main function - const c = content; - c.xml = { + cont.xml = { document: { title: { - '#text': content.title, + '#text': cont.title, '@level': 1, }, }, }; // assert that pre-processing has happened - assert.ok(content.body); - assert.ok(content.mdast); - assert.ok(content.meta); - assert.equal(content.meta.template, 'Medium'); - assert.equal(content.intro, 'Project Helix'); - assert.equal(content.title, 'Bill, Welcome to the future'); + assert.ok(cont.body); + assert.ok(cont.mdast); + assert.ok(cont.meta); + assert.equal(cont.meta.template, 'Medium'); + assert.equal(cont.intro, 'Project Helix'); + assert.equal(cont.title, 'Bill, Welcome to the future'); + // and return a different status code - return { content: c, response: { status: 201 } }; + setdefault(context, 'response', {}).status = 201; }, {}, { @@ -173,18 +175,16 @@ describe('Testing XML Pipeline', () => { }); it('xml.pipe can be extended', async () => { - const myfunc = ({ content }) => ({ - content: { - xml: { - document: { - title: { - '#text': content.title, - '@level': 1, - }, + const myfunc = (context) => { + context.content.xml = { + document: { + title: { + '#text': context.content.title, + '@level': 1, }, }, - }, - }); + }; + }; let calledfoo = false; let calledbar = false; @@ -247,7 +247,6 @@ describe('Testing XML Pipeline', () => { { request: { params }, secrets, - logger, }, ); @@ -282,26 +281,23 @@ describe('Testing XML Pipeline', () => { }); it('xml.pipe detects ESI tag in XML object', async () => { - const result = await pipe( - ({ content }) => { - const c = content; - c.xml = { - root: { - 'esi:include': { - '@src': 'foo.xml', - '@xmlns:esi': 'http://www.edge-delivery.org/esi/1.0', - }, + const result = await pipe((context) => { + context.content.xml = { + root: { + 'esi:include': { + '@src': 'foo.xml', + '@xmlns:esi': 'http://www.edge-delivery.org/esi/1.0', }, - }; - return { content: c, response: { status: 201 } }; - }, - {}, - { - request: { params }, - secrets, - logger, - }, - ); + }, + }; + setdefault(context, 'response', {}).status = 201; + }, + {}, + { + request: { params }, + secrets, + logger, + }); assert.equal(result.response.status, 201); assert.equal(result.response.headers['X-ESI'], 'enabled'); });