فهرست منبع

tests: add/update tests for provider based versions manager

Shiv Tyagi 2 ماه پیش
والد
کامیت
0cbe1e1ec6

+ 62 - 0
tests/metadata_manager/fixtures/manifest_sample.json

@@ -0,0 +1,62 @@
+{
+  "format-version": "1.0.0",
+  "firmware": [
+    {
+      "vehicletype": "Copter",
+      "platform": "CubeOrange",
+      "git-sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+      "url": "https://firmware.ardupilot.org/Copter/stable-4.6.3/CubeOrange/arducopter.apj",
+      "mav-firmware-version-type": "STABLE-4.6.3",
+      "mav-firmware-version": "4.6.3",
+      "format": "apj",
+      "latest": 0,
+      "mav-type": "QUADROTOR",
+      "image_size": 100
+    },
+    {
+      "vehicletype": "Copter",
+      "platform": "CubeOrange-heli",
+      "git-sha": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+      "url": "https://firmware.ardupilot.org/Copter/stable-4.6.3/CubeOrange-heli/arducopter-heli.apj",
+      "mav-firmware-version-type": "STABLE-4.6.3",
+      "mav-firmware-version": "4.6.3",
+      "format": "apj",
+      "latest": 0,
+      "mav-type": "HELICOPTER",
+      "image_size": 100
+    },
+    {
+      "vehicletype": "Copter",
+      "platform": "CubeOrange",
+      "git-sha": "cccccccccccccccccccccccccccccccccccccccc",
+      "url": "https://firmware.ardupilot.org/Copter/beta/CubeOrange/arducopter.apj",
+      "mav-firmware-version-type": "BETA",
+      "mav-firmware-version": "4.7.0",
+      "format": "apj",
+      "latest": 0,
+      "mav-type": "QUADROTOR"
+    },
+    {
+      "vehicletype": "AntennaTracker",
+      "platform": "CubeOrange",
+      "git-sha": "dddddddddddddddddddddddddddddddddddddddd",
+      "url": "https://firmware.ardupilot.org/AntennaTracker/stable-4.6.3/CubeOrange/antennatracker.apj",
+      "mav-firmware-version-type": "STABLE-4.6.3",
+      "mav-firmware-version": "4.6.3",
+      "format": "apj",
+      "latest": 0,
+      "mav-type": "ANTENNA_TRACKER"
+    },
+    {
+      "vehicletype": "Copter",
+      "platform": "CubeOrange",
+      "git-sha": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
+      "url": "https://firmware.ardupilot.org/Copter/latest/CubeOrange/arducopter.apj",
+      "mav-firmware-version-type": "DEV",
+      "mav-firmware-version": "4.8.0",
+      "format": "apj",
+      "latest": 1,
+      "mav-type": "QUADROTOR"
+    }
+  ]
+}

+ 54 - 0
tests/metadata_manager/test_firmware_server.py

@@ -0,0 +1,54 @@
+import json
+from pathlib import Path
+from unittest.mock import Mock, patch
+
+from metadata_manager import ManifestClient, ManifestIndex
+from metadata_manager.firmware_server.client import _CacheMeta
+
+
+FIXTURES_DIR = Path(__file__).parent / "fixtures"
+SAMPLE_MANIFEST = json.loads((FIXTURES_DIR / "manifest_sample.json").read_text())
+
+
+class TestManifestIndex:
+    def test_builds_releases_for_copter_heli_and_tracker(self):
+        index = ManifestIndex.build(SAMPLE_MANIFEST)
+
+        copter = index.get_releases("copter")
+        stable = [r for r in copter if r.release_type == "stable" and r.version_number == "4.6.3"]
+        assert len(stable) == 1
+        assert stable[0].commit_reference.startswith("aaaa")
+
+        heli = index.get_releases("heli")
+        heli_stable = [r for r in heli if r.release_type == "stable" and r.version_number == "4.6.3"]
+        assert len(heli_stable) == 1
+        assert heli_stable[0].commit_reference.startswith("bbbb")
+
+        tracker = index.get_releases("tracker")
+        assert any(r.version_number == "4.6.3" for r in tracker)
+
+        latest = [r for r in copter if r.release_type == "latest"]
+        assert len(latest) == 1
+        assert latest[0].commit_reference.startswith("eeee")
+        assert latest[0].version_number == "NA"
+        assert latest[0].ap_build_artifacts_url == (
+            "https://firmware.ardupilot.org/Copter/latest"
+        )
+
+
+class TestManifestClientCache:
+    def test_uses_cache_on_304(self, tmp_path):
+        client = ManifestClient(
+            url="https://example.com/manifest.json",
+            cache_dir=str(tmp_path),
+        )
+        client._write_cache(
+            b'{"format-version":"1.0.0","firmware":[]}',
+            _CacheMeta(etag='"abc"', last_modified="Mon, 01 Jan 2024 00:00:00 GMT"),
+        )
+
+        response = Mock(status_code=304, headers={}, content=b"")
+        with patch("metadata_manager.firmware_server.client.requests.get", return_value=response):
+            raw = client.fetch_raw()
+
+        assert raw == b'{"format-version":"1.0.0","firmware":[]}'

+ 123 - 0
tests/metadata_manager/test_versions_manager.py

@@ -0,0 +1,123 @@
+from unittest.mock import Mock, PropertyMock
+
+import pytest
+
+from metadata_manager import (
+    DEFAULT_WHITELISTED_FORK_REMOTES,
+    ForkRemoteSpec,
+    ManifestJSON,
+    ManifestJsonVersionsProvider,
+    RemoteInfo,
+    VehiclesManager,
+    VersionInfo,
+    VersionsManager,
+)
+
+
+@pytest.fixture
+def vehicles_manager():
+    existing = VehiclesManager.get_singleton()
+    if existing is not None:
+        return existing
+    return VehiclesManager()
+
+
+@pytest.fixture
+def versions_manager(vehicles_manager, tmp_path):
+    VersionsManager._VersionsManager__singleton = None
+    repo = Mock()
+    manager = VersionsManager(
+        ap_repo=repo,
+        remotes_json_path=str(tmp_path / "missing-remotes.json"),
+        providers=[],
+    )
+    yield manager
+    VersionsManager._VersionsManager__singleton = None
+
+
+class TestManifestJsonVersionsProvider:
+    def test_is_available_when_manifest_is_available(self, vehicles_manager):
+        manifest_json = Mock(spec=ManifestJSON)
+        type(manifest_json).is_available = PropertyMock(return_value=True)
+
+        provider = ManifestJsonVersionsProvider(manifest_json)
+
+        assert provider.is_available is True
+
+    def test_is_unavailable_when_manifest_is_unavailable(self, vehicles_manager):
+        manifest_json = Mock(spec=ManifestJSON)
+        type(manifest_json).is_available = PropertyMock(return_value=False)
+
+        provider = ManifestJsonVersionsProvider(manifest_json)
+
+        assert provider.is_available is False
+        assert provider.get_versions("copter") == []
+        assert provider.get_remotes() == []
+
+    def test_refresh_delegates_to_manifest_json(self, vehicles_manager):
+        manifest_json = Mock(spec=ManifestJSON)
+        provider = ManifestJsonVersionsProvider(manifest_json)
+
+        provider.refresh()
+
+        manifest_json.refresh.assert_called_once()
+
+
+class TestVersionsManagerDedup:
+    def _make_version(self, release_type: str, version_number: str, commit_ref: str):
+        remote = RemoteInfo("ardupilot", "https://github.com/ardupilot/ardupilot.git")
+        return VersionInfo(
+            remote_info=remote,
+            commit_ref=commit_ref,
+            release_type=release_type,
+            version_number=version_number,
+            ap_build_artifacts_url=None,
+        )
+
+    def test_stable_wins_when_beta_is_seen_first(self, versions_manager):
+        commit = "03c12698df56c600f5b3d39f8b17d414ea6d2a48"
+        beta = self._make_version("beta", "4.5.0", commit)
+        stable = self._make_version("stable", "4.5.0", commit)
+
+        beta_provider = Mock()
+        beta_provider.get_versions.return_value = [beta]
+        stable_provider = Mock()
+        stable_provider.get_versions.return_value = [stable]
+        versions_manager._providers = [beta_provider, stable_provider]
+
+        versions = versions_manager.get_versions_for_vehicle("sub")
+
+        assert len(versions) == 1
+        assert versions[0].release_type == "stable"
+        assert versions[0].version_number == "4.5.0"
+
+    def test_stable_wins_when_stable_is_seen_first(self, versions_manager):
+        commit = "03c12698df56c600f5b3d39f8b17d414ea6d2a48"
+        beta = self._make_version("beta", "4.5.0", commit)
+        stable = self._make_version("stable", "4.5.0", commit)
+
+        stable_provider = Mock()
+        stable_provider.get_versions.return_value = [stable]
+        beta_provider = Mock()
+        beta_provider.get_versions.return_value = [beta]
+        versions_manager._providers = [stable_provider, beta_provider]
+
+        versions = versions_manager.get_versions_for_vehicle("sub")
+
+        assert len(versions) == 1
+        assert versions[0].release_type == "stable"
+
+
+class TestForkRemoteSpec:
+    def test_default_rmackay9_uses_custom_repo_name(self):
+        rmackay9 = next(
+            spec for spec in DEFAULT_WHITELISTED_FORK_REMOTES if spec.owner == "rmackay9"
+        )
+        assert rmackay9.repo == "rmackay9-ardupilot"
+        assert rmackay9.github_repo == "rmackay9/rmackay9-ardupilot"
+        assert rmackay9.url == "https://github.com/rmackay9/rmackay9-ardupilot.git"
+
+    def test_fork_remote_spec_url(self):
+        spec = ForkRemoteSpec(owner="example", repo="my-ardupilot-fork")
+        assert spec.github_repo == "example/my-ardupilot-fork"
+        assert spec.url == "https://github.com/example/my-ardupilot-fork.git"

