mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-24 19:33:02 +03:00
🎨Added absolute_url
flag to public api (#9833)
closes #9832 The API _should_ be returning absolute URLs for everything, 3rd party applications require absolute urls to read and display ghost data correctly. Currently they have to concat the blog url and the resource url, which is very uncomfortable. Changing the public api like this would be considered a breaking change however so we've opted to put it behind a query parameter named `absolute_urls`.
This commit is contained in:
parent
a796d73ed0
commit
c9b8ddde4b
@ -39,7 +39,7 @@ posts = {
|
||||
* @returns {Promise<Posts>} Posts Collection with Meta
|
||||
*/
|
||||
browse: function browse(options) {
|
||||
var extraOptions = ['status', 'formats'],
|
||||
var extraOptions = ['status', 'formats', 'absolute_urls'],
|
||||
permittedOptions,
|
||||
tasks;
|
||||
|
||||
@ -83,7 +83,7 @@ posts = {
|
||||
read: function read(options) {
|
||||
var attrs = ['id', 'slug', 'status', 'uuid'],
|
||||
// NOTE: the scheduler API uses the post API and forwards custom options
|
||||
extraAllowedOptions = options.opts || ['formats'],
|
||||
extraAllowedOptions = options.opts || ['formats', 'absolute_urls'],
|
||||
tasks;
|
||||
|
||||
/**
|
||||
|
@ -22,7 +22,8 @@ tags = {
|
||||
* @returns {Promise<Tags>} Tags Collection
|
||||
*/
|
||||
browse: function browse(options) {
|
||||
var tasks;
|
||||
var tasks,
|
||||
permittedOptions = localUtils.browseDefaultOptions.concat('absolute_urls');
|
||||
|
||||
/**
|
||||
* ### Model Query
|
||||
@ -36,7 +37,7 @@ tags = {
|
||||
|
||||
// Push all of our tasks into a `tasks` array in the correct order
|
||||
tasks = [
|
||||
localUtils.validate(docName, {opts: localUtils.browseDefaultOptions}),
|
||||
localUtils.validate(docName, {opts: permittedOptions}),
|
||||
localUtils.convertOptions(allowedIncludes),
|
||||
localUtils.handlePublicPermissions(docName, 'browse'),
|
||||
doQuery
|
||||
@ -53,6 +54,7 @@ tags = {
|
||||
*/
|
||||
read: function read(options) {
|
||||
var attrs = ['id', 'slug', 'visibility'],
|
||||
permittedOptions = ['absolute_urls'],
|
||||
tasks;
|
||||
|
||||
/**
|
||||
@ -78,7 +80,7 @@ tags = {
|
||||
|
||||
// Push all of our tasks into a `tasks` array in the correct order
|
||||
tasks = [
|
||||
localUtils.validate(docName, {attrs: attrs}),
|
||||
localUtils.validate(docName, {attrs: attrs, opts: permittedOptions}),
|
||||
localUtils.convertOptions(allowedIncludes),
|
||||
localUtils.handlePublicPermissions(docName, 'read'),
|
||||
doQuery
|
||||
|
@ -25,7 +25,7 @@ users = {
|
||||
* @returns {Promise<Users>} Users Collection
|
||||
*/
|
||||
browse: function browse(options) {
|
||||
var extraOptions = ['status'],
|
||||
var extraOptions = ['status', 'absolute_urls'],
|
||||
permittedOptions = localUtils.browseDefaultOptions.concat(extraOptions),
|
||||
tasks;
|
||||
|
||||
@ -58,6 +58,7 @@ users = {
|
||||
*/
|
||||
read: function read(options) {
|
||||
var attrs = ['id', 'slug', 'status', 'email', 'role'],
|
||||
permittedOptions = ['absolute_urls'],
|
||||
tasks;
|
||||
|
||||
// Special handling for /users/me request
|
||||
@ -88,7 +89,7 @@ users = {
|
||||
|
||||
// Push all of our tasks into a `tasks` array in the correct order
|
||||
tasks = [
|
||||
localUtils.validate(docName, {attrs: attrs}),
|
||||
localUtils.validate(docName, {attrs: attrs, opts: permittedOptions}),
|
||||
localUtils.convertOptions(allowedIncludes),
|
||||
localUtils.handlePublicPermissions(docName, 'read'),
|
||||
doQuery
|
||||
|
@ -499,7 +499,7 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
|
||||
*/
|
||||
permittedOptions: function permittedOptions(methodName) {
|
||||
if (methodName === 'toJSON') {
|
||||
return ['shallow', 'withRelated', 'context', 'columns'];
|
||||
return ['shallow', 'withRelated', 'context', 'columns', 'absolute_urls'];
|
||||
}
|
||||
|
||||
// terms to whitelist for all methods.
|
||||
@ -659,7 +659,10 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
|
||||
* information about the request (page, limit), along with the
|
||||
* info needed for pagination (pages, total).
|
||||
*
|
||||
* @TODO: This model function does return JSON O_O.
|
||||
* @TODO:
|
||||
* - this model function does return JSON O_O
|
||||
* - if you refactor that out, you should double check the allowed filter options
|
||||
* - because `toJSON` is called in here and is using the filtered options for the `findPage` function
|
||||
*
|
||||
* **response:**
|
||||
*
|
||||
|
@ -10,6 +10,7 @@ var _ = require('lodash'),
|
||||
config = require('../config'),
|
||||
converters = require('../lib/mobiledoc/converters'),
|
||||
urlService = require('../services/url'),
|
||||
{urlFor, makeAbsoluteUrls} = require('../services/url/utils'),
|
||||
relations = require('./relations'),
|
||||
Post,
|
||||
Posts;
|
||||
@ -440,6 +441,7 @@ Post = ghostBookshelf.Model.extend({
|
||||
attrs = ghostBookshelf.Model.prototype.toJSON.call(this, options);
|
||||
|
||||
attrs = this.formatsToJSON(attrs, options);
|
||||
attrs.url = urlService.getUrlByResourceId(attrs.id);
|
||||
|
||||
// If the current column settings allow it...
|
||||
if (!options.columns || (options.columns && options.columns.indexOf('primary_tag') > -1)) {
|
||||
@ -451,8 +453,26 @@ Post = ghostBookshelf.Model.extend({
|
||||
}
|
||||
}
|
||||
|
||||
if (!options.columns || (options.columns && options.columns.indexOf('url') > -1)) {
|
||||
attrs.url = urlService.getUrlByResourceId(attrs.id);
|
||||
if (options.columns && !options.columns.includes('url')) {
|
||||
delete attrs.url;
|
||||
}
|
||||
|
||||
if (options && options.context && options.context.public && options.absolute_urls) {
|
||||
if (attrs.feature_image) {
|
||||
attrs.feature_image = urlFor('image', {image: attrs.feature_image}, true);
|
||||
}
|
||||
if (attrs.og_image) {
|
||||
attrs.og_image = urlFor('image', {image: attrs.og_image}, true);
|
||||
}
|
||||
if (attrs.twitter_image) {
|
||||
attrs.twitter_image = urlFor('image', {image: attrs.twitter_image}, true);
|
||||
}
|
||||
if (attrs.html) {
|
||||
attrs.html = makeAbsoluteUrls(attrs.html, urlFor('home', true), attrs.url).html();
|
||||
}
|
||||
if (attrs.url) {
|
||||
attrs.url = urlFor({relativeUrl: attrs.url}, true);
|
||||
}
|
||||
}
|
||||
|
||||
return attrs;
|
||||
@ -534,13 +554,13 @@ Post = ghostBookshelf.Model.extend({
|
||||
* @return {Array} Keys allowed in the `options` hash of the model's method.
|
||||
*/
|
||||
permittedOptions: function permittedOptions(methodName) {
|
||||
var options = ghostBookshelf.Model.permittedOptions(),
|
||||
var options = ghostBookshelf.Model.permittedOptions(methodName),
|
||||
|
||||
// whitelists for the `options` hash argument on methods, by method name.
|
||||
// these are the only options that can be passed to Bookshelf / Knex.
|
||||
validOptions = {
|
||||
findOne: ['columns', 'importing', 'withRelated', 'require'],
|
||||
findPage: ['page', 'limit', 'columns', 'filter', 'order', 'status', 'staticPages'],
|
||||
findPage: ['page', 'limit', 'columns', 'filter', 'order', 'status', 'staticPages', 'absolute_urls'],
|
||||
findAll: ['columns', 'filter'],
|
||||
destroy: ['destroyAll']
|
||||
};
|
||||
|
@ -1,4 +1,6 @@
|
||||
const ghostBookshelf = require('./base');
|
||||
const ghostBookshelf = require('./base'),
|
||||
urlService = require('../services/url'),
|
||||
{urlFor} = require('../services/url/utils');
|
||||
let Tag, Tags;
|
||||
|
||||
Tag = ghostBookshelf.Model.extend({
|
||||
@ -66,6 +68,15 @@ Tag = ghostBookshelf.Model.extend({
|
||||
attrs.parent = attrs.parent || attrs.parent_id;
|
||||
delete attrs.parent_id;
|
||||
|
||||
if (options && options.context && options.context.public && options.absolute_urls) {
|
||||
attrs.url = urlFor({
|
||||
relativeUrl: urlService.getUrlByResourceId(attrs.id)
|
||||
}, true);
|
||||
if (attrs.feature_image) {
|
||||
attrs.feature_image = urlFor('image', {image: attrs.feature_image}, true);
|
||||
}
|
||||
}
|
||||
|
||||
return attrs;
|
||||
}
|
||||
}, {
|
||||
@ -81,12 +92,12 @@ Tag = ghostBookshelf.Model.extend({
|
||||
},
|
||||
|
||||
permittedOptions: function permittedOptions(methodName) {
|
||||
var options = ghostBookshelf.Model.permittedOptions(),
|
||||
var options = ghostBookshelf.Model.permittedOptions(methodName),
|
||||
|
||||
// whitelists for the `options` hash argument on methods, by method name.
|
||||
// these are the only options that can be passed to Bookshelf / Knex.
|
||||
validOptions = {
|
||||
findPage: ['page', 'limit', 'columns', 'filter', 'order'],
|
||||
findPage: ['page', 'limit', 'columns', 'filter', 'order', 'absolute_urls'],
|
||||
findAll: ['columns'],
|
||||
findOne: ['visibility'],
|
||||
destroy: ['destroyAll']
|
||||
|
@ -9,12 +9,14 @@ const _ = require('lodash'),
|
||||
imageLib = require('../lib/image'),
|
||||
pipeline = require('../lib/promise/pipeline'),
|
||||
validation = require('../data/validation'),
|
||||
urlService = require('../services/url'),
|
||||
activeStates = ['active', 'warn-1', 'warn-2', 'warn-3', 'warn-4'],
|
||||
/**
|
||||
* inactive: owner user before blog setup, suspended users
|
||||
* locked user: imported users, they get a random passport
|
||||
*/
|
||||
inactiveStates = ['inactive', 'locked'],
|
||||
{urlFor} = require('../services/url/utils'),
|
||||
allStates = activeStates.concat(inactiveStates);
|
||||
|
||||
let User, Users;
|
||||
@ -209,6 +211,17 @@ User = ghostBookshelf.Model.extend({
|
||||
delete attrs.last_seen;
|
||||
delete attrs.status;
|
||||
delete attrs.ghost_auth_id;
|
||||
if (options.absolute_urls) {
|
||||
attrs.url = urlFor({
|
||||
relativeUrl: urlService.getUrlByResourceId(attrs.id)
|
||||
}, true);
|
||||
if (attrs.profile_image) {
|
||||
attrs.profile_image = urlFor('image', {image: attrs.profile_image}, true);
|
||||
}
|
||||
if (attrs.cover_image) {
|
||||
attrs.cover_image = urlFor('image', {image: attrs.cover_image}, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return attrs;
|
||||
@ -315,7 +328,7 @@ User = ghostBookshelf.Model.extend({
|
||||
* @return {Array} Keys allowed in the `options` hash of the model's method.
|
||||
*/
|
||||
permittedOptions: function permittedOptions(methodName, options) {
|
||||
var permittedOptionsToReturn = ghostBookshelf.Model.permittedOptions(),
|
||||
var permittedOptionsToReturn = ghostBookshelf.Model.permittedOptions(methodName),
|
||||
|
||||
// whitelists for the `options` hash argument on methods, by method name.
|
||||
// these are the only options that can be passed to Bookshelf / Knex.
|
||||
@ -324,7 +337,7 @@ User = ghostBookshelf.Model.extend({
|
||||
setup: ['id'],
|
||||
edit: ['withRelated', 'id', 'importPersistUser'],
|
||||
add: ['importPersistUser'],
|
||||
findPage: ['page', 'limit', 'columns', 'filter', 'order', 'status'],
|
||||
findPage: ['page', 'limit', 'columns', 'filter', 'order', 'status', 'absolute_urls'],
|
||||
findAll: ['filter']
|
||||
};
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
// # API routes
|
||||
const debug = require('ghost-ignition').debug('api'),
|
||||
boolParser = require('express-query-boolean'),
|
||||
express = require('express'),
|
||||
|
||||
// routes
|
||||
@ -33,6 +34,9 @@ module.exports = function setupApiApp() {
|
||||
apiApp.use(bodyParser.json({limit: '1mb'}));
|
||||
apiApp.use(bodyParser.urlencoded({extended: true, limit: '1mb'}));
|
||||
|
||||
// Query parsing
|
||||
apiApp.use(boolParser());
|
||||
|
||||
// send 503 json response in case of maintenance
|
||||
apiApp.use(maintenance);
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
var should = require('should'),
|
||||
supertest = require('supertest'),
|
||||
_ = require('lodash'),
|
||||
url = require('url'),
|
||||
cheerio = require('cheerio'),
|
||||
moment = require('moment'),
|
||||
testUtils = require('../../../utils'),
|
||||
configUtils = require('../../../utils/configUtils'),
|
||||
@ -53,6 +55,39 @@ describe('Public API', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('browse posts: request absolute urls', function (done) {
|
||||
request.get(testUtils.API.getApiQuery('posts/?client_id=ghost-admin&client_secret=not_available&absolute_urls=true'))
|
||||
.set('Origin', testUtils.API.getURL())
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
should.exist(res.body.posts);
|
||||
|
||||
// kitchen sink
|
||||
res.body.posts[9].slug.should.eql(testUtils.DataGenerator.Content.posts[1].slug);
|
||||
|
||||
let urlParts = url.parse(res.body.posts[9].feature_image);
|
||||
should.exist(urlParts.protocol);
|
||||
should.exist(urlParts.host);
|
||||
|
||||
urlParts = url.parse(res.body.posts[9].url);
|
||||
should.exist(urlParts.protocol);
|
||||
should.exist(urlParts.host);
|
||||
|
||||
const $ = cheerio.load(res.body.posts[9].html);
|
||||
urlParts = url.parse($('img').attr('src'));
|
||||
should.exist(urlParts.protocol);
|
||||
should.exist(urlParts.host);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('browse posts from different origin', function (done) {
|
||||
request.get(testUtils.API.getApiQuery('posts/?client_id=ghost-test&client_secret=not_available'))
|
||||
.set('Origin', 'https://example.com')
|
||||
|
@ -1,6 +1,7 @@
|
||||
/* eslint no-invalid-this:0 */
|
||||
|
||||
const should = require('should'),
|
||||
url = require('url'),
|
||||
sinon = require('sinon'),
|
||||
_ = require('lodash'),
|
||||
testUtils = require('../../utils'),
|
||||
@ -33,6 +34,63 @@ describe('Unit: models/post', function () {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('toJSON', function () {
|
||||
const toJSON = function toJSON(model, options) {
|
||||
return new models.Post(model).toJSON(options);
|
||||
};
|
||||
|
||||
describe('Public context', function () {
|
||||
const context = {
|
||||
public: true
|
||||
};
|
||||
|
||||
it('converts relative feature_image url to absolute when absolute_urls flag passed', function () {
|
||||
const model = {
|
||||
feature_image: '/content/images/feature_image.jpg'
|
||||
};
|
||||
const json = toJSON(model, {context, absolute_urls: true});
|
||||
const featureImageUrlObject = url.parse(json.feature_image);
|
||||
|
||||
should.exist(featureImageUrlObject.protocol);
|
||||
should.exist(featureImageUrlObject.host);
|
||||
});
|
||||
|
||||
it('converts relative twitter_image url to absolute when absolute_urls flag passed', function () {
|
||||
const model = {
|
||||
twitter_image: '/content/images/twitter_image.jpg'
|
||||
};
|
||||
const json = toJSON(model, {context, absolute_urls: true});
|
||||
const twitterImageUrlObject = url.parse(json.twitter_image);
|
||||
|
||||
should.exist(twitterImageUrlObject.protocol);
|
||||
should.exist(twitterImageUrlObject.host);
|
||||
});
|
||||
|
||||
it('converts relative og_image url to absolute when absolute_urls flag passed', function () {
|
||||
const model = {
|
||||
og_image: '/content/images/og_image.jpg'
|
||||
};
|
||||
const json = toJSON(model, {context, absolute_urls: true});
|
||||
const ogImageUrlObject = url.parse(json.og_image);
|
||||
|
||||
should.exist(ogImageUrlObject.protocol);
|
||||
should.exist(ogImageUrlObject.host);
|
||||
});
|
||||
|
||||
it('converts relative content urls to absolute when absolute_urls flag passed', function () {
|
||||
const model = {
|
||||
html: '<img src="/content/images/my-coole-image.jpg">'
|
||||
};
|
||||
const json = toJSON(model, {context, absolute_urls: true});
|
||||
const imgSrc = json.html.match(/src="([^"]+)"/)[1];
|
||||
const imgSrcUrlObject = url.parse(imgSrc);
|
||||
|
||||
should.exist(imgSrcUrlObject.protocol);
|
||||
should.exist(imgSrcUrlObject.host);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('add', function () {
|
||||
describe('ensure full set of data for model events', function () {
|
||||
it('default', function () {
|
||||
|
@ -1,4 +1,5 @@
|
||||
var should = require('should'),
|
||||
url = require('url'),
|
||||
sinon = require('sinon'),
|
||||
models = require('../../../server/models'),
|
||||
testUtils = require('../../utils'),
|
||||
@ -16,6 +17,29 @@ describe('Unit: models/tags', function () {
|
||||
before(testUtils.teardown);
|
||||
before(testUtils.setup('tags'));
|
||||
|
||||
describe('toJSON', function () {
|
||||
const toJSON = function toJSON(model, options) {
|
||||
return new models.Tag(model).toJSON(options);
|
||||
};
|
||||
|
||||
describe('Public context', function () {
|
||||
const context = {
|
||||
public: true
|
||||
};
|
||||
|
||||
it('converts relative feature_image url to absolute when absolute_urls flag passed', function () {
|
||||
const model = {
|
||||
feature_image: '/content/images/feature_image.jpg'
|
||||
};
|
||||
const json = toJSON(model, {context, absolute_urls: true});
|
||||
const featureImageUrlObject = url.parse(json.feature_image);
|
||||
|
||||
should.exist(featureImageUrlObject.protocol);
|
||||
should.exist(featureImageUrlObject.host);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edit', function () {
|
||||
it('resets given empty value to null', function () {
|
||||
return models.Tag.findOne({slug: 'kitchen-sink'})
|
||||
|
@ -1,4 +1,5 @@
|
||||
const should = require('should'),
|
||||
url = require('url'),
|
||||
sinon = require('sinon'),
|
||||
_ = require('lodash'),
|
||||
schema = require('../../../server/data/schema'),
|
||||
@ -21,6 +22,40 @@ describe('Unit: models/user', function () {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('toJSON', function () {
|
||||
const toJSON = function toJSON(model, options) {
|
||||
return new models.User(model).toJSON(options);
|
||||
};
|
||||
|
||||
describe('Public context', function () {
|
||||
const context = {
|
||||
public: true
|
||||
};
|
||||
|
||||
it('converts relative profile_image url to absolute when absolute_urls flag passed', function () {
|
||||
const model = {
|
||||
profile_image: '/content/images/profile_image.jpg'
|
||||
};
|
||||
const json = toJSON(model, {context, absolute_urls: true});
|
||||
const profileImageUrlObject = url.parse(json.profile_image);
|
||||
|
||||
should.exist(profileImageUrlObject.protocol);
|
||||
should.exist(profileImageUrlObject.host);
|
||||
});
|
||||
|
||||
it('converts relative cover_image url to absolute when absolute_urls flag passed', function () {
|
||||
const model = {
|
||||
cover_image: '/content/images/cover_image.jpg'
|
||||
};
|
||||
const json = toJSON(model, {context, absolute_urls: true});
|
||||
const coverImageUrlObject = url.parse(json.cover_image);
|
||||
|
||||
should.exist(coverImageUrlObject.protocol);
|
||||
should.exist(coverImageUrlObject.host);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('validation', function () {
|
||||
beforeEach(function () {
|
||||
sandbox.stub(security.password, 'hash').resolves('$2a$10$we16f8rpbrFZ34xWj0/ZC.LTPUux8ler7bcdTs5qIleN6srRHhilG');
|
||||
|
@ -92,7 +92,7 @@ describe('RSS: Generate Feed', function () {
|
||||
xmlData.should.match(/<guid isPermaLink="false">/);
|
||||
xmlData.should.match(/<\/guid><dc:creator><!\[CDATA\[Joe Bloggs\]\]><\/dc:creator>/);
|
||||
xmlData.should.match(/<pubDate>Thu, 01 Jan 2015/);
|
||||
xmlData.should.match(/<content:encoded><!\[CDATA\[<h1>HTML Ipsum Presents<\/h1><p><strong>Pellentes/);
|
||||
xmlData.should.match(/<content:encoded><!\[CDATA\[<h1>HTML Ipsum Presents<\/h1>/);
|
||||
xmlData.should.match(/<\/code><\/pre>\]\]><\/content:encoded><\/item>/);
|
||||
xmlData.should.not.match(/<author>/);
|
||||
|
||||
|
@ -85,7 +85,7 @@ describe('Unit: services/url/Resources', function () {
|
||||
options.event.should.eql('added');
|
||||
const obj = _.find(resources.data.posts, {data: {slug: 'test-1234'}}).data;
|
||||
|
||||
Object.keys(obj).should.eql([
|
||||
Object.keys(obj).sort().should.eql([
|
||||
'id',
|
||||
'uuid',
|
||||
'slug',
|
||||
@ -106,17 +106,17 @@ describe('Unit: services/url/Resources', function () {
|
||||
'primary_author',
|
||||
'primary_tag',
|
||||
'url'
|
||||
]);
|
||||
].sort());
|
||||
|
||||
should.exist(resources.getByIdAndType(options.eventData.type, options.eventData.id));
|
||||
obj.tags.length.should.eql(1);
|
||||
Object.keys(obj.tags[0]).should.eql(['id', 'slug']);
|
||||
Object.keys(obj.tags[0]).sort().should.eql(['id', 'slug'].sort());
|
||||
obj.authors.length.should.eql(1);
|
||||
Object.keys(obj.authors[0]).should.eql(['id', 'slug']);
|
||||
Object.keys(obj.authors[0]).sort().should.eql(['id', 'slug'].sort());
|
||||
should.exist(obj.primary_author);
|
||||
Object.keys(obj.primary_author).should.eql(['id', 'slug']);
|
||||
Object.keys(obj.primary_author).sort().should.eql(['id', 'slug'].sort());
|
||||
should.exist(obj.primary_tag);
|
||||
Object.keys(obj.primary_tag).should.eql(['id', 'slug']);
|
||||
Object.keys(obj.primary_tag).sort().should.eql(['id', 'slug'].sort());
|
||||
done();
|
||||
});
|
||||
|
||||
@ -184,7 +184,7 @@ describe('Unit: services/url/Resources', function () {
|
||||
|
||||
const obj = _.find(resources.data.posts, {data: {id: resourceToUpdate.data.id}}).data;
|
||||
|
||||
Object.keys(obj).should.eql([
|
||||
Object.keys(obj).sort().should.eql([
|
||||
'id',
|
||||
'uuid',
|
||||
'slug',
|
||||
@ -205,16 +205,16 @@ describe('Unit: services/url/Resources', function () {
|
||||
'primary_author',
|
||||
'primary_tag',
|
||||
'url'
|
||||
]);
|
||||
].sort());
|
||||
|
||||
should.exist(obj.tags);
|
||||
Object.keys(obj.tags[0]).should.eql(['id', 'slug']);
|
||||
Object.keys(obj.tags[0]).sort().should.eql(['id', 'slug'].sort());
|
||||
should.exist(obj.authors);
|
||||
Object.keys(obj.authors[0]).should.eql(['id', 'slug']);
|
||||
Object.keys(obj.authors[0]).sort().should.eql(['id', 'slug'].sort());
|
||||
should.exist(obj.primary_author);
|
||||
Object.keys(obj.primary_author).should.eql(['id', 'slug']);
|
||||
Object.keys(obj.primary_author).sort().should.eql(['id', 'slug'].sort());
|
||||
should.exist(obj.primary_tag);
|
||||
Object.keys(obj.primary_tag).should.eql(['id', 'slug']);
|
||||
Object.keys(obj.primary_tag).sort().should.eql(['id', 'slug'].sort());
|
||||
|
||||
done();
|
||||
});
|
||||
|
@ -37,8 +37,9 @@ DataGenerator.Content = {
|
||||
id: ObjectId.generate(),
|
||||
title: 'Ghostly Kitchen Sink',
|
||||
slug: 'ghostly-kitchen-sink',
|
||||
mobiledoc: DataGenerator.markdownToMobiledoc('<h1>HTML Ipsum Presents</h1><p><strong>Pellentesque habitant morbi tristique</strong> senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. <em>Aenean ultricies mi vitae est.</em> Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, <code>commodo vitae</code>, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. <a href=\\\"#\\\">Donec non enim</a> in turpis pulvinar facilisis. Ut felis.</p><h2>Header Level 2</h2><ol><li>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</li><li>Aliquam tincidunt mauris eu risus.</li></ol><blockquote><p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est.</p></blockquote><h3>Header Level 3</h3><ul><li>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</li><li>Aliquam tincidunt mauris eu risus.</li></ul><pre><code>#header h1 a{display: block;width: 300px;height: 80px;}</code></pre>'),
|
||||
published_at: new Date('2015-01-02')
|
||||
mobiledoc: DataGenerator.markdownToMobiledoc('<h1>HTML Ipsum Presents</h1><img src="/content/images/lol.jpg"><p><strong>Pellentesque habitant morbi tristique</strong> senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. <em>Aenean ultricies mi vitae est.</em> Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, <code>commodo vitae</code>, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. <a href=\\\"#\\\">Donec non enim</a> in turpis pulvinar facilisis. Ut felis.</p><h2>Header Level 2</h2><ol><li>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</li><li>Aliquam tincidunt mauris eu risus.</li></ol><blockquote><p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est.</p></blockquote><h3>Header Level 3</h3><ul><li>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</li><li>Aliquam tincidunt mauris eu risus.</li></ul><pre><code>#header h1 a{display: block;width: 300px;height: 80px;}</code></pre>'),
|
||||
published_at: new Date('2015-01-02'),
|
||||
feature_image: '/content/images/2018/hey.jpg'
|
||||
},
|
||||
{
|
||||
id: ObjectId.generate(),
|
||||
|
@ -51,6 +51,7 @@
|
||||
"express": "4.16.3",
|
||||
"express-brute": "1.0.1",
|
||||
"express-hbs": "1.0.4",
|
||||
"express-query-boolean": "2.0.0",
|
||||
"extract-zip": "1.6.7",
|
||||
"fs-extra": "3.0.1",
|
||||
"ghost-gql": "0.0.10",
|
||||
|
@ -1766,6 +1766,10 @@ express-hbs@1.0.4, express-hbs@^1.0.3:
|
||||
js-beautify "1.6.8"
|
||||
readdirp "2.1.0"
|
||||
|
||||
express-query-boolean@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/express-query-boolean/-/express-query-boolean-2.0.0.tgz#ea56ac8138e2b95b171b8eee2af88738302941c3"
|
||||
|
||||
express@4.16.3, express@^4.16.2:
|
||||
version "4.16.3"
|
||||
resolved "https://registry.yarnpkg.com/express/-/express-4.16.3.tgz#6af8a502350db3246ecc4becf6b5a34d22f7ed53"
|
||||
|
Loading…
Reference in New Issue
Block a user