diff --git a/test/fluent_api/conftest.py b/test/fluent_api/conftest.py index ced1a94..0204da0 100644 --- a/test/fluent_api/conftest.py +++ b/test/fluent_api/conftest.py @@ -11,7 +11,7 @@ @pytest.fixture def run_fluent_api_notecard_api_mapping_test(): - def _run_test(fluent_api, notecard_api_name, req_params, rename_map=None): + def _run_test(fluent_api, notecard_api_name, req_params, rename_key_map=None, rename_value_map=None): card = notecard.Notecard() card.Transaction = MagicMock() @@ -21,13 +21,24 @@ def _run_test(fluent_api, notecard_api_name, req_params, rename_map=None): # There are certain fluent APIs that have keyword arguments that don't # map exactly onto the Notecard API. For example, note.changes takes a # 'maximum' parameter, but in the JSON request that gets sent to the - # Notecard, it's sent as 'max'. The rename_map allows a test to specify + # Notecard, it's sent as 'max'. The rename_key_map allows a test to specify # how a fluent API's keyword args map to Notecard API args, in cases # where they differ. - if rename_map is not None: - for old_key, new_key in rename_map.items(): + if rename_key_map is not None: + for old_key, new_key in rename_key_map.items(): expected_notecard_api_req[new_key] = expected_notecard_api_req.pop(old_key) + # Additionally, some Notecard API args have values that are not directly + # mapped, but are instead derived from the value of another arg. For + # example, note.template takes a 'compact' parameter, but the value of + # that parameter is actually derived from the value of the 'format' + # parameter. The rename_value_map allows a test to specify how a fluent + # API's keyword args map to Notecard API args, in cases where the value + # of one arg is derived from the value of another arg. + if rename_value_map is not None: + for key, new_value in rename_value_map.items(): + expected_notecard_api_req[key] = new_value + card.Transaction.assert_called_once_with(expected_notecard_api_req) return _run_test @@ -35,7 +46,7 @@ def _run_test(fluent_api, notecard_api_name, req_params, rename_map=None): @pytest.fixture def run_fluent_api_invalid_notecard_test(): - def _run_test(fluent_api, req_params): + def _run_test(fluent_api, req_params, rename_key_map=None, rename_value_map=None): with pytest.raises(Exception, match='Notecard object required'): # Call with None instead of a valid Notecard object. fluent_api(None, **req_params) diff --git a/test/fluent_api/test_note.py b/test/fluent_api/test_note.py index dd72e98..abfbe3f 100644 --- a/test/fluent_api/test_note.py +++ b/test/fluent_api/test_note.py @@ -3,7 +3,7 @@ @pytest.mark.parametrize( - 'fluent_api,notecard_api,req_params,rename_map', + 'fluent_api,notecard_api,req_params,rename_key_map,rename_value_map', [ ( note.add, @@ -12,8 +12,10 @@ 'file': 'data.qo', 'body': {'key_a:', 'val_a', 'key_b', 42}, 'payload': 'ewogICJpbnRlcnZhbHMiOiI2MCwxMiwxNCIKfQ==', + 'port': 50, 'sync': True }, + None, None ), ( @@ -30,7 +32,8 @@ }, { 'maximum': 'max' - } + }, + None ), ( note.delete, @@ -41,7 +44,8 @@ }, { 'note_id': 'note' - } + }, + None ), ( note.get, @@ -54,7 +58,8 @@ }, { 'note_id': 'note' - } + }, + None ), ( note.template, @@ -62,9 +67,16 @@ { 'file': 'my-settings.db', 'body': {'key_a:', 'val_a', 'key_b', 42}, - 'length': 42 + 'length': 42, + 'port': 50, + 'compact': True }, - None + { + 'compact': 'format' + }, + { + 'format': 'compact' + } ), ( note.update, @@ -77,18 +89,21 @@ }, { 'note_id': 'note' - } + }, + None ) ] ) class TestNote: def test_fluent_api_maps_notecard_api_correctly( - self, fluent_api, notecard_api, req_params, rename_map, - run_fluent_api_notecard_api_mapping_test): + self, fluent_api, notecard_api, req_params, rename_key_map, + rename_value_map, run_fluent_api_notecard_api_mapping_test): run_fluent_api_notecard_api_mapping_test(fluent_api, notecard_api, - req_params, rename_map) + req_params, rename_key_map, + rename_value_map) def test_fluent_api_fails_with_invalid_notecard( - self, fluent_api, notecard_api, req_params, rename_map, - run_fluent_api_invalid_notecard_test): - run_fluent_api_invalid_notecard_test(fluent_api, req_params) + self, fluent_api, notecard_api, req_params, rename_key_map, + rename_value_map, run_fluent_api_invalid_notecard_test): + run_fluent_api_invalid_notecard_test(fluent_api, req_params, + rename_key_map, rename_value_map) diff --git a/test/test_md5.py b/test/test_md5.py new file mode 100644 index 0000000..2fb362b --- /dev/null +++ b/test/test_md5.py @@ -0,0 +1,39 @@ +import unittest +import sys + + +class TestMD5(unittest.TestCase): + def setUp(self): + # Store original implementation name + self.original_implementation = sys.implementation.name + # Clear the module from sys.modules to force reload + if 'notecard.md5' in sys.modules: + del sys.modules['notecard.md5'] + + def tearDown(self): + # Restore original implementation + sys.implementation.name = self.original_implementation + # Clear the module again + if 'notecard.md5' in sys.modules: + del sys.modules['notecard.md5'] + + def test_non_cpython_implementation(self): + """Test our custom MD5 implementation used in non-CPython environments""" + # Set implementation to non-cpython before importing + sys.implementation.name = 'non-cpython' + import notecard.md5 + + test_cases = [ + (b'', 'd41d8cd98f00b204e9800998ecf8427e'), + (b'hello', '5d41402abc4b2a76b9719d911017c592'), + (b'hello world', '5eb63bbbe01eeed093cb22bb8f5acdc3'), + (b'The quick brown fox jumps over the lazy dog', + '9e107d9d372bb6826bd81d3542a419d6'), + (b'123456789', '25f9e794323b453885f5181f1b624d0b'), + (b'!@#$%^&*()', '05b28d17a7b6e7024b6e5d8cc43a8bf7') + ] + + for input_bytes, expected in test_cases: + with self.subTest(input_bytes=input_bytes): + result = notecard.md5.digest(input_bytes) + self.assertEqual(result, expected) diff --git a/test/test_validators.py b/test/test_validators.py new file mode 100644 index 0000000..134de99 --- /dev/null +++ b/test/test_validators.py @@ -0,0 +1,73 @@ +import unittest +import sys +from unittest.mock import MagicMock, patch +from notecard import Notecard +from notecard.validators import validate_card_object + + +class TestValidators(unittest.TestCase): + + def setUp(self): + self.mock_notecard = MagicMock(spec=Notecard) + # Store original implementation name + self.original_implementation = sys.implementation.name + # Clear the module from sys.modules to force reload + if 'notecard.validators' in sys.modules: + del sys.modules['notecard.validators'] + + def tearDown(self): + # Restore original implementation + sys.implementation.name = self.original_implementation + # Clear the module again + if 'notecard.validators' in sys.modules: + del sys.modules['notecard.validators'] + + def test_validate_card_object_with_valid_notecard(self): + @validate_card_object + def test_func(card): + return True + + result = test_func(self.mock_notecard) + self.assertTrue(result) + + def test_validate_card_object_with_invalid_notecard(self): + @validate_card_object + def test_func(card): + return True + + with self.assertRaises(Exception) as context: + test_func("not a notecard") + self.assertEqual(str(context.exception), "Notecard object required") + + @unittest.skipIf(sys.implementation.name != "cpython", "Function metadata only preserved in CPython") + def test_validate_card_object_preserves_metadata(self): + @validate_card_object + def test_func(card): + """Test function docstring.""" + return True + + self.assertEqual(test_func.__name__, "test_func") + self.assertEqual(test_func.__doc__, "Test function docstring.") + + def test_validate_card_object_non_cpython(self): + sys.implementation.name = 'non-cpython' + from notecard.validators import validate_card_object + + @validate_card_object + def test_func(card): + return True + + result = test_func(self.mock_notecard) + self.assertTrue(result) + + def test_validate_card_object_non_cpython_with_invalid_notecard(self): + sys.implementation.name = 'non-cpython' + from notecard.validators import validate_card_object + + @validate_card_object + def test_func(card): + return True + + with self.assertRaises(Exception) as context: + test_func("not a notecard") + self.assertEqual(str(context.exception), "Notecard object required")