Compare commits

...

25 Commits

Author SHA1 Message Date
Amaury
086236f719 Fixed test for the previous commit 2021-07-06 14:52:50 +02:00
Amaury
fcf7c24b86 private some fields and now newEntries is called only for the second update 2021-07-06 14:45:25 +02:00
Amaury
9c034a6100 Clean the typescript config 2021-06-29 17:50:27 +02:00
Amaury
d89541fe3a added a early return when a double start is done. (cf issue 2) 2021-06-29 17:49:59 +02:00
Amaury
19a6869c55 Refactor name inside README and Tests 2021-06-29 17:49:01 +02:00
Amaury
1f4029dad2 Move the ListenerRSSInfos inside the ListenerRSS namespace 2021-06-28 19:05:56 +02:00
Florent
e6395483e2 fix double import of ListenerRSSInfo in listener-rss 2021-04-25 18:01:42 +02:00
Amaury
855880617d update readme 2021-04-18 18:14:47 +02:00
Florent Fayolle
ac12992c60 add prepublish script 2021-04-18 17:41:15 +02:00
Amaury
4c2c0b7220 add license 2021-04-18 17:24:43 +02:00
Amaury
c7d066790e remove cross-env 2021-04-18 17:10:04 +02:00
Amaury
b2f34a6b01 Deplacement de l'index de la racine vers le source 2021-04-18 16:16:15 +02:00
37744733b4 inside start function we're awaiting the fetch call 2021-04-10 17:43:59 +02:00
b39e47f4ea Adding getProperty doc into README 2021-03-28 16:20:54 +02:00
c02df46440 Adding getProperty to export the ListenerRSS properties + related tests 2021-03-28 13:22:55 +02:00
271f445aef update Readme 2021-03-23 18:20:42 +01:00
df3f2a3049 Add somes tests about lastEntriesLink field and patch an error about lastEntriesLink's typing 2021-03-23 18:10:57 +01:00
ce1d8f4ab6 Remove name field adding lastentries parameter and give the new entries for the first update 2021-03-21 21:35:22 +01:00
f919726e4d Remove name field adding lastentries parameter and give the new entries for the first update 2021-03-21 21:33:16 +01:00
67907d7cfb update readme + more comments 2021-03-12 13:53:08 +01:00
30f5e576d0 add new test + some little refactors 2021-03-08 09:27:32 +01:00
01392a2c20 add newEntries event 2021-03-08 09:26:38 +01:00
Florent
dd4bf59e41 Improve tests a little bit 2021-03-06 17:32:59 +01:00
f6d98e472e some refactors 2021-03-06 16:17:34 +01:00
920c160632 Implements ts-mock-imports 2021-02-28 18:56:33 +01:00
12 changed files with 4437 additions and 392 deletions

19
LICENSE Normal file
View File

@ -0,0 +1,19 @@
MIT License Copyright (c) <year> <copyright holders>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next
paragraph) shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

126
README.md
View File

