From d980558bee0b207483a0f13ac48d5e65ba479f4a Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Mon, 29 Jun 2026 17:51:15 +0530 Subject: [PATCH] feat: Setup celery worker --- app/core/exceptions.py | 6 +-- tests/api/test_jobs_api.py | 77 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 tests/api/test_jobs_api.py diff --git a/app/core/exceptions.py b/app/core/exceptions.py index e58ceab..96accdd 100644 --- a/app/core/exceptions.py +++ b/app/core/exceptions.py @@ -1,13 +1,12 @@ -from fastapi import HTTPException, status - - class AlemnoException(Exception): """Base exception for Alemno Payments Application""" + pass class JobNotFoundException(AlemnoException): """Raised when a job is not found""" + def __init__(self, job_id: str): self.job_id = job_id super().__init__(f"Job with ID {job_id} not found") @@ -15,6 +14,7 @@ class JobNotFoundException(AlemnoException): class InvalidCSVException(AlemnoException): """Raised when the uploaded CSV is invalid""" + def __init__(self, detail: str): self.detail = detail super().__init__(detail) diff --git a/tests/api/test_jobs_api.py b/tests/api/test_jobs_api.py new file mode 100644 index 0000000..5cd45ef --- /dev/null +++ b/tests/api/test_jobs_api.py @@ -0,0 +1,77 @@ +import io +import pytest +from httpx import AsyncClient +from app.worker import celery_app +pytestmark = pytest.mark.asyncio +celery_app.conf.task_always_eager = True +async def test_upload_csv_success(client: AsyncClient): + csv_content = ( + "transaction_id,date,amount,category,description\n" + "TX1001,2026-06-25,120.50,Groceries,Weekly grocery shop\n" + "TX1002,06/26/2026,15000.00,Electronics,New Laptop\n" # This will be flagged as anomaly (> $5,000) + ) + + files = { + "file": ( + "transactions.csv", + io.BytesIO(csv_content.encode("utf-8")), + "text/csv", + ) + } + response = await client.post("/api/v1/jobs/upload", files=files) + assert response.status_code == 201 + + data = response.json() + assert "id" in data + assert data["filename"] == "transactions.csv" + assert data["status"] == "pending" # Initial state returned by router immediately + job_id = data["id"] + status_response = await client.get(f"/api/v1/jobs/{job_id}/status") + assert status_response.status_code == 200 + status_data = status_response.json() + assert status_data["id"] == job_id + assert status_data["status"] == "completed" + assert status_data["summary"]["total_rows"] == 2 + assert status_data["summary"]["total_amount"] == 15120.50 + assert status_data["summary"]["anomalies_count"] == 1 + assert status_data["summary"]["top_category"] == "Electronics" + results_response = await client.get(f"/api/v1/jobs/{job_id}/results") + assert results_response.status_code == 200 + results_data = results_response.json() + assert results_data["id"] == job_id + assert results_data["status"] == "completed" + + results = results_data["results"] + assert len(results["cleaned_transactions"]) == 2 + assert len(results["flagged_anomalies"]) == 1 + assert results["flagged_anomalies"][0]["transaction_id"] == "TX1002" + assert results["spend_breakdown"]["Groceries"]["total_spend"] == 120.50 + assert "Successfully parsed and processed" in results["narrative_summary"] +async def test_upload_invalid_csv(client: AsyncClient): + csv_content = ( + "transaction_id,amount,category,description\n" + "TX1001,120.50,Groceries,Weekly grocery shop\n" + ) + files = { + "file": ( + "transactions.csv", + io.BytesIO(csv_content.encode("utf-8")), + "text/csv", + ) + } + + response = await client.post("/api/v1/jobs/upload", files=files) + assert response.status_code == 400 + assert "Missing required CSV columns" in response.json()["detail"] + + +async def test_get_nonexistent_job(client: AsyncClient): + response = await client.get("/api/v1/jobs/nonexistent-id-xyz/status") + assert response.status_code == 404 + assert "not found" in response.json()["detail"] + + +async def test_get_all_jobs(client: AsyncClient): + response = await client.get("/api/v1/jobs") + assert response.status_code == 200 + assert isinstance(response.json(), list)