diff --git a/borgmatic/actions/restore.py b/borgmatic/actions/restore.py index 2cc667d8..fe68d256 100644 --- a/borgmatic/actions/restore.py +++ b/borgmatic/actions/restore.py @@ -305,13 +305,13 @@ def collect_dumps_from_archive( # If we've successfully loaded any dumps metadata, we're done. if dumps_from_archive: - logger.debug(f'Collecting database dumps from archive data source dumps metadata files') + logger.debug('Collecting database dumps from archive data source dumps metadata files') return dumps_from_archive # No dumps metadata files were found, so for backwards compatibility, fall back to parsing the # paths of dumps found in the archive to get their respective dump metadata. - logger.debug(f'Collecting database dumps from archive data source dump paths (fallback)') + logger.debug('Collecting database dumps from archive data source dump paths (fallback)') borgmatic_source_directory = str( pathlib.Path(borgmatic.config.paths.get_borgmatic_source_directory(config)), ) diff --git a/borgmatic/hooks/data_source/dump.py b/borgmatic/hooks/data_source/dump.py index 5f0b4c35..4c484fe3 100644 --- a/borgmatic/hooks/data_source/dump.py +++ b/borgmatic/hooks/data_source/dump.py @@ -48,7 +48,7 @@ def write_data_source_dumps_metadata(borgmatic_runtime_directory, hook_name, dum dumps_metadata_path = os.path.join(borgmatic_runtime_directory, hook_name, 'dumps.json') try: - with open(dumps_metadata_path, 'w') as metadata_file: + with open(dumps_metadata_path, 'w', encoding='utf-8') as metadata_file: json.dump([dump._asdict() for dump in dumps_metadata], metadata_file, sort_keys=True) except OSError as error: raise ValueError(f'Error writing to dumps metadata at {dumps_metadata_path}: {error}') @@ -56,8 +56,8 @@ def write_data_source_dumps_metadata(borgmatic_runtime_directory, hook_name, dum def parse_data_source_dumps_metadata(dumps_json, dumps_metadata_path): ''' - Given a dumps metadata JSON string as extracted from an archive, parse it into a tuple of - borgmatic.actions.restore.Dump instances and return them. + Given a dumps metadata JSON string as extracted from an archive and its path within the archive, + parse it into a tuple of borgmatic.actions.restore.Dump instances and return them. Raise ValueError if parsing the JSON results in a JSON decode error or the data does not have the expected keys. diff --git a/tests/unit/hooks/data_source/test_dump.py b/tests/unit/hooks/data_source/test_dump.py index d1b48ede..7486dfe8 100644 --- a/tests/unit/hooks/data_source/test_dump.py +++ b/tests/unit/hooks/data_source/test_dump.py @@ -1,3 +1,6 @@ +import io +import sys + import pytest from flexmock import flexmock @@ -31,6 +34,66 @@ def test_make_data_source_dump_filename_with_invalid_name_raises(): module.make_data_source_dump_filename('databases', 'invalid/name') +def test_write_data_source_dumps_metadata_writes_json_to_file(): + dumps_metadata = [ + module.borgmatic.actions.restore.Dump('databases', 'foo'), + module.borgmatic.actions.restore.Dump('databases', 'bar'), + ] + dumps_stream = io.StringIO('password') + dumps_stream.name = '/run/borgmatic/databases/dumps.json' + builtins = flexmock(sys.modules['builtins']) + builtins.should_receive('open').with_args(dumps_stream.name, 'w', encoding='utf-8').and_return( + dumps_stream + ) + flexmock(dumps_stream).should_receive('close') # Prevent close() so getvalue() below works. + + module.write_data_source_dumps_metadata('/run/borgmatic', 'databases', dumps_metadata) + + assert ( + dumps_stream.getvalue() + == '[{"data_source_name": "foo", "hook_name": "databases", "hostname": "localhost", "port": null}, {"data_source_name": "bar", "hook_name": "databases", "hostname": "localhost", "port": null}]' + ) + + +def test_write_data_source_dumps_metadata_with_operating_system_error_raises(): + dumps_metadata = [ + module.borgmatic.actions.restore.Dump('databases', 'foo'), + module.borgmatic.actions.restore.Dump('databases', 'bar'), + ] + dumps_stream = io.StringIO('password') + dumps_stream.name = '/run/borgmatic/databases/dumps.json' + builtins = flexmock(sys.modules['builtins']) + builtins.should_receive('open').with_args(dumps_stream.name, 'w', encoding='utf-8').and_raise( + OSError + ) + + with pytest.raises(ValueError): + module.write_data_source_dumps_metadata('/run/borgmatic', 'databases', dumps_metadata) + + +def test_parse_data_source_dumps_metadata_converts_json_to_dump_instances(): + dumps_json = '[{"data_source_name": "foo", "hook_name": "databases", "hostname": "localhost", "port": null}, {"data_source_name": "bar", "hook_name": "databases", "hostname": "example.org", "port": 1234}]' + + assert module.parse_data_source_dumps_metadata( + dumps_json, 'borgmatic/databases/dumps.json' + ) == ( + module.borgmatic.actions.restore.Dump('databases', 'foo'), + module.borgmatic.actions.restore.Dump('databases', 'bar', 'example.org', 1234), + ) + + +def test_parse_data_source_dumps_metadata_with_invalid_json_raises(): + with pytest.raises(ValueError): + module.parse_data_source_dumps_metadata('[{', 'borgmatic/databases/dumps.json') + + +def test_parse_data_source_dumps_metadata_with_unknown_keys_raises(): + dumps_json = '[{"data_source_name": "foo", "hook_name": "databases", "wtf": "is this"}]' + + with pytest.raises(ValueError): + module.parse_data_source_dumps_metadata(dumps_json, 'borgmatic/databases/dumps.json') + + def test_create_parent_directory_for_dump_does_not_raise(): flexmock(module.os).should_receive('makedirs')