import { Assert, beforeEach, describe, it } from '@ephox/bedrock-client'; import { Singleton, Type } from '@ephox/katamari'; import * as Http from 'ephox/agar/api/Http'; import { TestStore } from 'ephox/agar/api/TestStore'; import * as Waiter from 'ephox/agar/api/Waiter'; interface State { readonly count: number; } describe('browser.agar.http.HttpMockingTest', () => { const store = TestStore(); const pauseControllerState = Singleton.value(); const abortSignalState = Singleton.value(); const httpHook = Http.mockHttpHook((state) => [ Http.get('/custom/test', async () => { return Http.makeResponse( JSON.stringify({ message: 'Get response' }), { status: 200, statusText: 'OK', headers: { 'Content-Type': 'application/json' } } ); }), Http.post('/custom/test', async ({ request }) => { const body = await request.json(); return Http.makeResponse( JSON.stringify({ received: body }), { status: 200, headers: { 'Content-Type': 'application/json' } } ); }), Http.put('/custom/test', async () => { return Http.makeResponse( JSON.stringify({ message: 'Put response' }), { status: 200, headers: { 'Content-Type': 'application/json' } } ); }), Http.del('/custom/test', async () => { return Http.makeResponse( JSON.stringify({ message: 'Delete response' }), { status: 200, headers: { 'Content-Type': 'application/json' } } ); }), Http.patch('/custom/test', async () => { return Http.makeResponse( JSON.stringify({ message: 'Patch response' }), { status: 200, headers: { 'Content-Type': 'application/json' } } ); }), Http.get('/custom/test/status/:status', async ({ params }) => { const status = parseInt(params.status, 10); return Http.makeResponse( JSON.stringify({ message: 'Not found' }), { status, headers: { 'Content-Type': 'application/json' } } ); }), Http.get('/custom/test/splat/*', async ({ params }) => { return Http.makeResponse( JSON.stringify({ message: params.splat0 }), { status: 200, headers: { 'Content-Type': 'application/json' } } ); }), Http.get('/custom/test/state', async () => { const currentState = state.get().getOr({ count: 0 }); state.set({ count: currentState.count + 1 }); return Http.makeResponse( JSON.stringify({ count: state.get().getOr({ count: 0 }).count }), { status: 200, headers: { 'Content-Type': 'application/json' } } ); }), Http.get('/custom/streaming', async ({ abortSignal }) => { abortSignalState.set(abortSignal); const getChunks = async function* () { const items = [ 'one', 'two', 'three', 'four' ]; for (const item of items) { yield item; store.add(item); if (items[items.length - 1] !== item) { const pauseController = pauseControllerState.get().getOrNull(); if (Type.isNonNullable(pauseController)) { await pauseController.wait(); } } } }; return Http.chunkedResponse(getChunks()); }), Http.post('/custom/upload', async ({ request }) => { const formData = await request.formData(); const file = formData.get('file') as File; const value = formData.get('field') as string; return Http.jsonResponse({ file: { name: file.name, type: file.type, size: file.size }, field: value }); }), Http.del('/custom/empty/200', async () => { return new window.Response(null, { status: 200 }); }), Http.del('/custom/empty/204', async () => { return new window.Response(null, { status: 204 }); }), Http.del('/custom/empty/304', async () => { return new window.Response(null, { status: 304 }); }) ], { logLevel: 'debug', name: 'test' }); beforeEach(() => { abortSignalState.clear(); pauseControllerState.clear(); store.clear(); }); it('TINY-13084: Should mock simple GET request', async () => { const response = await window.fetch('/custom/test'); const json = await response.json(); Assert.eq('Should be expected JSON response', { message: 'Get response' }, json); Assert.eq('Should be expected status', 200, response.status); Assert.eq('Should be expected statusText', 'OK', response.statusText); Assert.eq('Should be expected content-type', 'application/json', response.headers.get('Content-Type')); }); it('TINY-13084: Should mock simple POST request', async () => { const response = await window.fetch('/custom/test', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ name: 'John Doe' }), }); const json = await response.json(); Assert.eq('Should be expected JSON response', { received: { name: 'John Doe' }}, json); Assert.eq('Should be expected status', 200, response.status); Assert.eq('Should be expected content-type', 'application/json', response.headers.get('Content-Type')); }); it('TINY-13084: Should mock simple PUT request', async () => { const response = await window.fetch('/custom/test', { method: 'PUT', }); const json = await response.json(); Assert.eq('Should be expected JSON response', { message: 'Put response' }, json); Assert.eq('Should be expected status', 200, response.status); Assert.eq('Should be expected content-type', 'application/json', response.headers.get('Content-Type')); }); it('TINY-13084: Should mock simple DELETE request', async () => { const response = await window.fetch('/custom/test', { method: 'DELETE', }); const json = await response.json(); Assert.eq('Should be expected JSON response', { message: 'Delete response' }, json); Assert.eq('Should be expected status', 200, response.status); Assert.eq('Should be expected content-type', 'application/json', response.headers.get('Content-Type')); }); it('TINY-13084: Should mock simple PATCH request', async () => { const response = await window.fetch('/custom/test', { method: 'PATCH', }); const json = await response.json(); Assert.eq('Should be expected JSON response', { message: 'Patch response' }, json); Assert.eq('Should be expected status', 200, response.status); Assert.eq('Should be expected content-type', 'application/json', response.headers.get('Content-Type')); }); it('TINY-13084: Should return custom http status 403', async () => { const { status } = await window.fetch('/custom/test/status/404'); Assert.eq('Should be expected status', 404, status); }); it('TINY-13084: Should return custom http status 404', async () => { const { status } = await window.fetch('/custom/test/status/404'); Assert.eq('Should be expected status', 404, status); }); it('TINY-13084: Should return splat path', async () => { const json = await window.fetch('/custom/test/splat/a/b/c').then((res) => res.json()); Assert.eq('Should be expected status', { message: 'a/b/c' }, json); }); it('TINY-13084: Should update state', async () => { httpHook.state.clear(); const json1 = await window.fetch('/custom/test/state').then((res) => res.json()); Assert.eq('Should be expected state', { count: 1 }, json1); const json2 = await window.fetch('/custom/test/state').then((res) => res.json()); Assert.eq('Should be expected state', { count: 2 }, json2); }); it('TINY-13084: Should handle streaming response', async () => { pauseControllerState.set(Http.createPauseController()); const response = await window.fetch('/custom/streaming'); const body = response.body; if (Type.isNull(body)) { Assert.fail('Response body should not be null'); return; } Assert.eq('Should be expected content-type', 'text/plain', response.headers.get('Content-Type')); Assert.eq('Should be expected transfer-encoding', 'chunked', response.headers.get('Transfer-Encoding')); const reader = body.pipeThrough(new window.TextDecoderStream()).getReader(); const chunks: string[] = []; while (true) { const { done, value: chunk } = await reader.read(); if (done) { break; } chunks.push(chunk); pauseControllerState.get().each((pauseController) => pauseController.resume()); } Assert.eq('Should be expected chunks', [ 'one', 'two', 'three', 'four' ], chunks); }); it('TINY-13084: Should handle aborting streaming response', async () => { const abortController = new window.AbortController(); const chunks: string[] = []; try { pauseControllerState.set(Http.createPauseController()); const response = await window.fetch('/custom/streaming', { signal: abortController.signal }); const body = response.body; if (Type.isNull(body)) { Assert.fail('Response body should not be null'); return; } Assert.eq('Should be expected content-type', 'text/plain', response.headers.get('Content-Type')); Assert.eq('Should be expected transfer-encoding', 'chunked', response.headers.get('Transfer-Encoding')); const reader = body.pipeThrough(new window.TextDecoderStream()).getReader(); while (true) { const { done, value: chunk } = await reader.read(); if (done) { break; } chunks.push(chunk); if (chunk === 'two') { abortController.abort(); await Waiter.pTryUntilPredicate('Waited for abort signal to be set', () => abortSignalState.get().map((s) => s.aborted).getOr(false)); } pauseControllerState.get().each((pauseController) => pauseController.resume()); } } catch (e) { const isAbortError = (err: unknown): err is Error => err instanceof Error && err.name === 'AbortError'; if (!isAbortError(e)) { Assert.fail('Should be abort error'); } } pauseControllerState.clear(); Assert.eq('Should be only one and two from request since we aborted before three', [ 'one', 'two' ], chunks); await Waiter.pWait(100); // Wait a while to ensure that no more chunks are processed store.assertEq('Should be only one and two in store since we aborted before three', [ 'one', 'two' ]); }); it('TINY-13084: Should handle file uploads', async () => { const formData = new FormData(); formData.append('field', 'value'); formData.append('file', new Blob([ 'file contents' ], { type: 'text/plain' }), 'test.txt'); const response = await window.fetch('/custom/upload', { method: 'POST', body: formData }); const json = await response.json(); Assert.eq('Should be expected JSON response', { file: { name: 'test.txt', type: 'text/plain', size: 13 }, field: 'value' }, json); Assert.eq('Should be expected status', 200, response.status); Assert.eq('Should be expected content-type', 'application/json', response.headers.get('Content-Type')); }); it('TINY-13544: Should handle empty response for 200 del response', async () => { const response = await window.fetch('/custom/empty/200', { method: 'DELETE' }); Assert.eq('Should be expected status', 200, response.status); }); it('TINY-13544: Should handle empty response for 204 del response', async () => { const response = await window.fetch('/custom/empty/204', { method: 'DELETE' }); Assert.eq('Should be expected status', 204, response.status); }); it('TINY-13544: Should handle empty response for 304 del response', async () => { const response = await window.fetch('/custom/empty/304', { method: 'DELETE' }); Assert.eq('Should be expected status', 304, response.status); }); });