diff --git a/example/sinon.ts b/example/sinon.ts index 2719334..9031490 100644 --- a/example/sinon.ts +++ b/example/sinon.ts @@ -1,5 +1,5 @@ import sinon from 'sinon'; -import { SinonServer } from '../src/FakeRest'; +import { SinonServer, withDelay } from '../src/FakeRest'; import { data } from './data'; import { HttpError, type Options } from 'react-admin'; import simpleRestProvider from 'ra-data-simple-rest'; @@ -11,16 +11,19 @@ export const initializeSinon = () => { loggingEnabled: true, }); - restServer.addMiddleware((request, context, next) => { + restServer.addMiddleware(withDelay(3000)); + restServer.addMiddleware(async (request, context, next) => { if (request.requestHeaders.Authorization === undefined) { - request.respond(401, {}, 'Unauthorized'); - return null; + return { + status: 401, + headers: {}, + }; } return next(request, context); }); - restServer.addMiddleware((request, context, next) => { + restServer.addMiddleware(async (request, context, next) => { if (context.collection === 'books' && request.method === 'POST') { if ( restServer.collections[context.collection].getCount({ @@ -29,15 +32,15 @@ export const initializeSinon = () => { }, }) > 0 ) { - request.respond( - 401, - {}, - JSON.stringify({ + return { + status: 400, + headers: {}, + body: { errors: { title: 'An article with this title already exists. The title must be unique.', }, - }), - ); + }, + }; } } diff --git a/src/BaseServer.ts b/src/BaseServer.ts index 5c67d12..bea0f7b 100644 --- a/src/BaseServer.ts +++ b/src/BaseServer.ts @@ -26,20 +26,6 @@ export abstract class BaseServer< throw new Error('Not implemented'); } - extractContextSync( - request: RequestType, - ): Pick { - throw new Error('Not implemented'); - } - - respondSync( - response: BaseResponse | null, - request: RequestType, - context: FakeRestContext, - ): ResponseType { - throw new Error('Not implemented'); - } - async handle(request: RequestType): Promise { const context = this.getContext(await this.extractContext(request)); @@ -70,41 +56,6 @@ export abstract class BaseServer< } } - handleSync(request: RequestType): ResponseType | undefined { - const context = this.getContext(this.extractContextSync(request)); - - // Call middlewares - let index = 0; - const middlewares = [...this.middlewares]; - - const next = (req: RequestType, ctx: FakeRestContext) => { - const middleware = middlewares[index++]; - if (middleware) { - return middleware(req, ctx, next); - } - - return this.handleRequest(req, ctx); - }; - - try { - const response = next(request, context); - if (response instanceof Promise) { - throw new Error( - 'Middleware returned a promise in a sync context', - ); - } - if (response != null) { - return this.respondSync(response, request, context); - } - } catch (error) { - if (error instanceof Error) { - throw error; - } - - return error as ResponseType; - } - } - handleRequest(request: RequestType, ctx: FakeRestContext): BaseResponse { // Handle Single Objects for (const name of this.getSingleNames()) { diff --git a/src/adapters/SinonServer.spec.ts b/src/adapters/SinonServer.spec.ts index c403d9b..1114164 100644 --- a/src/adapters/SinonServer.spec.ts +++ b/src/adapters/SinonServer.spec.ts @@ -169,7 +169,7 @@ describe('SinonServer', () => { }); describe('addMiddleware', () => { - it('should allow request transformation', () => { + it('should allow request transformation', async () => { const server = new SinonServer(); server.addMiddleware((request, context, next) => { const start = context.params?._start @@ -197,7 +197,7 @@ describe('SinonServer', () => { let request: SinonFakeXMLHttpRequest | null; request = getFakeXMLHTTPRequest('GET', '/foo?_start=1&_end=1'); if (request == null) throw new Error('request is null'); - server.handleSync(request); + await server.handle(request); expect(request?.status).toEqual(206); // @ts-ignore expect(request.responseText).toEqual('[{"id":1,"name":"foo"}]'); @@ -206,7 +206,7 @@ describe('SinonServer', () => { ); request = getFakeXMLHTTPRequest('GET', '/foo?_start=2&_end=2'); if (request == null) throw new Error('request is null'); - server.handleSync(request); + await server.handle(request); expect(request?.status).toEqual(206); // @ts-ignore expect(request?.responseText).toEqual('[{"id":2,"name":"bar"}]'); @@ -215,7 +215,7 @@ describe('SinonServer', () => { ); }); - it('should allow response transformation', () => { + it('should allow response transformation', async () => { const server = new SinonServer(); server.addMiddleware((request, context, next) => { const response = next(request, context); @@ -241,7 +241,7 @@ describe('SinonServer', () => { ); const request = getFakeXMLHTTPRequest('GET', '/foo'); if (request == null) throw new Error('request is null'); - server.handleSync(request); + await server.handle(request); expect(request.status).toEqual(418); // @ts-ignore expect(request.responseText).toEqual( @@ -249,7 +249,7 @@ describe('SinonServer', () => { ); }); - it('should pass request in response interceptor', () => { + it('should pass request in response interceptor', async () => { const server = new SinonServer(); let requestUrl: string | undefined; server.addMiddleware((request, context, next) => { @@ -260,22 +260,22 @@ describe('SinonServer', () => { const request = getFakeXMLHTTPRequest('GET', '/foo'); if (request == null) throw new Error('request is null'); - server.handleSync(request); + await server.handle(request); expect(requestUrl).toEqual('/foo'); }); }); describe('handle', () => { - it('should respond a 404 to GET /whatever on non existing collection', () => { + it('should respond a 404 to GET /whatever on non existing collection', async () => { const server = new SinonServer(); const request = getFakeXMLHTTPRequest('GET', '/foo'); if (request == null) throw new Error('request is null'); - server.handleSync(request); + await server.handle(request); expect(request.status).toEqual(404); // not responded }); - it('should respond to GET /foo by sending all items in collection foo', () => { + it('should respond to GET /foo by sending all items in collection foo', async () => { const server = new SinonServer(); server.addCollection( 'foo', @@ -288,7 +288,7 @@ describe('SinonServer', () => { ); const request = getFakeXMLHTTPRequest('GET', '/foo'); if (request == null) throw new Error('request is null'); - server.handleSync(request); + await server.handle(request); expect(request.status).toEqual(200); // @ts-ignore expect(request.responseText).toEqual( @@ -302,7 +302,7 @@ describe('SinonServer', () => { ); }); - it('should respond to GET /foo?queryString by sending all items in collection foo satisfying query', () => { + it('should respond to GET /foo?queryString by sending all items in collection foo satisfying query', async () => { const server = new SinonServer(); server.addCollection( 'foos', @@ -323,7 +323,7 @@ describe('SinonServer', () => { '/foos?filter={"arg":true}&sort=name&slice=[0,10]&embed=["bars"]', ); if (request == null) throw new Error('request is null'); - server.handleSync(request); + await server.handle(request); expect(request.status).toEqual(200); // @ts-ignore expect(request.responseText).toEqual( @@ -337,7 +337,7 @@ describe('SinonServer', () => { ); }); - it('should respond to GET /foo?queryString with pagination by sending the correct content-range header', () => { + it('should respond to GET /foo?queryString with pagination by sending the correct content-range header', async () => { const server = new SinonServer(); server.addCollection( 'foo', @@ -348,40 +348,40 @@ describe('SinonServer', () => { let request: SinonFakeXMLHttpRequest | null; request = getFakeXMLHTTPRequest('GET', '/foo'); if (request == null) throw new Error('request is null'); - server.handleSync(request); + await server.handle(request); expect(request.status).toEqual(200); expect(request.getResponseHeader('Content-Range')).toEqual( 'items 0-10/11', ); request = getFakeXMLHTTPRequest('GET', '/foo?range=[0,4]'); if (request == null) throw new Error('request is null'); - server.handleSync(request); + await server.handle(request); expect(request.status).toEqual(206); expect(request.getResponseHeader('Content-Range')).toEqual( 'items 0-4/11', ); request = getFakeXMLHTTPRequest('GET', '/foo?range=[5,9]'); if (request == null) throw new Error('request is null'); - server.handleSync(request); + await server.handle(request); expect(request.status).toEqual(206); expect(request.getResponseHeader('Content-Range')).toEqual( 'items 5-9/11', ); request = getFakeXMLHTTPRequest('GET', '/foo?range=[10,14]'); if (request == null) throw new Error('request is null'); - server.handleSync(request); + await server.handle(request); expect(request.status).toEqual(206); expect(request.getResponseHeader('Content-Range')).toEqual( 'items 10-10/11', ); }); - it('should respond to GET /foo on an empty collection with a []', () => { + it('should respond to GET /foo on an empty collection with a []', async () => { const server = new SinonServer(); server.addCollection('foo', new Collection()); const request = getFakeXMLHTTPRequest('GET', '/foo'); if (request == null) throw new Error('request is null'); - server.handleSync(request); + await server.handle(request); expect(request.status).toEqual(200); // @ts-ignore expect(request.responseText).toEqual('[]'); @@ -390,7 +390,7 @@ describe('SinonServer', () => { ); }); - it('should respond to POST /foo by adding an item to collection foo', () => { + it('should respond to POST /foo by adding an item to collection foo', async () => { const server = new SinonServer(); server.addCollection( 'foo', @@ -407,7 +407,7 @@ describe('SinonServer', () => { JSON.stringify({ name: 'baz' }), ); if (request == null) throw new Error('request is null'); - server.handleSync(request); + await server.handle(request); expect(request.status).toEqual(201); // @ts-ignore expect(request.responseText).toEqual('{"name":"baz","id":3}'); @@ -422,7 +422,7 @@ describe('SinonServer', () => { ]); }); - it('should respond to POST /foo by adding an item to collection foo, even if the collection does not exist', () => { + it('should respond to POST /foo by adding an item to collection foo, even if the collection does not exist', async () => { const server = new SinonServer(); const request = getFakeXMLHTTPRequest( 'POST', @@ -430,7 +430,7 @@ describe('SinonServer', () => { JSON.stringify({ name: 'baz' }), ); if (request == null) throw new Error('request is null'); - server.handleSync(request); + await server.handle(request); expect(request.status).toEqual(201); // @ts-ignore expect(request.responseText).toEqual('{"name":"baz","id":0}'); @@ -441,7 +441,7 @@ describe('SinonServer', () => { expect(server.getAll('foo')).toEqual([{ id: 0, name: 'baz' }]); }); - it('should respond to GET /foo/:id by sending element of identifier id in collection foo', () => { + it('should respond to GET /foo/:id by sending element of identifier id in collection foo', async () => { const server = new SinonServer(); server.addCollection( 'foo', @@ -454,7 +454,7 @@ describe('SinonServer', () => { ); const request = getFakeXMLHTTPRequest('GET', '/foo/2'); if (request == null) throw new Error('request is null'); - server.handleSync(request); + await server.handle(request); expect(request.status).toEqual(200); // @ts-ignore expect(request.responseText).toEqual('{"id":2,"name":"bar"}'); @@ -463,16 +463,16 @@ describe('SinonServer', () => { ); }); - it('should respond to GET /foo/:id on a non-existing id with a 404', () => { + it('should respond to GET /foo/:id on a non-existing id with a 404', async () => { const server = new SinonServer(); server.addCollection('foo', new Collection()); const request = getFakeXMLHTTPRequest('GET', '/foo/3'); if (request == null) throw new Error('request is null'); - server.handleSync(request); + await server.handle(request); expect(request.status).toEqual(404); }); - it('should respond to PUT /foo/:id by updating element of identifier id in collection foo', () => { + it('should respond to PUT /foo/:id by updating element of identifier id in collection foo', async () => { const server = new SinonServer(); server.addCollection( 'foo', @@ -489,7 +489,7 @@ describe('SinonServer', () => { JSON.stringify({ name: 'baz' }), ); if (request == null) throw new Error('request is null'); - server.handleSync(request); + await server.handle(request); expect(request.status).toEqual(200); // @ts-ignore expect(request.responseText).toEqual('{"id":2,"name":"baz"}'); @@ -502,7 +502,7 @@ describe('SinonServer', () => { ]); }); - it('should respond to PUT /foo/:id on a non-existing id with a 404', () => { + it('should respond to PUT /foo/:id on a non-existing id with a 404', async () => { const server = new SinonServer(); server.addCollection('foo', new Collection({ items: [] })); const request = getFakeXMLHTTPRequest( @@ -511,11 +511,11 @@ describe('SinonServer', () => { JSON.stringify({ name: 'baz' }), ); if (request == null) throw new Error('request is null'); - server.handleSync(request); + await server.handle(request); expect(request.status).toEqual(404); }); - it('should respond to PATCH /foo/:id by updating element of identifier id in collection foo', () => { + it('should respond to PATCH /foo/:id by updating element of identifier id in collection foo', async () => { const server = new SinonServer(); server.addCollection( 'foo', @@ -532,7 +532,7 @@ describe('SinonServer', () => { JSON.stringify({ name: 'baz' }), ); if (request == null) throw new Error('request is null'); - server.handleSync(request); + await server.handle(request); expect(request.status).toEqual(200); // @ts-ignore expect(request.responseText).toEqual('{"id":2,"name":"baz"}'); @@ -545,7 +545,7 @@ describe('SinonServer', () => { ]); }); - it('should respond to PATCH /foo/:id on a non-existing id with a 404', () => { + it('should respond to PATCH /foo/:id on a non-existing id with a 404', async () => { const server = new SinonServer(); server.addCollection('foo', new Collection({ items: [] })); const request = getFakeXMLHTTPRequest( @@ -554,11 +554,11 @@ describe('SinonServer', () => { JSON.stringify({ name: 'baz' }), ); if (request == null) throw new Error('request is null'); - server.handleSync(request); + await server.handle(request); expect(request.status).toEqual(404); }); - it('should respond to DELETE /foo/:id by removing element of identifier id in collection foo', () => { + it('should respond to DELETE /foo/:id by removing element of identifier id in collection foo', async () => { const server = new SinonServer(); server.addCollection( 'foo', @@ -571,7 +571,7 @@ describe('SinonServer', () => { ); const request = getFakeXMLHTTPRequest('DELETE', '/foo/2'); if (request == null) throw new Error('request is null'); - server.handleSync(request); + await server.handle(request); expect(request.status).toEqual(200); // @ts-ignore expect(request.responseText).toEqual('{"id":2,"name":"bar"}'); @@ -581,22 +581,22 @@ describe('SinonServer', () => { expect(server.getAll('foo')).toEqual([{ id: 1, name: 'foo' }]); }); - it('should respond to DELETE /foo/:id on a non-existing id with a 404', () => { + it('should respond to DELETE /foo/:id on a non-existing id with a 404', async () => { const server = new SinonServer(); server.addCollection('foo', new Collection({ items: [] })); const request = getFakeXMLHTTPRequest('DELETE', '/foo/3'); if (request == null) throw new Error('request is null'); - server.handleSync(request); + await server.handle(request); expect(request.status).toEqual(404); }); - it('should respond to GET /foo/ with single item', () => { + it('should respond to GET /foo/ with single item', async () => { const server = new SinonServer(); server.addSingle('foo', new Single({ name: 'foo' })); const request = getFakeXMLHTTPRequest('GET', '/foo'); if (request == null) throw new Error('request is null'); - server.handleSync(request); + await server.handle(request); expect(request.status).toEqual(200); // @ts-ignore expect(request.responseText).toEqual('{"name":"foo"}'); @@ -605,7 +605,7 @@ describe('SinonServer', () => { ); }); - it('should respond to PUT /foo/ by updating the singleton record', () => { + it('should respond to PUT /foo/ by updating the singleton record', async () => { const server = new SinonServer(); server.addSingle('foo', new Single({ name: 'foo' })); @@ -615,7 +615,7 @@ describe('SinonServer', () => { JSON.stringify({ name: 'baz' }), ); if (request == null) throw new Error('request is null'); - server.handleSync(request); + await server.handle(request); expect(request.status).toEqual(200); // @ts-ignore expect(request.responseText).toEqual('{"name":"baz"}'); @@ -625,7 +625,7 @@ describe('SinonServer', () => { expect(server.getOnly('foo')).toEqual({ name: 'baz' }); }); - it('should respond to PATCH /foo/ by updating the singleton record', () => { + it('should respond to PATCH /foo/ by updating the singleton record', async () => { const server = new SinonServer(); server.addSingle('foo', new Single({ name: 'foo' })); @@ -635,7 +635,7 @@ describe('SinonServer', () => { JSON.stringify({ name: 'baz' }), ); if (request == null) throw new Error('request is null'); - server.handleSync(request); + await server.handle(request); expect(request.status).toEqual(200); // @ts-ignore expect(request.responseText).toEqual('{"name":"baz"}'); @@ -647,7 +647,7 @@ describe('SinonServer', () => { }); describe('setDefaultQuery', () => { - it('should set the default query string', () => { + it('should set the default query string', async () => { const server = new SinonServer(); server.addCollection( 'foo', @@ -660,7 +660,7 @@ describe('SinonServer', () => { }); const request = getFakeXMLHTTPRequest('GET', '/foo'); if (request == null) throw new Error('request is null'); - server.handleSync(request); + await server.handle(request); expect(request.status).toEqual(206); expect(request.getResponseHeader('Content-Range')).toEqual( 'items 2-4/10', @@ -670,7 +670,7 @@ describe('SinonServer', () => { expect(request.responseText).toEqual(JSON.stringify(expected)); }); - it('should not override any provided query string', () => { + it('should not override any provided query string', async () => { const server = new SinonServer(); server.addCollection( 'foo', @@ -681,7 +681,7 @@ describe('SinonServer', () => { server.setDefaultQuery((name) => ({ range: [2, 4] })); const request = getFakeXMLHTTPRequest('GET', '/foo?range=[0,4]'); if (request == null) throw new Error('request is null'); - server.handleSync(request); + await server.handle(request); expect(request.status).toEqual(206); expect(request.getResponseHeader('Content-Range')).toEqual( 'items 0-4/10', diff --git a/src/adapters/SinonServer.ts b/src/adapters/SinonServer.ts index d252cbb..f07d8fc 100644 --- a/src/adapters/SinonServer.ts +++ b/src/adapters/SinonServer.ts @@ -7,7 +7,7 @@ export class SinonServer extends BaseServer< SinonFakeXMLHttpRequest, SinonFakeRestResponse > { - extractContextSync(request: SinonFakeXMLHttpRequest) { + async extractContext(request: SinonFakeXMLHttpRequest) { const req: Request | SinonFakeXMLHttpRequest = typeof request === 'string' ? new Request(request) : request; @@ -34,7 +34,7 @@ export class SinonServer extends BaseServer< }; } - respondSync(response: BaseResponse, request: SinonFakeXMLHttpRequest) { + async respond(response: BaseResponse, request: SinonFakeXMLHttpRequest) { const sinonResponse = { status: response.status, body: response.body ?? '', @@ -62,7 +62,7 @@ export class SinonServer extends BaseServer< } // This is an internal property of SinonFakeXMLHttpRequest but we have to reset it to 1 - // to allow the request to be resolved by Sinon. + // to handle the request asynchronously. // See https://github.com/sinonjs/sinon/issues/637 // @ts-expect-error request.readyState = 1; @@ -120,8 +120,15 @@ export class SinonServer extends BaseServer< getHandler() { return (request: SinonFakeXMLHttpRequest) => { - const result = this.handleSync(request); - return result; + // This is an internal property of SinonFakeXMLHttpRequest but we have to set it to 4 to + // suppress sinon's synchronous processing (which would result in HTTP 404). This allows us + // to handle the request asynchronously. + // See https://github.com/sinonjs/sinon/issues/637 + // @ts-expect-error + request.readyState = 4; + this.handle(request); + // Let Sinon know we've handled the request + return true; }; } }