|
| 1 | +#!/usr/bin/env python |
| 2 | + |
| 3 | +# Copyright New York University and the TUF contributors |
| 4 | +# SPDX-License-Identifier: MIT OR Apache-2.0 |
| 5 | + |
| 6 | +"""Test __eq__ implementations of classes inside tuf/api/metadata.py.""" |
| 7 | + |
| 8 | + |
| 9 | +import copy |
| 10 | +import os |
| 11 | +import sys |
| 12 | +import unittest |
| 13 | +from typing import Any, ClassVar, Dict |
| 14 | + |
| 15 | +from securesystemslib.signer import Signature |
| 16 | + |
| 17 | +from tests import utils |
| 18 | +from tuf.api.metadata import ( |
| 19 | + TOP_LEVEL_ROLE_NAMES, |
| 20 | + DelegatedRole, |
| 21 | + Delegations, |
| 22 | + Key, |
| 23 | + Metadata, |
| 24 | + MetaFile, |
| 25 | + Role, |
| 26 | + Root, |
| 27 | + Snapshot, |
| 28 | + TargetFile, |
| 29 | + Targets, |
| 30 | + Timestamp, |
| 31 | +) |
| 32 | + |
| 33 | + |
| 34 | +class TestMetadataComparisions(unittest.TestCase): |
| 35 | + """Test __eq__ for all classes inside tuf/api/metadata.py.""" |
| 36 | + |
| 37 | + metadata: ClassVar[Dict[str, bytes]] |
| 38 | + |
| 39 | + @classmethod |
| 40 | + def setUpClass(cls) -> None: |
| 41 | + cls.repo_dir = os.path.join( |
| 42 | + utils.TESTS_DIR, "repository_data", "repository", "metadata" |
| 43 | + ) |
| 44 | + cls.metadata = {} |
| 45 | + for md in TOP_LEVEL_ROLE_NAMES: |
| 46 | + with open(os.path.join(cls.repo_dir, f"{md}.json"), "rb") as f: |
| 47 | + cls.metadata[md] = f.read() |
| 48 | + |
| 49 | + def copy_and_simple_assert(self, obj: Any) -> Any: |
| 50 | + # Assert that obj is not equal to an object from another type |
| 51 | + self.assertNotEqual(obj, "") |
| 52 | + result_obj = copy.deepcopy(obj) |
| 53 | + # Assert that __eq__ works for equal objects. |
| 54 | + self.assertEqual(obj, result_obj) |
| 55 | + return result_obj |
| 56 | + |
| 57 | + def test_metadata_eq_(self) -> None: |
| 58 | + md = Metadata.from_bytes(self.metadata["snapshot"]) |
| 59 | + md_2: Metadata = self.copy_and_simple_assert(md) |
| 60 | + |
| 61 | + for attr, value in [("signed", None), ("signatures", None)]: |
| 62 | + setattr(md_2, attr, value) |
| 63 | + self.assertNotEqual(md, md_2, f"Failed case: {attr}") |
| 64 | + |
| 65 | + def test_md_eq_signatures_reversed_order(self) -> None: |
| 66 | + # Test comparing objects with same signatures but different order. |
| 67 | + |
| 68 | + # Remove all signatures and create new ones. |
| 69 | + md = Metadata.from_bytes(self.metadata["snapshot"]) |
| 70 | + md.signatures = {"a": Signature("a", "a"), "b": Signature("b", "b")} |
| 71 | + md_2 = copy.deepcopy(md) |
| 72 | + # Reverse signatures order in md_2. |
| 73 | + # In python3.7 we need to cast to a list and then reverse. |
| 74 | + md_2.signatures = dict(reversed(list(md_2.signatures.items()))) |
| 75 | + # Assert that both objects are not the same because of signatures order. |
| 76 | + self.assertNotEqual(md, md_2) |
| 77 | + |
| 78 | + # but if we fix the signatures order they will be equal |
| 79 | + md_2.signatures = {"a": Signature("a", "a"), "b": Signature("b", "b")} |
| 80 | + self.assertEqual(md, md_2) |
| 81 | + |
| 82 | + def test_md_eq_special_signatures_tests(self) -> None: |
| 83 | + # Test that metadata objects with different signatures are not equal. |
| 84 | + md = Metadata.from_bytes(self.metadata["snapshot"]) |
| 85 | + md_2 = copy.deepcopy(md) |
| 86 | + md_2.signatures = {} |
| 87 | + self.assertNotEqual(md, md_2) |
| 88 | + |
| 89 | + # Test that metadata objects with empty signatures are equal |
| 90 | + md.signatures = {} |
| 91 | + self.assertEqual(md, md_2) |
| 92 | + |
| 93 | + # Metadata objects with different signatures types are not equal. |
| 94 | + md_2.signatures = "" # type: ignore |
| 95 | + self.assertNotEqual(md, md_2) |
| 96 | + |
| 97 | + def test_signed_eq_(self) -> None: |
| 98 | + md = Metadata.from_bytes(self.metadata["snapshot"]) |
| 99 | + md_2: Metadata = self.copy_and_simple_assert(md) |
| 100 | + |
| 101 | + # We don't need to make "signed" = None as that was done when testing |
| 102 | + # metadata attribute modifications. |
| 103 | + for attr, value in [("version", -1), ("spec_version", "0.0.0")]: |
| 104 | + setattr(md_2.signed, attr, value) |
| 105 | + self.assertNotEqual(md.signed, md_2.signed, f"Failed case: {attr}") |
| 106 | + |
| 107 | + def test_key_eq_(self) -> None: |
| 108 | + key_dict = { |
| 109 | + "keytype": "rsa", |
| 110 | + "scheme": "rsassa-pss-sha256", |
| 111 | + "keyval": {"public": "foo"}, |
| 112 | + } |
| 113 | + key = Key.from_dict("12sa12", key_dict) |
| 114 | + key_2: Key = self.copy_and_simple_assert(key) |
| 115 | + for attr, value in [ |
| 116 | + ("keyid", "a"), |
| 117 | + ("keytype", "foo"), |
| 118 | + ("scheme", "b"), |
| 119 | + ("keytype", "b"), |
| 120 | + ]: |
| 121 | + setattr(key_2, attr, value) |
| 122 | + self.assertNotEqual(key, key_2, f"Failed case: {attr}") |
| 123 | + |
| 124 | + def test_role_eq_(self) -> None: |
| 125 | + role_dict = { |
| 126 | + "keyids": ["keyid1", "keyid2"], |
| 127 | + "threshold": 3, |
| 128 | + } |
| 129 | + role = Role.from_dict(role_dict) |
| 130 | + role_2: Role = self.copy_and_simple_assert(role) |
| 131 | + |
| 132 | + for attr, value in [("keyids", []), ("threshold", 10)]: |
| 133 | + setattr(role_2, attr, value) |
| 134 | + self.assertNotEqual(role, role_2, f"Failed case: {attr}") |
| 135 | + |
| 136 | + def test_root_eq_(self) -> None: |
| 137 | + md = Metadata.from_bytes(self.metadata["root"]) |
| 138 | + signed_copy: Root = self.copy_and_simple_assert(md.signed) |
| 139 | + |
| 140 | + # Common attributes between Signed and Root doesn't need testing. |
| 141 | + # Ignore mypy request for type annotations on attr and value |
| 142 | + for attr, value in [ # type: ignore |
| 143 | + ("consistent_snapshot", None), |
| 144 | + ("keys", {}), |
| 145 | + ("roles", {}), |
| 146 | + ]: |
| 147 | + |
| 148 | + setattr(signed_copy, attr, value) |
| 149 | + self.assertNotEqual(md.signed, signed_copy, f"Failed case: {attr}") |
| 150 | + |
| 151 | + def test_metafile_eq_(self) -> None: |
| 152 | + metafile_dict = { |
| 153 | + "version": 1, |
| 154 | + "length": 12, |
| 155 | + "hashes": {"sha256": "abc"}, |
| 156 | + } |
| 157 | + metafile = MetaFile.from_dict(metafile_dict) |
| 158 | + metafile_2: MetaFile = self.copy_and_simple_assert(metafile) |
| 159 | + |
| 160 | + # Ignore mypy request for type annotations on attr and value |
| 161 | + for attr, value in [ # type: ignore |
| 162 | + ("version", None), |
| 163 | + ("length", None), |
| 164 | + ("hashes", {}), |
| 165 | + ]: |
| 166 | + setattr(metafile_2, attr, value) |
| 167 | + self.assertNotEqual(metafile, metafile_2, f"Failed case: {attr}") |
| 168 | + |
| 169 | + def test_timestamp_eq_(self) -> None: |
| 170 | + md = Metadata.from_bytes(self.metadata["timestamp"]) |
| 171 | + signed_copy: Timestamp = self.copy_and_simple_assert(md.signed) |
| 172 | + |
| 173 | + # Common attributes between Signed and Timestamp doesn't need testing. |
| 174 | + setattr(signed_copy, "snapshot_meta", None) |
| 175 | + self.assertNotEqual(md.signed, signed_copy) |
| 176 | + |
| 177 | + def test_snapshot_eq_(self) -> None: |
| 178 | + md = Metadata.from_bytes(self.metadata["snapshot"]) |
| 179 | + signed_copy: Snapshot = self.copy_and_simple_assert(md.signed) |
| 180 | + |
| 181 | + # Common attributes between Signed and Snapshot doesn't need testing. |
| 182 | + setattr(signed_copy, "meta", None) |
| 183 | + self.assertNotEqual(md.signed, signed_copy) |
| 184 | + |
| 185 | + def test_delegated_role_eq_(self) -> None: |
| 186 | + delegated_role_dict = { |
| 187 | + "keyids": ["keyid"], |
| 188 | + "name": "a", |
| 189 | + "terminating": False, |
| 190 | + "threshold": 1, |
| 191 | + "paths": ["fn1", "fn2"], |
| 192 | + } |
| 193 | + delegated_role = DelegatedRole.from_dict(delegated_role_dict) |
| 194 | + delegated_role_2: DelegatedRole = self.copy_and_simple_assert( |
| 195 | + delegated_role |
| 196 | + ) |
| 197 | + |
| 198 | + # Common attributes between DelegatedRole and Role doesn't need testing. |
| 199 | + for attr, value in [ |
| 200 | + ("name", ""), |
| 201 | + ("terminating", None), |
| 202 | + ("paths", [""]), |
| 203 | + ("path_hash_prefixes", [""]), |
| 204 | + ]: |
| 205 | + setattr(delegated_role_2, attr, value) |
| 206 | + msg = f"Failed case: {attr}" |
| 207 | + self.assertNotEqual(delegated_role, delegated_role_2, msg) |
| 208 | + |
| 209 | + def test_delegations_eq_(self) -> None: |
| 210 | + delegations_dict = { |
| 211 | + "keys": { |
| 212 | + "keyid2": { |
| 213 | + "keytype": "ed25519", |
| 214 | + "scheme": "ed25519", |
| 215 | + "keyval": {"public": "bar"}, |
| 216 | + } |
| 217 | + }, |
| 218 | + "roles": [ |
| 219 | + { |
| 220 | + "keyids": ["keyid2"], |
| 221 | + "name": "b", |
| 222 | + "terminating": True, |
| 223 | + "paths": ["fn2"], |
| 224 | + "threshold": 4, |
| 225 | + } |
| 226 | + ], |
| 227 | + } |
| 228 | + delegations = Delegations.from_dict(delegations_dict) |
| 229 | + delegations_2: Delegations = self.copy_and_simple_assert(delegations) |
| 230 | + # Ignore mypy request for type annotations on attr and value |
| 231 | + for attr, value in [("keys", {}), ("roles", {})]: # type: ignore |
| 232 | + setattr(delegations_2, attr, value) |
| 233 | + msg = f"Failed case: {attr}" |
| 234 | + self.assertNotEqual(delegations, delegations_2, msg) |
| 235 | + |
| 236 | + def test_targetfile_eq_(self) -> None: |
| 237 | + targetfile_dict = { |
| 238 | + "length": 12, |
| 239 | + "hashes": {"sha256": "abc"}, |
| 240 | + } |
| 241 | + targetfile = TargetFile.from_dict(targetfile_dict, "file1.txt") |
| 242 | + targetfile_2: TargetFile = self.copy_and_simple_assert(targetfile) |
| 243 | + |
| 244 | + # Common attr between TargetFile and MetaFile doesn't need testing. |
| 245 | + setattr(targetfile_2, "path", "") |
| 246 | + self.assertNotEqual(targetfile, targetfile_2) |
| 247 | + |
| 248 | + def test_delegations_eq_roles_reversed_order(self) -> None: |
| 249 | + # Test comparing objects with same delegated roles but different order. |
| 250 | + role_one_dict = { |
| 251 | + "keyids": ["keyid1"], |
| 252 | + "name": "a", |
| 253 | + "terminating": False, |
| 254 | + "paths": ["fn1"], |
| 255 | + "threshold": 1, |
| 256 | + } |
| 257 | + role_two_dict = { |
| 258 | + "keyids": ["keyid2"], |
| 259 | + "name": "b", |
| 260 | + "terminating": True, |
| 261 | + "paths": ["fn2"], |
| 262 | + "threshold": 4, |
| 263 | + } |
| 264 | + |
| 265 | + delegations_dict = { |
| 266 | + "keys": { |
| 267 | + "keyid2": { |
| 268 | + "keytype": "ed25519", |
| 269 | + "scheme": "ed25519", |
| 270 | + "keyval": {"public": "bar"}, |
| 271 | + } |
| 272 | + }, |
| 273 | + "roles": [role_one_dict, role_two_dict], |
| 274 | + } |
| 275 | + delegations = Delegations.from_dict(copy.deepcopy(delegations_dict)) |
| 276 | + |
| 277 | + # Create a second delegations obj with reversed roles order |
| 278 | + delegations_2 = copy.deepcopy(delegations) |
| 279 | + # In python3.7 we need to cast to a list and then reverse. |
| 280 | + delegations_2.roles = dict(reversed(list(delegations.roles.items()))) |
| 281 | + |
| 282 | + # Both objects are not the equal because of delegated roles order. |
| 283 | + self.assertNotEqual(delegations, delegations_2) |
| 284 | + |
| 285 | + # but if we fix the delegated roles order they will be equal |
| 286 | + delegations_2.roles = delegations.roles |
| 287 | + |
| 288 | + self.assertEqual(delegations, delegations_2) |
| 289 | + |
| 290 | + def test_targets_eq_(self) -> None: |
| 291 | + md = Metadata.from_bytes(self.metadata["targets"]) |
| 292 | + signed_copy: Targets = self.copy_and_simple_assert(md.signed) |
| 293 | + |
| 294 | + # Common attributes between Targets and Signed doesn't need testing. |
| 295 | + # Ignore mypy request for type annotations on attr and value |
| 296 | + for attr, value in [("targets", {}), ("delegations", [])]: # type: ignore |
| 297 | + setattr(signed_copy, attr, value) |
| 298 | + self.assertNotEqual(md.signed, signed_copy, f"Failed case: {attr}") |
| 299 | + |
| 300 | + |
| 301 | +# Run unit test. |
| 302 | +if __name__ == "__main__": |
| 303 | + utils.configure_test_logging(sys.argv) |
| 304 | + unittest.main() |
0 commit comments