UNPKG

mcdev

Version:

Accenture Salesforce Marketing Cloud DevTools

488 lines (457 loc) 19.7 kB
import fs from 'fs-extra'; import path from 'node:path'; import { XMLParser } from 'fast-xml-parser'; import { Util } from '../lib/util/util.js'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const projectRootHelper = __dirname.split(path.sep); projectRootHelper.pop(); const projectRoot = projectRootHelper.join(path.sep) + path.sep; const parser = new XMLParser(); const attributeParser = new XMLParser({ ignoreAttributes: false }); /** @type {typeof Util.color} */ const color = Util.color; export const tWarn = `${color.bgYellow}${color.fgBlack}TEST-WARNING${color.reset}`; export const tError = `${color.bgRed}${color.fgBlack}TEST-ERROR${color.reset}`; const loadingFile = 'loading server file:///'; /** * gets mock SOAP metadata for responding * * @param {string} mcdevAction SOAP action * @param {string} type metadata Type * @param {string} mid of Business Unit * @param {object|string} filter likely for customer key * @param {boolean} [QueryAllAccounts] get data from other BUs or not * @returns {Promise.<string>} relevant metadata stringified */ async function loadSOAPRecords(mcdevAction, type, mid, filter, QueryAllAccounts) { type = type[0].toLowerCase() + type.slice(1); const testPath = path.join('test', 'resources', mid.toString(), type, mcdevAction); const filterPath = getFilterPath(filter, QueryAllAccounts); if (await fs.pathExists(testPath + filterPath + '-response.xml')) { console.log(loadingFile + projectRoot + testPath + filterPath + '-response.xml'); // eslint-disable-line no-console return fs.readFile(testPath + filterPath + '-response.xml', { encoding: 'utf8', }); } else if (await fs.pathExists(testPath + '-response.xml')) { if (filterPath) { /* eslint-disable no-console */ console.log( `${tWarn}: You are loading your reponse from ${ testPath + '-response.xml' } instead of the more specific ${ testPath + filterPath + '-response.xml' }. Make sure this is intended` ); /* eslint-enable no-console */ } console.log(loadingFile + projectRoot + testPath + '-response.xml'); // eslint-disable-line no-console return fs.readFile(testPath + '-response.xml', { encoding: 'utf8', }); } /* eslint-disable no-console */ console.log( `${tError}: Please create file ${ filterPath ? testPath + filterPath + '-response.xml or ' : '' }${testPath + '-response.xml'}` ); /* eslint-enable no-console */ // return error process.exitCode = 404; return fs.readFile(path.join('test', 'resources', mcdevAction + '-response.xml'), { encoding: 'utf8', }); } /** * helper for {@link loadSOAPRecords} to get the filter path * * @param {object|string} filter likely for customer key * @param {boolean} [QueryAllAccounts] get data from other BUs or not * @param {number} [shorten] number of characters to shorten filters by to match windows max file length of 256 chars * @returns {string} filterPath value */ function getFilterPath(filter, QueryAllAccounts, shorten) { const filterPath = (typeof filter === 'string' && filter ? '-' + filter : filterToPath(filter, shorten)) + (QueryAllAccounts ? '-QAA' : ''); if ((filterPath + '-response.xml').length > 256) { shorten ||= 10; return getFilterPath(filter, QueryAllAccounts, --shorten); } else { return filterPath; } } /** * main filter to path function * * @param {object} filter main filter object * @param {string} filter.Property field name * @param {string} filter.SimpleOperator string representation of the comparison method * @param {string} filter.Value field value to check for * @param {object} filter.LeftOperand contains a filter object itself * @param {'AND'|'OR'} filter.LogicalOperator string representation of the comparison method * @param {object} filter.RightOperand field value to check for * @param {number} [shorten] number of characters to shorten filters by to match windows max file length of 256 chars * @returns {string} string represenation of the entire filter */ export function filterToPath(filter, shorten) { if (filter) { return '-' + _filterToPath(filter, shorten); } return ''; } /** * helper for filterToPath * * @param {object} filter main filter object * @param {string} filter.Property field name * @param {string} filter.SimpleOperator string representation of the comparison method * @param {string} filter.Value field value to check for * @param {object} filter.LeftOperand contains a filter object itself * @param {'AND'|'OR'} filter.LogicalOperator string representation of the comparison method * @param {object} filter.RightOperand field value to check for * @param {number} [shorten] number of characters to shorten filters by to match windows max file length of 256 chars * @returns {string} string represenation of the entire filter */ function _filterToPath(filter, shorten) { if (filter.Property && filter.SimpleOperator) { let value; if (filter.Value === undefined) { value = ''; } else if (Array.isArray(filter.Value)) { value = shorten ? filter.Value.map((val) => val.slice(0, Math.max(0, shorten))).join(',') : filter.Value.join(','); } else { value = shorten ? filter.Value.slice(0, Math.max(0, shorten)) : filter.Value; } return `${filter.Property}${filter.SimpleOperator.replace('equals', '=')}${value}`; } else if (filter.LeftOperand && filter.LogicalOperator && filter.RightOperand) { return ( _filterToPath(filter.LeftOperand, shorten) + filter.LogicalOperator + _filterToPath(filter.RightOperand, shorten) ); } else { throw new Error('unknown filter type'); } } /** * based on request, respond with different soap data * * @param {object} config mock api request object * @returns {Promise.<Array>} status code plus response in string form */ export const handleSOAPRequest = async (config) => { const jObj = parser.parse(config.data); const fullObj = attributeParser.parse(config.data); let responseXML; switch (config.headers.SOAPAction) { case 'Retrieve': { responseXML = await loadSOAPRecords( config.headers.SOAPAction.toLocaleLowerCase(), jObj.Envelope.Body.RetrieveRequestMsg.RetrieveRequest.ObjectType, jObj.Envelope.Header.fueloauth, jObj.Envelope.Body.RetrieveRequestMsg.RetrieveRequest.Filter, jObj.Envelope.Body.RetrieveRequestMsg.RetrieveRequest.QueryAllAccounts ); break; } case 'Create': { let filter = null; if (fullObj.Envelope.Body.CreateRequest.Objects['@_xsi:type'] === 'DataFolder') { filter = `ContentType=${fullObj.Envelope.Body.CreateRequest.Objects.ContentType},Name=${fullObj.Envelope.Body.CreateRequest.Objects.Name},ParentFolderID=${fullObj.Envelope.Body.CreateRequest.Objects.ParentFolder.ID}`; } responseXML = await loadSOAPRecords( config.headers.SOAPAction.toLocaleLowerCase(), fullObj.Envelope.Body.CreateRequest.Objects['@_xsi:type'], jObj.Envelope.Header.fueloauth, filter ); break; } case 'Update': { responseXML = await loadSOAPRecords( config.headers.SOAPAction.toLocaleLowerCase(), fullObj.Envelope.Body.UpdateRequest.Objects['@_xsi:type'], jObj.Envelope.Header.fueloauth, null ); break; } case 'Configure': { responseXML = await loadSOAPRecords( config.headers.SOAPAction.toLocaleLowerCase(), fullObj.Envelope.Body.ConfigureRequestMsg.Configurations.Configuration[0][ '@_xsi:type' ], jObj.Envelope.Header.fueloauth, null ); break; } case 'Delete': { responseXML = await loadSOAPRecords( config.headers.SOAPAction.toLocaleLowerCase(), fullObj.Envelope.Body.DeleteRequest.Objects['@_xsi:type'], jObj.Envelope.Header.fueloauth, null ); break; } case 'Schedule': { responseXML = await loadSOAPRecords( config.headers.SOAPAction.toLocaleLowerCase(), fullObj.Envelope.Body.ScheduleRequestMsg.Interactions.Interaction['@_xsi:type'], jObj.Envelope.Header.fueloauth, fullObj.Envelope.Body.ScheduleRequestMsg.Interactions.Interaction.ObjectID ); break; } case 'Perform': { responseXML = await loadSOAPRecords( config.headers.SOAPAction.toLocaleLowerCase(), fullObj.Envelope.Body.PerformRequestMsg.Definitions.Definition['@_xsi:type'], jObj.Envelope.Header.fueloauth, fullObj.Envelope.Body.PerformRequestMsg.Definitions.Definition.ObjectID ); break; } default: { throw new Error( `The SOAP Action ${config.headers.SOAPAction} is not supported by test handler` ); } } return [200, responseXML]; }; /** * helper to return soap base URL * * @returns {string} soap URL */ export const soapUrl = 'https://mct0l7nxfq2r988t1kxfy8sc4xxx.soap.marketingcloudapis.com/Service.asmx'; /** * based on request, respond with different soap data * * @param {object} config mock api request object * @returns {Promise.<Array>} status code plus response in string form */ export const handleRESTRequest = async (config) => { try { // check if filtered const urlObj = new URL( config.baseURL + (config.url.startsWith('/') ? config.url.slice(1) : config.url) ); let filterName; let filterBody; if (urlObj.searchParams.get('$filter')) { filterName = urlObj.searchParams.get('$filter').split(' eq ')[1]; } else if (urlObj.searchParams.get('action')) { filterName = urlObj.searchParams.get('action'); } else if (urlObj.searchParams.get('mostRecentVersionOnly')) { filterName = 'mostRecentVersionOnly'; } else if (urlObj.searchParams.get('versionNumber')) { filterName = 'versionNumber'; } else if (urlObj.searchParams.get('id')) { filterName = 'id'; } const testPath = path .join( 'test', 'resources', config.headers.Authorization.replace('Bearer ', ''), urlObj.pathname, config.method + '-response' ) .replace(':', '_'); // replace : with _ for Windows const testPathFilter = filterName ? testPath + '-' + (urlObj.searchParams.get('$filter') || urlObj.searchParams.get('action') || '') .replaceAll(' eq ', '=') .replaceAll(' ', '') + (urlObj.searchParams.get('id') ? 'id=' + urlObj.searchParams.get('id') : '') + (urlObj.searchParams.get('versionNumber') ? 'versionNumber=' + urlObj.searchParams.get('versionNumber') : '') + (urlObj.searchParams.get('mostRecentVersionOnly') ? 'mostRecentVersionOnly=' + urlObj.searchParams.get('mostRecentVersionOnly') : '') : null; if (!testPathFilter && config.method === 'post' && config.data) { const simpleOperators = { equal: '=', in: 'IN' }; const data = JSON.parse(config.data); const myObj = data.query?.rightOperand || data.query; if (myObj) { const op = simpleOperators[myObj.simpleOperator]; filterBody = `${myObj.property}${op}${op === 'IN' ? myObj.value.join(',') : myObj.value}`; } else if (config.url === '/email/v1/category') { const data = JSON.parse(config.data); filterBody = Object.keys(data) .map((key) => `${key}=${data[key]}`) .join(','); } else if (config.url === '/asset/v1/content/assets/') { const data = JSON.parse(config.data); if (data.customerKey) { filterBody = 'key=' + data.customerKey; } } } const testPathFilterBody = filterBody ? testPath + '-' + filterBody : null; if (testPathFilter && (await fs.pathExists(testPathFilter + '.json'))) { // build filter logic to ensure templating works if (filterName) { const response = JSON.parse( await fs.readFile(testPathFilter + '.json', { encoding: 'utf8', }) ); if ( response.items && filterName !== 'mostRecentVersionOnly' && filterName !== 'versionNumber' && filterName !== 'id' ) { response.items = response.items.filter((def) => def.name == filterName); } console.log(loadingFile + projectRoot + testPathFilter + '.json'); // eslint-disable-line no-console return [200, JSON.stringify(response)]; } else { console.log(loadingFile + projectRoot + testPathFilter + '.json'); // eslint-disable-line no-console return [ 200, await fs.readFile(testPathFilter + '.json', { encoding: 'utf8', }), ]; } } else if (testPathFilter && (await fs.pathExists(testPathFilter + '.txt'))) { console.log(loadingFile + projectRoot + testPathFilter + '.txt'); // eslint-disable-line no-console return [ 200, await fs.readFile(testPathFilter + '.txt', { encoding: 'utf8', }), ]; } else if (testPathFilterBody && (await fs.pathExists(testPathFilterBody + '.json'))) { console.log(loadingFile + projectRoot + testPathFilterBody + '.json'); // eslint-disable-line no-console return [ 200, await fs.readFile(testPathFilterBody + '.json', { encoding: 'utf8', }), ]; } else if (testPathFilterBody && (await fs.pathExists(testPathFilterBody + '.txt'))) { console.log(loadingFile + projectRoot + testPathFilterBody + '.txt'); // eslint-disable-line no-console return [ 200, await fs.readFile(testPathFilterBody + '.txt', { encoding: 'utf8', }), ]; } else if (await fs.pathExists(testPath + '.json')) { if (testPathFilter) { /* eslint-disable no-console */ console.log( `${tWarn}: You are loading your reponse from ${ testPath + '.json' } instead of the more specific ${ testPathFilter + '.json' }. Make sure this is intended` ); /* eslint-enable no-console */ } if (testPathFilterBody) { /* eslint-disable no-console */ console.log( `${tWarn}: You are loading your reponse from ${ testPath + '.json' } instead of the more specific ${ testPathFilterBody + '.json' }. Make sure this is intended` ); /* eslint-enable no-console */ } // build filter logic to ensure templating works if ( filterName && filterName !== 'mostRecentVersionOnly' && filterName !== 'versionNumber' && filterName !== 'id' ) { const response = JSON.parse( await fs.readFile(testPath + '.json', { encoding: 'utf8', }) ); response.items = response.items.filter((def) => def.name == filterName); response.count = response.items.length; console.log(loadingFile + projectRoot + testPath + '.json'); // eslint-disable-line no-console return [200, JSON.stringify(response)]; } else { console.log(loadingFile + projectRoot + testPath + '.json'); // eslint-disable-line no-console return [ 200, await fs.readFile(testPath + '.json', { encoding: 'utf8', }), ]; } } else if (await fs.pathExists(testPath + '.txt')) { if (testPathFilter) { /* eslint-disable no-console */ console.log( `${tWarn}: You are loading your reponse from ${ testPath + '.txt' } instead of the more specific ${ testPathFilter + '.txt' }. Make sure this is intended` ); /* eslint-enable no-console */ } if (testPathFilterBody) { /* eslint-disable no-console */ console.log( `${tWarn}: You are loading your reponse from ${ testPath + '.txt' } instead of the more specific ${ testPathFilterBody + '.txt' }. Make sure this is intended` ); /* eslint-enable no-console */ } console.log(loadingFile + projectRoot + testPath + '.txt'); // eslint-disable-line no-console return [ 200, await fs.readFile(testPath + '.txt', { encoding: 'utf8', }), ]; } else { /* eslint-disable no-console */ console.log( `${tError}: Please create file ${testPath}.json/.txt${filterName ? ` or ${testPathFilter}.json/.txt` : testPathFilterBody ? ` or ${testPathFilterBody}.json/.txt` : ''}` ); /* eslint-enable no-console */ process.exitCode = 404; return [ 404, await fs.readFile(path.join('test', 'resources', 'rest404-response.json'), { encoding: 'utf8', }), ]; } } catch (ex) { console.log(ex); // eslint-disable-line no-console return [500, {}]; } }; /** * helper to return rest base URL * * @returns {string} test URL */ export const restUrl = 'https://mct0l7nxfq2r988t1kxfy8sc4xxx.rest.marketingcloudapis.com/';