This commit is contained in:
2025-10-12 06:38:44 -07:00
parent 9e0bd322d3
commit 3886dcb9ab
158 changed files with 2022 additions and 9699 deletions

View File

@@ -4,15 +4,18 @@ from api.main import app
client = TestClient(app)
def test_analyze_workout_endpoint_exists():
response = client.post("/api/analyze/workout")
# Expecting a 422 Unprocessable Entity because no file is provided
# This confirms the endpoint is routed and expects input
assert response.status_code == 422 or response.status_code == 400
def test_analyze_workout_requires_file():
response = client.post("/api/analyze/workout", data={})
assert response.status_code == 422
assert "file" in response.json()["detail"][0]["loc"]
# More detailed tests will be added once the actual implementation is in place
# More detailed tests will be added once the actual implementation is in place

View File

@@ -1,101 +0,0 @@
import pytest
from fastapi.testclient import TestClient
from api.main import app
from uuid import uuid4
from unittest.mock import patch
import zipfile
import io
client = TestClient(app)
def create_zip_file(file_names_and_content):
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zf:
for name, content in file_names_and_content.items():
zf.writestr(name, content)
zip_buffer.seek(0)
return zip_buffer
@patch('src.db.session.get_db')
@patch('src.core.batch_processor.BatchProcessor')
def test_analyze_batch_success(mock_batch_processor_cls, mock_get_db):
mock_db_session = mock_get_db.return_value
mock_batch_processor_instance = mock_batch_processor_cls.return_value
mock_batch_processor_instance.process_zip_file.return_value = [
{"analysis_id": str(uuid4()), "file_name": "workout1.fit", "status": "completed"},
{"analysis_id": str(uuid4()), "file_name": "workout2.tcx", "status": "completed"}
]
zip_content = create_zip_file({"workout1.fit": b"dummy_fit_content", "workout2.tcx": b"dummy_tcx_content"})
response = client.post(
"/api/analyze/batch",
files={"zip_file": ("workouts.zip", zip_content.getvalue(), "application/zip")},
data={
"user_id": str(uuid4()),
"ftp_value": 250.0
}
)
assert response.status_code == 200
response_json = response.json()
assert "batch_id" in response_json
assert response_json["status"] == "completed"
assert response_json["total_files"] == 2
assert "results" in response_json
assert len(response_json["results"]) == 2
assert mock_batch_processor_instance.process_zip_file.called
@patch('src.db.session.get_db')
@patch('src.core.batch_processor.BatchProcessor')
def test_analyze_batch_empty_zip(mock_batch_processor_cls, mock_get_db):
zip_content = create_zip_file({})
response = client.post(
"/api/analyze/batch",
files={"zip_file": ("empty.zip", zip_content.getvalue(), "application/zip")}
)
assert response.status_code == 400
assert response.json()["code"] == "EMPTY_ZIP_FILE"
@patch('src.db.session.get_db')
@patch('src.core.batch_processor.BatchProcessor')
def test_analyze_batch_partial_failure(mock_batch_processor_cls, mock_get_db):
mock_db_session = mock_get_db.return_value
mock_batch_processor_instance = mock_batch_processor_cls.return_value
mock_batch_processor_instance.process_zip_file.return_value = [
{"analysis_id": str(uuid4()), "file_name": "workout1.fit", "status": "completed"},
{"file_name": "workout_bad.fit", "status": "failed", "error_message": "Corrupted file"}
]
zip_content = create_zip_file({"workout1.fit": b"dummy_fit_content", "workout_bad.fit": b"bad_content"})
response = client.post(
"/api/analyze/batch",
files={"zip_file": ("workouts.zip", zip_content.getvalue(), "application/zip")}
)
assert response.status_code == 200
response_json = response.json()
assert response_json["status"] == "completed_with_errors"
assert response_json["total_files"] == 2
assert len(response_json["results"]) == 2
assert any(r["status"] == "failed" for r in response_json["results"])
@patch('src.db.session.get_db')
@patch('src.core.batch_processor.BatchProcessor')
def test_analyze_batch_internal_error(mock_batch_processor_cls, mock_get_db):
mock_batch_processor_cls.side_effect = Exception("Unexpected error")
zip_content = create_zip_file({"workout1.fit": b"dummy_fit_content"})
response = client.post(
"/api/analyze/batch",
files={"zip_file": ("workouts.zip", zip_content.getvalue(), "application/zip")}
)
assert response.status_code == 500
assert response.json()["code"] == "INTERNAL_SERVER_ERROR"

