Procházet zdrojové kódy

Add automated tests for the systemd credential hook (#966).

Dan Helfman před 4 měsíci
rodič
revize
5baf091853

+ 3 - 1
borgmatic/config/credential.py

@@ -4,7 +4,9 @@ import borgmatic.hooks.dispatch
 def resolve_credentials(config, item=None):
     '''
     Resolves values like "!credential hookname credentialname" from the given configuration by
-    calling relevant hooks to get the actual credential values.
+    calling relevant hooks to get the actual credential values. The item parameter is used to
+    support recursing through the config hierarchy; it represents the current piece of config being
+    looked at.
 
     Raise ValueError if the config could not be parsed or the credential could not be loaded.
     '''

+ 2 - 4
borgmatic/config/load.py

@@ -69,7 +69,7 @@ def include_configuration(loader, filename_node, include_directory, config_paths
         ]
 
     raise ValueError(
-        'The value given for the !include tag is not supported; use a single filename or a list of filenames instead'
+        'The value given for the !include tag is invalid; use a single filename or a list of filenames instead'
     )
 
 
@@ -116,9 +116,7 @@ def reserialize_tag_node(loader, tag_node):
     if isinstance(tag_node.value, str):
         return f'{tag_node.tag} {tag_node.value}'
 
-    raise ValueError(
-        f'The value given for the {tag_node.tag} tag is not supported; use a string instead'
-    )
+    raise ValueError(f'The value given for the {tag_node.tag} tag is invalid; use a string instead')
 
 
 class Include_constructor(ruamel.yaml.SafeConstructor):

+ 14 - 0
tests/integration/config/test_load.py

@@ -225,6 +225,20 @@ def test_load_configuration_merges_multiple_file_include():
     assert config_paths == {'config.yaml', '/tmp/include1.yaml', '/tmp/include2.yaml', 'other.yaml'}
 
 
+def test_load_configuration_passes_through_credential_tag():
+    builtins = flexmock(sys.modules['builtins'])
+    flexmock(module.os).should_receive('getcwd').and_return('/tmp')
+    flexmock(module.os.path).should_receive('isabs').and_return(False)
+    flexmock(module.os.path).should_receive('exists').and_return(True)
+    config_file = io.StringIO('key: !credential foo bar')
+    config_file.name = 'config.yaml'
+    builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
+    config_paths = {'other.yaml'}
+
+    assert module.load_configuration('config.yaml', config_paths) == {'key': '!credential foo bar'}
+    assert config_paths == {'config.yaml', 'other.yaml'}
+
+
 def test_load_configuration_with_retain_tag_merges_include_but_keeps_local_values():
     builtins = flexmock(sys.modules['builtins'])
     flexmock(module.os).should_receive('getcwd').and_return('/tmp')

+ 30 - 0
tests/integration/config/test_validate.py

@@ -283,3 +283,33 @@ def test_parse_configuration_applies_normalization_after_environment_variable_in
     }
     assert config_paths == {'/tmp/config.yaml'}
     assert logs
+
+
+def test_parse_configuration_interpolates_credentials():
+    mock_config_and_schema(
+        '''
+        source_directories:
+            - /home
+
+        repositories:
+            - path: hostname.borg
+
+        encryption_passphrase: !credential systemd mycredential
+        '''
+    )
+    flexmock(os.environ).should_receive('get').replace_with(lambda variable_name: '/var')
+    credential_stream = io.StringIO('password')
+    credential_stream.name = '/var/mycredential'
+    builtins = flexmock(sys.modules['builtins'])
+    builtins.should_receive('open').with_args('/var/mycredential').and_return(credential_stream)
+
+    config, config_paths, logs = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
+
+    assert config == {
+        'source_directories': ['/home'],
+        'repositories': [{'path': 'hostname.borg'}],
+        'encryption_passphrase': 'password',
+        'bootstrap': {},
+    }
+    assert config_paths == {'/tmp/config.yaml'}
+    assert logs == []

