Skip to content

Commit 29ea5f3

Browse files
author
Jussi Kukkonen
committed
ngclient: Add initial testing
This testing lacks coverage but demonstrates the happy cases. Signed-off-by: Jussi Kukkonen <jkukkonen@vmware.com>
1 parent 942afa0 commit 29ea5f3

2 files changed

Lines changed: 295 additions & 0 deletions

File tree

tests/test_metadata_bundle.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import json
2+
import logging
3+
import os
4+
import shutil
5+
import sys
6+
import tempfile
7+
import unittest
8+
9+
from tuf import exceptions
10+
from tuf.api.metadata import Metadata
11+
from tuf.ngclient._internal.metadata_bundle import MetadataBundle
12+
13+
from tests import utils
14+
15+
logger = logging.getLogger(__name__)
16+
17+
class TestMetadataBundle(unittest.TestCase):
18+
19+
def test_update(self):
20+
repo_dir = os.path.join(os.getcwd(), 'repository_data', 'repository', 'metadata')
21+
22+
with open(os.path.join(repo_dir, "root.json"), "rb") as f:
23+
bundle = MetadataBundle(f.read())
24+
bundle.root_update_finished()
25+
26+
with open(os.path.join(repo_dir, "timestamp.json"), "rb") as f:
27+
bundle.update_timestamp(f.read())
28+
with open(os.path.join(repo_dir, "snapshot.json"), "rb") as f:
29+
bundle.update_snapshot(f.read())
30+
with open(os.path.join(repo_dir, "targets.json"), "rb") as f:
31+
bundle.update_targets(f.read())
32+
with open(os.path.join(repo_dir, "role1.json"), "rb") as f:
33+
bundle.update_delegated_targets(f.read(), "role1", "targets")
34+
with open(os.path.join(repo_dir, "role2.json"), "rb") as f:
35+
bundle.update_delegated_targets(f.read(), "role2", "role1")
36+
37+
def test_out_of_order_ops(self):
38+
repo_dir = os.path.join(os.getcwd(), 'repository_data', 'repository', 'metadata')
39+
data={}
40+
for md in ["root", "timestamp", "snapshot", "targets", "role1"]:
41+
with open(os.path.join(repo_dir, f"{md}.json"), "rb") as f:
42+
data[md] = f.read()
43+
44+
bundle = MetadataBundle(data["root"])
45+
46+
# Update timestamp before root is finished
47+
with self.assertRaises(RuntimeError):
48+
bundle.update_timestamp(data["timestamp"])
49+
50+
bundle.root_update_finished()
51+
with self.assertRaises(RuntimeError):
52+
bundle.root_update_finished()
53+
54+
# Update snapshot before timestamp
55+
with self.assertRaises(RuntimeError):
56+
bundle.update_snapshot(data["snapshot"])
57+
58+
bundle.update_timestamp(data["timestamp"])
59+
60+
# Update targets before snapshot
61+
with self.assertRaises(RuntimeError):
62+
bundle.update_targets(data["targets"])
63+
64+
bundle.update_snapshot(data["snapshot"])
65+
66+
#update timestamp after snapshot
67+
with self.assertRaises(RuntimeError):
68+
bundle.update_timestamp(data["timestamp"])
69+
70+
# Update delegated targets before targets
71+
with self.assertRaises(RuntimeError):
72+
bundle.update_delegated_targets(data["role1"], "role1", "targets")
73+
74+
bundle.update_targets(data["targets"])
75+
bundle.update_delegated_targets(data["role1"], "role1", "targets")
76+
77+
def test_update_with_invalid_json(self):
78+
repo_dir = os.path.join(os.getcwd(), 'repository_data', 'repository', 'metadata')
79+
data={}
80+
for md in ["root", "timestamp", "snapshot", "targets", "role1"]:
81+
with open(os.path.join(repo_dir, f"{md}.json"), "rb") as f:
82+
data[md] = f.read()
83+
84+
# root.json not a json file at all
85+
with self.assertRaises(exceptions.RepositoryError):
86+
MetadataBundle(b"")
87+
# root.json is invalid
88+
root = Metadata.from_bytes(data["root"])
89+
root.signed.version += 1
90+
with self.assertRaises(exceptions.RepositoryError):
91+
MetadataBundle(json.dumps(root.to_dict()).encode())
92+
93+
bundle = MetadataBundle(data["root"])
94+
bundle.root_update_finished()
95+
96+
top_level_md = [
97+
(data["timestamp"], bundle.update_timestamp),
98+
(data["snapshot"], bundle.update_snapshot),
99+
(data["targets"], bundle.update_targets),
100+
]
101+
for metadata, update_func in top_level_md:
102+
# metadata is not json
103+
with self.assertRaises(exceptions.RepositoryError):
104+
update_func(b"")
105+
# metadata is invalid
106+
md = Metadata.from_bytes(metadata)
107+
md.signed.version += 1
108+
with self.assertRaises(exceptions.RepositoryError):
109+
update_func(json.dumps(md.to_dict()).encode())
110+
111+
# metadata is of wrong type
112+
with self.assertRaises(exceptions.RepositoryError):
113+
update_func(data["root"])
114+
115+
update_func(metadata)
116+
117+
118+
# TODO test updating over initial metadata (new keys, newer timestamp, etc)
119+
# TODO test the actual specification checks
120+
121+
122+
if __name__ == '__main__':
123+
utils.configure_test_logging(sys.argv)
124+
unittest.main()

