Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com).
//
// WSO2 LLC. licenses this file to you under the Apache License,
// Version 2.0 (the "License"); you may not use this file except
// in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

import { renderHook, waitFor } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { describe, it, expect, vi, beforeEach } from "vitest";
import useGetCaseComments from "@api/useGetCaseComments";
import { mockCaseComments } from "@models/mockData";

vi.mock("@constants/apiConstants", async (importOriginal) => {
const actual =
await importOriginal<typeof import("@constants/apiConstants")>();
return {
...actual,
API_MOCK_DELAY: 0,
};
});

vi.mock("@asgardeo/react", () => ({
useAsgardeo: () => ({
getIdToken: vi.fn().mockResolvedValue("mock-token"),
isSignedIn: true,
isLoading: false,
}),
}));

const mockUseMockConfig = vi.fn().mockReturnValue({ isMockEnabled: true });

vi.mock("@providers/MockConfigProvider", () => ({
useMockConfig: () => mockUseMockConfig(),
}));

vi.mock("@hooks/useLogger", () => ({
useLogger: () => ({ debug: vi.fn(), error: vi.fn() }),
}));

const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});

const wrapper = ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);

describe("useGetCaseComments", () => {
beforeEach(() => {
queryClient.clear();
vi.clearAllMocks();
mockUseMockConfig.mockReturnValue({ isMockEnabled: true });
});

it("should return mock comments when isMockEnabled is true", async () => {
const { result } = renderHook(
() => useGetCaseComments("project-1", "case-001"),
{ wrapper },
);

await waitFor(() => expect(result.current.isSuccess).toBe(true));

expect(result.current.data).toBeDefined();
expect(result.current.data?.comments).toBeDefined();
expect(result.current.data?.comments.length).toBe(mockCaseComments.length);
expect(result.current.data?.totalRecords).toBe(mockCaseComments.length);
expect(result.current.data?.offset).toBe(0);
expect(result.current.data?.limit).toBe(20);
});

it("should not fetch when projectId or caseId is missing", () => {
const { result: r1 } = renderHook(
() => useGetCaseComments("", "case-001"),
{ wrapper },
);
expect(r1.current.isFetching).toBe(false);

const { result: r2 } = renderHook(
() => useGetCaseComments("project-1", ""),
{ wrapper },
);
expect(r2.current.isFetching).toBe(false);
});

it("should use correct query key", () => {
renderHook(() => useGetCaseComments("project-1", "case-001"), { wrapper });
const query = queryClient.getQueryCache().findAll({
queryKey: ["case-comments", "project-1", "case-001", 0, 20, true],
})[0];
expect(query).toBeDefined();
});
});
111 changes: 111 additions & 0 deletions apps/customer-portal/webapp/src/api/useGetCaseComments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com).
//
// WSO2 LLC. licenses this file to you under the Apache License,
// Version 2.0 (the "License"); you may not use this file except
// in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

import { useQuery, type UseQueryResult } from "@tanstack/react-query";
import { useAsgardeo } from "@asgardeo/react";
import { useMockConfig } from "@providers/MockConfigProvider";
import { useLogger } from "@hooks/useLogger";
import { mockCaseComments } from "@models/mockData";
import { ApiQueryKeys, API_MOCK_DELAY } from "@constants/apiConstants";
import { addApiHeaders } from "@utils/apiUtils";
import type { CaseCommentsResponse } from "@models/responses";

export interface UseGetCaseCommentsOptions {
offset?: number;
limit?: number;
}