+ 18 - 2
tests/unit/commands/test_borgmatic.py

@@ -1114,7 +1114,17 @@ def test_run_actions_runs_multiple_actions_in_argument_order():
     )
 
 
-def test_load_configurations_collects_parsed_configurations_and_logs():
+@pytest.mark.parametrize(
+    'resolve_env,resolve_credentials',
+    (
+        (True, True),
+        (False, True),
+        (True, False),
+    ),
+)
+def test_load_configurations_collects_parsed_configurations_and_logs(
+    resolve_env, resolve_credentials
+):
     configuration = flexmock()
     other_configuration = flexmock()
     test_expected_logs = [flexmock(), flexmock()]
@@ -1123,7 +1133,13 @@ def test_load_configurations_collects_parsed_configurations_and_logs():
         configuration, ['/tmp/test.yaml'], test_expected_logs
     ).and_return(other_configuration, ['/tmp/other.yaml'], other_expected_logs)
 
-    configs, config_paths, logs = tuple(module.load_configurations(('test.yaml', 'other.yaml')))
+    configs, config_paths, logs = tuple(
+        module.load_configurations(
+            ('test.yaml', 'other.yaml'),
+            resolve_env=resolve_env,
+            resolve_credentials=resolve_credentials,
+        )
+    )
 
     assert configs == {'test.yaml': configuration, 'other.yaml': other_configuration}
     assert config_paths == ['/tmp/other.yaml', '/tmp/test.yaml']

+ 127 - 0
tests/unit/config/test_credential.py

@@ -0,0 +1,127 @@
+import pytest
+from flexmock import flexmock
+
+
+from borgmatic.config import credential as module
+
+
+def test_resolve_credentials_ignores_string_without_credential_tag():
+    flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hook').never()
+
+    module.resolve_credentials(config=flexmock(), item='!no credentials here')
+
+
+def test_resolve_credentials_with_invalid_credential_tag_raises():
+    flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hook').never()
+
+    with pytest.raises(ValueError):
+        module.resolve_credentials(config=flexmock(), item='!credential systemd')
+
+
+def test_resolve_credentials_with_valid_credential_tag_loads_credential():
+    config = flexmock()
+    flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hook').with_args(
+        'load_credential',
+        config,
+        'systemd',
+        'mycredential',
+    ).and_return('result').once()
+
+    assert (
+        module.resolve_credentials(config=config, item='!credential systemd mycredential')
+        == 'result'
+    )
+
+
+def test_resolve_credentials_with_list_recurses_and_loads_credentials():
+    config = flexmock()
+    flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hook').with_args(
+        'load_credential',
+        config,
+        'systemd',
+        'mycredential',
+    ).and_return('result1').once()
+    flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hook').with_args(
+        'load_credential',
+        config,
+        'systemd',
+        'othercredential',
+    ).and_return('result2').once()
+
+    assert module.resolve_credentials(
+        config=config,
+        item=['!credential systemd mycredential', 'nope', '!credential systemd othercredential'],
+    ) == ['result1', 'nope', 'result2']
+
+
+def test_resolve_credentials_with_dict_recurses_and_loads_credentials():
+    config = flexmock()
+    flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hook').with_args(
+        'load_credential',
+        config,
+        'systemd',
+        'mycredential',
+    ).and_return('result1').once()
+    flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hook').with_args(
+        'load_credential',
+        config,
+        'systemd',
+        'othercredential',
+    ).and_return('result2').once()
+
+    assert module.resolve_credentials(
+        config=config,
+        item={
+            'a': '!credential systemd mycredential',
+            'b': 'nope',
+            'c': '!credential systemd othercredential',
+        },
+    ) == {'a': 'result1', 'b': 'nope', 'c': 'result2'}
+
+
+def test_resolve_credentials_with_list_of_dicts_recurses_and_loads_credentials():
+    config = flexmock()
+    flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hook').with_args(
+        'load_credential',
+        config,
+        'systemd',
+        'mycredential',
+    ).and_return('result1').once()
+    flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hook').with_args(
+        'load_credential',
+        config,
+        'systemd',
+        'othercredential',
+    ).and_return('result2').once()
+
+    assert module.resolve_credentials(
+        config=config,
+        item=[
+            {'a': '!credential systemd mycredential', 'b': 'nope'},
+            {'c': '!credential systemd othercredential'},
+        ],
+    ) == [{'a': 'result1', 'b': 'nope'}, {'c': 'result2'}]
+
+
+def test_resolve_credentials_with_dict_of_lists_recurses_and_loads_credentials():
+    config = flexmock()
+    flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hook').with_args(
+        'load_credential',
+        config,
+        'systemd',
+        'mycredential',
+    ).and_return('result1').once()
+    flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hook').with_args(
+        'load_credential',
+        config,
+        'systemd',
+        'othercredential',
+    ).and_return('result2').once()
+
+    assert module.resolve_credentials(
+        config=config,
+        item={
+            'a': ['!credential systemd mycredential', 'nope'],
+            'b': ['!credential systemd othercredential'],
+        },
+    ) == {'a': ['result1', 'nope'], 'b': ['result2']}

