mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-24 23:48:13 -05:00
Added more unit test coverage for recommendations package (#18450)
refs https://github.com/TryGhost/Product/issues/3954
This commit is contained in:
parent
80a6fe17d0
commit
22484d6f96
7 changed files with 1216 additions and 24 deletions
|
@ -141,9 +141,9 @@ export class Recommendation {
|
||||||
title: data.title,
|
title: data.title,
|
||||||
reason: data.reason,
|
reason: data.reason,
|
||||||
excerpt: data.excerpt,
|
excerpt: data.excerpt,
|
||||||
featuredImage: new UnsafeData(data.featuredImage).nullable.url,
|
featuredImage: new UnsafeData(data.featuredImage, {field: ['featuredImage']}).nullable.url,
|
||||||
favicon: new UnsafeData(data.favicon).nullable.url,
|
favicon: new UnsafeData(data.favicon, {field: ['favicon']}).nullable.url,
|
||||||
url: new UnsafeData(data.url).url,
|
url: new UnsafeData(data.url, {field: ['url']}).url,
|
||||||
oneClickSubscribe: data.oneClickSubscribe,
|
oneClickSubscribe: data.oneClickSubscribe,
|
||||||
createdAt: data.createdAt ?? new Date(),
|
createdAt: data.createdAt ?? new Date(),
|
||||||
updatedAt: data.updatedAt ?? null,
|
updatedAt: data.updatedAt ?? null,
|
||||||
|
|
|
@ -9,7 +9,6 @@ type Frame = {
|
||||||
data: unknown,
|
data: unknown,
|
||||||
options: unknown,
|
options: unknown,
|
||||||
user: unknown,
|
user: unknown,
|
||||||
member: unknown,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const RecommendationIncludesMap = {
|
const RecommendationIncludesMap = {
|
||||||
|
@ -101,10 +100,10 @@ export class RecommendationController {
|
||||||
|
|
||||||
const parts = str.split(',');
|
const parts = str.split(',');
|
||||||
const order: OrderOption<Recommendation> = [];
|
const order: OrderOption<Recommendation> = [];
|
||||||
for (const part of parts) {
|
for (const [index, part] of parts.entries()) {
|
||||||
const trimmed = part.trim();
|
const trimmed = part.trim();
|
||||||
const fieldData = new UnsafeData(trimmed.split(' ')[0].trim());
|
const fieldData = new UnsafeData(trimmed.split(' ')[0].trim(), {field: ['order', index.toString(), 'field']});
|
||||||
const directionData = new UnsafeData(trimmed.split(' ')[1]?.trim() ?? 'asc');
|
const directionData = new UnsafeData(trimmed.split(' ')[1]?.trim() ?? 'desc', {field: ['order', index.toString(), 'direction']});
|
||||||
|
|
||||||
const validatedField = fieldData.enum(
|
const validatedField = fieldData.enum(
|
||||||
Object.keys(RecommendationOrderMap) as (keyof typeof RecommendationOrderMap)[]
|
Object.keys(RecommendationOrderMap) as (keyof typeof RecommendationOrderMap)[]
|
||||||
|
@ -119,15 +118,6 @@ export class RecommendationController {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (order.length === 0) {
|
|
||||||
// Default order
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
field: 'createdAt' as const,
|
|
||||||
direction: 'desc' as const
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return order;
|
return order;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -218,8 +208,8 @@ export class RecommendationController {
|
||||||
favicon: entity.favicon?.toString() ?? null,
|
favicon: entity.favicon?.toString() ?? null,
|
||||||
url: entity.url.toString(),
|
url: entity.url.toString(),
|
||||||
one_click_subscribe: entity.oneClickSubscribe,
|
one_click_subscribe: entity.oneClickSubscribe,
|
||||||
created_at: entity.createdAt,
|
created_at: entity.createdAt.toISOString(),
|
||||||
updated_at: entity.updatedAt,
|
updated_at: entity.updatedAt?.toISOString() ?? null,
|
||||||
count: entity.clickCount !== undefined || entity.subscriberCount !== undefined ? {
|
count: entity.clickCount !== undefined || entity.subscriberCount !== undefined ? {
|
||||||
clicks: entity.clickCount,
|
clicks: entity.clickCount,
|
||||||
subscribers: entity.subscriberCount
|
subscribers: entity.subscriberCount
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import {BookshelfRepository, IncludeOption, OrderOption} from '@tryghost/bookshelf-repository';
|
import {IncludeOption, OrderOption} from '@tryghost/bookshelf-repository';
|
||||||
import errors from '@tryghost/errors';
|
import errors from '@tryghost/errors';
|
||||||
|
import {InMemoryRepository} from '@tryghost/in-memory-repository';
|
||||||
import logging from '@tryghost/logging';
|
import logging from '@tryghost/logging';
|
||||||
import tpl from '@tryghost/tpl';
|
import tpl from '@tryghost/tpl';
|
||||||
import {ClickEvent} from './ClickEvent';
|
import {ClickEvent} from './ClickEvent';
|
||||||
|
@ -23,8 +24,8 @@ const messages = {
|
||||||
|
|
||||||
export class RecommendationService {
|
export class RecommendationService {
|
||||||
repository: RecommendationRepository;
|
repository: RecommendationRepository;
|
||||||
clickEventRepository: BookshelfRepository<string, ClickEvent>;
|
clickEventRepository: InMemoryRepository<string, ClickEvent>;
|
||||||
subscribeEventRepository: BookshelfRepository<string, SubscribeEvent>;
|
subscribeEventRepository: InMemoryRepository<string, SubscribeEvent>;
|
||||||
|
|
||||||
wellknownService: WellknownService;
|
wellknownService: WellknownService;
|
||||||
mentionSendingService: MentionSendingService;
|
mentionSendingService: MentionSendingService;
|
||||||
|
@ -32,8 +33,8 @@ export class RecommendationService {
|
||||||
|
|
||||||
constructor(deps: {
|
constructor(deps: {
|
||||||
repository: RecommendationRepository,
|
repository: RecommendationRepository,
|
||||||
clickEventRepository: BookshelfRepository<string, ClickEvent>,
|
clickEventRepository: InMemoryRepository<string, ClickEvent>,
|
||||||
subscribeEventRepository: BookshelfRepository<string, SubscribeEvent>,
|
subscribeEventRepository: InMemoryRepository<string, SubscribeEvent>,
|
||||||
wellknownService: WellknownService,
|
wellknownService: WellknownService,
|
||||||
mentionSendingService: MentionSendingService,
|
mentionSendingService: MentionSendingService,
|
||||||
recommendationEnablerService: RecommendationEnablerService
|
recommendationEnablerService: RecommendationEnablerService
|
||||||
|
|
|
@ -25,7 +25,6 @@ export class WellknownService {
|
||||||
#formatRecommendation(recommendation: Recommendation) {
|
#formatRecommendation(recommendation: Recommendation) {
|
||||||
return {
|
return {
|
||||||
url: recommendation.url,
|
url: recommendation.url,
|
||||||
reason: recommendation.reason,
|
|
||||||
updated_at: (recommendation.updatedAt ?? recommendation.createdAt).toISOString(),
|
updated_at: (recommendation.updatedAt ?? recommendation.createdAt).toISOString(),
|
||||||
created_at: (recommendation.createdAt).toISOString()
|
created_at: (recommendation.createdAt).toISOString()
|
||||||
};
|
};
|
||||||
|
|
673
ghost/recommendations/test/RecommendationController.test.ts
Normal file
673
ghost/recommendations/test/RecommendationController.test.ts
Normal file
|
@ -0,0 +1,673 @@
|
||||||
|
import assert from 'assert/strict';
|
||||||
|
import {RecommendationController, RecommendationService} from '../src';
|
||||||
|
import sinon, {SinonSpy} from 'sinon';
|
||||||
|
|
||||||
|
describe('RecommendationController', function () {
|
||||||
|
let service: Partial<RecommendationService>;
|
||||||
|
let controller: RecommendationController;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
service = {};
|
||||||
|
controller = new RecommendationController({service: service as RecommendationService});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('read', function () {
|
||||||
|
it('should return a recommendation', async function () {
|
||||||
|
service.readRecommendation = async (id) => {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
title: 'Test',
|
||||||
|
reason: null,
|
||||||
|
excerpt: null,
|
||||||
|
featuredImage: new URL('https://example.com/image.png'),
|
||||||
|
favicon: new URL('https://example.com/favicon.ico'),
|
||||||
|
url: new URL('https://example.com'),
|
||||||
|
oneClickSubscribe: false,
|
||||||
|
createdAt: new Date('2020-01-01T00:00:00.000Z'),
|
||||||
|
updatedAt: null
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await controller.read({
|
||||||
|
data: {},
|
||||||
|
options: {
|
||||||
|
id: '1'
|
||||||
|
},
|
||||||
|
user: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(result, {
|
||||||
|
data: [{
|
||||||
|
id: '1',
|
||||||
|
title: 'Test',
|
||||||
|
reason: null,
|
||||||
|
excerpt: null,
|
||||||
|
featured_image: 'https://example.com/image.png',
|
||||||
|
favicon: 'https://example.com/favicon.ico',
|
||||||
|
url: 'https://example.com/',
|
||||||
|
one_click_subscribe: false,
|
||||||
|
created_at: '2020-01-01T00:00:00.000Z',
|
||||||
|
updated_at: null,
|
||||||
|
count: undefined
|
||||||
|
}],
|
||||||
|
meta: undefined
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('add', function () {
|
||||||
|
it('should add a recommendation', async function () {
|
||||||
|
service.addRecommendation = async (plain) => {
|
||||||
|
return {
|
||||||
|
id: '1',
|
||||||
|
title: plain.title,
|
||||||
|
reason: plain.reason,
|
||||||
|
excerpt: plain.excerpt,
|
||||||
|
featuredImage: plain.featuredImage ? new URL(plain.featuredImage.toString()) : null,
|
||||||
|
favicon: plain.favicon ? new URL(plain.favicon.toString()) : null,
|
||||||
|
url: new URL(plain.url.toString()),
|
||||||
|
oneClickSubscribe: plain.oneClickSubscribe,
|
||||||
|
createdAt: new Date('2020-01-01T00:00:00.000Z'),
|
||||||
|
updatedAt: null
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await controller.add({
|
||||||
|
data: {
|
||||||
|
recommendations: [
|
||||||
|
{
|
||||||
|
title: 'Test',
|
||||||
|
reason: 'My reason',
|
||||||
|
excerpt: 'My excerpt',
|
||||||
|
featured_image: 'https://example.com/image.png',
|
||||||
|
favicon: 'https://example.com/favicon.ico',
|
||||||
|
url: 'https://example.com/',
|
||||||
|
one_click_subscribe: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {},
|
||||||
|
user: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(result, {
|
||||||
|
data: [{
|
||||||
|
id: '1',
|
||||||
|
title: 'Test',
|
||||||
|
reason: 'My reason',
|
||||||
|
excerpt: 'My excerpt',
|
||||||
|
featured_image: 'https://example.com/image.png',
|
||||||
|
favicon: 'https://example.com/favicon.ico',
|
||||||
|
url: 'https://example.com/',
|
||||||
|
one_click_subscribe: true,
|
||||||
|
created_at: '2020-01-01T00:00:00.000Z',
|
||||||
|
updated_at: null,
|
||||||
|
count: undefined
|
||||||
|
}],
|
||||||
|
meta: undefined
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('works with all optional fields missing', async function () {
|
||||||
|
service.addRecommendation = async (plain) => {
|
||||||
|
return {
|
||||||
|
id: '1',
|
||||||
|
title: plain.title,
|
||||||
|
reason: plain.reason,
|
||||||
|
excerpt: plain.excerpt,
|
||||||
|
featuredImage: plain.featuredImage ? new URL(plain.featuredImage.toString()) : null,
|
||||||
|
favicon: plain.favicon ? new URL(plain.favicon.toString()) : null,
|
||||||
|
url: new URL(plain.url.toString()),
|
||||||
|
oneClickSubscribe: plain.oneClickSubscribe,
|
||||||
|
createdAt: new Date('2020-01-01T00:00:00.000Z'),
|
||||||
|
updatedAt: null
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await controller.add({
|
||||||
|
data: {
|
||||||
|
recommendations: [
|
||||||
|
{
|
||||||
|
title: 'Test',
|
||||||
|
url: 'https://example.com/'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {},
|
||||||
|
user: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(result, {
|
||||||
|
data: [{
|
||||||
|
id: '1',
|
||||||
|
title: 'Test',
|
||||||
|
reason: null,
|
||||||
|
excerpt: null,
|
||||||
|
featured_image: null,
|
||||||
|
favicon: null,
|
||||||
|
url: 'https://example.com/',
|
||||||
|
one_click_subscribe: false,
|
||||||
|
created_at: '2020-01-01T00:00:00.000Z',
|
||||||
|
updated_at: null,
|
||||||
|
count: undefined
|
||||||
|
}],
|
||||||
|
meta: undefined
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('edit', function () {
|
||||||
|
it('should edit a recommendation', async function () {
|
||||||
|
service.editRecommendation = async (id, edit) => {
|
||||||
|
return {
|
||||||
|
id: '1',
|
||||||
|
title: edit.title || 'Test',
|
||||||
|
reason: edit.reason || null,
|
||||||
|
excerpt: edit.excerpt || null,
|
||||||
|
featuredImage: edit.featuredImage ? new URL(edit.featuredImage.toString()) : null,
|
||||||
|
favicon: edit.favicon ? new URL(edit.favicon.toString()) : null,
|
||||||
|
url: edit.url ? new URL(edit.url.toString()) : new URL('https://example.com'),
|
||||||
|
oneClickSubscribe: edit.oneClickSubscribe || false,
|
||||||
|
createdAt: new Date('2020-01-01T00:00:00.000Z'),
|
||||||
|
updatedAt: new Date('2020-01-01T00:00:00.000Z')
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await controller.edit({
|
||||||
|
data: {
|
||||||
|
recommendations: [
|
||||||
|
{
|
||||||
|
title: 'Test'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
id: '1'
|
||||||
|
},
|
||||||
|
user: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(result, {
|
||||||
|
data: [{
|
||||||
|
id: '1',
|
||||||
|
title: 'Test',
|
||||||
|
reason: null,
|
||||||
|
excerpt: null,
|
||||||
|
featured_image: null,
|
||||||
|
favicon: null,
|
||||||
|
url: 'https://example.com/',
|
||||||
|
one_click_subscribe: false,
|
||||||
|
created_at: '2020-01-01T00:00:00.000Z',
|
||||||
|
updated_at: '2020-01-01T00:00:00.000Z',
|
||||||
|
count: undefined
|
||||||
|
}],
|
||||||
|
meta: undefined
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('works with all others keys', async function () {
|
||||||
|
service.editRecommendation = async (id, edit) => {
|
||||||
|
return {
|
||||||
|
id: '1',
|
||||||
|
title: edit.title || 'Test',
|
||||||
|
reason: edit.reason || null,
|
||||||
|
excerpt: edit.excerpt || null,
|
||||||
|
featuredImage: edit.featuredImage ? new URL(edit.featuredImage.toString()) : null,
|
||||||
|
favicon: edit.favicon ? new URL(edit.favicon.toString()) : null,
|
||||||
|
url: edit.url ? new URL(edit.url.toString()) : new URL('https://example.com'),
|
||||||
|
oneClickSubscribe: edit.oneClickSubscribe || false,
|
||||||
|
createdAt: new Date('2020-01-01T00:00:00.000Z'),
|
||||||
|
updatedAt: new Date('2020-01-01T00:00:00.000Z')
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await controller.edit({
|
||||||
|
data: {
|
||||||
|
recommendations: [
|
||||||
|
{
|
||||||
|
// All execpt title
|
||||||
|
reason: 'My reason',
|
||||||
|
excerpt: 'My excerpt',
|
||||||
|
featured_image: 'https://example.com/image.png',
|
||||||
|
favicon: 'https://example.com/favicon.ico',
|
||||||
|
url: 'https://example.com/',
|
||||||
|
one_click_subscribe: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
id: '1'
|
||||||
|
},
|
||||||
|
user: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(result, {
|
||||||
|
data: [{
|
||||||
|
id: '1',
|
||||||
|
title: 'Test',
|
||||||
|
reason: 'My reason',
|
||||||
|
excerpt: 'My excerpt',
|
||||||
|
featured_image: 'https://example.com/image.png',
|
||||||
|
favicon: 'https://example.com/favicon.ico',
|
||||||
|
url: 'https://example.com/',
|
||||||
|
one_click_subscribe: true,
|
||||||
|
created_at: '2020-01-01T00:00:00.000Z',
|
||||||
|
updated_at: '2020-01-01T00:00:00.000Z',
|
||||||
|
count: undefined
|
||||||
|
}],
|
||||||
|
meta: undefined
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('destroy', function () {
|
||||||
|
it('should delete a recommendation', async function () {
|
||||||
|
service.deleteRecommendation = async () => {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await controller.destroy({
|
||||||
|
data: {},
|
||||||
|
options: {
|
||||||
|
id: '1'
|
||||||
|
},
|
||||||
|
user: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(result, undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('browse', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
service.listRecommendations = async () => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
title: 'Test',
|
||||||
|
reason: null,
|
||||||
|
excerpt: null,
|
||||||
|
featuredImage: new URL('https://example.com/image.png'),
|
||||||
|
favicon: new URL('https://example.com/favicon.ico'),
|
||||||
|
url: new URL('https://example.com'),
|
||||||
|
oneClickSubscribe: false,
|
||||||
|
createdAt: new Date('2020-01-01T00:00:00.000Z'),
|
||||||
|
updatedAt: null
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
service.countRecommendations = async () => {
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('default options', async function () {
|
||||||
|
const result = await controller.browse({
|
||||||
|
data: {},
|
||||||
|
options: {},
|
||||||
|
user: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(result, {
|
||||||
|
data: [{
|
||||||
|
id: '1',
|
||||||
|
title: 'Test',
|
||||||
|
reason: null,
|
||||||
|
excerpt: null,
|
||||||
|
featured_image: 'https://example.com/image.png',
|
||||||
|
favicon: 'https://example.com/favicon.ico',
|
||||||
|
url: 'https://example.com/',
|
||||||
|
one_click_subscribe: false,
|
||||||
|
created_at: '2020-01-01T00:00:00.000Z',
|
||||||
|
updated_at: null,
|
||||||
|
count: undefined
|
||||||
|
}],
|
||||||
|
meta: {
|
||||||
|
pagination: {
|
||||||
|
page: 1,
|
||||||
|
limit: 5,
|
||||||
|
pages: 1,
|
||||||
|
total: 1,
|
||||||
|
next: null,
|
||||||
|
prev: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('all options', async function () {
|
||||||
|
service.listRecommendations = async () => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
title: 'Test',
|
||||||
|
reason: null,
|
||||||
|
excerpt: null,
|
||||||
|
featuredImage: new URL('https://example.com/image.png'),
|
||||||
|
favicon: new URL('https://example.com/favicon.ico'),
|
||||||
|
url: new URL('https://example.com'),
|
||||||
|
oneClickSubscribe: false,
|
||||||
|
createdAt: new Date('2020-01-01T00:00:00.000Z'),
|
||||||
|
updatedAt: null
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
service.countRecommendations = async () => {
|
||||||
|
return 11;
|
||||||
|
};
|
||||||
|
const result = await controller.browse({
|
||||||
|
data: {},
|
||||||
|
options: {
|
||||||
|
page: 2,
|
||||||
|
limit: 5,
|
||||||
|
filter: 'id:2'
|
||||||
|
},
|
||||||
|
user: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(result, {
|
||||||
|
data: [{
|
||||||
|
id: '1',
|
||||||
|
title: 'Test',
|
||||||
|
reason: null,
|
||||||
|
excerpt: null,
|
||||||
|
featured_image: 'https://example.com/image.png',
|
||||||
|
favicon: 'https://example.com/favicon.ico',
|
||||||
|
url: 'https://example.com/',
|
||||||
|
one_click_subscribe: false,
|
||||||
|
created_at: '2020-01-01T00:00:00.000Z',
|
||||||
|
updated_at: null,
|
||||||
|
count: undefined
|
||||||
|
}],
|
||||||
|
meta: {
|
||||||
|
pagination: {
|
||||||
|
page: 2,
|
||||||
|
limit: 5,
|
||||||
|
pages: 3,
|
||||||
|
total: 11,
|
||||||
|
next: 3,
|
||||||
|
prev: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('order', function () {
|
||||||
|
let listSpy: SinonSpy;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
listSpy = sinon.spy(service, 'listRecommendations');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('orders by createdAt by default', async function () {
|
||||||
|
await controller.browse({
|
||||||
|
data: {},
|
||||||
|
options: {
|
||||||
|
order: ''
|
||||||
|
},
|
||||||
|
user: {}
|
||||||
|
});
|
||||||
|
assert(listSpy.calledOnce);
|
||||||
|
const args = listSpy.getCall(0).args[0];
|
||||||
|
assert.deepEqual(args.order, [
|
||||||
|
{
|
||||||
|
field: 'createdAt',
|
||||||
|
direction: 'desc'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('order by custom field', async function () {
|
||||||
|
await controller.browse({
|
||||||
|
data: {},
|
||||||
|
options: {
|
||||||
|
order: 'created_at'
|
||||||
|
},
|
||||||
|
user: {}
|
||||||
|
});
|
||||||
|
assert(listSpy.calledOnce);
|
||||||
|
const args = listSpy.getCall(0).args[0];
|
||||||
|
assert.deepEqual(args.order, [
|
||||||
|
{
|
||||||
|
field: 'createdAt',
|
||||||
|
direction: 'desc'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('order by multiple custom field', async function () {
|
||||||
|
await controller.browse({
|
||||||
|
data: {},
|
||||||
|
options: {
|
||||||
|
order: 'created_at, count.clicks'
|
||||||
|
},
|
||||||
|
user: {}
|
||||||
|
});
|
||||||
|
assert(listSpy.calledOnce);
|
||||||
|
const args = listSpy.getCall(0).args[0];
|
||||||
|
assert.deepEqual(args.order, [
|
||||||
|
{
|
||||||
|
field: 'createdAt',
|
||||||
|
direction: 'desc'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'clickCount',
|
||||||
|
direction: 'desc'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('order by multiple custom field with directions', async function () {
|
||||||
|
await controller.browse({
|
||||||
|
data: {},
|
||||||
|
options: {
|
||||||
|
order: 'created_at asc, count.clicks desc'
|
||||||
|
},
|
||||||
|
user: {}
|
||||||
|
});
|
||||||
|
assert(listSpy.calledOnce);
|
||||||
|
const args = listSpy.getCall(0).args[0];
|
||||||
|
assert.deepEqual(args.order, [
|
||||||
|
{
|
||||||
|
field: 'createdAt',
|
||||||
|
direction: 'asc'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'clickCount',
|
||||||
|
direction: 'desc'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cannot order by invalid fields', async function () {
|
||||||
|
await assert.rejects(
|
||||||
|
controller.browse({
|
||||||
|
data: {},
|
||||||
|
options: {
|
||||||
|
order: 'invalid desc'
|
||||||
|
},
|
||||||
|
user: {}
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
message: 'order.0.field must be one of count.clicks, count.subscribers, created_at'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cannot order by invalid direction', async function () {
|
||||||
|
await assert.rejects(
|
||||||
|
controller.browse({
|
||||||
|
data: {},
|
||||||
|
options: {
|
||||||
|
order: 'created_at down'
|
||||||
|
},
|
||||||
|
user: {}
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
message: 'order.0.direction must be one of asc, desc'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('include', function () {
|
||||||
|
let listSpy: SinonSpy;
|
||||||
|
let rec = {
|
||||||
|
id: '1',
|
||||||
|
title: 'Test',
|
||||||
|
reason: null,
|
||||||
|
excerpt: null,
|
||||||
|
featuredImage: new URL('https://example.com/image.png'),
|
||||||
|
favicon: new URL('https://example.com/favicon.ico'),
|
||||||
|
url: new URL('https://example.com'),
|
||||||
|
oneClickSubscribe: false,
|
||||||
|
createdAt: new Date('2020-01-01T00:00:00.000Z'),
|
||||||
|
updatedAt: null,
|
||||||
|
clickCount: 5,
|
||||||
|
subscriberCount: 10
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
service.listRecommendations = async () => {
|
||||||
|
return [
|
||||||
|
rec
|
||||||
|
];
|
||||||
|
};
|
||||||
|
listSpy = sinon.spy(service, 'listRecommendations');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can include clicks and subscribes', async function () {
|
||||||
|
await controller.browse({
|
||||||
|
data: {},
|
||||||
|
options: {
|
||||||
|
withRelated: ['count.clicks', 'count.subscribers']
|
||||||
|
},
|
||||||
|
user: {}
|
||||||
|
});
|
||||||
|
assert(listSpy.calledOnce);
|
||||||
|
const args = listSpy.getCall(0).args[0];
|
||||||
|
assert.deepEqual(args.include, ['clickCount', 'subscriberCount']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws for invalid include', async function () {
|
||||||
|
await assert.rejects(
|
||||||
|
controller.browse({
|
||||||
|
data: {},
|
||||||
|
options: {
|
||||||
|
withRelated: ['invalid']
|
||||||
|
},
|
||||||
|
user: {}
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
message: 'withRelated.0 must be one of count.clicks, count.subscribers'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('trackClicked', function () {
|
||||||
|
it('should track a click', async function () {
|
||||||
|
service.trackClicked = async ({id, memberId}) => {
|
||||||
|
assert.equal(id, '1');
|
||||||
|
assert.equal(memberId, undefined);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await controller.trackClicked({
|
||||||
|
data: {},
|
||||||
|
options: {
|
||||||
|
id: '1',
|
||||||
|
context: {}
|
||||||
|
},
|
||||||
|
user: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(result, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('authenticated', async function () {
|
||||||
|
service.trackClicked = async ({id, memberId}) => {
|
||||||
|
assert.equal(id, '1');
|
||||||
|
assert.equal(memberId, '1');
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await controller.trackClicked({
|
||||||
|
data: {},
|
||||||
|
options: {
|
||||||
|
id: '1',
|
||||||
|
context: {
|
||||||
|
member: {
|
||||||
|
id: '1'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
user: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(result, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws if invalid member context', async function () {
|
||||||
|
await assert.rejects(async () => {
|
||||||
|
await controller.trackClicked({
|
||||||
|
data: {},
|
||||||
|
options: {
|
||||||
|
id: '1',
|
||||||
|
context: {
|
||||||
|
member: {
|
||||||
|
missingId: 'example'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
user: {}
|
||||||
|
});
|
||||||
|
}, {
|
||||||
|
message: 'context.member.id is required'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('trackSubscribed', function () {
|
||||||
|
it('works if authenticated', async function () {
|
||||||
|
service.trackSubscribed = async () => {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await controller.trackSubscribed({
|
||||||
|
data: {},
|
||||||
|
options: {
|
||||||
|
id: '1',
|
||||||
|
context: {
|
||||||
|
member: {
|
||||||
|
id: '1'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
user: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(result, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws if not authenticated', async function () {
|
||||||
|
service.trackSubscribed = async () => {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
await assert.rejects(async () => {
|
||||||
|
await controller.trackSubscribed({
|
||||||
|
data: {},
|
||||||
|
options: {
|
||||||
|
id: '1',
|
||||||
|
context: {}
|
||||||
|
},
|
||||||
|
user: {}
|
||||||
|
});
|
||||||
|
}, {
|
||||||
|
message: 'Member not found'
|
||||||
|
}, 'trackSubscribed should throw if not authenticated');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
451
ghost/recommendations/test/RecommendationService.test.ts
Normal file
451
ghost/recommendations/test/RecommendationService.test.ts
Normal file
|
@ -0,0 +1,451 @@
|
||||||
|
import assert from 'assert/strict';
|
||||||
|
import {ClickEvent, InMemoryRecommendationRepository, Recommendation, RecommendationService, SubscribeEvent, WellknownService} from '../src';
|
||||||
|
import {InMemoryRepository} from '@tryghost/in-memory-repository';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
class InMemoryClickEventRepository<T extends ClickEvent|SubscribeEvent> extends InMemoryRepository<string, T> {
|
||||||
|
toPrimitive(entity: T): object {
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('RecommendationService', function () {
|
||||||
|
let service: RecommendationService;
|
||||||
|
let enabled = false;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
enabled = false;
|
||||||
|
service = new RecommendationService({
|
||||||
|
repository: new InMemoryRecommendationRepository(),
|
||||||
|
clickEventRepository: new InMemoryClickEventRepository<ClickEvent>(),
|
||||||
|
subscribeEventRepository: new InMemoryClickEventRepository<SubscribeEvent>(),
|
||||||
|
wellknownService: {
|
||||||
|
getPath() {
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
getURL() {
|
||||||
|
return new URL('http://localhost/.well-known/recommendations.json');
|
||||||
|
},
|
||||||
|
set() {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
} as unknown as WellknownService,
|
||||||
|
mentionSendingService: {
|
||||||
|
sendAll() {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
recommendationEnablerService: {
|
||||||
|
getSetting() {
|
||||||
|
return enabled.toString();
|
||||||
|
},
|
||||||
|
setSetting(e) {
|
||||||
|
enabled = e === 'true';
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('init', function () {
|
||||||
|
it('should update wellknown', async function () {
|
||||||
|
const updateWellknown = sinon.stub(service.wellknownService, 'set').resolves();
|
||||||
|
await service.init();
|
||||||
|
assert(updateWellknown.calledOnce);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateRecommendationsEnabledSetting', function () {
|
||||||
|
it('should set to true if more than one', async function () {
|
||||||
|
enabled = false;
|
||||||
|
await service.updateRecommendationsEnabledSetting([
|
||||||
|
Recommendation.create({
|
||||||
|
url: 'http://localhost/1',
|
||||||
|
title: 'Test',
|
||||||
|
reason: null,
|
||||||
|
excerpt: null,
|
||||||
|
featuredImage: null,
|
||||||
|
favicon: null,
|
||||||
|
oneClickSubscribe: false
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
assert(enabled);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep enabled true if already enabled', async function () {
|
||||||
|
enabled = true;
|
||||||
|
await service.updateRecommendationsEnabledSetting([
|
||||||
|
Recommendation.create({
|
||||||
|
url: 'http://localhost/1',
|
||||||
|
title: 'Test',
|
||||||
|
reason: null,
|
||||||
|
excerpt: null,
|
||||||
|
featuredImage: null,
|
||||||
|
favicon: null,
|
||||||
|
oneClickSubscribe: false
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
assert(enabled);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set to false if none', async function () {
|
||||||
|
enabled = false;
|
||||||
|
await service.updateRecommendationsEnabledSetting([]);
|
||||||
|
assert.equal(enabled, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set to false if none if currently enabled', async function () {
|
||||||
|
enabled = true;
|
||||||
|
await service.updateRecommendationsEnabledSetting([]);
|
||||||
|
assert.equal(enabled, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('readRecommendation', function () {
|
||||||
|
it('throws if not found', async function () {
|
||||||
|
await assert.rejects(() => service.readRecommendation('1'), {
|
||||||
|
name: 'NotFoundError',
|
||||||
|
message: 'Recommendation with id 1 not found'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns plain if found', async function () {
|
||||||
|
const recommendation = Recommendation.create({
|
||||||
|
id: '2',
|
||||||
|
url: 'http://localhost/1',
|
||||||
|
title: 'Test',
|
||||||
|
reason: null,
|
||||||
|
excerpt: null,
|
||||||
|
featuredImage: null,
|
||||||
|
favicon: null,
|
||||||
|
oneClickSubscribe: false
|
||||||
|
});
|
||||||
|
await service.repository.save(recommendation);
|
||||||
|
|
||||||
|
const response = await service.readRecommendation('2');
|
||||||
|
assert.deepEqual(response, recommendation.plain);
|
||||||
|
|
||||||
|
// Check not instance of Recommendation
|
||||||
|
assert.equal(response instanceof Recommendation, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addRecommendation', function () {
|
||||||
|
it('throws if already exists', async function () {
|
||||||
|
const recommendation = Recommendation.create({
|
||||||
|
id: '2',
|
||||||
|
url: 'http://localhost/1',
|
||||||
|
title: 'Test',
|
||||||
|
reason: null,
|
||||||
|
excerpt: null,
|
||||||
|
featuredImage: null,
|
||||||
|
favicon: null,
|
||||||
|
oneClickSubscribe: false
|
||||||
|
});
|
||||||
|
await service.repository.save(recommendation);
|
||||||
|
|
||||||
|
await assert.rejects(() => service.addRecommendation({
|
||||||
|
url: 'http://localhost/1',
|
||||||
|
title: 'Test 2',
|
||||||
|
reason: null,
|
||||||
|
excerpt: null,
|
||||||
|
featuredImage: null,
|
||||||
|
favicon: null,
|
||||||
|
oneClickSubscribe: false
|
||||||
|
}), {
|
||||||
|
name: 'ValidationError',
|
||||||
|
message: 'A recommendation with this URL already exists.'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns plain if sucessful', async function () {
|
||||||
|
const response = await service.addRecommendation({
|
||||||
|
url: 'http://localhost/1',
|
||||||
|
title: 'Test',
|
||||||
|
reason: null,
|
||||||
|
excerpt: null,
|
||||||
|
featuredImage: null,
|
||||||
|
favicon: null,
|
||||||
|
oneClickSubscribe: false
|
||||||
|
});
|
||||||
|
assert.deepEqual(response, {
|
||||||
|
title: 'Test',
|
||||||
|
reason: null,
|
||||||
|
excerpt: null,
|
||||||
|
featuredImage: null,
|
||||||
|
favicon: null,
|
||||||
|
oneClickSubscribe: false,
|
||||||
|
clickCount: undefined,
|
||||||
|
subscriberCount: undefined,
|
||||||
|
updatedAt: null,
|
||||||
|
|
||||||
|
// Ignored
|
||||||
|
url: response.url,
|
||||||
|
id: response.id,
|
||||||
|
createdAt: response.createdAt
|
||||||
|
});
|
||||||
|
|
||||||
|
assert(response.id);
|
||||||
|
assert(response.url);
|
||||||
|
assert(response.createdAt);
|
||||||
|
|
||||||
|
assert(response.url instanceof URL);
|
||||||
|
assert(response.createdAt instanceof Date);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not throw if sendMentionToRecommendation throws', async function () {
|
||||||
|
const recommendation = Recommendation.create({
|
||||||
|
id: '2',
|
||||||
|
url: 'http://localhost/1',
|
||||||
|
title: 'Test',
|
||||||
|
reason: null,
|
||||||
|
excerpt: null,
|
||||||
|
featuredImage: null,
|
||||||
|
favicon: null,
|
||||||
|
oneClickSubscribe: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateRecommendationsEnabledSetting = sinon.stub(service.mentionSendingService, 'sendAll').rejects(new Error('Test'));
|
||||||
|
await service.repository.save(recommendation);
|
||||||
|
|
||||||
|
await assert.doesNotReject(() => service.addRecommendation({
|
||||||
|
url: 'http://localhost/2',
|
||||||
|
title: 'Test 2',
|
||||||
|
reason: null,
|
||||||
|
excerpt: null,
|
||||||
|
featuredImage: null,
|
||||||
|
favicon: null,
|
||||||
|
oneClickSubscribe: false
|
||||||
|
}));
|
||||||
|
|
||||||
|
assert(updateRecommendationsEnabledSetting.calledOnce);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('editRecommendation', function () {
|
||||||
|
it('throws if not found', async function () {
|
||||||
|
await assert.rejects(() => service.editRecommendation('1', {
|
||||||
|
title: 'Test 2'
|
||||||
|
}), {
|
||||||
|
name: 'NotFoundError',
|
||||||
|
message: 'Recommendation with id 1 not found'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns plain if sucessful', async function () {
|
||||||
|
const recommendation = Recommendation.create({
|
||||||
|
id: '2',
|
||||||
|
url: 'http://localhost/1',
|
||||||
|
title: 'Test',
|
||||||
|
reason: null,
|
||||||
|
excerpt: null,
|
||||||
|
featuredImage: null,
|
||||||
|
favicon: null,
|
||||||
|
oneClickSubscribe: false
|
||||||
|
});
|
||||||
|
await service.repository.save(recommendation);
|
||||||
|
|
||||||
|
const response = await service.editRecommendation('2', {
|
||||||
|
title: 'Test 2'
|
||||||
|
});
|
||||||
|
assert.deepEqual(response, {
|
||||||
|
title: 'Test 2',
|
||||||
|
reason: null,
|
||||||
|
excerpt: null,
|
||||||
|
featuredImage: null,
|
||||||
|
favicon: null,
|
||||||
|
oneClickSubscribe: false,
|
||||||
|
clickCount: undefined,
|
||||||
|
subscriberCount: undefined,
|
||||||
|
|
||||||
|
// Ignored
|
||||||
|
url: response.url,
|
||||||
|
id: response.id,
|
||||||
|
createdAt: response.createdAt,
|
||||||
|
updatedAt: response.updatedAt
|
||||||
|
});
|
||||||
|
|
||||||
|
assert(response.id);
|
||||||
|
assert(response.url);
|
||||||
|
assert(response.createdAt);
|
||||||
|
assert(response.updatedAt);
|
||||||
|
|
||||||
|
assert(response.url instanceof URL);
|
||||||
|
assert(response.createdAt instanceof Date);
|
||||||
|
assert(response.updatedAt instanceof Date);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteRecommendation', function () {
|
||||||
|
it('throws if not found', async function () {
|
||||||
|
await assert.rejects(() => service.deleteRecommendation('1'), {
|
||||||
|
name: 'NotFoundError',
|
||||||
|
message: 'Recommendation with id 1 not found'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deletes if found', async function () {
|
||||||
|
const recommendation = Recommendation.create({
|
||||||
|
id: '2',
|
||||||
|
url: 'http://localhost/1',
|
||||||
|
title: 'Test',
|
||||||
|
reason: null,
|
||||||
|
excerpt: null,
|
||||||
|
featuredImage: null,
|
||||||
|
favicon: null,
|
||||||
|
oneClickSubscribe: false
|
||||||
|
});
|
||||||
|
await service.repository.save(recommendation);
|
||||||
|
|
||||||
|
assert.equal(await service.repository.getCount({}), 1);
|
||||||
|
await service.deleteRecommendation('2');
|
||||||
|
assert.equal(await service.repository.getCount({}), 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('listRecommendations', function () {
|
||||||
|
it('returns plain if sucessful', async function () {
|
||||||
|
const recommendation = Recommendation.create({
|
||||||
|
id: '2',
|
||||||
|
url: 'http://localhost/1',
|
||||||
|
title: 'Test',
|
||||||
|
reason: null,
|
||||||
|
excerpt: null,
|
||||||
|
featuredImage: null,
|
||||||
|
favicon: null,
|
||||||
|
oneClickSubscribe: false
|
||||||
|
});
|
||||||
|
await service.repository.save(recommendation);
|
||||||
|
|
||||||
|
const response = await service.listRecommendations();
|
||||||
|
assert.equal(response.length, 1);
|
||||||
|
assert.equal(response[0] instanceof Recommendation, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns pages', async function () {
|
||||||
|
const recommendation = Recommendation.create({
|
||||||
|
id: '2',
|
||||||
|
url: 'http://localhost/1',
|
||||||
|
title: 'Test',
|
||||||
|
reason: null,
|
||||||
|
excerpt: null,
|
||||||
|
featuredImage: null,
|
||||||
|
favicon: null,
|
||||||
|
oneClickSubscribe: false
|
||||||
|
});
|
||||||
|
await service.repository.save(recommendation);
|
||||||
|
|
||||||
|
const recommendation2 = Recommendation.create({
|
||||||
|
id: '3',
|
||||||
|
url: 'http://localhost/2',
|
||||||
|
title: 'Test 2',
|
||||||
|
reason: null,
|
||||||
|
excerpt: null,
|
||||||
|
featuredImage: null,
|
||||||
|
favicon: null,
|
||||||
|
oneClickSubscribe: false
|
||||||
|
});
|
||||||
|
await service.repository.save(recommendation2);
|
||||||
|
|
||||||
|
const response = await service.listRecommendations({
|
||||||
|
limit: 1,
|
||||||
|
order: [
|
||||||
|
{
|
||||||
|
field: 'id',
|
||||||
|
direction: 'desc'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
assert.equal(response.length, 1);
|
||||||
|
assert.equal(response[0].id, '3');
|
||||||
|
assert.equal(response[0] instanceof Recommendation, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses a default limit and page', async function () {
|
||||||
|
const recommendation = Recommendation.create({
|
||||||
|
id: '2',
|
||||||
|
url: 'http://localhost/1',
|
||||||
|
title: 'Test',
|
||||||
|
reason: null,
|
||||||
|
excerpt: null,
|
||||||
|
featuredImage: null,
|
||||||
|
favicon: null,
|
||||||
|
oneClickSubscribe: false
|
||||||
|
});
|
||||||
|
await service.repository.save(recommendation);
|
||||||
|
|
||||||
|
const recommendation2 = Recommendation.create({
|
||||||
|
id: '3',
|
||||||
|
url: 'http://localhost/2',
|
||||||
|
title: 'Test 2',
|
||||||
|
reason: null,
|
||||||
|
excerpt: null,
|
||||||
|
featuredImage: null,
|
||||||
|
favicon: null,
|
||||||
|
oneClickSubscribe: false
|
||||||
|
});
|
||||||
|
await service.repository.save(recommendation2);
|
||||||
|
|
||||||
|
const response = await service.listRecommendations({});
|
||||||
|
assert.equal(response.length, 2);
|
||||||
|
assert.equal(response[0] instanceof Recommendation, false);
|
||||||
|
assert.equal(response[1] instanceof Recommendation, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('countRecommendations', function () {
|
||||||
|
it('returns count', async function () {
|
||||||
|
const recommendation = Recommendation.create({
|
||||||
|
id: '2',
|
||||||
|
url: 'http://localhost/1',
|
||||||
|
title: 'Test',
|
||||||
|
reason: null,
|
||||||
|
excerpt: null,
|
||||||
|
featuredImage: null,
|
||||||
|
favicon: null,
|
||||||
|
oneClickSubscribe: false
|
||||||
|
});
|
||||||
|
await service.repository.save(recommendation);
|
||||||
|
|
||||||
|
assert.equal(await service.countRecommendations({}), 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('trackClicked', function () {
|
||||||
|
it('adds click event', async function () {
|
||||||
|
await service.trackClicked({id: '1'});
|
||||||
|
assert.equal(await service.clickEventRepository.getCount({}), 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('trackSubscribed', function () {
|
||||||
|
it('adds subscribe event', async function () {
|
||||||
|
await service.trackSubscribed({id: '1', memberId: '1'});
|
||||||
|
assert.equal(await service.subscribeEventRepository.getCount({}), 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('readRecommendationByUrl', function () {
|
||||||
|
it('returns if found', async function () {
|
||||||
|
const recommendation = Recommendation.create({
|
||||||
|
id: '2',
|
||||||
|
url: 'http://localhost/1',
|
||||||
|
title: 'Test',
|
||||||
|
reason: null,
|
||||||
|
excerpt: null,
|
||||||
|
featuredImage: null,
|
||||||
|
favicon: null,
|
||||||
|
oneClickSubscribe: false
|
||||||
|
});
|
||||||
|
await service.repository.save(recommendation);
|
||||||
|
|
||||||
|
const response = await service.readRecommendationByUrl(new URL('http://localhost/1'));
|
||||||
|
assert.deepEqual(response, recommendation.plain);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null if not found', async function () {
|
||||||
|
const response = await service.readRecommendationByUrl(new URL('http://localhost/1'));
|
||||||
|
assert.equal(response, null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
78
ghost/recommendations/test/WellknownService.test.ts
Normal file
78
ghost/recommendations/test/WellknownService.test.ts
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
import assert from 'assert/strict';
|
||||||
|
import fs from 'fs/promises';
|
||||||
|
import path from 'path';
|
||||||
|
import {Recommendation} from '../src/Recommendation';
|
||||||
|
import {WellknownService} from '../src/WellknownService';
|
||||||
|
|
||||||
|
const dir = path.join(__dirname, 'data');
|
||||||
|
|
||||||
|
async function getContent() {
|
||||||
|
const content = await fs.readFile(path.join(dir, '.well-known', 'recommendations.json'), 'utf8');
|
||||||
|
return JSON.parse(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('WellknownService', function () {
|
||||||
|
const service = new WellknownService({
|
||||||
|
urlUtils: {
|
||||||
|
relativeToAbsolute(url: string) {
|
||||||
|
return 'https://example.com' + url;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dir
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async function () {
|
||||||
|
// Remove folder
|
||||||
|
await fs.rm(dir, {recursive: true, force: true});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Can save recommendations', async function () {
|
||||||
|
const recommendations = [
|
||||||
|
Recommendation.create({
|
||||||
|
title: 'My Blog',
|
||||||
|
reason: null,
|
||||||
|
excerpt: null,
|
||||||
|
featuredImage: null,
|
||||||
|
favicon: null,
|
||||||
|
url: 'https://example.com/blog',
|
||||||
|
oneClickSubscribe: false,
|
||||||
|
createdAt: new Date('2021-01-01T00:00:00Z'),
|
||||||
|
updatedAt: new Date('2021-02-01T00:00:00Z')
|
||||||
|
}),
|
||||||
|
Recommendation.create({
|
||||||
|
title: 'My Other Blog',
|
||||||
|
reason: null,
|
||||||
|
excerpt: null,
|
||||||
|
featuredImage: null,
|
||||||
|
favicon: null,
|
||||||
|
url: 'https://example.com/blog2',
|
||||||
|
oneClickSubscribe: false,
|
||||||
|
createdAt: new Date('2021-01-01T00:00:00Z'),
|
||||||
|
updatedAt: null
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
await service.set(recommendations);
|
||||||
|
|
||||||
|
// Check that the file exists
|
||||||
|
assert.deepEqual(await getContent(), [
|
||||||
|
{
|
||||||
|
url: 'https://example.com/blog',
|
||||||
|
created_at: '2021-01-01T00:00:00.000Z',
|
||||||
|
updated_at: '2021-02-01T00:00:00.000Z'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'https://example.com/blog2',
|
||||||
|
created_at: '2021-01-01T00:00:00.000Z',
|
||||||
|
updated_at: '2021-01-01T00:00:00.000Z'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Can get URL', async function () {
|
||||||
|
assert.equal(
|
||||||
|
(await service.getURL()).toString(),
|
||||||
|
'https://example.com/.well-known/recommendations.json'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Add table
Reference in a new issue