From c5687e2e68be3d74fca9f059bca3271778312557 Mon Sep 17 00:00:00 2001 From: dena Date: Thu, 27 Aug 2026 16:11:03 +0200 Subject: [PATCH 1/5] ran 'npx msw init public --save' --- package.json | 9 +- public/mockServiceWorker.js | 416 +++++++++++++++++++++++------------- 2 files changed, 272 insertions(+), 153 deletions(-) diff --git a/package.json b/package.json index 27b611cd..b1c64926 100644 --- a/package.json +++ b/package.json @@ -67,5 +67,10 @@ "node": ">= 11.0.0", "npm": ">= 6.4.1" }, - "readme": "ERROR: No README data found!" -} + "readme": "ERROR: No README data found!", + "msw": { + "workerDirectory": [ + "public" + ] + } +} \ No newline at end of file diff --git a/public/mockServiceWorker.js b/public/mockServiceWorker.js index d8703e53..461e2600 100644 --- a/public/mockServiceWorker.js +++ b/public/mockServiceWorker.js @@ -1,30 +1,41 @@ +/* eslint-disable */ +/* tslint:disable */ + /** * 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 = 'd1e0e502f550d40a34bee90822e4bf98' -const bypassHeaderName = 'x-msw-bypass' -let clients = {} +const PACKAGE_VERSION = '2.12.7' +const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() -self.addEventListener('install', function () { - return self.skipWaiting() +addEventListener('install', function () { + self.skipWaiting() }) -self.addEventListener('activate', async function (event) { - return self.clients.claim() +addEventListener('activate', function (event) { + event.waitUntil(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) +addEventListener('message', async function (event) { + const clientId = Reflect.get(event.source || {}, 'id') + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) switch (event.data) { case 'KEEPALIVE_REQUEST': { @@ -37,29 +48,32 @@ self.addEventListener('message', async function (event) { case 'INTEGRITY_CHECK_REQUEST': { sendToClient(client, { type: 'INTEGRITY_CHECK_RESPONSE', - payload: INTEGRITY_CHECKSUM, + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, }) break } case 'MOCK_ACTIVATE': { - clients = ensureKeys(allClientIds, clients) - clients[clientId] = true + activeClientIds.add(clientId) sendToClient(client, { type: 'MOCKING_ENABLED', - payload: true, + payload: { + client: { + id: client.id, + frameType: client.frameType, + }, + }, }) break } - case 'MOCK_DEACTIVATE': { - clients = ensureKeys(allClientIds, clients) - clients[clientId] = false - break - } - case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + const remainingClients = allClients.filter((client) => { return client.id !== clientId }) @@ -74,162 +88,262 @@ self.addEventListener('message', async function (event) { } }) -self.addEventListener('fetch', async function (event) { - const { clientId, request } = event - const requestClone = request.clone() - const getOriginalResponse = () => fetch(requestClone) +addEventListener('fetch', function (event) { + const requestInterceptedAt = Date.now() + + // Bypass navigation requests. + if (event.request.mode === 'navigate') { + 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') { + if ( + event.request.cache === 'only-if-cached' && + event.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 all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been terminated (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } - // 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 requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId, requestInterceptedAt)) +}) - const originalRequest = new Request(requestClone, { - headers: new Headers(modifiedHeaders), - }) +/** + * @param {FetchEvent} event + * @param {string} requestId + * @param {number} requestInterceptedAt + */ +async function handleRequest(event, requestId, requestInterceptedAt) { + const client = await resolveMainClient(event) + const requestCloneForEvents = event.request.clone() + const response = await getResponse( + event, + client, + requestId, + requestInterceptedAt, + ) - return resolve(fetch(originalRequest)) - } + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + const serializedRequest = await serializeRequest(requestCloneForEvents) - const reqHeaders = serializeHeaders(request.headers) - const body = await request.text() + // Clone the response so both the client and the library could consume it. + const responseClone = response.clone() - const rawClientMessage = await sendToClient(client, { - type: 'REQUEST', + sendToClient( + client, + { + type: 'RESPONSE', 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, + isMockedResponse: IS_MOCKED_RESPONSE in response, + request: { + id: requestId, + ...serializedRequest, + }, + response: { + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + headers: Object.fromEntries(responseClone.headers.entries()), + body: responseClone.body, + }, }, - }) + }, + responseClone.body ? [serializedRequest.body, responseClone.body] : [], + ) + } - 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, + return response +} + +/** + * Resolve the main client for the given event. + * Client that issues a request doesn't necessarily equal the client + * that registered the worker. It's with the latter the worker should + * communicate with during the response resolving phase. + * @param {FetchEvent} event + * @returns {Promise} + */ +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (activeClientIds.has(event.clientId)) { + return client + } + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +/** + * @param {FetchEvent} event + * @param {Client | undefined} client + * @param {string} requestId + * @param {number} requestInterceptedAt + * @returns {Promise} + */ +async function getResponse(event, client, requestId, requestInterceptedAt) { + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = event.request.clone() + + function passthrough() { + // Cast the request headers to a new Headers instance + // so the headers can be manipulated with. + const headers = new Headers(requestClone.headers) + + // Remove the "accept" header value that marked this request as passthrough. + // This prevents request alteration and also keeps it compliant with the + // user-defined CORS policies. + const acceptHeader = headers.get('accept') + if (acceptHeader) { + const values = acceptHeader.split(',').map((value) => value.trim()) + const filteredValues = values.filter( + (value) => value !== 'msw/passthrough', ) - }), + + if (filteredValues.length > 0) { + headers.set('accept', filteredValues.join(', ')) + } else { + headers.delete('accept') + } + } + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const serializedRequest = await serializeRequest(event.request) + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + interceptedAt: requestInterceptedAt, + ...serializedRequest, + }, + }, + [serializedRequest.body], ) -}) -function serializeHeaders(headers) { - const reqHeaders = {} - headers.forEach((value, name) => { - reqHeaders[name] = reqHeaders[name] - ? [].concat(reqHeaders[name]).concat(value) - : value - }) - return reqHeaders + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'PASSTHROUGH': { + return passthrough() + } + } + + return passthrough() } -function sendToClient(client, message) { +/** + * @param {Client} client + * @param {any} message + * @param {Array} transferrables + * @returns {Promise} + */ +function sendToClient(client, message, transferrables = []) { 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) + return reject(event.data.error) } + + resolve(event.data) } - client.postMessage(JSON.stringify(message), [channel.port2]) + client.postMessage(message, [ + channel.port2, + ...transferrables.filter(Boolean), + ]) }) } -function createResponse(clientMessage) { - return new Response(clientMessage.payload.body, { - ...clientMessage.payload, - headers: clientMessage.payload.headers, +/** + * @param {Response} response + * @returns {Response} + */ +function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, }) -} -function ensureKeys(keys, obj) { - return Object.keys(obj).reduce((acc, key) => { - if (keys.includes(key)) { - acc[key] = obj[key] - } + return mockedResponse +} - return acc - }, {}) +/** + * @param {Request} request + */ +async function serializeRequest(request) { + return { + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: await request.arrayBuffer(), + keepalive: request.keepalive, + } } From 03e2865f7146abfec543c358e1356e1aeebe4498 Mon Sep 17 00:00:00 2001 From: dena Date: Thu, 27 Aug 2026 16:11:31 +0200 Subject: [PATCH 2/5] wip: start mock service without handlers --- src/backend/mocks/browser.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/backend/mocks/browser.js b/src/backend/mocks/browser.js index 159e6b9e..cc917c5d 100644 --- a/src/backend/mocks/browser.js +++ b/src/backend/mocks/browser.js @@ -1,4 +1,5 @@ -import { setupWorker } from 'msw' -import { handlers } from './default_handlers' +import { setupWorker } from 'msw/browser' +//import { handlers } from './default_handlers' -export const worker = setupWorker(...handlers) +//export const worker = setupWorker(...handlers) +export const worker = setupWorker() \ No newline at end of file From e93dd9e5eb704cee525a1f9999d94ba16ce1406d Mon Sep 17 00:00:00 2001 From: dena Date: Fri, 28 Aug 2026 13:25:23 +0200 Subject: [PATCH 3/5] cleanup, restructure --- src/backend/mocks/browser.js | 5 +- src/backend/mocks/default_handlers.js | 224 +-------------------- src/backend/mocks/old.default_handlers.js | 226 ++++++++++++++++++++++ 3 files changed, 233 insertions(+), 222 deletions(-) create mode 100644 src/backend/mocks/old.default_handlers.js diff --git a/src/backend/mocks/browser.js b/src/backend/mocks/browser.js index cc917c5d..e2b29cc7 100644 --- a/src/backend/mocks/browser.js +++ b/src/backend/mocks/browser.js @@ -1,5 +1,4 @@ import { setupWorker } from 'msw/browser' -//import { handlers } from './default_handlers' +import { handlers } from './default_handlers' -//export const worker = setupWorker(...handlers) -export const worker = setupWorker() \ No newline at end of file +export const worker = setupWorker(...handlers) diff --git a/src/backend/mocks/default_handlers.js b/src/backend/mocks/default_handlers.js index 33b4061a..61d7941c 100644 --- a/src/backend/mocks/default_handlers.js +++ b/src/backend/mocks/default_handlers.js @@ -1,223 +1,9 @@ -import { rest } from 'msw' +import { http, HttpResponse } from 'msw' -let myWikis = JSON.parse(localStorage.getItem('msw-myWikis')) || [] -let lastWikiId = (myWikis.length && myWikis[myWikis.length - 1].id) || 0 -let user = makeUser() -let getEntityImportCalledTimes = 0 - -function makeUser (email = 'test@local') { - return { - id: 1, - email, - verified: true, - created_at: '2020-01-01', - updated_at: '2020-01-01', - } -} - -const makeNewWiki = ({ domain, sitename }) => { - const newWiki = { - id: ++lastWikiId, - domain, - sitename, - deleted_at: null, - created_at: '2020-01-01', - updated_at: '2020-01-01', - pivot: { - user_id: user.id, - wiki_id: lastWikiId, - }, - wiki_managers: [{ - email: user.email, - pivot: { - user_id: user.id, - wiki_id: lastWikiId, - }, - }], - wiki_db_version: { - id: 101, - wiki_id: lastWikiId, - version: 'mw1.33-wbs1', - }, - public_settings: [], - } - - myWikis.push(newWiki) - localStorage.setItem('msw-myWikis', JSON.stringify(myWikis)) - - return newWiki -} - -const removeWiki = wikiIndex => { - myWikis = myWikis.splice(wikiIndex, 1) - localStorage.setItem('msw-myWikis', JSON.stringify(myWikis)) -} - -const wikiDiscovery = (referrer, params) => { - const pseudorandom = { - seed: 1, - next: function () { - const x = Math.sin(this.seed++) * 10000 - return x - Math.floor(x) - }, - } - - const names = [ - 'Wikibase Name', - 'A Very Long Wikibase Name', - ] - - let wikis = [...Array(75).keys()].map((id) => { - const wiki = { - id, - domain: id + '-wikibase.wbaas.localhost', - sitename: id + ' - ' + names[id % names.length], - wiki_site_stats: null, - logo_url: null, - } - - if (pseudorandom.next() >= 0.1) { - wiki.wiki_site_stats = { - pages: Math.ceil(pseudorandom.next() * 250), - } - } - - if (pseudorandom.next() >= 0.5) { - wiki.logo_url = new URL(referrer).origin + '/favicon.ico' - } - return wiki - }) - - if (parseInt(params.get('is_active'))) { - wikis = wikis.filter((wiki) => { - const stats = wiki.wiki_site_stats - return stats && stats.pages > 1 - }) - } - - if (params.get('sort') === 'sitename') { - wikis = wikis.sort((a, b) => { - let sort = a.sitename.localeCompare(b.sitename, 'en', { numeric: true }) - if (params.get('direction') === 'desc') { - sort *= -1 - } - return sort - }) - } - - if (params.get('sort') === 'pages') { - wikis = wikis.sort((a, b) => { - const aPages = a.wiki_site_stats ? a.wiki_site_stats.pages : 0 - const bPages = b.wiki_site_stats ? b.wiki_site_stats.pages : 0 - if (params.get('direction') === 'desc') { - return bPages - aPages - } - return aPages - bPages - }) - } - - const currentPage = parseInt(params.get('page')) - const resultsPerPage = parseInt(params.get('per_page')) - const start = (currentPage - 1) * resultsPerPage - const end = start + resultsPerPage - - return { - data: wikis.slice(start, end), - meta: { - last_page: Math.ceil(wikis.length / resultsPerPage), - total: wikis.length, - }, - } -} +// implementations for msw v2 handlers go in this file +// see old.default_handlers.js for reference +// this note can get removed after migration export const handlers = [ - /* User endpoints */ - rest.post(/\/api\/auth\/login$/, (req, res, ctx) => { - user = makeUser(req.body.email) - return res(ctx.json({ - user, - }), ctx.cookie('authToken', 'token_value')) - }), - rest.get(/\/api\/auth\/login$/, (req, res, ctx) => { - const { authToken } = req.cookies - if (authToken !== 'token_value') { - return res(ctx.status(401)) - } - user = makeUser(req.body.email) - return res(ctx.json({ - user, - })) - }), - rest.delete(/\/api\/auth\/login$/, (req, res, ctx) => { - user = makeUser(req.body.email) - return res(ctx.status(204)) - }), - rest.post(/\/api\/user\/forgotPassword$/, (req, res, ctx) => { - if (req.body.email === 'serverError@example.com') { - return res(ctx.status(400, 'Mocked Server Error')) - } - return res(ctx.status(200)) - }, - ), - rest.post(/\/api\/user\/resetPassword$/, (_, res, ctx) => res(ctx.status(200))), - rest.post(/\/api\/user\/sendVerifyEmail$/, (_, res, ctx) => res(ctx.json({ message: 'Already verified' }))), - rest.post(/\/api\/user\/verifyEmail$/, (_, res, ctx) => res(ctx.status(200))), - rest.post(/\/api\/complaint\/sendMessage$/, (req, res, ctx) => res(ctx.status(200))), - rest.post(/\/api\/contact\/sendMessage$/, (req, res, ctx) => { - if (req.body.name === 'recaptchaError') { - return res(ctx.status(401, 'Mocked recaptcha Error')) - } - if (req.body.name === '') { - return res(ctx.status(400, 'Mocked empty name Error')) - } - if (req.body.recaptcha === '') { - return res(ctx.status(400, 'Mocked recaptcha empty Error')) - } - return res(ctx.status(200)) - }, - ), - - /* Wiki endpoints */ - rest.post(/\/api\/wiki\/entityImport$/, (_, res, ctx) => { - return res(ctx.json({ data: { status: 'pending', payload: {}, started_at: new Date().toJSON() } })) - }), - rest.get(/\/api\/wiki\/entityImport$/, (_, res, ctx) => { - getEntityImportCalledTimes++ - switch (getEntityImportCalledTimes) { - case 1: - return res(ctx.json({ data: [] })) - case 2: - return res(ctx.json({ data: [{ status: 'pending' }] })) - default: - return res(ctx.json({ data: [{ status: 'success' }] })) - } - }), - rest.get(/\/api\/wiki\/count$/, (_, res, ctx) => res(ctx.json({ data: 1 }))), - rest.post(/\/api\/wiki\/mine$/, (_, res, ctx) => res(ctx.json({ wikis: myWikis, count: myWikis.length, limit: false }))), - rest.post(/\/api\/wiki\/create$/, (req, res, ctx) => { - return res(ctx.json({ data: makeNewWiki(req.body) })) - }), - rest.post(/\/api\/wiki\/delete$/, (req, res, ctx) => { - const wikiId = req.body.wiki - const wikiIndex = myWikis.findIndex(w => w.id === Number(wikiId)) - if (wikiIndex < 0) { - return res(ctx.status(404)) - } - removeWiki(wikiIndex) - return res(ctx.status(200)) - }), - rest.post(/\/api\/wiki\/logo\/update$/, (_, res, ctx) => res(ctx.status(200))), - rest.post(/\/api\/wiki\/setting\/.*?\/update$/, (_, res, ctx) => res(ctx.status(200))), - rest.post(/\/api\/wiki\/details$/, (req, res, ctx) => { - const wikiId = req.body.wiki - const wikiDetails = myWikis.find(w => w.id === Number(wikiId)) - if (!wikiDetails) { - return res(ctx.status(404)) - } - return res(ctx.json({ data: wikiDetails }), ctx.status(200)) - }), - rest.get(/\/api\/wiki$/, (req, res, ctx) => { - return res(ctx.json(wikiDiscovery(req.referrer, req.url.searchParams))) - }), -] +] \ No newline at end of file diff --git a/src/backend/mocks/old.default_handlers.js b/src/backend/mocks/old.default_handlers.js new file mode 100644 index 00000000..42278322 --- /dev/null +++ b/src/backend/mocks/old.default_handlers.js @@ -0,0 +1,226 @@ +// this file exists only as reference until all +// legacy handlers are migrated to default_handlers.js + +import { rest } from 'msw' + +let myWikis = JSON.parse(localStorage.getItem('msw-myWikis')) || [] +let lastWikiId = (myWikis.length && myWikis[myWikis.length - 1].id) || 0 +let user = makeUser() +let getEntityImportCalledTimes = 0 + +function makeUser (email = 'test@local') { + return { + id: 1, + email, + verified: true, + created_at: '2020-01-01', + updated_at: '2020-01-01', + } +} + +const makeNewWiki = ({ domain, sitename }) => { + const newWiki = { + id: ++lastWikiId, + domain, + sitename, + deleted_at: null, + created_at: '2020-01-01', + updated_at: '2020-01-01', + pivot: { + user_id: user.id, + wiki_id: lastWikiId, + }, + wiki_managers: [{ + email: user.email, + pivot: { + user_id: user.id, + wiki_id: lastWikiId, + }, + }], + wiki_db_version: { + id: 101, + wiki_id: lastWikiId, + version: 'mw1.33-wbs1', + }, + public_settings: [], + } + + myWikis.push(newWiki) + localStorage.setItem('msw-myWikis', JSON.stringify(myWikis)) + + return newWiki +} + +const removeWiki = wikiIndex => { + myWikis = myWikis.splice(wikiIndex, 1) + localStorage.setItem('msw-myWikis', JSON.stringify(myWikis)) +} + +const wikiDiscovery = (referrer, params) => { + const pseudorandom = { + seed: 1, + next: function () { + const x = Math.sin(this.seed++) * 10000 + return x - Math.floor(x) + }, + } + + const names = [ + 'Wikibase Name', + 'A Very Long Wikibase Name', + ] + + let wikis = [...Array(75).keys()].map((id) => { + const wiki = { + id, + domain: id + '-wikibase.wbaas.localhost', + sitename: id + ' - ' + names[id % names.length], + wiki_site_stats: null, + logo_url: null, + } + + if (pseudorandom.next() >= 0.1) { + wiki.wiki_site_stats = { + pages: Math.ceil(pseudorandom.next() * 250), + } + } + + if (pseudorandom.next() >= 0.5) { + wiki.logo_url = new URL(referrer).origin + '/favicon.ico' + } + return wiki + }) + + if (parseInt(params.get('is_active'))) { + wikis = wikis.filter((wiki) => { + const stats = wiki.wiki_site_stats + return stats && stats.pages > 1 + }) + } + + if (params.get('sort') === 'sitename') { + wikis = wikis.sort((a, b) => { + let sort = a.sitename.localeCompare(b.sitename, 'en', { numeric: true }) + if (params.get('direction') === 'desc') { + sort *= -1 + } + return sort + }) + } + + if (params.get('sort') === 'pages') { + wikis = wikis.sort((a, b) => { + const aPages = a.wiki_site_stats ? a.wiki_site_stats.pages : 0 + const bPages = b.wiki_site_stats ? b.wiki_site_stats.pages : 0 + if (params.get('direction') === 'desc') { + return bPages - aPages + } + return aPages - bPages + }) + } + + const currentPage = parseInt(params.get('page')) + const resultsPerPage = parseInt(params.get('per_page')) + const start = (currentPage - 1) * resultsPerPage + const end = start + resultsPerPage + + return { + data: wikis.slice(start, end), + meta: { + last_page: Math.ceil(wikis.length / resultsPerPage), + total: wikis.length, + }, + } +} + +export const handlers = [ + /* User endpoints */ + rest.post(/\/api\/auth\/login$/, (req, res, ctx) => { + user = makeUser(req.body.email) + return res(ctx.json({ + user, + }), ctx.cookie('authToken', 'token_value')) + }), + rest.get(/\/api\/auth\/login$/, (req, res, ctx) => { + const { authToken } = req.cookies + if (authToken !== 'token_value') { + return res(ctx.status(401)) + } + user = makeUser(req.body.email) + return res(ctx.json({ + user, + })) + }), + rest.delete(/\/api\/auth\/login$/, (req, res, ctx) => { + user = makeUser(req.body.email) + return res(ctx.status(204)) + }), + rest.post(/\/api\/user\/forgotPassword$/, (req, res, ctx) => { + if (req.body.email === 'serverError@example.com') { + return res(ctx.status(400, 'Mocked Server Error')) + } + return res(ctx.status(200)) + }, + ), + rest.post(/\/api\/user\/resetPassword$/, (_, res, ctx) => res(ctx.status(200))), + rest.post(/\/api\/user\/sendVerifyEmail$/, (_, res, ctx) => res(ctx.json({ message: 'Already verified' }))), + rest.post(/\/api\/user\/verifyEmail$/, (_, res, ctx) => res(ctx.status(200))), + rest.post(/\/api\/complaint\/sendMessage$/, (req, res, ctx) => res(ctx.status(200))), + rest.post(/\/api\/contact\/sendMessage$/, (req, res, ctx) => { + if (req.body.name === 'recaptchaError') { + return res(ctx.status(401, 'Mocked recaptcha Error')) + } + if (req.body.name === '') { + return res(ctx.status(400, 'Mocked empty name Error')) + } + if (req.body.recaptcha === '') { + return res(ctx.status(400, 'Mocked recaptcha empty Error')) + } + return res(ctx.status(200)) + }, + ), + + /* Wiki endpoints */ + rest.post(/\/api\/wiki\/entityImport$/, (_, res, ctx) => { + return res(ctx.json({ data: { status: 'pending', payload: {}, started_at: new Date().toJSON() } })) + }), + rest.get(/\/api\/wiki\/entityImport$/, (_, res, ctx) => { + getEntityImportCalledTimes++ + switch (getEntityImportCalledTimes) { + case 1: + return res(ctx.json({ data: [] })) + case 2: + return res(ctx.json({ data: [{ status: 'pending' }] })) + default: + return res(ctx.json({ data: [{ status: 'success' }] })) + } + }), + rest.get(/\/api\/wiki\/count$/, (_, res, ctx) => res(ctx.json({ data: 1 }))), + rest.post(/\/api\/wiki\/mine$/, (_, res, ctx) => res(ctx.json({ wikis: myWikis, count: myWikis.length, limit: false }))), + rest.post(/\/api\/wiki\/create$/, (req, res, ctx) => { + return res(ctx.json({ data: makeNewWiki(req.body) })) + }), + rest.post(/\/api\/wiki\/delete$/, (req, res, ctx) => { + const wikiId = req.body.wiki + const wikiIndex = myWikis.findIndex(w => w.id === Number(wikiId)) + if (wikiIndex < 0) { + return res(ctx.status(404)) + } + + removeWiki(wikiIndex) + return res(ctx.status(200)) + }), + rest.post(/\/api\/wiki\/logo\/update$/, (_, res, ctx) => res(ctx.status(200))), + rest.post(/\/api\/wiki\/setting\/.*?\/update$/, (_, res, ctx) => res(ctx.status(200))), + rest.post(/\/api\/wiki\/details$/, (req, res, ctx) => { + const wikiId = req.body.wiki + const wikiDetails = myWikis.find(w => w.id === Number(wikiId)) + if (!wikiDetails) { + return res(ctx.status(404)) + } + return res(ctx.json({ data: wikiDetails }), ctx.status(200)) + }), + rest.get(/\/api\/wiki$/, (req, res, ctx) => { + return res(ctx.json(wikiDiscovery(req.referrer, req.url.searchParams))) + }), +] From aec176851e072521c71f2dbd3617de151c205465 Mon Sep 17 00:00:00 2001 From: dena Date: Fri, 28 Aug 2026 13:28:00 +0200 Subject: [PATCH 4/5] linting --- src/backend/mocks/default_handlers.js | 4 ++-- src/store/policies.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/backend/mocks/default_handlers.js b/src/backend/mocks/default_handlers.js index 61d7941c..64a18d4e 100644 --- a/src/backend/mocks/default_handlers.js +++ b/src/backend/mocks/default_handlers.js @@ -1,4 +1,4 @@ -import { http, HttpResponse } from 'msw' +// import { http, HttpResponse } from 'msw' // implementations for msw v2 handlers go in this file // see old.default_handlers.js for reference @@ -6,4 +6,4 @@ import { http, HttpResponse } from 'msw' export const handlers = [ -] \ No newline at end of file +] diff --git a/src/store/policies.js b/src/store/policies.js index 944a707b..bfaf01d7 100644 --- a/src/store/policies.js +++ b/src/store/policies.js @@ -31,7 +31,7 @@ const actions = { // User is prompted to reload the page, which will reset this state commit('failedToGetMissingPolicies', true) dispatch('reportLatestError', { - error: error, + error, message: 'Failed to get policies. Please try reloading the page.', dismissible: false, }) @@ -43,7 +43,7 @@ const actions = { await dispatch('fetchMissingPolicies') } catch (error) { dispatch('reportLatestError', { - error: error, + error, message: 'Failed to save policy acceptances. Please try again.', }) } From f566f40e11ea18274694f3349940be83cb7cfba9 Mon Sep 17 00:00:00 2001 From: dena Date: Fri, 28 Aug 2026 14:24:37 +0200 Subject: [PATCH 5/5] unstage unrelated file --- src/store/policies.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/store/policies.js b/src/store/policies.js index bfaf01d7..944a707b 100644 --- a/src/store/policies.js +++ b/src/store/policies.js @@ -31,7 +31,7 @@ const actions = { // User is prompted to reload the page, which will reset this state commit('failedToGetMissingPolicies', true) dispatch('reportLatestError', { - error, + error: error, message: 'Failed to get policies. Please try reloading the page.', dismissible: false, }) @@ -43,7 +43,7 @@ const actions = { await dispatch('fetchMissingPolicies') } catch (error) { dispatch('reportLatestError', { - error, + error: error, message: 'Failed to save policy acceptances. Please try again.', }) }