+ 21 - 22
tests/web/conftest.py

@@ -25,7 +25,7 @@ TEST_REMOTES_JSON = [
         "url": "https://github.com/test/ardupilot.git",
         "vehicles": [
             {
-                "name": "Copter",
+                "id": "copter",
                 "releases": [
                     {
                         "release_type": "latest",
@@ -40,7 +40,7 @@ TEST_REMOTES_JSON = [
                 ]
             },
             {
-                "name": "Plane",
+                "id": "plane",
                 "releases": [
                     {
                         "release_type": "latest",
@@ -56,7 +56,7 @@ TEST_REMOTES_JSON = [
         "url": "https://github.com/another/ardupilot.git",
         "vehicles": [
             {
-                "name": "Rover",
+                "id": "rover",
                 "releases": [
                     {
                         "release_type": "Custom",
@@ -90,10 +90,10 @@ def test_base_dir() -> Generator[str, None, None]:
     with open(remotes_json_path, "w") as f:
         json.dump(TEST_REMOTES_JSON, f, indent=2)
 
-    # Create remote reload token file
-    token_file_path = os.path.join(temp_dir, "secrets", "reload_token")
+    # Create admin token file
+    token_file_path = os.path.join(temp_dir, "secrets", "admin_token")
     with open(token_file_path, "w") as f:
-        f.write("test-remote-reload-token-12345")
+        f.write("test-admin-token-12345")
 
     yield temp_dir
 
@@ -130,9 +130,9 @@ def mock_ap_src_metadata_fetcher():
 
 
 @pytest.fixture
-def mock_versions_fetcher(test_base_dir):
+def mock_versions_manager(test_base_dir):
     """
-    Create a mock VersionsFetcher that doesn't actually fetch versions.
+    Create a mock VersionsManager that doesn't actually fetch versions.
 
     This allows tests to run without starting background threads or
     making actual git operations.
@@ -141,27 +141,27 @@ def mock_versions_fetcher(test_base_dir):
         test_base_dir: Test base directory fixture
 
     Returns:
-        Mock: Mock VersionsFetcher instance
+        Mock: Mock VersionsManager instance
     """
-    from metadata_manager.versions_fetcher import RemoteInfo
+    from metadata_manager import RemoteInfo
 
-    mock_fetcher = Mock()
+    mock_manager = Mock()
 
-    # Mock the reload_remotes_json method
-    mock_fetcher.reload_remotes_json = Mock(return_value=None)
+    # Mock the refresh methods
+    mock_manager.refresh_all = Mock(return_value=None)
 
     # Mock get_all_remotes_info to return test remotes
     test_remotes = [
         RemoteInfo(name="test-remote-1", url="https://github.com/test/ardupilot.git"),
         RemoteInfo(name="test-remote-2", url="https://github.com/another/ardupilot.git")
     ]
-    mock_fetcher.get_all_remotes_info = Mock(return_value=test_remotes)
+    mock_manager.get_all_remotes_info = Mock(return_value=test_remotes)
 
     # Mock start/stop methods (no-op for tests)
-    mock_fetcher.start = Mock()
-    mock_fetcher.stop = Mock()
+    mock_manager.start = Mock()
+    mock_manager.stop = Mock()
 
-    return mock_fetcher
+    return mock_manager
 
 
 @pytest.fixture
@@ -191,7 +191,6 @@ def mock_vehicles_manager():
         Mock: Mock VehiclesManager instance
     """
     mock_manager = Mock()
-    mock_manager.get_vehicle_names = Mock(return_value=["Copter", "Plane", "Rover"])
     return mock_manager
 
 
@@ -199,7 +198,7 @@ def mock_vehicles_manager():
 def app_with_mocked_dependencies(
     test_base_dir,
     mock_git_repo,
-    mock_versions_fetcher,
+    mock_versions_manager,
     mock_build_manager,
     mock_vehicles_manager,
 ):
@@ -215,7 +214,7 @@ def app_with_mocked_dependencies(
     Args:
         test_base_dir: Test base directory
         mock_git_repo: Mock git repository
-        mock_versions_fetcher: Mock versions fetcher
+        mock_versions_manager: Mock versions manager
         mock_build_manager: Mock build manager
         mock_vehicles_manager: Mock vehicles manager
 
@@ -240,7 +239,7 @@ def app_with_mocked_dependencies(
         """Test lifespan that doesn't start background tasks."""
         # Setup: Attach mocked dependencies to app state
         app.state.repo = mock_git_repo
-        app.state.versions_fetcher = mock_versions_fetcher
+        app.state.versions_manager = mock_versions_manager
         app.state.vehicles_manager = mock_vehicles_manager
         app.state.build_manager = mock_build_manager
         app.state.limiter = limiter
@@ -250,7 +249,7 @@ def app_with_mocked_dependencies(
         app.state.ap_src_metadata_fetcher = mock_ap_src_fetcher
 
         # Don't start background tasks in test mode
-        # versions_fetcher.start()
+        # versions_manager.start()
         # cleaner.start()
         # progress_updater.start()
 

+ 24 - 24
tests/web/test_admin_api.py

@@ -9,11 +9,11 @@ from fastapi import status
 from web.core.config import get_settings
 
 
-class TestAdminRefreshRemotesEndpoint:
-    """Test suite for the /admin/refresh_remotes endpoint."""
+class TestAdminRefreshVersionsEndpoint:
+    """Test suite for the /admin/refresh_versions endpoint."""
 
-    AUTH_HEADERS = {"Authorization": "Bearer test-remote-reload-token-12345"}
-    TEST_TOKEN = "test-remote-reload-token-12345"
+    AUTH_HEADERS = {"Authorization": "Bearer test-admin-token-12345"}
+    TEST_TOKEN = "test-admin-token-12345"
 
     @staticmethod
     @contextmanager
@@ -25,8 +25,8 @@ class TestAdminRefreshRemotesEndpoint:
         finally:
             client.app.dependency_overrides.pop(get_settings, None)
 
-    def test_refresh_remotes_success(self, client, test_base_dir):
-        """Test successful refresh of remotes with valid auth and verifies against remotes.json."""
+    def test_refresh_versions_success(self, client, test_base_dir):
+        """Test successful refresh with valid auth and verifies against remotes.json."""
         import os
         import json
 
@@ -37,10 +37,10 @@ class TestAdminRefreshRemotesEndpoint:
             initial_remotes = json.load(f)
 
         mock_settings = Mock()
-        mock_settings.remote_reload_token = self.TEST_TOKEN
+        mock_settings.admin_token = self.TEST_TOKEN
         with self.override_settings(client, mock_settings):
             response = client.post(
-                "/api/v1/admin/refresh_remotes",
+                "/api/v1/admin/refresh_versions",
                 headers=self.AUTH_HEADERS
             )
 
@@ -54,22 +54,22 @@ class TestAdminRefreshRemotesEndpoint:
         for name in expected_names:
             assert name in data["remotes"]
 
-    def test_refresh_remotes_no_auth(self, client):
+    def test_refresh_versions_no_auth(self, client):
         """Test refresh without authentication - should fail."""
         mock_settings = Mock()
-        mock_settings.remote_reload_token = self.TEST_TOKEN
+        mock_settings.admin_token = self.TEST_TOKEN
         with self.override_settings(client, mock_settings):
-            response = client.post("/api/v1/admin/refresh_remotes")
+            response = client.post("/api/v1/admin/refresh_versions")
 
         assert response.status_code == status.HTTP_401_UNAUTHORIZED
 
-    def test_refresh_remotes_invalid_token(self, client):
+    def test_refresh_versions_invalid_token(self, client):
         """Test refresh with invalid token - should fail."""
         mock_settings = Mock()
-        mock_settings.remote_reload_token = self.TEST_TOKEN
+        mock_settings.admin_token = self.TEST_TOKEN
         with self.override_settings(client, mock_settings):
             response = client.post(
-                "/api/v1/admin/refresh_remotes",
+                "/api/v1/admin/refresh_versions",
                 headers={"Authorization": "Bearer invalid-token-xyz"}
             )
 
@@ -79,31 +79,31 @@ class TestAdminRefreshRemotesEndpoint:
         assert "detail" in data
         assert "Invalid authentication token" in data["detail"]
 
-    def test_refresh_remotes_malformed_auth_header(self, client):
+    def test_refresh_versions_malformed_auth_header(self, client):
         """Test refresh with malformed authorization header."""
         mock_settings = Mock()
-        mock_settings.remote_reload_token = self.TEST_TOKEN
+        mock_settings.admin_token = self.TEST_TOKEN
         with self.override_settings(client, mock_settings):
             response = client.post(
-                "/api/v1/admin/refresh_remotes",
-                headers={"Authorization": "test-remote-reload-token-12345"}
+                "/api/v1/admin/refresh_versions",
+                headers={"Authorization": "test-admin-token-12345"}
             )
 
         assert response.status_code == status.HTTP_401_UNAUTHORIZED
 
-    def test_refresh_remotes_empty_token(self, client):
+    def test_refresh_versions_empty_token(self, client):
         """Test refresh with empty token."""
         mock_settings = Mock()
-        mock_settings.remote_reload_token = self.TEST_TOKEN
+        mock_settings.admin_token = self.TEST_TOKEN
         with self.override_settings(client, mock_settings):
             response = client.post(
-                "/api/v1/admin/refresh_remotes",
+                "/api/v1/admin/refresh_versions",
                 headers={"Authorization": "Bearer "}
             )
 
         assert response.status_code == status.HTTP_401_UNAUTHORIZED
 
-    def test_refresh_remotes_method_not_allowed(self, client):
+    def test_refresh_versions_method_not_allowed(self, client):
         """Test that only POST method is allowed."""
         disallowed_methods = [
             ("GET", client.get),
@@ -113,11 +113,11 @@ class TestAdminRefreshRemotesEndpoint:
         ]
 
         mock_settings = Mock()
-        mock_settings.remote_reload_token = self.TEST_TOKEN
+        mock_settings.admin_token = self.TEST_TOKEN
         with self.override_settings(client, mock_settings):
             for method_name, method_func in disallowed_methods:
                 response = method_func(
-                    "/api/v1/admin/refresh_remotes",
+                    "/api/v1/admin/refresh_versions",
                     headers=self.AUTH_HEADERS
                 )
                 assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED, \

+ 17 - 17
tests/web/test_admin_service.py

@@ -12,46 +12,46 @@ class TestAdminService:
     @pytest.mark.asyncio
     async def test_verify_token_success(self):
         """Test successful token verification."""
-        admin_service = AdminService(remote_reload_token="valid-token")
-        result = await admin_service.verify_remote_reload_token("valid-token")
+        admin_service = AdminService(admin_token="valid-token")
+        result = await admin_service.verify_admin_token("valid-token")
         assert result is True
 
     @pytest.mark.asyncio
     async def test_verify_token_failure(self):
         """Test token verification with incorrect token."""
-        admin_service = AdminService(remote_reload_token="valid-token")
-        result = await admin_service.verify_remote_reload_token("invalid-token")
+        admin_service = AdminService(admin_token="valid-token")
+        result = await admin_service.verify_admin_token("invalid-token")
         assert result is False
 
     @pytest.mark.asyncio
-    async def test_refresh_remotes_success(self, mock_versions_fetcher):
-        """Test successful refresh of remote metadata."""
+    async def test_refresh_versions_success(self, mock_versions_manager):
+        """Test successful refresh of version metadata."""
         admin_service = AdminService(
-            remote_reload_token="some-token",
-            versions_fetcher=mock_versions_fetcher
+            admin_token="some-token",
+            versions_manager=mock_versions_manager
         )
-        remotes = await admin_service.refresh_remotes()
+        remotes = await admin_service.refresh_versions()
 
         assert len(remotes) == 2
         assert "test-remote-1" in remotes
         assert "test-remote-2" in remotes
 
-        mock_versions_fetcher.reload_remotes_json.assert_called_once()
-        mock_versions_fetcher.get_all_remotes_info.assert_called_once()
+        mock_versions_manager.refresh_all.assert_called_once()
+        mock_versions_manager.get_all_remotes_info.assert_called_once()
 
     @pytest.mark.asyncio
-    async def test_refresh_remotes_empty_result(self):
+    async def test_refresh_versions_empty_result(self):
         """Test refresh when no remotes are configured."""
         mock_fetcher = Mock()
-        mock_fetcher.reload_remotes_json = Mock()
+        mock_fetcher.refresh_all = Mock()
         mock_fetcher.get_all_remotes_info = Mock(return_value=[])
 
         admin_service = AdminService(
-            remote_reload_token="some-token",
-            versions_fetcher=mock_fetcher
+            admin_token="some-token",
+            versions_manager=mock_fetcher
         )
 
-        remotes = await admin_service.refresh_remotes()
+        remotes = await admin_service.refresh_versions()
 
         assert len(remotes) == 0
-        mock_fetcher.reload_remotes_json.assert_called_once()
+        mock_fetcher.refresh_all.assert_called_once()

+ 12 - 13
tests/web/test_builds_service.py

@@ -7,7 +7,7 @@ from unittest.mock import Mock, MagicMock
 
 import build_manager as bm
 from metadata_manager import RemoteInfo as ManagerRemoteInfo
-from metadata_manager.versions_fetcher import RemoteInfo, VersionInfo
+from metadata_manager import RemoteInfo, VersionInfo
 from web.schemas import BuildRequest
 from web.services.builds import BuildsService
 
@@ -15,14 +15,14 @@ from web.services.builds import BuildsService
 @pytest.fixture
 def service(
     mock_build_manager,
-    mock_versions_fetcher,
+    mock_versions_manager,
     mock_ap_src_metadata_fetcher,
     mock_git_repo,
     mock_vehicles_manager,
 ):
     """Create a BuildsService instance with mocked dependencies."""
-    mock_versions_fetcher.get_version_info.return_value = make_version_info()
-    mock_versions_fetcher.get_remote_info.return_value = RemoteInfo(
+    mock_versions_manager.get_version_info.return_value = make_version_info()
+    mock_versions_manager.get_remote_info.return_value = RemoteInfo(
         name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"
     )
     mock_ap_src_metadata_fetcher.get_boards.return_value = ["MatekH743", "CubeOrange"]
@@ -37,11 +37,10 @@ def service(
     mock_vehicles_manager.get_vehicle_by_id = Mock(
         side_effect=lambda vid: vehicles.get(vid)
     )
-    mock_vehicles_manager.get_vehicle_names = Mock(return_value=[v.name for v in vehicles.values()])
 
     return BuildsService(
         build_manager=mock_build_manager,
-        versions_fetcher=mock_versions_fetcher,
+        versions_manager=mock_versions_manager,
         ap_src_metadata_fetcher=mock_ap_src_metadata_fetcher,
         repo=mock_git_repo,
         vehicles_manager=mock_vehicles_manager,
@@ -179,10 +178,10 @@ class TestBuildsService:
             service.create_build(request)
 
     def test_create_build_raises_value_error_for_invalid_version(
-        self, service, mock_versions_fetcher
+        self, service, mock_versions_manager
     ):
         """ValueError is raised when the version_id is not found."""
-        mock_versions_fetcher.get_version_info.return_value = None
+        mock_versions_manager.get_version_info.return_value = None
         request = BuildRequest(
             vehicle_id="copter",
             board_id="MatekH743",
@@ -194,10 +193,10 @@ class TestBuildsService:
             service.create_build(request)
 
     def test_create_build_queries_version_info_with_correct_params(
-        self, service, mock_versions_fetcher
+        self, service, mock_versions_manager
     ):
         """get_version_info is called with the correct vehicle_id and version_id."""
-        mock_versions_fetcher.get_version_info.return_value = None
+        mock_versions_manager.get_version_info.return_value = None
         request = BuildRequest(
             vehicle_id="plane",
             board_id="CubeOrange",
@@ -208,16 +207,16 @@ class TestBuildsService:
         with pytest.raises(ValueError):
             service.create_build(request)
 
-        mock_versions_fetcher.get_version_info.assert_called_once_with(
+        mock_versions_manager.get_version_info.assert_called_once_with(
             vehicle_id="plane",
             version_id="plane-4.4.0-stable",
         )
 
     def test_create_build_raises_value_error_when_remote_not_found(
-        self, service, mock_versions_fetcher
+        self, service, mock_versions_manager
     ):
         """ValueError is raised when the remote is not found."""
-        mock_versions_fetcher.get_remote_info.return_value = None
+        mock_versions_manager.get_remote_info.return_value = None
         request = BuildRequest(
             vehicle_id="some-vehicle",
             board_id="some-board",

+ 14 - 14
tests/web/test_config.py

@@ -44,27 +44,27 @@ class TestSettings:
             assert settings.enable_inbuilt_builder is False
 
 
-class TestRemoteReloadToken:
-    """Test suite for remote_reload_token property."""
+class TestAdminToken:
+    """Test suite for admin_token property."""
 
     def test_token_from_file(self, tmp_path):
         """Test that token is read from file when it exists."""
         secrets_dir = tmp_path / 'secrets'
         secrets_dir.mkdir()
-        token_file = secrets_dir / 'reload_token'
+        token_file = secrets_dir / 'admin_token'
 
         expected_token = "test-token-from-file"
         token_file.write_text(f"  {expected_token}  \n")  # Test whitespace stripping
 
         with patch.dict(os.environ, {"CBS_BASEDIR": str(tmp_path)}, clear=True):
             settings = Settings()
-            assert settings.remote_reload_token == expected_token
+            assert settings.admin_token == expected_token
 
     def test_token_file_takes_precedence_over_env(self, tmp_path):
         """Test that token from file takes precedence over environment variable."""
         secrets_dir = tmp_path / 'secrets'
         secrets_dir.mkdir()
-        token_file = secrets_dir / 'reload_token'
+        token_file = secrets_dir / 'admin_token'
 
         file_token = "token-from-file"
         env_token = "token-from-env"
@@ -73,10 +73,10 @@ class TestRemoteReloadToken:
 
         with patch.dict(os.environ, {
             "CBS_BASEDIR": str(tmp_path),
-            "CBS_REMOTES_RELOAD_TOKEN": env_token
+            "CBS_ADMIN_TOKEN": env_token
         }, clear=True):
             settings = Settings()
-            assert settings.remote_reload_token == file_token
+            assert settings.admin_token == file_token
 
     def test_token_from_env_when_file_not_found(self, tmp_path):
         """Test that token falls back to environment variable when file doesn't exist."""
@@ -84,10 +84,10 @@ class TestRemoteReloadToken:
 
         with patch.dict(os.environ, {
             "CBS_BASEDIR": str(tmp_path),
-            "CBS_REMOTES_RELOAD_TOKEN": expected_token
+            "CBS_ADMIN_TOKEN": expected_token
         }, clear=True):
             settings = Settings()
-            assert settings.remote_reload_token == expected_token
+            assert settings.admin_token == expected_token
 
     def test_token_from_env_on_file_read_error(self, tmp_path):
         """Test that token falls back to env var when file cannot be read."""
@@ -95,23 +95,23 @@ class TestRemoteReloadToken:
 
         with patch.dict(os.environ, {
             "CBS_BASEDIR": str(tmp_path),
-            "CBS_REMOTES_RELOAD_TOKEN": env_token
+            "CBS_ADMIN_TOKEN": env_token
         }, clear=True):
             with patch("builtins.open", side_effect=PermissionError("No access")):
                 settings = Settings()
-                assert settings.remote_reload_token == env_token
+                assert settings.admin_token == env_token
 
     def test_token_none_when_not_configured(self, tmp_path):
         """Test that token is None when neither file nor env var is set."""
         with patch.dict(os.environ, {"CBS_BASEDIR": str(tmp_path)}, clear=True):
             settings = Settings()
-            assert settings.remote_reload_token is None
+            assert settings.admin_token is None
 
     def test_token_none_when_env_is_empty_string(self, tmp_path):
         """Test that token is None when env var is empty string."""
         with patch.dict(os.environ, {
             "CBS_BASEDIR": str(tmp_path),
-            "CBS_REMOTES_RELOAD_TOKEN": ""
+            "CBS_ADMIN_TOKEN": ""
         }, clear=True):
             settings = Settings()
-            assert settings.remote_reload_token is None
+            assert settings.admin_token is None

+ 99 - 103
tests/web/test_vehicles_service.py

@@ -4,16 +4,19 @@ Tests for the Vehicles Service.
 import pytest
 from unittest.mock import Mock
 
-from metadata_manager import Vehicle
-from metadata_manager.versions_fetcher import VersionInfo, RemoteInfo
+from metadata_manager import (
+    RemoteInfo,
+    Vehicle,
+    VersionInfo,
+)
 from web.services.vehicles import VehiclesService
 
 
 @pytest.fixture
-def service(mock_vehicles_manager, mock_versions_fetcher, mock_ap_src_metadata_fetcher, mock_git_repo):
+def service(mock_vehicles_manager, mock_versions_manager, mock_ap_src_metadata_fetcher, mock_git_repo):
     return VehiclesService(
         vehicle_manager=mock_vehicles_manager,
-        versions_fetcher=mock_versions_fetcher,
+        versions_manager=mock_versions_manager,
         ap_src_metadata_fetcher=mock_ap_src_metadata_fetcher,
         repo=mock_git_repo,
     )
@@ -31,14 +34,12 @@ class TestVehiclesService:
                 id="copter",
                 name="Copter",
                 ap_source_subdir="ArduCopter",
-                fw_server_vehicle_sdir="Copter",
                 waf_build_command="copter"
             ),
             Vehicle(
                 id="plane",
                 name="Plane",
                 ap_source_subdir="ArduPlane",
-                fw_server_vehicle_sdir="Plane",
                 waf_build_command="plane"
             ),
         ]
@@ -64,7 +65,6 @@ class TestVehiclesService:
                 id="copter",
                 name="Copter",
                 ap_source_subdir="ArduCopter",
-                fw_server_vehicle_sdir="Copter",
                 waf_build_command="copter"
             ),
         ]
@@ -80,21 +80,18 @@ class TestVehiclesService:
                 id="plane",
                 name="Plane",
                 ap_source_subdir="ArduPlane",
-                fw_server_vehicle_sdir="Plane",
                 waf_build_command="plane"
             ),
             Vehicle(
                 id="copter",
                 name="Copter",
                 ap_source_subdir="ArduCopter",
-                fw_server_vehicle_sdir="Copter",
                 waf_build_command="copter"
             ),
             Vehicle(
                 id="rover",
                 name="Rover",
                 ap_source_subdir="ArduRover",
-                fw_server_vehicle_sdir="Rover",
                 waf_build_command="rover"
             ),
         ]
@@ -118,7 +115,6 @@ class TestVehiclesService:
             id="copter",
             name="Copter",
             ap_source_subdir="ArduCopter",
-            fw_server_vehicle_sdir="Copter",
             waf_build_command="copter"
         )
         vehicle = service.get_vehicle("copter")
@@ -143,16 +139,16 @@ class TestVehiclesService:
 
     # Tests for get_versions
 
-    def test_get_versions_empty(self, service, mock_versions_fetcher):
+    def test_get_versions_empty(self, service, mock_versions_manager):
         """Test that an empty list is returned when no versions exist."""
-        mock_versions_fetcher.get_versions_for_vehicle.return_value = []
+        mock_versions_manager.get_versions_for_vehicle.return_value = []
         versions = service.get_versions("copter")
 
         assert versions == []
 
-    def test_get_versions_single(self, service, mock_versions_fetcher):
+    def test_get_versions_single(self, service, mock_versions_manager):
         """Test fetching versions when only one version exists."""
-        mock_versions_fetcher.get_versions_for_vehicle.return_value = [
+        mock_versions_manager.get_versions_for_vehicle.return_value = [
             VersionInfo(
                 remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
                 commit_ref="refs/heads/master",
@@ -165,9 +161,9 @@ class TestVehiclesService:
 
         assert len(versions) == 1
 
-    def test_get_versions_many(self, service, mock_versions_fetcher):
+    def test_get_versions_many(self, service, mock_versions_manager):
         """Test fetching versions when multiple versions exist."""
-        mock_versions_fetcher.get_versions_for_vehicle.return_value = [
+        mock_versions_manager.get_versions_for_vehicle.return_value = [
             VersionInfo(
                 remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
                 commit_ref="refs/heads/master",
@@ -194,9 +190,9 @@ class TestVehiclesService:
 
         assert len(versions) == 3
 
-    def test_get_versions_sorted_by_name(self, service, mock_versions_fetcher):
+    def test_get_versions_sorted_by_name(self, service, mock_versions_manager):
         """Test that versions are returned sorted by their display name."""
-        mock_versions_fetcher.get_versions_for_vehicle.return_value = [
+        mock_versions_manager.get_versions_for_vehicle.return_value = [
             VersionInfo(
                 remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
                 commit_ref="refs/tags/Copter-4.5.0",
@@ -225,21 +221,21 @@ class TestVehiclesService:
         assert names == sorted(names)
 
     def test_get_versions_calls_fetcher_once_with_correct_vehicle_id(
-        self, service, mock_versions_fetcher
+        self, service, mock_versions_manager
     ):
         """Test that get_versions calls the fetcher exactly once with the correct vehicle_id."""
-        mock_versions_fetcher.get_versions_for_vehicle.return_value = []
+        mock_versions_manager.get_versions_for_vehicle.return_value = []
         service.get_versions("copter")
 
-        mock_versions_fetcher.get_versions_for_vehicle.assert_called_once_with(
+        mock_versions_manager.get_versions_for_vehicle.assert_called_once_with(
             vehicle_id="copter"
         )
 
     def test_get_versions_type_filter_keeps_matching(
-        self, service, mock_versions_fetcher
+        self, service, mock_versions_manager
     ):
         """Test that type_filter returns only versions of the specified type."""
-        mock_versions_fetcher.get_versions_for_vehicle.return_value = [
+        mock_versions_manager.get_versions_for_vehicle.return_value = [
             VersionInfo(
                 remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
                 commit_ref="refs/tags/Copter-4.5.0",
@@ -268,10 +264,10 @@ class TestVehiclesService:
         assert versions[0].type == "stable"
 
     def test_get_versions_type_filter_excludes_non_matching(
-        self, service, mock_versions_fetcher
+        self, service, mock_versions_manager
     ):
         """Test that type_filter excludes versions that do not match."""
-        mock_versions_fetcher.get_versions_for_vehicle.return_value = [
+        mock_versions_manager.get_versions_for_vehicle.return_value = [
             VersionInfo(
                 remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
                 commit_ref="refs/tags/Copter-4.5.0",
@@ -292,10 +288,10 @@ class TestVehiclesService:
         assert versions == []
 
     def test_get_versions_type_filter_none_returns_all(
-        self, service, mock_versions_fetcher
+        self, service, mock_versions_manager
     ):
         """Test that passing no type_filter returns all versions."""
-        mock_versions_fetcher.get_versions_for_vehicle.return_value = [
+        mock_versions_manager.get_versions_for_vehicle.return_value = [
             VersionInfo(
                 remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
                 commit_ref="refs/tags/Copter-4.5.0",
@@ -323,10 +319,10 @@ class TestVehiclesService:
         assert len(versions) == 3
 
     def test_get_versions_type_filter_multiple_matches(
-        self, service, mock_versions_fetcher
+        self, service, mock_versions_manager
     ):
         """Test that type_filter returns all versions matching the type when there are multiple."""
-        mock_versions_fetcher.get_versions_for_vehicle.return_value = [
+        mock_versions_manager.get_versions_for_vehicle.return_value = [
             VersionInfo(
                 remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
                 commit_ref="refs/tags/Copter-4.4.0",
@@ -355,10 +351,10 @@ class TestVehiclesService:
         assert all(v.type == "stable" for v in versions)
 
     def test_get_versions_latest_name_format(
-        self, service, mock_versions_fetcher
+        self, service, mock_versions_manager
     ):
         """Test that latest versions have the correct display name format."""
-        mock_versions_fetcher.get_versions_for_vehicle.return_value = [
+        mock_versions_manager.get_versions_for_vehicle.return_value = [
             VersionInfo(
                 remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
                 commit_ref="refs/heads/master",
@@ -372,10 +368,10 @@ class TestVehiclesService:
         assert versions[0].name == "Latest (ardupilot)"
 
     def test_get_versions_non_latest_name_format(
-        self, service, mock_versions_fetcher
+        self, service, mock_versions_manager
     ):
         """Test that non-latest versions have the correct display name format."""
-        mock_versions_fetcher.get_versions_for_vehicle.return_value = [
+        mock_versions_manager.get_versions_for_vehicle.return_value = [
             VersionInfo(
                 remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
                 commit_ref="refs/tags/Copter-4.5.0",
@@ -390,7 +386,7 @@ class TestVehiclesService:
 
     # Tests for get_version
 
-    def test_get_version_found(self, service, mock_versions_fetcher):
+    def test_get_version_found(self, service, mock_versions_manager):
         """Test that the correct version is returned when it exists."""
         version_info = VersionInfo(
             remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
@@ -399,16 +395,16 @@ class TestVehiclesService:
             version_number="4.5.0",
             ap_build_artifacts_url=None,
         )
-        mock_versions_fetcher.get_versions_for_vehicle.return_value = [version_info]
+        mock_versions_manager.get_versions_for_vehicle.return_value = [version_info]
 
         result = service.get_version("copter", version_info.version_id)
 
         assert result is not None
         assert result.id == version_info.version_id
 
-    def test_get_version_not_found(self, service, mock_versions_fetcher):
+    def test_get_version_not_found(self, service, mock_versions_manager):
         """Test that None is returned when the version does not exist."""
-        mock_versions_fetcher.get_versions_for_vehicle.return_value = [
+        mock_versions_manager.get_versions_for_vehicle.return_value = [
             VersionInfo(
                 remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
                 commit_ref="refs/tags/Copter-4.5.0",
@@ -422,15 +418,15 @@ class TestVehiclesService:
 
         assert result is None
 
-    def test_get_version_no_versions_available(self, service, mock_versions_fetcher):
+    def test_get_version_no_versions_available(self, service, mock_versions_manager):
         """Test that None is returned when there are no versions at all."""
-        mock_versions_fetcher.get_versions_for_vehicle.return_value = []
+        mock_versions_manager.get_versions_for_vehicle.return_value = []
 
         result = service.get_version("copter", "any-version-id")
 
         assert result is None
 
-    def test_get_version_returns_correct_match_among_many(self, service, mock_versions_fetcher):
+    def test_get_version_returns_correct_match_among_many(self, service, mock_versions_manager):
         """Test that only the matching version is returned when multiple exist."""
         stable_info = VersionInfo(
             remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
@@ -453,7 +449,7 @@ class TestVehiclesService:
             version_number="NA",
             ap_build_artifacts_url=None,
         )
-        mock_versions_fetcher.get_versions_for_vehicle.return_value = [
+        mock_versions_manager.get_versions_for_vehicle.return_value = [
             stable_info, beta_info, latest_info,
         ]
 
@@ -465,29 +461,29 @@ class TestVehiclesService:
 
     # Tests for get_boards
 
-    def test_get_boards_version_not_found_returns_empty(self, service, mock_versions_fetcher):
+    def test_get_boards_version_not_found_returns_empty(self, service, mock_versions_manager):
         """Test that an empty list is returned when the version does not exist."""
-        mock_versions_fetcher.get_version_info.return_value = None
+        mock_versions_manager.get_version_info.return_value = None
 
         result = service.get_boards("copter", "nonexistent-version-id")
 
         assert result == []
 
     def test_get_boards_version_info_queried_with_correct_params(
-        self, service, mock_versions_fetcher
+        self, service, mock_versions_manager
     ):
         """Test that get_version_info is called with the correct vehicle and version IDs."""
-        mock_versions_fetcher.get_version_info.return_value = None
+        mock_versions_manager.get_version_info.return_value = None
 
         service.get_boards("copter", "some-version-id")
 
-        mock_versions_fetcher.get_version_info.assert_called_once_with(
+        mock_versions_manager.get_version_info.assert_called_once_with(
             vehicle_id="copter",
             version_id="some-version-id",
         )
 
     def test_get_boards_empty(
-        self, service, mock_versions_fetcher, mock_ap_src_metadata_fetcher
+        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
     ):
         """Test that an empty list is returned when there are no boards for a version."""
         version_info = VersionInfo(
@@ -497,7 +493,7 @@ class TestVehiclesService:
             version_number="4.5.0",
             ap_build_artifacts_url=None,
         )
-        mock_versions_fetcher.get_version_info.return_value = version_info
+        mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_boards.return_value = []
 
         result = service.get_boards("copter", version_info.version_id)
@@ -505,7 +501,7 @@ class TestVehiclesService:
         assert result == []
 
     def test_get_boards_single(
-        self, service, mock_versions_fetcher, mock_ap_src_metadata_fetcher
+        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
     ):
         """Test that a single board is returned correctly."""
         version_info = VersionInfo(
@@ -515,7 +511,7 @@ class TestVehiclesService:
             version_number="4.5.0",
             ap_build_artifacts_url=None,
         )
-        mock_versions_fetcher.get_version_info.return_value = version_info
+        mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_boards.return_value = ["CubeRed"]
 
         result = service.get_boards("copter", version_info.version_id)
@@ -525,7 +521,7 @@ class TestVehiclesService:
         assert result[0].name == "CubeRed"
 
     def test_get_boards_many(
-        self, service, mock_versions_fetcher, mock_ap_src_metadata_fetcher
+        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
     ):
         """Test that multiple boards are returned correctly."""
         version_info = VersionInfo(
@@ -535,7 +531,7 @@ class TestVehiclesService:
             version_number="4.5.0",
             ap_build_artifacts_url=None,
         )
-        mock_versions_fetcher.get_version_info.return_value = version_info
+        mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_boards.return_value = [
             "CubeRed", "CubeOrange", "MatekF405",
         ]
@@ -546,7 +542,7 @@ class TestVehiclesService:
         assert [b.id for b in result] == ["CubeRed", "CubeOrange", "MatekF405"]
 
     def test_get_boards_sets_correct_vehicle_and_version_ids(
-        self, service, mock_versions_fetcher, mock_ap_src_metadata_fetcher
+        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
     ):
         """Test that returned boards carry the correct vehicle_id and version_id."""
         version_info = VersionInfo(
@@ -556,7 +552,7 @@ class TestVehiclesService:
             version_number="4.5.0",
             ap_build_artifacts_url=None,
         )
-        mock_versions_fetcher.get_version_info.return_value = version_info
+        mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_boards.return_value = ["CubeRed"]
 
         result = service.get_boards("copter", version_info.version_id)
@@ -565,7 +561,7 @@ class TestVehiclesService:
         assert result[0].version_id == version_info.version_id
 
     def test_get_boards_fetcher_called_with_correct_params(
-        self, service, mock_versions_fetcher, mock_ap_src_metadata_fetcher
+        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
     ):
         """Test that the metadata fetcher is called with remote name, commit ref, and vehicle ID from version info."""
         version_info = VersionInfo(
@@ -575,7 +571,7 @@ class TestVehiclesService:
             version_number="4.5.0",
             ap_build_artifacts_url=None,
         )
-        mock_versions_fetcher.get_version_info.return_value = version_info
+        mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_boards.return_value = []
 
         service.get_boards("copter", version_info.version_id)
@@ -588,7 +584,7 @@ class TestVehiclesService:
 
     # Tests for get_board
 
-    def test_get_board_found(self, service, mock_versions_fetcher, mock_ap_src_metadata_fetcher):
+    def test_get_board_found(self, service, mock_versions_manager, mock_ap_src_metadata_fetcher):
         """Test that the correct board is returned when it exists."""
         version_info = VersionInfo(
             remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
@@ -597,7 +593,7 @@ class TestVehiclesService:
             version_number="4.5.0",
             ap_build_artifacts_url=None,
         )
-        mock_versions_fetcher.get_version_info.return_value = version_info
+        mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_boards.return_value = ["CubeRed", "CubeOrange"]
 
         result = service.get_board("copter", version_info.version_id, "CubeRed")
@@ -608,7 +604,7 @@ class TestVehiclesService:
         assert result.vehicle_id == "copter"
         assert result.version_id == version_info.version_id
 
-    def test_get_board_not_found(self, service, mock_versions_fetcher, mock_ap_src_metadata_fetcher):
+    def test_get_board_not_found(self, service, mock_versions_manager, mock_ap_src_metadata_fetcher):
         """Test that None is returned when the board does not exist."""
         version_info = VersionInfo(
             remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
@@ -617,7 +613,7 @@ class TestVehiclesService:
             version_number="4.5.0",
             ap_build_artifacts_url=None,
         )
-        mock_versions_fetcher.get_version_info.return_value = version_info
+        mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_boards.return_value = ["CubeRed", "CubeOrange"]
 
         result = service.get_board("copter", version_info.version_id, "NonExistentBoard")
@@ -625,7 +621,7 @@ class TestVehiclesService:
         assert result is None
 
     def test_get_board_returns_correct_match_among_many(
-        self, service, mock_versions_fetcher, mock_ap_src_metadata_fetcher
+        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
     ):
         """Test that only the matching board is returned when multiple boards exist."""
         version_info = VersionInfo(
@@ -635,7 +631,7 @@ class TestVehiclesService:
             version_number="4.5.0",
             ap_build_artifacts_url=None,
         )
-        mock_versions_fetcher.get_version_info.return_value = version_info
+        mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_boards.return_value = [
             "CubeRed", "CubeOrange", "MatekF405",
         ]
@@ -648,17 +644,17 @@ class TestVehiclesService:
     # Tests for get_features
 
     def test_get_features_version_not_found_returns_empty(
-        self, service, mock_versions_fetcher
+        self, service, mock_versions_manager
     ):
         """Test that an empty list is returned when the version does not exist."""
-        mock_versions_fetcher.get_version_info.return_value = None
+        mock_versions_manager.get_version_info.return_value = None
 
         result = service.get_features("copter", "nonexistent-version-id", "CubeRed")
 
         assert result == []
 
     def test_get_features_zero_options_returns_empty(
-        self, service, mock_versions_fetcher, mock_ap_src_metadata_fetcher
+        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
     ):
         """Test that an empty list is returned when there are no build options."""
         version_info = VersionInfo(
@@ -668,7 +664,7 @@ class TestVehiclesService:
             version_number="4.5.0",
             ap_build_artifacts_url=None,
         )
-        mock_versions_fetcher.get_version_info.return_value = version_info
+        mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = []
 
         result = service.get_features("copter", version_info.version_id, "CubeRed")
@@ -676,7 +672,7 @@ class TestVehiclesService:
         assert result == []
 
     def test_get_features_one_option(
-        self, service, mock_versions_fetcher, mock_ap_src_metadata_fetcher
+        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
     ):
         """Test that a single feature is returned correctly."""
         version_info = VersionInfo(
@@ -693,7 +689,7 @@ class TestVehiclesService:
         opt.description = ""
         opt.default = 1
         opt.dependency = None
-        mock_versions_fetcher.get_version_info.return_value = version_info
+        mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt]
         mock_ap_src_metadata_fetcher.get_board_defaults_from_fw_server.return_value = None
 
@@ -704,7 +700,7 @@ class TestVehiclesService:
         assert result[0].name == "HAL_LOGGING_ENABLED"
 
     def test_get_features_many_options(
-        self, service, mock_versions_fetcher, mock_ap_src_metadata_fetcher
+        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
     ):
         """Test that all features are returned when multiple options exist."""
         version_info = VersionInfo(
@@ -723,7 +719,7 @@ class TestVehiclesService:
         opt_sensors = Mock()
         opt_sensors.label, opt_sensors.define, opt_sensors.category = "HAL_BEACON_ENABLED", "HAL_BEACON_ENABLED", "Sensors"
         opt_sensors.description, opt_sensors.default, opt_sensors.dependency = "", 1, None
-        mock_versions_fetcher.get_version_info.return_value = version_info
+        mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt_logging, opt_ekf, opt_sensors]
         mock_ap_src_metadata_fetcher.get_board_defaults_from_fw_server.return_value = None
 
@@ -732,7 +728,7 @@ class TestVehiclesService:
         assert len(result) == 3
 
     def test_get_features_sorted_by_category(
-        self, service, mock_versions_fetcher, mock_ap_src_metadata_fetcher
+        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
     ):
         """Test that features are sorted by category name."""
         version_info = VersionInfo(
@@ -751,7 +747,7 @@ class TestVehiclesService:
         opt_m = Mock()
         opt_m.label, opt_m.define, opt_m.category = "FEATURE_M", "DEFINE_M", "Logging"
         opt_m.description, opt_m.default, opt_m.dependency = "", 1, None
-        mock_versions_fetcher.get_version_info.return_value = version_info
+        mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt_z, opt_a, opt_m]
         mock_ap_src_metadata_fetcher.get_board_defaults_from_fw_server.return_value = None
 
@@ -760,7 +756,7 @@ class TestVehiclesService:
         assert [f.category.name for f in result] == ["EKF", "Logging", "Sensors"]
 
     def test_get_features_uses_fallback_defaults_when_no_artifacts_url(
-        self, service, mock_versions_fetcher, mock_ap_src_metadata_fetcher
+        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
     ):
         """Test that build-options-py defaults are used when ap_build_artifacts_url is None."""
         version_info = VersionInfo(
@@ -776,7 +772,7 @@ class TestVehiclesService:
         opt_off = Mock()
         opt_off.label, opt_off.define, opt_off.category = "FEATURE_OFF", "DEFINE_OFF", "Cat"
         opt_off.description, opt_off.default, opt_off.dependency = "", 0, None
-        mock_versions_fetcher.get_version_info.return_value = version_info
+        mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt_on, opt_off]
 
         result = service.get_features("copter", version_info.version_id, "CubeRed")
@@ -789,7 +785,7 @@ class TestVehiclesService:
         mock_ap_src_metadata_fetcher.get_board_defaults_from_fw_server.assert_not_called()
 
     def test_get_features_uses_firmware_server_defaults_when_available(
-        self, service, mock_versions_fetcher, mock_ap_src_metadata_fetcher
+        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
     ):
         """Test that firmware-server defaults override build-options-py when present."""
         version_info = VersionInfo(
@@ -805,7 +801,7 @@ class TestVehiclesService:
         opt_b = Mock()
         opt_b.label, opt_b.define, opt_b.category = "FEATURE_B", "DEFINE_B", "Cat"
         opt_b.description, opt_b.default, opt_b.dependency = "", 1, None
-        mock_versions_fetcher.get_version_info.return_value = version_info
+        mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt_a, opt_b]
         # firmware server says DEFINE_A is disabled, DEFINE_B is enabled
         mock_ap_src_metadata_fetcher.get_board_defaults_from_fw_server.return_value = {
@@ -822,7 +818,7 @@ class TestVehiclesService:
         assert by_id["FEATURE_B"].source == "firmware-server"
 
     def test_get_features_falls_back_to_defaults_when_firmware_server_returns_none(
-        self, service, mock_versions_fetcher, mock_ap_src_metadata_fetcher
+        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
     ):
         """Test that build-options-py fallback is used when firmware server fetch fails."""
         version_info = VersionInfo(
@@ -835,7 +831,7 @@ class TestVehiclesService:
         opt = Mock()
         opt.label, opt.define, opt.category = "FEATURE_A", "DEFINE_A", "Cat"
         opt.description, opt.default, opt.dependency = "", 1, None
-        mock_versions_fetcher.get_version_info.return_value = version_info
+        mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt]
         mock_ap_src_metadata_fetcher.get_board_defaults_from_fw_server.return_value = None
 
@@ -845,7 +841,7 @@ class TestVehiclesService:
         assert result[0].default.source == "build-options-py"
 
     def test_get_features_firmware_server_overrides_only_known_defines(
-        self, service, mock_versions_fetcher, mock_ap_src_metadata_fetcher
+        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
     ):
         """Test that a define absent from firmware-server data falls back to build-options-py."""
         version_info = VersionInfo(
@@ -861,7 +857,7 @@ class TestVehiclesService:
         opt_unknown = Mock()
         opt_unknown.label, opt_unknown.define, opt_unknown.category = "FEATURE_UNKNOWN", "DEFINE_UNKNOWN", "Cat"
         opt_unknown.description, opt_unknown.default, opt_unknown.dependency = "", 1, None
-        mock_versions_fetcher.get_version_info.return_value = version_info
+        mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt_known, opt_unknown]
         # firmware server only knows about DEFINE_KNOWN
         mock_ap_src_metadata_fetcher.get_board_defaults_from_fw_server.return_value = {
@@ -877,7 +873,7 @@ class TestVehiclesService:
         assert by_id["FEATURE_UNKNOWN"].source == "build-options-py"
 
     def test_get_features_dependency_none(
-        self, service, mock_versions_fetcher, mock_ap_src_metadata_fetcher
+        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
     ):
         """Test that a feature with no dependency produces an empty dependencies list."""
         version_info = VersionInfo(
@@ -890,7 +886,7 @@ class TestVehiclesService:
         opt = Mock()
         opt.label, opt.define, opt.category = "FEATURE_A", "DEFINE_A", "Cat"
         opt.description, opt.default, opt.dependency = "", 1, None
-        mock_versions_fetcher.get_version_info.return_value = version_info
+        mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt]
         mock_ap_src_metadata_fetcher.get_board_defaults_from_fw_server.return_value = None
 
@@ -899,7 +895,7 @@ class TestVehiclesService:
         assert result[0].dependencies == []
 
     def test_get_features_dependency_single(
-        self, service, mock_versions_fetcher, mock_ap_src_metadata_fetcher
+        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
     ):
         """Test that a single dependency string is parsed into a one-element list."""
         version_info = VersionInfo(
@@ -912,7 +908,7 @@ class TestVehiclesService:
         opt = Mock()
         opt.label, opt.define, opt.category = "FEATURE_A", "DEFINE_A", "Cat"
         opt.description, opt.default, opt.dependency = "", 1, "DEP_ONE"
-        mock_versions_fetcher.get_version_info.return_value = version_info
+        mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt]
         mock_ap_src_metadata_fetcher.get_board_defaults_from_fw_server.return_value = None
 
@@ -921,7 +917,7 @@ class TestVehiclesService:
         assert result[0].dependencies == ["DEP_ONE"]
 
     def test_get_features_dependency_multiple_comma_separated(
-        self, service, mock_versions_fetcher, mock_ap_src_metadata_fetcher
+        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
     ):
         """Test that a comma-separated dependency string is split into multiple entries."""
         version_info = VersionInfo(
@@ -934,7 +930,7 @@ class TestVehiclesService:
         opt = Mock()
         opt.label, opt.define, opt.category = "FEATURE_A", "DEFINE_A", "Cat"
         opt.description, opt.default, opt.dependency = "", 1, "DEP_ONE,DEP_TWO,DEP_THREE"
-        mock_versions_fetcher.get_version_info.return_value = version_info
+        mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt]
         mock_ap_src_metadata_fetcher.get_board_defaults_from_fw_server.return_value = None
 
@@ -943,7 +939,7 @@ class TestVehiclesService:
         assert result[0].dependencies == ["DEP_ONE", "DEP_TWO", "DEP_THREE"]
 
     def test_get_features_dependency_with_spaces(
-        self, service, mock_versions_fetcher, mock_ap_src_metadata_fetcher
+        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
     ):
         """Test that spaces around dependency labels are stripped."""
         version_info = VersionInfo(
@@ -956,7 +952,7 @@ class TestVehiclesService:
         opt = Mock()
         opt.label, opt.define, opt.category = "FEATURE_A", "DEFINE_A", "Cat"
         opt.description, opt.default, opt.dependency = "", 1, "DEP_ONE , DEP_TWO , DEP_THREE"
-        mock_versions_fetcher.get_version_info.return_value = version_info
+        mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt]
         mock_ap_src_metadata_fetcher.get_board_defaults_from_fw_server.return_value = None
 
@@ -965,7 +961,7 @@ class TestVehiclesService:
         assert result[0].dependencies == ["DEP_ONE", "DEP_TWO", "DEP_THREE"]
 
     def test_get_features_ids_filled_correctly(
-        self, service, mock_versions_fetcher, mock_ap_src_metadata_fetcher
+        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
     ):
         """Test that vehicle_id, version_id, and board_id are correctly set on each feature."""
         version_info = VersionInfo(
@@ -978,7 +974,7 @@ class TestVehiclesService:
         opt = Mock()
         opt.label, opt.define, opt.category = "FEATURE_A", "DEFINE_A", "Cat"
         opt.description, opt.default, opt.dependency = "", 1, None
-        mock_versions_fetcher.get_version_info.return_value = version_info
+        mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt]
         mock_ap_src_metadata_fetcher.get_board_defaults_from_fw_server.return_value = None
 
@@ -989,7 +985,7 @@ class TestVehiclesService:
         assert result[0].board_id == "CubeRed"
 
     def test_get_features_category_filter_keeps_matching(
-        self, service, mock_versions_fetcher, mock_ap_src_metadata_fetcher
+        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
     ):
         """Test that category_id filter returns only features whose category matches."""
         version_info = VersionInfo(
@@ -1008,7 +1004,7 @@ class TestVehiclesService:
         opt_sensors = Mock()
         opt_sensors.label, opt_sensors.define, opt_sensors.category = "HAL_BEACON_ENABLED", "HAL_BEACON_ENABLED", "Sensors"
         opt_sensors.description, opt_sensors.default, opt_sensors.dependency = "", 1, None
-        mock_versions_fetcher.get_version_info.return_value = version_info
+        mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt_logging, opt_ekf, opt_sensors]
         mock_ap_src_metadata_fetcher.get_board_defaults_from_fw_server.return_value = None
 
@@ -1019,7 +1015,7 @@ class TestVehiclesService:
         assert result[0].category.name == "Logging"
 
     def test_get_features_category_filter_excludes_non_matching(
-        self, service, mock_versions_fetcher, mock_ap_src_metadata_fetcher
+        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
     ):
         """Test that category_id filter excludes features whose category does not match."""
         version_info = VersionInfo(
@@ -1035,7 +1031,7 @@ class TestVehiclesService:
         opt_ekf = Mock()
         opt_ekf.label, opt_ekf.define, opt_ekf.category = "HAL_NAVEKF3_AVAILABLE", "HAL_NAVEKF3_AVAILABLE", "EKF"
         opt_ekf.description, opt_ekf.default, opt_ekf.dependency = "", 1, None
-        mock_versions_fetcher.get_version_info.return_value = version_info
+        mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt_logging, opt_ekf]
         mock_ap_src_metadata_fetcher.get_board_defaults_from_fw_server.return_value = None
 
@@ -1044,7 +1040,7 @@ class TestVehiclesService:
         assert result == []
 
     def test_get_features_category_filter_no_matches_returns_empty(
-        self, service, mock_versions_fetcher, mock_ap_src_metadata_fetcher
+        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
     ):
         """Test that a category_id with no matching features returns an empty list."""
         version_info = VersionInfo(
@@ -1060,7 +1056,7 @@ class TestVehiclesService:
         opt_b = Mock()
         opt_b.label, opt_b.define, opt_b.category = "FEATURE_B", "DEFINE_B", "Logging"
         opt_b.description, opt_b.default, opt_b.dependency = "", 1, None
-        mock_versions_fetcher.get_version_info.return_value = version_info
+        mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt_a, opt_b]
         mock_ap_src_metadata_fetcher.get_board_defaults_from_fw_server.return_value = None
 
@@ -1071,7 +1067,7 @@ class TestVehiclesService:
     # Tests for get_feature
 
     def test_get_feature_found(
-        self, service, mock_versions_fetcher, mock_ap_src_metadata_fetcher
+        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
     ):
         """Test that the correct feature is returned when it exists."""
         version_info = VersionInfo(
@@ -1084,7 +1080,7 @@ class TestVehiclesService:
         opt = Mock()
         opt.label, opt.define, opt.category = "HAL_LOGGING_ENABLED", "HAL_LOGGING_ENABLED", "Logging"
         opt.description, opt.default, opt.dependency = "", 1, None
-        mock_versions_fetcher.get_version_info.return_value = version_info
+        mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt]
         mock_ap_src_metadata_fetcher.get_board_defaults_from_fw_server.return_value = None
 
@@ -1095,7 +1091,7 @@ class TestVehiclesService:
         assert result.name == "HAL_LOGGING_ENABLED"
 
     def test_get_feature_not_found(
-        self, service, mock_versions_fetcher, mock_ap_src_metadata_fetcher
+        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
     ):
         """Test that None is returned when the feature does not exist."""
         version_info = VersionInfo(
@@ -1108,7 +1104,7 @@ class TestVehiclesService:
         opt = Mock()
         opt.label, opt.define, opt.category = "HAL_LOGGING_ENABLED", "HAL_LOGGING_ENABLED", "Logging"
         opt.description, opt.default, opt.dependency = "", 1, None
-        mock_versions_fetcher.get_version_info.return_value = version_info
+        mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt]
         mock_ap_src_metadata_fetcher.get_board_defaults_from_fw_server.return_value = None
 
@@ -1117,7 +1113,7 @@ class TestVehiclesService:
         assert result is None
 
     def test_get_feature_returns_correct_match_among_many(
-        self, service, mock_versions_fetcher, mock_ap_src_metadata_fetcher
+        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
     ):
         """Test that only the matching feature is returned when multiple features exist."""
         version_info = VersionInfo(
@@ -1136,7 +1132,7 @@ class TestVehiclesService:
         opt_c = Mock()
         opt_c.label, opt_c.define, opt_c.category = "FEATURE_C", "DEFINE_C", "Cat"
         opt_c.description, opt_c.default, opt_c.dependency = "", 1, None
-        mock_versions_fetcher.get_version_info.return_value = version_info
+        mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt_a, opt_b, opt_c]
         mock_ap_src_metadata_fetcher.get_board_defaults_from_fw_server.return_value = None