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

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 

12 

13 

14class MoveService: 

15 def __init__( 

16 self, 

17 repository: ProbeRepository, 

18 ) -> None: 

19 self.repository = repository 

20 

21 async def process( 

22 self, 

23 move: MoveRequest, 

24 ) -> MoveResponse: 

25 probe = await self.repository.find_by_id(move.id) 

26 

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 ) 

35 

36 grid = probe.grid 

37 from_x = probe.x 

38 from_y = probe.y 

39 from_direction = probe.direction 

40 

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 ) 

47 

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 ) 

56 

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 ) 

68 

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 ) 

99 

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 )