From 2ad31609f8bcb7cccb7da24a422c4b7b68fe9f83 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 17 Apr 2026 13:12:10 +0300 Subject: [PATCH] test(e2e): add bookmark E2E test suite (22 tests) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add tests/e2e/bookmark/save-post.test.ts - Add tests/e2e/bookmark/remove-post.test.ts - Add tests/e2e/bookmark/save-comment.test.ts - Add tests/e2e/bookmark/remove-comment.test.ts - Add tests/e2e/bookmark/get-bookmarks.test.ts Each suite prepares its own state via beforeAll (register → login → create post/comment → seed bookmarks). No mocking. Tests cover happy path, idempotency, 404 for non-existent resources, and 401 for unauthenticated access. --- tests/e2e/bookmark/get-bookmarks.test.ts | 213 ++++++++++++++++++++++ tests/e2e/bookmark/remove-comment.test.ts | 108 +++++++++++ tests/e2e/bookmark/remove-post.test.ts | 100 ++++++++++ tests/e2e/bookmark/save-comment.test.ts | 103 +++++++++++ tests/e2e/bookmark/save-post.test.ts | 94 ++++++++++ 5 files changed, 618 insertions(+) create mode 100644 tests/e2e/bookmark/get-bookmarks.test.ts create mode 100644 tests/e2e/bookmark/remove-comment.test.ts create mode 100644 tests/e2e/bookmark/remove-post.test.ts create mode 100644 tests/e2e/bookmark/save-comment.test.ts create mode 100644 tests/e2e/bookmark/save-post.test.ts diff --git a/tests/e2e/bookmark/get-bookmarks.test.ts b/tests/e2e/bookmark/get-bookmarks.test.ts new file mode 100644 index 0000000..ff631dc --- /dev/null +++ b/tests/e2e/bookmark/get-bookmarks.test.ts @@ -0,0 +1,213 @@ +import { authRequest, parseBody, request } from "../setup"; +import { beforeAll, describe, expect, it } from "vitest"; + +/** + * E2E tests for the Get Bookmarks endpoint. + * Covers empty state responses for new users, mixed post/comment results + * for seeded users, pagination behaviour, and unauthenticated access rejection. + */ +describe("GET /posts/bookmarks - Get User Bookmarks", () => { + /** + * Tests the response for a user who has no bookmarks yet. + * Validates that the endpoint returns empty arrays with zero totals. + */ + describe("empty state — user with no bookmarks", () => { + const ts = Date.now(); + const userA = { + email: `bgba-${ts}@test.com`, + password: "password123", + username: `bgba${ts}`, + }; + + let tokenA = ""; + + beforeAll(async () => { + await request({ + method: "POST", + url: "/auth/register", + payload: userA, + }); + + const loginRes = await request({ + method: "POST", + url: "/auth/login", + payload: { identifier: userA.email, password: userA.password }, + }); + + tokenA = parseBody<{ data: { accessToken: string } }>(loginRes).data + .accessToken; + }); + + it("should return 200 with empty arrays for a new user", async () => { + const response = await authRequest(tokenA, { + method: "GET", + url: "/posts/bookmarks", + }); + const body = parseBody<{ + data: { posts: unknown[]; comments: unknown[] }; + meta: { + postTotal: number; + commentTotal: number; + page: number; + timestamp: string; + }; + }>(response); + + expect(response.statusCode).toBe(200); + expect(body.data.posts).toEqual([]); + expect(body.data.comments).toEqual([]); + expect(body.meta.postTotal).toBe(0); + expect(body.meta.commentTotal).toBe(0); + expect(body.meta.page).toBe(1); + expect(body.meta).toHaveProperty("timestamp", expect.any(String)); + }); + }); + + /** + * Tests responses for a user who has seeded post and comment bookmarks. + * Validates the structure, totals, content, and pagination of the response. + */ + describe("seeded state — user with bookmarked posts and comments", () => { + const ts = Date.now() + 1; + const userB = { + email: `bgbb-${ts}@test.com`, + password: "password123", + username: `bgbb${ts}`, + }; + + let tokenB = ""; + let postIdA = ""; + let postIdB = ""; + + /** + * Registers user B, logs in, creates two posts and saves both as bookmarks, + * then creates a comment on the first post and saves it as a bookmark. + * This provides a known mixed state for all seeded-state tests. + */ + beforeAll(async () => { + await request({ + method: "POST", + url: "/auth/register", + payload: userB, + }); + + const loginRes = await request({ + method: "POST", + url: "/auth/login", + payload: { identifier: userB.email, password: userB.password }, + }); + + tokenB = parseBody<{ data: { accessToken: string } }>(loginRes).data + .accessToken; + + const postResA = await authRequest(tokenB, { + method: "POST", + url: "/posts", + payload: { content: "Seeded bookmark post A" }, + }); + postIdA = parseBody<{ data: { id: string } }>(postResA).data.id; + + const postResB = await authRequest(tokenB, { + method: "POST", + url: "/posts", + payload: { content: "Seeded bookmark post B" }, + }); + postIdB = parseBody<{ data: { id: string } }>(postResB).data.id; + + await authRequest(tokenB, { + method: "POST", + url: `/posts/${postIdA}/save`, + }); + + await authRequest(tokenB, { + method: "POST", + url: `/posts/${postIdB}/save`, + }); + + const commentRes = await authRequest(tokenB, { + method: "POST", + url: `/posts/${postIdA}/comments`, + payload: { content: "Seeded bookmark comment" }, + }); + const commentId = parseBody<{ data: { id: string } }>(commentRes) + .data.id; + + await authRequest(tokenB, { + method: "POST", + url: `/comments/${commentId}/save`, + }); + }); + + it("should return 200 with the saved posts in the response", async () => { + const response = await authRequest(tokenB, { + method: "GET", + url: "/posts/bookmarks", + }); + const body = parseBody<{ + data: { posts: { id: string }[] }; + meta: { postTotal: number }; + }>(response); + + expect(response.statusCode).toBe(200); + expect(body.data.posts.length).toBeGreaterThanOrEqual(2); + expect(body.meta.postTotal).toBeGreaterThanOrEqual(2); + }); + + it("should return 200 with the saved comment in the response", async () => { + const response = await authRequest(tokenB, { + method: "GET", + url: "/posts/bookmarks", + }); + const body = parseBody<{ + data: { comments: { id: string }[] }; + meta: { commentTotal: number }; + }>(response); + + expect(response.statusCode).toBe(200); + expect(body.data.comments.length).toBeGreaterThanOrEqual(1); + expect(body.meta.commentTotal).toBeGreaterThanOrEqual(1); + }); + + it("should return correct postTotal and commentTotal in meta", async () => { + const response = await authRequest(tokenB, { + method: "GET", + url: "/posts/bookmarks", + }); + const body = parseBody<{ + meta: { postTotal: number; commentTotal: number; page: number }; + }>(response); + + expect(response.statusCode).toBe(200); + expect(body.meta.postTotal).toBe(2); + expect(body.meta.commentTotal).toBe(1); + expect(body.meta.page).toBe(1); + }); + + it("should support pagination with page and limit query params", async () => { + const response = await authRequest(tokenB, { + method: "GET", + url: "/posts/bookmarks?page=1&limit=1", + }); + const body = parseBody<{ + data: { posts: unknown[] }; + meta: { postTotal: number; page: number }; + }>(response); + + expect(response.statusCode).toBe(200); + expect(body.data.posts).toHaveLength(1); + expect(body.meta.postTotal).toBe(2); + expect(body.meta.page).toBe(1); + }); + + it("should return 401 when not authenticated", async () => { + const response = await request({ + method: "GET", + url: "/posts/bookmarks", + }); + const body = parseBody<{ title: string }>(response); + + expect(response.statusCode).toBe(401); + expect(body.title).toBe("UnauthorizedError"); + }); + }); +}); diff --git a/tests/e2e/bookmark/remove-comment.test.ts b/tests/e2e/bookmark/remove-comment.test.ts new file mode 100644 index 0000000..78d3dcd --- /dev/null +++ b/tests/e2e/bookmark/remove-comment.test.ts @@ -0,0 +1,108 @@ +import { authRequest, parseBody, request } from "../setup"; +import { beforeAll, describe, expect, it } from "vitest"; + +const FAKE_UUID = "00000000-0000-0000-0000-000000000000"; + +/** + * E2E tests for the Remove Comment Bookmark endpoint. + * Validates that an authenticated user can remove a comment bookmark, + * that the operation is idempotent when no bookmark exists, + * and that proper errors are returned for invalid comment IDs or unauthenticated requests. + */ +describe("DELETE /comments/:commentId/unsave - Remove Comment Bookmark", () => { + const ts = Date.now(); + const user = { + email: `brc-${ts}@test.com`, + password: "password123", + username: `brc${ts}`, + }; + + let accessToken = ""; + let commentId = ""; + + /** + * Registers a new test user, logs in to obtain an access token, + * creates a post, creates a comment on that post, then saves the comment + * as a bookmark so the first unsave test has a bookmark to remove. + */ + beforeAll(async () => { + await request({ + method: "POST", + url: "/auth/register", + payload: user, + }); + + const loginRes = await request({ + method: "POST", + url: "/auth/login", + payload: { identifier: user.email, password: user.password }, + }); + + accessToken = parseBody<{ data: { accessToken: string } }>(loginRes) + .data.accessToken; + + const postRes = await authRequest(accessToken, { + method: "POST", + url: "/posts", + payload: { content: "E2E test post for comment bookmark remove" }, + }); + + const postId = parseBody<{ data: { id: string } }>(postRes).data.id; + + const commentRes = await authRequest(accessToken, { + method: "POST", + url: `/posts/${postId}/comments`, + payload: { content: "E2E test comment for bookmark remove" }, + }); + + commentId = parseBody<{ data: { id: string } }>(commentRes).data.id; + + await authRequest(accessToken, { + method: "POST", + url: `/comments/${commentId}/save`, + }); + }); + + it("should return 200 when removing a saved comment bookmark", async () => { + const response = await authRequest(accessToken, { + method: "DELETE", + url: `/comments/${commentId}/unsave`, + }); + const body = parseBody<{ meta: { timestamp: string } }>(response); + + expect(response.statusCode).toBe(200); + expect(body.meta).toHaveProperty("timestamp", expect.any(String)); + }); + + it("should return 200 when removing a non-existing comment bookmark (idempotent)", async () => { + const response = await authRequest(accessToken, { + method: "DELETE", + url: `/comments/${commentId}/unsave`, + }); + + expect(response.statusCode).toBe(200); + }); + + it("should return 404 when the comment does not exist", async () => { + const response = await authRequest(accessToken, { + method: "DELETE", + url: `/comments/${FAKE_UUID}/unsave`, + }); + const body = parseBody<{ title: string; detail: string }>(response); + + expect(response.statusCode).toBe(404); + expect(body.title).toBe("NotFoundError"); + expect(body.detail).toBe("Comment not found."); + }); + + it("should return 401 when not authenticated", async () => { + const response = await request({ + method: "DELETE", + url: `/comments/${commentId}/unsave`, + }); + const body = parseBody<{ title: string }>(response); + + expect(response.statusCode).toBe(401); + expect(body.title).toBe("UnauthorizedError"); + }); +}); diff --git a/tests/e2e/bookmark/remove-post.test.ts b/tests/e2e/bookmark/remove-post.test.ts new file mode 100644 index 0000000..f64d057 --- /dev/null +++ b/tests/e2e/bookmark/remove-post.test.ts @@ -0,0 +1,100 @@ +import { authRequest, parseBody, request } from "../setup"; +import { beforeAll, describe, expect, it } from "vitest"; + +const FAKE_UUID = "00000000-0000-0000-0000-000000000000"; + +/** + * E2E tests for the Remove Post Bookmark endpoint. + * Validates that an authenticated user can remove a post bookmark, + * that the operation is idempotent when no bookmark exists, + * and that proper errors are returned for invalid post IDs or unauthenticated requests. + */ +describe("DELETE /posts/:id/unsave - Remove Post Bookmark", () => { + const ts = Date.now(); + const user = { + email: `brp-${ts}@test.com`, + password: "password123", + username: `brp${ts}`, + }; + + let accessToken = ""; + let postId = ""; + + /** + * Registers a new test user, logs in to obtain an access token, + * creates a post, and saves it as a bookmark so the first unsave test + * has a bookmark to remove. + */ + beforeAll(async () => { + await request({ + method: "POST", + url: "/auth/register", + payload: user, + }); + + const loginRes = await request({ + method: "POST", + url: "/auth/login", + payload: { identifier: user.email, password: user.password }, + }); + + accessToken = parseBody<{ data: { accessToken: string } }>(loginRes) + .data.accessToken; + + const postRes = await authRequest(accessToken, { + method: "POST", + url: "/posts", + payload: { content: "E2E test post for bookmark remove" }, + }); + + postId = parseBody<{ data: { id: string } }>(postRes).data.id; + + await authRequest(accessToken, { + method: "POST", + url: `/posts/${postId}/save`, + }); + }); + + it("should return 200 when removing a saved bookmark", async () => { + const response = await authRequest(accessToken, { + method: "DELETE", + url: `/posts/${postId}/unsave`, + }); + const body = parseBody<{ meta: { timestamp: string } }>(response); + + expect(response.statusCode).toBe(200); + expect(body.meta).toHaveProperty("timestamp", expect.any(String)); + }); + + it("should return 200 when removing a non-existing bookmark (idempotent)", async () => { + const response = await authRequest(accessToken, { + method: "DELETE", + url: `/posts/${postId}/unsave`, + }); + + expect(response.statusCode).toBe(200); + }); + + it("should return 404 when the post does not exist", async () => { + const response = await authRequest(accessToken, { + method: "DELETE", + url: `/posts/${FAKE_UUID}/unsave`, + }); + const body = parseBody<{ title: string; detail: string }>(response); + + expect(response.statusCode).toBe(404); + expect(body.title).toBe("NotFoundError"); + expect(body.detail).toBe("Post not found."); + }); + + it("should return 401 when not authenticated", async () => { + const response = await request({ + method: "DELETE", + url: `/posts/${postId}/unsave`, + }); + const body = parseBody<{ title: string }>(response); + + expect(response.statusCode).toBe(401); + expect(body.title).toBe("UnauthorizedError"); + }); +}); diff --git a/tests/e2e/bookmark/save-comment.test.ts b/tests/e2e/bookmark/save-comment.test.ts new file mode 100644 index 0000000..e623248 --- /dev/null +++ b/tests/e2e/bookmark/save-comment.test.ts @@ -0,0 +1,103 @@ +import { authRequest, parseBody, request } from "../setup"; +import { beforeAll, describe, expect, it } from "vitest"; + +const FAKE_UUID = "00000000-0000-0000-0000-000000000000"; + +/** + * E2E tests for the Save Comment Bookmark endpoint. + * Validates that an authenticated user can bookmark a comment, + * that the operation is idempotent, and that proper errors are returned + * for invalid comment IDs or unauthenticated requests. + */ +describe("POST /comments/:commentId/save - Save Comment Bookmark", () => { + const ts = Date.now(); + const user = { + email: `bsc-${ts}@test.com`, + password: "password123", + username: `bsc${ts}`, + }; + + let accessToken = ""; + let commentId = ""; + + /** + * Registers a new test user, logs in to obtain an access token, + * creates a post, then creates a comment on that post to use + * as the bookmark target throughout all tests. + */ + beforeAll(async () => { + await request({ + method: "POST", + url: "/auth/register", + payload: user, + }); + + const loginRes = await request({ + method: "POST", + url: "/auth/login", + payload: { identifier: user.email, password: user.password }, + }); + + accessToken = parseBody<{ data: { accessToken: string } }>(loginRes) + .data.accessToken; + + const postRes = await authRequest(accessToken, { + method: "POST", + url: "/posts", + payload: { content: "E2E test post for comment bookmark save" }, + }); + + const postId = parseBody<{ data: { id: string } }>(postRes).data.id; + + const commentRes = await authRequest(accessToken, { + method: "POST", + url: `/posts/${postId}/comments`, + payload: { content: "E2E test comment for bookmark save" }, + }); + + commentId = parseBody<{ data: { id: string } }>(commentRes).data.id; + }); + + it("should return 201 when saving a comment bookmark", async () => { + const response = await authRequest(accessToken, { + method: "POST", + url: `/comments/${commentId}/save`, + }); + const body = parseBody<{ meta: { timestamp: string } }>(response); + + expect(response.statusCode).toBe(201); + expect(body.meta).toHaveProperty("timestamp", expect.any(String)); + }); + + it("should return 201 when saving an already-bookmarked comment (idempotent)", async () => { + const response = await authRequest(accessToken, { + method: "POST", + url: `/comments/${commentId}/save`, + }); + + expect(response.statusCode).toBe(201); + }); + + it("should return 404 when the comment does not exist", async () => { + const response = await authRequest(accessToken, { + method: "POST", + url: `/comments/${FAKE_UUID}/save`, + }); + const body = parseBody<{ title: string; detail: string }>(response); + + expect(response.statusCode).toBe(404); + expect(body.title).toBe("NotFoundError"); + expect(body.detail).toBe("Comment not found."); + }); + + it("should return 401 when not authenticated", async () => { + const response = await request({ + method: "POST", + url: `/comments/${commentId}/save`, + }); + const body = parseBody<{ title: string }>(response); + + expect(response.statusCode).toBe(401); + expect(body.title).toBe("UnauthorizedError"); + }); +}); diff --git a/tests/e2e/bookmark/save-post.test.ts b/tests/e2e/bookmark/save-post.test.ts new file mode 100644 index 0000000..dbaabc0 --- /dev/null +++ b/tests/e2e/bookmark/save-post.test.ts @@ -0,0 +1,94 @@ +import { authRequest, parseBody, request } from "../setup"; +import { beforeAll, describe, expect, it } from "vitest"; + +const FAKE_UUID = "00000000-0000-0000-0000-000000000000"; + +/** + * E2E tests for the Save Post Bookmark endpoint. + * Validates that an authenticated user can bookmark a post, + * that the operation is idempotent, and that proper errors are returned + * for invalid post IDs or unauthenticated requests. + */ +describe("POST /posts/:id/save - Save Post Bookmark", () => { + const ts = Date.now(); + const user = { + email: `bsp-${ts}@test.com`, + password: "password123", + username: `bsp${ts}`, + }; + + let accessToken = ""; + let postId = ""; + + /** + * Registers a new test user, logs in to obtain an access token, + * and creates a post to use as the bookmark target throughout all tests. + */ + beforeAll(async () => { + await request({ + method: "POST", + url: "/auth/register", + payload: user, + }); + + const loginRes = await request({ + method: "POST", + url: "/auth/login", + payload: { identifier: user.email, password: user.password }, + }); + + accessToken = parseBody<{ data: { accessToken: string } }>(loginRes) + .data.accessToken; + + const postRes = await authRequest(accessToken, { + method: "POST", + url: "/posts", + payload: { content: "E2E test post for bookmark save" }, + }); + + postId = parseBody<{ data: { id: string } }>(postRes).data.id; + }); + + it("should return 201 when saving a post bookmark", async () => { + const response = await authRequest(accessToken, { + method: "POST", + url: `/posts/${postId}/save`, + }); + const body = parseBody<{ meta: { timestamp: string } }>(response); + + expect(response.statusCode).toBe(201); + expect(body.meta).toHaveProperty("timestamp", expect.any(String)); + }); + + it("should return 201 when saving an already-bookmarked post (idempotent)", async () => { + const response = await authRequest(accessToken, { + method: "POST", + url: `/posts/${postId}/save`, + }); + + expect(response.statusCode).toBe(201); + }); + + it("should return 404 when the post does not exist", async () => { + const response = await authRequest(accessToken, { + method: "POST", + url: `/posts/${FAKE_UUID}/save`, + }); + const body = parseBody<{ title: string; detail: string }>(response); + + expect(response.statusCode).toBe(404); + expect(body.title).toBe("NotFoundError"); + expect(body.detail).toBe("Post not found."); + }); + + it("should return 401 when not authenticated", async () => { + const response = await request({ + method: "POST", + url: `/posts/${postId}/save`, + }); + const body = parseBody<{ title: string }>(response); + + expect(response.statusCode).toBe(401); + expect(body.title).toBe("UnauthorizedError"); + }); +});