mockttp-mvs
Version:
Mock HTTP server for testing HTTP clients and stubbing webservices
431 lines (408 loc) • 17.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.MockttpAdminRequestBuilder = void 0;
const _ = require("lodash");
const graphql_tag_1 = require("graphql-tag");
const request_utils_1 = require("../util/request-utils");
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");
function normalizeHttpMessage(message, event) {
if (message.timingEvents) {
// Timing events are serialized as raw JSON
message.timingEvents = JSON.parse(message.timingEvents);
}
else if (event !== 'tls-client-error' && event !== 'client-error') {
// For backwards compat, all except errors should have timing events if they're missing
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);
}
else if (message.headers) {
// Backward compat for older servers:
message.headers = JSON.parse(message.headers);
message.rawHeaders = (0, header_utils_1.objectHeadersToRaw)(message.headers);
}
if (message.body !== undefined) {
// Body is serialized as the raw encoded buffer in base64
message.body = (0, request_utils_1.buildBodyReader)(Buffer.from(message.body, 'base64'), message.headers);
}
// For backwards compat, all except errors should have tags if they're missing
if (!message.tags)
message.tags = [];
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.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) {
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 {
protocol,
method,
url,
path,
hostname
${this.schema.typeHasField('Request', 'rawHeaders')
? 'rawHeaders'
: 'headers'}
body,
${this.schema.asOptionalField('Request', 'timingEvents')}
${this.schema.asOptionalField('Request', 'httpVersion')}
}
${this.schema.asOptionalField('MockedEndpoint', 'isPending')}
}
}
`,
variables: { id: ruleId }
});
const mockedEndpoint = result.mockedEndpoint;
if (!mockedEndpoint)
return null;
mockedEndpoint.seenRequests.forEach(req => normalizeHttpMessage(req));
return mockedEndpoint;
};
}
buildAddRequestRulesQuery(rules, reset, adminStream) {
const requestName = (reset ? 'Set' : 'Add') + 'Rules';
const mutationName = (reset ? 'set' : 'add') + 'Rules';
const serializedRules = rules.map((rule) => {
const serializedRule = (0, rule_serialization_1.serializeRuleData)(rule, adminStream);
if (!this.schema.typeHasInputField('MockRule', 'id')) {
delete serializedRule.id;
}
return serializedRule;
});
return {
query: (0, graphql_tag_1.default) `
mutation ${requestName}($newRules: [MockRule!]!) {
endpoints: ${mutationName}(input: $newRules) {
id,
${this.schema.asOptionalField('MockedEndpoint', '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)));
}
};
}
buildAddWebSocketRulesQuery(rules, reset, adminStream) {
// Seperate and simpler than buildAddRequestRulesQuery, because it doesn't have to
// deal with backward compatibility.
const requestName = (reset ? 'Set' : 'Add') + 'WebSocketRules';
const mutationName = (reset ? 'set' : 'add') + 'WebSocketRules';
const serializedRules = rules.map((rule) => (0, rule_serialization_1.serializeRuleData)(rule, adminStream));
return {
query: (0, graphql_tag_1.default) `
mutation ${requestName}($newRules: [WebSocketMockRule!]!) {
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')},
hostname,
${this.schema.typeHasField('InitiatedRequest', 'rawHeaders')
? 'rawHeaders'
: 'headers'}
timingEvents,
httpVersion,
${this.schema.asOptionalField('InitiatedRequest', 'tags')}
}
}`,
request: (0, graphql_tag_1.default) `subscription OnRequest {
requestReceived {
id,
${this.schema.asOptionalField('Request', 'matchedRuleId')}
protocol,
method,
url,
path,
${this.schema.asOptionalField('Request', 'remoteIpAddress')},
${this.schema.asOptionalField('Request', 'remotePort')},
hostname,
${this.schema.typeHasField('Request', 'rawHeaders')
? 'rawHeaders'
: 'headers'}
body,
${this.schema.asOptionalField('Request', 'timingEvents')}
${this.schema.asOptionalField('Request', 'httpVersion')}
${this.schema.asOptionalField('Request', 'tags')}
}
}`,
response: (0, graphql_tag_1.default) `subscription OnResponse {
responseCompleted {
id,
statusCode,
statusMessage,
${this.schema.typeHasField('Response', 'rawHeaders')
? 'rawHeaders'
: 'headers'}
body,
${this.schema.asOptionalField('Response', 'timingEvents')}
${this.schema.asOptionalField('Response', 'tags')}
}
}`,
'websocket-request': (0, graphql_tag_1.default) `subscription OnWebSocketRequest {
webSocketRequest {
id,
matchedRuleId,
protocol,
method,
url,
path,
remoteIpAddress,
remotePort,
hostname,
rawHeaders,
body,
timingEvents,
httpVersion,
tags
}
}`,
'websocket-accepted': (0, graphql_tag_1.default) `subscription OnWebSocketAccepted {
webSocketAccepted {
id,
statusCode,
statusMessage,
rawHeaders,
body,
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,
hostname,
${this.schema.typeHasField('Request', 'rawHeaders')
? 'rawHeaders'
: 'headers'}
${this.schema.asOptionalField('Request', 'timingEvents')}
${this.schema.asOptionalField('Request', 'tags')}
${this.schema.asOptionalField('AbortedRequest', 'error')}
}
}`,
'tls-passthrough-opened': (0, graphql_tag_1.default) `subscription OnTlsPassthroughOpened {
tlsPassthroughOpened {
id
upstreamPort
hostname
remoteIpAddress
remotePort
tags
timingEvents
${this.schema.asOptionalField('TlsPassthroughEvent', 'tlsMetadata')}
}
}`,
'tls-passthrough-closed': (0, graphql_tag_1.default) `subscription OnTlsPassthroughClosed {
tlsPassthroughClosed {
id
upstreamPort
hostname
remoteIpAddress
remotePort
tags
timingEvents
${this.schema.asOptionalField('TlsPassthroughEvent', 'tlsMetadata')}
}
}`,
'tls-client-error': (0, graphql_tag_1.default) `subscription OnTlsClientError {
failedTlsRequest {
failureCause
hostname
remoteIpAddress
${this.schema.asOptionalField(['TlsHandshakeFailure', 'TlsRequest'], 'remotePort')}
${this.schema.asOptionalField(['TlsHandshakeFailure', 'TlsRequest'], 'tags')}
${this.schema.asOptionalField(['TlsHandshakeFailure', 'TlsRequest'], 'timingEvents')}
${this.schema.asOptionalField(['TlsHandshakeFailure', 'TlsRequest'], 'tlsMetadata')}
}
}`,
'client-error': (0, graphql_tag_1.default) `subscription OnClientError {
failedClientRequest {
errorCode
request {
id
timingEvents
tags
protocol
httpVersion
method
url
path
${this.schema.typeHasField('ClientErrorRequest', 'rawHeaders')
? 'rawHeaders'
: 'headers'}
${this.schema.asOptionalField('ClientErrorRequest', 'remoteIpAddress')},
${this.schema.asOptionalField('ClientErrorRequest', 'remotePort')},
}
response {
id
timingEvents
tags
statusCode
statusMessage
${this.schema.typeHasField('Response', 'rawHeaders')
? 'rawHeaders'
: 'headers'}
body
}
}
}`
}[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 === 'abort') {
normalizeHttpMessage(data, event);
data.error = data.error ? JSON.parse(data.error) : undefined;
}
else {
normalizeHttpMessage(data, event);
}
return data;
}
};
}
}
exports.MockttpAdminRequestBuilder = MockttpAdminRequestBuilder;
//# sourceMappingURL=mockttp-admin-request-builder.js.map