From 85b77d345351db6c9a3aaad3236c41997ea8ea33 Mon Sep 17 00:00:00 2001 From: Tony Air Date: Sat, 13 Feb 2021 01:00:26 +0700 Subject: [PATCH] IMPR: apollo + mocking service worker --- src/_graphql/mockServiceWorker.js | 283 ++++++++++++++++++++++++++++++ src/js/_components/_apollo.js | 50 ++++++ src/mocks/browser.js | 5 + src/mocks/handlers.js | 94 ++++++++++ src/mocks/server.js | 5 + 5 files changed, 437 insertions(+) create mode 100644 src/_graphql/mockServiceWorker.js create mode 100644 src/js/_components/_apollo.js create mode 100644 src/mocks/browser.js create mode 100644 src/mocks/handlers.js create mode 100644 src/mocks/server.js diff --git a/src/_graphql/mockServiceWorker.js b/src/_graphql/mockServiceWorker.js new file mode 100644 index 0000000..f4faf1b --- /dev/null +++ b/src/_graphql/mockServiceWorker.js @@ -0,0 +1,283 @@ +/** + * 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 = 'dc3d39c97ba52ee7fff0d667f7bc098c' +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 + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll() + const allClientIds = allClients.map((client) => client.id) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + 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', function (event) { + const { clientId, request } = event + const requestId = uuidv4() + const requestClone = request.clone() + const getOriginalResponse = () => fetch(requestClone) + + // Bypass navigation requests. + if (request.mode === 'navigate') { + return + } + + // Bypass mocking if the current client isn't present in the internal clients map + // (i.e. has the mocking disabled). + if (!clients[clientId]) { + return + } + + // 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 self.clients.get(clientId) + + // Bypass mocking when the request client is not active. + if (!client) { + 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: { + id: requestId, + 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)) + } + } + }) + .then(async (response) => { + const client = await self.clients.get(clientId) + const clonedResponse = response.clone() + + sendToClient(client, { + type: 'RESPONSE', + payload: { + requestId, + type: clonedResponse.type, + ok: clonedResponse.ok, + status: clonedResponse.status, + statusText: clonedResponse.statusText, + body: + clonedResponse.body === null ? null : await clonedResponse.text(), + headers: serializeHeaders(clonedResponse.headers), + redirected: clonedResponse.redirected, + }, + }) + + return response + }) + .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 + }, {}) +} + +function uuidv4() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + const r = (Math.random() * 16) | 0 + const v = c == 'x' ? r : (r & 0x3) | 0x8 + return v.toString(16) + }) +} diff --git a/src/js/_components/_apollo.js b/src/js/_components/_apollo.js new file mode 100644 index 0000000..f7f9ee2 --- /dev/null +++ b/src/js/_components/_apollo.js @@ -0,0 +1,50 @@ +import { + ApolloClient, + HttpLink, + ApolloLink, + InMemoryCache, + concat, +} from '@apollo/client'; + +//import { IonicStorageModule } from '@ionic/storage'; +//import { persistCache, IonicStorageWrapper } from 'apollo3-cache-persist'; +import { persistCacheSync, LocalStorageWrapper } from 'apollo3-cache-persist'; + +const cache = new InMemoryCache(); + +// await before instantiating ApolloClient, else queries might run before the cache is persisted +//await persistCache({ +persistCacheSync({ + cache, + storage: new LocalStorageWrapper(window.localStorage), + key: 'web-persist', + maxSize: 1048576, // 1Mb + //new IonicStorageWrapper(IonicStorageModule), +}); + +const authMiddleware = new ApolloLink((operation, forward) => { + // add the authorization to the headers + operation.setContext({ + headers: { + apikey: `${GRAPHQL_API_KEY}`, + }, + }); + + return forward(operation); +}); + +const link = new HttpLink({ + uri: 'http://127.0.0.1/graphql', + + // Use explicit `window.fetch` so tha outgoing requests + // are captured and deferred until the Service Worker is ready. + fetch: (...args) => fetch(...args), + credentials: 'same-origin', //'include', +}); + +// Isolate Apollo client so it could be reused +// in both application runtime and tests. +export const client = new ApolloClient({ + cache, + link: concat(authMiddleware, link), +}); diff --git a/src/mocks/browser.js b/src/mocks/browser.js new file mode 100644 index 0000000..16c1b63 --- /dev/null +++ b/src/mocks/browser.js @@ -0,0 +1,5 @@ +// src/mocks/browser.js +import { setupWorker } from 'msw'; +import { handlers } from './handlers'; +// This configures a Service Worker with the given request handlers. +export const worker = setupWorker(...handlers); diff --git a/src/mocks/handlers.js b/src/mocks/handlers.js new file mode 100644 index 0000000..a8c72ad --- /dev/null +++ b/src/mocks/handlers.js @@ -0,0 +1,94 @@ +// src/mocks/handlers.js +import { graphql } from 'msw'; + +export const handlers = [ + // Handles a "Login" mutation + /*graphql.mutation('Login', (req, res, ctx) => { + const { username } = req.variables; + sessionStorage.setItem('is-authenticated', username); + return res( + ctx.data({ + login: { + username, + }, + }), + ); + }),*/ + // Handles a "Pages" query + graphql.query('Pages11', (req, res, ctx) => { + const apiKey = req.headers.map.apikey; + if ( + !req.headers.map.apikey || + req.headers.map.apikey !== `${GRAPHQL_API_KEY}` + ) { + // When not authenticated, respond with an error + return res( + ctx.errors([ + { + message: 'Not authenticated', + errorType: 'AuthenticationError', + }, + ]), + ); + } + // When authenticated, respond with a query payload + return res( + ctx.data({ + readPages: { + edges: [ + { + node: { + ID: '1', + Title: 'Home', + ClassName: 'Site\\Pages\\HomePage', + CSSClass: 'Site-Pages-HomePage', + Summary: + "That's my personal website, I'm full-stack developer mostly specializing on SilverStipe backend projects and share some of my hobbies at this website.", + Link: '/en/', + URLSegment: 'home', + Elements: { + edges: [ + { + node: { + ID: '3', + Title: 'Slider', + Render: + '<div\nid="e3"\nclass="element site__elements__sliderelement\n\t\n\t"\n>\n\t<div class="element-container container">\n\t\t\n\n\n <div id="Carousel3" class="carousel slide js-carousel">\n <div class="carousel-inner">\n \n <div class="carousel-item carousel-item-Video carousel-item-nocontrols active">\n <div class="carousel-slide">\n \n \n <div class="video">\n <iframe width="200" height="113" src="https://www.youtube.com/embed/IF1F_es1SaU?feature=oembed&wmode=transparent&enablejsapi=1&disablekb=1&iv_load_policy=3&modestbranding=1&rel=0&showinfo=0&autoplay=1&mute=1&loop=1&playlist=IF1F_es1SaU" allow="autoplay" allow="autoplay" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>\n </div>\n \n\n\n\n\n </div>\n </div>\n \n </div>\n </div>\n\n\n\t</div>\n</div>\n', + }, + }, + { + node: { + ID: '7', + Title: 'Categories List', + Render: + '<div\nid="e7"\nclass="element dnadesign__elementallist__model__elementlist\n\t\n\t"\n>\n\t<div class="element-container container">\n\t\t\n<div class="list-element__container row" data-listelement-count="3">\n \n \n\t <div\nid="e9"\nclass="element dynamic__elements__image__elements__elementimage\n\t\n\t col-block col-md"\n>\n\t<div class="element-container">\n\t\t\n <div class="image-element__image height400 width400">\n <img\n src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"\n data-lazy-src="/assets/Uploads/ElementImage/1609765749853__FillWzQwMCw0MDBd.jpg" class="img-responsive" alt="Aquascaping"\n />\n </div>\n\n\n\n<div class="image-element__caption img-content">\n <h3 class="image-element__title title">Aquascaping</h3>\n\n \n</div>\n\n\n\n <a href="/en/aquascaping/" class="stretched-link">\n <b class="sr-only">Aquascaping</b>\n </a>\n\n\n\t</div>\n</div>\n\n \n\t <div\nid="e10"\nclass="element dynamic__elements__image__elements__elementimage\n\t\n\t col-block col-md"\n>\n\t<div class="element-container">\n\t\t\n <div class="image-element__image height400 width400">\n <img\n src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"\n data-lazy-src="/assets/Uploads/ElementImage/1609766816754__FillWzQwMCw0MDBd.jpg" class="img-responsive" alt="Car Projects"\n />\n </div>\n\n\n\n<div class="image-element__caption img-content">\n <h3 class="image-element__title title">Car Projects</h3>\n\n \n</div>\n\n\n\n <a href="/en/car/" class="stretched-link">\n <b class="sr-only">Car Projects</b>\n </a>\n\n\n\t</div>\n</div>\n\n \n\t <div\nid="e12"\nclass="element dynamic__elements__image__elements__elementimage\n\t\n\t col-block col-md"\n>\n\t<div class="element-container">\n\t\t\n <div class="image-element__image height400 width400">\n <img\n src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"\n data-lazy-src="/assets/Uploads/ElementImage/Screenshot-from-2021-01-04-20-30-19__FillWzQwMCw0MDBd.png" class="img-responsive" alt="Programming"\n />\n </div>\n\n\n\n<div class="image-element__caption img-content">\n <h3 class="image-element__title title">Programming</h3>\n\n \n</div>\n\n\n\n <a href="/en/development/" class="stretched-link">\n <b class="sr-only">Programming</b>\n </a>\n\n\n\t</div>\n</div>\n\n \n\n\n</div>\n\n\t</div>\n</div>\n', + }, + }, + { + node: { + ID: '4', + Title: "Hello, I'm Tony Air", + Render: + '<div\nid="e4"\nclass="element dnadesign__elemental__models__elementcontent\n\t\n\t"\n>\n\t<div class="element-container container">\n\t\t<div\nclass="content-element__content"\n>\n \n\t\n <h2 class="content-element__title">Hello, I&#039;m Tony Air</h2>\n \n\n <div class="typography">\n <p>That's my personal website, I'm full-stack developer mostly specializing on SilverStipe backend projects and share some of my hobbies at this website.<br><br>As for the things I do for work:<br><br>Here's front-end UI kit:&nbsp;<a rel="noopener" href="https://github.com/a2nt/webpack-bootstrap-ui-kit" target="_blank">https://github.com/a2nt/webpack-bootstrap-ui-kit</a><br>Here's SilverStipe quick start template:&nbsp;<a rel="noopener" href="https://github.com/a2nt/silverstripe-webpack" target="_blank">https://github.com/a2nt/silverstripe-webpack</a><br><br>More at my github:&nbsp;<a rel="noopener" href="https://github.com/a2nt" target="_blank">https://github.com/a2nt</a></p>\n </div>\n\n \n</div>\n\n\t</div>\n</div>\n', + }, + }, + ], + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + totalCount: 3, + }, + }, + }, + }, + ], + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + totalCount: 1, + }, + }, + }), + ); + }), +]; diff --git a/src/mocks/server.js b/src/mocks/server.js new file mode 100644 index 0000000..76fbb61 --- /dev/null +++ b/src/mocks/server.js @@ -0,0 +1,5 @@ +// src/mocks/server.js +import { setupServer } from 'msw/node'; +import { handlers } from './handlers'; +// This configures a request mocking server with the given request handlers. +export const server = setupServer(...handlers);