Add a retargetable, human-readable vanity link namespace /l/<alias> that sits alongside the untouched /share/... routes. - New share_aliases table (workspace-scoped, UNIQUE(workspace_id, alias), page_id nullable ON DELETE SET NULL so the address outlives its target). - ShareAliasRepo + ShareAliasService (create / no-op / 409 reassign guard / availability / request-time readable-target resolution through the single existing share boundary). - Public ShareAliasRedirectController (GET /l/:alias) issues a 302 (never 301, the target is mutable) to the canonical /share/:key/p/:slug page; unknown / dangling / no-longer-readable aliases serve the SPA index with no leak. 'l/:alias' excluded from the global /api prefix. - Authenticated ShareAliasController (set/remove/availability/for-page). - Shared ASCII-only normalize/validate util (server + client copies). - Client: Custom address block in the share modal (live normalize + debounced availability + copy + reassign confirmation dialog). - Unit tests: util, repo SQL-shape, service semantics, migration/entity sanity (server jest) + client alias util (vitest). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
33 lines
1.1 KiB
TypeScript
33 lines
1.1 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import {
|
|
isValidShareAlias,
|
|
normalizeShareAlias,
|
|
} from "@/features/share/share-alias.util.ts";
|
|
|
|
// Mirrors the server-side util so the modal's live feedback matches what the
|
|
// server will accept/store.
|
|
describe("normalizeShareAlias", () => {
|
|
it("lowercases, trims and maps separators to single hyphens", () => {
|
|
expect(normalizeShareAlias(" My Cool_Page ")).toBe("my-cool-page");
|
|
});
|
|
|
|
it("collapses repeated hyphens and trims edges", () => {
|
|
expect(normalizeShareAlias("--a---b--")).toBe("a-b");
|
|
});
|
|
});
|
|
|
|
describe("isValidShareAlias", () => {
|
|
it("accepts ascii hyphen-separated slugs of length 2..60", () => {
|
|
expect(isValidShareAlias("hello-world")).toBe(true);
|
|
expect(isValidShareAlias("a".repeat(60))).toBe(true);
|
|
});
|
|
|
|
it("rejects too short, edge/double hyphens, uppercase and non-ascii", () => {
|
|
expect(isValidShareAlias("a")).toBe(false);
|
|
expect(isValidShareAlias("-a")).toBe(false);
|
|
expect(isValidShareAlias("a--b")).toBe(false);
|
|
expect(isValidShareAlias("Hello")).toBe(false);
|
|
expect(isValidShareAlias("привет")).toBe(false);
|
|
});
|
|
});
|