halson
Version:
The HAL+JSON Resource Object
617 lines (513 loc) • 24.5 kB
JavaScript
var assert = require('assert');
var expect = require('chai').expect;
var halson = require('./index');
function clone(data) {
return JSON.parse(JSON.stringify(data));
}
describe('halson enhanced features', function() {
describe('IanaRels', function() {
it('expose standard relation types', function() {
expect(halson.IanaRels).to.be.an('object');
assert.strictEqual(halson.IanaRels.SELF, 'self');
assert.strictEqual(halson.IanaRels.EDIT, 'edit');
assert.strictEqual(halson.IanaRels.DELETE, 'delete');
assert.strictEqual(halson.IanaRels.NEXT, 'next');
assert.strictEqual(halson.IanaRels.PREV, 'prev');
assert.strictEqual(halson.IanaRels.PREVIOUS, 'previous');
assert.strictEqual(halson.IanaRels.FIRST, 'first');
assert.strictEqual(halson.IanaRels.LAST, 'last');
assert.strictEqual(halson.IanaRels.RELATED, 'related');
assert.strictEqual(halson.IanaRels.COLLECTION, 'collection');
assert.strictEqual(halson.IanaRels.ITEM, 'item');
assert.strictEqual(halson.IanaRels.UP, 'up');
assert.strictEqual(halson.IanaRels.SEARCH, 'search');
});
it('work as rel arguments to addLink/getLink', function() {
var res = halson();
res.addLink(halson.IanaRels.SELF, '/test');
assert.strictEqual(res.getLink('self').href, '/test');
});
});
describe('hasLink()', function() {
it('return false on empty resource', function() {
assert.strictEqual(halson().hasLink('self'), false);
});
it('return false for missing rel', function() {
var res = halson().addLink('self', '/test');
assert.strictEqual(res.hasLink('edit'), false);
});
it('return true for existing rel', function() {
var res = halson().addLink('self', '/test');
assert.strictEqual(res.hasLink('self'), true);
});
it('return true for rel with multiple links', function() {
var res = halson()
.addLink('related', '/a')
.addLink('related', '/b');
assert.strictEqual(res.hasLink('related'), true);
});
});
describe('getHref()', function() {
it('return undefined on empty resource', function() {
assert.strictEqual(halson().getHref('self'), undefined);
});
it('return undefined for missing rel', function() {
var res = halson().addLink('self', '/test');
assert.strictEqual(res.getHref('edit'), undefined);
});
it('return href string for existing link', function() {
var res = halson()
.addLink('self', '/users/1')
.addLink('edit', {href: '/users/1/edit', method: 'PUT'});
assert.strictEqual(res.getHref('self'), '/users/1');
assert.strictEqual(res.getHref('edit'), '/users/1/edit');
});
it('return first href when rel has multiple links', function() {
var res = halson()
.addLink('related', '/a')
.addLink('related', '/b');
assert.strictEqual(res.getHref('related'), '/a');
});
});
describe('hasAnyLink()', function() {
it('return false for empty array', function() {
var res = halson().addLink('self', '/test');
assert.strictEqual(res.hasAnyLink([]), false);
});
it('return false when none match', function() {
var res = halson().addLink('self', '/test');
assert.strictEqual(res.hasAnyLink(['edit', 'delete']), false);
});
it('return true when at least one matches', function() {
var res = halson()
.addLink('self', '/test')
.addLink('edit', '/test/edit');
assert.strictEqual(res.hasAnyLink(['delete', 'edit']), true);
});
it('return false for non-array input', function() {
var res = halson().addLink('self', '/test');
assert.strictEqual(res.hasAnyLink('self'), false);
assert.strictEqual(res.hasAnyLink(null), false);
});
});
describe('addTemplate()', function() {
it('create a templated link', function() {
var res = halson();
res.addTemplate('search', '/users{?name,status}');
var link = res.getLink('search');
assert.strictEqual(link.href, '/users{?name,status}');
assert.strictEqual(link.templated, true);
});
it('return this for chaining', function() {
var res = halson();
assert.strictEqual(res.addTemplate('search', '/search{?q}'), res);
});
});
describe('isTemplated()', function() {
it('return false for non-templated link', function() {
var res = halson().addLink('self', '/users/1');
assert.strictEqual(res.isTemplated('self'), false);
});
it('return true for templated link', function() {
var res = halson().addTemplate('search', '/users{?q}');
assert.strictEqual(res.isTemplated('search'), true);
});
it('return false for missing rel', function() {
assert.strictEqual(halson().isTemplated('missing'), false);
});
});
describe('getTemplateVariables()', function() {
it('return empty array for non-templated link', function() {
var res = halson().addLink('self', '/users/1');
assert.deepEqual(res.getTemplateVariables('self'), []);
});
it('return empty array for missing rel', function() {
assert.deepEqual(halson().getTemplateVariables('missing'), []);
});
it('extract simple path variables', function() {
var res = halson().addTemplate('user', '/users/{id}');
var vars = res.getTemplateVariables('user');
expect(vars).to.include('id');
});
it('extract query variables', function() {
var res = halson().addTemplate('search', '/users{?name,email,status}');
var vars = res.getTemplateVariables('search');
expect(vars).to.include('name');
expect(vars).to.include('email');
expect(vars).to.include('status');
});
});
describe('expandTemplate()', function() {
it('expand simple path variable', function() {
var res = halson().addTemplate('user', '/users/{id}');
assert.strictEqual(res.expandTemplate('user', {id: '123'}), '/users/123');
});
it('expand query variables', function() {
var res = halson().addTemplate('search', '/users{?name,status}');
var expanded = res.expandTemplate('search', {name: 'John', status: 'active'});
assert.strictEqual(expanded, '/users?name=John');
});
it('encode special characters', function() {
var res = halson().addTemplate('search', '/search{?q}');
var expanded = res.expandTemplate('search', {q: 'hello world'});
assert.strictEqual(expanded, '/search?q=hello%20world');
});
it('return href as-is for non-templated link', function() {
var res = halson().addLink('self', '/users/1');
assert.strictEqual(res.expandTemplate('self'), '/users/1');
});
it('return undefined for missing rel', function() {
assert.strictEqual(halson().expandTemplate('missing'), undefined);
});
it('escape regex-special characters in variable keys', function() {
var res = halson().addTemplate('test', '/test/{a.b}');
var expanded = res.expandTemplate('test', {'a.b': 'val'});
assert.strictEqual(expanded, '/test/val');
});
it('do not match partial key via unescaped regex', function() {
var res = halson().addTemplate('test', '/test/{a.b}/{axb}');
// "a.b" unescaped would match "axb" too; with RegExp.escape it should not
var expanded = res.expandTemplate('test', {'a.b': 'one'});
assert.strictEqual(expanded, '/test/one/');
});
it('encode variable key in query output', function() {
var res = halson().addTemplate('test', '/test{?a+b}');
var expanded = res.expandTemplate('test', {'a+b': 'val'});
assert.strictEqual(expanded, '/test?a%2Bb=val');
});
});
describe('addCurie()', function() {
it('add a curie link', function() {
var res = halson();
res.addCurie('acme', 'https://api.acme.com/rels/{rel}');
expect(res._links.curies).to.be.an('array');
assert.strictEqual(res._links.curies.length, 1);
assert.strictEqual(res._links.curies[0].name, 'acme');
assert.strictEqual(res._links.curies[0].href, 'https://api.acme.com/rels/{rel}');
assert.strictEqual(res._links.curies[0].templated, true);
});
it('add multiple curies', function() {
var res = halson()
.addCurie('acme', 'https://api.acme.com/rels/{rel}')
.addCurie('other', 'https://other.com/rels/{rel}');
assert.strictEqual(res._links.curies.length, 2);
assert.strictEqual(res._links.curies[0].name, 'acme');
assert.strictEqual(res._links.curies[1].name, 'other');
});
it('return this for chaining', function() {
var res = halson();
assert.strictEqual(res.addCurie('acme', 'https://api.acme.com/{rel}'), res);
});
it('allow explicit templated=false', function() {
var res = halson();
res.addCurie('acme', 'https://api.acme.com/rels/fixed', false);
assert.strictEqual(res._links.curies[0].templated, false);
});
});
describe('hasCurie()', function() {
it('return false on empty resource', function() {
assert.strictEqual(halson().hasCurie('acme'), false);
});
it('return false for missing curie', function() {
var res = halson().addCurie('acme', 'https://api.acme.com/{rel}');
assert.strictEqual(res.hasCurie('other'), false);
});
it('return true for existing curie', function() {
var res = halson().addCurie('acme', 'https://api.acme.com/{rel}');
assert.strictEqual(res.hasCurie('acme'), true);
});
});
describe('expandCurie()', function() {
it('expand compact URI', function() {
var res = halson().addCurie('acme', 'https://api.acme.com/rels/{rel}');
assert.strictEqual(
res.expandCurie('acme:orders'),
'https://api.acme.com/rels/orders'
);
});
it('return non-curie rel as-is', function() {
var res = halson().addCurie('acme', 'https://api.acme.com/{rel}');
assert.strictEqual(res.expandCurie('self'), 'self');
});
it('return unknown prefix as-is', function() {
var res = halson().addCurie('acme', 'https://api.acme.com/{rel}');
assert.strictEqual(res.expandCurie('other:thing'), 'other:thing');
});
it('return null/undefined as-is', function() {
var res = halson();
assert.strictEqual(res.expandCurie(null), null);
});
});
describe('curie serialization', function() {
it('preserve curies in JSON output', function() {
var res = halson({id: 1});
res.addCurie('acme', 'https://api.acme.com/rels/{rel}');
res.addLink('acme:orders', '/users/1/orders');
var json = JSON.parse(JSON.stringify(res));
expect(json._links.curies).to.be.an('array');
assert.strictEqual(json._links.curies[0].name, 'acme');
expect(json._links['acme:orders']).to.exist;
});
});
describe('validate()', function() {
it('pass with no options', function() {
var result = halson().validate();
assert.strictEqual(result.valid, true);
assert.deepEqual(result.errors, []);
assert.deepEqual(result.warnings, []);
});
it('pass when required links are present', function() {
var res = halson()
.addLink('self', '/test')
.addLink('edit', '/test/edit');
var result = res.validate({requireLinks: ['self', 'edit']});
assert.strictEqual(result.valid, true);
assert.strictEqual(result.errors.length, 0);
});
it('fail when required links are missing', function() {
var result = halson().validate({requireLinks: ['self', 'edit']});
assert.strictEqual(result.valid, false);
assert.strictEqual(result.errors.length, 2);
expect(result.errors[0]).to.include('self');
expect(result.errors[1]).to.include('edit');
});
it('demote missing link to warning via allowMissingLinks', function() {
var result = halson().validate({
requireLinks: ['self', 'edit'],
allowMissingLinks: ['edit']
});
assert.strictEqual(result.valid, false);
assert.strictEqual(result.errors.length, 1);
assert.strictEqual(result.warnings.length, 1);
expect(result.errors[0]).to.include('self');
expect(result.warnings[0]).to.include('edit');
});
it('warn about missing self in strict mode', function() {
var result = halson().validate({strict: true});
assert.strictEqual(result.valid, true);
assert.strictEqual(result.warnings.length, 1);
expect(result.warnings[0]).to.include('self');
});
it('no self-link warning when self is present in strict mode', function() {
var res = halson().addLink('self', '/test');
var result = res.validate({strict: true});
assert.strictEqual(result.valid, true);
assert.strictEqual(result.warnings.length, 0);
});
it('combine strict and requireLinks', function() {
var res = halson().addLink('self', '/test').addLink('edit', '/edit');
var result = res.validate({
strict: true,
requireLinks: ['self', 'edit']
});
assert.strictEqual(result.valid, true);
assert.strictEqual(result.errors.length, 0);
assert.strictEqual(result.warnings.length, 0);
});
});
describe('accepts()', function() {
it('accept application/hal+json', function() {
assert.strictEqual(halson().accepts('application/hal+json'), true);
});
it('accept application/json', function() {
assert.strictEqual(halson().accepts('application/json'), true);
});
it('reject unsupported types', function() {
assert.strictEqual(halson().accepts('application/xml'), false);
assert.strictEqual(halson().accepts('text/html'), false);
});
});
describe('asJson()', function() {
it('strip _links and _embedded', function() {
var res = halson({id: 1, name: 'Test'});
res.addLink('self', '/test');
res.addEmbed('child', halson({childId: 2}));
var json = res.asJson();
assert.strictEqual(json.id, 1);
assert.strictEqual(json.name, 'Test');
assert.strictEqual(json._links, undefined);
assert.strictEqual(json._embedded, undefined);
});
it('strip className', function() {
var json = halson({id: 1}).asJson();
assert.strictEqual(json.className, undefined);
});
it('return empty object for empty resource', function() {
assert.deepEqual(halson().asJson(), {});
});
});
describe('asHal()', function() {
it('preserve full HAL structure', function() {
var res = halson({id: 1});
res.addLink('self', '/test');
var hal = res.asHal();
assert.strictEqual(hal.id, 1);
expect(hal._links).to.exist;
assert.strictEqual(hal._links.self.href, '/test');
});
it('return plain object (not HALSONResource)', function() {
var hal = halson({id: 1}).asHal();
expect(hal.className).to.be.an('undefined');
});
});
describe('getContentType()', function() {
it('return application/hal+json', function() {
assert.strictEqual(halson().getContentType(), 'application/hal+json');
});
});
describe('resolve()', function() {
it('return href for regular link', function() {
var res = halson().addLink('next', '/page/2');
assert.strictEqual(res.resolve('next'), '/page/2');
});
it('expand template when variables provided', function() {
var res = halson().addTemplate('search', '/search{?q}');
assert.strictEqual(res.resolve('search', {q: 'test'}), '/search?q=test');
});
it('return undefined for missing rel', function() {
assert.strictEqual(halson().resolve('missing'), undefined);
});
it('return raw template href when no variables', function() {
var res = halson().addTemplate('search', '/search{?q}');
// no variables → not templated expansion path
assert.strictEqual(res.resolve('search'), '/search{?q}');
});
});
describe('follow()', function() {
it('return a Promise', function() {
var res = halson().addLink('self', '/test');
var result = res.follow('self');
expect(result).to.be.an.instanceOf(Promise);
// suppress unhandled rejection
result.catch(function() {});
});
it('reject when link not found', function() {
return halson().follow('missing').then(
function() { throw new Error('should have rejected'); },
function(err) {
expect(err.message).to.include('not found');
}
);
});
it('reject when following a link fails', function() {
var res = halson().addLink('self', '/test');
return res.follow('self').then(
function() { throw new Error('should have rejected'); },
function(err) {
expect(err).to.be.an('error');
}
);
});
});
describe('followAll()', function() {
it('resolve to empty array for missing rel', function() {
return halson().followAll('missing').then(function(results) {
assert.deepEqual(results, []);
});
});
});
describe('HALResourceBuilder', function() {
it('build a resource with links and embeds', function() {
var resource = halson.HALResourceBuilder({id: 1, name: 'Test'})
.link('self', '/test')
.link('edit', {href: '/test/edit', method: 'PUT'})
.embed('child', halson({childId: 2}))
.build();
assert.strictEqual(resource.id, 1);
assert.strictEqual(resource.name, 'Test');
assert.strictEqual(resource.getHref('self'), '/test');
assert.strictEqual(resource.getLink('edit').method, 'PUT');
expect(resource.getEmbed('child')).to.exist;
});
it('support templates', function() {
var resource = halson.HALResourceBuilder({})
.template('search', '/search{?q}')
.build();
assert.strictEqual(resource.isTemplated('search'), true);
});
it('support curies', function() {
var resource = halson.HALResourceBuilder({})
.curie('acme', 'https://api.acme.com/rels/{rel}')
.build();
assert.strictEqual(resource.hasCurie('acme'), true);
});
it('build empty resource from no data', function() {
var resource = halson.HALResourceBuilder().build();
expect(resource.className).to.equal(halson.Resource.prototype.className);
});
});
describe('createPagedResource()', function() {
it('attach page metadata', function() {
var paged = halson.createPagedResource(
{items: ['a', 'b']},
{number: 0, size: 10, totalElements: 25, totalPages: 3}
);
assert.strictEqual(paged.page.number, 0);
assert.strictEqual(paged.page.size, 10);
assert.strictEqual(paged.page.totalElements, 25);
assert.strictEqual(paged.page.totalPages, 3);
assert.deepEqual(paged.items, ['a', 'b']);
});
it('hasNext/hasPrev reflect page position', function() {
var first = halson.createPagedResource({}, {number: 0, size: 10, totalElements: 30, totalPages: 3});
assert.strictEqual(first.hasNext(), true);
assert.strictEqual(first.hasPrev(), false);
var last = halson.createPagedResource({}, {number: 2, size: 10, totalElements: 30, totalPages: 3});
assert.strictEqual(last.hasNext(), false);
assert.strictEqual(last.hasPrev(), true);
var mid = halson.createPagedResource({}, {number: 1, size: 10, totalElements: 30, totalPages: 3});
assert.strictEqual(mid.hasNext(), true);
assert.strictEqual(mid.hasPrev(), true);
});
it('next/prev return hrefs from links', function() {
var paged = halson.createPagedResource({}, {number: 1, size: 10, totalElements: 30, totalPages: 3});
paged.addLink('next', '/page/2');
paged.addLink('prev', '/page/0');
assert.strictEqual(paged.next(), '/page/2');
assert.strictEqual(paged.prev(), '/page/0');
});
it('next/prev return null at boundaries', function() {
var first = halson.createPagedResource({}, {number: 0, size: 10, totalElements: 20, totalPages: 2});
assert.strictEqual(first.prev(), null);
var last = halson.createPagedResource({}, {number: 1, size: 10, totalElements: 20, totalPages: 2});
assert.strictEqual(last.next(), null);
});
it('provide default page metadata', function() {
var paged = halson.createPagedResource({});
assert.strictEqual(paged.page.number, 0);
assert.strictEqual(paged.page.size, 20);
assert.strictEqual(paged.page.totalElements, 0);
assert.strictEqual(paged.page.totalPages, 0);
});
it('be a regular HALSONResource', function() {
var paged = halson.createPagedResource({id: 1});
paged.addLink('self', '/page/1');
expect(paged.className).to.equal(halson.Resource.prototype.className);
assert.strictEqual(paged.getHref('self'), '/page/1');
});
});
describe('chaining across new methods', function() {
it('addLink + addTemplate + addCurie chains preserve state', function() {
var res = halson({id: 1})
.addLink('self', '/test')
.addTemplate('search', '/search{?q}')
.addCurie('acme', 'https://api.acme.com/rels/{rel}');
assert.strictEqual(res.id, 1);
assert.strictEqual(res.hasLink('self'), true);
assert.strictEqual(res.isTemplated('search'), true);
assert.strictEqual(res.hasCurie('acme'), true);
});
});
describe('round-trip serialization with new features', function() {
it('preserve curies and links through JSON round-trip', function() {
var res = halson({data: 'test'});
res.addLink('self', '/test');
res.addCurie('acme', 'https://api.acme.com/rels/{rel}');
var serialized = JSON.stringify(res);
var deserialized = halson(serialized);
assert.strictEqual(deserialized.data, 'test');
assert.strictEqual(deserialized.hasLink('self'), true);
assert.strictEqual(deserialized.hasCurie('acme'), true);
});
});
});