Skip to content

Commit

Permalink
chore: add a mockserver
Browse files Browse the repository at this point in the history
there's been the idea of a mockserver for quite a while. there has also been an
implementation once that has never been finished.

this is a quite versatile and lightweight approach that makes use of service
workers.

during development (and only if CLUSTER_URL is not set) we'll load
src/mocks/handlers.ts. that file contains only a couple routes for now that have
been copied from cypress and sets a cookie as else we're redirected to the
login-screen.

generally we can get rid of the http-service mocking AND the cypress mocking
with this approach. let's go there eventually.

the first use case will be the implementation of "jobs with dependencies"
related stuff as i don't have a metronome build around to test against.
  • Loading branch information
pierrebeitz committed Aug 31, 2020
1 parent 65d98e1 commit 753e04a
Show file tree
Hide file tree
Showing 12 changed files with 1,407 additions and 50 deletions.
228 changes: 228 additions & 0 deletions dist/mockServiceWorker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
/**
* Mock Service Worker.
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
* - Please do NOT serve this file on production.
*/
/* eslint-disable */
/* tslint:disable */

const INTEGRITY_CHECKSUM = "ca2c3cd7453d8c614e2c19db63ede1a1";
const bypassHeaderName = "x-msw-bypass";

let clients = {};

self.addEventListener("install", function () {
return self.skipWaiting();
});

self.addEventListener("activate", async function (event) {
return self.clients.claim();
});

self.addEventListener("message", async function (event) {
const clientId = event.source.id;
const client = await event.currentTarget.clients.get(clientId);
const allClients = await self.clients.matchAll();
const allClientIds = allClients.map((client) => client.id);

switch (event.data) {
case "INTEGRITY_CHECK_REQUEST": {
sendToClient(client, {
type: "INTEGRITY_CHECK_RESPONSE",
payload: INTEGRITY_CHECKSUM,
});
break;
}

case "MOCK_ACTIVATE": {
clients = ensureKeys(allClientIds, clients);
clients[clientId] = true;

sendToClient(client, {
type: "MOCKING_ENABLED",
payload: true,
});
break;
}

case "MOCK_DEACTIVATE": {
clients = ensureKeys(allClientIds, clients);
clients[clientId] = false;
break;
}

case "CLIENT_CLOSED": {
const remainingClients = allClients.filter((client) => {
return client.id !== clientId;
});

// Unregister itself when there are no more clients
if (remainingClients.length === 0) {
self.registration.unregister();
}

break;
}
}
});

self.addEventListener("fetch", async function (event) {
const { clientId, request } = event;
const requestClone = request.clone();
const getOriginalResponse = () => fetch(requestClone);

// Opening the DevTools triggers the "only-if-cached" request
// that cannot be handled by the worker. Bypass such requests.
if (request.cache === "only-if-cached" && request.mode !== "same-origin") {
return;
}

event.respondWith(
new Promise(async (resolve, reject) => {
const client = await event.target.clients.get(clientId);

if (
// Bypass mocking when no clients active
!client ||
// Bypass mocking if the current client has mocking disabled
!clients[clientId] ||
// Bypass mocking for navigation requests
request.mode === "navigate"
) {
return resolve(getOriginalResponse());
}

// Bypass requests with the explicit bypass header
if (requestClone.headers.get(bypassHeaderName) === "true") {
const modifiedHeaders = serializeHeaders(requestClone.headers);
// Remove the bypass header to comply with the CORS preflight check
delete modifiedHeaders[bypassHeaderName];

const originalRequest = new Request(requestClone, {
headers: new Headers(modifiedHeaders),
});

return resolve(fetch(originalRequest));
}

const reqHeaders = serializeHeaders(request.headers);
const body = await request.text();

const rawClientMessage = await sendToClient(client, {
type: "REQUEST",
payload: {
url: request.url,
method: request.method,
headers: reqHeaders,
cache: request.cache,
mode: request.mode,
credentials: request.credentials,
destination: request.destination,
integrity: request.integrity,
redirect: request.redirect,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
body,
bodyUsed: request.bodyUsed,
keepalive: request.keepalive,
},
});

const clientMessage = rawClientMessage;

switch (clientMessage.type) {
case "MOCK_SUCCESS": {
setTimeout(
resolve.bind(this, createResponse(clientMessage)),
clientMessage.payload.delay
);
break;
}

case "MOCK_NOT_FOUND": {
return resolve(getOriginalResponse());
}

case "NETWORK_ERROR": {
const { name, message } = clientMessage.payload;
const networkError = new Error(message);
networkError.name = name;

// Rejecting a request Promise emulates a network error.
return reject(networkError);
}

case "INTERNAL_ERROR": {
const parsedBody = JSON.parse(clientMessage.payload.body);

console.error(
`\
[MSW] Request handler function for "%s %s" has thrown the following exception:
${parsedBody.errorType}: ${parsedBody.message}
(see more detailed error stack trace in the mocked response body)
This exception has been gracefully handled as a 500 response, however, it's strongly recommended to resolve this error.
If you wish to mock an error response, please refer to this guide: https://mswjs.io/docs/recipes/mocking-error-responses\
`,
request.method,
request.url
);

return resolve(createResponse(clientMessage));
}
}
}).catch((error) => {
console.error(
'[MSW] Failed to mock a "%s" request to "%s": %s',
request.method,
request.url,
error
);
})
);
});

function serializeHeaders(headers) {
const reqHeaders = {};
headers.forEach((value, name) => {
reqHeaders[name] = reqHeaders[name]
? [].concat(reqHeaders[name]).concat(value)
: value;
});
return reqHeaders;
}

function sendToClient(client, message) {
return new Promise((resolve, reject) => {
const channel = new MessageChannel();

channel.port1.onmessage = (event) => {
if (event.data && event.data.error) {
reject(event.data.error);
} else {
resolve(event.data);
}
};

client.postMessage(JSON.stringify(message), [channel.port2]);
});
}

function createResponse(clientMessage) {
return new Response(clientMessage.payload.body, {
...clientMessage.payload,
headers: clientMessage.payload.headers,
});
}

function ensureKeys(keys, obj) {
return Object.keys(obj).reduce((acc, key) => {
if (keys.includes(key)) {
acc[key] = obj[key];
}

return acc;
}, {});
}

0 comments on commit 753e04a

Please sign in to comment.