diff --git a/server/routes/api/feed-monitor.test.ts b/server/routes/api/feed-monitor.test.ts new file mode 100644 index 00000000..0dd10ff0 --- /dev/null +++ b/server/routes/api/feed-monitor.test.ts @@ -0,0 +1,334 @@ +import fs from 'fs'; +import supertest from 'supertest'; + +import app from '../../app'; +import {getAuthToken} from './auth'; +import {getTempPath} from '../../models/TemporaryStorage'; + +import type {AddFeedOptions, AddRuleOptions, ModifyFeedOptions} from '../../../shared/types/api/feed-monitor'; +import type {Feed, Rule} from '../../../shared/types/Feed'; + +const request = supertest(app); + +const authToken = `jwt=${getAuthToken('_config')}`; + +const feed: AddFeedOptions = { + label: 'NYTimes', + url: 'https://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml', + interval: 1, +}; + +let addedFeed: Feed | null = null; + +describe('GET /api/feed-monitor', () => { + it('Expects nothing, yet. Verifies data structure.', (done) => { + request + .get('/api/feed-monitor') + .send() + .set('Cookie', [authToken]) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + .end((err, res) => { + if (err) done(err); + + const expectedResponse = { + feeds: [], + rules: [], + }; + + expect(res.body).toStrictEqual(expectedResponse); + + done(); + }); + }); +}); + +describe('PUT /api/feed-monitor/feeds', () => { + it('Subscribes to a feed', (done) => { + request + .put('/api/feed-monitor/feeds') + .send(feed) + .set('Cookie', [authToken]) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + .end((err, res) => { + if (err) done(err); + + const response: Feed = res.body; + + expect(response).toMatchObject(feed); + + expect(response._id).not.toBeNull(); + expect(typeof response._id).toBe('string'); + + addedFeed = response; + + done(); + }); + }); + + it('GET /api/feed-monitor to verify added feed', (done) => { + request + .get('/api/feed-monitor') + .send() + .set('Cookie', [authToken]) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + .end((err, res) => { + if (err) done(err); + + const expectedResponse = { + feeds: [addedFeed], + rules: [], + }; + + expect(res.body).toStrictEqual(expectedResponse); + + done(); + }); + }); + + it('GET /api/feed-monitor/feeds to verify added feed', (done) => { + request + .get('/api/feed-monitor/feeds') + .send() + .set('Cookie', [authToken]) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + .end((err, res) => { + if (err) done(err); + + expect(res.body).toStrictEqual([addedFeed]); + + done(); + }); + }); +}); + +describe('PATCH /api/feed-monitor/feeds/{id}', () => { + const modifyFeedOptions: ModifyFeedOptions = { + label: 'Modified Feed', + }; + + it('Modifies the added feed', (done) => { + expect(addedFeed).not.toBe(null); + if (addedFeed == null) return; + + request + .patch(`/api/feed-monitor/feeds/${addedFeed._id}`) + .send(modifyFeedOptions) + .set('Cookie', [authToken]) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + .end((err, _res) => { + if (err) done(err); + + done(); + }); + }); + + it('GET /api/feed-monitor/feeds/{id} to verify modified feed', (done) => { + expect(addedFeed).not.toBe(null); + if (addedFeed == null) return; + + request + .get(`/api/feed-monitor/feeds/${addedFeed._id}`) + .send() + .set('Cookie', [authToken]) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + .end((err, res) => { + if (err) done(err); + + addedFeed = {...(addedFeed as Feed), ...modifyFeedOptions}; + + expect(res.body).toStrictEqual([addedFeed]); + + done(); + }); + }); +}); + +describe('GET /api/feed-monitor/feeds/{id}/items', () => { + it('Requests items of the feed', (done) => { + expect(addedFeed).not.toBe(null); + if (addedFeed == null) return; + + request + .get(`/api/feed-monitor/feeds/${addedFeed._id}/items`) + .send() + .set('Cookie', [authToken]) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + .end((err, res) => { + if (err) done(err); + + expect(Array.isArray(res.body)).toBe(true); + + done(); + }); + }); +}); + +const tempDirectory = getTempPath('download'); + +fs.mkdirSync(tempDirectory, {recursive: true}); + +let addedRule: Rule; + +describe('GET /api/feed-monitor/rules', () => { + it('Expects nothing, verifies the response is an array', (done) => { + request + .get(`/api/feed-monitor/rules`) + .send() + .set('Cookie', [authToken]) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + .end((err, res) => { + if (err) done(err); + + expect(Array.isArray(res.body)).toBe(true); + + done(); + }); + }); +}); + +describe('PUT /api/feed-monitor/rules', () => { + const rule: AddRuleOptions = { + label: 'Test rule', + feedIDs: [''], + match: '', + exclude: '.*', + destination: tempDirectory, + tags: ['FeedItem'], + startOnLoad: false, + }; + + it('Adds an automation rule', (done) => { + expect(addedFeed).not.toBe(null); + if (addedFeed == null) return; + rule.feedIDs = [addedFeed._id]; + + request + .put('/api/feed-monitor/rules') + .send(rule) + .set('Cookie', [authToken]) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + .end((err, res) => { + if (err) done(err); + + const response: Rule = res.body; + + expect(response).toMatchObject(rule); + + expect(response._id).not.toBeNull(); + expect(typeof response._id).toBe('string'); + + addedRule = response; + + done(); + }); + }); + + it('GET /api/feed-monitor to verify added rule', (done) => { + request + .get('/api/feed-monitor') + .send() + .set('Cookie', [authToken]) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + .end((err, res) => { + if (err) done(err); + + expect(res.body.rules).toStrictEqual([addedRule]); + + done(); + }); + }); + + it('GET /api/feed-monitor/rules to verify added rule', (done) => { + request + .get('/api/feed-monitor/rules') + .send() + .set('Cookie', [authToken]) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + .end((err, res) => { + if (err) done(err); + + expect(res.body).toStrictEqual([addedRule]); + + done(); + }); + }); +}); + +describe('DELETE /api/feed-monitor/{id}', () => { + it('Deletes the added feed', (done) => { + expect(addedFeed).not.toBe(null); + if (addedFeed == null) return; + + request + .delete(`/api/feed-monitor/${addedFeed._id}`) + .send() + .set('Cookie', [authToken]) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + .end((err, _res) => { + if (err) done(err); + + done(); + }); + }); + + it('Deletes the added rule', (done) => { + request + .delete(`/api/feed-monitor/${addedRule._id}`) + .send() + .set('Cookie', [authToken]) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + .end((err, _res) => { + if (err) done(err); + + done(); + }); + }); + + it('GET /api/feed-monitor to verify feed and rule are deleted', (done) => { + request + .get('/api/feed-monitor') + .send() + .set('Cookie', [authToken]) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + .end((err, res) => { + if (err) done(err); + + const expectedResponse = { + feeds: [], + rules: [], + }; + + expect(res.body).toStrictEqual(expectedResponse); + + done(); + }); + }); +}); diff --git a/server/routes/api/feed-monitor.ts b/server/routes/api/feed-monitor.ts index 8a8f04de..2bff2363 100644 --- a/server/routes/api/feed-monitor.ts +++ b/server/routes/api/feed-monitor.ts @@ -50,18 +50,19 @@ router.delete<{id: string}>('/:id', (req, res) => { }); /** - * GET /api/feed-monitor/feeds + * GET /api/feed-monitor/feeds/{id?} * @summary Gets subscribed feeds * @tags Feeds * @security User + * @param id.path.optional - Unique ID of the feed subscription * @return {Array}} 200 - success response - application/json * @return {Error} 500 - failure response - application/json */ -router.get('/feeds', (req, res) => { +router.get<{id?: string}>('/feeds/:id?', (req, res) => { const callback = ajaxUtil.getResponseFn(res); req.services?.feedService - .getFeeds() + .getFeeds(req.params.id) .then((feeds) => { callback(feeds); }) @@ -173,8 +174,8 @@ router.put('/rules', (req, res) => { req.services?.feedService .addRule(req.body) - .then(() => { - callback(null); + .then((rule) => { + callback(rule); }) .catch((error) => { callback(null, error); diff --git a/server/services/feedService.ts b/server/services/feedService.ts index cf9e30db..f9aaeb28 100644 --- a/server/services/feedService.ts +++ b/server/services/feedService.ts @@ -84,11 +84,13 @@ class FeedService extends BaseService { throw new Error(); } + // JSON.parse(JSON.stringify()) to remove undefined properties + modifiedFeedReader.stopReader(); - modifiedFeedReader.modify({feedLabel: label, url, interval}); + modifiedFeedReader.modify(JSON.parse(JSON.stringify({feedLabel: label, url, interval}))); return new Promise((resolve, reject) => { - this.db.update({_id: id}, {$set: {url, label, interval}}, {}, (err) => { + this.db.update({_id: id}, {$set: JSON.parse(JSON.stringify({label, url, interval}))}, {}, (err) => { if (err) { reject(err); return; @@ -160,9 +162,9 @@ class FeedService extends BaseService { }); } - async getFeeds(): Promise> { + async getFeeds(id?: string): Promise> { return new Promise>((resolve, reject) => { - this.db.find({type: 'feed'}, (err: Error | null, feeds: Array) => { + this.db.find(id ? {_id: id} : {type: 'feed'}, (err: Error | null, feeds: Array) => { if (err) { reject(err); return; diff --git a/server/tsconfig.json b/server/tsconfig.json index 8012c8e5..546696f8 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -14,6 +14,5 @@ }, "outDir": "../dist" }, - "include": ["./**/*.ts", "../shared/**/*.ts", "../config.js", "../config.ts"], - "exclude": ["node_modules", "**/*.spec.ts", "**/*.test.ts"] + "include": ["./**/*.ts", "../shared/**/*.ts", "../config.js", "../config.ts"] } diff --git a/shared/types/Feed.ts b/shared/types/Feed.ts index d871ef29..287d1931 100644 --- a/shared/types/Feed.ts +++ b/shared/types/Feed.ts @@ -6,7 +6,7 @@ export interface Feed { label: string; // URL of the feed. url: string; - // Interval between checking the feed for new items. + // Interval between checking the feed for new items. (in minutes) interval: number; // How many times rules have matched items of the feed. count?: number;