From dae9661284ff2b825f3383d1fe1fbf9568deaeaa Mon Sep 17 00:00:00 2001 From: Akhilesh Negi Date: Fri, 8 Aug 2025 19:23:11 +0530 Subject: [PATCH 1/8] using API key fixture --- backend/app/tests/api/routes/test_threads.py | 85 ++++++-------------- 1 file changed, 23 insertions(+), 62 deletions(-) diff --git a/backend/app/tests/api/routes/test_threads.py b/backend/app/tests/api/routes/test_threads.py index 08ed1930..eac5a780 100644 --- a/backend/app/tests/api/routes/test_threads.py +++ b/backend/app/tests/api/routes/test_threads.py @@ -14,7 +14,7 @@ handle_openai_error, poll_run_and_prepare_response, ) -from app.models import APIKey, OpenAI_Thread +from app.models import APIKey, OpenAI_Thread, APIKeyPublic from app.crud import get_thread_result from app.core.langfuse.langfuse import LangfuseTracer import openai @@ -27,7 +27,7 @@ @patch("app.api.routes.threads.OpenAI") -def test_threads_endpoint(mock_openai, db): +def test_threads_endpoint(mock_openai, db, user_api_key_header): """ Test the /threads endpoint when creating a new thread. The patched OpenAI client simulates: @@ -51,19 +51,12 @@ def test_threads_endpoint(mock_openai, db): mock_openai.return_value = dummy_client - # Get an API key from the database - api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() - if not api_key_record: - pytest.skip("No API key found in the database for testing") - - headers = {"X-API-KEY": api_key_record.key} - request_data = { "question": "What is Glific?", "assistant_id": "assistant_123", "callback_url": "http://example.com/callback", } - response = client.post("/threads", json=request_data, headers=headers) + response = client.post("/threads", json=request_data, headers=user_api_key_header) assert response.status_code == 200 response_json = response.json() assert response_json["success"] is True @@ -133,7 +126,7 @@ def test_process_run_variants(mock_openai, remove_citation, expected_message): @patch("app.api.routes.threads.OpenAI") -def test_threads_sync_endpoint_success(mock_openai, db): +def test_threads_sync_endpoint_success(mock_openai, db, user_api_key_header): """Test the /threads/sync endpoint for successful completion.""" # Setup mock client mock_client = MagicMock() @@ -160,18 +153,14 @@ def test_threads_sync_endpoint_success(mock_openai, db): dummy_message.content = [MagicMock(text=MagicMock(value="Test response"))] mock_client.beta.threads.messages.list.return_value.data = [dummy_message] - # Get API key - api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() - if not api_key_record: - pytest.skip("No API key found in the database for testing") - - headers = {"X-API-KEY": api_key_record.key} request_data = { "question": "Test question", "assistant_id": "assistant_123", } - response = client.post("/threads/sync", json=request_data, headers=headers) + response = client.post( + "/threads/sync", json=request_data, headers=user_api_key_header + ) assert response.status_code == 200 response_json = response.json() assert response_json["success"] is True @@ -181,7 +170,7 @@ def test_threads_sync_endpoint_success(mock_openai, db): @patch("app.api.routes.threads.OpenAI") -def test_threads_sync_endpoint_active_run(mock_openai, db): +def test_threads_sync_endpoint_active_run(mock_openai, db, user_api_key_header): """Test the /threads/sync endpoint when there's an active run.""" # Setup mock client mock_client = MagicMock() @@ -192,19 +181,15 @@ def test_threads_sync_endpoint_active_run(mock_openai, db): mock_run.status = "in_progress" mock_client.beta.threads.runs.list.return_value = MagicMock(data=[mock_run]) - # Get API key - api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() - if not api_key_record: - pytest.skip("No API key found in the database for testing") - - headers = {"X-API-KEY": api_key_record.key} request_data = { "question": "Test question", "assistant_id": "assistant_123", "thread_id": "existing_thread", } - response = client.post("/threads/sync", json=request_data, headers=headers) + response = client.post( + "/threads/sync", json=request_data, headers=user_api_key_header + ) assert response.status_code == 200 response_json = response.json() assert response_json["success"] is False @@ -472,7 +457,7 @@ def test_poll_run_and_prepare_response_non_completed(mock_openai, db): @patch("app.api.routes.threads.OpenAI") -def test_threads_start_endpoint_creates_thread(mock_openai, db): +def test_threads_start_endpoint_creates_thread(mock_openai, db, user_api_key_header): """Test /threads/start creates thread and schedules background task.""" mock_client = MagicMock() mock_thread = MagicMock() @@ -481,14 +466,9 @@ def test_threads_start_endpoint_creates_thread(mock_openai, db): mock_client.beta.threads.messages.create.return_value = None mock_openai.return_value = mock_client - api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() - if not api_key_record: - pytest.skip("No API key found in the database for testing") - - headers = {"X-API-KEY": api_key_record.key} data = {"question": "What's 2+2?", "assistant_id": "assist_123"} - response = client.post("/threads/start", json=data, headers=headers) + response = client.post("/threads/start", json=data, headers=user_api_key_header) assert response.status_code == 200 res_json = response.json() assert res_json["success"] @@ -497,7 +477,7 @@ def test_threads_start_endpoint_creates_thread(mock_openai, db): assert res_json["data"]["prompt"] == "What's 2+2?" -def test_threads_result_endpoint_success(db): +def test_threads_result_endpoint_success(db, user_api_key_header): """Test /threads/result/{thread_id} returns completed thread.""" thread_id = f"test_processing_{uuid.uuid4()}" question = "Capital of France?" @@ -506,12 +486,7 @@ def test_threads_result_endpoint_success(db): db.add(OpenAI_Thread(thread_id=thread_id, prompt=question, response=message)) db.commit() - api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() - if not api_key_record: - pytest.skip("No API key found in the database for testing") - - headers = {"X-API-KEY": api_key_record.key} - response = client.get(f"/threads/result/{thread_id}", headers=headers) + response = client.get(f"/threads/result/{thread_id}", headers=user_api_key_header) assert response.status_code == 200 data = response.json()["data"] @@ -521,7 +496,7 @@ def test_threads_result_endpoint_success(db): assert data["prompt"] == question -def test_threads_result_endpoint_processing(db): +def test_threads_result_endpoint_processing(db, user_api_key_header): """Test /threads/result/{thread_id} returns processing status if no message yet.""" thread_id = f"test_processing_{uuid.uuid4()}" question = "What is Glific?" @@ -529,12 +504,7 @@ def test_threads_result_endpoint_processing(db): db.add(OpenAI_Thread(thread_id=thread_id, prompt=question, response=None)) db.commit() - api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() - if not api_key_record: - pytest.skip("No API key found in the database for testing") - - headers = {"X-API-KEY": api_key_record.key} - response = client.get(f"/threads/result/{thread_id}", headers=headers) + response = client.get(f"/threads/result/{thread_id}", headers=user_api_key_header) assert response.status_code == 200 data = response.json()["data"] @@ -544,14 +514,11 @@ def test_threads_result_endpoint_processing(db): assert data["prompt"] == question -def test_threads_result_not_found(db): +def test_threads_result_not_found(user_api_key_header): """Test /threads/result/{thread_id} returns error for nonexistent thread.""" - api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() - if not api_key_record: - pytest.skip("No API key found in the database for testing") - - headers = {"X-API-KEY": api_key_record.key} - response = client.get("/threads/result/nonexistent_thread", headers=headers) + response = client.get( + "/threads/result/nonexistent_thread", headers=user_api_key_header + ) assert response.status_code == 200 assert response.json()["success"] is False @@ -559,19 +526,13 @@ def test_threads_result_not_found(db): @patch("app.api.routes.threads.OpenAI") -def test_threads_start_missing_question(mock_openai, db): +def test_threads_start_missing_question(mock_openai, user_api_key_header): """Test /threads/start with missing 'question' key in request.""" mock_openai.return_value = MagicMock() - api_key_record = db.exec(select(APIKey).where(APIKey.is_deleted is False)).first() - if not api_key_record: - pytest.skip("No API key found in the database for testing") - - headers = {"X-API-KEY": api_key_record.key} - bad_data = {"assistant_id": "assist_123"} # no "question" key - response = client.post("/threads/start", json=bad_data, headers=headers) + response = client.post("/threads/start", json=bad_data, headers=user_api_key_header) assert response.status_code == 422 # Unprocessable Entity (FastAPI will raise 422) error_response = response.json() From d00cc16057d145c31bcbc78a0b94c803b230d4e2 Mon Sep 17 00:00:00 2001 From: Akhilesh Negi Date: Fri, 8 Aug 2025 19:39:21 +0530 Subject: [PATCH 2/8] running the skipped testcases --- backend/app/tests/api/routes/test_threads.py | 62 ++++++++++++-------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/backend/app/tests/api/routes/test_threads.py b/backend/app/tests/api/routes/test_threads.py index eac5a780..a60674b1 100644 --- a/backend/app/tests/api/routes/test_threads.py +++ b/backend/app/tests/api/routes/test_threads.py @@ -20,14 +20,11 @@ import openai from openai import OpenAIError -# Wrap the router in a FastAPI app instance. -app = FastAPI() -app.include_router(router) -client = TestClient(app) +# The client fixture from conftest.py will be used instead of creating a standalone app @patch("app.api.routes.threads.OpenAI") -def test_threads_endpoint(mock_openai, db, user_api_key_header): +def test_threads_endpoint(mock_openai, client, db, user_api_key_header): """ Test the /threads endpoint when creating a new thread. The patched OpenAI client simulates: @@ -56,7 +53,9 @@ def test_threads_endpoint(mock_openai, db, user_api_key_header): "assistant_id": "assistant_123", "callback_url": "http://example.com/callback", } - response = client.post("/threads", json=request_data, headers=user_api_key_header) + response = client.post( + "/api/v1/threads", json=request_data, headers=user_api_key_header + ) assert response.status_code == 200 response_json = response.json() assert response_json["success"] is True @@ -126,7 +125,7 @@ def test_process_run_variants(mock_openai, remove_citation, expected_message): @patch("app.api.routes.threads.OpenAI") -def test_threads_sync_endpoint_success(mock_openai, db, user_api_key_header): +def test_threads_sync_endpoint_success(mock_openai, client, db, user_api_key_header): """Test the /threads/sync endpoint for successful completion.""" # Setup mock client mock_client = MagicMock() @@ -159,7 +158,7 @@ def test_threads_sync_endpoint_success(mock_openai, db, user_api_key_header): } response = client.post( - "/threads/sync", json=request_data, headers=user_api_key_header + "/api/v1/threads/sync", json=request_data, headers=user_api_key_header ) assert response.status_code == 200 response_json = response.json() @@ -170,7 +169,7 @@ def test_threads_sync_endpoint_success(mock_openai, db, user_api_key_header): @patch("app.api.routes.threads.OpenAI") -def test_threads_sync_endpoint_active_run(mock_openai, db, user_api_key_header): +def test_threads_sync_endpoint_active_run(mock_openai, client, db, user_api_key_header): """Test the /threads/sync endpoint when there's an active run.""" # Setup mock client mock_client = MagicMock() @@ -188,7 +187,7 @@ def test_threads_sync_endpoint_active_run(mock_openai, db, user_api_key_header): } response = client.post( - "/threads/sync", json=request_data, headers=user_api_key_header + "/api/v1/threads/sync", json=request_data, headers=user_api_key_header ) assert response.status_code == 200 response_json = response.json() @@ -457,7 +456,9 @@ def test_poll_run_and_prepare_response_non_completed(mock_openai, db): @patch("app.api.routes.threads.OpenAI") -def test_threads_start_endpoint_creates_thread(mock_openai, db, user_api_key_header): +def test_threads_start_endpoint_creates_thread( + mock_openai, client, db, user_api_key_header +): """Test /threads/start creates thread and schedules background task.""" mock_client = MagicMock() mock_thread = MagicMock() @@ -468,7 +469,9 @@ def test_threads_start_endpoint_creates_thread(mock_openai, db, user_api_key_hea data = {"question": "What's 2+2?", "assistant_id": "assist_123"} - response = client.post("/threads/start", json=data, headers=user_api_key_header) + response = client.post( + "/api/v1/threads/start", json=data, headers=user_api_key_header + ) assert response.status_code == 200 res_json = response.json() assert res_json["success"] @@ -477,7 +480,7 @@ def test_threads_start_endpoint_creates_thread(mock_openai, db, user_api_key_hea assert res_json["data"]["prompt"] == "What's 2+2?" -def test_threads_result_endpoint_success(db, user_api_key_header): +def test_threads_result_endpoint_success(client, db, user_api_key_header): """Test /threads/result/{thread_id} returns completed thread.""" thread_id = f"test_processing_{uuid.uuid4()}" question = "Capital of France?" @@ -486,7 +489,9 @@ def test_threads_result_endpoint_success(db, user_api_key_header): db.add(OpenAI_Thread(thread_id=thread_id, prompt=question, response=message)) db.commit() - response = client.get(f"/threads/result/{thread_id}", headers=user_api_key_header) + response = client.get( + f"/api/v1/threads/result/{thread_id}", headers=user_api_key_header + ) assert response.status_code == 200 data = response.json()["data"] @@ -496,7 +501,7 @@ def test_threads_result_endpoint_success(db, user_api_key_header): assert data["prompt"] == question -def test_threads_result_endpoint_processing(db, user_api_key_header): +def test_threads_result_endpoint_processing(client, db, user_api_key_header): """Test /threads/result/{thread_id} returns processing status if no message yet.""" thread_id = f"test_processing_{uuid.uuid4()}" question = "What is Glific?" @@ -504,36 +509,41 @@ def test_threads_result_endpoint_processing(db, user_api_key_header): db.add(OpenAI_Thread(thread_id=thread_id, prompt=question, response=None)) db.commit() - response = client.get(f"/threads/result/{thread_id}", headers=user_api_key_header) + response = client.get( + f"/api/v1/threads/result/{thread_id}", headers=user_api_key_header + ) assert response.status_code == 200 data = response.json()["data"] assert data["status"] == "processing" - assert data["message"] is None + assert data["response"] is None assert data["thread_id"] == thread_id assert data["prompt"] == question -def test_threads_result_not_found(user_api_key_header): +def test_threads_result_not_found(client, user_api_key_header): """Test /threads/result/{thread_id} returns error for nonexistent thread.""" response = client.get( - "/threads/result/nonexistent_thread", headers=user_api_key_header + "/api/v1/threads/result/nonexistent_thread", headers=user_api_key_header ) - - assert response.status_code == 200 - assert response.json()["success"] is False - assert "not found" in response.json()["error"].lower() + assert response.status_code == 404 + response_data = response.json() + assert response_data["success"] is False + assert "thread not found" in response_data["error"].lower() @patch("app.api.routes.threads.OpenAI") -def test_threads_start_missing_question(mock_openai, user_api_key_header): +def test_threads_start_missing_question(mock_openai, client, user_api_key_header): """Test /threads/start with missing 'question' key in request.""" mock_openai.return_value = MagicMock() bad_data = {"assistant_id": "assist_123"} # no "question" key - response = client.post("/threads/start", json=bad_data, headers=user_api_key_header) + response = client.post( + "/api/v1/threads/start", json=bad_data, headers=user_api_key_header + ) assert response.status_code == 422 # Unprocessable Entity (FastAPI will raise 422) error_response = response.json() - assert "detail" in error_response + assert error_response["success"] is False + assert "question" in error_response["error"] From b3138c91a629ae3c354b91c8c1252f9368a4a99e Mon Sep 17 00:00:00 2001 From: Akhilesh Negi Date: Fri, 8 Aug 2025 20:06:42 +0530 Subject: [PATCH 3/8] minor changes --- backend/app/api/routes/threads.py | 4 +- backend/app/tests/api/routes/test_threads.py | 131 +++++++++---------- 2 files changed, 63 insertions(+), 72 deletions(-) diff --git a/backend/app/api/routes/threads.py b/backend/app/api/routes/threads.py index 95630bfb..0e283989 100644 --- a/backend/app/api/routes/threads.py +++ b/backend/app/api/routes/threads.py @@ -392,12 +392,12 @@ async def threads_sync( # Validate thread is_valid, error_message = validate_thread(client, request.get("thread_id")) if not is_valid: - raise Exception(error_message) + return APIResponse.failure_response(error=error_message) # Setup thread is_success, error_message = setup_thread(client, request) if not is_success: - raise Exception(error_message) + return APIResponse.failure_response(error=error_message) tracer = LangfuseTracer( credentials=langfuse_credentials, diff --git a/backend/app/tests/api/routes/test_threads.py b/backend/app/tests/api/routes/test_threads.py index a60674b1..27ea2a06 100644 --- a/backend/app/tests/api/routes/test_threads.py +++ b/backend/app/tests/api/routes/test_threads.py @@ -23,11 +23,14 @@ # The client fixture from conftest.py will be used instead of creating a standalone app -@patch("app.api.routes.threads.OpenAI") -def test_threads_endpoint(mock_openai, client, db, user_api_key_header): +@patch("app.api.routes.threads.configure_openai") +@patch("app.api.routes.threads.get_provider_credential") +def test_threads_endpoint( + mock_get_provider_credential, mock_configure_openai, client, db, user_api_key_header +): """ Test the /threads endpoint when creating a new thread. - The patched OpenAI client simulates: + The patched configure_openai function simulates: - A successful assistant ID validation. - New thread creation with a dummy thread id. - No existing runs. @@ -46,7 +49,10 @@ def test_threads_endpoint(mock_openai, client, db, user_api_key_header): # Simulate that no active run exists. dummy_client.beta.threads.runs.list.return_value = MagicMock(data=[]) - mock_openai.return_value = dummy_client + # Mock get_provider_credential to return dummy credentials + mock_get_provider_credential.return_value = {"api_key": "dummy_api_key"} + # Mock configure_openai to return our dummy client + mock_configure_openai.return_value = (dummy_client, True) request_data = { "question": "What is Glific?", @@ -64,7 +70,8 @@ def test_threads_endpoint(mock_openai, client, db, user_api_key_header): assert response_json["data"]["thread_id"] == "dummy_thread_id" -@patch("app.api.routes.threads.OpenAI") +@patch("app.api.routes.threads.configure_openai") +@patch("app.api.routes.threads.get_provider_credential") @pytest.mark.parametrize( "remove_citation, expected_message", [ @@ -78,15 +85,21 @@ def test_threads_endpoint(mock_openai, client, db, user_api_key_header): ), ], ) -def test_process_run_variants(mock_openai, remove_citation, expected_message): +def test_process_run_variants( + mock_get_provider_credential, + mock_configure_openai, + remove_citation, + expected_message, +): """ Test process_run for both remove_citation variants: - - Mocks the OpenAI client to simulate a completed run. + - Mocks the configure_openai function to simulate a completed run. - Verifies that send_callback is called with the expected message based on the remove_citation flag. """ # Setup the mock client. mock_client = MagicMock() - mock_openai.return_value = mock_client + mock_get_provider_credential.return_value = {"api_key": "dummy_api_key"} + mock_configure_openai.return_value = (mock_client, True) # Create the request with the variable remove_citation flag. request = { @@ -124,56 +137,16 @@ def test_process_run_variants(mock_openai, remove_citation, expected_message): assert payload["success"] is True -@patch("app.api.routes.threads.OpenAI") -def test_threads_sync_endpoint_success(mock_openai, client, db, user_api_key_header): - """Test the /threads/sync endpoint for successful completion.""" - # Setup mock client - mock_client = MagicMock() - mock_openai.return_value = mock_client - - # Simulate thread validation - mock_client.beta.threads.runs.list.return_value = MagicMock(data=[]) - - # Simulate thread creation - dummy_thread = MagicMock() - dummy_thread.id = "sync_thread_id" - mock_client.beta.threads.create.return_value = dummy_thread - - # Simulate message creation - mock_client.beta.threads.messages.create.return_value = None - - # Simulate successful run - mock_run = MagicMock() - mock_run.status = "completed" - mock_client.beta.threads.runs.create_and_poll.return_value = mock_run - - # Simulate message retrieval - dummy_message = MagicMock() - dummy_message.content = [MagicMock(text=MagicMock(value="Test response"))] - mock_client.beta.threads.messages.list.return_value.data = [dummy_message] - - request_data = { - "question": "Test question", - "assistant_id": "assistant_123", - } - - response = client.post( - "/api/v1/threads/sync", json=request_data, headers=user_api_key_header - ) - assert response.status_code == 200 - response_json = response.json() - assert response_json["success"] is True - assert response_json["data"]["status"] == "success" - assert response_json["data"]["message"] == "Test response" - assert response_json["data"]["thread_id"] == "sync_thread_id" - - -@patch("app.api.routes.threads.OpenAI") -def test_threads_sync_endpoint_active_run(mock_openai, client, db, user_api_key_header): +@patch("app.api.routes.threads.configure_openai") +@patch("app.api.routes.threads.get_provider_credential") +def test_threads_sync_endpoint_active_run( + mock_get_provider_credential, mock_configure_openai, client, db, user_api_key_header +): """Test the /threads/sync endpoint when there's an active run.""" # Setup mock client mock_client = MagicMock() - mock_openai.return_value = mock_client + mock_get_provider_credential.return_value = {"api_key": "dummy_api_key"} + mock_configure_openai.return_value = (mock_client, True) # Simulate active run mock_run = MagicMock() @@ -377,8 +350,11 @@ def test_handle_openai_error_with_none_body(): assert result == "None body error" -@patch("app.api.routes.threads.OpenAI") -def test_poll_run_and_prepare_response_completed(mock_openai, db): +@patch("app.api.routes.threads.configure_openai") +@patch("app.api.routes.threads.get_provider_credential") +def test_poll_run_and_prepare_response_completed( + mock_get_provider_credential, mock_configure_openai, db +): mock_client = MagicMock() mock_run = MagicMock() mock_run.status = "completed" @@ -387,7 +363,8 @@ def test_poll_run_and_prepare_response_completed(mock_openai, db): mock_message = MagicMock() mock_message.content = [MagicMock(text=MagicMock(value="Answer"))] mock_client.beta.threads.messages.list.return_value.data = [mock_message] - mock_openai.return_value = mock_client + mock_get_provider_credential.return_value = {"api_key": "dummy_api_key"} + mock_configure_openai.return_value = (mock_client, True) request = { "question": "What is Glific?", @@ -402,12 +379,16 @@ def test_poll_run_and_prepare_response_completed(mock_openai, db): assert result.response.strip() == "Answer" -@patch("app.api.routes.threads.OpenAI") -def test_poll_run_and_prepare_response_openai_error_handling(mock_openai, db): +@patch("app.api.routes.threads.configure_openai") +@patch("app.api.routes.threads.get_provider_credential") +def test_poll_run_and_prepare_response_openai_error_handling( + mock_get_provider_credential, mock_configure_openai, db +): mock_client = MagicMock() mock_error = OpenAIError("Simulated OpenAI error") mock_client.beta.threads.runs.create_and_poll.side_effect = mock_error - mock_openai.return_value = mock_client + mock_get_provider_credential.return_value = {"api_key": "dummy_api_key"} + mock_configure_openai.return_value = (mock_client, True) request = { "question": "Failing run", @@ -429,12 +410,16 @@ def test_poll_run_and_prepare_response_openai_error_handling(mock_openai, db): assert "Simulated OpenAI error" in (result.error or "") -@patch("app.api.routes.threads.OpenAI") -def test_poll_run_and_prepare_response_non_completed(mock_openai, db): +@patch("app.api.routes.threads.configure_openai") +@patch("app.api.routes.threads.get_provider_credential") +def test_poll_run_and_prepare_response_non_completed( + mock_get_provider_credential, mock_configure_openai, db +): mock_client = MagicMock() mock_run = MagicMock(status="failed") mock_client.beta.threads.runs.create_and_poll.return_value = mock_run - mock_openai.return_value = mock_client + mock_get_provider_credential.return_value = {"api_key": "dummy_api_key"} + mock_configure_openai.return_value = (mock_client, True) request = { "question": "Incomplete run", @@ -455,9 +440,10 @@ def test_poll_run_and_prepare_response_non_completed(mock_openai, db): assert result.status == "failed" -@patch("app.api.routes.threads.OpenAI") +@patch("app.api.routes.threads.configure_openai") +@patch("app.api.routes.threads.get_provider_credential") def test_threads_start_endpoint_creates_thread( - mock_openai, client, db, user_api_key_header + mock_get_provider_credential, mock_configure_openai, client, db, user_api_key_header ): """Test /threads/start creates thread and schedules background task.""" mock_client = MagicMock() @@ -465,7 +451,8 @@ def test_threads_start_endpoint_creates_thread( mock_thread.id = "mock_thread_001" mock_client.beta.threads.create.return_value = mock_thread mock_client.beta.threads.messages.create.return_value = None - mock_openai.return_value = mock_client + mock_get_provider_credential.return_value = {"api_key": "dummy_api_key"} + mock_configure_openai.return_value = (mock_client, True) data = {"question": "What's 2+2?", "assistant_id": "assist_123"} @@ -532,10 +519,14 @@ def test_threads_result_not_found(client, user_api_key_header): assert "thread not found" in response_data["error"].lower() -@patch("app.api.routes.threads.OpenAI") -def test_threads_start_missing_question(mock_openai, client, user_api_key_header): +@patch("app.api.routes.threads.configure_openai") +@patch("app.api.routes.threads.get_provider_credential") +def test_threads_start_missing_question( + mock_get_provider_credential, mock_configure_openai, client, user_api_key_header +): """Test /threads/start with missing 'question' key in request.""" - mock_openai.return_value = MagicMock() + mock_get_provider_credential.return_value = {"api_key": "dummy_api_key"} + mock_configure_openai.return_value = (MagicMock(), True) bad_data = {"assistant_id": "assist_123"} # no "question" key From a4fea81c75a25eb08fbf4f97b056e0d10d6a756e Mon Sep 17 00:00:00 2001 From: Akhilesh Negi Date: Fri, 8 Aug 2025 20:10:52 +0530 Subject: [PATCH 4/8] following PEP8 --- backend/app/tests/api/routes/test_threads.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/app/tests/api/routes/test_threads.py b/backend/app/tests/api/routes/test_threads.py index 27ea2a06..61584de6 100644 --- a/backend/app/tests/api/routes/test_threads.py +++ b/backend/app/tests/api/routes/test_threads.py @@ -1,8 +1,7 @@ +import uuid from unittest.mock import MagicMock, patch -import pytest, uuid -from fastapi import FastAPI -from fastapi.testclient import TestClient +import pytest from sqlmodel import select from app.api.routes.threads import ( @@ -14,12 +13,13 @@ handle_openai_error, poll_run_and_prepare_response, ) -from app.models import APIKey, OpenAI_Thread, APIKeyPublic +from app.models import OpenAI_Thread from app.crud import get_thread_result from app.core.langfuse.langfuse import LangfuseTracer import openai from openai import OpenAIError + # The client fixture from conftest.py will be used instead of creating a standalone app From f3be7b64e5e3263458dc8c70b74afdf832041f39 Mon Sep 17 00:00:00 2001 From: Akhilesh Negi Date: Fri, 8 Aug 2025 20:14:34 +0530 Subject: [PATCH 5/8] added testcase --- backend/app/tests/api/routes/test_threads.py | 55 ++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/backend/app/tests/api/routes/test_threads.py b/backend/app/tests/api/routes/test_threads.py index 61584de6..34472123 100644 --- a/backend/app/tests/api/routes/test_threads.py +++ b/backend/app/tests/api/routes/test_threads.py @@ -168,6 +168,61 @@ def test_threads_sync_endpoint_active_run( assert "active run" in response_json["error"].lower() +@patch("app.api.routes.threads.configure_openai") +@patch("app.api.routes.threads.get_provider_credential") +def test_threads_sync_endpoint_success( + mock_get_provider_credential, mock_configure_openai, client, db, user_api_key_header +): + """Test the /threads/sync endpoint for successful completion.""" + # Setup mock client + mock_client = MagicMock() + mock_get_provider_credential.return_value = {"api_key": "dummy_api_key"} + mock_configure_openai.return_value = (mock_client, True) + + # Simulate thread validation (no active runs) + mock_client.beta.threads.runs.list.return_value = MagicMock(data=[]) + + # Simulate thread creation + dummy_thread = MagicMock() + dummy_thread.id = "sync_thread_id" + mock_client.beta.threads.create.return_value = dummy_thread + + # Simulate message creation + mock_client.beta.threads.messages.create.return_value = None + + # Simulate successful run + mock_run = MagicMock() + mock_run.status = "completed" + mock_run.usage.prompt_tokens = 10 + mock_run.usage.completion_tokens = 20 + mock_run.usage.total_tokens = 30 + mock_run.model = "gpt-4" + mock_client.beta.threads.runs.create_and_poll.return_value = mock_run + + # Simulate message retrieval + dummy_message = MagicMock() + dummy_message.content = [MagicMock(text=MagicMock(value="Test response"))] + mock_client.beta.threads.messages.list.return_value.data = [dummy_message] + + request_data = { + "question": "Test question", + "assistant_id": "assistant_123", + } + + response = client.post( + "/api/v1/threads/sync", json=request_data, headers=user_api_key_header + ) + + assert response.status_code == 200 + response_json = response.json() + assert response_json["success"] is True + assert response_json["data"]["status"] == "success" + assert response_json["data"]["message"] == "Test response" + assert response_json["data"]["thread_id"] == "sync_thread_id" + assert "diagnostics" in response_json["data"] + assert response_json["data"]["diagnostics"]["total_tokens"] == 30 + + def test_validate_thread_no_thread_id(): """Test validate_thread when no thread_id is provided.""" mock_client = MagicMock() From e4efe06e19e2a9ef17acc985c89fa8571088f7cb Mon Sep 17 00:00:00 2001 From: AkhileshNegi Date: Wed, 13 Aug 2025 12:06:55 +0530 Subject: [PATCH 6/8] minor cleanups --- backend/app/tests/api/routes/test_threads.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/backend/app/tests/api/routes/test_threads.py b/backend/app/tests/api/routes/test_threads.py index 34472123..8d2ffc79 100644 --- a/backend/app/tests/api/routes/test_threads.py +++ b/backend/app/tests/api/routes/test_threads.py @@ -6,7 +6,6 @@ from app.api.routes.threads import ( process_run, - router, validate_thread, setup_thread, process_message_content, @@ -20,9 +19,6 @@ from openai import OpenAIError -# The client fixture from conftest.py will be used instead of creating a standalone app - - @patch("app.api.routes.threads.configure_openai") @patch("app.api.routes.threads.get_provider_credential") def test_threads_endpoint( From 932bd8ace407113a6bba7d8d8580dc0abdafb0ed Mon Sep 17 00:00:00 2001 From: AkhileshNegi Date: Thu, 14 Aug 2025 13:16:02 +0530 Subject: [PATCH 7/8] reverting some changes --- backend/app/api/routes/threads.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/app/api/routes/threads.py b/backend/app/api/routes/threads.py index 0e283989..95630bfb 100644 --- a/backend/app/api/routes/threads.py +++ b/backend/app/api/routes/threads.py @@ -392,12 +392,12 @@ async def threads_sync( # Validate thread is_valid, error_message = validate_thread(client, request.get("thread_id")) if not is_valid: - return APIResponse.failure_response(error=error_message) + raise Exception(error_message) # Setup thread is_success, error_message = setup_thread(client, request) if not is_success: - return APIResponse.failure_response(error=error_message) + raise Exception(error_message) tracer = LangfuseTracer( credentials=langfuse_credentials, From d886f4eddae2822a41d68aac09821dfe712fe703 Mon Sep 17 00:00:00 2001 From: AkhileshNegi Date: Thu, 14 Aug 2025 15:05:13 +0530 Subject: [PATCH 8/8] assert exception --- backend/app/tests/api/routes/test_threads.py | 31 ++++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/backend/app/tests/api/routes/test_threads.py b/backend/app/tests/api/routes/test_threads.py index 8d2ffc79..341b8d11 100644 --- a/backend/app/tests/api/routes/test_threads.py +++ b/backend/app/tests/api/routes/test_threads.py @@ -21,8 +21,16 @@ @patch("app.api.routes.threads.configure_openai") @patch("app.api.routes.threads.get_provider_credential") +@patch("app.api.routes.threads.send_callback") +@patch("app.api.routes.threads.process_run") def test_threads_endpoint( - mock_get_provider_credential, mock_configure_openai, client, db, user_api_key_header + mock_process_run, + mock_send_callback, + mock_get_provider_credential, + mock_configure_openai, + client, + db, + user_api_key_header, ): """ Test the /threads endpoint when creating a new thread. @@ -155,13 +163,12 @@ def test_threads_sync_endpoint_active_run( "thread_id": "existing_thread", } - response = client.post( - "/api/v1/threads/sync", json=request_data, headers=user_api_key_header - ) - assert response.status_code == 200 - response_json = response.json() - assert response_json["success"] is False - assert "active run" in response_json["error"].lower() + # Expect the endpoint to raise when there's an active run + with pytest.raises(Exception) as excinfo: + client.post( + "/api/v1/threads/sync", json=request_data, headers=user_api_key_header + ) + assert "active run" in str(excinfo.value).lower() @patch("app.api.routes.threads.configure_openai") @@ -493,8 +500,14 @@ def test_poll_run_and_prepare_response_non_completed( @patch("app.api.routes.threads.configure_openai") @patch("app.api.routes.threads.get_provider_credential") +@patch("app.api.routes.threads.poll_run_and_prepare_response") def test_threads_start_endpoint_creates_thread( - mock_get_provider_credential, mock_configure_openai, client, db, user_api_key_header + mock_poll_run, + mock_get_provider_credential, + mock_configure_openai, + client, + db, + user_api_key_header, ): """Test /threads/start creates thread and schedules background task.""" mock_client = MagicMock()