From 1de9f46517613f4ceac69a89da8aa448922750cc Mon Sep 17 00:00:00 2001 From: Ireneusz Bachanowicz Date: Fri, 18 Jul 2025 00:57:28 +0200 Subject: [PATCH] feat: Add endpoint to create Jira analysis records and update existing endpoints for consistency --- api/handlers.py | 27 +++++++++++--- database/crud.py | 9 +++-- database/models.py | 1 - jira_analyses.db | Bin 36864 -> 16384 bytes jira_webhook_llm.py | 86 +++++++++++++++++++------------------------- llm/models.py | 1 - tests/conftest.py | 37 ++++++++++--------- tests/test_core.py | 27 +++++++++++--- 8 files changed, 107 insertions(+), 81 deletions(-) diff --git a/api/handlers.py b/api/handlers.py index 5eaf8fd..a41e0c5 100644 --- a/api/handlers.py +++ b/api/handlers.py @@ -2,10 +2,10 @@ from fastapi import APIRouter, Request, HTTPException, Depends from fastapi.responses import JSONResponse from typing import Dict, Any import config -from llm.models import LLMResponse +from llm.models import LLMResponse, JiraWebhookPayload from database.database import get_db_session # Removed Session import here from sqlalchemy.orm import Session # Added correct SQLAlchemy import -from database.crud import get_all_analysis_records, delete_all_analysis_records, get_analysis_by_id +from database.crud import get_all_analysis_records, delete_all_analysis_records, get_analysis_by_id, create_analysis_record router = APIRouter( prefix="/api", @@ -13,7 +13,7 @@ router = APIRouter( ) -@router.get("/requests") +@router.get("/request") async def get_analysis_records_endpoint(db: Session = Depends(get_db_session)): """Get analysis records""" try: @@ -28,6 +28,21 @@ async def get_analysis_records_endpoint(db: Session = Depends(get_db_session)): content={"error": str(e)} ) +@router.post("/request", status_code=201) +async def create_analysis_record_endpoint( + payload: JiraWebhookPayload, + db: Session = Depends(get_db_session) +): + """Create a new Jira analysis record""" + try: + db_record = create_analysis_record(db, payload) + return JSONResponse( + status_code=201, + content={"message": "Record created successfully", "record_id": db_record.id} + ) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to create record: {str(e)}") + @router.post("/test-llm") async def test_llm_endpoint(db: Session = Depends(get_db_session)): """Test endpoint for LLM integration""" @@ -50,7 +65,7 @@ async def test_llm_endpoint(db: Session = Depends(get_db_session)): } ) -@router.delete("/requests") +@router.delete("/request") async def delete_analysis_records_endpoint(db: Session = Depends(get_db_session)): """Delete analysis records""" try: @@ -63,7 +78,9 @@ async def delete_analysis_records_endpoint(db: Session = Depends(get_db_session) return JSONResponse( status_code=500, content={"error": str(e)}) -@router.get("/requests/{record_id}") + + +@router.get("/request/{record_id}") async def get_analysis_record_endpoint(record_id: int, db: Session = Depends(get_db_session)): """Get specific analysis record by ID""" record = get_analysis_by_id(db, record_id) diff --git a/database/crud.py b/database/crud.py index 1200c46..d317385 100644 --- a/database/crud.py +++ b/database/crud.py @@ -1,6 +1,6 @@ from loguru import logger from sqlalchemy.orm import Session -from datetime import datetime +from datetime import datetime, timezone import json from typing import Dict, Any, Optional @@ -11,12 +11,11 @@ def create_analysis_record(db: Session, payload: JiraWebhookPayload) -> JiraAnal """Creates a new Jira analysis record in the database.""" db_analysis = JiraAnalysis( issue_key=payload.issueKey, - project_key=payload.projectKey, status="pending", issue_summary=payload.summary, request_payload=payload.model_dump(), - created_at=datetime.utcnow(), - updated_at=datetime.utcnow() + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc) ) db.add(db_analysis) db.commit() @@ -45,7 +44,7 @@ def update_analysis_record( db_analysis = db.query(JiraAnalysis).filter(JiraAnalysis.id == record_id).first() if db_analysis: db_analysis.status = status - db_analysis.updated_at = datetime.utcnow() + db_analysis.updated_at = datetime.now(timezone.utc) if analysis_result: db_analysis.analysis_result = analysis_result if error_message: diff --git a/database/models.py b/database/models.py index 2429c86..95f2be2 100644 --- a/database/models.py +++ b/database/models.py @@ -9,7 +9,6 @@ class JiraAnalysis(Base): id = Column(Integer, primary_key=True, index=True) issue_key = Column(String, index=True, nullable=False) - project_key = Column(String, index=True, nullable=False) status = Column(String, default="pending", nullable=False) # pending, processing, completed, failed issue_summary = Column(Text, nullable=False) request_payload = Column(JSON, nullable=False) # Store the original Jira webhook payload diff --git a/jira_analyses.db b/jira_analyses.db index 52d57ae29634a88b3ae77a95ee64e992139ddcca..64627cbef8dfcd48163c5cc70f1a11094342da3c 100644 GIT binary patch delta 197 zcmZozz|_#dI6<0~nSp_UWuk(;Ff)T*Stl=$$IQExfp0ROKJV6zjn2GG70i?U`HUxv z@hMEs<>Q#Vicgj)iD~j{UhTEOLKD* ziz*dDTq8mz2l3C*ElMnpFG?*g$j>WIRqzV-_tR0};tUA#^mPo1RPc6edstzU5m26RRC|a@Mx{KlNP~3RA z%kB?KwBdt`UV5wlg8nPL_0&VIJqIXS^w0uDFYTe}tejbBWF2`~XBzyz286JP>NfC(@GCh#^1bTjW=DK4eQwrj~_``Gx> z_Jtu_;fwXT4(9XTanDlu~+t;wo^K3f5Qsxbo#wrcs`XBLyVYpl zr`yf@@qk@ko`cTKX8)=*Zz21y>_4-ApCsOFKw{YUms**|9gfKz@k0Vco%m;e)C0!)AjFaajO z1egF5U;^(Lf$wFmrnmN6*K6fkaqe_t_H-h1Ix+Kp=ITOHX0rbOI+gu5e*6Dh_AlA5 z-?0vIbC>`VU;<2l2`~XBzyz286JP>NfC(^xZ$w}&lTOdgspIUdI%YEJI5U&UEG($l z{Pet)!XLkw025#WOn?b6fj3Ct`N!$^Q&&I!?d?>f@$4}1y!V9dNbAXB99wu>ju-2+ znB*v8#bqr)4eQIu4Lvf&FpPXbmZYI@L*Y1rOwT36Z`!6XQC0@XbkxNc zEtp6>I43nvJ09*9A&!W7jAiLAilS|%x;Uo5b8ORw;GySJPmFLBLvM_6cK5a#VuJn<2 zrksk?tW!ZNl&@>6*R@hdFV_oty;RFrkM~dU#Be(-JY~QM_r5(Iui;X9mvs+ zNQgz&OQa`|FpK~yNxCLVh3Gs+7mpXw4P}m8Pn8`h zr8&7l6_3$=dgrF+o3eeJrw5;{3t)D=BinqOqn0hr$KwElqn8Teh^}q>^3n3c9CiA3 zK(Qj@h$EAROuv0HIG^l?P|%Pbgh7( zg#k`%_mIY36c;*)d8L6E4%KzkkE|T|_F+G~L3?g|mxua&LZdAu3e5o=I|Qn}7daMn zq3VdW#YL#rbcgTd=?m2aGr6%3LDE&CaBEzb3Ai_CL%4CHf@lau4$%YE7Sj>F-5aY$ z0Bs!i3j+#nsD8PR;sU5fSR<$uMquTH?)CCq~ISeT(Ro@fO|RH};uD5qj(P3fZA_KdbR?sT?0`9iHw!pOsL)W!V+r3J96 zah%+9C1z+CV+6y<)AkY!$Lve}IP#k)x%!m-54y8>j$gyF4UN6A`qg0}{mj z?7)mBBq3JP5vE3EQKmOS(#k_S50$dMYO0`9Dnu(uZ$l{tD|8(%dhqj`Ul_*D-a%lf zaj`>38as_nqh&xq07W+RveB9hrg0EYD8}756h1Ep&O-dqf#TPpS5)aeVqL&>4@N}?4 zfE@z=%MO7ji||^v;10Eij%)^|fU8vb=io@_sb#xzRYyVS4Wz%X9G8jH+LaMpyv_w4 z1-+Qpi?vFraL%KEoMNR|t`@dk_+GKK;m%GAP69X%T=)l+XF-I#{y+cqjIS^OCcp%k z026pC1fE~Y@cN%02$=q{R6F_clOG7+^*^uwC(q{a0|ER%z<29`fH$}PuaKtIb-k`> zd96^aR;w@8|4B}Eh{^#{S2Ly0H z00#tcKtLQ{^nVc$@J84FwR*8oFO~AORjpFgPp|)xQ$+AtUf0Scz53eq|5vH`ufEkc z7`~YaFaajO1egF5c$WzLCO3CEb@d1FW&^1tv_OSaBQlUT8{o|bc(VcZKVtJ{1Jm1E naBKm`7Vu^R-<8b 0 else word for i, word in enumerate(x.split('_'))), populate_by_name=True) issueKey: str - projectKey: Optional[str] = None # Added missing field summary: str description: Optional[str] = None comment: Optional[str] = None diff --git a/tests/conftest.py b/tests/conftest.py index b17cd59..3543b7f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,7 @@ import pytest from fastapi.testclient import TestClient -from jira_webhook_llm import create_app, app # Assuming app is created via factory -from database.database import engine, Base # Add import +from jira_webhook_llm import create_app +from database.database import get_db_session, Base # Import get_db_session and Base from sqlalchemy.orm import sessionmaker import os @@ -33,7 +33,6 @@ def setup_db(monkeypatch): def mock_full_jira_payload(setup_db): mock_data = { "issueKey": "PROJ-123", - "projectKey": "PROJ", "summary": "Test Issue", "description": "Test Description", "comment": "Test Comment", @@ -45,26 +44,32 @@ def mock_full_jira_payload(setup_db): } return mock_data -@pytest.fixture -def test_client(setup_db): - # Ensure the database is set up before creating the app - test_engine = setup_db +@pytest.fixture(scope="function") +def test_client(setup_db, monkeypatch): + # Prevent signal handling in tests to avoid "set_wakeup_fd" error + monkeypatch.setattr("jira_webhook_llm.handle_shutdown_signal", lambda *args: None) - # Import and patch the database module to use test database - from database import database as db - db.engine = test_engine - db.SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=test_engine) - - # Create fresh app instance with test DB - from jira_webhook_llm import create_app + # Create a test app instance app = create_app() - return TestClient(app) + + # Override the get_db_session dependency to use the test database + def override_get_db_session(): + with setup_db.connect() as connection: + with sessionmaker(autocommit=False, autoflush=False, bind=connection)() as session: + yield session + + app.dependency_overrides[get_db_session] = override_get_db_session + + with TestClient(app) as client: + yield client + + # Clean up dependency override + app.dependency_overrides.clear() @pytest.fixture def mock_jira_payload(): return { - "projectKey": "TEST-PROJECT", "issueKey": "TEST-123", "summary": "Test Issue", "description": "Test Description", diff --git a/tests/test_core.py b/tests/test_core.py index 518ba24..c4171bc 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -2,6 +2,9 @@ import pytest from fastapi import HTTPException from jira_webhook_llm import app from llm.models import JiraWebhookPayload +from database.crud import create_analysis_record, get_analysis_by_id +from database.models import JiraAnalysis +from database.database import get_db def test_error_handling_middleware(test_client, mock_jira_payload): # Test 404 error handling @@ -12,13 +15,13 @@ def test_error_handling_middleware(test_client, mock_jira_payload): # Test validation error handling invalid_payload = mock_jira_payload.copy() invalid_payload.pop("issueKey") - response = test_client.post("/jira-webhook", json=invalid_payload) + response = test_client.post("/api/jira-webhook", json=invalid_payload) assert response.status_code == 422 assert "details" in response.json() def test_webhook_handler(setup_db, test_client, mock_full_jira_payload): # Test successful webhook handling with full payload - response = test_client.post("/jira-webhook", json=mock_full_jira_payload) + response = test_client.post("/api/jira-webhook", json=mock_full_jira_payload) assert response.status_code == 200 response_data = response.json() assert "status" in response_data @@ -34,14 +37,30 @@ def test_webhook_handler(setup_db, test_client, mock_full_jira_payload): assert record is not None assert record.issue_summary == mock_full_jira_payload["summary"] assert record.request_payload == mock_full_jira_payload - assert record.project_key == mock_full_jira_payload["projectKey"] def test_llm_test_endpoint(test_client): # Test LLM test endpoint - response = test_client.post("/test-llm") + response = test_client.post("/api/test-llm") assert response.status_code == 200 assert "response" in response.json() +def test_create_analysis_record_endpoint(setup_db, test_client, mock_full_jira_payload): + # Test successful creation of a new analysis record via API + response = test_client.post("/api/request", json=mock_full_jira_payload) + assert response.status_code == 201 + response_data = response.json() + assert "message" in response_data + assert response_data["message"] == "Record created successfully" + assert "record_id" in response_data + + # Verify the record exists in the database + with get_db() as db: + record = get_analysis_by_id(db, response_data["record_id"]) + assert record is not None + assert record.issue_key == mock_full_jira_payload["issueKey"] + assert record.issue_summary == mock_full_jira_payload["summary"] + assert record.request_payload == mock_full_jira_payload + @pytest.mark.asyncio async def test_retry_decorator(): # Test retry decorator functionality