Removed api integration tests (#9940)

refs #9866 

- moved the tests either to unit tests or routing tests
- or removed test case (a lot)
- this commit is very big 🤪, it was not rly possible to create clean commits for this
- it only changes the test env, no real code is touched

Next steps:
- optimise folder structure + make v2 testing possible
- reduce some more tests from routing and model integeration tests
This commit is contained in:
Katharina Irrgang 2018-10-06 22:13:52 +02:00 committed by GitHub
parent 476ac185aa
commit db1d2f62dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 2720 additions and 7645 deletions

View File

@ -114,5 +114,9 @@ module.exports = {
common.events.removeListener('settings.edited', _private.updateSettingFromModel); common.events.removeListener('settings.edited', _private.updateSettingFromModel);
common.events.removeListener('settings.added', _private.updateSettingFromModel); common.events.removeListener('settings.added', _private.updateSettingFromModel);
common.events.removeListener('settings.deleted', _private.updateSettingFromModel); common.events.removeListener('settings.deleted', _private.updateSettingFromModel);
},
reset() {
settingsCache = {};
} }
}; };

View File

@ -15,239 +15,401 @@ var should = require('should'),
describe('Authentication API', function () { describe('Authentication API', function () {
var accesstoken = '', ghostServer; var accesstoken = '', ghostServer;
before(function () { describe('auth & authorize', function () {
return ghost() before(function () {
.then(function (_ghostServer) { return ghost()
ghostServer = _ghostServer; .then(function (_ghostServer) {
request = supertest.agent(config.get('url')); ghostServer = _ghostServer;
}) request = supertest.agent(config.get('url'));
.then(function () { })
return localUtils.doAuth(request); .then(function () {
}) return localUtils.doAuth(request);
.then(function (token) { })
accesstoken = token; .then(function (token) {
}); accesstoken = token;
}); });
});
afterEach(function () { afterEach(function () {
return testUtils.clearBruteData(); return testUtils.clearBruteData();
}); });
it('can authenticate', function (done) { it('can authenticate', function (done) {
request.post(localUtils.API.getApiQuery('authentication/token')) request.post(localUtils.API.getApiQuery('authentication/token'))
.set('Origin', config.get('url')) .set('Origin', config.get('url'))
.send({ .send({
grant_type: 'password', grant_type: 'password',
username: user.email, username: user.email,
password: user.password, password: user.password,
client_id: 'ghost-admin', client_id: 'ghost-admin',
client_secret: 'not_available' client_secret: 'not_available'
}) })
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
// TODO: make it possible to override oauth2orize's header so that this is consistent // TODO: make it possible to override oauth2orize's header so that this is consistent
.expect('Cache-Control', 'no-store') .expect('Cache-Control', 'no-store')
.expect(200) .expect(200)
.end(function (err, res) { .end(function (err, res) {
if (err) { if (err) {
return done(err); return done(err);
} }
should.not.exist(res.headers['x-cache-invalidate']); should.not.exist(res.headers['x-cache-invalidate']);
var jsonResponse = res.body, var jsonResponse = res.body,
newAccessToken; newAccessToken;
should.exist(jsonResponse.access_token); should.exist(jsonResponse.access_token);
should.exist(jsonResponse.refresh_token); should.exist(jsonResponse.refresh_token);
should.exist(jsonResponse.expires_in); should.exist(jsonResponse.expires_in);
should.exist(jsonResponse.token_type); should.exist(jsonResponse.token_type);
models.Accesstoken.findOne({ models.Accesstoken.findOne({
token: jsonResponse.access_token token: jsonResponse.access_token
}).then(function (_newAccessToken) { }).then(function (_newAccessToken) {
newAccessToken = _newAccessToken; newAccessToken = _newAccessToken;
return models.Refreshtoken.findOne({ return models.Refreshtoken.findOne({
token: jsonResponse.refresh_token token: jsonResponse.refresh_token
}); });
}).then(function (newRefreshToken) { }).then(function (newRefreshToken) {
newAccessToken.get('issued_by').should.eql(newRefreshToken.id); newAccessToken.get('issued_by').should.eql(newRefreshToken.id);
done();
}).catch(done);
});
});
it('can\'t authenticate unknown user', function (done) {
request.post(localUtils.API.getApiQuery('authentication/token'))
.set('Origin', config.get('url'))
.set('Accept', 'application/json')
.send({
grant_type: 'password',
username: 'invalid@email.com',
password: user.password,
client_id: 'ghost-admin',
client_secret: 'not_available'
}).expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.end(function (err, res) {
if (err) {
return done(err);
}
var jsonResponse = res.body;
should.exist(jsonResponse.errors[0].errorType);
jsonResponse.errors[0].errorType.should.eql('NotFoundError');
done(); done();
}).catch(done); });
}); });
});
it('can\'t authenticate unknown user', function (done) { it('can\'t authenticate invalid password user', function (done) {
request.post(localUtils.API.getApiQuery('authentication/token')) request.post(localUtils.API.getApiQuery('authentication/token'))
.set('Origin', config.get('url')) .set('Origin', config.get('url'))
.set('Accept', 'application/json') .set('Accept', 'application/json')
.send({ .send({
grant_type: 'password', grant_type: 'password',
username: 'invalid@email.com', username: user.email,
password: user.password, password: 'invalid',
client_id: 'ghost-admin', client_id: 'ghost-admin',
client_secret: 'not_available' client_secret: 'not_available'
}).expect('Content-Type', /json/) }).expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private) .expect('Cache-Control', testUtils.cacheRules.private)
.expect(404) .expect(422)
.end(function (err, res) { .end(function (err, res) {
if (err) { if (err) {
return done(err); return done(err);
} }
var jsonResponse = res.body; var jsonResponse = res.body;
should.exist(jsonResponse.errors[0].errorType); should.exist(jsonResponse.errors[0].errorType);
jsonResponse.errors[0].errorType.should.eql('NotFoundError'); jsonResponse.errors[0].errorType.should.eql('ValidationError');
done(); done();
}); });
}); });
it('can\'t authenticate invalid password user', function (done) { it('can request new access token', function (done) {
request.post(localUtils.API.getApiQuery('authentication/token')) request.post(localUtils.API.getApiQuery('authentication/token'))
.set('Origin', config.get('url')) .set('Origin', config.get('url'))
.set('Accept', 'application/json') .send({
.send({ grant_type: 'password',
grant_type: 'password', username: user.email,
username: user.email, password: user.password,
password: 'invalid', client_id: 'ghost-admin',
client_id: 'ghost-admin', client_secret: 'not_available'
client_secret: 'not_available' })
}).expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private) // TODO: make it possible to override oauth2orize's header so that this is consistent
.expect(422) .expect('Cache-Control', 'no-store')
.end(function (err, res) { .expect(200)
if (err) { .end(function (err, res) {
return done(err); if (err) {
} return done(err);
var jsonResponse = res.body; }
should.exist(jsonResponse.errors[0].errorType);
jsonResponse.errors[0].errorType.should.eql('ValidationError');
done();
});
});
it('can request new access token', function (done) { var refreshToken = res.body.refresh_token;
request.post(localUtils.API.getApiQuery('authentication/token'))
.set('Origin', config.get('url'))
.send({
grant_type: 'password',
username: user.email,
password: user.password,
client_id: 'ghost-admin',
client_secret: 'not_available'
})
.expect('Content-Type', /json/)
// TODO: make it possible to override oauth2orize's header so that this is consistent
.expect('Cache-Control', 'no-store')
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
var refreshToken = res.body.refresh_token; models.Accesstoken.findOne({
token: accesstoken
}).then(function (oldAccessToken) {
moment(oldAccessToken.get('expires')).diff(moment(), 'minutes').should.be.above(6);
models.Accesstoken.findOne({ request.post(localUtils.API.getApiQuery('authentication/token'))
token: accesstoken .set('Origin', config.get('url'))
}).then(function (oldAccessToken) { .set('Authorization', 'Bearer ' + accesstoken)
moment(oldAccessToken.get('expires')).diff(moment(), 'minutes').should.be.above(6); .send({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: 'ghost-admin',
client_secret: 'not_available'
})
.expect('Content-Type', /json/)
// TODO: make it possible to override oauth2orize's header so that this is consistent
.expect('Cache-Control', 'no-store')
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
request.post(localUtils.API.getApiQuery('authentication/token')) var jsonResponse = res.body;
should.exist(jsonResponse.access_token);
should.exist(jsonResponse.expires_in);
models.Accesstoken.findOne({
token: accesstoken
}).then(function (oldAccessToken) {
moment(oldAccessToken.get('expires')).diff(moment(), 'minutes').should.be.below(6);
return models.Refreshtoken.findOne({
token: refreshToken
});
}).then(function (refreshTokenModel) {
// NOTE: the static 6 month ms number in our constants are based on 30 days
// We have to compare against the static number. We can't compare against the month in
// the next 6 month dynamically, because each month has a different number of days,
// which results in a different ms number.
moment(Date.now() + constants.SIX_MONTH_MS)
.startOf('day')
.diff(moment(refreshTokenModel.get('expires')).startOf('day'), 'month').should.eql(0);
done();
});
});
});
});
});
it('can\'t request new access token with invalid refresh token', function (done) {
request.post(localUtils.API.getApiQuery('authentication/token'))
.set('Origin', config.get('url'))
.set('Accept', 'application/json')
.send({
grant_type: 'refresh_token',
refresh_token: 'invalid',
client_id: 'ghost-admin',
client_secret: 'not_available'
}).expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(403)
.end(function (err, res) {
if (err) {
return done(err);
}
var jsonResponse = res.body;
should.exist(jsonResponse.errors[0].errorType);
jsonResponse.errors[0].errorType.should.eql('NoPermissionError');
done();
});
});
it('reset password', function (done) {
models.User.getOwnerUser(testUtils.context.internal)
.then(function (ownerUser) {
var token = security.tokens.resetToken.generateHash({
expires: Date.now() + (1000 * 60),
email: user.email,
dbHash: settingsCache.get('db_hash'),
password: ownerUser.get('password')
});
request.put(localUtils.API.getApiQuery('authentication/passwordreset'))
.set('Origin', config.get('url')) .set('Origin', config.get('url'))
.set('Authorization', 'Bearer ' + accesstoken) .set('Accept', 'application/json')
.send({ .send({
grant_type: 'refresh_token', passwordreset: [{
refresh_token: refreshToken, token: token,
client_id: 'ghost-admin', newPassword: 'thisissupersafe',
client_secret: 'not_available' ne2Password: 'thisissupersafe'
}]
}) })
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
// TODO: make it possible to override oauth2orize's header so that this is consistent .expect('Cache-Control', testUtils.cacheRules.private)
.expect('Cache-Control', 'no-store')
.expect(200) .expect(200)
.end(function (err, res) { .end(function (err) {
if (err) { if (err) {
return done(err); return done(err);
} }
var jsonResponse = res.body; done();
should.exist(jsonResponse.access_token);
should.exist(jsonResponse.expires_in);
models.Accesstoken.findOne({
token: accesstoken
}).then(function (oldAccessToken) {
moment(oldAccessToken.get('expires')).diff(moment(), 'minutes').should.be.below(6);
return models.Refreshtoken.findOne({
token: refreshToken
});
}).then(function (refreshTokenModel) {
// NOTE: the static 6 month ms number in our constants are based on 30 days
// We have to compare against the static number. We can't compare against the month in
// the next 6 month dynamically, because each month has a different number of days,
// which results in a different ms number.
moment(Date.now() + constants.SIX_MONTH_MS)
.startOf('day')
.diff(moment(refreshTokenModel.get('expires')).startOf('day'), 'month').should.eql(0);
done();
});
}); });
})
.catch(done);
});
it('reset password: invalid token', function () {
return request
.put(localUtils.API.getApiQuery('authentication/passwordreset'))
.set('Origin', config.get('url'))
.set('Accept', 'application/json')
.send({
passwordreset: [{
token: 'invalid',
newPassword: 'thisissupersafe',
ne2Password: 'thisissupersafe'
}]
})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(401);
});
it('revoke token', function () {
return request
.post(localUtils.API.getApiQuery('authentication/revoke'))
.set('Authorization', 'Bearer ' + accesstoken)
.set('Origin', config.get('url'))
.set('Accept', 'application/json')
.send({
token: accesstoken,
token_type_hint: 'access_token'
})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.then(() => {
return request
.get(localUtils.API.getApiQuery('posts/'))
.set('Authorization', 'Bearer ' + accesstoken)
.expect(401);
}); });
}); });
}); });
it('can\'t request new access token with invalid refresh token', function (done) { describe('Blog setup', function () {
request.post(localUtils.API.getApiQuery('authentication/token')) before(function () {
.set('Origin', config.get('url')) return ghost({forceStart: true})
.set('Accept', 'application/json') .then(function (_ghostServer) {
.send({ ghostServer = _ghostServer;
grant_type: 'refresh_token', request = supertest.agent(config.get('url'));
refresh_token: 'invalid', });
client_id: 'ghost-admin', });
client_secret: 'not_available'
}).expect('Content-Type', /json/) it('is setup? no', function () {
.expect('Cache-Control', testUtils.cacheRules.private) return request
.expect(403) .get(localUtils.API.getApiQuery('authentication/setup'))
.end(function (err, res) { .set('Origin', config.get('url'))
if (err) { .expect('Content-Type', /json/)
return done(err); .expect(200)
} .then((res) => {
var jsonResponse = res.body; res.body.setup[0].status.should.be.false();
should.exist(jsonResponse.errors[0].errorType); });
jsonResponse.errors[0].errorType.should.eql('NoPermissionError'); });
done();
}); it('complete setup', function () {
return request
.post(localUtils.API.getApiQuery('authentication/setup'))
.set('Origin', config.get('url'))
.send({
setup: [{
name: 'test user',
email: 'test@example.com',
password: 'thisissupersafe',
blogTitle: 'a test blog'
}]
})
.expect('Content-Type', /json/)
.expect(201)
.then((res) => {
const jsonResponse = res.body;
should.exist(jsonResponse.users);
should.not.exist(jsonResponse.meta);
jsonResponse.users.should.have.length(1);
testUtils.API.checkResponse(jsonResponse.users[0], 'user');
const newUser = jsonResponse.users[0];
newUser.id.should.equal(testUtils.DataGenerator.Content.users[0].id);
newUser.name.should.equal('test user');
newUser.email.should.equal('test@example.com');
});
});
it('is setup? yes', function () {
return request
.get(localUtils.API.getApiQuery('authentication/setup'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect(200)
.then((res) => {
res.body.setup[0].status.should.be.true();
});
});
it('complete setup again', function () {
return request
.post(localUtils.API.getApiQuery('authentication/setup'))
.set('Origin', config.get('url'))
.send({
setup: [{
name: 'test user',
email: 'test-leo@example.com',
password: 'thisissupersafe',
blogTitle: 'a test blog'
}]
})
.expect('Content-Type', /json/)
.expect(403);
});
}); });
it('reset password', function (done) { describe('Invitation', function () {
models.User.getOwnerUser(testUtils.context.internal) before(function () {
.then(function (ownerUser) { return ghost()
var token = security.tokens.resetToken.generateHash({ .then(function (_ghostServer) {
expires: Date.now() + (1000 * 60), ghostServer = _ghostServer;
email: user.email, request = supertest.agent(config.get('url'));
dbHash: settingsCache.get('db_hash'),
password: ownerUser.get('password') // simulates blog setup (initialises the owner)
return localUtils.doAuth(request, 'invites');
}); });
});
request.put(localUtils.API.getApiQuery('authentication/passwordreset')) it('try to accept without invite', function () {
.set('Origin', config.get('url')) return request
.set('Accept', 'application/json') .post(localUtils.API.getApiQuery('authentication/invitation'))
.send({ .set('Origin', config.get('url'))
passwordreset: [{ .send({
token: token, invitation: [{
newPassword: 'thisissupersafe', token: 'lul11111',
ne2Password: 'thisissupersafe' password: 'lel123456',
}] email: 'not-invited@example.org',
}) name: 'not invited'
.expect('Content-Type', /json/) }]
.expect('Cache-Control', testUtils.cacheRules.private) })
.expect(200) .expect('Content-Type', /json/)
.end(function (err) { .expect(404);
if (err) { });
return done(err);
}
done(); it('try to accept with invite', function () {
}); return request
}) .post(localUtils.API.getApiQuery('authentication/invitation'))
.catch(done); .set('Origin', config.get('url'))
.send({
invitation: [{
token: testUtils.DataGenerator.forKnex.invites[0].token,
password: '12345678910',
email: testUtils.DataGenerator.forKnex.invites[0].email,
name: 'invited'
}]
})
.expect('Content-Type', /json/)
.expect(200);
});
}); });
}); });

View File

@ -24,7 +24,7 @@ describe('Configuration API', function () {
}); });
describe('success', function () { describe('success', function () {
it('can retrieve public configuration', function (done) { it('can retrieve public configuration and all expected properties', function (done) {
request.get(localUtils.API.getApiQuery('configuration/')) request.get(localUtils.API.getApiQuery('configuration/'))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private) .expect('Cache-Control', testUtils.cacheRules.private)
@ -35,6 +35,48 @@ describe('Configuration API', function () {
} }
should.exist(res.body.configuration); should.exist(res.body.configuration);
res.body.configuration.should.be.an.Array().with.lengthOf(1);
const props = res.body.configuration[0];
props.blogUrl.should.eql('http://127.0.0.1:2369/');
props.useGravatar.should.eql(false);
props.publicAPI.should.eql(true);
props.clientId.should.eql('ghost-admin');
props.clientSecret.should.eql('not_available');
// value not available, because settings API was not called yet
props.hasOwnProperty('blogTitle').should.eql(true);
done();
});
});
it('can read about config and get all expected properties', function (done) {
request.get(localUtils.API.getApiQuery('configuration/about/'))
.set('Authorization', 'Bearer ' + accesstoken)
.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.configuration);
res.body.configuration.should.be.an.Array().with.lengthOf(1);
const props = res.body.configuration[0];
// Check the structure
props.should.have.property('version').which.is.a.String();
props.should.have.property('environment').which.is.a.String();
props.should.have.property('database').which.is.a.String();
props.should.have.property('mail').which.is.a.String();
// Check a few values
props.environment.should.match(/^testing/);
props.version.should.eql(require('../../../../../package.json').version);
done(); done();
}); });
}); });

View File

@ -1,18 +1,20 @@
var should = require('should'), const should = require('should');
supertest = require('supertest'), const supertest = require('supertest');
Promise = require('bluebird'), const Promise = require('bluebird');
testUtils = require('../../../utils'), const testUtils = require('../../../utils');
localUtils = require('./utils'), const path = require('path');
path = require('path'), const sinon = require('sinon');
sinon = require('sinon'), const config = require('../../../../../core/server/config');
config = require('../../../../../core/server/config'), const models = require('../../../../../core/server/models');
models = require('../../../../../core/server/models'), const fs = require('fs-extra');
fs = require('fs-extra'), const _ = require('lodash');
_ = require('lodash'), const common = require('../../../../server/lib/common');
ghost = testUtils.startGhost, const localUtils = require('./utils');
request,
sandbox = sinon.sandbox.create(); let ghost = testUtils.startGhost;
let request;
let sandbox = sinon.sandbox.create();
let eventsTriggered;
describe('DB API', function () { describe('DB API', function () {
var accesstoken = '', ghostServer, clients, backupClient, schedulerClient, backupQuery, schedulerQuery, fsStub; var accesstoken = '', ghostServer, clients, backupClient, schedulerClient, backupQuery, schedulerQuery, fsStub;
@ -37,6 +39,18 @@ describe('DB API', function () {
}); });
}); });
beforeEach(function () {
eventsTriggered = {};
sandbox.stub(common.events, 'emit').callsFake(function (eventName, eventObj) {
if (!eventsTriggered[eventName]) {
eventsTriggered[eventName] = [];
}
eventsTriggered[eventName].push(eventObj);
});
});
afterEach(function () { afterEach(function () {
sandbox.restore(); sandbox.restore();
}); });
@ -182,4 +196,46 @@ describe('DB API', function () {
done(); done();
}); });
}); });
it('delete all content (owner)', function (done) {
request.get(localUtils.API.getApiQuery('posts/'))
.set('Authorization', 'Bearer ' + accesstoken)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
let jsonResponse = res.body;
let results = jsonResponse.posts;
jsonResponse.posts.should.have.length(7);
_.filter(results, {page: false, status: 'published'}).length.should.equal(7);
request.delete(localUtils.API.getApiQuery('db/'))
.set('Authorization', 'Bearer ' + accesstoken)
.set('Accept', 'application/json')
.expect(204)
.end(function (err, res) {
if (err) {
return done(err);
}
request.get(localUtils.API.getApiQuery('posts/'))
.set('Authorization', 'Bearer ' + accesstoken)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
res.body.posts.should.have.length(0);
eventsTriggered['post.unpublished'].length.should.eql(7);
eventsTriggered['post.deleted'].length.should.eql(7);
eventsTriggered['tag.deleted'].length.should.eql(1);
done();
});
});
});
});
}); });

View File

@ -0,0 +1,166 @@
const should = require('should');
const supertest = require('supertest');
const sinon = require('sinon');
const testUtils = require('../../../utils');
const localUtils = require('./utils');
const config = require('../../../../../core/server/config');
const mailService = require('../../../../../core/server/services/mail');
const ghost = testUtils.startGhost;
const sandbox = sinon.sandbox.create();
let request;
describe('Invites API', function () {
var accesstoken = '', ghostServer;
before(function () {
return ghost()
.then(function (_ghostServer) {
ghostServer = _ghostServer;
request = supertest.agent(config.get('url'));
})
.then(function () {
return localUtils.doAuth(request, 'invites');
})
.then(function (token) {
accesstoken = token;
});
});
beforeEach(function () {
sandbox.stub(mailService.GhostMailer.prototype, 'send').resolves('Mail is disabled');
});
afterEach(function () {
sandbox.restore();
});
describe('browse', function () {
it('default', function (done) {
request.get(localUtils.API.getApiQuery('invites/'))
.set('Authorization', 'Bearer ' + accesstoken)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
should.not.exist(res.headers['x-cache-invalidate']);
const jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.invites);
jsonResponse.invites.should.have.length(2);
testUtils.API.checkResponse(jsonResponse, 'invites');
testUtils.API.checkResponse(jsonResponse.invites[0], 'invite');
jsonResponse.invites[0].status.should.eql('sent');
jsonResponse.invites[0].email.should.eql('test1@ghost.org');
jsonResponse.invites[0].role_id.should.eql(testUtils.roles.ids.admin);
jsonResponse.invites[1].status.should.eql('sent');
jsonResponse.invites[1].email.should.eql('test2@ghost.org');
jsonResponse.invites[1].role_id.should.eql(testUtils.roles.ids.author);
mailService.GhostMailer.prototype.send.called.should.be.false();
done();
});
});
});
describe('read', function () {
it('default', function (done) {
request.get(localUtils.API.getApiQuery(`invites/${testUtils.DataGenerator.forKnex.invites[0].id}/`))
.set('Authorization', 'Bearer ' + accesstoken)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
should.not.exist(res.headers['x-cache-invalidate']);
const jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.invites);
jsonResponse.invites.should.have.length(1);
testUtils.API.checkResponse(jsonResponse.invites[0], 'invite');
mailService.GhostMailer.prototype.send.called.should.be.false();
done();
});
});
});
describe('add', function () {
it('default', function (done) {
request.post(localUtils.API.getApiQuery('invites/'))
.set('Authorization', 'Bearer ' + accesstoken)
.send({
invites: [{email: 'test@example.com', role_id: testUtils.existingData.roles[1].id}]
})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
should.not.exist(res.headers['x-cache-invalidate']);
const jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.invites);
jsonResponse.invites.should.have.length(1);
testUtils.API.checkResponse(jsonResponse.invites[0], 'invite');
jsonResponse.invites[0].role_id.should.eql(testUtils.existingData.roles[1].id);
mailService.GhostMailer.prototype.send.called.should.be.true();
done();
});
});
it('user exists', function (done) {
request.post(localUtils.API.getApiQuery('invites/'))
.set('Authorization', 'Bearer ' + accesstoken)
.send({
invites: [{email: 'ghost-author@example.com', role_id: testUtils.existingData.roles[1].id}]
})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(422)
.end(function (err, res) {
if (err) {
return done(err);
}
mailService.GhostMailer.prototype.send.called.should.be.false();
done();
});
});
});
describe('destroy', function () {
it('default', function (done) {
request.del(localUtils.API.getApiQuery(`invites/${testUtils.DataGenerator.forKnex.invites[0].id}/`))
.set('Authorization', 'Bearer ' + accesstoken)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(204)
.end(function (err) {
if (err) {
return done(err);
}
mailService.GhostMailer.prototype.send.called.should.be.false();
done();
});
});
});
});

View File

@ -24,13 +24,14 @@ describe('Notifications API', function () {
}); });
describe('Add', function () { describe('Add', function () {
var newNotification = { it('creates a new notification and sets default fields', function (done) {
type: 'info', const newNotification = {
message: 'test notification', type: 'info',
custom: true message: 'test notification',
}; custom: true,
id: 'customId'
};
it('creates a new notification', function (done) {
request.post(localUtils.API.getApiQuery('notifications/')) request.post(localUtils.API.getApiQuery('notifications/'))
.set('Authorization', 'Bearer ' + accesstoken) .set('Authorization', 'Bearer ' + accesstoken)
.send({notifications: [newNotification]}) .send({notifications: [newNotification]})
@ -51,6 +52,10 @@ describe('Notifications API', function () {
jsonResponse.notifications[0].type.should.equal(newNotification.type); jsonResponse.notifications[0].type.should.equal(newNotification.type);
jsonResponse.notifications[0].message.should.equal(newNotification.message); jsonResponse.notifications[0].message.should.equal(newNotification.message);
jsonResponse.notifications[0].status.should.equal('alert'); jsonResponse.notifications[0].status.should.equal('alert');
jsonResponse.notifications[0].dismissible.should.be.true();
should.exist(jsonResponse.notifications[0].location);
jsonResponse.notifications[0].location.should.equal('bottom');
jsonResponse.notifications[0].id.should.be.a.String();
done(); done();
}); });
@ -99,6 +104,57 @@ describe('Notifications API', function () {
}); });
}); });
}); });
it('should have correct order', function () {
const firstNotification = {
status: 'alert',
type: 'info',
custom: true,
id: 'firstId',
dismissible: true,
message: '1'
};
const secondNotification = {
status: 'alert',
type: 'info',
custom: true,
id: 'secondId',
dismissible: true,
message: '2'
};
return request.post(localUtils.API.getApiQuery('notifications/'))
.set('Authorization', 'Bearer ' + accesstoken)
.send({notifications: [firstNotification]})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(201)
.then(() => {
return request.post(localUtils.API.getApiQuery('notifications/'))
.set('Authorization', 'Bearer ' + accesstoken)
.send({notifications: [secondNotification]})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(201);
})
.then(() => {
return request.get(localUtils.API.getApiQuery('notifications/'))
.set('Authorization', 'Bearer ' + accesstoken)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.then(res => {
const jsonResponse = res.body;
jsonResponse.notifications.should.be.an.Array().with.lengthOf(4);
jsonResponse.notifications[0].id.should.equal(secondNotification.id);
jsonResponse.notifications[1].id.should.equal(firstNotification.id);
jsonResponse.notifications[2].id.should.equal('customId-2');
jsonResponse.notifications[3].id.should.equal('customId');
});
});
});
}); });
describe('Delete', function () { describe('Delete', function () {
@ -147,5 +203,16 @@ describe('Notifications API', function () {
}); });
}); });
}); });
it('returns 404 when removing notification with unknown id', function () {
return request.del(localUtils.API.getApiQuery('notifications/unknown'))
.set('Authorization', 'Bearer ' + accesstoken)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.then(res => {
res.body.errors[0].message.should.equal('Notification does not exist.');
});
});
}); });
}); });

View File

