import {describe, beforeEach, afterEach, test, expect, vi} from 'vitest'; import { getArrayBuffer, getJSON, type AJAXError, getReferrer, sameOrigin } from './ajax.ts'; import {isAbortError} from './abort_error.ts'; import {ensureError} from './util.ts'; import {fakeServer, type FakeServer} from 'nise'; function readAsText(blob) { return new Promise((resolve, reject) => { const fileReader = new FileReader(); fileReader.onload = () => resolve(fileReader.result); fileReader.onerror = () => reject(fileReader.error); fileReader.readAsText(blob); }); } async function expectRejection(promise: Promise): Promise { const error = await promise.catch((e) => e); expect(error).toBeInstanceOf(Error); return error as AJAXError; } const originalFetch = global.fetch; describe('ajax', () => { let server: FakeServer; beforeEach(() => { global.fetch = null; server = fakeServer.create(); }); afterEach(() => { server.restore(); }); test('getArrayBuffer, 404', async () => { server.respondWith(request => { request.respond(404, undefined, '404 Not Found'); }); const promise = getArrayBuffer({url: 'http://example.com/test.bin'}, new AbortController()); server.respond(); const ajaxError = await expectRejection(promise); expect(ajaxError.status).toBe(404); expect(ajaxError.statusText).toBe('Not Found'); expect(ajaxError.url).toBe('http://example.com/test.bin'); await expect(readAsText(ajaxError.body)).resolves.toBe('404 Not Found'); }); test('getJSON', async () => { server.respondWith(request => { request.respond(200, {'Content-Type': 'application/json'}, '{"foo": "bar"}'); }); const promise = getJSON({url: ''}, new AbortController()); server.respond(); const body = await promise; expect(body.data).toEqual({foo: 'bar'}); }); test('getJSON, invalid syntax', async () => { server.respondWith(request => { request.respond(200, {'Content-Type': 'application/json'}, 'how do i even'); }); const promise = getJSON({url: ''}, new AbortController()); server.respond(); await expect(promise).rejects.toThrow(SyntaxError); }); test('getJSON, 404', async () => { server.respondWith(request => { request.respond(404, undefined, '404 Not Found'); }); const promise = getJSON({url: 'http://example.com/test.json'}, new AbortController()); server.respond(); const ajaxError = await expectRejection(promise); expect(ajaxError.status).toBe(404); expect(ajaxError.statusText).toBe('Not Found'); expect(ajaxError.url).toBe('http://example.com/test.json'); await expect(readAsText(ajaxError.body)).resolves.toBe('404 Not Found'); }); test('getJSON, aborted', async () => { const abortController = new AbortController(); server.respondWith(request => { request.respond(404, undefined, '404 Not Found'); }); const promise = getJSON({url: 'http://example.com/test.json'}, abortController); abortController.abort(); server.respond(); const error = await expectRejection(promise); expect(ensureError(error).name).toBe('AbortError'); expect(isAbortError(error)).toBe(true); }); test('getJSON with fetch, aborted', async () => { // Mock Request.prototype.signal to simulate environment with fetch and AbortController support Object.defineProperty(Request.prototype, 'signal', {}); // Re-enable fetch for this test global.fetch = originalFetch; const abortController = new AbortController(); server.respondWith(request => { request.respond(404, undefined, '404 Not Found'); }); const promise = getJSON({url: 'http://example.com/test.json'}, abortController); abortController.abort(); server.respond(); const error = await expectRejection(promise); expect(ensureError(error).name).toBe('AbortError'); expect(isAbortError(error)).toBe(true); }); test('getReferrer method (outside Worker), same origin https URL', () => { vi.spyOn(window, 'location', 'get').mockReturnValue({ href: 'https://somewhere.com', protocol: 'https:' } as any); expect(getReferrer()).toBe('https://somewhere.com'); }); test('getReferrer method (outside Worker), same origin blob URL', () => { vi.spyOn(window, 'parent', 'get').mockReturnValue({ location: { href: 'https://somewhere.com', protocol: 'https:', host: 'somewhere.com' } } as any); vi.spyOn(window, 'location', 'get').mockReturnValue({ href: 'blob:https://somewhere.com/123e4567-e89b-12d3-a456-426614174000', protocol: 'blob:', host: '' } as any); expect(getReferrer()).toBe('https://somewhere.com'); }); test('getReferrer method (outside Worker), different origin https URL', () => { vi.spyOn(window, 'parent', 'get').mockReturnValue({ location: { get href() { throw new DOMException(); }, get protocol() { throw new DOMException(); }, get host() { throw new DOMException(); } } } as any); vi.spyOn(window, 'location', 'get').mockReturnValue({ href: 'https://somewhere.com', protocol: 'https:', host: 'somewhere.com' } as any); expect(getReferrer()).toBe('https://somewhere.com'); }); test('getReferrer method (outside Worker), different origin blob URL', () => { vi.spyOn(window, 'parent', 'get').mockReturnValue({ location: { get href() { throw new DOMException(); }, get protocol() { throw new DOMException(); }, get host() { throw new DOMException(); } } } as any); vi.spyOn(window, 'location', 'get').mockReturnValue({ href: 'blob:https://somewhere.com/123e4567-e89b-12d3-a456-426614174000', protocol: 'blob:', host: '' } as any); expect(getReferrer()).toBe('blob:https://somewhere.com/123e4567-e89b-12d3-a456-426614174000'); }); test('sameOrigin method', () => { vi.spyOn(window, 'location', 'get').mockReturnValue({ protocol: 'https:', host: 'somewhere.com' } as any); expect(sameOrigin('https://somewhere.com')).toBe(true); expect(sameOrigin('https://somewhere.com/path')).toBe(true); expect(sameOrigin('https://somewhere.com/path/?q=abc')).toBe(true); expect(sameOrigin('https://somewhere.com:443/path')).toBe(true); expect(sameOrigin('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQYV2NgAAIAAAUAAarVyFEAAAAASUVORK5CYII=')).toBe(true); expect(sameOrigin('blob:https://www.bing.com/09f36686-e57a-420f-9004-918548219b75')).toBe(false); expect(sameOrigin('blob:https://somewhere.com/123e4567-e89b-12d3-a456-426614174000')).toBe(true); expect(sameOrigin('blob:http://somewhere.com/123e4567-e89b-12d3-a456-426614174000')).toBe(false); expect(sameOrigin('blob:null/123e4567-e89b-12d3-a456-426614174000')).toBe(false); // relative URL is same origin for sure expect(sameOrigin('/foo')).toBe(true); expect(sameOrigin('foo')).toBe(true); // empty string is considered as relative, and should be true expect(sameOrigin('')).toBe(true); expect(sameOrigin(null)).toBe(true); expect(sameOrigin(undefined)).toBe(true); expect(sameOrigin('HTTPS://somewhere.com')).toBe(true); // different domain expect(sameOrigin('httpS://www.somewhere.com')).toBe(false); // different protocol expect(sameOrigin('HTTP://somewhere.com')).toBe(false); expect(sameOrigin('file:///c:/temp/foo.html')).toBe(false); // file url vi.spyOn(window, 'location', 'get').mockReturnValue({ protocol: 'file:', host: '' } as any); expect(sameOrigin('file:///C:/Temp/abc.html')).toBe(true); expect(sameOrigin('HTTP://somewhere.com')).toBe(false); // relative URL (for file URL) is same origin as well expect(sameOrigin('/foo')).toBe(true); expect(sameOrigin('foo')).toBe(true); // edge case expect(sameOrigin('://foo')).toBe(true); }); describe('requests parameters', () => { test('should be provided to fetch API in getArrayBuffer function', async () => { server.respondWith(new ArrayBuffer(1)); const promise = getArrayBuffer({url: 'http://example.com/test-params.json', cache: 'force-cache', headers: {'Authorization': 'Bearer 123'}}, new AbortController()); server.respond(); await promise; expect(server.requests).toHaveLength(1); expect(server.requests[0].url).toBe('http://example.com/test-params.json'); expect(server.requests[0].method).toBe('GET'); expect(server.requests[0].requestHeaders['Authorization']).toBe('Bearer 123'); }); test('should be provided to fetch API in getJSON function', async () => { server.respondWith(request => { request.respond(200, {'Content-Type': 'application/json'}, '{"foo": "bar"}'); }); const promise = getJSON({url: 'http://example.com/test-params.json', cache: 'force-cache', headers: {'Authorization': 'Bearer 123'}}, new AbortController()); server.respond(); await promise; expect(server.requests).toHaveLength(1); expect(server.requests[0].url).toBe('http://example.com/test-params.json'); expect(server.requests[0].method).toBe('GET'); expect(server.requests[0].requestHeaders['Authorization']).toBe('Bearer 123'); }); test('should preserve user-specified Accept header', async () => { server.respondWith(request => { // Note that PostgREST responds to this type of request with application/geo+json request.respond(200, {'Content-Type': 'application/geo+json'}, '{"foo": "bar"}'); }); const promise = getJSON({url: 'http://example.com/test-params.json', cache: 'force-cache', headers: {'Authorization': 'Bearer 123', 'Accept': 'application/geo+json'}}, new AbortController()); server.respond(); await promise; expect(server.requests).toHaveLength(1); expect(server.requests[0].url).toBe('http://example.com/test-params.json'); expect(server.requests[0].method).toBe('GET'); expect(server.requests[0].requestHeaders['Authorization']).toBe('Bearer 123'); expect(server.requests[0].requestHeaders['Accept']).toBe('application/geo+json'); }); test('should add default Accept header when user has not specified one', async () => { server.respondWith(request => { request.respond(200, {'Content-Type': 'application/json'}, '{"foo": "bar"}'); }); const promise = getJSON({url: 'http://example.com/test-params.json', cache: 'force-cache', headers: {'Authorization': 'Bearer 123'}}, new AbortController()); server.respond(); await promise; expect(server.requests).toHaveLength(1); expect(server.requests[0].url).toBe('http://example.com/test-params.json'); expect(server.requests[0].method).toBe('GET'); expect(server.requests[0].requestHeaders['Authorization']).toBe('Bearer 123'); expect(server.requests[0].requestHeaders['Accept']).toBe('application/json'); }); test('should add default Accept header when user has not specified one, even for file:// requests', async () => { server.respondWith(request => { request.respond(200, {'Content-Type': 'application/json'}, '{"foo": "bar"}'); }); const promise = getJSON({url: 'file:///C:/Temp/abc.json', cache: 'force-cache', headers: {'Authorization': 'Bearer 123'}}, new AbortController()); server.respond(); await promise; expect(server.requests).toHaveLength(1); expect(server.requests[0].url).toBe('file:///C:/Temp/abc.json'); expect(server.requests[0].method).toBe('GET'); expect(server.requests[0].requestHeaders['Authorization']).toBe('Bearer 123'); expect(server.requests[0].requestHeaders['Accept']).toBe('application/json'); }); test('should not add default Accept header when user has already specified one, even for file:// requests', async () => { server.respondWith(request => { request.respond(200, {'Content-Type': 'application/json'}, '{"foo": "bar"}'); }); const promise = getJSON({url: 'file:///C:/Temp/abc.json', cache: 'force-cache', headers: {'Authorization': 'Bearer 123', 'Accept': 'application/geo+json'}}, new AbortController()); server.respond(); await promise; expect(server.requests).toHaveLength(1); expect(server.requests[0].url).toBe('file:///C:/Temp/abc.json'); expect(server.requests[0].method).toBe('GET'); expect(server.requests[0].requestHeaders['Authorization']).toBe('Bearer 123'); expect(server.requests[0].requestHeaders['Accept']).toBe('application/geo+json'); }); }); describe('referrerPolicy', () => { test('should pass referrerPolicy to fetch Request', async () => { global.fetch = originalFetch; const fetchSpy = vi.spyOn(global, 'fetch').mockResolvedValue(new Response(new ArrayBuffer(1))); await getArrayBuffer({url: 'http://example.com/test.json', referrerPolicy: 'origin-when-cross-origin'}, new AbortController()); expect(fetchSpy).toHaveBeenCalledTimes(1); const request = fetchSpy.mock.calls[0][0] as Request; expect(request.referrerPolicy).toBe('origin-when-cross-origin'); fetchSpy.mockRestore(); }); test('should default referrerPolicy to empty string when not provided', async () => { global.fetch = originalFetch; const fetchSpy = vi.spyOn(global, 'fetch').mockResolvedValue(new Response(new ArrayBuffer(1))); await getArrayBuffer({url: 'http://example.com/test.json'}, new AbortController()); expect(fetchSpy).toHaveBeenCalledTimes(1); const request = fetchSpy.mock.calls[0][0] as Request; expect(request.referrerPolicy).toBe(''); fetchSpy.mockRestore(); }); }); });