diff --git a/__test__/features/Calendars/services/getCalendarsListAsync.test.tsx b/__test__/features/Calendars/services/getCalendarsListAsync.test.tsx index dd95c85..04d5565 100644 --- a/__test__/features/Calendars/services/getCalendarsListAsync.test.tsx +++ b/__test__/features/Calendars/services/getCalendarsListAsync.test.tsx @@ -1,21 +1,18 @@ import { getCalendarsListAsync } from "@/features/Calendars/services/getCalendarsListAsync"; -import { - getOpenPaasUser, - getResourceDetails, - getUserDetails, -} from "@/features/User/userAPI"; +import { getOpenPaasUser } from "@/features/User/userAPI"; +import { fetchOwnerData } from "@/features/Calendars/services/helpers"; import { getCalendars } from "@/features/Calendars/CalendarApi"; import { formatReduxError } from "@/utils/errorUtils"; import { normalizeCalendar } from "@/features/Calendars/utils/normalizeCalendar"; jest.mock("@/features/User/userAPI"); +jest.mock("@/features/Calendars/services/helpers"); jest.mock("@/features/Calendars/CalendarApi"); jest.mock("@/utils/errorUtils"); jest.mock("@/features/Calendars/utils/normalizeCalendar"); const mockedGetOpenPaasUser = getOpenPaasUser as jest.Mock; -const mockedGetUserDetails = getUserDetails as jest.Mock; -const mockedGetResourceDetails = getResourceDetails as jest.Mock; +const mockedFetchOwnerData = fetchOwnerData as jest.Mock; const mockedGetCalendars = getCalendars as jest.Mock; const mockedFormatReduxError = formatReduxError as jest.Mock; const mockedNormalizeCalendar = normalizeCalendar as jest.Mock; @@ -82,7 +79,7 @@ describe("getCalendarsListAsync", () => { invite: [{ href: "", principal: "", access: 3, inviteStatus: 1 }], }); - mockedGetUserDetails + mockedFetchOwnerData .mockResolvedValueOnce({ firstname: "John", lastname: "Doe", @@ -162,7 +159,38 @@ describe("getCalendarsListAsync", () => { }); }); - it("should fetch resource detail as fallback when user not found", async () => { + it("should handle error when fetching owner data fails", async () => { + getState.mockReturnValue({ + calendars: {}, + user: { userData: { openpaasId: "user-123" } }, + }); + mockedGetCalendars.mockResolvedValue({ + _embedded: { "dav:calendar": [{ id: "cal-1" }] }, + }); + mockedNormalizeCalendar.mockReturnValue({ + cal: { "dav:name": "Error Cal" }, + id: "cal-1", + ownerId: "error-123", + }); + + // fetchOwnerData fails + mockedFetchOwnerData.mockRejectedValueOnce(new Error("Network Error")); + + const thunk = getCalendarsListAsync(); + const result = await thunk(dispatch, getState, undefined); + + const payload = result.payload as any; + expect(mockedFetchOwnerData).toHaveBeenCalledWith("error-123"); + expect(payload.importedCalendars["cal-1"].owner).toEqual({ + firstname: "", + lastname: "Unknown User", + emails: [], + }); + // Errors array should contain the error + expect(payload.errors).toContain("Network Error"); + }); + + it("should return owner data mapping properly (including resource: true)", async () => { getState.mockReturnValue({ calendars: {}, user: { userData: { openpaasId: "user-123" } }, @@ -176,27 +204,19 @@ describe("getCalendarsListAsync", () => { ownerId: "resource-123", }); - // getUserDetails fails with 404 for the resource ID, succeeds for the creator - mockedGetUserDetails.mockImplementation((id: string) => { - if (id === "resource-123") - return Promise.reject({ response: { status: 404 } }); - if (id === "creator-456") - return Promise.resolve({ - firstname: "Creator", - lastname: "User", - emails: [], - }); - return Promise.resolve({ firstname: "", lastname: "", emails: [] }); + // fetchOwnerData succeeds and returns a resource config structure + mockedFetchOwnerData.mockResolvedValueOnce({ + firstname: "Creator", + lastname: "User", + emails: [], + resource: true, }); - // Then getResourceDetails is called and succeeds - mockedGetResourceDetails.mockResolvedValueOnce({ creator: "creator-456" }); const thunk = getCalendarsListAsync(); const result = await thunk(dispatch, getState, undefined); const payload = result.payload as any; - expect(mockedGetResourceDetails).toHaveBeenCalledWith("resource-123"); - expect(mockedGetUserDetails).toHaveBeenCalledWith("creator-456"); + expect(mockedFetchOwnerData).toHaveBeenCalledWith("resource-123"); expect(payload.importedCalendars["cal-1"].owner).toEqual({ firstname: "Creator", lastname: "User", diff --git a/__test__/features/Calendars/services/helpers.test.tsx b/__test__/features/Calendars/services/helpers.test.tsx new file mode 100644 index 0000000..eca326b --- /dev/null +++ b/__test__/features/Calendars/services/helpers.test.tsx @@ -0,0 +1,85 @@ +import { fetchOwnerData } from "@/features/Calendars/services/helpers"; +import { getResourceDetails, getUserDetails } from "@/features/User/userAPI"; + +jest.mock("@/features/User/userAPI"); + +const mockedGetUserDetails = getUserDetails as jest.Mock; +const mockedGetResourceDetails = getResourceDetails as jest.Mock; + +describe("helpers", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe("fetchOwnerData", () => { + it("should return user details successfully", async () => { + const mockUser = { + firstname: "John", + lastname: "Doe", + emails: ["john@example.com"], + }; + mockedGetUserDetails.mockResolvedValueOnce(mockUser); + + const result = await fetchOwnerData("user-123"); + + expect(mockedGetUserDetails).toHaveBeenCalledWith("user-123"); + expect(mockedGetResourceDetails).not.toHaveBeenCalled(); + expect(result).toEqual(mockUser); + }); + + it("should fetch resource details and its creator when user is not found", async () => { + const mockResource = { creator: "creator-456" }; + const mockCreator = { + firstname: "Creator", + lastname: "User", + emails: ["creator@example.com"], + }; + + // Mock getUserDetails to fail with 404 for the initial call + mockedGetUserDetails.mockRejectedValueOnce({ + response: { status: 404 }, + }); + + // Mock getResourceDetails to succeed and return a creator ID + mockedGetResourceDetails.mockResolvedValueOnce(mockResource); + + // Mock getUserDetails to succeed when called for the creator + mockedGetUserDetails.mockResolvedValueOnce(mockCreator); + + const result = await fetchOwnerData("resource-123"); + + expect(mockedGetUserDetails).toHaveBeenNthCalledWith(1, "resource-123"); + expect(mockedGetResourceDetails).toHaveBeenCalledWith("resource-123"); + expect(mockedGetUserDetails).toHaveBeenNthCalledWith(2, "creator-456"); + expect(result).toEqual({ ...mockCreator, resource: true }); + }); + + it("should throw error when getUserDetails fails with non-404 error", async () => { + const mockError = { response: { status: 500 } }; + mockedGetUserDetails.mockRejectedValueOnce(mockError); + + await expect(fetchOwnerData("user-123")).rejects.toEqual(mockError); + + expect(mockedGetUserDetails).toHaveBeenCalledWith("user-123"); + expect(mockedGetResourceDetails).not.toHaveBeenCalled(); + }); + + it("should throw error when getResourceDetails fails", async () => { + const mockError = new Error("Resource not found"); + + // Mock getUserDetails to fail with 404 for the initial call + mockedGetUserDetails.mockRejectedValueOnce({ + response: { status: 404 }, + }); + + // Mock getResourceDetails to fail + mockedGetResourceDetails.mockRejectedValueOnce(mockError); + + await expect(fetchOwnerData("resource-123")).rejects.toEqual(mockError); + + expect(mockedGetUserDetails).toHaveBeenCalledWith("resource-123"); + expect(mockedGetResourceDetails).toHaveBeenCalledWith("resource-123"); + expect(mockedGetUserDetails).toHaveBeenCalledTimes(1); // Only called once + }); + }); +}); diff --git a/src/components/Attendees/PeopleSearch.tsx b/src/components/Attendees/PeopleSearch.tsx index 2840bad..9eb309c 100644 --- a/src/components/Attendees/PeopleSearch.tsx +++ b/src/components/Attendees/PeopleSearch.tsx @@ -25,6 +25,7 @@ import { type SyntheticEvent, } from "react"; import { useI18n } from "twake-i18n"; +import { ResourceIcon } from "./ResourceIcon"; export interface User { email: string; @@ -289,14 +290,21 @@ export function PeopleSearch({ renderOption={(props, option) => { if (selectedUsers.find((u) => u.email === option.email)) return null; const { key, ...otherProps } = props; + const isResource = option.objectType === "resource"; return ( - + {isResource ? ( + + ) : ( + + )} ; errors: string }, @@ -43,42 +40,11 @@ export const getCalendarsListAsync = createAsyncThunk< const ownerDataMap = new Map(); const OWNER_BATCH_SIZE = 20; - const fetchResourceData = async (ownerId: string) => { + const mapOwnerData = async (ownerId: string) => { try { - const data = await getResourceDetails(ownerId); - const ownerData = await getUserDetails(data.creator); - ownerDataMap.set(ownerId, { - ...ownerData, - resource: true, - }); - } catch (error) { - console.error( - `Failed to fetch resource details for ${ownerId}:`, - error - ); - ownerDataMap.set(ownerId, { - firstname: "", - lastname: "Unknown User", - emails: [], - resource: true, - }); - errors.push(formatReduxError(error)); - } - }; - - const fetchOwnerData = async (ownerId: string) => { - try { - const data = await getUserDetails(ownerId); + const data = await fetchOwnerData(ownerId); ownerDataMap.set(ownerId, data); } catch (error) { - const status = (error as { response?: { status?: number } }).response - ?.status; - - if (status === 404) { - await fetchResourceData(ownerId); - return; - } - console.error(`Failed to fetch user details for ${ownerId}:`, error); ownerDataMap.set(ownerId, { firstname: "", @@ -91,7 +57,7 @@ export const getCalendarsListAsync = createAsyncThunk< for (let i = 0; i < uniqueOwnerIds.length; i += OWNER_BATCH_SIZE) { const chunk = uniqueOwnerIds.slice(i, i + OWNER_BATCH_SIZE); - await Promise.all(chunk.map((ownerId) => fetchOwnerData(ownerId))); + await Promise.all(chunk.map((ownerId) => mapOwnerData(ownerId))); } normalizedCalendars.forEach( diff --git a/src/features/Calendars/services/getTempCalendarsListAsync.ts b/src/features/Calendars/services/getTempCalendarsListAsync.ts index 8e4fdc7..4c1a44d 100644 --- a/src/features/Calendars/services/getTempCalendarsListAsync.ts +++ b/src/features/Calendars/services/getTempCalendarsListAsync.ts @@ -1,11 +1,11 @@ import { User } from "@/components/Attendees/PeopleSearch"; import { getCalendarVisibility } from "@/components/Calendar/utils/calendarUtils"; -import { getUserDetails } from "@/features/User/userAPI"; import { formatReduxError } from "@/utils/errorUtils"; import { createAsyncThunk } from "@reduxjs/toolkit"; import { getCalendars } from "../CalendarApi"; import { Calendar } from "../CalendarTypes"; import { RejectedError } from "../types/RejectedError"; +import { fetchOwnerData } from "./helpers"; export const getTempCalendarsListAsync = createAsyncThunk< Record, @@ -49,7 +49,7 @@ export const getTempCalendarsListAsync = createAsyncThunk< const id = source.replace("/calendars/", "").replace(".json", ""); const visibility = getCalendarVisibility(cal["acl"] ?? []); - const ownerData = await getUserDetails(id.split("/")[0]); + const ownerData = await fetchOwnerData(id.split("/")[0]); importedCalendars[id] = { id, diff --git a/src/features/Calendars/services/helpers.ts b/src/features/Calendars/services/helpers.ts new file mode 100644 index 0000000..e7b21fd --- /dev/null +++ b/src/features/Calendars/services/helpers.ts @@ -0,0 +1,38 @@ +import { OpenPaasUserData } from "@/features/User/type/OpenPaasUserData"; +import { getResourceDetails, getUserDetails } from "@/features/User/userAPI"; + +const fetchOwnerOfResource = async ( + ownerId: string +): Promise => { + try { + const data = await getResourceDetails(ownerId); + const ownerData = await getUserDetails(data.creator); + return ownerData; + } catch (error) { + console.error(`Failed to fetch resource details for ${ownerId}:`, error); + throw error; + } +}; + +export const fetchOwnerData = async ( + ownerId: string +): Promise => { + try { + const owner = await getUserDetails(ownerId); + return owner; + } catch (error) { + const status = (error as { response?: { status?: number } }).response + ?.status; + + if (status === 404) { + const owner = await fetchOwnerOfResource(ownerId); + return { + ...owner, + resource: true, + }; + } + + console.error(`Failed to fetch user details for ${ownerId}:`, error); + throw error; + } +};