aboutsummaryrefslogtreecommitdiffstats
path: root/packages/web/server/api
diff options
context:
space:
mode:
authorMohamedBassem <me@mbassem.com>2024-02-28 15:43:32 +0000
committerMohamedBassem <me@mbassem.com>2024-02-28 15:43:32 +0000
commitf67ae821230da9bc92a3c9ff6c550a36d48c0ee9 (patch)
tree75ff0d4e07bd066d3acc7e7cfa6ef126ea0eebc7 /packages/web/server/api
parent0f0e7ca8d134c2cfc02ac62539ad10c811319b38 (diff)
downloadkarakeep-f67ae821230da9bc92a3c9ff6c550a36d48c0ee9.tar.zst
tests: Add tests for the bookmarks routes
Diffstat (limited to 'packages/web/server/api')
-rw-r--r--packages/web/server/api/client.ts4
-rw-r--r--packages/web/server/api/routers/apiKeys.ts5
-rw-r--r--packages/web/server/api/routers/bookmarks.test.ts182
-rw-r--r--packages/web/server/api/routers/bookmarks.ts115
-rw-r--r--packages/web/server/api/routers/tags.ts0
-rw-r--r--packages/web/server/api/routers/users.ts6
-rw-r--r--packages/web/server/api/trpc.ts2
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