Compare commits
No commits in common. "0940ca54c77fd4c64dc90657ef433dc58fdb2139" and "5b0701fc9378b89e4e344c1c2e17d63ab622f4fd" have entirely different histories.
0940ca54c7
...
5b0701fc93
6
TODO.md
6
TODO.md
|
@ -1,6 +1,4 @@
|
||||||
- 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
|
|
|
@ -6,9 +6,7 @@
|
||||||
"@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",
|
||||||
"@types/supertest": "^6.0.2",
|
"bun-types": "latest"
|
||||||
"bun-types": "latest",
|
|
||||||
"supertest": "^6.3.4"
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5.0.0"
|
"typescript": "^5.0.0"
|
||||||
|
|
18
src/app.ts
18
src/app.ts
|
@ -1,18 +0,0 @@
|
||||||
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;
|
|
|
@ -1,15 +0,0 @@
|
||||||
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();
|
|
19
src/index.ts
19
src/index.ts
|
@ -1,7 +1,18 @@
|
||||||
import app from "./app";
|
import compression from "compression";
|
||||||
|
import express from "express";
|
||||||
|
import listEndpoints from "express-list-endpoints";
|
||||||
|
|
||||||
const PORT = 3000;
|
const app = express();
|
||||||
|
const port = 3000;
|
||||||
|
|
||||||
app.listen(PORT, () =>
|
app.use(express.json());
|
||||||
console.log(`Express server listening on port ${PORT}`)
|
app.use(compression());
|
||||||
|
|
||||||
|
app.get("/", (_, res) => {
|
||||||
|
const endpoints = listEndpoints(app);
|
||||||
|
res.json({ endpoints });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(port, () =>
|
||||||
|
console.log(`Express server listening on port ${port}`)
|
||||||
);
|
);
|
|
@ -1,7 +1,7 @@
|
||||||
import { BotApiResponse } from "../types/BotApiResponse";
|
import { BotApiResponse } from "../types/BotApiResponse";
|
||||||
|
|
||||||
class BotApiService {
|
class BotApiService {
|
||||||
readonly BOT_API_URL = "http://192.168.178.27:30000";
|
readonly BOT_API_URL = "piparadis: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 {
|
||||||
return res.json();
|
res.json();
|
||||||
}
|
}
|
||||||
}) as BotApiResponse;
|
}) as BotApiResponse;
|
||||||
return response;
|
return response;
|
||||||
|
|
|
@ -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 {
|
||||||
return res.json();
|
res.json();
|
||||||
}
|
}
|
||||||
}) as GelbooruApiResponse;
|
}) as GelbooruApiResponse;
|
||||||
|
|
||||||
|
|
|
@ -23,12 +23,22 @@ 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((post): Image => {
|
.map(async (post): Promise<Image> => {
|
||||||
return { url: post.url, tags: post.tags };
|
const imageData = await this.urlToImageData(post.url);
|
||||||
|
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();
|
|
@ -1,4 +1,5 @@
|
||||||
export default interface Image {
|
export default interface Image {
|
||||||
url: string;
|
url: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
|
content: string;
|
||||||
}
|
}
|
|
@ -1,42 +0,0 @@
|
||||||
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);
|
|
||||||
});
|
|
||||||
})
|
|
|
@ -1,29 +1,10 @@
|
||||||
import { afterEach, describe, expect, it, mock, spyOn } from "bun:test";
|
import { 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";
|
||||||
|
@ -58,4 +39,16 @@ 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);
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in New Issue