feat: add removable reference feature vertical slice
This commit is contained in:
@@ -1,126 +1,35 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
NAVIGATION_ROUTES,
|
||||
ROUTE_REGISTRY,
|
||||
} from "../../src/features/installed-feature-contracts.js";
|
||||
import {
|
||||
createRedirectLoopGuard,
|
||||
decideRouteAccess,
|
||||
} from "../../src/presentation/routes/navigation-policy.js";
|
||||
import {
|
||||
NAVIGATION_ROUTES,
|
||||
ROUTE_REGISTRY,
|
||||
} from "../../src/contracts/routes.js";
|
||||
|
||||
describe("route registry", () => {
|
||||
it("matches the stable registry snapshot", () => {
|
||||
expect(ROUTE_REGISTRY).toMatchInlineSnapshot(`
|
||||
{
|
||||
"APP_HOME": {
|
||||
"access": "public",
|
||||
"chunkId": "route-home",
|
||||
"errorSurface": "route-boundary",
|
||||
"loadingSurface": "app-shell",
|
||||
"navigationLabel": "시작",
|
||||
"navigationOrder": 10,
|
||||
"paramsSchema": null,
|
||||
"path": "/",
|
||||
"routeId": "APP_HOME",
|
||||
"searchSchema": null,
|
||||
"title": "시작",
|
||||
},
|
||||
"EXAMPLES_AUTH": {
|
||||
"access": "public",
|
||||
"chunkId": "route-examples-auth",
|
||||
"errorSurface": "route-boundary",
|
||||
"loadingSurface": "example-page",
|
||||
"navigationLabel": "인증 연동",
|
||||
"navigationOrder": 40,
|
||||
"paramsSchema": null,
|
||||
"path": "/examples/auth",
|
||||
"routeId": "EXAMPLES_AUTH",
|
||||
"searchSchema": null,
|
||||
"title": "인증 연동",
|
||||
},
|
||||
"EXAMPLES_STATES": {
|
||||
"access": "public",
|
||||
"chunkId": "route-examples-states",
|
||||
"errorSurface": "route-boundary",
|
||||
"loadingSurface": "example-page",
|
||||
"navigationLabel": "화면 상태",
|
||||
"navigationOrder": 30,
|
||||
"paramsSchema": null,
|
||||
"path": "/examples/states",
|
||||
"routeId": "EXAMPLES_STATES",
|
||||
"searchSchema": null,
|
||||
"title": "화면 상태",
|
||||
},
|
||||
"EXAMPLES_UI": {
|
||||
"access": "public",
|
||||
"chunkId": "route-examples-ui",
|
||||
"errorSurface": "route-boundary",
|
||||
"loadingSurface": "example-page",
|
||||
"navigationLabel": "UI 구성요소",
|
||||
"navigationOrder": 20,
|
||||
"paramsSchema": null,
|
||||
"path": "/examples/ui",
|
||||
"routeId": "EXAMPLES_UI",
|
||||
"searchSchema": null,
|
||||
"title": "UI 구성요소",
|
||||
},
|
||||
"NOT_FOUND": {
|
||||
"access": "public",
|
||||
"chunkId": "route-not-found",
|
||||
"errorSurface": "not-found",
|
||||
"loadingSurface": "none",
|
||||
"navigationLabel": null,
|
||||
"navigationOrder": null,
|
||||
"paramsSchema": "NotFoundSplat",
|
||||
"path": "*",
|
||||
"routeId": "NOT_FOUND",
|
||||
"searchSchema": null,
|
||||
"title": "페이지를 찾을 수 없음",
|
||||
},
|
||||
"SAMPLE_RESOURCE_LIST": {
|
||||
"access": "integration-defined",
|
||||
"chunkId": "route-sample-resources",
|
||||
"errorSurface": "feature-boundary",
|
||||
"loadingSurface": "sample-resource-list",
|
||||
"navigationLabel": "보호된 연동 지점",
|
||||
"navigationOrder": 50,
|
||||
"paramsSchema": null,
|
||||
"path": "/sample/resources",
|
||||
"routeId": "SAMPLE_RESOURCE_LIST",
|
||||
"searchSchema": "SampleResourceListQuery",
|
||||
"title": "보호된 연동 지점",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
describe("installed route registry", () => {
|
||||
it("derives visible navigation in explicit order", () => {
|
||||
expect(NAVIGATION_ROUTES.map(({ routeId }) => routeId)).toEqual([
|
||||
"APP_HOME",
|
||||
"EXAMPLES_UI",
|
||||
"EXAMPLES_STATES",
|
||||
"EXAMPLES_AUTH",
|
||||
"SAMPLE_RESOURCE_LIST",
|
||||
]);
|
||||
const expected = Object.values(ROUTE_REGISTRY)
|
||||
.filter((route) => route.navigationOrder !== null)
|
||||
.sort(
|
||||
(left, right) =>
|
||||
/** @type {number} */ (left.navigationOrder) -
|
||||
/** @type {number} */ (right.navigationOrder),
|
||||
)
|
||||
.map((route) => route.routeId);
|
||||
expect(NAVIGATION_ROUTES.map(({ routeId }) => routeId)).toEqual(expected);
|
||||
});
|
||||
|
||||
it("treats client access as a UX hint, not authorization", () => {
|
||||
it("allows public routes without consulting a product permission", () => {
|
||||
expect(decideRouteAccess("APP_HOME", "unauthenticated")).toEqual({
|
||||
allowed: true,
|
||||
action: "none",
|
||||
});
|
||||
expect(decideRouteAccess("SAMPLE_RESOURCE_LIST", "unauthenticated")).toEqual({
|
||||
allowed: false,
|
||||
action: "show-sign-in",
|
||||
});
|
||||
expect(decideRouteAccess("SAMPLE_RESOURCE_LIST", "authenticated")).toEqual({
|
||||
allowed: true,
|
||||
action: "none",
|
||||
});
|
||||
});
|
||||
|
||||
it("allows at most one automatic redirect per source-target pair", () => {
|
||||
it("bounds automatic redirects by pair and maximum hops", () => {
|
||||
const guard = createRedirectLoopGuard(2);
|
||||
expect(guard.allow("/private", "/signin")).toBe(true);
|
||||
expect(guard.allow("/private", "/signin")).toBe(false);
|
||||
|
||||
Reference in New Issue
Block a user