From 380f3b5ca2b8fa1b2b24f6be6d9e33cc749e9a55 Mon Sep 17 00:00:00 2001 From: Sag Date: Wed, 4 Oct 2023 18:52:22 -0300 Subject: [PATCH] Added missing unit tests to the Recommendations package (#18489) refs https://github.com/TryGhost/Product/issues/3954 --- ghost/recommendations/package.json | 2 +- .../src/IncomingRecommendation.ts | 9 - .../src/IncomingRecommendationController.ts | 3 +- .../IncomingRecommendationEmailRenderer.ts | 2 +- .../src/IncomingRecommendationService.ts | 26 ++- .../IncomingRecommendationController.test.ts | 157 ++++++++++++++++++ .../IncomingRecommendationService.test.ts | 69 ++++++++ ghost/recommendations/test/hello.test.ts | 8 - 8 files changed, 249 insertions(+), 27 deletions(-) delete mode 100644 ghost/recommendations/src/IncomingRecommendation.ts create mode 100644 ghost/recommendations/test/IncomingRecommendationController.test.ts delete mode 100644 ghost/recommendations/test/hello.test.ts diff --git a/ghost/recommendations/package.json b/ghost/recommendations/package.json index e14e69162a..adc8b3500d 100644 --- a/ghost/recommendations/package.json +++ b/ghost/recommendations/package.json @@ -11,7 +11,7 @@ "build": "tsc", "build:ts": "yarn build", "prepare": "tsc", - "test:unit": "NODE_ENV=testing c8 --src src --all --reporter text --reporter cobertura mocha -r ts-node/register './test/**/*.test.ts'", + "test:unit": "NODE_ENV=testing c8 --src src --all --check-coverage --100 --reporter text --reporter cobertura mocha -r ts-node/register './test/**/*.test.ts'", "test": "yarn test:types && yarn test:unit", "test:types": "tsc --noEmit", "lint:code": "eslint src/ --ext .ts --cache", diff --git a/ghost/recommendations/src/IncomingRecommendation.ts b/ghost/recommendations/src/IncomingRecommendation.ts deleted file mode 100644 index 3d1f4e4ad5..0000000000 --- a/ghost/recommendations/src/IncomingRecommendation.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type IncomingRecommendation = { - id: string; - title: string; - url: URL; - excerpt: string|null; - favicon: URL|null; - featuredImage: URL|null; - recommendingBack: boolean; -} diff --git a/ghost/recommendations/src/IncomingRecommendationController.ts b/ghost/recommendations/src/IncomingRecommendationController.ts index 1b0bde3800..2ca7cc84f4 100644 --- a/ghost/recommendations/src/IncomingRecommendationController.ts +++ b/ghost/recommendations/src/IncomingRecommendationController.ts @@ -1,5 +1,5 @@ import {IncomingRecommendationService} from './IncomingRecommendationService'; -import {IncomingRecommendation} from './IncomingRecommendation'; +import {IncomingRecommendation} from './IncomingRecommendationService'; import {UnsafeData} from './UnsafeData'; type Frame = { @@ -23,6 +23,7 @@ export class IncomingRecommendationController { const page = options.optionalKey('page')?.integer ?? 1; const limit = options.optionalKey('limit')?.integer ?? 5; + const {incomingRecommendations, meta} = await this.service.listIncomingRecommendations({page, limit}); return this.#serialize( diff --git a/ghost/recommendations/src/IncomingRecommendationEmailRenderer.ts b/ghost/recommendations/src/IncomingRecommendationEmailRenderer.ts index 36eaf8f87d..8511d5b5d0 100644 --- a/ghost/recommendations/src/IncomingRecommendationEmailRenderer.ts +++ b/ghost/recommendations/src/IncomingRecommendationEmailRenderer.ts @@ -1,5 +1,5 @@ import {EmailRecipient} from './IncomingRecommendationService'; -import {IncomingRecommendation} from './IncomingRecommendation'; +import {IncomingRecommendation} from './IncomingRecommendationService'; type StaffService = { api: { diff --git a/ghost/recommendations/src/IncomingRecommendationService.ts b/ghost/recommendations/src/IncomingRecommendationService.ts index fb9767d3af..507a867ef3 100644 --- a/ghost/recommendations/src/IncomingRecommendationService.ts +++ b/ghost/recommendations/src/IncomingRecommendationService.ts @@ -1,8 +1,17 @@ -import {IncomingRecommendation} from './IncomingRecommendation'; import {IncomingRecommendationEmailRenderer} from './IncomingRecommendationEmailRenderer'; import {RecommendationService} from './RecommendationService'; import logging from '@tryghost/logging'; +export type IncomingRecommendation = { + id: string; + title: string; + url: URL; + excerpt: string|null; + favicon: URL|null; + featuredImage: URL|null; + recommendingBack: boolean; +} + export type Report = { startDate: Date, endDate: Date, @@ -21,7 +30,14 @@ type Mention = { } type MentionMeta = { - pagination: object, + pagination: { + page: number; + limit: number; + pages: number; + total: number; + next: null | number; + prev: null | number; + } } type MentionsAPI = { @@ -75,11 +91,7 @@ export class IncomingRecommendationService { } #getMentionFilter() { - const base = `source:~$'/.well-known/recommendations.json'`; - // if (verified) { - // return `${base}+verified:true`; - // } - return base; + return `source:~$'/.well-known/recommendations.json'`; } async #updateIncomingRecommendations() { diff --git a/ghost/recommendations/test/IncomingRecommendationController.test.ts b/ghost/recommendations/test/IncomingRecommendationController.test.ts new file mode 100644 index 0000000000..772fd9d020 --- /dev/null +++ b/ghost/recommendations/test/IncomingRecommendationController.test.ts @@ -0,0 +1,157 @@ +import assert from 'assert/strict'; +import {IncomingRecommendationController, IncomingRecommendationService} from '../src'; +import sinon, {SinonSpy} from 'sinon'; + +describe('IncomingRecommendationController', function () { + let service: Partial; + let controller: IncomingRecommendationController; + + beforeEach(function () { + service = {}; + controller = new IncomingRecommendationController({service: service as IncomingRecommendationService}); + }); + + describe('browse', function () { + beforeEach(function () { + service.listIncomingRecommendations = async () => { + return { + incomingRecommendations: [ + { + id: '1', + title: 'Test 1', + url: new URL('https://test1.com'), + excerpt: 'Excerpt 1', + favicon: new URL('https://test1.com/favicon.ico'), + featuredImage: new URL('https://test1.com/image.png'), + recommendingBack: true + }, + { + id: '2', + title: 'Test 2', + url: new URL('https://test2.com'), + excerpt: 'Excerpt 2', + favicon: null, + featuredImage: null, + recommendingBack: false + } + ], + meta: { + pagination: { + page: 1, + limit: 5, + pages: 1, + total: 2, + next: null, + prev: null + } + } + }; + }; + }); + + it('without options', async function () { + const result = await controller.browse({ + data: {}, + options: {} + }); + + assert.deepEqual(result, { + data: [{ + id: '1', + title: 'Test 1', + excerpt: 'Excerpt 1', + featured_image: 'https://test1.com/image.png', + favicon: 'https://test1.com/favicon.ico', + url: 'https://test1.com/', + recommending_back: true + }, + { + id: '2', + title: 'Test 2', + excerpt: 'Excerpt 2', + featured_image: null, + favicon: null, + url: 'https://test2.com/', + recommending_back: false + }], + meta: { + pagination: { + page: 1, + limit: 5, + pages: 1, + total: 2, + next: null, + prev: null + } + } + }); + }); + + describe('with options', function () { + let listSpy: SinonSpy; + + beforeEach(function () { + listSpy = sinon.spy(service, 'listIncomingRecommendations'); + }); + + it('limit is set to 5 by default', async function () { + await controller.browse({ + data: {}, + options: {} + }); + assert(listSpy.calledOnce); + const args = listSpy.getCall(0).args[0]; + assert.deepEqual(args.limit, 5); + }); + + it('limit can be set to 100', async function () { + await controller.browse({ + data: {}, + options: { + limit: 100 + } + }); + assert(listSpy.calledOnce); + const args = listSpy.getCall(0).args[0]; + assert.deepEqual(args.limit, 100); + }); + + it('limit cannot be set to "all"', async function () { + await assert.rejects( + controller.browse({ + data: {}, + options: { + limit: 'all' + } + }), + { + message: 'limit must be an integer' + } + ); + }); + + it('page is set to 1 by default', async function () { + await controller.browse({ + data: {}, + options: { + } + }); + assert(listSpy.calledOnce); + const args = listSpy.getCall(0).args[0]; + assert.deepEqual(args.page, 1); + }); + + it('page can be set to 2', async function () { + await controller.browse({ + data: {}, + options: { + page: 2 + } + }); + assert(listSpy.calledOnce); + const args = listSpy.getCall(0).args[0]; + assert.deepEqual(args.page, 2); + }); + }); + }); +}); diff --git a/ghost/recommendations/test/IncomingRecommendationService.test.ts b/ghost/recommendations/test/IncomingRecommendationService.test.ts index 7820726aa3..8ca5d91e10 100644 --- a/ghost/recommendations/test/IncomingRecommendationService.test.ts +++ b/ghost/recommendations/test/IncomingRecommendationService.test.ts @@ -103,4 +103,73 @@ describe('IncomingRecommendationService', function () { assert(!send.calledOnce); }); }); + + describe('listIncomingRecommendations', function () { + beforeEach(function () { + refreshMentions = sinon.stub().resolves(); + send = sinon.stub().resolves(); + readRecommendationByUrl = sinon.stub().resolves(null); + service = new IncomingRecommendationService({ + recommendationService: { + readRecommendationByUrl + } as any as RecommendationService, + mentionsApi: { + refreshMentions, + listMentions: () => Promise.resolve({data: [ + { + id: 'Incoming recommendation', + source: new URL('https://incoming-rec.com/.well-known/recommendations.json'), + sourceTitle: 'Incoming recommendation title', + sourceSiteTitle: null, + sourceAuthor: null, + sourceExcerpt: 'Incoming recommendation excerpt', + sourceFavicon: new URL('https://incoming-rec.com/favicon.ico'), + sourceFeaturedImage: new URL('https://incoming-rec.com/image.png') + } + ], meta: { + pagination: { + page: 1, + limit: 5, + pages: 1, + total: 1, + next: null, + prev: null + } + }}) + }, + emailService: { + send + }, + emailRenderer: { + renderSubject: () => Promise.resolve(''), + renderHTML: () => Promise.resolve(''), + renderText: () => Promise.resolve('') + } as any as IncomingRecommendationEmailRenderer, + getEmailRecipients: () => Promise.resolve([ + { + email: 'example@example.com' + } + ]) + }); + }); + + it('returns a list of incoming recommendations and pagination', async function () { + const list = await service.listIncomingRecommendations({}); + + assert.equal(list.incomingRecommendations.length, 1); + assert.equal(list.incomingRecommendations[0].id, 'Incoming recommendation'); + assert.equal(list.incomingRecommendations[0].title, 'Incoming recommendation title'); + assert.equal(list.incomingRecommendations[0].excerpt, 'Incoming recommendation excerpt'); + assert.equal(list.incomingRecommendations[0].url.toString(), 'https://incoming-rec.com/'); + assert.equal(list.incomingRecommendations[0].favicon?.toString(), 'https://incoming-rec.com/favicon.ico'); + assert.equal(list.incomingRecommendations[0].featuredImage?.toString(), 'https://incoming-rec.com/image.png'); + + assert.equal(list.meta?.pagination.page, 1); + assert.equal(list.meta?.pagination.limit, 5); + assert.equal(list.meta?.pagination.pages, 1); + assert.equal(list.meta?.pagination.total, 1); + assert.equal(list.meta?.pagination.prev, null); + assert.equal(list.meta?.pagination.next, null); + }); + }); }); diff --git a/ghost/recommendations/test/hello.test.ts b/ghost/recommendations/test/hello.test.ts deleted file mode 100644 index e66b88fad4..0000000000 --- a/ghost/recommendations/test/hello.test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import assert from 'assert/strict'; - -describe('Hello world', function () { - it('Runs a test', function () { - // TODO: Write me! - assert.ok(require('../')); - }); -});