From 74df8bd789ee2d56d0620e9852aa3eb7c48f0823 Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Sat, 13 Dec 2025 14:36:37 +0000 Subject: feat: Add limits on number of rss feeds and webhooks per user --- packages/trpc/routers/feeds.test.ts | 154 +++++++++++++++++++++++++++++++++ packages/trpc/routers/webhooks.test.ts | 22 +++++ 2 files changed, 176 insertions(+) create mode 100644 packages/trpc/routers/feeds.test.ts (limited to 'packages/trpc/routers') diff --git a/packages/trpc/routers/feeds.test.ts b/packages/trpc/routers/feeds.test.ts new file mode 100644 index 00000000..e80aab0a --- /dev/null +++ b/packages/trpc/routers/feeds.test.ts @@ -0,0 +1,154 @@ +import { beforeEach, describe, expect, test } from "vitest"; + +import type { CustomTestContext } from "../testUtils"; +import { defaultBeforeEach } from "../testUtils"; + +beforeEach(defaultBeforeEach(true)); + +describe("Feed Routes", () => { + test("create feed", async ({ apiCallers }) => { + const api = apiCallers[0].feeds; + const newFeed = await api.create({ + name: "Test Feed", + url: "https://example.com/feed.xml", + enabled: true, + }); + + expect(newFeed).toBeDefined(); + expect(newFeed.name).toEqual("Test Feed"); + expect(newFeed.url).toEqual("https://example.com/feed.xml"); + expect(newFeed.enabled).toBe(true); + }); + + test("update feed", async ({ apiCallers }) => { + const api = apiCallers[0].feeds; + + // First, create a feed to update + const createdFeed = await api.create({ + name: "Test Feed", + url: "https://example.com/feed.xml", + enabled: true, + }); + + // Update it + const updatedFeed = await api.update({ + feedId: createdFeed.id, + name: "Updated Feed", + url: "https://updated-example.com/feed.xml", + enabled: false, + }); + + expect(updatedFeed.name).toEqual("Updated Feed"); + expect(updatedFeed.url).toEqual("https://updated-example.com/feed.xml"); + expect(updatedFeed.enabled).toBe(false); + + // Test updating a non-existent feed + await expect(() => + api.update({ + feedId: "non-existent-id", + name: "Fail", + url: "https://fail.com", + enabled: true, + }), + ).rejects.toThrow(/Feed not found/); + }); + + test("list feeds", async ({ apiCallers }) => { + const api = apiCallers[0].feeds; + + // Create a couple of feeds + await api.create({ + name: "Feed 1", + url: "https://example1.com/feed.xml", + enabled: true, + }); + await api.create({ + name: "Feed 2", + url: "https://example2.com/feed.xml", + enabled: true, + }); + + const result = await api.list(); + expect(result.feeds).toBeDefined(); + expect(result.feeds.length).toBeGreaterThanOrEqual(2); + expect(result.feeds.some((f) => f.name === "Feed 1")).toBe(true); + expect(result.feeds.some((f) => f.name === "Feed 2")).toBe(true); + }); + + test("delete feed", async ({ apiCallers }) => { + const api = apiCallers[0].feeds; + + // Create a feed to delete + const createdFeed = await api.create({ + name: "Test Feed", + url: "https://example.com/feed.xml", + enabled: true, + }); + + // Delete it + await api.delete({ feedId: createdFeed.id }); + + // Verify it's deleted + await expect(() => + api.update({ + feedId: createdFeed.id, + name: "Updated", + url: "https://updated.com", + enabled: true, + }), + ).rejects.toThrow(/Feed not found/); + }); + + test("privacy for feeds", async ({ apiCallers }) => { + const user1Feed = await apiCallers[0].feeds.create({ + name: "User 1 Feed", + url: "https://user1-feed.com/feed.xml", + enabled: true, + }); + const user2Feed = await apiCallers[1].feeds.create({ + name: "User 2 Feed", + url: "https://user2-feed.com/feed.xml", + enabled: true, + }); + + // User 1 should not access User 2's feed + await expect(() => + apiCallers[0].feeds.delete({ feedId: user2Feed.id }), + ).rejects.toThrow(/User is not allowed to access resource/); + await expect(() => + apiCallers[0].feeds.update({ + feedId: user2Feed.id, + name: "Fail", + url: "https://fail.com", + enabled: true, + }), + ).rejects.toThrow(/User is not allowed to access resource/); + + // List should only show the correct user's feeds + const user1List = await apiCallers[0].feeds.list(); + expect(user1List.feeds.some((f) => f.id === user1Feed.id)).toBe(true); + expect(user1List.feeds.some((f) => f.id === user2Feed.id)).toBe(false); + }); + + test("feed limit enforcement", async ({ apiCallers }) => { + const api = apiCallers[0].feeds; + + // Create 1000 feeds (the maximum) + for (let i = 0; i < 1000; i++) { + await api.create({ + name: `Feed ${i}`, + url: `https://example${i}.com/feed.xml`, + enabled: true, + }); + } + + // The 1001st feed should fail + await expect(() => + api.create({ + name: "Feed 1001", + url: "https://example1001.com/feed.xml", + enabled: true, + }), + ).rejects.toThrow(/Maximum number of RSS feeds \(1000\) reached/); + }); +}); diff --git a/packages/trpc/routers/webhooks.test.ts b/packages/trpc/routers/webhooks.test.ts index 5a136a31..de27b11e 100644 --- a/packages/trpc/routers/webhooks.test.ts +++ b/packages/trpc/routers/webhooks.test.ts @@ -125,4 +125,26 @@ describe("Webhook Routes", () => { false, ); }); + + test("webhook limit enforcement", async ({ + apiCallers, + }) => { + const api = apiCallers[0].webhooks; + + // Create 100 webhooks (the maximum) + for (let i = 0; i < 100; i++) { + await api.create({ + url: `https://example${i}.com/webhook`, + events: ["created"], + }); + } + + // The 101st webhook should fail + await expect(() => + api.create({ + url: "https://example101.com/webhook", + events: ["created"], + }), + ).rejects.toThrow(/Maximum number of webhooks \(100\) reached/); + }); }); -- cgit v1.2.3-70-g09d2