Skip to content

Commit d40657c

Browse files
committed
add interceptor for http clients
1 parent 08878a1 commit d40657c

File tree

3 files changed

+3622
-3457
lines changed

3 files changed

+3622
-3457
lines changed

integrations/khttp/require.ts

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

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"dependencies": {
4343
"@grpc/grpc-js": "^1.7.1",
4444
"@grpc/proto-loader": "^0.7.3",
45+
"@mswjs/interceptors": "^0.22.7",
4546
"cors": "^2.8.5",
4647
"express": "^4.17.3",
4748
"got": "^11.5.2",

0 commit comments

Comments
 (0)