Skip to content

Commit 6c9211f

Browse files
committed
tests: add tests for standard artifacts api endpoint
1 parent 44a5be2 commit 6c9211f

4 files changed

Lines changed: 315 additions & 0 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<html>
2+
<body>
3+
<table width=80%>
4+
<tr bgcolor="#aaaaaa"><td width=50 align=center><b>Type</b></td><td><b>Filename</b></td><td><b>Date</b></td><td><b>Size</b></td></tr>
5+
<tr bgcolor="#ffffff"><td align=center><img src="/icons/back.gif"></td><td><a href="/Copter/stable-4.5.0"><b>Parent Directory</B> </a></td>
6+
<td>--</td><td>--</td>
7+
<tr bgcolor="#cacaca">
8+
<td align=center><img src="/icons/text.gif"></td>
9+
<td><a href="/Copter/stable-4.5.0/CubeOrange/arducopter.abin">arducopter.abin</a></td>
10+
<td>Tue Apr 2 05:11:12 2024</td>
11+
<td>1814971</td>
12+
</tr>
13+
<tr bgcolor="#ffffff">
14+
<td align=center><img src="/icons/text.gif"></td>
15+
<td><a href="/Copter/stable-4.5.0/CubeOrange/arducopter.apj">arducopter.apj</a></td>
16+
<td>Tue Apr 15 05:11:12 2024</td>
17+
<td>1640045</td>
18+
</tr>
19+
<tr bgcolor="#cacaca">
20+
<td align=center><img src="/icons/text.gif"></td>
21+
<td><a href="/Copter/stable-4.5.0/CubeOrange/features.txt">features.txt</a></td>
22+
<td>Tue Apr 2 05:11:12 2024</td>
23+
<td>8294</td>
24+
</tr>
25+
</table>
26+
</body>
27+
</html>
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""
2+
Tests for Apache directory listing parser.
3+
"""
4+
from datetime import datetime, timezone
5+
from pathlib import Path
6+
7+
from utils.apache_dir_listing import parse_apache_dir_listing, _parse_apache_date
8+
9+
10+
FIXTURES_DIR = Path(__file__).parent / "fixtures"
11+
SAMPLE_LISTING_HTML = (FIXTURES_DIR / "apache_dir_listing_sample.html").read_text()
12+
BASE_URL = "https://firmware.ardupilot.org/Copter/stable-4.5.0/CubeOrange/"
13+
14+
15+
class TestParseApacheDate:
16+
def test_parses_single_digit_day(self):
17+
result = _parse_apache_date("Tue Apr 2 05:11:12 2024")
18+
assert result == datetime(2024, 4, 2, 5, 11, 12, tzinfo=timezone.utc)
19+
20+
def test_parses_double_digit_day(self):
21+
result = _parse_apache_date("Tue Apr 15 05:11:12 2024")
22+
assert result == datetime(2024, 4, 15, 5, 11, 12, tzinfo=timezone.utc)
23+
24+
def test_returns_none_for_dash(self):
25+
assert _parse_apache_date("--") is None
26+
27+
def test_returns_none_for_invalid(self):
28+
assert _parse_apache_date("not a date") is None
29+
30+
31+
class TestParseApacheDirListing:
32+
def test_parses_files_and_skips_parent_directory(self):
33+
entries = parse_apache_dir_listing(SAMPLE_LISTING_HTML, BASE_URL)
34+
35+
assert len(entries) == 3
36+
assert entries[0]["name"] == "arducopter.abin"
37+
assert entries[1]["name"] == "arducopter.apj"
38+
assert entries[2]["name"] == "features.txt"
39+
40+
def test_resolves_absolute_urls(self):
41+
entries = parse_apache_dir_listing(SAMPLE_LISTING_HTML, BASE_URL)
42+
43+
assert entries[0]["url"] == (
44+
"https://firmware.ardupilot.org/Copter/stable-4.5.0/"
45+
"CubeOrange/arducopter.abin"
46+
)
47+
48+
def test_parses_size_and_modified(self):
49+
entries = parse_apache_dir_listing(SAMPLE_LISTING_HTML, BASE_URL)
50+
51+
assert entries[0]["size"] == 1814971
52+
assert entries[0]["modified"] == datetime(
53+
2024, 4, 2, 5, 11, 12, tzinfo=timezone.utc
54+
)
55+
assert entries[1]["modified"] == datetime(
56+
2024, 4, 15, 5, 11, 12, tzinfo=timezone.utc
57+
)
58+
59+
def test_returns_empty_list_when_no_table(self):
60+
assert parse_apache_dir_listing("<html><body></body></html>", BASE_URL) == []

tests/web/test_vehicles_api.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
VehicleBase,
1010
VersionOut,
1111
BoardOut,
12+
StandardArtifactOut,
1213
FeatureOut,
1314
CategoryBase,
1415
FeatureDefault,
@@ -78,6 +79,18 @@ def dummy_feature(
7879
dependencies=[],
7980
)
8081

82+
@staticmethod
83+
def dummy_standard_artifact(
84+
name="arducopter.apj",
85+
url="https://firmware.ardupilot.org/Copter/stable-4.5.0/CubeOrange/arducopter.apj",
86+
):
87+
return StandardArtifactOut(
88+
name=name,
89+
url=url,
90+
size=1640045,
91+
modified=None,
92+
)
93+
8194
# GET /vehicles
8295

8396
def test_list_vehicles_returns_200_with_vehicle_list(self, client):
@@ -442,6 +455,75 @@ def test_get_board_method_not_allowed(self, client):
442455
response = method("/api/v1/vehicles/copter/versions/v1/boards/b1")
443456
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
444457

458+
# GET /vehicles/{vehicle_id}/versions/{version_id}/boards/{board_id}/standard_artifacts
459+
460+
_STANDARD_ARTIFACTS_URL = (
461+
"/api/v1/vehicles/copter/versions/copter-4.5.0-stable/"
462+
"boards/MatekH743/standard_artifacts"
463+
)
464+
465+
def test_list_board_standard_artifacts_returns_200(self, client):
466+
mock_vehicles_service = Mock()
467+
mock_vehicles_service.get_board_standard_artifacts.return_value = [
468+
self.dummy_standard_artifact()
469+
]
470+
with self.override_vehicles_service(client, mock_vehicles_service):
471+
response = client.get(self._STANDARD_ARTIFACTS_URL)
472+
473+
assert response.status_code == status.HTTP_200_OK
474+
assert "application/json" in response.headers["content-type"]
475+
476+
def test_list_board_standard_artifacts_returns_404_when_not_found(self, client):
477+
mock_vehicles_service = Mock()
478+
mock_vehicles_service.get_board_standard_artifacts.return_value = None
479+
with self.override_vehicles_service(client, mock_vehicles_service):
480+
response = client.get(self._STANDARD_ARTIFACTS_URL)
481+
482+
assert response.status_code == status.HTTP_404_NOT_FOUND
483+
assert "MatekH743" in response.json()["detail"]
484+
485+
def test_list_board_standard_artifacts_returns_502_on_fw_server_error(self, client):
486+
from metadata_manager.ap_src_meta_fetcher import FirmwareServerUnavailableError
487+
488+
mock_vehicles_service = Mock()
489+
mock_vehicles_service.get_board_standard_artifacts.side_effect = (
490+
FirmwareServerUnavailableError("connection failed")
491+
)
492+
with self.override_vehicles_service(client, mock_vehicles_service):
493+
response = client.get(self._STANDARD_ARTIFACTS_URL)
494+
495+
assert response.status_code == status.HTTP_502_BAD_GATEWAY
496+
497+
def test_list_board_standard_artifacts_response_schema(self, client):
498+
mock_vehicles_service = Mock()
499+
mock_vehicles_service.get_board_standard_artifacts.return_value = [
500+
self.dummy_standard_artifact()
501+
]
502+
with self.override_vehicles_service(client, mock_vehicles_service):
503+
response = client.get(self._STANDARD_ARTIFACTS_URL)
504+
505+
data = response.json()
506+
assert len(data) == 1
507+
for field in ["name", "url", "size", "modified"]:
508+
assert field in data[0]
509+
510+
def test_list_board_standard_artifacts_service_called_with_correct_ids(self, client):
511+
mock_vehicles_service = Mock()
512+
mock_vehicles_service.get_board_standard_artifacts.return_value = [
513+
self.dummy_standard_artifact()
514+
]
515+
with self.override_vehicles_service(client, mock_vehicles_service):
516+
client.get(self._STANDARD_ARTIFACTS_URL)
517+
518+
mock_vehicles_service.get_board_standard_artifacts.assert_called_once_with(
519+
"copter", "copter-4.5.0-stable", "MatekH743"
520+
)
521+
522+
def test_list_board_standard_artifacts_method_not_allowed(self, client):
523+
for method in [client.post, client.put, client.patch, client.delete]:
524+
response = method(self._STANDARD_ARTIFACTS_URL)
525+
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
526+
445527
# GET /vehicles/{vehicle_id}/versions/{version_id}/boards/{board_id}/features
446528

447529
_FEATURES_URL = "/api/v1/vehicles/copter/versions/copter-4.5.0-stable/boards/MatekH743/features"

tests/web/test_vehicles_service.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1145,3 +1145,149 @@ def test_get_feature_returns_correct_match_among_many(
11451145
assert result is not None
11461146
assert result.id == "FEATURE_B"
11471147
assert result.default.enabled is False
1148+
1149+
# Tests for get_board_standard_artifacts
1150+
1151+
def test_get_board_standard_artifacts_version_not_found_returns_none(
1152+
self, service, mock_versions_fetcher
1153+
):
1154+
mock_versions_fetcher.get_version_info.return_value = None
1155+
1156+
result = service.get_board_standard_artifacts(
1157+
"copter", "nonexistent-version-id", "CubeRed"
1158+
)
1159+
1160+
assert result is None
1161+
1162+
def test_get_board_standard_artifacts_board_not_found_returns_none(
1163+
self, service, mock_versions_fetcher, mock_ap_src_metadata_fetcher
1164+
):
1165+
version_info = VersionInfo(
1166+
remote_info=RemoteInfo(
1167+
name="ardupilot",
1168+
url="https://github.com/ArduPilot/ardupilot.git",
1169+
),
1170+
commit_ref="refs/tags/Copter-4.5.0",
1171+
release_type="stable",
1172+
version_number="4.5.0",
1173+
ap_build_artifacts_url="https://firmware.ardupilot.org/Copter/stable-4.5.0",
1174+
)
1175+
mock_versions_fetcher.get_version_info.return_value = version_info
1176+
mock_ap_src_metadata_fetcher.get_boards.return_value = ["CubeOrange"]
1177+
1178+
result = service.get_board_standard_artifacts(
1179+
"copter", version_info.version_id, "UnknownBoard"
1180+
)
1181+
1182+
assert result is None
1183+
1184+
def test_get_board_standard_artifacts_no_artifacts_url_returns_none(
1185+
self, service, mock_versions_fetcher, mock_ap_src_metadata_fetcher
1186+
):
1187+
version_info = VersionInfo(
1188+
remote_info=RemoteInfo(
1189+
name="ardupilot",
1190+
url="https://github.com/ArduPilot/ardupilot.git",
1191+
),
1192+
commit_ref="refs/tags/Copter-4.5.0",
1193+
release_type="stable",
1194+
version_number="4.5.0",
1195+
ap_build_artifacts_url=None,
1196+
)
1197+
mock_versions_fetcher.get_version_info.return_value = version_info
1198+
mock_ap_src_metadata_fetcher.get_boards.return_value = ["CubeRed"]
1199+
1200+
result = service.get_board_standard_artifacts(
1201+
"copter", version_info.version_id, "CubeRed"
1202+
)
1203+
1204+
assert result is None
1205+
mock_ap_src_metadata_fetcher.get_board_standard_artifacts_from_fw_server.assert_not_called()
1206+
1207+
def test_get_board_standard_artifacts_success(
1208+
self, service, mock_versions_fetcher, mock_ap_src_metadata_fetcher
1209+
):
1210+
from datetime import datetime, timezone
1211+
1212+
version_info = VersionInfo(
1213+
remote_info=RemoteInfo(
1214+
name="ardupilot",
1215+
url="https://github.com/ArduPilot/ardupilot.git",
1216+
),
1217+
commit_ref="refs/tags/Copter-4.5.0",
1218+
release_type="stable",
1219+
version_number="4.5.0",
1220+
ap_build_artifacts_url="https://firmware.ardupilot.org/Copter/stable-4.5.0",
1221+
)
1222+
mock_versions_fetcher.get_version_info.return_value = version_info
1223+
mock_ap_src_metadata_fetcher.get_boards.return_value = ["CubeRed"]
1224+
mock_ap_src_metadata_fetcher.get_board_standard_artifacts_from_fw_server.return_value = [
1225+
{
1226+
"name": "arducopter.apj",
1227+
"url": "https://firmware.ardupilot.org/Copter/stable-4.5.0/CubeRed/arducopter.apj",
1228+
"size": 1640045,
1229+
"modified": datetime(2024, 4, 2, 5, 11, 12, tzinfo=timezone.utc),
1230+
}
1231+
]
1232+
1233+
result = service.get_board_standard_artifacts(
1234+
"copter", version_info.version_id, "CubeRed"
1235+
)
1236+
1237+
assert len(result) == 1
1238+
assert result[0].name == "arducopter.apj"
1239+
assert result[0].size == 1640045
1240+
mock_ap_src_metadata_fetcher.get_board_standard_artifacts_from_fw_server.assert_called_once_with(
1241+
version_artifacts_url="https://firmware.ardupilot.org/Copter/stable-4.5.0",
1242+
board_id="CubeRed",
1243+
vehicle_id="copter",
1244+
)
1245+
1246+
def test_get_board_standard_artifacts_fw_server_404_returns_none(
1247+
self, service, mock_versions_fetcher, mock_ap_src_metadata_fetcher
1248+
):
1249+
version_info = VersionInfo(
1250+
remote_info=RemoteInfo(
1251+
name="ardupilot",
1252+
url="https://github.com/ArduPilot/ardupilot.git",
1253+
),
1254+
commit_ref="refs/tags/Copter-4.5.0",
1255+
release_type="stable",
1256+
version_number="4.5.0",
1257+
ap_build_artifacts_url="https://firmware.ardupilot.org/Copter/stable-4.5.0",
1258+
)
1259+
mock_versions_fetcher.get_version_info.return_value = version_info
1260+
mock_ap_src_metadata_fetcher.get_boards.return_value = ["CubeRed"]
1261+
mock_ap_src_metadata_fetcher.get_board_standard_artifacts_from_fw_server.return_value = None
1262+
1263+
result = service.get_board_standard_artifacts(
1264+
"copter", version_info.version_id, "CubeRed"
1265+
)
1266+
1267+
assert result is None
1268+
1269+
def test_get_board_standard_artifacts_fw_server_error_propagates(
1270+
self, service, mock_versions_fetcher, mock_ap_src_metadata_fetcher
1271+
):
1272+
from metadata_manager.ap_src_meta_fetcher import FirmwareServerUnavailableError
1273+
1274+
version_info = VersionInfo(
1275+
remote_info=RemoteInfo(
1276+
name="ardupilot",
1277+
url="https://github.com/ArduPilot/ardupilot.git",
1278+
),
1279+
commit_ref="refs/tags/Copter-4.5.0",
1280+
release_type="stable",
1281+
version_number="4.5.0",
1282+
ap_build_artifacts_url="https://firmware.ardupilot.org/Copter/stable-4.5.0",
1283+
)
1284+
mock_versions_fetcher.get_version_info.return_value = version_info
1285+
mock_ap_src_metadata_fetcher.get_boards.return_value = ["CubeRed"]
1286+
mock_ap_src_metadata_fetcher.get_board_standard_artifacts_from_fw_server.side_effect = (
1287+
FirmwareServerUnavailableError("connection failed")
1288+
)
1289+
1290+
with pytest.raises(FirmwareServerUnavailableError):
1291+
service.get_board_standard_artifacts(
1292+
"copter", version_info.version_id, "CubeRed"
1293+
)

0 commit comments

Comments
 (0)