View File

@@ -1,86 +1,152 @@
import pytest
from fastapi.testclient import TestClient
from api.main import app
from uuid import uuid4
from unittest.mock import patch
from uuid import UUID, uuid4
from unittest.mock import patch, AsyncMock
import httpx
from src.core.workout_data import WorkoutData, WorkoutMetadata, PowerData, HeartRateData, SpeedData, ElevationData
import pandas as pd
from datetime import datetime, timedelta
client = TestClient(app)
@pytest.fixture
def mock_workout_analysis():
# Mock a WorkoutAnalysis object that would be returned by the database
class MockWorkoutAnalysis:
def __init__(self, analysis_id, chart_paths):
self.id = analysis_id
self.chart_paths = chart_paths
def mock_workout_data():
# Create a mock WorkoutData object
timestamps = pd.to_datetime(
[datetime(2025, 1, 1, 10, 0, 0) + timedelta(seconds=i) for i in range(600)]
)
power = pd.Series([150 + 50 * (i % 10) for i in range(600)], index=timestamps)
return MockWorkoutAnalysis(uuid4(), {
"power_curve": "/tmp/power_curve.png",
"elevation_profile": "/tmp/elevation_profile.png",
"zone_distribution_power": "/tmp/zone_distribution_power.png",
"zone_distribution_hr": "/tmp/zone_distribution_hr.png",
"zone_distribution_speed": "/tmp/zone_distribution_speed.png"
})
time_series_data = pd.DataFrame({"power": power})
@patch('src.db.session.get_db')
@patch('src.core.chart_generator.ChartGenerator')
def test_get_analysis_charts_success(mock_chart_generator, mock_get_db, mock_workout_analysis):
# Mock the database session to return our mock_workout_analysis
mock_db_session = mock_get_db.return_value
mock_db_session.query.return_value.filter.return_value.first.return_value = mock_workout_analysis
metadata = WorkoutMetadata(
start_time=datetime(2025, 1, 1, 10, 0, 0),
duration=timedelta(minutes=10),
device="Garmin",
file_type="FIT",
)
# Mock the ChartGenerator to simulate chart generation
mock_chart_instance = mock_chart_generator.return_value
mock_chart_instance.generate_power_curve_chart.return_value = None
mock_chart_instance.generate_elevation_profile_chart.return_value = None
mock_chart_instance.generate_zone_distribution_chart.return_value = None
power_data = PowerData(
raw_power_stream=power.tolist(),
average_power=power.mean(),
normalized_power=power.mean() * 1.05, # Dummy value
intensity_factor=0.8,
training_stress_score=50,
zone_distribution={"Z1": 100, "Z2": 200, "Z3": 300},
)
# Create dummy chart files for the test
for chart_type, path in mock_workout_analysis.chart_paths.items():
with open(path, "wb") as f:
f.write(b"dummy_png_content")
return WorkoutData(
metadata=metadata,
time_series_data=time_series_data,
power_data=power_data,
heart_rate_data=HeartRateData(),
speed_data=SpeedData(),
elevation_data=ElevationData(),
)
@patch("api.routers.analysis.CentralDBClient")
@patch("api.routers.analysis.FitParser")
async def test_get_analysis_charts_success(
mock_fit_parser,
mock_centraldb_client,
mock_workout_data,
client,
):
mock_centraldb_instance = AsyncMock()
mock_centraldb_instance.retrieve_chart = AsyncMock(
side_effect=httpx.HTTPStatusError(
"", request=None, response=httpx.Response(status_code=404)
)
)
mock_centraldb_instance.download_fit_file = AsyncMock(
return_value=b"dummy_fit_content"
)
mock_centraldb_instance.upload_chart = AsyncMock()
mock_centraldb_instance.get_analysis_artifact = AsyncMock(
side_effect=httpx.HTTPStatusError(
"", request=None, response=httpx.Response(status_code=404)
)
)
mock_centraldb_instance.create_analysis_artifact = AsyncMock()
mock_centraldb_client.return_value = mock_centraldb_instance
mock_fit_parser.return_value.parse.return_value = mock_workout_data
analysis_id = uuid4()
chart_type = "power_curve"
response = client.get(f"/api/analysis/{mock_workout_analysis.id}/charts?chart_type={chart_type}")
response = client.get(f"/api/analysis/{analysis_id}/charts?chart_type={chart_type}")
assert response.status_code == 200
assert response.headers["content-type"] == "image/png"
assert response.content == b"dummy_png_content"
assert len(response.content) > 0
@patch('src.db.session.get_db')
def test_get_analysis_charts_not_found(mock_get_db):
mock_db_session = mock_get_db.return_value
mock_db_session.query.return_value.filter.return_value.first.return_value = None
@patch("api.routers.analysis.CentralDBClient")
@patch("api.routers.analysis.FitParser")
async def test_get_analysis_charts_not_found(
mock_fit_parser, mock_centraldb_client, client
):
mock_centraldb_instance = AsyncMock()
mock_centraldb_instance.retrieve_chart = AsyncMock(
side_effect=httpx.HTTPStatusError(
"", request=None, response=httpx.Response(status_code=404)
)
)
mock_centraldb_instance.download_fit_file = AsyncMock(
side_effect=httpx.HTTPStatusError(
"", request=None, response=httpx.Response(status_code=404)
)
)
mock_centraldb_client.return_value = mock_centraldb_instance
analysis_id = uuid4()
chart_type = "power_curve"
response = client.get(f"/api/analysis/{analysis_id}/charts?chart_type={chart_type}")
assert response.status_code == 404
assert response.json()["code"] == "ANALYSIS_NOT_FOUND"
assert response.json()["code"] == "CHART_RETRIEVAL_ERROR"
@patch('src.db.session.get_db')
def test_get_analysis_charts_chart_type_not_found(mock_get_db, mock_workout_analysis):
mock_db_session = mock_get_db.return_value
mock_db_session.query.return_value.filter.return_value.first.return_value = mock_workout_analysis
# Remove the chart path for the requested type to simulate not found
mock_workout_analysis.chart_paths.pop("power_curve")
@patch("api.routers.analysis.CentralDBClient")
@patch("api.routers.analysis.FitParser")
async def test_get_analysis_charts_chart_type_not_found(
mock_fit_parser, mock_centraldb_client, client
):
mock_centraldb_instance = AsyncMock()
mock_centraldb_instance.retrieve_chart = AsyncMock(
side_effect=httpx.HTTPStatusError(
"", request=None, response=httpx.Response(status_code=404)
)
)
mock_centraldb_instance.download_fit_file = AsyncMock(
return_value=b"dummy_fit_content"
)
mock_centraldb_client.return_value = mock_centraldb_instance
analysis_id = uuid4()
chart_type = "invalid_chart_type"
response = client.get(f"/api/analysis/{analysis_id}/charts?chart_type={chart_type}")
assert response.status_code == 400
assert response.json()["code"] == "INVALID_CHART_TYPE"
@patch("api.routers.analysis.CentralDBClient")
async def test_get_analysis_charts_retrieval_error(mock_centraldb_client, client):
mock_centraldb_instance = AsyncMock()
mock_centraldb_instance.retrieve_chart = AsyncMock(
side_effect=httpx.HTTPStatusError(
"", request=None, response=httpx.Response(status_code=500)
)
)
mock_centraldb_client.return_value = mock_centraldb_instance
analysis_id = uuid4()
chart_type = "power_curve"
response = client.get(f"/api/analysis/{mock_workout_analysis.id}/charts?chart_type={chart_type}")
assert response.status_code == 404
assert response.json()["code"] == "CHART_NOT_FOUND"
@patch('src.db.session.get_db')
def test_get_analysis_charts_file_not_found(mock_get_db, mock_workout_analysis):
mock_db_session = mock_get_db.return_value
mock_db_session.query.return_value.filter.return_value.first.return_value = mock_workout_analysis
# Ensure the dummy file is not created to simulate file not found
chart_type = "power_curve"
response = client.get(f"/api/analysis/{mock_workout_analysis.id}/charts?chart_type={chart_type}")
response = client.get(f"/api/analysis/{analysis_id}/charts?chart_type={chart_type}")
assert response.status_code == 500
assert response.json()["code"] == "CHART_FILE_ERROR"
assert response.json()["code"] == "CHART_RETRIEVAL_ERROR"