diff --git a/tests/test_updater_with_simulator.py b/tests/test_updater_with_simulator.py index 25d536f456..e15df5d2f7 100644 --- a/tests/test_updater_with_simulator.py +++ b/tests/test_updater_with_simulator.py @@ -6,11 +6,13 @@ """Test ngclient Updater using the repository simulator. """ +import builtins import os import sys import tempfile import unittest from typing import Optional, Tuple +from unittest.mock import MagicMock, Mock, patch from tests import utils from tests.repository_simulator import RepositorySimulator @@ -230,6 +232,32 @@ def test_snapshot_rollback_with_local_snapshot_hash_mismatch(self): with self.assertRaises(BadVersionNumberError): self._run_refresh() + @patch.object(builtins, "open", wraps=builtins.open) + def test_not_loading_targets_twice(self, wrapped_open: MagicMock): + # Do not load targets roles more than once when traversing + # the delegations tree + + # Add new delegated targets, update the snapshot + spec_version = ".".join(SPECIFICATION_VERSION) + targets = Targets(1, spec_version, self.sim.safe_expiry, {}, None) + self.sim.add_delegation("targets", "role1", targets, False, ["*"], None) + self.sim.update_snapshot() + + # Run refresh, top-level roles are loaded + updater = self._run_refresh() + # Clean up calls to open during refresh() + wrapped_open.reset_mock() + + # First time looking for "somepath", only 'role1' must be loaded + updater.get_targetinfo("somepath") + wrapped_open.assert_called_once_with( + os.path.join(self.metadata_dir, "role1.json"), "rb" + ) + wrapped_open.reset_mock() + # Second call to get_targetinfo, all metadata is already loaded + updater.get_targetinfo("somepath") + wrapped_open.assert_not_called() + if __name__ == "__main__": if "--dump" in sys.argv: diff --git a/tuf/ngclient/updater.py b/tuf/ngclient/updater.py index 4e9aa6ecc2..649a2b4bc3 100644 --- a/tuf/ngclient/updater.py +++ b/tuf/ngclient/updater.py @@ -370,6 +370,11 @@ def _load_snapshot(self) -> None: def _load_targets(self, role: str, parent_role: str) -> Metadata[Targets]: """Load local (and if needed remote) metadata for 'role'.""" + + # Avoid loading 'role' more than once during "get_targetinfo" + if role in self._trusted_set: + return self._trusted_set[role] + try: data = self._load_local_metadata(role) delegated_targets = self._trusted_set.update_delegated_targets(