Skip to content

Commit 9490f3f

Browse files
authored
feat: adds hook for node-fetch integration (#35)
1 parent 7a887e7 commit 9490f3f

File tree

2 files changed

+3573
-3398
lines changed

2 files changed

+3573
-3398
lines changed

integrations/node-fetch/require.ts

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
// @ts-ignore
3+
import Hook from "require-in-the-middle";
4+
import { Headers,ResponseInit } from "node-fetch";
5+
import mixin from "merge-descriptors";
6+
import { getExecutionContext } from "../../src/context";
7+
import { Readable } from "stream";
8+
import { ProcessDep, stringToBinary } from "../../src/util";
9+
import { putMocks } from "../../mock/utils";
10+
import { HTTP_EXPORT, V1_BETA1 } from "../../src/keploy";
11+
import { getRequestHeader, getResponseHeader } from "../express/middleware";
12+
import { getReasonPhrase } from "http-status-codes";
13+
import { DataBytes } from "../../proto/services/DataBytes";
14+
import { MockIds } from "../../mock/mock";
15+
16+
// @ts-ignore
17+
Hook(["node-fetch"], function (exported) {
18+
const wrappedFetch = wrappedNodeFetch(exported);
19+
exported = wrappedFetch
20+
return exported;
21+
});
22+
23+
24+
function getHeadersInit(headers: { [k: string]: string[] }): {
25+
[k: string]: string;
26+
} {
27+
const result: { [key: string]: string } = {};
28+
for (const key in headers) {
29+
result[key] = headers[key].join(", ");
30+
}
31+
return result;
32+
}
33+
34+
export function wrappedNodeFetch(fetch:any) {
35+
const fetchFunc = fetch;
36+
async function wrappedFetch(
37+
this: { fetch: (url: any, options: any) => any },
38+
url: any,
39+
options: any
40+
) {
41+
if (
42+
getExecutionContext() == undefined ||
43+
getExecutionContext().context == undefined
44+
) {
45+
console.error("keploy context is not present to mock dependencies");
46+
return;
47+
}
48+
const ctx = getExecutionContext().context;
49+
let resp = new fetch.Response();
50+
let rinit: ResponseInit = {};
51+
const meta = {
52+
name: "node-fetch",
53+
url: url,
54+
options: options,
55+
type: "HTTP_CLIENT",
56+
};
57+
switch (ctx.mode) {
58+
case "record":
59+
resp = await fetchFunc.apply(this, [url, options]);
60+
const clonedResp = resp.clone();
61+
rinit = {
62+
headers: getHeadersInit(clonedResp.headers.raw()),
63+
status: resp.status,
64+
statusText: resp.statusText,
65+
};
66+
const respData: Buffer[] = [];
67+
clonedResp?.body?.on("data", function (chunk: Buffer) {
68+
respData.push(chunk);
69+
});
70+
clonedResp?.body?.on("end", async function () {
71+
const httpMock = {
72+
Version: V1_BETA1,
73+
Name: ctx.testId,
74+
Kind: HTTP_EXPORT,
75+
Spec: {
76+
Metadata: meta,
77+
Req: {
78+
URL: url,
79+
Body: JSON.stringify(options?.body),
80+
Header: getRequestHeader(options?.headers),
81+
Method: options?.method,
82+
// URLParams:
83+
},
84+
Res: {
85+
StatusCode: rinit.status,
86+
Header: getResponseHeader(rinit.headers),
87+
Body: respData.toString(),
88+
},
89+
},
90+
};
91+
// record mocks for unit-test-mock-library
92+
if (ctx.fileExport === true) {
93+
MockIds[ctx.testId] !== true ? putMocks(httpMock) : "";
94+
} else {
95+
ctx.mocks.push(httpMock);
96+
// ProcessDep(meta, [respData, rinit]);
97+
const res: DataBytes[] = [];
98+
// for (let i = 0; i < outputs.length; i++) {
99+
res.push({ Bin: stringToBinary(JSON.stringify(respData)) });
100+
res.push({ Bin: stringToBinary(JSON.stringify(rinit)) });
101+
// }
102+
ctx.deps.push({
103+
Name: meta.name,
104+
Type: meta.type,
105+
Meta: meta,
106+
Data: res,
107+
});
108+
}
109+
});
110+
break;
111+
case "test":
112+
const outputs = new Array(2);
113+
if (
114+
ctx.mocks != undefined &&
115+
ctx.mocks.length > 0 &&
116+
ctx.mocks[0].Kind == HTTP_EXPORT
117+
) {
118+
const header: { [key: string]: string[] } = {};
119+
for (const k in ctx.mocks[0].Spec?.Res?.Header) {
120+
header[k] = ctx.mocks[0].Spec?.Res?.Header[k]?.Value;
121+
}
122+
outputs[1] = {
123+
headers: getHeadersInit(header),
124+
status: ctx.mocks[0].Spec.Res.StatusCode,
125+
statusText: getReasonPhrase(ctx.mocks[0].Spec.Res.StatusCode),
126+
};
127+
outputs[0] = [ctx.mocks[0].Spec.Res.Body];
128+
if (ctx?.fileExport) {
129+
console.log(
130+
"🤡 Returned the mocked outputs for Http dependency call with meta: ",
131+
meta
132+
);
133+
}
134+
ctx.mocks.shift();
135+
} else {
136+
ProcessDep({}, outputs);
137+
}
138+
rinit.headers = new Headers(outputs[1].headers);
139+
rinit.status = outputs[1].status;
140+
rinit.statusText = outputs[1].statusText;
141+
const buf: Buffer[] = [];
142+
outputs[0].map((el: any) => {
143+
buf.push(Buffer.from(el));
144+
});
145+
resp = new fetch.Response(Readable.from(buf), rinit);
146+
break;
147+
case "off":
148+
return fetchFunc.apply(this, [url, options]);
149+
default:
150+
console.debug(
151+
"mode is not valid. Please set valid keploy mode using env variables"
152+
);
153+
return fetchFunc.apply(this, [url, options]);
154+
}
155+
return resp;
156+
}
157+
return mixin(wrappedFetch, fetchFunc, false);
158+
}
159+

0 commit comments

Comments
 (0)