From 01d55c23aac116abc3501c2c0b77262f88fcc4ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stig=20B=2E=20D=C3=B8rm=C3=A6nen?= Date: Sun, 11 Dec 2022 19:02:27 +0100 Subject: [PATCH] =?UTF-8?q?Validering=20av=20kj=C3=B8replan=20Fixes=20#118?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validerer: - at race ikkje har 0 contestants - at races er i kronlogisk rekkefølge - at antall contestants i race stemmer med raceplan - at antall contestants stemmer med ageclasses --- noxfile.py | 45 +++ race_service/app.py | 7 +- race_service/commands/raceplans_commands.py | 91 ++++- race_service/services/__init__.py | 1 + race_service/views/__init__.py | 2 +- race_service/views/raceplans_commands.py | 37 +- tests/contract/test_raceplans.py | 23 ++ tests/integration/test_raceplans.py | 414 ++++++++++++++++++++ 8 files changed, 615 insertions(+), 5 deletions(-) diff --git a/noxfile.py b/noxfile.py index e7be7cd..20df9a5 100644 --- a/noxfile.py +++ b/noxfile.py @@ -20,6 +20,51 @@ ) +@session(python=["3.10"]) +def clean(session: Session) -> None: + """Clean the project.""" + session.run( + "py3clean", + ".", + external=True, + ) + session.run( + "rm", + "-rf", + ".cache", + external=True, + ) + session.run( + "rm", + "-rf", + ".pytest_cache", + external=True, + ) + session.run( + "rm", + "-rf", + ".pytype", + external=True, + ) + session.run( + "rm", + "-rf", + "dist", + external=True, + ) + session.run( + "rm", + "-rf", + ".mypy_cache", + external=True, + ) + session.run( + "rm", + ".coverage", + external=True, + ) + + @session(python="3.10") def unit_tests(session: Session) -> None: """Run the unit test suite.""" diff --git a/race_service/app.py b/race_service/app.py index c48049f..b5e160c 100644 --- a/race_service/app.py +++ b/race_service/app.py @@ -4,7 +4,8 @@ from typing import Any from aiohttp import web -from aiohttp_middlewares import cors_middleware, error_middleware +from aiohttp_middlewares.cors import cors_middleware +from aiohttp_middlewares.error import error_middleware import motor.motor_asyncio from .views import ( @@ -24,6 +25,7 @@ StartlistView, TimeEventsView, TimeEventView, + ValidateRaceplanView, ) @@ -53,6 +55,7 @@ async def create_app() -> web.Application: "/raceplans/generate-raceplan-for-event", GenerateRaceplanForEventView ), web.view("/raceplans/{raceplanId}", RaceplanView), + web.view("/raceplans/{raceplanId}/validate", ValidateRaceplanView), web.view("/races", RacesView), web.view("/races/{raceId}", RaceView), web.view("/races/{raceId}/race-results", RaceResultsView), @@ -91,6 +94,6 @@ async def mongo_context(app: Any) -> Any: mongo.close() - app.cleanup_ctx.append(mongo_context) + app.cleanup_ctx.append(mongo_context) # type: ignore return app diff --git a/race_service/commands/raceplans_commands.py b/race_service/commands/raceplans_commands.py index e688b61..0f6b59c 100644 --- a/race_service/commands/raceplans_commands.py +++ b/race_service/commands/raceplans_commands.py @@ -7,7 +7,9 @@ EventNotFoundException, EventsAdapter, RaceplansAdapter, + RacesAdapter, ) +from race_service.models import Raceplan from race_service.services import ( RaceplanAllreadyExistException, RaceplansService, @@ -87,6 +89,93 @@ async def generate_raceplan_for_event( # noqa: C901 "Something went wrong when creating raceplan." ) from None + @classmethod + async def validate_raceplan( # noqa: C901 + cls: Any, db: Any, token: str, raceplan: Raceplan + ) -> Dict[int, List[str]]: + """Validate a given raceplan and return validation results.""" + # First we get the event from the event-service: + try: + event = await get_event(token, raceplan.event_id) + except EventNotFoundException as e: # pragma: no cover + raise e from e + # We fetch the competition-format: + try: + competition_format = await get_competition_format( + token, raceplan.event_id, event["competition_format"] + ) + except CompetitionFormatNotFoundException as e: # pragma: no cover + raise CompetitionFormatNotSupportedException( + f'Competition-format {event["competition_format"]} is not supported.' + ) from e + # Then we fetch the raceclasses: + raceclasses = await get_raceclasses(token, raceplan.event_id) + + results: Dict[int, List[str]] = {} + + races: List[Dict] = [] + for race_id in raceplan.races: + race = await RacesAdapter.get_race_by_id(db, race_id) + races.append(race) + + races.sort(key=lambda x: x["order"]) + + # Check if races are in chronological order: + for i in range(0, len(races) - 1): + if races[i]["start_time"] >= races[i + 1]["start_time"]: + results[races[i + 1]["order"]] = [ + "Start time is not in chronological order." + ] + + # Check each race and sum up the number of contestants: + sum_no_of_contestants = 0 + for race in races: + # Check if race has contestants: + if race["no_of_contestants"] == 0: + if race["order"] in results: + results[race["order"]].append("Race has no contestants.") + else: + results[race["order"]] = [("Race has no contestants.")] + + # Sum up the number of contestants in first rounds: + if race["round"] in [ + competition_format["rounds_ranked_classes"][0], + competition_format["rounds_non_ranked_classes"][0], + ]: + sum_no_of_contestants += race["no_of_contestants"] + + # Check if the sum of contestants in races is equal to the number of contestants in the raceplan: + if sum_no_of_contestants != raceplan.no_of_contestants: + results[0] = [ + f"The sum of contestants in races ({sum_no_of_contestants})" + f" is not equal to the number of contestants in the raceplan ({raceplan.no_of_contestants})." + ] + + # Check if the number of contestants in the plan is equal to + # the number of contestants in the raceclasses: + no_of_contestants_in_raceclasses = sum( + raceclass["no_of_contestants"] for raceclass in raceclasses + ) + if raceplan.no_of_contestants != no_of_contestants_in_raceclasses: + if 0 in results: + results[0].append( + ( + f"Number of contestants in raceplan ({raceplan.no_of_contestants})" + " is not equal to the number of contestants" + f" in the raceclasses ({no_of_contestants_in_raceclasses})." + ) + ) + else: # pragma: no cover + results[0] = [ + ( + f"Number of contestants in raceplan ({raceplan.no_of_contestants})" + f" is not equal to the number of contestants" + f"in the raceclasses ({no_of_contestants_in_raceclasses})." + ) + ] + + return results + # helpers async def get_raceplan(db: Any, token: str, event_id: str) -> None: @@ -249,6 +338,6 @@ async def get_raceclasses(token: str, event_id: str) -> List[dict]: # noqa: C90 for _raceclasses in raceclasses_grouped: if len(set([r["ranking"] for r in _raceclasses])) > 1: raise InconsistentValuesInRaceclassesException( - f'Ranking-value differs in group {raceclass["group"]}.' + f'Ranking-value differs in group {_raceclasses[0]["group"]}.' ) return raceclasses diff --git a/race_service/services/__init__.py b/race_service/services/__init__.py index 97fca42..fc3dbb4 100644 --- a/race_service/services/__init__.py +++ b/race_service/services/__init__.py @@ -11,6 +11,7 @@ RaceplanAllreadyExistException, RaceplanNotFoundException, RaceplansService, + validate_raceplan, ) from .races_service import ( RaceNotFoundException, diff --git a/race_service/views/__init__.py b/race_service/views/__init__.py index 9c50114..075486d 100644 --- a/race_service/views/__init__.py +++ b/race_service/views/__init__.py @@ -2,7 +2,7 @@ from .liveness import Ping, Ready from .race_results import RaceResultsView, RaceResultView from .raceplans import RaceplansView, RaceplanView -from .raceplans_commands import GenerateRaceplanForEventView +from .raceplans_commands import GenerateRaceplanForEventView, ValidateRaceplanView from .races import RacesView, RaceView from .start_entries import StartEntriesView, StartEntryView from .startlists import StartlistsView, StartlistView diff --git a/race_service/views/raceplans_commands.py b/race_service/views/raceplans_commands.py index 95c9f2a..1cad0ef 100644 --- a/race_service/views/raceplans_commands.py +++ b/race_service/views/raceplans_commands.py @@ -1,4 +1,5 @@ """Resource module for raceplan command resources.""" +import json import os from aiohttp import hdrs @@ -26,7 +27,11 @@ NoRaceclassesInEventException, RaceplansCommands, ) -from race_service.services import RaceplanAllreadyExistException +from race_service.services import ( + RaceplanAllreadyExistException, + RaceplanNotFoundException, + RaceplansService, +) from .utils import extract_token_from_request load_dotenv() @@ -73,3 +78,33 @@ async def post(self) -> Response: raise HTTPBadRequest(reason=str(e)) from e headers = MultiDict([(hdrs.LOCATION, f"{BASE_URL}/raceplans/{raceplan_id}")]) return Response(status=201, headers=headers) + + +class ValidateRaceplanView(View): + """Class representing the validation of a given raceplan.""" + + async def post(self) -> Response: + """Post route function.""" + # Authorize: + db = self.request.app["db"] + token = extract_token_from_request(self.request) + assert token # noqa: S101 + try: + await UsersAdapter.authorize(token, roles=["admin", "event-admin"]) + except Exception as e: # pragma: no cover + raise e + + raceplan_id = self.request.match_info["raceplanId"] + + # Fetch the raceplan: + try: + raceplan = await RaceplansService.get_raceplan_by_id(db, raceplan_id) + except RaceplanNotFoundException as e: # pragma: no cover + raise HTTPNotFound(reason=str(e)) from e + + # Validate + result = await RaceplansCommands.validate_raceplan(db, token, raceplan) + headers = MultiDict([(hdrs.CONTENT_TYPE, "application/json")]) + body = json.dumps(result, default=str, ensure_ascii=False) + + return Response(status=200, headers=headers, body=body) diff --git a/tests/contract/test_raceplans.py b/tests/contract/test_raceplans.py index 1fd44d1..87ee9b7 100644 --- a/tests/contract/test_raceplans.py +++ b/tests/contract/test_raceplans.py @@ -395,6 +395,29 @@ async def test_get_raceplan( i += 1 +@pytest.mark.contract +@pytest.mark.asyncio +async def test_validate_raceplan( + http_service: Any, token: MockFixture, context: dict +) -> None: + """Should return 200 OK and and a body with a list of results.""" + raceplan_id = context["id"] + url = f"{http_service}/raceplans/{raceplan_id}/validate" + headers = { + hdrs.AUTHORIZATION: f"Bearer {token}", + } + session = ClientSession() + async with session.post(url, headers=headers) as response: + status = response.status + results = await response.json() + await session.close() + + assert status == 200 + assert "application/json" in response.headers[hdrs.CONTENT_TYPE] + assert type(results) is dict + assert len(results) == 0 + + @pytest.mark.contract @pytest.mark.asyncio async def test_update_race( diff --git a/tests/integration/test_raceplans.py b/tests/integration/test_raceplans.py index 80a4591..21736dd 100644 --- a/tests/integration/test_raceplans.py +++ b/tests/integration/test_raceplans.py @@ -3,6 +3,7 @@ from datetime import datetime from json import dumps import os +from typing import Any, Dict, List from aiohttp import hdrs from aiohttp.test_utils import TestClient as _TestClient @@ -30,6 +31,89 @@ def token_unsufficient_role() -> str: return jwt.encode(payload, secret, algorithm) # type: ignore +@pytest.fixture +async def event() -> Dict[str, Any]: + """An event object for testing.""" + return { + "id": "290e70d5-0933-4af0-bb53-1d705ba7eb95", + "name": "Oslo Skagen sprint", + "competition_format": "Interval Start", + "date_of_event": "2021-08-31", + "time_of_event": "09:00:00", + "organiser": "Lyn Ski", + "webpage": "https://example.com", + "information": "Testarr for å teste den nye løysinga.", + } + + +@pytest.fixture +async def competition_format() -> Dict[str, Any]: + """A competition-format for testing.""" + return { + "id": "290e70d5-0933-4af0-bb53-1d705ba7eb95", + "name": "Individual Sprint", + "starting_order": "Draw", + "start_procedure": "Heat Start", + "time_between_groups": "00:15:00", + "time_between_rounds": "00:10:00", + "time_between_heats": "00:02:30", + "rounds_ranked_classes": ["Q", "S", "F"], + "rounds_non_ranked_classes": ["R1", "R2"], + "max_no_of_contestants_in_raceclass": 80, + "max_no_of_contestants_in_race": 10, + "datatype": "individual_sprint", + "race_config_non_ranked": None, + "race_config_ranked": None, + } + + +@pytest.fixture +async def raceclasses() -> List[Dict[str, Any]]: + """An raceclasses object for testing.""" + return [ + { + "id": "190e70d5-0933-4af0-bb53-1d705ba7eb95", + "name": "G15", + "ageclasses": ["G 15 år"], + "event_id": "290e70d5-0933-4af0-bb53-1d705ba7eb95", + "no_of_contestants": 8, + "ranking": True, + "group": 1, + "order": 2, + }, + { + "id": "290e70d5-0933-4af0-bb53-1d705ba7eb95", + "name": "G16", + "ageclasses": ["G 16 år"], + "event_id": "290e70d5-0933-4af0-bb53-1d705ba7eb95", + "no_of_contestants": 8, + "ranking": True, + "group": 1, + "order": 4, + }, + { + "id": "390e70d5-0933-4af0-bb53-1d705ba7eb95", + "name": "J15", + "ageclasses": ["J 15 år"], + "event_id": "290e70d5-0933-4af0-bb53-1d705ba7eb95", + "no_of_contestants": 8, + "ranking": True, + "group": 1, + "order": 1, + }, + { + "id": "490e70d5-0933-4af0-bb53-1d705ba7eb95", + "name": "J16", + "ageclasses": ["J 16 år"], + "event_id": "290e70d5-0933-4af0-bb53-1d705ba7eb95", + "no_of_contestants": 8, + "ranking": True, + "group": 1, + "order": 3, + }, + ] + + @pytest.fixture async def new_raceplan_interval_start() -> dict: """Create a raceplan object.""" @@ -231,6 +315,9 @@ async def raceplan_individual_sprint() -> dict: "event_id": "event_1", "raceplan_id": "290e70d5-0933-4af0-bb53-1d705ba7eb95", "start_entries": [], + "round": "Q", + "index": "A", + "heat": 1, }, { "raceclass": "G16", @@ -243,6 +330,9 @@ async def raceplan_individual_sprint() -> dict: "event_id": "event_1", "raceplan_id": "290e70d5-0933-4af0-bb53-1d705ba7eb95", "start_entries": [], + "round": "Q", + "index": "A", + "heat": 1, }, { "raceclass": "G16", @@ -255,6 +345,9 @@ async def raceplan_individual_sprint() -> dict: "event_id": "event_1", "raceplan_id": "290e70d5-0933-4af0-bb53-1d705ba7eb95", "start_entries": [], + "round": "Q", + "index": "A", + "heat": 1, }, { "raceclass": "G16", @@ -267,6 +360,9 @@ async def raceplan_individual_sprint() -> dict: "event_id": "event_1", "raceplan_id": "290e70d5-0933-4af0-bb53-1d705ba7eb95", "start_entries": [], + "round": "Q", + "index": "A", + "heat": 1, }, ], } @@ -382,6 +478,324 @@ async def test_get_raceplan_by_id( assert race["start_time"] +@pytest.mark.integration +async def test_validate_raceplan( + client: _TestClient, + mocker: MockFixture, + token: MockFixture, + event: dict, + competition_format: dict, + raceclasses: dict, + raceplan_individual_sprint: dict, +) -> None: + """Should return OK, and an empty list.""" + RACEPLAN_ID = raceplan_individual_sprint["id"] + mocker.patch( + "race_service.adapters.raceplans_adapter.RaceplansAdapter.get_raceplan_by_id", + return_value=raceplan_individual_sprint, + ) + mocker.patch( + "race_service.adapters.raceplans_adapter.RaceplansAdapter.get_raceplan_by_event_id", + return_value=None, + ) + mocker.patch( + "race_service.adapters.races_adapter.RacesAdapter.get_race_by_id", + side_effect=raceplan_individual_sprint["races"], + ) + mocker.patch( + "race_service.adapters.events_adapter.EventsAdapter.get_raceclasses", + return_value=raceclasses, + ) + mocker.patch( + "race_service.adapters.events_adapter.EventsAdapter.get_event_by_id", + return_value=event, + ) + mocker.patch( + "race_service.adapters.events_adapter.EventsAdapter.get_competition_format", + return_value=competition_format, + ) + + headers = { + hdrs.CONTENT_TYPE: "application/json", + hdrs.AUTHORIZATION: f"Bearer {token}", + } + + with aioresponses(passthrough=["http://127.0.0.1"]) as m: + m.post("http://users.example.com:8080/authorize", status=204) + + resp = await client.post(f"/raceplans/{RACEPLAN_ID}/validate", headers=headers) + assert resp.status == 200 + assert "application/json" in resp.headers[hdrs.CONTENT_TYPE] + body = await resp.json() + assert type(body) is dict + assert len(body) == 0 + + +@pytest.mark.integration +async def test_validate_raceplan_race_has_no_contestants( + client: _TestClient, + mocker: MockFixture, + token: MockFixture, + event: dict, + competition_format: dict, + raceclasses: dict, + raceplan_individual_sprint: dict, +) -> None: + """Should return OK, and an empty list.""" + RACEPLAN_ID = raceplan_individual_sprint["id"] + raceplan_individual_sprint_with_no_contestants = deepcopy( + raceplan_individual_sprint + ) + raceplan_individual_sprint_with_no_contestants["races"][0]["no_of_contestants"] = 0 + + mocker.patch( + "race_service.adapters.raceplans_adapter.RaceplansAdapter.get_raceplan_by_id", + return_value=raceplan_individual_sprint, + ) + mocker.patch( + "race_service.adapters.raceplans_adapter.RaceplansAdapter.get_raceplan_by_event_id", + return_value=None, + ) + mocker.patch( + "race_service.adapters.races_adapter.RacesAdapter.get_race_by_id", + side_effect=raceplan_individual_sprint_with_no_contestants["races"], + ) + mocker.patch( + "race_service.adapters.events_adapter.EventsAdapter.get_raceclasses", + return_value=raceclasses, + ) + mocker.patch( + "race_service.adapters.events_adapter.EventsAdapter.get_event_by_id", + return_value=event, + ) + mocker.patch( + "race_service.adapters.events_adapter.EventsAdapter.get_competition_format", + return_value=competition_format, + ) + + headers = { + hdrs.CONTENT_TYPE: "application/json", + hdrs.AUTHORIZATION: f"Bearer {token}", + } + + with aioresponses(passthrough=["http://127.0.0.1"]) as m: + m.post("http://users.example.com:8080/authorize", status=204) + + resp = await client.post(f"/raceplans/{RACEPLAN_ID}/validate", headers=headers) + assert resp.status == 200 + assert "application/json" in resp.headers[hdrs.CONTENT_TYPE] + body = await resp.json() + assert type(body) is dict + assert len(body) == 2 + assert "0" in body # error on raceplan-result (key "0") + assert len(body["0"]) == 1 + assert body["0"] == [ + "The sum of contestants in races (24) is not equal to the number of contestants in the raceplan (32)." # noqa: B950 + ] + assert "1" in body # it is race with order 1 that has no contestants + assert len(body["1"]) == 1 + assert body["1"] == ["Race has no contestants."] + + +@pytest.mark.integration +async def test_validate_raceplan_race_no_chronological_time( + client: _TestClient, + mocker: MockFixture, + token: MockFixture, + event: dict, + competition_format: dict, + raceclasses: dict, + raceplan_individual_sprint: dict, +) -> None: + """Should return OK, and an empty list.""" + RACEPLAN_ID = raceplan_individual_sprint["id"] + raceplan_individual_sprint_with_no_contestants = deepcopy( + raceplan_individual_sprint + ) + raceplan_individual_sprint_with_no_contestants["races"][-1][ + "start_time" + ] = raceplan_individual_sprint_with_no_contestants["races"][0]["start_time"] + + mocker.patch( + "race_service.adapters.raceplans_adapter.RaceplansAdapter.get_raceplan_by_id", + return_value=raceplan_individual_sprint, + ) + mocker.patch( + "race_service.adapters.raceplans_adapter.RaceplansAdapter.get_raceplan_by_event_id", + return_value=None, + ) + mocker.patch( + "race_service.adapters.races_adapter.RacesAdapter.get_race_by_id", + side_effect=raceplan_individual_sprint_with_no_contestants["races"], + ) + mocker.patch( + "race_service.adapters.events_adapter.EventsAdapter.get_raceclasses", + return_value=raceclasses, + ) + mocker.patch( + "race_service.adapters.events_adapter.EventsAdapter.get_event_by_id", + return_value=event, + ) + mocker.patch( + "race_service.adapters.events_adapter.EventsAdapter.get_competition_format", + return_value=competition_format, + ) + + headers = { + hdrs.CONTENT_TYPE: "application/json", + hdrs.AUTHORIZATION: f"Bearer {token}", + } + + with aioresponses(passthrough=["http://127.0.0.1"]) as m: + m.post("http://users.example.com:8080/authorize", status=204) + + resp = await client.post(f"/raceplans/{RACEPLAN_ID}/validate", headers=headers) + assert resp.status == 200 + assert "application/json" in resp.headers[hdrs.CONTENT_TYPE] + body = await resp.json() + assert type(body) is dict + assert len(body) == 1 + assert "4" in body # it is race with order 4 that has wrong time + assert len(body["4"]) == 1 + assert body["4"] == ["Start time is not in chronological order."] + + +@pytest.mark.integration +async def test_validate_raceplan_race_has_no_contestants_and_faulty_time( + client: _TestClient, + mocker: MockFixture, + token: MockFixture, + event: dict, + competition_format: dict, + raceclasses: dict, + raceplan_individual_sprint: dict, +) -> None: + """Should return OK, and an empty list.""" + RACEPLAN_ID = raceplan_individual_sprint["id"] + raceplan_individual_sprint_with_no_contestants = deepcopy( + raceplan_individual_sprint + ) + raceplan_individual_sprint_with_no_contestants["races"][-1]["no_of_contestants"] = 0 + raceplan_individual_sprint_with_no_contestants["races"][-1][ + "start_time" + ] = raceplan_individual_sprint_with_no_contestants["races"][0]["start_time"] + + mocker.patch( + "race_service.adapters.raceplans_adapter.RaceplansAdapter.get_raceplan_by_id", + return_value=raceplan_individual_sprint, + ) + mocker.patch( + "race_service.adapters.raceplans_adapter.RaceplansAdapter.get_raceplan_by_event_id", + return_value=None, + ) + mocker.patch( + "race_service.adapters.races_adapter.RacesAdapter.get_race_by_id", + side_effect=raceplan_individual_sprint_with_no_contestants["races"], + ) + mocker.patch( + "race_service.adapters.events_adapter.EventsAdapter.get_raceclasses", + return_value=raceclasses, + ) + mocker.patch( + "race_service.adapters.events_adapter.EventsAdapter.get_event_by_id", + return_value=event, + ) + mocker.patch( + "race_service.adapters.events_adapter.EventsAdapter.get_competition_format", + return_value=competition_format, + ) + + headers = { + hdrs.CONTENT_TYPE: "application/json", + hdrs.AUTHORIZATION: f"Bearer {token}", + } + + with aioresponses(passthrough=["http://127.0.0.1"]) as m: + m.post("http://users.example.com:8080/authorize", status=204) + + resp = await client.post(f"/raceplans/{RACEPLAN_ID}/validate", headers=headers) + assert resp.status == 200 + assert "application/json" in resp.headers[hdrs.CONTENT_TYPE] + body = await resp.json() + assert type(body) is dict + assert len(body) == 2 + assert "0" in body # error on raceplan-result (key "0") + assert len(body["0"]) == 1 + assert body["0"] == [ + "The sum of contestants in races (24) is not equal to the number of contestants in the raceplan (32)." # noqa: B950 + ] + assert "4" in body # it is race with order 4 that has no contestants + assert len(body["4"]) == 2 + assert body["4"] == [ + "Start time is not in chronological order.", + "Race has no contestants.", + ] + + +@pytest.mark.integration +async def test_validate_raceplan_contestants_not_equal_no_of_contestants_in_raceclasses( + client: _TestClient, + mocker: MockFixture, + token: MockFixture, + event: dict, + competition_format: dict, + raceclasses: dict, + raceplan_individual_sprint: dict, +) -> None: + """Should return OK, and an empty list.""" + RACEPLAN_ID = raceplan_individual_sprint["id"] + raceplan_individual_sprint_with_faulty_contestants = deepcopy( + raceplan_individual_sprint + ) + raceplan_individual_sprint_with_faulty_contestants["no_of_contestants"] = 30 + + mocker.patch( + "race_service.adapters.raceplans_adapter.RaceplansAdapter.get_raceplan_by_id", + return_value=raceplan_individual_sprint_with_faulty_contestants, + ) + mocker.patch( + "race_service.adapters.raceplans_adapter.RaceplansAdapter.get_raceplan_by_event_id", + return_value=None, + ) + mocker.patch( + "race_service.adapters.races_adapter.RacesAdapter.get_race_by_id", + side_effect=raceplan_individual_sprint_with_faulty_contestants["races"], + ) + mocker.patch( + "race_service.adapters.events_adapter.EventsAdapter.get_raceclasses", + return_value=raceclasses, + ) + mocker.patch( + "race_service.adapters.events_adapter.EventsAdapter.get_event_by_id", + return_value=event, + ) + mocker.patch( + "race_service.adapters.events_adapter.EventsAdapter.get_competition_format", + return_value=competition_format, + ) + + headers = { + hdrs.CONTENT_TYPE: "application/json", + hdrs.AUTHORIZATION: f"Bearer {token}", + } + + with aioresponses(passthrough=["http://127.0.0.1"]) as m: + m.post("http://users.example.com:8080/authorize", status=204) + + resp = await client.post(f"/raceplans/{RACEPLAN_ID}/validate", headers=headers) + assert resp.status == 200 + assert "application/json" in resp.headers[hdrs.CONTENT_TYPE] + body = await resp.json() + assert type(body) is dict + assert len(body) == 1 + assert "0" in body # error on raceplan-result (key "0") + assert len(body["0"]) == 2 + assert body["0"] == [ + "The sum of contestants in races (32) is not equal to the number of contestants in the raceplan (30).", # noqa: B950 + "Number of contestants in raceplan (30) is not equal to the number of contestants in the raceclasses (32).", # noqa: B950 + ] + + @pytest.mark.integration async def test_get_raceplan_by_event_id( client: _TestClient,