Skip to content
File

Blob: tests/client/components/workspace/layout-gating.test.ts

typescript123 lines
1import { describe, expect, it } from "vitest";
2import type { WorkspaceRouteState } from "@/client/lib/workspace-route-model";
3import { shouldBlockMemberOnlyRouteContent, shouldRedirectMemberOnlyRoute } from "@/client/lib/workspace-layout-model";
4 
5const WS_ID = "ws-1";
6 
7const states: Record<string, WorkspaceRouteState> = {
8 loadingEmpty: { phase: "loading", workspaceId: null },
9 loadingSeeded: { phase: "loading", workspaceId: WS_ID },
10 readyMember: { phase: "ready", workspaceId: WS_ID, accessMode: "member" },
11 readyShared: { phase: "ready", workspaceId: WS_ID, accessMode: "shared" },
12 degradedStaleSharedNoIdentity: {
13 phase: "degraded",
14 workspaceId: null,
15 workspaceSlug: "my-ws",
16 reason: "stale-shared",
17 },
18 degradedStaleShared: { phase: "degraded", workspaceId: WS_ID, workspaceSlug: "my-ws", reason: "stale-shared" },
19 errorNotFound: { phase: "error", errorKind: "not-found", message: "Workspace not found" },
20 errorNetwork: { phase: "error", errorKind: "network", message: "Failed to load workspace" },
21};
22 
23describe("member-only gating - redirect decisions", () => {
24 describe("isMemberOnlyRoute: true (workspace index, settings)", () => {
25 it("redirects when route is error", () => {
26 expect(shouldRedirectMemberOnlyRoute(states.errorNotFound, true)).toBe(true);
27 expect(shouldRedirectMemberOnlyRoute(states.errorNetwork, true)).toBe(true);
28 });
29 
30 it("redirects when route is shared access", () => {
31 expect(shouldRedirectMemberOnlyRoute(states.readyShared, true)).toBe(true);
32 });
33 
34 it("redirects when route is degraded", () => {
35 expect(shouldRedirectMemberOnlyRoute(states.degradedStaleSharedNoIdentity, true)).toBe(true);
36 expect(shouldRedirectMemberOnlyRoute(states.degradedStaleShared, true)).toBe(true);
37 });
38 
39 it("does not redirect when route is ready-member", () => {
40 expect(shouldRedirectMemberOnlyRoute(states.readyMember, true)).toBe(false);
41 });
42 
43 it("does not redirect during loading phases", () => {
44 expect(shouldRedirectMemberOnlyRoute(states.loadingEmpty, true)).toBe(false);
45 expect(shouldRedirectMemberOnlyRoute(states.loadingSeeded, true)).toBe(false);
46 });
47 });
48 
49 describe("isMemberOnlyRoute: false (page route)", () => {
50 it("never redirects regardless of route state", () => {
51 for (const route of Object.values(states)) {
52 expect(shouldRedirectMemberOnlyRoute(route, false)).toBe(false);
53 }
54 });
55 });
56});
57 
58describe("member-only gating - render blocking", () => {
59 describe("isMemberOnlyRoute: true", () => {
60 it("blocks render during loading phases", () => {
61 expect(shouldBlockMemberOnlyRouteContent(states.loadingEmpty, true)).toBe(true);
62 expect(shouldBlockMemberOnlyRouteContent(states.loadingSeeded, true)).toBe(true);
63 });
64 
65 it("blocks render on error", () => {
66 expect(shouldBlockMemberOnlyRouteContent(states.errorNotFound, true)).toBe(true);
67 });
68 
69 it("blocks render for shared access", () => {
70 expect(shouldBlockMemberOnlyRouteContent(states.readyShared, true)).toBe(true);
71 });
72 
73 it("blocks render for degraded phases", () => {
74 expect(shouldBlockMemberOnlyRouteContent(states.degradedStaleSharedNoIdentity, true)).toBe(true);
75 expect(shouldBlockMemberOnlyRouteContent(states.degradedStaleShared, true)).toBe(true);
76 });
77 
78 it("allows render for confirmed member access", () => {
79 expect(shouldBlockMemberOnlyRouteContent(states.readyMember, true)).toBe(false);
80 });
81 });
82 
83 describe("isMemberOnlyRoute: false", () => {
84 it("never blocks render, allowing child recovery paths", () => {
85 for (const route of Object.values(states)) {
86 expect(shouldBlockMemberOnlyRouteContent(route, false)).toBe(false);
87 }
88 });
89 });
90});
91 
92describe("combined gating scenarios", () => {
93 it("/$bad-slug (index, member-only, error) -> redirect + block", () => {
94 expect(shouldRedirectMemberOnlyRoute(states.errorNotFound, true)).toBe(true);
95 expect(shouldBlockMemberOnlyRouteContent(states.errorNotFound, true)).toBe(true);
96 });
97 
98 it("/$bad-slug/$pageId (page, not member-only, error) -> no redirect, no block (recovery possible)", () => {
99 expect(shouldRedirectMemberOnlyRoute(states.errorNotFound, false)).toBe(false);
100 expect(shouldBlockMemberOnlyRouteContent(states.errorNotFound, false)).toBe(false);
101 });
102 
103 it("/$valid-slug/settings with shared access (member-only) -> redirect + block", () => {
104 expect(shouldRedirectMemberOnlyRoute(states.readyShared, true)).toBe(true);
105 expect(shouldBlockMemberOnlyRouteContent(states.readyShared, true)).toBe(true);
106 });
107 
108 it("/$unknown-slug (member-only degraded) -> redirect + block", () => {
109 expect(shouldRedirectMemberOnlyRoute(states.degradedStaleSharedNoIdentity, true)).toBe(true);
110 expect(shouldBlockMemberOnlyRouteContent(states.degradedStaleSharedNoIdentity, true)).toBe(true);
111 });
112 
113 it("/$valid-slug/$pageId with shared access (not member-only) -> no redirect (shared view allowed)", () => {
114 expect(shouldRedirectMemberOnlyRoute(states.readyShared, false)).toBe(false);
115 expect(shouldBlockMemberOnlyRouteContent(states.readyShared, false)).toBe(false);
116 });
117 
118 it("/$valid-slug (member, member-only) -> normal render", () => {
119 expect(shouldRedirectMemberOnlyRoute(states.readyMember, true)).toBe(false);
120 expect(shouldBlockMemberOnlyRouteContent(states.readyMember, true)).toBe(false);
121 });
122});