diff options
| author | MohamedBassem <me@mbassem.com> | 2024-02-28 15:43:32 +0000 |
|---|---|---|
| committer | MohamedBassem <me@mbassem.com> | 2024-02-28 15:43:32 +0000 |
| commit | f67ae821230da9bc92a3c9ff6c550a36d48c0ee9 (patch) | |
| tree | 75ff0d4e07bd066d3acc7e7cfa6ef126ea0eebc7 /packages/web/server | |
| parent | 0f0e7ca8d134c2cfc02ac62539ad10c811319b38 (diff) | |
| download | karakeep-f67ae821230da9bc92a3c9ff6c550a36d48c0ee9.tar.zst | |
tests: Add tests for the bookmarks routes
Diffstat (limited to 'packages/web/server')
| -rw-r--r-- | packages/web/server/api/client.ts | 4 | ||||
| -rw-r--r-- | packages/web/server/api/routers/apiKeys.ts | 5 | ||||
| -rw-r--r-- | packages/web/server/api/routers/bookmarks.test.ts | 182 | ||||
| -rw-r--r-- | packages/web/server/api/routers/bookmarks.ts | 115 | ||||
| -rw-r--r-- | packages/web/server/api/routers/tags.ts | 0 | ||||
| -rw-r--r-- | packages/web/server/api/routers/users.ts | 6 | ||||
| -rw-r--r-- | packages/web/server/api/trpc.ts | 2 |
7 files changed, 255 insertions, 59 deletions
diff --git a/packages/web/server/api/client.ts b/packages/web/server/api/client.ts index 7b4e6378..130f4f87 100644 --- a/packages/web/server/api/client.ts +++ b/packages/web/server/api/client.ts @@ -1,11 +1,13 @@ import { appRouter } from "./routers/_app"; import { getServerAuthSession } from "@/server/auth"; import { Context, createCallerFactory } from "./trpc"; +import { db } from "@hoarder/db"; -export const createContext = async (): Promise<Context> => { +export const createContext = async (database?: typeof db): Promise<Context> => { const session = await getServerAuthSession(); return { user: session?.user ?? null, + db: database ?? db, }; }; diff --git a/packages/web/server/api/routers/apiKeys.ts b/packages/web/server/api/routers/apiKeys.ts index 0538a34d..9eb36974 100644 --- a/packages/web/server/api/routers/apiKeys.ts +++ b/packages/web/server/api/routers/apiKeys.ts @@ -1,6 +1,5 @@ import { generateApiKey } from "@/server/auth"; import { authedProcedure, router } from "../trpc"; -import { db } from "@hoarder/db"; import { z } from "zod"; import { apiKeys } from "@hoarder/db/schema"; import { eq, and } from "drizzle-orm"; @@ -30,7 +29,7 @@ export const apiKeysAppRouter = router({ }), ) .mutation(async ({ input, ctx }) => { - await db + await ctx.db .delete(apiKeys) .where(and(eq(apiKeys.id, input.id), eq(apiKeys.userId, ctx.user.id))); }), @@ -48,7 +47,7 @@ export const apiKeysAppRouter = router({ }), ) .query(async ({ ctx }) => { - const resp = await db.query.apiKeys.findMany({ + const resp = await ctx.db.query.apiKeys.findMany({ where: eq(apiKeys.userId, ctx.user.id), columns: { id: true, diff --git a/packages/web/server/api/routers/bookmarks.test.ts b/packages/web/server/api/routers/bookmarks.test.ts new file mode 100644 index 00000000..16f82992 --- /dev/null +++ b/packages/web/server/api/routers/bookmarks.test.ts @@ -0,0 +1,182 @@ +import { CustomTestContext, defaultBeforeEach } from "@/lib/testUtils"; +import { expect, describe, test, beforeEach } from "vitest"; + +beforeEach<CustomTestContext>(defaultBeforeEach); + +describe("Bookmark Routes", () => { + test<CustomTestContext>("create bookmark", async ({ apiCallers }) => { + const api = apiCallers[0].bookmarks; + const bookmark = await api.bookmarkLink({ + url: "https://google.com", + type: "link", + }); + + const res = await api.getBookmark({ bookmarkId: bookmark.id }); + expect(res.content.url).toEqual("https://google.com"); + expect(res.favourited).toEqual(false); + expect(res.archived).toEqual(false); + expect(res.content.type).toEqual("link"); + }); + + test<CustomTestContext>("delete bookmark", async ({ apiCallers }) => { + const api = apiCallers[0].bookmarks; + + // Create the bookmark + const bookmark = await api.bookmarkLink({ + url: "https://google.com", + type: "link", + }); + + // It should exist + await api.getBookmark({ bookmarkId: bookmark.id }); + + // Delete it + await api.deleteBookmark({ bookmarkId: bookmark.id }); + + // It shouldn't be there anymore + await expect(() => + api.getBookmark({ bookmarkId: bookmark.id }), + ).rejects.toThrow(/Bookmark not found/); + }); + + test<CustomTestContext>("update bookmark", async ({ apiCallers }) => { + const api = apiCallers[0].bookmarks; + + // Create the bookmark + const bookmark = await api.bookmarkLink({ + url: "https://google.com", + type: "link", + }); + + await api.updateBookmark({ + bookmarkId: bookmark.id, + archived: true, + favourited: true, + }); + + const res = await api.getBookmark({ bookmarkId: bookmark.id }); + expect(res.archived).toBeTruthy(); + expect(res.favourited).toBeTruthy(); + }); + + test<CustomTestContext>("list bookmarks", async ({ apiCallers }) => { + const api = apiCallers[0].bookmarks; + const emptyBookmarks = await api.getBookmarks({}); + expect(emptyBookmarks.bookmarks.length).toEqual(0); + + const bookmark1 = await api.bookmarkLink({ + url: "https://google.com", + type: "link", + }); + + const bookmark2 = await api.bookmarkLink({ + url: "https://google2.com", + type: "link", + }); + + { + const bookmarks = await api.getBookmarks({}); + expect(bookmarks.bookmarks.length).toEqual(2); + } + + // Archive and favourite bookmark1 + await api.updateBookmark({ + bookmarkId: bookmark1.id, + archived: true, + favourited: true, + }); + + { + const bookmarks = await api.getBookmarks({ archived: false }); + expect(bookmarks.bookmarks.length).toEqual(1); + expect(bookmarks.bookmarks[0].id).toEqual(bookmark2.id); + } + + { + const bookmarks = await api.getBookmarks({ favourited: true }); + expect(bookmarks.bookmarks.length).toEqual(1); + expect(bookmarks.bookmarks[0].id).toEqual(bookmark1.id); + } + + { + const bookmarks = await api.getBookmarks({ archived: true }); + expect(bookmarks.bookmarks.length).toEqual(1); + expect(bookmarks.bookmarks[0].id).toEqual(bookmark1.id); + } + + { + const bookmarks = await api.getBookmarks({ ids: [bookmark1.id] }); + expect(bookmarks.bookmarks.length).toEqual(1); + expect(bookmarks.bookmarks[0].id).toEqual(bookmark1.id); + } + }); + + test<CustomTestContext>("update tags", async ({ apiCallers }) => { + const api = apiCallers[0].bookmarks; + let bookmark = await api.bookmarkLink({ + url: "https://google.com", + type: "link", + }); + + await api.updateTags({ + bookmarkId: bookmark.id, + attach: [{ tag: "tag1" }, { tag: "tag2" }], + detach: [], + }); + + bookmark = await api.getBookmark({ bookmarkId: bookmark.id }); + expect(bookmark.tags.map((t) => t.name).sort()).toEqual(["tag1", "tag2"]); + + const tag1Id = bookmark.tags.filter((t) => t.name == "tag1")[0].id; + + await api.updateTags({ + bookmarkId: bookmark.id, + attach: [{ tag: "tag3" }], + detach: [{ tagId: tag1Id }], + }); + + bookmark = await api.getBookmark({ bookmarkId: bookmark.id }); + expect(bookmark.tags.map((t) => t.name).sort()).toEqual(["tag2", "tag3"]); + }); + + test<CustomTestContext>("privacy", async ({ apiCallers }) => { + const user1Bookmark = await apiCallers[0].bookmarks.bookmarkLink({ + type: "link", + url: "https://google.com", + }); + const user2Bookmark = await apiCallers[1].bookmarks.bookmarkLink({ + type: "link", + url: "https://google.com", + }); + + // All interactions with the wrong user should fail + await expect(() => + apiCallers[0].bookmarks.deleteBookmark({ bookmarkId: user2Bookmark.id }), + ).rejects.toThrow(/User is not allowed to access resource/); + await expect(() => + apiCallers[0].bookmarks.getBookmark({ bookmarkId: user2Bookmark.id }), + ).rejects.toThrow(/User is not allowed to access resource/); + await expect(() => + apiCallers[0].bookmarks.updateBookmark({ bookmarkId: user2Bookmark.id }), + ).rejects.toThrow(/User is not allowed to access resource/); + await expect(() => + apiCallers[0].bookmarks.updateTags({ + bookmarkId: user2Bookmark.id, + attach: [], + detach: [], + }), + ).rejects.toThrow(/User is not allowed to access resource/); + + // Get bookmarks should only show the correct one + expect( + (await apiCallers[0].bookmarks.getBookmarks({})).bookmarks.map( + (b) => b.id, + ), + ).toEqual([user1Bookmark.id]); + expect( + (await apiCallers[1].bookmarks.getBookmarks({})).bookmarks.map( + (b) => b.id, + ), + ).toEqual([user2Bookmark.id]); + }); +}); diff --git a/packages/web/server/api/routers/bookmarks.ts b/packages/web/server/api/routers/bookmarks.ts index 3070eac3..64755e4e 100644 --- a/packages/web/server/api/routers/bookmarks.ts +++ b/packages/web/server/api/routers/bookmarks.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { authedProcedure, router } from "../trpc"; +import { Context, authedProcedure, router } from "../trpc"; import { ZBookmark, ZBookmarkContent, @@ -10,7 +10,6 @@ import { zNewBookmarkRequestSchema, zUpdateBookmarksRequestSchema, } from "@/lib/types/api/bookmarks"; -import { db } from "@hoarder/db"; import { bookmarkLinks, bookmarkTags, @@ -19,20 +18,27 @@ import { } from "@hoarder/db/schema"; import { LinkCrawlerQueue } from "@hoarder/shared/queues"; import { TRPCError, experimental_trpcMiddleware } from "@trpc/server"; -import { User } from "next-auth"; import { and, desc, eq, inArray } from "drizzle-orm"; import { ZBookmarkTags } from "@/lib/types/api/tags"; +import { db as DONT_USE_db } from "@hoarder/db"; + const ensureBookmarkOwnership = experimental_trpcMiddleware<{ - ctx: { user: User }; + ctx: Context; input: { bookmarkId: string }; }>().create(async (opts) => { - const bookmark = await db.query.bookmarks.findFirst({ + const bookmark = await opts.ctx.db.query.bookmarks.findFirst({ where: eq(bookmarks.id, opts.input.bookmarkId), columns: { userId: true, }, }); + if (!opts.ctx.user) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "User is not authorized", + }); + } if (!bookmark) { throw new TRPCError({ code: "NOT_FOUND", @@ -50,7 +56,7 @@ const ensureBookmarkOwnership = experimental_trpcMiddleware<{ }); async function dummyDrizzleReturnType() { - const x = await db.query.bookmarks.findFirst({ + const x = await DONT_USE_db.query.bookmarks.findFirst({ with: { tagsOnBookmarks: { with: { @@ -95,37 +101,39 @@ export const bookmarksAppRouter = router({ .mutation(async ({ input, ctx }) => { const { url } = input; - const bookmark = await db.transaction(async (tx): Promise<ZBookmark> => { - const bookmark = ( - await tx - .insert(bookmarks) - .values({ - userId: ctx.user.id, - }) - .returning() - )[0]; + const bookmark = await ctx.db.transaction( + async (tx): Promise<ZBookmark> => { + const bookmark = ( + await tx + .insert(bookmarks) + .values({ + userId: ctx.user.id, + }) + .returning() + )[0]; - const link = ( - await tx - .insert(bookmarkLinks) - .values({ - id: bookmark.id, - url, - }) - .returning() - )[0]; + const link = ( + await tx + .insert(bookmarkLinks) + .values({ + id: bookmark.id, + url, + }) + .returning() + )[0]; - const content: ZBookmarkContent = { - type: "link", - ...link, - }; + const content: ZBookmarkContent = { + type: "link", + ...link, + }; - return { - tags: [] as ZBookmarkTags[], - content, - ...bookmark, - }; - }); + return { + tags: [] as ZBookmarkTags[], + content, + ...bookmark, + }; + }, + ); // Enqueue crawling request await LinkCrawlerQueue.add("crawl", { @@ -140,7 +148,7 @@ export const bookmarksAppRouter = router({ .output(zBareBookmarkSchema) .use(ensureBookmarkOwnership) .mutation(async ({ input, ctx }) => { - const res = await db + const res = await ctx.db .update(bookmarks) .set({ archived: input.archived, @@ -166,7 +174,7 @@ export const bookmarksAppRouter = router({ .input(z.object({ bookmarkId: z.string() })) .use(ensureBookmarkOwnership) .mutation(async ({ input, ctx }) => { - await db + await ctx.db .delete(bookmarks) .where( and( @@ -186,15 +194,16 @@ export const bookmarksAppRouter = router({ getBookmark: authedProcedure .input( z.object({ - id: z.string(), + bookmarkId: z.string(), }), ) .output(zBookmarkSchema) + .use(ensureBookmarkOwnership) .query(async ({ input, ctx }) => { - const bookmark = await db.query.bookmarks.findFirst({ + const bookmark = await ctx.db.query.bookmarks.findFirst({ where: and( eq(bookmarks.userId, ctx.user.id), - eq(bookmarks.id, input.id), + eq(bookmarks.id, input.bookmarkId), ), with: { tagsOnBookmarks: { @@ -218,7 +227,7 @@ export const bookmarksAppRouter = router({ .input(zGetBookmarksRequestSchema) .output(zGetBookmarksResponseSchema) .query(async ({ input, ctx }) => { - const results = await db.query.bookmarks.findMany({ + const results = await ctx.db.query.bookmarks.findMany({ where: and( eq(bookmarks.userId, ctx.user.id), input.archived !== undefined @@ -253,22 +262,24 @@ export const bookmarksAppRouter = router({ tag: z.string(), }), ), - detach: z.array(z.string()), + // Detach by tag ids + detach: z.array(z.object({ tagId: z.string() })), }), ) .use(ensureBookmarkOwnership) .mutation(async ({ input, ctx }) => { - await db.transaction(async (tx) => { + await ctx.db.transaction(async (tx) => { // Detaches if (input.detach.length > 0) { - await db - .delete(tagsOnBookmarks) - .where( - and( - eq(tagsOnBookmarks.bookmarkId, input.bookmarkId), - inArray(tagsOnBookmarks.tagId, input.detach), + await ctx.db.delete(tagsOnBookmarks).where( + and( + eq(tagsOnBookmarks.bookmarkId, input.bookmarkId), + inArray( + tagsOnBookmarks.tagId, + input.detach.map((t) => t.tagId), ), - ); + ), + ); } if (input.attach.length == 0) { @@ -284,7 +295,7 @@ export const bookmarksAppRouter = router({ })); if (toBeCreatedTags.length > 0) { - await db + await ctx.db .insert(bookmarkTags) .values(toBeCreatedTags) .onConflictDoNothing() @@ -292,7 +303,7 @@ export const bookmarksAppRouter = router({ } const allIds = ( - await db.query.bookmarkTags.findMany({ + await ctx.db.query.bookmarkTags.findMany({ where: and( eq(bookmarkTags.userId, ctx.user.id), inArray( @@ -306,7 +317,7 @@ export const bookmarksAppRouter = router({ }) ).map((t) => t.id); - await db + await ctx.db .insert(tagsOnBookmarks) .values( allIds.map((i) => ({ diff --git a/packages/web/server/api/routers/tags.ts b/packages/web/server/api/routers/tags.ts deleted file mode 100644 index e69de29b..00000000 --- a/packages/web/server/api/routers/tags.ts +++ /dev/null diff --git a/packages/web/server/api/routers/users.ts b/packages/web/server/api/routers/users.ts index 032385ac..3078a42a 100644 --- a/packages/web/server/api/routers/users.ts +++ b/packages/web/server/api/routers/users.ts @@ -1,6 +1,6 @@ import { zSignUpSchema } from "@/lib/types/api/users"; import { publicProcedure, router } from "../trpc"; -import { SqliteError, db } from "@hoarder/db"; +import { SqliteError } from "@hoarder/db"; import { z } from "zod"; import { hashPassword } from "@/server/auth"; import { TRPCError } from "@trpc/server"; @@ -15,9 +15,9 @@ export const usersAppRouter = router({ email: z.string(), }), ) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { try { - const result = await db + const result = await ctx.db .insert(users) .values({ name: input.name, diff --git a/packages/web/server/api/trpc.ts b/packages/web/server/api/trpc.ts index 7df98372..93fc961a 100644 --- a/packages/web/server/api/trpc.ts +++ b/packages/web/server/api/trpc.ts @@ -1,3 +1,4 @@ +import { db } from "@hoarder/db"; import serverConfig from "@hoarder/shared/config"; import { TRPCError, initTRPC } from "@trpc/server"; import { User } from "next-auth"; @@ -5,6 +6,7 @@ import superjson from "superjson"; export type Context = { user: User | null; + db: typeof db; }; // Avoid exporting the entire t-object |
