UNPKG

mockttp-mvs

Version:

Mock HTTP server for testing HTTP clients and stubbing webservices

431 lines (408 loc) 17.8 kB
"use strict"; 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