const assert = require('assert/strict'); const ObjectID = require('bson-objectid'); const Mention = require('../lib/Mention'); const MentionsAPI = require('../lib/MentionsAPI'); const InMemoryMentionRepository = require('../lib/InMemoryMentionRepository'); const sinon = require('sinon'); const cheerio = require('cheerio'); const mockRoutingService = { async pageExists() { return true; } }; const mockResourceService = { async getByURL() { return { type: null, id: null }; } }; const mockWebmentionMetadata = { async fetch() { return { siteTitle: 'Clickbait News', title: 'This egg breakfast will make you cry', excerpt: 'How many times have you woken up and almost cancelled your church plans? Well this breakfast is about to change everything, a hearty, faith restoring egg dish that will get your tastebuds in a twist.', author: 'Dr Egg Man', image: new URL('https://unsplash.com/photos/QAND9huzD04'), favicon: new URL('https://ghost.org/favicon.ico'), body: `
Some HTML and a mentioned url
`, contentType: 'text/html' }; } }; function addMinutes(date, minutes) { date.setMinutes(date.getMinutes() + minutes); return date; } describe('MentionsAPI', function () { beforeEach(function () { sinon.restore(); }); it('Can generate a mentions report', async function () { // this.retries(1); const repository = new InMemoryMentionRepository(); const api = new MentionsAPI({ repository, routingService: mockRoutingService, resourceService: mockResourceService, webmentionMetadata: mockWebmentionMetadata }); const mention = await api.processWebmention({ source: new URL('https://source.com'), target: new URL('https://target.com'), payload: {} }); assert(mention instanceof Mention); const now = new Date(); const report = await api.getMentionReport(new Date(0), now); assert.deepEqual(report.startDate, new Date(0)); assert.deepEqual(report.endDate, now); }); it('Can list paginated mentions', async function () { const repository = new InMemoryMentionRepository(); const api = new MentionsAPI({ repository, routingService: mockRoutingService, resourceService: mockResourceService, webmentionMetadata: mockWebmentionMetadata }); const mention = await api.processWebmention({ source: new URL('https://source.com'), target: new URL('https://target.com'), payload: {} }); assert(mention instanceof Mention); const page = await api.listMentions({ limit: 1, page: 1 }); assert.equal(page.data[0].id, mention.id); }); it('Can list all mentions', async function () { const repository = new InMemoryMentionRepository(); const api = new MentionsAPI({ repository, routingService: mockRoutingService, resourceService: mockResourceService, webmentionMetadata: mockWebmentionMetadata }); const mention = await api.processWebmention({ source: new URL('https://source.com'), target: new URL('https://target.com'), payload: {} }); assert(mention instanceof Mention); const page = await api.listMentions({ limit: 'all' }); assert.equal(page.data[0].id, mention.id); }); it('Can list filtered mentions', async function () { const repository = new InMemoryMentionRepository(); const api = new MentionsAPI({ repository, routingService: mockRoutingService, resourceService: mockResourceService, webmentionMetadata: mockWebmentionMetadata }); const mentionOne = await api.processWebmention({ source: new URL('https://diff-source.com'), target: new URL('https://target.com'), payload: {} }); const mentionTwo = await api.processWebmention({ source: new URL('https://source.com'), target: new URL('https://target.com'), payload: {} }); assert(mentionOne instanceof Mention); assert(mentionTwo instanceof Mention); const page = await api.listMentions({ filter: 'source.host:source.com', limit: 'all' }); assert(page.meta.pagination.total === 1); assert(page.data[0].id === mentionTwo.id); }); it('Can list mentions in descending order', async function () { const repository = new InMemoryMentionRepository(); const api = new MentionsAPI({ repository, routingService: mockRoutingService, resourceService: mockResourceService, webmentionMetadata: mockWebmentionMetadata }); const mentionOne = await api.processWebmention({ source: new URL('https://source.com'), target: new URL('https://target.com'), payload: {} }); sinon.useFakeTimers(addMinutes(new Date(), 10).getTime()); const mentionTwo = await api.processWebmention({ source: new URL('https://source2.com'), target: new URL('https://target.com'), payload: {} }); assert(mentionOne instanceof Mention); assert(mentionTwo instanceof Mention); const page = await api.listMentions({ limit: 'all', order: 'created_at desc' }); assert(page.meta.pagination.total === 2); assert(page.data[0].id === mentionTwo.id, 'First mention should be the second one in descending order'); assert(page.data[1].id === mentionOne.id, 'Second mention should be the first one in descending order'); }); it('Can list mentions in ascending order', async function () { const repository = new InMemoryMentionRepository(); const api = new MentionsAPI({ repository, routingService: mockRoutingService, resourceService: mockResourceService, webmentionMetadata: mockWebmentionMetadata }); const mentionOne = await api.processWebmention({ source: new URL('https://source.com'), target: new URL('https://target.com'), payload: {} }); sinon.useFakeTimers(addMinutes(new Date(), 10).getTime()); const mentionTwo = await api.processWebmention({ source: new URL('https://source2.com'), target: new URL('https://target.com'), payload: {} }); assert(mentionOne instanceof Mention); assert(mentionTwo instanceof Mention); const page = await api.listMentions({ limit: 'all', order: 'created_at asc' }); assert(page.meta.pagination.total === 2); assert(page.data[0].id === mentionOne.id, 'First mention should be the first one in ascending order'); assert(page.data[1].id === mentionTwo.id, 'Second mention should be the second one in ascending order'); }); it('Can update recommendations', async function () { const repository = new InMemoryMentionRepository(); const api = new MentionsAPI({ repository, routingService: mockRoutingService, resourceService: mockResourceService, webmentionMetadata: { fetch: sinon.stub() .onFirstCall().resolves(mockWebmentionMetadata.fetch()) .onSecondCall().resolves(mockWebmentionMetadata.fetch()) .onThirdCall().resolves(mockWebmentionMetadata.fetch()) .onCall(3).rejects() } }); await api.processWebmention({ source: new URL('https://source.com'), target: new URL('https://target.com'), payload: {} }); sinon.useFakeTimers(addMinutes(new Date(), 10).getTime()); await api.processWebmention({ source: new URL('https://source2.com'), target: new URL('https://target.com'), payload: {} }); let page = await api.listMentions({ limit: 'all' }); assert(page.meta.pagination.total === 2); // Now we invalidate the second mention await api.refreshMentions({ limit: 'all' }); page = await api.listMentions({ limit: 'all' }); assert(page.meta.pagination.total === 1); }); it('Can handle updating mentions', async function () { const repository = new InMemoryMentionRepository(); const api = new MentionsAPI({ repository, routingService: mockRoutingService, resourceService: mockResourceService, webmentionMetadata: mockWebmentionMetadata }); const mentionOne = await api.processWebmention({ source: new URL('https://source.com'), target: new URL('https://target.com'), payload: {} }); const mentionTwo = await api.processWebmention({ source: new URL('https://source.com'), target: new URL('https://target.com'), payload: { new: 'info' } }); assert(mentionOne.id === mentionTwo.id); const page = await api.listMentions({ limit: 'all' }); assert(page.meta.pagination.total === 1); assert(page.data[0].id === mentionOne.id); }); it('Will error if the target page does not exist', async function () { const repository = new InMemoryMentionRepository(); const api = new MentionsAPI({ repository, routingService: { async pageExists() { return false; } }, resourceService: mockResourceService, webmentionMetadata: mockWebmentionMetadata }); let errored = false; try { await api.processWebmention({ source: new URL('https://source.com'), target: new URL('https://target.com'), payload: {} }); } catch (err) { errored = true; } finally { assert(errored); } }); it('Handles verify errors', async function () { const repository = new InMemoryMentionRepository(); sinon.stub(cheerio, 'load').throws(new Error('Test error')); const api = new MentionsAPI({ repository, routingService: { async pageExists() { return true; } }, resourceService: mockResourceService, webmentionMetadata: mockWebmentionMetadata }); await api.processWebmention({ source: new URL('https://source.com'), target: new URL('https://target.com'), payload: {} }); }); it('Will only store resource if if the resource type is post', async function () { const repository = new InMemoryMentionRepository(); const api = new MentionsAPI({ repository, routingService: mockRoutingService, resourceService: { async getByURL() { return { type: 'post', id: new ObjectID }; } }, webmentionMetadata: mockWebmentionMetadata }); const mention = await api.processWebmention({ source: new URL('https://source.com'), target: new URL('https://target.com'), payload: {} }); assert(mention instanceof Mention); const page = await api.listMentions({ limit: 'all' }); assert.equal(page.data[0].id, mention.id); }); it('Will delete an existing mention if the target page does not exist', async function () { const repository = new InMemoryMentionRepository(); const api = new MentionsAPI({ repository, routingService: { pageExists: sinon.stub().onFirstCall().resolves(true).onSecondCall().resolves(false) }, resourceService: { async getByURL() { return { type: 'post', id: new ObjectID }; } }, webmentionMetadata: mockWebmentionMetadata }); checkFirstMention: { const mention = await api.processWebmention({ source: new URL('https://source.com'), target: new URL('https://target.com'), payload: {} }); const page = await api.listMentions({ limit: 'all' }); assert.equal(page.data[0].id, mention.id); break checkFirstMention; } checkMentionDeleted: { await api.processWebmention({ source: new URL('https://source.com'), target: new URL('https://target.com'), payload: {} }); const page = await api.listMentions({ limit: 'all' }); assert.equal(page.data.length, 0); break checkMentionDeleted; } }); it('Will delete and restore an existing mention if the source page does not exist', async function () { const repository = new InMemoryMentionRepository(); const api = new MentionsAPI({ repository, routingService: mockRoutingService, resourceService: { async getByURL() { return { type: 'post', id: new ObjectID }; } }, webmentionMetadata: { fetch: sinon.stub() .onFirstCall().resolves(mockWebmentionMetadata.fetch()) .onSecondCall().rejects() .onThirdCall().resolves(mockWebmentionMetadata.fetch()) } }); checkFirstMention: { const mention = await api.processWebmention({ source: new URL('https://source.com'), target: new URL('https://target.com'), payload: {} }); const page = await api.listMentions({ limit: 'all' }); assert.equal(page.data[0].id, mention.id); break checkFirstMention; } checkMentionDeleted: { await api.processWebmention({ source: new URL('https://source.com'), target: new URL('https://target.com'), payload: {} }); const page = await api.listMentions({ limit: 'all' }); assert.equal(page.data.length, 0); break checkMentionDeleted; } checkRestored: { const mention = await api.processWebmention({ source: new URL('https://source.com'), target: new URL('https://target.com'), payload: {} }); const page = await api.listMentions({ limit: 'all' }); assert.equal(page.data[0].id, mention.id); break checkRestored; } }); it('Will delete and restore an existing mention if the target url is not present on the source page', async function () { const repository = new InMemoryMentionRepository(); const api = new MentionsAPI({ repository, routingService: mockRoutingService, resourceService: { async getByURL() { return { type: 'post', id: new ObjectID }; } }, webmentionMetadata: { fetch: sinon.stub() .onFirstCall().resolves(mockWebmentionMetadata.fetch()) .onSecondCall().resolves({...(await mockWebmentionMetadata.fetch()), body: 'test'}) .onThirdCall().resolves(mockWebmentionMetadata.fetch()) } }); checkFirstMention: { const mention = await api.processWebmention({ source: new URL('https://source.com'), target: new URL('https://target.com'), payload: {} }); const page = await api.listMentions({ limit: 'all' }); assert.equal(page.data[0].id, mention.id); break checkFirstMention; } checkMentionDeleted: { await api.processWebmention({ source: new URL('https://source.com'), target: new URL('https://target.com'), payload: {} }); const page = await api.listMentions({ limit: 'all' }); assert.equal(page.data.length, 0); break checkMentionDeleted; } checkRestored: { const mention = await api.processWebmention({ source: new URL('https://source.com'), target: new URL('https://target.com'), payload: {} }); const page = await api.listMentions({ limit: 'all' }); assert.equal(page.data[0].id, mention.id); break checkRestored; } }); it('Will throw for new mentions if the source page is not found', async function () { const repository = new InMemoryMentionRepository(); const api = new MentionsAPI({ repository, routingService: mockRoutingService, resourceService: { async getByURL() { return { type: 'post', id: new ObjectID }; } }, webmentionMetadata: { fetch: sinon.stub().rejects(new Error('')) } }); let error = null; try { await api.processWebmention({ source: new URL('https://source.com'), target: new URL('https://target.com'), payload: {} }); } catch (err) { error = err; } finally { assert(error); } }); });