import { eq } from "drizzle-orm"; import { assert, beforeEach, describe, expect, test } from "vitest"; import { bookmarkLinks, users } from "@karakeep/db/schema"; import { BookmarkTypes } from "@karakeep/shared/types/bookmarks"; import type { CustomTestContext } from "../testUtils"; import { buildTestContext, getApiCaller } from "../testUtils"; beforeEach(async (context) => { const testContext = await buildTestContext(true); Object.assign(context, testContext); }); describe("Admin Routes", () => { describe("getBookmarkDebugInfo", () => { test("admin can access bookmark debug info for link bookmark", async ({ apiCallers, db, }) => { // Create an admin user const adminUser = await db .insert(users) .values({ name: "Admin User", email: "admin@test.com", role: "admin", }) .returning(); const adminApi = getApiCaller( db, adminUser[0].id, adminUser[0].email, "admin", ); // Create a bookmark as a regular user const bookmark = await apiCallers[0].bookmarks.createBookmark({ url: "https://example.com", type: BookmarkTypes.LINK, }); // Update the bookmark link with some metadata await db .update(bookmarkLinks) .set({ crawlStatus: "success", crawlStatusCode: 200, crawledAt: new Date(), htmlContent: "Test content", title: "Test Title", description: "Test Description", }) .where(eq(bookmarkLinks.id, bookmark.id)); // Admin should be able to access debug info const debugInfo = await adminApi.admin.getBookmarkDebugInfo({ bookmarkId: bookmark.id, }); expect(debugInfo.id).toEqual(bookmark.id); expect(debugInfo.type).toEqual(BookmarkTypes.LINK); expect(debugInfo.linkInfo).toBeDefined(); assert(debugInfo.linkInfo); expect(debugInfo.linkInfo.url).toEqual("https://example.com"); expect(debugInfo.linkInfo.crawlStatus).toEqual("success"); expect(debugInfo.linkInfo.crawlStatusCode).toEqual(200); expect(debugInfo.linkInfo.hasHtmlContent).toEqual(true); expect(debugInfo.linkInfo.htmlContentPreview).toBeDefined(); expect(debugInfo.linkInfo.htmlContentPreview).toContain("Test content"); }); test("admin can access bookmark debug info for text bookmark", async ({ apiCallers, db, }) => { // Create an admin user const adminUser = await db .insert(users) .values({ name: "Admin User", email: "admin@test.com", role: "admin", }) .returning(); const adminApi = getApiCaller( db, adminUser[0].id, adminUser[0].email, "admin", ); // Create a text bookmark const bookmark = await apiCallers[0].bookmarks.createBookmark({ text: "This is a test text bookmark", type: BookmarkTypes.TEXT, }); // Admin should be able to access debug info const debugInfo = await adminApi.admin.getBookmarkDebugInfo({ bookmarkId: bookmark.id, }); expect(debugInfo.id).toEqual(bookmark.id); expect(debugInfo.type).toEqual(BookmarkTypes.TEXT); expect(debugInfo.textInfo).toBeDefined(); assert(debugInfo.textInfo); expect(debugInfo.textInfo.hasText).toEqual(true); }); test("admin can see bookmark tags in debug info", async ({ apiCallers, db, }) => { // Create an admin user const adminUser = await db .insert(users) .values({ name: "Admin User", email: "admin@test.com", role: "admin", }) .returning(); const adminApi = getApiCaller( db, adminUser[0].id, adminUser[0].email, "admin", ); // Create a bookmark with tags const bookmark = await apiCallers[0].bookmarks.createBookmark({ url: "https://example.com", type: BookmarkTypes.LINK, }); // Add tags to the bookmark await apiCallers[0].bookmarks.updateTags({ bookmarkId: bookmark.id, attach: [{ tagName: "test-tag-1" }, { tagName: "test-tag-2" }], detach: [], }); // Admin should be able to see tags in debug info const debugInfo = await adminApi.admin.getBookmarkDebugInfo({ bookmarkId: bookmark.id, }); expect(debugInfo.tags).toHaveLength(2); expect(debugInfo.tags.map((t) => t.name).sort()).toEqual([ "test-tag-1", "test-tag-2", ]); expect(debugInfo.tags[0].attachedBy).toEqual("human"); }); test("non-admin user cannot access bookmark debug info", async ({ apiCallers, }) => { // Create a bookmark const bookmark = await apiCallers[0].bookmarks.createBookmark({ url: "https://example.com", type: BookmarkTypes.LINK, }); // Non-admin user should not be able to access debug info // The admin procedure itself will throw FORBIDDEN await expect(() => apiCallers[0].admin.getBookmarkDebugInfo({ bookmarkId: bookmark.id }), ).rejects.toThrow(/FORBIDDEN/); }); test("debug info includes asset URLs with signed tokens", async ({ apiCallers, db, }) => { // Create an admin user const adminUser = await db .insert(users) .values({ name: "Admin User", email: "admin@test.com", role: "admin", }) .returning(); const adminApi = getApiCaller( db, adminUser[0].id, adminUser[0].email, "admin", ); // Create a bookmark const bookmark = await apiCallers[0].bookmarks.createBookmark({ url: "https://example.com", type: BookmarkTypes.LINK, }); // Get debug info const debugInfo = await adminApi.admin.getBookmarkDebugInfo({ bookmarkId: bookmark.id, }); // Check that assets array is present expect(debugInfo.assets).toBeDefined(); expect(Array.isArray(debugInfo.assets)).toBe(true); // If there are assets, check that they have signed URLs if (debugInfo.assets.length > 0) { const asset = debugInfo.assets[0]; expect(asset.url).toBeDefined(); expect(asset.url).toContain("/api/public/assets/"); expect(asset.url).toContain("token="); } }); test("debug info truncates HTML content preview", async ({ apiCallers, db, }) => { // Create an admin user const adminUser = await db .insert(users) .values({ name: "Admin User", email: "admin@test.com", role: "admin", }) .returning(); const adminApi = getApiCaller( db, adminUser[0].id, adminUser[0].email, "admin", ); // Create a bookmark const bookmark = await apiCallers[0].bookmarks.createBookmark({ url: "https://example.com", type: BookmarkTypes.LINK, }); // Create a large HTML content const largeContent = "" + "x".repeat(2000) + ""; await db .update(bookmarkLinks) .set({ htmlContent: largeContent, }) .where(eq(bookmarkLinks.id, bookmark.id)); // Get debug info const debugInfo = await adminApi.admin.getBookmarkDebugInfo({ bookmarkId: bookmark.id, }); // Check that HTML preview is truncated to 1000 characters assert(debugInfo.linkInfo); expect(debugInfo.linkInfo.htmlContentPreview).toBeDefined(); expect(debugInfo.linkInfo.htmlContentPreview!.length).toBeLessThanOrEqual( 1000, ); }); }); });