@ -52,6 +52,13 @@ describe('Post API', function () {
_.isBoolean(jsonResponse.posts[0].featured).should.eql(true); _.isBoolean(jsonResponse.posts[0].featured).should.eql(true);
_.isBoolean(jsonResponse.posts[0].page).should.eql(true); _.isBoolean(jsonResponse.posts[0].page).should.eql(true);
// Ensure default order
jsonResponse.posts[0].slug.should.eql('welcome');
jsonResponse.posts[10].slug.should.eql('html-ipsum');
// Relative by default
jsonResponse.posts[0].url.should.eql('/welcome/');
done(); done();
}); });
}); });
@ -82,7 +89,7 @@ describe('Post API', function () {
}); });
it('can retrieve multiple post formats', function (done) { it('can retrieve multiple post formats', function (done) {
request.get(localUtils.API.getApiQuery('posts/?formats=plaintext,mobiledoc')) request.get(localUtils.API.getApiQuery('posts/?formats=plaintext,mobiledoc&limit=3&order=title%20ASC'))
.set('Authorization', 'Bearer ' + ownerAccessToken) .set('Authorization', 'Bearer ' + ownerAccessToken)
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private) .expect('Cache-Control', testUtils.cacheRules.private)
@ -96,12 +103,15 @@ describe('Post API', function () {
var jsonResponse = res.body; var jsonResponse = res.body;
should.exist(jsonResponse.posts); should.exist(jsonResponse.posts);
testUtils.API.checkResponse(jsonResponse, 'posts'); testUtils.API.checkResponse(jsonResponse, 'posts');
jsonResponse.posts.should.have.length(11); jsonResponse.posts.should.have.length(3);
testUtils.API.checkResponse(jsonResponse.posts[0], 'post', ['mobiledoc', 'plaintext'], ['html']); testUtils.API.checkResponse(jsonResponse.posts[0], 'post', ['mobiledoc', 'plaintext'], ['html']);
testUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination'); testUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
_.isBoolean(jsonResponse.posts[0].featured).should.eql(true); _.isBoolean(jsonResponse.posts[0].featured).should.eql(true);
_.isBoolean(jsonResponse.posts[0].page).should.eql(true); _.isBoolean(jsonResponse.posts[0].page).should.eql(true);
// ensure order works
jsonResponse.posts[0].slug.should.eql('apps-integrations');
done(); done();
}); });
}); });
@ -218,7 +228,7 @@ describe('Post API', function () {
}); });
it('can retrieve all published posts and pages', function (done) { it('can retrieve all published posts and pages', function (done) {
request.get(localUtils.API.getApiQuery('posts/?staticPages=all')) request.get(localUtils.API.getApiQuery('posts/?filter=page:[false,true]'))
.set('Authorization', 'Bearer ' + ownerAccessToken) .set('Authorization', 'Bearer ' + ownerAccessToken)
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private) .expect('Cache-Control', testUtils.cacheRules.private)
@ -240,7 +250,6 @@ describe('Post API', function () {
}); });
// Test bits of the API we don't use in the app yet to ensure the API behaves properly // Test bits of the API we don't use in the app yet to ensure the API behaves properly
it('can retrieve all status posts and pages', function (done) { it('can retrieve all status posts and pages', function (done) {
request.get(localUtils.API.getApiQuery('posts/?staticPages=all&status=all')) request.get(localUtils.API.getApiQuery('posts/?staticPages=all&status=all'))
.set('Authorization', 'Bearer ' + ownerAccessToken) .set('Authorization', 'Bearer ' + ownerAccessToken)
@ -300,9 +309,39 @@ describe('Post API', function () {
var jsonResponse = res.body; var jsonResponse = res.body;
should.exist(jsonResponse.posts); should.exist(jsonResponse.posts);
testUtils.API.checkResponse(jsonResponse, 'posts'); testUtils.API.checkResponse(jsonResponse, 'posts');
jsonResponse.posts.should.have.length(2); jsonResponse.posts.length.should.eql(2);
testUtils.API.checkResponse(jsonResponse.posts[0], 'post'); testUtils.API.checkResponse(jsonResponse.posts[0], 'post');
testUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination'); testUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
const featured = _.map(jsonResponse.posts, 'featured');
featured.should.matchEach(true);
done();
});
});
it('can retrieve just non featured posts', function (done) {
request.get(localUtils.API.getApiQuery('posts/?filter=featured:false'))
.set('Authorization', 'Bearer ' + ownerAccessToken)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
should.not.exist(res.headers['x-cache-invalidate']);
var jsonResponse = res.body;
should.exist(jsonResponse.posts);
testUtils.API.checkResponse(jsonResponse, 'posts');
jsonResponse.posts.should.be.an.Array().with.lengthOf(9);
testUtils.API.checkResponse(jsonResponse.posts[0], 'post');
testUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
const featured = _.map(jsonResponse.posts, 'featured');
featured.should.matchEach(false);
done(); done();
}); });
}); });
@ -325,6 +364,11 @@ describe('Post API', function () {
jsonResponse.posts.should.have.length(1); jsonResponse.posts.should.have.length(1);
testUtils.API.checkResponse(jsonResponse.posts[0], 'post'); testUtils.API.checkResponse(jsonResponse.posts[0], 'post');
testUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination'); testUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
_.each(jsonResponse.posts, (post) => {
post.status.should.eql('draft');
});
done(); done();
}); });
}); });

View File

@ -8,6 +8,7 @@ var should = require('should'),
localUtils = require('./utils'), localUtils = require('./utils'),
configUtils = require('../../../utils/configUtils'), configUtils = require('../../../utils/configUtils'),
config = require('../../../../../core/server/config'), config = require('../../../../../core/server/config'),
models = require('../../../../../core/server/models'),
ghost = testUtils.startGhost, ghost = testUtils.startGhost,
request; request;
@ -21,7 +22,7 @@ describe('Public API', function () {
request = supertest.agent(config.get('url')); request = supertest.agent(config.get('url'));
}) })
.then(function () { .then(function () {
return localUtils.doAuth(request, 'users:no-owner', 'posts', 'tags:extra', 'client:trusted-domain'); return localUtils.doAuth(request, 'users:no-owner', 'user:inactive', 'posts', 'tags:extra', 'client:trusted-domain');
}); });
}); });
@ -52,6 +53,26 @@ describe('Public API', function () {
testUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination'); testUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
_.isBoolean(jsonResponse.posts[0].featured).should.eql(true); _.isBoolean(jsonResponse.posts[0].featured).should.eql(true);
_.isBoolean(jsonResponse.posts[0].page).should.eql(true); _.isBoolean(jsonResponse.posts[0].page).should.eql(true);
// Public API does not return drafts
_.map(jsonResponse.posts, (post) => {
post.status.should.eql('published');
});
// Default order check
jsonResponse.posts[0].slug.should.eql('welcome');
jsonResponse.posts[10].slug.should.eql('html-ipsum');
// check meta response for this test
jsonResponse.meta.pagination.page.should.eql(1);
jsonResponse.meta.pagination.limit.should.eql(15);
jsonResponse.meta.pagination.pages.should.eql(1);
jsonResponse.meta.pagination.total.should.eql(11);
jsonResponse.meta.pagination.hasOwnProperty('next').should.be.true();
jsonResponse.meta.pagination.hasOwnProperty('prev').should.be.true();
should.not.exist(jsonResponse.meta.pagination.next);
should.not.exist(jsonResponse.meta.pagination.prev);
done(); done();
}); });
}); });
@ -79,6 +100,166 @@ describe('Public API', function () {
}); });
}); });
it('browse posts with basic filters', function (done) {
request.get(localUtils.API.getApiQuery('posts/?client_id=ghost-admin&client_secret=not_available&filter=tag:kitchen-sink,featured:true&include=tags'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
const jsonResponse = res.body;
const ids = _.map(jsonResponse.posts, 'id');
should.not.exist(res.headers['x-cache-invalidate']);
should.exist(jsonResponse.posts);
testUtils.API.checkResponse(jsonResponse, 'posts');
testUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
// should content filtered data and order
jsonResponse.posts.should.have.length(4);
ids.should.eql([
testUtils.DataGenerator.Content.posts[4].id,
testUtils.DataGenerator.Content.posts[2].id,
testUtils.DataGenerator.Content.posts[1].id,
testUtils.DataGenerator.Content.posts[0].id,
]);
// API does not return drafts
jsonResponse.posts.forEach((post) => {
post.page.should.be.false();
post.status.should.eql('published');
});
// Each post must either be featured or have the tag 'kitchen-sink'
_.each(jsonResponse.posts, (post) => {
if (post.featured) {
post.featured.should.equal(true);
} else {
const tags = _.map(post.tags, 'slug');
tags.should.containEql('kitchen-sink');
}
});
// The meta object should contain the right detail
jsonResponse.meta.should.have.property('pagination');
jsonResponse.meta.pagination.should.be.an.Object().with.properties(['page', 'limit', 'pages', 'total', 'next', 'prev']);
jsonResponse.meta.pagination.page.should.eql(1);
jsonResponse.meta.pagination.limit.should.eql(15);
jsonResponse.meta.pagination.pages.should.eql(1);
jsonResponse.meta.pagination.total.should.eql(4);
should.equal(jsonResponse.meta.pagination.next, null);
should.equal(jsonResponse.meta.pagination.prev, null);
done();
});
});
it('browse posts with author filter', function (done) {
request.get(localUtils.API.getApiQuery('posts/?client_id=ghost-admin&client_secret=not_available&filter=authors:[joe-bloggs,pat,ghost]&include=authors'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
const jsonResponse = res.body;
const ids = _.map(jsonResponse.posts, 'id');
should.not.exist(res.headers['x-cache-invalidate']);
should.exist(jsonResponse.posts);
testUtils.API.checkResponse(jsonResponse, 'posts');
testUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
// 2. The data part of the response should be correct
// We should have 2 matching items
jsonResponse.posts.should.be.an.Array().with.lengthOf(11);
// Each post must either have the author 'joe-bloggs' or 'ghost', 'pat' is non existing author
const authors = _.map(jsonResponse.posts, function (post) {
return post.primary_author.slug;
});
authors.should.matchAny(/joe-bloggs|ghost'/);
done();
});
});
it('browse posts with page filter', function (done) {
request.get(localUtils.API.getApiQuery('posts/?client_id=ghost-admin&client_secret=not_available&filter=page:true'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
const jsonResponse = res.body;
should.not.exist(res.headers['x-cache-invalidate']);
should.exist(jsonResponse.posts);
testUtils.API.checkResponse(jsonResponse, 'posts');
testUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
jsonResponse.posts.should.be.an.Array().with.lengthOf(1);
const page = _.map(jsonResponse.posts, 'page');
page.should.matchEach(true);
jsonResponse.posts[0].id.should.equal(testUtils.DataGenerator.Content.posts[5].id);
done();
});
});
it('browse posts with published and draft status, should not return drafts', function (done) {
request.get(localUtils.API.getApiQuery('posts/?client_id=ghost-admin&client_secret=not_available&filter=status:published,status:draft'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
const jsonResponse = res.body;
jsonResponse.posts.should.be.an.Array().with.lengthOf(11);
jsonResponse.posts.forEach((post) => {
post.page.should.be.false();
post.status.should.eql('published');
});
done();
});
});
it('[deprecated] browse posts with page non matching filter', function (done) {
request.get(localUtils.API.getApiQuery('posts/?client_id=ghost-admin&client_secret=not_available&filter=tag:no-posts'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
const jsonResponse = res.body;
jsonResponse.posts.should.be.an.Array().with.lengthOf(0);
jsonResponse.meta.should.have.property('pagination');
jsonResponse.meta.pagination.should.be.an.Object().with.properties(['page', 'limit', 'pages', 'total', 'next', 'prev']);
jsonResponse.meta.pagination.page.should.eql(1);
jsonResponse.meta.pagination.limit.should.eql(15);
jsonResponse.meta.pagination.pages.should.eql(1);
jsonResponse.meta.pagination.total.should.eql(0);
should.equal(jsonResponse.meta.pagination.next, null);
should.equal(jsonResponse.meta.pagination.prev, null);
done();
});
});
it('browse posts: request absolute urls', function (done) { it('browse posts: request absolute urls', function (done) {
request.get(localUtils.API.getApiQuery('posts/?client_id=ghost-admin&client_secret=not_available&absolute_urls=true')) request.get(localUtils.API.getApiQuery('posts/?client_id=ghost-admin&client_secret=not_available&absolute_urls=true'))
.set('Origin', testUtils.API.getURL()) .set('Origin', testUtils.API.getURL())
@ -341,6 +522,34 @@ describe('Public API', function () {
}); });
}); });
it('browse tags - limit=all should fetch all tags and include count.posts', function (done) {
request.get(localUtils.API.getApiQuery('tags/?limit=all&client_id=ghost-admin&client_secret=not_available&include=count.posts'))
.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);
}
const jsonResponse = res.body;
should.exist(jsonResponse.tags);
jsonResponse.tags.should.be.an.Array().with.lengthOf(56);
// Each tag should have the correct count
_.find(jsonResponse.tags, {name: 'Getting Started'}).count.posts.should.eql(7);
_.find(jsonResponse.tags, {name: 'kitchen sink'}).count.posts.should.eql(2);
_.find(jsonResponse.tags, {name: 'bacon'}).count.posts.should.eql(2);
_.find(jsonResponse.tags, {name: 'chorizo'}).count.posts.should.eql(1);
_.find(jsonResponse.tags, {name: 'pollo'}).count.posts.should.eql(0);
_.find(jsonResponse.tags, {name: 'injection'}).count.posts.should.eql(0);
done();
});
});
it('denies access with invalid client_secret', function (done) { it('denies access with invalid client_secret', function (done) {
request.get(localUtils.API.getApiQuery('posts/?client_id=ghost-admin&client_secret=invalid_secret')) request.get(localUtils.API.getApiQuery('posts/?client_id=ghost-admin&client_secret=invalid_secret'))
.set('Origin', testUtils.API.getURL()) .set('Origin', testUtils.API.getURL())
@ -462,11 +671,17 @@ describe('Public API', function () {
var jsonResponse = res.body; var jsonResponse = res.body;
should.exist(jsonResponse.users); should.exist(jsonResponse.users);
testUtils.API.checkResponse(jsonResponse, 'users'); testUtils.API.checkResponse(jsonResponse, 'users');
jsonResponse.users.should.have.length(6); jsonResponse.users.should.have.length(7);
// We don't expose the email address. // We don't expose the email address, status and other attrs.
testUtils.API.checkResponse(jsonResponse.users[0], 'user', null, null, null, {public: true}); testUtils.API.checkResponse(jsonResponse.users[0], 'user', null, null, null, {public: true});
done();
// Public api returns all users, but no status! Locked/Inactive users can still have written articles.
models.User.findPage(Object.assign({status: 'all'}, testUtils.context.internal))
.then((response) => {
_.map(response.data, (model) => model.toJSON()).length.should.eql(7);
done();
});
}); });
}); });
@ -485,7 +700,7 @@ describe('Public API', function () {
var jsonResponse = res.body; var jsonResponse = res.body;
should.exist(jsonResponse.users); should.exist(jsonResponse.users);
testUtils.API.checkResponse(jsonResponse, 'users'); testUtils.API.checkResponse(jsonResponse, 'users');
jsonResponse.users.should.have.length(6); jsonResponse.users.should.have.length(7);
// We don't expose the email address. // We don't expose the email address.
testUtils.API.checkResponse(jsonResponse.users[0], 'user', null, null, null, {public: true}); testUtils.API.checkResponse(jsonResponse.users[0], 'user', null, null, null, {public: true});
@ -581,6 +796,51 @@ describe('Public API', function () {
}); });
}); });
it('browse user with count.posts', function (done) {
request.get(localUtils.API.getApiQuery('users/?client_id=ghost-admin&client_secret=not_available&include=count.posts&order=count.posts ASC'))
.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);
}
var jsonResponse = res.body;
should.exist(jsonResponse.users);
jsonResponse.users.should.have.length(7);
// We don't expose the email address.
testUtils.API.checkResponse(jsonResponse.users[0], 'user', ['count'], null, null, {public: true});
// Each user should have the correct count
_.find(jsonResponse.users, {slug:'joe-bloggs'}).count.posts.should.eql(4);
_.find(jsonResponse.users, {slug:'contributor'}).count.posts.should.eql(0);
_.find(jsonResponse.users, {slug:'slimer-mcectoplasm'}).count.posts.should.eql(0);
_.find(jsonResponse.users, {slug:'jimothy-bogendath'}).count.posts.should.eql(0);
_.find(jsonResponse.users, {slug: 'smith-wellingsworth'}).count.posts.should.eql(0);
_.find(jsonResponse.users, {slug:'ghost'}).count.posts.should.eql(7);
_.find(jsonResponse.users, {slug:'inactive'}).count.posts.should.eql(0);
const ids = jsonResponse.users
.filter(user => (user.slug !== 'ghost'))
.filter(user => (user.slug !== 'inactive'))
.map(user=> user.id);
ids.should.eql([
testUtils.DataGenerator.Content.users[1].id,
testUtils.DataGenerator.Content.users[2].id,
testUtils.DataGenerator.Content.users[3].id,
testUtils.DataGenerator.Content.users[7].id,
testUtils.DataGenerator.Content.users[0].id
]);
done();
});
});
it('[unsupported] browse user by email', function (done) { it('[unsupported] browse user by email', function (done) {
request request
.get(localUtils.API.getApiQuery('users/email/ghost-author@ghost.org/?client_id=ghost-admin&client_secret=not_available')) .get(localUtils.API.getApiQuery('users/email/ghost-author@ghost.org/?client_id=ghost-admin&client_secret=not_available'))
@ -614,7 +874,6 @@ describe('Public API', function () {
should.exist(jsonResponse.users); should.exist(jsonResponse.users);
jsonResponse.users.should.have.length(1); jsonResponse.users.should.have.length(1);
// We don't expose the email address.
testUtils.API.checkResponse(jsonResponse.users[0], 'user', null, null, null, {public: true}); testUtils.API.checkResponse(jsonResponse.users[0], 'user', null, null, null, {public: true});
done(); done();
}); });
@ -635,7 +894,7 @@ describe('Public API', function () {
var jsonResponse = res.body; var jsonResponse = res.body;
should.exist(jsonResponse.users); should.exist(jsonResponse.users);
testUtils.API.checkResponse(jsonResponse, 'users'); testUtils.API.checkResponse(jsonResponse, 'users');
jsonResponse.users.should.have.length(6); jsonResponse.users.should.have.length(7);
// We don't expose the email address. // We don't expose the email address.
testUtils.API.checkResponse(jsonResponse.users[0], 'user', ['count'], null, null, {public: true}); testUtils.API.checkResponse(jsonResponse.users[0], 'user', ['count'], null, null, {public: true});
@ -658,7 +917,7 @@ describe('Public API', function () {
var jsonResponse = res.body; var jsonResponse = res.body;
should.exist(jsonResponse.users); should.exist(jsonResponse.users);
testUtils.API.checkResponse(jsonResponse, 'users'); testUtils.API.checkResponse(jsonResponse, 'users');
jsonResponse.users.should.have.length(6); jsonResponse.users.should.have.length(7);
// We don't expose the email address. // We don't expose the email address.
testUtils.API.checkResponse(jsonResponse.users[0], 'user', null, null, null, {public: true}); testUtils.API.checkResponse(jsonResponse.users[0], 'user', null, null, null, {public: true});

View File

@ -0,0 +1,84 @@
const should = require('should');
const supertest = require('supertest');
const testUtils = require('../../../utils');
const localUtils = require('./utils');
const config = require('../../../../../core/server/config');
const ghost = testUtils.startGhost;
let request;
describe('Roles API', function () {
var accesstoken = '', ghostServer;
before(function () {
return ghost()
.then(function (_ghostServer) {
ghostServer = _ghostServer;
request = supertest.agent(config.get('url'));
})
.then(function () {
return localUtils.doAuth(request, 'posts');
})
.then(function (token) {
accesstoken = token;
});
});
describe('browse', function () {
it('default', function (done) {
request.get(localUtils.API.getApiQuery('roles/'))
.set('Authorization', 'Bearer ' + accesstoken)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
should.not.exist(res.headers['x-cache-invalidate']);
const response = res.body;
should.exist(response);
should.exist(response.roles);
testUtils.API.checkResponse(response, 'roles');
response.roles.should.have.length(6);
testUtils.API.checkResponse(response.roles[0], 'role');
testUtils.API.checkResponse(response.roles[1], 'role');
testUtils.API.checkResponse(response.roles[2], 'role');
testUtils.API.checkResponse(response.roles[3], 'role');
testUtils.API.checkResponse(response.roles[4], 'role');
testUtils.API.checkResponse(response.roles[5], 'role');
done();
});
});
it('permissions=assign', function (done) {
request.get(localUtils.API.getApiQuery('roles/?permissions=assign'))
.set('Authorization', 'Bearer ' + accesstoken)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
should.not.exist(res.headers['x-cache-invalidate']);
const response = res.body;
should.exist(response.roles);
testUtils.API.checkResponse(response, 'roles');
response.roles.should.have.length(4);
testUtils.API.checkResponse(response.roles[0], 'role');
testUtils.API.checkResponse(response.roles[1], 'role');
testUtils.API.checkResponse(response.roles[2], 'role');
testUtils.API.checkResponse(response.roles[3], 'role');
response.roles[0].name.should.equal('Administrator');
response.roles[1].name.should.equal('Editor');
response.roles[2].name.should.equal('Author');
response.roles[3].name.should.equal('Contributor');
done();
});
});
});
});

View File

@ -0,0 +1,135 @@
const should = require('should');
const supertest = require('supertest');
const Promise = require('bluebird');
const sinon = require('sinon');
const moment = require('moment-timezone');
const testUtils = require('../../../utils');
const localUtils = require('./utils');
const SchedulingDefault = require('../../../../../core/server/adapters/scheduling/SchedulingDefault');
const models = require('../../../../../core/server/models');
const config = require('../../../../../core/server/config');
const ghost = testUtils.startGhost;
const sandbox = sinon.sandbox.create();
describe('Schedules API', function () {
const posts = [];
let request;
let accesstoken;
let ghostServer;
before(function () {
models.init();
// @NOTE: mock the post scheduler, otherwise it will auto publish the post
sandbox.stub(SchedulingDefault.prototype, '_pingUrl').resolves();
});
after(function () {
sandbox.restore();
});
before(function () {
return ghost()
.then(function (_ghostServer) {
ghostServer = _ghostServer;
request = supertest.agent(config.get('url'));
})
.then(function (token) {
accesstoken = token;
posts.push(testUtils.DataGenerator.forKnex.createPost({
created_by: testUtils.existingData.users[0].id,
author_id: testUtils.existingData.users[0].id,
published_by: testUtils.existingData.users[0].id,
published_at: moment().add(30, 'seconds').toDate(),
status: 'scheduled',
slug: 'first'
}));
posts.push(testUtils.DataGenerator.forKnex.createPost({
created_by: testUtils.existingData.users[0].id,
author_id: testUtils.existingData.users[0].id,
published_by: testUtils.existingData.users[0].id,
published_at: moment().subtract(30, 'seconds').toDate(),
status: 'scheduled',
slug: 'second'
}));
posts.push(testUtils.DataGenerator.forKnex.createPost({
created_by: testUtils.existingData.users[0].id,
author_id: testUtils.existingData.users[0].id,
published_by: testUtils.existingData.users[0].id,
published_at: moment().add(10, 'minute').toDate(),
status: 'scheduled',
slug: 'third'
}));
posts.push(testUtils.DataGenerator.forKnex.createPost({
created_by: testUtils.existingData.users[0].id,
author_id: testUtils.existingData.users[0].id,
published_by: testUtils.existingData.users[0].id,
published_at: moment().subtract(10, 'minute').toDate(),
status: 'scheduled',
slug: 'fourth'
}));
return Promise.mapSeries(posts, function (post) {
return models.Post.add(post, {context: {internal: true}});
}).then(function (result) {
result.length.should.eql(4);
});
});
});
describe('publish', function () {
it('default', function () {
return request
.put(localUtils.API.getApiQuery(`schedules/posts/${posts[0].id}/?client_id=ghost-scheduler&client_secret=${testUtils.existingData.clients[0].secret}`))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.then((res) => {
should.exist(res.headers['x-cache-invalidate']);
const jsonResponse = res.body;
should.exist(jsonResponse);
jsonResponse.posts[0].id.should.eql(posts[0].id);
jsonResponse.posts[0].status.should.eql('published');
});
});
it('no access', function () {
return request
.put(localUtils.API.getApiQuery(`schedules/posts/${posts[0].id}/?client_id=ghost-admin&client_secret=${testUtils.existingData.clients[0].secret}`))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(403);
});
it('published_at is x seconds in past, but still in tolerance', function () {
return request
.put(localUtils.API.getApiQuery(`schedules/posts/${posts[1].id}/?client_id=ghost-scheduler&client_secret=${testUtils.existingData.clients[0].secret}`))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
});
it('not found', function () {
return request
.put(localUtils.API.getApiQuery(`schedules/posts/${posts[2].id}/?client_id=ghost-scheduler&client_secret=${testUtils.existingData.clients[0].secret}`))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404);
});
it('force publish', function () {
return request
.put(localUtils.API.getApiQuery(`schedules/posts/${posts[3].id}/?client_id=ghost-scheduler&client_secret=${testUtils.existingData.clients[0].secret}`))
.send({
force: true
})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
});
});
});

View File

@ -26,8 +26,11 @@ describe('Settings API', function () {
}); });
}); });
// TODO: currently includes values of type=core after(function () {
it('can retrieve all settings', function (done) { return ghostServer.stop();
});
it('browse', function (done) {
request.get(localUtils.API.getApiQuery('settings/')) request.get(localUtils.API.getApiQuery('settings/'))
.set('Authorization', 'Bearer ' + accesstoken) .set('Authorization', 'Bearer ' + accesstoken)
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
@ -48,11 +51,18 @@ describe('Settings API', function () {
JSON.parse(_.find(jsonResponse.settings, {key: 'amp'}).value).should.eql(true); JSON.parse(_.find(jsonResponse.settings, {key: 'amp'}).value).should.eql(true);
should.not.exist(_.find(jsonResponse.settings, {key: 'permalinks'})); should.not.exist(_.find(jsonResponse.settings, {key: 'permalinks'}));
testUtils.API.isISO8601(jsonResponse.settings[0].created_at).should.be.true();
jsonResponse.settings[0].created_at.should.be.an.instanceof(String);
should.not.exist(_.find(jsonResponse.settings, function (setting) {
return setting.type === 'core';
}));
done(); done();
}); });
}); });
it('can retrieve a setting', function (done) { it('read', function (done) {
request.get(localUtils.API.getApiQuery('settings/title/')) request.get(localUtils.API.getApiQuery('settings/title/'))
.set('Authorization', 'Bearer ' + accesstoken) .set('Authorization', 'Bearer ' + accesstoken)
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
@ -76,7 +86,7 @@ describe('Settings API', function () {
}); });
}); });
it('can\'t retrieve permalinks', function (done) { it('can\'t read permalinks', function (done) {
request.get(localUtils.API.getApiQuery('settings/permalinks/')) request.get(localUtils.API.getApiQuery('settings/permalinks/'))
.set('Authorization', 'Bearer ' + accesstoken) .set('Authorization', 'Bearer ' + accesstoken)
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
@ -91,7 +101,7 @@ describe('Settings API', function () {
}); });
}); });
it('can\'t retrieve non existent setting', function (done) { it('can\'t read non existent setting', function (done) {
request.get(localUtils.API.getApiQuery('settings/testsetting/')) request.get(localUtils.API.getApiQuery('settings/testsetting/'))
.set('Authorization', 'Bearer ' + accesstoken) .set('Authorization', 'Bearer ' + accesstoken)
.set('Accept', 'application/json') .set('Accept', 'application/json')
@ -123,7 +133,7 @@ describe('Settings API', function () {
} }
var jsonResponse = res.body, var jsonResponse = res.body,
changedValue = 'Ghost changed', changedValue = [],
settingToChange = { settingToChange = {
settings: [ settings: [
{key: 'title', value: changedValue} {key: 'title', value: changedValue}
@ -147,7 +157,7 @@ describe('Settings API', function () {
var putBody = res.body; var putBody = res.body;
res.headers['x-cache-invalidate'].should.eql('/*'); res.headers['x-cache-invalidate'].should.eql('/*');
should.exist(putBody); should.exist(putBody);
putBody.settings[0].value.should.eql(changedValue); putBody.settings[0].value.should.eql(JSON.stringify(changedValue));
testUtils.API.checkResponse(putBody, 'settings'); testUtils.API.checkResponse(putBody, 'settings');
done(); done();
}); });

View File

@ -0,0 +1,80 @@
const should = require('should');
const supertest = require('supertest');
const sinon = require('sinon');
const testUtils = require('../../../utils');
const localUtils = require('./utils');
const config = require('../../../../../core/server/config');
const labs = require('../../../../../core/server/services/labs');
const ghost = testUtils.startGhost;
const sandbox = sinon.sandbox.create();
let request;
describe('Subscribers API', function () {
let accesstoken = '', ghostServer;
before(function () {
sandbox.stub(labs, 'isSet').withArgs('subscribers').returns(true);
});
after(function () {
sandbox.restore();
});
before(function () {
return ghost()
.then(function (_ghostServer) {
ghostServer = _ghostServer;
request = supertest.agent(config.get('url'));
})
.then(function () {
return localUtils.doAuth(request, 'subscriber');
})
.then(function (token) {
accesstoken = token;
});
});
it('browse', function () {
return request
.get(localUtils.API.getApiQuery('subscribers/'))
.set('Authorization', 'Bearer ' + accesstoken)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.then((res) => {
should.not.exist(res.headers['x-cache-invalidate']);
const jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.subscribers);
jsonResponse.subscribers.should.have.length(1);
testUtils.API.checkResponse(jsonResponse.subscribers[0], 'subscriber');
testUtils.API.isISO8601(jsonResponse.subscribers[0].created_at).should.be.true();
jsonResponse.subscribers[0].created_at.should.be.an.instanceof(String);
jsonResponse.meta.pagination.should.have.property('page', 1);
jsonResponse.meta.pagination.should.have.property('limit', 15);
jsonResponse.meta.pagination.should.have.property('pages', 1);
jsonResponse.meta.pagination.should.have.property('total', 1);
jsonResponse.meta.pagination.should.have.property('next', null);
jsonResponse.meta.pagination.should.have.property('prev', null);
});
});
it('read', function () {
return request
.get(localUtils.API.getApiQuery(`subscribers/${testUtils.DataGenerator.Content.subscribers[0].id}/`))
.set('Authorization', 'Bearer ' + accesstoken)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.then((res) => {
should.not.exist(res.headers['x-cache-invalidate']);
const jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.subscribers);
jsonResponse.subscribers.should.have.length(1);
testUtils.API.checkResponse(jsonResponse.subscribers[0], 'subscriber');
});
});
});

View File

@ -23,26 +23,137 @@ describe('Tag API', function () {
}); });
}); });
it('can retrieve all tags', function (done) { it('browse', function () {
request.get(localUtils.API.getApiQuery('tags/')) return request
.get(localUtils.API.getApiQuery('tags/?include=count.posts'))
.set('Authorization', 'Bearer ' + accesstoken) .set('Authorization', 'Bearer ' + accesstoken)
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private) .expect('Cache-Control', testUtils.cacheRules.private)
.expect(200) .expect(200)
.end(function (err, res) { .then((res) => {
if (err) {
return done(err);
}
should.not.exist(res.headers['x-cache-invalidate']); should.not.exist(res.headers['x-cache-invalidate']);
var jsonResponse = res.body; const jsonResponse = res.body;
should.exist(jsonResponse); should.exist(jsonResponse);
should.exist(jsonResponse.tags); should.exist(jsonResponse.tags);
jsonResponse.tags.should.have.length(6); jsonResponse.tags.should.have.length(6);
testUtils.API.checkResponse(jsonResponse.tags[0], 'tag'); testUtils.API.checkResponse(jsonResponse.tags[0], 'tag', 'count');
testUtils.API.isISO8601(jsonResponse.tags[0].created_at).should.be.true();
done(); testUtils.API.isISO8601(jsonResponse.tags[0].created_at).should.be.true();
jsonResponse.tags[0].created_at.should.be.an.instanceof(String);
jsonResponse.meta.pagination.should.have.property('page', 1);
jsonResponse.meta.pagination.should.have.property('limit', 15);
jsonResponse.meta.pagination.should.have.property('pages', 1);
jsonResponse.meta.pagination.should.have.property('total', 6);
jsonResponse.meta.pagination.should.have.property('next', null);
jsonResponse.meta.pagination.should.have.property('prev', null);
should.exist(jsonResponse.tags[0].count.posts);
});
});
it('read', function () {
return request
.get(localUtils.API.getApiQuery(`tags/${testUtils.existingData.tags[0].id}/?include=count.posts`))
.set('Authorization', 'Bearer ' + accesstoken)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.then((res) => {
should.not.exist(res.headers['x-cache-invalidate']);
const jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.tags);
jsonResponse.tags.should.have.length(1);
testUtils.API.checkResponse(jsonResponse.tags[0], 'tag', 'count');
should.exist(jsonResponse.tags[0].count.posts);
});
});
it('add', function () {
const tag = testUtils.DataGenerator.forKnex.createTag();
return request
.post(localUtils.API.getApiQuery('tags/'))
.set('Authorization', 'Bearer ' + accesstoken)
.send({
tags: [tag]
})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(201)
.then((res) => {
should.exist(res.headers['x-cache-invalidate']);
const jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.tags);
jsonResponse.tags.should.have.length(1);
// @TODO: model layer has no defaults for these properties
testUtils.API.checkResponse(jsonResponse.tags[0], 'tag', null, [
'feature_image',
'meta_description',
'meta_title',
'parent'
]);
testUtils.API.isISO8601(jsonResponse.tags[0].created_at).should.be.true();
});
});
it('add internal', function () {
const tag = testUtils.DataGenerator.forKnex.createTag({
name: '#test',
slug: null
});
return request
.post(localUtils.API.getApiQuery('tags/'))
.set('Authorization', 'Bearer ' + accesstoken)
.send({
tags: [tag]
})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(201)
.then((res) => {
should.exist(res.headers['x-cache-invalidate']);
const jsonResponse = res.body;
should.exist(jsonResponse);
jsonResponse.tags[0].visibility.should.eql('internal');
jsonResponse.tags[0].name.should.eql('#test');
jsonResponse.tags[0].slug.should.eql('hash-test');
});
});
it('edit', function () {
return request
.put(localUtils.API.getApiQuery(`tags/${testUtils.existingData.tags[0].id}`))
.set('Authorization', 'Bearer ' + accesstoken)
.send({
tags: [Object.assign({}, testUtils.existingData.tags[0], {description: 'hey ho ab ins klo'})]
})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.then((res) => {
should.exist(res.headers['x-cache-invalidate']);
const jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.tags);
jsonResponse.tags.should.have.length(1);
testUtils.API.checkResponse(jsonResponse.tags[0], 'tag');
jsonResponse.tags[0].description.should.eql('hey ho ab ins klo');
});
});
it('destroy', function () {
return request
.del(localUtils.API.getApiQuery(`tags/${testUtils.existingData.tags[0].id}`))
.set('Authorization', 'Bearer ' + accesstoken)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(204)
.then((res) => {
should.exist(res.headers['x-cache-invalidate']);
res.body.should.eql({});
}); });
}); });
}); });