tests/test_updater_ng.py

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
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 Updater class
7+
"""
8+
9+
import os
10+
import time
11+
import shutil
12+
import copy
13+
import tempfile
14+
import logging
15+
import errno
16+
import sys
17+
import unittest
18+
import json
19+
import tracemalloc
20+
21+
if sys.version_info >= (3, 3):
22+
import unittest.mock as mock
23+
else:
24+
import mock
25+
26+
import tuf
27+
import tuf.exceptions
28+
import tuf.log
29+
import tuf.repository_tool as repo_tool
30+
import tuf.unittest_toolbox as unittest_toolbox
31+
32+
from tests import utils
33+
from tuf.api import metadata
34+
from tuf import ngclient
35+
36+
import securesystemslib
37+
38+
logger = logging.getLogger(__name__)
39+
40+
41+
class TestUpdater(unittest_toolbox.Modified_TestCase):
42+
43+
@classmethod
44+
def setUpClass(cls):
45+
# Create a temporary directory to store the repository, metadata, and target
46+
# files. 'temporary_directory' must be deleted in TearDownModule() so that
47+
# temporary files are always removed, even when exceptions occur.
48+
cls.temporary_directory = tempfile.mkdtemp(dir=os.getcwd())
49+
50+
# Needed because in some tests simple_server.py cannot be found.
51+
# The reason is that the current working directory
52+
# has been changed when executing a subprocess.
53+
cls.SIMPLE_SERVER_PATH = os.path.join(os.getcwd(), 'simple_server.py')
54+
55+
# Launch a SimpleHTTPServer (serves files in the current directory).
56+
# Test cases will request metadata and target files that have been
57+
# pre-generated in 'tuf/tests/repository_data', which will be served
58+
# by the SimpleHTTPServer launched here. The test cases of 'test_updater.py'
59+
# assume the pre-generated metadata files have a specific structure, such
60+
# as a delegated role 'targets/role1', three target files, five key files,
61+
# etc.
62+
cls.server_process_handler = utils.TestServerProcess(log=logger,
63+
server=cls.SIMPLE_SERVER_PATH)
64+
65+
66+
67+
@classmethod
68+
def tearDownClass(cls):
69+
# Cleans the resources and flush the logged lines (if any).
70+
cls.server_process_handler.clean()
71+
72+
# Remove the temporary repository directory, which should contain all the
73+
# metadata, targets, and key files generated for the test cases
74+
shutil.rmtree(cls.temporary_directory)
75+
76+
77+
78+
def setUp(self):
79+
# We are inheriting from custom class.
80+
unittest_toolbox.Modified_TestCase.setUp(self)
81+
82+
# Copy the original repository files provided in the test folder so that
83+
# any modifications made to repository files are restricted to the copies.
84+
# The 'repository_data' directory is expected to exist in 'tuf.tests/'.
85+
original_repository_files = os.path.join(os.getcwd(), 'repository_data')
86+
temporary_repository_root = \
87+
self.make_temp_directory(directory=self.temporary_directory)
88+
89+
# The original repository, keystore, and client directories will be copied
90+
# for each test case.
91+
original_repository = os.path.join(original_repository_files, 'repository')
92+
original_keystore = os.path.join(original_repository_files, 'keystore')
93+
original_client = os.path.join(original_repository_files, 'client', 'test_repository1', 'metadata', 'current')
94+
95+
# Save references to the often-needed client repository directories.
96+
# Test cases need these references to access metadata and target files.
97+
self.repository_directory = \
98+
os.path.join(temporary_repository_root, 'repository')
99+
self.keystore_directory = \
100+
os.path.join(temporary_repository_root, 'keystore')
101+
102+
self.client_directory = os.path.join(temporary_repository_root, 'client')
103+
104+
# Copy the original 'repository', 'client', and 'keystore' directories
105+
# to the temporary repository the test cases can use.
106+
shutil.copytree(original_repository, self.repository_directory)
107+
shutil.copytree(original_client, self.client_directory)
108+
shutil.copytree(original_keystore, self.keystore_directory)
109+
110+
# 'path/to/tmp/repository' -> 'localhost:8001/tmp/repository'.
111+
repository_basepath = self.repository_directory[len(os.getcwd()):]
112+
url_prefix = 'http://' + utils.TEST_HOST_ADDRESS + ':' \
113+
+ str(self.server_process_handler.port) + repository_basepath
114+
115+
metadata_url = f"{url_prefix}/metadata/"
116+
targets_url = f"{url_prefix}/targets/"
117+
# Creating a repository instance. The test cases will use this client
118+
# updater to refresh metadata, fetch target files, etc.
119+
self.repository_updater = ngclient.Updater(self.client_directory,
120+
metadata_url,
121+
targets_url)
122+
123+
def tearDown(self):
124+
# We are inheriting from custom class.
125+
unittest_toolbox.Modified_TestCase.tearDown(self)
126+
127+
# Logs stdout and stderr from the sever subprocess.
128+
self.server_process_handler.flush_log()
129+
130+
def test_refresh(self):
131+
# All metadata is in local directory already
132+
self.repository_updater.refresh()
133+
134+
# Get targetinfo for 'file1.txt' listed in targets
135+
targetinfo1 = self.repository_updater.get_one_valid_targetinfo('file1.txt')
136+
# Get targetinfo for 'file3.txt' listed in the delegated role1
137+
targetinfo3= self.repository_updater.get_one_valid_targetinfo('file3.txt')
138+
139+
destination_directory = self.make_temp_directory()
140+
updated_targets = self.repository_updater.updated_targets([targetinfo1, targetinfo3],
141+
destination_directory)
142+
143+
self.assertListEqual(updated_targets, [targetinfo1, targetinfo3])
144+
145+
self.repository_updater.download_target(targetinfo1, destination_directory)
146+
updated_targets = self.repository_updater.updated_targets(updated_targets,
147+
destination_directory)
148+
149+
self.assertListEqual(updated_targets, [targetinfo3])
150+
151+
152+
self.repository_updater.download_target(targetinfo3, destination_directory)
153+
updated_targets = self.repository_updater.updated_targets(updated_targets,
154+
destination_directory)
155+
156+
self.assertListEqual(updated_targets, [])
157+
158+
def test_refresh_with_only_local_root(self):
159+
os.remove(os.path.join(self.client_directory, "timestamp.json"))
160+
os.remove(os.path.join(self.client_directory, "snapshot.json"))
161+
os.remove(os.path.join(self.client_directory, "targets.json"))
162+
os.remove(os.path.join(self.client_directory, "role1.json"))
163+
164+
self.repository_updater.refresh()
165+
166+
# Get targetinfo for 'file3.txt' listed in the delegated role1
167+
targetinfo3= self.repository_updater.get_one_valid_targetinfo('file3.txt')
168+
169+
if __name__ == '__main__':
170+
utils.configure_test_logging(sys.argv)
171+
unittest.main()

0 commit comments

Comments
 (0)