Compare commits

..

2 Commits

Author SHA1 Message Date
Sugui 0940ca54c7 Added ImageController, removed base64 and exported app to app.ts 2024-04-15 10:54:01 +02:00
Sugui 864506d9a4 Added task to TODO 2024-04-14 12:04:53 +02:00
12 changed files with 112 additions and 48 deletions

View File

@ -1,4 +1,6 @@
- Parametrize bot API url in env variable - Parametrize bot API url in env variable
- Encode images in base64 with proper prefix "data:image/png;base64,<encoding>"
- Parametrize tags - Parametrize tags
- Generalize the ImageService test with faker - Generalize the ImageService test with faker
- Is there a better way to import modules without using relative paths?
- !!! Remove base64 encoding, not necessary anymore

BIN
bun.lockb

Binary file not shown.

View File

@ -6,7 +6,9 @@
"@types/compression": "^1.7.5", "@types/compression": "^1.7.5",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/express-list-endpoints": "^6.0.3", "@types/express-list-endpoints": "^6.0.3",
"bun-types": "latest" "@types/supertest": "^6.0.2",
"bun-types": "latest",
"supertest": "^6.3.4"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "^5.0.0" "typescript": "^5.0.0"

18
src/app.ts Normal file
View File

@ -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;

View File

@ -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();

View File

@ -1,18 +1,7 @@
import compression from "compression"; import app from "./app";
import express from "express";
import listEndpoints from "express-list-endpoints";
const app = express(); const PORT = 3000;
const port = 3000;
app.use(express.json()); app.listen(PORT, () =>
app.use(compression()); console.log(`Express server listening on port ${PORT}`)
app.get("/", (_, res) => {
const endpoints = listEndpoints(app);
res.json({ endpoints });
});
app.listen(port, () =>
console.log(`Express server listening on port ${port}`)
); );

View File

@ -1,7 +1,7 @@
import { BotApiResponse } from "../types/BotApiResponse"; import { BotApiResponse } from "../types/BotApiResponse";
class BotApiService { class BotApiService {
readonly BOT_API_URL = "piparadis:30000"; readonly BOT_API_URL = "http://192.168.178.27:30000";
async getAll(): Promise<BotApiResponse> { async getAll(): Promise<BotApiResponse> {
const get_url = `${this.BOT_API_URL}/images`; const get_url = `${this.BOT_API_URL}/images`;
@ -10,7 +10,7 @@ class BotApiService {
if (!res.ok) { if (!res.ok) {
throw new Error("Error fetching images"); throw new Error("Error fetching images");
} else { } else {
res.json(); return res.json();
} }
}) as BotApiResponse; }) as BotApiResponse;
return response; return response;

View File

@ -11,7 +11,7 @@ class GelbooruApiService {
if (!res.ok) { if (!res.ok) {
throw new Error("Error fetching images"); throw new Error("Error fetching images");
} else { } else {
res.json(); return res.json();
} }
}) as GelbooruApiResponse; }) as GelbooruApiResponse;

View File

@ -23,22 +23,12 @@ class ImageService {
const validPosts = Promise.all(posts const validPosts = Promise.all(posts
.filter(post => !imagesUrls.some(url => url === post.url)) .filter(post => !imagesUrls.some(url => url === post.url))
.map(async (post): Promise<Image> => { .map((post): Image => {
const imageData = await this.urlToImageData(post.url); return { url: post.url, tags: post.tags };
const encodedImage = this.imageToBase64(imageData);
return { url: post.url, tags: post.tags, content: encodedImage };
})); }));
return validPosts; return validPosts;
} }
private async urlToImageData(url: string): Promise<string> {
return await (await fetch(url)).text();
}
private imageToBase64(imageData: string): string {
return Buffer.from(imageData, "binary").toString("base64");
}
} }
export default new ImageService(); export default new ImageService();

View File

@ -1,5 +1,4 @@
export default interface Image { export default interface Image {
url: string; url: string;
tags: string[]; tags: string[];
content: string;
} }

View File

@ -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);
});
})

View File

@ -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 Image from "../../src/types/Image";
import ImageService from "../../src/services/ImageService"; import ImageService from "../../src/services/ImageService";
import GelbooruApiResponse from "../../src/types/GelbooruServiceResponse"; import GelbooruApiResponse from "../../src/types/GelbooruServiceResponse";
import { BotApiResponse } from "../../src/types/BotApiResponse"; import { BotApiResponse } from "../../src/types/BotApiResponse";
import GelbooruApiService from "../../src/services/GelbooruApiService";
import BotApiService from "../../src/services/BotApiService";
import fs from "node:fs"; 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", () => { 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 () => { 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"; 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(); const image: Image = await ImageService.get();
expect(image.url).not.toBe(REPEATED_URL); 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);
})
})