Added full unit test coverage to recommendations package
fixes https://github.com/TryGhost/Product/issues/3954
This commit is contained in:
parent
9f9f028e82
commit
bed3e05eee
@ -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",
|
||||
|
@ -78,17 +78,17 @@ export class IncomingRecommendationService {
|
||||
}
|
||||
}
|
||||
|
||||
#getMentionFilter({verified = true} = {}) {
|
||||
#getMentionFilter() {
|
||||
const base = `source:~$'/.well-known/recommendations.json'`;
|
||||
if (verified) {
|
||||
return `${base}+verified:true`;
|
||||
}
|
||||
// if (verified) {
|
||||
// return `${base}+verified:true`;
|
||||
// }
|
||||
return base;
|
||||
}
|
||||
|
||||
async #updateIncomingRecommendations() {
|
||||
// Note: we also recheck recommendations that were not verified (verification could have failed)
|
||||
const filter = this.#getMentionFilter({verified: false});
|
||||
const filter = this.#getMentionFilter();
|
||||
await this.#mentionsApi.refreshMentions({filter, limit: 100});
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,93 @@
|
||||
import assert from 'assert/strict';
|
||||
import {BookshelfClickEventRepository, ClickEvent} from '../src';
|
||||
import sinon from 'sinon';
|
||||
|
||||
describe('BookshelfClickEventRepository', function () {
|
||||
afterEach(function () {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it('toPrimitive', async function () {
|
||||
const repository = new BookshelfClickEventRepository({} as any, {
|
||||
sentry: undefined
|
||||
});
|
||||
assert.deepEqual(
|
||||
repository.toPrimitive(ClickEvent.create({
|
||||
id: 'id',
|
||||
recommendationId: 'recommendationId',
|
||||
memberId: 'memberId',
|
||||
createdAt: new Date('2021-01-01')
|
||||
})),
|
||||
{
|
||||
id: 'id',
|
||||
recommendation_id: 'recommendationId',
|
||||
member_id: 'memberId',
|
||||
created_at: new Date('2021-01-01')
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('modelToEntity', async function () {
|
||||
const repository = new BookshelfClickEventRepository({} as any, {
|
||||
sentry: undefined
|
||||
});
|
||||
const entity = repository.modelToEntity({
|
||||
id: 'id',
|
||||
get: (key: string) => {
|
||||
return {
|
||||
recommendation_id: 'recommendationId',
|
||||
member_id: 'memberId',
|
||||
created_at: new Date('2021-01-01')
|
||||
}[key];
|
||||
}
|
||||
} as any);
|
||||
|
||||
assert.deepEqual(
|
||||
entity,
|
||||
ClickEvent.create({
|
||||
id: 'id',
|
||||
recommendationId: 'recommendationId',
|
||||
memberId: 'memberId',
|
||||
createdAt: new Date('2021-01-01')
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('modelToEntity returns null on errors', async function () {
|
||||
const captureException = sinon.stub();
|
||||
const repository = new BookshelfClickEventRepository({} as any, {
|
||||
sentry: {
|
||||
captureException
|
||||
}
|
||||
});
|
||||
|
||||
sinon.stub(ClickEvent, 'create').throws(new Error('test'));
|
||||
const entity = repository.modelToEntity({
|
||||
id: 'id',
|
||||
get: (key: string) => {
|
||||
return {
|
||||
recommendation_id: 'recommendationId',
|
||||
member_id: 'memberId',
|
||||
created_at: new Date('2021-01-01')
|
||||
}[key];
|
||||
}
|
||||
} as any);
|
||||
|
||||
assert.deepEqual(
|
||||
entity,
|
||||
null
|
||||
);
|
||||
sinon.assert.calledOnce(captureException);
|
||||
});
|
||||
|
||||
it('getFieldToColumnMap returns', async function () {
|
||||
const captureException = sinon.stub();
|
||||
const repository = new BookshelfClickEventRepository({} as any, {
|
||||
sentry: {
|
||||
captureException
|
||||
}
|
||||
});
|
||||
|
||||
assert.ok(repository.getFieldToColumnMap());
|
||||
});
|
||||
});
|
@ -0,0 +1,245 @@
|
||||
import assert from 'assert/strict';
|
||||
import {BookshelfRecommendationRepository, Recommendation} from '../src';
|
||||
import sinon from 'sinon';
|
||||
|
||||
describe('BookshelfRecommendationRepository', function () {
|
||||
afterEach(function () {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it('toPrimitive', async function () {
|
||||
const repository = new BookshelfRecommendationRepository({} as any, {
|
||||
sentry: undefined
|
||||
});
|
||||
assert.deepEqual(
|
||||
repository.toPrimitive(Recommendation.create({
|
||||
id: 'id',
|
||||
title: 'title',
|
||||
reason: 'reason',
|
||||
excerpt: 'excerpt',
|
||||
featuredImage: new URL('https://example.com'),
|
||||
favicon: new URL('https://example.com'),
|
||||
url: new URL('https://example.com'),
|
||||
oneClickSubscribe: true,
|
||||
createdAt: new Date('2021-01-01'),
|
||||
updatedAt: new Date('2021-01-02')
|
||||
})),
|
||||
{
|
||||
id: 'id',
|
||||
title: 'title',
|
||||
reason: 'reason',
|
||||
excerpt: 'excerpt',
|
||||
featured_image: 'https://example.com/',
|
||||
favicon: 'https://example.com/',
|
||||
url: 'https://example.com/',
|
||||
one_click_subscribe: true,
|
||||
created_at: new Date('2021-01-01'),
|
||||
updated_at: new Date('2021-01-02')
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('modelToEntity', async function () {
|
||||
const repository = new BookshelfRecommendationRepository({} as any, {
|
||||
sentry: undefined
|
||||
});
|
||||
const entity = repository.modelToEntity({
|
||||
id: 'id',
|
||||
get: (key: string) => {
|
||||
return {
|
||||
title: 'title',
|
||||
reason: 'reason',
|
||||
excerpt: 'excerpt',
|
||||
featured_image: 'https://example.com/',
|
||||
favicon: 'https://example.com/',
|
||||
url: 'https://example.com/',
|
||||
one_click_subscribe: true,
|
||||
created_at: new Date('2021-01-01'),
|
||||
updated_at: new Date('2021-01-02')
|
||||
}[key];
|
||||
}
|
||||
} as any);
|
||||
|
||||
assert.deepEqual(
|
||||
entity,
|
||||
Recommendation.create({
|
||||
id: 'id',
|
||||
title: 'title',
|
||||
reason: 'reason',
|
||||
excerpt: 'excerpt',
|
||||
featuredImage: new URL('https://example.com'),
|
||||
favicon: new URL('https://example.com'),
|
||||
url: new URL('https://example.com'),
|
||||
oneClickSubscribe: true,
|
||||
createdAt: new Date('2021-01-01'),
|
||||
updatedAt: new Date('2021-01-02')
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('modelToEntity returns null on errors', async function () {
|
||||
const captureException = sinon.stub();
|
||||
const repository = new BookshelfRecommendationRepository({} as any, {
|
||||
sentry: {
|
||||
captureException
|
||||
}
|
||||
});
|
||||
|
||||
sinon.stub(Recommendation, 'create').throws(new Error('test'));
|
||||
const entity = repository.modelToEntity({
|
||||
id: 'id',
|
||||
get: () => {
|
||||
return null;
|
||||
}
|
||||
} as any);
|
||||
|
||||
assert.deepEqual(
|
||||
entity,
|
||||
null
|
||||
);
|
||||
sinon.assert.calledOnce(captureException);
|
||||
});
|
||||
|
||||
it('getByUrl returns null if not found', async function () {
|
||||
const repository = new BookshelfRecommendationRepository({} as any, {
|
||||
sentry: undefined
|
||||
});
|
||||
const stub = sinon.stub(repository, 'getAll').returns(Promise.resolve([]));
|
||||
const entity = await repository.getByUrl(new URL('https://example.com'));
|
||||
|
||||
assert.deepEqual(
|
||||
entity,
|
||||
null
|
||||
);
|
||||
sinon.assert.calledOnce(stub);
|
||||
});
|
||||
|
||||
it('getByUrl returns if matching hostname', async function () {
|
||||
const repository = new BookshelfRecommendationRepository({} as any, {
|
||||
sentry: undefined
|
||||
});
|
||||
const recommendation = Recommendation.create({
|
||||
id: 'id',
|
||||
title: 'title',
|
||||
reason: 'reason',
|
||||
excerpt: 'excerpt',
|
||||
featuredImage: new URL('https://example.com'),
|
||||
favicon: new URL('https://example.com'),
|
||||
url: new URL('https://example.com/path'),
|
||||
oneClickSubscribe: true,
|
||||
createdAt: new Date('2021-01-01'),
|
||||
updatedAt: new Date('2021-01-02')
|
||||
});
|
||||
const stub = sinon.stub(repository, 'getAll').returns(Promise.resolve([
|
||||
recommendation
|
||||
]));
|
||||
const entity = await repository.getByUrl(new URL('https://www.example.com/path'));
|
||||
|
||||
assert.equal(
|
||||
entity,
|
||||
recommendation
|
||||
);
|
||||
sinon.assert.calledOnce(stub);
|
||||
});
|
||||
|
||||
it('getByUrl returns null if not matching path', async function () {
|
||||
const repository = new BookshelfRecommendationRepository({} as any, {
|
||||
sentry: undefined
|
||||
});
|
||||
const recommendation = Recommendation.create({
|
||||
id: 'id',
|
||||
title: 'title',
|
||||
reason: 'reason',
|
||||
excerpt: 'excerpt',
|
||||
featuredImage: new URL('https://example.com'),
|
||||
favicon: new URL('https://example.com'),
|
||||
url: new URL('https://example.com/other-path'),
|
||||
oneClickSubscribe: true,
|
||||
createdAt: new Date('2021-01-01'),
|
||||
updatedAt: new Date('2021-01-02')
|
||||
});
|
||||
const stub = sinon.stub(repository, 'getAll').returns(Promise.resolve([
|
||||
recommendation
|
||||
]));
|
||||
const entity = await repository.getByUrl(new URL('https://www.example.com/path'));
|
||||
|
||||
assert.equal(
|
||||
entity,
|
||||
null
|
||||
);
|
||||
sinon.assert.calledOnce(stub);
|
||||
});
|
||||
|
||||
it('getFieldToColumnMap returns', async function () {
|
||||
const captureException = sinon.stub();
|
||||
const repository = new BookshelfRecommendationRepository({} as any, {
|
||||
sentry: {
|
||||
captureException
|
||||
}
|
||||
});
|
||||
|
||||
assert.ok(repository.getFieldToColumnMap());
|
||||
});
|
||||
|
||||
it('applyCustomQuery returns', async function () {
|
||||
const captureException = sinon.stub();
|
||||
const repository = new BookshelfRecommendationRepository({} as any, {
|
||||
sentry: {
|
||||
captureException
|
||||
}
|
||||
});
|
||||
|
||||
const builder = {
|
||||
select: function (arg: any) {
|
||||
if (typeof arg === 'function') {
|
||||
arg(this);
|
||||
}
|
||||
},
|
||||
count: function () {
|
||||
return this;
|
||||
},
|
||||
from: function () {
|
||||
return this;
|
||||
},
|
||||
where: function () {
|
||||
return this;
|
||||
},
|
||||
as: function () {
|
||||
return this;
|
||||
},
|
||||
client: {
|
||||
raw: function () {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
} as any;
|
||||
|
||||
assert.doesNotThrow(() => {
|
||||
repository.applyCustomQuery(
|
||||
builder,
|
||||
{
|
||||
include: ['clickCount', 'subscriberCount']
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
assert.doesNotThrow(() => {
|
||||
repository.applyCustomQuery(
|
||||
builder,
|
||||
{
|
||||
include: [],
|
||||
order: [
|
||||
{
|
||||
field: 'clickCount',
|
||||
direction: 'asc'
|
||||
},
|
||||
{
|
||||
field: 'subscriberCount',
|
||||
direction: 'desc'
|
||||
}
|
||||
]
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,93 @@
|
||||
import assert from 'assert/strict';
|
||||
import {BookshelfSubscribeEventRepository, SubscribeEvent} from '../src';
|
||||
import sinon from 'sinon';
|
||||
|
||||
describe('BookshelfSubscribeEventRepository', function () {
|
||||
afterEach(function () {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it('toPrimitive', async function () {
|
||||
const repository = new BookshelfSubscribeEventRepository({} as any, {
|
||||
sentry: undefined
|
||||
});
|
||||
assert.deepEqual(
|
||||
repository.toPrimitive(SubscribeEvent.create({
|
||||
id: 'id',
|
||||
recommendationId: 'recommendationId',
|
||||
memberId: 'memberId',
|
||||
createdAt: new Date('2021-01-01')
|
||||
})),
|
||||
{
|
||||
id: 'id',
|
||||
recommendation_id: 'recommendationId',
|
||||
member_id: 'memberId',
|
||||
created_at: new Date('2021-01-01')
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('modelToEntity', async function () {
|
||||
const repository = new BookshelfSubscribeEventRepository({} as any, {
|
||||
sentry: undefined
|
||||
});
|
||||
const entity = repository.modelToEntity({
|
||||
id: 'id',
|
||||
get: (key: string) => {
|
||||
return {
|
||||
recommendation_id: 'recommendationId',
|
||||
member_id: 'memberId',
|
||||
created_at: new Date('2021-01-01')
|
||||
}[key];
|
||||
}
|
||||
} as any);
|
||||
|
||||
assert.deepEqual(
|
||||
entity,
|
||||
SubscribeEvent.create({
|
||||
id: 'id',
|
||||
recommendationId: 'recommendationId',
|
||||
memberId: 'memberId',
|
||||
createdAt: new Date('2021-01-01')
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('modelToEntity returns null on errors', async function () {
|
||||
const captureException = sinon.stub();
|
||||
const repository = new BookshelfSubscribeEventRepository({} as any, {
|
||||
sentry: {
|
||||
captureException
|
||||
}
|
||||
});
|
||||
|
||||
sinon.stub(SubscribeEvent, 'create').throws(new Error('test'));
|
||||
const entity = repository.modelToEntity({
|
||||
id: 'id',
|
||||
get: (key: string) => {
|
||||
return {
|
||||
recommendation_id: 'recommendationId',
|
||||
member_id: 'memberId',
|
||||
created_at: new Date('2021-01-01')
|
||||
}[key];
|
||||
}
|
||||
} as any);
|
||||
|
||||
assert.deepEqual(
|
||||
entity,
|
||||
null
|
||||
);
|
||||
sinon.assert.calledOnce(captureException);
|
||||
});
|
||||
|
||||
it('getFieldToColumnMap returns', async function () {
|
||||
const captureException = sinon.stub();
|
||||
const repository = new BookshelfSubscribeEventRepository({} as any, {
|
||||
sentry: {
|
||||
captureException
|
||||
}
|
||||
});
|
||||
|
||||
assert.ok(repository.getFieldToColumnMap());
|
||||
});
|
||||
});
|
@ -0,0 +1,24 @@
|
||||
import assert from 'assert/strict';
|
||||
import {IncomingRecommendationEmailRenderer} from '../src';
|
||||
|
||||
describe('IncomingRecommendationEmailRenderer', function () {
|
||||
it('passes all calls', async function () {
|
||||
const service = new IncomingRecommendationEmailRenderer({
|
||||
staffService: {
|
||||
api: {
|
||||
emails: {
|
||||
renderHTML: async () => 'html',
|
||||
renderText: async () => 'text'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
assert.equal(await service.renderSubject({
|
||||
title: 'title',
|
||||
siteTitle: 'title'
|
||||
} as any), '👍 New recommendation: title');
|
||||
|
||||
assert.equal(await service.renderHTML({} as any, {} as any), 'html');
|
||||
assert.equal(await service.renderText({} as any, {} as any), 'text');
|
||||
});
|
||||
});
|
104
ghost/recommendations/test/IncomingRecommendationService.test.ts
Normal file
104
ghost/recommendations/test/IncomingRecommendationService.test.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import assert from 'assert/strict';
|
||||
import sinon from 'sinon';
|
||||
import {IncomingRecommendationEmailRenderer, IncomingRecommendationService, RecommendationService} from '../src';
|
||||
|
||||
describe('IncomingRecommendationService', function () {
|
||||
let service: IncomingRecommendationService;
|
||||
let refreshMentions: sinon.SinonStub;
|
||||
let clock: sinon.SinonFakeTimers;
|
||||
let send: sinon.SinonStub;
|
||||
let readRecommendationByUrl: sinon.SinonStub;
|
||||
|
||||
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: []})
|
||||
},
|
||||
emailService: {
|
||||
send
|
||||
},
|
||||
emailRenderer: {
|
||||
renderSubject: () => Promise.resolve(''),
|
||||
renderHTML: () => Promise.resolve(''),
|
||||
renderText: () => Promise.resolve('')
|
||||
} as any as IncomingRecommendationEmailRenderer,
|
||||
getEmailRecipients: () => Promise.resolve([
|
||||
{
|
||||
email: 'example@example.com'
|
||||
}
|
||||
])
|
||||
});
|
||||
clock = sinon.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore();
|
||||
clock.restore();
|
||||
});
|
||||
|
||||
describe('init', function () {
|
||||
it('should update incoming recommendations on boot', async function () {
|
||||
// Sandbox time
|
||||
const saved = process.env.NODE_ENV;
|
||||
try {
|
||||
process.env.NODE_ENV = 'development';
|
||||
await service.init();
|
||||
clock.tick(1000 * 60 * 60 * 24);
|
||||
assert(refreshMentions.calledOnce);
|
||||
} finally {
|
||||
process.env.NODE_ENV = saved;
|
||||
}
|
||||
});
|
||||
|
||||
it('ignores errors when update incoming recommendations on boot', async function () {
|
||||
// Sandbox time
|
||||
const saved = process.env.NODE_ENV;
|
||||
try {
|
||||
process.env.NODE_ENV = 'development';
|
||||
|
||||
refreshMentions.rejects(new Error('test'));
|
||||
await service.init();
|
||||
clock.tick(1000 * 60 * 60 * 24);
|
||||
assert(refreshMentions.calledOnce);
|
||||
} finally {
|
||||
process.env.NODE_ENV = saved;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendRecommendationEmail', function () {
|
||||
it('should send email', async function () {
|
||||
await service.sendRecommendationEmail({
|
||||
source: new URL('https://example.com'),
|
||||
sourceTitle: 'Example',
|
||||
sourceSiteTitle: 'Example',
|
||||
sourceAuthor: 'Example',
|
||||
sourceExcerpt: 'Example',
|
||||
sourceFavicon: new URL('https://example.com/favicon.ico'),
|
||||
sourceFeaturedImage: new URL('https://example.com/featured.png')
|
||||
});
|
||||
assert(send.calledOnce);
|
||||
});
|
||||
|
||||
it('ignores when mention not convertable to incoming recommendation', async function () {
|
||||
readRecommendationByUrl.rejects(new Error('test'));
|
||||
await service.sendRecommendationEmail({
|
||||
source: new URL('https://example.com'),
|
||||
sourceTitle: 'Example',
|
||||
sourceSiteTitle: 'Example',
|
||||
sourceAuthor: 'Example',
|
||||
sourceExcerpt: 'Example',
|
||||
sourceFavicon: new URL('https://example.com/favicon.ico'),
|
||||
sourceFeaturedImage: new URL('https://example.com/featured.png')
|
||||
});
|
||||
assert(!send.calledOnce);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user