Files
Oleg Isonen 6bfc8a80a7 feat: Allow owners to use Content mode (#6619)
A Free project owner could not switch to Content mode directly, but
could open a free Builder share link and use Content mode there. Owners
can now use the switch on any plan. Share-link visitors can use it only
when the project plan includes Content mode.

## Changes

- Keep Content visible in the mode menu. When it is unavailable, disable
it and show a Pro badge instead of the shortcut.
- Let project owners use Content mode on any plan.
- On Free projects, disable Content mode for every share-link
permission. A Content-only link opens in Preview; a Builder link still
opens in Design.

## Checklist

- [x] Keep the Content menu row visible and test its badge and shortcut.
- [x] Update the permission rule and tests for Free owners, Free share
links, and Pro share links.
- [x] Verify a Free Content-only link falls back to Preview mode.
- [x] Review Builder docs. The Modes guide already says owners can try
Content mode, and the Share links guide says Content-only links require
Pro. No copy change is needed.
- [x] Run focused tests, Builder typecheck, and lint after the
permission change.
- [x] Review the non-test diff for redundant code.
- [x] Set a paid plan only in E2E tests that open Content mode through
an Editor share link. Keep unrelated fixtures on their original plan.
- [x] Fix the Design-mode fallback for Free Content-only links and add a
regression test.
- [x] Confirm required CI checks pass on the latest commit.

## Verification

- Builder typecheck: passed.
- Oxlint on changed code: passed.
- Focused tests: 66 passed across mode, permission, and state suites.
The two new Design-entry cases failed before the fix.
- Latest-commit CI: Main workflow, Visual regression, Check submodules,
and Lint PR passed. Every E2E shard passed.
- Manual Storybook screenshots were not run because approval was not
received.

## Known limit

The Share dialog blocks Content-only links on Free plans. Token creation
and update do not enforce that plan limit on the server. This PR
controls which Builder mode a share-link visitor can enter; it does not
change token issuance.

Closes #6618
2026-10-28 23:36:18 +01:00

816 lines
22 KiB
TypeScript

import { describe, test, expect } from "vitest";
import { getPermissions } from "./permissions";
// Free plan permission values
const defaultPlanPermissions = {
canDownloadAssets: false,
canRestoreBackups: false,
allowDynamicData: false,
allowAuth: false,
allowContentMode: false,
allowStagingPublish: false,
allowAdditionalPermissions: false,
maxContactEmailsPerProject: 0,
maxDomainsAllowedPerUser: 0,
maxDailyPublishesPerUser: 10,
maxWorkspaces: 0,
canInviteMembers: false,
canCreateWorkspace: false,
};
const proPlan = {
canDownloadAssets: true,
canRestoreBackups: true,
allowAdditionalPermissions: true,
allowDynamicData: true,
allowAuth: true,
allowContentMode: true,
allowStagingPublish: true,
maxContactEmailsPerProject: 5,
maxDomainsAllowedPerUser: 100,
maxDailyPublishesPerUser: 100,
maxWorkspaces: 0,
maxProjectsAllowedPerUser: 100,
maxAssetsPerProject: 500,
seatsIncluded: 0,
maxSeatsPerWorkspace: 0,
};
const freePlan = {
canDownloadAssets: false,
canRestoreBackups: false,
allowAdditionalPermissions: false,
allowDynamicData: false,
allowAuth: false,
allowContentMode: false,
allowStagingPublish: false,
maxContactEmailsPerProject: 0,
maxDomainsAllowedPerUser: 0,
maxDailyPublishesPerUser: 10,
maxWorkspaces: 0,
maxProjectsAllowedPerUser: 2,
maxAssetsPerProject: 50,
seatsIncluded: 0,
maxSeatsPerWorkspace: 0,
};
describe("getPermissions", () => {
describe("role-based permissions", () => {
test("owner has full role permissions", () => {
expect(
getPermissions({
role: "own",
planFeatures: freePlan,
workspaces: [],
})
).toEqual({
canCreateProject: true,
canDuplicate: true,
canRename: true,
canShare: true,
canDelete: true,
canTransfer: true,
canEditTags: true,
canOpenSettings: true,
canPublishToStagingOnly: false,
...defaultPlanPermissions,
});
});
test("administrators can create, duplicate, rename, and edit tags", () => {
const result = getPermissions({
role: "administrators",
planFeatures: freePlan,
workspaces: [],
});
expect(result).toEqual({
canCreateProject: true,
canDuplicate: true,
canRename: true,
canShare: false,
canDelete: false,
canTransfer: true,
canEditTags: true,
canOpenSettings: false,
canPublishToStagingOnly: false,
...defaultPlanPermissions,
});
});
test("builders can create, duplicate, rename, and edit tags", () => {
const result = getPermissions({
role: "builders",
planFeatures: freePlan,
workspaces: [],
});
expect(result).toEqual({
canCreateProject: true,
canDuplicate: true,
canRename: true,
canShare: false,
canDelete: false,
canTransfer: false,
canEditTags: true,
canOpenSettings: false,
canPublishToStagingOnly: true,
...defaultPlanPermissions,
});
});
test("editors can rename and edit tags only", () => {
const result = getPermissions({
role: "editors",
planFeatures: freePlan,
workspaces: [],
});
expect(result).toEqual({
canCreateProject: false,
canDuplicate: false,
canRename: true,
canShare: false,
canDelete: false,
canTransfer: false,
canEditTags: true,
canOpenSettings: false,
canPublishToStagingOnly: false,
...defaultPlanPermissions,
});
});
test("viewers have no role-based permissions", () => {
const result = getPermissions({
role: "viewers",
planFeatures: freePlan,
workspaces: [],
});
expect(result).toEqual({
canCreateProject: false,
canDuplicate: false,
canRename: false,
canShare: false,
canDelete: false,
canTransfer: false,
canEditTags: false,
canOpenSettings: false,
canPublishToStagingOnly: false,
...defaultPlanPermissions,
});
});
});
describe("canCreateProject by role", () => {
test("own → true", () => {
expect(
getPermissions({
role: "own",
planFeatures: freePlan,
workspaces: [],
}).canCreateProject
).toBe(true);
});
test("administrators → true", () => {
expect(
getPermissions({
role: "administrators",
planFeatures: freePlan,
workspaces: [],
}).canCreateProject
).toBe(true);
});
test("builders → true", () => {
expect(
getPermissions({
role: "builders",
planFeatures: freePlan,
workspaces: [],
}).canCreateProject
).toBe(true);
});
test("editors → false", () => {
expect(
getPermissions({
role: "editors",
planFeatures: freePlan,
workspaces: [],
}).canCreateProject
).toBe(false);
});
test("viewers → false", () => {
expect(
getPermissions({
role: "viewers",
planFeatures: freePlan,
workspaces: [],
}).canCreateProject
).toBe(false);
});
});
describe("plan-based permissions", () => {
test("pro plan enables all plan features", () => {
const result = getPermissions({
role: "own",
planFeatures: proPlan,
workspaces: [],
});
expect(result).toMatchObject({
allowDynamicData: true,
allowAuth: true,
allowContentMode: true,
allowStagingPublish: true,
allowAdditionalPermissions: true,
maxContactEmailsPerProject: 5,
maxDomainsAllowedPerUser: 100,
maxDailyPublishesPerUser: 100,
maxWorkspaces: 0,
canInviteMembers: false,
canCreateWorkspace: false,
});
});
test("free plan restricts plan features", () => {
const result = getPermissions({
role: "own",
planFeatures: freePlan,
workspaces: [],
});
expect(result).toMatchObject({
allowDynamicData: false,
allowAuth: false,
allowContentMode: false,
allowStagingPublish: false,
allowAdditionalPermissions: false,
maxContactEmailsPerProject: 0,
maxDomainsAllowedPerUser: 0,
maxDailyPublishesPerUser: 10,
maxWorkspaces: 0,
canInviteMembers: false,
canCreateWorkspace: false,
});
});
});
describe("allowContentMode with authPermit", () => {
test("content mode denied for free shared project (view permit)", () => {
const result = getPermissions({
role: "own",
authPermit: "view",
planFeatures: freePlan,
workspaces: [],
});
expect(result.allowContentMode).toBe(false);
});
test("content mode denied for free shared project (edit permit)", () => {
const result = getPermissions({
role: "own",
authPermit: "edit",
planFeatures: freePlan,
workspaces: [],
});
expect(result.allowContentMode).toBe(false);
});
test("content mode denied for free shared project (build permit)", () => {
const result = getPermissions({
role: "own",
authPermit: "build",
planFeatures: freePlan,
workspaces: [],
});
expect(result.allowContentMode).toBe(false);
});
test("content mode denied for free shared project (admin permit)", () => {
const result = getPermissions({
role: "own",
authPermit: "admin",
planFeatures: freePlan,
workspaces: [],
});
expect(result.allowContentMode).toBe(false);
});
test("content mode allowed for shared project with Pro plan", () => {
const result = getPermissions({
role: "own",
authPermit: "edit",
planFeatures: proPlan,
workspaces: [],
});
expect(result.allowContentMode).toBe(true);
});
test("content mode is allowed for own project on any plan", () => {
expect(
getPermissions({
role: "own",
authPermit: "own",
planFeatures: freePlan,
workspaces: [],
}).allowContentMode
).toBe(true);
expect(
getPermissions({
role: "own",
authPermit: "own",
planFeatures: proPlan,
workspaces: [],
}).allowContentMode
).toBe(true);
});
test("content mode denied without authPermit on free plan", () => {
expect(
getPermissions({
role: "own",
planFeatures: freePlan,
workspaces: [],
}).allowContentMode
).toBe(false);
});
test("content mode allowed without authPermit on pro plan", () => {
const result = getPermissions({
role: "own",
planFeatures: proPlan,
workspaces: [],
});
expect(result.allowContentMode).toBe(true);
});
});
describe("combined role and plan permissions", () => {
test("viewer on free plan has minimal permissions", () => {
expect(
getPermissions({
role: "viewers",
planFeatures: freePlan,
workspaces: [],
})
).toEqual({
canCreateProject: false,
canDuplicate: false,
canRename: false,
canShare: false,
canDelete: false,
canTransfer: false,
canEditTags: false,
canOpenSettings: false,
canDownloadAssets: false,
canRestoreBackups: false,
allowDynamicData: false,
allowAuth: false,
allowContentMode: false,
allowStagingPublish: false,
allowAdditionalPermissions: false,
maxContactEmailsPerProject: 0,
maxDomainsAllowedPerUser: 0,
maxDailyPublishesPerUser: 10,
maxWorkspaces: 0,
canPublishToStagingOnly: false,
canInviteMembers: false,
canCreateWorkspace: false,
});
});
test("builder on pro plan has builder role + all plan features", () => {
expect(
getPermissions({
role: "builders",
planFeatures: proPlan,
workspaces: [],
})
).toEqual({
canCreateProject: true,
canDuplicate: true,
canRename: true,
canShare: false,
canDelete: false,
canTransfer: false,
canEditTags: true,
canOpenSettings: false,
canDownloadAssets: true,
canRestoreBackups: true,
allowDynamicData: true,
allowAuth: true,
allowContentMode: true,
allowStagingPublish: true,
allowAdditionalPermissions: true,
maxContactEmailsPerProject: 5,
maxDomainsAllowedPerUser: 100,
maxDailyPublishesPerUser: 100,
maxWorkspaces: 0,
canPublishToStagingOnly: true,
canInviteMembers: false,
canCreateWorkspace: false,
});
});
test("owner on pro plan has all permissions", () => {
expect(
getPermissions({
role: "own",
planFeatures: proPlan,
workspaces: [],
})
).toEqual({
canCreateProject: true,
canDuplicate: true,
canRename: true,
canShare: true,
canDelete: true,
canTransfer: true,
canEditTags: true,
canOpenSettings: true,
canDownloadAssets: true,
canRestoreBackups: true,
allowDynamicData: true,
allowAuth: true,
allowContentMode: true,
allowStagingPublish: true,
allowAdditionalPermissions: true,
maxContactEmailsPerProject: 5,
maxDomainsAllowedPerUser: 100,
maxDailyPublishesPerUser: 100,
maxWorkspaces: 0,
canPublishToStagingOnly: false,
canInviteMembers: false,
canCreateWorkspace: false,
});
});
test("administrator on free plan has admin role but limited plan features", () => {
expect(
getPermissions({
role: "administrators",
planFeatures: freePlan,
workspaces: [],
})
).toEqual({
canCreateProject: true,
canDuplicate: true,
canRename: true,
canShare: false,
canDelete: false,
canTransfer: true,
canEditTags: true,
canOpenSettings: false,
canDownloadAssets: false,
canRestoreBackups: false,
allowDynamicData: false,
allowAuth: false,
allowContentMode: false,
allowStagingPublish: false,
allowAdditionalPermissions: false,
maxContactEmailsPerProject: 0,
maxDomainsAllowedPerUser: 0,
maxDailyPublishesPerUser: 10,
maxWorkspaces: 0,
canPublishToStagingOnly: false,
canInviteMembers: false,
canCreateWorkspace: false,
});
});
test("editor on pro plan has editor role + all plan features", () => {
expect(
getPermissions({
role: "editors",
planFeatures: proPlan,
workspaces: [],
})
).toEqual({
canCreateProject: false,
canDuplicate: false,
canRename: true,
canShare: false,
canDelete: false,
canTransfer: false,
canEditTags: true,
canOpenSettings: false,
canDownloadAssets: true,
canRestoreBackups: true,
allowDynamicData: true,
allowAuth: true,
allowContentMode: true,
allowStagingPublish: true,
allowAdditionalPermissions: true,
maxContactEmailsPerProject: 5,
maxDomainsAllowedPerUser: 100,
maxDailyPublishesPerUser: 100,
maxWorkspaces: 0,
canPublishToStagingOnly: false,
canInviteMembers: false,
canCreateWorkspace: false,
});
});
});
describe("all three parameters combined", () => {
test("viewer + free plan + shared project does not enable content mode", () => {
const result = getPermissions({
role: "viewers",
planFeatures: freePlan,
authPermit: "edit",
workspaces: [],
});
// role: all false
expect(result.canCreateProject).toBe(false);
expect(result.canDuplicate).toBe(false);
expect(result.canRename).toBe(false);
expect(result.canShare).toBe(false);
expect(result.canDelete).toBe(false);
expect(result.canEditTags).toBe(false);
expect(result.canOpenSettings).toBe(false);
// plan: all false
expect(result.allowDynamicData).toBe(false);
expect(result.allowContentMode).toBe(false);
expect(result.allowStagingPublish).toBe(false);
expect(result.allowAdditionalPermissions).toBe(false);
});
test("builder + pro plan + shared project has builder role + all plan features", () => {
const result = getPermissions({
role: "builders",
planFeatures: proPlan,
authPermit: "build",
workspaces: [],
});
expect(result.canCreateProject).toBe(true);
expect(result.canDuplicate).toBe(true);
expect(result.canRename).toBe(true);
expect(result.canShare).toBe(false);
expect(result.canDelete).toBe(false);
expect(result.canEditTags).toBe(true);
expect(result.canOpenSettings).toBe(false);
expect(result.allowDynamicData).toBe(true);
expect(result.allowContentMode).toBe(true);
expect(result.allowStagingPublish).toBe(true);
expect(result.allowAdditionalPermissions).toBe(true);
});
});
describe("maxContactEmailsPerProject", () => {
test("defaults to 0 on free plan", () => {
expect(
getPermissions({
role: "own",
planFeatures: freePlan,
workspaces: [],
}).maxContactEmailsPerProject
).toBe(0);
});
test("reflects plan value", () => {
expect(
getPermissions({
role: "own",
planFeatures: proPlan,
workspaces: [],
}).maxContactEmailsPerProject
).toBe(5);
});
});
describe("maxDomainsAllowedPerUser", () => {
test("defaults to 0 on free plan", () => {
expect(
getPermissions({
role: "own",
planFeatures: freePlan,
workspaces: [],
}).maxDomainsAllowedPerUser
).toBe(0);
});
test("reflects plan value", () => {
expect(
getPermissions({
role: "own",
planFeatures: proPlan,
workspaces: [],
}).maxDomainsAllowedPerUser
).toBe(100);
});
test("free plan has 0", () => {
expect(
getPermissions({
role: "own",
planFeatures: freePlan,
workspaces: [],
}).maxDomainsAllowedPerUser
).toBe(0);
});
});
describe("maxDailyPublishesPerUser", () => {
test("defaults to 10 on free plan", () => {
expect(
getPermissions({
role: "own",
planFeatures: freePlan,
workspaces: [],
}).maxDailyPublishesPerUser
).toBe(10);
});
test("reflects plan value for pro", () => {
expect(
getPermissions({
role: "own",
planFeatures: proPlan,
workspaces: [],
}).maxDailyPublishesPerUser
).toBe(100);
});
test("free plan allows limited publishes", () => {
expect(
getPermissions({
role: "own",
planFeatures: freePlan,
workspaces: [],
}).maxDailyPublishesPerUser
).toBe(10);
});
});
describe("canInviteMembers", () => {
test("false when maxWorkspaces is 0", () => {
expect(
getPermissions({
role: "own",
planFeatures: freePlan,
workspaces: [],
}).canInviteMembers
).toBe(false);
});
test("false when maxWorkspaces is 1", () => {
expect(
getPermissions({
role: "own",
planFeatures: { ...freePlan, maxWorkspaces: 1 },
workspaces: [],
}).canInviteMembers
).toBe(false);
});
test("true when maxWorkspaces is greater than 1", () => {
expect(
getPermissions({
role: "own",
planFeatures: { ...freePlan, maxWorkspaces: 5 },
workspaces: [],
}).canInviteMembers
).toBe(true);
});
});
describe("canCreateWorkspace", () => {
test("false when owned count equals maxWorkspaces", () => {
expect(
getPermissions({
role: "own",
planFeatures: { ...freePlan, maxWorkspaces: 1 },
workspaces: [{ role: "own" }],
}).canCreateWorkspace
).toBe(false);
});
test("false when owned count exceeds maxWorkspaces", () => {
expect(
getPermissions({
role: "own",
planFeatures: { ...freePlan, maxWorkspaces: 1 },
workspaces: [{ role: "own" }, { role: "own" }],
}).canCreateWorkspace
).toBe(false);
});
test("true when owned count is below maxWorkspaces", () => {
expect(
getPermissions({
role: "own",
planFeatures: { ...freePlan, maxWorkspaces: 3 },
workspaces: [{ role: "own" }],
}).canCreateWorkspace
).toBe(true);
});
test("counts only owned workspaces, not shared", () => {
expect(
getPermissions({
role: "own",
planFeatures: { ...freePlan, maxWorkspaces: 2 },
workspaces: [
{ role: "own" },
{ role: "editors" },
{ role: "viewers" },
],
}).canCreateWorkspace
).toBe(true);
});
test("false when maxWorkspaces is 0", () => {
expect(
getPermissions({
role: "own",
planFeatures: freePlan,
workspaces: [],
}).canCreateWorkspace
).toBe(false);
});
test("defaults to empty workspaces", () => {
expect(
getPermissions({
role: "own",
planFeatures: { ...freePlan, maxWorkspaces: 5 },
workspaces: [],
}).canCreateWorkspace
).toBe(true);
});
});
describe("canPublishToStagingOnly", () => {
test("own → false (full publish access)", () => {
expect(
getPermissions({
role: "own",
planFeatures: freePlan,
workspaces: [],
}).canPublishToStagingOnly
).toBe(false);
});
test("administrators → false (full publish access)", () => {
expect(
getPermissions({
role: "administrators",
planFeatures: freePlan,
workspaces: [],
}).canPublishToStagingOnly
).toBe(false);
});
test("builders → true (staging only)", () => {
expect(
getPermissions({
role: "builders",
planFeatures: freePlan,
workspaces: [],
}).canPublishToStagingOnly
).toBe(true);
});
test("editors → false (no publish access)", () => {
expect(
getPermissions({
role: "editors",
planFeatures: freePlan,
workspaces: [],
}).canPublishToStagingOnly
).toBe(false);
});
test("viewers → false (no publish access)", () => {
expect(
getPermissions({
role: "viewers",
planFeatures: freePlan,
workspaces: [],
}).canPublishToStagingOnly
).toBe(false);
});
test("authPermit 'build' → true (share-link builder, staging only)", () => {
expect(
getPermissions({
role: "own",
authPermit: "build",
planFeatures: freePlan,
workspaces: [],
}).canPublishToStagingOnly
).toBe(true);
});
test("authPermit 'admin' → false (share-link admin, full access through canPublish)", () => {
expect(
getPermissions({
role: "own",
authPermit: "admin",
planFeatures: freePlan,
workspaces: [],
}).canPublishToStagingOnly
).toBe(false);
});
});
});