diff --git a/database/s3.ts b/database/s3.ts index 2e5816a..2c9d820 100644 --- a/database/s3.ts +++ b/database/s3.ts @@ -29,18 +29,17 @@ export default abstract class S3 { /** * Uploads a compressed image to Minio as a base64 string. - * @param {Blob} data The image data. + * @param {string} data The base64 string of the image. * @param {string} options.path The path to upload the image to. * @param {string} options.creator The creator of the image. * @returns {Promise} The path of the uploaded image. */ - public static async putImage(data: Blob, options?: { path?: string; creator?: string }): Promise { + public static async putImage(data: string, options?: { path?: string; creator?: string }): Promise { try { const imagePath = options?.path ?? randomUUID(); + const image = await compress(data); - const compressedData = await compress(data); - const buffer = Buffer.from(compressedData, "base64"); - await S3.client.putObject(globals.env.MINIO_DEFAULT_BUCKETS, imagePath, buffer, compressedData.length, { + await S3.client.putObject(globals.env.MINIO_DEFAULT_BUCKETS, imagePath, image, image.length, { "Content-Type": "application/octet-stream", "Last-Modified": new Date().toUTCString(), "x-amz-acl": "public-read", @@ -60,23 +59,28 @@ export default abstract class S3 { * @returns {Promise} */ public static async getImage(path: string): Promise { - const data: internal.Readable = await S3.client.getObject(globals.env.MINIO_DEFAULT_BUCKETS, path); - if (!data) return null; + try { + const data: internal.Readable = await S3.client.getObject(globals.env.MINIO_DEFAULT_BUCKETS, path); + if (!data) return null; - const buffer = await new Promise((resolve, reject) => { - const chunks: Buffer[] = []; - data.on("data", (chunk: Buffer) => { - chunks.push(chunk); - }) - .on("end", () => { - resolve(Buffer.concat(chunks)); + const buffer = await new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + data.on("data", (chunk: Buffer) => { + chunks.push(chunk); }) - .on("error", (err) => { - reject(err); - }); - }); + .on("end", () => { + resolve(Buffer.concat(chunks)); + }) + .on("error", (err) => { + reject(err); + }); + }); - return buffer.toString("base64"); + return buffer.toString("base64"); + } catch (err) { + Logger.error("s3.ts::getImage | Error getting image", err); + return null; + } } /** diff --git a/i18n/en/errors.json b/i18n/en/errors.json index 4f6c608..798b8c1 100644 --- a/i18n/en/errors.json +++ b/i18n/en/errors.json @@ -2,7 +2,10 @@ "template": "This is a template error message", "validation": "Validation error", "database": "Database error", - "image": "Invalid image", + "image": { + "invalid": "Invalid image", + "notFound": "Image not found" + }, "auth": { "admin": "Invalid X-ADMIN-KEY header", "toomany": "Too many requests", diff --git a/routes/blob.ts b/routes/blob.ts index cdb4121..d2fc08e 100644 --- a/routes/blob.ts +++ b/routes/blob.ts @@ -21,7 +21,7 @@ export default async function Route_Blob_Read(req: Request, res: Response, next: if (!data) { return Status.send(req, next, { status: 404, - error: "errors.notFound" + error: "errors.image.notFound" }); } diff --git a/routes/users/update.ts b/routes/users/update.ts index 9389aaf..0cd7ecb 100644 --- a/routes/users/update.ts +++ b/routes/users/update.ts @@ -60,7 +60,7 @@ export default async function Route_Users_Update(req: Request, res: Response, ne if (!avatarUrl) { return Status.send(req, next, { status: 500, - error: "errors.image" + error: "errors.image.invalid" }); } } diff --git a/tests/assets/avatar.png b/tests/assets/avatar.png new file mode 100644 index 0000000..e5025b9 Binary files /dev/null and b/tests/assets/avatar.png differ diff --git a/tests/e2e/users.test.ts b/tests/e2e/users.test.ts index 47591eb..9660cc4 100644 --- a/tests/e2e/users.test.ts +++ b/tests/e2e/users.test.ts @@ -1,9 +1,15 @@ import createApp from "@/app"; import AuthController from "@/controllers/auth"; import { get, post, put } from "../utils"; +import { readFileSync } from "fs"; +import path from "path"; const app = createApp("e2e-users"); +const testGlobals = { + userAvatarUrl: null +}; + describe("Test users", () => { const email = "test-users@no-reply.local"; const token = AuthController.generateCreationToken(email); @@ -112,11 +118,13 @@ describe("Test users", () => { const authToken = AuthController.generateAuthToken(email); test("should edit the user", async () => { + const avatar = readFileSync(path.join(__dirname, "..", "assets", "avatar.png"), "base64"); + const res = await put( app, "/users/:uuid", { uuid: userUuid }, - { quote: "test" }, + { quote: "test", avatar: avatar }, { Authorization: `Bearer ${authToken}` } ); @@ -131,13 +139,33 @@ describe("Test users", () => { uuid: userUuid, email: email, username: "test-users", - avatarUrl: null, + avatarUrl: expect.stringMatching( + /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/ + ), // UUID v4 regex clubId: null, quote: "test" } } ] }); + + testGlobals.userAvatarUrl = res.body.response[0].data.avatarUrl; + }); + + test("should get the user avatar", async () => { + const res = await get(app, "/blob/:uuid", { uuid: testGlobals.userAvatarUrl ?? "invalid" }); + + expect(res.body).toStrictEqual({ + masterStatus: 200, + sentAt: expect.any(Number), + response: [ + { + status: 200, + success: true, + data: expect.any(String) + } + ] + }); }); test("should get 'invalid token' error", async () => { @@ -179,7 +207,7 @@ describe("Test users", () => { uuid: userUuid, email: email, username: "test-users", - avatarUrl: null, + avatarUrl: testGlobals.userAvatarUrl, clubId: null, quote: "test" } diff --git a/tests/unit/blob.test.ts b/tests/unit/blob.test.ts new file mode 100644 index 0000000..f816772 --- /dev/null +++ b/tests/unit/blob.test.ts @@ -0,0 +1,23 @@ +import createApp from "@/app"; +import { get } from "../utils"; + +const app = createApp("unit-blob"); + +describe("Test status page", () => { + test("should get a 'image not found' error", async () => { + const res = await get(app, "/blob/:uuid", { uuid: "definitely-not-a-valid-uuid" }); + + expect(res.body).toStrictEqual({ + masterStatus: 404, + sentAt: expect.any(Number), + response: [ + { + status: 404, + success: false, + error: "errors.image.notFound", + translatedError: "Image not found" + } + ] + }); + }); +});