From 00b51584b6886075e909b14d90b3804e9a288e9f Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Sun, 7 Apr 2024 11:03:20 +0100 Subject: [PATCH 1/3] Add image helper class to handle reddit galleries --- package.json | 1 + src/imageHelper.ts | 19 +++++++++++++++++ src/index.ts | 23 +++++++++++++++++--- tests/imageHelper.test.ts | 9 ++++++++ tests/index.test.ts | 20 +++++++++++------ yarn.lock | 45 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 107 insertions(+), 10 deletions(-) create mode 100644 src/imageHelper.ts create mode 100644 tests/imageHelper.test.ts diff --git a/package.json b/package.json index 9b67710..597b732 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "dependencies": { "glob-parent": "^6.0.0", "got-cjs": "^12.5.4", + "htmlparser2": "^9.1.0", "linqts": "^1.14.4" }, "scripts": { diff --git a/src/imageHelper.ts b/src/imageHelper.ts new file mode 100644 index 0000000..793b5ce --- /dev/null +++ b/src/imageHelper.ts @@ -0,0 +1,19 @@ +import fetch from "got-cjs"; +import * as htmlparser from "htmlparser2"; + +export default class ImageHelper { + public static async FetchImageFromRedditGallery(url: string): Promise { + const fetched = await fetch(url); + + if (!fetched) { + return undefined; + } + + const dom = htmlparser.parseDocument(fetched.body); + const img = htmlparser.DomUtils.findOne((x => x.tagName == "img" && x.attributes.find(y => y.value.includes("https://preview.redd.it")) != null), dom.children, true); + + const imgSrc = img?.attributes.find(x => x.name == "src")?.value; + + return imgSrc; + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 400567e..8f85175 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ import IRedditResult from "./contracts/IRedditResult.js"; import fetch from "got-cjs"; import { List } from 'linqts'; import IFetchResult from "./contracts/IFetchResult.js"; +import ImageHelper from "./imageHelper"; const sortable = [ 'new', @@ -13,7 +14,7 @@ const sortable = [ export default async function randomBunny(subreddit: string, sortBy?: string): Promise { if (!sortBy || !sortable.includes(sortBy)) sortBy = 'hot'; - const result = await fetch(`https://reddit.com/r/${subreddit}/${sortBy}.json`); + const result = await fetch(`https://reddit.com/r/${subreddit}/${sortBy}.json?limit=100`); if (!result) { return { @@ -32,7 +33,7 @@ export default async function randomBunny(subreddit: string, sortBy?: string): P const data: IFetchResult[] = json.data.children; const dataWithImages = new List(data) - .Where(x => x!.data.url.includes('.jpg') || x!.data.url.includes('.png')) + .Where(x => x!.data.url.includes('.jpg') || x!.data.url.includes('.png') || x!.data.url.includes("/gallery/")) .ToArray(); let random = 0; @@ -49,6 +50,22 @@ export default async function randomBunny(subreddit: string, sortBy?: string): P const randomData = randomSelect.data; + let url: string; + + if (randomData.url.includes("/gallery")) { + const galleryImage = await ImageHelper.FetchImageFromRedditGallery(randomData.url); + + if (!galleryImage) { + return { + IsSuccess: false, + } + } + + url = galleryImage; + } else { + url = randomData.url; + } + const redditResult: IRedditResult = { Archived: randomData['archived'], Downs: randomData['downs'], @@ -58,7 +75,7 @@ export default async function randomBunny(subreddit: string, sortBy?: string): P SubredditSubscribers: randomData['subreddit_subscribers'], Title: randomData['title'], Ups: randomData['ups'], - Url: randomData['url'] + Url: url, }; return { diff --git a/tests/imageHelper.test.ts b/tests/imageHelper.test.ts new file mode 100644 index 0000000..556264c --- /dev/null +++ b/tests/imageHelper.test.ts @@ -0,0 +1,9 @@ +describe("FetchImageFromRedditGallery", () => { + test.todo("EXPECT image url to be returned"); + + test.todo("GIVEN fetch is unable to return data, EXPECT undefined returned"); + + test.todo("GIVEN image tag is not found, EXPECT undefined returned"); + + test.todo("GIVEN image source attribute is not found, EXPECT undefined returned"); +}); \ No newline at end of file diff --git a/tests/index.test.ts b/tests/index.test.ts index 6eafc35..87efd7a 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -33,7 +33,7 @@ describe('randomBunny', () => { expect(result.IsSuccess).toBeTruthy(); expect(result.Result).toBeDefined(); - expect(fetchMock).toBeCalledWith('https://reddit.com/r/rabbits/new.json'); + expect(fetchMock).toBeCalledWith('https://reddit.com/r/rabbits/new.json?limit=100'); }); test('GIVEN sortBy is NOT supplied, expect it to default to hot', async () => { @@ -64,7 +64,7 @@ describe('randomBunny', () => { expect(result.IsSuccess).toBeTruthy(); expect(result.Result).toBeDefined(); - expect(fetchMock).toBeCalledWith('https://reddit.com/r/rabbits/hot.json'); + expect(fetchMock).toBeCalledWith('https://reddit.com/r/rabbits/hot.json?limit=100'); }); test('GIVEN sortBy is NOT valid, expect it to default to hot', async () => { @@ -95,7 +95,7 @@ describe('randomBunny', () => { expect(result.IsSuccess).toBeTruthy(); expect(result.Result).toBeDefined(); - expect(fetchMock).toBeCalledWith('https://reddit.com/r/rabbits/hot.json'); + expect(fetchMock).toBeCalledWith('https://reddit.com/r/rabbits/hot.json?limit=100'); }); test('GIVEN the fetch fails, EXPECT failure result', async () => { @@ -106,7 +106,7 @@ describe('randomBunny', () => { expect(result.IsSuccess).toBeFalsy(); expect(result.Result).toBeUndefined(); - expect(fetchMock).toBeCalledWith('https://reddit.com/r/rabbits/new.json'); + expect(fetchMock).toBeCalledWith('https://reddit.com/r/rabbits/new.json?limit=100'); }); test('GIVEN the result is NOT valid JSON, EXPECT failure result', async () => { @@ -119,7 +119,7 @@ describe('randomBunny', () => { expect(result.IsSuccess).toBeFalsy(); expect(result.Result).toBeUndefined(); - expect(fetchMock).toBeCalledWith('https://reddit.com/r/rabbits/new.json'); + expect(fetchMock).toBeCalledWith('https://reddit.com/r/rabbits/new.json?limit=100'); }); test('GIVEN randomSelect does NOT find a response, EXPECT failure result', async () => { @@ -136,7 +136,7 @@ describe('randomBunny', () => { expect(result.IsSuccess).toBeFalsy(); expect(result.Result).toBeUndefined(); - expect(fetchMock).toBeCalledWith('https://reddit.com/r/rabbits/new.json'); + expect(fetchMock).toBeCalledWith('https://reddit.com/r/rabbits/new.json?limit=100'); }); test('GIVEN randomSelect does NOT find a valid response, EXPECT failure result', async () => { @@ -167,6 +167,12 @@ describe('randomBunny', () => { expect(result.IsSuccess).toBeFalsy(); expect(result.Result).toBeUndefined(); - expect(fetchMock).toBeCalledWith('https://reddit.com/r/rabbits/new.json'); + expect(fetchMock).toBeCalledWith('https://reddit.com/r/rabbits/new.json?limit=100'); }); + + test.todo("GIVEN data fetched is a gallery AND an image is returned from the helper, EXPECT this to be used"); + + test.todo("GIVEN data fetched is a gallery AND an image is not returned from the helper, EXPECT error"); + + test.todo("GIVEN data fetched is not a gallery, EXPECT url to be used directly"); }); \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 1414a90..cc0a27a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1716,6 +1716,36 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-serializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" + integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.2" + entities "^4.2.0" + +domelementtype@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + +domhandler@^5.0.2, domhandler@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" + integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== + dependencies: + domelementtype "^2.3.0" + +domutils@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e" + integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== + dependencies: + dom-serializer "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + dot-prop@^5.2.0: version "5.3.0" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" @@ -1757,6 +1787,11 @@ end-of-stream@^1.1.0: dependencies: once "^1.4.0" +entities@^4.2.0, entities@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" @@ -2286,6 +2321,16 @@ html-escaper@^2.0.0: resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== +htmlparser2@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-9.1.0.tgz#cdb498d8a75a51f739b61d3f718136c369bc8c23" + integrity sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.3" + domutils "^3.1.0" + entities "^4.5.0" + http-cache-semantics@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" From 9e25e5a0e0918648364741f9748e532d10cc96c9 Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Fri, 12 Apr 2024 17:54:12 +0100 Subject: [PATCH 2/3] Update tests --- src/imageHelper.ts | 2 +- tests/imageHelper.test.ts | 89 +++++++++++++++++++++++++++++++++++++-- tests/index.test.ts | 69 ++++++++++++++++++++++++++++-- 3 files changed, 152 insertions(+), 8 deletions(-) diff --git a/src/imageHelper.ts b/src/imageHelper.ts index 793b5ce..3da0fc9 100644 --- a/src/imageHelper.ts +++ b/src/imageHelper.ts @@ -5,7 +5,7 @@ export default class ImageHelper { public static async FetchImageFromRedditGallery(url: string): Promise { const fetched = await fetch(url); - if (!fetched) { + if (!fetched || fetched.errored || fetched.statusCode != 200) { return undefined; } diff --git a/tests/imageHelper.test.ts b/tests/imageHelper.test.ts index 556264c..77227b1 100644 --- a/tests/imageHelper.test.ts +++ b/tests/imageHelper.test.ts @@ -1,9 +1,90 @@ +import ImageHelper from "../src/imageHelper"; +import fetch from "got-cjs"; + +jest.mock('got-cjs'); +const fetchMock = jest.mocked(fetch); + describe("FetchImageFromRedditGallery", () => { - test.todo("EXPECT image url to be returned"); + test("EXPECT image url to be returned", async () => { + fetchMock.mockResolvedValue({ + body: "", + errored: undefined, + statusCode: 200, + }); - test.todo("GIVEN fetch is unable to return data, EXPECT undefined returned"); + const result = await ImageHelper.FetchImageFromRedditGallery("https://redd.it/gallery/image"); - test.todo("GIVEN image tag is not found, EXPECT undefined returned"); + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fetchMock).toHaveBeenCalledWith("https://redd.it/gallery/image"); - test.todo("GIVEN image source attribute is not found, EXPECT undefined returned"); + expect(result).toBe("https://preview.redd.it/image.png"); + }); + + test("GIVEN fetch is unable to return data, EXPECT undefined returned", async () => { + fetchMock.mockResolvedValue(null); + + const result = await ImageHelper.FetchImageFromRedditGallery("https://redd.it/gallery/image"); + + expect(result).toBeUndefined(); + }); + + test("GIVEN fetch is an error, EXPECT undefined returned", async () => { + fetchMock.mockResolvedValue({ + body: "", + errored: "Error", + statusCode: 200, + }); + + const result = await ImageHelper.FetchImageFromRedditGallery("https://redd.it/gallery/image"); + + expect(result).toBeUndefined(); + }); + + test("GIVEN fetch is not status code of 200, EXPECT undefined returned", async () => { + fetchMock.mockResolvedValue({ + body: "", + errored: undefined, + statusCode: 500, + }); + + const result = await ImageHelper.FetchImageFromRedditGallery("https://redd.it/gallery/image"); + + expect(result).toBeUndefined(); + }); + + test("GIVEN image tag is not found, EXPECT undefined returned", async () => { + fetchMock.mockResolvedValue({ + body: "", + errored: undefined, + statusCode: 200, + }); + + const result = await ImageHelper.FetchImageFromRedditGallery("https://redd.it/gallery/image"); + + expect(result).toBeUndefined(); + }); + + test("GIVEN image source attribute is not found, EXPECT undefined returned", async () => { + fetchMock.mockResolvedValue({ + body: "", + errored: undefined, + statusCode: 200, + }); + + const result = await ImageHelper.FetchImageFromRedditGallery("https://redd.it/gallery/image"); + + expect(result).toBeUndefined(); + }); + + test("GIVEN image source attribute is not found that is a preview.redd.it url, EXPECT undefined returned", async () => { + fetchMock.mockResolvedValue({ + body: "", + errored: undefined, + statusCode: 200, + }); + + const result = await ImageHelper.FetchImageFromRedditGallery("https://redd.it/gallery/image"); + + expect(result).toBeUndefined(); + }); }); \ No newline at end of file diff --git a/tests/index.test.ts b/tests/index.test.ts index 87efd7a..e495d24 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -1,3 +1,4 @@ +import ImageHelper from "../src/imageHelper"; import randomBunny from "../src/index"; import fetch from "got-cjs"; @@ -170,9 +171,71 @@ describe('randomBunny', () => { expect(fetchMock).toBeCalledWith('https://reddit.com/r/rabbits/new.json?limit=100'); }); - test.todo("GIVEN data fetched is a gallery AND an image is returned from the helper, EXPECT this to be used"); + test("GIVEN data fetched is a gallery AND an image is returned from the helper, EXPECT this to be used", async () => { + fetchMock.mockResolvedValue({ + body: JSON.stringify({ + data: { + children: [ + { + data: { + archived: false, + downs: 0, + hidden: false, + permalink: '/r/Rabbits/comments/12pa5te/someone_told_pickles_its_monday_internal_fury/', + subreddit: 'Rabbits', + subreddit_subscribers: 298713, + title: 'Someone told pickles it’s Monday… *internal fury*', + ups: 1208, + url: 'https://i.redd.it/gallery/cr8xudsnkgua1', + }, + }, + ], + } + }), + }); - test.todo("GIVEN data fetched is a gallery AND an image is not returned from the helper, EXPECT error"); + ImageHelper.FetchImageFromRedditGallery = jest.fn().mockResolvedValue("https://i.redd.it/cr8xudsnkgua1.jpg") - test.todo("GIVEN data fetched is not a gallery, EXPECT url to be used directly"); + const result = await randomBunny('rabbits', 'new'); + + expect(result.IsSuccess).toBeTruthy(); + expect(result.Result).toBeDefined(); + + expect(fetchMock).toBeCalledWith('https://reddit.com/r/rabbits/new.json?limit=100'); + + expect(ImageHelper.FetchImageFromRedditGallery).toHaveBeenCalledTimes(1); + expect(ImageHelper.FetchImageFromRedditGallery).toHaveBeenCalledWith("https://i.redd.it/gallery/cr8xudsnkgua1") + }); + + test("GIVEN data fetched is a gallery AND an image is not returned from the helper, EXPECT error", async () => { + fetchMock.mockResolvedValue({ + body: JSON.stringify({ + data: { + children: [ + { + data: { + archived: false, + downs: 0, + hidden: false, + permalink: '/r/Rabbits/comments/12pa5te/someone_told_pickles_its_monday_internal_fury/', + subreddit: 'Rabbits', + subreddit_subscribers: 298713, + title: 'Someone told pickles it’s Monday… *internal fury*', + ups: 1208, + url: 'https://i.redd.it/gallery/cr8xudsnkgua1', + }, + }, + ], + } + }), + }); + + ImageHelper.FetchImageFromRedditGallery = jest.fn().mockResolvedValue(undefined) + + const result = await randomBunny('rabbits', 'new'); + + expect(ImageHelper.FetchImageFromRedditGallery).toHaveBeenCalledTimes(1); + + expect(result.IsSuccess).toBe(false); + }); }); \ No newline at end of file From 8a726b386ed0d995de81ca7816f3b005a4a229bd Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Fri, 12 Apr 2024 17:57:13 +0100 Subject: [PATCH 3/3] v2.1.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 597b732..7a374ad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "random-bunny", - "version": "2.1.5", + "version": "2.1.6", "description": "Get a random subreddit image url", "license": "MIT", "author": "Vylpes",