@ -1,6 +1,6 @@
# easy-rss-parser
# listener rss
A lightweight library to give some additions for the [rss-parser package](https://github.com/rbren/rss-parser).
A lightweight library to make simple actions with a RSS feed.
# USAGE
@ -10,15 +10,15 @@ You can parse RSS from a URL with some custom data.
An example :
```js
const easyParser = require("easy-rss-parser");
const ListenerRss = easyParser.ListenerRss;
const ListenerModule = require("ListenerRSS");
const ListenerRss = ListenerModule.ListenerRss;
let listener = new ListenerRss("my-test-service", "fake.rss.service");
const listener = new ListenerRss({
address: "fake.rss.service"
});
// make a request to the adr 'fake.rss.service'
myListener.fetchRSS().then((obj, err) => {
// some act
});
const feed = await myListener.fetchRSS();
```
## Recurrent usage
@ -27,16 +27,19 @@ You can parse RSS from a URL each n times.
An example :
```js
const easyParser = require("easy-rss-parser");
const ListenerRss = easyParser.ListenerRss;
const ListenerModule = require("ListenerRSS");
const ListenerRss = ListenerModule.ListenerRss;
let listener = new ListenerRss("my-test-service", "fake.rss.service", 5 * 60);
const listener = new ListenerRss({
address: "fake.rss.service"
});
let callback_fun = (obj, err) => {
// some act
};
// call callback_fun each 5 minutes
listener.start(callback_fun);
listener.on("update", feed => { /* ... */ });
listener.on("error", err => { /* ... */ });
listener.on("newEntries", feedEntries => { /* ... */ });
listener.start();
/*...*/
@ -45,34 +48,25 @@ listener.stop();
# Documentation
## ListenerRSSInfo
## ListenerRss.Config
A class to structure listener's data.
An interface to structure listener's data.
### Constructor
`constructor(name, address, timeloop, customfields)`
- name : the service name
- address : the service address
- [optional] timeloop : time to wait between 2 request in seconds (default 5 minutes)
- [optional] customfields : to notice field who's custom to the service (default blank)
[cf annexe CustomFields](#customfields)
- [optional] lastEntriesLinks : to specify an predefined history.
## ListenerRSS
### Constructor
`constructor(listenerRSSInfo)`
`constructor(ListenerRss.Config)`
- listenerRSSInfo : object from the ListenerRSSInfo's class.
`constructor(name, address, timeloop, customfields)`
- name : the service name
- address : the service address
- [optional] timeloop : time to wait between 2 request in seconds (default 5 minutes)
- [optional] customfields : to notice field who's custom to the service (default blank)
- ListenerRss.Config : object from the ListenerRss.Config's class.
### fetchRSS()
@ -85,26 +79,45 @@ object who's contain the data. [cf Annexe Output](#output)
#### Issues
Return an error if the server can't be resolved.
Reject the promise if the server can't be resolved.
### start(callbackFun)
### start()
This function will execute the callbackFun each time loop.
This function will call the `update` event to each success update, the
`error` event to each fail update, and the `newEntries` event for
each update who contains a new item.
#### Parameter
#### Events
The `callbackFun` is the function who's going to be called each time loop. She need to be under the shape :
Each event take one arg into the callback function.
```js
(obj, err) => {
/*...*/
};
listener.on("update", feed => { /* ... */ });
listener.on("error", err => { /* ... */ });
listener.on("newEntries", feedEntries => { /* ... */ });
```
#### update
It used a callback who receive the received object entirely inside an object.
#### error
It used a callback who receive an error object.
#### newEntries
It used a callback who receive only new entries inside an array.
### stop()
This function will stop the execution of the callbackFun each time loop.
### getProperty()
This function will return a ListenerRss.Config (a.k.a. a JSON object) item corresponding to the internal configuration of the class.
# Annexe
## CustomFields
@ -146,21 +159,24 @@ In this case it's useless to specify the parent field, so you can just omit the
Here an example of what type of json object is output during a fetch :
```json
feedUrl: 'fake.rrs.service'
title: 'myFakeApiTitle'
description: 'My Fake api desc'
link: 'fake.rrs.service'
items:
- title: 'My last item'
link: 'fake.rrs.service/item1'
pubDate: 'Thu, 12 Nov 2015 21:16:39 +0000'
creator: 'someone'
content: '<a href="http://example.com">this is a link</a> &amp; <b>this is bold text</b>'
contentSnippet: 'this is a link & this is bold text'
guid: 'fake.rrs.service/item1'
categories:
- test
- npm
- fakeInfos
isoDate: '2015-11-12T21:16:39.000Z'
{
"feedUrl": "fake.rrs.service",
"title": "myFakeApiTitle",
"description": "My Fake api desc",
"link": "fake.rrs.service",
"items": [
{
"title": "My last item",
"link": "fake.rrs.service/item1",
"pubDate": "Thu, 12 Nov 2015 21:16:39 +0000",
"creator": "someone",
"content": "<a href=\"http://example.com\">this is a link</a> &amp; <b>this is bold text</b>",
"contentSnippet": "this is a link & this is bold text",
"guid": "fake.rrs.service/item1",
"categories": ["test", "npm", "fakeInfos"],
"isoDate": "2015-11-12T21:16:39.000Z"
}
/*Some Others items*/
]
}
```

3804
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,14 @@
{
"name": "listener-rss",
"version": "1.0.0",
"description": "A lightweight library to give some additions for the [rss-parser package](https://github.com/rbren/rss-parser).",
"main": "index.js",
"version": "0.0.2",
"description": "A lightweight library to create a listener from a rss feed.",
"main": "build/index.js",
"types": "build/index.d.ts",
"scripts": {
"test": "cross-env TS_NODE_PROJECT='./tests/tsconfig.json' mocha --require ts-node/register ./tests/**/*-spec.ts",
"build": "tsc -p ./src"
"test": "env TS_NODE_PROJECT='./tests/tsconfig.json' mocha --require ts-node/register ./tests/**/*-spec.ts",
"build": "tsc",
"lint": "eslint 'tests/**/*.ts' 'src/**/*.ts'",
"prepublish": "npm run-script build && npm run-script lint && npm test"
},
"repository": {
"type": "git",
@ -15,10 +18,13 @@
"rss",
"rss-parser"
],
"author": "Amaury Joly <amaury.joly@hotmail.com>",
"author": "Amaury Joly <joly.amaury@hotmail.com>",
"contributors": [
"Florent Fayolle <florent.git@zeteo.me>"
],
"files": [
"build/"
],
"license": "MIT",
"devDependencies": {
"@types/chai": "^4.2.14",
@ -26,7 +32,6 @@
"@types/node": "^14.14.25",
"@typescript-eslint/eslint-plugin": "^4.14.2",
"@typescript-eslint/parser": "^4.14.2",
"cross-env": "7.0.3",
"chai": "4.3.0",
"eslint": "^7.19.0",
"eslint-config-airbnb-base": "^14.2.1",
@ -38,12 +43,13 @@
"prettier": "2.2.1",
"proxyquire": "2.1.3",
"sinon-chai": "3.5.0",
"ts-sinon": "2.0.1",
"ts-mock-imports": "1.3.3",
"ts-node": "9.1.1",
"ts-sinon": "2.0.1",
"tsc-watch": "^4.2.9",
"typescript": "^4.1.3"
},
"dependencies": {
"rss-parser": "3.11.0"
"rss-parser": "^3.11.0"
}
}

View File

@ -1,6 +0,0 @@
export interface ListenerRSSInfos {
readonly name: string; // name of the listener
readonly address: string; // feed's address
readonly timeloop?: number; // update time RSS feed
readonly customfields?: { [key: string]: string | string[] }; // rss fields custom
}

View File

@ -1,2 +1 @@
export { ListenerRss } from "./listener-rss";
export { ListenerRSSInfos } from "./Models/ListenerRSSInfos";

View File

@ -1,89 +1,104 @@
import Parser from "rss-parser/index";
import { ListenerRSSInfos as ListenerInfo } from "./Models/ListenerRSSInfos";
import Parser from "rss-parser";
import EventEmitter from "events";
const DEFAULT_TIMELOOP: number = 5 * 60; // default timeloop is 5 min
namespace ListenerRss {
export type Config = {
address: string; // feed's address
timeloop?: number; // update time RSS feed
customfields?: { [key: string]: string | string[] }; // rss fields custom
lastEntriesLinks?: string[]; // links from lastentries
};
}
/**
* Emit 'update' when he's making a fetch during the start fun
* Emit 'update_err' when the fetch has an issue
* Emit 'error' when the fetch has an issue
* Emit 'newEntries' when the fetch has new entris
*/
export class ListenerRss extends EventEmitter {
name: string = "";
class ListenerRss extends EventEmitter {
address: string = "";
timeloop: number = DEFAULT_TIMELOOP; // time in seconds
customfields?: { [key: string]: string[] | string };
// private fields
parser: Parser | undefined = undefined;
parser: Parser;
loopRunning: boolean = false;
lastEntriesLinks: string[] = [];
private firstUpdate = true;
/**
* @brief constructor
* @param config ListenerRSSInfos interface who's contain the ListenerInfos
* @param config ListenerRSSInfos interface who contains the ListenerInfos
*/
constructor(config: ListenerInfo) {
constructor(config: ListenerRss.Config) {
super();
this.setData(config);
this.setParser();
}
/**
* @brief Private function. Is useed to initilize the parser object with the customfields var
*/
setParser() {
// set parser
this.parser = new Parser(
this.customfields !== undefined
? {
customFields: {
feed: [],
item: Object.entries(this.customfields).map(([, value]) => {
return Array.isArray(value) ? value[0] : value;
}),
},
}
: {}
); // if customfield is set -> let's set the parser with, else let the option empty
}
/**
* @brief Private function. Initialized the listener with an ListenerRSSInfos interface
* @param infos ListenerRSSInfos interface who's contain the ListenerInfos
*/
setData(infos: ListenerInfo) {
// Set data
this.name = infos.name;
this.address = infos.address;
this.address = config.address;
this.timeloop =
infos.timeloop === undefined ? DEFAULT_TIMELOOP : infos.timeloop;
this.customfields = infos.customfields;
config.timeloop === undefined ? DEFAULT_TIMELOOP : config.timeloop;
this.customfields = config.customfields;
this.lastEntriesLinks =
config.lastEntriesLinks === undefined ? [] : config.lastEntriesLinks;
this.parser = this.generateParser();
}
/**
* @brief use the parseURL function from rss-parser with the objects datas
* @brief Private function. Is used to initilize the parser object with the customfields var
*/
generateParser() {
const parserConfig = this.customfields && {
customFields: {
feed: [],
item: Object.entries(this.customfields).map(([, value]) => {
return Array.isArray(value) ? value[0] : value;
}),
},
};
return new Parser(parserConfig);
}
/**
* @brief use the parseURL function from rss-parser with the objects data
* @return return a promise with the received data
*/
fetchRSS(): Promise<Parser.Output<any>> {
if (this.parser !== undefined && this.address !== undefined) {
return this.parser.parseURL(this.address).catch((err) => {
throw new Error("bad address or no access : " + err);
});
} else throw new Error("listener must be first initialized");
return this.parser.parseURL(this.address);
}
/**
* @brief call the callback function each looptime
* @param callback function who's going to be called with the latest get
*/
start(): void {
if (this.loopRunning) return;
this.loopRunning = true;
const fun: () => void = () => {
this.fetchRSS()
.then((obj: { [key: string]: any }) => this.emit("update", obj))
.catch((err) => this.emit("update_err", err));
const fun: () => void = async () => {
await Promise.resolve(
await this.fetchRSS()
.then((obj: { [key: string]: any }) => {
this.emit("update", obj);
const updatedEntriesLinks = obj.items.map(
(item: { link: string }) => item.link
);
const newEntries = obj.items.filter(
(item: { link: string }) =>
!this.lastEntriesLinks.includes(item.link)
);
this.lastEntriesLinks = updatedEntriesLinks;
if (!this.firstUpdate && newEntries.length !== 0)
this.emit("newEntries", newEntries);
this.firstUpdate = false;
})
.catch((err) => this.emit("error", err))
);
};
(async () => {
@ -100,4 +115,19 @@ export class ListenerRss extends EventEmitter {
stop(): void {
this.loopRunning = false;
}
/**
* @brief parse the datas inti a ListenerRSSInfos object
* @return return a ListenerRSSInfos object
*/
getProperty(): ListenerRss.Config {
return {
address: this.address,
customfields: this.customfields,
lastEntriesLinks: this.lastEntriesLinks,
timeloop: this.timeloop,
};
}
}
export { ListenerRss };

View File

@ -1,10 +0,0 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"moduleResolution": "node"
},
"exclude": [
"build/",
"node_modules"
]
}

View File

@ -1,53 +1,43 @@
// external lib
import Parser from "rss-parser";
import * as Parser from "rss-parser";
// tested class
import {
ListenerRSSInfos as ListenerRRSInfo,
ListenerRss as Listeners,
} from "./../src/index";
import { ListenerRss } from "../";
// Unit test
import assert from "assert";
import * as chai from "chai";
import * as sinon from "ts-sinon";
const sinonChai = require("sinon-chai");
import sinon from "ts-sinon";
import sinonChai from "sinon-chai";
import { ImportMock, InPlaceMockManager } from "ts-mock-imports";
chai.use(sinonChai);
const expect = chai.expect;
describe("test class RSS: jsonfile", function () {
// let myListener: Listeners | undefined = undefined;
const infosListener: ListenerRRSInfo = {
name: "my-test-service",
const infosListener: ListenerRss.Config = {
address: "fake.rss.service",
timeloop: 15,
customfields: {
description: ["media:group", "media:description"],
icon: ["media:group", "media:thumbnail"],
},
lastEntriesLinks: ["my_url_02.com"],
};
// parseURL tests
let stubListener: sinon.StubbedInstance<Listeners>;
let stubParser: sinon.StubbedInstance<Parser>;
const mockedRSSOutput: Parser.Output<{
"media:group": { [key: string]: string | [any] };
}> = {
const mockedRSSOutput: Parser.Output<any> = {
items: [
{
title: "my title 00",
title: "my title 02",
"media:group": {
"media:description": "my description 00",
"media:description": "my description 02",
"media:thumbnail": [
{ $: { height: 360, width: 420, url: "my_image00.jpg" } },
{ $: { height: 360, width: 420, url: "my_image02.jpg" } },
],
},
link: "my_url_00.com",
pubDate: "myDate00",
link: "my_url_02.com",
pubDate: "myDate02",
},
{
title: "my title 01",
@ -61,86 +51,34 @@ describe("test class RSS: jsonfile", function () {
pubDate: "myDate01",
},
{
title: "my title 02",
title: "my title 00",
"media:group": {
"media:description": "my description 02",
"media:description": "my description 00",
"media:thumbnail": [
{ $: { height: 360, width: 420, url: "my_image02.jpg" } },
{ $: { height: 360, width: 420, url: "my_image00.jpg" } },
],
},
link: "my_url_02.com",
pubDate: "myDate02",
link: "my_url_00.com",
pubDate: "myDate00",
},
],
};
/**
* The function create my Stubs for my Listener and my Parser
*/
function fun_initStub(
myListener: Listeners
): sinon.StubbedInstance<Listeners> {
stubListener = sinon.stubObject<Listeners>(myListener, ["setParser"]);
stubListener.setParser.callsFake(() => {
if (stubListener.parser !== undefined) {
stubParser = sinon.stubObject<Parser>(stubListener.parser, [
"parseURL",
]);
stubParser.parseURL
.withArgs(infosListener.address)
.resolves(mockedRSSOutput);
stubParser.parseURL
.withArgs("bad.rss.service")
.rejects(new Error("connect ECONNREFUSED 127.0.0.1:80"));
}
stubListener.parser = stubParser;
});
stubListener.setParser();
// stubListener.fetchRSS.returns(stubParser.parseURL(stubListener.address));
return stubListener;
}
// afterEach(function () {
// // restore stubs
// myListener = undefined;
// });
describe("Building Ytb listener", function () {
it("The build without issues (infosListener parameters)", function () {
let myListener = new Listeners(infosListener);
// assertions
// myListener data
expect(myListener.timeloop).to.eql(15);
expect(myListener.name).to.eql("my-test-service");
expect(myListener.address).to.eql("fake.rss.service");
expect(myListener.customfields).to.eql({
description: ["media:group", "media:description"],
icon: ["media:group", "media:thumbnail"],
});
expect(myListener.parser)
.to.have.property("options")
.to.have.property("customFields")
.to.be.eql({
feed: [],
item: ["media:group", "media:group"],
});
});
it("The build without issues (raw infos : 4 params)", function () {
let myListener = new Listeners({
name: "my-test-service",
it("builds with 4 params", function () {
const myListener = new ListenerRss({
address: "fake.rss.service",
timeloop: 15,
customfields: {
description: ["media:group", "media:description"],
icon: ["media:group", "media:thumbnail"],
},
lastEntriesLinks: ["my_url_02.com"],
});
// assertions
// myListener data
expect(myListener.timeloop).to.eql(15);
expect(myListener.name).to.eql("my-test-service");
expect(myListener.address).to.eql("fake.rss.service");
expect(myListener.customfields).to.eql({
description: ["media:group", "media:description"],
@ -153,17 +91,19 @@ describe("test class RSS: jsonfile", function () {
feed: [],
item: ["media:group", "media:group"],
});
expect(myListener.lastEntriesLinks).to.be.eql(["my_url_02.com"]);
});
it("The build without issues (raw infos : just 2 params)", function () {
let myListener = new Listeners({
name: "my-test-service",
it("builds with 3 params (no custom fields)", function () {
const myListener = new ListenerRss({
address: "fake.rss.service",
timeloop: 15,
lastEntriesLinks: ["my_url_02.com"],
});
// assertions
// myListener data
expect(myListener.timeloop).to.eql(5 * 60);
expect(myListener.name).to.eql("my-test-service");
expect(myListener.timeloop).to.eql(15);
expect(myListener.address).to.eql("fake.rss.service");
expect(myListener.customfields).to.eql(undefined);
expect(myListener.parser)
@ -173,194 +113,400 @@ describe("test class RSS: jsonfile", function () {
feed: [],
item: [],
});
});
});
it("The build without issues (raw infos : just 3 params (no custom fields))", function () {
let myListener = new Listeners({
name: "my-test-service",
address: "fake.rss.service",
timeloop: 15,
expect(myListener.lastEntriesLinks).to.be.eql(["my_url_02.com"]);
});
// assertions
// myListener data
expect(myListener.timeloop).to.eql(15);
expect(myListener.name).to.eql("my-test-service");
expect(myListener.address).to.eql("fake.rss.service");
expect(myListener.customfields).to.eql(undefined);
expect(myListener.parser)
.to.have.property("options")
.to.have.property("customFields")
.to.be.eql({
feed: [],
item: [],
it("build with 3 params (no timeloop)", function () {
const myListener = new ListenerRss({
address: "fake.rss.service",
customfields: {
description: ["media:group", "media:description"],
icon: ["media:group", "media:thumbnail"],
},
lastEntriesLinks: ["my_url_02.com"],
});
});
it("The build without issues (raw infos : just 3 params (no timeloop))", function () {
let myListener = new Listeners({
name: "my-test-service",
address: "fake.rss.service",
customfields: {
// assertions
// myListener data
expect(myListener.timeloop).to.eql(5 * 60);
expect(myListener.address).to.eql("fake.rss.service");
expect(myListener.customfields).to.eql({
description: ["media:group", "media:description"],
icon: ["media:group", "media:thumbnail"],
},
});
expect(myListener.parser)
.to.have.property("options")
.to.have.property("customFields")
.to.be.eql({
feed: [],
item: ["media:group", "media:group"],
});
expect(myListener.lastEntriesLinks).to.be.eql(["my_url_02.com"]);
});
// assertions
// myListener data
expect(myListener.timeloop).to.eql(5 * 60);
expect(myListener.name).to.eql("my-test-service");
expect(myListener.address).to.eql("fake.rss.service");
expect(myListener.customfields).to.eql({
description: ["media:group", "media:description"],
icon: ["media:group", "media:thumbnail"],
});
expect(myListener.parser)
.to.have.property("options")
.to.have.property("customFields")
.to.be.eql({
feed: [],
item: ["media:group", "media:group"],
it("build with 3 params (no lastEntries)", function () {
const myListener = new ListenerRss({
address: "fake.rss.service",
customfields: {
description: ["media:group", "media:description"],
icon: ["media:group", "media:thumbnail"],
},
});
// assertions
// myListener data
expect(myListener.timeloop).to.eql(5 * 60);
expect(myListener.address).to.eql("fake.rss.service");
expect(myListener.customfields).to.eql({
description: ["media:group", "media:description"],
icon: ["media:group", "media:thumbnail"],
});
expect(myListener.parser)
.to.have.property("options")
.to.have.property("customFields")
.to.be.eql({
feed: [],
item: ["media:group", "media:group"],
});
expect(myListener.lastEntriesLinks).to.be.eql([]);
});
it("builds with 1 params (only address)", function () {
const myListener = new ListenerRss({
address: "fake.rss.service",
});
// assertions
// myListener data
expect(myListener.timeloop).to.eql(5 * 60);
expect(myListener.address).to.eql("fake.rss.service");
expect(myListener.customfields).to.eql(undefined);
expect(myListener.parser)
.to.have.property("options")
.to.have.property("customFields")
.to.be.eql({
feed: [],
item: [],
});
expect(myListener.lastEntriesLinks).to.be.eql([]);
});
});
describe("fetch some data", function () {
it("fetch without issues", function () {
let myListener = new Listeners(infosListener);
fun_initStub();
describe("export property", function () {
it("should export properties into a ListenerRSSInfos", function () {
// given
const myListener = new ListenerRss(infosListener);
expect(myListener).to.not.be.undefined;
if (myListener !== undefined) {
// fetch
let res = stubListener.fetchRSS();
// assertions
expect(myListener.getProperty()).to.be.eql(infosListener);
});
});
//assertion
// calls
expect(stubParser.parseURL).to.have.been.calledOnce;
expect(stubParser.parseURL).to.have.been.calledWith(
infosListener.address
);
describe("data fetching", function () {
it("fetches without issues", async function () {
// given
const mockManager: InPlaceMockManager<Parser> = ImportMock.mockClassInPlace<Parser>(
Parser
);
const stubParser = mockManager.mock("parseURL");
stubParser.resolves(mockedRSSOutput);
res
.then((obj: any) => {
expect(obj).to.be.eql(mockedRSSOutput);
})
.catch((err) => {
expect(err).to.be.undefined;
});
} else throw new Error("Error into the before instruction");
const myListener = new ListenerRss(infosListener);
// when
const res = await myListener.fetchRSS();
// then
expect(stubParser).to.have.been.calledOnce;
expect(stubParser).to.have.been.calledWith(infosListener.address);
expect(res).to.be.eql(mockedRSSOutput);
});
it("fetch with bad address", function () {
let myListener = new Listeners({
name: "my-test-service",
it("rejects when fetching fails", async function () {
// given
const mockManager: InPlaceMockManager<Parser> = ImportMock.mockClassInPlace<Parser>(
Parser
);
const stubParser = mockManager.mock("parseURL");
const err = new Error("connect ECONNREFUSED 127.0.0.1:80");
stubParser.rejects(err);
const myListener = new ListenerRss({
address: "bad.rss.service",
customfields: {
description: ["media:group", "media:description"],
icon: ["media:group", "media:thumbnail"],
},
});
let stubListener = fun_initStub(myListener);
// fetch
let res = stubListener.fetchRSS();
//assertion
// calls
expect(stubParser.parseURL).to.have.been.calledOnce;
expect(stubParser.parseURL).to.have.been.calledWith("bad.rss.service");
// Promise
res
.then((obj: any) => {
expect(obj).to.be.undefined;
})
.catch((err) => {
expect(err).to.be.eql(new Error("connect ECONNREFUSED 127.0.0.1:80"));
});
// when
await assert.rejects(() => myListener.fetchRSS(), err);
});
});
describe.skip("start", function () {
it("Let's start the timer", async function () {
let clock: sinon.default.SinonFakeTimers = sinon.default.useFakeTimers();
describe("start", function () {
it("fetches immediately the RSS information", async function () {
// given
const clock = sinon.useFakeTimers();
const mockManager: InPlaceMockManager<Parser> = ImportMock.mockClassInPlace<Parser>(
Parser
);
const stubParser = mockManager.mock("parseURL");
stubParser.resolves(mockedRSSOutput);
// classic build
let myListener = new Listeners({
name: "my-test-service",
address: "fake.rss.service",
timeloop: 60,
customfields: {
description: ["media:group", "media:description"],
icon: ["media:group", "media:thumbnail"],
},
});
let stubListener = fun_initStub(myListener);
stubListener.fetchRSS.reset();
stubListener.fetchRSS.resolves(mockedRSSOutput);
const myListener = new ListenerRss(infosListener);
//spy
let fun_spy: sinon.default.SinonSpy = sinon.default.spy((obj, err) => {
expect(obj).to.be.eql(mockedRSSOutput);
expect(err).to.be.eql(undefined);
});
const updateListenerSpy = sinon.spy();
// start timer
stubListener.on("update", (obj) => fun_spy(obj));
// stubListener.start(fun_spy);
myListener.on("update", updateListenerSpy);
// wait and assertion
// After 1ms
myListener.start();
// when
await clock.tickAsync(1);
expect(stubListener.fetchRSS).to.have.been.calledOnce;
expect(fun_spy).to.have.been.calledOnce;
// After 60s
await clock.tickAsync(59999);
expect(stubListener.fetchRSS).to.have.been.calledTwice;
expect(fun_spy).to.have.been.calledTwice;
stubListener.stop();
// then
expect(updateListenerSpy).to.have.been.calledOnce;
expect(updateListenerSpy).to.have.been.calledWith(mockedRSSOutput);
expect(stubParser).to.have.calledWith(myListener.address);
myListener.stop();
});
it("Let's start the timer (with a bad address)", async function () {
let clock: sinon.default.SinonFakeTimers = sinon.default.useFakeTimers();
it("has fetched multiple times after a while", async function () {
// given
const clock = sinon.useFakeTimers();
const mockManager: InPlaceMockManager<Parser> = ImportMock.mockClassInPlace<Parser>(
Parser
);
const stubParser = mockManager.mock("parseURL");
stubParser.resolves(mockedRSSOutput);
// classic build
let myListener = new Listeners({
name: "my-test-service",
address: "fake.rss.service",
timeloop: 60,
customfields: {
description: ["media:group", "media:description"],
icon: ["media:group", "media:thumbnail"],
},
const myListener = new ListenerRss({
...infosListener,
timeloop: 15,
});
let stubListener = fun_initStub(myListener);
stubListener.fetchRSS.reset();
stubListener.fetchRSS.rejects(
new Error("connect ECONNREFUSED 127.0.0.1:80")
);
//spy
let fun_spy: sinon.default.SinonSpy = sinon.default.spy((obj, err) => {
expect(obj).to.be.eql(undefined);
expect(err).to.not.be.eql(undefined);
});
const updateListenerSpy: sinon.SinonSpy = sinon.spy();
const newRSSOutput = {
...mockedRSSOutput,
items: mockedRSSOutput.items.concat({
title: "my title 03",
"media:group": {
"media:description": "my description 03",
"media:thumbnail": [
{ $: { height: 360, width: 420, url: "my_image03.jpg" } },
],
},
link: "my_url_03.com",
pubDate: "myDate03",
}),
};
// start timer
// stubListener.start(fun_spy);
myListener.on("update", updateListenerSpy);
myListener.start();
// when
await clock.tickAsync(1);
stubParser.resolves(newRSSOutput);
await clock.tickAsync(60000);
// then
expect(updateListenerSpy).to.have.been.callCount(5);
expect(updateListenerSpy.firstCall).to.have.been.calledWith(
mockedRSSOutput
);
expect(updateListenerSpy.secondCall).to.have.been.calledWith(
newRSSOutput
);
myListener.stop();
});
it("notifies with a 'error' when fetching fails", async function () {
const clock = sinon.useFakeTimers();
const mockManager: InPlaceMockManager<Parser> = ImportMock.mockClassInPlace<Parser>(
Parser
);
const stubParser = mockManager.mock("parseURL");
const expectedErr = new Error("connect ECONNREFUSED 127.0.0.1:80");
stubParser.rejects(expectedErr);
// classic build
const myListener = new ListenerRss({
...infosListener,
timeloop: 60,
});
//spy
const updateListenerSpy = sinon.spy();
const updateErrorListenerSpy = sinon.spy();
myListener.on("error", updateErrorListenerSpy);
myListener.on("update", updateListenerSpy);
// start timer
myListener.start();
// wait and assertion
// After 1ms
await clock.tickAsync(1);
expect(stubListener.fetchRSS).to.have.been.calledOnce;
expect(fun_spy).to.have.been.calledOnce;
expect(updateErrorListenerSpy).to.have.been.calledOnce;
expect(updateListenerSpy).to.not.have.been.called;
expect(updateErrorListenerSpy).to.have.been.calledWith(expectedErr);
// After 60s
await clock.tickAsync(59999);
expect(stubListener.fetchRSS).to.have.been.calledTwice;
expect(fun_spy).to.have.been.calledTwice;
await clock.tickAsync(60000);
expect(updateErrorListenerSpy).to.have.been.calledTwice;
expect(updateListenerSpy).to.not.have.been.called;
stubListener.stop();
// When the service is back
stubParser.resolves(mockedRSSOutput);
await clock.tickAsync(60000);
expect(updateListenerSpy).to.have.been.calledOnce;
expect(updateListenerSpy).to.have.been.calledWith(mockedRSSOutput);
myListener.stop();
});
it("notifies with 'newEntries' when a new entry is detected", async function () {
// given
const clock = sinon.useFakeTimers();
const mockManager = ImportMock.mockClassInPlace<Parser>(Parser);
const stubParser = mockManager.mock("parseURL");
stubParser.resolves(mockedRSSOutput);
const newEntry = {
title: "my title 03",
"media:group": {
"media:description": "my description 03",
"media:thumbnail": [
{ $: { height: 360, width: 420, url: "my_image03.jpg" } },
],
},
link: "my_url_03.com",
pubDate: "myDate03",
};
const newRSSOutput = {
...mockedRSSOutput,
items: [newEntry, ...mockedRSSOutput.items],
};
// classic build
const myListener = new ListenerRss({
...infosListener,
timeloop: 60,
});
//spy
const updateListenerSpy = sinon.spy();
const newEntriesListenerSpy = sinon.spy();
myListener.on("update", updateListenerSpy);
myListener.on("newEntries", newEntriesListenerSpy);
// when
myListener.start();
// then
await clock.tickAsync(1);
expect(updateListenerSpy).to.have.been.calledOnce;
expect(newEntriesListenerSpy).to.not.have.been.called;
// given
stubParser.resolves(newRSSOutput);
// then
await clock.tickAsync(60000);
expect(updateListenerSpy).to.have.been.calledTwice;
expect(newEntriesListenerSpy).to.have.been.calledOnce;
expect(newEntriesListenerSpy).to.have.been.calledWith([newEntry]);
// given
newEntriesListenerSpy.resetHistory();
// then
await clock.tickAsync(60000);
expect(updateListenerSpy).to.have.been.calledThrice;
expect(updateListenerSpy).to.have.been.calledWith(mockedRSSOutput);
expect(newEntriesListenerSpy).to.not.have.been.called;
myListener.stop();
});
it("not notifies with 'newEntries' when a new entry is detected but she's already in the history", async function () {
// given
const clock = sinon.useFakeTimers();
const mockManager = ImportMock.mockClassInPlace<Parser>(Parser);
const stubParser = mockManager.mock("parseURL");
stubParser.resolves(mockedRSSOutput);
const newEntry = {
title: "my title 03",
"media:group": {
"media:description": "my description 03",
"media:thumbnail": [
{ $: { height: 360, width: 420, url: "my_image03.jpg" } },
],
},
link: "my_url_03.com",
pubDate: "myDate03",
};
const newRSSOutput = {
...mockedRSSOutput,
items: [newEntry, ...mockedRSSOutput.items],
};
// classic build
const myListener = new ListenerRss({
...infosListener,
timeloop: 60,
lastEntriesLinks: ["my_url_02.com", "my_url_01.com", "my_url_00.com"],
});
//spy
const updateListenerSpy = sinon.spy();
const newEntriesListenerSpy = sinon.spy();
myListener.on("update", updateListenerSpy);
myListener.on("newEntries", newEntriesListenerSpy);
// when
myListener.start();
// then
await clock.tickAsync(1);
expect(updateListenerSpy).to.have.been.calledOnce;
expect(newEntriesListenerSpy).to.not.have.been.calledOnce;
// given
stubParser.resolves(newRSSOutput);
// then
await clock.tickAsync(60000);
expect(updateListenerSpy).to.have.been.calledTwice;
expect(newEntriesListenerSpy).to.have.been.calledOnce;
expect(newEntriesListenerSpy).to.have.been.calledWith([newEntry]);
// given
newEntriesListenerSpy.resetHistory();
// then
await clock.tickAsync(60000);
expect(updateListenerSpy).to.have.been.calledThrice;
expect(updateListenerSpy).to.have.been.calledWith(mockedRSSOutput);
expect(newEntriesListenerSpy).to.not.have.been.called;
myListener.stop();
});
});
});

View File

@ -1,7 +1,6 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"module": "commonjs",
"esModuleInterop": true
}
},
"files": ["./index-spec.ts"]
}

View File

@ -1,21 +1,72 @@
{
"compilerOptions": {
"target": "es2018",
"module": "es2015",
"lib": ["es6"],
"allowJs": true,
"outDir": "build",
"rootDir": "src",
"strict": true,
"moduleResolution": "node",
"noImplicitAny": true,
"esModuleInterop": true,
"resolveJsonModule": false,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"exclude": [
"build/",
"node_modules"
]
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
// "outDir": "./build/", /* Redirect output structure to the directory. */
// "rootDir": "./src/", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
}
}

11
tsconfig.json Normal file
View File

@ -0,0 +1,11 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"declaration": true, /* Generates corresponding '.d.ts' file. */
"declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
"outDir": "./build/", /* Redirect output structure to the directory. */
"rootDir": "./src/", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
},
"files": ["./src/index.ts", "./src/listener-rss.ts"],
"exclude": ["./tests/**/*"]
}