From a52109d34800d8bafb35d9d86cae6552343e5082 Mon Sep 17 00:00:00 2001 From: Ajit Padhi Date: Thu, 16 Apr 2026 19:22:04 +0530 Subject: [PATCH 1/3] Improve Code Quality isses --- .../workshop/Challenge-5/python/utility.py | 5 +- .../fabric_scripts/create_fabric_items.py | 19 +- .../02_create_cu_template_audio.py | 2 +- .../02_create_cu_template_text.py | 2 +- .../index_scripts/03_cu_process_data_text.py | 5 +- .../04_cu_process_custom_data.py | 14 +- infra/scripts/validate_bicep_params.py | 2 +- src/App/src/App.tsx | 11 +- .../src/chartComponents/WordCloudChart.tsx | 1 - src/App/src/components/Chart/Chart.tsx | 4 +- .../components/ChartFilter/ChartFilter.tsx | 3 +- src/App/src/components/Chat/Chat.tsx | 12 +- .../ChatHistoryPanel/ChatHistoryPanel.tsx | 4 +- .../CitationPanel/CitationPanel.tsx | 2 +- .../src/components/Citations/AnswerParser.tsx | 7 - .../src/components/Citations/Citations.tsx | 1 - src/App/src/types/AppTypes.ts | 2 - src/api/common/database/cosmosdb_service.py | 2 +- src/api/common/database/sqldb_service.py | 3 +- src/api/services/chat_service.py | 2 +- src/tests/api/api/test_api_routes.py | 414 +++++----- src/tests/api/auth/test_auth_utils.py | 134 ++-- .../api/common/logging/test_event_utils.py | 2 +- src/tests/api/helpers/test_utils.py | 446 +++++------ src/tests/api/services/test_chat_service.py | 1 - .../api/services/test_history_service.py | 1 - tests/e2e-test/base/base.py | 107 ++- tests/e2e-test/config/constants.py | 76 +- tests/e2e-test/pages/HomePage.py | 1 + tests/e2e-test/pages/KMGenericPage.py | 1 + tests/e2e-test/pages/loginPage.py | 1 + .../tests/test_ithelpdesk_smoke_tc.py | 723 +++++++++--------- tests/e2e-test/tests/test_telecom_smoke_tc.py | 3 +- 33 files changed, 988 insertions(+), 1025 deletions(-) diff --git a/docs/workshop/docs/workshop/Challenge-5/python/utility.py b/docs/workshop/docs/workshop/Challenge-5/python/utility.py index 39de0dbe1..6ea009e93 100644 --- a/docs/workshop/docs/workshop/Challenge-5/python/utility.py +++ b/docs/workshop/docs/workshop/Challenge-5/python/utility.py @@ -242,7 +242,7 @@ def schema_to_tool(schema: Any): return json.loads( assistant_message.tool_calls[0].function.arguments, strict=False ) - except: + except Exception: return assistant_message.tool_calls[0].function.arguments def get_structured_output_answer( @@ -348,7 +348,6 @@ def generate_scenes( scene_generation_prompt = Template(SCENE_GENERATION_PROMPT).substitute( descriptions=next_segment_content ) - scence_response = VideoSceneResponse(scenes=[]) scence_response = openai_assistant.get_structured_output_answer( "", scene_generation_prompt, VideoSceneResponse ) @@ -433,7 +432,6 @@ def generate_chapters( chapter_generation_prompt = Template(CHAPTER_GENERATION_PROMPT).substitute( descriptions=scene_descriptions ) - chapter_response = VideoChapterResponse(chapters=[]) chapter_response = openai_assistant.get_structured_output_answer( "", chapter_generation_prompt, VideoChapterResponse ) @@ -460,7 +458,6 @@ def aggregate_tags( tags_dedup = set(map(lambda x: re.sub(r'^ ', '', x), tags)) tag_dedup_prompt = Template(DEDUP_PROMPT).substitute(tag_list=tags_dedup) - tag_response = VideoTagResponse(tags=[]) tag_response = openai_assistant.get_structured_output_answer( "", tag_dedup_prompt, VideoTagResponse ) diff --git a/infra/scripts/fabric_scripts/create_fabric_items.py b/infra/scripts/fabric_scripts/create_fabric_items.py index e032423cb..bc5f8c0e1 100644 --- a/infra/scripts/fabric_scripts/create_fabric_items.py +++ b/infra/scripts/fabric_scripts/create_fabric_items.py @@ -1,8 +1,6 @@ -from azure.identity import ManagedIdentityCredential import base64 import json import requests -import pandas as pd import os from glob import iglob import zipfile @@ -98,11 +96,11 @@ # upload extracted folder file_names = [f for f in iglob(os.path.join(local_path, "**", "*"), recursive=True) if os.path.isfile(f)] # print('file_names ex', file_names) - for file_name in file_names: - upload_file_name = os.path.basename(file_name) + for extracted_file in file_names: + upload_file_name = os.path.basename(extracted_file) file_client = directory_client.get_file_client("cu_audio_files_all/" + upload_file_name) - # with open(file=os.path.join(extract_dir, file_name), mode="rb") as data: - with open(file=file_name, mode="rb") as data: + # with open(file=os.path.join(extract_dir, extracted_file), mode="rb") as data: + with open(file=extracted_file, mode="rb") as data: # print('data', data) file_client.upload_data(data, overwrite=True) @@ -127,7 +125,7 @@ env_res = requests.get(fabric_env_url, headers=fabric_headers) env_res_id = env_res.json()['value'][0]['id'] # print(env_res.json()) -except: +except Exception: # Environments may not be provisioned yet env_res_id = '' #create notebook items @@ -150,14 +148,14 @@ notebook_json['metadata']['dependencies']['lakehouse']['default_lakehouse'] = lakehouse_res.json()['id'] notebook_json['metadata']['dependencies']['lakehouse']['default_lakehouse_name'] = lakehouse_res.json()['displayName'] notebook_json['metadata']['dependencies']['lakehouse']['default_lakehouse_workspace_id'] = lakehouse_res.json()['workspaceId'] - except: + except Exception: # Lakehouse metadata may not be available pass if env_res_id != '': try: notebook_json['metadata']['dependencies']['environment']['environmentId'] = env_res_id notebook_json['metadata']['dependencies']['environment']['workspaceId'] = lakehouse_res.json()['workspaceId'] - except: + except Exception: # Environment metadata may not be available pass @@ -178,8 +176,7 @@ } } - fabric_response = requests.post(fabric_items_url, headers=fabric_headers, json=notebook_data) - #print(fabric_response.json()) + requests.post(fabric_items_url, headers=fabric_headers, json=notebook_data) time.sleep(120) diff --git a/infra/scripts/index_scripts/02_create_cu_template_audio.py b/infra/scripts/index_scripts/02_create_cu_template_audio.py index 72279c29d..4eda5ae70 100644 --- a/infra/scripts/index_scripts/02_create_cu_template_audio.py +++ b/infra/scripts/index_scripts/02_create_cu_template_audio.py @@ -36,7 +36,7 @@ analyzer = client.get_analyzer_detail_by_id(ANALYZER_ID) if analyzer is not None: client.delete_analyzer(ANALYZER_ID) -except Exception: +except Exception: # Analyzer may not exist yet, safe to ignore pass response = client.begin_create_analyzer(ANALYZER_ID, analyzer_template_path=ANALYZER_TEMPLATE_FILE) diff --git a/infra/scripts/index_scripts/02_create_cu_template_text.py b/infra/scripts/index_scripts/02_create_cu_template_text.py index a9080f2ed..65d14e86d 100644 --- a/infra/scripts/index_scripts/02_create_cu_template_text.py +++ b/infra/scripts/index_scripts/02_create_cu_template_text.py @@ -31,7 +31,7 @@ analyzer = client.get_analyzer_detail_by_id(ANALYZER_ID) if analyzer is not None: client.delete_analyzer(ANALYZER_ID) -except Exception: +except Exception: # Analyzer may not exist yet, safe to ignore pass response = client.begin_create_analyzer(ANALYZER_ID, analyzer_template_path=ANALYZER_TEMPLATE_FILE) diff --git a/infra/scripts/index_scripts/03_cu_process_data_text.py b/infra/scripts/index_scripts/03_cu_process_data_text.py index 30aaa1970..d18438d1f 100644 --- a/infra/scripts/index_scripts/03_cu_process_data_text.py +++ b/infra/scripts/index_scripts/03_cu_process_data_text.py @@ -381,10 +381,10 @@ async def process_files(): docs.extend(await prepare_search_doc(content, conversation_id, path.name, embeddings_client)) counter += 1 - except Exception: + except Exception: # Skip files that fail processing pass if docs != [] and counter % 10 == 0: - result = search_client.upload_documents(documents=docs) + search_client.upload_documents(documents=docs) docs = [] if docs: search_client.upload_documents(documents=docs) @@ -533,7 +533,6 @@ async def call_topic_mining_agent(topics_str1): column_names = [i[0] for i in cursor.description] df_topics = pd.DataFrame(rows, columns=column_names) mined_topics_list = df_topics['label'].tolist() - mined_topics = ", ".join(mined_topics_list) print(f"✓ Mined {len(mined_topics_list)} topics") async def call_topic_mapping_agent(agent, input_text, list_of_topics): diff --git a/infra/scripts/index_scripts/04_cu_process_custom_data.py b/infra/scripts/index_scripts/04_cu_process_custom_data.py index f751cf9dd..f4df4824b 100644 --- a/infra/scripts/index_scripts/04_cu_process_custom_data.py +++ b/infra/scripts/index_scripts/04_cu_process_custom_data.py @@ -190,7 +190,7 @@ def create_search_index(): connection_string = f"DRIVER={driver};SERVER={SQL_SERVER};DATABASE={SQL_DATABASE};" conn = pyodbc.connect(connection_string, attrs_before={SQL_COPT_SS_ACCESS_TOKEN: token_struct}) cursor = conn.cursor() -except: +except Exception: # Fall back to ODBC Driver 17 driver = "{ODBC Driver 17 for SQL Server}" token_bytes = credential.get_token("https://database.windows.net/.default").token.encode("utf-16-LE") token_struct = struct.pack(f" dict[str, list[str]]: try: data = json.loads(sanitized) params = data.get("parameters", {}) - except json.JSONDecodeError: + except json.JSONDecodeError: # Parameters file may have azd variable placeholders pass # Walk each top-level parameter and scan its entire serialized value diff --git a/src/App/src/App.tsx b/src/App/src/App.tsx index 229db1442..24aa23d48 100644 --- a/src/App/src/App.tsx +++ b/src/App/src/App.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useRef } from "react"; +import React, { useEffect, useState } from "react"; import Chart from "./components/Chart/Chart"; import Chat from "./components/Chat/Chat"; import { @@ -8,7 +8,6 @@ import { Body2, webLightTheme, Avatar, - Tag, } from "@fluentui/react-components"; import { SparkleRegular } from "@fluentui/react-icons"; import "./App.css"; @@ -17,7 +16,6 @@ import { ChatHistoryPanel } from "./components/ChatHistoryPanel/ChatHistoryPanel import { getUserInfo, getLayoutConfig, - historyDelete, historyDeleteAll, historyList, historyRead, @@ -25,7 +23,6 @@ import { import { useAppContext } from "./state/useAppContext"; import { actionConstants } from "./state/ActionConstants"; -import { ChatMessage, Conversation } from "./types/AppTypes"; import { AppLogo } from "./components/Svg/Svg"; import CustomSpinner from "./components/CustomSpinner/CustomSpinner"; import CitationPanel from "./components/CitationPanel/CitationPanel"; @@ -67,7 +64,7 @@ const Dashboard: React.FC = () => { const [clearing, setClearing] = React.useState(false); const [clearingError, setClearingError] = React.useState(false); const [isInitialAPItriggered, setIsInitialAPItriggered] = useState(false); - const [showAuthMessage, setShowAuthMessage] = useState(); + const [, setShowAuthMessage] = useState(); const [offset, setOffset] = useState(0); const OFFSET_INCREMENT = 25; const [hasMoreRecords, setHasMoreRecords] = useState(true); @@ -110,7 +107,7 @@ const Dashboard: React.FC = () => { }).catch((err) => { console.error('Error fetching user info: ', err) }) - }, []) + }, []); const updateLayoutWidths = (newState: Record) => { const noOfWidgetsOpen = Object.values(newState).filter((val) => val).length; @@ -232,8 +229,6 @@ const Dashboard: React.FC = () => { } }, [isInitialAPItriggered]); - const [ASSISTANT, TOOL, ERROR, USER] = ["assistant", "tool", "error", "user"]; - const onSelectConversation = async (id: string) => { if (!id) { console.error("No conversation ID found"); diff --git a/src/App/src/chartComponents/WordCloudChart.tsx b/src/App/src/chartComponents/WordCloudChart.tsx index 49ae879bd..b232d5fd7 100644 --- a/src/App/src/chartComponents/WordCloudChart.tsx +++ b/src/App/src/chartComponents/WordCloudChart.tsx @@ -29,7 +29,6 @@ const WordCloudChart: React.FC = ({ height: containerHeight, }); - const currentWidth = useRef(widthInPixels); const [wordsUpdatedFlag, setWordsUpdatedFlag] = useState(true); // Observe container size changes dynamically diff --git a/src/App/src/components/Chart/Chart.tsx b/src/App/src/components/Chart/Chart.tsx index 2e440dd25..015678c44 100644 --- a/src/App/src/components/Chart/Chart.tsx +++ b/src/App/src/components/Chart/Chart.tsx @@ -46,9 +46,9 @@ const Chart = (props: ChartProps) => { const { config: layoutConfig } = state; const { layoutWidthUpdated } = props; - const [widths, setWidths] = useState>({}); + const [, setWidths] = useState>({}); const [appliedFetch, setAppliedFetch] = useState(false); - const [widgetsGapInPercentage, setWidgetsGapInPercentage] = + const [widgetsGapInPercentage] = useState(1); const [windowSize, setWindowSize] = useState({ diff --git a/src/App/src/components/ChartFilter/ChartFilter.tsx b/src/App/src/components/ChartFilter/ChartFilter.tsx index 16c02402f..83792919e 100644 --- a/src/App/src/components/ChartFilter/ChartFilter.tsx +++ b/src/App/src/components/ChartFilter/ChartFilter.tsx @@ -2,7 +2,6 @@ import React, { useMemo, useState } from "react"; import { Stack, DefaultButton, - PrimaryButton, DirectionalHint, IContextualMenuListProps, IContextualMenuItem, @@ -35,7 +34,7 @@ const ChartFilter: React.FC = (props) => { const { state, dispatch } = useAppContext(); const { selectedFilters, filtersMeta } = state.dashboards; const { applyFilters, fetchingCharts } = props; - const initialDateRange = typeof Array.isArray(selectedFilters.DateRange) + const initialDateRange = Array.isArray(selectedFilters.DateRange) ? selectedFilters.DateRange : [""]; diff --git a/src/App/src/components/Chat/Chat.tsx b/src/App/src/components/Chat/Chat.tsx index 8aba8bbd8..dd4feb1a6 100644 --- a/src/App/src/components/Chat/Chat.tsx +++ b/src/App/src/components/Chat/Chat.tsx @@ -3,12 +3,9 @@ import { Button, Textarea, Subtitle2, - Subtitle1, Body1, - Title3, } from "@fluentui/react-components"; import "./Chat.css"; -import { SparkleRegular } from "@fluentui/react-icons"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import supersub from "remark-supersub"; @@ -35,8 +32,7 @@ type ChatProps = { panelShowStates: Record; }; -const [ASSISTANT, TOOL, ERROR, USER] = ["assistant", "tool", "error", "user"]; -const NO_CONTENT_ERROR = "No content in messages object."; +const [ASSISTANT, ERROR] = ["assistant", "error"]; const Chat: React.FC = ({ onHandlePanelStates, @@ -46,7 +42,7 @@ const Chat: React.FC = ({ const { state, dispatch } = useAppContext(); const { userMessage, generatingResponse } = state?.chat; const questionInputRef = useRef(null); - const [isChartLoading, setIsChartLoading] = useState(false) + const [isChartLoading, setIsChartLoading] = useState(false); const abortFuncs = useRef([] as AbortController[]); const chatMessageStreamEnd = useRef(null); const [isCharthDisplayDefault , setIsCharthDisplayDefault] = useState(false); @@ -442,7 +438,7 @@ const Chat: React.FC = ({ if (generatingResponse || !question.trim()) { return; } - const isChatReq = isChartQuery(userMessage) ? "graph" : "Text" + const isChatReq = isChartQuery(userMessage) ? "graph" : "Text"; const newMessage: ChatMessage = { id: generateUUIDv4(), role: "user", @@ -647,7 +643,7 @@ const Chat: React.FC = ({ dispatch({ type: actionConstants.NEW_CONVERSATION_START }); dispatch({ type: actionConstants.UPDATE_CITATION,payload: { activeCitation: null, showCitation: false }}) }; - const { messages, citations } = state.chat; + const { messages } = state.chat; return (
diff --git a/src/App/src/components/ChatHistoryPanel/ChatHistoryPanel.tsx b/src/App/src/components/ChatHistoryPanel/ChatHistoryPanel.tsx index dc54b7756..f3e1fd92c 100644 --- a/src/App/src/components/ChatHistoryPanel/ChatHistoryPanel.tsx +++ b/src/App/src/components/ChatHistoryPanel/ChatHistoryPanel.tsx @@ -10,8 +10,6 @@ import { IContextualMenuItem, PrimaryButton, Stack, - StackItem, - Text, } from "@fluentui/react"; import styles from "./ChatHistoryPanel.module.css"; @@ -57,7 +55,7 @@ export const ChatHistoryPanel: React.FC = (props) => { showClearAllConfirmationDialog, onClickClearAllOption, } = props; - const { state, dispatch } = useAppContext(); + const { state } = useAppContext(); const { chatHistory } = state; const [showClearAllContextMenu, setShowClearAllContextMenu] = useState(false); diff --git a/src/App/src/components/CitationPanel/CitationPanel.tsx b/src/App/src/components/CitationPanel/CitationPanel.tsx index f5f341c30..8c23bbc8c 100644 --- a/src/App/src/components/CitationPanel/CitationPanel.tsx +++ b/src/App/src/components/CitationPanel/CitationPanel.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState } from 'react'; +import React from 'react'; import ReactMarkdown from 'react-markdown'; import { Stack } from '@fluentui/react'; import { DismissRegular } from '@fluentui/react-icons'; diff --git a/src/App/src/components/Citations/AnswerParser.tsx b/src/App/src/components/Citations/AnswerParser.tsx index e3de5f528..6e5fa014d 100644 --- a/src/App/src/components/Citations/AnswerParser.tsx +++ b/src/App/src/components/Citations/AnswerParser.tsx @@ -6,13 +6,6 @@ type ParsedAnswer = { markdownFormatText: string; }; -let filteredCitations = [] as Citation[]; - -// Define a function to check if a citation with the same Chunk_Id already exists in filteredCitations -const isDuplicate = (citation: Citation,citationIndex:string) => { - return filteredCitations.some((c) => c.chunk_id === citation.chunk_id) ; -}; - export function parseAnswer(answer: AskResponse): ParsedAnswer { // let answerText = answer.answer; // const citationLinks = answerText.match(/\[(doc\d\d?\d?)]/g); diff --git a/src/App/src/components/Citations/Citations.tsx b/src/App/src/components/Citations/Citations.tsx index c09d7c579..2ac4a810b 100644 --- a/src/App/src/components/Citations/Citations.tsx +++ b/src/App/src/components/Citations/Citations.tsx @@ -17,7 +17,6 @@ const Citations = ({ answer, index }: Props) => { const { state, dispatch } = useAppContext(); const parsedAnswer = useMemo(() => parseAnswer(answer), [answer]); - const filePathTruncationLimit = 50; const createCitationFilepath = ( citation: Citation, index: number, diff --git a/src/App/src/types/AppTypes.ts b/src/App/src/types/AppTypes.ts index 47d8f96bc..48c40b73f 100644 --- a/src/App/src/types/AppTypes.ts +++ b/src/App/src/types/AppTypes.ts @@ -1,5 +1,3 @@ -import { ReactNode } from "react"; - export type FilterObject = { key: string; displayValue: string; diff --git a/src/api/common/database/cosmosdb_service.py b/src/api/common/database/cosmosdb_service.py index 7a3dab8e8..62950e1fd 100644 --- a/src/api/common/database/cosmosdb_service.py +++ b/src/api/common/database/cosmosdb_service.py @@ -115,7 +115,7 @@ async def delete_messages(self, conversation_id, user_id): item=message["id"], partition_key=user_id ) response_list.append(resp) - return response_list + return response_list async def get_conversations(self, user_id, limit, sort_order="DESC", offset=0): parameters = [{"name": "@userId", "value": user_id}] diff --git a/src/api/common/database/sqldb_service.py b/src/api/common/database/sqldb_service.py index 294b93cb6..3b7ca02bd 100644 --- a/src/api/common/database/sqldb_service.py +++ b/src/api/common/database/sqldb_service.py @@ -64,6 +64,7 @@ async def get_db_connection(): if conn is None: raise RuntimeError("Unable to connect using ODBC Driver 18 or 17 with Azure Credential") + return conn except Exception as e: logging.error("Failed with Azure Credential: %s", str(e)) raise RuntimeError("Unable to connect to SQL database using Microsoft Entra authentication.") from e @@ -179,7 +180,7 @@ async def fetch_chart_data(chart_filters: ChartFilters = ''): req_body = '' try: req_body = chart_filters.model_dump() - except BaseException: + except Exception: # model_dump may fail if filters are empty or invalid pass if req_body != '': where_clause = '' diff --git a/src/api/services/chat_service.py b/src/api/services/chat_service.py index 146cd0a61..ca972f898 100644 --- a/src/api/services/chat_service.py +++ b/src/api/services/chat_service.py @@ -262,7 +262,7 @@ def replace_citation_marker(match): if db_conn is not None: try: db_conn.close() - except Exception: + except Exception: # Best-effort connection cleanup pass # Only emit fallback and tool citations if no error occurred diff --git a/src/tests/api/api/test_api_routes.py b/src/tests/api/api/test_api_routes.py index 27b0b657f..2ef4dcf51 100644 --- a/src/tests/api/api/test_api_routes.py +++ b/src/tests/api/api/test_api_routes.py @@ -1,208 +1,208 @@ -import json -import pytest -from unittest.mock import AsyncMock, patch, Mock -from fastapi import FastAPI -from fastapi.testclient import TestClient - -from api import api_routes - -@pytest.fixture -def create_test_client(): - def _create_client(): - app = FastAPI() - app.include_router(api_routes.router) - return TestClient(app) - return _create_client - - -def test_fetch_chart_data_basic(create_test_client): - with patch("api.api_routes.ChartService") as MockChartService: - mock_instance = MockChartService.return_value - mock_instance.fetch_chart_data = AsyncMock(return_value={"data": "mocked"}) - - client = create_test_client() - response = client.get("/fetchChartData") - - assert response.status_code == 200 - assert response.json() == {"data": "mocked"} - - - -def test_fetch_filter_data_basic(create_test_client): - with patch("api.api_routes.ChartService") as MockChartService: - mock_instance = MockChartService.return_value - mock_instance.fetch_filter_data = AsyncMock(return_value={"filters": "mocked"}) - - client = create_test_client() - response = client.get("/fetchFilterData") - - assert response.status_code == 200 - assert response.json() == {"filters": "mocked"} - - -def test_fetch_chart_data_with_filters_basic(create_test_client): - with patch("api.api_routes.ChartService") as MockChartService: - mock_instance = MockChartService.return_value - mock_instance.fetch_chart_data_with_filters = AsyncMock(return_value=[ - { - "id": "TOTAL_CALLS", - "chart_name": "Total Calls", - "chart_type": "card", - "chart_value": [ - {"name": "Total Calls", "value": float("nan"), "unit_of_measurement": ""} - ] - } - ]) - - client = create_test_client() - payload = { - "selected_filters": { - "Topic": ["Tech"], - "Sentiment": ["Positive"], - "DateRange": ["Last 30 Days"] - } - } - response = client.post("/fetchChartDataWithFilters", json=payload) - expected = [ - { - "id": "TOTAL_CALLS", - "chart_name": "Total Calls", - "chart_type": "card", - "chart_value": [ - {"name": "Total Calls", "value": None, "unit_of_measurement": ""} - ] - } - ] - assert response.status_code == 200 - assert response.json() == expected - -def test_fetch_chart_data_with_filters_error(create_test_client): - with patch("api.api_routes.ChartService") as MockChartService: - mock_instance = MockChartService.return_value - mock_instance.fetch_chart_data_with_filters = AsyncMock(side_effect=Exception("fail")) - - client = create_test_client() - payload = { - "selected_filters": { - "Topic": ["Tech"], - "Sentiment": ["Positive"], - "DateRange": ["Last 30 Days"] - } - } - response = client.post("/fetchChartDataWithFilters", json=payload) - - assert response.status_code == 500 - assert "error" in response.json() - - -def test_fetch_chart_data_error_handling(create_test_client): - with patch("api.api_routes.ChartService") as MockChartService: - mock_instance = MockChartService.return_value - mock_instance.fetch_chart_data = AsyncMock(side_effect=Exception("fail")) - - client = create_test_client() - response = client.get("/fetchChartData") - - assert response.status_code == 500 - assert "error" in response.json() - - -def test_chat_endpoint_basic(create_test_client): - with patch("api.api_routes.ChatService") as MockChatService: - mock_instance = MockChatService.return_value - mock_instance.stream_chat_request = AsyncMock(return_value=iter([b'{"message": "mocked stream"}'])) - - client = create_test_client() - payload = { - "conversation_id": "test", - "messages": [{"content": "Show me a chart"}], - "last_rag_response": "previous data" - } - - response = client.post("/chat", json=payload) - - assert response.status_code == 200 - assert response.json() == {"message": "mocked stream"} - - -def test_get_layout_config_valid(create_test_client, monkeypatch): - test_config = {"layout": "mocked"} - monkeypatch.setenv("REACT_APP_LAYOUT_CONFIG", json.dumps(test_config)) - - client = create_test_client() - response = client.get("/layout-config") - - assert response.status_code == 200 - assert response.json() == test_config - - -def test_get_layout_config_invalid_json(create_test_client, monkeypatch): - monkeypatch.setenv("REACT_APP_LAYOUT_CONFIG", "{bad json") - - client = create_test_client() - response = client.get("/layout-config") - - assert response.status_code == 400 - assert "error" in response.json() - - -def test_get_chart_config_found(create_test_client, monkeypatch): - monkeypatch.setenv("DISPLAY_CHART_DEFAULT", "true") - - client = create_test_client() - response = client.get("/display-chart-default") - - assert response.status_code == 200 - assert response.json() == {"isChartDisplayDefault": "true"} - - -def test_get_chart_config_missing(create_test_client, monkeypatch): - monkeypatch.delenv("DISPLAY_CHART_DEFAULT", raising=False) - - client = create_test_client() - response = client.get("/display-chart-default") - - assert response.status_code == 400 - assert "error" in response.json() - - -def test_fetch_filter_data_error_handling(create_test_client): - with patch("api.api_routes.ChartService") as MockChartService: - mock_instance = MockChartService.return_value - mock_instance.fetch_filter_data = AsyncMock(side_effect=Exception("fail")) - - client = create_test_client() - response = client.get("/fetchFilterData") - - assert response.status_code == 500 - assert "error" in response.json() - - -def test_layout_config_json_decode_error(create_test_client, monkeypatch): - monkeypatch.setenv("REACT_APP_LAYOUT_CONFIG", "not-a-json") - - client = create_test_client() - response = client.get("/layout-config") - - assert response.status_code == 400 - assert "error" in response.json() - - -def test_get_chart_config_success(create_test_client, monkeypatch): - monkeypatch.setenv("DISPLAY_CHART_DEFAULT", "false") - - client = create_test_client() - response = client.get("/display-chart-default") - - assert response.status_code == 200 - assert response.json() == {"isChartDisplayDefault": "false"} - - -def test_get_chart_config_env_missing(create_test_client, monkeypatch): - monkeypatch.delenv("DISPLAY_CHART_DEFAULT", raising=False) - - client = create_test_client() - response = client.get("/display-chart-default") - - assert response.status_code == 400 +import json +import pytest +from unittest.mock import AsyncMock, patch +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from api import api_routes + +@pytest.fixture +def create_test_client(): + def _create_client(): + app = FastAPI() + app.include_router(api_routes.router) + return TestClient(app) + return _create_client + + +def test_fetch_chart_data_basic(create_test_client): + with patch("api.api_routes.ChartService") as MockChartService: + mock_instance = MockChartService.return_value + mock_instance.fetch_chart_data = AsyncMock(return_value={"data": "mocked"}) + + client = create_test_client() + response = client.get("/fetchChartData") + + assert response.status_code == 200 + assert response.json() == {"data": "mocked"} + + + +def test_fetch_filter_data_basic(create_test_client): + with patch("api.api_routes.ChartService") as MockChartService: + mock_instance = MockChartService.return_value + mock_instance.fetch_filter_data = AsyncMock(return_value={"filters": "mocked"}) + + client = create_test_client() + response = client.get("/fetchFilterData") + + assert response.status_code == 200 + assert response.json() == {"filters": "mocked"} + + +def test_fetch_chart_data_with_filters_basic(create_test_client): + with patch("api.api_routes.ChartService") as MockChartService: + mock_instance = MockChartService.return_value + mock_instance.fetch_chart_data_with_filters = AsyncMock(return_value=[ + { + "id": "TOTAL_CALLS", + "chart_name": "Total Calls", + "chart_type": "card", + "chart_value": [ + {"name": "Total Calls", "value": float("nan"), "unit_of_measurement": ""} + ] + } + ]) + + client = create_test_client() + payload = { + "selected_filters": { + "Topic": ["Tech"], + "Sentiment": ["Positive"], + "DateRange": ["Last 30 Days"] + } + } + response = client.post("/fetchChartDataWithFilters", json=payload) + expected = [ + { + "id": "TOTAL_CALLS", + "chart_name": "Total Calls", + "chart_type": "card", + "chart_value": [ + {"name": "Total Calls", "value": None, "unit_of_measurement": ""} + ] + } + ] + assert response.status_code == 200 + assert response.json() == expected + +def test_fetch_chart_data_with_filters_error(create_test_client): + with patch("api.api_routes.ChartService") as MockChartService: + mock_instance = MockChartService.return_value + mock_instance.fetch_chart_data_with_filters = AsyncMock(side_effect=Exception("fail")) + + client = create_test_client() + payload = { + "selected_filters": { + "Topic": ["Tech"], + "Sentiment": ["Positive"], + "DateRange": ["Last 30 Days"] + } + } + response = client.post("/fetchChartDataWithFilters", json=payload) + + assert response.status_code == 500 + assert "error" in response.json() + + +def test_fetch_chart_data_error_handling(create_test_client): + with patch("api.api_routes.ChartService") as MockChartService: + mock_instance = MockChartService.return_value + mock_instance.fetch_chart_data = AsyncMock(side_effect=Exception("fail")) + + client = create_test_client() + response = client.get("/fetchChartData") + + assert response.status_code == 500 + assert "error" in response.json() + + +def test_chat_endpoint_basic(create_test_client): + with patch("api.api_routes.ChatService") as MockChatService: + mock_instance = MockChatService.return_value + mock_instance.stream_chat_request = AsyncMock(return_value=iter([b'{"message": "mocked stream"}'])) + + client = create_test_client() + payload = { + "conversation_id": "test", + "messages": [{"content": "Show me a chart"}], + "last_rag_response": "previous data" + } + + response = client.post("/chat", json=payload) + + assert response.status_code == 200 + assert response.json() == {"message": "mocked stream"} + + +def test_get_layout_config_valid(create_test_client, monkeypatch): + test_config = {"layout": "mocked"} + monkeypatch.setenv("REACT_APP_LAYOUT_CONFIG", json.dumps(test_config)) + + client = create_test_client() + response = client.get("/layout-config") + + assert response.status_code == 200 + assert response.json() == test_config + + +def test_get_layout_config_invalid_json(create_test_client, monkeypatch): + monkeypatch.setenv("REACT_APP_LAYOUT_CONFIG", "{bad json") + + client = create_test_client() + response = client.get("/layout-config") + + assert response.status_code == 400 + assert "error" in response.json() + + +def test_get_chart_config_found(create_test_client, monkeypatch): + monkeypatch.setenv("DISPLAY_CHART_DEFAULT", "true") + + client = create_test_client() + response = client.get("/display-chart-default") + + assert response.status_code == 200 + assert response.json() == {"isChartDisplayDefault": "true"} + + +def test_get_chart_config_missing(create_test_client, monkeypatch): + monkeypatch.delenv("DISPLAY_CHART_DEFAULT", raising=False) + + client = create_test_client() + response = client.get("/display-chart-default") + + assert response.status_code == 400 + assert "error" in response.json() + + +def test_fetch_filter_data_error_handling(create_test_client): + with patch("api.api_routes.ChartService") as MockChartService: + mock_instance = MockChartService.return_value + mock_instance.fetch_filter_data = AsyncMock(side_effect=Exception("fail")) + + client = create_test_client() + response = client.get("/fetchFilterData") + + assert response.status_code == 500 + assert "error" in response.json() + + +def test_layout_config_json_decode_error(create_test_client, monkeypatch): + monkeypatch.setenv("REACT_APP_LAYOUT_CONFIG", "not-a-json") + + client = create_test_client() + response = client.get("/layout-config") + + assert response.status_code == 400 + assert "error" in response.json() + + +def test_get_chart_config_success(create_test_client, monkeypatch): + monkeypatch.setenv("DISPLAY_CHART_DEFAULT", "false") + + client = create_test_client() + response = client.get("/display-chart-default") + + assert response.status_code == 200 + assert response.json() == {"isChartDisplayDefault": "false"} + + +def test_get_chart_config_env_missing(create_test_client, monkeypatch): + monkeypatch.delenv("DISPLAY_CHART_DEFAULT", raising=False) + + client = create_test_client() + response = client.get("/display-chart-default") + + assert response.status_code == 400 assert "error" in response.json() \ No newline at end of file diff --git a/src/tests/api/auth/test_auth_utils.py b/src/tests/api/auth/test_auth_utils.py index deda9c27f..05e660589 100644 --- a/src/tests/api/auth/test_auth_utils.py +++ b/src/tests/api/auth/test_auth_utils.py @@ -1,67 +1,67 @@ -import unittest -from unittest.mock import patch, MagicMock -import base64 -import json - -from auth import auth_utils,sample_user - - -class TestAuthUtils(unittest.TestCase): - - @patch("auth.sample_user") - def test_get_authenticated_user_details_dev_mode(self, mock_sample_user): - mock_sample_user.sample_user = { - "x-ms-client-principal-id": "123", - "x-ms-client-principal-name": "testuser", - "x-ms-client-principal-idp": "aad", - "x-ms-token-aad-id-token": "token123", - "x-ms-client-principal": "encodedstring" - } - - request_headers = {} - result = auth_utils.get_authenticated_user_details(request_headers) - - self.assertEqual(result["user_principal_id"], "123") - self.assertEqual(result["user_name"], "testuser") - self.assertEqual(result["auth_provider"], "aad") - self.assertEqual(result["auth_token"], "token123") - self.assertEqual(result["client_principal_b64"], "encodedstring") - self.assertEqual(result["aad_id_token"], "token123") - - def test_get_authenticated_user_details_prod_mode(self): - request_headers = { - "x-ms-client-principal-id": "123", - "x-ms-client-principal-name": "testuser", - "x-ms-client-principal-idp": "aad", - "x-ms-token-aad-id-token": "token123", - "x-ms-client-principal": "encodedstring" - } - - result = auth_utils.get_authenticated_user_details(request_headers) - - self.assertEqual(result["user_principal_id"], "123") - self.assertEqual(result["user_name"], "testuser") - self.assertEqual(result["auth_provider"], "aad") - self.assertEqual(result["auth_token"], "token123") - self.assertEqual(result["client_principal_b64"], "encodedstring") - self.assertEqual(result["aad_id_token"], "token123") - - def test_get_tenantid_valid_b64(self): - payload = {"tid": "tenant123"} - b64_encoded = base64.b64encode(json.dumps(payload).encode()).decode() - - result = auth_utils.get_tenantid(b64_encoded) - self.assertEqual(result, "tenant123") - - def test_get_tenantid_invalid_b64(self): - with self.assertLogs(level='ERROR'): - result = auth_utils.get_tenantid("notbase64!!!") - self.assertEqual(result, "") - - def test_get_tenantid_none(self): - result = auth_utils.get_tenantid(None) - self.assertEqual(result, "") - - -if __name__ == '__main__': - unittest.main() +import unittest +from unittest.mock import patch +import base64 +import json + +from auth import auth_utils,sample_user + + +class TestAuthUtils(unittest.TestCase): + + @patch("auth.sample_user") + def test_get_authenticated_user_details_dev_mode(self, mock_sample_user): + mock_sample_user.sample_user = { + "x-ms-client-principal-id": "123", + "x-ms-client-principal-name": "testuser", + "x-ms-client-principal-idp": "aad", + "x-ms-token-aad-id-token": "token123", + "x-ms-client-principal": "encodedstring" + } + + request_headers = {} + result = auth_utils.get_authenticated_user_details(request_headers) + + self.assertEqual(result["user_principal_id"], "123") + self.assertEqual(result["user_name"], "testuser") + self.assertEqual(result["auth_provider"], "aad") + self.assertEqual(result["auth_token"], "token123") + self.assertEqual(result["client_principal_b64"], "encodedstring") + self.assertEqual(result["aad_id_token"], "token123") + + def test_get_authenticated_user_details_prod_mode(self): + request_headers = { + "x-ms-client-principal-id": "123", + "x-ms-client-principal-name": "testuser", + "x-ms-client-principal-idp": "aad", + "x-ms-token-aad-id-token": "token123", + "x-ms-client-principal": "encodedstring" + } + + result = auth_utils.get_authenticated_user_details(request_headers) + + self.assertEqual(result["user_principal_id"], "123") + self.assertEqual(result["user_name"], "testuser") + self.assertEqual(result["auth_provider"], "aad") + self.assertEqual(result["auth_token"], "token123") + self.assertEqual(result["client_principal_b64"], "encodedstring") + self.assertEqual(result["aad_id_token"], "token123") + + def test_get_tenantid_valid_b64(self): + payload = {"tid": "tenant123"} + b64_encoded = base64.b64encode(json.dumps(payload).encode()).decode() + + result = auth_utils.get_tenantid(b64_encoded) + self.assertEqual(result, "tenant123") + + def test_get_tenantid_invalid_b64(self): + with self.assertLogs(level='ERROR'): + result = auth_utils.get_tenantid("notbase64!!!") + self.assertEqual(result, "") + + def test_get_tenantid_none(self): + result = auth_utils.get_tenantid(None) + self.assertEqual(result, "") + + +if __name__ == '__main__': + unittest.main() diff --git a/src/tests/api/common/logging/test_event_utils.py b/src/tests/api/common/logging/test_event_utils.py index 159367ea4..db633ff85 100644 --- a/src/tests/api/common/logging/test_event_utils.py +++ b/src/tests/api/common/logging/test_event_utils.py @@ -1,5 +1,5 @@ import logging -from unittest.mock import patch, MagicMock +from unittest.mock import patch import pytest from common.logging.event_utils import track_event_if_configured diff --git a/src/tests/api/helpers/test_utils.py b/src/tests/api/helpers/test_utils.py index 6fcff6c34..fec2b1190 100644 --- a/src/tests/api/helpers/test_utils.py +++ b/src/tests/api/helpers/test_utils.py @@ -1,223 +1,223 @@ -import pytest -import json -from unittest.mock import patch, AsyncMock, MagicMock - -import helpers.utils as utils - - -@pytest.mark.asyncio -async def test_format_as_ndjson_success(): - mock_data = [{"key": "value"}, {"another": "entry"}] - - async def async_gen(): - for item in mock_data: - yield item - - result = [] - async for line in utils.format_as_ndjson(async_gen()): - result.append(line.strip()) - - expected = [json.dumps(item) for item in mock_data] - assert result == expected - - -@pytest.mark.asyncio -async def test_format_as_ndjson_exception(): - async def async_gen(): - raise Exception("Test error") - yield - - result = [] - async for line in utils.format_as_ndjson(async_gen()): - result.append(json.loads(line.strip())) - assert result[0]["error"] == "Test error" - - -def test_parse_multi_columns_pipe(): - assert utils.parse_multi_columns("a|b|c") == ["a", "b", "c"] - - -def test_parse_multi_columns_comma(): - assert utils.parse_multi_columns("a,b,c") == ["a", "b", "c"] - - -@patch("helpers.utils.requests.get") -def test_fetchUserGroups_success(mock_get): - mock_response = { - "value": [{"id": "123"}], - } - mock_get.return_value.status_code = 200 - mock_get.return_value.json.return_value = mock_response - - result = utils.fetchUserGroups("fake_token") - assert result == [{"id": "123"}] - - -@patch("helpers.utils.requests.get") -def test_fetchUserGroups_with_nextLink(mock_get): - mock_response_1 = { - "value": [{"id": "123"}], - "@odata.nextLink": "next_link" - } - mock_response_2 = { - "value": [{"id": "456"}], - } - - def side_effect(url, headers): - mock = MagicMock() - if url == "https://graph.microsoft.com/v1.0/me/transitiveMemberOf?$select=id": - mock.status_code = 200 - mock.json.return_value = mock_response_1 - else: - mock.status_code = 200 - mock.json.return_value = mock_response_2 - return mock - - mock_get.side_effect = side_effect - - result = utils.fetchUserGroups("fake_token") - assert {"id": "123"} in result and {"id": "456"} in result - - -@patch("helpers.utils.requests.get", side_effect=Exception("Request error")) -def test_fetchUserGroups_exception(mock_get): - result = utils.fetchUserGroups("fake_token") - assert result == [] - - -@patch("helpers.utils.fetchUserGroups") -@patch("helpers.utils.AZURE_SEARCH_PERMITTED_GROUPS_COLUMN", "group_column") -def test_generateFilterString(mock_fetch): - mock_fetch.return_value = [{"id": "1"}, {"id": "2"}] - result = utils.generateFilterString("token") - assert "group_column/any(g:search.in(g, '1, 2'))" in result - - -@patch("helpers.utils.fetchUserGroups", return_value=[]) -@patch("helpers.utils.AZURE_SEARCH_PERMITTED_GROUPS_COLUMN", "group_column") -def test_generateFilterString_empty_groups(mock_fetch): - result = utils.generateFilterString("token") - assert "group_column/any(g:search.in(g, ''))" in result - - -def test_format_non_streaming_response_with_context(): - chatCompletion = MagicMock() - chatCompletion.id = "1" - chatCompletion.model = "gpt" - chatCompletion.created = 123 - chatCompletion.object = "chat" - message = MagicMock() - message.context = {"source": "test"} - message.content = "response" - choice = MagicMock() - choice.message = message - chatCompletion.choices = [choice] - - result = utils.format_non_streaming_response(chatCompletion, {"meta": 1}, "req-id") - assert result["choices"][0]["messages"][0]["role"] == "tool" - assert result["choices"][0]["messages"][1]["role"] == "assistant" - - -def test_format_non_streaming_response_no_choices(): - chatCompletion = MagicMock() - chatCompletion.id = "1" - chatCompletion.model = "gpt" - chatCompletion.created = 123 - chatCompletion.object = "chat" - chatCompletion.choices = [] - - result = utils.format_non_streaming_response(chatCompletion, {}, "req-id") - assert result == {} - - -def test_format_stream_response_with_context(): - chunk = MagicMock() - chunk.id = "1" - chunk.model = "gpt" - chunk.created = 123 - chunk.object = "chat" - delta = MagicMock() - delta.context = {"source": "stream"} - delta.role = "tool" - choice = MagicMock() - choice.delta = delta - chunk.choices = [choice] - - result = utils.format_stream_response(chunk, {"meta": 1}, "req-id") - assert result["choices"][0]["messages"][0]["role"] == "tool" - - -def test_format_stream_response_with_content(): - chunk = MagicMock() - chunk.id = "1" - chunk.model = "gpt" - chunk.created = 123 - chunk.object = "chat" - - delta = MagicMock() - delta.content = "Hello" - delta.role = "assistant" - # Ensure delta does NOT have a context attribute - del delta.context - - choice = MagicMock() - choice.delta = delta - - chunk.choices = [choice] - - result = utils.format_stream_response(chunk, {}, "req-id") - assert result["choices"][0]["messages"][0]["content"] == "Hello" - assert result["choices"][0]["messages"][0]["role"] == "assistant" - - -def test_format_stream_response_empty(): - chunk = MagicMock() - chunk.id = "1" - chunk.model = "gpt" - chunk.created = 123 - chunk.object = "chat" - chunk.choices = [] - - result = utils.format_stream_response(chunk, {}, "req-id") - assert result == {} - - -def test_format_pf_non_streaming_response_valid(): - chatCompletion = { - "id": "1", - "response": "Answer", - "citations": "Refs" - } - result = utils.format_pf_non_streaming_response( - chatCompletion, {}, "response", "citations" - ) - assert result["choices"][0]["messages"][0]["content"] == "Answer" - - -def test_format_pf_non_streaming_response_error_key(): - chatCompletion = {"error": "Failure"} - result = utils.format_pf_non_streaming_response(chatCompletion, {}, "r", "c") - assert result["error"] == "Failure" - - -def test_format_pf_non_streaming_response_none(): - result = utils.format_pf_non_streaming_response(None, {}, "r", "c") - assert "error" in result - - -def test_format_pf_non_streaming_response_exception(): - badCompletion = {"id": "1", "invalid": object()} - result = utils.format_pf_non_streaming_response(badCompletion, {}, "invalid", "c") - assert isinstance(result, dict) - - -def test_convert_to_pf_format_valid(): - input_json = { - "messages": [ - {"role": "user", "content": "Hello"}, - {"role": "assistant", "content": "Hi"} - ] - } - result = utils.convert_to_pf_format(input_json, "input", "output") - assert result[0]["inputs"]["input"] == "Hello" - assert result[0]["outputs"]["output"] == "Hi" +import pytest +import json +from unittest.mock import patch, MagicMock + +import helpers.utils as utils + + +@pytest.mark.asyncio +async def test_format_as_ndjson_success(): + mock_data = [{"key": "value"}, {"another": "entry"}] + + async def async_gen(): + for item in mock_data: + yield item + + result = [] + async for line in utils.format_as_ndjson(async_gen()): + result.append(line.strip()) + + expected = [json.dumps(item) for item in mock_data] + assert result == expected + + +@pytest.mark.asyncio +async def test_format_as_ndjson_exception(): + async def async_gen(): + raise Exception("Test error") + yield + + result = [] + async for line in utils.format_as_ndjson(async_gen()): + result.append(json.loads(line.strip())) + assert result[0]["error"] == "Test error" + + +def test_parse_multi_columns_pipe(): + assert utils.parse_multi_columns("a|b|c") == ["a", "b", "c"] + + +def test_parse_multi_columns_comma(): + assert utils.parse_multi_columns("a,b,c") == ["a", "b", "c"] + + +@patch("helpers.utils.requests.get") +def test_fetchUserGroups_success(mock_get): + mock_response = { + "value": [{"id": "123"}], + } + mock_get.return_value.status_code = 200 + mock_get.return_value.json.return_value = mock_response + + result = utils.fetchUserGroups("fake_token") + assert result == [{"id": "123"}] + + +@patch("helpers.utils.requests.get") +def test_fetchUserGroups_with_nextLink(mock_get): + mock_response_1 = { + "value": [{"id": "123"}], + "@odata.nextLink": "next_link" + } + mock_response_2 = { + "value": [{"id": "456"}], + } + + def side_effect(url, headers): + mock = MagicMock() + if url == "https://graph.microsoft.com/v1.0/me/transitiveMemberOf?$select=id": + mock.status_code = 200 + mock.json.return_value = mock_response_1 + else: + mock.status_code = 200 + mock.json.return_value = mock_response_2 + return mock + + mock_get.side_effect = side_effect + + result = utils.fetchUserGroups("fake_token") + assert {"id": "123"} in result and {"id": "456"} in result + + +@patch("helpers.utils.requests.get", side_effect=Exception("Request error")) +def test_fetchUserGroups_exception(mock_get): + result = utils.fetchUserGroups("fake_token") + assert result == [] + + +@patch("helpers.utils.fetchUserGroups") +@patch("helpers.utils.AZURE_SEARCH_PERMITTED_GROUPS_COLUMN", "group_column") +def test_generateFilterString(mock_fetch): + mock_fetch.return_value = [{"id": "1"}, {"id": "2"}] + result = utils.generateFilterString("token") + assert "group_column/any(g:search.in(g, '1, 2'))" in result + + +@patch("helpers.utils.fetchUserGroups", return_value=[]) +@patch("helpers.utils.AZURE_SEARCH_PERMITTED_GROUPS_COLUMN", "group_column") +def test_generateFilterString_empty_groups(mock_fetch): + result = utils.generateFilterString("token") + assert "group_column/any(g:search.in(g, ''))" in result + + +def test_format_non_streaming_response_with_context(): + chatCompletion = MagicMock() + chatCompletion.id = "1" + chatCompletion.model = "gpt" + chatCompletion.created = 123 + chatCompletion.object = "chat" + message = MagicMock() + message.context = {"source": "test"} + message.content = "response" + choice = MagicMock() + choice.message = message + chatCompletion.choices = [choice] + + result = utils.format_non_streaming_response(chatCompletion, {"meta": 1}, "req-id") + assert result["choices"][0]["messages"][0]["role"] == "tool" + assert result["choices"][0]["messages"][1]["role"] == "assistant" + + +def test_format_non_streaming_response_no_choices(): + chatCompletion = MagicMock() + chatCompletion.id = "1" + chatCompletion.model = "gpt" + chatCompletion.created = 123 + chatCompletion.object = "chat" + chatCompletion.choices = [] + + result = utils.format_non_streaming_response(chatCompletion, {}, "req-id") + assert result == {} + + +def test_format_stream_response_with_context(): + chunk = MagicMock() + chunk.id = "1" + chunk.model = "gpt" + chunk.created = 123 + chunk.object = "chat" + delta = MagicMock() + delta.context = {"source": "stream"} + delta.role = "tool" + choice = MagicMock() + choice.delta = delta + chunk.choices = [choice] + + result = utils.format_stream_response(chunk, {"meta": 1}, "req-id") + assert result["choices"][0]["messages"][0]["role"] == "tool" + + +def test_format_stream_response_with_content(): + chunk = MagicMock() + chunk.id = "1" + chunk.model = "gpt" + chunk.created = 123 + chunk.object = "chat" + + delta = MagicMock() + delta.content = "Hello" + delta.role = "assistant" + # Ensure delta does NOT have a context attribute + del delta.context + + choice = MagicMock() + choice.delta = delta + + chunk.choices = [choice] + + result = utils.format_stream_response(chunk, {}, "req-id") + assert result["choices"][0]["messages"][0]["content"] == "Hello" + assert result["choices"][0]["messages"][0]["role"] == "assistant" + + +def test_format_stream_response_empty(): + chunk = MagicMock() + chunk.id = "1" + chunk.model = "gpt" + chunk.created = 123 + chunk.object = "chat" + chunk.choices = [] + + result = utils.format_stream_response(chunk, {}, "req-id") + assert result == {} + + +def test_format_pf_non_streaming_response_valid(): + chatCompletion = { + "id": "1", + "response": "Answer", + "citations": "Refs" + } + result = utils.format_pf_non_streaming_response( + chatCompletion, {}, "response", "citations" + ) + assert result["choices"][0]["messages"][0]["content"] == "Answer" + + +def test_format_pf_non_streaming_response_error_key(): + chatCompletion = {"error": "Failure"} + result = utils.format_pf_non_streaming_response(chatCompletion, {}, "r", "c") + assert result["error"] == "Failure" + + +def test_format_pf_non_streaming_response_none(): + result = utils.format_pf_non_streaming_response(None, {}, "r", "c") + assert "error" in result + + +def test_format_pf_non_streaming_response_exception(): + badCompletion = {"id": "1", "invalid": object()} + result = utils.format_pf_non_streaming_response(badCompletion, {}, "invalid", "c") + assert isinstance(result, dict) + + +def test_convert_to_pf_format_valid(): + input_json = { + "messages": [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi"} + ] + } + result = utils.convert_to_pf_format(input_json, "input", "output") + assert result[0]["inputs"]["input"] == "Hello" + assert result[0]["outputs"]["output"] == "Hi" diff --git a/src/tests/api/services/test_chat_service.py b/src/tests/api/services/test_chat_service.py index 79a39d8d0..9f372483a 100644 --- a/src/tests/api/services/test_chat_service.py +++ b/src/tests/api/services/test_chat_service.py @@ -1,4 +1,3 @@ -import asyncio import json import time from unittest.mock import AsyncMock, MagicMock, patch diff --git a/src/tests/api/services/test_history_service.py b/src/tests/api/services/test_history_service.py index 9dcd2be64..f6ea4314f 100644 --- a/src/tests/api/services/test_history_service.py +++ b/src/tests/api/services/test_history_service.py @@ -4,7 +4,6 @@ # ---- Import service under test ---- from services.history_service import HistoryService -from azure.ai.agents.models import MessageRole @pytest.fixture diff --git a/tests/e2e-test/base/base.py b/tests/e2e-test/base/base.py index 5114809cc..c14e79924 100644 --- a/tests/e2e-test/base/base.py +++ b/tests/e2e-test/base/base.py @@ -1,55 +1,52 @@ -""" -BasePage Module -Contains base page object class with common methods -""" -from config.constants import * -import json -from dotenv import load_dotenv -import os -import uuid -import time - -class BasePage: - def __init__(self, page): - self.page = page - - def scroll_into_view(self,locator): - reference_list = locator - locator.nth(reference_list.count()-1).scroll_into_view_if_needed() - - def is_visible(self,locator): - locator.is_visible() - - def validate_response_status(self,questions): - load_dotenv() - WEB_URL = os.getenv("web_url") - - url = f"{API_URL}/api/chat" - - - user_message_id = str(uuid.uuid4()) - assistant_message_id = str(uuid.uuid4()) - conversation_id = str(uuid.uuid4()) - - payload = { - "messages": [{"role": "user", "content": questions, - "id": user_message_id}], - "conversation_id": conversation_id, - } - # Serialize the payload to JSON - payload_json = json.dumps(payload) - headers = { - "Content-Type": "application/json-lines", - "Accept": "*/*" - } - # response = self.page.request.post(url, headers=headers, data=payload_json, timeout=60000) - start = time.time() - response = self.page.request.post(url, headers=headers, data=payload_json, timeout=90000) - duration = time.time() - start - - print(f"✅succeeded in {duration:.2f}s") - # Check the response status code - assert response.status == 200, "response code is " + str(response.status) - - self.page.wait_for_timeout(4000) - +""" +BasePage Module +Contains base page object class with common methods +""" +from config.constants import * +import json +from dotenv import load_dotenv +import uuid +import time + +class BasePage: + def __init__(self, page): + self.page = page + + def scroll_into_view(self,locator): + reference_list = locator + locator.nth(reference_list.count()-1).scroll_into_view_if_needed() + + def is_visible(self,locator): + locator.is_visible() + + def validate_response_status(self,questions): + load_dotenv() + + url = f"{API_URL}/api/chat" + + + user_message_id = str(uuid.uuid4()) + conversation_id = str(uuid.uuid4()) + + payload = { + "messages": [{"role": "user", "content": questions, + "id": user_message_id}], + "conversation_id": conversation_id, + } + # Serialize the payload to JSON + payload_json = json.dumps(payload) + headers = { + "Content-Type": "application/json-lines", + "Accept": "*/*" + } + # response = self.page.request.post(url, headers=headers, data=payload_json, timeout=60000) + start = time.time() + response = self.page.request.post(url, headers=headers, data=payload_json, timeout=90000) + duration = time.time() - start + + print(f"✅succeeded in {duration:.2f}s") + # Check the response status code + assert response.status == 200, "response code is " + str(response.status) + + self.page.wait_for_timeout(4000) + diff --git a/tests/e2e-test/config/constants.py b/tests/e2e-test/config/constants.py index 5dcb76b26..8a7ac7d61 100644 --- a/tests/e2e-test/config/constants.py +++ b/tests/e2e-test/config/constants.py @@ -1,38 +1,38 @@ -""" -Constants Module -Contains configuration constants and loads test data -""" -from dotenv import load_dotenv -import os -import json - -load_dotenv() -URL = os.getenv('url') -if URL.endswith('/'): - URL = URL[:-1] - -load_dotenv() -API_URL = os.getenv('api_url') -if API_URL.endswith('/'): - API_URL = API_URL[:-1] - -# Get the absolute path to the repository root -repo_root = os.getenv('GITHUB_WORKSPACE', os.getcwd()) - -#remove 'tests/e2e-test' from below path if running locally - -# Load Telecom prompts -telecom_json_file_path = os.path.join(repo_root, 'tests/e2e-test', 'testdata', 'telecom_prompts.json') -with open(telecom_json_file_path, 'r') as file: - telecom_data = json.load(file) - telecom_questions = telecom_data['questions'] - -# Load ITHelpdesk prompts -ithelpdesk_json_file_path = os.path.join(repo_root, 'tests/e2e-test', 'testdata', 'ithelpdesk_prompts.json') -with open(ithelpdesk_json_file_path, 'r') as file: - ithelpdesk_data = json.load(file) - ithelpdesk_questions = ithelpdesk_data['questions'] - -# Backward compatibility - keep 'questions' as alias for telecom_questions -questions = telecom_questions - +""" +Constants Module +Contains configuration constants and loads test data +""" +from dotenv import load_dotenv +import os +import json + +load_dotenv() +URL = os.getenv('url') +if URL.endswith('/'): + URL = URL[:-1] + +load_dotenv() +API_URL = os.getenv('api_url') +if API_URL.endswith('/'): + API_URL = API_URL[:-1] + +# Get the absolute path to the repository root +repo_root = os.getenv('GITHUB_WORKSPACE', os.getcwd()) + +#remove 'tests/e2e-test' from below path if running locally + +# Load Telecom prompts +telecom_json_file_path = os.path.join(repo_root, 'tests/e2e-test', 'testdata', 'telecom_prompts.json') +with open(telecom_json_file_path, 'r') as file: + telecom_data = json.load(file) + telecom_questions = telecom_data['questions'] + +# Load ITHelpdesk prompts +ithelpdesk_json_file_path = os.path.join(repo_root, 'tests/e2e-test', 'testdata', 'ithelpdesk_prompts.json') +with open(ithelpdesk_json_file_path, 'r') as file: + ithelpdesk_data = json.load(file) + ithelpdesk_questions = ithelpdesk_data['questions'] + +# Backward compatibility - keep 'questions' as alias for telecom_questions +questions = telecom_questions + diff --git a/tests/e2e-test/pages/HomePage.py b/tests/e2e-test/pages/HomePage.py index 775de27ff..0722d9e71 100644 --- a/tests/e2e-test/pages/HomePage.py +++ b/tests/e2e-test/pages/HomePage.py @@ -23,6 +23,7 @@ class HomePage(BasePage): def __init__(self, page): + super().__init__(page) self.page = page def home_page_load(self): diff --git a/tests/e2e-test/pages/KMGenericPage.py b/tests/e2e-test/pages/KMGenericPage.py index 8a3957b74..493e81592 100644 --- a/tests/e2e-test/pages/KMGenericPage.py +++ b/tests/e2e-test/pages/KMGenericPage.py @@ -7,6 +7,7 @@ class KMGenericPage(BasePage): def __init__(self, page): + super().__init__(page) self.page = page def open_url(self): diff --git a/tests/e2e-test/pages/loginPage.py b/tests/e2e-test/pages/loginPage.py index 0ee59f779..b3205b8e4 100644 --- a/tests/e2e-test/pages/loginPage.py +++ b/tests/e2e-test/pages/loginPage.py @@ -11,6 +11,7 @@ class LoginPage(BasePage): PERMISSION_ACCEPT_BUTTON = "//input[@type='submit']" def __init__(self, page): + super().__init__(page) self.page = page def authenticate(self, username,password): diff --git a/tests/e2e-test/tests/test_ithelpdesk_smoke_tc.py b/tests/e2e-test/tests/test_ithelpdesk_smoke_tc.py index 100348ec3..e24cb38f9 100644 --- a/tests/e2e-test/tests/test_ithelpdesk_smoke_tc.py +++ b/tests/e2e-test/tests/test_ithelpdesk_smoke_tc.py @@ -1,362 +1,361 @@ -""" -KM Generic Smoke Test Module - ITHelpdesk -Tests the complete smoke testing workflow for KM Generic application -""" -from pages.KMGenericPage import KMGenericPage -import logging -from pages.HomePage import HomePage -from playwright.sync_api import expect -import time -from config.constants import ithelpdesk_questions -import io - -logger = logging.getLogger(__name__) - - -def test_user_filter_functioning(login_logout, request): - """ - KM Generic Smoke Test - ITHelpdesk: - 1. Open KM Generic URL - 2. Validate charts, labels, chat & history panels - 3. Confirm user filter is visible - 4. Change filter combinations - 5. Click Apply - 6. Verify screen blur + chart update - """ - - # Set custom test name for pytest HTML report - request.node._nodeid = "14480 - KM Generic - ITHelpdesk - Validate filter functionality should work as per filtered data selected" - - page = login_logout - km_page = KMGenericPage(page) - - logger.info("Step 1: Open KM Generic URL") - km_page.open_url() - - logger.info("Step 2: Validate charts, labels, chat & history panels") - km_page.validate_dashboard_ui() - - logger.info("Step 3: Confirm user filter is visible") - km_page.validate_user_filter_visible() - - logger.info("Step 4: Change filter combinations") - km_page.update_filters() - - logger.info("Step 5: Click Apply") - km_page.click_apply_button() - - logger.info("Step 6: Verify screen blur + chart update") - km_page.verify_blur_and_chart_update() - - -def test_after_filter_functioning(login_logout, request): - """ - KM Generic Smoke Test - ITHelpdesk: - 1. Open KM Generic URL - 2. Changes the value of user filter - 3. Notice the value/data change in the chart/graphs tables - """ - - # Remove custom test name logic for pytest HTML report - request.node._nodeid = "14482 - KM Generic - ITHelpdesk - Validate after applying filter charts/graphs should show filtered data" - - page = login_logout - km_page = KMGenericPage(page) - - logger.info("Step 1: Open KM Generic URL") - km_page.open_url() - - logger.info("Step 2: Changes the value of user filter") - km_page.update_filters() - - logger.info("Step 3: Click Apply") - km_page.click_apply_button() - - logger.info("Step 4: Validate filter data is reflecting in charts/graphs") - performance_issue_data = km_page.validate_trending_topics_entry("Laptop Performance Issues") - logger.info(f"Laptop performance issues data validated: {performance_issue_data}") - - km_page.validate_dashboard_charts() - - -def test_hide_dashboard_and_chat_buttons(login_logout, request): - """ - KM Generic Smoke Test - ITHelpdesk: - 1. Open KM Generic URL - 2. Changes the value of user filter - 3. Notice the value/data change in the chart/graphs tables - """ - - # Set custom test name for pytest HTML report - request.node._nodeid = "14485 - KM Generic - ITHelpdesk - Validate Hide Dashboard and Hide Chat buttons" - - page = login_logout - km_page = KMGenericPage(page) - - logger.info("Step 1: Open KM Generic URL") - km_page.open_url() - - logger.info("Step 2: On the left side of profile icon observe two buttons are present, Hide Dashboard & Hide Chat") - km_page.verify_hide_dashboard_and_chat_buttons() - - -def test_refine_chat_chart_output(login_logout, request): - """ - KM Generic Smoke Test - ITHelpdesk: - 1. Open KM Generic URL - 2. On chat window enter the prompt which provides chat info: EX: Average handling time by topic - 3. On chat window enter the prompt which provides chat info: EX: Generate Chart - """ - - # Set custom test name for pytest HTML report - request.node._nodeid = "14526 - US_12962_KM Generic - ITHelpdesk - Improve Chart Generation Experience in Chat" - - page = login_logout - km_page = KMGenericPage(page) - home_page = HomePage(page) - - logger.info("Step 1: Open KM Generic URL") - km_page.open_url() - - logger.info("Step 2: Verify chat response generation") - logger.info("Step 3: On chat window enter the prompt which provides chat info: EX: Average handling time by topic") - home_page.validate_chat_response('Average handling time by topic') - home_page.validate_response_status('Average handling time by topic') - - logger.info("Step 4: On chat window enter the prompt which provides chat info: EX: Generate chart") - home_page.validate_chat_response('Generate chart', True) - home_page.validate_response_status('Generate chart') - - -def test_chat_greeting_responses(login_logout, request): - - """ - KM Generic Smoke Test - ITHelpdesk: - 1. Deploy KM Generic - 2. Open KM Generic URL - 3. On chat window enter the Greeting related info: EX: Hi, Good morning, Hello. - """ - - # Set custom test name for pytest HTML report - request.node._nodeid = "21426 - US_20054_KM Generic - ITHelpdesk - Greeting related experience in Chat" - - page = login_logout - km_page = KMGenericPage(page) - home_page = HomePage(page) - - logger.info("Step 1: Open KM Generic URL") - km_page.open_url() - - greetings = ["Hi, Good morning", "Hello"] - logger.info("Step 2: On chat window enter the Greeting related info: EX: Hi, Good morning, Hello.") - for greeting in greetings: - home_page.enter_chat_question(greeting) - home_page.click_send_button() - - # Check last assistant message for a greeting-style reply - assistant_messages = home_page.page.locator("div.chat-message.assistant") - last_message = assistant_messages.last - - # Validate greeting response - p = last_message.locator("p") - message_text = p.inner_text().lower() - - if any(keyword in message_text for keyword in ["how can i assist", "how can i help", "hello again"]): - logger.info(f"Valid greeting response received for: {greeting}") - else: - raise AssertionError(f"Unexpected greeting response for '{greeting}': {message_text}") - - # Optional wait between messages - home_page.page.wait_for_timeout(1000) - - -def test_chat_history_panel(login_logout, request): - """ - KM Generic Smoke Test - ITHelpdesk: - Refactored to reuse golden path logic plus additional chat history operations - 1. Reuse golden path test execution (load home page, delete history, execute questions) - 2. Edit chat thread title - 3. Verify chat history operations (delete thread, create new chat, clear all history) - """ - - # Set custom test name for pytest HTML report - request.node._nodeid = "14483 - KM Generic - ITHelpdesk - Validate Chat History- user able to edit, save, delete and delete all chat history" - - page = login_logout - home_page = HomePage(page) - home_page.page = page - - log_capture = io.StringIO() - handler = logging.StreamHandler(log_capture) - logger.addHandler(handler) - - try: - # Reuse golden path logic - Steps 1-2: Load home page and clear chat history - logger.info("Step 1: Validate home page is loaded") - start = time.time() - home_page.home_page_load() - duration = time.time() - start - logger.info(f"Execution Time for 'Validate home page is loaded': {duration:.2f}s") - - logger.info("Step 2: Validate delete chat history") - start = time.time() - home_page.delete_chat_history() - duration = time.time() - start - logger.info(f"Execution Time for 'Validate delete chat history': {duration:.2f}s") - - # Reuse golden path logic - Execute all golden path questions - failed_questions = [] # Track failed questions for final reporting - - for i, question in enumerate(ithelpdesk_questions, start=1): - logger.info(f"Step {i+2}: Validate response for GP Prompt: {question}") - start = time.time() - - # Retry logic: attempt up to 2 times if response is invalid - max_retries = 2 - question_passed = False - - for attempt in range(max_retries): - try: - # Enter question and get response - home_page.enter_chat_question(question) - home_page.click_send_button() - home_page.page.wait_for_timeout(8000) # Wait before validating response status - home_page.validate_response_status(question) - home_page.page.wait_for_timeout(5000) # Wait after validating response status - home_page.validate_response_text(question) - - # If we reach here, the response was valid - break out of retry loop - logger.info(f"[{question}] Valid response received on attempt {attempt + 1}") - question_passed = True - break - - except Exception as e: - if attempt < max_retries - 1: # Not the last attempt - logger.warning(f"[{question}] Attempt {attempt + 1} failed: {str(e)}") - logger.info(f"[{question}] Retrying... (attempt {attempt + 2}/{max_retries})") - # Wait a bit before retrying - home_page.page.wait_for_timeout(10000) - else: # Last attempt failed - logger.error(f"[{question}] All {max_retries} attempts failed. Last error: {str(e)}") - failed_questions.append({"question": question, "error": str(e)}) - - # Only handle citations if the question passed - if question_passed and home_page.has_reference_link(): - logger.info(f"[{question}] Reference link found. Opening citation.") - home_page.click_reference_link_in_response() - logger.info(f"[{question}] Closing citation.") - home_page.close_citation() - - duration = time.time() - start - logger.info(f"Execution Time for 'Validate response for GP Prompt: {question}': {duration:.2f}s") - - # Log summary of failed questions - if failed_questions: - logger.warning(f"Chat history test completed with {len(failed_questions)} failed questions out of {len(ithelpdesk_questions)} total") - for failed in failed_questions: - logger.error(f"Failed question: '{failed['question']}' - {failed['error']}") - else: - logger.info("All golden path questions passed successfully") - - # Additional chat history specific operations - logger.info("Step 7: Try editing the title of chat thread") - home_page.edit_chat_title("Updated Title") - - home_page.page.wait_for_timeout(2000) - - logger.info("Step 8: Verify the chat history is getting stored properly or not") - logger.info("Step 9: Try deleting the chat thread from chat history panel") - home_page.delete_first_chat_thread() - - home_page.page.wait_for_timeout(2000) - - logger.info("Step 10: Try clicking on + icon present before chat box") - home_page.create_new_chat() - - home_page.page.wait_for_timeout(2000) - - home_page.close_chat_history() - - logger.info("Step 11: Click on eclipse (3 dots) and select Clear all chat history") - home_page.delete_chat_history() - - finally: - logger.removeHandler(handler) - - -def test_clear_citations_on_chat_delete(login_logout, request): - """ - KM Generic Smoke Test - ITHelpdesk: - 1. Open KM Generic URL - 2. Ask questions in the chat area, where the citations are provided. - 3. Click on the any citation link. - 4. Open Chat history panel. - 5. In chat history panel delete complete chat history. - 6. Observe Citation Section. - """ - - # Set custom test name for pytest HTML report - request.node._nodeid = "18631 - Bug 17326 - KM Generic - ITHelpdesk - Citation should get cleared after deleting complete chat history" - - page = login_logout - km_page = KMGenericPage(page) - home_page = HomePage(page) - - logger.info("Step 2: Send a query to trigger a citation") - question= "Provide a summary of performance issues users reported this week" - home_page.enter_chat_question(question) - home_page.click_send_button() - # home_page.validate_chat_response(question) - home_page.page.wait_for_timeout(3000) - - logger.info("Step 3: Validate citation link appears in response") - logger.info("Step 4: Click on the citation link to open the panel") - home_page.click_reference_link_in_response() - home_page.page.wait_for_timeout(5000) - - # 6. Delete entire chat history - home_page.delete_chat_history() - - # 7. Check citation section is not visible after chat history deletion - citations_locator = page.locator("//div[contains(text(),'Citations')]") - expect(citations_locator).not_to_be_visible(timeout=3000) - logger.info("Citations section is not visible after chat history deletion") - - -def test_citation_panel_closes_with_chat(login_logout, request): - """ - Test to ensure citation panel closes when chat section is hidden. - """ - - # Set custom test name for pytest HTML report - request.node._nodeid = "19433 - KM Generic - ITHelpdesk - Citation panel should close after hiding chat" - - page = login_logout - km_page = KMGenericPage(page) - home_page = HomePage(page) - - logger.info("Step 1: Navigate to KM Generic URL") - home_page.page.reload(wait_until="networkidle") - home_page.page.wait_for_timeout(2000) - - logger.info("Step 2: Send a query to trigger a citation") - question= "Provide a summary of performance issues users reported this week" - home_page.enter_chat_question(question) - home_page.click_send_button() - # home_page.validate_chat_response(question) - home_page.page.wait_for_timeout(3000) - - logger.info("Step 3: Validate citation link appears in response") - logger.info("Step 4: Click on the citation link to open the panel") - home_page.click_reference_link_in_response() - home_page.page.wait_for_timeout(3000) - - logger.info("Step 5: Click on 'Hide Chat' button") - km_page.verify_hide_dashboard_and_chat_buttons() - home_page.page.wait_for_timeout(3000) - - logger.info("Step 6: Verify citation panel is closed after hiding chat") - citation_panel = km_page.page.locator("div.citationPanel") - expect(citation_panel).not_to_be_visible(timeout=3000) - - logger.info("✅ Citation panel successfully closed with chat.") +""" +KM Generic Smoke Test Module - ITHelpdesk +Tests the complete smoke testing workflow for KM Generic application +""" +from pages.KMGenericPage import KMGenericPage +import logging +from pages.HomePage import HomePage +from playwright.sync_api import expect +import time +from config.constants import ithelpdesk_questions +import io + +logger = logging.getLogger(__name__) + + +def test_user_filter_functioning(login_logout, request): + """ + KM Generic Smoke Test - ITHelpdesk: + 1. Open KM Generic URL + 2. Validate charts, labels, chat & history panels + 3. Confirm user filter is visible + 4. Change filter combinations + 5. Click Apply + 6. Verify screen blur + chart update + """ + + # Set custom test name for pytest HTML report + request.node._nodeid = "14480 - KM Generic - ITHelpdesk - Validate filter functionality should work as per filtered data selected" + + page = login_logout + km_page = KMGenericPage(page) + + logger.info("Step 1: Open KM Generic URL") + km_page.open_url() + + logger.info("Step 2: Validate charts, labels, chat & history panels") + km_page.validate_dashboard_ui() + + logger.info("Step 3: Confirm user filter is visible") + km_page.validate_user_filter_visible() + + logger.info("Step 4: Change filter combinations") + km_page.update_filters() + + logger.info("Step 5: Click Apply") + km_page.click_apply_button() + + logger.info("Step 6: Verify screen blur + chart update") + km_page.verify_blur_and_chart_update() + + +def test_after_filter_functioning(login_logout, request): + """ + KM Generic Smoke Test - ITHelpdesk: + 1. Open KM Generic URL + 2. Changes the value of user filter + 3. Notice the value/data change in the chart/graphs tables + """ + + # Remove custom test name logic for pytest HTML report + request.node._nodeid = "14482 - KM Generic - ITHelpdesk - Validate after applying filter charts/graphs should show filtered data" + + page = login_logout + km_page = KMGenericPage(page) + + logger.info("Step 1: Open KM Generic URL") + km_page.open_url() + + logger.info("Step 2: Changes the value of user filter") + km_page.update_filters() + + logger.info("Step 3: Click Apply") + km_page.click_apply_button() + + logger.info("Step 4: Validate filter data is reflecting in charts/graphs") + performance_issue_data = km_page.validate_trending_topics_entry("Laptop Performance Issues") + logger.info(f"Laptop performance issues data validated: {performance_issue_data}") + + km_page.validate_dashboard_charts() + + +def test_hide_dashboard_and_chat_buttons(login_logout, request): + """ + KM Generic Smoke Test - ITHelpdesk: + 1. Open KM Generic URL + 2. Changes the value of user filter + 3. Notice the value/data change in the chart/graphs tables + """ + + # Set custom test name for pytest HTML report + request.node._nodeid = "14485 - KM Generic - ITHelpdesk - Validate Hide Dashboard and Hide Chat buttons" + + page = login_logout + km_page = KMGenericPage(page) + + logger.info("Step 1: Open KM Generic URL") + km_page.open_url() + + logger.info("Step 2: On the left side of profile icon observe two buttons are present, Hide Dashboard & Hide Chat") + km_page.verify_hide_dashboard_and_chat_buttons() + + +def test_refine_chat_chart_output(login_logout, request): + """ + KM Generic Smoke Test - ITHelpdesk: + 1. Open KM Generic URL + 2. On chat window enter the prompt which provides chat info: EX: Average handling time by topic + 3. On chat window enter the prompt which provides chat info: EX: Generate Chart + """ + + # Set custom test name for pytest HTML report + request.node._nodeid = "14526 - US_12962_KM Generic - ITHelpdesk - Improve Chart Generation Experience in Chat" + + page = login_logout + km_page = KMGenericPage(page) + home_page = HomePage(page) + + logger.info("Step 1: Open KM Generic URL") + km_page.open_url() + + logger.info("Step 2: Verify chat response generation") + logger.info("Step 3: On chat window enter the prompt which provides chat info: EX: Average handling time by topic") + home_page.validate_chat_response('Average handling time by topic') + home_page.validate_response_status('Average handling time by topic') + + logger.info("Step 4: On chat window enter the prompt which provides chat info: EX: Generate chart") + home_page.validate_chat_response('Generate chart', True) + home_page.validate_response_status('Generate chart') + + +def test_chat_greeting_responses(login_logout, request): + + """ + KM Generic Smoke Test - ITHelpdesk: + 1. Deploy KM Generic + 2. Open KM Generic URL + 3. On chat window enter the Greeting related info: EX: Hi, Good morning, Hello. + """ + + # Set custom test name for pytest HTML report + request.node._nodeid = "21426 - US_20054_KM Generic - ITHelpdesk - Greeting related experience in Chat" + + page = login_logout + km_page = KMGenericPage(page) + home_page = HomePage(page) + + logger.info("Step 1: Open KM Generic URL") + km_page.open_url() + + greetings = ["Hi, Good morning", "Hello"] + logger.info("Step 2: On chat window enter the Greeting related info: EX: Hi, Good morning, Hello.") + for greeting in greetings: + home_page.enter_chat_question(greeting) + home_page.click_send_button() + + # Check last assistant message for a greeting-style reply + assistant_messages = home_page.page.locator("div.chat-message.assistant") + last_message = assistant_messages.last + + # Validate greeting response + p = last_message.locator("p") + message_text = p.inner_text().lower() + + if any(keyword in message_text for keyword in ["how can i assist", "how can i help", "hello again"]): + logger.info(f"Valid greeting response received for: {greeting}") + else: + raise AssertionError(f"Unexpected greeting response for '{greeting}': {message_text}") + + # Optional wait between messages + home_page.page.wait_for_timeout(1000) + + +def test_chat_history_panel(login_logout, request): + """ + KM Generic Smoke Test - ITHelpdesk: + Refactored to reuse golden path logic plus additional chat history operations + 1. Reuse golden path test execution (load home page, delete history, execute questions) + 2. Edit chat thread title + 3. Verify chat history operations (delete thread, create new chat, clear all history) + """ + + # Set custom test name for pytest HTML report + request.node._nodeid = "14483 - KM Generic - ITHelpdesk - Validate Chat History- user able to edit, save, delete and delete all chat history" + + page = login_logout + home_page = HomePage(page) + home_page.page = page + + log_capture = io.StringIO() + handler = logging.StreamHandler(log_capture) + logger.addHandler(handler) + + try: + # Reuse golden path logic - Steps 1-2: Load home page and clear chat history + logger.info("Step 1: Validate home page is loaded") + start = time.time() + home_page.home_page_load() + duration = time.time() - start + logger.info(f"Execution Time for 'Validate home page is loaded': {duration:.2f}s") + + logger.info("Step 2: Validate delete chat history") + start = time.time() + home_page.delete_chat_history() + duration = time.time() - start + logger.info(f"Execution Time for 'Validate delete chat history': {duration:.2f}s") + + # Reuse golden path logic - Execute all golden path questions + failed_questions = [] # Track failed questions for final reporting + + for i, question in enumerate(ithelpdesk_questions, start=1): + logger.info(f"Step {i+2}: Validate response for GP Prompt: {question}") + start = time.time() + + # Retry logic: attempt up to 2 times if response is invalid + max_retries = 2 + question_passed = False + + for attempt in range(max_retries): + try: + # Enter question and get response + home_page.enter_chat_question(question) + home_page.click_send_button() + home_page.page.wait_for_timeout(8000) # Wait before validating response status + home_page.validate_response_status(question) + home_page.page.wait_for_timeout(5000) # Wait after validating response status + home_page.validate_response_text(question) + + # If we reach here, the response was valid - break out of retry loop + logger.info(f"[{question}] Valid response received on attempt {attempt + 1}") + question_passed = True + break + + except Exception as e: + if attempt < max_retries - 1: # Not the last attempt + logger.warning(f"[{question}] Attempt {attempt + 1} failed: {str(e)}") + logger.info(f"[{question}] Retrying... (attempt {attempt + 2}/{max_retries})") + # Wait a bit before retrying + home_page.page.wait_for_timeout(10000) + else: # Last attempt failed + logger.error(f"[{question}] All {max_retries} attempts failed. Last error: {str(e)}") + failed_questions.append({"question": question, "error": str(e)}) + + # Only handle citations if the question passed + if question_passed and home_page.has_reference_link(): + logger.info(f"[{question}] Reference link found. Opening citation.") + home_page.click_reference_link_in_response() + logger.info(f"[{question}] Closing citation.") + home_page.close_citation() + + duration = time.time() - start + logger.info(f"Execution Time for 'Validate response for GP Prompt: {question}': {duration:.2f}s") + + # Log summary of failed questions + if failed_questions: + logger.warning(f"Chat history test completed with {len(failed_questions)} failed questions out of {len(ithelpdesk_questions)} total") + for failed in failed_questions: + logger.error(f"Failed question: '{failed['question']}' - {failed['error']}") + else: + logger.info("All golden path questions passed successfully") + + # Additional chat history specific operations + logger.info("Step 7: Try editing the title of chat thread") + home_page.edit_chat_title("Updated Title") + + home_page.page.wait_for_timeout(2000) + + logger.info("Step 8: Verify the chat history is getting stored properly or not") + logger.info("Step 9: Try deleting the chat thread from chat history panel") + home_page.delete_first_chat_thread() + + home_page.page.wait_for_timeout(2000) + + logger.info("Step 10: Try clicking on + icon present before chat box") + home_page.create_new_chat() + + home_page.page.wait_for_timeout(2000) + + home_page.close_chat_history() + + logger.info("Step 11: Click on eclipse (3 dots) and select Clear all chat history") + home_page.delete_chat_history() + + finally: + logger.removeHandler(handler) + + +def test_clear_citations_on_chat_delete(login_logout, request): + """ + KM Generic Smoke Test - ITHelpdesk: + 1. Open KM Generic URL + 2. Ask questions in the chat area, where the citations are provided. + 3. Click on the any citation link. + 4. Open Chat history panel. + 5. In chat history panel delete complete chat history. + 6. Observe Citation Section. + """ + + # Set custom test name for pytest HTML report + request.node._nodeid = "18631 - Bug 17326 - KM Generic - ITHelpdesk - Citation should get cleared after deleting complete chat history" + + page = login_logout + home_page = HomePage(page) + + logger.info("Step 2: Send a query to trigger a citation") + question= "Provide a summary of performance issues users reported this week" + home_page.enter_chat_question(question) + home_page.click_send_button() + # home_page.validate_chat_response(question) + home_page.page.wait_for_timeout(3000) + + logger.info("Step 3: Validate citation link appears in response") + logger.info("Step 4: Click on the citation link to open the panel") + home_page.click_reference_link_in_response() + home_page.page.wait_for_timeout(5000) + + # 6. Delete entire chat history + home_page.delete_chat_history() + + # 7. Check citation section is not visible after chat history deletion + citations_locator = page.locator("//div[contains(text(),'Citations')]") + expect(citations_locator).not_to_be_visible(timeout=3000) + logger.info("Citations section is not visible after chat history deletion") + + +def test_citation_panel_closes_with_chat(login_logout, request): + """ + Test to ensure citation panel closes when chat section is hidden. + """ + + # Set custom test name for pytest HTML report + request.node._nodeid = "19433 - KM Generic - ITHelpdesk - Citation panel should close after hiding chat" + + page = login_logout + km_page = KMGenericPage(page) + home_page = HomePage(page) + + logger.info("Step 1: Navigate to KM Generic URL") + home_page.page.reload(wait_until="networkidle") + home_page.page.wait_for_timeout(2000) + + logger.info("Step 2: Send a query to trigger a citation") + question= "Provide a summary of performance issues users reported this week" + home_page.enter_chat_question(question) + home_page.click_send_button() + # home_page.validate_chat_response(question) + home_page.page.wait_for_timeout(3000) + + logger.info("Step 3: Validate citation link appears in response") + logger.info("Step 4: Click on the citation link to open the panel") + home_page.click_reference_link_in_response() + home_page.page.wait_for_timeout(3000) + + logger.info("Step 5: Click on 'Hide Chat' button") + km_page.verify_hide_dashboard_and_chat_buttons() + home_page.page.wait_for_timeout(3000) + + logger.info("Step 6: Verify citation panel is closed after hiding chat") + citation_panel = km_page.page.locator("div.citationPanel") + expect(citation_panel).not_to_be_visible(timeout=3000) + + logger.info("✅ Citation panel successfully closed with chat.") diff --git a/tests/e2e-test/tests/test_telecom_smoke_tc.py b/tests/e2e-test/tests/test_telecom_smoke_tc.py index 44a4f11e8..b5aee1a3c 100644 --- a/tests/e2e-test/tests/test_telecom_smoke_tc.py +++ b/tests/e2e-test/tests/test_telecom_smoke_tc.py @@ -73,7 +73,7 @@ def test_after_filter_functioning(login_logout, request): km_page.click_apply_button() logger.info("Step 4: Validate filter data is reflecting in charts/graphs") - billing_data = km_page.validate_trending_topics_entry("Billing Issues") + km_page.validate_trending_topics_entry("Billing Issues") logger.info("Billing issues data validation completed") km_page.validate_dashboard_charts() @@ -299,7 +299,6 @@ def test_clear_citations_on_chat_delete(login_logout, request): request.node._nodeid = "17631 - Bug 17326 - KM Generic - Telecom - Citation should get cleared after deleting complete chat history" page = login_logout - km_page = KMGenericPage(page) home_page = HomePage(page) logger.info("Step 2: Send a query to trigger a citation") From 1884e213b656cb5b855cb11ae5481132c4ec3b32 Mon Sep 17 00:00:00 2001 From: Ajit Padhi Date: Fri, 17 Apr 2026 10:45:01 +0530 Subject: [PATCH 2/3] fix unit test --- .../common/database/test_cosmosdb_service.py | 504 +++++++++--------- 1 file changed, 252 insertions(+), 252 deletions(-) diff --git a/src/tests/api/common/database/test_cosmosdb_service.py b/src/tests/api/common/database/test_cosmosdb_service.py index 1dce7b7df..41145de5f 100644 --- a/src/tests/api/common/database/test_cosmosdb_service.py +++ b/src/tests/api/common/database/test_cosmosdb_service.py @@ -1,252 +1,252 @@ -from unittest.mock import AsyncMock, MagicMock, patch -import pytest -from azure.cosmos import exceptions -from common.database.cosmosdb_service import CosmosConversationClient - - -class AsyncIteratorWrapper: - """Utility class to wrap async iteration over items.""" - def __init__(self, items): - self._items = items - - def __aiter__(self): - return self._async_gen() - - async def _async_gen(self): - for item in self._items: - yield item - - -@pytest.fixture -def mock_cosmos_clients(): - """Fixture to mock Cosmos DB container, database, and client.""" - mock_container = MagicMock() - mock_database = MagicMock() - mock_database.get_container_client.return_value = mock_container - mock_cosmos = MagicMock() - mock_cosmos.get_database_client.return_value = mock_database - return mock_cosmos, mock_database, mock_container - - -@pytest.fixture -def cosmos_client(mock_cosmos_clients): - """Fixture to create a CosmosConversationClient instance with mocked CosmosClient.""" - cosmos_mock, _, _ = mock_cosmos_clients - with patch("common.database.cosmosdb_service.CosmosClient", return_value=cosmos_mock): - return CosmosConversationClient( - cosmosdb_endpoint="https://fake-cosmos.documents.azure.com", - credential="fake-key", - database_name="test-db", - container_name="test-container" - ) - - -class TestCosmosDbService: - - @pytest.mark.asyncio - async def test_ensure_success(self, cosmos_client, mock_cosmos_clients): - """Test ensure() returns success if both DB and container are accessible.""" - _, db, container = mock_cosmos_clients - db.read = AsyncMock(return_value=True) - container.read = AsyncMock(return_value=True) - result, msg = await cosmos_client.ensure() - assert result is True and "successfully" in msg - - @pytest.mark.asyncio - async def test_ensure_fail_when_client_is_none(self): - """Test ensure() fails if cosmos client is not initialized.""" - client = CosmosConversationClient("url", "key", "db", "container") - client.cosmosdb_client = None - result, msg = await client.ensure() - assert result is False and "not initialized" in msg - - @pytest.mark.asyncio - async def test_ensure_database_read_fails(self, cosmos_client, mock_cosmos_clients): - """Test ensure() fails when reading DB fails.""" - _, db, _ = mock_cosmos_clients - db.read = AsyncMock(side_effect=Exception("Fail")) - result, msg = await cosmos_client.ensure() - assert result is False and "not found" in msg - - @pytest.mark.asyncio - async def test_ensure_container_read_fails(self, cosmos_client, mock_cosmos_clients): - """Test ensure() fails when reading container fails.""" - _, db, container = mock_cosmos_clients - db.read = AsyncMock(return_value=True) - container.read = AsyncMock(side_effect=Exception("Fail")) - result, msg = await cosmos_client.ensure() - assert result is False and "container" in msg - - def test_constructor_invalid_credential(self): - """Test constructor raises ValueError on bad credentials.""" - with patch("common.database.cosmosdb_service.CosmosClient", side_effect=exceptions.CosmosHttpResponseError(status_code=401)): - with pytest.raises(ValueError, match="Invalid credentials"): - CosmosConversationClient("url", "bad", "db", "container") - - def test_constructor_invalid_database(self): - """Test constructor raises ValueError for invalid DB name.""" - cosmos_mock = MagicMock() - cosmos_mock.get_database_client.side_effect = exceptions.CosmosResourceNotFoundError() - with patch("common.database.cosmosdb_service.CosmosClient", return_value=cosmos_mock): - with pytest.raises(ValueError, match="Invalid CosmosDB database name"): - CosmosConversationClient("url", "key", "invalid", "container") - - def test_constructor_invalid_container(self): - """Test constructor raises ValueError for invalid container.""" - cosmos_mock = MagicMock() - db_mock = MagicMock() - db_mock.get_container_client.side_effect = exceptions.CosmosResourceNotFoundError() - cosmos_mock.get_database_client.return_value = db_mock - with patch("common.database.cosmosdb_service.CosmosClient", return_value=cosmos_mock): - with pytest.raises(ValueError, match="Invalid CosmosDB container name"): - CosmosConversationClient("url", "key", "db", "bad") - - @pytest.mark.asyncio - async def test_create_conversation_success(self, cosmos_client, mock_cosmos_clients): - """Test successful creation of conversation.""" - _, _, container = mock_cosmos_clients - container.upsert_item = AsyncMock(return_value={"id": "c1"}) - result = await cosmos_client.create_conversation("user1", "c1", "title") - assert result["id"] == "c1" - - @pytest.mark.asyncio - async def test_create_conversation_failure(self, cosmos_client, mock_cosmos_clients): - """Test failure to create conversation returns False.""" - _, _, container = mock_cosmos_clients - container.upsert_item = AsyncMock(return_value=None) - result = await cosmos_client.create_conversation("user1", "c2", "title") - assert result is False - - @pytest.mark.asyncio - async def test_upsert_conversation_success(self, cosmos_client, mock_cosmos_clients): - """Test successful upsert of conversation.""" - _, _, container = mock_cosmos_clients - container.upsert_item = AsyncMock(return_value={"id": "x"}) - result = await cosmos_client.upsert_conversation({"id": "x"}) - assert result["id"] == "x" - - @pytest.mark.asyncio - async def test_upsert_conversation_failure(self, cosmos_client, mock_cosmos_clients): - """Test upsert returns False when result is None.""" - _, _, container = mock_cosmos_clients - container.upsert_item = AsyncMock(return_value=None) - result = await cosmos_client.upsert_conversation({"id": "x"}) - assert result is False - - @pytest.mark.asyncio - async def test_get_conversation_found(self, cosmos_client, mock_cosmos_clients): - """Test get_conversation returns a result if found.""" - _, _, container = mock_cosmos_clients - container.query_items.return_value = AsyncIteratorWrapper([{"id": "c1"}]) - result = await cosmos_client.get_conversation("user1", "c1") - assert result["id"] == "c1" - - @pytest.mark.asyncio - async def test_get_conversation_not_found(self, cosmos_client, mock_cosmos_clients): - """Test get_conversation returns None when not found.""" - _, _, container = mock_cosmos_clients - container.query_items.return_value = AsyncIteratorWrapper([]) - result = await cosmos_client.get_conversation("user1", "none") - assert result is None - - @pytest.mark.asyncio - async def test_get_conversations_with_limit(self, cosmos_client, mock_cosmos_clients): - """Test get_conversations returns a list of messages.""" - _, _, container = mock_cosmos_clients - container.query_items.return_value = AsyncIteratorWrapper([{"id": "1"}, {"id": "2"}]) - result = await cosmos_client.get_conversations("user1", limit=2, offset=0) - assert len(result) == 2 - - @pytest.mark.asyncio - async def test_create_message_with_feedback(self, cosmos_client, mock_cosmos_clients): - """Test message creation with feedback enabled.""" - _, _, container = mock_cosmos_clients - cosmos_client.enable_message_feedback = True - container.upsert_item = AsyncMock(return_value={"id": "m1"}) - cosmos_client.get_conversation = AsyncMock(return_value={"id": "c1", "updatedAt": "old"}) - cosmos_client.upsert_conversation = AsyncMock() - result = await cosmos_client.create_message("m1", "c1", "user1", {"role": "user", "text": "hi"}) - assert result["id"] == "m1" - - @pytest.mark.asyncio - async def test_create_message_without_feedback(self, cosmos_client, mock_cosmos_clients): - """Test message creation with feedback disabled.""" - _, _, container = mock_cosmos_clients - cosmos_client.enable_message_feedback = False - container.upsert_item = AsyncMock(return_value={"id": "m2"}) - cosmos_client.get_conversation = AsyncMock(return_value={"id": "c2", "updatedAt": "old"}) - cosmos_client.upsert_conversation = AsyncMock() - result = await cosmos_client.create_message("m2", "c2", "user1", {"role": "assistant", "text": "hello"}) - assert result["id"] == "m2" - - @pytest.mark.asyncio - async def test_create_message_conversation_not_found(self, cosmos_client, mock_cosmos_clients): - """Test message creation fails when conversation not found.""" - _, _, container = mock_cosmos_clients - cosmos_client.enable_message_feedback = True - container.upsert_item = AsyncMock(return_value={"id": "m3"}) - cosmos_client.get_conversation = AsyncMock(return_value=None) - result = await cosmos_client.create_message("m3", "notfound", "user1", {"role": "user", "text": "nope"}) - assert result == "Conversation not found" - - @pytest.mark.asyncio - async def test_update_message_feedback_success(self, cosmos_client, mock_cosmos_clients): - """Test updating message feedback successfully.""" - _, _, container = mock_cosmos_clients - container.read_item = AsyncMock(return_value={"id": "m1"}) - container.upsert_item = AsyncMock(return_value={"id": "m1", "feedback": "Good"}) - result = await cosmos_client.update_message_feedback("user1", "m1", "Good") - assert result["feedback"] == "Good" - - @pytest.mark.asyncio - async def test_update_message_feedback_not_found(self, cosmos_client, mock_cosmos_clients): - """Test updating feedback fails when message is missing.""" - _, _, container = mock_cosmos_clients - container.read_item = AsyncMock(return_value=None) - result = await cosmos_client.update_message_feedback("user1", "m2", "Bad") - assert result is False - - @pytest.mark.asyncio - async def test_get_messages(self, cosmos_client, mock_cosmos_clients): - """Test getting messages for a conversation.""" - _, _, container = mock_cosmos_clients - container.query_items.return_value = AsyncIteratorWrapper([ - {"id": "m1"}, {"id": "m2"} - ]) - result = await cosmos_client.get_messages("user1", "c1") - assert len(result) == 2 - - @pytest.mark.asyncio - async def test_delete_messages_with_messages(self, cosmos_client, mock_cosmos_clients): - """Test deleting messages when messages exist.""" - _, _, container = mock_cosmos_clients - cosmos_client.get_messages = AsyncMock(return_value=[ - {"id": "m1"}, {"id": "m2"} - ]) - container.delete_item = AsyncMock(return_value=True) - result = await cosmos_client.delete_messages("c1", "user1") - assert len(result) == 2 - - @pytest.mark.asyncio - async def test_delete_messages_no_messages(self, cosmos_client): - """Test delete_messages returns None when there are no messages.""" - cosmos_client.get_messages = AsyncMock(return_value=[]) - result = await cosmos_client.delete_messages("c1", "user1") - assert result is None - - @pytest.mark.asyncio - async def test_delete_conversation_found(self, cosmos_client, mock_cosmos_clients): - """Test deleting an existing conversation.""" - _, _, container = mock_cosmos_clients - container.read_item = AsyncMock(return_value={"id": "c1"}) - container.delete_item = AsyncMock(return_value=True) - result = await cosmos_client.delete_conversation("user1", "c1") - assert result is True - - @pytest.mark.asyncio - async def test_delete_conversation_not_found(self, cosmos_client, mock_cosmos_clients): - """Test deleting a non-existent conversation returns True (no-op).""" - _, _, container = mock_cosmos_clients - container.read_item = AsyncMock(return_value=None) - result = await cosmos_client.delete_conversation("user1", "none") - assert result is True +from unittest.mock import AsyncMock, MagicMock, patch +import pytest +from azure.cosmos import exceptions +from common.database.cosmosdb_service import CosmosConversationClient + + +class AsyncIteratorWrapper: + """Utility class to wrap async iteration over items.""" + def __init__(self, items): + self._items = items + + def __aiter__(self): + return self._async_gen() + + async def _async_gen(self): + for item in self._items: + yield item + + +@pytest.fixture +def mock_cosmos_clients(): + """Fixture to mock Cosmos DB container, database, and client.""" + mock_container = MagicMock() + mock_database = MagicMock() + mock_database.get_container_client.return_value = mock_container + mock_cosmos = MagicMock() + mock_cosmos.get_database_client.return_value = mock_database + return mock_cosmos, mock_database, mock_container + + +@pytest.fixture +def cosmos_client(mock_cosmos_clients): + """Fixture to create a CosmosConversationClient instance with mocked CosmosClient.""" + cosmos_mock, _, _ = mock_cosmos_clients + with patch("common.database.cosmosdb_service.CosmosClient", return_value=cosmos_mock): + return CosmosConversationClient( + cosmosdb_endpoint="https://fake-cosmos.documents.azure.com", + credential="fake-key", + database_name="test-db", + container_name="test-container" + ) + + +class TestCosmosDbService: + + @pytest.mark.asyncio + async def test_ensure_success(self, cosmos_client, mock_cosmos_clients): + """Test ensure() returns success if both DB and container are accessible.""" + _, db, container = mock_cosmos_clients + db.read = AsyncMock(return_value=True) + container.read = AsyncMock(return_value=True) + result, msg = await cosmos_client.ensure() + assert result is True and "successfully" in msg + + @pytest.mark.asyncio + async def test_ensure_fail_when_client_is_none(self): + """Test ensure() fails if cosmos client is not initialized.""" + client = CosmosConversationClient("url", "key", "db", "container") + client.cosmosdb_client = None + result, msg = await client.ensure() + assert result is False and "not initialized" in msg + + @pytest.mark.asyncio + async def test_ensure_database_read_fails(self, cosmos_client, mock_cosmos_clients): + """Test ensure() fails when reading DB fails.""" + _, db, _ = mock_cosmos_clients + db.read = AsyncMock(side_effect=Exception("Fail")) + result, msg = await cosmos_client.ensure() + assert result is False and "not found" in msg + + @pytest.mark.asyncio + async def test_ensure_container_read_fails(self, cosmos_client, mock_cosmos_clients): + """Test ensure() fails when reading container fails.""" + _, db, container = mock_cosmos_clients + db.read = AsyncMock(return_value=True) + container.read = AsyncMock(side_effect=Exception("Fail")) + result, msg = await cosmos_client.ensure() + assert result is False and "container" in msg + + def test_constructor_invalid_credential(self): + """Test constructor raises ValueError on bad credentials.""" + with patch("common.database.cosmosdb_service.CosmosClient", side_effect=exceptions.CosmosHttpResponseError(status_code=401)): + with pytest.raises(ValueError, match="Invalid credentials"): + CosmosConversationClient("url", "bad", "db", "container") + + def test_constructor_invalid_database(self): + """Test constructor raises ValueError for invalid DB name.""" + cosmos_mock = MagicMock() + cosmos_mock.get_database_client.side_effect = exceptions.CosmosResourceNotFoundError() + with patch("common.database.cosmosdb_service.CosmosClient", return_value=cosmos_mock): + with pytest.raises(ValueError, match="Invalid CosmosDB database name"): + CosmosConversationClient("url", "key", "invalid", "container") + + def test_constructor_invalid_container(self): + """Test constructor raises ValueError for invalid container.""" + cosmos_mock = MagicMock() + db_mock = MagicMock() + db_mock.get_container_client.side_effect = exceptions.CosmosResourceNotFoundError() + cosmos_mock.get_database_client.return_value = db_mock + with patch("common.database.cosmosdb_service.CosmosClient", return_value=cosmos_mock): + with pytest.raises(ValueError, match="Invalid CosmosDB container name"): + CosmosConversationClient("url", "key", "db", "bad") + + @pytest.mark.asyncio + async def test_create_conversation_success(self, cosmos_client, mock_cosmos_clients): + """Test successful creation of conversation.""" + _, _, container = mock_cosmos_clients + container.upsert_item = AsyncMock(return_value={"id": "c1"}) + result = await cosmos_client.create_conversation("user1", "c1", "title") + assert result["id"] == "c1" + + @pytest.mark.asyncio + async def test_create_conversation_failure(self, cosmos_client, mock_cosmos_clients): + """Test failure to create conversation returns False.""" + _, _, container = mock_cosmos_clients + container.upsert_item = AsyncMock(return_value=None) + result = await cosmos_client.create_conversation("user1", "c2", "title") + assert result is False + + @pytest.mark.asyncio + async def test_upsert_conversation_success(self, cosmos_client, mock_cosmos_clients): + """Test successful upsert of conversation.""" + _, _, container = mock_cosmos_clients + container.upsert_item = AsyncMock(return_value={"id": "x"}) + result = await cosmos_client.upsert_conversation({"id": "x"}) + assert result["id"] == "x" + + @pytest.mark.asyncio + async def test_upsert_conversation_failure(self, cosmos_client, mock_cosmos_clients): + """Test upsert returns False when result is None.""" + _, _, container = mock_cosmos_clients + container.upsert_item = AsyncMock(return_value=None) + result = await cosmos_client.upsert_conversation({"id": "x"}) + assert result is False + + @pytest.mark.asyncio + async def test_get_conversation_found(self, cosmos_client, mock_cosmos_clients): + """Test get_conversation returns a result if found.""" + _, _, container = mock_cosmos_clients + container.query_items.return_value = AsyncIteratorWrapper([{"id": "c1"}]) + result = await cosmos_client.get_conversation("user1", "c1") + assert result["id"] == "c1" + + @pytest.mark.asyncio + async def test_get_conversation_not_found(self, cosmos_client, mock_cosmos_clients): + """Test get_conversation returns None when not found.""" + _, _, container = mock_cosmos_clients + container.query_items.return_value = AsyncIteratorWrapper([]) + result = await cosmos_client.get_conversation("user1", "none") + assert result is None + + @pytest.mark.asyncio + async def test_get_conversations_with_limit(self, cosmos_client, mock_cosmos_clients): + """Test get_conversations returns a list of messages.""" + _, _, container = mock_cosmos_clients + container.query_items.return_value = AsyncIteratorWrapper([{"id": "1"}, {"id": "2"}]) + result = await cosmos_client.get_conversations("user1", limit=2, offset=0) + assert len(result) == 2 + + @pytest.mark.asyncio + async def test_create_message_with_feedback(self, cosmos_client, mock_cosmos_clients): + """Test message creation with feedback enabled.""" + _, _, container = mock_cosmos_clients + cosmos_client.enable_message_feedback = True + container.upsert_item = AsyncMock(return_value={"id": "m1"}) + cosmos_client.get_conversation = AsyncMock(return_value={"id": "c1", "updatedAt": "old"}) + cosmos_client.upsert_conversation = AsyncMock() + result = await cosmos_client.create_message("m1", "c1", "user1", {"role": "user", "text": "hi"}) + assert result["id"] == "m1" + + @pytest.mark.asyncio + async def test_create_message_without_feedback(self, cosmos_client, mock_cosmos_clients): + """Test message creation with feedback disabled.""" + _, _, container = mock_cosmos_clients + cosmos_client.enable_message_feedback = False + container.upsert_item = AsyncMock(return_value={"id": "m2"}) + cosmos_client.get_conversation = AsyncMock(return_value={"id": "c2", "updatedAt": "old"}) + cosmos_client.upsert_conversation = AsyncMock() + result = await cosmos_client.create_message("m2", "c2", "user1", {"role": "assistant", "text": "hello"}) + assert result["id"] == "m2" + + @pytest.mark.asyncio + async def test_create_message_conversation_not_found(self, cosmos_client, mock_cosmos_clients): + """Test message creation fails when conversation not found.""" + _, _, container = mock_cosmos_clients + cosmos_client.enable_message_feedback = True + container.upsert_item = AsyncMock(return_value={"id": "m3"}) + cosmos_client.get_conversation = AsyncMock(return_value=None) + result = await cosmos_client.create_message("m3", "notfound", "user1", {"role": "user", "text": "nope"}) + assert result == "Conversation not found" + + @pytest.mark.asyncio + async def test_update_message_feedback_success(self, cosmos_client, mock_cosmos_clients): + """Test updating message feedback successfully.""" + _, _, container = mock_cosmos_clients + container.read_item = AsyncMock(return_value={"id": "m1"}) + container.upsert_item = AsyncMock(return_value={"id": "m1", "feedback": "Good"}) + result = await cosmos_client.update_message_feedback("user1", "m1", "Good") + assert result["feedback"] == "Good" + + @pytest.mark.asyncio + async def test_update_message_feedback_not_found(self, cosmos_client, mock_cosmos_clients): + """Test updating feedback fails when message is missing.""" + _, _, container = mock_cosmos_clients + container.read_item = AsyncMock(return_value=None) + result = await cosmos_client.update_message_feedback("user1", "m2", "Bad") + assert result is False + + @pytest.mark.asyncio + async def test_get_messages(self, cosmos_client, mock_cosmos_clients): + """Test getting messages for a conversation.""" + _, _, container = mock_cosmos_clients + container.query_items.return_value = AsyncIteratorWrapper([ + {"id": "m1"}, {"id": "m2"} + ]) + result = await cosmos_client.get_messages("user1", "c1") + assert len(result) == 2 + + @pytest.mark.asyncio + async def test_delete_messages_with_messages(self, cosmos_client, mock_cosmos_clients): + """Test deleting messages when messages exist.""" + _, _, container = mock_cosmos_clients + cosmos_client.get_messages = AsyncMock(return_value=[ + {"id": "m1"}, {"id": "m2"} + ]) + container.delete_item = AsyncMock(return_value=True) + result = await cosmos_client.delete_messages("c1", "user1") + assert len(result) == 2 + + @pytest.mark.asyncio + async def test_delete_messages_no_messages(self, cosmos_client): + """Test delete_messages returns empty list when there are no messages.""" + cosmos_client.get_messages = AsyncMock(return_value=[]) + result = await cosmos_client.delete_messages("c1", "user1") + assert result == [] + + @pytest.mark.asyncio + async def test_delete_conversation_found(self, cosmos_client, mock_cosmos_clients): + """Test deleting an existing conversation.""" + _, _, container = mock_cosmos_clients + container.read_item = AsyncMock(return_value={"id": "c1"}) + container.delete_item = AsyncMock(return_value=True) + result = await cosmos_client.delete_conversation("user1", "c1") + assert result is True + + @pytest.mark.asyncio + async def test_delete_conversation_not_found(self, cosmos_client, mock_cosmos_clients): + """Test deleting a non-existent conversation returns True (no-op).""" + _, _, container = mock_cosmos_clients + container.read_item = AsyncMock(return_value=None) + result = await cosmos_client.delete_conversation("user1", "none") + assert result is True From 3a015bea1fcff4fcf43830aeddc09b68f1c1333e Mon Sep 17 00:00:00 2001 From: Ajit Padhi Date: Mon, 20 Apr 2026 19:42:28 +0530 Subject: [PATCH 3/3] fix: restore imports lost during merge in App.tsx The merge of origin/dev into PSL-US-33784 incorrectly resolved the import block in App.tsx, dropping: - closing brace and source for @fluentui/react-components - SparkleRegular import from @fluentui/react-icons - App.css stylesheet import - ChatHistoryPanel component import This caused the Docker build-and-push CI job to fail with: SyntaxError: Unexpected keyword 'import'. (11:0) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/App/src/App.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/App/src/App.tsx b/src/App/src/App.tsx index 74d4f770b..ac4659c46 100644 --- a/src/App/src/App.tsx +++ b/src/App/src/App.tsx @@ -8,6 +8,10 @@ import { FluentProvider, Subtitle2, webLightTheme, +} from "@fluentui/react-components"; +import { SparkleRegular } from "@fluentui/react-icons"; +import "./App.css"; +import { ChatHistoryPanel } from "./components/ChatHistoryPanel/ChatHistoryPanel"; import { getUserInfo } from "./api/api"; import { useAppDispatch, useAppSelector } from "./state/hooks"; import {