import assert from 'assert/strict'; import {RecommendationController, RecommendationService} from '../src'; import sinon, {SinonSpy} from 'sinon'; describe('RecommendationController', function () { let service: Partial; 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', description: 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', description: 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, description: plain.description, 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', description: 'My description', 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', description: 'My description', 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, description: plain.description, 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', description: 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('check', function () { it('should return url metadata', async function () { service.checkRecommendation = async (url) => { return { excerpt: 'Updated excerpt', url }; }; const result = await controller.check({ data: { recommendations: [ { url: 'https://example.com/' } ] }, options: {}, user: {} }); assert.deepEqual(result, { data: [{ excerpt: 'Updated excerpt', created_at: null, updated_at: null, description: null, favicon: null, featured_image: null, id: null, one_click_subscribe: null, title: null, url: 'https://example.com/', count: undefined }], meta: undefined }); }); it('should serialize undefined url', async function () { service.checkRecommendation = async () => { return { excerpt: 'Updated excerpt', url: undefined }; }; const result = await controller.check({ data: { recommendations: [ { url: 'https://example.com/' } ] }, options: {}, user: {} }); assert.deepEqual(result, { data: [{ excerpt: 'Updated excerpt', created_at: null, updated_at: null, description: null, favicon: null, featured_image: null, id: null, one_click_subscribe: null, title: null, url: 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', description: edit.description || 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', description: 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', description: edit.description || 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 description: 'My description', 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', description: 'My description', 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', description: 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', description: 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', description: 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', description: 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 title, description, excerpt, one_click_subscribe, created_at, updated_at, count.clicks, count.subscribers' } ); }); 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', description: 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'); }); }); });