diff --git a/src/Models/ListenerRSSInfos.ts b/src/Models/ListenerRSSInfos.ts new file mode 100644 index 0000000..f1098ca --- /dev/null +++ b/src/Models/ListenerRSSInfos.ts @@ -0,0 +1,46 @@ +class ListenerRSSInfos { + _name: string = ""; // name of the listener + _address: string = ""; // feed's address + _timeloop: number | undefined = 5 * 60; // update time RSS feed + _customfields: string[][] | undefined = []; // rss fields custom + + constructor( + name: string, + address: string, + timeloop?: number, + customfields?: string[][] + ) { + if (name !== undefined && address !== undefined) { + this._name = name; + this._address = address; + this._timeloop = timeloop; + this._customfields = customfields; + } else throw new Error("Bad constructor's args"); + } + + set timeloop(value) { + this._timeloop = value; + } + + set customfields(value) { + this._customfields = value; + } + + get name() { + return this._name; + } + + get address() { + return this._address; + } + + get timeloop() { + return this._timeloop; + } + + get customfields() { + return this._customfields; + } +} + +module.exports = ListenerRSSInfos; diff --git a/src/index.ts b/src/index.ts index 6586c32..25c4a2a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,85 +1,8 @@ -import Parser from "rss-parser"; +import ListenerRss from "./listener-rss.ts"; +import ListenerRSSInfo from "./Models/ListenerRSSInfos"; +// TODO J'ai des erreurs sur les imports, que je ne comprend pas trop -const DEFAULT_TIMELOOP = 5 * 60; // default timeloop is 5 min - -class ListenerRss { - name = ""; - address = ""; - timeloop = DEFAULT_TIMELOOP; // time in seconds - customfields = []; - - // private fields - parser: Parser; - loopRunning = false; - - constructor( - name: String, - address: String, - timeloop: number, - customfields: String[][] - ) { - this.parser = new Parser(); - // if (name !== undefined && name instanceof ListenerInfo) { - // // constructor with 1 arg - // this.setData({name}); - // } else if (address !== undefined && typeof address === "string") { - // // construct with between 2 and 4 args - // this.setData(new ListenerInfo(name, address, timeloop, customfields)); - // } else throw new Error("the constructor must have args"); - // this.setParser(); - } - - // setParser() { - // // set parser - // this.parser = new Parser( - // this.customfields !== undefined - // ? { - // customFields: { - // item: this.customfields.map((elt) => { - // return Array.isArray(elt[1]) ? elt[1][0] : elt[1]; - // }), - // }, - // } - // : {} - // ); // if customfield is set -> let's set the parser with, else let the option empty - // } - - // setData({name: String, address: String, timeloop: number = DEFAULT_TIMELOOP}) { - // // Set data - // this.name = name; - // this.address = address; - // this.timeloop = - // this.timeloop === undefined ? DEFAULT_TIMELOOP : infos._timeloop; - // this.customfields = infos._customfields === undefined ? [] : infos._customfields; - // } - - // fetchRSS() { - // 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(callback) { - // this.loopRunning = true; - - // (async () => { - // while (this.loopRunning === true) { - // this.fetchRSS().then((obj, err) => callback(obj, err)); - // await new Promise((res) => setTimeout(res, 2000)); - // } - // })(); - // } - - // /** - // * @brief stop the async function - // */ - // stop() { - // this.loopRunning = false; - // } -} - -module.exports = ListenerRss; +module.exports = { + ListenerRss: ListenerRss, + ListenerRSSInfo: ListenerRSSInfo, +}; diff --git a/src/listener-rss.ts b/src/listener-rss.ts new file mode 100644 index 0000000..af25966 --- /dev/null +++ b/src/listener-rss.ts @@ -0,0 +1,97 @@ +import Parser from "rss-parser"; +import { ListenerRSSInfo as ListenerInfo } from "./Models/ListenerRSSInfos"; + +const DEFAULT_TIMELOOP = 5 * 60; // default timeloop is 5 min + +class ListenerRss { + name = undefined; + address = undefined; + timeloop = DEFAULT_TIMELOOP; // time in seconds + customfields = []; + + // private fields + parser: Parser | undefined = undefined; + loopRunning: boolean = false; + + constructor( + name: string | ListenerInfo, + address?: string, + timeloop?: number, + customfields?: string[][] + ) { + if (name !== undefined && name instanceof ListenerInfo) { + // constructor with 1 arg + this.setData(name); + } else if (address !== undefined) { + // construct with between 2 and 4 args + this.setData(new ListenerInfo(name, address, timeloop, customfields)); + } else throw new Error("the constructor must have args"); + this.setParser(); + } + + setParser() { + // set parser + this.parser = new Parser( + this.customfields !== undefined + ? { + customFields: { + item: this.customfields.map((elt) => { + return Array.isArray(elt[1]) ? elt[1][0] : elt[1]; + }), + }, + } + : {} + ); // if customfield is set -> let's set the parser with, else let the option empty + } + + 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 === undefined ? [] : infos._customfields; + } + + fetchRSS(): any { + // TODO Pas Bien + 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"); + } + + /** + * @brief call the callback function each looptime + * @param callback function who's going to be called with the latest get + */ + start(callback: any) { + // TODO any = Pas bien !! + /** + * Un des soucis qu'on a c'est sur le typage de l'objet de retour. le problème étant que la nature de l'obj de + * retour ne peut pas etre connue puisque il depend des custom fields qui sont definit par l'utilisateur. L'idée + * pourrait etre de creer une classe generique (cf le type CustomFields du ficher index.d.ts du package rss-parser). + * Après quelques recherches ca doit etre la manière ""correct"" de faire. Entre autre avec l'utilisation des mots + * clés keyof U ou encore Array. Je vais continuer a gratter dans cette direction perso. + */ + this.loopRunning = true; + + (async () => { + while (this.loopRunning) { + this.fetchRSS().then((obj: any, err: any) => callback(obj, err)); // TODO Erreur a la compile + await new Promise((res) => setTimeout(res, 2000)); + } + })(); + } + + /** + * @brief stop the async function + */ + stop() { + this.loopRunning = false; + } +} + +module.exports = ListenerRss; diff --git a/tests/index-spec.ts b/tests/index-spec.ts index 5639c7a..9b114b4 100644 --- a/tests/index-spec.ts +++ b/tests/index-spec.ts @@ -1,9 +1,281 @@ -const { expect } = require("chai"); +// external lib +import Parser from "rss-parser"; -describe("Calculator", function () { - describe("Add", function () { - it("Should return 3 when a = 1 and b = 2", () => { - expect(2 + 1).to.equal(3); +// tested class +import { + ListenerRss as Listeners, + ListenerRSSInfo as ListenerRRSInfo, +} from "/src/index"; // TODO import bloque ?? not found + +// Unit test +import chai from "chai"; +import sinon from "ts-sinon"; +import sinon_chai from "sinon-chai"; +import { SinonSpy } from "sinon"; +chai.use(sinon_chai); + +const expect = chai.expect; + +describe("test class RSS: jsonfile", function () { + let myListener: Listeners | undefined = undefined; + + const infosListener: ListenerRRSInfo = new ListenerRRSInfo( + "my-test-service", + "fake.rss.service", + 15, + [ + ["description", ["media:group", "media:description"]], + ["icon", ["media:group", "media:thumbnail"]], + ] + ); + + // parseURL tests + let stubParser: sinon.SinonStub; + const mockedRSSOutput: any = { + // TODO any = pas bien + items: [ + { + title: "my title 00", + "media:group": { + "media:description": "my description 00", + "media:thumbnail": [ + { $: { height: 360, width: 420, url: "my_image00.jpg" } }, + ], + }, + link: "my_url_00.com", + pubDate: "myDate00", + }, + { + title: "my title 01", + "media:group": { + "media:description": "my description 01", + "media:thumbnail": [ + { $: { height: 360, width: 420, url: "my_image01.jpg" } }, + ], + }, + link: "my_url_01.com", + pubDate: "myDate01", + }, + { + title: "my title 02", + "media:group": { + "media:description": "my description 02", + "media:thumbnail": [ + { $: { height: 360, width: 420, url: "my_image02.jpg" } }, + ], + }, + link: "my_url_02.com", + pubDate: "myDate02", + }, + ], + }; + + beforeEach(function () { + // stubs + stubParser = sinon.stub(Parser.prototype, "parseURL"); + stubParser.withArgs(infosListener.address).resolves(mockedRSSOutput); + stubParser + .withArgs("bad.rss.service") + .resolves(new Error("connect ECONNREFUSED 127.0.0.1:80")); + + // constructor + myListener = undefined; + }); + + afterEach(function () { + // restore stubs + stubParser.restore(); + }); + + describe("Building Ytb listener", function () { + it("The build without issues (infosListener parameters)", function () { + 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.options.customFields).to.eql({ + feed: [], + item: ["media:group", "media:group"], + }); + }); + it("The build without issues (raw infos : 4 params)", function () { + myListener = new Listeners("my-test-service", "fake.rss.service", 15, [ + ["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.options.customFields).to.eql({ + feed: [], + item: ["media:group", "media:group"], + }); + }); + it("The build without issues (raw infos : just 2 params)", function () { + myListener = new Listeners("my-test-service", "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([]); + expect(myListener.parser.options.customFields).to.eql({ + feed: [], + item: [], + }); + }); + it("The build without issues (raw infos : just 3 params (no custom fields))", function () { + myListener = new Listeners("my-test-service", "fake.rss.service", 15); + + // 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([]); + expect(myListener.parser.options.customFields).to.eql({ + feed: [], + item: [], + }); + }); + it("The build without issues (raw infos : just 3 params (no timeloop))", function () { + myListener = new Listeners( + "my-test-service", + "fake.rss.service", + undefined, + [ + ["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.options.customFields).to.eql({ + feed: [], + item: ["media:group", "media:group"], + }); + }); + }); + + describe("fetch some data", function () { + it("fetch without issues", function () { + // classic build + myListener = new Listeners(infosListener); + // fetch + let res = myListener.fetchRSS(); + + //assertion + // calls + expect(stubParser).to.have.been.calledOnce; + expect(stubParser).to.have.been.calledWith(infosListener._address); + // Promise + //await expect(Promise.resolve(res)).to.be.eql(mockedRSSOutput); + res.then((obj: any, err: Error) => { + // TODO + expect(obj).to.be.eql(mockedRSSOutput); + expect(err).to.be.undefined; + }); + }); + it("fetch with bad address", function () { + // classic build + myListener = new Listeners( + "my-test-service", + "bad.rss.service", + undefined, + [ + ["description", ["media:group", "media:description"]], + ["icon", ["media:group", "media:thumbnail"]], + ] + ); + // fetch + let res = myListener.fetchRSS(); + + //assertion + // calls + expect(stubParser).to.have.been.calledOnce; + expect(stubParser).to.have.been.calledWith("bad.rss.service"); + // Promise + res.then((obj: any, err: Error) => { + expect(obj).to.be.undefined; + expect(err).to.be.eql(new Error("connect ECONNREFUSED 127.0.0.1:80")); + }); + }); + }); + + describe("start", function () { + it("Let's start the timer", async function () { + //custom timeout + this.timeout(15000); + + // classic build + myListener = new Listeners("my-test-service", "fake.rss.service", 2, [ + ["description", ["media:group", "media:description"]], + ["icon", ["media:group", "media:thumbnail"]], + ]); + + //spy + const fun_spy: SinonSpy = sinon.spy(); + + // start timer + myListener.start(fun_spy); + + await new Promise((res) => setTimeout(res, 5 * 1000)); + + myListener.stop(); + + //assertion + // calls + expect(1).to.be.eql(1); + expect(fun_spy).to.have.been.callCount(3); + expect(fun_spy).to.have.been.calledWith(mockedRSSOutput, undefined); + }); + it("Let's start the timer (with a bad address)", async function () { + //custom timeout + this.timeout(15000); + + // classic build + myListener = new Listeners("my-test-service", "bad.rss.service", 2, [ + ["description", ["media:group", "media:description"]], + ["icon", ["media:group", "media:thumbnail"]], + ]); + + //spy + const fun_spy: SinonSpy = sinon.spy(); + + // start timer + myListener.start(fun_spy); + + await new Promise((res) => setTimeout(res, 5 * 1000)); + + myListener.stop(); + //assertion + // calls + expect(1).to.be.eql(1); + expect(fun_spy).to.have.been.callCount(3); + expect(fun_spy).to.have.been.calledWith(undefined, Error); //yagni }); }); });