Ghost/ghost/update-check-service/test/update-check-service.test.js
Hannah Wolfe 6161f94910
Updated to use assert/strict everywhere (#17047)
refs: https://github.com/TryGhost/Toolbox/issues/595

We're rolling out new rules around the node assert library, the first of which is enforcing the use of assert/strict. This means we don't need to use the strict version of methods, as the standard version will work that way by default.

This caught some gotchas in our existing usage of assert where the lack of strict mode had unexpected results:
- Url matching needs to be done on `url.href` see aa58b354a4
- Null and undefined are not the same thing,  there were a few cases of this being confused
- Particularly questionable changes in [PostExporter tests](c1a468744b) tracked [here](https://github.com/TryGhost/Team/issues/3505).
- A typo see eaac9c293a

Moving forward, using assert strict should help us to catch unexpected behaviour, particularly around nulls and undefineds during implementation.
2023-06-21 09:56:59 +01:00

413 lines
15 KiB
JavaScript

require('./utils');
const sinon = require('sinon');
const moment = require('moment');
const uuid = require('uuid');
const assert = require('assert/strict');
const logging = require('@tryghost/logging');
const UpdateCheckService = require('../lib/UpdateCheckService');
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.');
}
});
});
});