forked from baron/baron-sso
262 lines
7.4 KiB
TypeScript
262 lines
7.4 KiB
TypeScript
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
import { MemoryRouter, Route, Routes } from "react-router-dom";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { createI18nMock } from "../../../test/i18nMock";
|
|
import TenantUsersPage from "./TenantUsersPage";
|
|
|
|
const exportUsersCSVMock = vi.hoisted(() => vi.fn());
|
|
const updateUserMock = vi.hoisted(() => vi.fn());
|
|
const bulkUpdateUsersMock = vi.hoisted(() => vi.fn());
|
|
const fetchUsersMock = vi.hoisted(() => vi.fn());
|
|
|
|
vi.mock("../../../lib/i18n", () => createI18nMock());
|
|
|
|
vi.mock("../../../lib/adminApi", () => ({
|
|
fetchTenant: vi.fn(async () => ({
|
|
id: "tenant-team-id",
|
|
name: "기술기획팀",
|
|
slug: "tech-planning",
|
|
})),
|
|
fetchUsers: fetchUsersMock,
|
|
bulkUpdateUsers: bulkUpdateUsersMock,
|
|
exportUsersCSV: exportUsersCSVMock,
|
|
updateUser: updateUserMock,
|
|
}));
|
|
|
|
function renderTenantUsersPage() {
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: { queries: { retry: false } },
|
|
});
|
|
const result = render(
|
|
<QueryClientProvider client={queryClient}>
|
|
<MemoryRouter initialEntries={["/tenants/tenant-team-id/users"]}>
|
|
<Routes>
|
|
<Route
|
|
path="/tenants/:tenantId/users"
|
|
element={<TenantUsersPage />}
|
|
/>
|
|
</Routes>
|
|
</MemoryRouter>
|
|
</QueryClientProvider>,
|
|
);
|
|
|
|
return { ...result, queryClient };
|
|
}
|
|
|
|
describe("TenantUsersPage export", () => {
|
|
beforeEach(() => {
|
|
exportUsersCSVMock.mockReset();
|
|
updateUserMock.mockReset();
|
|
bulkUpdateUsersMock.mockReset();
|
|
fetchUsersMock.mockReset();
|
|
fetchUsersMock.mockResolvedValue({
|
|
items: [
|
|
{
|
|
id: "user-1",
|
|
name: "Alice",
|
|
email: "alice@example.com",
|
|
role: "user",
|
|
status: "active",
|
|
},
|
|
],
|
|
total: 1,
|
|
});
|
|
exportUsersCSVMock.mockResolvedValue({
|
|
blob: new Blob(["email,name\nalice@example.com,Alice\n"], {
|
|
type: "text/csv",
|
|
}),
|
|
filename: "users_export_20260609.csv",
|
|
});
|
|
updateUserMock.mockResolvedValue({});
|
|
vi.spyOn(window.URL, "createObjectURL").mockReturnValue(
|
|
"blob:tenant-users-export",
|
|
);
|
|
vi.spyOn(window.URL, "revokeObjectURL").mockImplementation(() => {});
|
|
bulkUpdateUsersMock.mockResolvedValue({ results: [] });
|
|
});
|
|
|
|
it("exports only the currently opened tenant users by tenant slug", async () => {
|
|
renderTenantUsersPage();
|
|
|
|
await screen.findByText("Alice");
|
|
|
|
fireEvent.click(screen.getByTestId("tenant-users-export-menu-item"));
|
|
|
|
await waitFor(() => {
|
|
expect(exportUsersCSVMock).toHaveBeenCalledWith(
|
|
"",
|
|
"tech-planning",
|
|
false,
|
|
);
|
|
});
|
|
});
|
|
|
|
it("queues searched users and adds all queued users to the tenant at once", async () => {
|
|
fetchUsersMock
|
|
.mockResolvedValueOnce({ items: [], total: 0 })
|
|
.mockResolvedValueOnce({
|
|
items: [
|
|
{
|
|
id: "user-2",
|
|
name: "Bob",
|
|
email: "bob@example.com",
|
|
role: "user",
|
|
status: "active",
|
|
},
|
|
{
|
|
id: "user-3",
|
|
name: "Carol",
|
|
email: "carol@example.com",
|
|
role: "user",
|
|
status: "active",
|
|
},
|
|
],
|
|
total: 2,
|
|
})
|
|
.mockResolvedValue({ items: [], total: 0 });
|
|
updateUserMock.mockResolvedValue({});
|
|
|
|
renderTenantUsersPage();
|
|
|
|
const addButton = await screen.findByTestId(
|
|
"tenant-member-add-existing-btn",
|
|
);
|
|
await waitFor(() => expect(addButton).not.toBeDisabled());
|
|
fireEvent.click(addButton);
|
|
fireEvent.change(screen.getByTestId("tenant-member-search-input"), {
|
|
target: { value: "bo" },
|
|
});
|
|
|
|
fireEvent.click(await screen.findByText("Bob"));
|
|
fireEvent.click(await screen.findByText("Carol"));
|
|
|
|
expect(screen.getByTestId("tenant-member-add-queue")).toHaveTextContent(
|
|
"Bob",
|
|
);
|
|
expect(screen.getByTestId("tenant-member-add-queue")).toHaveTextContent(
|
|
"Carol",
|
|
);
|
|
|
|
fireEvent.click(screen.getByTestId("tenant-member-add-submit-btn"));
|
|
|
|
await waitFor(() => {
|
|
expect(bulkUpdateUsersMock).toHaveBeenCalledWith({
|
|
userIds: ["user-2", "user-3"],
|
|
tenantSlug: "tech-planning",
|
|
isAddTenant: true,
|
|
});
|
|
});
|
|
expect(updateUserMock).not.toHaveBeenCalledWith(
|
|
expect.any(String),
|
|
expect.objectContaining({ isAddTenant: true }),
|
|
);
|
|
});
|
|
|
|
it("queues orgfront multi picker users and adds them with one bulk request", async () => {
|
|
fetchUsersMock
|
|
.mockResolvedValueOnce({
|
|
items: [
|
|
{
|
|
id: "existing-user",
|
|
name: "Existing",
|
|
email: "existing@example.com",
|
|
role: "user",
|
|
status: "active",
|
|
},
|
|
],
|
|
total: 1,
|
|
})
|
|
.mockResolvedValue({ items: [], total: 0 });
|
|
|
|
renderTenantUsersPage();
|
|
|
|
const addButton = await screen.findByTestId(
|
|
"tenant-member-add-existing-btn",
|
|
);
|
|
await waitFor(() => expect(addButton).not.toBeDisabled());
|
|
fireEvent.click(addButton);
|
|
|
|
const picker = await screen.findByTitle("조직도에서 구성원 선택");
|
|
expect(decodeURIComponent(picker.getAttribute("src") ?? "")).toContain(
|
|
"/embed/picker?mode=multiple&select=user",
|
|
);
|
|
|
|
fireEvent(
|
|
window,
|
|
new MessageEvent("message", {
|
|
data: {
|
|
type: "orgfront:picker:confirm",
|
|
payload: {
|
|
mode: "multiple",
|
|
selections: [
|
|
{ type: "tenant", id: "team-1", name: "플랫폼팀" },
|
|
{
|
|
type: "user",
|
|
id: "picked-user-1",
|
|
name: "Picked One",
|
|
email: "picked1@example.com",
|
|
},
|
|
{
|
|
type: "user",
|
|
id: "picked-user-2",
|
|
name: "Picked Two",
|
|
},
|
|
{
|
|
type: "user",
|
|
id: "existing-user",
|
|
name: "Existing",
|
|
email: "existing@example.com",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
|
|
expect(screen.getByTestId("tenant-member-add-queue")).toHaveTextContent(
|
|
"Picked One",
|
|
);
|
|
expect(screen.getByTestId("tenant-member-add-queue")).toHaveTextContent(
|
|
"Picked Two",
|
|
);
|
|
expect(screen.getByTestId("tenant-member-add-queue")).not.toHaveTextContent(
|
|
"Existing",
|
|
);
|
|
|
|
fireEvent.click(screen.getByTestId("tenant-member-add-submit-btn"));
|
|
|
|
await waitFor(() => {
|
|
expect(bulkUpdateUsersMock).toHaveBeenCalledWith({
|
|
userIds: ["picked-user-1", "picked-user-2"],
|
|
tenantSlug: "tech-planning",
|
|
isAddTenant: true,
|
|
});
|
|
});
|
|
});
|
|
|
|
it("removes a member from the tenant and invalidates the user detail cache", async () => {
|
|
const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(true);
|
|
const { queryClient } = renderTenantUsersPage();
|
|
queryClient.setQueryData(["user", "user-1"], {
|
|
id: "user-1",
|
|
name: "Alice",
|
|
});
|
|
|
|
await screen.findByText("Alice");
|
|
|
|
fireEvent.click(screen.getByTestId("tenant-member-remove-user-1"));
|
|
|
|
await waitFor(() => {
|
|
expect(updateUserMock).toHaveBeenCalledWith("user-1", {
|
|
tenantSlug: "tech-planning",
|
|
isRemoveTenant: true,
|
|
});
|
|
});
|
|
expect(queryClient.getQueryState(["user", "user-1"])?.isInvalidated).toBe(
|
|
true,
|
|
);
|
|
confirmSpy.mockRestore();
|
|
});
|
|
});
|