|
| 1 | +import { NextRequest } from "next/server"; |
| 2 | +import { describe, expect, it, vi } from "vitest"; |
| 3 | +import { proxy } from "../../proxy"; |
| 4 | + |
| 5 | +vi.mock("@cap/database", () => ({ |
| 6 | + db: () => ({ |
| 7 | + select: () => ({ from: () => ({ where: async () => [] }) }), |
| 8 | + }), |
| 9 | +})); |
| 10 | + |
| 11 | +vi.mock("@cap/database/schema", () => ({ organizations: {} })); |
| 12 | + |
| 13 | +vi.mock("@cap/env", () => ({ |
| 14 | + buildEnv: { NEXT_PUBLIC_IS_CAP: "true" }, |
| 15 | + serverEnv: () => ({ |
| 16 | + WEB_URL: "https://cap.so", |
| 17 | + VERCEL_URL_HOST: undefined, |
| 18 | + VERCEL_BRANCH_URL_HOST: undefined, |
| 19 | + VERCEL_PROJECT_PRODUCTION_URL_HOST: undefined, |
| 20 | + }), |
| 21 | +})); |
| 22 | + |
| 23 | +const send = (url: string, headers: Record<string, string> = {}) => |
| 24 | + proxy(new NextRequest(url, { headers })); |
| 25 | + |
| 26 | +const frameAncestors = (response: Response) => |
| 27 | + [...response.headers.values()].filter((value) => |
| 28 | + value.startsWith("frame-ancestors"), |
| 29 | + ); |
| 30 | + |
| 31 | +const expectFrameHeaders = (response: Response) => { |
| 32 | + expect(response.headers.get("x-frame-options")).toBe("SAMEORIGIN"); |
| 33 | + expect(frameAncestors(response)).toEqual(["frame-ancestors https://cap.so"]); |
| 34 | +}; |
| 35 | + |
| 36 | +const expectNoFrameHeaders = (response: Response) => { |
| 37 | + expect(response.headers.get("x-frame-options")).toBeNull(); |
| 38 | + expect(frameAncestors(response)).toEqual([]); |
| 39 | +}; |
| 40 | + |
| 41 | +describe("proxy frame headers", () => { |
| 42 | + it.each([ |
| 43 | + "/login", |
| 44 | + "/dashboard", |
| 45 | + "/dashboard/caps", |
| 46 | + "/dashboard/settings/account", |
| 47 | + "/dashboard/settings/organization/billing", |
| 48 | + "/onboarding", |
| 49 | + "/onboarding/organization-setup", |
| 50 | + "/invite/invite-id", |
| 51 | + "/cli/authorize", |
| 52 | + "/mcp/authorize", |
| 53 | + ])("sets frame headers on %s", async (path) => { |
| 54 | + const response = await send(`https://cap.so${path}`); |
| 55 | + expect(response.status).toBe(200); |
| 56 | + expectFrameHeaders(response); |
| 57 | + }); |
| 58 | + |
| 59 | + it.each([ |
| 60 | + "/", |
| 61 | + "/s/video-id", |
| 62 | + "/embed/video-id", |
| 63 | + "/c/collection-id", |
| 64 | + "/pricing", |
| 65 | + "/blog/some-post", |
| 66 | + "/cli/complete", |
| 67 | + "/dashboards", |
| 68 | + "/invited", |
| 69 | + ])("leaves %s without frame headers", async (path) => { |
| 70 | + const response = await send(`https://cap.so${path}`); |
| 71 | + expectNoFrameHeaders(response); |
| 72 | + }); |
| 73 | + |
| 74 | + it("leaves the share iframe redirect without frame headers", async () => { |
| 75 | + const response = await send("https://cap.so/s/video-id", { |
| 76 | + "sec-fetch-dest": "iframe", |
| 77 | + }); |
| 78 | + expect(response.headers.get("location")).toBe( |
| 79 | + "https://cap.so/embed/video-id", |
| 80 | + ); |
| 81 | + expectNoFrameHeaders(response); |
| 82 | + }); |
| 83 | + |
| 84 | + it("keeps redirecting app pages on custom domains to the main origin", async () => { |
| 85 | + const response = await send("https://videos.customer.example/dashboard"); |
| 86 | + expect(response.headers.get("location")).toBe("https://cap.so/dashboard"); |
| 87 | + expectFrameHeaders(response); |
| 88 | + }); |
| 89 | + |
| 90 | + it("keeps serving share pages on verified custom domains without frame headers", async () => { |
| 91 | + const response = await send("https://videos.customer.example/s/video-id", { |
| 92 | + cookie: "verified_domain=videos.customer.example", |
| 93 | + }); |
| 94 | + expect(response.status).toBe(200); |
| 95 | + expectNoFrameHeaders(response); |
| 96 | + }); |
| 97 | +}); |
0 commit comments