mirror of
https://github.com/sstent/FitTrack_ReportGenerator.git
synced 2026-01-26 00:52:03 +00:00
sync
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user