Skip to content

Commit

Permalink
Overwriting cy.intercept to save pacts. Saving needs improvements as …
Browse files Browse the repository at this point in the history
…cy.task needs to be bypassed.
  • Loading branch information
thomaswinkler committed Jan 12, 2024
1 parent c6125c9 commit 4c1cf94
Show file tree
Hide file tree
Showing 2 changed files with 249 additions and 8 deletions.
212 changes: 212 additions & 0 deletions lib/commands/intercept.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
const { _ } = Cypress;

const wrapResponseFunction = (fn: any) => {
var that = this;
return function (req: any) {
let startTime: number;
// see Cypress before-request.ts for implementation details of overwritten functions

// wrap continue() function
const reqContinue = req.continue;
const reqContinueFn = (resFn: any) => {
const resWrapperFn = (res: any) => {
if (!res.duration) {
res.duration = Date.now() - startTime;
}
resFn(res);
emitInterceptionEvent(req, res);
};
startTime = Date.now();
reqContinue(resWrapperFn);
};
req.continue = reqContinueFn;

// wrap reply() function
const reqReply = req.reply;
const resReplyFn = function (...args: any[]) {
if (!args || _.isEmpty(args)) {
reqReply(...args);
} else {
const replyOptions: any = {};
if (_.isFunction(args[0])) {
// route handler - not supported as it seems only supported for compatibility
// with old implementation of continue() based on reply()
reqReply(...args);
} else if (
(_.isString(args[0]) || !hasStaticResponseKeys(args[0])) &&
args.length <= 2
) {
replyOptions.body = args[0];
if (args.length > 1) {
replyOptions.headers = args[1];
}
} else if (_.isObjectLike(args[0])) {
if (!hasStaticResponseWithOptionsKeys(args[0])) {
replyOptions.body = args[0];
} else {
_.extend(replyOptions, args[0]);
}
} else if (_.isNumber(args[0])) {
replyOptions.statusCode = args[0];
if (args.length > 1 && !_.isUndefined(args[1])) {
replyOptions.body = args[1];
}
if (args.length > 2 && !_.isUndefined(args[1])) {
replyOptions.headers = args[2];
}
}
reqReply(...args);
emitInterceptionEvent(req, replyOptions);
}
};
req.reply = resReplyFn;

fn(req);
};
};

const wrapResponseObject = (obj: any) => {
return function (req: any) {
req.reply(obj);
emitInterceptionEvent(req, obj);
};
};

const wrapMissingResponse = () => {
return function (req: any) {
req.continue((res: any) => {
res.send();
emitInterceptionEvent(req, res);
});
};
};

Cypress.on("log:intercept", (options) => {
const { req, res } = options;
const cypressResponse: Cypress.Response = {
body: res.body,
url: req.url,
headers: res.headers,
status: res.statusCode,
duration: res.duration,
requestHeaders: req.headers,
requestBody: req.body,
statusText: res.statusMessage,
method: req.method || "GET",
allRequestResponses: [],
isOkStatusCode: res.statusCode >= 200 && res.statusCode < 300,
};

if (Cypress.c8ypact.isRecordingEnabled()) {
// @ts-ignore
Cypress.c8ypact.savePact(cypressResponse, {});
}
});

Cypress.Commands.overwrite("intercept", (originalFn, ...args) => {
const method =
typeof args[0] === "string" && HTTP_METHODS.includes(args[0].toUpperCase())
? args[0]
: undefined;
const matcher = method ? args[1] : args[0];
let response = method ? args[2] : args[1];

let updatedArgs: any[] = [];
if (method) {
updatedArgs.push(method);
}
if (matcher) {
updatedArgs.push(matcher);
}
if (response) {
try {
if (typeof response === "function") {
response = wrapResponseFunction(response);
} else {
response = wrapResponseObject(response);
}
} catch (error) {
console.log(`Failed to intercept response: ${error}`);
}
updatedArgs.push(response);
} else {
response = wrapMissingResponse();
updatedArgs.push(response);
}

// @ts-ignore
return originalFn(...updatedArgs);
});

function emitInterceptionEvent(req: any, res: any) {
Cypress.emit("log:intercept", {
req,
res,
});
}

function hasStaticResponseKeys(obj: any) {
return (
!_.isArray(obj) &&
(_.intersection(_.keys(obj), STATIC_RESPONSE_KEYS).length || _.isEmpty(obj))
);
}

function hasStaticResponseWithOptionsKeys(obj: any) {
return (
!_.isArray(obj) &&
(_.intersection(_.keys(obj), STATIC_RESPONSE_WITH_OPTIONS_KEYS).length ||
_.isEmpty(obj))
);
}

// cypress static response keys, see packages/driver/src/cy/net-stubbing/static-response-utils.ts
const STATIC_RESPONSE_KEYS = [
"body",
"fixture",
"statusCode",
"headers",
"forceNetworkError",
"throttleKbps",
"delay",
"delayMs",
];

const HTTP_METHODS = [
"ACL",
"BIND",
"CHECKOUT",
"CONNECT",
"COPY",
"DELETE",
"GET",
"HEAD",
"LINK",
"LOCK",
"M-SEARCH",
"MERGE",
"MKACTIVITY",
"MKCALENDAR",
"MKCOL",
"MOVE",
"NOTIFY",
"OPTIONS",
"PATCH",
"POST",
"PROPFIND",
"PROPPATCH",
"PURGE",
"PUT",
"REBIND",
"REPORT",
"SEARCH",
"SOURCE",
"SUBSCRIBE",
"TRACE",
"UNBIND",
"UNLINK",
"UNLOCK",
"UNSUBSCRIBE",
];

const STATIC_RESPONSE_WITH_OPTIONS_KEYS = [...STATIC_RESPONSE_KEYS, "log"];
45 changes: 37 additions & 8 deletions lib/pacts/c8ypact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,14 +368,43 @@ function savePact(response: Cypress.Response<any>, client?: C8yClient) {
};
Cypress.c8ypact.preprocessor?.apply(pact);

cy.task(
"c8ypact:save",
{
...pact,
folder,
},
debugLogger()
);
const name = "c8ypact:save";
const data = {
...pact,
folder,
};

// @ts-ignore
const ret = cy.state("commandIntermediateValue");
if (ret) {
// @ts-ignore
const { args, promise } = Cypress.emitMap("command:invocation", {
name: "task",
args: [name, data],
})[0];
new Promise((r) => promise.then(r))
.then(() =>
// @ts-ignore
Cypress.backend("run:privileged", {
commandName: "task",
args,
options: {
task: name,
arg: data,
},
})
)
.catch(() => {});
} else {
cy.task(
"c8ypact:save",
{
...pact,
folder,
},
debugLogger()
);
}
}

function currentPact(): Cypress.Chainable<C8yPact | null> {
Expand Down

0 comments on commit 4c1cf94

Please sign in to comment.