Explorar o código

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

Dan Helfman hai 4 meses
pai
achega
5baf091853

+ 3 - 1
borgmatic/config/credential.py

@@ -4,7 +4,9 @@ import borgmatic.hooks.dispatch
 def resolve_credentials(config, item=None):
 def resolve_credentials(config, item=None):
     '''
     '''
     Resolves values like "!credential hookname credentialname" from the given configuration by
     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.
     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(
     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):
     if isinstance(tag_node.value, str):
         return f'{tag_node.tag} {tag_node.value}'
         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):
 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'}
     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():
 def test_load_configuration_with_retain_tag_merges_include_but_keeps_local_values():
     builtins = flexmock(sys.modules['builtins'])
     builtins = flexmock(sys.modules['builtins'])
     flexmock(module.os).should_receive('getcwd').and_return('/tmp')
     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 config_paths == {'/tmp/config.yaml'}
     assert logs
     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()
     configuration = flexmock()
     other_configuration = flexmock()
     other_configuration = flexmock()
     test_expected_logs = [flexmock(), 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
         configuration, ['/tmp/test.yaml'], test_expected_logs
     ).and_return(other_configuration, ['/tmp/other.yaml'], other_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 configs == {'test.yaml': configuration, 'other.yaml': other_configuration}
     assert config_paths == ['/tmp/other.yaml', '/tmp/test.yaml']
     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):
     with pytest.raises(FileNotFoundError):
         module.probe_and_include_file('include.yaml', ['/etc', '/var'], config_paths=set())
         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')