73dcc4553d
Проба доступности алиаса возвращала currentPageId — id страницы, на которую
алиас уже указывает — ЛЮБОМУ аутентифицированному участнику воркспейса без
проверки прав на просмотр этой страницы. Перебором имён алиасов можно было
смапить их на id страниц, к которым доступа нет.
Теперь checkAvailability отдаёт только {alias, valid, available}. Бита
taken/free достаточно для пробы; заголовок целевой страницы всплывает лишь
ПОСЛЕ реальной попытки setAlias (путь 409 ALIAS_REASSIGN_REQUIRED), который
проверяет права. Клиент currentPageId нигде не использовал — убран из типа,
стейта и теста. Серверный спек утверждает отсутствие currentPageId в ответе.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
146 lines
4.7 KiB
TypeScript
146 lines
4.7 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
|
import { MantineProvider } from "@mantine/core";
|
|
import type { IShareAlias } from "@/features/share/types/share.types";
|
|
|
|
// matchMedia / storage are stubbed globally in vitest.setup.ts.
|
|
|
|
// The mutation + query hooks reach react-query/network; the availability probe
|
|
// hits the API. Stub them so the section renders in isolation and we can drive
|
|
// the exact branches (taken name -> hint, 409 -> reassign modal).
|
|
const setMutateAsync = vi.fn();
|
|
let currentAlias: IShareAlias | null = null;
|
|
let availabilityResult: {
|
|
valid: boolean;
|
|
available: boolean;
|
|
} = { valid: true, available: true };
|
|
|
|
vi.mock("@/features/share/queries/share-query.ts", () => ({
|
|
useShareAliasForPageQuery: () => ({ data: currentAlias }),
|
|
useSetShareAliasMutation: () => ({
|
|
mutateAsync: setMutateAsync,
|
|
isPending: false,
|
|
}),
|
|
useRemoveShareAliasMutation: () => ({
|
|
mutateAsync: vi.fn(),
|
|
isPending: false,
|
|
}),
|
|
}));
|
|
|
|
vi.mock("@/features/share/services/share-service.ts", () => ({
|
|
checkShareAliasAvailability: vi.fn(async () => availabilityResult),
|
|
}));
|
|
|
|
import ShareAliasSection from "./share-alias-section";
|
|
|
|
const aliasRow = (alias: string, pageId: string): IShareAlias => ({
|
|
id: `alias-${alias}`,
|
|
workspaceId: "ws-1",
|
|
alias,
|
|
pageId,
|
|
creatorId: "user-1",
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
});
|
|
|
|
function renderSection(pageId = "page-Y") {
|
|
return render(
|
|
<MantineProvider>
|
|
<ShareAliasSection pageId={pageId} readOnly={false} />
|
|
</MantineProvider>,
|
|
);
|
|
}
|
|
|
|
describe("ShareAliasSection — taken-name handling is never a dead end", () => {
|
|
beforeEach(() => {
|
|
setMutateAsync.mockReset();
|
|
currentAlias = null;
|
|
availabilityResult = { valid: true, available: true };
|
|
});
|
|
|
|
it("shows a 'will move it here' HINT (not a terminal error) when the name belongs to another page, and keeps Save enabled", async () => {
|
|
// Page Y already owns "bee"; the user retypes a name owned by page X.
|
|
currentAlias = aliasRow("bee", "page-Y");
|
|
availabilityResult = {
|
|
valid: true,
|
|
available: false,
|
|
};
|
|
|
|
renderSection("page-Y");
|
|
const input = screen.getByPlaceholderText("my-page") as HTMLInputElement;
|
|
fireEvent.change(input, { target: { value: "test2" } });
|
|
|
|
// The reassign hint replaces the old dead-end red error.
|
|
await waitFor(
|
|
() =>
|
|
expect(
|
|
screen.getByText(
|
|
"This address is in use. Saving will move it to this page.",
|
|
),
|
|
).toBeDefined(),
|
|
{ timeout: 2000 },
|
|
);
|
|
// The old terminal "already in use" error must NOT be shown.
|
|
expect(screen.queryByText("This address is already in use")).toBeNull();
|
|
|
|
// Save stays enabled so the confirm-reassign flow can run.
|
|
const saveBtn = screen.getByRole("button", {
|
|
name: "Save",
|
|
}) as HTMLButtonElement;
|
|
expect(saveBtn.disabled).toBe(false);
|
|
});
|
|
|
|
it("opens the reassign-confirm modal on a 409 ALIAS_REASSIGN_REQUIRED (path forward, not a dead end)", async () => {
|
|
currentAlias = aliasRow("bee", "page-Y");
|
|
availabilityResult = {
|
|
valid: true,
|
|
available: false,
|
|
};
|
|
// The server rejects the un-confirmed save asking the client to confirm.
|
|
setMutateAsync.mockRejectedValueOnce({
|
|
status: 409,
|
|
response: {
|
|
status: 409,
|
|
data: {
|
|
code: "ALIAS_REASSIGN_REQUIRED",
|
|
currentPageTitle: "Alias Test Page X",
|
|
},
|
|
},
|
|
});
|
|
|
|
renderSection("page-Y");
|
|
const input = screen.getByPlaceholderText("my-page") as HTMLInputElement;
|
|
fireEvent.change(input, { target: { value: "test2" } });
|
|
|
|
const saveBtn = screen.getByRole("button", {
|
|
name: "Save",
|
|
}) as HTMLButtonElement;
|
|
await waitFor(() => expect(saveBtn.disabled).toBe(false), {
|
|
timeout: 2000,
|
|
});
|
|
fireEvent.click(saveBtn);
|
|
|
|
// First save sent WITHOUT confirmReassign.
|
|
await waitFor(() =>
|
|
expect(setMutateAsync).toHaveBeenCalledWith(
|
|
expect.objectContaining({ alias: "test2", confirmReassign: false }),
|
|
),
|
|
);
|
|
|
|
// The "Move custom address?" confirm modal must appear (the path forward).
|
|
await waitFor(() =>
|
|
expect(screen.getByText("Move custom address?")).toBeDefined(),
|
|
);
|
|
expect(screen.getByRole("button", { name: "Move here" })).toBeDefined();
|
|
|
|
// Confirming retries WITH confirmReassign: true.
|
|
setMutateAsync.mockResolvedValueOnce(aliasRow("test2", "page-Y"));
|
|
fireEvent.click(screen.getByRole("button", { name: "Move here" }));
|
|
await waitFor(() =>
|
|
expect(setMutateAsync).toHaveBeenCalledWith(
|
|
expect.objectContaining({ alias: "test2", confirmReassign: true }),
|
|
),
|
|
);
|
|
});
|
|
});
|