UNPKG

@wral/resource-deref

Version:

A Library for dereferencing $ref references in Javascript objects

324 lines (286 loc) 10.2 kB
import { compile, deref, applyAuthOptions, retrieveResource, transformResource, } from './index.mjs'; import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'; import { mockClient } from 'aws-sdk-client-mock'; import { jest } from '@jest/globals'; /** * Helper to mock a response * By default, the response will resemble a JSON response * @param {Object} obj response data * @param {Object} headers map of http headers * @returns {Object} */ function response(obj, _headers = {}) { const headers = new Headers(Object.assign({ 'content-type': 'application/json', }, _headers)); const arrayBuffer = () => { const [contentType] = headers.get('content-type').split(';').map(s => s.trim()); if (contentType === 'application/json') { return Buffer.from(JSON.stringify(obj)); } } return { ok: true, headers, arrayBuffer, }; } describe('compile function', () => { afterEach(() => { jest.restoreAllMocks(); }); it('should return a fetch wrapper function', async () => { const fetchSpy = jest.spyOn(global, 'fetch') .mockResolvedValue(response({foo: 'bar'})); const compiled = compile({ trustedDomains: { 'example.com': { headers: { Authorization: 'test', }, }, }, }); await compiled({$ref: 'https://example.com'}, {headers: {'foo':'bar'}}); // fetch should have been called with Auth headers expect(fetchSpy).toHaveBeenCalledWith('https://example.com', expect.objectContaining({ headers: expect.objectContaining({ Authorization: 'test', 'foo':'bar' }), })); await compiled({$ref: 'https://example.net/foo'}, {headers: {'foo':'bar'}}); // fetch should have been called with Auth headers expect(fetchSpy).toHaveBeenCalledWith('https://example.net/foo', expect.objectContaining({ headers: {'foo': 'bar'}, })); }); it('uses default options', async () => { const fetchSpy = jest.spyOn(global, 'fetch') .mockResolvedValue(response({baz: 'qux'})); const compiled = compile(); await compiled({$ref: 'https://example.com'}); expect(fetchSpy).toHaveBeenCalledWith('https://example.com', expect.any(Object)); }); }); describe('deref function', () => { afterEach(() => { jest.restoreAllMocks(); }); it('should return a resolved promise if $ref is not specified', async () => { const result = await deref({ foo: 'bar' }); expect(result).toEqual({ foo: 'bar' }); }); it('should handle undefined input', async () => { const result = await deref(); expect(result).toEqual(undefined); }); it('should call fetch with url from $ref', async () => { const fetchSpy = jest.spyOn(global, 'fetch') .mockResolvedValue(response({foo: 'bar'})); const $ref = 'https://example.com'; const result = await deref({ $ref }); expect(fetchSpy).toHaveBeenCalledWith($ref, expect.any(Object)); expect(result).toEqual({'foo': 'bar'}); }); it('should call fetch without auth headers', async () => { const fetchSpy = jest.spyOn(global, 'fetch') .mockResolvedValue(response({foo: 'bar'})); const $ref = 'https://example.com'; await deref({ $ref }, { trustedDomains: { 'example.net': { headers: { Authorization: 'test' } }, }, fetchOptions: { headers: { foo: 'bar' } }, }); // Expect fetch to have been called, but without an Authorization header expect(fetchSpy).toHaveBeenCalledWith($ref, expect.objectContaining({ headers: expect.not.objectContaining({ Authorization: 'test' }), })); }); it('should call fetch with auth headers', async () => { const fetchSpy = jest.spyOn(global, 'fetch') .mockResolvedValue(response({foo: 'bar'})); const $ref = 'https://example.com'; await deref({ $ref }, { trustedDomains: { 'example.com': { headers: { Authorization: 'test' } }, }, }); // Expect fetch to have been called with an Authorization header expect(fetchSpy).toHaveBeenCalledWith($ref, expect.objectContaining({ headers: expect.objectContaining({ Authorization: 'test' }), })); }); it('should recursively dereference nested $ref properties', async () => { const fetchSpy = jest.spyOn(global, 'fetch') .mockResolvedValueOnce(response({ $ref: 'https://example.com/inner' })) .mockResolvedValueOnce(response({ foo: 'bar' })); const result = await deref({ $ref: 'https://example.com' }); // First fetch expect(fetchSpy).toHaveBeenNthCalledWith(1, 'https://example.com', expect.any(Object)); // Second fetch for nested $ref expect(fetchSpy).toHaveBeenNthCalledWith(2, 'https://example.com/inner', expect.any(Object)); expect(result).toEqual({ foo: 'bar' }); }); it('should respect the depth option to limit recursion', async () => { const fetchSpy = jest.spyOn(global, 'fetch') .mockResolvedValueOnce(response({ $ref: 'https://example.com/inner' })) .mockResolvedValueOnce(response({ foo: 'bar' })); const result = await deref({ $ref: 'https://example.com' }, { depth: 1 }); // Only the first fetch should occur expect(fetchSpy).toHaveBeenCalledTimes(1); expect(result).toEqual({ $ref: 'https://example.com/inner' }); }); it('should dereference $ref in arrays', async () => { const fetchSpy = jest.spyOn(global, 'fetch') .mockResolvedValueOnce(response({ foo: 'bar' })); const result = await deref({ items: [ { $ref: 'https://example.com/item' }, { foo: 'baz' } ] }); expect(fetchSpy).toHaveBeenCalledWith('https://example.com/item', expect.any(Object)); expect(result).toEqual({ items: [ { foo: 'bar' }, { foo: 'baz' } ] }); }); it('should handle deeply nested structures with mixed content', async () => { const fetchSpy = jest.spyOn(global, 'fetch') .mockResolvedValueOnce(response({ foo: 'bar' })) .mockResolvedValueOnce(response({ bar: 'baz' })); const result = await deref({ user: { $ref: 'https://example.com/user' }, profile: { contacts: [ { $ref: 'https://example.com/contact/1' }, { name: 'John Doe' }, ], }, }); expect(fetchSpy).toHaveBeenNthCalledWith(1, 'https://example.com/user', expect.any(Object)); expect(fetchSpy).toHaveBeenNthCalledWith(2, 'https://example.com/contact/1', expect.any(Object)); expect(result).toEqual({ user: { foo: 'bar' }, profile: { contacts: [ { bar: 'baz' }, { name: 'John Doe' }, ], }, }); }); it('treats non-ok responses as errors', async () => { const fetchSpy = jest.spyOn(global, 'fetch') .mockResolvedValue({ ok: false, headers: new Headers({ 'content-type': 'application/json', }), arrayBuffer: () => Buffer.from('"foo"'), }); const $ref = 'https://example.com'; await expect(deref({ $ref })).rejects.toThrow(); fetchSpy.mockRestore(); }); it('should stop recursion and return original when depth is 0', async () => { const fetchSpy = jest.spyOn(global, 'fetch'); // Spy, but should not be called const obj = { $ref: 'https://example.com' }; const result = await deref(obj, { depth: 0 }); // Ensure no fetch call was made expect(fetchSpy).not.toHaveBeenCalled(); // The result should be the original object, because depth was 0 expect(result).toEqual(obj); }); }); describe('applyAuthOptions helper function', () => { it('should return options with header field added', () => { const fetchOptions = { headers: {}, }; const options = applyAuthOptions(fetchOptions, { headers: { Authorization: 'test', }, }); expect(options.headers).toEqual({ Authorization: 'test', }); }); it('handles empty auth options', () => { const fetchOptions = { headers: { foo: 'bar'}, }; const options = applyAuthOptions(fetchOptions, {}); expect(options.headers).toEqual({ foo: 'bar'}); }); }); describe('retrieveResource helper', () => { it('will fetch from http', async () => { const fetchSpy = jest.spyOn(global, 'fetch') .mockResolvedValue(response({foo: 'bar'})); const { contentType, raw } = await retrieveResource({ $ref: 'http://example.com' }, { trustedDomains: {}, fetchOptions: {}, }); expect(fetchSpy).toHaveBeenCalledWith('http://example.com', expect.any(Object)); expect(contentType).toEqual('application/json'); expect(raw).toEqual(Buffer.from(JSON.stringify({foo: 'bar'}))); fetchSpy.mockRestore(); }); it('will fetch from https', async () => { const fetchSpy = jest.spyOn(global, 'fetch') .mockResolvedValue(response({foo: 'bar'})); const { contentType, raw } = await retrieveResource({ $ref: 'https://example.com' }, { trustedDomains: {}, fetchOptions: {}, }); expect(fetchSpy).toHaveBeenCalledWith('https://example.com', expect.any(Object)); expect(contentType).toEqual('application/json'); expect(raw).toEqual(Buffer.from(JSON.stringify({foo: 'bar'}))); fetchSpy.mockRestore(); }); it('will use S3 client to get objects from S3', async () => { const s3ClientMock = mockClient(S3Client); s3ClientMock.on(GetObjectCommand).resolves({ ContentType: 'application/json', Body: { transformToByteArray: () => Buffer.from(JSON.stringify({foo: 'bar'})), }, }); const result = await retrieveResource({ $ref: 's3://bucket/key', }, {}); expect(result.contentType).toEqual('application/json'); expect(result.raw).toEqual(Buffer.from(JSON.stringify({foo: 'bar'}))); s3ClientMock.restore(); }); it('will return empty object from unsupported protocols', () => { const warnSpy = jest.spyOn(console, 'warn').mockImplementation(); expect(retrieveResource({ $ref: 'no-such-protocol://example.com' })) .toEqual({}); expect(warnSpy).toHaveBeenCalled(); warnSpy.mockRestore(); }); }); describe('transformResource helper', () => { it('transforms json', () => { const obj = { foo: 'bar' }; const result = transformResource( Buffer.from(JSON.stringify(obj)), { contentType: 'application/json' } ); expect(result).toEqual(obj); }); it('returns object with _raw and a warning for unsupported content', () => { const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); const result = transformResource( Buffer.from('foo'), { contentType: 'application/x-no-such-type' } ); expect(result).toEqual({ _raw: expect.any(Buffer) }); expect(consoleWarnSpy).toHaveBeenCalled(); consoleWarnSpy.mockRestore(); }); });