/**
* Fetches comments for a case.
*
* @param {string} projectId - The project id.
* @param {string} caseId - The case id.
* @param {UseGetCaseCommentsOptions} options - Optional offset and limit for pagination.
* @returns {UseQueryResult<CaseCommentsResponse, Error>} Query result with comments.
*/
export default function useGetCaseComments(
projectId: string,
caseId: string,
options?: UseGetCaseCommentsOptions,
): UseQueryResult<CaseCommentsResponse, Error> {
const logger = useLogger();
const { getIdToken, isSignedIn, isLoading: isAuthLoading } = useAsgardeo();
const { isMockEnabled } = useMockConfig();
const { offset = 0, limit = 20 } = options ?? {};

return useQuery<CaseCommentsResponse, Error>({
queryKey: [
ApiQueryKeys.CASE_COMMENTS,
projectId,
caseId,
offset,
limit,
isMockEnabled,
],
queryFn: async (): Promise<CaseCommentsResponse> => {
logger.debug(
`Fetching case comments: projectId=${projectId}, caseId=${caseId}, mock=${isMockEnabled}`,
);

if (isMockEnabled) {
await new Promise((resolve) => setTimeout(resolve, API_MOCK_DELAY));
const sliced = mockCaseComments.slice(offset, offset + limit);
return {
comments: sliced.length > 0 ? sliced : mockCaseComments,
totalRecords: mockCaseComments.length,
offset,
limit,
};
}

try {
const baseUrl = window.config?.CUSTOMER_PORTAL_BACKEND_BASE_URL;
if (!baseUrl) {
throw new Error("CUSTOMER_PORTAL_BACKEND_BASE_URL is not configured");
}

const idToken = await getIdToken();
const params = new URLSearchParams();
if (offset !== undefined) params.set("offset", String(offset));
if (limit !== undefined) params.set("limit", String(limit));
const query = params.toString();
const requestUrl = `${baseUrl}/projects/${projectId}/cases/${caseId}/comments${query ? `?${query}` : ""}`;
const response = await fetch(requestUrl, {
method: "GET",
headers: addApiHeaders(idToken),
});

if (!response.ok) {
throw new Error(
`Error fetching case comments: ${response.status} ${response.statusText}`,
);
}

const data: CaseCommentsResponse = await response.json();
logger.debug("[useGetCaseComments] Data received:", data);
return data;
} catch (error) {
logger.error("[useGetCaseComments] Error:", error);
throw error;
}
},
enabled:
!!projectId &&
!!caseId &&
(isMockEnabled || (isSignedIn && !isAuthLoading)),
staleTime: 5 * 60 * 1000, // 5 minutes
});
}
1 change: 1 addition & 0 deletions apps/customer-portal/webapp/src/constants/apiConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const ApiQueryKeys = {
PROJECT_STATS: "project-stats",
PROJECT_CASES: "project-cases",
CASE_DETAILS: "case-details",
CASE_COMMENTS: "case-comments",
CHAT_HISTORY: "chat-history",
DEPLOYMENTS: "deployments",
TIME_TRACKING_STATS: "time-tracking-stats",
Expand Down
28 changes: 28 additions & 0 deletions apps/customer-portal/webapp/src/models/mockData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import type {
CaseListItem,
CaseDetails,
CaseComment,
ProjectListItem,
ProjectDetails,
UserDetails,
Expand Down Expand Up @@ -932,6 +933,33 @@ export const mockCaseDetails: CaseDetails = {
severity: { id: 10, label: "Critical (P1)" },
};

// Mock case comments.
export const mockCaseComments: CaseComment[] = [
{
id: "1398232c1bceb290a002c9d3604bcb27",
content:
"[code]<p>Test comment</p><p><img src=\"/db98232c1bceb290a002c9d3604bcb27.iix\"></p><p><br></p><p>test</p>[/code]",
type: "comments",
createdOn: "2025-12-23 14:49:58",
createdBy: "para-admin@wso2.com",
isEscalated: false,
},
{
id: "712727a81bceb290a002c9d3604bcbcc",
content: "[code]<p>www</p><p><img src=\"/3d2727a81bceb290a002c9d3604bcbcc.iix\"></p>[/code]",
type: "comments",
createdOn: "2025-12-23 14:43:36",
createdBy: "para-admin@wso2.com",
isEscalated: false,
},
{
id: "61f6a7a81bceb290a002c9d3604bcb53",
content:
"[code]<br><b> <u>Description</u> </b><br><p><p>Test Description</p></p>[/code]",
type: "comments",
createdOn: "2025-12-23 14:42:46",
createdBy: "para-admin@wso2.com",
isEscalated: false,
// Mock deployments for project detail Deployments tab.
export const mockDeployments: Deployment[] = [
{
Expand Down
18 changes: 18 additions & 0 deletions apps/customer-portal/webapp/src/models/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,24 @@ export interface CaseDetails {
severity: CaseStatus | null;
}

// Case comment
export interface CaseComment {
id: string;
content: string;
type: string;
createdOn: string;
createdBy: string;
isEscalated: boolean;
}

// Response for case comments list
export interface CaseCommentsResponse {
comments: CaseComment[];
totalRecords: number;
offset: number;
limit: number;
}

// Project Stats Response
export interface ProjectStatsResponse {
projectStats: {
Expand Down