+ 14 - 0
tests/unit/config/test_load.py

@@ -43,3 +43,17 @@ def test_probe_and_include_file_with_relative_path_and_missing_files_raises():
 
     with pytest.raises(FileNotFoundError):
         module.probe_and_include_file('include.yaml', ['/etc', '/var'], config_paths=set())
+
+
+def test_reserialize_tag_node_turns_it_into_string():
+    assert (
+        module.reserialize_tag_node(loader=flexmock(), tag_node=flexmock(tag='!tag', value='value'))
+        == '!tag value'
+    )
+
+
+def test_reserialize_tag_node_with_invalid_value_raises():
+    with pytest.raises(ValueError):
+        assert module.reserialize_tag_node(
+            loader=flexmock(), tag_node=flexmock(tag='!tag', value=['value'])
+        )

+ 51 - 0
tests/unit/hooks/credential/test_systemd.py

@@ -0,0 +1,51 @@
+import io
+import sys
+
+from flexmock import flexmock
+import pytest
+
+from borgmatic.hooks.credential import systemd as module
+
+
+def test_load_credential_without_credentials_directory_raises():
+    flexmock(module.os.environ).should_receive('get').with_args('CREDENTIALS_DIRECTORY').and_return(
+        None
+    )
+
+    with pytest.raises(ValueError):
+        module.load_credential(hook_config={}, config={}, credential_name='mycredential')
+
+
+def test_load_credential_with_invalid_credential_name_raises():
+    flexmock(module.os.environ).should_receive('get').with_args('CREDENTIALS_DIRECTORY').and_return(
+        '/var'
+    )
+
+    with pytest.raises(ValueError):
+        module.load_credential(hook_config={}, config={}, credential_name='../../my!@#$credential')
+
+
+def test_load_credential_reads_named_credential_from_file():
+    flexmock(module.os.environ).should_receive('get').with_args('CREDENTIALS_DIRECTORY').and_return(
+        '/var'
+    )
+    credential_stream = io.StringIO('password')
+    credential_stream.name = '/var/mycredential'
+    builtins = flexmock(sys.modules['builtins'])
+    builtins.should_receive('open').with_args('/var/mycredential').and_return(credential_stream)
+
+    assert (
+        module.load_credential(hook_config={}, config={}, credential_name='mycredential')
+        == 'password'
+    )
+
+
+def test_load_credential_with_file_not_found_error_raises():
+    flexmock(module.os.environ).should_receive('get').with_args('CREDENTIALS_DIRECTORY').and_return(
+        '/var'
+    )
+    builtins = flexmock(sys.modules['builtins'])
+    builtins.should_receive('open').with_args('/var/mycredential').and_raise(FileNotFoundError)
+
+    with pytest.raises(ValueError):
+        module.load_credential(hook_config={}, config={}, credential_name='mycredential')