diff --git a/src/backend/mocks/default_handlers.js b/src/backend/mocks/default_handlers.js index 64a18d4e..299d1de4 100644 --- a/src/backend/mocks/default_handlers.js +++ b/src/backend/mocks/default_handlers.js @@ -1,9 +1 @@ -// import { http, HttpResponse } from 'msw' - -// 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 = [ - -] +export { handlers } from './handlers' diff --git a/src/backend/mocks/handlers/auth.js b/src/backend/mocks/handlers/auth.js new file mode 100644 index 00000000..328eff00 --- /dev/null +++ b/src/backend/mocks/handlers/auth.js @@ -0,0 +1,47 @@ +import { rest } from 'msw' +import { makeUser } from './shared' +import { mockState } from './state' + +export const authHandlers = [ + rest.post(/\/api\/auth\/login$/, (req, res, ctx) => { + mockState.user = makeUser(req.body.email) + return res(ctx.json({ + user: mockState.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)) + } + return res(ctx.json({ + user: mockState.user, + })) + }), + rest.delete(/\/api\/auth\/login$/, (_, res, ctx) => { + mockState.user = makeUser() + 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$/, (_, 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)) + }), +] diff --git a/src/backend/mocks/handlers/index.js b/src/backend/mocks/handlers/index.js new file mode 100644 index 00000000..3ced186f --- /dev/null +++ b/src/backend/mocks/handlers/index.js @@ -0,0 +1,6 @@ +export const handlers = typeof Response === 'undefined' + ? [] + : [ + ...require('./auth').authHandlers, + ...require('./wiki').wikiHandlers, + ] diff --git a/src/backend/mocks/handlers/shared.js b/src/backend/mocks/handlers/shared.js new file mode 100644 index 00000000..95056dc2 --- /dev/null +++ b/src/backend/mocks/handlers/shared.js @@ -0,0 +1,98 @@ +export function makeUser (email = 'test@local') { + return { + id: 1, + email, + verified: true, + created_at: '2020-01-01', + updated_at: '2020-01-01', + } +} + +export function getStoredWikis () { + try { + return JSON.parse(localStorage.getItem('msw-myWikis')) || [] + } catch (error) { + return [] + } +} + +export function persistWikis (wikis) { + localStorage.setItem('msw-myWikis', JSON.stringify(wikis)) +} + +export function 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, + }, + } +} diff --git a/src/backend/mocks/handlers/state.js b/src/backend/mocks/handlers/state.js new file mode 100644 index 00000000..8918090e --- /dev/null +++ b/src/backend/mocks/handlers/state.js @@ -0,0 +1,27 @@ +import { makeUser, getStoredWikis, persistWikis } from './shared' + +export const mockState = { + user: makeUser(), + myWikis: getStoredWikis(), + entityImportCalledTimes: 0, +} + +export function syncMyWikis () { + mockState.myWikis = getStoredWikis() +} + +export function nextWikiId () { + const lastWikiId = mockState.myWikis.reduce((max, wiki) => { + const id = Number(wiki.id) + return Number.isFinite(id) ? Math.max(max, id) : max + }, 0) + + return lastWikiId + 1 +} + +export function resetMockData () { + mockState.user = makeUser() + mockState.myWikis = [] + mockState.entityImportCalledTimes = 0 + persistWikis(mockState.myWikis) +} diff --git a/src/backend/mocks/handlers/wiki.js b/src/backend/mocks/handlers/wiki.js new file mode 100644 index 00000000..db9a60f2 --- /dev/null +++ b/src/backend/mocks/handlers/wiki.js @@ -0,0 +1,87 @@ +import { rest } from 'msw' +import { mockState, nextWikiId, syncMyWikis } from './state' +import { persistWikis, wikiDiscovery } from './shared' + +const makeNewWiki = ({ domain, sitename }) => { + const wikiId = nextWikiId() + const newWiki = { + id: wikiId, + domain, + sitename, + deleted_at: null, + created_at: '2020-01-01', + updated_at: '2020-01-01', + pivot: { + user_id: mockState.user.id, + wiki_id: wikiId, + }, + wiki_managers: [{ + email: mockState.user.email, + pivot: { + user_id: mockState.user.id, + wiki_id: wikiId, + }, + }], + wiki_db_version: { + id: 101, + wiki_id: wikiId, + version: 'mw1.33-wbs1', + }, + public_settings: [], + } + + mockState.myWikis.push(newWiki) + persistWikis(mockState.myWikis) + + return newWiki +} + +export const wikiHandlers = [ + 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) => { + mockState.entityImportCalledTimes += 1 + switch (mockState.entityImportCalledTimes) { + 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) => { + syncMyWikis() + return res(ctx.json({ wikis: mockState.myWikis, count: mockState.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 = mockState.myWikis.findIndex(w => w.id === Number(wikiId)) + if (wikiIndex < 0) { + return res(ctx.status(404)) + } + + mockState.myWikis.splice(wikiIndex, 1) + persistWikis(mockState.myWikis) + 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) => { + syncMyWikis() + const wikiId = req.body.wiki + const wikiDetails = mockState.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))) + }), +] diff --git a/src/backend/mocks/index.js b/src/backend/mocks/index.js new file mode 100644 index 00000000..259c62f1 --- /dev/null +++ b/src/backend/mocks/index.js @@ -0,0 +1,5 @@ +export { MOCK_SCENARIOS, getMockScenario, setMockScenario, resetMockState } from './state' + +export const handlers = typeof Response === 'undefined' + ? [] + : require('./handlers').handlers diff --git a/src/backend/mocks/state.js b/src/backend/mocks/state.js new file mode 100644 index 00000000..eab5975b --- /dev/null +++ b/src/backend/mocks/state.js @@ -0,0 +1,35 @@ +const STORAGE_KEY = 'msw-scenario' +const DEFAULT_SCENARIO = 'happy-path' + +export const MOCK_SCENARIOS = [ + 'happy-path', + 'empty-state', + 'auth-error', + 'server-error', +] + +export function getMockScenario () { + if (typeof localStorage === 'undefined') { + return DEFAULT_SCENARIO + } + + const saved = localStorage.getItem(STORAGE_KEY) + return MOCK_SCENARIOS.includes(saved) ? saved : DEFAULT_SCENARIO +} + +export function setMockScenario (scenario) { + if (!MOCK_SCENARIOS.includes(scenario)) { + throw new Error(`Unsupported mock scenario: ${scenario}`) + } + + localStorage.setItem(STORAGE_KEY, scenario) + return getMockScenario() +} + +export function resetMockState () { + if (typeof localStorage !== 'undefined') { + localStorage.removeItem(STORAGE_KEY) + localStorage.removeItem('msw-myWikis') + } + return DEFAULT_SCENARIO +} diff --git a/tests/unit/mocks.spec.js b/tests/unit/mocks.spec.js new file mode 100644 index 00000000..141c851c --- /dev/null +++ b/tests/unit/mocks.spec.js @@ -0,0 +1,26 @@ +import { getMockScenario, setMockScenario, resetMockState } from '@/backend/mocks' +import { handlers as modularHandlers } from '@/backend/mocks/handlers' +import { handlers as publicHandlers } from '@/backend/mocks' + +describe('MSW mock configuration', () => { + beforeEach(() => { + localStorage.clear() + resetMockState() + }) + + it('defaults to the happy-path scenario', () => { + expect(getMockScenario()).toBe('happy-path') + }) + + it('allows switching the active scenario in local storage', () => { + setMockScenario('empty-state') + + expect(getMockScenario()).toBe('empty-state') + }) + + it('uses the same shared handler registry for browser and tests', () => { + expect(Array.isArray(publicHandlers)).toBe(true) + expect(Array.isArray(modularHandlers)).toBe(true) + expect(modularHandlers).toEqual(publicHandlers) + }) +})