diff --git a/apps/web/__tests__/unit/video-download-permissions.test.ts b/apps/web/__tests__/unit/video-download-permissions.test.ts new file mode 100644 index 0000000000..2a13a7fc1e --- /dev/null +++ b/apps/web/__tests__/unit/video-download-permissions.test.ts @@ -0,0 +1,123 @@ +import { + organizationMembers, + sharedVideos, + spaceMembers, + spaceVideos, +} from "@cap/database/schema"; +import type { Organisation, User, Video } from "@cap/web-domain"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { canUserDownloadVideo } from "../../../lib/video-download-permissions"; + +let mockSharedOrgs: Array<{ organizationId: string }> = []; +let mockOrgMembers: Array<{ id: string }> = []; +let mockSharedSpaces: Array<{ spaceId: string }> = []; +let mockSpaceMembers: Array<{ id: string }> = []; + +vi.mock("@cap/database", () => ({ + db: () => ({ + select: () => ({ + from: (table: unknown) => ({ + where: (condition: any) => { + let result: unknown[] = []; + if (table === sharedVideos) { + result = mockSharedOrgs; + } else if (table === organizationMembers) { + // Simple condition check for tests: if the where clause doesn't include the target orgs/users, return empty + const getCircularReplacer = () => { + const seen = new WeakSet(); + return (key: string, value: any) => { + if (typeof value === "object" && value !== null) { + if (seen.has(value)) return; + seen.add(value); + } + return value; + }; + }; + const conditionStr = JSON.stringify(condition, getCircularReplacer()) || ""; + if (mockOrgMembers.length > 0 && !conditionStr.includes("org-789")) { + result = []; + } else { + result = mockOrgMembers; + } + } else if (table === spaceVideos) { + result = mockSharedSpaces; + } else if (table === spaceMembers) { + result = mockSpaceMembers; + } + const promise = Promise.resolve(result); + (promise as Record).limit = () => + Promise.resolve(result); + return promise; + }, + }), + }), + }), +})); + +describe("canUserDownloadVideo", () => { + beforeEach(() => { + mockSharedOrgs = []; + mockOrgMembers = []; + mockSharedSpaces = []; + mockSpaceMembers = []; + }); + + it("grants download access to the video owner", async () => { + const allowed = await canUserDownloadVideo({ + userId: "user-123" as User.UserId, + ownerId: "user-123" as User.UserId, + videoId: "vid-456" as Video.VideoId, + orgId: "org-789" as Organisation.OrganisationId, + }); + + expect(allowed).toBe(true); + }); + + it("denies download access to an org member when the video is not explicitly shared with the organization", async () => { + mockSharedOrgs = []; + mockOrgMembers = [{ id: "member-1" }]; + mockSharedSpaces = []; + mockSpaceMembers = []; + + const allowed = await canUserDownloadVideo({ + userId: "user-123" as User.UserId, + ownerId: "user-456" as User.UserId, + videoId: "vid-789" as Video.VideoId, + orgId: "org-789" as Organisation.OrganisationId, + }); + + expect(allowed).toBe(false); + }); + + it("grants download access to an org member when the video is explicitly shared with the organization", async () => { + mockSharedOrgs = [{ organizationId: "org-789" }]; + mockOrgMembers = [{ id: "member-1" }]; + mockSharedSpaces = []; + mockSpaceMembers = []; + + const allowed = await canUserDownloadVideo({ + userId: "user-123" as User.UserId, + ownerId: "user-456" as User.UserId, + videoId: "vid-789" as Video.VideoId, + orgId: "org-789" as Organisation.OrganisationId, + }); + + expect(allowed).toBe(true); + }); + + it("denies download access when the video is shared with a different organization", async () => { + mockSharedOrgs = [{ organizationId: "other-org-id" }]; + mockOrgMembers = [{ id: "member-1" }]; + mockSharedSpaces = []; + mockSpaceMembers = []; + + const allowed = await canUserDownloadVideo({ + userId: "user-123" as User.UserId, + ownerId: "user-456" as User.UserId, + videoId: "vid-789" as Video.VideoId, + orgId: "org-789" as Organisation.OrganisationId, + }); + + expect(allowed).toBe(false); + }); +}); diff --git a/apps/web/lib/video-download-permissions.ts b/apps/web/lib/video-download-permissions.ts index 0fc626bf64..d718bad01d 100644 --- a/apps/web/lib/video-download-permissions.ts +++ b/apps/web/lib/video-download-permissions.ts @@ -12,7 +12,7 @@ export async function canUserDownloadVideo({ userId, ownerId, videoId, - orgId, + orgId: _orgId, }: { userId: User.UserId; ownerId: User.UserId; @@ -26,20 +26,22 @@ export async function canUserDownloadVideo({ .from(sharedVideos) .where(eq(sharedVideos.videoId, videoId)); - const orgIds = [orgId, ...sharedOrgs.map((org) => org.organizationId)]; + if (sharedOrgs.length > 0) { + const orgIds = sharedOrgs.map((org) => org.organizationId); - const [orgMembership] = await db() - .select({ id: organizationMembers.id }) - .from(organizationMembers) - .where( - and( - eq(organizationMembers.userId, userId), - inArray(organizationMembers.organizationId, orgIds), - ), - ) - .limit(1); + const [orgMembership] = await db() + .select({ id: organizationMembers.id }) + .from(organizationMembers) + .where( + and( + eq(organizationMembers.userId, userId), + inArray(organizationMembers.organizationId, orgIds), + ), + ) + .limit(1); - if (orgMembership) return true; + if (orgMembership) return true; + } const sharedSpaces = await db() .select({ spaceId: spaceVideos.spaceId }) diff --git a/packages/sdk-recorder/src/index.ts b/packages/sdk-recorder/src/index.ts index 0f7eb10e1f..991d95e8d0 100644 --- a/packages/sdk-recorder/src/index.ts +++ b/packages/sdk-recorder/src/index.ts @@ -82,9 +82,11 @@ export class CapRecorder { this.listeners.set(event, new Set()); } const set = this.listeners.get(event); - if (set) set.add(handler as EventHandler); + if (!set) return () => {}; + const typedHandler = handler as unknown as EventHandler; + set.add(typedHandler); return () => { - this.listeners.get(event)?.delete(handler); + set.delete(typedHandler); }; }