Skip to content
Merged
4 changes: 2 additions & 2 deletions apps/web/__tests__/unit/dashboard-mobile-tab-infra.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,14 @@ describe("dashboard mobile tabs", () => {
).toBe("Infra");
});

it("does not show Infra to someone who cannot use Infra", () => {
it("shows Infra to any org owner, not only a preview list", () => {
context.user = { id: "owner", email: "someone@example.com" };

expect(hrefs()).not.toContain("/dashboard/infra");
expect(hrefs()).toEqual([
"/dashboard/spaces/browse",
"/dashboard/caps",
"/dashboard/settings/organization",
"/dashboard/infra",
]);
});

Expand Down
16 changes: 3 additions & 13 deletions apps/web/__tests__/unit/developer-actions-preview-gate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,21 +103,11 @@ const actions: [string, () => Promise<unknown>][] = [
],
];

describe("Cap Infra console actions and the preview list", () => {
describe("Cap Infra console actions and sign-in", () => {
beforeEach(() => {
mocks.touched = [];
});

it.each(actions)(
"does not let an account outside the preview %s",
async (_name, run) => {
mocks.user = { id: "user-1", email: "someone@example.com" };

await expect(run()).rejects.toThrow("Unauthorized");
expect(mocks.touched).toEqual([]);
},
);

it.each(actions)(
"does not let a signed-out visitor %s",
async (_name, run) => {
Expand All @@ -129,9 +119,9 @@ describe("Cap Infra console actions and the preview list", () => {
);

it.each(actions)(
"lets an account on the preview list through to %s",
"lets any signed-in account through to %s",
async (_name, run) => {
mocks.user = { id: "user-1", email: "richie@cap.so" };
mocks.user = { id: "user-1", email: "someone@example.com" };

const outcome = await run().then(
() => "returned",
Expand Down
16 changes: 3 additions & 13 deletions apps/web/__tests__/unit/developer-credits-checkout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,27 +222,17 @@ describe("POST /api/developer/credits/checkout", () => {
expect(res.status).toBe(400);
});

it("refuses a purchase from an account outside the preview list", async () => {
it("lets any signed-in account reach the purchase on Cap's own deployment", async () => {
const { buildEnv } = await import("@cap/env");
const env = buildEnv as { NEXT_PUBLIC_IS_CAP?: string };
env.NEXT_PUBLIC_IS_CAP = "true";
try {
mockGetCurrentUser.mockResolvedValue(mockUser);
const res = await POST(
makeRequest({ appId: "app-456", amountCents: 1000 }),
);
expect(res.status).toBe(403);
expect(mockStripe.checkout.sessions.create).not.toHaveBeenCalled();

mockGetCurrentUser.mockResolvedValue({
...mockUser,
email: "richie@cap.so",
});
mockDb.limit.mockResolvedValueOnce([]);
const allowed = await POST(
const res = await POST(
makeRequest({ appId: "app-456", amountCents: 1000 }),
);
expect(allowed.status).toBe(404);
expect(res.status).toBe(404);
} finally {
env.NEXT_PUBLIC_IS_CAP = "";
}
Expand Down
36 changes: 9 additions & 27 deletions apps/web/__tests__/unit/infra-access.test.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,17 @@
import { beforeEach, describe, expect, it, vi } from "vitest";

const env = vi.hoisted(() => ({
NEXT_PUBLIC_IS_CAP: "true" as string | undefined,
}));

vi.mock("@cap/env", () => ({ buildEnv: env }));
import { describe, expect, it } from "vitest";

import { canUseInfra } from "@/lib/infra-access";

describe("Cap Infra preview access", () => {
beforeEach(() => {
env.NEXT_PUBLIC_IS_CAP = "true";
});

it("lets a preview account in, whatever the letter case", () => {
describe("Cap Infra access", () => {
it("is open to every signed-in account", () => {
expect(canUseInfra("richie@cap.so")).toBe(true);
expect(canUseInfra("Richie@Cap.so")).toBe(true);
});

it("keeps everyone else out on Cap's own deployment", () => {
expect(canUseInfra("someone@example.com")).toBe(false);
expect(canUseInfra("richie@cap.so.example.com")).toBe(false);
expect(canUseInfra("")).toBe(false);
expect(canUseInfra(null)).toBe(false);
expect(canUseInfra(undefined)).toBe(false);
expect(canUseInfra("someone@example.com")).toBe(true);
expect(canUseInfra("Someone@Example.com")).toBe(true);
});

it("stays open on self-hosted deployments", () => {
for (const value of ["false", "", undefined]) {
env.NEXT_PUBLIC_IS_CAP = value;
expect(canUseInfra("someone@example.com")).toBe(true);
}
it("reports open access when asked without an account", () => {
expect(canUseInfra(null)).toBe(true);
expect(canUseInfra(undefined)).toBe(true);
expect(canUseInfra("")).toBe(true);
});
});
23 changes: 7 additions & 16 deletions apps/web/__tests__/unit/infra-auto-top-up.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,18 +236,6 @@ describe("auto top-up settings", () => {
});

describe("auto top-up charge", () => {
it("never charges an account outside the preview list", async () => {
ledger.account.email = "someone@example.com";

expect(await runAutoTopUp("account-1", now())).toEqual({
state: "skipped",
reason: "not_allowed",
});
expect(mocks.createPaymentIntent).not.toHaveBeenCalled();
expect(ledger.rows).toHaveLength(0);
expect(ledger.account.autoTopUpEnabled).toBe(true);
});

it("does nothing when switched off, without a card, misconfigured or above the threshold", async () => {
ledger.account.autoTopUpEnabled = false;
expect(await runAutoTopUp("account-1", now())).toMatchObject({
Expand Down Expand Up @@ -1447,12 +1435,15 @@ describe("billing emails", () => {
expect(mocks.sendEmail).toHaveBeenCalledTimes(1);
});

it("never emails an account outside the preview list", async () => {
it("emails any account owner when the switch is on", async () => {
mocks.emailsEnabled = true;
ledger.account.email = "someone@example.com";

expect(await outOfCredit()).toBe(false);
expect(await topUpFailed()).toBe(false);
expect(mocks.sendEmail).not.toHaveBeenCalled();
expect(await outOfCredit()).toBe(true);
expect(await topUpFailed()).toBe(true);
expect(mocks.sendEmail).toHaveBeenCalledTimes(2);
expect(mocks.sendEmail.mock.calls[0]?.[0]).toMatchObject({
email: "someone@example.com",
});
});
});
24 changes: 16 additions & 8 deletions apps/web/__tests__/unit/infra-billing-gates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,9 @@ describe("creating an upload", () => {
});

describe("console upload route", () => {
it("is refused for an account outside the preview list without reading the app", async () => {
it("checks ownership for any signed-in account, not a preview list", async () => {
mocks.selects = [[]];

expect(await ownsInfraApp(outsider as never, "app-1")).toBe(false);
expect(mocks.selects).toHaveLength(0);
});
Expand All @@ -254,10 +256,11 @@ describe("console upload route", () => {
});

describe("saving a card", () => {
it("refuses an account outside the preview list before reaching the payment provider", async () => {
it("treats any signed-in account by ownership before reaching the payment provider", async () => {
mocks.user = outsider;
mocks.selects = [[]];

expect(await cardCheckout()).toEqual({ status: "forbidden" });
expect(await cardCheckout()).toEqual({ status: "not_found" });
expect(mocks.ensureCustomer).not.toHaveBeenCalled();
expect(mocks.createSession).not.toHaveBeenCalled();
});
Expand Down Expand Up @@ -337,11 +340,12 @@ describe("auto top-up settings action", () => {
extra: Partial<Parameters<typeof updateDeveloperAutoTopUp>[0]> = {},
) => updateDeveloperAutoTopUp({ appId: "app-1", enabled: true, ...extra });

it("refuses a signed-out user and an account outside the preview list", async () => {
it("refuses a signed-out user, and any account for an app it does not own", async () => {
mocks.user = null;
await expect(save()).rejects.toThrow("Unauthorized");
mocks.user = outsider;
await expect(save()).rejects.toThrow("Unauthorized");
mocks.selects = [[]];
await expect(save()).rejects.toThrow("App not found");
expect(mocks.sets).toHaveLength(0);
});

Expand Down Expand Up @@ -422,12 +426,16 @@ describe("auto top-up settings action", () => {
});

describe("removing a saved card", () => {
it("refuses an account outside the preview list", async () => {
mocks.user = outsider;

it("refuses a signed-out user, and any account for an app it does not own", async () => {
mocks.user = null;
await expect(removeDeveloperSavedCard("app-1")).rejects.toThrow(
"Unauthorized",
);
mocks.user = outsider;
mocks.selects = [[]];
await expect(removeDeveloperSavedCard("app-1")).rejects.toThrow(
"App not found",
);
expect(mocks.detach).not.toHaveBeenCalled();
});

Expand Down
27 changes: 14 additions & 13 deletions apps/web/__tests__/unit/infra-landing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,18 +160,18 @@ describe("the Cap Infra landing page", () => {
).toContain("{STORAGE_RULE}");
});

it("asks for access while Cap Infra is in preview", async () => {
it("lets anyone start building on Cap's own deployment", async () => {
const content = await load("true");

expect(content.OPEN_ACCESS).toBe(false);
expect(content.START.label).toBe("Request access");
expect(content.START.href).toBe(
"mailto:hello@cap.so?subject=Cap%20Infra%20access",
);
expect(content.PREVIEW_NOTE).toBe(
"Cap Infra is in preview. Email hello@cap.so for access.",
);
expect(content.infraFaqs[0]?.answer).toBe(content.PREVIEW_NOTE);
expect(content.OPEN_ACCESS).toBe(true);
expect(content.START).toEqual({
label: "Start building",
href: "/dashboard/infra",
});
expect(content.PREVIEW_NOTE).toBeNull();
expect(
content.infraFaqs.some((faq) => faq.answer.includes("preview")),
).toBe(false);
});

it("opens the console where anyone can use it", async () => {
Expand Down Expand Up @@ -241,8 +241,9 @@ describe("the rendered page", () => {
const embed = content.PLAYER.find((entry) => entry.id === "embed")?.code;
expect(embed).toContain('loading="lazy"');
expect(text).toContain(embed);
expect(text).toContain("Request access");
expect(text).not.toContain("Start building");
expect(text).toContain("Start building");
expect(text).not.toContain("Request access");
expect(text).not.toContain("in preview");
for (const image of page.querySelectorAll("img")) {
expect(image.hasAttribute("alt")).toBe(true);
const eager =
Expand Down Expand Up @@ -392,7 +393,7 @@ describe("the agent section", () => {
expect(text).toContain(rest.join("\n").trim());
expect(text).toContain(CAP_INFRA_POINTER_PROMPT);
expect(text).toContain("Copy prompt");
expect(text).toContain("Cap Infra is in preview.");
expect(text).not.toContain("Cap Infra is in preview.");
expect(text).not.toMatch(CLI);
const hrefs = [...page.querySelectorAll("a")].map((link) =>
link.getAttribute("href"),
Expand Down
7 changes: 6 additions & 1 deletion apps/web/app/(org)/dashboard/_components/MobileTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,12 @@ const MobileTab = () => {
{Tabs.filter((i) => !i.adminOnly || canViewSettings)
.filter((i) => !i.infraOnly || showInfra)
.map((tab) => (
<Link href={tab.href} key={tab.href} aria-label={tab.label}>
<Link
href={tab.href}
key={tab.href}
aria-label={tab.label}
prefetch={tab.infraOnly ? false : undefined}
>
{tab.icon}
</Link>
))}
Expand Down
3 changes: 2 additions & 1 deletion apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { SignedImageUrl } from "@/components/SignedImageUrl";
import { Tooltip } from "@/components/Tooltip";
import { UsageButton } from "@/components/UsageButton";
import { canUseInfra } from "@/lib/infra-access";
import { INFRA_BASE_PATH } from "@/lib/infra-routes";
import {
loomImportDestinationFromPathname,
loomImportPageHref,
Expand Down Expand Up @@ -555,7 +556,7 @@ const NavItem = ({
onMouseLeave={() => {
iconRef.current?.stopAnimation();
}}
prefetch={true}
prefetch={href !== INFRA_BASE_PATH}
passHref
className={classNames(
"relative border border-transparent transition z-3",
Expand Down
1 change: 1 addition & 0 deletions apps/web/app/(site)/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const footerLinks = {
{ label: "Changelog", href: "/changelog" },
{ label: "Docs", href: "/docs" },
{ label: "Cap for Agents", href: "/agents" },
{ label: "Cap Infra", href: "/infra" },
{ label: "Pricing", href: "/pricing" },
{ label: "Download", href: "/download" },
{
Expand Down
10 changes: 2 additions & 8 deletions apps/web/lib/infra-access.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
import { buildEnv } from "@cap/env";

const INFRA_PREVIEW_EMAILS = new Set(["richie@cap.so"]);

export function canUseInfra(email: string | null | undefined) {
if (buildEnv.NEXT_PUBLIC_IS_CAP !== "true") return true;
if (!email) return false;
return INFRA_PREVIEW_EMAILS.has(email.toLowerCase());
export function canUseInfra(_email?: string | null) {
return true;
}
Loading