UNPKG

mockttp

Version:

Mock HTTP server for testing HTTP clients and stubbing webservices

504 lines (467 loc) 19.7 kB
"use strict"; 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