Ghost/ghost/update-check-service/test/update-check-service.test.js
Naz 96a64b4982
Added error rethrow to update check run as a job
closes https://github.com/TryGhost/Toolbox/issues/525

- When the update check is run as an offloaded job (in a worker thread) the errors that are handled over "logger.error" do not get bubbled up to the job manager to be handled fully. With an optional "rethrowErrors" flag it's not possible to throw errors when the update check is run as a "job" and handle as usual when run in the main thread (through the admin route trigger)
2023-02-27 19:48:42 +08:00

413 lines
15 KiB
JavaScript

require('./utils');
const sinon = require('sinon');
const moment = require('moment');
const uuid = require('uuid');
const assert = require('assert');
const logging = require('@tryghost/logging');
const UpdateCheckService = require('../lib/update-check-service');
describe('Update Check', function () {
const internal = {context: {internal: true}};
let settingsStub;
let requestStub;
beforeEach(function () {
settingsStub = sinon.stub().resolves({
settings: []
});
settingsStub.withArgs(Object.assign({key: 'db_hash'}, internal)).resolves({
settings: [{
value: 'dummy_db_hash'
}]
});
settingsStub.withArgs(Object.assign({key: 'active_theme'}, internal)).resolves({
settings: [{
value: 'casperito'
}]
});
sinon.stub(logging, 'error');
requestStub = sinon.stub();
});
afterEach(function () {
sinon.restore();
});
describe('UpdateCheck execution', function () {
it('update check was executed', async function () {
const updateCheckService = new UpdateCheckService({
api: {
settings: {
read: settingsStub,
edit: settingsStub
},
users: {
browse: sinon.stub().resolves()
},
posts: {
browse: sinon.stub().resolves()
}
},
config: {
checkEndpoint: 'https://updates.ghost.org',
siteUrl: 'https://localhost:2368/test',
isPrivacyDisabled: true,
ghostVersion: '0.8.0'
},
request: requestStub
});
await updateCheckService.check();
requestStub.calledOnce.should.equal(true);
requestStub.args[0][0].should.equal('https://updates.ghost.org');
requestStub.args[0][1].query.ghost_version.should.equal('0.8.0');
});
it('update check won\'t happen if it\'s too early', async function () {
const lateSettingStub = sinon.stub().resolves({
settings: [{
value: moment().add('10', 'minutes').unix()
}]
});
const updateCheckService = new UpdateCheckService({
api: {
settings: {
read: lateSettingStub
}
},
config: {},
request: requestStub
});
await updateCheckService.check();
requestStub.called.should.equal(false);
});
it('update check will happen if it\'s time to check', async function () {
const updateCheckDelayPassed = sinon.stub().resolves({
settings: [{
value: moment().subtract('10', 'minutes').unix()
}]
});
const updateCheckService = new UpdateCheckService({
api: {
settings: {
read: updateCheckDelayPassed,
edit: settingsStub
},
users: {
browse: sinon.stub().resolves()
},
posts: {
browse: sinon.stub().resolves()
}
},
config: {
checkEndpoint: 'https://updates.ghost.org',
siteUrl: 'https://example.com',
isPrivacyDisabled: true,
ghostVersion: '5.3.4'
},
request: requestStub
});
await updateCheckService.check();
requestStub.calledOnce.should.equal(true);
requestStub.args[0][0].should.equal('https://updates.ghost.org');
requestStub.args[0][1].query.ghost_version.should.equal('5.3.4');
});
});
describe('Data sent with the POST request', function () {
it('should report the correct data', async function () {
const updateCheckService = new UpdateCheckService({
api: {
settings: {
read: settingsStub,
edit: settingsStub
},
users: {
browse: sinon.stub().resolves({
users: [{
created_at: '1995-12-24T23:15:00Z'
}, {}]
})
},
posts: {
browse: sinon.stub().resolves({
meta: {
pagination: {
total: 13
}
}
})
}
},
config: {
checkEndpoint: 'https://updates.ghost.org',
siteUrl: 'https://localhost:2368/test',
isPrivacyDisabled: false,
env: process.env.NODE_ENV,
databaseType: 'mysql',
ghostVersion: '4.0.0'
},
request: requestStub,
ghostMailer: {
send: sinon.stub().resolves()
}
});
await updateCheckService.check();
requestStub.calledOnce.should.equal(true);
requestStub.args[0][0].should.equal('https://updates.ghost.org');
const data = requestStub.args[0][1].body;
data.ghost_version.should.equal('4.0.0');
data.node_version.should.equal(process.versions.node);
data.env.should.equal(process.env.NODE_ENV);
data.database_type.should.match(/sqlite3|mysql/);
data.blog_id.should.be.a.String();
data.blog_id.should.not.be.empty();
data.theme.should.be.equal('casperito');
data.blog_created_at.should.equal(819846900);
data.user_count.should.be.equal(2);
data.post_count.should.be.equal(13);
data.npm_version.should.be.a.String();
data.npm_version.should.not.be.empty();
});
});
describe('Notifications', function () {
it('should create a release notification for target version', async function () {
const notification = {
id: 1,
custom: 0,
messages: [{
id: uuid.v4(),
version: '999.9.x',
content: '<p>Hey there! This is for 999.9.0 version</p>',
dismissible: true,
top: true
}]
};
const notificationsAPIAddStub = sinon.stub().resolves();
const usersBrowseStub = sinon.stub().resolves({
users: [{
roles: [{
name: 'Owner'
}]
}]
});
const updateCheckService = new UpdateCheckService({
api: {
settings: {
read: settingsStub,
edit: settingsStub
},
users: {
browse: usersBrowseStub
},
posts: {
browse: sinon.stub().resolves()
},
notifications: {
add: notificationsAPIAddStub
}
},
config: {
siteUrl: 'https://localhost:2368/test'
},
request: sinon.stub().resolves({
body: {
notifications: [notification]
}
})
});
await updateCheckService.check();
notificationsAPIAddStub.calledOnce.should.equal(true);
notificationsAPIAddStub.args[0][0].notifications.length.should.equal(1);
const targetNotification = notificationsAPIAddStub.args[0][0].notifications[0];
targetNotification.dismissible.should.eql(notification.messages[0].dismissible);
targetNotification.id.should.eql(notification.messages[0].id);
targetNotification.top.should.eql(notification.messages[0].top);
targetNotification.type.should.eql('info');
targetNotification.message.should.eql(notification.messages[0].content);
usersBrowseStub.calledTwice.should.eql(true);
// Second (non statistical) call should be looking for admin users with an 'active' status only
usersBrowseStub.args[1][0].should.eql({
limit: 'all',
include: ['roles'],
filter: 'status:active',
context: {
internal: true
}
});
});
it('should send an email for critical notification', async function () {
const notification = {
id: 1,
messages: [{
id: uuid.v4(),
version: 'custom1',
content: '<p>Critical message. Upgrade your site!</p>',
dismissible: false,
top: true,
type: 'alert'
}]
};
const notificationsAPIAddStub = sinon.stub().resolves();
const sendEmailStub = sinon.stub().resolves();
const updateCheckService = new UpdateCheckService({
api: {
settings: {
read: settingsStub,
edit: settingsStub
},
users: {
browse: sinon.stub().resolves({
users: [{
email: 'jbloggs@example.com',
roles: [{
name: 'Owner'
}]
}]
})
},
posts: {
browse: sinon.stub().resolves()
},
notifications: {
add: notificationsAPIAddStub
}
},
config: {
siteUrl: 'http://127.0.0.1:2369'
},
request: sinon.stub().resolves({
body: [notification]
}),
sendEmail: sendEmailStub
});
await updateCheckService.check();
sendEmailStub.called.should.be.true();
sendEmailStub.args[0][0].to.should.equal('jbloggs@example.com');
sendEmailStub.args[0][0].subject.should.equal('Action required: Critical alert from Ghost instance http://127.0.0.1:2369');
sendEmailStub.args[0][0].html.should.equal('<p>Critical message. Upgrade your site!</p>');
sendEmailStub.args[0][0].forceTextContent.should.equal(true);
notificationsAPIAddStub.calledOnce.should.equal(true);
notificationsAPIAddStub.args[0][0].notifications.length.should.equal(1);
});
it('not create a notification if the check response has no messages', async function () {
const notificationsAPIAddStub = sinon.stub().resolves();
const updateCheckService = new UpdateCheckService({
api: {
settings: {
read: settingsStub,
edit: settingsStub
},
users: {
browse: sinon.stub().resolves({
users: [{
roles: [{
name: 'Owner'
}]
}]
})
},
posts: {
browse: sinon.stub().resolves()
},
notifications: {
add: notificationsAPIAddStub
}
},
config: {
siteUrl: 'https://localhost:2368/test'
},
request: sinon.stub().resolves({
body: {
notifications: []
}
})
});
await updateCheckService.check();
notificationsAPIAddStub.calledOnce.should.equal(false);
});
});
describe('Error handling', function () {
it('logs an error when error', function () {
const updateCheckService = new UpdateCheckService({
api: {
settings: {
edit: settingsStub
}
},
config: {}
});
updateCheckService.updateCheckError({});
settingsStub.called.should.be.true();
logging.error.called.should.be.true();
logging.error.args[0][0].context.should.equal('Checking for updates failed, your site will continue to function.');
logging.error.args[0][0].help.should.equal('If you get this error repeatedly, please seek help from https://ghost.org/docs/');
});
it('logs and rethrows an error when error with rethrow configuration', function () {
const updateCheckService = new UpdateCheckService({
api: {
settings: {
edit: settingsStub
}
},
config: {
rethrowErrors: true
}
});
try {
updateCheckService.updateCheckError({});
assert.fail('should have thrown');
} catch (e) {
settingsStub.called.should.be.true();
logging.error.called.should.be.true();
logging.error.args[0][0].context.should.equal('Checking for updates failed, your site will continue to function.');
logging.error.args[0][0].help.should.equal('If you get this error repeatedly, please seek help from https://ghost.org/docs/');
e.context.should.equal('Checking for updates failed, your site will continue to function.');
}
});
});
});