mockttp
Version:
Mock HTTP server for testing HTTP clients and stubbing webservices
504 lines (467 loc) • 19.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.MockttpAdminRequestBuilder = void 0;
const buffer_1 = require("buffer");
const _ = require("lodash");
const graphql_tag_1 = require("graphql-tag");
const header_utils_1 = require("../util/header-utils");
const mocked_endpoint_client_1 = require("./mocked-endpoint-client");
const rule_serialization_1 = require("../rules/rule-serialization");
const body_serialization_1 = require("../serialization/body-serialization");
const util_1 = require("@httptoolkit/util");
function normalizeHttpMessage(message, event) {
if (message.timingEvents) {
// Timing events are serialized as raw JSON
message.timingEvents = JSON.parse(message.timingEvents);
}
if (message.rawHeaders) {
message.rawHeaders = JSON.parse(message.rawHeaders);
// We use raw headers where possible to derive headers, instead of using any pre-derived
// header data, for maximum accuracy (and to avoid any need to query for both).
message.headers = (0, header_utils_1.rawHeadersToObject)(message.rawHeaders);
}
if (message.rawTrailers) {
message.rawTrailers = JSON.parse(message.rawTrailers);
message.trailers = (0, header_utils_1.rawHeadersToObject)(message.rawTrailers);
}
else if (message.rawHeaders && message.body) { // HTTP events with bodies should have trailers
message.rawTrailers = [];
message.trailers = {};
}
if (message.body !== undefined) {
// This will be unset if a) no decoding is required (so message.body is already decoded implicitly),
// b) if messageBodyDecoding is set to 'none', or c) if the server is <v4 and doesn't do decoding.
let { decoded, decodingError } = message.decodedBody || {};
message.body = (0, body_serialization_1.deserializeBodyReader)(message.body, decoded, decodingError, message.headers);
}
delete message.decodedBody;
if (event?.startsWith('tls-')) {
// TLS passthrough & error events should have raw JSON socket metadata:
if (message.tlsMetadata) {
message.tlsMetadata = JSON.parse(message.tlsMetadata);
}
else {
// For old servers, just use empty metadata:
message.tlsMetadata = {};
}
}
}
function normalizeWebSocketMessage(message) {
// Timing events are serialized as raw JSON
message.timingEvents = JSON.parse(message.timingEvents);
// Content is serialized as the raw encoded buffer in base64
message.content = buffer_1.Buffer.from(message.content, 'base64');
}
/**
* This is part of Mockttp's experimental 'pluggable admin' API. This may change
* unpredictably, even in minor releases.
*
* @internal
*/
class MockttpAdminRequestBuilder {
constructor(schema, options = { messageBodyDecoding: 'server-side' }) {
this.schema = schema;
this.getEndpointDataGetter = (adminClient, ruleId) => async () => {
let result = await adminClient.sendQuery({
query: (0, graphql_tag_1.default) `
query GetEndpointData($id: ID!) {
mockedEndpoint(id: $id) {
seenRequests {
id,
protocol,
method,
url,
path,
hostname
rawHeaders
body
${this.schema.typeHasField('Request', 'decodedBody') && this.messageBodyDecoding === 'server-side'
? 'decodedBody { decoded, decodingError }'
: ''}
timingEvents
httpVersion
}
isPending
}
}
`,
variables: { id: ruleId }
});
const mockedEndpoint = result.mockedEndpoint;
if (!mockedEndpoint)
return null;
mockedEndpoint.seenRequests.forEach(req => normalizeHttpMessage(req));
return mockedEndpoint;
};
this.messageBodyDecoding = options.messageBodyDecoding;
}
buildAddRulesQuery(type, rules, reset, adminStream) {
const ruleTypeName = type === 'http'
? ''
: type === 'ws'
? 'WebSocket'
: (0, util_1.unreachableCheck)(type);
const requestName = (reset ? 'Set' : 'Add') + ruleTypeName + 'Rules';
const mutationName = (reset ? 'set' : 'add') + ruleTypeName + 'Rules';
// Backward compatibility for old servers that don't support steps:
const supportsSteps = this.schema.typeHasInputField('MockRule', 'steps');
const serializedRules = rules.map((rule) => (0, rule_serialization_1.serializeRuleData)(rule, adminStream, { supportsSteps }));
return {
query: (0, graphql_tag_1.default) `
mutation ${requestName}($newRules: [${ruleTypeName}MockRule!]!) {
endpoints: ${mutationName}(input: $newRules) {
id,
explanation
}
}
`,
variables: {
newRules: serializedRules
},
transformResponse: (response, { adminClient }) => {
return response.endpoints.map(({ id, explanation }) => new mocked_endpoint_client_1.MockedEndpointClient(id, explanation, this.getEndpointDataGetter(adminClient, id)));
}
};
}
;
buildMockedEndpointsQuery() {
return {
query: (0, graphql_tag_1.default) `
query GetAllEndpointData {
mockedEndpoints {
id,
${this.schema.asOptionalField('MockedEndpoint', 'explanation')}
}
}
`,
transformResponse: (response, { adminClient }) => {
const mockedEndpoints = response.mockedEndpoints;
return mockedEndpoints.map(({ id, explanation }) => new mocked_endpoint_client_1.MockedEndpointClient(id, explanation, this.getEndpointDataGetter(adminClient, id)));
}
};
}
buildPendingEndpointsQuery() {
return {
query: (0, graphql_tag_1.default) `
query GetPendingEndpointData {
pendingEndpoints {
id,
explanation
}
}
`,
transformResponse: (response, { adminClient }) => {
const pendingEndpoints = response.pendingEndpoints;
return pendingEndpoints.map(({ id, explanation }) => new mocked_endpoint_client_1.MockedEndpointClient(id, explanation, this.getEndpointDataGetter(adminClient, id)));
}
};
}
buildSubscriptionRequest(event) {
// Note the asOptionalField checks - these are a quick hack for backward compatibility,
// introspecting the server schema to avoid requesting fields that don't exist on old servers.
const query = {
'request-initiated': (0, graphql_tag_1.default) `subscription OnRequestInitiated {
requestInitiated {
id
protocol
method
url
path
${this.schema.asOptionalField('InitiatedRequest', 'remoteIpAddress')}
${this.schema.asOptionalField('InitiatedRequest', 'remotePort')}
${this.schema.typeHasField('InitiatedRequest', 'destination')
? 'destination { hostname, port }'
: 'hostname' // Backward compat for old servers
}
rawHeaders
timingEvents
httpVersion
tags
}
}`,
request: (0, graphql_tag_1.default) `subscription OnRequest {
requestReceived {
id
matchedRuleId
protocol
method
url
path
${this.schema.asOptionalField('Request', 'remoteIpAddress')}
${this.schema.asOptionalField('Request', 'remotePort')}
${this.schema.typeHasField('Request', 'destination')
? 'destination { hostname, port }'
: 'hostname' // Backward compat for old servers
}
rawHeaders
body
${this.schema.typeHasField('Request', 'decodedBody') && this.messageBodyDecoding === 'server-side'
? 'decodedBody { decoded, decodingError }'
: ''}
${this.schema.asOptionalField('Request', 'rawTrailers')}
timingEvents
httpVersion
tags
}
}`,
response: (0, graphql_tag_1.default) `subscription OnResponse {
responseCompleted {
id
statusCode
statusMessage
rawHeaders
body
${this.schema.typeHasField('Response', 'decodedBody') && this.messageBodyDecoding === 'server-side'
? 'decodedBody { decoded, decodingError }'
: ''}
${this.schema.asOptionalField('Response', 'rawTrailers')}
timingEvents
tags
}
}`,
'websocket-request': (0, graphql_tag_1.default) `subscription OnWebSocketRequest {
webSocketRequest {
id
matchedRuleId
protocol
method
url
path
remoteIpAddress
remotePort
${this.schema.typeHasField('Request', 'destination')
? 'destination { hostname, port }'
: 'hostname' // Backward compat for old servers
}
rawHeaders
body
${this.schema.typeHasField('Request', 'decodedBody') && this.messageBodyDecoding === 'server-side'
? 'decodedBody { decoded, decodingError }'
: ''}
${this.schema.asOptionalField('Request', 'rawTrailers')}
timingEvents
httpVersion
tags
}
}`,
'websocket-accepted': (0, graphql_tag_1.default) `subscription OnWebSocketAccepted {
webSocketAccepted {
id
statusCode
statusMessage
rawHeaders
body
${this.schema.typeHasField('Response', 'decodedBody') && this.messageBodyDecoding === 'server-side'
? 'decodedBody { decoded, decodingError }'
: ''}
${this.schema.asOptionalField('Response', 'rawTrailers')}
timingEvents
tags
}
}`,
'websocket-message-received': (0, graphql_tag_1.default) `subscription OnWebSocketMessageReceived {
webSocketMessageReceived {
streamId
direction
content
isBinary
eventTimestamp
timingEvents
tags
}
}`,
'websocket-message-sent': (0, graphql_tag_1.default) `subscription OnWebSocketMessageSent {
webSocketMessageSent {
streamId
direction
content
isBinary
eventTimestamp
timingEvents
tags
}
}`,
'websocket-close': (0, graphql_tag_1.default) `subscription OnWebSocketClose {
webSocketClose {
streamId
closeCode
closeReason
timingEvents
tags
}
}`,
abort: (0, graphql_tag_1.default) `subscription OnAbort {
requestAborted {
id
protocol
method
url
path
${this.schema.typeHasField('AbortedRequest', 'destination')
? 'destination { hostname, port }'
: 'hostname' // Backward compat for old servers
}
rawHeaders
timingEvents
tags
error
}
}`,
'tls-passthrough-opened': (0, graphql_tag_1.default) `subscription OnTlsPassthroughOpened {
tlsPassthroughOpened {
id
${this.schema.typeHasField('TlsPassthroughEvent', 'destination')
? 'destination { hostname, port }'
: `
hostname
upstreamPort
`}
remoteIpAddress
remotePort
tags
timingEvents
tlsMetadata
}
}`,
'tls-passthrough-closed': (0, graphql_tag_1.default) `subscription OnTlsPassthroughClosed {
tlsPassthroughClosed {
id
${this.schema.typeHasField('TlsPassthroughEvent', 'destination')
? 'destination { hostname, port }'
: `
hostname
upstreamPort
`}
remoteIpAddress
remotePort
tags
timingEvents
tlsMetadata
}
}`,
'tls-client-error': (0, graphql_tag_1.default) `subscription OnTlsClientError {
failedTlsRequest {
failureCause
${this.schema.typeHasField('TlsHandshakeFailure', 'destination')
? 'destination { hostname, port }'
: 'hostname'}
remoteIpAddress
remotePort
tags
timingEvents
tlsMetadata
}
}`,
'client-error': (0, graphql_tag_1.default) `subscription OnClientError {
failedClientRequest {
errorCode
request {
id
timingEvents
tags
protocol
httpVersion
method
url
path
rawHeaders
${this.schema.asOptionalField('ClientErrorRequest', 'remoteIpAddress')}
${this.schema.asOptionalField('ClientErrorRequest', 'remotePort')}
${this.schema.asOptionalField('ClientErrorRequest', 'destination', 'destination { hostname, port }')}
}
response {
id
timingEvents
tags
statusCode
statusMessage
rawHeaders
body
${this.schema.typeHasField('Response', 'decodedBody') && this.messageBodyDecoding === 'server-side'
? 'decodedBody { decoded, decodingError }'
: ''}
${this.schema.asOptionalField('Response', 'rawTrailers')}
}
}
}`,
'raw-passthrough-opened': (0, graphql_tag_1.default) `subscription OnRawPassthroughOpened {
rawPassthroughOpened {
id
destination { hostname, port }
remoteIpAddress
remotePort
tags
timingEvents
}
}`,
'raw-passthrough-closed': (0, graphql_tag_1.default) `subscription OnRawPassthroughClosed {
rawPassthroughClosed {
id
destination { hostname, port }
remoteIpAddress
remotePort
tags
timingEvents
}
}`,
'raw-passthrough-data': (0, graphql_tag_1.default) `subscription OnRawPassthroughData {
rawPassthroughData {
id
direction
content
eventTimestamp
}
}`,
'rule-event': (0, graphql_tag_1.default) `subscription OnRuleEvent {
ruleEvent {
requestId
ruleId
eventType
eventData
}
}`
}[event];
if (!query)
return; // Unrecognized event, we can't subscribe to this.
return {
query,
transformResponse: (data) => {
if (event === 'client-error') {
data.request = _.mapValues(data.request, (v) =>
// Normalize missing values to undefined to match the local result
v === null ? undefined : v);
normalizeHttpMessage(data.request, event);
if (data.response) {
normalizeHttpMessage(data.response, event);
}
else {
data.response = 'aborted';
}
}
else if (event === 'websocket-message-received' || event === 'websocket-message-sent') {
normalizeWebSocketMessage(data);
}
else if (event === 'raw-passthrough-data') {
data.content = buffer_1.Buffer.from(data.content, 'base64');
}
else if (event === 'abort') {
normalizeHttpMessage(data, event);
data.error = data.error ? JSON.parse(data.error) : undefined;
}
else if (event === 'rule-event') {
const { eventData } = data;
// Events may include raw body data buffers, serialized as base64:
if (eventData.rawBody !== undefined) {
eventData.rawBody = buffer_1.Buffer.from(eventData.rawBody, 'base64');
}
}
else {
normalizeHttpMessage(data, event);
}
return data;
}
};
}
}
exports.MockttpAdminRequestBuilder = MockttpAdminRequestBuilder;
//# sourceMappingURL=mockttp-admin-request-builder.js.map