Ghost/ghost/external-media-inliner/test/ExternalMediaInliner.test.js

676 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const assert = require('assert/strict');
const fs = require('fs');
const sinon = require('sinon');
const nock = require('nock');
const path = require('path');
const loggingLib = require('@tryghost/logging');
const ExternalMediaInliner = require('../index');
describe('ExternalMediaInliner', function () {
let logging;
let ghostLogoPng;
let exeFile;
let GIF1x1;
let postModelStub;
let postMetaModelStub;
let tagModelStub;
let userModelStub;
beforeEach(function () {
// use a 1x1 gif in nock responses because it's really small and easy to work with
ghostLogoPng = fs.readFileSync(path.join(__dirname, 'fixtures', 'ghost-logo.png'));
exeFile = fs.readFileSync(path.join(__dirname, 'fixtures', 'fixture.exe'));
GIF1x1 = Buffer.from('R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==', 'base64');
logging = {
info: sinon.stub(loggingLib, 'info'),
error: sinon.stub(loggingLib, 'error'),
warn: sinon.stub(loggingLib, 'warn')
};
postModelStub = {
findPage: sinon.stub().resolves({
data: []
}),
edit: sinon.stub().resolves()
};
postMetaModelStub = {
findPage: sinon.stub().resolves({
data: []
}),
edit: sinon.stub().resolves()
};
tagModelStub = {
findPage: sinon.stub().resolves({
data: []
}),
edit: sinon.stub().resolves()
};
userModelStub = {
findPage: sinon.stub().resolves({
data: []
}),
edit: sinon.stub().resolves()
};
});
afterEach(function () {
sinon.restore();
nock.cleanAll();
});
it('Creates an External Media Inliner instance', function () {
assert.ok(new ExternalMediaInliner({}));
});
describe('inline', function () {
it('inlines image in the post\'s mobiledoc content', async function () {
const imageURL = 'https://img.stockfresh.com/files/f/image.jpg';
const requestMock = nock('https://img.stockfresh.com')
.get('/files/f/image.jpg')
.reply(200, GIF1x1);
const postModelInstanceStub = {
id: 'inlined-post-id',
get: sinon.stub()
.withArgs('mobiledoc')
.returns(`{"version":"0.3.1","atoms":[],"cards":[["image",{"src":"${imageURL}"}]]}`)
};
postModelStub = {
findPage: sinon.stub().returns({
data: [postModelInstanceStub]
}),
edit: sinon.stub().resolves()
};
sinon.stub(path, 'relative')
.withArgs('/content/images', '/content/images/unique-image.jpg')
.returns('unique-image.jpg');
const inliner = new ExternalMediaInliner({
PostModel: postModelStub,
PostMetaModel: postMetaModelStub,
TagModel: tagModelStub,
UserModel: userModelStub,
getMediaStorage: sinon.stub().withArgs('.jpg').returns({
getTargetDir: () => '/content/images',
getUniqueFileName: () => '/content/images/unique-image.jpg',
saveRaw: () => '/content/images/unique-image.jpg'
})
});
await inliner.inline(['https://img.stockfresh.com']);
assert.ok(requestMock.isDone());
assert.ok(postModelStub.edit.calledOnce);
assert.ok(postModelStub.edit.calledWith({
mobiledoc: '{"version":"0.3.1","atoms":[],"cards":[["image",{"src":"__GHOST_URL__/content/images/unique-image.jpg"}]]}'
}, {
id: 'inlined-post-id',
context: {
internal: true
}
}));
});
it('inlines the image from post\'s mobiledoc containing html card', async function () {
const imageURL = 'https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/39719fcb-5af0-4764-bf8b-d375f37a09e5_1141x860';
const requestMock = nock('https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com')
.get('/public/images/39719fcb-5af0-4764-bf8b-d375f37a09e5_1141x860')
.reply(200, GIF1x1);
const postModelInstanceStub = {
id: 'inlined-post-with-htmlcard-id',
get: sinon.stub()
.withArgs('mobiledoc')
.returns(`{"version":"0.3.1","atoms":[],"cards":[["html",{"html":"<img src="${imageURL}" alt="Lorem ipsum">"}]],"markups":[],"sections":[[10,0],[1,"p",[]]],"ghostVersion":"4.0"}`)
};
postModelStub = {
findPage: sinon.stub().returns({
data: [postModelInstanceStub]
}),
edit: sinon.stub().resolves()
};
sinon.stub(path, 'relative')
.withArgs('/content/images', '/content/images/unique-image.jpg')
.returns('unique-image.jpg');
const inliner = new ExternalMediaInliner({
PostModel: postModelStub,
PostMetaModel: postMetaModelStub,
TagModel: tagModelStub,
UserModel: userModelStub,
getMediaStorage: sinon.stub().withArgs('.jpg').returns({
getTargetDir: () => '/content/images',
getUniqueFileName: () => '/content/images/unique-image.jpg',
saveRaw: () => '/content/images/unique-image.jpg'
})
});
await inliner.inline(['https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com']);
assert.ok(requestMock.isDone());
assert.ok(postModelStub.edit.calledOnce);
assert.deepEqual(postModelStub.edit.args[0][0], {
mobiledoc: `{"version":"0.3.1","atoms":[],"cards":[["html",{"html":"<img src="__GHOST_URL__/content/images/unique-image.jpg" alt="Lorem ipsum">"}]],"markups":[],"sections":[[10,0],[1,"p",[]]],"ghostVersion":"4.0"}`
});
assert.deepEqual(postModelStub.edit.args[0][1], {
id: 'inlined-post-with-htmlcard-id',
context: {
internal: true
}
});
});
it('logs an error when fetching an external media fails', async function () {
const imageURL = 'https://img.stockfresh.com/files/f/image.jpg';
const requestMock = nock('https://img.stockfresh.com')
.get('/files/f/image.jpg')
.reply(404);
const postModelInstanceStub = {
id: 'inlined-post-id',
get: sinon.stub()
.withArgs('mobiledoc')
.returns(`{"version":"0.3.1","atoms":[],"cards":[["image",{"src":"${imageURL}"}]]}`)
};
postModelStub = {
findPage: sinon.stub().returns({
data: [postModelInstanceStub]
})
};
const inliner = new ExternalMediaInliner({
PostModel: postModelStub,
PostMetaModel: postMetaModelStub,
TagModel: tagModelStub,
UserModel: userModelStub
});
await inliner.inline(['https://img.stockfresh.com']);
assert.ok(requestMock.isDone());
assert.equal(logging.error.args[0][0], 'Error downloading remote media: https://img.stockfresh.com/files/f/image.jpg');
});
it('logs an error when fetching an external media for simple fields fails', async function () {
const imageURL = 'https://img.stockfresh.com/files/f/simple-image.jpg';
const requestMock = nock('https://img.stockfresh.com')
.get('/files/f/simple-image.jpg')
.reply(500);
const userModelInstanceStub = {
id: 'inlined-user-id',
get: sinon.stub()
.withArgs('profile_image')
.returns(imageURL)
};
userModelStub = {
findPage: sinon.stub().returns({
data: [userModelInstanceStub]
})
};
const inliner = new ExternalMediaInliner({
PostModel: postModelStub,
PostMetaModel: postMetaModelStub,
TagModel: tagModelStub,
UserModel: userModelStub
});
await inliner.inline(['https://img.stockfresh.com']);
assert.ok(requestMock.isDone());
assert.equal(logging.error.args[0][0], 'Error downloading remote media: https://img.stockfresh.com/files/f/simple-image.jpg');
});
it('logs a warning when no suitable storage adapter found for inlined media extension', async function () {
const fileURL = 'https://img.stockfresh.com/files/f/inlined.exe';
const requestMock = nock('https://img.stockfresh.com')
.get('/files/f/inlined.exe')
.reply(200, exeFile);
const postModelInstanceStub = {
id: 'inlined-post-id',
get: sinon.stub()
.withArgs('mobiledoc')
.returns(`{"version":"0.3.1","atoms":[],"cards":[["image",{"src":"${fileURL}"}]]}`)
};
postModelStub = {
findPage: sinon.stub().returns({
data: [postModelInstanceStub]
}),
edit: sinon.stub().resolves()
};
const inliner = new ExternalMediaInliner({
PostModel: postModelStub,
PostMetaModel: postMetaModelStub,
TagModel: tagModelStub,
UserModel: userModelStub,
getMediaStorage: sinon.stub().withArgs('.exe').returns(null)
});
await inliner.inline(['https://img.stockfresh.com']);
assert.ok(requestMock.isDone());
assert.ok(logging.warn.calledOnce);
assert.equal(logging.warn.args[0][0], 'No storage adapter found for file extension: .exe');
});
it('logs an error when handling post inlining throws an error', async function (){
const imageURL = 'https://img.stockfresh.com/files/f/image.jpg';
const requestMock = nock('https://img.stockfresh.com')
.get('/files/f/image.jpg')
.reply(200, GIF1x1);
postModelStub = {
id: 'errored-post-id',
get: sinon.stub()
.withArgs('mobiledoc')
.returns(`{"version":"0.3.1","atoms":[],"cards":[["image",{"src":"${imageURL}"}]]}`)
};
postModelStub = {
findPage: sinon.stub().returns({
data: [postModelStub]
}),
edit: sinon.stub().throws(new Error('Error saving the post'))
};
sinon.stub(path, 'relative')
.withArgs('/content/images', '/content/images/unique-image.jpg')
.returns('unique-image.jpg');
const inliner = new ExternalMediaInliner({
PostModel: postModelStub,
PostMetaModel: postMetaModelStub,
TagModel: tagModelStub,
UserModel: userModelStub,
getMediaStorage: sinon.stub().withArgs('.jpg').returns({
getTargetDir: () => '/content/images',
getUniqueFileName: () => '/content/images/unique-image.jpg',
saveRaw: () => '/content/images/unique-image.jpg'
})
});
await inliner.inline(['https://img.stockfresh.com']);
assert.ok(requestMock.isDone());
assert.ok(postModelStub.edit.calledOnce);
assert.equal(logging.error.args[0][0], 'Error inlining media for post: errored-post-id');
});
it('logs an error when handling tag simple fields inlining throws an error', async function (){
const imageURL = 'https://img.stockfresh.com/files/f/simple-image.jpg';
const requestMock = nock('https://img.stockfresh.com')
.get('/files/f/simple-image.jpg')
.reply(200, GIF1x1);
const getMethodStub = sinon.stub();
getMethodStub.withArgs('feature_image').returns(imageURL);
getMethodStub.withArgs('og_image').returns(null);
getMethodStub.withArgs('twitter_image').returns(null);
const tagModelInstanceStub = {
id: 'errored-tag-id',
get: getMethodStub
};
tagModelStub.findPage = sinon.stub().returns({
data: [tagModelInstanceStub]
});
tagModelStub.edit = sinon.stub().throws(new Error('Error saving the tag'));
sinon.stub(path, 'relative')
.withArgs('/content/images', '/content/images/unique-image.jpg')
.returns('unique-image.jpg');
const inliner = new ExternalMediaInliner({
PostModel: postModelStub,
PostMetaModel: postMetaModelStub,
TagModel: tagModelStub,
UserModel: userModelStub,
getMediaStorage: sinon.stub().withArgs('.jpg').returns({
getTargetDir: () => '/content/images',
getUniqueFileName: () => '/content/images/unique-image.jpg',
saveRaw: () => '/content/images/unique-image.jpg'
})
});
await inliner.inline(['https://img.stockfresh.com']);
assert.ok(requestMock.isDone());
assert.ok(tagModelStub.edit.calledOnce);
assert.equal(logging.error.args[0][0], 'Error inlining media for: errored-tag-id');
});
it('inlines image in the post\'s feature_image field', async function () {
const imageURL = 'https://img.stockfresh.com/files/f/posts_feature_image.jpg';
const requestMock = nock('https://img.stockfresh.com')
.get('/files/f/posts_feature_image.jpg')
.reply(200, GIF1x1);
postModelStub = {
id: 'inlined-post-id',
get: sinon.stub()
.withArgs('feature_image')
.returns(imageURL)
};
const postModelMock = {
findPage: sinon.stub().returns({
data: [postModelStub]
}),
edit: sinon.stub().resolves()
};
sinon.stub(path, 'relative')
.withArgs('/content/images', '/content/images/unique-feature-image.jpg')
.returns('unique-feature-image.jpg');
const inliner = new ExternalMediaInliner({
PostModel: postModelMock,
PostMetaModel: postMetaModelStub,
TagModel: tagModelStub,
UserModel: userModelStub,
getMediaStorage: sinon.stub().withArgs('.jpg').returns({
getTargetDir: () => '/content/images',
getUniqueFileName: () => '/content/images/unique-feature-image.jpg',
saveRaw: () => '/content/images/unique-feature-image.jpg'
})
});
await inliner.inline(['https://img.stockfresh.com']);
assert.ok(requestMock.isDone());
assert.ok(postModelMock.edit.calledOnce);
assert.ok(postModelMock.edit.calledWith({
feature_image: '__GHOST_URL__/content/images/unique-feature-image.jpg'
}, {
id: 'inlined-post-id',
context: {
internal: true
}
}));
});
it('inlines og_image image in posts_meta table', async function () {
const imageURL = 'https://img.stockfresh.com/files/f/posts_meta_image.jpg';
const requestMock = nock('https://img.stockfresh.com')
.get('/files/f/posts_meta_image.jpg')
.reply(200, GIF1x1);
const getMethodStub = sinon.stub();
getMethodStub.withArgs('og_image').returns(imageURL);
getMethodStub.withArgs('twitter_image').returns(null);
const postsMetaModelInstanceStub = {
id: 'inlined-post-meta-id',
get: getMethodStub
};
postMetaModelStub.findPage = sinon.stub().resolves({
data: [postsMetaModelInstanceStub]
});
sinon.stub(path, 'relative')
.withArgs('/content/images', '/content/images/unique-feature-image.jpg')
.returns('unique-feature-image.jpg');
const inliner = new ExternalMediaInliner({
PostModel: postModelStub,
PostMetaModel: postMetaModelStub,
TagModel: tagModelStub,
UserModel: userModelStub,
getMediaStorage: sinon.stub().withArgs('.jpg').returns({
getTargetDir: () => '/content/images',
getUniqueFileName: () => '/content/images/unique-posts-meta-image.jpg',
saveRaw: () => '/content/images/unique-posts-meta-image.jpg'
})
});
await inliner.inline(['https://img.stockfresh.com']);
assert.ok(requestMock.isDone());
assert.ok(postMetaModelStub.edit.calledOnce);
assert.deepEqual(postMetaModelStub.edit.args[0][0], {
og_image: '__GHOST_URL__/content/images/unique-posts-meta-image.jpg'
});
assert.deepEqual(postMetaModelStub.edit.args[0][1], {
id: 'inlined-post-meta-id',
context: {
internal: true
}
});
});
it('inlines twitter_image image in tags table', async function () {
const imageURL = 'https://img.stockfresh.com/files/f/tag_twitter_image.jpg';
const requestMock = nock('https://img.stockfresh.com')
.get('/files/f/tag_twitter_image.jpg')
.reply(200, GIF1x1);
const getMethodStub = sinon.stub();
getMethodStub.withArgs('twitter_image').returns(imageURL);
getMethodStub.returns(null);
const tagModelInstanceStub = {
id: 'inlined-tag-id',
get: getMethodStub
};
tagModelStub.findPage = sinon.stub().resolves({
data: [tagModelInstanceStub]
});
sinon.stub(path, 'relative')
.withArgs('/content/images', '/content/images/unique-tag-twitter-image.jpg')
.returns('unique-tag-twitter-image.jpg');
const inliner = new ExternalMediaInliner({
PostModel: postModelStub,
PostMetaModel: postMetaModelStub,
TagModel: tagModelStub,
UserModel: userModelStub,
getMediaStorage: sinon.stub().withArgs('.jpg').returns({
getTargetDir: () => '/content/images',
getUniqueFileName: () => '/content/images/unique-tag-twitter-image.jpg',
saveRaw: () => '/content/images/unique-tag-twitter-image.jpg'
})
});
await inliner.inline(['https://img.stockfresh.com']);
assert.ok(requestMock.isDone());
assert.ok(tagModelStub.edit.calledOnce);
assert.deepEqual(tagModelStub.edit.args[0][0], {
twitter_image: '__GHOST_URL__/content/images/unique-tag-twitter-image.jpg'
});
assert.deepEqual(tagModelStub.edit.args[0][1], {
id: 'inlined-tag-id',
context: {
internal: true
}
});
});
it('inlines cover_image image in users table', async function () {
const imageURL = 'https://img.stockfresh.com/files/f/user_cover_image.jpg';
const requestMock = nock('https://img.stockfresh.com')
.get('/files/f/user_cover_image.jpg')
.reply(200, GIF1x1);
const getMethodStub = sinon.stub();
getMethodStub.withArgs('cover_image').returns(imageURL);
getMethodStub.returns(null);
const userModelInstanceStub = {
id: 'inlined-user-id',
get: getMethodStub
};
userModelStub.findPage = sinon.stub().resolves({
data: [userModelInstanceStub]
});
sinon.stub(path, 'relative')
.withArgs('/content/images', '/content/images/user-cover-image.jpg')
.returns('user-cover-image.jpg');
const inliner = new ExternalMediaInliner({
PostModel: postModelStub,
PostMetaModel: postMetaModelStub,
TagModel: tagModelStub,
UserModel: userModelStub,
getMediaStorage: sinon.stub().withArgs('.jpg').returns({
getTargetDir: () => '/content/images',
getUniqueFileName: () => '/content/images/user-cover-image.jpg',
saveRaw: () => '/content/images/user-cover-image.jpg'
})
});
await inliner.inline(['https://img.stockfresh.com']);
assert.ok(requestMock.isDone());
assert.ok(userModelStub.edit.calledOnce);
assert.deepEqual(userModelStub.edit.args[0][0], {
cover_image: '__GHOST_URL__/content/images/user-cover-image.jpg'
});
assert.deepEqual(userModelStub.edit.args[0][1], {
id: 'inlined-user-id',
context: {
internal: true
}
});
});
});
describe('Special URL & file type handling', function () {
it('Handles URLs with quotes', async function () {
const imageURL = 'https://img.stockfresh.com/files/f/ghost-logos-cool.png';
const requestMock = nock('https://img.stockfresh.com')
.get(encodeURI('/files/f/ghost-logos-cool.png'))
.reply(200, ghostLogoPng);
const inliner = new ExternalMediaInliner({});
const response = await inliner.getRemoteMedia(imageURL);
const fileData = await inliner.extractFileDataFromResponse(imageURL, response);
assert.ok(requestMock.isDone());
assert.equal(fileData.filename, 'ghost-logos-cool.png');
assert.equal(fileData.extension, '.png');
});
it('Handles URLs with spaces', async function () {
const imageURL = 'https://img.stockfresh.com/files/f/ghost logo with spaces.png';
const requestMock = nock('https://img.stockfresh.com')
.get(encodeURI('/files/f/ghost logo with spaces.png'))
.reply(200, ghostLogoPng);
const inliner = new ExternalMediaInliner({});
const response = await inliner.getRemoteMedia(imageURL);
const fileData = await inliner.extractFileDataFromResponse(imageURL, response);
assert.ok(requestMock.isDone());
assert.equal(fileData.filename, 'ghost-logo-with-spaces.png');
assert.equal(fileData.extension, '.png');
});
it('Handles URLs with no extension', async function () {
const imageURL = 'https://img.stockfresh.com/files/f/ghost-logo';
const requestMock = nock('https://img.stockfresh.com')
.get('/files/f/ghost-logo')
.reply(200, ghostLogoPng);
const inliner = new ExternalMediaInliner({});
const response = await inliner.getRemoteMedia(imageURL);
const fileData = await inliner.extractFileDataFromResponse(imageURL, response);
assert.ok(requestMock.isDone());
assert.equal(fileData.filename, 'ghost-logo.png');
assert.equal(fileData.extension, '.png');
});
it('Handles URLs with unicode characters', async function () {
const imageURL = 'https://img.stockfresh.com/files/f/你好.png';
const requestMock = nock('https://img.stockfresh.com')
.get(encodeURI('/files/f/你好.png'))
.reply(200, ghostLogoPng);
const inliner = new ExternalMediaInliner({});
const response = await inliner.getRemoteMedia(imageURL);
const fileData = await inliner.extractFileDataFromResponse(imageURL, response);
assert.ok(requestMock.isDone());
assert.equal(fileData.filename, 'ni-hao.png');
assert.equal(fileData.extension, '.png');
});
it('Handles URLs with no scheme', async function () {
const imageURL = '//img.stockfresh.com/files/f/ghost-logo.png';
const requestMock = nock('https://img.stockfresh.com')
.get('/files/f/ghost-logo.png')
.reply(200, ghostLogoPng);
const inliner = new ExternalMediaInliner({});
const response = await inliner.getRemoteMedia(imageURL);
const fileData = await inliner.extractFileDataFromResponse(imageURL, response);
assert.ok(requestMock.isDone());
assert.equal(fileData.filename, 'ghost-logo.png');
assert.equal(fileData.extension, '.png');
});
it('Handles URLs with query params', async function () {
const imageURL = 'https://img.stockfresh.com/files/f/ghost-logo.png?version=1&size=large';
const requestMock = nock('https://img.stockfresh.com')
.get('/files/f/ghost-logo.png?version=1&size=large')
.reply(200, ghostLogoPng);
const inliner = new ExternalMediaInliner({});
const response = await inliner.getRemoteMedia(imageURL);
const fileData = await inliner.extractFileDataFromResponse(imageURL, response);
assert.ok(requestMock.isDone());
assert.equal(fileData.filename, 'ghost-logo-version-1-size-large.png');
assert.equal(fileData.extension, '.png');
});
it('Handles URLs with duplicated characters', async function () {
const imageURL = 'https://img.stockfresh.com/files/f/ghost---logo.png';
const requestMock = nock('https://img.stockfresh.com')
.get('/files/f/ghost---logo.png')
.reply(200, ghostLogoPng);
const inliner = new ExternalMediaInliner({});
const response = await inliner.getRemoteMedia(imageURL);
const fileData = await inliner.extractFileDataFromResponse(imageURL, response);
assert.ok(requestMock.isDone());
assert.equal(fileData.filename, 'ghost---logo.png');
assert.equal(fileData.extension, '.png');
});
it('Handles falling back to `content-type` for type', async function () {
const imageURL = 'https://img.stockfresh.com/files/f/photo.gif?v=1&s=2';
const requestMock = nock('https://img.stockfresh.com')
.defaultReplyHeaders({
'content-type': 'image/gif'
})
.get('/files/f/photo.gif?v=1&s=2')
.reply(200);
const inliner = new ExternalMediaInliner({});
const response = await inliner.getRemoteMedia(imageURL);
const fileData = await inliner.extractFileDataFromResponse(imageURL, response);
assert.ok(requestMock.isDone());
assert.equal(fileData.filename, 'photo-v-1-s-2.gif');
assert.equal(fileData.extension, '.gif');
});
it('Handles falling back to file path for type', async function () {
const imageURL = 'https://img.stockfresh.com/files/f/photo.gif?v=1&s=2';
const requestMock = nock('https://img.stockfresh.com')
.get('/files/f/photo.gif?v=1&s=2')
.reply(200);
const inliner = new ExternalMediaInliner({});
const response = await inliner.getRemoteMedia(imageURL);
const fileData = await inliner.extractFileDataFromResponse(imageURL, response);
assert.ok(requestMock.isDone());
assert.equal(fileData.filename, 'photo-v-1-s-2.gif');
assert.equal(fileData.extension, '.gif');
});
});
});