aboutsummaryrefslogtreecommitdiffstats
path: root/packages
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
parent0f0e7ca8d134c2cfc02ac62539ad10c811319b38 (diff)
downloadkarakeep-f67ae821230da9bc92a3c9ff6c550a36d48c0ee9.tar.zst
tests: Add tests for the bookmarks routes
Diffstat (limited to 'packages')
-rw-r--r--packages/db/drizzle.ts11
-rw-r--r--packages/web/app/api/trpc/[trpc]/route.ts3
-rw-r--r--packages/web/app/dashboard/bookmarks/components/LinkCard.tsx2
-rw-r--r--packages/web/app/dashboard/bookmarks/components/TagModal.tsx4
-rw-r--r--packages/web/lib/testUtils.ts59
-rw-r--r--packages/web/package.json10
-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
-rw-r--r--packages/web/vitest.config.ts14
14 files changed, 351 insertions, 66 deletions
diff --git a/packages/db/drizzle.ts b/packages/db/drizzle.ts
index def1fc0a..adfe4884 100644
--- a/packages/db/drizzle.ts
+++ b/packages/db/drizzle.ts
@@ -2,6 +2,17 @@ import "dotenv/config";
import { drizzle } from "drizzle-orm/better-sqlite3";
import Database from "better-sqlite3";
import * as schema from "./schema";
+import { migrate } from "drizzle-orm/better-sqlite3/migrator";
+import path from "path";
const sqlite = new Database(process.env.DATABASE_URL);
export const db = drizzle(sqlite, { schema, logger: true });
+
+export function getInMemoryDB(runMigrations: boolean) {
+ const mem = new Database(":memory:");
+ const db = drizzle(mem, { schema, logger: true });
+ if (runMigrations) {
+ migrate(db, { migrationsFolder: path.resolve(__dirname, "./drizzle") });
+ }
+ return db;
+}
diff --git a/packages/web/app/api/trpc/[trpc]/route.ts b/packages/web/app/api/trpc/[trpc]/route.ts
index aea9bc70..7d56cadc 100644
--- a/packages/web/app/api/trpc/[trpc]/route.ts
+++ b/packages/web/app/api/trpc/[trpc]/route.ts
@@ -2,6 +2,7 @@ import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { appRouter } from "@/server/api/routers/_app";
import { createContext } from "@/server/api/client";
import { authenticateApiKey } from "@/server/auth";
+import { db } from "@hoarder/db";
const handler = (req: Request) =>
fetchRequestHandler({
@@ -23,7 +24,7 @@ const handler = (req: Request) =>
const token = authorizationHeader.split(" ")[1];
try {
const user = await authenticateApiKey(token);
- return { user };
+ return { user, db };
} catch (e) {
// Fallthrough to cookie-based auth
}
diff --git a/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx b/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx
index 56e3d243..cd0f128c 100644
--- a/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx
+++ b/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx
@@ -60,7 +60,7 @@ export default function LinkCard({
}) {
const { data: bookmark } = api.bookmarks.getBookmark.useQuery(
{
- id: initialData.id,
+ bookmarkId: initialData.id,
},
{
initialData,
diff --git a/packages/web/app/dashboard/bookmarks/components/TagModal.tsx b/packages/web/app/dashboard/bookmarks/components/TagModal.tsx
index c1618541..b0e391b7 100644
--- a/packages/web/app/dashboard/bookmarks/components/TagModal.tsx
+++ b/packages/web/app/dashboard/bookmarks/components/TagModal.tsx
@@ -130,7 +130,7 @@ export default function TagModal({
toast({
description: "Tags has been updated!",
});
- bookmarkInvalidationFunction({ id: bookmark.id });
+ bookmarkInvalidationFunction({ bookmarkId: bookmark.id });
},
onError: () => {
toast({
@@ -153,7 +153,7 @@ export default function TagModal({
}
for (const t of bookmark.tags) {
if (!tags.has(t.name)) {
- detach.push(t.id);
+ detach.push({ tagId: t.id });
}
}
mutate({
diff --git a/packages/web/lib/testUtils.ts b/packages/web/lib/testUtils.ts
new file mode 100644
index 00000000..ca9a6474
--- /dev/null
+++ b/packages/web/lib/testUtils.ts
@@ -0,0 +1,59 @@
+import { users } from "@hoarder/db/schema";
+import { getInMemoryDB } from "@hoarder/db/drizzle";
+import { appRouter } from "@/server/api/routers/_app";
+import { createCallerFactory } from "@/server/api/trpc";
+import { beforeEach } from "vitest";
+
+export function getTestDB() {
+ return getInMemoryDB(true);
+}
+
+export type TestDB = ReturnType<typeof getTestDB>;
+
+export async function seedUsers(db: TestDB) {
+ return await db
+ .insert(users)
+ .values([
+ {
+ name: "Test User 1",
+ email: "test1@test.com",
+ },
+ {
+ name: "Test User 2",
+ email: "test2@test.com",
+ },
+ ])
+ .returning();
+}
+
+export function getApiCaller(db: TestDB, userId: string) {
+ const createCaller = createCallerFactory(appRouter);
+ return createCaller({
+ user: {
+ id: userId,
+ },
+ db,
+ });
+}
+
+export type APICallerType = ReturnType<typeof getApiCaller>;
+
+export interface CustomTestContext {
+ apiCallers: APICallerType[];
+ db: TestDB;
+}
+
+export async function buildTestContext(): Promise<CustomTestContext> {
+ const db = getTestDB();
+ const users = await seedUsers(db);
+ const callers = users.map((u) => getApiCaller(db, u.id));
+
+ return {
+ apiCallers: callers,
+ db,
+ };
+}
+
+export const defaultBeforeEach = async (context: object) => {
+ Object.assign(context, await buildTestContext());
+};
diff --git a/packages/web/package.json b/packages/web/package.json
index 488af90f..8b135f9c 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -7,12 +7,13 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
- "lint": "next lint"
+ "lint": "next lint",
+ "test": "vitest"
},
"dependencies": {
+ "@auth/drizzle-adapter": "^0.7.0",
"@hoarder/db": "0.1.0",
"@hoarder/shared": "0.1.0",
- "@auth/drizzle-adapter": "^0.7.0",
"@hookform/resolvers": "^3.3.4",
"@next/eslint-plugin-next": "^14.1.0",
"@radix-ui/react-dialog": "^1.0.5",
@@ -54,6 +55,9 @@
"@types/react-dom": "^18",
"autoprefixer": "^10.0.1",
"postcss": "^8",
- "tailwindcss": "^3.3.0"
+ "tailwindcss": "^3.3.0",
+ "ts-node": "^10.9.2",
+ "vite-tsconfig-paths": "^4.3.1",
+ "vitest": "^1.3.1"
}
}
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
diff --git a/packages/web/vitest.config.ts b/packages/web/vitest.config.ts
new file mode 100644
index 00000000..c3d02f71
--- /dev/null
+++ b/packages/web/vitest.config.ts
@@ -0,0 +1,14 @@
+/// <reference types="vitest" />
+
+import { defineConfig } from "vitest/config";
+import tsconfigPaths from "vite-tsconfig-paths";
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [tsconfigPaths()],
+ test: {
+ alias: {
+ "@/*": "./*",
+ },
+ },
+});