diff --git a/26_T1/afl_player_tracking_and_crowd_monitoring/backend/app/config.py b/26_T1/afl_player_tracking_and_crowd_monitoring/backend/app/config.py index 99ace030..c3df82ff 100644 --- a/26_T1/afl_player_tracking_and_crowd_monitoring/backend/app/config.py +++ b/26_T1/afl_player_tracking_and_crowd_monitoring/backend/app/config.py @@ -10,7 +10,8 @@ USE_MOCK_SERVICES = os.getenv("USE_MOCK_SERVICES", "true").lower() == "true" -DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://user:password@localhost:5432/orion_db") +# Have just added async driver ('+asyncpg') to URL to match app - Lucas +DATABASE_URL = os.getenv("DATABASE_URL", "postgresql+asyncpg://user:password@localhost:5432/orion_db") JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "your-secret-key-here") JWT_ALGORITHM = os.getenv("JWT_ALGORITHM", "HS256") diff --git a/26_T1/afl_player_tracking_and_crowd_monitoring/backend/app/main.py b/26_T1/afl_player_tracking_and_crowd_monitoring/backend/app/main.py index b90ffeb0..e6933ecd 100644 --- a/26_T1/afl_player_tracking_and_crowd_monitoring/backend/app/main.py +++ b/26_T1/afl_player_tracking_and_crowd_monitoring/backend/app/main.py @@ -13,7 +13,7 @@ ) logger = logging.getLogger(__name__) -Base.metadata.create_all(bind=engine) +# Base.metadata.create_all(bind=engine.sync_engine) app = FastAPI( title="Project Orion Backend API", diff --git a/26_T1/afl_player_tracking_and_crowd_monitoring/backend/app/routes/upload.py b/26_T1/afl_player_tracking_and_crowd_monitoring/backend/app/routes/upload.py index 9e059af9..e2860b43 100644 --- a/26_T1/afl_player_tracking_and_crowd_monitoring/backend/app/routes/upload.py +++ b/26_T1/afl_player_tracking_and_crowd_monitoring/backend/app/routes/upload.py @@ -12,6 +12,8 @@ from app.services.player_client import get_player_data from app.services.crowd_client import get_crowd_data +from sqlalchemy import select + router = APIRouter() ALLOWED_EXTENSIONS = {".mp4", ".avi", ".mov"} @@ -44,16 +46,17 @@ async def process_video(job_id: str, file_path: str): status = "done" error = None - job = db.query(Job).filter(Job.job_id == job_id).first() + result = await db.execute(select(Job).where(Job.job_id == job_id)) + job = result.scalar_one_or_none() if job: job.status = status job.player_result = player_data job.crowd_result = crowd_data job.error = error job.updated_at = datetime.now(timezone.utc) - db.commit() + await db.commit() finally: - db.close() + await db.close() if os.path.exists(file_path): os.remove(file_path) diff --git a/26_T1/afl_player_tracking_and_crowd_monitoring/backend/tests/test_jobs.py b/26_T1/afl_player_tracking_and_crowd_monitoring/backend/tests/test_jobs.py index e69de29b..8ccf087d 100644 --- a/26_T1/afl_player_tracking_and_crowd_monitoring/backend/tests/test_jobs.py +++ b/26_T1/afl_player_tracking_and_crowd_monitoring/backend/tests/test_jobs.py @@ -0,0 +1,180 @@ +from datetime import datetime, timezone +from types import SimpleNamespace + +from app.main import app +from app.auth.dependencies import get_current_user + +# Bypass authentication +def override_get_current_user(): + return {"sub": "test_user", "role": "admin"} + +# Mock DB record +def make_job( + job_id="job-123", + status="done", + user_id="test_user", + player_result=None, + crowd_result=None, + error=None, + video_path="uploads/test.mp4", + ): + now = datetime.now(timezone.utc) + + return SimpleNamespace( + job_id=job_id, + status=status, + user_id=user_id, + player_result=player_result, + crowd_result=crowd_result, + error=error, + video_path=video_path, + created_at=now, + updated_at=now, + ) + +# Test: Get job status (success) +def test_get_status_success(client, mock_db): + app.dependency_overrides[get_current_user] = override_get_current_user + fake_job = make_job( + status="done", + player_result={"players": 10}, + crowd_result={"crowd": 50}, + ) + mock_db.query.return_value.filter.return_value.first.return_value = fake_job # Mock DB query to return fake job + response = client.get("/status/job-123") # Send GET request to endpoint + assert response.status_code == 200 + data = response.json() + assert data["job_id"] == "job-123" + assert data["status"] == "done" + assert "results" in data + +# Test: Job status not found +def test_get_status_not_found(client, mock_db): + app.dependency_overrides[get_current_user] = override_get_current_user + + mock_db.query.return_value.filter.return_value.first.return_value = None + + response = client.get("/status/missing-job") + + assert response.status_code == 404 + assert response.json()["detail"] == "Job not found" + +# Test: List jobs (pagination) +def test_list_jobs_with_pagination(client, mock_db): + app.dependency_overrides[get_current_user] = override_get_current_user + + # Create fake jobs list + jobs = [ + make_job(job_id="job-1"), + make_job(job_id="job-2"), + make_job(job_id="job-3"), + ] + + mock_db.query.return_value.count.return_value = 3 + mock_db.query.return_value.order_by.return_value.offset.return_value.limit.return_value.all.return_value = jobs[:2] + + response = client.get("/jobs?page=1&limit=2") + assert response.status_code == 200 + data = response.json() + + # Check pagination fields + assert data["total"] == 3 + assert data["page"] == 1 + assert data["limit"] == 2 + assert len(data["jobs"]) == 2 # only 2 returned + +# Test: Get job details +def test_get_job_success(client, mock_db): + app.dependency_overrides[get_current_user] = override_get_current_user + + # Fake completed job + fake_job = make_job( + status="done", + player_result={"players": 10}, + crowd_result={"crowd": 50}, + ) + + mock_db.query.return_value.filter.return_value.first.return_value = fake_job + + response = client.get("/jobs/job-123") + assert response.status_code == 200 + data = response.json() + # Validate job data + assert data["job_id"] == "job-123" + assert data["status"] == "done" + assert "results" in data + +# Test Get job not found +def test_get_job_not_found(client, mock_db): + app.dependency_overrides[get_current_user] = override_get_current_user + + # No job returned + mock_db.query.return_value.filter.return_value.first.return_value = None + + response = client.get("/jobs/missing-job") + assert response.status_code == 404 + assert response.json()["detail"] == "Job not found" + +# Test: Retry job (success case) +def test_retry_job_success(client, mock_db, monkeypatch): + app.dependency_overrides[get_current_user] = override_get_current_user + + # Partial job (only one result missing) + fake_job = make_job( + status="partial", + player_result=None, + crowd_result={"crowd": 50}, + ) + + mock_db.query.return_value.filter.return_value.first.return_value = fake_job + + # Mock async player service response + async def fake_player_data(video_path): + return {"players": 12} + + # Replace real service call with fake one + monkeypatch.setattr("app.routes.jobs.get_player_data", fake_player_data) + + response = client.post("/jobs/job-123/retry") + assert response.status_code == 200 + data = response.json() + + # Job should now be completed + assert data["job_id"] == "job-123" + assert data["status"] == "done" + +# Test: Retry invalid job +def test_retry_job_not_partial(client, mock_db): + app.dependency_overrides[get_current_user] = override_get_current_user + + # Job already complete + fake_job = make_job( + status="done", + player_result={"players": 10}, + crowd_result={"crowd": 50}, + ) + + mock_db.query.return_value.filter.return_value.first.return_value = fake_job + + response = client.post("/jobs/job-123/retry") + + # Should fail + assert response.status_code == 400 + assert response.json()["detail"] == "Only partial jobs can be retried" + +# Test: Delete job +def test_delete_job_success(client, mock_db): + app.dependency_overrides[get_current_user] = override_get_current_user + + fake_job = make_job() + + # Mock DB returning a job + mock_db.query.return_value.filter.return_value.first.return_value = fake_job + + response = client.delete("/jobs/job-123") + assert response.status_code == 200 + assert response.json()["message"] == "job deleted" + + # Ensure DB methods were called + mock_db.delete.assert_called_once_with(fake_job) + mock_db.commit.assert_called() \ No newline at end of file diff --git a/26_T1/afl_player_tracking_and_crowd_monitoring/backend/tests/test_upload.py b/26_T1/afl_player_tracking_and_crowd_monitoring/backend/tests/test_upload.py index e69de29b..f401fdbf 100644 --- a/26_T1/afl_player_tracking_and_crowd_monitoring/backend/tests/test_upload.py +++ b/26_T1/afl_player_tracking_and_crowd_monitoring/backend/tests/test_upload.py @@ -0,0 +1,28 @@ +from app.main import app +from app.auth.dependencies import get_current_user + +def override_get_current_user(): + return {"sub": "test_user", "role": "admin"} + +async def fake_process_video(job_id, file_path): + return None + +def test_upload_valid_file(client, monkeypatch): + app.dependency_overrides[get_current_user] = override_get_current_user + monkeypatch.setattr("app.routes.upload.process_video", fake_process_video) + response = client.post("/upload", files={"file": ("test.mp4", b"fake video content", "video/mp4")}) + assert response.status_code == 200 + assert "job_id" in response.json() + assert response.json()["status"] == "processing" + +def test_upload_invalid_file_type(client): + app.dependency_overrides[get_current_user] = override_get_current_user + response = client.post("/upload", files={"file": ("text.txt", b"dummy,data", "text/plain")}) + assert response.status_code == 400 + assert "invalid" in str(response.json()).lower() + +def test_missing_file(client): + app.dependency_overrides[get_current_user] = override_get_current_user + response = client.post("/upload", files={}) + assert response.status_code == 422 + assert "file" in str(response.json()) \ No newline at end of file