View File

@ -2,10 +2,12 @@ var should = require('should'),
_ = require('lodash'), _ = require('lodash'),
supertest = require('supertest'), supertest = require('supertest'),
moment = require('moment'), moment = require('moment'),
Promise = require('bluebird'),
testUtils = require('../../../utils'), testUtils = require('../../../utils'),
localUtils = require('./utils'), localUtils = require('./utils'),
ObjectId = require('bson-objectid'), ObjectId = require('bson-objectid'),
config = require('../../../../../core/server/config'), config = require('../../../../../core/server/config'),
models = require('../../../../../core/server/models'),
ghost = testUtils.startGhost, ghost = testUtils.startGhost,
request; request;
@ -13,7 +15,7 @@ describe('User API', function () {
var ownerAccessToken = '', var ownerAccessToken = '',
editorAccessToken = '', editorAccessToken = '',
authorAccessToken = '', authorAccessToken = '',
editor, author, ghostServer, inactiveUser; editor, author, ghostServer, inactiveUser, admin;
before(function () { before(function () {
return ghost() return ghost()
@ -49,6 +51,15 @@ describe('User API', function () {
.then(function (_user3) { .then(function (_user3) {
inactiveUser = _user3; inactiveUser = _user3;
// create admin user
return testUtils.createUser({
user: testUtils.DataGenerator.forKnex.createUser({email: 'test+admin@ghost.org'}),
role: testUtils.DataGenerator.Content.roles[0].name
});
})
.then(function (_user4) {
admin = _user4;
// by default we login with the owner // by default we login with the owner
return localUtils.doAuth(request); return localUtils.doAuth(request);
}) })
@ -72,39 +83,8 @@ describe('User API', function () {
describe('As Owner', function () { describe('As Owner', function () {
describe('Browse', function () { describe('Browse', function () {
it('returns dates in ISO 8601 format', function (done) { it('returns dates in ISO 8601 format', function (done) {
request.get(localUtils.API.getApiQuery('users/?order=id%20ASC')) // @NOTE: ASC is default
.set('Authorization', 'Bearer ' + ownerAccessToken) request.get(localUtils.API.getApiQuery('users/?order=id%20DESC'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
var jsonResponse = res.body;
should.exist(jsonResponse.users);
testUtils.API.checkResponse(jsonResponse, 'users');
// owner use + ghost-author user when Ghost starts
// and two extra users, see createUser in before
jsonResponse.users.should.have.length(5);
testUtils.API.checkResponse(jsonResponse.users[0], 'user');
testUtils.API.isISO8601(jsonResponse.users[0].last_seen).should.be.true();
testUtils.API.isISO8601(jsonResponse.users[0].created_at).should.be.true();
testUtils.API.isISO8601(jsonResponse.users[0].updated_at).should.be.true();
testUtils.API.isISO8601(jsonResponse.users[2].last_seen).should.be.true();
testUtils.API.isISO8601(jsonResponse.users[2].created_at).should.be.true();
testUtils.API.isISO8601(jsonResponse.users[2].updated_at).should.be.true();
done();
});
});
it('can retrieve all users', function (done) {
request.get(localUtils.API.getApiQuery('users/'))
.set('Authorization', 'Bearer ' + ownerAccessToken) .set('Authorization', 'Bearer ' + ownerAccessToken)
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private) .expect('Cache-Control', testUtils.cacheRules.private)
@ -115,13 +95,29 @@ describe('User API', function () {
} }
should.not.exist(res.headers['x-cache-invalidate']); should.not.exist(res.headers['x-cache-invalidate']);
var jsonResponse = res.body; var jsonResponse = res.body;
should.exist(jsonResponse.users); should.exist(jsonResponse.users);
testUtils.API.checkResponse(jsonResponse, 'users'); testUtils.API.checkResponse(jsonResponse, 'users');
jsonResponse.users.should.have.length(5); // owner use + ghost-author user when Ghost starts
// and two extra users, see createUser in before
jsonResponse.users.should.have.length(6);
testUtils.API.checkResponse(jsonResponse.users[0], 'user'); testUtils.API.checkResponse(jsonResponse.users[0], 'user');
jsonResponse.users[4].status.should.eql(inactiveUser.status); testUtils.API.isISO8601(jsonResponse.users[5].last_seen).should.be.true();
testUtils.API.isISO8601(jsonResponse.users[5].created_at).should.be.true();
testUtils.API.isISO8601(jsonResponse.users[5].updated_at).should.be.true();
testUtils.API.isISO8601(jsonResponse.users[2].last_seen).should.be.true();
testUtils.API.isISO8601(jsonResponse.users[2].created_at).should.be.true();
testUtils.API.isISO8601(jsonResponse.users[2].updated_at).should.be.true();
jsonResponse.users[0].email.should.eql('test+admin@ghost.org');
jsonResponse.users[1].email.should.eql('test+3@ghost.org');
jsonResponse.users[1].status.should.eql(inactiveUser.status);
jsonResponse.users[5].email.should.eql(testUtils.DataGenerator.Content.users[0].email);
done(); done();
}); });
}); });
@ -142,7 +138,7 @@ describe('User API', function () {
should.exist(jsonResponse.users); should.exist(jsonResponse.users);
testUtils.API.checkResponse(jsonResponse, 'users'); testUtils.API.checkResponse(jsonResponse, 'users');
jsonResponse.users.should.have.length(5); jsonResponse.users.should.have.length(6);
testUtils.API.checkResponse(jsonResponse.users[0], 'user', 'roles'); testUtils.API.checkResponse(jsonResponse.users[0], 'user', 'roles');
done(); done();
}); });
@ -421,7 +417,10 @@ describe('User API', function () {
dataToSend = { dataToSend = {
users: [ users: [
{website: changedValue} {
website: changedValue,
password: 'mynewfancypasswordwhichisnotallowed'
}
] ]
}; };
@ -442,7 +441,21 @@ describe('User API', function () {
putBody.users[0].website.should.eql(changedValue); putBody.users[0].website.should.eql(changedValue);
putBody.users[0].email.should.eql(jsonResponse.users[0].email); putBody.users[0].email.should.eql(jsonResponse.users[0].email);
testUtils.API.checkResponse(putBody.users[0], 'user'); testUtils.API.checkResponse(putBody.users[0], 'user');
done();
should.not.exist(putBody.users[0].password);
models.User.findOne({id: putBody.users[0].id})
.then((user) => {
return models.User.isPasswordCorrect({
plainPassword: 'mynewfancypasswordwhichisnotallowed',
hashedPassword: user.get('password')
});
})
.then(Promise.reject)
.catch((err) => {
err.code.should.eql('PASSWORD_INCORRECT');
done();
});
}); });
}); });
}); });
@ -528,16 +541,33 @@ describe('User API', function () {
}); });
describe('Destroy', function () { describe('Destroy', function () {
it('[success] Destroy active user', function (done) { it('[success] Destroy active user', function () {
request.delete(localUtils.API.getApiQuery('users/' + editor.id)) return request
.get(localUtils.API.getApiQuery(`posts/?filter=author_id:${testUtils.existingData.users[1].id}`))
.set('Authorization', 'Bearer ' + ownerAccessToken) .set('Authorization', 'Bearer ' + ownerAccessToken)
.expect(204) .expect(200)
.end(function (err) { .then((res) => {
if (err) { res.body.posts.length.should.eql(7);
return done(err);
}
done(); return request
.delete(localUtils.API.getApiQuery(`users/${testUtils.existingData.users[1].id}`))
.set('Authorization', 'Bearer ' + ownerAccessToken)
.expect(204);
})
.then(() => {
return request
.get(localUtils.API.getApiQuery(`users/${testUtils.existingData.users[1].id}/`))
.set('Authorization', 'Bearer ' + ownerAccessToken)
.expect(404);
})
.then(() => {
return request
.get(localUtils.API.getApiQuery(`posts/?filter=author_id:${testUtils.existingData.users[1].id}`))
.set('Authorization', 'Bearer ' + ownerAccessToken)
.expect(200);
})
.then((res) => {
res.body.posts.length.should.eql(0);
}); });
}); });
@ -556,6 +586,26 @@ describe('User API', function () {
}); });
}); });
describe('Transfer ownership', function () {
it('Owner can transfer ownership to admin user', function () {
return request
.put(localUtils.API.getApiQuery('users/owner'))
.set('Authorization', 'Bearer ' + ownerAccessToken)
.send({
owner: [{
id: admin.id
}]
})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.then((res) => {
res.body.users[0].roles[0].name.should.equal(testUtils.DataGenerator.Content.roles[0].name);
res.body.users[1].roles[0].name.should.equal(testUtils.DataGenerator.Content.roles[3].name);
});
});
});
describe('As Editor', function () { describe('As Editor', function () {
before(function () { before(function () {
return ghost() return ghost()

File diff suppressed because it is too large Load Diff

View File

@ -1,598 +0,0 @@
var should = require('should'),
sinon = require('sinon'),
testUtils = require('../../utils'),
_ = require('lodash'),
Promise = require('bluebird'),
AuthAPI = require('../../../server/api/v0.1/authentication'),
mail = require('../../../server/api/v0.1/mail'),
models = require('../../../server/models'),
common = require('../../../server/lib/common'),
security = require('../../../server/lib/security'),
context = testUtils.context,
accessToken,
refreshToken,
User,
sandbox = sinon.sandbox.create();
describe('Authentication API', function () {
var testInvite = {
invitation: [{
token: 'abc',
password: 'abcdefgh',
email: 'test@testghost.org',
name: 'Jo Bloggs'
}]
},
testGenerateReset = {
passwordreset: [{
email: 'jbloggs@example.com'
}]
},
testReset = {
passwordreset: [{
token: 'abc',
newPassword: 'abcdefghij',
ne2Password: 'abcdefghij'
}]
};
// Keep the DB clean
before(testUtils.teardown);
afterEach(testUtils.teardown);
// Stub mail
beforeEach(function () {
sandbox.stub(mail, 'send').callsFake(function () {
return Promise.resolve();
});
});
afterEach(function () {
sandbox.restore();
});
should.exist(AuthAPI);
describe('Setup', function () {
describe('Cannot run', function () {
before(function () {
User = require('../../../server/models/user').User;
});
beforeEach(testUtils.setup('owner:pre', 'settings', 'perms:setting', 'perms:mail', 'perms:init'));
describe('Invalid database state', function () {
it('should not allow setup to be run if owner missing from database', function (done) {
var setupData = {
name: 'test user',
email: 'test@example.com',
password: 'thisissupersafe',
blogTitle: 'a test blog'
};
User.fetchAll().call('invokeThen', 'destroy').then(function () {
AuthAPI.setup({setup: [setupData]}).then(function () {
done(new Error('Setup ran when it should not have.'));
}).catch(function (err) {
should.exist(err);
err.name.should.equal('NotFoundError');
err.message.should.equal('Owner not found');
err.statusCode.should.equal(404);
done();
}).catch(done);
});
});
});
});
describe('Not completed', function () {
// TODO: stub settings
beforeEach(testUtils.setup('owner:pre', 'settings', 'perms:setting', 'perms:mail', 'perms:init'));
it('should report that setup has not been completed', function (done) {
AuthAPI.isSetup().then(function (result) {
should.exist(result);
result.setup[0].status.should.be.false();
done();
}).catch(done);
});
it('should allow setup to be completed', function (done) {
var setupData = {
name: 'test user',
email: 'test@example.com',
password: 'thisissupersafe',
blogTitle: 'a test blog'
};
AuthAPI.setup({setup: [setupData]}).then(function (result) {
should.exist(result);
should.exist(result.users);
should.not.exist(result.meta);
result.users.should.have.length(1);
testUtils.API.checkResponse(result.users[0], 'user');
var newUser = result.users[0];
newUser.id.should.equal(testUtils.DataGenerator.Content.users[0].id);
newUser.name.should.equal(setupData.name);
newUser.email.should.equal(setupData.email);
done();
}).catch(done);
});
it('should allow setup to be completed without a blog title', function (done) {
var setupData = {
name: 'test user',
email: 'test@example.com',
password: 'thisissupersafe'
};
AuthAPI.setup({setup: [setupData]}).then(function (result) {
should.exist(result);
should.exist(result.users);
should.not.exist(result.meta);
result.users.should.have.length(1);
testUtils.API.checkResponse(result.users[0], 'user');
var newUser = result.users[0];
newUser.id.should.equal(testUtils.DataGenerator.Content.users[0].id);
newUser.name.should.equal(setupData.name);
newUser.email.should.equal(setupData.email);
done();
}).catch(done);
});
it('should return an error for an invitation check', function (done) {
AuthAPI.isInvitation({email: 'a@example.com'}).then(function () {
done(new Error('Did not receive an error response'));
}).catch(function (err) {
should.exist(err);
err.name.should.equal('NoPermissionError');
err.statusCode.should.equal(403);
done();
}).catch(done);
});
it('should not allow an invitation to be accepted', function (done) {
AuthAPI.acceptInvitation(testInvite).then(function () {
done(new Error('Invitation was allowed to be accepted'));
}).catch(function (err) {
should.exist(err);
err.name.should.equal('NoPermissionError');
err.statusCode.should.equal(403);
done();
}).catch(done);
});
it('should not generate a password reset token', function (done) {
AuthAPI.generateResetToken(testGenerateReset).then(function () {
done(new Error('Reset token was generated'));
}).catch(function (err) {
should.exist(err);
err.name.should.equal('NoPermissionError');
err.statusCode.should.equal(403);
done();
}).catch(done);
});
it('should not allow a password reset', function (done) {
AuthAPI.resetPassword(testReset).then(function () {
done(new Error('Password was reset'));
}).catch(function (err) {
should.exist(err);
err.name.should.equal('NoPermissionError');
err.statusCode.should.equal(403);
done();
}).catch(done);
});
});
describe('Completed', function () {
before(function () {
accessToken = require('../../../server/models/accesstoken').Accesstoken;
refreshToken = require('../../../server/models/refreshtoken').Refreshtoken;
User = require('../../../server/models/user').User;
});
beforeEach(testUtils.setup('invites', 'roles', 'owner', 'clients', 'settings', 'perms:setting', 'perms:mail', 'perms:init'));
it('should report that setup has been completed', function (done) {
AuthAPI.isSetup().then(function (result) {
should.exist(result);
result.setup[0].status.should.be.true();
done();
}).catch(done);
});
it('should not allow setup to be run again', function (done) {
var setupData = {
name: 'test user',
email: 'test@example.com',
password: 'thisissupersafe',
blogTitle: 'a test blog'
};
AuthAPI.setup({setup: [setupData]}).then(function () {
done(new Error('Setup was able to be run'));
}).catch(function (err) {
should.exist(err);
err.name.should.equal('NoPermissionError');
err.statusCode.should.equal(403);
done();
}).catch(done);
});
it('should allow an invitation to be accepted, but fail on token validation', function (done) {
AuthAPI.acceptInvitation(testInvite).then(function () {
done(new Error('invitation did not fail on token validation'));
}).catch(function (err) {
should.exist(err);
err.name.should.equal('NotFoundError');
err.statusCode.should.equal(404);
err.message.should.equal('Invite not found.');
done();
}).catch(done);
});
it('should allow an invitation to be accepted', function () {
var invite;
return models.Invite.add({
email: '123@meins.de',
role_id: testUtils.DataGenerator.Content.roles[0].id
}, context.internal)
.then(function (_invite) {
invite = _invite;
invite.toJSON().role_id.should.eql(testUtils.DataGenerator.Content.roles[0].id);
return models.Invite.edit({status: 'sent'}, _.merge({}, {id: invite.id}, context.internal));
})
.then(function () {
return AuthAPI.acceptInvitation({
invitation: [
{
token: invite.get('token'),
email: invite.get('email'),
name: invite.get('email'),
password: 'tencharacterslong'
}
]
});
})
.then(function (res) {
should.exist(res.invitation[0].message);
return models.Invite.findOne({id: invite.id}, context.internal);
})
.then(function (_invite) {
should.not.exist(_invite);
return models.User.findOne({
email: invite.get('email')
}, _.merge({withRelated: ['roles']}, context.internal));
})
.then(function (user) {
user.toJSON().roles.length.should.eql(1);
user.toJSON().roles[0].id.should.eql(testUtils.DataGenerator.Content.roles[0].id);
});
});
it('should not allow an invitation to be accepted: expired', function () {
var invite;
return models.Invite.add({email: '123@meins.de', role_id: testUtils.roles.ids.author}, context.internal)
.then(function (_invite) {
invite = _invite;
return models.Invite.edit({
status: 'sent',
expires: Date.now() - 10000
}, _.merge({}, {id: invite.id}, context.internal));
})
.then(function () {
return AuthAPI.acceptInvitation({
invitation: [
{
token: invite.get('token'),
email: invite.get('email'),
name: invite.get('email'),
password: 'tencharacterslong'
}
]
});
})
.then(function () {
throw new Error('should not pass the test: expected expired invitation');
})
.catch(function (err) {
should.exist(err);
(err instanceof common.errors.NotFoundError).should.eql(true);
err.message.should.eql('Invite is expired.');
});
});
it('should generate a password reset token', function (done) {
AuthAPI.generateResetToken(testGenerateReset).then(function (result) {
should.exist(result);
result.passwordreset.should.be.an.Array().with.lengthOf(1);
result.passwordreset[0].should.have.property('message', 'Check your email for further instructions.');
done();
}).catch(done);
});
it('should not generate a password reset token for an invalid email address', function (done) {
var badResetRequest = {
passwordreset: [{email: ''}]
};
AuthAPI.generateResetToken(badResetRequest).then(function () {
done(new Error('reset token was generated for invalid email address'));
}).catch(function (err) {
should.exist(err);
err.name.should.equal('BadRequestError');
err.statusCode.should.equal(400);
done();
}).catch(done);
});
it('should not allow a password reset', function (done) {
AuthAPI.resetPassword(testReset).then(function () {
done(new Error('password reset did not fail on token validation'));
}).catch(function (err) {
should.exist(err);
err.name.should.equal('UnauthorizedError');
err.statusCode.should.equal(401);
err.message.should.equal('Invalid token structure');
done();
}).catch(done);
});
it('should allow an access token to be revoked', function (done) {
var id = security.identifier.uid(191);
accessToken.add({
token: id,
expires: Date.now() + 8640000,
user_id: testUtils.DataGenerator.Content.users[0].id,
client_id: testUtils.DataGenerator.forKnex.clients[0].id
}, testUtils.context.internal).then(function (token) {
should.exist(token);
token.get('token').should.equal(id);
return AuthAPI.revoke({
token: token.get('token'),
token_type_hint: 'access_token'
});
}).then(function (response) {
should.exist(response);
response.token.should.equal(id);
return accessToken.findOne({token: id});
}).then(function (token) {
should.not.exist(token);
done();
}).catch(done);
});
it('should know an email address has an active invitation', function () {
return AuthAPI.isInvitation({email: testUtils.DataGenerator.forKnex.invites[0].email})
.then(function (response) {
should.exist(response);
response.invitation[0].valid.should.be.true();
response.invitation[0].invitedBy.should.eql('Joe Bloggs');
});
});
it('should know an email address does not have an active invitation', function (done) {
var user = {
name: 'uninvited user',
email: 'notinvited@example.com',
password: 'thisissupersafe',
status: 'active'
},
options = {
context: {internal: true}
};
User.add(user, options).then(function (user) {
return AuthAPI.isInvitation({email: user.get('email')});
}).then(function (response) {
should.exist(response);
response.invitation[0].valid.should.be.false();
done();
}).catch(done);
});
it('should know an unknown email address is not an active invitation', function (done) {
AuthAPI.isInvitation({email: 'unknown@example.com'}).then(function (response) {
should.exist(response);
response.invitation[0].valid.should.be.false();
done();
}).catch(done);
});
it('should allow a refresh token to be revoked', function (done) {
var id = security.identifier.uid(191);
refreshToken.add({
token: id,
expires: Date.now() + 8640000,
user_id: testUtils.DataGenerator.Content.users[0].id,
client_id: testUtils.DataGenerator.forKnex.clients[0].id
}).then(function (token) {
should.exist(token);
token.get('token').should.equal(id);
return AuthAPI.revoke({
token: token.get('token'),
token_type_hint: 'refresh_token'
});
}).then(function (response) {
should.exist(response);
response.token.should.equal(id);
return refreshToken.findOne({token: id});
}).then(function (token) {
should.not.exist(token);
done();
}).catch(done);
});
it('should return success when attempting to revoke an invalid token', function (done) {
var id = security.identifier.uid(191);
accessToken.add({
token: id,
expires: Date.now() + 8640000,
user_id: testUtils.DataGenerator.Content.users[0].id,
client_id: testUtils.DataGenerator.forKnex.clients[0].id
}).then(function (token) {
should.exist(token);
token.get('token').should.equal(id);
return AuthAPI.revoke({
token: 'notavalidtoken',
token_type_hint: 'access_token'
});
}).then(function (response) {
should.exist(response);
response.token.should.equal('notavalidtoken');
response.error.should.equal('Invalid token provided');
done();
}).catch(done);
});
});
});
describe('Setup Update', function () {
describe('Setup not complete', function () {
beforeEach(testUtils.setup('owner:pre', 'settings', 'perms:setting', 'perms:init'));
it('should report that setup has not been completed', function (done) {
AuthAPI.isSetup().then(function (result) {
should.exist(result);
result.setup[0].status.should.be.false();
done();
}).catch(done);
});
it('should not allow setup to be updated', function (done) {
var setupData = {
name: 'test user',
email: 'test@example.com',
password: 'thisissupersafe',
blogTitle: 'a test blog'
};
AuthAPI.updateSetup({setup: [setupData]}, {}).then(function () {
done(new Error('Update was able to be run'));
}).catch(function (err) {
should.exist(err);
err.name.should.equal('NoPermissionError');
err.statusCode.should.equal(403);
done();
}).catch(done);
});
});
describe('Not Owner', function () {
beforeEach(testUtils.setup('users:roles', 'settings', 'perms:setting', 'perms:init', 'perms:user'));
it('should report that setup has been completed', function (done) {
AuthAPI.isSetup().then(function (result) {
should.exist(result);
result.setup[0].status.should.be.true();
done();
}).catch(done);
});
it('should not allow setup to be updated', function (done) {
var setupData = {
name: 'test user',
email: 'test@example.com',
password: 'thisissupersafe',
blogTitle: 'a test blog'
};
AuthAPI.updateSetup({setup: [setupData]}, context.author).then(function () {
done(new Error('Update was able to be run'));
}).catch(function (err) {
should.exist(err);
err.name.should.equal('NoPermissionError');
err.statusCode.should.equal(403);
done();
}).catch(done);
});
});
describe('Owner', function () {
beforeEach(testUtils.setup('users:roles', 'settings', 'perms:setting', 'perms:init'));
it('should report that setup has been completed', function (done) {
AuthAPI.isSetup().then(function (result) {
should.exist(result);
result.setup[0].status.should.be.true();
done();
}).catch(done);
});
it('should allow setup to be updated', function (done) {
var setupData = {
name: 'test user',
email: 'test@example.com',
password: 'thisissupersafe',
blogTitle: 'a test blog'
};
AuthAPI.updateSetup({setup: [setupData]}, context.owner).then(function (result) {
should.exist(result);
should.exist(result.users);
should.not.exist(result.meta);
result.users.should.have.length(1);
testUtils.API.checkResponse(result.users[0], 'user');
var newUser = result.users[0];
newUser.id.should.equal(testUtils.DataGenerator.Content.users[0].id);
newUser.name.should.equal(setupData.name);
newUser.email.should.equal(setupData.email);
done();
}).catch(done);
});
});
});
});

View File

@ -1,64 +0,0 @@
var should = require('should'),
testUtils = require('../../utils'),
rewire = require('rewire'),
configUtils = require('../../utils/configUtils'),
// Stuff we are testing
ConfigurationAPI = rewire('../../../server/api/v0.1/configuration');
describe('Configuration API', function () {
// Keep the DB clean
before(testUtils.teardown);
beforeEach(testUtils.setup('clients', 'settings'));
afterEach(function () {
configUtils.restore();
return testUtils.teardown();
});
should.exist(ConfigurationAPI);
it('can read basic config and get all expected properties', function (done) {
ConfigurationAPI.read().then(function (response) {
var props;
should.exist(response);
should.exist(response.configuration);
response.configuration.should.be.an.Array().with.lengthOf(1);
props = response.configuration[0];
props.blogUrl.should.eql('http://127.0.0.1:2369/');
props.useGravatar.should.eql(false);
props.publicAPI.should.eql(true);
props.clientId.should.eql('ghost-admin');
props.clientSecret.should.eql('not_available');
// value not available, because settings API was not called yet
props.hasOwnProperty('blogTitle').should.eql(true);
done();
}).catch(done);
});
it('can read about config and get all expected properties', function (done) {
ConfigurationAPI.read({key: 'about'}).then(function (response) {
var props;
should.exist(response);
should.exist(response.configuration);
response.configuration.should.be.an.Array().with.lengthOf(1);
props = response.configuration[0];
// Check the structure
props.should.have.property('version').which.is.a.String();
props.should.have.property('environment').which.is.a.String();
props.should.have.property('database').which.is.a.String();
props.should.have.property('mail').which.is.a.String();
// Check a few values
props.environment.should.match(/^testing/);
props.version.should.eql(require('../../../../package.json').version);
done();
}).catch(done);
});
});

View File

@ -1,178 +0,0 @@
var should = require('should'),
_ = require('lodash'),
sinon = require('sinon'),
testUtils = require('../../utils'),
common = require('../../../server/lib/common'),
dbAPI = require('../../../server/api/v0.1/db'),
models = require('../../../server/models'),
sandbox = sinon.sandbox.create();
describe('DB API', function () {
var eventsTriggered;
afterEach(function () {
sandbox.restore();
});
// Keep the DB clean
before(testUtils.teardown);
afterEach(testUtils.teardown);
beforeEach(testUtils.setup('users:roles', 'settings', 'posts', 'subscriber', 'perms:db', 'perms:init'));
beforeEach(function () {
eventsTriggered = {};
sandbox.stub(common.events, 'emit').callsFake(function (eventName, eventObj) {
if (!eventsTriggered[eventName]) {
eventsTriggered[eventName] = [];
}
eventsTriggered[eventName].push(eventObj);
});
});
should.exist(dbAPI);
it('delete all content (owner)', function () {
return models.Post.findAll(testUtils.context.internal)
.then(function (results) {
results = results.toJSON();
results.length.should.eql(8);
_.filter(results, {page: false, status: 'published'}).length.should.equal(4);
_.filter(results, {page: false, status: 'draft'}).length.should.equal(1);
_.filter(results, {page: false, status: 'scheduled'}).length.should.equal(1);
_.filter(results, {page: true, status: 'published'}).length.should.equal(1);
_.filter(results, {page: true, status: 'draft'}).length.should.equal(1);
})
.then(function () {
return dbAPI.deleteAllContent(testUtils.context.owner);
})
.then(function (result) {
should.exist(result.db);
result.db.should.be.instanceof(Array);
result.db.should.be.empty();
return models.Tag.findAll(testUtils.context.internal);
})
.then(function (results) {
should.exist(results);
results.length.should.equal(0);
return models.Post.findAll(testUtils.context.internal);
})
.then(function (results) {
should.exist(results);
results.length.should.equal(0);
return models.Subscriber.findAll(testUtils.context.internal);
})
.then(function (results) {
should.exist(results);
results.length.should.equal(1);
})
.then(function () {
eventsTriggered['post.unpublished'].length.should.eql(4);
eventsTriggered['post.deleted'].length.should.eql(6);
eventsTriggered['page.unpublished'].length.should.eql(1);
eventsTriggered['page.deleted'].length.should.eql(2);
eventsTriggered['tag.deleted'].length.should.eql(5);
});
});
it('delete all content (admin)', function () {
return dbAPI.deleteAllContent(testUtils.context.admin).then(function (result) {
should.exist(result.db);
result.db.should.be.instanceof(Array);
result.db.should.be.empty();
}).then(function () {
return models.Tag.findAll(testUtils.context.admin).then(function (results) {
should.exist(results);
results.length.should.equal(0);
});
}).then(function () {
return models.Post.findAll(testUtils.context.admin).then(function (results) {
should.exist(results);
results.length.should.equal(0);
});
});
});
it('delete all content is denied (editor, author, contributor & without authentication)', function () {
return dbAPI.deleteAllContent(testUtils.context.editor).then(function () {
throw new Error('Delete all content is not denied for editor.');
}, function (error) {
error.errorType.should.eql('NoPermissionError');
return dbAPI.deleteAllContent(testUtils.context.author);
}).then(function () {
throw new Error('Delete all content is not denied for author.');
}, function (error) {
error.errorType.should.eql('NoPermissionError');
return dbAPI.deleteAllContent(testUtils.context.contributor);
}).then(function () {
throw new Error('Delete all content is not denied for contributor.');
}, function (error) {
error.errorType.should.eql('NoPermissionError');
return dbAPI.deleteAllContent();
}).then(function () {
throw new Error('Delete all content is not denied without authentication.');
}).catch(function (error) {
error.errorType.should.eql('NoPermissionError');
});
});
it('export content is denied (editor, author, contributor & without authentication)', function () {
return dbAPI.exportContent(testUtils.context.editor).then(function () {
throw new Error('Export content is not denied for editor.');
}, function (error) {
error.errorType.should.eql('NoPermissionError');
return dbAPI.exportContent(testUtils.context.author);
}).then(function () {
throw new Error('Export content is not denied for author.');
}, function (error) {
error.errorType.should.eql('NoPermissionError');
return dbAPI.exportContent(testUtils.context.contributor);
}).then(function () {
throw new Error('Export content is not denied for contributor.');
}, function (error) {
error.errorType.should.eql('NoPermissionError');
return dbAPI.exportContent();
}).then(function () {
throw new Error('Export content is not denied without authentication.');
}).catch(function (error) {
error.errorType.should.eql('NoPermissionError');
});
});
it('import content is denied (editor, author, contributor & without authentication)', function () {
var file = {
originalname: 'myFile.json',
path: '/my/path/myFile.json',
mimetype: 'application/json'
};
return dbAPI.importContent(_.extend(testUtils.context.editor, file)).then(function () {
throw new Error('Import content is not denied for editor.');
}, function (error) {
error.errorType.should.eql('NoPermissionError');
return dbAPI.importContent(_.extend(testUtils.context.author, file));
}).then(function () {
throw new Error('Import content is not denied for author.');
}, function (error) {
error.errorType.should.eql('NoPermissionError');
return dbAPI.importContent(_.extend(testUtils.context.contributor, file));
}).then(function () {
throw new Error('Import content is not denied for contributor.');
}, function (error) {
error.errorType.should.eql('NoPermissionError');
return dbAPI.importContent(file);
}).then(function () {
throw new Error('Import content is not denied without authentication.');
}).catch(function (error) {
error.errorType.should.eql('NoPermissionError');
});
});
});

View File

@ -1,444 +0,0 @@
var should = require('should'),
sinon = require('sinon'),
testUtils = require('../../utils'),
_ = require('lodash'),
ObjectId = require('bson-objectid'),
Promise = require('bluebird'),
InvitesAPI = require('../../../server/api/v0.1/invites'),
mail = require('../../../server/api/v0.1/mail'),
common = require('../../../server/lib/common'),
context = testUtils.context,
sandbox = sinon.sandbox.create();
describe('Invites API', function () {
before(testUtils.teardown);
before(testUtils.setup('invites', 'settings', 'users:roles', 'perms:invite', 'perms:init'));
beforeEach(function () {
sandbox.stub(mail, 'send').callsFake(function () {
return Promise.resolve();
});
});
afterEach(function () {
sandbox.restore();
});
after(testUtils.teardown);
describe('CRUD', function () {
describe('Browse', function () {
it('browse invites', function (done) {
InvitesAPI.browse(testUtils.context.owner)
.then(function (response) {
response.invites.length.should.eql(2);
response.invites[0].status.should.eql('sent');
response.invites[0].email.should.eql('test1@ghost.org');
response.invites[0].role_id.should.eql(testUtils.roles.ids.admin);
response.invites[1].status.should.eql('sent');
response.invites[1].email.should.eql('test2@ghost.org');
response.invites[1].role_id.should.eql(testUtils.roles.ids.author);
should.not.exist(response.invites[0].token);
should.exist(response.invites[0].expires);
should.not.exist(response.invites[1].token);
should.exist(response.invites[1].expires);
done();
}).catch(done);
});
});
describe('Add', function () {
it('add invite 1', function (done) {
InvitesAPI.add({
invites: [{email: 'test@example.com', role_id: testUtils.roles.ids.editor}]
}, testUtils.context.owner)
.then(function (response) {
response.invites.length.should.eql(1);
response.invites[0].role_id.should.eql(testUtils.roles.ids.editor);
done();
}).catch(done);
});
it('add invite 2', function (done) {
InvitesAPI.add({
invites: [{email: 'test2@example.com', role_id: testUtils.roles.ids.author}]
}, testUtils.context.owner)
.then(function (response) {
response.invites.length.should.eql(1);
response.invites[0].role_id.should.eql(testUtils.roles.ids.author);
done();
}).catch(done);
});
it('add invite 3', function (done) {
InvitesAPI.add({
invites: [{email: 'test3@example.com', role_id: testUtils.roles.ids.contributor}]
}, testUtils.context.owner)
.then(function (response) {
response.invites.length.should.eql(1);
response.invites[0].role_id.should.eql(testUtils.roles.ids.contributor);
done();
}).catch(done);
});
it('add invite: empty invites object', function (done) {
InvitesAPI.add({invites: []}, testUtils.context.owner)
.then(function () {
throw new Error('expected validation error');
})
.catch(function (err) {
should.exist(err);
done();
});
});
it('add invite: no email provided', function (done) {
InvitesAPI.add({invites: [{status: 'sent'}]}, testUtils.context.owner)
.then(function () {
throw new Error('expected validation error');
})
.catch(function (err) {
(err instanceof common.errors.ValidationError).should.eql(true);
done();
});
});
it('add invite: invite existing user', function (done) {
InvitesAPI.add({
invites: [{
email: testUtils.DataGenerator.Content.users[0].email,
role_id: testUtils.roles.ids.author
}]
}, testUtils.context.owner)
.then(function () {
throw new Error('expected validation error');
})
.catch(function (err) {
(err instanceof common.errors.ValidationError).should.eql(true);
done();
});
});
});
describe('Read', function () {
it('read invites: not found', function (done) {
InvitesAPI.read(_.merge({}, testUtils.context.owner, {
email: 'not-existend@hey.org'
})).then(function () {
throw new Error('expected not found error for invite');
}).catch(function (err) {
(err instanceof common.errors.NotFoundError).should.eql(true);
done();
});
});
it('read invite', function (done) {
InvitesAPI.read(_.merge({}, {email: 'test1@ghost.org'}, testUtils.context.owner))
.then(function (response) {
response.invites.length.should.eql(1);
response.invites[0].role_id.should.eql(testUtils.roles.ids.admin);
done();
}).catch(done);
});
it('read invite', function (done) {
InvitesAPI.read(_.merge({}, testUtils.context.owner, {email: 'test2@ghost.org'}))
.then(function (response) {
response.invites.length.should.eql(1);
response.invites[0].role_id.should.eql(testUtils.roles.ids.author);
done();
}).catch(done);
});
});
describe('Destroy', function () {
it('destroy invite', function (done) {
InvitesAPI.destroy(_.merge({}, testUtils.context.owner, {id: testUtils.DataGenerator.forKnex.invites[0].id}))
.then(function () {
return InvitesAPI.read(_.merge({}, testUtils.context.owner, {
email: 'test1@ghost.org'
})).catch(function (err) {
(err instanceof common.errors.NotFoundError).should.eql(true);
done();
});
}).catch(done);
});
it('destroy invite: id does not exist', function (done) {
InvitesAPI.destroy(_.merge({id: ObjectId.generate()}, testUtils.context.owner))
.then(function () {
throw new Error('expect error on destroy invite');
})
.catch(function (err) {
(err instanceof common.errors.NotFoundError).should.eql(true);
done();
});
});
});
});
describe('Permissions', function () {
function checkForErrorType(type, done) {
return function checkForErrorType(error) {
if (error.errorType) {
error.errorType.should.eql(type);
done();
} else {
done(error);
}
};
}
function checkAddResponse(response) {
should.exist(response);
should.exist(response.invites);
should.not.exist(response.meta);
response.invites.should.have.length(1);
testUtils.API.checkResponse(response.invites[0], 'invites');
response.invites[0].created_at.should.be.an.instanceof(Date);
}
describe('Owner', function () {
it('CANNOT invite an Owner', function (done) {
InvitesAPI.add({
invites: [
{
email: 'test@example.com',
role_id: testUtils.roles.ids.owner
}
]
}, context.owner).then(function () {
done(new Error('Owner should not be able to add an owner'));
}).catch(checkForErrorType('NoPermissionError', done));
});
it('Can invite an Admin', function (done) {
InvitesAPI.add({
invites: [
{
email: 'test@example.com',
role_id: testUtils.roles.ids.admin
}
]
}, testUtils.context.owner).then(function (response) {
checkAddResponse(response);
response.invites[0].role_id.should.equal(testUtils.roles.ids.admin);
done();
}).catch(done);
});
it('Can invite an Editor', function (done) {
InvitesAPI.add({
invites: [
{
email: 'test@example.com',
role_id: testUtils.roles.ids.editor
}
]
}, testUtils.context.owner).then(function (response) {
checkAddResponse(response);
response.invites[0].role_id.should.equal(testUtils.roles.ids.editor);
done();
}).catch(done);
});
it('Can invite an Author', function (done) {
InvitesAPI.add({
invites: [
{
email: 'test@example.com',
role_id: testUtils.roles.ids.author
}
]
}, testUtils.context.owner).then(function (response) {
checkAddResponse(response);
response.invites[0].role_id.should.equal(testUtils.roles.ids.author);
done();
}).catch(done);
});
it('Can invite a Contributor', function (done) {
InvitesAPI.add({
invites: [
{
email: 'test@example.com',
role_id: testUtils.roles.ids.contributor
}
]
}, testUtils.context.owner).then(function (response) {
checkAddResponse(response);
response.invites[0].role_id.should.equal(testUtils.roles.ids.contributor);
done();
}).catch(done);
});
it('Can invite with role set as string', function (done) {
InvitesAPI.add({
invites: [
{
email: 'test@example.com',
role_id: testUtils.roles.ids.author.toString()
}
]
}, testUtils.context.owner).then(function (response) {
checkAddResponse(response);
response.invites[0].role_id.should.equal(testUtils.roles.ids.author);
done();
}).catch(done);
});
});
describe('Admin', function () {
it('CANNOT invite an Owner', function (done) {
InvitesAPI.add({
invites: [
{
email: 'test@example.com',
role_id: testUtils.roles.ids.owner
}
]
}, testUtils.context.admin).then(function () {
done(new Error('Admin should not be able to add an owner'));
}).catch(checkForErrorType('NoPermissionError', done));
});
it('Can invite an Admin', function (done) {
InvitesAPI.add({
invites: [
{
email: 'test@example.com',
role_id: testUtils.roles.ids.admin
}
]
}, _.merge({}, {include: 'roles'}, testUtils.context.admin)).then(function (response) {
checkAddResponse(response);
response.invites[0].role_id.should.equal(testUtils.roles.ids.admin);
done();
}).catch(done);
});
it('Can invite an Editor', function (done) {
InvitesAPI.add({
invites: [
{
email: 'test@example.com',
role_id: testUtils.roles.ids.editor
}
]
}, testUtils.context.admin).then(function (response) {
checkAddResponse(response);
response.invites[0].role_id.should.equal(testUtils.roles.ids.editor);
done();
}).catch(done);
});
it('Can invite an Author', function (done) {
InvitesAPI.add({
invites: [
{
email: 'test@example.com',
role_id: testUtils.roles.ids.author
}
]
}, testUtils.context.admin).then(function (response) {
checkAddResponse(response);
response.invites[0].role_id.should.equal(testUtils.roles.ids.author);
done();
}).catch(done);
});
it('Can invite a Contributor', function (done) {
InvitesAPI.add({
invites: [
{
email: 'test@example.com',
role_id: testUtils.roles.ids.contributor
}
]
}, testUtils.context.admin).then(function (response) {
checkAddResponse(response);
response.invites[0].role_id.should.equal(testUtils.roles.ids.contributor);
done();
}).catch(done);
});
});
describe('Editor', function () {
it('CANNOT invite an Owner', function (done) {
InvitesAPI.add({
invites: [
{
email: 'test@example.com',
role_id: testUtils.roles.ids.owner
}
]
}, context.editor).then(function () {
done(new Error('Editor should not be able to invite an owner'));
}).catch(checkForErrorType('NoPermissionError', done));
});
it('CANNOT invite an Adminstrator', function (done) {
InvitesAPI.add({
invites: [
{
email: 'test@example.com',
role_id: testUtils.roles.ids.admin
}
]
}, context.editor).then(function () {
done(new Error('Editor should not be able to invite an administrator'));
}).catch(checkForErrorType('NoPermissionError', done));
});
it('CANNOT invite an Editor', function (done) {
InvitesAPI.add({
invites: [
{
email: 'test@example.com',
role_id: testUtils.roles.ids.editor
}
]
}, context.editor).then(function () {
done(new Error('Editor should not be able to invite an editor'));
}).catch(checkForErrorType('NoPermissionError', done));
});
it('Can invite an Author', function (done) {
InvitesAPI.add({
invites: [
{
email: 'test@example.com',
role_id: testUtils.roles.ids.author
}
]
}, context.editor).then(function (response) {
checkAddResponse(response);
response.invites[0].role_id.should.equal(testUtils.roles.ids.author);
done();
}).catch(done);
});
it('Can invite a Contributor', function (done) {
InvitesAPI.add({
invites: [
{
email: 'test@example.com',
role_id: testUtils.roles.ids.contributor
}
]
}, context.editor).then(function (response) {
checkAddResponse(response);
response.invites[0].role_id.should.equal(testUtils.roles.ids.contributor);
done();
}).catch(done);
});
});
});
});

View File

@ -1,66 +0,0 @@
var should = require('should'),
testUtils = require('../../utils'),
_ = require('lodash'),
configUtils = require('../../utils/configUtils'),
common = require('../../../server/lib/common'),
mailData = {
mail: [{
message: {
to: 'joe@example.com',
subject: 'testemail',
html: '<p>This</p>'
},
options: {}
}]
};
common.i18n.init();
describe('Mail API', function () {
before(testUtils.teardown);
afterEach(testUtils.teardown);
beforeEach(testUtils.setup('perms:mail', 'perms:init'));
beforeEach(function () {
_.each(require.cache, function (value, key) {
if (key.match(/server\/api\/v0.1\/mail/)) {
delete require.cache[key];
}
});
require('../../../server/api/v0.1/mail');
});
afterEach(function () {
configUtils.restore();
});
it('returns a success', function (done) {
configUtils.set({mail: {transport: 'stub'}});
var MailAPI = require('../../../server/api/v0.1/mail');
MailAPI.send(mailData, testUtils.context.internal).then(function (response) {
should.exist(response.mail);
should.exist(response.mail[0].message);
should.exist(response.mail[0].status);
response.mail[0].message.subject.should.eql('testemail');
done();
}).catch(done);
});
it('returns a boo boo', function (done) {
configUtils.set({mail: {transport: 'stub', options: {error: 'Stub made a boo boo :('}}});
var MailAPI = require('../../../server/api/v0.1/mail');
MailAPI.send(mailData, testUtils.context.internal).then(function () {
done(new Error('Stub did not error'));
}).catch(function (error) {
error.stack.should.match(/Error: Stub made a boo boo/);
error.errorType.should.eql('EmailError');
done();
}).catch(done);
});
});

View File

@ -1,297 +0,0 @@
var should = require('should'),
_ = require('lodash'),
uuid = require('uuid'),
ObjectId = require('bson-objectid'),
testUtils = require('../../utils'),
models = require('../../../server/models'),
NotificationsAPI = require('../../../server/api/v0.1/notifications');
describe('Notifications API', function () {
// Keep the DB clean
before(testUtils.teardown);
after(testUtils.teardown);
before(testUtils.setup('settings', 'users:roles', 'perms:setting', 'perms:notification', 'perms:init'));
beforeEach(function () {
return models.Settings.edit({key: 'notifications', value: '[]'}, testUtils.context.internal);
});
should.exist(NotificationsAPI);
it('can add, adds defaults (internal)', function (done) {
var msg = {
type: 'info',
message: 'Hello, this is dog'
};
NotificationsAPI.add({notifications: [msg]}, testUtils.context.internal).then(function (result) {
var notification;
should.exist(result);
should.exist(result.notifications);
notification = result.notifications[0];
notification.dismissible.should.be.true();
should.exist(notification.location);
notification.location.should.equal('bottom');
done();
}).catch(done);
});
it('can add, adds defaults (owner)', function (done) {
var msg = {
type: 'info',
message: 'Hello, this is another dog'
};
NotificationsAPI.add({notifications: [msg]}, testUtils.context.owner).then(function (result) {
var notification;
should.exist(result);
should.exist(result.notifications);
notification = result.notifications[0];
notification.dismissible.should.be.true();
should.exist(notification.location);
notification.location.should.equal('bottom');
notification.id.should.be.a.String();
done();
}).catch(done);
});
it('can add, adds id and status (internal)', function (done) {
var msg = {
type: 'info',
message: 'Hello, this is dog number 3',
// id can't be passed from outside
id: ObjectId.generate()
};
NotificationsAPI.add({notifications: [msg]}, testUtils.context.internal).then(function (result) {
var notification;
should.exist(result);
should.exist(result.notifications);
notification = result.notifications[0];
notification.id.should.be.a.String();
should.exist(notification.status);
notification.status.should.equal('alert');
done();
}).catch(done);
});
it('duplicates', function (done) {
var customNotification1 = {
status: 'alert',
type: 'info',
location: 'test.to-be-deleted1',
custom: true,
id: uuid.v1(),
dismissible: true,
message: 'Hello, this is dog number 1'
};
NotificationsAPI
.add({notifications: [customNotification1]}, testUtils.context.internal)
.then(function () {
return NotificationsAPI.add({notifications: [customNotification1]}, testUtils.context.internal);
})
.then(function () {
return NotificationsAPI.browse(testUtils.context.internal);
})
.then(function (response) {
response.notifications.length.should.eql(1);
done();
})
.catch(done);
});
it('can browse (internal)', function (done) {
var msg = {
type: 'error', // this can be 'error', 'success', 'warn' and 'info'
message: 'This is an error', // A string. Should fit in one line.
custom: true
};
NotificationsAPI.add({notifications: [msg]}, testUtils.context.internal).then(function () {
NotificationsAPI.browse(testUtils.context.internal).then(function (results) {
should.exist(results);
should.exist(results.notifications);
results.notifications.length.should.be.above(0);
testUtils.API.checkResponse(results.notifications[0], 'notification');
done();
}).catch(done);
});
});
it('can browse (owner)', function (done) {
var msg = {
type: 'error', // this can be 'error', 'success', 'warn' and 'info'
message: 'This is an error', // A string. Should fit in one line.
custom: true
};
NotificationsAPI.add({notifications: [msg]}, testUtils.context.owner).then(function () {
NotificationsAPI.browse(testUtils.context.owner).then(function (results) {
should.exist(results);
should.exist(results.notifications);
results.notifications.length.should.be.above(0);
testUtils.API.checkResponse(results.notifications[0], 'notification');
done();
}).catch(done);
});
});
it('receive correct order', function (done) {
var customNotification1 = {
status: 'alert',
type: 'info',
custom: true,
id: uuid.v1(),
dismissible: true,
message: '1'
}, customNotification2 = {
status: 'alert',
type: 'info',
custom: true,
id: uuid.v1(),
dismissible: true,
message: '2'
};
NotificationsAPI
.add({notifications: [customNotification1]}, testUtils.context.internal)
.then(function () {
return NotificationsAPI.add({notifications: [customNotification2]}, testUtils.context.internal);
})
.then(function () {
return NotificationsAPI.browse(testUtils.context.internal);
})
.then(function (response) {
response.notifications.length.should.eql(2);
response.notifications[0].message.should.eql('2');
response.notifications[1].message.should.eql('1');
done();
})
.catch(done);
});
it('can destroy (internal)', function (done) {
var msg = {
type: 'error',
message: 'Goodbye, cruel world!'
};
NotificationsAPI.add({notifications: [msg]}, testUtils.context.internal).then(function (result) {
var notification = result.notifications[0];
NotificationsAPI
.destroy(_.extend({}, testUtils.context.internal, {id: notification.id}))
.then(function (result) {
should.not.exist(result);
done();
})
.catch(done);
});
});
it('can destroy (owner)', function (done) {
var msg = {
type: 'error',
message: 'Goodbye, cruel world!'
};
NotificationsAPI.add({notifications: [msg]}, testUtils.context.internal).then(function (result) {
var notification = result.notifications[0];
NotificationsAPI
.destroy(_.extend({}, testUtils.context.owner, {id: notification.id}))
.then(function (result) {
should.not.exist(result);
done();
})
.catch(done);
});
});
it('ensure notification get\'s removed', function (done) {
var customNotification = {
status: 'alert',
type: 'info',
location: 'test.to-be-deleted',
custom: true,
id: uuid.v1(),
dismissible: true,
message: 'Hello, this is dog number 4'
};
NotificationsAPI.add({notifications: [customNotification]}, testUtils.context.internal).then(function (result) {
var notification = result.notifications[0];
return NotificationsAPI.browse(testUtils.context.internal)
.then(function (response) {
response.notifications.length.should.eql(1);
return NotificationsAPI.destroy(_.extend({}, testUtils.context.internal, {id: notification.id}));
})
.then(function () {
return NotificationsAPI.browse(testUtils.context.internal);
})
.then(function (response) {
response.notifications.length.should.eql(0);
done();
})
.catch(done);
});
});
it('destroy unknown id', function (done) {
NotificationsAPI
.destroy(_.extend({}, testUtils.context.internal, {id: 1}))
.then(function () {
done(new Error('Expected notification error.'));
})
.catch(function (err) {
err.statusCode.should.eql(404);
done();
});
});
it('destroy all', function (done) {
var customNotification1 = {
status: 'alert',
type: 'info',
location: 'test.to-be-deleted1',
custom: true,
id: uuid.v1(),
dismissible: true,
message: 'Hello, this is dog number 1'
}, customNotification2 = {
status: 'alert',
type: 'info',
location: 'test.to-be-deleted2',
custom: true,
id: uuid.v1(),
dismissible: true,
message: 'Hello, this is dog number 2'
};
NotificationsAPI
.add({notifications: [customNotification1]}, testUtils.context.internal)
.then(function () {
return NotificationsAPI.add({notifications: [customNotification2]}, testUtils.context.internal);
})
.then(function () {
return NotificationsAPI.destroyAll(testUtils.context.internal);
})
.then(function () {
return NotificationsAPI.browse(testUtils.context.internal);
})
.then(function (response) {
response.notifications.length.should.eql(0);
done();
})
.catch(done);
});
});

View File

@ -1,713 +0,0 @@
var should = require('should'),
sinon = require('sinon'),
testUtils = require('../../utils'),
_ = require('lodash'),
moment = require('moment'),
ObjectId = require('bson-objectid'),
configUtils = require('../../utils/configUtils'),
common = require('../../../server/lib/common'),
PostAPI = require('../../../server/api/v0.1/posts'),
urlService = require('../../../server/services/url'),
settingsCache = require('../../../server/services/settings/cache'),
sandbox = sinon.sandbox.create();
describe('Post API', function () {
var localSettingsCache = {};
before(testUtils.teardown);
after(testUtils.teardown);
before(testUtils.setup('users:roles', 'perms:post', 'perms:init', 'posts'));
beforeEach(function () {
sandbox.stub(settingsCache, 'get').callsFake(function (key) {
return localSettingsCache[key];
});
});
afterEach(function () {
sandbox.restore();
localSettingsCache = {};
});
should.exist(PostAPI);
describe('Browse', function () {
beforeEach(function () {
localSettingsCache.permalinks = '/:slug/';
});
afterEach(function () {
configUtils.restore();
});
it('can fetch all posts with internal context in correct order', function () {
return PostAPI.browse({context: {internal: true}}).then(function (results) {
should.exist(results.posts);
results.posts.length.should.eql(8);
results.posts[0].status.should.eql('scheduled');
results.posts[1].status.should.eql('draft');
results.posts[2].status.should.eql('draft');
results.posts[3].status.should.eql('published');
results.posts[4].status.should.eql('published');
results.posts[5].status.should.eql('published');
results.posts[6].status.should.eql('published');
results.posts[7].status.should.eql('published');
});
});
it('can fetch featured posts for user 1', function () {
return PostAPI.browse(_.merge({filter: 'featured:true'}, testUtils.context.owner)).then(function (results) {
should.exist(results.posts);
results.posts.length.should.eql(2);
results.posts[0].featured.should.eql(true);
});
});
it('can fetch featured posts for user 2', function () {
return PostAPI.browse(_.merge({filter: 'featured:true'}, testUtils.context.admin)).then(function (results) {
should.exist(results.posts);
results.posts.length.should.eql(2);
results.posts[0].featured.should.eql(true);
});
});
it('can exclude featured posts for user 1', function () {
return PostAPI.browse(_.merge({
status: 'all',
filter: 'featured:false'
}, testUtils.context.owner)).then(function (results) {
should.exist(results.posts);
results.posts.length.should.eql(4);
results.posts[0].featured.should.eql(false);
});
});
it('can limit the number of posts', function () {
return PostAPI.browse({context: {user: 1}, status: 'all', limit: 3}).then(function (results) {
should.exist(results.posts);
results.posts.length.should.eql(3);
results.meta.pagination.limit.should.eql(3);
});
});
it('can fetch only static posts', function () {
return PostAPI.browse({context: {user: 1}, staticPages: true}).then(function (results) {
should.exist(results.posts);
results.posts.length.should.eql(1);
results.posts[0].page.should.eql(true);
});
});
it('can fetch only static posts with string \'true\'', function () {
return PostAPI.browse({context: {user: 1}, staticPages: 'true'}).then(function (results) {
should.exist(results.posts);
results.posts.length.should.eql(1);
results.posts[0].page.should.eql(true);
});
});
it('can fetch only static posts with string \'1\'', function () {
return PostAPI.browse({context: {user: 1}, staticPages: '1'}).then(function (results) {
should.exist(results.posts);
results.posts.length.should.eql(1);
results.posts[0].page.should.eql(true);
});
});
it('can exclude static posts', function () {
return PostAPI.browse({context: {user: 1}, staticPages: false}).then(function (results) {
should.exist(results.posts);
results.posts.length.should.eql(4);
results.posts[0].page.should.eql(false);
});
});
it('can fetch static and normal posts', function () {
return PostAPI.browse({context: {user: 1}, staticPages: 'all'}).then(function (results) {
should.exist(results.posts);
results.posts.length.should.eql(5);
});
});
it('can fetch static and normal posts (filter version)', function () {
return PostAPI.browse({context: {user: 1}, filter: 'page:[false,true]'}).then(function (results) {
// should be the same as the current staticPages: 'all'
should.exist(results.posts);
results.posts.length.should.eql(5);
});
});
it('can fetch page 1', function () {
return PostAPI.browse({context: {user: 1}, page: 1, limit: 2, status: 'all'}).then(function (results) {
should.exist(results.posts);
results.posts.length.should.eql(2);
results.posts[0].slug.should.eql('scheduled-post');
results.posts[1].slug.should.eql('unfinished');
results.meta.pagination.page.should.eql(1);
results.meta.pagination.next.should.eql(2);
});
});
it('can fetch page 2', function () {
return PostAPI.browse({context: {user: 1}, page: 2, limit: 2, status: 'all'}).then(function (results) {
should.exist(results.posts);
results.posts.length.should.eql(2);
results.posts[0].slug.should.eql('not-so-short-bit-complex');
results.posts[1].slug.should.eql('short-and-sweet');
results.meta.pagination.page.should.eql(2);
results.meta.pagination.next.should.eql(3);
results.meta.pagination.prev.should.eql(1);
});
});
it('without context.user cannot fetch all posts', function () {
return PostAPI.browse({status: 'all'}).then(function (results) {
should.not.exist(results);
throw new Error('should not provide results if invalid status provided');
}).catch(function (err) {
err.errorType.should.eql('NoPermissionError');
});
});
it('without context.user cannot fetch draft posts', function () {
return PostAPI.browse({status: 'draft'}).then(function (results) {
should.not.exist(results);
throw new Error('should not provide results if invalid status provided');
}).catch(function (err) {
err.errorType.should.eql('NoPermissionError');
});
});
it('without context.user cannot use uuid to fetch draft posts in browse', function () {
return PostAPI.browse({status: 'draft', uuid: 'imastring'}).then(function (results) {
should.not.exist(results);
throw new Error('should not provide results if invalid status provided');
}).catch(function (err) {
err.errorType.should.eql('NoPermissionError');
});
});
it('with context.user can fetch drafts', function () {
return PostAPI.browse({context: {user: 1}, status: 'draft'}).then(function (results) {
should.exist(results);
testUtils.API.checkResponse(results, 'posts');
should.exist(results.posts);
results.posts.length.should.eql(1);
results.posts[0].status.should.eql('draft');
testUtils.API.checkResponse(results.posts[0], 'post');
});
});
it('with context.user can fetch all posts', function () {
return PostAPI.browse({context: {user: 1}, status: 'all'}).then(function (results) {
should.exist(results);
testUtils.API.checkResponse(results, 'posts');
should.exist(results.posts);
// DataGenerator creates 6 posts by default + 2 static pages
results.posts.length.should.eql(6);
testUtils.API.checkResponse(results.posts[0], 'post');
});
});
it('can include tags', function () {
return PostAPI.browse({context: {user: 1}, status: 'all', include: 'tags'}).then(function (results) {
results.posts[0].tags.length.should.eql(0);
results.posts[1].tags.length.should.eql(1);
results.posts[1].tags[0].name.should.eql('pollo');
});
});
it('[DEPRECATED] can include author (using status:all)', function () {
return PostAPI.browse({context: {user: 1}, status: 'all', include: 'author'}).then(function (results) {
should.exist(results.posts);
should.exist(results.posts[0].author.name);
results.posts[0].author.name.should.eql('Joe Bloggs');
});
});
it('[DEPRECATED] can include author', function () {
return PostAPI.read({
context: {user: testUtils.DataGenerator.Content.users[1].id},
id: testUtils.DataGenerator.Content.posts[1].id,
include: 'author'
}).then(function (results) {
should.exist(results.posts[0].author.name);
results.posts[0].author.name.should.eql('Joe Bloggs');
});
});
it('can include authors', function () {
return PostAPI.browse({context: {user: 1}, status: 'all', include: 'authors'}).then(function (results) {
should.exist(results.posts);
should.exist(results.posts[0].authors);
should.exist(results.posts[0].authors[0]);
results.posts[0].authors[0].name.should.eql('Joe Bloggs');
});
});
it('can fetch all posts for a tag', function () {
return PostAPI.browse({
context: {user: 1},
status: 'all',
filter: 'tags:kitchen-sink',
include: 'tags'
}).then(function (results) {
results.posts.length.should.be.eql(2);
_.each(results.posts, function (post) {
var slugs = _.map(post.tags, 'slug');
slugs.should.containEql('kitchen-sink');
});
});
});
it('can include authors and be case insensitive', function () {
return PostAPI.browse({context: {user: 1}, status: 'all', include: 'Authors'}).then(function (results) {
should.exist(results.posts);
should.exist(results.posts[0].authors);
should.exist(results.posts[0].authors[0]);
results.posts[0].authors[0].name.should.eql('Joe Bloggs');
});
});
it('can include authors and ignore space in include', function () {
return PostAPI.browse({context: {user: 1}, status: 'all', include: ' authors'}).then(function (results) {
should.exist(results.posts);
should.exist(results.posts[0].authors);
should.exist(results.posts[0].authors[0]);
results.posts[0].authors[0].name.should.eql('Joe Bloggs');
});
});
it('[DEPRECATED] can fetch all posts for an author', function () {
return PostAPI.browse({
context: {user: 1},
status: 'all',
filter: 'author:joe-bloggs',
include: 'author'
}).then(function (results) {
should.exist(results.posts);
results.posts.length.should.eql(6);
_.each(results.posts, function (post) {
post.author.slug.should.eql('joe-bloggs');
});
});
});
it('can fetch all posts for an author', function () {
return PostAPI.browse({
context: {user: 1},
status: 'all',
filter: 'authors:joe-bloggs',
include: 'authors'
}).then(function (results) {
should.exist(results.posts);
results.posts.length.should.eql(6);
_.each(results.posts, function (post) {
post.primary_author.slug.should.eql('joe-bloggs');
});
_.find(results.posts, {id: testUtils.DataGenerator.forKnex.posts[0].id}).authors.length.should.eql(1);
_.find(results.posts, {id: testUtils.DataGenerator.forKnex.posts[3].id}).authors.length.should.eql(2);
});
});
// @TODO: ensure filters are fully validated
it.skip('cannot fetch all posts for a tag with invalid slug', function () {
return PostAPI.browse({filter: 'tags:invalid!'}).then(function () {
throw new Error('Should not return a result with invalid tag');
}).catch(function (err) {
should.exist(err);
err.message.should.eql('Validation (isSlug) failed for tag');
err.statusCode.should.eql(422);
});
});
it.skip('cannot fetch all posts for an author with invalid slug', function () {
return PostAPI.browse({filter: 'authors:invalid!'}).then(function () {
throw new Error('Should not return a result with invalid author');
}).catch(function (err) {
should.exist(err);
err.message.should.eql('Validation (isSlug) failed for author');
err.statusCode.should.eql(422);
});
});
it('with context.user can fetch a single field', function () {
return PostAPI.browse({context: {user: 1}, status: 'all', limit: 5, fields: 'title'}).then(function (results) {
should.exist(results.posts);
should.exist(results.posts[0].title);
should.not.exist(results.posts[0].slug);
});
});
it('with context.user can fetch multiple fields', function () {
return PostAPI.browse({
context: {user: 1},
status: 'all',
limit: 5,
fields: 'slug,published_at'
}).then(function (results) {
should.exist(results.posts);
should.exist(results.posts[0].published_at);
should.exist(results.posts[0].slug);
should.not.exist(results.posts[0].title);
});
});
it('with context.user can fetch url and author fields', function () {
sandbox.stub(urlService, 'getUrlByResourceId').withArgs(testUtils.DataGenerator.Content.posts[7].id).returns('/html-ipsum/');
return PostAPI.browse({context: {user: 1}, status: 'all', limit: 5}).then(function (results) {
should.exist(results.posts);
should.exist(results.posts[0].url);
should.notEqual(results.posts[0].url, 'undefined');
should.exist(results.posts[0].author);
});
});
it('with context.user can fetch multiple fields and be case insensitive', function () {
return PostAPI.browse({
context: {user: 1},
status: 'all',
limit: 5,
fields: 'Slug,Published_At'
}).then(function (results) {
should.exist(results.posts);
should.exist(results.posts[0].published_at);
should.exist(results.posts[0].slug);
should.not.exist(results.posts[0].title);
});
});
it('with context.user can fetch multiple fields ignoring spaces', function () {
return PostAPI.browse({
context: {user: 1},
status: 'all',
limit: 5,
fields: ' slug , published_at '
}).then(function (results) {
should.exist(results.posts);
should.exist(results.posts[0].published_at);
should.exist(results.posts[0].slug);
should.not.exist(results.posts[0].title);
});
});
it('with context.user can fetch a field and not return invalid field', function () {
return PostAPI.browse({context: {user: 1}, status: 'all', limit: 5, fields: 'foo,title'})
.then(function (results) {
var objectKeys;
should.exist(results.posts);
should.exist(results.posts[0].title);
should.not.exist(results.posts[0].foo);
objectKeys = _.keys(results.posts[0]);
objectKeys.length.should.eql(1);
});
});
it('can order posts using asc', function () {
var posts, expectedTitles;
posts = _(testUtils.DataGenerator.Content.posts).reject('page').value();
expectedTitles = _(posts).map('title').sortBy().value();
return PostAPI.browse({
context: {user: 1},
status: 'all',
order: 'title asc',
fields: 'title'
}).then(function (results) {
should.exist(results.posts);
var titles = _.map(results.posts, 'title');
titles.should.eql(expectedTitles);
});
});
it('can order posts using desc', function () {
var posts, expectedTitles;
posts = _(testUtils.DataGenerator.Content.posts).reject('page').value();
expectedTitles = _(posts).map('title').sortBy().reverse().value();
return PostAPI.browse({
context: {user: 1},
status: 'all',
order: 'title DESC',
fields: 'title'
}).then(function (results) {
should.exist(results.posts);
var titles = _.map(results.posts, 'title');
titles.should.eql(expectedTitles);
});
});
it('can order posts and filter disallowed attributes', function () {
var posts, expectedTitles;
posts = _(testUtils.DataGenerator.Content.posts).reject('page').value();
expectedTitles = _(posts).map('title').sortBy().value();
return PostAPI.browse({
context: {user: 1},
status: 'all',
order: 'bunny DESC, title ASC',
fields: 'title'
}).then(function (results) {
should.exist(results.posts);
var titles = _.map(results.posts, 'title');
titles.should.eql(expectedTitles);
});
});
it('can fetch all posts with correct order when unpublished draft is present', function () {
return testUtils.fixtures.insertPosts([{
id: ObjectId.generate(),
title: 'Not published draft post',
slug: 'not-published-draft-post',
status: 'draft',
updated_at: moment().add(3, 'minutes').toDate(),
published_at: null
},
{
id: ObjectId.generate(),
title: 'Unpublished post',
slug: 'unpublished-post',
status: 'draft',
updated_at: moment().add(2, 'minutes').toDate(),
published_at: moment().add(1, 'minutes').toDate()
}])
.then(function () {
return PostAPI.browse({context: {internal: true}});
})
.then(function (results) {
should.exist(results.posts);
results.posts.length.should.eql(10);
results.posts[1].slug.should.eql('not-published-draft-post');
results.posts[2].slug.should.eql('unpublished-post');
results.posts[0].status.should.eql('scheduled');
results.posts[1].status.should.eql('draft');
results.posts[2].status.should.eql('draft');
results.posts[3].status.should.eql('draft');
results.posts[4].status.should.eql('draft');
results.posts[5].status.should.eql('published');
results.posts[6].status.should.eql('published');
results.posts[7].status.should.eql('published');
results.posts[8].status.should.eql('published');
results.posts[9].status.should.eql('published');
});
});
});
describe('Read', function () {
it('can fetch a post', function () {
var firstPost;
return PostAPI.browse().then(function (results) {
should.exist(results);
should.exist(results.posts);
results.posts.length.should.be.above(0);
firstPost = _.find(results.posts, {title: testUtils.DataGenerator.Content.posts[0].title});
return PostAPI.read({slug: firstPost.slug, include: 'tags'});
}).then(function (found) {
var post;
should.exist(found);
testUtils.API.checkResponse(found.posts[0], 'post', 'tags');
post = found.posts[0];
post.created_at.should.be.an.instanceof(Date);
should.exist(post.tags);
post.tags.length.should.be.above(0);
testUtils.API.checkResponse(post.tags[0], 'tag');
});
});
it('without context.user cannot fetch draft', function () {
return PostAPI.read({slug: 'unfinished', status: 'draft'}).then(function () {
throw new Error('Should not return a result with no permission');
}).catch(function (err) {
should.exist(err);
err.errorType.should.eql('NoPermissionError');
});
});
it('with context.user can fetch a draft', function () {
return PostAPI.read({context: {user: 1}, slug: 'unfinished', status: 'draft'}).then(function (results) {
should.exist(results.posts);
results.posts[0].status.should.eql('draft');
});
});
it('without context.user can fetch a draft if uuid is provided', function () {
return PostAPI.read({uuid: 'd52c42ae-2755-455c-80ec-70b2ec55c903', status: 'draft'}).then(function (results) {
should.exist(results.posts);
results.posts[0].slug.should.eql('unfinished');
});
});
it('cannot fetch post with unknown id', function () {
return PostAPI.read({context: {user: 1}, slug: 'not-a-post'}).then(function () {
throw new Error('Should not return a result with unknown id');
}).catch(function (err) {
should.exist(err);
err.message.should.eql('Post not found.');
});
});
it('can fetch post with by id', function () {
return PostAPI.read({
context: {user: testUtils.DataGenerator.Content.users[1].id},
id: testUtils.DataGenerator.Content.posts[1].id,
status: 'all'
}).then(function (results) {
should.exist(results.posts);
results.posts[0].id.should.eql(testUtils.DataGenerator.Content.posts[1].id);
results.posts[0].slug.should.eql('ghostly-kitchen-sink');
});
});
it('can fetch post returning a slug only permalink', function () {
sandbox.stub(urlService, 'getUrlByResourceId').withArgs(testUtils.DataGenerator.Content.posts[0].id).returns('/html-ipsum/');
return PostAPI.read({
id: testUtils.DataGenerator.Content.posts[0].id
})
.then(function (result) {
should.exist(result);
result.posts[0].url.should.equal('/html-ipsum/');
});
});
it('can fetch post returning a dated permalink', function () {
sandbox.stub(urlService, 'getUrlByResourceId').withArgs(testUtils.DataGenerator.Content.posts[0].id).returns('/2015/01/01/html-ipsum/');
return PostAPI.read({
id: testUtils.DataGenerator.Content.posts[0].id
})
.then(function (result) {
should.exist(result);
// published_at of post 1 is 2015-01-01 00:00:00
// default blog TZ is UTC
result.posts[0].url.should.equal('/2015/01/01/html-ipsum/');
});
});
it('can include tags', function () {
return PostAPI.read({
context: {user: testUtils.DataGenerator.Content.users[1].id},
id: testUtils.DataGenerator.Content.posts[2].id,
include: 'tags'
}).then(function (results) {
should.exist(results.posts[0].tags);
results.posts[0].tags[0].slug.should.eql('chorizo');
});
});
// TODO: this should be a 422?
it('cannot fetch a post with an invalid slug', function () {
return PostAPI.read({slug: 'invalid!'}).then(function () {
throw new Error('Should not return a result with invalid slug');
}).catch(function (err) {
should.exist(err);
err.message.should.eql('Post not found.');
});
});
});
describe('Destroy', function () {
beforeEach(testUtils.teardown);
beforeEach(testUtils.setup('users:roles', 'perms:post', 'perms:init', 'posts'));
after(testUtils.teardown);
it('can delete a post', function () {
var options = {
context: {user: testUtils.DataGenerator.Content.users[1].id},
id: testUtils.DataGenerator.Content.posts[0].id
};
PostAPI.read(options).then(function (results) {
should.exist(results.posts[0]);
return PostAPI.destroy(options);
}).then(function (results) {
should.not.exist(results);
return PostAPI.read(options);
}).then(function () {
throw new Error('Post still exists when it should have been deleted');
}).catch(function (error) {
error.errorType.should.eql('NotFoundError');
});
});
it('returns an error when attempting to delete a non-existent post', function () {
var options = {context: {user: testUtils.DataGenerator.Content.users[1].id}, id: ObjectId.generate()};
return PostAPI.destroy(options).then(function () {
throw new Error('No error was thrown');
}).catch(function (error) {
error.errorType.should.eql('NotFoundError');
});
});
});
describe('Edit', function () {
beforeEach(testUtils.teardown);
beforeEach(testUtils.setup('users:roles', 'perms:post', 'perms:init', 'posts'));
after(testUtils.teardown);
it('can edit own post', function () {
return PostAPI.edit({posts: [{status: 'test'}]}, {
context: {user: testUtils.DataGenerator.Content.users[1].id},
id: testUtils.DataGenerator.Content.posts[0].id
}).then(function (results) {
should.exist(results.posts);
});
});
it('cannot edit others post', function () {
return PostAPI.edit(
{posts: [{status: 'test'}]},
{
context: {user: testUtils.DataGenerator.Content.users[3].id},
id: testUtils.DataGenerator.Content.posts[0].id
}
).then(function () {
throw new Error('expected permission error');
}).catch(function (err) {
should.exist(err);
(err instanceof common.errors.NoPermissionError).should.eql(true);
});
});
});
});

View File

@ -1,144 +0,0 @@
var should = require('should'),
testUtils = require('../../utils'),
_ = require('lodash'),
RoleAPI = require('../../../server/api/v0.1/roles'),
context = testUtils.context;
describe('Roles API', function () {
// Keep the DB clean
before(testUtils.teardown);
after(testUtils.teardown);
before(testUtils.setup('users:roles', 'perms:role', 'perms:init'));
describe('Browse', function () {
function checkBrowseResponse(response) {
should.exist(response);
testUtils.API.checkResponse(response, 'roles');
should.exist(response.roles);
response.roles.should.have.length(6);
testUtils.API.checkResponse(response.roles[0], 'role');
testUtils.API.checkResponse(response.roles[1], 'role');
testUtils.API.checkResponse(response.roles[2], 'role');
testUtils.API.checkResponse(response.roles[3], 'role');
testUtils.API.checkResponse(response.roles[4], 'role');
testUtils.API.checkResponse(response.roles[5], 'role');
}
it('Owner can browse', function (done) {
RoleAPI.browse(context.owner).then(function (response) {
checkBrowseResponse(response);
done();
}).catch(done);
});
it('Admin can browse', function (done) {
RoleAPI.browse(context.admin).then(function (response) {
checkBrowseResponse(response);
done();
}).catch(done);
});
it('Editor can browse', function (done) {
RoleAPI.browse(context.editor).then(function (response) {
checkBrowseResponse(response);
done();
}).catch(done);
});
it('Author can browse', function (done) {
RoleAPI.browse(context.author).then(function (response) {
checkBrowseResponse(response);
done();
}).catch(done);
});
it('Contributor can browse', function (done) {
RoleAPI.browse(context.contributor).then(function (response) {
checkBrowseResponse(response);
done();
}).catch(done);
});
it('No-auth CANNOT browse', function (done) {
RoleAPI.browse().then(function () {
done(new Error('Browse roles is not denied without authentication.'));
}, function () {
done();
}).catch(done);
});
});
describe('Browse permissions=assign', function () {
function checkBrowseResponse(response) {
should.exist(response);
should.exist(response.roles);
testUtils.API.checkResponse(response, 'roles');
response.roles.should.have.length(4);
testUtils.API.checkResponse(response.roles[0], 'role');
testUtils.API.checkResponse(response.roles[1], 'role');
testUtils.API.checkResponse(response.roles[2], 'role');
testUtils.API.checkResponse(response.roles[3], 'role');
response.roles[0].name.should.equal('Administrator');
response.roles[1].name.should.equal('Editor');
response.roles[2].name.should.equal('Author');
response.roles[3].name.should.equal('Contributor');
}
it('Owner can assign all', function (done) {
RoleAPI.browse(_.extend({}, context.owner, {permissions: 'assign'})).then(function (response) {
checkBrowseResponse(response);
done();
}).catch(done);
});
it('Admin can assign all', function (done) {
RoleAPI.browse(_.extend({}, context.admin, {permissions: 'assign'})).then(function (response) {
checkBrowseResponse(response);
done();
}).catch(done);
});
it('Editor can assign Author & Contributor', function (done) {
RoleAPI.browse(_.extend({}, context.editor, {permissions: 'assign'})).then(function (response) {
should.exist(response);
should.exist(response.roles);
testUtils.API.checkResponse(response, 'roles');
response.roles.should.have.length(2);
testUtils.API.checkResponse(response.roles[0], 'role');
testUtils.API.checkResponse(response.roles[1], 'role');
response.roles[0].name.should.equal('Author');
response.roles[1].name.should.equal('Contributor');
done();
}).catch(done);
});
it('Author CANNOT assign any', function (done) {
RoleAPI.browse(_.extend({}, context.author, {permissions: 'assign'})).then(function (response) {
should.exist(response);
should.exist(response.roles);
testUtils.API.checkResponse(response, 'roles');
response.roles.should.have.length(0);
done();
}).catch(done);
});
it('Contributor CANNOT assign any', function (done) {
RoleAPI.browse(_.extend({}, context.contributor, {permissions: 'assign'})).then(function (response) {
should.exist(response);
should.exist(response.roles);
testUtils.API.checkResponse(response, 'roles');
response.roles.should.have.length(0);
done();
}).catch(done);
});
it('No-auth CANNOT browse', function (done) {
RoleAPI.browse({permissions: 'assign'}).then(function () {
done(new Error('Browse roles is not denied without authentication.'));
}, function () {
done();
}).catch(done);
});
});
});

View File

@ -1,487 +0,0 @@
var should = require('should'),
sinon = require('sinon'),
moment = require('moment'),
Promise = require('bluebird'),
ObjectId = require('bson-objectid'),
testUtils = require('../../utils'),
config = require('../../../server/config'),
sequence = require('../../../server/lib/promise/sequence'),
common = require('../../../server/lib/common'),
api = require('../../../server/api'),
models = require('../../../server/models'),
sandbox = sinon.sandbox.create();
describe('Schedules API', function () {
var scope = {posts: []};
after(testUtils.teardown);
describe('fn: getScheduledPosts', function () {
before(function (done) {
sequence([
testUtils.teardown,
testUtils.setup('clients', 'users:roles', 'perms:post', 'perms:init')
]).then(function () {
done();
}).catch(done);
});
describe('success', function () {
before(function (done) {
scope.posts.push(testUtils.DataGenerator.forKnex.createPost({
created_by: testUtils.users.ids.editor,
author_id: testUtils.users.ids.editor,
published_by: testUtils.users.ids.editor,
created_at: moment().add(2, 'days').set('hours', 8).toDate(),
published_at: moment().add(5, 'days').toDate(),
status: 'scheduled',
slug: '2'
}));
scope.posts.push(testUtils.DataGenerator.forKnex.createPost({
created_by: testUtils.users.ids.owner,
author_id: testUtils.users.ids.owner,
published_by: testUtils.users.ids.owner,
created_at: moment().add(2, 'days').set('hours', 12).toDate(),
published_at: moment().add(5, 'days').toDate(),
status: 'scheduled',
page: 1,
slug: '5'
}));
scope.posts.push(testUtils.DataGenerator.forKnex.createPost({
created_by: testUtils.users.ids.author,
author_id: testUtils.users.ids.author,
published_by: testUtils.users.ids.author,
created_at: moment().add(5, 'days').set('hours', 6).toDate(),
published_at: moment().add(10, 'days').toDate(),
status: 'scheduled',
slug: '1'
}));
scope.posts.push(testUtils.DataGenerator.forKnex.createPost({
created_by: testUtils.users.ids.owner,
author_id: testUtils.users.ids.owner,
published_by: testUtils.users.ids.owner,
created_at: moment().add(6, 'days').set('hours', 10).set('minutes', 0).toDate(),
published_at: moment().add(7, 'days').toDate(),
status: 'scheduled',
slug: '3'
}));
scope.posts.push(testUtils.DataGenerator.forKnex.createPost({
created_by: testUtils.users.ids.owner,
author_id: testUtils.users.ids.owner,
published_by: testUtils.users.ids.owner,
created_at: moment().add(6, 'days').set('hours', 11).toDate(),
published_at: moment().add(8, 'days').toDate(),
status: 'scheduled',
slug: '4'
}));
scope.posts.push(testUtils.DataGenerator.forKnex.createPost({
created_by: testUtils.users.ids.owner,
author_id: testUtils.users.ids.owner,
published_by: testUtils.users.ids.owner,
status: 'draft',
slug: '6'
}));
Promise.all(scope.posts.map(function (post) {
return models.Post.add(post, {context: {internal: true}, importing: true});
})).then(function () {
return done();
}).catch(done);
});
after(function () {
scope.posts = [];
});
it('all', function (done) {
api.schedules.getScheduledPosts()
.then(function (result) {
result.posts.length.should.eql(5);
testUtils.API.checkResponse(result, 'posts', null, ['meta']);
done();
})
.catch(done);
});
it('for specific datetime', function (done) {
api.schedules.getScheduledPosts({
from: moment().add(2, 'days').startOf('day').toDate(),
to: moment().add(2, 'days').endOf('day').toDate()
}).then(function (result) {
result.posts.length.should.eql(2);
done();
}).catch(done);
});
it('for specific datetime', function (done) {
api.schedules.getScheduledPosts({
from: moment().add(2, 'days').startOf('day').toDate(),
to: moment().add(2, 'days').set('hours', 8).toDate()
}).then(function (result) {
result.posts.length.should.eql(1);
done();
}).catch(done);
});
it('for specific date', function (done) {
api.schedules.getScheduledPosts({
from: moment().add(5, 'days').startOf('day').toDate(),
to: moment().add(6, 'days').endOf('day').toDate()
}).then(function (result) {
result.posts.length.should.eql(3);
done();
}).catch(done);
});
it('for specific date', function (done) {
api.schedules.getScheduledPosts({
from: moment().add(6, 'days').set('hours', 10).set('minutes', 30).toDate(),
to: moment().add(6, 'days').endOf('day').toDate()
}).then(function (result) {
result.posts.length.should.eql(1);
done();
}).catch(done);
});
it('for specific date', function (done) {
api.schedules.getScheduledPosts({
from: moment().add(1, 'days').toDate()
}).then(function (result) {
result.posts.length.should.eql(5);
done();
}).catch(done);
});
});
describe('error', function () {
it('from is invalid', function (done) {
api.schedules.getScheduledPosts({
from: 'bee'
}).catch(function (err) {
should.exist(err);
(err instanceof common.errors.ValidationError).should.eql(true);
done();
});
});
});
});
describe('fn: publishPost', function () {
before(function (done) {
sequence([
testUtils.teardown,
testUtils.setup('clients', 'users:roles', 'perms:post', 'perms:init')
]).then(function () {
done();
}).catch(done);
});
afterEach(function () {
sandbox.restore();
});
describe('success', function () {
beforeEach(function (done) {
scope.posts.push(testUtils.DataGenerator.forKnex.createPost({
created_by: testUtils.users.ids.author,
author_id: testUtils.users.ids.author,
published_by: testUtils.users.ids.author,
published_at: moment().toDate(),
status: 'scheduled',
title: 'title',
slug: 'first'
}));
scope.posts.push(testUtils.DataGenerator.forKnex.createPost({
created_by: testUtils.users.ids.author,
author_id: testUtils.users.ids.author,
published_by: testUtils.users.ids.author,
published_at: moment().add(30, 'seconds').toDate(),
status: 'scheduled',
slug: 'second'
}));
scope.posts.push(testUtils.DataGenerator.forKnex.createPost({
created_by: testUtils.users.ids.author,
author_id: testUtils.users.ids.author,
published_by: testUtils.users.ids.author,
published_at: moment().subtract(30, 'seconds').toDate(),
status: 'scheduled',
slug: 'third'
}));
scope.posts.push(testUtils.DataGenerator.forKnex.createPost({
created_by: testUtils.users.ids.author,
author_id: testUtils.users.ids.author,
published_by: testUtils.users.ids.author,
published_at: moment().subtract(10, 'minute').toDate(),
status: 'scheduled',
slug: 'fourth'
}));
Promise.mapSeries(scope.posts, function (post) {
return models.Post.add(post, {context: {internal: true}});
}).then(function (result) {
result.length.should.eql(4);
return done();
}).catch(done);
});
afterEach(function () {
scope.posts = [];
});
it('client with specific perms has access to publish post', function (done) {
api.schedules.publishPost({id: scope.posts[0].id, context: {client: 'ghost-scheduler'}})
.then(function (result) {
result.posts[0].id.should.eql(scope.posts[0].id);
result.posts[0].status.should.eql('published');
done();
})
.catch(done);
});
it('can publish with tolerance (30 seconds in the future)', function (done) {
api.schedules.publishPost({id: scope.posts[1].id, context: {client: 'ghost-scheduler'}})
.then(function (result) {
result.posts[0].id.should.eql(scope.posts[1].id);
result.posts[0].status.should.eql('published');
done();
})
.catch(done);
});
it('can publish with tolerance (30seconds in the past)', function (done) {
api.schedules.publishPost({id: scope.posts[2].id, context: {client: 'ghost-scheduler'}})
.then(function (result) {
result.posts[0].id.should.eql(scope.posts[2].id);
result.posts[0].status.should.eql('published');
done();
})
.catch(done);
});
it('can publish a post in the past with force flag', function (done) {
api.schedules.publishPost({force: true}, {id: scope.posts[3].id, context: {client: 'ghost-scheduler'}})
.then(function (result) {
result.posts[0].id.should.eql(scope.posts[3].id);
result.posts[0].status.should.eql('published');
done();
})
.catch(done);
});
it('collision protection', function (done) {
var originalPostApi = api.posts.edit,
postId = scope.posts[0].id, // first post is status=scheduled!
requestCanComeIn = false,
interval;
// this request get's blocked
interval = setInterval(function () {
if (requestCanComeIn) {
clearInterval(interval);
// happens in a transaction, request has to wait until the scheduler api finished
return models.Post.edit({title: 'Berlin'}, {id: postId, context: {internal: true}})
.then(function (post) {
post.id.should.eql(postId);
post.get('status').should.eql('published');
post.get('title').should.eql('Berlin');
done();
})
.catch(done);
}
}, 500);
// target post to publish was read already, simulate a client request
sandbox.stub(api.posts, 'edit').callsFake(function () {
var self = this,
args = arguments;
requestCanComeIn = true;
return Promise.delay(2000)
.then(function () {
return originalPostApi.apply(self, args);
});
});
api.schedules.publishPost({id: postId, context: {client: 'ghost-scheduler'}})
.then(function (result) {
result.posts[0].id.should.eql(postId);
result.posts[0].status.should.eql('published');
result.posts[0].title.should.eql('title');
})
.catch(done);
});
});
describe('error', function () {
beforeEach(function (done) {
scope.posts.push(testUtils.DataGenerator.forKnex.createPost({
created_by: testUtils.users.ids.author,
author_id: testUtils.users.ids.author,
published_by: testUtils.users.ids.author,
published_at: moment().add(2, 'days').toDate(),
status: 'scheduled',
slug: 'first'
}));
scope.posts.push(testUtils.DataGenerator.forKnex.createPost({
created_by: testUtils.users.ids.author,
author_id: testUtils.users.ids.author,
published_by: testUtils.users.ids.author,
published_at: moment().add(2, 'days').toDate(),
status: 'draft',
slug: 'second'
}));
scope.posts.push(testUtils.DataGenerator.forKnex.createPost({
created_by: testUtils.users.ids.author,
author_id: testUtils.users.ids.author,
published_by: testUtils.users.ids.author,
published_at: moment().add(4, 'minutes').toDate(),
status: 'scheduled',
slug: 'third'
}));
scope.posts.push(testUtils.DataGenerator.forKnex.createPost({
created_by: testUtils.users.ids.author,
author_id: testUtils.users.ids.author,
published_by: testUtils.users.ids.author,
published_at: moment().subtract(4, 'minutes').toDate(),
status: 'scheduled',
slug: 'fourth'
}));
Promise.all(scope.posts.map(function (post) {
return models.Post.add(post, {context: {internal: true}});
})).then(function (result) {
result.length.should.eql(4);
return done();
}).catch(done);
});
afterEach(function () {
scope.posts = [];
});
it('ghost admin has no access', function (done) {
api.schedules.publishPost({id: scope.posts[0].id, context: {client: 'ghost-admin'}})
.then(function () {
done(new Error('expected NoPermissionError'));
})
.catch(function (err) {
should.exist(err);
(err instanceof common.errors.NoPermissionError).should.eql(true);
done();
});
});
it('owner has no access (this is how it is right now!)', function (done) {
api.schedules.publishPost({id: scope.posts[1].id, context: {user: testUtils.users.ids.author}})
.then(function () {
done(new Error('expected NoPermissionError'));
})
.catch(function (err) {
should.exist(err);
(err instanceof common.errors.NoPermissionError).should.eql(true);
done();
});
});
it('other user has no access', function (done) {
testUtils.fixtures.insertOne('User', 'users', 'createUser', 4)
.then(function (result) {
api.schedules.publishPost({id: scope.posts[0].id, context: {user: result[0]}})
.then(function () {
done(new Error('expected NoPermissionError'));
})
.catch(function (err) {
should.exist(err);
(err instanceof common.errors.NoPermissionError).should.eql(true);
done();
});
})
.catch(done);
});
it('invalid params: id is integer', function (done) {
api.schedules.publishPost({id: 100, context: {client: 'ghost-scheduler'}})
.then(function () {
done(new Error('expected ValidationError'));
})
.catch(function (err) {
should.exist(err);
(err instanceof common.errors.ValidationError).should.eql(true);
done();
});
});
it('post does not exist', function (done) {
api.schedules.publishPost({id: ObjectId.generate(), context: {client: 'ghost-scheduler'}})
.then(function () {
done(new Error('expected ValidationError'));
})
.catch(function (err) {
should.exist(err);
(err instanceof common.errors.NotFoundError).should.eql(true);
done();
});
});
it('publish at a wrong time', function (done) {
api.schedules.publishPost({id: scope.posts[0].id, context: {client: 'ghost-scheduler'}})
.then(function () {
done(new Error('expected ValidationError'));
})
.catch(function (err) {
should.exist(err);
(err instanceof common.errors.NotFoundError).should.eql(true);
done();
});
});
it('publish at a wrong time', function (done) {
api.schedules.publishPost({id: scope.posts[2].id, context: {client: 'ghost-scheduler'}})
.then(function () {
done(new Error('expected ValidationError'));
})
.catch(function (err) {
should.exist(err);
(err instanceof common.errors.NotFoundError).should.eql(true);
done();
});
});
it('publish at a wrong time', function (done) {
api.schedules.publishPost({id: scope.posts[3].id, context: {client: 'ghost-scheduler'}})
.then(function () {
done(new Error('expected ValidationError'));
})
.catch(function (err) {
should.exist(err);
(err instanceof common.errors.NotFoundError).should.eql(true);
done();
});
});
it('publish, but status is draft', function (done) {
api.schedules.publishPost({id: scope.posts[1].id, context: {client: 'ghost-scheduler'}})
.then(function () {
done(new Error('expected ValidationError'));
})
.catch(function (err) {
should.exist(err);
(err instanceof common.errors.NotFoundError).should.eql(true);
done();
});
});
});
});
});

View File

@ -1,197 +0,0 @@
var should = require('should'),
testUtils = require('../../utils'),
_ = require('lodash'),
// Stuff we are testing
SettingsAPI = require('../../../server/api/v0.1/settings'),
settingsCache = require('../../../server/services/settings/cache'),
defaultContext = {user: 1},
internalContext = {internal: true},
callApiWithContext,
getErrorDetails;
describe('Settings API', function () {
// Keep the DB clean
before(testUtils.teardown);
after(testUtils.teardown);
before(testUtils.setup('settings', 'users:roles', 'perms:setting', 'perms:init'));
should.exist(SettingsAPI);
callApiWithContext = function (context, method) {
var args = _.toArray(arguments),
options = args[args.length - 1];
if (_.isObject(options)) {
options.context = _.clone(context);
}
return SettingsAPI[method].apply({}, args.slice(2));
};
getErrorDetails = function (err) {
if (err instanceof Error) {
throw err;
}
throw new Error(err.message);
};
it('uses Date objects for dateTime fields', function () {
return callApiWithContext(defaultContext, 'browse', {}).then(function (results) {
should.exist(results);
results.settings[0].created_at.should.be.an.instanceof(Date);
}).catch(getErrorDetails);
});
it('can browse', function () {
return callApiWithContext(defaultContext, 'browse', {}).then(function (results) {
should.exist(results);
testUtils.API.checkResponse(results, 'settings');
results.settings.length.should.be.above(0);
testUtils.API.checkResponse(results.settings[0], 'setting');
// Check for a core setting
should.not.exist(_.find(results.settings, function (setting) {
return setting.type === 'core';
}));
}).catch(getErrorDetails);
});
it('can browse by type', function () {
return callApiWithContext(defaultContext, 'browse', {type: 'blog'}).then(function (results) {
should.exist(results);
testUtils.API.checkResponse(results, 'settings');
results.settings.length.should.be.above(0);
testUtils.API.checkResponse(results.settings[0], 'setting');
// Check for a core setting
should.not.exist(_.find(results.settings, function (setting) {
return setting.type === 'core';
}));
}).catch(getErrorDetails);
});
it('returns core settings for internal requests when browsing', function () {
return callApiWithContext(internalContext, 'browse', {}).then(function (results) {
should.exist(results);
testUtils.API.checkResponse(results, 'settings');
results.settings.length.should.be.above(0);
testUtils.API.checkResponse(results.settings[0], 'setting');
// Check for a core setting
should.exist(_.find(results.settings, function (setting) {
return setting.type === 'core';
}));
}).catch(getErrorDetails);
});
it('can read blog settings by string', function () {
return SettingsAPI.read('title').then(function (response) {
should.exist(response);
testUtils.API.checkResponse(response, 'settings');
response.settings.length.should.equal(1);
testUtils.API.checkResponse(response.settings[0], 'setting');
}).catch(getErrorDetails);
});
it('cannot read core settings if not an internal request', function () {
return callApiWithContext(defaultContext, 'read', {key: 'db_hash'}).then(function () {
throw new Error('Allowed to read db_hash with external request');
}).catch(function (error) {
should.exist(error);
error.errorType.should.eql('NoPermissionError');
});
});
it('can read core settings if an internal request', function () {
return callApiWithContext(internalContext, 'read', {key: 'db_hash'}).then(function (response) {
should.exist(response);
testUtils.API.checkResponse(response, 'settings');
response.settings.length.should.equal(1);
testUtils.API.checkResponse(response.settings[0], 'setting');
}).catch(getErrorDetails);
});
it('can read by object key', function () {
return callApiWithContext(defaultContext, 'read', {key: 'title'}).then(function (response) {
should.exist(response);
testUtils.API.checkResponse(response, 'settings');
response.settings.length.should.equal(1);
testUtils.API.checkResponse(response.settings[0], 'setting');
}).catch(getErrorDetails);
});
it('can edit', function () {
// see default-settings.json
settingsCache.get('title').should.eql('Ghost');
return callApiWithContext(defaultContext, 'edit', {settings: [{key: 'title', value: 'UpdatedGhost'}]}, {})
.then(function (response) {
should.exist(response);
testUtils.API.checkResponse(response, 'settings');
response.settings.length.should.equal(1);
testUtils.API.checkResponse(response.settings[0], 'setting');
settingsCache.get('title').should.eql('UpdatedGhost');
});
});
it('cannot edit a core setting if not an internal request', function () {
return callApiWithContext(defaultContext, 'edit', {settings: [{key: 'db_hash', value: 'hash'}]}, {})
.then(function () {
throw new Error('Allowed to edit a core setting as external request');
}).catch(function (err) {
should.exist(err);
err.errorType.should.eql('NoPermissionError');
});
});
it('can edit a core setting with an internal request', function () {
return callApiWithContext(internalContext, 'edit', {settings: [{key: 'db_hash', value: 'hash'}]}, {})
.then(function (response) {
should.exist(response);
testUtils.API.checkResponse(response, 'settings');
response.settings.length.should.equal(1);
testUtils.API.checkResponse(response.settings[0], 'setting');
});
});
it('cannot edit the active theme setting via API even with internal context', function () {
return callApiWithContext(internalContext, 'edit', 'active_theme', {
settings: [{key: 'active_theme', value: 'rasper'}]
}).then(function () {
throw new Error('Allowed to change active theme settting');
}).catch(function (err) {
should.exist(err);
err.errorType.should.eql('BadRequestError');
err.message.should.eql('Attempted to change active_theme via settings API');
});
});
it('ensures values are stringified before saving to database', function () {
return callApiWithContext(defaultContext, 'edit', 'title', []).then(function (response) {
should.exist(response);
testUtils.API.checkResponse(response, 'settings');
response.settings.length.should.equal(1);
testUtils.API.checkResponse(response.settings[0], 'setting');
response.settings[0].value.should.equal('[]');
});
});
it('set active_timezone: unknown timezone', function () {
return callApiWithContext(defaultContext, 'edit', {settings: [{key: 'active_timezone', value: 'MFG'}]}, {})
.then(function () {
throw new Error('We expect that the active_timezone cannot be stored');
}).catch(function (errors) {
should.exist(errors);
errors.length.should.eql(1);
errors[0].errorType.should.eql('ValidationError');
});
});
it('set active_timezone: known timezone', function () {
return callApiWithContext(defaultContext, 'edit', {settings: [{key: 'active_timezone', value: 'Etc/UTC'}]}, {});
});
});

View File

@ -1,82 +0,0 @@
var should = require('should'),
testUtils = require('../../utils'),
SlugAPI = require('../../../server/api/v0.1/slugs');
describe('Slug API', function () {
// Keep the DB clean
before(testUtils.teardown);
after(testUtils.teardown);
before(testUtils.setup('settings', 'users:roles', 'perms:slug', 'perms:init'));
should.exist(SlugAPI);
it('can generate post slug', function (done) {
SlugAPI.generate({context: {user: 1}, type: 'post', name: 'A fancy Title'})
.then(function (results) {
should.exist(results);
testUtils.API.checkResponse(results, 'slugs');
results.slugs.length.should.be.above(0);
testUtils.API.checkResponse(results.slugs[0], 'slug');
results.slugs[0].slug.should.equal('a-fancy-title');
done();
}).catch(done);
});
it('can generate tag slug', function (done) {
SlugAPI.generate({context: {user: 1}, type: 'tag', name: 'A fancy Title'})
.then(function (results) {
should.exist(results);
testUtils.API.checkResponse(results, 'slugs');
results.slugs.length.should.be.above(0);
testUtils.API.checkResponse(results.slugs[0], 'slug');
results.slugs[0].slug.should.equal('a-fancy-title');
done();
}).catch(done);
});
it('can generate user slug', function (done) {
SlugAPI.generate({context: {user: 1}, type: 'user', name: 'user name'})
.then(function (results) {
should.exist(results);
testUtils.API.checkResponse(results, 'slugs');
results.slugs.length.should.be.above(0);
testUtils.API.checkResponse(results.slugs[0], 'slug');
results.slugs[0].slug.should.equal('user-name');
done();
}).catch(done);
});
it('can generate app slug', function (done) {
SlugAPI.generate({context: {user: 1}, type: 'tag', name: 'app name'})
.then(function (results) {
should.exist(results);
testUtils.API.checkResponse(results, 'slugs');
results.slugs.length.should.be.above(0);
testUtils.API.checkResponse(results.slugs[0], 'slug');
results.slugs[0].slug.should.equal('app-name');
done();
}).catch(done);
});
it('rejects unknown types with BadRequestError', function (done) {
SlugAPI.generate({context: {user: 1}, type: 'unknown-type', name: 'A fancy Title'})
.then(function () {
done(new Error('Generate a slug for an unknown type is not rejected.'));
}).catch(function (error) {
error.errorType.should.equal('BadRequestError');
done();
}).catch(done);
});
it('rejects invalid types with ValidationError', function (done) {
SlugAPI.generate({context: {user: 1}, type: 'unknown type', name: 'A fancy Title'})
.then(function () {
done(new Error('Generate a slug for an unknown type is not rejected.'));
}).catch(function (errors) {
errors.should.have.property('errorType', 'ValidationError');
done();
}).catch(done);
});
});

View File

@ -1,382 +0,0 @@
var should = require('should'),
sinon = require('sinon'),
testUtils = require('../../utils'),
Promise = require('bluebird'),
ObjectId = require('bson-objectid'),
fs = require('fs-extra'),
_ = require('lodash'),
context = testUtils.context,
common = require('../../../server/lib/common'),
fsLib = require('../../../server/lib/fs'),
SubscribersAPI = require('../../../server/api/v0.1/subscribers'),
sandbox = sinon.sandbox.create();
describe('Subscribers API', function () {
// Keep the DB clean
before(testUtils.teardown);
after(testUtils.teardown);
afterEach(function () {
sandbox.restore();
});
before(testUtils.setup('settings', 'users:roles', 'perms:subscriber', 'perms:init', 'posts'));
should.exist(SubscribersAPI);
describe('Add', function () {
var newSubscriber;
beforeEach(function () {
return testUtils.fixtures.insertOne('Subscriber', 'subscribers', 'createSubscriber');
});
afterEach(function () {
return testUtils.truncate('subscribers');
});
beforeEach(function () {
newSubscriber = _.clone(testUtils.DataGenerator.forKnex.createSubscriber(testUtils.DataGenerator.Content.subscribers[1]));
Promise.resolve(newSubscriber);
});
it('can add a subscriber (admin)', function (done) {
SubscribersAPI.add({subscribers: [newSubscriber]}, testUtils.context.admin)
.then(function (results) {
should.exist(results);
should.exist(results.subscribers);
results.subscribers.length.should.be.above(0);
done();
}).catch(done);
});
it('can add a subscriber (editor)', function (done) {
SubscribersAPI.add({subscribers: [newSubscriber]}, testUtils.context.editor)
.then(function (results) {
should.exist(results);
should.exist(results.subscribers);
results.subscribers.length.should.be.above(0);
done();
}).catch(done);
});
it('can add a subscriber (author)', function (done) {
SubscribersAPI.add({subscribers: [newSubscriber]}, testUtils.context.author)
.then(function (results) {
should.exist(results);
should.exist(results.subscribers);
results.subscribers.length.should.be.above(0);
done();
}).catch(done);
});
it('can add a subscriber (external)', function (done) {
SubscribersAPI.add({subscribers: [newSubscriber]}, testUtils.context.external)
.then(function (results) {
should.exist(results);
should.exist(results.subscribers);
results.subscribers.length.should.be.above(0);
done();
}).catch(done);
});
it('duplicate subscriber', function (done) {
SubscribersAPI.add({subscribers: [newSubscriber]}, testUtils.context.external)
.then(function () {
SubscribersAPI.add({subscribers: [newSubscriber]}, testUtils.context.external)
.then(function () {
return done();
})
.catch(done);
})
.catch(done);
});
it('CANNOT add subscriber without context', function (done) {
SubscribersAPI.add({subscribers: [newSubscriber]})
.then(function () {
done(new Error('Add subscriber without context should have no access.'));
})
.catch(function (err) {
(err instanceof common.errors.NoPermissionError).should.eql(true);
done();
});
});
});
describe('Edit', function () {
beforeEach(function () {
return testUtils.fixtures.insertOne('Subscriber', 'subscribers', 'createSubscriber');
});
afterEach(function () {
return testUtils.truncate('subscribers');
});
var newSubscriberEmail = 'subscriber@updated.com',
firstSubscriber = testUtils.DataGenerator.Content.subscribers[0].id;
it('can edit a subscriber (admin)', function (done) {
SubscribersAPI.edit({subscribers: [{email: newSubscriberEmail}]}, _.extend({}, context.admin, {id: firstSubscriber}))
.then(function (results) {
should.exist(results);
should.exist(results.subscribers);
results.subscribers.length.should.be.above(0);
done();
}).catch(done);
});
it('can edit subscriber (external)', function (done) {
SubscribersAPI.edit({subscribers: [{email: newSubscriberEmail}]}, _.extend({}, context.external, {id: firstSubscriber}))
.then(function (results) {
should.exist(results);
should.exist(results.subscribers);
results.subscribers.length.should.be.above(0);
done();
}).catch(done);
});
it('CANNOT edit a subscriber (editor)', function (done) {
SubscribersAPI.edit({subscribers: [{email: newSubscriberEmail}]}, _.extend({}, context.editor, {id: firstSubscriber}))
.then(function () {
done(new Error('Edit subscriber as author should have no access.'));
})
.catch(function (err) {
(err instanceof common.errors.NoPermissionError).should.eql(true);
done();
});
});
it('CANNOT edit subscriber (author)', function (done) {
SubscribersAPI.edit({subscribers: [{email: newSubscriberEmail}]}, _.extend({}, context.author, {id: firstSubscriber}))
.then(function () {
done(new Error('Edit subscriber as author should have no access.'));
})
.catch(function (err) {
(err instanceof common.errors.NoPermissionError).should.eql(true);
done();
});
});
it('CANNOT edit subscriber that doesn\'t exit', function (done) {
SubscribersAPI.edit({subscribers: [{email: newSubscriberEmail}]}, _.extend({}, context.internal, {id: ObjectId.generate()}))
.then(function () {
done(new Error('Edit non-existent subscriber is possible.'));
}, function (err) {
should.exist(err);
(err instanceof common.errors.NotFoundError).should.eql(true);
done();
}).catch(done);
});
});
describe('Destroy', function () {
beforeEach(function () {
return testUtils.fixtures.insertOne('Subscriber', 'subscribers', 'createSubscriber');
});
afterEach(function () {
return testUtils.truncate('subscribers');
});
var firstSubscriber = testUtils.DataGenerator.Content.subscribers[0];
it('can destroy subscriber as admin', function (done) {
SubscribersAPI.destroy(_.extend({}, testUtils.context.admin, {id: firstSubscriber.id}))
.then(function (results) {
should.not.exist(results);
done();
}).catch(done);
});
it('can destroy subscriber by email', function (done) {
SubscribersAPI.destroy(_.extend({}, testUtils.context.admin, {email: firstSubscriber.email}))
.then(function (results) {
should.not.exist(results);
done();
}).catch(done);
});
it('returns NotFoundError for unknown email', function (done) {
SubscribersAPI.destroy(_.extend({}, testUtils.context.admin, {email: 'unknown@example.com'}))
.then(function (results) {
done(new Error('Destroy subscriber should not be possible with unknown email.'));
}, function (err) {
should.exist(err);
(err instanceof common.errors.NotFoundError).should.eql(true);
done();
}).catch(done);
});
it('CANNOT destroy subscriber', function (done) {
SubscribersAPI.destroy(_.extend({}, testUtils.context.editor, {id: firstSubscriber.id}))
.then(function () {
done(new Error('Destroy subscriber should not be possible as editor.'));
}, function (err) {
should.exist(err);
(err instanceof common.errors.NoPermissionError).should.eql(true);
done();
}).catch(done);
});
});
describe('Browse', function () {
beforeEach(function () {
return testUtils.fixtures.insertOne('Subscriber', 'subscribers', 'createSubscriber');
});
afterEach(function () {
return testUtils.truncate('subscribers');
});
it('can browse (internal)', function (done) {
SubscribersAPI.browse(testUtils.context.internal).then(function (results) {
should.exist(results);
should.exist(results.subscribers);
results.subscribers.should.have.lengthOf(1);
testUtils.API.checkResponse(results.subscribers[0], 'subscriber');
results.subscribers[0].created_at.should.be.an.instanceof(Date);
results.meta.pagination.should.have.property('page', 1);
results.meta.pagination.should.have.property('limit', 15);
results.meta.pagination.should.have.property('pages', 1);
results.meta.pagination.should.have.property('total', 1);
results.meta.pagination.should.have.property('next', null);
results.meta.pagination.should.have.property('prev', null);
done();
}).catch(done);
});
it('CANNOT browse subscriber (external)', function (done) {
SubscribersAPI.browse(testUtils.context.external)
.then(function () {
done(new Error('Browse subscriber should be denied with external context.'));
})
.catch(function (err) {
(err instanceof common.errors.NoPermissionError).should.eql(true);
done();
});
});
});
describe('Read', function () {
beforeEach(function () {
return testUtils.fixtures.insertOne('Subscriber', 'subscribers', 'createSubscriber');
});
afterEach(function () {
return testUtils.truncate('subscribers');
});
it('with id', function (done) {
SubscribersAPI.browse({context: {user: 1}}).then(function (results) {
should.exist(results);
should.exist(results.subscribers);
results.subscribers.length.should.be.above(0);
var firstSubscriber = _.find(results.subscribers, {id: testUtils.DataGenerator.Content.subscribers[0].id});
return SubscribersAPI.read({context: {user: 1}, id: firstSubscriber.id});
}).then(function (found) {
should.exist(found);
testUtils.API.checkResponse(found.subscribers[0], 'subscriber');
done();
}).catch(done);
});
it('with email', function (done) {
SubscribersAPI.browse({context: {user: 1}}).then(function (results) {
should.exist(results);
should.exist(results.subscribers);
results.subscribers.length.should.be.above(0);
var firstSubscriber = _.find(results.subscribers, {id: testUtils.DataGenerator.Content.subscribers[0].id});
return SubscribersAPI.read({context: {user: 1}, email: firstSubscriber.email});
}).then(function (found) {
should.exist(found);
testUtils.API.checkResponse(found.subscribers[0], 'subscriber');
done();
}).catch(done);
});
it('CANNOT fetch a subscriber which doesn\'t exist', function (done) {
SubscribersAPI.read({context: {user: 1}, id: 999}).then(function () {
done(new Error('Should not return a result'));
}).catch(function (err) {
should.exist(err);
(err instanceof common.errors.NotFoundError).should.eql(true);
done();
});
});
});
describe('Read CSV', function () {
var scope = {};
beforeEach(function () {
return testUtils.fixtures.insertOne('Subscriber', 'subscribers', 'createSubscriber');
});
afterEach(function () {
return testUtils.truncate('subscribers');
});
beforeEach(function () {
sandbox.stub(fs, 'unlink').resolves();
sandbox.stub(fsLib, 'readCSV').value(function () {
if (scope.csvError) {
return Promise.reject(new Error('csv'));
}
return Promise.resolve(scope.values);
});
});
afterEach(function () {
scope.csvError = false;
});
it('check that fn works in general', function (done) {
scope.values = [{email: 'lol@hallo.de'}, {email: 'test'}, {email: 'lol@hallo.de'}];
SubscribersAPI.importCSV(_.merge(testUtils.context.internal, {path: '/somewhere'}))
.then(function (result) {
result.meta.stats.imported.should.eql(1);
result.meta.stats.duplicates.should.eql(1);
result.meta.stats.invalid.should.eql(1);
done();
})
.catch(done);
});
it('check that fn works in general', function (done) {
scope.values = [{email: 'lol@hallo.de'}, {email: '1@kate.de'}];
SubscribersAPI.importCSV(_.merge(testUtils.context.internal, {path: '/somewhere'}))
.then(function (result) {
result.meta.stats.imported.should.eql(2);
result.meta.stats.duplicates.should.eql(0);
result.meta.stats.invalid.should.eql(0);
done();
})
.catch(done);
});
it('read csv throws a not found error', function (done) {
scope.csvError = true;
SubscribersAPI.importCSV(_.merge(testUtils.context.internal, {path: '/somewhere'}))
.then(function () {
done(new Error('we expected an error here!'));
})
.catch(function (err) {
err.message.should.eql('csv');
done();
});
});
});
});

View File

@ -1,433 +0,0 @@
var should = require('should'),
testUtils = require('../../utils'),
_ = require('lodash'),
// Stuff we are testing
context = testUtils.context,
TagAPI = require('../../../server/api/v0.1/tags');
// there are some random generated tags in test database
// which can't be sorted easily using _.sortBy()
// so we filter them out and leave only pre-built fixtures
// usage: tags.filter(onlyFixtures)
function onlyFixtures(slug) {
return testUtils.DataGenerator.Content.tags.indexOf(slug) >= 0;
}
describe('Tags API', function () {
// Keep the DB clean
before(testUtils.teardown);
after(testUtils.teardown);
before(testUtils.setup('settings', 'users:roles', 'perms:tag', 'perms:init'));
should.exist(TagAPI);
describe('Add', function () {
var newTag;
beforeEach(function () {
return testUtils.fixtures.insertTags();
});
afterEach(function () {
return testUtils.truncate('tags');
});
beforeEach(function () {
newTag = _.clone(_.omit(testUtils.DataGenerator.forKnex.createTag(testUtils.DataGenerator.Content.tags[0]), 'id'));
});
it('can add a tag (admin)', function (done) {
TagAPI.add({tags: [newTag]}, testUtils.context.admin)
.then(function (results) {
should.exist(results);
should.exist(results.tags);
results.tags.length.should.be.above(0);
done();
}).catch(done);
});
it('can add a tag (editor)', function (done) {
TagAPI.add({tags: [newTag]}, testUtils.context.editor)
.then(function (results) {
should.exist(results);
should.exist(results.tags);
results.tags.length.should.be.above(0);
results.tags[0].visibility.should.eql('public');
done();
}).catch(done);
});
it('can add a tag (author)', function (done) {
TagAPI.add({tags: [newTag]}, testUtils.context.author)
.then(function (results) {
should.exist(results);
should.exist(results.tags);
results.tags.length.should.be.above(0);
results.tags[0].visibility.should.eql('public');
done();
}).catch(done);
});
it('add internal tag', function (done) {
TagAPI
.add({tags: [{name: '#test'}]}, testUtils.context.editor)
.then(function (results) {
should.exist(results);
should.exist(results.tags);
results.tags.length.should.be.above(0);
results.tags[0].visibility.should.eql('internal');
results.tags[0].name.should.eql('#test');
results.tags[0].slug.should.eql('hash-test');
done();
}).catch(done);
});
it('CANNOT add tag (contributor)', function (done) {
TagAPI.add({tags: [newTag]}, testUtils.context.contributor)
.then(function () {
done(new Error('Add tag is not denied for contributor.'));
}, function () {
done();
}).catch(done);
});
it('No-auth CANNOT add tag', function (done) {
TagAPI.add({tags: [newTag]}).then(function () {
done(new Error('Add tag is not denied without authentication.'));
}, function () {
done();
}).catch(done);
});
it('rejects invalid names with ValidationError', function (done) {
var invalidTag = _.clone(newTag);
invalidTag.name = ', starts with a comma';
TagAPI.add({tags: [invalidTag]}, testUtils.context.admin)
.then(function () {
done(new Error('Adding a tag with an invalid name is not rejected.'));
}).catch(function (errors) {
errors[0].errorType.should.eql('ValidationError');
done();
}).catch(done);
});
});
describe('Edit', function () {
beforeEach(function () {
return testUtils.fixtures.insertTags();
});
afterEach(function () {
return testUtils.truncate('tags');
});
var newTagName = 'tagNameUpdated',
firstTag = testUtils.DataGenerator.Content.tags[0].id;
it('can edit a tag (admin)', function (done) {
TagAPI.edit({tags: [{name: newTagName}]}, _.extend({}, context.admin, {id: firstTag}))
.then(function (results) {
should.exist(results);
should.exist(results.tags);
results.tags.length.should.be.above(0);
done();
}).catch(done);
});
it('can edit a tag (editor)', function (done) {
TagAPI.edit({tags: [{name: newTagName}]}, _.extend({}, context.editor, {id: firstTag}))
.then(function (results) {
should.exist(results);
should.exist(results.tags);
results.tags.length.should.be.above(0);
done();
}).catch(done);
});
it('CANNOT edit a tag (author)', function (done) {
TagAPI.edit({tags: [{name: newTagName}]}, _.extend({}, context.author, {id: firstTag}))
.then(function () {
done(new Error('Add tag is not denied for author.'));
}, function () {
done();
}).catch(done);
});
it('No-auth CANNOT edit tag', function (done) {
TagAPI.edit({tags: [{name: newTagName}]}, _.extend({}, {id: firstTag}))
.then(function () {
done(new Error('Add tag is not denied without authentication.'));
}, function () {
done();
}).catch(done);
});
it('rejects invalid names with ValidationError', function (done) {
var invalidTagName = ', starts with a comma';
TagAPI.edit({tags: [{name: invalidTagName}]}, _.extend({}, context.editor, {id: firstTag}))
.then(function () {
done(new Error('Adding a tag with an invalid name is not rejected.'));
}).catch(function (errors) {
errors[0].errorType.should.eql('ValidationError');
done();
}).catch(done);
});
});
describe('Destroy', function () {
beforeEach(function () {
return testUtils.fixtures.insertTags();
});
afterEach(function () {
return testUtils.truncate('tags');
});
var firstTag = testUtils.DataGenerator.Content.tags[0].id;
it('can destroy Tag', function (done) {
TagAPI.destroy(_.extend({}, testUtils.context.admin, {id: firstTag}))
.then(function (results) {
should.not.exist(results);
done();
}).catch(done);
});
});
describe('Browse', function () {
before(function () {
return testUtils.fixtures.insertPostsAndTags();
});
after(testUtils.teardown);
before(function () {
return testUtils.fixtures.insertExtraTags();
});
it('can browse (internal)', function (done) {
TagAPI.browse(testUtils.context.internal).then(function (results) {
should.exist(results);
should.exist(results.tags);
results.tags.should.have.lengthOf(15);
testUtils.API.checkResponse(results.tags[0], 'tag');
results.tags[0].created_at.should.be.an.instanceof(Date);
results.meta.pagination.should.have.property('page', 1);
results.meta.pagination.should.have.property('limit', 15);
results.meta.pagination.should.have.property('pages', 4);
results.meta.pagination.should.have.property('total', 55);
results.meta.pagination.should.have.property('next', 2);
results.meta.pagination.should.have.property('prev', null);
done();
}).catch(done);
});
it('can browse page 2 (internal)', function (done) {
TagAPI.browse(_.extend({}, testUtils.context.internal, {page: 2})).then(function (results) {
should.exist(results);
should.exist(results.tags);
results.tags.should.have.lengthOf(15);
testUtils.API.checkResponse(results.tags[0], 'tag');
results.tags[0].created_at.should.be.an.instanceof(Date);
results.meta.pagination.should.have.property('page', 2);
results.meta.pagination.should.have.property('limit', 15);
results.meta.pagination.should.have.property('pages', 4);
results.meta.pagination.should.have.property('total', 55);
results.meta.pagination.should.have.property('next', 3);
results.meta.pagination.should.have.property('prev', 1);
done();
}).catch(done);
});
it('can browse (owner)', function (done) {
TagAPI.browse({context: {user: 1}}).then(function (results) {
should.exist(results);
should.exist(results.tags);
results.tags.length.should.be.above(0);
testUtils.API.checkResponse(results.tags[0], 'tag');
results.tags[0].created_at.should.be.an.instanceof(Date);
done();
}).catch(done);
});
it('can browse (admin)', function (done) {
TagAPI.browse(testUtils.context.admin).then(function (results) {
should.exist(results);
should.exist(results.tags);
results.tags.length.should.be.above(0);
testUtils.API.checkResponse(results.tags[0], 'tag');
results.tags[0].created_at.should.be.an.instanceof(Date);
done();
}).catch(done);
});
it('can browse (editor)', function (done) {
TagAPI.browse(testUtils.context.editor).then(function (results) {
should.exist(results);
should.exist(results.tags);
results.tags.length.should.be.above(0);
testUtils.API.checkResponse(results.tags[0], 'tag');
results.tags[0].created_at.should.be.an.instanceof(Date);
done();
}).catch(done);
});
it('can browse (author)', function (done) {
TagAPI.browse(testUtils.context.author).then(function (results) {
should.exist(results);
should.exist(results.tags);
results.tags.length.should.be.above(0);
testUtils.API.checkResponse(results.tags[0], 'tag');
results.tags[0].created_at.should.be.an.instanceof(Date);
done();
}).catch(done);
});
it('can browse with include count.posts', function (done) {
TagAPI.browse({context: {user: 1}, include: 'count.posts'}).then(function (results) {
should.exist(results);
should.exist(results.tags);
results.tags.should.have.lengthOf(15);
testUtils.API.checkResponse(results.tags[0], 'tag', 'count');
should.exist(results.tags[0].count.posts);
results.tags[0].count.posts.should.eql(2);
results.tags[1].count.posts.should.eql(2);
results.meta.pagination.should.have.property('page', 1);
results.meta.pagination.should.have.property('limit', 15);
results.meta.pagination.should.have.property('pages', 4);
results.meta.pagination.should.have.property('total', 55);
results.meta.pagination.should.have.property('next', 2);
results.meta.pagination.should.have.property('prev', null);
done();
}).catch(done);
});
it('can browse page 4 with include count.posts', function (done) {
TagAPI.browse({context: {user: 1}, include: 'count.posts', page: 4}).then(function (results) {
should.exist(results);
should.exist(results.tags);
results.tags.should.have.lengthOf(10);
testUtils.API.checkResponse(results.tags[0], 'tag', 'count');
should.exist(results.tags[0].count.posts);
results.meta.pagination.should.have.property('page', 4);
results.meta.pagination.should.have.property('limit', 15);
results.meta.pagination.should.have.property('pages', 4);
results.meta.pagination.should.have.property('total', 55);
results.meta.pagination.should.have.property('next', null);
results.meta.pagination.should.have.property('prev', 3);
done();
}).catch(done);
});
it('can browse and order by slug using asc', function (done) {
var expectedTags;
TagAPI.browse({context: {user: 1}})
.then(function (results) {
should.exist(results);
expectedTags = _(results.tags).map('slug').filter(onlyFixtures).sortBy().value();
return TagAPI.browse({context: {user: 1}, order: 'slug asc'});
})
.then(function (results) {
var tags;
should.exist(results);
tags = _(results.tags).map('slug').filter(onlyFixtures).value();
tags.should.eql(expectedTags);
})
.then(done)
.catch(done);
});
it('can browse and order by slug using desc', function (done) {
var expectedTags;
TagAPI.browse({context: {user: 1}})
.then(function (results) {
should.exist(results);
expectedTags = _(results.tags).map('slug').filter(onlyFixtures).sortBy().reverse().value();
return TagAPI.browse({context: {user: 1}, order: 'slug desc'});
})
.then(function (results) {
var tags;
should.exist(results);
tags = _(results.tags).map('slug').filter(onlyFixtures).value();
tags.should.eql(expectedTags);
})
.then(done)
.catch(done);
});
});
describe('Read', function () {
before(testUtils.setup('users:roles', 'posts'));
it('returns count.posts with include count.posts', function (done) {
TagAPI.read({context: {user: 1}, include: 'count.posts', slug: 'kitchen-sink'}).then(function (results) {
should.exist(results);
should.exist(results.tags);
results.tags.length.should.be.above(0);
testUtils.API.checkResponse(results.tags[0], 'tag', 'count');
should.exist(results.tags[0].count.posts);
results.tags[0].count.posts.should.equal(2);
done();
}).catch(done);
});
it('with slug', function (done) {
TagAPI.browse({context: {user: 1}}).then(function (results) {
should.exist(results);
should.exist(results.tags);
results.tags.length.should.be.above(0);
var firstTag = _.find(results.tags, {id: testUtils.DataGenerator.Content.tags[0].id});
return TagAPI.read({context: {user: 1}, slug: firstTag.slug});
}).then(function (found) {
should.exist(found);
testUtils.API.checkResponse(found.tags[0], 'tag');
done();
}).catch(done);
});
// TODO: this should be a 422?
it('cannot fetch a tag with an invalid slug', function (done) {
TagAPI.read({slug: 'invalid!'}).then(function () {
done(new Error('Should not return a result with invalid slug'));
}).catch(function (err) {
should.exist(err);
err.message.should.eql('Tag not found.');
done();
});
});
});
});

File diff suppressed because it is too large Load Diff

View File

@ -1,147 +0,0 @@
var _ = require('lodash'),
should = require('should'),
sinon = require('sinon'),
testUtils = require('../../utils'),
Promise = require('bluebird'),
WebhookAPI = require('../../../server/api/v0.1/webhooks'),
sandbox = sinon.sandbox.create();
describe('Webhooks API', function () {
beforeEach(testUtils.teardown);
beforeEach(testUtils.setup('webhooks', 'users:roles', 'perms:webhook', 'perms:init'));
afterEach(function () {
sandbox.restore();
});
after(testUtils.teardown);
function checkForErrorType(type, done) {
return function checkForErrorType(error) {
if (Array.isArray(error)) {
error = error[0];
}
if (error.errorType) {
error.errorType.should.eql(type);
done();
} else {
done(error);
}
};
}
describe('Validations', function () {
it('Prevents mixed case event names', function (done) {
WebhookAPI.add({webhooks: [{
event: 'Mixed.Case',
target_url: 'https://example.com/hooks/test'
}]}, testUtils.context.owner)
.then(function () {
done(new Error('Should not allow mixed case event names'));
}).catch(checkForErrorType('ValidationError', done));
});
it('Prevents duplicate event/target pairs', function (done) {
var duplicate = testUtils.DataGenerator.Content.webhooks[0];
WebhookAPI.add({webhooks: [{
event: duplicate.event,
target_url: duplicate.target_url
}]}, testUtils.context.owner)
.then(function () {
done(new Error('Should not allow duplicate event/target'));
}).catch(checkForErrorType('ValidationError', done));
});
});
describe('Permissions', function () {
var firstWebhook = testUtils.DataGenerator.Content.webhooks[0].id;
var newWebhook;
function checkAddResponse(response) {
should.exist(response);
should.exist(response.webhooks);
should.not.exist(response.meta);
response.webhooks.should.have.length(1);
testUtils.API.checkResponse(response.webhooks[0], 'webhook');
response.webhooks[0].created_at.should.be.an.instanceof(Date);
}
beforeEach(function () {
newWebhook = {
event: 'test.added',
target_url: 'https://example.com/webhooks/test-added'
};
});
describe('Owner', function () {
it('Can add', function (done) {
WebhookAPI.add({webhooks: [newWebhook]}, testUtils.context.owner)
.then(function (response) {
checkAddResponse(response);
done();
}).catch(done);
});
it('Can delete', function (done) {
WebhookAPI.destroy(_.extend({}, testUtils.context.owner, {id: firstWebhook}))
.then(function (results) {
should.not.exist(results);
done();
});
});
});
describe('Admin', function () {
it('Can add', function (done) {
WebhookAPI.add({webhooks: [newWebhook]}, testUtils.context.admin)
.then(function (response) {
checkAddResponse(response);
done();
}).catch(done);
});
it('Can delete', function (done) {
WebhookAPI.destroy(_.extend({}, testUtils.context.admin, {id: firstWebhook}))
.then(function (results) {
should.not.exist(results);
done();
});
});
});
describe('Editor', function () {
it('CANNOT add', function (done) {
WebhookAPI.add({webhooks: [newWebhook]}, testUtils.context.editor)
.then(function () {
done(new Error('Editor should not be able to add a webhook'));
}).catch(checkForErrorType('NoPermissionError', done));
});
it('CANNOT delete', function (done) {
WebhookAPI.destroy(_.extend({}, testUtils.context.editor, {id: firstWebhook}))
.then(function () {
done(new Error('Editor should not be able to delete a webhook'));
}).catch(checkForErrorType('NoPermissionError', done));
});
});
describe('Author', function () {
it('CANNOT add', function (done) {
WebhookAPI.add({webhooks: [newWebhook]}, testUtils.context.author)
.then(function () {
done(new Error('Author should not be able to add a webhook'));
}).catch(checkForErrorType('NoPermissionError', done));
});
it('CANNOT delete', function (done) {
WebhookAPI.destroy(_.extend({}, testUtils.context.author, {id: firstWebhook}))
.then(function () {
done(new Error('Author should not be able to delete a webhook'));
}).catch(checkForErrorType('NoPermissionError', done));
});
});
});
});

View File

@ -1,112 +0,0 @@
var should = require('should'),
sinon = require('sinon'),
testUtils = require('../../utils'),
Promise = require('bluebird'),
RedirectsAPI = require('../../../server/api/v0.1/redirects'),
mail = require('../../../server/api/v0.1/mail'),
sandbox = sinon.sandbox.create();
should.equal(true, true);
describe('Redirects API', function () {
beforeEach(testUtils.teardown);
beforeEach(testUtils.setup('settings', 'users:roles', 'perms:redirect', 'perms:init'));
beforeEach(function () {
sandbox.stub(mail, 'send').callsFake(function () {
return Promise.resolve();
});
});
afterEach(function () {
sandbox.restore();
});
after(testUtils.teardown);
describe('Permissions', function () {
describe('Owner', function () {
it('Can upload', function (done) {
RedirectsAPI.upload(testUtils.context.owner)
.then(function () {
done();
})
.catch(done);
});
it('Can download', function (done) {
RedirectsAPI.download(testUtils.context.owner)
.then(function () {
done();
})
.catch(done);
});
});
describe('Admin', function () {
it('Can upload', function (done) {
RedirectsAPI.upload(testUtils.context.admin)
.then(function () {
done();
})
.catch(done);
});
it('Can download', function (done) {
RedirectsAPI.download(testUtils.context.admin)
.then(function () {
done();
})
.catch(done);
});
});
describe('Editor', function () {
it('Can\'t upload', function (done) {
RedirectsAPI.upload(testUtils.context.editor)
.then(function () {
done(new Error('Editor is not allowed to upload redirects.'));
})
.catch(function (err) {
err.statusCode.should.eql(403);
done();
});
});
it('Can\'t download', function (done) {
RedirectsAPI.upload(testUtils.context.editor)
.then(function () {
done(new Error('Editor is not allowed to download redirects.'));
})
.catch(function (err) {
err.statusCode.should.eql(403);
done();
});
});
});
describe('Author', function () {
it('Can\'t upload', function (done) {
RedirectsAPI.upload(testUtils.context.author)
.then(function () {
done(new Error('Author is not allowed to upload redirects.'));
})
.catch(function (err) {
err.statusCode.should.eql(403);
done();
});
});
it('Can\'t download', function (done) {
RedirectsAPI.upload(testUtils.context.author)
.then(function () {
done(new Error('Author is not allowed to download redirects.'));
})
.catch(function (err) {
err.statusCode.should.eql(403);
done();
});
});
});
});
});

View File

@ -105,6 +105,26 @@ describe('Validation', function () {
}); });
}); });
describe('webhooks.add', function () {
it('event name is not lowercase', function () {
const webhook = models.Webhook.forge(testUtils.DataGenerator.forKnex.createWebhook({event: 'Test'}));
// NOTE: Fields with `defaultTo` are getting ignored. This is handled on the DB level.
return validation.validateSchema('webhooks', webhook, {method: 'insert'})
.then(function () {
throw new Error('Expected ValidationError.');
})
.catch(function (err) {
if (!_.isArray(err)) {
throw err;
}
err.length.should.eql(1);
err[0].errorType.should.eql('ValidationError');
});
});
});
describe('models.edit', function () { describe('models.edit', function () {
it('uuid is invalid', function () { it('uuid is invalid', function () {
const postModel = models.Post.forge({id: ObjectId.generate(), uuid: '1234'}); const postModel = models.Post.forge({id: ObjectId.generate(), uuid: '1234'});

View File

@ -1,10 +1,12 @@
var should = require('should'), var should = require('should'),
sinon = require('sinon'), sinon = require('sinon'),
_ = require('lodash'), _ = require('lodash'),
Promise = require('bluebird'),
security = require('../../../../server/lib/security'),
models = require('../../../../server/models'), models = require('../../../../server/models'),
ghostBookshelf, urlService = require('../../../../server/services/url'),
filters = require('../../../../server/filters'),
testUtils = require('../../../utils'), testUtils = require('../../../utils'),
sandbox = sinon.sandbox.create(); sandbox = sinon.sandbox.create();
describe('Models: base', function () { describe('Models: base', function () {
@ -16,7 +18,92 @@ describe('Models: base', function () {
sandbox.restore(); sandbox.restore();
}); });
describe('fn: sanitizeData', function () { describe('generateSlug', function () {
let Model;
let options = {};
beforeEach(function () {
sandbox.stub(security.string, 'safe');
sandbox.stub(filters, 'doFilter').resolves();
sandbox.stub(urlService.utils, 'getProtectedSlugs').returns(['upsi', 'schwupsi']);
Model = sandbox.stub();
Model.prototype = {
tableName: 'tableName'
};
Model.findOne = sandbox.stub();
});
it('default', function () {
Model.findOne.resolves(false);
security.string.safe.withArgs('My-Slug').returns('my-slug');
return models.Base.Model.generateSlug(Model, 'My-Slug', options)
.then((slug) => {
slug.should.eql('my-slug');
});
});
it('slug exists', function () {
let i = 0;
Model.findOne.callsFake(() => {
i = i + 1;
if (i === 1) {
return Promise.resolve(true);
}
return Promise.resolve(false);
});
security.string.safe.withArgs('My-Slug').returns('my-slug');
return models.Base.Model.generateSlug(Model, 'My-Slug', options)
.then((slug) => {
slug.should.eql('my-slug-2');
});
});
it('too long', function () {
Model.findOne.resolves(false);
const slug = new Array(500).join('a');
security.string.safe.withArgs(slug).returns(slug);
return models.Base.Model.generateSlug(Model, slug, options)
.then((slug) => {
slug.should.eql(new Array(186).join('a'));
});
});
it('protected slug', function () {
Model.findOne.resolves(false);
const slug = 'upsi';
security.string.safe.withArgs(slug).returns(slug);
return models.Base.Model.generateSlug(Model, slug, options)
.then((slug) => {
slug.should.eql('upsi-tableName');
});
});
it('internal tag', function () {
Model.findOne.resolves(false);
const slug = '#lul';
Model.prototype = {
tableName: 'tag'
};
security.string.safe.withArgs(slug).returns(slug);
return models.Base.Model.generateSlug(Model, slug, options)
.then((slug) => {
slug.should.eql('hash-#lul');
});
});
});
describe('sanitizeData', function () {
it('date is invalid', function () { it('date is invalid', function () {
const data = testUtils.DataGenerator.forKnex.createPost({updated_at: '0000-00-00 00:00:00'}); const data = testUtils.DataGenerator.forKnex.createPost({updated_at: '0000-00-00 00:00:00'});
@ -74,7 +161,7 @@ describe('Models: base', function () {
}); });
}); });
describe('fn: setEmptyValuesToNull', function () { describe('setEmptyValuesToNull', function () {
it('resets given empty value to null', function () { it('resets given empty value to null', function () {
const base = models.Base.Model.forge({a: '', b: ''}); const base = models.Base.Model.forge({a: '', b: ''});
@ -96,7 +183,7 @@ describe('Models: base', function () {
}); });
}); });
describe('static destroy()', function () { describe('destroy', function () {
it('forges model using destroyBy, fetches it, and calls destroy, passing filtered options', function () { it('forges model using destroyBy, fetches it, and calls destroy, passing filtered options', function () {
const unfilteredOptions = { const unfilteredOptions = {
destroyBy: { destroyBy: {
@ -154,7 +241,7 @@ describe('Models: base', function () {
}); });
}); });
describe('static findOne(data, unfilteredOptions)', function () { describe('findOne', function () {
it('forges model using filtered data, fetches it passing filtered options and resolves with the fetched model', function () { it('forges model using filtered data, fetches it passing filtered options and resolves with the fetched model', function () {
const data = { const data = {
id: 670 id: 670
@ -192,7 +279,7 @@ describe('Models: base', function () {
}); });
}); });
describe('static edit(data, unfilteredOptions)', function () { describe('edit', function () {
it('resolves with the savedModel after forges model w/ id, fetches w/ filtered options, saves w/ filtered data and options and method=update', function () { it('resolves with the savedModel after forges model w/ id, fetches w/ filtered options, saves w/ filtered data and options and method=update', function () {
const data = { const data = {
life: 'suffering' life: 'suffering'
@ -272,7 +359,7 @@ describe('Models: base', function () {
}); });
}); });
describe('static add(data, unfilteredOptions)', function () { describe('add', function () {
it('forges model w/ filtered data, saves w/ null and options and method=insert', function () { it('forges model w/ filtered data, saves w/ null and options and method=insert', function () {
const data = { const data = {
rum: 'ham' rum: 'ham'

View File

@ -1,5 +1,6 @@
const should = require('should'), const should = require('should'),
sinon = require('sinon'), sinon = require('sinon'),
Promise = require('bluebird'),
common = require('../../../server/lib/common'), common = require('../../../server/lib/common'),
models = require('../../../server/models'), models = require('../../../server/models'),
settingsCache = require('../../../server/services/settings/cache'), settingsCache = require('../../../server/services/settings/cache'),
@ -9,10 +10,13 @@ const should = require('should'),
describe('Unit: models/invite', function () { describe('Unit: models/invite', function () {
before(function () { before(function () {
models.init(); models.init();
});
beforeEach(function () {
sandbox.stub(settingsCache, 'get').withArgs('db_hash').returns('12345678'); sandbox.stub(settingsCache, 'get').withArgs('db_hash').returns('12345678');
}); });
after(function () { afterEach(function () {
sandbox.restore(); sandbox.restore();
}); });
@ -68,4 +72,258 @@ describe('Unit: models/invite', function () {
}); });
}); });
}); });
describe('permissible', function () {
describe('action: add', function () {
let inviteModel;
let context;
let unsafeAttrs;
let roleModel;
let loadedPermissions;
before(function () {
inviteModel = {};
context = {};
unsafeAttrs = {role_id: 'role_id'};
roleModel = sandbox.stub();
roleModel.get = sandbox.stub();
loadedPermissions = {
user: {
roles: []
}
};
});
it('role does not exist', function () {
sandbox.stub(models.Role, 'findOne').withArgs({id: 'role_id'}).resolves(null);
return models.Invite.permissible(inviteModel, 'add', context, unsafeAttrs)
.then(Promise.reject)
.catch((err) => {
(err instanceof common.errors.NotFoundError).should.eql(true);
});
});
it('invite owner', function () {
sandbox.stub(models.Role, 'findOne').withArgs({id: 'role_id'}).resolves(roleModel);
roleModel.get.withArgs('name').returns('Owner');
return models.Invite.permissible(inviteModel, 'add', context, unsafeAttrs)
.then(Promise.reject)
.catch((err) => {
(err instanceof common.errors.NoPermissionError).should.eql(true);
});
});
describe('as owner', function () {
beforeEach(function () {
loadedPermissions.user.roles = [{name: 'Owner'}];
});
it('invite administrator', function () {
sandbox.stub(models.Role, 'findOne').withArgs({id: 'role_id'}).resolves(roleModel);
roleModel.get.withArgs('name').returns('Administrator');
return models.Invite.permissible(inviteModel, 'add', context, unsafeAttrs, loadedPermissions);
});
it('invite editor', function () {
sandbox.stub(models.Role, 'findOne').withArgs({id: 'role_id'}).resolves(roleModel);
roleModel.get.withArgs('name').returns('Editor');
return models.Invite.permissible(inviteModel, 'add', context, unsafeAttrs, loadedPermissions);
});
it('invite author', function () {
sandbox.stub(models.Role, 'findOne').withArgs({id: 'role_id'}).resolves(roleModel);
roleModel.get.withArgs('name').returns('Author');
return models.Invite.permissible(inviteModel, 'add', context, unsafeAttrs, loadedPermissions);
});
it('invite contributor', function () {
sandbox.stub(models.Role, 'findOne').withArgs({id: 'role_id'}).resolves(roleModel);
roleModel.get.withArgs('name').returns('Contributor');
return models.Invite.permissible(inviteModel, 'add', context, unsafeAttrs, loadedPermissions);
});
});
describe('as administrator', function () {
beforeEach(function () {
loadedPermissions.user.roles = [{name: 'Administrator'}];
});
it('invite administrator', function () {
sandbox.stub(models.Role, 'findOne').withArgs({id: 'role_id'}).resolves(roleModel);
roleModel.get.withArgs('name').returns('Administrator');
return models.Invite.permissible(inviteModel, 'add', context, unsafeAttrs, loadedPermissions);
});
it('invite editor', function () {
sandbox.stub(models.Role, 'findOne').withArgs({id: 'role_id'}).resolves(roleModel);
roleModel.get.withArgs('name').returns('Editor');
return models.Invite.permissible(inviteModel, 'add', context, unsafeAttrs, loadedPermissions);
});
it('invite author', function () {
sandbox.stub(models.Role, 'findOne').withArgs({id: 'role_id'}).resolves(roleModel);
roleModel.get.withArgs('name').returns('Author');
return models.Invite.permissible(inviteModel, 'add', context, unsafeAttrs, loadedPermissions);
});
it('invite contributor', function () {
sandbox.stub(models.Role, 'findOne').withArgs({id: 'role_id'}).resolves(roleModel);
roleModel.get.withArgs('name').returns('Contributor');
return models.Invite.permissible(inviteModel, 'add', context, unsafeAttrs, loadedPermissions);
});
});
describe('as editor', function () {
beforeEach(function () {
loadedPermissions.user.roles = [{name: 'Editor'}];
});
it('invite administrator', function () {
sandbox.stub(models.Role, 'findOne').withArgs({id: 'role_id'}).resolves(roleModel);
roleModel.get.withArgs('name').returns('Administrator');
return models.Invite.permissible(inviteModel, 'add', context, unsafeAttrs, loadedPermissions)
.then(Promise.reject)
.catch((err) => {
(err instanceof common.errors.NoPermissionError).should.eql(true);
});
});
it('invite editor', function () {
sandbox.stub(models.Role, 'findOne').withArgs({id: 'role_id'}).resolves(roleModel);
roleModel.get.withArgs('name').returns('Editor');
return models.Invite.permissible(inviteModel, 'add', context, unsafeAttrs, loadedPermissions)
.then(Promise.reject)
.catch((err) => {
(err instanceof common.errors.NoPermissionError).should.eql(true);
});
});
it('invite author', function () {
sandbox.stub(models.Role, 'findOne').withArgs({id: 'role_id'}).resolves(roleModel);
roleModel.get.withArgs('name').returns('Author');
return models.Invite.permissible(inviteModel, 'add', context, unsafeAttrs, loadedPermissions);
});
it('invite contributor', function () {
sandbox.stub(models.Role, 'findOne').withArgs({id: 'role_id'}).resolves(roleModel);
roleModel.get.withArgs('name').returns('Contributor');
return models.Invite.permissible(inviteModel, 'add', context, unsafeAttrs, loadedPermissions);
});
});
describe('as author', function () {
beforeEach(function () {
loadedPermissions.user.roles = [{name: 'Author'}];
});
it('invite administrator', function () {
sandbox.stub(models.Role, 'findOne').withArgs({id: 'role_id'}).resolves(roleModel);
roleModel.get.withArgs('name').returns('Administrator');
return models.Invite.permissible(inviteModel, 'add', context, unsafeAttrs, loadedPermissions)
.then(Promise.reject)
.catch((err) => {
(err instanceof common.errors.NoPermissionError).should.eql(true);
});
});
it('invite editor', function () {
sandbox.stub(models.Role, 'findOne').withArgs({id: 'role_id'}).resolves(roleModel);
roleModel.get.withArgs('name').returns('Editor');
return models.Invite.permissible(inviteModel, 'add', context, unsafeAttrs, loadedPermissions)
.then(Promise.reject)
.catch((err) => {
(err instanceof common.errors.NoPermissionError).should.eql(true);
});
});
it('invite author', function () {
sandbox.stub(models.Role, 'findOne').withArgs({id: 'role_id'}).resolves(roleModel);
roleModel.get.withArgs('name').returns('Author');
return models.Invite.permissible(inviteModel, 'add', context, unsafeAttrs, loadedPermissions)
.then(Promise.reject)
.catch((err) => {
(err instanceof common.errors.NoPermissionError).should.eql(true);
});
});
it('invite contributor', function () {
sandbox.stub(models.Role, 'findOne').withArgs({id: 'role_id'}).resolves(roleModel);
roleModel.get.withArgs('name').returns('Contributor');
return models.Invite.permissible(inviteModel, 'add', context, unsafeAttrs, loadedPermissions)
.then(Promise.reject)
.catch((err) => {
(err instanceof common.errors.NoPermissionError).should.eql(true);
});
});
});
describe('as contributor', function () {
beforeEach(function () {
loadedPermissions.user.roles = [{name: 'Contributor'}];
});
it('invite administrator', function () {
sandbox.stub(models.Role, 'findOne').withArgs({id: 'role_id'}).resolves(roleModel);
roleModel.get.withArgs('name').returns('Administrator');
return models.Invite.permissible(inviteModel, 'add', context, unsafeAttrs, loadedPermissions)
.then(Promise.reject)
.catch((err) => {
(err instanceof common.errors.NoPermissionError).should.eql(true);
});
});
it('invite editor', function () {
sandbox.stub(models.Role, 'findOne').withArgs({id: 'role_id'}).resolves(roleModel);
roleModel.get.withArgs('name').returns('Editor');
return models.Invite.permissible(inviteModel, 'add', context, unsafeAttrs, loadedPermissions)
.then(Promise.reject)
.catch((err) => {
(err instanceof common.errors.NoPermissionError).should.eql(true);
});
});
it('invite author', function () {
sandbox.stub(models.Role, 'findOne').withArgs({id: 'role_id'}).resolves(roleModel);
roleModel.get.withArgs('name').returns('Author');
return models.Invite.permissible(inviteModel, 'add', context, unsafeAttrs, loadedPermissions)
.then(Promise.reject)
.catch((err) => {
(err instanceof common.errors.NoPermissionError).should.eql(true);
});
});
it('invite contributor', function () {
sandbox.stub(models.Role, 'findOne').withArgs({id: 'role_id'}).resolves(roleModel);
roleModel.get.withArgs('name').returns('Contributor');
return models.Invite.permissible(inviteModel, 'add', context, unsafeAttrs, loadedPermissions)
.then(Promise.reject)
.catch((err) => {
(err instanceof common.errors.NoPermissionError).should.eql(true);
});
});
});
});
});
}); });

View File

@ -4,16 +4,258 @@ const should = require('should'),
url = require('url'), url = require('url'),
sinon = require('sinon'), sinon = require('sinon'),
_ = require('lodash'), _ = require('lodash'),
Promise = require('bluebird'),
testUtils = require('../../utils'), testUtils = require('../../utils'),
knex = require('../../../server/data/db').knex, knex = require('../../../server/data/db').knex,
db = require('../../../server/data/db'),
urlService = require('../../../server/services/url'), urlService = require('../../../server/services/url'),
schema = require('../../../server/data/schema'), schema = require('../../../server/data/schema'),
models = require('../../../server/models'), models = require('../../../server/models'),
common = require('../../../server/lib/common'), common = require('../../../server/lib/common'),
security = require('../../../server/lib/security'), security = require('../../../server/lib/security'),
sandbox = sinon.sandbox.create(); sandbox = sinon.sandbox.create(),
userIdFor = testUtils.users.ids,
context = testUtils.context;
describe('Unit: models/post', function () { describe('Unit: models/post', function () {
const mockDb = require('mock-knex');
let tracker;
before(function () {
models.init();
mockDb.mock(knex);
tracker = mockDb.getTracker();
});
afterEach(function () {
sandbox.restore();
});
after(function () {
mockDb.unmock(knex);
});
describe('filter', function () {
it('generates correct query for - filter: tags: [photo, video] + id: -{id},limit of: 3, with related: tags', function () {
const queries = [];
tracker.install();
tracker.on('query', (query) => {
queries.push(query);
query.response([]);
});
return models.Post.findPage({
filter: 'tags: [photo, video] + id: -' + testUtils.filterData.data.posts[3].id,
limit: 3,
withRelated: ['tags']
}).then(() => {
queries.length.should.eql(2);
queries[0].sql.should.eql('select count(distinct posts.id) as aggregate from `posts` left outer join `posts_tags` on `posts_tags`.`post_id` = `posts`.`id` left outer join `tags` on `posts_tags`.`tag_id` = `tags`.`id` where (`posts`.`page` = ? and `posts`.`status` = ?) and (`tags`.`slug` in (?, ?) and `posts`.`id` != ?) order by count(tags.id) DESC');
queries[0].bindings.should.eql([
false,
'published',
'photo',
'video',
testUtils.filterData.data.posts[3].id
]);
queries[1].sql.should.eql('select `posts`.* from `posts` left outer join `posts_tags` on `posts_tags`.`post_id` = `posts`.`id` left outer join `tags` on `posts_tags`.`tag_id` = `tags`.`id` where (`posts`.`page` = ? and `posts`.`status` = ?) and (`tags`.`slug` in (?, ?) and `posts`.`id` != ?) group by `posts`.`id` order by count(tags.id) DESC, CASE WHEN posts.status = \'scheduled\' THEN 1 WHEN posts.status = \'draft\' THEN 2 ELSE 3 END ASC,CASE WHEN posts.status != \'draft\' THEN posts.published_at END DESC,posts.updated_at DESC,posts.id DESC limit ?');
queries[1].bindings.should.eql([
false,
'published',
'photo',
'video',
testUtils.filterData.data.posts[3].id,
3
]);
});
});
it('generates correct query for - filter: authors:[leslie,pat]+(tag:hash-audio,feature_image:-null), with related: authors,tags', function () {
const queries = [];
tracker.install();
tracker.on('query', (query) => {
queries.push(query);
query.response([]);
});
return models.Post.findPage({
filter: 'authors:[leslie,pat]+(tag:hash-audio,feature_image:-null)',
withRelated: ['authors', 'tags']
}).then(() => {
queries.length.should.eql(2);
queries[0].sql.should.eql('select count(distinct posts.id) as aggregate from `posts` left outer join `posts_tags` on `posts_tags`.`post_id` = `posts`.`id` left outer join `tags` on `posts_tags`.`tag_id` = `tags`.`id` left outer join `posts_authors` on `posts_authors`.`post_id` = `posts`.`id` left outer join `users` as `authors` on `posts_authors`.`author_id` = `authors`.`id` where (`posts`.`page` = ? and `posts`.`status` = ?) and (`authors`.`slug` in (?, ?) and (`tags`.`slug` = ? or `posts`.`feature_image` is not null)) order by count(authors.id) DESC');
queries[0].bindings.should.eql([
false,
'published',
'leslie',
'pat',
'hash-audio'
]);
queries[1].sql.should.eql('select `posts`.* from `posts` left outer join `posts_tags` on `posts_tags`.`post_id` = `posts`.`id` left outer join `tags` on `posts_tags`.`tag_id` = `tags`.`id` left outer join `posts_authors` on `posts_authors`.`post_id` = `posts`.`id` left outer join `users` as `authors` on `posts_authors`.`author_id` = `authors`.`id` where (`posts`.`page` = ? and `posts`.`status` = ?) and (`authors`.`slug` in (?, ?) and (`tags`.`slug` = ? or `posts`.`feature_image` is not null)) group by `posts`.`id`, `posts`.`id` order by count(authors.id) DESC, CASE WHEN posts.status = \'scheduled\' THEN 1 WHEN posts.status = \'draft\' THEN 2 ELSE 3 END ASC,CASE WHEN posts.status != \'draft\' THEN posts.published_at END DESC,posts.updated_at DESC,posts.id DESC limit ?');
queries[1].bindings.should.eql([
false,
'published',
'leslie',
'pat',
'hash-audio',
15
]);
});
});
it('generates correct query for - filter: published_at:>\'2015-07-20\', limit of: 5, with related: tags', function () {
const queries = [];
tracker.install();
tracker.on('query', (query) => {
queries.push(query);
query.response([]);
});
return models.Post.findPage({
filter: 'published_at:>\'2015-07-20\'',
limit: 5,
withRelated: ['tags']
}).then(() => {
queries.length.should.eql(2);
queries[0].sql.should.eql('select count(distinct posts.id) as aggregate from `posts` where (`posts`.`page` = ? and `posts`.`status` = ?) and (`posts`.`published_at` > ?)');
queries[0].bindings.should.eql([
false,
'published',
'2015-07-20'
]);
queries[1].sql.should.eql('select `posts`.* from `posts` where (`posts`.`page` = ? and `posts`.`status` = ?) and (`posts`.`published_at` > ?) order by CASE WHEN posts.status = \'scheduled\' THEN 1 WHEN posts.status = \'draft\' THEN 2 ELSE 3 END ASC,CASE WHEN posts.status != \'draft\' THEN posts.published_at END DESC,posts.updated_at DESC,posts.id DESC limit ?');
queries[1].bindings.should.eql([
false,
'published',
'2015-07-20',
5
]);
});
});
describe('primary_tag/primary_author', function () {
it('generates correct query for - filter: primary_tag:photo, with related: tags', function () {
const queries = [];
tracker.install();
tracker.on('query', (query) => {
queries.push(query);
query.response([]);
});
return models.Post.findPage({
filter: 'primary_tag:photo',
withRelated: ['tags']
}).then(() => {
queries.length.should.eql(2);
queries[0].sql.should.eql('select count(distinct posts.id) as aggregate from `posts` left outer join `posts_tags` on `posts_tags`.`post_id` = `posts`.`id` left outer join `tags` on `posts_tags`.`tag_id` = `tags`.`id` where (`posts`.`page` = ? and `posts`.`status` = ?) and ((`tags`.`slug` = ? and `posts_tags`.`sort_order` = ? and `tags`.`visibility` = ?))');
queries[0].bindings.should.eql([
false,
'published',
'photo',
0,
'public'
]);
queries[1].sql.should.eql('select `posts`.* from `posts` left outer join `posts_tags` on `posts_tags`.`post_id` = `posts`.`id` left outer join `tags` on `posts_tags`.`tag_id` = `tags`.`id` where (`posts`.`page` = ? and `posts`.`status` = ?) and ((`tags`.`slug` = ? and `posts_tags`.`sort_order` = ? and `tags`.`visibility` = ?)) group by `posts`.`id` order by CASE WHEN posts.status = \'scheduled\' THEN 1 WHEN posts.status = \'draft\' THEN 2 ELSE 3 END ASC,CASE WHEN posts.status != \'draft\' THEN posts.published_at END DESC,posts.updated_at DESC,posts.id DESC limit ?');
queries[1].bindings.should.eql([
false,
'published',
'photo',
0,
'public',
15
]);
});
});
it('generates correct query for - filter: primary_author:leslie, with related: authors', function () {
const queries = [];
tracker.install();
tracker.on('query', (query) => {
queries.push(query);
query.response([]);
});
return models.Post.findPage({
filter: 'primary_author:leslie',
withRelated: ['authors']
}).then(() => {
queries.length.should.eql(2);
queries[0].sql.should.eql('select count(distinct posts.id) as aggregate from `posts` left outer join `posts_authors` on `posts_authors`.`post_id` = `posts`.`id` left outer join `users` as `authors` on `posts_authors`.`author_id` = `authors`.`id` where (`posts`.`page` = ? and `posts`.`status` = ?) and ((`authors`.`slug` = ? and `posts_authors`.`sort_order` = ? and `authors`.`visibility` = ?))');
queries[0].bindings.should.eql([
false,
'published',
'leslie',
0,
'public'
]);
queries[1].sql.should.eql('select `posts`.* from `posts` left outer join `posts_authors` on `posts_authors`.`post_id` = `posts`.`id` left outer join `users` as `authors` on `posts_authors`.`author_id` = `authors`.`id` where (`posts`.`page` = ? and `posts`.`status` = ?) and ((`authors`.`slug` = ? and `posts_authors`.`sort_order` = ? and `authors`.`visibility` = ?)) group by `posts`.`id` order by CASE WHEN posts.status = \'scheduled\' THEN 1 WHEN posts.status = \'draft\' THEN 2 ELSE 3 END ASC,CASE WHEN posts.status != \'draft\' THEN posts.published_at END DESC,posts.updated_at DESC,posts.id DESC limit ?');
queries[1].bindings.should.eql([
false,
'published',
'leslie',
0,
'public',
15
]);
});
});
});
describe('bad behavior', function () {
it('generates correct query for - filter: status:[published,draft], limit of: all', function () {
const queries = [];
tracker.install();
tracker.on('query', (query) => {
queries.push(query);
query.response([]);
});
return models.Post.findPage({
filter: 'status:[published,draft]',
limit: 'all',
status: 'published',
where: {
statements: [{
prop: 'status',
op: '=',
value: 'published'
}]
}
}).then(() => {
queries.length.should.eql(2);
queries[0].sql.should.eql('select count(distinct posts.id) as aggregate from `posts` where (`posts`.`page` = ?) and (`posts`.`status` in (?, ?) and `posts`.`status` = ?)');
queries[0].bindings.should.eql([
false,
'published',
'draft',
'published'
]);
queries[1].sql.should.eql('select `posts`.* from `posts` where (`posts`.`page` = ?) and (`posts`.`status` in (?, ?) and `posts`.`status` = ?) order by CASE WHEN posts.status = \'scheduled\' THEN 1 WHEN posts.status = \'draft\' THEN 2 ELSE 3 END ASC,CASE WHEN posts.status != \'draft\' THEN posts.published_at END DESC,posts.updated_at DESC,posts.id DESC');
queries[1].bindings.should.eql([
false,
'published',
'draft',
'published'
]);
});
});
});
});
});
describe('Unit: models/post: uses database (@TODO: fix me)', function () {
before(function () { before(function () {
models.init(); models.init();
}); });
@ -34,6 +276,95 @@ describe('Unit: models/post', function () {
sandbox.restore(); sandbox.restore();
}); });
describe('processOptions', function () {
it('generates correct where statement when filter contains unpermitted values', function () {
const options = {
filter: 'status:[published,draft]',
limit: 'all',
status: 'published'
};
models.Post.processOptions(options);
options.where.statements.should.be.an.Array().with.lengthOf(1);
options.where.statements[0].should.deepEqual({
prop: 'status',
op: '=',
value: 'published'
});
});
});
describe('enforcedFilters', function () {
const enforcedFilters = function enforcedFilters(model, options) {
return new models.Post(model).enforcedFilters(options);
};
it('returns published status filter for public context', function () {
const options = {
context: {
public: true
}
};
const filter = enforcedFilters({}, options);
filter.should.equal('status:published');
});
it('returns no status filter for non public context', function () {
const options = {
context: {
internal: true
}
};
const filter = enforcedFilters({}, options);
should(filter).equal(null);
});
});
describe('defaultFilters', function () {
const defaultFilters = function defaultFilters(model, options) {
return new models.Post(model).defaultFilters(options);
};
it('returns no default filter for internal context', function () {
const options = {
context: {
internal: true
}
};
const filter = defaultFilters({}, options);
should(filter).equal(null);
});
it('returns page:false filter for public context', function () {
const options = {
context: {
public: true
}
};
const filter = defaultFilters({}, options);
filter.should.equal('page:false');
});
it('returns page:false+status:published filter for non public context', function () {
const options = {
context: 'user'
};
const filter = defaultFilters({}, options);
filter.should.equal('page:false+status:published');
});
});
describe('add', function () { describe('add', function () {
describe('ensure full set of data for model events', function () { describe('ensure full set of data for model events', function () {
it('default', function () { it('default', function () {
@ -269,6 +600,47 @@ describe('Unit: models/post', function () {
}); });
describe('edit', function () { describe('edit', function () {
it('ensure `forUpdate` works', function (done) {
const originalFn = models.Post.prototype.onSaving;
let requestCanComeIn = false;
let postId = testUtils.DataGenerator.forKnex.posts[4].id;
testUtils.DataGenerator.forKnex.posts[4].featured.should.eql(true);
// @NOTE: simulate that the onSaving hook takes longer
sandbox.stub(models.Post.prototype, 'onSaving').callsFake(function () {
var self = this,
args = arguments;
models.Post.prototype.onSaving.restore();
requestCanComeIn = true;
return Promise.delay(2000)
.then(function () {
return originalFn.apply(self, args);
});
});
const interval = setInterval(function () {
if (requestCanComeIn) {
clearInterval(interval);
// @NOTE: second call, should wait till the delay finished
models.Post.edit({title: 'Berlin'}, {id: postId, context: {internal: true}})
.then(function (post) {
post.id.should.eql(postId);
post.get('title').should.eql('Berlin');
post.get('status').should.eql('published');
post.get('featured').should.be.false();
done();
})
.catch(done);
}
}, 10);
// @NOTE: first call to db locks the row (!)
models.Post.edit({title: 'First', featured: false, status: 'published'}, _.merge({id: postId, migrating: true}, testUtils.context.editor));
});
it('update post with options.migrating', function () { it('update post with options.migrating', function () {
const events = { const events = {
post: [], post: [],

View File

@ -1,23 +1,71 @@
var should = require('should'), const should = require('should');
url = require('url'), const url = require('url');
sinon = require('sinon'), const sinon = require('sinon');
models = require('../../../server/models'), const models = require('../../../server/models');
testUtils = require('../../utils'), const testUtils = require('../../utils');
sandbox = sinon.sandbox.create(); const {knex} = require('../../../server/data/db');
describe('Unit: models/tags', function () { const sandbox = sinon.sandbox.create();
describe('Unit: models/tag', function () {
before(function () { before(function () {
models.init(); models.init();
}); });
after(function () { afterEach(function () {
sandbox.restore(); sandbox.restore();
}); });
before(testUtils.teardown); describe('SQL', function () {
before(testUtils.setup('tags')); const mockDb = require('mock-knex');
let tracker;
before(function () {
mockDb.mock(knex);
tracker = mockDb.getTracker();
});
after(function () {
sandbox.restore();
});
after(function () {
mockDb.unmock(knex);
});
it('generates correct query for - filter: count.posts:>=1, order: count.posts DESC, limit of: all, withRelated: count.posts', function () {
const queries = [];
tracker.install();
tracker.on('query', (query) => {
queries.push(query);
query.response([]);
});
return models.Tag.findPage({
filter: 'count.posts:>=1',
order: 'count.posts DESC',
limit: 'all',
withRelated: ['count.posts']
}).then(() => {
queries.length.should.eql(2);
queries[0].sql.should.eql('select count(distinct tags.id) as aggregate from `tags` where `count`.`posts` >= ?');
queries[0].bindings.should.eql([
1
]);
queries[1].sql.should.eql('select `tags`.*, (select count(`posts`.`id`) from `posts` left outer join `posts_tags` on `posts`.`id` = `posts_tags`.`post_id` where posts_tags.tag_id = tags.id) as `count__posts` from `tags` where `count`.`posts` >= ? order by `count__posts` DESC');
queries[1].bindings.should.eql([
1
]);
});
});
});
describe('Edit', function () { describe('Edit', function () {
before(testUtils.teardown);
before(testUtils.setup('tags'));
it('resets given empty value to null', function () { it('resets given empty value to null', function () {
return models.Tag.findOne({slug: 'kitchen-sink'}) return models.Tag.findOne({slug: 'kitchen-sink'})
.then(function (tag) { .then(function (tag) {

View File

@ -1,9 +1,11 @@
const should = require('should'), const should = require('should'),
url = require('url'), url = require('url'),
sinon = require('sinon'), sinon = require('sinon'),
Promise = require('bluebird'),
_ = require('lodash'), _ = require('lodash'),
schema = require('../../../server/data/schema'), schema = require('../../../server/data/schema'),
models = require('../../../server/models'), models = require('../../../server/models'),
permissions = require('../../../server/services/permissions'),
validation = require('../../../server/data/validation'), validation = require('../../../server/data/validation'),
common = require('../../../server/lib/common'), common = require('../../../server/lib/common'),
security = require('../../../server/lib/security'), security = require('../../../server/lib/security'),
@ -149,15 +151,16 @@ describe('Unit: models/user', function () {
}); });
}); });
describe('fn: permissible', function () { describe('permissible', function () {
function getUserModel(id, role) { function getUserModel(id, role, roleId) {
var hasRole = sandbox.stub(); var hasRole = sandbox.stub();
hasRole.withArgs(role).returns(true); hasRole.withArgs(role).returns(true);
return { return {
id: id,
hasRole: hasRole, hasRole: hasRole,
related: sandbox.stub().returns([{name: role}]), related: sandbox.stub().returns([{name: role, id: roleId}]),
get: sandbox.stub().returns(id) get: sandbox.stub().returns(id)
}; };
} }
@ -184,6 +187,134 @@ describe('Unit: models/user', function () {
}); });
}); });
it('cannot edit my status to inactive', function () {
var mockUser = getUserModel(3, 'Editor'),
context = {user: 3};
return models.User.permissible(mockUser, 'edit', context, {status: 'inactive'}, testUtils.permissions.editor, false, true)
.then(Promise.reject)
.catch((err) => {
err.should.be.an.instanceof(common.errors.NoPermissionError);
});
});
it('without related roles', function () {
sandbox.stub(models.User, 'findOne').withArgs({
id: 3,
status: 'all'
}, {withRelated: ['roles']}).resolves(getUserModel(3, 'Contributor'));
const mockUser = {id: 3, related: sandbox.stub().returns()};
const context = {user: 3};
return models.User.permissible(mockUser, 'edit', context, {}, testUtils.permissions.contributor, false, true)
.then(() => {
models.User.findOne.calledOnce.should.be.true();
});
});
describe('change role', function () {
function getUserToEdit(id, role) {
var hasRole = sandbox.stub();
hasRole.withArgs(role).returns(true);
return {
id: id,
hasRole: hasRole,
related: sandbox.stub().returns([role]),
get: sandbox.stub().returns(id)
};
}
beforeEach(function () {
sandbox.stub(models.User, 'getOwnerUser');
sandbox.stub(permissions, 'canThis');
models.User.getOwnerUser.resolves({
id: testUtils.context.owner.context.user,
related: () => {
return {
at: () => {
return testUtils.permissions.owner.user.roles[0].id;
}
};
}
});
});
it('cannot change own role', function () {
const mockUser = getUserToEdit(testUtils.context.admin.context.user, testUtils.permissions.editor.user.roles[0]);
const context = testUtils.context.admin.context;
const unsafeAttrs = testUtils.permissions.editor.user;
return models.User.permissible(mockUser, 'edit', context, unsafeAttrs, testUtils.permissions.admin, false, true)
.then(Promise.reject)
.catch((err) => {
err.should.be.an.instanceof(common.errors.NoPermissionError);
});
});
it('is owner and does not change the role', function () {
const mockUser = getUserToEdit(testUtils.context.owner.context.user, testUtils.permissions.owner.user.roles[0]);
const context = testUtils.context.owner.context;
const unsafeAttrs = testUtils.permissions.owner.user;
return models.User.permissible(mockUser, 'edit', context, unsafeAttrs, testUtils.permissions.owner, false, true)
.then(() => {
models.User.getOwnerUser.calledOnce.should.be.true();
});
});
it('cannot change owner\'s role', function () {
const mockUser = getUserToEdit(testUtils.context.owner.context.user, testUtils.permissions.owner.user.roles[0]);
const context = testUtils.context.admin.context;
const unsafeAttrs = testUtils.permissions.editor.user;
return models.User.permissible(mockUser, 'edit', context, unsafeAttrs, testUtils.permissions.admin, false, true)
.then(Promise.reject)
.catch((err) => {
err.should.be.an.instanceof(common.errors.NoPermissionError);
});
});
it('admin can change author role', function () {
const mockUser = getUserToEdit(testUtils.context.author.context.user, testUtils.permissions.author.user.roles[0]);
const context = testUtils.context.admin.context;
const unsafeAttrs = testUtils.permissions.editor.user;
permissions.canThis.returns({
assign: {
role: sandbox.stub().resolves()
}
});
return models.User.permissible(mockUser, 'edit', context, unsafeAttrs, testUtils.permissions.admin, true, true)
.then(() => {
models.User.getOwnerUser.calledOnce.should.be.true();
permissions.canThis.calledOnce.should.be.true();
});
});
it('author can\'t change admin role', function () {
const mockUser = getUserToEdit(testUtils.context.admin.context.user, testUtils.permissions.admin.user.roles[0]);
const context = testUtils.context.author.context;
const unsafeAttrs = testUtils.permissions.editor.user;
permissions.canThis.returns({
assign: {
role: sandbox.stub().resolves()
}
});
return models.User.permissible(mockUser, 'edit', context, unsafeAttrs, testUtils.permissions.author, false, true)
.then(Promise.reject)
.catch((err) => {
err.should.be.an.instanceof(common.errors.NoPermissionError);
});
});
});
describe('as editor', function () { describe('as editor', function () {
it('can\'t edit another editor', function (done) { it('can\'t edit another editor', function (done) {
var mockUser = getUserModel(3, 'Editor'), var mockUser = getUserModel(3, 'Editor'),
@ -199,6 +330,20 @@ describe('Unit: models/user', function () {
}); });
}); });
it('can\'t edit owner', function (done) {
var mockUser = getUserModel(3, 'Owner'),
context = {user: 2};
models.User.permissible(mockUser, 'edit', context, {}, testUtils.permissions.editor, true, true).then(() => {
done(new Error('Permissible function should have errored'));
}).catch((error) => {
error.should.be.an.instanceof(common.errors.NoPermissionError);
should(mockUser.hasRole.called).be.true();
should(mockUser.get.calledOnce).be.true();
done();
});
});
it('can\'t edit an admin', function (done) { it('can\'t edit an admin', function (done) {
var mockUser = getUserModel(3, 'Administrator'), var mockUser = getUserModel(3, 'Administrator'),
context = {user: 2}; context = {user: 2};
@ -374,4 +519,87 @@ describe('Unit: models/user', function () {
}); });
}); });
}); });
describe('transferOwnership', function () {
let ownerRole;
beforeEach(function () {
ownerRole = sandbox.stub();
sandbox.stub(models.Role, 'findOne');
models.Role.findOne
.withArgs({name: 'Owner'})
.resolves(testUtils.permissions.owner.user.roles[0]);
models.Role.findOne
.withArgs({name: 'Administrator'})
.resolves(testUtils.permissions.admin.user.roles[0]);
sandbox.stub(models.User, 'findOne');
});
it('Cannot transfer ownership if not owner', function () {
const loggedInUser = testUtils.context.admin;
const userToChange = loggedInUser;
const contextUser = sandbox.stub();
contextUser.toJSON = sandbox.stub().returns(testUtils.permissions.admin.user);
models.User
.findOne
.withArgs({id: loggedInUser.context.user}, {withRelated: ['roles']})
.resolves(contextUser);
return models.User.transferOwnership({id: loggedInUser.context.user}, loggedInUser)
.then(Promise.reject)
.catch((err) => {
err.should.be.an.instanceof(common.errors.NoPermissionError);
});
});
it('Owner tries to transfer ownership to author', function () {
const loggedInUser = testUtils.context.owner;
const userToChange = testUtils.context.editor;
const contextUser = sandbox.stub();
contextUser.toJSON = sandbox.stub().returns(testUtils.permissions.owner.user);
models.User
.findOne
.withArgs({id: loggedInUser.context.user}, {withRelated: ['roles']})
.resolves(contextUser);
models.User
.findOne
.withArgs({id: userToChange.context.user}, {withRelated: ['roles']})
.resolves(contextUser);
return models.User.transferOwnership({id: userToChange.context.user}, loggedInUser)
.then(Promise.reject)
.catch((err) => {
err.should.be.an.instanceof(common.errors.ValidationError);
});
});
});
describe('isSetup', function () {
it('active', function () {
sandbox.stub(models.User, 'getOwnerUser').resolves({get: sandbox.stub().returns('active')});
return models.User.isSetup()
.then((result) => {
result.should.be.true();
});
});
it('inactive', function () {
sandbox.stub(models.User, 'getOwnerUser').resolves({get: sandbox.stub().returns('inactive')});
return models.User.isSetup()
.then((result) => {
result.should.be.false();
});
});
});
}); });

View File

@ -0,0 +1,28 @@
const should = require('should');
const url = require('url');
const sinon = require('sinon');
const models = require('../../../server/models');
const testUtils = require('../../utils');
const {knex} = require('../../../server/data/db');
const sandbox = sinon.sandbox.create();
describe('Unit: models/webhooks', function () {
before(function () {
models.init();
});
after(function () {
sandbox.restore();
});
before(testUtils.teardown);
before(testUtils.setup('webhooks'));
it('can correctly use getByEventAndTarget', function () {
return models.Webhook.getByEventAndTarget('subscriber.added', 'https://example.com/webhooks/subscriber-added')
.then(function (webhook) {
webhook.get('event').should.eql('subscriber.added');
webhook.get('target_url').should.eql('https://example.com/webhooks/subscriber-added');
});
});
});

View File

@ -55,7 +55,8 @@ var _ = require('lodash'),
notification: ['type', 'message', 'status', 'id', 'dismissible', 'location', 'custom'], notification: ['type', 'message', 'status', 'id', 'dismissible', 'location', 'custom'],
theme: ['name', 'package', 'active'], theme: ['name', 'package', 'active'],
themes: ['themes'], themes: ['themes'],
invites: _(schema.invites).keys().without('token').value(), invites: ['invites', 'meta'],
invite: _(schema.invites).keys().without('token').value(),
webhook: _.keys(schema.webhooks) webhook: _.keys(schema.webhooks)
}; };

View File

@ -303,6 +303,16 @@ fixtures = {
}); });
}, },
createInactiveUser() {
const user = DataGenerator.forKnex.createUser({
email: 'inactive@test.org',
slug: 'inactive',
status: 'inactive'
});
return models.User.add(user, module.exports.context.internal);
},
createExtraUsers: function createExtraUsers() { createExtraUsers: function createExtraUsers() {
// grab 3 more users // grab 3 more users
var extraUsers = _.cloneDeep(DataGenerator.Content.users.slice(2, 6)); var extraUsers = _.cloneDeep(DataGenerator.Content.users.slice(2, 6));
@ -596,6 +606,9 @@ toDoList = {
'users:no-owner': function createUsersWithoutOwner() { 'users:no-owner': function createUsersWithoutOwner() {
return fixtures.createUsersWithoutOwner(); return fixtures.createUsersWithoutOwner();
}, },
'user:inactive': function createInactiveUser() {
return fixtures.createInactiveUser();
},
'users:extra': function createExtraUsers() { 'users:extra': function createExtraUsers() {
return fixtures.createExtraUsers(); return fixtures.createExtraUsers();
}, },
@ -906,7 +919,33 @@ startGhost = function startGhost(options) {
web.shared.middlewares.customRedirects.reload(); web.shared.middlewares.customRedirects.reload();
common.events.emit('server.start'); common.events.emit('server.start');
return ghostServer;
/**
* @TODO: this is dirty, but makes routing testing a lot easier for now, because the routing test
* has no easy way to access existing resource id's, which are added from the Ghost fixtures.
* I can do `testUtils.existingData.roles[0].id`.
*/
module.exports.existingData = {};
return models.Role.findAll({columns: ['id']})
.then((roles) => {
module.exports.existingData.roles = roles.toJSON();
return models.Client.findAll({columns: ['id', 'secret']});
})
.then((clients) => {
module.exports.existingData.clients = clients.toJSON();
return models.User.findAll({columns: ['id']});
})
.then((users) => {
module.exports.existingData.users = users.toJSON();
return models.Tag.findAll({columns: ['id']});
})
.then((tags) => {
module.exports.existingData.tags = tags.toJSON();
})
.return(ghostServer);
}); });
} }
@ -917,6 +956,8 @@ startGhost = function startGhost(options) {
} }
}) })
.then(function initialiseDatabase() { .then(function initialiseDatabase() {
settingsCache.shutdown();
settingsCache.reset();
return knexMigrator.init(); return knexMigrator.init();
}) })
.then(function initializeGhost() { .then(function initializeGhost() {
@ -953,7 +994,32 @@ startGhost = function startGhost(options) {
}); });
}) })
.then(function returnGhost() { .then(function returnGhost() {
return ghostServer; /**
* @TODO: this is dirty, but makes routing testing a lot easier for now, because the routing test
* has no easy way to access existing resource id's, which are added from the Ghost fixtures.
* I can do `testUtils.existingData.roles[0].id`.
*/
module.exports.existingData = {};
return models.Role.findAll({columns: ['id']})
.then((roles) => {
module.exports.existingData.roles = roles.toJSON();
return models.Client.findAll({columns: ['id', 'secret']});
})
.then((clients) => {
module.exports.existingData.clients = clients.toJSON();
return models.User.findAll({columns: ['id']});
})
.then((users) => {
module.exports.existingData.users = users.toJSON();
return models.Tag.findAll({columns: ['id']});
})
.then((tags) => {
module.exports.existingData.tags = tags.toJSON();
})
.return(ghostServer);
}); });
}; };

View File

@ -128,6 +128,7 @@
"matchdep": "2.0.0", "matchdep": "2.0.0",
"minimist": "1.2.0", "minimist": "1.2.0",
"mocha": "4.1.0", "mocha": "4.1.0",
"mock-knex": "0.4.2",
"nock": "9.4.0", "nock": "9.4.0",
"proxyquire": "2.1.0", "proxyquire": "2.1.0",
"rewire": "3.0.2", "rewire": "3.0.2",

View File

@ -3652,6 +3652,10 @@ lodash@4.17.10, lodash@^4.13.1, lodash@^4.16.4, lodash@^4.17.10, lodash@^4.17.4,
version "4.17.10" version "4.17.10"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
lodash@^4.14.2:
version "4.17.11"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
lodash@~0.9.2: lodash@~0.9.2:
version "0.9.2" version "0.9.2"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-0.9.2.tgz#8f3499c5245d346d682e5b0d3b40767e09f1a92c" resolved "https://registry.yarnpkg.com/lodash/-/lodash-0.9.2.tgz#8f3499c5245d346d682e5b0d3b40767e09f1a92c"
@ -3996,6 +4000,14 @@ mocha@^3.1.2:
mkdirp "0.5.1" mkdirp "0.5.1"
supports-color "3.1.2" supports-color "3.1.2"
mock-knex@0.4.2:
version "0.4.2"
resolved "https://registry.yarnpkg.com/mock-knex/-/mock-knex-0.4.2.tgz#6a7c1941908dd6b9761c24a071c392b56c20bb5f"
dependencies:
bluebird "^3.4.1"
lodash "^4.14.2"
semver "^5.3.0"
module-not-found-error@^1.0.0: module-not-found-error@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/module-not-found-error/-/module-not-found-error-1.0.1.tgz#cf8b4ff4f29640674d6cdd02b0e3bc523c2bbdc0" resolved "https://registry.yarnpkg.com/module-not-found-error/-/module-not-found-error-1.0.1.tgz#cf8b4ff4f29640674d6cdd02b0e3bc523c2bbdc0"