// external lib import * as Parser from "rss-parser"; // tested class import { ListenerRss } from "../"; const ListenerRSSInfo = ListenerRss.Config; // 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"; chai.use(sinonChai); const expect = chai.expect; describe("test class RSS: jsonfile", function () { const infosListener: ListenerRRSInfo = { 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 = { items: [ { 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", }, { 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 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", }, ], }; describe("Building Ytb listener", function () { it("builds with 4 params", function () { const myListener = new Listeners({ 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.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"]); }); it("builds with 3 params (no custom fields)", function () { const myListener = new Listeners({ address: "fake.rss.service", timeloop: 15, lastEntriesLinks: ["my_url_02.com"], }); // assertions // myListener data expect(myListener.timeloop).to.eql(15); 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(["my_url_02.com"]); }); it("build with 3 params (no timeloop)", function () { const myListener = new Listeners({ 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(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"]); }); it("build with 3 params (no lastEntries)", function () { const myListener = new Listeners({ 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 Listeners({ 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("export property", function () { it("should export properties into a ListenerRSSInfos", function () { // given const myListener = new Listeners(infosListener); // assertions expect(myListener.getProperty()).to.be.eql(infosListener); }); }); describe("data fetching", function () { it("fetches without issues", async function () { // given const mockManager: InPlaceMockManager = ImportMock.mockClassInPlace( Parser ); const stubParser = mockManager.mock("parseURL"); stubParser.resolves(mockedRSSOutput); const myListener = new Listeners(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("rejects when fetching fails", async function () { // given const mockManager: InPlaceMockManager = ImportMock.mockClassInPlace( Parser ); const stubParser = mockManager.mock("parseURL"); const err = new Error("connect ECONNREFUSED 127.0.0.1:80"); stubParser.rejects(err); const myListener = new Listeners({ address: "bad.rss.service", customfields: { description: ["media:group", "media:description"], icon: ["media:group", "media:thumbnail"], }, }); // when await assert.rejects(() => myListener.fetchRSS(), err); }); }); describe("start", function () { it("fetches immediately the RSS information", async function () { // given const clock = sinon.useFakeTimers(); const mockManager: InPlaceMockManager = ImportMock.mockClassInPlace( Parser ); const stubParser = mockManager.mock("parseURL"); stubParser.resolves(mockedRSSOutput); // classic build const myListener = new Listeners(infosListener); //spy const updateListenerSpy = sinon.spy(); // start timer myListener.on("update", updateListenerSpy); 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 = ImportMock.mockClassInPlace( Parser ); const stubParser = mockManager.mock("parseURL"); stubParser.resolves(mockedRSSOutput); // classic build const myListener = new Listeners({ ...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 = ImportMock.mockClassInPlace( 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 Listeners({ ...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); 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 Listeners({ ...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.have.been.calledOnce; // given stubParser.resolves(newRSSOutput); // then await clock.tickAsync(60000); expect(updateListenerSpy).to.have.been.calledTwice; expect(newEntriesListenerSpy).to.have.been.calledTwice; 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); 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 Listeners({ ...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(); }); }); });