diff --git a/TODO.md b/TODO.md index b26a90a..f9f0f22 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,6 @@ - Parametrize bot API url in env variable -- Encode images in base64 with proper prefix "data:image/png;base64," - Parametrize tags - Generalize the ImageService test with faker -- Is there a better way to import modules without using relative paths? \ No newline at end of file +- Is there a better way to import modules without using relative paths? + +- !!! Remove base64 encoding, not necessary anymore \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index 0d56a6c..ac0c08e 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 35bf0ff..1090331 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "@types/compression": "^1.7.5", "@types/express": "^4.17.21", "@types/express-list-endpoints": "^6.0.3", - "bun-types": "latest" + "@types/supertest": "^6.0.2", + "bun-types": "latest", + "supertest": "^6.3.4" }, "peerDependencies": { "typescript": "^5.0.0" diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 0000000..abb68a2 --- /dev/null +++ b/src/app.ts @@ -0,0 +1,18 @@ +import compression from "compression"; +import express from "express"; +import listEndpoints from "express-list-endpoints"; +import ImageController from "./controllers/ImageController"; + +const app = express(); + +app.use(express.json()); +app.use(compression()); + +app.get("/", (_, res) => { + const endpoints = listEndpoints(app); + res.json({ endpoints }); +}); + +app.get("/image", ImageController.get); + +export default app; \ No newline at end of file diff --git a/src/controllers/ImageController.ts b/src/controllers/ImageController.ts index e69de29..3b2035c 100644 --- a/src/controllers/ImageController.ts +++ b/src/controllers/ImageController.ts @@ -0,0 +1,15 @@ +import { Request, Response } from "express"; +import ImageService from "../services/ImageService"; + +class ImageController { + async get(req: Request, res: Response) { + try { + const image = await ImageService.get(); + res.json(image); + } catch (error: any) { + res.status(500).json({ "error": "Internal server error" }); + } + } +} + +export default new ImageController(); \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index eea0e28..54a7b81 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,18 +1,7 @@ -import compression from "compression"; -import express from "express"; -import listEndpoints from "express-list-endpoints"; +import app from "./app"; -const app = express(); -const port = 3000; +const PORT = 3000; -app.use(express.json()); -app.use(compression()); - -app.get("/", (_, res) => { - const endpoints = listEndpoints(app); - res.json({ endpoints }); -}); - -app.listen(port, () => - console.log(`Express server listening on port ${port}`) +app.listen(PORT, () => + console.log(`Express server listening on port ${PORT}`) ); \ No newline at end of file diff --git a/src/services/BotApiService.ts b/src/services/BotApiService.ts index 13363c2..44598e4 100644 --- a/src/services/BotApiService.ts +++ b/src/services/BotApiService.ts @@ -1,7 +1,7 @@ import { BotApiResponse } from "../types/BotApiResponse"; class BotApiService { - readonly BOT_API_URL = "piparadis:30000"; + readonly BOT_API_URL = "http://192.168.178.27:30000"; async getAll(): Promise { const get_url = `${this.BOT_API_URL}/images`; @@ -10,7 +10,7 @@ class BotApiService { if (!res.ok) { throw new Error("Error fetching images"); } else { - res.json(); + return res.json(); } }) as BotApiResponse; return response; diff --git a/src/services/GelbooruApiService.ts b/src/services/GelbooruApiService.ts index 7856075..480d2b9 100644 --- a/src/services/GelbooruApiService.ts +++ b/src/services/GelbooruApiService.ts @@ -11,7 +11,7 @@ class GelbooruApiService { if (!res.ok) { throw new Error("Error fetching images"); } else { - res.json(); + return res.json(); } }) as GelbooruApiResponse; diff --git a/src/services/ImageService.ts b/src/services/ImageService.ts index 6b47e37..56924c0 100644 --- a/src/services/ImageService.ts +++ b/src/services/ImageService.ts @@ -23,22 +23,12 @@ class ImageService { const validPosts = Promise.all(posts .filter(post => !imagesUrls.some(url => url === post.url)) - .map(async (post): Promise => { - const imageData = await this.urlToImageData(post.url); - const encodedImage = this.imageToBase64(imageData); - return { url: post.url, tags: post.tags, content: encodedImage }; + .map((post): Image => { + return { url: post.url, tags: post.tags }; })); return validPosts; } - - private async urlToImageData(url: string): Promise { - return await (await fetch(url)).text(); - } - - private imageToBase64(imageData: string): string { - return Buffer.from(imageData, "binary").toString("base64"); - } } export default new ImageService(); \ No newline at end of file diff --git a/src/types/Image.ts b/src/types/Image.ts index fe6d63f..1f2ab10 100644 --- a/src/types/Image.ts +++ b/src/types/Image.ts @@ -1,5 +1,4 @@ export default interface Image { url: string; tags: string[]; - content: string; } \ No newline at end of file diff --git a/test/ImageController/ImageController.test.ts b/test/ImageController/ImageController.test.ts new file mode 100644 index 0000000..41a0d31 --- /dev/null +++ b/test/ImageController/ImageController.test.ts @@ -0,0 +1,42 @@ +import { afterEach, describe, expect, it, mock, jest } from "bun:test"; +import Image from "../../src/types/Image"; +import ImageService from "../../src/services/ImageService"; +import GelbooruApiResponse from "../../src/types/GelbooruServiceResponse"; +import { BotApiResponse } from "../../src/types/BotApiResponse"; +import fs from "node:fs"; +import ImageController from "../../src/controllers/ImageController"; +import app from "../../src/app"; +import request from "supertest"; + +const imageServiceOriginal = ImageService; + +afterEach(() => { + mock.restore(); + mock.module("../../src/services/ImageService", () => ({ + default: imageServiceOriginal, + })); +}) + +describe("endpoint returns the correct status codes", () => { + it("should return 200 if successful", async () => { + // Can't mock ImageService partially because of bun incomplete implementation of testing :( + // It goes to the network directly to test + const res = await request(app).get("/image"); + expect(res.status).toBe(200); + }); + + it("should return 500 if any error happens", async () => { + mock.module("../../src/services/ImageService", () => { + let alreadyCalled = false; + return { + default: { + get: () => { + throw new Error("Controlled error"); + } + } + } + }); + const res = await request(app).get("/image"); + expect(res.status).toBe(500); + }); +}) \ No newline at end of file diff --git a/test/ImageService/ImageService.test.ts b/test/ImageService/ImageService.test.ts index 359a131..9ae0912 100644 --- a/test/ImageService/ImageService.test.ts +++ b/test/ImageService/ImageService.test.ts @@ -1,10 +1,29 @@ -import { describe, expect, it, mock, spyOn } from "bun:test"; +import { afterEach, describe, expect, it, mock, spyOn } from "bun:test"; import Image from "../../src/types/Image"; import ImageService from "../../src/services/ImageService"; import GelbooruApiResponse from "../../src/types/GelbooruServiceResponse"; import { BotApiResponse } from "../../src/types/BotApiResponse"; +import GelbooruApiService from "../../src/services/GelbooruApiService"; +import BotApiService from "../../src/services/BotApiService"; import fs from "node:fs"; +const imageServiceOriginal = ImageService; +const gelbooruApiServiceOriginal = GelbooruApiService; +const botApiServiceOriginal = BotApiService; + +afterEach(() => { + mock.restore(); + mock.module("../../src/services/ImageService", () => ({ + default: imageServiceOriginal, + })); + mock.module("../../src/services/GelbooruApiService", () => ({ + default: gelbooruApiServiceOriginal, + })); + mock.module("../../src/services/BotApiService", () => ({ + default: botApiServiceOriginal + })); +}) + describe("endpoint gets a non repeated image", () => { it("should return an image that is not in the response of the /images endpoint of the bot API", async () => { const REPEATED_URL = "https://fastly.picsum.photos/id/1/10/20.jpg?hmac=gY6PvUXFacKfYpBpTTVcNLxumpyMmoCamM-J5DOPwNc"; @@ -39,16 +58,4 @@ describe("endpoint gets a non repeated image", () => { const image: Image = await ImageService.get(); expect(image.url).not.toBe(REPEATED_URL); }); -}); - -describe("imageToBase64 works as expected", () => { - it("should encode image to base64 properly", () => { - const imageData = fs.readFileSync('./test/ImageService/testImage.jpg'); - const imageToBase64Spy = spyOn(ImageService as any, 'imageToBase64'); - - const actualEncoding = imageToBase64Spy.call(ImageService, imageData); - const expectedEncoding = fs.readFileSync('./test/ImageService/testImage.b64', "base64").toString(); - - expect(actualEncoding).toBe(expectedEncoding); - }) -}) \ No newline at end of file +}); \ No newline at end of file