Skip to content

Commit 15e458c

Browse files
committed
ngtests: Add consistent_snapshot tests
Add tests for ngclient.Updater toggling 'consitent_snapshot' and 'prefix_targets_with_hash'. Signed-off-by: Teodora Sechkova <[email protected]>
1 parent 993c2e4 commit 15e458c

File tree

1 file changed

+236
-0
lines changed

1 file changed

+236
-0
lines changed
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
#!/usr/bin/env python
2+
3+
# Copyright 2021, New York University and the TUF contributors
4+
# SPDX-License-Identifier: MIT OR Apache-2.0
5+
6+
"""Test ngclient Updater consistent snapshot"""
7+
8+
import os
9+
import sys
10+
import tempfile
11+
import unittest
12+
from typing import Any, Dict, Iterable, List, Optional
13+
from unittest.mock import call, patch
14+
15+
from tests import utils
16+
from tests.repository_simulator import RepositorySimulator
17+
from tuf.api.metadata import (
18+
SPECIFICATION_VERSION,
19+
TOP_LEVEL_ROLE_NAMES,
20+
Targets,
21+
)
22+
from tuf.ngclient import Updater
23+
24+
25+
class TestConsistentSnapshot(unittest.TestCase):
26+
"""Test different combinations of consistent_snapshot and
27+
prefix_targets_with_hash"""
28+
29+
def setUp(self) -> None:
30+
self.temp_dir = tempfile.TemporaryDirectory()
31+
self.metadata_dir = os.path.join(self.temp_dir.name, "metadata")
32+
self.targets_dir = os.path.join(self.temp_dir.name, "targets")
33+
os.mkdir(self.metadata_dir)
34+
os.mkdir(self.targets_dir)
35+
36+
def tearDown(self) -> None:
37+
self.temp_dir.cleanup()
38+
39+
def _init_repo(
40+
self, consistent_snapshot: bool, prefix_targets: bool = True
41+
) -> RepositorySimulator:
42+
"""Create a new RepositorySimulator instance"""
43+
sim = RepositorySimulator()
44+
sim.root.consistent_snapshot = consistent_snapshot
45+
sim.root.version += 1
46+
sim.publish_root()
47+
sim.prefix_targets_with_hash = prefix_targets
48+
49+
# Init trusted root with the latest consistent_snapshot
50+
with open(os.path.join(self.metadata_dir, "root.json"), "bw") as f:
51+
root = sim.download_bytes(
52+
"https://example.com/metadata/2.root.json", 100000
53+
)
54+
f.write(root)
55+
56+
return sim
57+
58+
def _init_updater(self, sim: RepositorySimulator) -> Updater:
59+
"""Create a new Updater instance"""
60+
return Updater(
61+
self.metadata_dir,
62+
"https://example.com/metadata/",
63+
self.targets_dir,
64+
"https://example.com/targets/",
65+
sim,
66+
)
67+
68+
@staticmethod
69+
def _cleanup_dir(path: str) -> None:
70+
"""Delete all files inside a directory"""
71+
for filepath in [
72+
os.path.join(path, filename) for filename in os.listdir(path)
73+
]:
74+
os.remove(filepath)
75+
76+
def _assert_metadata_files_exist(self, roles: Iterable[str]) -> None:
77+
"""Assert that local metadata files exist for 'roles'"""
78+
local_metadata_files = os.listdir(self.metadata_dir)
79+
for role in roles:
80+
self.assertIn(f"{role}.json", local_metadata_files)
81+
82+
def _assert_targets_files_exist(self, filenames: Iterable[str]) -> None:
83+
"""Assert that local files with 'filenames' exist"""
84+
local_target_files = os.listdir(self.targets_dir)
85+
for filename in filenames:
86+
self.assertIn(filename, local_target_files)
87+
88+
top_level_roles_data: utils.DataSet = {
89+
"consistent_snaphot disabled": {
90+
"consistent_snapshot": False,
91+
"calls": [
92+
call("root", 3),
93+
call("timestamp", None),
94+
call("snapshot", None),
95+
call("targets", None),
96+
],
97+
},
98+
"consistent_snaphot enabled": {
99+
"consistent_snapshot": True,
100+
"calls": [
101+
call("root", 3),
102+
call("timestamp", None),
103+
call("snapshot", 1),
104+
call("targets", 1),
105+
],
106+
},
107+
}
108+
109+
@utils.run_sub_tests_with_dataset(top_level_roles_data)
110+
def test_top_level_roles_update(self, test_case_data: Dict[str, Any]):
111+
# Test if the client fetches and stores metadata files with the
112+
# correct version prefix, depending on 'consistent_snapshot' config
113+
consistent_snapshot: bool = test_case_data["consistent_snapshot"]
114+
expected_calls: List[Any] = test_case_data["calls"]
115+
116+
sim = self._init_repo(consistent_snapshot)
117+
updater = self._init_updater(sim)
118+
119+
with patch.object(
120+
sim, "_fetch_metadata", wraps=sim._fetch_metadata
121+
) as wrapped_fetch_metadata:
122+
updater.refresh()
123+
124+
# metadata files are fetched with the expected version (or None)
125+
wrapped_fetch_metadata.assert_has_calls(expected_calls)
126+
# metadata files are always persisted without a version prefix
127+
self._assert_metadata_files_exist(TOP_LEVEL_ROLE_NAMES)
128+
129+
self._cleanup_dir(self.metadata_dir)
130+
131+
delegated_roles_data: utils.DataSet = {
132+
"consistent_snaphot disabled": {
133+
"consistent_snapshot": False,
134+
"expected_version": None,
135+
},
136+
"consistent_snaphot enabled": {
137+
"consistent_snapshot": True,
138+
"expected_version": 1,
139+
},
140+
}
141+
142+
@utils.run_sub_tests_with_dataset(delegated_roles_data)
143+
def test_delegated_roles_update(self, test_case_data: Dict[str, Any]):
144+
# Test if the client fetches and stores delegated metadata files with
145+
# the correct version prefix, depending on 'consistent_snapshot' config
146+
consistent_snapshot: bool = test_case_data["consistent_snapshot"]
147+
expected_version: Optional[int] = test_case_data["expected_version"]
148+
rolenames = ["role1", "..", "."]
149+
expected_calls = [call(role, expected_version) for role in rolenames]
150+
151+
sim = self._init_repo(consistent_snapshot)
152+
# Add new delegated targets
153+
spec_version = ".".join(SPECIFICATION_VERSION)
154+
targets = Targets(1, spec_version, sim.safe_expiry, {}, None)
155+
for role in rolenames:
156+
sim.add_delegation("targets", role, targets, False, ["*"], None)
157+
sim.update_snapshot()
158+
updater = self._init_updater(sim)
159+
updater.refresh()
160+
161+
with patch.object(
162+
sim, "_fetch_metadata", wraps=sim._fetch_metadata
163+
) as wrapped_fetch_metadata:
164+
# trigger updater to fetch the delegated metadata
165+
updater.get_targetinfo("anything")
166+
# metadata files are fetched with the expected version (or None)
167+
wrapped_fetch_metadata.assert_has_calls(expected_calls)
168+
# metadata files are always persisted without a version prefix
169+
self._assert_metadata_files_exist(rolenames)
170+
171+
self._cleanup_dir(self.metadata_dir)
172+
173+
targets_download_data: utils.DataSet = {
174+
"consistent_snaphot disabled": {
175+
"consistent_snapshot": False,
176+
"prefix_targets": True,
177+
"hash_algo": None,
178+
},
179+
"consistent_snaphot enabled without prefixed targets": {
180+
"consistent_snapshot": True,
181+
"prefix_targets": False,
182+
"hash_algo": None,
183+
},
184+
"consistent_snaphot_enabled with prefixed targets": {
185+
"consistent_snapshot": True,
186+
"prefix_targets": True,
187+
"hash_algo": "sha256",
188+
},
189+
}
190+
191+
@utils.run_sub_tests_with_dataset(targets_download_data)
192+
def test_download_targets(self, test_case_data: Dict[str, Any]):
193+
# Test if the client fetches and stores target files with
194+
# the correct hash prefix, depending on 'consistent_snapshot'
195+
# and 'prefix_targets_with_hash' config
196+
consistent_snapshot: bool = test_case_data["consistent_snapshot"]
197+
prefix_targets_with_hash: bool = test_case_data["prefix_targets"]
198+
hash_algo: Optional[str] = test_case_data["hash_algo"]
199+
targetpaths = ["file", "file.txt", "..file.ext", "f.le"]
200+
201+
sim = self._init_repo(consistent_snapshot, prefix_targets_with_hash)
202+
# Add targets to repository
203+
for targetpath in targetpaths:
204+
sim.targets.version += 1
205+
sim.add_target("targets", b"content", targetpath)
206+
sim.update_snapshot()
207+
208+
updater = self._init_updater(sim)
209+
updater.config.prefix_targets_with_hash = prefix_targets_with_hash
210+
updater.refresh()
211+
212+
for targetpath in targetpaths:
213+
info = updater.get_targetinfo(targetpath)
214+
215+
with patch.object(
216+
sim, "_fetch_target", wraps=sim._fetch_target
217+
) as wrapped_fetch_target:
218+
219+
updater.download_target(info)
220+
expected_prefix = (
221+
None if not hash_algo else info.hashes[hash_algo]
222+
)
223+
# files are fetched with the expected hash prefix (or None)
224+
wrapped_fetch_target.assert_called_with(
225+
info.path, expected_prefix
226+
)
227+
# target files are always persisted without hash prefix
228+
self._assert_targets_files_exist([info.path])
229+
230+
self._cleanup_dir(self.targets_dir)
231+
232+
233+
if __name__ == "__main__":
234+
235+
utils.configure_test_logging(sys.argv)
236+
unittest.main()

0 commit comments

Comments
 (0)