aboutsummaryrefslogtreecommitdiffstats
path: root/packages/e2e_tests/tests/api
diff options
context:
space:
mode:
Diffstat (limited to 'packages/e2e_tests/tests/api')
-rw-r--r--packages/e2e_tests/tests/api/assets.test.ts4
-rw-r--r--packages/e2e_tests/tests/api/bookmarks.test.ts274
-rw-r--r--packages/e2e_tests/tests/api/public.test.ts322
-rw-r--r--packages/e2e_tests/tests/api/rss.test.ts155
-rw-r--r--packages/e2e_tests/tests/api/tags.test.ts27
5 files changed, 724 insertions, 58 deletions
diff --git a/packages/e2e_tests/tests/api/assets.test.ts b/packages/e2e_tests/tests/api/assets.test.ts
index 5c294929..78a5c7fe 100644
--- a/packages/e2e_tests/tests/api/assets.test.ts
+++ b/packages/e2e_tests/tests/api/assets.test.ts
@@ -39,7 +39,7 @@ describe("Assets API", () => {
// Retrieve the asset
const resp = await fetch(
- `http://localhost:${port}/api/assets/${uploadResponse.assetId}`,
+ `http://localhost:${port}/api/v1/assets/${uploadResponse.assetId}`,
{
headers: {
authorization: `Bearer ${apiKey}`,
@@ -123,7 +123,7 @@ describe("Assets API", () => {
// Verify asset is deleted
const assetResponse = await fetch(
- `http://localhost:${port}/api/assets/${uploadResponse.assetId}`,
+ `http://localhost:${port}/api/v1/assets/${uploadResponse.assetId}`,
{
headers: {
authorization: `Bearer ${apiKey}`,
diff --git a/packages/e2e_tests/tests/api/bookmarks.test.ts b/packages/e2e_tests/tests/api/bookmarks.test.ts
index 6c56f689..d40c1add 100644
--- a/packages/e2e_tests/tests/api/bookmarks.test.ts
+++ b/packages/e2e_tests/tests/api/bookmarks.test.ts
@@ -397,54 +397,258 @@ describe("Bookmarks API", () => {
expect(finalPage!.nextCursor).toBeNull();
});
- it("should support precrawling via singlefile", async () => {
- const file = new File(["<html>HELLO WORLD</html>"], "test.html", {
- type: "text/html",
- });
+ describe("singlefile", () => {
+ async function uploadSinglefileAsset(ifexists?: string) {
+ const file = new File(["<html>HELLO WORLD</html>"], "test.html", {
+ type: "text/html",
+ });
- const formData = new FormData();
- formData.append("url", "https://example.com");
- formData.append("file", file);
+ const formData = new FormData();
+ formData.append("url", "https://example.com");
+ formData.append("file", file);
- // OpenAPI typescript doesn't support multipart/form-data
- // Upload the singlefile archive
- const response = await fetch(
- `http://localhost:${port}/api/v1/bookmarks/singlefile`,
- {
+ const url = new URL(
+ `http://localhost:${port}/api/v1/bookmarks/singlefile`,
+ );
+ if (ifexists) {
+ url.searchParams.append("ifexists", ifexists);
+ }
+
+ const response = await fetch(url.toString(), {
method: "POST",
headers: {
authorization: `Bearer ${apiKey}`,
},
body: formData,
- },
- );
+ });
- if (!response.ok) {
- throw new Error(`Failed to upload asset: ${response.statusText}`);
+ if (!response.ok) {
+ return [null, response] as const;
+ }
+
+ const data = (await response.json()) as { id: string };
+ return [data, response] as const;
}
- expect(response.status).toBe(201);
+ it("should support precrawling via singlefile with ifexists=skip", async () => {
+ // First upload: create a bookmark
+ const [data, response] = await uploadSinglefileAsset();
+ expect(response?.status).toBe(201);
+ const bookmarkId = data?.id;
+ if (!bookmarkId) throw new Error("Bookmark ID not found");
- const { id: bookmarkId } = (await response.json()) as {
- id: string;
- };
+ // Get the bookmark and record the precrawled asset id
+ const { data: bookmark, response: getResponse1 } = await client.GET(
+ "/bookmarks/{bookmarkId}",
+ {
+ params: { path: { bookmarkId } },
+ },
+ );
+ expect(getResponse1.status).toBe(200);
+ const assetIds = bookmark!.assets
+ .filter((a) => a.assetType === "precrawledArchive")
+ .map((a) => a.id);
+ expect(assetIds.length).toBe(1);
+ const firstAssetId = assetIds[0];
- // Get the created bookmark
- const { data: retrievedBookmark, response: getResponse } = await client.GET(
- "/bookmarks/{bookmarkId}",
- {
- params: {
- path: {
- bookmarkId: bookmarkId,
- },
+ // Second upload with skip
+ const [data2, response2] = await uploadSinglefileAsset("skip");
+ expect(response2?.status).toBe(200);
+ expect(data2?.id).toBe(bookmarkId);
+
+ // Get the bookmark again
+ const { data: bookmark2, response: getResponse2 } = await client.GET(
+ "/bookmarks/{bookmarkId}",
+ {
+ params: { path: { bookmarkId } },
},
- },
- );
+ );
+ expect(getResponse2.status).toBe(200);
+ const assetIds2 = bookmark2!.assets
+ .filter((a) => a.assetType === "precrawledArchive")
+ .map((a) => a.id);
+ expect(assetIds2).toEqual([firstAssetId]); // same asset
+ });
- expect(getResponse.status).toBe(200);
- assert(retrievedBookmark!.content.type === "link");
- expect(retrievedBookmark!.assets.map((a) => a.assetType)).toContain(
- "precrawledArchive",
- );
+ it("should support precrawling via singlefile with ifexists=overwrite", async () => {
+ // First upload
+ const [data, response] = await uploadSinglefileAsset("overwrite");
+ expect(response?.status).toBe(201);
+ const bookmarkId = data?.id;
+ if (!bookmarkId) throw new Error("Bookmark ID not found");
+
+ // Record the asset
+ const { data: bookmark, response: getResponse1 } = await client.GET(
+ "/bookmarks/{bookmarkId}",
+ {
+ params: { path: { bookmarkId } },
+ },
+ );
+ expect(getResponse1.status).toBe(200);
+ const firstAssetId = bookmark!.assets.find(
+ (a) => a.assetType === "precrawledArchive",
+ )?.id;
+ expect(firstAssetId).toBeDefined();
+
+ // Second upload with overwrite
+ const [data2, response2] = await uploadSinglefileAsset("overwrite");
+ expect(response2?.status).toBe(200);
+ expect(data2?.id).toBe(bookmarkId);
+
+ // Get the bookmark again
+ const { data: bookmark2, response: getResponse2 } = await client.GET(
+ "/bookmarks/{bookmarkId}",
+ {
+ params: { path: { bookmarkId } },
+ },
+ );
+ expect(getResponse2.status).toBe(200);
+ const secondAssetId = bookmark2!.assets.find(
+ (a) => a.assetType === "precrawledArchive",
+ )?.id;
+ expect(secondAssetId).toBeDefined();
+ expect(secondAssetId).not.toBe(firstAssetId);
+ // There should be only one precrawledArchive asset
+ const precrawledAssets = bookmark2!.assets.filter(
+ (a) => a.assetType === "precrawledArchive",
+ );
+ expect(precrawledAssets.length).toBe(1);
+ });
+
+ it("should support precrawling via singlefile with ifexists=overwrite-recrawl", async () => {
+ // First upload
+ const [data, response] = await uploadSinglefileAsset("overwrite-recrawl");
+ expect(response?.status).toBe(201);
+ const bookmarkId = data?.id;
+ if (!bookmarkId) throw new Error("Bookmark ID not found");
+
+ // Record the asset
+ const { data: bookmark, response: getResponse1 } = await client.GET(
+ "/bookmarks/{bookmarkId}",
+ {
+ params: { path: { bookmarkId } },
+ },
+ );
+ expect(getResponse1.status).toBe(200);
+ const firstAssetId = bookmark!.assets.find(
+ (a) => a.assetType === "precrawledArchive",
+ )?.id;
+ expect(firstAssetId).toBeDefined();
+
+ // Second upload with overwrite-recrawl
+ const [data2, response2] =
+ await uploadSinglefileAsset("overwrite-recrawl");
+ expect(response2?.status).toBe(200);
+ expect(data2?.id).toBe(bookmarkId);
+
+ // Get the bookmark again
+ const { data: bookmark2, response: getResponse2 } = await client.GET(
+ "/bookmarks/{bookmarkId}",
+ {
+ params: { path: { bookmarkId } },
+ },
+ );
+ expect(getResponse2.status).toBe(200);
+ const secondAssetId = bookmark2!.assets.find(
+ (a) => a.assetType === "precrawledArchive",
+ )?.id;
+ expect(secondAssetId).toBeDefined();
+ expect(secondAssetId).not.toBe(firstAssetId);
+ // There should be only one precrawledArchive asset
+ const precrawledAssets = bookmark2!.assets.filter(
+ (a) => a.assetType === "precrawledArchive",
+ );
+ expect(precrawledAssets.length).toBe(1);
+ });
+
+ it("should support precrawling via singlefile with ifexists=append", async () => {
+ // First upload
+ const [data, response] = await uploadSinglefileAsset("append");
+ expect(response?.status).toBe(201);
+ const bookmarkId = data?.id;
+ if (!bookmarkId) throw new Error("Bookmark ID not found");
+
+ // Record the first asset
+ const { data: bookmark, response: getResponse1 } = await client.GET(
+ "/bookmarks/{bookmarkId}",
+ {
+ params: { path: { bookmarkId } },
+ },
+ );
+ expect(getResponse1.status).toBe(200);
+ const firstAssetId = bookmark!.assets.find(
+ (a) => a.assetType === "precrawledArchive",
+ )?.id;
+ expect(firstAssetId).toBeDefined();
+
+ // Second upload with append
+ const [data2, response2] = await uploadSinglefileAsset("append");
+ expect(response2?.status).toBe(200);
+ expect(data2?.id).toBe(bookmarkId);
+
+ // Get the bookmark again
+ const { data: bookmark2, response: getResponse2 } = await client.GET(
+ "/bookmarks/{bookmarkId}",
+ {
+ params: { path: { bookmarkId } },
+ },
+ );
+ expect(getResponse2.status).toBe(200);
+ const precrawledAssets = bookmark2!.assets.filter(
+ (a) => a.assetType === "precrawledArchive",
+ );
+ expect(precrawledAssets.length).toBe(2);
+ expect(precrawledAssets.map((a) => a.id)).toContain(firstAssetId);
+ // The second asset id should be different
+ const secondAssetId = precrawledAssets.find(
+ (asset) => asset.id !== firstAssetId,
+ )?.id;
+ expect(secondAssetId).toBeDefined();
+ });
+
+ it("should support precrawling via singlefile with ifexists=append-recrawl", async () => {
+ // First upload
+ const [data, response] = await uploadSinglefileAsset("append-recrawl");
+ expect(response?.status).toBe(201);
+ const bookmarkId = data?.id;
+ if (!bookmarkId) throw new Error("Bookmark ID not found");
+
+ // Record the first asset
+ const { data: bookmark, response: getResponse1 } = await client.GET(
+ "/bookmarks/{bookmarkId}",
+ {
+ params: { path: { bookmarkId } },
+ },
+ );
+ expect(getResponse1.status).toBe(200);
+ const firstAssetId = bookmark!.assets.find(
+ (a) => a.assetType === "precrawledArchive",
+ )?.id;
+ expect(firstAssetId).toBeDefined();
+
+ // Second upload with append-recrawl
+ const [data2, response2] = await uploadSinglefileAsset("append-recrawl");
+ expect(response2?.status).toBe(200);
+ expect(data2?.id).toBe(bookmarkId);
+
+ // Get the bookmark again
+ const { data: bookmark2, response: getResponse2 } = await client.GET(
+ "/bookmarks/{bookmarkId}",
+ {
+ params: { path: { bookmarkId } },
+ },
+ );
+ expect(getResponse2.status).toBe(200);
+ const precrawledAssets = bookmark2!.assets.filter(
+ (a) => a.assetType === "precrawledArchive",
+ );
+ expect(precrawledAssets.length).toBe(2);
+ expect(precrawledAssets.map((a) => a.id)).toContain(firstAssetId);
+ // The second asset id should be different
+ const secondAssetId = precrawledAssets.find(
+ (asset) => asset.id !== firstAssetId,
+ )?.id;
+ expect(secondAssetId).toBeDefined();
+ });
});
});
diff --git a/packages/e2e_tests/tests/api/public.test.ts b/packages/e2e_tests/tests/api/public.test.ts
new file mode 100644
index 00000000..54ef79ea
--- /dev/null
+++ b/packages/e2e_tests/tests/api/public.test.ts
@@ -0,0 +1,322 @@
+import { assert, beforeEach, describe, expect, inject, it } from "vitest";
+import { z } from "zod";
+
+import { createSignedToken } from "../../../shared/signedTokens";
+import { zAssetSignedTokenSchema } from "../../../shared/types/assets";
+import { BookmarkTypes } from "../../../shared/types/bookmarks";
+import { createTestUser, uploadTestAsset } from "../../utils/api";
+import { waitUntil } from "../../utils/general";
+import { getTrpcClient } from "../../utils/trpc";
+
+describe("Public API", () => {
+ const port = inject("karakeepPort");
+
+ if (!port) {
+ throw new Error("Missing required environment variables");
+ }
+
+ let apiKey: string; // For the primary test user
+
+ async function seedDatabase(currentApiKey: string) {
+ const trpcClient = getTrpcClient(currentApiKey);
+
+ // Create two lists
+ const publicList = await trpcClient.lists.create.mutate({
+ name: "Public List",
+ icon: "🚀",
+ type: "manual",
+ });
+
+ await trpcClient.lists.edit.mutate({
+ listId: publicList.id,
+ public: true,
+ });
+
+ // Create two bookmarks
+ const createBookmark1 = await trpcClient.bookmarks.createBookmark.mutate({
+ title: "Test Bookmark #1",
+ url: "http://nginx:80/hello.html",
+ type: BookmarkTypes.LINK,
+ });
+
+ // Create a second bookmark with an asset
+ const file = new File(["test content"], "test.pdf", {
+ type: "application/pdf",
+ });
+
+ const uploadResponse = await uploadTestAsset(currentApiKey, port, file);
+ const createBookmark2 = await trpcClient.bookmarks.createBookmark.mutate({
+ title: "Test Bookmark #2",
+ type: BookmarkTypes.ASSET,
+ assetType: "pdf",
+ assetId: uploadResponse.assetId,
+ });
+
+ await trpcClient.lists.addToList.mutate({
+ listId: publicList.id,
+ bookmarkId: createBookmark1.id,
+ });
+ await trpcClient.lists.addToList.mutate({
+ listId: publicList.id,
+ bookmarkId: createBookmark2.id,
+ });
+
+ return { publicList, createBookmark1, createBookmark2 };
+ }
+
+ beforeEach(async () => {
+ apiKey = await createTestUser();
+ });
+
+ it("should get public bookmarks", async () => {
+ const { publicList } = await seedDatabase(apiKey);
+ const trpcClient = getTrpcClient(apiKey);
+
+ const res = await trpcClient.publicBookmarks.getPublicBookmarksInList.query(
+ {
+ listId: publicList.id,
+ },
+ );
+
+ expect(res.bookmarks.length).toBe(2);
+ });
+
+ it("should be able to access the assets of the public bookmarks", async () => {
+ const { publicList, createBookmark1, createBookmark2 } =
+ await seedDatabase(apiKey);
+
+ const trpcClient = getTrpcClient(apiKey);
+ // Wait for link bookmark to be crawled and have a banner image (screenshot)
+ await waitUntil(
+ async () => {
+ const res = await trpcClient.bookmarks.getBookmark.query({
+ bookmarkId: createBookmark1.id,
+ });
+ assert(res.content.type === BookmarkTypes.LINK);
+ // Check for screenshotAssetId as bannerImageUrl might be derived from it or original imageUrl
+ return !!res.content.screenshotAssetId || !!res.content.imageUrl;
+ },
+ "Bookmark is crawled and has banner info",
+ 20000, // Increased timeout as crawling can take time
+ );
+
+ const res = await trpcClient.publicBookmarks.getPublicBookmarksInList.query(
+ {
+ listId: publicList.id,
+ },
+ );
+
+ const b1Resp = res.bookmarks.find((b) => b.id === createBookmark1.id);
+ expect(b1Resp).toBeDefined();
+ const b2Resp = res.bookmarks.find((b) => b.id === createBookmark2.id);
+ expect(b2Resp).toBeDefined();
+
+ assert(b1Resp!.content.type === BookmarkTypes.LINK);
+ assert(b2Resp!.content.type === BookmarkTypes.ASSET);
+
+ {
+ // Banner image fetch for link bookmark
+ assert(
+ b1Resp!.bannerImageUrl,
+ "Link bookmark should have a bannerImageUrl",
+ );
+ const assetFetch = await fetch(b1Resp!.bannerImageUrl);
+ expect(assetFetch.status).toBe(200);
+ }
+
+ {
+ // Actual asset fetch for asset bookmark
+ assert(
+ b2Resp!.content.assetUrl,
+ "Asset bookmark should have an assetUrl",
+ );
+ const assetFetch = await fetch(b2Resp!.content.assetUrl);
+ expect(assetFetch.status).toBe(200);
+ }
+ });
+
+ it("Accessing non public list should fail", async () => {
+ const trpcClient = getTrpcClient(apiKey);
+ const nonPublicList = await trpcClient.lists.create.mutate({
+ name: "Non Public List",
+ icon: "🚀",
+ type: "manual",
+ });
+
+ await expect(
+ trpcClient.publicBookmarks.getPublicBookmarksInList.query({
+ listId: nonPublicList.id,
+ }),
+ ).rejects.toThrow(/List not found/);
+ });
+
+ describe("Public asset token validation", () => {
+ let userId: string;
+ let assetId: string; // Asset belonging to the primary user (userId)
+
+ beforeEach(async () => {
+ const trpcClient = getTrpcClient(apiKey);
+ const whoami = await trpcClient.users.whoami.query();
+ userId = whoami.id;
+ const assetUpload = await uploadTestAsset(
+ apiKey,
+ port,
+ new File(["test content for token validation"], "token_test.pdf", {
+ type: "application/pdf",
+ }),
+ );
+ assetId = assetUpload.assetId;
+ });
+
+ it("should succeed with a valid token", async () => {
+ const token = createSignedToken(
+ {
+ assetId,
+ userId,
+ } as z.infer<typeof zAssetSignedTokenSchema>,
+ Date.now() + 60000, // Expires in 60 seconds
+ );
+ const res = await fetch(
+ `http://localhost:${port}/api/public/assets/${assetId}?token=${token}`,
+ );
+ expect(res.status).toBe(200);
+ expect((await res.blob()).type).toBe("application/pdf");
+ });
+
+ it("should fail without a token", async () => {
+ const res = await fetch(
+ `http://localhost:${port}/api/public/assets/${assetId}`,
+ );
+ expect(res.status).toBe(400); // Bad Request due to missing token query param
+ });
+
+ it("should fail with a malformed token string (e.g., not base64)", async () => {
+ const res = await fetch(
+ `http://localhost:${port}/api/public/assets/${assetId}?token=thisIsNotValidBase64!@#`,
+ );
+ expect(res.status).toBe(403);
+ expect(await res.json()).toEqual(
+ expect.objectContaining({ error: "Invalid or expired token" }),
+ );
+ });
+
+ it("should fail with a token having a structurally invalid inner payload", async () => {
+ // Payload that doesn't conform to zAssetSignedTokenSchema (e.g. misspelled key)
+ const malformedInnerPayload = {
+ asset_id_mispelled: assetId,
+ userId: userId,
+ };
+ const token = createSignedToken(
+ malformedInnerPayload,
+ Date.now() + 60000,
+ );
+ const res = await fetch(
+ `http://localhost:${port}/api/public/assets/${assetId}?token=${token}`,
+ );
+ expect(res.status).toBe(403);
+ expect(await res.json()).toEqual(
+ expect.objectContaining({ error: "Invalid or expired token" }),
+ );
+ });
+
+ it("should fail after token expiry", async () => {
+ const token = createSignedToken(
+ {
+ assetId,
+ userId,
+ } as z.infer<typeof zAssetSignedTokenSchema>,
+ Date.now() + 1000, // Expires in 1 second
+ );
+
+ // Wait for more than 1 second to ensure expiry
+ await new Promise((resolve) => setTimeout(resolve, 2000));
+
+ const res = await fetch(
+ `http://localhost:${port}/api/public/assets/${assetId}?token=${token}`,
+ );
+ expect(res.status).toBe(403);
+ expect(await res.json()).toEqual(
+ expect.objectContaining({ error: "Invalid or expired token" }),
+ );
+ });
+
+ it("should fail when using a valid token for a different asset", async () => {
+ const anotherAssetUpload = await uploadTestAsset(
+ apiKey, // Same user
+ port,
+ new File(["other content"], "other_asset.pdf", {
+ type: "application/pdf",
+ }),
+ );
+ const anotherAssetId = anotherAssetUpload.assetId;
+
+ // Token is valid for 'anotherAssetId'
+ const tokenForAnotherAsset = createSignedToken(
+ {
+ assetId: anotherAssetId,
+ userId,
+ } as z.infer<typeof zAssetSignedTokenSchema>,
+ Date.now() + 60000,
+ );
+
+ // Attempt to use this token to access the original 'assetId'
+ const res = await fetch(
+ `http://localhost:${port}/api/public/assets/${assetId}?token=${tokenForAnotherAsset}`,
+ );
+ expect(res.status).toBe(403);
+ expect(await res.json()).toEqual(
+ expect.objectContaining({ error: "Invalid or expired token" }),
+ );
+ });
+
+ it("should fail if token's userId does not own the requested assetId (expect 404)", async () => {
+ // User1 (primary, `apiKey`, `userId`) owns `assetId` (from beforeEach)
+
+ // Create User2 - ensure unique email for user creation
+ const apiKeyUser2 = await createTestUser();
+ const trpcClientUser2 = getTrpcClient(apiKeyUser2);
+ const whoamiUser2 = await trpcClientUser2.users.whoami.query();
+ const userIdUser2 = whoamiUser2.id;
+
+ // Generate a token where the payload claims assetId is being accessed by userIdUser2,
+ // but assetId actually belongs to the original userId.
+ const tokenForUser2AttemptingAsset1 = createSignedToken(
+ {
+ assetId: assetId, // assetId belongs to user1 (userId)
+ userId: userIdUser2, // token claims user2 is accessing it
+ } as z.infer<typeof zAssetSignedTokenSchema>,
+ Date.now() + 60000,
+ );
+
+ // User2 attempts to access assetId (owned by User1) using a token that has User2's ID in its payload.
+ // The API route will use userIdUser2 from the token to query the DB for assetId.
+ // Since assetId is not owned by userIdUser2, the DB query will find nothing.
+ const res = await fetch(
+ `http://localhost:${port}/api/public/assets/${assetId}?token=${tokenForUser2AttemptingAsset1}`,
+ );
+ expect(res.status).toBe(404);
+ expect(await res.json()).toEqual(
+ expect.objectContaining({ error: "Asset not found" }),
+ );
+ });
+
+ it("should fail for a token referencing a non-existent assetId (expect 404)", async () => {
+ const nonExistentAssetId = `nonexistent-asset-${Date.now()}`;
+ const token = createSignedToken(
+ {
+ assetId: nonExistentAssetId,
+ userId, // Valid userId from the primary user
+ } as z.infer<typeof zAssetSignedTokenSchema>,
+ Date.now() + 60000,
+ );
+
+ const res = await fetch(
+ `http://localhost:${port}/api/public/assets/${nonExistentAssetId}?token=${token}`,
+ );
+ expect(res.status).toBe(404);
+ expect(await res.json()).toEqual(
+ expect.objectContaining({ error: "Asset not found" }),
+ );
+ });
+ });
+});
diff --git a/packages/e2e_tests/tests/api/rss.test.ts b/packages/e2e_tests/tests/api/rss.test.ts
new file mode 100644
index 00000000..8a7447e6
--- /dev/null
+++ b/packages/e2e_tests/tests/api/rss.test.ts
@@ -0,0 +1,155 @@
+import { beforeEach, describe, expect, inject, it } from "vitest";
+
+import { BookmarkTypes } from "../../../shared/types/bookmarks";
+import { createTestUser } from "../../utils/api";
+import { getTrpcClient } from "../../utils/trpc";
+
+describe("RSS Feed API", () => {
+ const port = inject("karakeepPort");
+
+ if (!port) {
+ throw new Error("Missing required environment variables");
+ }
+
+ async function fetchRssFeed(listId: string, token: string) {
+ return await fetch(
+ `http://localhost:${port}/api/v1/rss/lists/${listId}?token=${token}`,
+ );
+ }
+
+ async function seedDatabase() {
+ const trpcClient = getTrpcClient(apiKey);
+
+ // Create two lists
+ const manualList = await trpcClient.lists.create.mutate({
+ name: "Test List #1",
+ icon: "🚀",
+ type: "manual",
+ });
+
+ const smartList = await trpcClient.lists.create.mutate({
+ name: "Test List #2",
+ icon: "🚀",
+ type: "smart",
+ query: "is:fav",
+ });
+
+ // Create two bookmarks
+ const createBookmark1 = await trpcClient.bookmarks.createBookmark.mutate({
+ title: "Test Bookmark #1",
+ url: "https://example.com",
+ type: BookmarkTypes.LINK,
+ });
+
+ const createBookmark2 = await trpcClient.bookmarks.createBookmark.mutate({
+ title: "Test Bookmark #2",
+ url: "https://example.com/2",
+ type: BookmarkTypes.LINK,
+ favourited: true,
+ });
+
+ await trpcClient.lists.addToList.mutate({
+ listId: manualList.id,
+ bookmarkId: createBookmark1.id,
+ });
+
+ return { manualList, smartList, createBookmark1, createBookmark2 };
+ }
+
+ let apiKey: string;
+
+ beforeEach(async () => {
+ apiKey = await createTestUser();
+ });
+
+ it("should generate rss feed for manual lists", async () => {
+ const { manualList } = await seedDatabase();
+ const trpcClient = getTrpcClient(apiKey);
+
+ // Enable rss feed
+ const token = await trpcClient.lists.regenRssToken.mutate({
+ listId: manualList.id,
+ });
+
+ const res = await fetchRssFeed(manualList.id, token.token);
+ expect(res.status).toBe(200);
+ expect(res.headers.get("Content-Type")).toBe("application/rss+xml");
+
+ const text = await res.text();
+ expect(text).toContain("Test Bookmark #1");
+ expect(text).not.toContain("Test Bookmark #2");
+ });
+
+ it("should generate rss feed for smart lists", async () => {
+ const { smartList } = await seedDatabase();
+ const trpcClient = getTrpcClient(apiKey);
+
+ // Enable rss feed
+ const token = await trpcClient.lists.regenRssToken.mutate({
+ listId: smartList.id,
+ });
+
+ const res = await fetchRssFeed(smartList.id, token.token);
+ expect(res.status).toBe(200);
+ expect(res.headers.get("Content-Type")).toBe("application/rss+xml");
+
+ const text = await res.text();
+ expect(text).not.toContain("Test Bookmark #1");
+ expect(text).toContain("Test Bookmark #2");
+ });
+
+ it("should fail when the token is invalid", async () => {
+ const { smartList } = await seedDatabase();
+ const trpcClient = getTrpcClient(apiKey);
+
+ // Enable rss feed
+ const token = await trpcClient.lists.regenRssToken.mutate({
+ listId: smartList.id,
+ });
+
+ let res = await fetchRssFeed(smartList.id, token.token);
+ expect(res.status).toBe(200);
+
+ // Invalidate the token
+ await trpcClient.lists.regenRssToken.mutate({
+ listId: smartList.id,
+ });
+
+ res = await fetchRssFeed(smartList.id, token.token);
+ expect(res.status).toBe(404);
+ });
+
+ it("should fail when rss gets disabled", async () => {
+ const { smartList } = await seedDatabase();
+ const trpcClient = getTrpcClient(apiKey);
+
+ // Enable rss feed
+ const token = await trpcClient.lists.regenRssToken.mutate({
+ listId: smartList.id,
+ });
+
+ const res = await fetchRssFeed(smartList.id, token.token);
+ expect(res.status).toBe(200);
+
+ // Disable rss feed
+ await trpcClient.lists.clearRssToken.mutate({
+ listId: smartList.id,
+ });
+
+ const res2 = await fetchRssFeed(smartList.id, token.token);
+ expect(res2.status).toBe(404);
+ });
+
+ it("should fail when no token is provided", async () => {
+ const { smartList } = await seedDatabase();
+ const trpcClient = getTrpcClient(apiKey);
+
+ // Enable rss feed
+ await trpcClient.lists.regenRssToken.mutate({
+ listId: smartList.id,
+ });
+
+ const res2 = await fetchRssFeed(smartList.id, "");
+ expect(res2.status).toBe(400);
+ });
+});
diff --git a/packages/e2e_tests/tests/api/tags.test.ts b/packages/e2e_tests/tests/api/tags.test.ts
index 3e3cacc0..6c387628 100644
--- a/packages/e2e_tests/tests/api/tags.test.ts
+++ b/packages/e2e_tests/tests/api/tags.test.ts
@@ -26,31 +26,16 @@ describe("Tags API", () => {
});
it("should get, update and delete a tag", async () => {
- // Create a bookmark first
- const { data: createdBookmark } = await client.POST("/bookmarks", {
+ // Create a tag by attaching it to the bookmark
+ const { data: tag } = await client.POST("/tags", {
body: {
- type: "text",
- title: "Test Bookmark",
- text: "This is a test bookmark",
+ name: "Test Tag",
},
});
+ expect(tag).toBeDefined();
+ expect(tag!.name).toBe("Test Tag");
- // Create a tag by attaching it to the bookmark
- const { data: addTagResponse } = await client.POST(
- "/bookmarks/{bookmarkId}/tags",
- {
- params: {
- path: {
- bookmarkId: createdBookmark!.id,
- },
- },
- body: {
- tags: [{ tagName: "Test Tag" }],
- },
- },
- );
-
- const tagId = addTagResponse!.attached[0];
+ const tagId = tag!.id;
// Get the tag
const { data: retrievedTag, response: getResponse } = await client.GET(