UNPKG

cumulocity-cypress

Version:
260 lines (259 loc) 10.2 kB
import _ from "lodash"; import { isURL, removeBaseUrlFromRequestUrl } from "../url"; import { toPactAuthObject } from "../auth"; import { isCypressResponse, isPact, toPactRequest, toPactResponse, } from "./c8ypact"; import { C8yDefaultPactRecord, createPactRecord } from "./c8ydefaultpactrecord"; /** * Default implementation of C8yPact. Use C8yDefaultPact.from to create a C8yPact from * a Cypress.Response object, a serialized pact as string or an object implementing the * C8yPact interface. Note, objects implementing the C8yPact interface may not provide * all required functions and properties. */ export class C8yDefaultPact { constructor(records, info, id) { this.recordIndex = 0; this.iteratorIndex = 0; this.requestIndexMap = {}; this.records = records; this.info = info; this.id = id; } /** * Creates a C8yPact from a Cypress.Response object, a serialized pact as string * or an object containing the pact records and info object. Throws an error if * the input can not be converted to a C8yPact. * @param obj The Cypress.Response, string or object to create a pact from. * @param info The C8yPactInfo object containing additional information for the pact. * @param client The optional C8yClient for options and auth information. */ static from(...args) { const obj = args[0]; if (!obj) { throw new Error("Can not create pact from null or undefined."); } if (isCypressResponse(obj)) { const info = args && args.length > 1 ? args[1] : undefined; if (!info) { throw new Error(`Can not create pact from response without C8yPactInfo.`); } const client = args[2]; const r = _.cloneDeep(obj); const pactRecord = new C8yDefaultPactRecord(toPactRequest(r) || {}, toPactResponse(r) || {}, client?._options, client?._auth ? toPactAuthObject(client?._auth) : undefined); removeBaseUrlFromRequestUrl(pactRecord, info.baseUrl); return new C8yDefaultPact([pactRecord], info, info.id); } else { let pact; if (_.isString(obj)) { pact = JSON.parse(obj); } else if (_.isObjectLike(obj)) { pact = obj; } else { throw new Error(`Can not create pact from ${typeof obj}.`); } // required to map the record object to a C8yPactRecord here as this can // not be done in the plugin pact.records = pact.records?.map((record) => { return new C8yDefaultPactRecord(record.request, record.response, record.options || {}, record.auth, record.createdObject); }); const result = new C8yDefaultPact(pact.records, pact.info, pact.id); if (!isPact(result)) { throw new Error(`Invalid pact object. Can not create pact from ${typeof obj}.`); } return result; } } clearRecords() { this.records = []; this.requestIndexMap = {}; this.recordIndex = 0; this.iteratorIndex = 0; } appendRecord(record, skipIfExists = false) { if (skipIfExists) { if (!record.request.url) return false; const matches = this.getRecordsMatchingRequest(record.request); if (matches && !_.isEmpty(matches)) return false; } this.records.push(record); return true; } replaceRecord(record) { const key = this.indexMapKey(record.request, this.info.baseUrl); if (!key) return false; const matches = this.getRecordsMatchingRequest(record.request); if (!matches) { this.appendRecord(record); } else { const currentIndex = Math.max(0, this.getIndexForKey(key)); const match = matches[currentIndex]; if (!match) { this.appendRecord(record); } else { const index = this.records.indexOf(match); if (index >= 0) { this.records[index] = record; this.setIndexForKey(key, currentIndex + 1); } else { return false; } } } return true; } /** * Returns the next pact record or null if no more records are available. * If an id is provided, the record is looked up by requestId or record id * and the cursor is advanced to the position after the matched record. * If no id is provided, the next record by sequential index is returned. */ nextRecord(id) { if (id) { const matches = this.records.filter((r) => r.options?.requestId === id || r.id === id); if (!matches.length) return null; const currentIndex = Math.max(0, this.getIndexForKey(id)); const result = matches[Math.min(currentIndex, matches.length - 1)]; this.requestIndexMap[id] = currentIndex + 1; const recordsIndex = this.records.indexOf(result); if (recordsIndex >= 0) { this.recordIndex = recordsIndex + 1; } return result; } if (this.recordIndex >= this.records.length) { return null; } return this.records[this.recordIndex++]; } currentRecordIndex() { return this.recordIndex; } nextRecordMatchingRequest(request, baseUrl) { if (!request?.url) return null; const key = this.indexMapKey(request, baseUrl); if (!key) return null; const matches = this.getRecordsMatchingRequest(request); if (!matches) return null; const currentIndex = Math.max(0, this.getIndexForKey(key)); const result = matches[Math.min(currentIndex, matches.length - 1)]; this.requestIndexMap[key] = currentIndex + 1; return result; } getIndexForKey(key) { return this.requestIndexMap[key] || -1; } setIndexForKey(key, index) { this.requestIndexMap[key] = index; } indexMapKey(request, baseUrl) { if (!request.url) return undefined; const url = this.normalizeUrl(request.url, undefined, baseUrl); const method = _.lowerCase(request.method || "get"); return `${method}:${url}`; } normalizeUrl(url, parametersToRemove, baseUrl) { const urlObj = isURL(url) ? url : new URL(decodeURIComponent(url), this.info.baseUrl); const p = parametersToRemove || this.info.requestMatching?.ignoreUrlParameters || []; p.forEach((name) => { urlObj.searchParams.delete(name); }); if (!baseUrl) { return decodeURIComponent(urlObj.pathname + urlObj.search + urlObj.hash); } return decodeURIComponent(urlObj.toString()?.replace(this.info.baseUrl, "")?.replace(baseUrl, "")); } matchUrls(url1, url2, baseUrl) { if (!url1 || !url2) return false; const ignoreParameters = this.info.requestMatching?.ignoreUrlParameters || []; const n1 = this.normalizeUrl(url1, ignoreParameters, baseUrl); const n2 = this.normalizeUrl(url2, ignoreParameters, baseUrl); return _.isEqual(n1, n2); } // debugging and test purposes only getRequesIndex(key) { return this.requestIndexMap[key] || 0; } /** * Returns the pact record for the given request or null if no record is found. * Currently only url and method are used for matching. * @param req The request to use for matching. */ getRecordsMatchingRequest(req, baseUrl) { const records = this.records.filter((record) => { return (record.request?.url && req.url && this.matchUrls(record.request.url, req.url, baseUrl) && (req.method != null ? _.lowerCase(req.method) === _.lowerCase(record.request.method) : true)); }); return records.length ? records : null; } /** * Returns an iterator for the pact records to iterate records using `for (const record of pact) {...}`. */ [Symbol.iterator]() { return { next: () => { if (this.iteratorIndex < this.records.length) { return { value: this.records[this.iteratorIndex++], done: false }; } else { return { value: null, done: true }; } }, }; } } export function toSerializablePactRecord(response, options = {}) { const recordOptions = { loggedInUser: options?.loggedInUser, loggedInUserAlias: options?.loggedInUserAlias, authType: options?.authType, }; const record = createPactRecord(response, options?.client, recordOptions); removeBaseUrlFromRequestUrl(record, options.baseUrl); if (options?.modifiedResponse && isCypressResponse(options?.modifiedResponse)) { const modifiedPactRecord = createPactRecord(options.modifiedResponse, options?.client, recordOptions); record.modifiedResponse = modifiedPactRecord.response; } const matchingProperties = ["request", "response"]; const p = _.pick(record, matchingProperties); options?.preprocessor?.apply(p); if (p.request == null) { p.request = {}; } if (p.response == null) { p.response = {}; } const result = { ...p, ..._.omit(record, matchingProperties) }; return result; } export async function toPactSerializableObject(response, info, options = {}) { if (options.baseUrl == null) { options.baseUrl = info.baseUrl; } const record = toSerializablePactRecord(response, options); const pact = new C8yDefaultPact([record], info, info.id); const keysToSave = ["id", "info", "records"]; return { ..._.pick(pact, keysToSave) }; }