@wral/resource-deref
Version:
A Library for dereferencing $ref references in Javascript objects
324 lines (286 loc) • 10.2 kB
JavaScript
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();
});
});