From ec03b3cfc57273d87913daf0b2cbcf51d5c748d7 Mon Sep 17 00:00:00 2001 From: Naz Gargol Date: Tue, 6 Nov 2018 16:12:12 +0100 Subject: [PATCH] Content API v2 date formatting (#10095) closes #10065 - Added UTC offset to dates returned by Content API - Added test checking new format is compatible with Admin API - Refactored output serializer mapping logic --- .../api/v2/utils/serializers/output/pages.js | 6 +- .../api/v2/utils/serializers/output/posts.js | 6 +- .../api/v2/utils/serializers/output/tags.js | 6 +- .../api/v2/utils/serializers/output/users.js | 6 +- .../v2/utils/serializers/output/utils/date.js | 32 ++++ .../utils/serializers/output/utils/mapper.js | 61 +++++++ .../v2/utils/serializers/output/utils/url.js | 19 --- .../functional/api/v2/admin/posts_spec.js | 12 +- .../v2/utils/serializers/output/pages_spec.js | 54 +++++++ .../v2/utils/serializers/output/posts_spec.js | 123 +++------------ .../v2/utils/serializers/output/tags_spec.js | 65 ++++++++ .../serializers/output/utils/date_spec.js | 26 +++ .../serializers/output/utils/mapper_spec.js | 149 ++++++++++++++++++ .../serializers/output/utils/url_spec.js | 55 +++++++ 14 files changed, 488 insertions(+), 132 deletions(-) create mode 100644 core/server/api/v2/utils/serializers/output/utils/date.js create mode 100644 core/server/api/v2/utils/serializers/output/utils/mapper.js create mode 100644 core/test/unit/api/v2/utils/serializers/output/pages_spec.js create mode 100644 core/test/unit/api/v2/utils/serializers/output/tags_spec.js create mode 100644 core/test/unit/api/v2/utils/serializers/output/utils/date_spec.js create mode 100644 core/test/unit/api/v2/utils/serializers/output/utils/mapper_spec.js create mode 100644 core/test/unit/api/v2/utils/serializers/output/utils/url_spec.js diff --git a/core/server/api/v2/utils/serializers/output/pages.js b/core/server/api/v2/utils/serializers/output/pages.js index 0b8524bdad97..a26ef7e25221 100644 --- a/core/server/api/v2/utils/serializers/output/pages.js +++ b/core/server/api/v2/utils/serializers/output/pages.js @@ -1,5 +1,5 @@ const debug = require('ghost-ignition').debug('api:v2:utils:serializers:output:pages'); -const url = require('./utils/url'); +const mapper = require('./utils/mapper'); module.exports = { all(models, apiConfig, frame) { @@ -7,7 +7,7 @@ module.exports = { if (models.meta) { frame.response = { - pages: models.data.map(model => url.forPost(model.id, model.toJSON(frame.options), frame.options)), + pages: models.data.map(model => mapper.mapPost(model, frame)), meta: models.meta }; @@ -15,7 +15,7 @@ module.exports = { } frame.response = { - pages: [url.forPost(models.id, models.toJSON(frame.options), frame.options)] + pages: [mapper.mapPost(models, frame)] }; debug(frame.response); diff --git a/core/server/api/v2/utils/serializers/output/posts.js b/core/server/api/v2/utils/serializers/output/posts.js index 4f24170a0816..40fae9382d82 100644 --- a/core/server/api/v2/utils/serializers/output/posts.js +++ b/core/server/api/v2/utils/serializers/output/posts.js @@ -1,5 +1,5 @@ const debug = require('ghost-ignition').debug('api:v2:utils:serializers:output:posts'); -const url = require('./utils/url'); +const mapper = require('./utils/mapper'); module.exports = { all(models, apiConfig, frame) { @@ -12,7 +12,7 @@ module.exports = { if (models.meta) { frame.response = { - posts: models.data.map(model => url.forPost(model.id, model.toJSON(frame.options), frame.options)), + posts: models.data.map(model => mapper.mapPost(model, frame)), meta: models.meta }; @@ -21,7 +21,7 @@ module.exports = { } frame.response = { - posts: [url.forPost(models.id, models.toJSON(frame.options), frame.options)] + posts: [mapper.mapPost(models, frame)] }; debug(frame.response); diff --git a/core/server/api/v2/utils/serializers/output/tags.js b/core/server/api/v2/utils/serializers/output/tags.js index e370648235a1..2f5681c3d414 100644 --- a/core/server/api/v2/utils/serializers/output/tags.js +++ b/core/server/api/v2/utils/serializers/output/tags.js @@ -1,5 +1,5 @@ const debug = require('ghost-ignition').debug('api:v2:utils:serializers:output:tags'); -const url = require('./utils/url'); +const mapper = require('./utils/mapper'); module.exports = { all(models, apiConfig, frame) { @@ -11,7 +11,7 @@ module.exports = { if (models.meta) { frame.response = { - tags: models.data.map(model => url.forTag(model.id, model.toJSON(frame.options), frame.options)), + tags: models.data.map(model => mapper.mapTag(model, frame)), meta: models.meta }; @@ -19,7 +19,7 @@ module.exports = { } frame.response = { - tags: [url.forTag(models.id, models.toJSON(frame.options), frame.options)] + tags: [mapper.mapTag(models, frame)] }; debug(frame.response); diff --git a/core/server/api/v2/utils/serializers/output/users.js b/core/server/api/v2/utils/serializers/output/users.js index c6b59194d6ff..5936b8ba67f0 100644 --- a/core/server/api/v2/utils/serializers/output/users.js +++ b/core/server/api/v2/utils/serializers/output/users.js @@ -1,13 +1,13 @@ const debug = require('ghost-ignition').debug('api:v2:utils:serializers:output:users'); const common = require('../../../../../lib/common'); -const url = require('./utils/url'); +const mapper = require('./utils/mapper'); module.exports = { browse(models, apiConfig, frame) { debug('browse'); frame.response = { - users: models.data.map(model => url.forUser(model.id, model.toJSON(frame.options), frame.options)), + users: models.data.map(model => mapper.mapUser(model, frame)), meta: models.meta }; @@ -18,7 +18,7 @@ module.exports = { debug('read'); frame.response = { - users: [url.forUser(model.id, model.toJSON(frame.options), frame.options)] + users: [mapper.mapUser(model, frame)] }; debug(frame.response); diff --git a/core/server/api/v2/utils/serializers/output/utils/date.js b/core/server/api/v2/utils/serializers/output/utils/date.js new file mode 100644 index 000000000000..ad4bbeafe7c3 --- /dev/null +++ b/core/server/api/v2/utils/serializers/output/utils/date.js @@ -0,0 +1,32 @@ +const moment = require('moment-timezone'); +const settingsCache = require('../../../../../../services/settings/cache'); + +const format = (date) => { + return moment(date) + .tz(settingsCache.get('active_timezone')) + .toISOString(true); +}; + +const forPost = (attrs) => { + ['created_at', 'updated_at', 'published_at'].forEach((field) => { + if (attrs[field]) { + attrs[field] = format(attrs[field]); + } + }); + + return attrs; +}; + +const forTag = (attrs) => { + ['created_at', 'updated_at'].forEach((field) => { + if (attrs[field]) { + attrs[field] = format(attrs[field]); + } + }); + + return attrs; +}; + +module.exports.format = format; +module.exports.forPost = forPost; +module.exports.forTag = forTag; diff --git a/core/server/api/v2/utils/serializers/output/utils/mapper.js b/core/server/api/v2/utils/serializers/output/utils/mapper.js new file mode 100644 index 000000000000..265a533f00ff --- /dev/null +++ b/core/server/api/v2/utils/serializers/output/utils/mapper.js @@ -0,0 +1,61 @@ +const utils = require('../../../index'); +const url = require('./url'); +const date = require('./date'); + +const mapPost = (model, frame) => { + const jsonModel = model.toJSON(frame.options); + + url.forPost(model.id, jsonModel, frame.options); + + if (utils.isContentAPI(frame)) { + date.forPost(jsonModel); + } + + if (frame.options && frame.options.withRelated) { + frame.options.withRelated.forEach((relation) => { + // @NOTE: this block also decorates primary_tag/primary_author objects as they + // are being passed by reference in tags/authors. Might be refactored into more explicit call + // in the future, but is good enough for current use-case + if (relation === 'tags' && jsonModel.tags) { + jsonModel.tags = jsonModel.tags.map(tag => url.forTag(tag.id, tag)); + + if (utils.isContentAPI(frame)) { + jsonModel.tags = jsonModel.tags.map(tag => date.forTag(tag)); + } + } + + if (relation === 'author' && jsonModel.author) { + jsonModel.author = url.forUser(jsonModel.author.id, jsonModel.author); + } + + if (relation === 'authors' && jsonModel.authors) { + jsonModel.authors = jsonModel.authors.map(author => url.forUser(author.id, author)); + } + }); + } + + return jsonModel; +}; + +const mapUser = (model, frame) => { + const jsonModel = model.toJSON(frame.options); + + url.forUser(model.id, jsonModel); + + return jsonModel; +}; + +const mapTag = (model, frame) => { + const jsonModel = model.toJSON(frame.options); + url.forTag(model.id, jsonModel); + + if (utils.isContentAPI(frame)) { + date.forTag(jsonModel); + } + + return jsonModel; +}; + +module.exports.mapPost = mapPost; +module.exports.mapUser = mapUser; +module.exports.mapTag = mapTag; diff --git a/core/server/api/v2/utils/serializers/output/utils/url.js b/core/server/api/v2/utils/serializers/output/utils/url.js index e795f2fb3d73..01e45a482e16 100644 --- a/core/server/api/v2/utils/serializers/output/utils/url.js +++ b/core/server/api/v2/utils/serializers/output/utils/url.js @@ -36,25 +36,6 @@ const forPost = (id, attrs, options) => { delete attrs.url; } - if (options && options.withRelated) { - options.withRelated.forEach((relation) => { - // @NOTE: this block also decorates primary_tag/primary_author objects as they - // are being passed by reference in tags/authors. Might be refactored into more explicit call - // in the future, but is good enough for current use-case - if (relation === 'tags' && attrs.tags) { - attrs.tags = attrs.tags.map(tag => forTag(tag.id, tag)); - } - - if (relation === 'author' && attrs.author) { - attrs.author = forUser(attrs.author.id, attrs.author, options); - } - - if (relation === 'authors' && attrs.authors) { - attrs.authors = attrs.authors.map(author => forUser(author.id, author, options)); - } - }); - } - return attrs; }; diff --git a/core/test/functional/api/v2/admin/posts_spec.js b/core/test/functional/api/v2/admin/posts_spec.js index cecdf53d07d5..9d828a6ab610 100644 --- a/core/test/functional/api/v2/admin/posts_spec.js +++ b/core/test/functional/api/v2/admin/posts_spec.js @@ -387,7 +387,6 @@ describe('Posts API V2', function () { res.body.posts[0].title.should.eql(post.title); res.body.posts[0].status.should.eql(post.status); res.body.posts[0].published_at.should.eql('2016-05-30T07:00:00.000Z'); - res.body.posts[0].published_at = '2016-05-30T09:00:00.000Z'; res.body.posts[0].created_at.should.not.eql(post.created_at.toISOString()); res.body.posts[0].updated_at.should.not.eql(post.updated_at.toISOString()); res.body.posts[0].updated_by.should.not.eql(post.updated_by); @@ -395,10 +394,13 @@ describe('Posts API V2', function () { }); }); - it('published post', function () { + it('published post with response timestamps in UTC format respecting original UTC offset', function () { const post = { posts: [{ - status: 'published' + status: 'published', + published_at: '2016-05-31T07:00:00.000+06:00', + created_at: '2016-05-30T03:00:00.000Z', + updated_at: '2016-05-30T07:00:00.000' }] }; @@ -413,6 +415,10 @@ describe('Posts API V2', function () { testUtils.API.checkResponse(res.body.posts[0], 'post'); res.body.posts[0].status.should.eql('published'); res.headers['x-cache-invalidate'].should.eql('/*'); + + res.body.posts[0].published_at.should.eql('2016-05-31T01:00:00.000Z'); + res.body.posts[0].created_at.should.eql('2016-05-30T03:00:00.000Z'); + res.body.posts[0].updated_at.should.eql('2016-05-30T07:00:00.000Z'); }); }); }); diff --git a/core/test/unit/api/v2/utils/serializers/output/pages_spec.js b/core/test/unit/api/v2/utils/serializers/output/pages_spec.js new file mode 100644 index 000000000000..c72aca3e7775 --- /dev/null +++ b/core/test/unit/api/v2/utils/serializers/output/pages_spec.js @@ -0,0 +1,54 @@ +const should = require('should'); +const sinon = require('sinon'); +const testUtils = require('../../../../../../utils'); +const mapper = require('../../../../../../../server/api/v2/utils/serializers/output/utils/mapper'); +const serializers = require('../../../../../../../server/api/v2/utils/serializers'); + +const sandbox = sinon.sandbox.create(); + +describe('Unit: v2/utils/serializers/output/pages', () => { + let pageModel; + + beforeEach(() => { + pageModel = (data) => { + return Object.assign(data, {toJSON: sandbox.stub().returns(data)}); + }; + + sandbox.stub(mapper, 'mapPost').returns({}); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('calls the mapper', () => { + const apiConfig = {}; + const frame = { + options: { + withRelated: ['tags', 'authors'], + context: { + private: false + } + } + }; + + const ctrlResponse = { + data: [ + pageModel(testUtils.DataGenerator.forKnex.createPost({ + id: 'id1', + page: true + })), + pageModel(testUtils.DataGenerator.forKnex.createPost({ + id: 'id2', + page: true + })) + ], + meta: {} + }; + + serializers.output.pages.all(ctrlResponse, apiConfig, frame); + + mapper.mapPost.callCount.should.equal(2); + mapper.mapPost.getCall(0).args.should.eql([ctrlResponse.data[0], frame]); + }); +}); diff --git a/core/test/unit/api/v2/utils/serializers/output/posts_spec.js b/core/test/unit/api/v2/utils/serializers/output/posts_spec.js index d63a8e7f6110..6100cf908401 100644 --- a/core/test/unit/api/v2/utils/serializers/output/posts_spec.js +++ b/core/test/unit/api/v2/utils/serializers/output/posts_spec.js @@ -1,121 +1,48 @@ const should = require('should'); const sinon = require('sinon'); const testUtils = require('../../../../../../utils'); -const urlService = require('../../../../../../../server/services/url'); +const mapper = require('../../../../../../../server/api/v2/utils/serializers/output/utils/mapper'); const serializers = require('../../../../../../../server/api/v2/utils/serializers'); + const sandbox = sinon.sandbox.create(); -describe('Unit: v2/utils/serializers/output/posts', function () { +describe('Unit: v2/utils/serializers/output/posts', () => { let postModel; - beforeEach(function () { + beforeEach(() => { postModel = (data) => { return Object.assign(data, {toJSON: sandbox.stub().returns(data)}); }; - sandbox.stub(urlService, 'getUrlByResourceId').returns('getUrlByResourceId'); - sandbox.stub(urlService.utils, 'urlFor').returns('urlFor'); - sandbox.stub(urlService.utils, 'makeAbsoluteUrls').returns({html: sandbox.stub()}); + sandbox.stub(mapper, 'mapPost').returns({}); }); - afterEach(function () { + afterEach(() => { sandbox.restore(); }); - describe('Ensure absolute urls are returned by default', function () { - it('meta & models & relations', function () { - const apiConfig = {}; - const frame = { - options: { - withRelated: ['tags', 'authors'] + it('calls the mapper', () => { + const apiConfig = {}; + const frame = { + options: { + withRelated: ['tags', 'authors'], + context: { + private: false } - }; - - const ctrlResponse = { - data: [ - postModel(testUtils.DataGenerator.forKnex.createPost({ - id: 'id1', - feature_image: 'value', - tags: [{ - id: 'id3', - feature_image: 'value' - }], - authors: [{ - id: 'id4', - name: 'Ghosty' - }] - })), - postModel(testUtils.DataGenerator.forKnex.createPost({ - id: 'id2', - html: ' { + let tagModel; + + beforeEach(() => { + tagModel = (data) => { + return Object.assign(data, {toJSON: sandbox.stub().returns(data)}); + }; + + sandbox.stub(mapper, 'mapTag').returns({}); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('calls the mapper when single tag present', () => { + const apiConfig = {}; + const frame = { + options: { + context: { + public: true + } + } + }; + + const ctrlResponse = tagModel(testUtils.DataGenerator.forKnex.createTag()); + + serializers.output.tags.all(ctrlResponse, apiConfig, frame); + + mapper.mapTag.callCount.should.equal(1); + mapper.mapTag.getCall(0).args.should.eql([ctrlResponse, frame]); + }); + + it('calls the mapper with multiple tags', () => { + const apiConfig = {}; + const frame = { + options: { + context: { + public: true + } + } + }; + + const ctrlResponse = tagModel({ + data: [ + testUtils.DataGenerator.forKnex.createTag(), + testUtils.DataGenerator.forKnex.createTag() + ], + meta: {} + }); + + serializers.output.tags.all(ctrlResponse, apiConfig, frame); + + mapper.mapTag.callCount.should.equal(2); + mapper.mapTag.getCall(0).args.should.eql([ctrlResponse.data[0], frame]); + }); +}); diff --git a/core/test/unit/api/v2/utils/serializers/output/utils/date_spec.js b/core/test/unit/api/v2/utils/serializers/output/utils/date_spec.js new file mode 100644 index 000000000000..1bf174d6520f --- /dev/null +++ b/core/test/unit/api/v2/utils/serializers/output/utils/date_spec.js @@ -0,0 +1,26 @@ +const should = require('should'); +const sinon = require('sinon'); +const settingsCache = require('../../../../../../../../server/services/settings/cache'); +const dateUtil = require('../../../../../../../../server/api/v2/utils/serializers/output/utils/date'); + +const sandbox = sinon.sandbox.create(); + +describe('Unit: v2/utils/serializers/output/utils/date', () => { + afterEach(() => { + sandbox.restore(); + }); + + it('creates date strings in ISO 8601 format with UTC offset', () => { + const timezone = 'Europe/Oslo'; + const testDates = [ + {in: '2014-01-01T01:28:58.593Z', out: '2014-01-01T02:28:58.593+01:00'}, + {in:'2014-12-31T23:28:58.123Z', out: '2015-01-01T00:28:58.123+01:00'}, + {in:'2014-03-01T01:28:58.593Z', out: '2014-03-01T02:28:58.593+01:00'} + ]; + sandbox.stub(settingsCache, 'get').returns(timezone); + + testDates.forEach((date) => { + dateUtil.format(date.in).should.equal(date.out); + }); + }); +}); diff --git a/core/test/unit/api/v2/utils/serializers/output/utils/mapper_spec.js b/core/test/unit/api/v2/utils/serializers/output/utils/mapper_spec.js new file mode 100644 index 000000000000..46f0b356720d --- /dev/null +++ b/core/test/unit/api/v2/utils/serializers/output/utils/mapper_spec.js @@ -0,0 +1,149 @@ +const should = require('should'); +const sinon = require('sinon'); +const testUtils = require('../../../../../../../utils'); +const dateUtil = require('../../../../../../../../server/api/v2/utils/serializers/output/utils/date'); +const urlUtil = require('../../../../../../../../server/api/v2/utils/serializers/output/utils/url'); +const mapper = require('../../../../../../../../server/api/v2/utils/serializers/output/utils/mapper'); + +const sandbox = sinon.sandbox.create(); + +describe('Unit: v2/utils/serializers/output/utils/mapper', () => { + beforeEach(() => { + sandbox.stub(dateUtil, 'forPost').returns({}); + sandbox.stub(dateUtil, 'forTag').returns({}); + + sandbox.stub(urlUtil, 'forPost').returns({}); + sandbox.stub(urlUtil, 'forTag').returns({}); + sandbox.stub(urlUtil, 'forUser').returns({}); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('mapPost', () => { + let postModel; + + beforeEach(() => { + postModel = (data) => { + return Object.assign(data, {toJSON: sandbox.stub().returns(data)}); + }; + }); + + it('calls mapper on relations', () => { + const frame = { + options: { + withRelated: ['tags', 'authors'], + context: { + public: true + } + } + }; + + const post = postModel(testUtils.DataGenerator.forKnex.createPost({ + id: 'id1', + feature_image: 'value', + page: true, + tags: [{ + id: 'id3', + feature_image: 'value' + }], + authors: [{ + id: 'id4', + name: 'Ghosty' + }] + })); + + mapper.mapPost(post, frame); + + dateUtil.forPost.callCount.should.equal(1); + dateUtil.forTag.callCount.should.equal(1); + + urlUtil.forPost.callCount.should.equal(1); + urlUtil.forTag.callCount.should.equal(1); + urlUtil.forUser.callCount.should.equal(1); + + urlUtil.forTag.getCall(0).args.should.eql(['id3', {id: 'id3', feature_image: 'value'}]); + urlUtil.forUser.getCall(0).args.should.eql(['id4', {name: 'Ghosty', id: 'id4'}]); + }); + }); + + describe('mapUser', () => { + let userModel; + + beforeEach(() => { + userModel = (data) => { + return Object.assign(data, {toJSON: sandbox.stub().returns(data)}); + }; + }); + + it('calls utils', () => { + const frame = { + options: {} + }; + + const user = userModel(testUtils.DataGenerator.forKnex.createUser({ + id: 'id1', + name: 'Ghosty' + })); + + mapper.mapUser(user, frame); + + urlUtil.forUser.callCount.should.equal(1); + + urlUtil.forUser.getCall(0).args.should.eql(['id1', user]); + }); + }); + + describe('mapTag', () => { + let tagModel; + + beforeEach(() => { + tagModel = (data) => { + return Object.assign(data, {toJSON: sandbox.stub().returns(data)}); + }; + }); + + it('calls utils', () => { + const frame = { + options: { + context: { + public: true + } + }, + }; + + const tag = tagModel(testUtils.DataGenerator.forKnex.createTag({ + id: 'id3', + feature_image: 'value' + })); + + mapper.mapTag(tag, frame); + + urlUtil.forTag.callCount.should.equal(1); + dateUtil.forTag.callCount.should.equal(1); + + urlUtil.forTag.getCall(0).args.should.eql(['id3', tag]); + dateUtil.forTag.getCall(0).args.should.eql([tag]); + }); + + it('does not call date formatter in private context', () => { + const frame = { + options: { + context: { + public: false + } + }, + }; + + const tag = tagModel(testUtils.DataGenerator.forKnex.createTag({ + id: 'id3', + feature_image: 'value' + })); + + mapper.mapTag(tag, frame); + + dateUtil.forTag.callCount.should.equal(0); + }); + }); +}); diff --git a/core/test/unit/api/v2/utils/serializers/output/utils/url_spec.js b/core/test/unit/api/v2/utils/serializers/output/utils/url_spec.js new file mode 100644 index 000000000000..89a643e59757 --- /dev/null +++ b/core/test/unit/api/v2/utils/serializers/output/utils/url_spec.js @@ -0,0 +1,55 @@ +const should = require('should'); +const sinon = require('sinon'); +const testUtils = require('../../../../../../../utils'); +const urlService = require('../../../../../../../../server/services/url'); +const urlUtil = require('../../../../../../../../server/api/v2/utils/serializers/output/utils/url'); + +const sandbox = sinon.sandbox.create(); + +describe('Unit: v2/utils/serializers/output/utils/url', () => { + beforeEach(() => { + sandbox.stub(urlService, 'getUrlByResourceId').returns('getUrlByResourceId'); + sandbox.stub(urlService.utils, 'urlFor').returns('urlFor'); + sandbox.stub(urlService.utils, 'makeAbsoluteUrls').returns({html: sandbox.stub()}); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('Ensure calls url service', () => { + let pageModel; + + beforeEach(() => { + pageModel = (data) => { + return Object.assign(data, {toJSON: sandbox.stub().returns(data)}); + }; + }); + + it('meta & models & relations', () => { + const post = pageModel(testUtils.DataGenerator.forKnex.createPost({ + id: 'id1', + feature_image: 'value', + })); + + urlUtil.forPost(post.id, post, {}); + + post.hasOwnProperty('url').should.be.true(); + + urlService.utils.urlFor.callCount.should.eql(2); + urlService.utils.urlFor.getCall(0).args.should.eql(['image', {image: 'value'}, true]); + urlService.utils.urlFor.getCall(1).args.should.eql(['home', true]); + + urlService.utils.makeAbsoluteUrls.callCount.should.eql(1); + urlService.utils.makeAbsoluteUrls.getCall(0).args.should.eql([ + '## markdown', + 'urlFor', + 'getUrlByResourceId', + {assetsOnly: true} + ]); + + urlService.getUrlByResourceId.callCount.should.eql(1); + urlService.getUrlByResourceId.getCall(0).args.should.eql(['id1', {absolute: true}]); + }); + }); +});