Compare commits

..

1 Commits

Author SHA1 Message Date
Florent
d2f8a2fb75 WIP: use ts-mock-imports 2021-02-28 17:49:31 +01:00
12 changed files with 399 additions and 4449 deletions

19
LICENSE
View File

@ -1,19 +0,0 @@
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 @@
# listener rss
# easy-rss-parser
A lightweight library to make simple actions with a RSS feed.
A lightweight library to give some additions for the [rss-parser package](https://github.com/rbren/rss-parser).
# USAGE
@ -10,15 +10,15 @@ You can parse RSS from a URL with some custom data.
An example :
```js
const ListenerModule = require("ListenerRSS");
const ListenerRss = ListenerModule.ListenerRss;
const easyParser = require("easy-rss-parser");
const ListenerRss = easyParser.ListenerRss;
const listener = new ListenerRss({
address: "fake.rss.service"
});
let listener = new ListenerRss("my-test-service", "fake.rss.service");
// make a request to the adr 'fake.rss.service'
const feed = await myListener.fetchRSS();
myListener.fetchRSS().then((obj, err) => {
// some act
});
```
## Recurrent usage
@ -27,19 +27,16 @@ You can parse RSS from a URL each n times.
An example :
```js
const ListenerModule = require("ListenerRSS");
const ListenerRss = ListenerModule.ListenerRss;
const easyParser = require("easy-rss-parser");
const ListenerRss = easyParser.ListenerRss;
const listener = new ListenerRss({
address: "fake.rss.service"
});
let listener = new ListenerRss("my-test-service", "fake.rss.service", 5 * 60);
listener.on("update", feed => { /* ... */ });
listener.on("error", err => { /* ... */ });
listener.on("newEntries", feedEntries => { /* ... */ });
listener.start();
let callback_fun = (obj, err) => {
// some act
};
// call callback_fun each 5 minutes
listener.start(callback_fun);
/*...*/
@ -48,25 +45,34 @@ listener.stop();
# Documentation
## ListenerRss.Config
## ListenerRSSInfo
An interface to structure listener's data.
A class 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(ListenerRss.Config)`
`constructor(listenerRSSInfo)`
- ListenerRss.Config : object from the ListenerRss.Config's class.
- 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)
### fetchRSS()
@ -79,45 +85,26 @@ object who's contain the data. [cf Annexe Output](#output)
#### Issues
Reject the promise if the server can't be resolved.
Return an error if the server can't be resolved.
### start()
### start(callbackFun)
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.
This function will execute the callbackFun each time loop.
#### Events
#### Parameter
Each event take one arg into the callback function.
The `callbackFun` is the function who's going to be called each time loop. She need to be under the shape :
```js
listener.on("update", feed => { /* ... */ });
listener.on("error", err => { /* ... */ });
listener.on("newEntries", feedEntries => { /* ... */ });
(obj, err) => {
/*...*/
};
```
#### 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
@ -159,24 +146,21 @@ 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"
}
/*Some Others items*/
]
}
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'
```

3800
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,11 @@
{
"name": "listener-rss",
"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",
"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",
"scripts": {
"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"
"test": "cross-env TS_NODE_PROJECT='./tests/tsconfig.json' mocha --require ts-node/register ./tests/**/*-spec.ts",
"build": "tsc -p ./src"
},
"repository": {
"type": "git",
@ -18,13 +15,10 @@
"rss",
"rss-parser"
],
"author": "Amaury Joly <joly.amaury@hotmail.com>",
"author": "Amaury Joly <amaury.joly@hotmail.com>",
"contributors": [
"Florent Fayolle <florent.git@zeteo.me>"
],
"files": [
"build/"
],
"license": "MIT",
"devDependencies": {
"@types/chai": "^4.2.14",
@ -32,6 +26,7 @@
"@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",
@ -50,6 +45,6 @@
"typescript": "^4.1.3"
},
"dependencies": {
"rss-parser": "^3.11.0"
"rss-parser": "3.11.0"
}
}

View File

@ -0,0 +1,6 @@
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 +1,2 @@
export { ListenerRss } from "./listener-rss";
export { ListenerRSSInfos } from "./Models/ListenerRSSInfos";

View File

@ -1,23 +1,15 @@
import Parser from "rss-parser";
import { ListenerRSSInfos as ListenerInfo } from "./Models/ListenerRSSInfos";
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 'error' when the fetch has an issue
* Emit 'newEntries' when the fetch has new entris
* Emit 'update_err' when the fetch has an issue
*/
class ListenerRss extends EventEmitter {
export class ListenerRss extends EventEmitter {
name: string = "";
address: string = "";
timeloop: number = DEFAULT_TIMELOOP; // time in seconds
customfields?: { [key: string]: string[] | string };
@ -25,30 +17,20 @@ class ListenerRss extends EventEmitter {
// private fields
parser: Parser;
loopRunning: boolean = false;
lastEntriesLinks: string[] = [];
private firstUpdate = true;
/**
* @brief constructor
* @param config ListenerRSSInfos interface who contains the ListenerInfos
* @param config ListenerRSSInfos interface who's contain the ListenerInfos
*/
constructor(config: ListenerRss.Config) {
constructor(config: ListenerInfo) {
super();
this.address = config.address;
this.timeloop =
config.timeloop === undefined ? DEFAULT_TIMELOOP : config.timeloop;
this.customfields = config.customfields;
this.lastEntriesLinks =
config.lastEntriesLinks === undefined ? [] : config.lastEntriesLinks;
this.setData(config);
// set parser
this.parser = this.generateParser();
}
/**
* @brief Private function. Is used to initilize the parser object with the customfields var
*/
generateParser() {
generateParser(): Parser {
const parserConfig = this.customfields && {
customFields: {
feed: [],
@ -57,48 +39,44 @@ class ListenerRss extends EventEmitter {
}),
},
};
return new Parser(parserConfig);
return new Parser(parserConfig); // 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.timeloop =
infos.timeloop === undefined ? DEFAULT_TIMELOOP : infos.timeloop;
this.customfields = infos.customfields;
}
/**
* @brief use the parseURL function from rss-parser with the objects data
* @brief use the parseURL function from rss-parser with the objects datas
* @return return a promise with the received data
*/
fetchRSS(): Promise<Parser.Output<any>> {
return this.parser.parseURL(this.address);
return this.parser.parseURL(this.address).catch((err) => {
throw new Error("bad address or no access : " + err);
});
}
/**
* @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 = 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))
);
const fun: () => void = () => {
this.fetchRSS()
.then((obj: { [key: string]: any }) => this.emit("update", obj))
.catch((err) => this.emit("update_err", err));
};
(async () => {
@ -115,19 +93,4 @@ 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 };

10
src/tsconfig.json Normal file
View File

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

View File

@ -1,43 +1,52 @@
// external lib
import * as Parser from "rss-parser";
import * as parserModule from "rss-parser";
// tested class
import { ListenerRss } from "../";
import {
ListenerRSSInfos as ListenerRRSInfo,
ListenerRss as Listeners,
} from "./../src/index";
// Unit test
import assert from "assert";
import * as chai from "chai";
import sinon from "ts-sinon";
import sinonChai from "sinon-chai";
import { ImportMock, InPlaceMockManager } from "ts-mock-imports";
import * as sinon from "ts-sinon";
import { ImportMock } from "ts-mock-imports";
const sinonChai = require("sinon-chai");
chai.use(sinonChai);
const expect = chai.expect;
describe("test class RSS: jsonfile", function () {
const infosListener: ListenerRss.Config = {
const infosListener: ListenerRRSInfo = {
name: "my-test-service",
address: "fake.rss.service",
timeloop: 15,
customfields: {
description: ["media:group", "media:description"],
icon: ["media:group", "media:thumbnail"],
},
lastEntriesLinks: ["my_url_02.com"],
};
const mockedRSSOutput: Parser.Output<any> = {
// parseURL tests
let stubListener: sinon.StubbedInstance<Listeners>;
let stubParser: sinon.StubbedInstance<Parser>;
const mockedRSSOutput: Parser.Output<{
"media:group": { [key: string]: string | [any] };
}> = {
items: [
{
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",
},
{
title: "my title 01",
@ -51,34 +60,58 @@ describe("test class RSS: jsonfile", function () {
pubDate: "myDate01",
},
{
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",
},
],
};
/**
* 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("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"],
});
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"],
@ -91,19 +124,45 @@ describe("test class RSS: jsonfile", function () {
feed: [],
item: ["media:group", "media:group"],
});
expect(myListener.lastEntriesLinks).to.be.eql(["my_url_02.com"]);
});
it("builds with 3 params (no custom fields)", function () {
const myListener = new ListenerRss({
it("The build without issues (raw infos : 4 params)", function () {
let myListener = new Listeners({
name: "my-test-service",
address: "fake.rss.service",
timeloop: 15,
lastEntriesLinks: ["my_url_02.com"],
customfields: {
description: ["media:group", "media:description"],
icon: ["media:group", "media:thumbnail"],
},
});
// 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 : just 2 params)", function () {
let myListener = new Listeners({
name: "my-test-service",
address: "fake.rss.service",
});
// 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(undefined);
expect(myListener.parser)
@ -113,400 +172,190 @@ describe("test class RSS: jsonfile", function () {
feed: [],
item: [],
});
expect(myListener.lastEntriesLinks).to.be.eql(["my_url_02.com"]);
});
});
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,
});
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"],
// 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: [],
});
// assertions
// myListener data
expect(myListener.timeloop).to.eql(5 * 60);
expect(myListener.address).to.eql("fake.rss.service");
expect(myListener.customfields).to.eql({
});
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: {
description: ["media:group", "media:description"],
icon: ["media:group", "media:thumbnail"],
},
});
// 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"],
});
expect(myListener.parser)
.to.have.property("options")
.to.have.property("customFields")
.to.be.eql({
feed: [],
item: ["media:group", "media:group"],
});
describe("fetch some data", function () {
it.only("fetch without issues", function () {
const mockManager = ImportMock.mockClass<
parserModule.default,
typeof parserModule
>(parserModule.default);
mockManager.mock("parseURL").resolves(mockedRSSOutput);
const myListener = new Listeners(infosListener);
// fetch
let res = myListener.fetchRSS();
//assertion
// calls
return res
.then((obj: any) => {
expect(obj).to.be.eql(mockedRSSOutput);
})
.catch((err) => {
expect(err).to.be.undefined;
});
expect(myListener.lastEntriesLinks).to.be.eql(["my_url_02.com"]);
});
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"],
},
});
// it("fetch with bad address", function () {
// let myListener = new Listeners({
// name: "my-test-service",
// 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();
// 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([]);
});
// //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"));
// });
// });
});
describe("export property", function () {
it("should export properties into a ListenerRSSInfos", function () {
// given
const myListener = new ListenerRss(infosListener);
// describe.skip("start", function () {
// it("Let's start the timer", async function () {
// let clock: sinon.default.SinonFakeTimers = sinon.default.useFakeTimers();
// assertions
expect(myListener.getProperty()).to.be.eql(infosListener);
});
});
// // 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);
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);
// //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 myListener = new ListenerRss(infosListener);
// // start timer
// stubListener.on("update", (obj) => fun_spy(obj));
// // stubListener.start(fun_spy);
// when
const res = await myListener.fetchRSS();
// // wait and assertion
// // After 1ms
// await clock.tickAsync(1);
// expect(stubListener.fetchRSS).to.have.been.calledOnce;
// expect(fun_spy).to.have.been.calledOnce;
// then
expect(stubParser).to.have.been.calledOnce;
expect(stubParser).to.have.been.calledWith(infosListener.address);
expect(res).to.be.eql(mockedRSSOutput);
});
// // After 60s
// await clock.tickAsync(59999);
// expect(stubListener.fetchRSS).to.have.been.calledTwice;
// expect(fun_spy).to.have.been.calledTwice;
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);
// stubListener.stop();
// });
const myListener = new ListenerRss({
address: "bad.rss.service",
customfields: {
description: ["media:group", "media:description"],
icon: ["media:group", "media:thumbnail"],
},
});
// when
await assert.rejects(() => myListener.fetchRSS(), err);
});
});
// it("Let's start the timer (with a bad address)", 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();
// // 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.rejects(
// new Error("connect ECONNREFUSED 127.0.0.1:80")
// );
const mockManager: InPlaceMockManager<Parser> = ImportMock.mockClassInPlace<Parser>(
Parser
);
const stubParser = mockManager.mock("parseURL");
stubParser.resolves(mockedRSSOutput);
// //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);
// });
// classic build
const myListener = new ListenerRss(infosListener);
// // start timer
// // stubListener.start(fun_spy);
//spy
const updateListenerSpy = sinon.spy();
// // wait and assertion
// // After 1ms
// await clock.tickAsync(1);
// expect(stubListener.fetchRSS).to.have.been.calledOnce;
// expect(fun_spy).to.have.been.calledOnce;
// start timer
myListener.on("update", updateListenerSpy);
// // After 60s
// await clock.tickAsync(59999);
// expect(stubListener.fetchRSS).to.have.been.calledTwice;
// expect(fun_spy).to.have.been.calledTwice;
myListener.start();
// when
await clock.tickAsync(1);
// then
expect(updateListenerSpy).to.have.been.calledOnce;
expect(updateListenerSpy).to.have.been.calledWith(mockedRSSOutput);
expect(stubParser).to.have.calledWith(myListener.address);
myListener.stop();
});
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
const myListener = new ListenerRss({
...infosListener,
timeloop: 15,
});
//spy
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
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(updateErrorListenerSpy).to.have.been.calledOnce;
expect(updateListenerSpy).to.not.have.been.called;
expect(updateErrorListenerSpy).to.have.been.calledWith(expectedErr);
// After 60s
await clock.tickAsync(60000);
expect(updateErrorListenerSpy).to.have.been.calledTwice;
expect(updateListenerSpy).to.not.have.been.called;
// 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();
});
});
// stubListener.stop();
// });
// });
});

View File

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

View File

@ -1,72 +1,21 @@
{
"compilerOptions": {
/* 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. */
}
"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"
]
}

View File

@ -1,11 +0,0 @@
{
"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/**/*"]
}