diff --git a/README.md b/README.md index 8bd0556..93fce80 100644 --- a/README.md +++ b/README.md @@ -99,12 +99,6 @@ $ touch .env && echo MONGODB_CONNECTION_STRING="$ConnectionString" > .env │   ├── test_motor_service.py │   └── test_rocket_serice.py │ - ├── test_routes - │   ├── test_environment_route.py - │   ├── test_flight_route.py - │   ├── test_motor_route.py - │   └── test_rocket_route.py - │ ├── test_repositories │   ├── test_environment_repo.py │   ├── test_flight_repo.py diff --git a/lib/models/rocket.py b/lib/models/rocket.py index c1763ea..e3b4e6c 100644 --- a/lib/models/rocket.py +++ b/lib/models/rocket.py @@ -17,6 +17,12 @@ class CoordinateSystemOrientation(str, Enum): class Rocket(BaseModel): + + def __eq__(self, other): + if not isinstance(other, Rocket): + return False + return self.dict() == other.dict() + # Required parameters motor: Motor radius: float diff --git a/pyproject.toml b/pyproject.toml index a341da2..eb8954a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,6 @@ disable = """ broad-exception-caught, raise-missing-from, too-many-instance-attributes, - redefined-outer-name, import-error, too-many-arguments, redefined-outer-name, diff --git a/tests/test_routes/conftest.py b/tests/test_routes/conftest.py new file mode 100644 index 0000000..deecd99 --- /dev/null +++ b/tests/test_routes/conftest.py @@ -0,0 +1,95 @@ +import json +import pytest + +from lib.models.rocket import Rocket +from lib.models.motor import Motor, MotorTank, TankFluids, TankKinds +from lib.models.environment import Env + + +@pytest.fixture +def stub_env(): + env = Env(latitude=0, longitude=0) + env_json = env.model_dump_json() + return json.loads(env_json) + + +@pytest.fixture +def stub_motor(): + motor = Motor( + thrust_source=[[0, 0]], + burn_time=0, + nozzle_radius=0, + dry_mass=0, + dry_inertia=[0, 0, 0], + center_of_dry_mass_position=0, + ) + motor_json = motor.model_dump_json() + return json.loads(motor_json) + + +@pytest.fixture +def stub_tank(): + tank = MotorTank( + geometry=[[(0, 0), 0]], + gas=TankFluids(name='gas', density=0), + liquid=TankFluids(name='liquid', density=0), + flux_time=(0, 0), + position=0, + discretize=0, + name='tank', + ) + tank_json = tank.model_dump_json() + return json.loads(tank_json) + + +@pytest.fixture +def stub_level_tank(stub_tank): + stub_tank.update({'tank_kind': TankKinds.LEVEL, 'liquid_height': 0}) + return stub_tank + + +@pytest.fixture +def stub_mass_flow_tank(stub_tank): + stub_tank.update( + { + 'tank_kind': TankKinds.MASS_FLOW, + 'gas_mass_flow_rate_in': 0, + 'gas_mass_flow_rate_out': 0, + 'liquid_mass_flow_rate_in': 0, + 'liquid_mass_flow_rate_out': 0, + 'initial_liquid_mass': 0, + 'initial_gas_mass': 0, + } + ) + return stub_tank + + +@pytest.fixture +def stub_ullage_tank(stub_tank): + stub_tank.update({'tank_kind': TankKinds.ULLAGE, 'ullage': 0}) + return stub_tank + + +@pytest.fixture +def stub_mass_tank(stub_tank): + stub_tank.update( + {'tank_kind': TankKinds.MASS, 'liquid_mass': 0, 'gas_mass': 0} + ) + return stub_tank + + +@pytest.fixture +def stub_rocket(stub_motor): + rocket = Rocket( + motor=stub_motor, + radius=0, + mass=0, + motor_position=0, + center_of_mass_without_motor=0, + inertia=[0, 0, 0], + power_off_drag=[(0, 0)], + power_on_drag=[(0, 0)], + coordinate_system_orientation='TAIL_TO_NOSE', + ) + rocket_json = rocket.model_dump_json() + return json.loads(rocket_json) diff --git a/tests/test_routes/test_environments_route.py b/tests/test_routes/test_environments_route.py index a32bf48..ceea53a 100644 --- a/tests/test_routes/test_environments_route.py +++ b/tests/test_routes/test_environments_route.py @@ -16,13 +16,6 @@ client = TestClient(app) -@pytest.fixture -def stub_env(): - env = Env(latitude=0, longitude=0) - env_json = env.model_dump_json() - return json.loads(env_json) - - @pytest.fixture def stub_env_summary(): env_summary = EnvSummary() diff --git a/tests/test_routes/test_flights_route.py b/tests/test_routes/test_flights_route.py new file mode 100644 index 0000000..d297145 --- /dev/null +++ b/tests/test_routes/test_flights_route.py @@ -0,0 +1,372 @@ +from unittest.mock import patch +import json +import pytest +from fastapi.testclient import TestClient +from fastapi import HTTPException, status +from lib.models.environment import Env +from lib.models.flight import Flight +from lib.models.motor import Motor, MotorKinds +from lib.models.rocket import Rocket +from lib.controllers.flight import FlightController +from lib.views.motor import MotorView +from lib.views.rocket import RocketView +from lib.views.flight import ( + FlightCreated, + FlightUpdated, + FlightDeleted, + FlightSummary, + FlightView, +) +from lib import app + +client = TestClient(app) + + +@pytest.fixture +def stub_flight(stub_env, stub_rocket): + flight = { + 'name': 'Test Flight', + 'environment': stub_env, + 'rocket': stub_rocket, + 'rail_length': 1, + 'time_overshoot': True, + 'terminate_on_apogee': True, + 'equations_of_motion': 'STANDARD', + } + return flight + + +@pytest.fixture +def stub_flight_summary(): + flight_summary = FlightSummary() + flight_summary_json = flight_summary.model_dump_json() + return json.loads(flight_summary_json) + + +def test_create_flight(stub_flight): + with patch.object( + FlightController, + 'create_flight', + return_value=FlightCreated(flight_id='123'), + ) as mock_create_flight: + with patch.object( + Motor, 'set_motor_kind', side_effect=None + ) as mock_set_motor_kind: + response = client.post( + '/flights/', json=stub_flight, params={'motor_kind': 'HYBRID'} + ) + assert response.status_code == 200 + assert response.json() == { + 'flight_id': '123', + 'message': 'Flight successfully created', + } + mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) + mock_create_flight.assert_called_once_with(Flight(**stub_flight)) + + +def test_create_flight_optional_params(stub_flight): + stub_flight.update( + { + 'inclination': 0, + 'heading': 0, + 'max_time': 1, + 'max_time_step': 1.0, + 'min_time_step': 1, + 'rtol': 1.0, + 'atol': 1.0, + 'verbose': True, + } + ) + with patch.object( + FlightController, + 'create_flight', + return_value=FlightCreated(flight_id='123'), + ) as mock_create_flight: + with patch.object( + Motor, 'set_motor_kind', side_effect=None + ) as mock_set_motor_kind: + response = client.post( + '/flights/', json=stub_flight, params={'motor_kind': 'HYBRID'} + ) + assert response.status_code == 200 + assert response.json() == { + 'flight_id': '123', + 'message': 'Flight successfully created', + } + mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID) + mock_create_flight.assert_called_once_with(Flight(**stub_flight)) + + +def test_create_flight_invalid_input(): + response = client.post( + '/flights/', json={'environment': 'foo', 'rocket': 'bar'} + ) + assert response.status_code == 422 + + +def test_create_flight_server_error(stub_flight): + with patch.object( + FlightController, + 'create_flight', + side_effect=HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR + ), + ): + response = client.post( + '/flights/', json=stub_flight, params={'motor_kind': 'HYBRID'} + ) + assert response.status_code == 500 + assert response.json() == {'detail': 'Internal Server Error'} + + +def test_read_flight(stub_flight, stub_rocket, stub_motor): + del stub_rocket['motor'] + del stub_flight['rocket'] + motor_view = MotorView(**stub_motor, selected_motor_kind=MotorKinds.HYBRID) + rocket_view = RocketView(**stub_rocket, motor=motor_view) + flight_view = FlightView(**stub_flight, rocket=rocket_view) + with patch.object( + FlightController, + 'get_flight_by_id', + return_value=flight_view, + ) as mock_read_flight: + response = client.get('/flights/123') + assert response.status_code == 200 + assert response.json() == json.loads(flight_view.model_dump_json()) + mock_read_flight.assert_called_once_with('123') + + +def test_read_flight_not_found(): + with patch.object( + FlightController, + 'get_flight_by_id', + side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND), + ) as mock_read_flight: + response = client.get('/flights/123') + assert response.status_code == 404 + assert response.json() == {'detail': 'Not Found'} + mock_read_flight.assert_called_once_with('123') + + +def test_read_flight_server_error(): + with patch.object( + FlightController, + 'get_flight_by_id', + side_effect=HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR + ), + ): + response = client.get('/flights/123') + assert response.status_code == 500 + assert response.json() == {'detail': 'Internal Server Error'} + + +def test_update_flight(stub_flight): + with patch.object( + FlightController, + 'update_flight_by_id', + return_value=FlightUpdated(flight_id='123'), + ) as mock_update_flight: + with patch.object( + Motor, 'set_motor_kind', side_effect=None + ) as mock_set_motor_kind: + response = client.put( + '/flights/123', + json=stub_flight, + params={'motor_kind': 'GENERIC'}, + ) + assert response.status_code == 200 + assert response.json() == { + 'flight_id': '123', + 'message': 'Flight successfully updated', + } + mock_update_flight.assert_called_once_with( + '123', Flight(**stub_flight) + ) + mock_set_motor_kind.assert_called_once_with(MotorKinds.GENERIC) + + +def test_update_env_by_flight_id(stub_env): + with patch.object( + FlightController, + 'update_env_by_flight_id', + return_value=FlightUpdated(flight_id='123'), + ) as mock_update_flight: + response = client.put('/flights/123/env', json=stub_env) + assert response.status_code == 200 + assert response.json() == { + 'flight_id': '123', + 'message': 'Flight successfully updated', + } + mock_update_flight.assert_called_once_with('123', env=Env(**stub_env)) + + +def test_update_rocket_by_flight_id(stub_rocket): + with patch.object( + FlightController, + 'update_rocket_by_flight_id', + return_value=FlightUpdated(flight_id='123'), + ) as mock_update_flight: + response = client.put( + '/flights/123/rocket', + json=stub_rocket, + params={'motor_kind': 'GENERIC'}, + ) + assert response.status_code == 200 + assert response.json() == { + 'flight_id': '123', + 'message': 'Flight successfully updated', + } + mock_update_flight.assert_called_once_with( + '123', rocket=Rocket(**stub_rocket) + ) + + +def test_update_env_by_flight_id_invalid_input(): + response = client.put('/flights/123', json={'environment': 'foo'}) + assert response.status_code == 422 + + +def test_update_rocket_by_flight_id_invalid_input(): + response = client.put('/flights/123', json={'rocket': 'bar'}) + assert response.status_code == 422 + + +def test_update_flight_invalid_input(): + response = client.put( + '/flights/123', + json={'environment': 'foo', 'rocket': 'bar'}, + params={'motor_kind': 'GENERIC'}, + ) + assert response.status_code == 422 + + +def test_update_flight_not_found(stub_flight): + with patch.object( + FlightController, + 'update_flight_by_id', + side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND), + ): + response = client.put( + '/flights/123', json=stub_flight, params={'motor_kind': 'HYBRID'} + ) + assert response.status_code == 404 + assert response.json() == {'detail': 'Not Found'} + + +def test_update_flight_server_error(stub_flight): + with patch.object( + FlightController, + 'update_flight_by_id', + side_effect=HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR + ), + ): + response = client.put( + '/flights/123', json=stub_flight, params={'motor_kind': 'HYBRID'} + ) + assert response.status_code == 500 + assert response.json() == {'detail': 'Internal Server Error'} + + +def test_delete_flight(): + with patch.object( + FlightController, + 'delete_flight_by_id', + return_value=FlightDeleted(flight_id='123'), + ) as mock_delete_flight: + response = client.delete('/flights/123') + assert response.status_code == 200 + assert response.json() == { + 'flight_id': '123', + 'message': 'Flight successfully deleted', + } + mock_delete_flight.assert_called_once_with('123') + + +def test_delete_flight_server_error(): + with patch.object( + FlightController, + 'delete_flight_by_id', + side_effect=HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR + ), + ): + response = client.delete('/flights/123') + assert response.status_code == 500 + assert response.json() == {'detail': 'Internal Server Error'} + + +def test_simulate_flight(stub_flight_summary): + with patch.object( + FlightController, + 'simulate_flight', + return_value=FlightSummary(**stub_flight_summary), + ) as mock_simulate_flight: + response = client.get('/flights/123/summary') + assert response.status_code == 200 + assert response.json() == stub_flight_summary + mock_simulate_flight.assert_called_once_with('123') + + +def test_simulate_flight_not_found(): + with patch.object( + FlightController, + 'simulate_flight', + side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND), + ) as mock_simulate_flight: + response = client.get('/flights/123/summary') + assert response.status_code == 404 + assert response.json() == {'detail': 'Not Found'} + mock_simulate_flight.assert_called_once_with('123') + + +def test_simulate_flight_server_error(): + with patch.object( + FlightController, + 'simulate_flight', + side_effect=HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR + ), + ): + response = client.get('/flights/123/summary') + assert response.status_code == 500 + assert response.json() == {'detail': 'Internal Server Error'} + + +def test_read_rocketpy_flight(): + with patch.object( + FlightController, + 'get_rocketpy_flight_binary', + return_value=b'rocketpy', + ) as mock_read_rocketpy_flight: + response = client.get('/flights/123/rocketpy') + assert response.status_code == 203 + assert response.content == b'rocketpy' + assert response.headers['content-type'] == 'application/octet-stream' + mock_read_rocketpy_flight.assert_called_once_with('123') + + +def test_read_rocketpy_flight_not_found(): + with patch.object( + FlightController, + 'get_rocketpy_flight_binary', + side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND), + ) as mock_read_rocketpy_flight: + response = client.get('/flights/123/rocketpy') + assert response.status_code == 404 + assert response.json() == {'detail': 'Not Found'} + mock_read_rocketpy_flight.assert_called_once_with('123') + + +def test_read_rocketpy_flight_server_error(): + with patch.object( + FlightController, + 'get_rocketpy_flight_binary', + side_effect=HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR + ), + ): + response = client.get('/flights/123/rocketpy') + assert response.status_code == 500 + assert response.json() == {'detail': 'Internal Server Error'} diff --git a/tests/test_routes/test_motors_route.py b/tests/test_routes/test_motors_route.py index b7cab89..9f32a7b 100644 --- a/tests/test_routes/test_motors_route.py +++ b/tests/test_routes/test_motors_route.py @@ -6,9 +6,6 @@ from lib.models.motor import ( Motor, MotorKinds, - MotorTank, - TankFluids, - TankKinds, ) from lib.controllers.motor import MotorController from lib.views.motor import ( @@ -23,71 +20,6 @@ client = TestClient(app) -@pytest.fixture -def stub_motor(): - motor = Motor( - thrust_source=[[0, 0]], - burn_time=0, - nozzle_radius=0, - dry_mass=0, - dry_inertia=[0, 0, 0], - center_of_dry_mass_position=0, - ) - motor_json = motor.model_dump_json() - return json.loads(motor_json) - - -@pytest.fixture -def stub_tank(): - tank = MotorTank( - geometry=[[(0, 0), 0]], - gas=TankFluids(name='gas', density=0), - liquid=TankFluids(name='liquid', density=0), - flux_time=(0, 0), - position=0, - discretize=0, - name='tank', - ) - tank_json = tank.model_dump_json() - return json.loads(tank_json) - - -@pytest.fixture -def stub_level_tank(stub_tank): - stub_tank.update({'tank_kind': TankKinds.LEVEL, 'liquid_height': 0}) - return stub_tank - - -@pytest.fixture -def stub_mass_flow_tank(stub_tank): - stub_tank.update( - { - 'tank_kind': TankKinds.MASS_FLOW, - 'gas_mass_flow_rate_in': 0, - 'gas_mass_flow_rate_out': 0, - 'liquid_mass_flow_rate_in': 0, - 'liquid_mass_flow_rate_out': 0, - 'initial_liquid_mass': 0, - 'initial_gas_mass': 0, - } - ) - return stub_tank - - -@pytest.fixture -def stub_ullage_tank(stub_tank): - stub_tank.update({'tank_kind': TankKinds.ULLAGE, 'ullage': 0}) - return stub_tank - - -@pytest.fixture -def stub_mass_tank(stub_tank): - stub_tank.update( - {'tank_kind': TankKinds.MASS, 'liquid_mass': 0, 'gas_mass': 0} - ) - return stub_tank - - @pytest.fixture def stub_motor_summary(): motor_summary = MotorSummary() diff --git a/tests/test_routes/test_rockets_route.py b/tests/test_routes/test_rockets_route.py index 8583e4b..852a012 100644 --- a/tests/test_routes/test_rockets_route.py +++ b/tests/test_routes/test_rockets_route.py @@ -14,9 +14,6 @@ from lib.models.motor import ( Motor, MotorKinds, - MotorTank, - TankFluids, - TankKinds, ) from lib.controllers.rocket import RocketController from lib.views.motor import MotorView @@ -32,71 +29,6 @@ client = TestClient(app) -@pytest.fixture -def stub_motor(): - motor = Motor( - thrust_source=[[0, 0]], - burn_time=0, - nozzle_radius=0, - dry_mass=0, - dry_inertia=[0, 0, 0], - center_of_dry_mass_position=0, - ) - motor_json = motor.model_dump_json() - return json.loads(motor_json) - - -@pytest.fixture -def stub_tank(): - tank = MotorTank( - geometry=[[(0, 0), 0]], - gas=TankFluids(name='gas', density=0), - liquid=TankFluids(name='liquid', density=0), - flux_time=(0, 0), - position=0, - discretize=0, - name='tank', - ) - tank_json = tank.model_dump_json() - return json.loads(tank_json) - - -@pytest.fixture -def stub_level_tank(stub_tank): - stub_tank.update({'tank_kind': TankKinds.LEVEL, 'liquid_height': 0}) - return stub_tank - - -@pytest.fixture -def stub_mass_flow_tank(stub_tank): - stub_tank.update( - { - 'tank_kind': TankKinds.MASS_FLOW, - 'gas_mass_flow_rate_in': 0, - 'gas_mass_flow_rate_out': 0, - 'liquid_mass_flow_rate_in': 0, - 'liquid_mass_flow_rate_out': 0, - 'initial_liquid_mass': 0, - 'initial_gas_mass': 0, - } - ) - return stub_tank - - -@pytest.fixture -def stub_ullage_tank(stub_tank): - stub_tank.update({'tank_kind': TankKinds.ULLAGE, 'ullage': 0}) - return stub_tank - - -@pytest.fixture -def stub_mass_tank(stub_tank): - stub_tank.update( - {'tank_kind': TankKinds.MASS, 'liquid_mass': 0, 'gas_mass': 0} - ) - return stub_tank - - @pytest.fixture def stub_rocket_summary(): rocket_summary = RocketSummary() @@ -171,23 +103,6 @@ def stub_parachute(): return json.loads(parachute_json) -@pytest.fixture -def stub_rocket(stub_motor): - rocket = Rocket( - motor=stub_motor, - radius=0, - mass=0, - motor_position=0, - center_of_mass_without_motor=0, - inertia=[0, 0, 0], - power_off_drag=[(0, 0)], - power_on_drag=[(0, 0)], - coordinate_system_orientation='TAIL_TO_NOSE', - ) - rocket_json = rocket.model_dump_json() - return json.loads(rocket_json) - - def test_create_rocket(stub_rocket): with patch.object( RocketController, @@ -466,9 +381,9 @@ def test_create_rocket_server_error(stub_rocket): def test_read_rocket(stub_rocket, stub_motor): + del stub_rocket['motor'] motor_view = MotorView(**stub_motor, selected_motor_kind=MotorKinds.HYBRID) - stub_rocket.update(motor=motor_view) - rocket_view = RocketView(**stub_rocket) + rocket_view = RocketView(**stub_rocket, motor=motor_view) with patch.object( RocketController, 'get_rocket_by_id',