edf158f13f
Co-authored-by: Camille Moussu <cmoussu@linagora.com>
435 lines
12 KiB
TypeScript
435 lines
12 KiB
TypeScript
import { AccessTab } from "@/components/Calendar/AccessTab";
|
||
import {
|
||
CalendarAccessRights,
|
||
UserWithAccess,
|
||
} from "@/components/Calendar/CalendarAccessRights";
|
||
import CalendarPopover from "@/components/Calendar/CalendarModal";
|
||
import { updateDelegationCalendar } from "@/features/Calendars/api/updateDelegationCalendar";
|
||
import { AccessRight, Calendar } from "@/features/Calendars/CalendarTypes";
|
||
import * as eventThunks from "@/features/Calendars/services";
|
||
import * as delegationThunks from "@/features/Calendars/services/updateDelegationCalendarAsync";
|
||
import { getUserDetails } from "@/features/User/userAPI";
|
||
import { accessRightToDavProp } from "@/utils/accessRightToDavProp";
|
||
import { api } from "@/utils/apiUtils";
|
||
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
||
import { renderWithProviders } from "../../utils/Renderwithproviders";
|
||
|
||
jest.mock("@/utils/apiUtils", () => ({
|
||
api: { post: jest.fn() },
|
||
}));
|
||
|
||
jest.mock("@/features/User/userAPI", () => ({
|
||
getUserDetails: jest.fn(),
|
||
}));
|
||
|
||
jest.mock("@/features/Calendars/CalendarApi", () => ({
|
||
getSecretLink: jest.fn().mockReturnValue(""),
|
||
exportCalendar: jest.fn(),
|
||
}));
|
||
|
||
const mockThunkWithUnwrap = (resolvedValue: unknown = {}) =>
|
||
jest.fn().mockImplementation(() => {
|
||
const result = Object.assign(Promise.resolve(resolvedValue), {
|
||
unwrap: () => Promise.resolve(resolvedValue),
|
||
});
|
||
return jest.fn().mockReturnValue(result);
|
||
});
|
||
|
||
describe("accessRightToDavProp", () => {
|
||
it("maps 5 (ADMIN) → dav:administration", () => {
|
||
expect(accessRightToDavProp(5)).toBe("dav:administration");
|
||
});
|
||
|
||
it("maps 3 (EDITOR) → dav:read-write", () => {
|
||
expect(accessRightToDavProp(3)).toBe("dav:read-write");
|
||
});
|
||
|
||
it("maps 2 (VIEW) → dav:read", () => {
|
||
expect(accessRightToDavProp(2)).toBe("dav:read");
|
||
});
|
||
|
||
it("defaults to dav:read for unknown values (covered by default case)", () => {
|
||
expect(accessRightToDavProp(999 as AccessRight)).toBe("dav:read");
|
||
});
|
||
});
|
||
|
||
describe("updateDelegationCalendar", () => {
|
||
beforeEach(() => jest.clearAllMocks());
|
||
|
||
it("posts to the correct DAV endpoint with the share body", async () => {
|
||
(api.post as jest.Mock).mockResolvedValue({ ok: true });
|
||
|
||
const share = {
|
||
set: [{ "dav:href": "mailto:alice@example.com", "dav:read": true }],
|
||
remove: [],
|
||
};
|
||
|
||
await updateDelegationCalendar("/calendars/user/cal1.json", share);
|
||
|
||
await waitFor(() =>
|
||
expect(api.post).toHaveBeenCalledWith(
|
||
"dav/calendars/user/cal1.json",
|
||
expect.objectContaining({
|
||
body: JSON.stringify({ share }),
|
||
})
|
||
)
|
||
);
|
||
});
|
||
});
|
||
|
||
const baseCalendar: Calendar = {
|
||
id: "user1/cal1",
|
||
name: "My Calendar",
|
||
description: "",
|
||
color: { color: "#0062FF", dark: "#FFF" },
|
||
link: "/calendars/user1/cal1.json",
|
||
visibility: "public",
|
||
events: {},
|
||
invite: [],
|
||
owner: { emails: ["user1@example.com"] },
|
||
access: { write: true },
|
||
delegated: true,
|
||
};
|
||
|
||
describe("CalendarAccessRights", () => {
|
||
const mockOnChange = jest.fn();
|
||
const mockOnInvitesLoaded = jest.fn();
|
||
|
||
beforeEach(() => jest.clearAllMocks());
|
||
|
||
it("renders the grant access rights section", () => {
|
||
renderWithProviders(
|
||
<CalendarAccessRights
|
||
calendar={baseCalendar}
|
||
value={[]}
|
||
onChange={mockOnChange}
|
||
onInvitesLoaded={mockOnInvitesLoaded}
|
||
/>,
|
||
{ ...userState }
|
||
);
|
||
|
||
expect(
|
||
screen.getByText("calendarPopover.access.grantAccessRights")
|
||
).toBeInTheDocument();
|
||
});
|
||
|
||
it("shows a list of users already passed in via value prop", () => {
|
||
const users: UserWithAccess[] = [
|
||
{
|
||
openpaasId: "user1",
|
||
displayName: "Alice",
|
||
email: "alice@example.com",
|
||
accessRight: 2,
|
||
},
|
||
];
|
||
|
||
renderWithProviders(
|
||
<CalendarAccessRights
|
||
calendar={baseCalendar}
|
||
value={users}
|
||
onChange={mockOnChange}
|
||
onInvitesLoaded={mockOnInvitesLoaded}
|
||
/>
|
||
);
|
||
|
||
expect(screen.getByText("Alice")).toBeInTheDocument();
|
||
expect(screen.getByText("alice@example.com")).toBeInTheDocument();
|
||
});
|
||
|
||
it("calls onChange with user removed when remove button is clicked", () => {
|
||
const users: UserWithAccess[] = [
|
||
{
|
||
openpaasId: "user1",
|
||
displayName: "Alice",
|
||
email: "alice@example.com",
|
||
accessRight: 5,
|
||
},
|
||
];
|
||
|
||
renderWithProviders(
|
||
<CalendarAccessRights
|
||
calendar={baseCalendar}
|
||
value={users}
|
||
onChange={mockOnChange}
|
||
onInvitesLoaded={mockOnInvitesLoaded}
|
||
/>,
|
||
{ ...userState }
|
||
);
|
||
|
||
fireEvent.click(screen.getByLabelText(/remove/i));
|
||
expect(mockOnChange).toHaveBeenCalledWith([]);
|
||
});
|
||
|
||
it("loads invited users from calendar.invite on mount", async () => {
|
||
const calendarWithInvite: Calendar = {
|
||
...baseCalendar,
|
||
invite: [
|
||
{
|
||
href: "mailto:bob@example.com",
|
||
principal: "/principals/users/bob123",
|
||
access: 3,
|
||
inviteStatus: 1,
|
||
},
|
||
],
|
||
};
|
||
|
||
(getUserDetails as jest.Mock).mockResolvedValue({
|
||
preferredEmail: "bob@example.com",
|
||
firstname: "Bob",
|
||
lastname: "Smith",
|
||
emails: ["bob@example.com"],
|
||
});
|
||
|
||
renderWithProviders(
|
||
<CalendarAccessRights
|
||
calendar={calendarWithInvite}
|
||
value={[]}
|
||
onChange={mockOnChange}
|
||
onInvitesLoaded={mockOnInvitesLoaded}
|
||
/>,
|
||
{ ...userState }
|
||
);
|
||
|
||
await waitFor(() => expect(getUserDetails).toHaveBeenCalledWith("bob123"));
|
||
await waitFor(() =>
|
||
expect(mockOnInvitesLoaded).toHaveBeenCalledWith(
|
||
expect.arrayContaining([
|
||
expect.objectContaining({
|
||
email: "bob@example.com",
|
||
accessRight: 3,
|
||
}),
|
||
])
|
||
)
|
||
);
|
||
});
|
||
|
||
it("skips invite entries where getUserDetails throws", async () => {
|
||
const calendarWithInvite: Calendar = {
|
||
...baseCalendar,
|
||
invite: [
|
||
{
|
||
href: "mailto:ghost@example.com",
|
||
principal: "/principals/users/ghost",
|
||
access: 2,
|
||
inviteStatus: 1,
|
||
},
|
||
],
|
||
};
|
||
|
||
(getUserDetails as jest.Mock).mockRejectedValue(new Error("Not found"));
|
||
|
||
renderWithProviders(
|
||
<CalendarAccessRights
|
||
calendar={calendarWithInvite}
|
||
value={[]}
|
||
onChange={mockOnChange}
|
||
onInvitesLoaded={mockOnInvitesLoaded}
|
||
/>,
|
||
{ ...userState }
|
||
);
|
||
|
||
await waitFor(() => expect(mockOnInvitesLoaded).toHaveBeenCalledWith([]));
|
||
});
|
||
|
||
it("shows a loading spinner while invite users are being fetched", async () => {
|
||
const calendarWithInvite: Calendar = {
|
||
...baseCalendar,
|
||
invite: [
|
||
{
|
||
href: "mailto:carol@example.com",
|
||
principal: "/principals/users/carol",
|
||
access: 2,
|
||
inviteStatus: 1,
|
||
},
|
||
],
|
||
};
|
||
|
||
// Never resolves during the assertion window
|
||
(getUserDetails as jest.Mock).mockImplementation(
|
||
() => new Promise(() => {})
|
||
);
|
||
|
||
renderWithProviders(
|
||
<CalendarAccessRights
|
||
calendar={calendarWithInvite}
|
||
value={[]}
|
||
onChange={mockOnChange}
|
||
onInvitesLoaded={mockOnInvitesLoaded}
|
||
/>,
|
||
{ ...userState }
|
||
);
|
||
|
||
expect(document.querySelector('[role="progressbar"]')).toBeInTheDocument();
|
||
});
|
||
});
|
||
|
||
const userState = {
|
||
user: {
|
||
userData: { openpaasId: "user1", email: "user1@example.com" },
|
||
},
|
||
};
|
||
|
||
describe("AccessTab – conditional rendering of CalendarAccessRights", () => {
|
||
const noop = jest.fn();
|
||
|
||
afterEach(() => jest.clearAllMocks());
|
||
|
||
it("shows CalendarAccessRights when the current user owns the calendar", () => {
|
||
renderWithProviders(
|
||
<AccessTab
|
||
calendar={baseCalendar}
|
||
usersWithAccess={[]}
|
||
onUsersWithAccessChange={noop}
|
||
onInvitesLoaded={noop}
|
||
/>,
|
||
{
|
||
...userState,
|
||
calendars: { list: { "user1/cal1": baseCalendar } },
|
||
}
|
||
);
|
||
|
||
expect(
|
||
screen.getByText("calendarPopover.access.grantAccessRights")
|
||
).toBeInTheDocument();
|
||
});
|
||
|
||
it("hides CalendarAccessRights textfield for a non-owner without admin delegation", () => {
|
||
const foreignCalendar: Calendar = {
|
||
...baseCalendar,
|
||
id: "otherUser/cal1",
|
||
invite: [],
|
||
};
|
||
|
||
renderWithProviders(
|
||
<AccessTab
|
||
calendar={foreignCalendar}
|
||
usersWithAccess={[]}
|
||
onUsersWithAccessChange={noop}
|
||
onInvitesLoaded={noop}
|
||
/>,
|
||
{
|
||
...userState,
|
||
calendars: { list: { "otherUser/cal1": foreignCalendar } },
|
||
}
|
||
);
|
||
|
||
expect(screen.queryByText("peopleSearch.label")).not.toBeInTheDocument();
|
||
expect(
|
||
screen.getByText("calendarPopover.access.accessRights")
|
||
).toBeInTheDocument();
|
||
});
|
||
|
||
it("shows CalendarAccessRights when the user has access=5 (admin) delegation", () => {
|
||
const delegatedCalendar: Calendar = {
|
||
...baseCalendar,
|
||
id: "otherUser/cal1",
|
||
invite: [
|
||
{
|
||
href: "mailto:user1@example.com",
|
||
principal: "/principals/users/user1",
|
||
access: 5,
|
||
inviteStatus: 1,
|
||
},
|
||
],
|
||
};
|
||
|
||
renderWithProviders(
|
||
<AccessTab
|
||
calendar={delegatedCalendar}
|
||
usersWithAccess={[]}
|
||
onUsersWithAccessChange={noop}
|
||
onInvitesLoaded={noop}
|
||
/>,
|
||
{
|
||
...userState,
|
||
calendars: { list: { "otherUser/cal1": delegatedCalendar } },
|
||
}
|
||
);
|
||
|
||
expect(
|
||
screen.getByText("calendarPopover.access.grantAccessRights")
|
||
).toBeInTheDocument();
|
||
});
|
||
});
|
||
|
||
const existingCalendar: Calendar = {
|
||
id: "user1/cal1",
|
||
name: "Existing Cal",
|
||
description: "Desc",
|
||
color: { color: "#0062FF", dark: "#FFF" },
|
||
link: "/calendars/user/cal1",
|
||
visibility: "public",
|
||
events: {},
|
||
invite: [],
|
||
owner: { emails: ["user1@example.com"] },
|
||
};
|
||
|
||
describe("CalendarModal – updateDelegationCalendarAsync integration", () => {
|
||
const mockOnClose = jest.fn();
|
||
|
||
beforeEach(() => {
|
||
jest.clearAllMocks();
|
||
jest
|
||
.spyOn(eventThunks, "patchCalendarAsync")
|
||
.mockImplementation(mockThunkWithUnwrap());
|
||
jest
|
||
.spyOn(eventThunks, "patchACLCalendarAsync")
|
||
.mockImplementation(mockThunkWithUnwrap());
|
||
jest
|
||
.spyOn(delegationThunks, "updateDelegationCalendarAsync")
|
||
.mockImplementation(mockThunkWithUnwrap());
|
||
});
|
||
|
||
it("does NOT call updateDelegationCalendarAsync when no users are added or removed", async () => {
|
||
renderWithProviders(
|
||
<CalendarPopover
|
||
open={true}
|
||
onClose={mockOnClose}
|
||
calendar={existingCalendar}
|
||
/>,
|
||
{ ...userState, calendars: { list: { "user1/cal1": existingCalendar } } }
|
||
);
|
||
|
||
fireEvent.click(screen.getByRole("button", { name: /save/i }));
|
||
|
||
await waitFor(() => expect(mockOnClose).toHaveBeenCalled());
|
||
|
||
expect(
|
||
delegationThunks.updateDelegationCalendarAsync
|
||
).not.toHaveBeenCalled();
|
||
});
|
||
});
|
||
|
||
describe("CalendarModal – cancel button", () => {
|
||
const mockOnClose = jest.fn();
|
||
|
||
beforeEach(() => {
|
||
jest.clearAllMocks();
|
||
jest
|
||
.spyOn(delegationThunks, "updateDelegationCalendarAsync")
|
||
.mockImplementation(mockThunkWithUnwrap());
|
||
jest
|
||
.spyOn(eventThunks, "patchCalendarAsync")
|
||
.mockImplementation(mockThunkWithUnwrap());
|
||
});
|
||
|
||
it("calls onClose without saving when Cancel is clicked", async () => {
|
||
renderWithProviders(
|
||
<CalendarPopover
|
||
open={true}
|
||
onClose={mockOnClose}
|
||
calendar={existingCalendar}
|
||
/>,
|
||
{ ...userState }
|
||
);
|
||
|
||
fireEvent.click(screen.getByRole("button", { name: /cancel/i }));
|
||
|
||
await waitFor(() => expect(mockOnClose).toHaveBeenCalled());
|
||
expect(
|
||
delegationThunks.updateDelegationCalendarAsync
|
||
).not.toHaveBeenCalled();
|
||
expect(eventThunks.patchCalendarAsync).not.toHaveBeenCalled();
|
||
});
|
||
});
|