Coverage for app/services/move_service.py: 100%
37 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-16 20:06 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-16 20:06 +0000
1from app.core.events import ProbeEvents
2from app.core.logging import Logger
3from app.core.observability import Observability
4from app.domain.probe.entities.probe import Probe as DomainProbe
5from app.domain.probe.entities.grid import Grid
6from app.domain.probe.exceptions import InvalidCommandError, InvalidMovementError
7from app.domain.services.CommandRunner import CommandRunner
8from app.models.Probe import Probe as ModelProbe
9from app.repositories.probe_repository import ProbeRepository
10from app.schemas.move import MoveResponse, MoveRequest
11from fastapi import HTTPException
14class MoveService:
15 def __init__(
16 self,
17 repository: ProbeRepository,
18 ) -> None:
19 self.repository = repository
21 async def process(
22 self,
23 move: MoveRequest,
24 ) -> MoveResponse:
25 probe = await self.repository.find_by_id(move.id)
27 if probe is None:
28 raise HTTPException(
29 status_code=404,
30 detail={
31 "code": "PROBE_NOT_FOUND",
32 "message": f"Probe {move.id} not found.",
33 },
34 )
36 grid = probe.grid
37 from_x = probe.x
38 from_y = probe.y
39 from_direction = probe.direction
41 try:
42 command_runner = CommandRunner(grid=Grid(x_size=grid.x, y_size=grid.y))
43 new_probe = command_runner.run(
44 probe=DomainProbe(x=probe.x, y=probe.y, direction=probe.direction),
45 commands=move.command,
46 )
48 persisted_probe = await self.repository.save(
49 probe=ModelProbe(
50 id=probe.id,
51 x=new_probe.x,
52 y=new_probe.y,
53 direction=new_probe.direction,
54 )
55 )
57 Observability.emit(
58 ProbeEvents.PROBE_COMMAND_SENT,
59 probe_id=str(probe.id),
60 command=move.command,
61 from_x=from_x,
62 from_y=from_y,
63 from_direction=from_direction,
64 to_x=persisted_probe.x,
65 to_y=persisted_probe.y,
66 to_direction=persisted_probe.direction,
67 )
69 return MoveResponse(
70 id=persisted_probe.id,
71 x=persisted_probe.x,
72 y=persisted_probe.y,
73 direction=persisted_probe.direction,
74 )
75 except InvalidCommandError as e:
76 Observability.emit(
77 ProbeEvents.PROBE_INVALID_COMMAND,
78 probe_id=str(probe.id),
79 command=move.command,
80 )
81 raise HTTPException(
82 status_code=400,
83 detail={
84 "code": "INVALID_COMMAND_ERROR",
85 "message": f"{e} For security, no commands were delivered to the probe.",
86 },
87 )
88 except InvalidMovementError as e:
89 Observability.emit(
90 ProbeEvents.PROBE_INVALID_COMMAND,
91 probe_id=str(probe.id),
92 command=move.command,
93 from_x=probe.x,
94 from_y=probe.y,
95 from_direction=probe.direction,
96 grid_x=grid.x,
97 grid_y=grid.y,
98 )
100 raise HTTPException(
101 status_code=422,
102 detail={
103 "code": "INVALID_MOVEMENT_ERROR",
104 "message": f"{e} For security, no commands were delivered to the probe.",
105 },
106 )
107 except Exception:
108 Logger.log(
109 "move_unexpected_error",
110 probe_id=str(probe.id),
111 command=move.command,
112 )
113 raise HTTPException(
114 status_code=500,
115 detail={
116 "code": "MOVE_UNEXPECTED_ERROR",
117 "message": "Unexpected error. Try again. For security, no commands were delivered to the probe.",
118 },
119 )