import hashlib import json from unittest import TestCase from unittest.mock import patch from samtranslator.translator.logical_id_generator import LogicalIdGenerator class TestLogicalIdGenerator(TestCase): """ Test the implementation of LogicalIDGenerator """ def setUp(self): self.prefix = "prefix" @patch.object(LogicalIdGenerator, "_stringify") def test_gen_no_data(self, stringify_mock): generator = LogicalIdGenerator(self.prefix) self.assertEqual(self.prefix, generator.gen()) # Calling gen() again should return the same result self.assertEqual(generator.gen(), generator.gen()) stringify_mock.assert_not_called() @patch.object(LogicalIdGenerator, "get_hash") @patch.object(LogicalIdGenerator, "_stringify") def test_gen_dict_data(self, stringify_mock, get_hash_mock): data = {"foo": "bar"} stringified_data = "stringified data" hash_value = "some hash value" get_hash_mock.return_value = hash_value stringify_mock.return_value = stringified_data generator = LogicalIdGenerator(self.prefix, data_obj=data) expected = f"{self.prefix}{hash_value}" self.assertEqual(expected, generator.gen()) get_hash_mock.assert_called_once_with() stringify_mock.assert_called_once_with(data) self.assertEqual(generator.gen(), generator.gen()) @patch.object(LogicalIdGenerator, "_stringify") def test_gen_hash_data_override(self, stringify_mock): data = {"foo": "bar"} stringified_data = "stringified data" hash_value = "6b86b273ff" stringify_mock.return_value = stringified_data generator = LogicalIdGenerator(self.prefix, data_obj=data, data_hash=hash_value) expected = f"{self.prefix}{hash_value}" self.assertEqual(expected, generator.gen()) stringify_mock.assert_called_once_with(data) self.assertEqual(generator.gen(), generator.gen()) @patch.object(LogicalIdGenerator, "_stringify") def test_gen_hash_data_empty(self, stringify_mock): data = {"foo": "bar"} stringified_data = "stringified data" hash_value = "" stringify_mock.return_value = stringified_data generator = LogicalIdGenerator(self.prefix, data_obj=data, data_hash=hash_value) stringify_mock.assert_called_once_with(data) self.assertEqual(generator.gen(), generator.gen()) def test_gen_stability_with_copy(self): data = {"foo": "bar", "a": "b"} generator = LogicalIdGenerator(self.prefix, data_obj=data) old = generator.gen() new = LogicalIdGenerator(self.prefix, data_obj=data.copy()).gen() # Create a copy of data obj self.assertEqual(old, new) def test_gen_stability_with_different_dict_ordering(self): data = {"foo": "bar", "nested": {"a": "b", "c": "d"}} data_other = {"nested": {"c": "d", "a": "b"}, "foo": "bar"} # Same content but different ordering of keys generator = LogicalIdGenerator(self.prefix, data_obj=data) old = generator.gen() new = LogicalIdGenerator(self.prefix, data_obj=data_other).gen() self.assertEqual(old, new) def test_gen_changes_on_different_dict_data(self): data = {"foo": "bar", "nested": {"a": "b", "c": "d"}} data_other = {"foo2": "bar", "nested": {"a": "b", "c": "d"}} # Just changing one key old = LogicalIdGenerator(self.prefix, data_obj=data) new = LogicalIdGenerator(self.prefix, data_obj=data_other).gen() self.assertNotEqual(old, new) def test_gen_changes_on_different_string_data(self): data = "some data" data_other = "some other data" old = LogicalIdGenerator(self.prefix, data_obj=data) new = LogicalIdGenerator(self.prefix, data_obj=data_other).gen() self.assertNotEqual(old, new) @patch.object(LogicalIdGenerator, "get_hash") @patch.object(LogicalIdGenerator, "_stringify") def test_error_stringifying(self, stringify_mock, get_hash_mock): data = {"foo": "bar"} hash_value = "some hash value" get_hash_mock.return_value = hash_value stringify_mock.side_effect = TypeError("error") with self.assertRaises(TypeError): LogicalIdGenerator(self.prefix, data_obj=data) get_hash_mock.assert_not_called() @patch.object(LogicalIdGenerator, "_stringify") def testget_hash(self, stringify_mock): data = "some data" stringified_data = "some stringified data" stringify_mock.return_value = stringified_data generator = LogicalIdGenerator(self.prefix, data_obj=data) # We are essentially duplicating the implementation here. This is done on purpose to prevent # accidental change of the algorithm. Any changes to the hash generation must be backwards compatible. # This test will help catch such issues before hand. utf_data = str(stringified_data).encode("utf8") expected = hashlib.sha1(bytes(utf_data)).hexdigest()[:10] self.assertEqual(expected, generator.get_hash()) stringify_mock.assert_called_once_with(data) @patch.object(LogicalIdGenerator, "_stringify") def testget_hash_no_data(self, stringify_mock): data = "some data" stringified_data = None stringify_mock.return_value = stringified_data generator = LogicalIdGenerator(self.prefix, data_obj=data) self.assertEqual("", generator.get_hash()) stringify_mock.assert_called_once_with(data) def test_stringify_basic_objects(self): data = {"a": "b", "c": [4, 3, 1]} expected = '{"a":"b","c":[4,3,1]}' generator = LogicalIdGenerator(self.prefix, data_obj=data) self.assertEqual(expected, generator._stringify(data)) def test_stringify_basic_objects_sorting(self): data = {"c": [4, 3, 1], "a": "b"} expected = '{"a":"b","c":[4,3,1]}' generator = LogicalIdGenerator(self.prefix, data_obj=data) self.assertEqual(expected, generator._stringify(data)) def test_stringify_array_sorting(self): data = ["a", 1, {"z": "x", "b": "d"}] expected = '["a",1,{"b":"d","z":"x"}]' generator = LogicalIdGenerator(self.prefix, data_obj=data) self.assertEqual(expected, generator._stringify(data)) def test_stringify_strings(self): data = "some data" generator = LogicalIdGenerator(self.prefix, data_obj=data) # Strings should be returned unmodified ie. json dump is short circuited self.assertEqual(data, generator._stringify(data)) @patch.object(json, "dumps") def test_stringify_expectations(self, json_dumps_mock): data = ["foo"] expected = "bar" json_dumps_mock.return_value = expected generator = LogicalIdGenerator(self.prefix, data_obj=data) self.assertEqual(expected, generator._stringify(data)) json_dumps_mock.assert_called_with(data, separators=(",", ":"), sort_keys=True) @patch.object(json, "dumps") def test_stringify_expectations_for_string(self, json_dumps_mock): data = "foo" generator = LogicalIdGenerator(self.prefix, data_obj=data) self.assertEqual(data, generator._stringify(data)) json_dumps_mock.assert_not_called()