Skip to content

Commit

Permalink
Fix Sinon integration
Browse files Browse the repository at this point in the history
  • Loading branch information
djhi committed Jun 4, 2024
1 parent 5570922 commit 7e8e47e
Show file tree
Hide file tree
Showing 10 changed files with 339 additions and 65 deletions.
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ run-msw:
run-fetch-mock:
@NODE_ENV=development VITE_MOCK=fetch-mock npm run dev

run-sinon:
@NODE_ENV=development VITE_MOCK=sinon npm run dev

watch:
@NODE_ENV=development npm run build --watch

Expand Down
4 changes: 2 additions & 2 deletions example/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import React from 'react';
import {
Admin,
Create,
type DataProvider,
EditGuesser,
ListGuesser,
Resource,
ShowGuesser,
} from 'react-admin';
import { QueryClient } from 'react-query';
import { dataProvider } from './dataProvider';

const queryClient = new QueryClient({
defaultOptions: {
Expand All @@ -18,7 +18,7 @@ const queryClient = new QueryClient({
},
});

export const App = () => {
export const App = ({ dataProvider }: { dataProvider: DataProvider }) => {
return (
<Admin
dataProvider={dataProvider}
Expand Down
10 changes: 9 additions & 1 deletion example/authProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,15 @@ export const authProvider: AuthProvider = {
localStorage.removeItem('user');
return Promise.resolve();
},
checkError: () => Promise.resolve(),
checkError: (error) => {
const status = error.status;
if (status === 401 || status === 403) {
localStorage.removeItem('auth');
return Promise.reject();
}
// other error code (404, 500, etc): no need to log out
return Promise.resolve();
},
checkAuth: () =>
localStorage.getItem('user') ? Promise.resolve() : Promise.reject(),
getPermissions: () => {
Expand Down
9 changes: 7 additions & 2 deletions example/fetchMock.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import fetchMock from 'fetch-mock';
import FakeRest from 'fakerest';
import { FetchServer, withDelay } from 'fakerest';
import { data } from './data';
import { dataProvider as defaultDataProvider } from './dataProvider';

export const initializeFetchMock = () => {
const restServer = new FakeRest.FetchServer({
const restServer = new FetchServer({
baseUrl: 'http://localhost:3000',
data,
loggingEnabled: true,
Expand All @@ -12,6 +13,8 @@ export const initializeFetchMock = () => {
// @ts-ignore
window.restServer = restServer; // give way to update data in the console
}

restServer.addMiddleware(withDelay(300));
restServer.addMiddleware(async (request, context, next) => {
if (!request.headers?.get('Authorization')) {
return new Response(null, { status: 401 });
Expand All @@ -32,3 +35,5 @@ export const initializeFetchMock = () => {
});
fetchMock.mock('begin:http://localhost:3000', restServer.getHandler());
};

export const dataProvider = defaultDataProvider;
34 changes: 27 additions & 7 deletions example/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,39 @@ import { App } from './App';
switch (import.meta.env.VITE_MOCK) {
case 'fetch-mock':
import('./fetchMock')
.then(({ initializeFetchMock }) => {
.then(({ initializeFetchMock, dataProvider }) => {
initializeFetchMock();
return dataProvider;
})
.then(() => {
ReactDom.render(<App />, document.getElementById('root'));
.then((dataProvider) => {
ReactDom.render(
<App dataProvider={dataProvider} />,
document.getElementById('root'),
);
});
break;
case 'sinon':
import('./sinon')
.then(({ initializeSinon, dataProvider }) => {
initializeSinon();
return dataProvider;
})
.then((dataProvider) => {
ReactDom.render(
<App dataProvider={dataProvider} />,
document.getElementById('root'),
);
});
break;
default:
import('./msw')
.then(({ worker }) => {
return worker.start();
.then(({ worker, dataProvider }) => {
return worker.start().then(() => dataProvider);
})
.then(() => {
ReactDom.render(<App />, document.getElementById('root'));
.then((dataProvider) => {
ReactDom.render(
<App dataProvider={dataProvider} />,
document.getElementById('root'),
);
});
}
7 changes: 5 additions & 2 deletions example/msw.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { setupWorker } from 'msw/browser';
import { HttpResponse } from 'msw';
import { MswServer, withDelay } from '../src/FakeRest';
import { data } from './data';
import { HttpResponse } from 'msw';
import { dataProvider as defaultDataProvider } from './dataProvider';

const restServer = new MswServer({
baseUrl: 'http://localhost:3000',
data,
});

restServer.addMiddleware(withDelay(5000));
restServer.addMiddleware(withDelay(300));
restServer.addMiddleware(async (request, context, next) => {
if (!request.headers?.get('Authorization')) {
throw new HttpResponse(null, { status: 401 });
Expand All @@ -29,3 +30,5 @@ restServer.addMiddleware(async (request, context, next) => {
});

export const worker = setupWorker(...restServer.getHandlers());

export const dataProvider = defaultDataProvider;
206 changes: 206 additions & 0 deletions example/sinon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import sinon from 'sinon';
import { SinonServer } from '../src/FakeRest';
import { data } from './data';
import { type DataProvider, HttpError } from 'react-admin';

export const initializeSinon = () => {
const restServer = new SinonServer({
baseUrl: 'http://localhost:3000',
data,
loggingEnabled: true,
});

restServer.addMiddleware((request, context, next) => {
if (request.requestHeaders.Authorization === undefined) {
request.respond(401, {}, 'Unauthorized');
return null;
}

return next(request, context);
});

// use sinon.js to monkey-patch XmlHttpRequest
const server = sinon.fakeServer.create();
// this is required when doing asynchronous XmlHttpRequest
server.autoRespond = true;
if (window) {
// @ts-ignore
window.restServer = restServer; // give way to update data in the console
// @ts-ignore
window.sinonServer = server; // give way to update data in the console
}
server.respondWith(restServer.getHandler());
};

export const dataProvider: DataProvider = {
async getList(resource, params) {
const { page, perPage } = params.pagination;
const { field, order } = params.sort;

const rangeStart = (page - 1) * perPage;
const rangeEnd = page * perPage - 1;
const query = {
sort: JSON.stringify([field, order]),
range: JSON.stringify([rangeStart, rangeEnd]),
filter: JSON.stringify(params.filter),
};
const json = await sendRequest(
`http://localhost:3000/${resource}?${new URLSearchParams(query)}`,
);
return {
data: json.json,
total: Number.parseInt(
json.headers['Content-Range'].split('/').pop() ?? '0',
10,
),
};
},
async getMany(resource, params) {
const query = {
filter: JSON.stringify({ id: params.ids }),
};
const json = await sendRequest(
`http://localhost:3000/${resource}?${new URLSearchParams(query)}`,
);
return {
data: json.json,
};
},
async getManyReference(resource, params) {
const { page, perPage } = params.pagination;
const { field, order } = params.sort;
const rangeStart = (page - 1) * perPage;
const rangeEnd = page * perPage - 1;
const query = {
sort: JSON.stringify([field, order]),
range: JSON.stringify([rangeStart, rangeEnd]),
filter: JSON.stringify({
...params.filter,
[params.target]: params.id,
}),
};
const json = await sendRequest(
`http://localhost:3000/${resource}?${new URLSearchParams(query)}`,
);
return {
data: json.json,
total: Number.parseInt(
json.headers['Content-Range'].split('/').pop() ?? '0',
10,
),
};
},
async getOne(resource, params) {
const json = await sendRequest(
`http://localhost:3000/${resource}/${params.id}`,
);
return {
data: json.json,
};
},
async create(resource, params) {
const json = await sendRequest(
`http://localhost:3000/${resource}`,
'POST',
JSON.stringify(params.data),
);
return {
data: json.json,
};
},
async update(resource, params) {
const json = await sendRequest(
`http://localhost:3000/${resource}/${params.id}`,
'PUT',
JSON.stringify(params.data),
);
return {
data: json.json,
};
},
async updateMany(resource, params) {
return Promise.all(
params.ids.map((id) =>
this.update(resource, { id, data: params.data }),
),
).then((responses) => ({ data: responses.map(({ json }) => json.id) }));
},
async delete(resource, params) {
const json = await sendRequest(
`http://localhost:3000/${resource}/${params.id}`,
'DELETE',
null,
);
return {
data: json.json,
};
},
async deleteMany(resource, params) {
return Promise.all(
params.ids.map((id) => this.delete(resource, { id })),
).then((responses) => ({
data: responses.map(({ data }) => data.id),
}));
},
};

const sendRequest = (
url: string,
method = 'GET',
body: any = null,
): Promise<any> => {
const request = new XMLHttpRequest();
request.open(method, url);

const persistedUser = localStorage.getItem('user');
const user = persistedUser ? JSON.parse(persistedUser) : null;
if (user) {
request.setRequestHeader('Authorization', `Bearer ${user.id}`);
}

// add content-type header
request.overrideMimeType('application/json');
request.send(body);

return new Promise((resolve, reject) => {
request.onloadend = (e) => {
let json: any;
try {
json = JSON.parse(request.responseText);
} catch (e) {
// not json, no big deal
}
// Get the raw header string
const headers = request.getAllResponseHeaders();

// Convert the header string into an array
// of individual headers
const arr = headers.trim().split(/[\r\n]+/);

// Create a map of header names to values
const headerMap: Record<string, string> = {};
for (const line of arr) {
const parts = line.split(': ');
const header = parts.shift();
if (!header) continue;
const value = parts.join(': ');
headerMap[header] = value;
}
if (request.status < 200 || request.status >= 300) {
return reject(
new HttpError(
json?.message || request.statusText,
request.status,
json,
),
);
}
resolve({
status: request.status,
headers: headerMap,
body: request.responseText,
json,
});
};
});
};
2 changes: 1 addition & 1 deletion example/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/// <reference types="vite/client" />

interface ImportMetaEnv {
readonly VITE_MOCK: 'msw' | 'fetch-mock';
readonly VITE_MOCK: 'msw' | 'fetch-mock' | 'sinon';
}

interface ImportMeta {
Expand Down
Loading

0 comments on commit 7e8e47e

Please sign in to comment.