diff --git a/apps/api/src/__tests__/routes/tablo_data.test.ts b/apps/api/src/__tests__/routes/tablo_data.test.ts index de642a6..3b099e5 100644 --- a/apps/api/src/__tests__/routes/tablo_data.test.ts +++ b/apps/api/src/__tests__/routes/tablo_data.test.ts @@ -230,6 +230,116 @@ describe("TabloData Endpoint", () => { }); }); + describe("POST /tablo-data/:tabloId/:fileName - Upload File (Member Access)", () => { + // Helper function to upload file + const postTabloFileRequest = async ( + user: TestUserData, + // biome-ignore lint/suspicious/noExplicitAny: testClient requires any for dynamic route access + client: any, + tabloId: string, + fileName: string, + content: string + ) => { + return await client["tablo-data"][":tabloId"][":fileName"].$post( + { + param: { tabloId, fileName }, + json: { content, contentType: "text/plain" }, + }, + { + headers: { Authorization: `Bearer ${user.accessToken}` }, + } + ); + }; + + describe("Temporary User Access", () => { + it("should allow temp user to upload file to their own tablo", async () => { + const res = await postTabloFileRequest( + temporaryUser, + client, + "test_tablo_temp_private", + "new-file.txt", + "Test content" + ); + + expect(res.status).toBe(200); + const data = await res.json(); + expect(data.message).toBe("File uploaded successfully"); + expect(data.fileName).toBe("new-file.txt"); + }); + + it("should allow temp user to upload file to owner's shared tablo", async () => { + const res = await postTabloFileRequest( + temporaryUser, + client, + "test_tablo_owner_shared", + "temp-upload.txt", + "Temporary user content" + ); + + expect(res.status).toBe(200); + const data = await res.json(); + expect(data.message).toBe("File uploaded successfully"); + }); + + it("should deny temp user from uploading file to owner's private tablo", async () => { + const res = await postTabloFileRequest( + temporaryUser, + client, + "test_tablo_owner_private", + "unauthorized.txt", + "Should fail" + ); + + expect(res.status).toBe(403); + }); + }); + + describe("Owner Access", () => { + it("should allow owner to upload file to their own tablo", async () => { + const res = await postTabloFileRequest( + ownerUser, + client, + "test_tablo_owner_private", + "owner-file.txt", + "Owner content" + ); + + expect(res.status).toBe(200); + const data = await res.json(); + expect(data.message).toBe("File uploaded successfully"); + }); + }); + + describe("Validation", () => { + it("should return 400 if content is missing", async () => { + const res = await client["tablo-data"][":tabloId"][":fileName"].$post( + { + param: { tabloId: "test_tablo_owner_private", fileName: "test.txt" }, + json: { contentType: "text/plain" }, + }, + { + headers: { Authorization: `Bearer ${ownerUser.accessToken}` }, + } + ); + + expect(res.status).toBe(400); + const data = await res.json(); + expect(data.error).toBe("Content is required"); + }); + }); + + describe("Unauthenticated Access", () => { + it("should deny unauthenticated upload attempt", async () => { + const res = await client["tablo-data"][":tabloId"][":fileName"].$post({ + param: { tabloId: "test_tablo_owner_private", fileName: "test.txt" }, + json: { content: "test content" }, + }); + + expect(res.status).toBe(401); + }); + }); + }); + describe("DELETE /tablo-data/:tabloId/:fileName - Delete File (Admin Only)", () => { // Helper function to delete file const deleteTabloFileRequest = async ( diff --git a/apps/api/src/routers/tablo_data.ts b/apps/api/src/routers/tablo_data.ts index bdc8081..b41ad54 100644 --- a/apps/api/src/routers/tablo_data.ts +++ b/apps/api/src/routers/tablo_data.ts @@ -54,40 +54,39 @@ const getTabloFile = factory.createHandlers(checkTabloMember, async (c) => { } }); -const postTabloFile = (middlewareManager: ReturnType) => - factory.createHandlers(middlewareManager.regularUserCheck, checkTabloMember, async (c) => { - const tabloId = c.req.param("tabloId"); - const fileName = c.req.param("fileName"); +const postTabloFile = factory.createHandlers(checkTabloMember, async (c) => { + const tabloId = c.req.param("tabloId"); + const fileName = c.req.param("fileName"); - const s3_client = c.get("s3_client"); + const s3_client = c.get("s3_client"); - try { - const body = await c.req.json(); - const { content, contentType = "text/plain" } = body; + try { + const body = await c.req.json(); + const { content, contentType = "text/plain" } = body; - if (!content) { - return c.json({ error: "Content is required" }, 400); - } - - await s3_client.send( - new PutObjectCommand({ - Bucket: "tablo-data", - Key: `${tabloId}/${fileName}`, - Body: content, - ContentType: contentType, - }) - ); - - return c.json({ - message: "File uploaded successfully", - fileName, - tabloId, - }); - } catch (error) { - console.error("Error uploading file:", error); - return c.json({ error: "Failed to upload file" }, 500); + if (!content) { + return c.json({ error: "Content is required" }, 400); } - }); + + await s3_client.send( + new PutObjectCommand({ + Bucket: "tablo-data", + Key: `${tabloId}/${fileName}`, + Body: content, + ContentType: contentType, + }) + ); + + return c.json({ + message: "File uploaded successfully", + fileName, + tabloId, + }); + } catch (error) { + console.error("Error uploading file:", error); + return c.json({ error: "Failed to upload file" }, 500); + } +}); // // PUT /tablo-data/:tabloId/:fileName - Update a file // tabloDataRouter.put("/:tabloId/:fileName", async (c) => { @@ -162,7 +161,7 @@ export const getTabloDataRouter = () => { tabloDataRouter.get("/:tabloId/filenames", ...getTabloFilenames); tabloDataRouter.get("/:tabloId/:fileName", ...getTabloFile); - tabloDataRouter.post("/:tabloId/:fileName", ...postTabloFile(middlewareManager)); + tabloDataRouter.post("/:tabloId/:fileName", ...postTabloFile); tabloDataRouter.delete("/:tabloId/:fileName", ...deleteTabloFile(middlewareManager)); return tabloDataRouter;