test_vehicles_api.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  1. """
  2. Tests for the Vehicles API endpoints.
  3. """
  4. from contextlib import contextmanager
  5. from unittest.mock import Mock
  6. from fastapi import status
  7. from web.schemas import (
  8. VehicleBase,
  9. VersionOut,
  10. BoardOut,
  11. FeatureOut,
  12. CategoryBase,
  13. FeatureDefault,
  14. RemoteInfo,
  15. )
  16. class TestVehiclesAPI:
  17. """
  18. Tests for all Vehicles API endpoints.
  19. """
  20. @staticmethod
  21. @contextmanager
  22. def override_vehicles_service(client, mock_service):
  23. """Temporarily override the get_vehicles_service dependency."""
  24. from web.services.vehicles import get_vehicles_service
  25. client.app.dependency_overrides[get_vehicles_service] = lambda: mock_service
  26. try:
  27. yield
  28. finally:
  29. client.app.dependency_overrides.pop(get_vehicles_service, None)
  30. @staticmethod
  31. def dummy_version():
  32. return VersionOut(
  33. id="copter-4.5.0-stable",
  34. name="stable 4.5.0 (ardupilot)",
  35. type="stable",
  36. remote=RemoteInfo(
  37. name="ardupilot",
  38. url="https://github.com/ArduPilot/ardupilot.git"
  39. ),
  40. commit_ref="refs/tags/Copter-4.5.0",
  41. vehicle_id="copter",
  42. )
  43. @staticmethod
  44. def dummy_board(
  45. vehicle_id="copter",
  46. version_id="copter-4.5.0-stable",
  47. board_id="MatekH743",
  48. ):
  49. return BoardOut(
  50. id=board_id,
  51. name=board_id,
  52. vehicle_id=vehicle_id,
  53. version_id=version_id,
  54. )
  55. @staticmethod
  56. def dummy_feature(
  57. vehicle_id="copter",
  58. version_id="copter-4.5.0-stable",
  59. board_id="MatekH743",
  60. feature_id="FEATURE_A",
  61. ):
  62. return FeatureOut(
  63. id=feature_id,
  64. name="Feature A",
  65. category=CategoryBase(id="cat1", name="Category 1"),
  66. description="A test feature",
  67. vehicle_id=vehicle_id,
  68. version_id=version_id,
  69. board_id=board_id,
  70. default=FeatureDefault(enabled=True, source="build-options-py"),
  71. dependencies=[],
  72. )
  73. # GET /vehicles
  74. def test_list_vehicles_returns_200_with_vehicle_list(self, client):
  75. """Returns 200 and a list of vehicles when service has data."""
  76. mock_vehicles_service = Mock()
  77. mock_vehicles_service.get_all_vehicles.return_value = [
  78. VehicleBase(id="copter", name="Copter"),
  79. VehicleBase(id="plane", name="Plane"),
  80. ]
  81. with self.override_vehicles_service(client, mock_vehicles_service):
  82. response = client.get("/api/v1/vehicles")
  83. assert response.status_code == status.HTTP_200_OK
  84. assert "application/json" in response.headers["content-type"]
  85. def test_list_vehicles_returns_200_with_empty_list(self, client):
  86. """Returns 200 with an empty list when no vehicles are available."""
  87. mock_vehicles_service = Mock()
  88. mock_vehicles_service.get_all_vehicles.return_value = []
  89. with self.override_vehicles_service(client, mock_vehicles_service):
  90. response = client.get("/api/v1/vehicles")
  91. assert response.status_code == status.HTTP_200_OK
  92. assert response.json() == []
  93. def test_list_vehicles_response_schema_has_required_fields(self, client):
  94. """Each vehicle in the response has 'id' and 'name' fields."""
  95. mock_vehicles_service = Mock()
  96. mock_vehicles_service.get_all_vehicles.return_value = [
  97. VehicleBase(id="copter", name="Copter"),
  98. ]
  99. with self.override_vehicles_service(client, mock_vehicles_service):
  100. response = client.get("/api/v1/vehicles")
  101. data = response.json()
  102. assert len(data) == 1
  103. assert "id" in data[0]
  104. assert "name" in data[0]
  105. def test_list_vehicles_method_not_allowed(self, client):
  106. """Non-GET methods on /vehicles return 405."""
  107. for method in [client.post, client.put, client.patch, client.delete]:
  108. response = method("/api/v1/vehicles")
  109. assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
  110. # GET /vehicles/{vehicle_id}
  111. def test_get_vehicle_returns_200_when_found(self, client):
  112. """Returns 200 when the vehicle exists."""
  113. mock_vehicles_service = Mock()
  114. mock_vehicles_service.get_vehicle.return_value = VehicleBase(id="copter", name="Copter")
  115. with self.override_vehicles_service(client, mock_vehicles_service):
  116. response = client.get("/api/v1/vehicles/copter")
  117. assert response.status_code == status.HTTP_200_OK
  118. def test_get_vehicle_returns_404_when_not_found(self, client):
  119. """Returns 404 when the service returns None."""
  120. mock_vehicles_service = Mock()
  121. mock_vehicles_service.get_vehicle.return_value = None
  122. with self.override_vehicles_service(client, mock_vehicles_service):
  123. response = client.get("/api/v1/vehicles/unknown")
  124. assert response.status_code == status.HTTP_404_NOT_FOUND
  125. def test_get_vehicle_404_detail_contains_vehicle_id(self, client):
  126. """The 404 error detail mentions the requested vehicle ID."""
  127. mock_vehicles_service = Mock()
  128. mock_vehicles_service.get_vehicle.return_value = None
  129. with self.override_vehicles_service(client, mock_vehicles_service):
  130. response = client.get("/api/v1/vehicles/some-vehicle-id")
  131. assert "some-vehicle-id" in response.json()["detail"]
  132. def test_get_vehicle_response_schema_has_required_fields(self, client):
  133. """Response body contains 'id' and 'name'."""
  134. mock_vehicles_service = Mock()
  135. mock_vehicles_service.get_vehicle.return_value = VehicleBase(id="copter", name="Copter")
  136. with self.override_vehicles_service(client, mock_vehicles_service):
  137. response = client.get("/api/v1/vehicles/copter")
  138. data = response.json()
  139. assert data["id"] == "copter"
  140. assert data["name"] == "Copter"
  141. def test_get_vehicle_service_called_with_correct_vehicle_id(self, client):
  142. """The vehicle_id path param is forwarded to the service."""
  143. mock_vehicles_service = Mock()
  144. mock_vehicles_service.get_vehicle.return_value = VehicleBase(id="plane", name="Plane")
  145. with self.override_vehicles_service(client, mock_vehicles_service):
  146. client.get("/api/v1/vehicles/plane")
  147. mock_vehicles_service.get_vehicle.assert_called_once_with("plane")
  148. def test_get_vehicle_method_not_allowed(self, client):
  149. """Non-GET methods on /vehicles/{vehicle_id} return 405."""
  150. for method in [client.post, client.put, client.patch, client.delete]:
  151. response = method("/api/v1/vehicles/copter")
  152. assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
  153. # GET /vehicles/{vehicle_id}/versions
  154. def test_list_versions_returns_200_with_version_list(self, client):
  155. """Returns 200 and a list of versions."""
  156. mock_vehicles_service = Mock()
  157. mock_vehicles_service.get_versions.return_value = [self.dummy_version()]
  158. with self.override_vehicles_service(client, mock_vehicles_service):
  159. response = client.get("/api/v1/vehicles/copter/versions")
  160. assert response.status_code == status.HTTP_200_OK
  161. assert "application/json" in response.headers["content-type"]
  162. def test_list_versions_returns_200_with_empty_list(self, client):
  163. """Returns 200 with an empty list when no versions exist."""
  164. mock_vehicles_service = Mock()
  165. mock_vehicles_service.get_versions.return_value = []
  166. with self.override_vehicles_service(client, mock_vehicles_service):
  167. response = client.get("/api/v1/vehicles/copter/versions")
  168. assert response.status_code == status.HTTP_200_OK
  169. assert response.json() == []
  170. def test_list_versions_response_schema_has_required_fields(self, client):
  171. """Each version in the response has the required schema fields."""
  172. mock_vehicles_service = Mock()
  173. mock_vehicles_service.get_versions.return_value = [self.dummy_version()]
  174. with self.override_vehicles_service(client, mock_vehicles_service):
  175. response = client.get("/api/v1/vehicles/copter/versions")
  176. data = response.json()
  177. assert len(data) == 1
  178. version = data[0]
  179. for field in ["id", "name", "type", "remote", "commit_ref", "vehicle_id"]:
  180. assert field in version
  181. assert "name" in version["remote"]
  182. assert "url" in version["remote"]
  183. def test_list_versions_type_query_param_forwarded_to_service(self, client):
  184. """The 'type' query param is passed as type_filter to the service."""
  185. mock_vehicles_service = Mock()
  186. mock_vehicles_service.get_versions.return_value = []
  187. with self.override_vehicles_service(client, mock_vehicles_service):
  188. client.get("/api/v1/vehicles/copter/versions?type=stable")
  189. mock_vehicles_service.get_versions.assert_called_once_with(
  190. "copter", type_filter="stable"
  191. )
  192. def test_list_versions_no_type_query_param_passes_none_to_service(self, client):
  193. """When 'type' is absent, type_filter=None is passed to the service."""
  194. mock_vehicles_service = Mock()
  195. mock_vehicles_service.get_versions.return_value = []
  196. with self.override_vehicles_service(client, mock_vehicles_service):
  197. client.get("/api/v1/vehicles/copter/versions")
  198. mock_vehicles_service.get_versions.assert_called_once_with(
  199. "copter", type_filter=None
  200. )
  201. def test_list_versions_vehicle_id_forwarded_to_service(self, client):
  202. """The vehicle_id path param is forwarded to the service."""
  203. mock_vehicles_service = Mock()
  204. mock_vehicles_service.get_versions.return_value = []
  205. with self.override_vehicles_service(client, mock_vehicles_service):
  206. client.get("/api/v1/vehicles/plane/versions")
  207. mock_vehicles_service.get_versions.assert_called_once_with(
  208. "plane", type_filter=None
  209. )
  210. def test_list_versions_method_not_allowed(self, client):
  211. """Non-GET methods on /vehicles/{vehicle_id}/versions return 405."""
  212. for method in [client.post, client.put, client.patch, client.delete]:
  213. response = method("/api/v1/vehicles/copter/versions")
  214. assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
  215. # GET /vehicles/{vehicle_id}/versions/{version_id}
  216. def test_get_version_returns_200_when_found(self, client):
  217. """Returns 200 when the version exists."""
  218. mock_vehicles_service = Mock()
  219. mock_vehicles_service.get_version.return_value = self.dummy_version()
  220. with self.override_vehicles_service(client, mock_vehicles_service):
  221. response = client.get("/api/v1/vehicles/copter/versions/copter-4.5.0-stable")
  222. assert response.status_code == status.HTTP_200_OK
  223. def test_get_version_returns_404_when_not_found(self, client):
  224. """Returns 404 when the service returns None."""
  225. mock_vehicles_service = Mock()
  226. mock_vehicles_service.get_version.return_value = None
  227. with self.override_vehicles_service(client, mock_vehicles_service):
  228. response = client.get("/api/v1/vehicles/copter/versions/nonexistent")
  229. assert response.status_code == status.HTTP_404_NOT_FOUND
  230. def test_get_version_404_detail_contains_vehicle_and_version_id(self, client):
  231. """The 404 error detail mentions both the vehicle ID and version ID."""
  232. mock_vehicles_service = Mock()
  233. mock_vehicles_service.get_version.return_value = None
  234. with self.override_vehicles_service(client, mock_vehicles_service):
  235. response = client.get("/api/v1/vehicles/copter/versions/nonexistent")
  236. detail = response.json()["detail"]
  237. assert "copter" in detail
  238. assert "nonexistent" in detail
  239. def test_get_version_response_schema_has_required_fields(self, client):
  240. """Response body matches VersionOut schema."""
  241. mock_vehicles_service = Mock()
  242. mock_vehicles_service.get_version.return_value = self.dummy_version()
  243. with self.override_vehicles_service(client, mock_vehicles_service):
  244. response = client.get("/api/v1/vehicles/copter/versions/copter-4.5.0-stable")
  245. data = response.json()
  246. for field in ["id", "name", "type", "remote", "commit_ref", "vehicle_id"]:
  247. assert field in data
  248. def test_get_version_service_called_with_correct_ids(self, client):
  249. """Both vehicle_id and version_id are forwarded to the service."""
  250. mock_vehicles_service = Mock()
  251. mock_vehicles_service.get_version.return_value = self.dummy_version()
  252. with self.override_vehicles_service(client, mock_vehicles_service):
  253. client.get("/api/v1/vehicles/copter/versions/copter-4.5.0-stable")
  254. mock_vehicles_service.get_version.assert_called_once_with(
  255. "copter", "copter-4.5.0-stable"
  256. )
  257. def test_get_version_method_not_allowed(self, client):
  258. """Non-GET methods on /vehicles/{vehicle_id}/versions/{version_id} return 405."""
  259. for method in [client.post, client.put, client.patch, client.delete]:
  260. response = method("/api/v1/vehicles/copter/versions/v1")
  261. assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
  262. # GET /vehicles/{vehicle_id}/versions/{version_id}/boards
  263. def test_list_boards_returns_200_when_boards_exist(self, client):
  264. """Returns 200 and a list of boards when boards are available."""
  265. mock_vehicles_service = Mock()
  266. mock_vehicles_service.get_boards.return_value = [self.dummy_board()]
  267. with self.override_vehicles_service(client, mock_vehicles_service):
  268. response = client.get("/api/v1/vehicles/copter/versions/copter-4.5.0-stable/boards")
  269. assert response.status_code == status.HTTP_200_OK
  270. assert "application/json" in response.headers["content-type"]
  271. def test_list_boards_returns_404_when_no_boards(self, client):
  272. """Returns 404 (not 200) when service returns an empty list."""
  273. mock_vehicles_service = Mock()
  274. mock_vehicles_service.get_boards.return_value = []
  275. with self.override_vehicles_service(client, mock_vehicles_service):
  276. response = client.get("/api/v1/vehicles/copter/versions/copter-4.5.0-stable/boards")
  277. assert response.status_code == status.HTTP_404_NOT_FOUND
  278. def test_list_boards_404_detail_contains_vehicle_and_version_id(self, client):
  279. """The 404 error detail mentions both the vehicle ID and version ID."""
  280. mock_vehicles_service = Mock()
  281. mock_vehicles_service.get_boards.return_value = []
  282. with self.override_vehicles_service(client, mock_vehicles_service):
  283. response = client.get("/api/v1/vehicles/copter/versions/copter-4.5.0-stable/boards")
  284. detail = response.json()["detail"]
  285. assert "copter" in detail
  286. assert "copter-4.5.0-stable" in detail
  287. def test_list_boards_response_schema_has_required_fields(self, client):
  288. """Each board in the response has the required schema fields."""
  289. mock_vehicles_service = Mock()
  290. mock_vehicles_service.get_boards.return_value = [self.dummy_board()]
  291. with self.override_vehicles_service(client, mock_vehicles_service):
  292. response = client.get("/api/v1/vehicles/copter/versions/copter-4.5.0-stable/boards")
  293. data = response.json()
  294. assert len(data) == 1
  295. board = data[0]
  296. for field in ["id", "name", "vehicle_id", "version_id"]:
  297. assert field in board
  298. def test_list_boards_service_called_with_correct_ids(self, client):
  299. """Both vehicle_id and version_id are forwarded to the service."""
  300. mock_vehicles_service = Mock()
  301. mock_vehicles_service.get_boards.return_value = [self.dummy_board()]
  302. with self.override_vehicles_service(client, mock_vehicles_service):
  303. client.get("/api/v1/vehicles/copter/versions/copter-4.5.0-stable/boards")
  304. mock_vehicles_service.get_boards.assert_called_once_with(
  305. "copter", "copter-4.5.0-stable"
  306. )
  307. def test_list_boards_method_not_allowed(self, client):
  308. """Non-GET methods on .../boards return 405."""
  309. for method in [client.post, client.put, client.patch, client.delete]:
  310. response = method("/api/v1/vehicles/copter/versions/v1/boards")
  311. assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
  312. # GET /vehicles/{vehicle_id}/versions/{version_id}/boards/{board_id}
  313. def test_get_board_returns_200_when_found(self, client):
  314. """Returns 200 when the board exists."""
  315. mock_vehicles_service = Mock()
  316. mock_vehicles_service.get_board.return_value = self.dummy_board()
  317. with self.override_vehicles_service(client, mock_vehicles_service):
  318. response = client.get(
  319. "/api/v1/vehicles/copter/versions/copter-4.5.0-stable/boards/MatekH743"
  320. )
  321. assert response.status_code == status.HTTP_200_OK
  322. def test_get_board_returns_404_when_not_found(self, client):
  323. """Returns 404 when the service returns None."""
  324. mock_vehicles_service = Mock()
  325. mock_vehicles_service.get_board.return_value = None
  326. with self.override_vehicles_service(client, mock_vehicles_service):
  327. response = client.get(
  328. "/api/v1/vehicles/copter/versions/copter-4.5.0-stable/boards/unknown"
  329. )
  330. assert response.status_code == status.HTTP_404_NOT_FOUND
  331. def test_get_board_404_detail_contains_board_id(self, client):
  332. """The 404 error detail mentions the requested board ID."""
  333. mock_vehicles_service = Mock()
  334. mock_vehicles_service.get_board.return_value = None
  335. with self.override_vehicles_service(client, mock_vehicles_service):
  336. response = client.get(
  337. "/api/v1/vehicles/copter/versions/copter-4.5.0-stable/boards/unknown"
  338. )
  339. assert "unknown" in response.json()["detail"]
  340. def test_get_board_response_schema_has_required_fields(self, client):
  341. """Response body matches BoardOut schema."""
  342. mock_vehicles_service = Mock()
  343. mock_vehicles_service.get_board.return_value = self.dummy_board()
  344. with self.override_vehicles_service(client, mock_vehicles_service):
  345. response = client.get(
  346. "/api/v1/vehicles/copter/versions/copter-4.5.0-stable/boards/MatekH743"
  347. )
  348. data = response.json()
  349. for field in ["id", "name", "vehicle_id", "version_id"]:
  350. assert field in data
  351. def test_get_board_service_called_with_correct_ids(self, client):
  352. """All three path params are forwarded to the service."""
  353. mock_vehicles_service = Mock()
  354. mock_vehicles_service.get_board.return_value = self.dummy_board()
  355. with self.override_vehicles_service(client, mock_vehicles_service):
  356. client.get(
  357. "/api/v1/vehicles/copter/versions/copter-4.5.0-stable/boards/MatekH743"
  358. )
  359. mock_vehicles_service.get_board.assert_called_once_with(
  360. "copter", "copter-4.5.0-stable", "MatekH743"
  361. )
  362. def test_get_board_method_not_allowed(self, client):
  363. """Non-GET methods on .../boards/{board_id} return 405."""
  364. for method in [client.post, client.put, client.patch, client.delete]:
  365. response = method("/api/v1/vehicles/copter/versions/v1/boards/b1")
  366. assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
  367. # GET /vehicles/{vehicle_id}/versions/{version_id}/boards/{board_id}/features
  368. _FEATURES_URL = "/api/v1/vehicles/copter/versions/copter-4.5.0-stable/boards/MatekH743/features"
  369. def test_list_features_returns_200_with_feature_list(self, client):
  370. """Returns 200 and a list of features."""
  371. mock_vehicles_service = Mock()
  372. mock_vehicles_service.get_features.return_value = [self.dummy_feature()]
  373. with self.override_vehicles_service(client, mock_vehicles_service):
  374. response = client.get(self._FEATURES_URL)
  375. assert response.status_code == status.HTTP_200_OK
  376. assert "application/json" in response.headers["content-type"]
  377. def test_list_features_returns_200_with_empty_list(self, client):
  378. """Returns 200 with empty list (unlike boards, empty features is not a 404)."""
  379. mock_vehicles_service = Mock()
  380. mock_vehicles_service.get_features.return_value = []
  381. with self.override_vehicles_service(client, mock_vehicles_service):
  382. response = client.get(self._FEATURES_URL)
  383. assert response.status_code == status.HTTP_200_OK
  384. assert response.json() == []
  385. def test_list_features_response_schema_has_required_fields(self, client):
  386. """Each feature in the response has the required schema fields."""
  387. mock_vehicles_service = Mock()
  388. mock_vehicles_service.get_features.return_value = [self.dummy_feature()]
  389. with self.override_vehicles_service(client, mock_vehicles_service):
  390. response = client.get(self._FEATURES_URL)
  391. data = response.json()
  392. assert len(data) == 1
  393. feature = data[0]
  394. for field in ["id", "name", "category", "vehicle_id", "version_id", "board_id", "default", "dependencies"]:
  395. assert field in feature
  396. assert "enabled" in feature["default"]
  397. assert "source" in feature["default"]
  398. def test_list_features_category_id_query_param_forwarded_to_service(self, client):
  399. """The 'category_id' query param is forwarded to the service."""
  400. mock_vehicles_service = Mock()
  401. mock_vehicles_service.get_features.return_value = []
  402. with self.override_vehicles_service(client, mock_vehicles_service):
  403. client.get(self._FEATURES_URL + "?category_id=cat1")
  404. mock_vehicles_service.get_features.assert_called_once_with(
  405. "copter", "copter-4.5.0-stable", "MatekH743", "cat1"
  406. )
  407. def test_list_features_no_category_id_passes_none_to_service(self, client):
  408. """When 'category_id' is absent, None is passed to the service."""
  409. mock_vehicles_service = Mock()
  410. mock_vehicles_service.get_features.return_value = []
  411. with self.override_vehicles_service(client, mock_vehicles_service):
  412. client.get(self._FEATURES_URL)
  413. mock_vehicles_service.get_features.assert_called_once_with(
  414. "copter", "copter-4.5.0-stable", "MatekH743", None
  415. )
  416. def test_list_features_service_called_with_correct_path_params(self, client):
  417. """All three path params are forwarded to the service."""
  418. mock_vehicles_service = Mock()
  419. mock_vehicles_service.get_features.return_value = []
  420. with self.override_vehicles_service(client, mock_vehicles_service):
  421. client.get(
  422. "/api/v1/vehicles/plane/versions/plane-4.4.0-stable/boards/CubeOrange/features"
  423. )
  424. mock_vehicles_service.get_features.assert_called_once_with(
  425. "plane", "plane-4.4.0-stable", "CubeOrange", None
  426. )
  427. def test_list_features_method_not_allowed(self, client):
  428. """Non-GET methods on .../features return 405."""
  429. for method in [client.post, client.put, client.patch, client.delete]:
  430. response = method(self._FEATURES_URL)
  431. assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
  432. # GET /vehicles/{vehicle_id}/versions/{version_id}/boards/{board_id}/features/{feature_id}
  433. def test_get_feature_returns_200_when_found(self, client):
  434. """Returns 200 when the feature exists."""
  435. mock_vehicles_service = Mock()
  436. mock_vehicles_service.get_feature.return_value = self.dummy_feature()
  437. with self.override_vehicles_service(client, mock_vehicles_service):
  438. response = client.get(f"{self._FEATURES_URL}/FEATURE_A")
  439. assert response.status_code == status.HTTP_200_OK
  440. def test_get_feature_returns_404_when_not_found(self, client):
  441. """Returns 404 when the service returns None."""
  442. mock_vehicles_service = Mock()
  443. mock_vehicles_service.get_feature.return_value = None
  444. with self.override_vehicles_service(client, mock_vehicles_service):
  445. response = client.get(f"{self._FEATURES_URL}/UNKNOWN_FEATURE")
  446. assert response.status_code == status.HTTP_404_NOT_FOUND
  447. def test_get_feature_404_detail_contains_feature_id(self, client):
  448. """The 404 error detail mentions the requested feature ID."""
  449. mock_vehicles_service = Mock()
  450. mock_vehicles_service.get_feature.return_value = None
  451. with self.override_vehicles_service(client, mock_vehicles_service):
  452. response = client.get(f"{self._FEATURES_URL}/UNKNOWN_FEATURE")
  453. assert "UNKNOWN_FEATURE" in response.json()["detail"]
  454. def test_get_feature_response_schema_has_required_fields(self, client):
  455. """Response body matches FeatureOut schema."""
  456. mock_vehicles_service = Mock()
  457. mock_vehicles_service.get_feature.return_value = self.dummy_feature()
  458. with self.override_vehicles_service(client, mock_vehicles_service):
  459. response = client.get(f"{self._FEATURES_URL}/FEATURE_A")
  460. data = response.json()
  461. for field in ["id", "name", "category", "vehicle_id", "version_id", "board_id", "default", "dependencies"]:
  462. assert field in data
  463. def test_get_feature_service_called_with_correct_ids(self, client):
  464. """All four path params are forwarded to the service."""
  465. mock_vehicles_service = Mock()
  466. mock_vehicles_service.get_feature.return_value = self.dummy_feature()
  467. with self.override_vehicles_service(client, mock_vehicles_service):
  468. client.get(f"{self._FEATURES_URL}/FEATURE_A")
  469. mock_vehicles_service.get_feature.assert_called_once_with(
  470. "copter", "copter-4.5.0-stable", "MatekH743", "FEATURE_A"
  471. )
  472. def test_get_feature_method_not_allowed(self, client):
  473. """Non-GET methods on .../features/{feature_id} return 405."""
  474. for method in [client.post, client.put, client.patch, client.delete]:
  475. response = method(f"{self._FEATURES_URL}/FEATURE_A")
  476. assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED