|
| 1 | +import os |
| 2 | +from pathlib import Path |
| 3 | +from typing import TYPE_CHECKING |
| 4 | + |
| 5 | +import pytest |
| 6 | +from pyfakefs.fake_filesystem import FakeFilesystem |
| 7 | + |
| 8 | +from cycode.cli.utils.yaml_utils import read_yaml_file, update_yaml_file, write_yaml_file |
| 9 | + |
| 10 | +if TYPE_CHECKING: |
| 11 | + from pytest_mock import MockerFixture |
| 12 | + |
| 13 | +_CLIENT_ID = 'b3a1f2c4-1111-2222-3333-444455556666' |
| 14 | +_CORRUPT_CONTENT = 'cycode_client_id: valid\nycode_client_id\nleftover tail\n' |
| 15 | + |
| 16 | +_DIRECTORY = '/home/user/.cycode' |
| 17 | +_FILENAME = f'{_DIRECTORY}/credentials.yaml' |
| 18 | + |
| 19 | + |
| 20 | +def test_reading_a_corrupt_file_quarantines_it_and_returns_empty(fs: FakeFilesystem) -> None: |
| 21 | + fs.create_dir(_DIRECTORY) |
| 22 | + |
| 23 | + for marker in ('first', 'second'): |
| 24 | + contents = f'{marker}\n{_CORRUPT_CONTENT}' |
| 25 | + with open(_FILENAME, 'w', encoding='UTF-8') as file: |
| 26 | + file.write(contents) |
| 27 | + |
| 28 | + assert read_yaml_file(_FILENAME) == {} |
| 29 | + assert not os.path.exists(_FILENAME) |
| 30 | + # only the most recent corrupt file is kept, so repeated failures cannot pile up |
| 31 | + assert [path.name for path in Path(_DIRECTORY).iterdir()] == ['credentials.yaml.corrupt'] |
| 32 | + assert Path(f'{_FILENAME}.corrupt').read_text(encoding='UTF-8') == contents |
| 33 | + |
| 34 | + |
| 35 | +def test_updating_a_corrupt_file_recovers_instead_of_raising(fs: FakeFilesystem) -> None: |
| 36 | + fs.create_file(_FILENAME, contents=_CORRUPT_CONTENT) |
| 37 | + |
| 38 | + update_yaml_file(_FILENAME, {'cycode_client_id': 'recovered'}) |
| 39 | + |
| 40 | + assert read_yaml_file(_FILENAME) == {'cycode_client_id': 'recovered'} |
| 41 | + |
| 42 | + |
| 43 | +@pytest.mark.parametrize('read_only_path', [_DIRECTORY, _FILENAME]) |
| 44 | +def test_write_is_skipped_on_a_read_only_filesystem( |
| 45 | + fs: FakeFilesystem, mocker: 'MockerFixture', read_only_path: str |
| 46 | +) -> None: |
| 47 | + fs.create_dir(_DIRECTORY) |
| 48 | + write_yaml_file(_FILENAME, {'cycode_client_id': 'original'}) |
| 49 | + |
| 50 | + real_access = os.access |
| 51 | + mocker.patch( |
| 52 | + 'os.access', |
| 53 | + side_effect=lambda path, mode: ( |
| 54 | + False if mode == os.W_OK and str(path) == read_only_path else real_access(path, mode) |
| 55 | + ), |
| 56 | + ) |
| 57 | + |
| 58 | + write_yaml_file(_FILENAME, {'cycode_client_id': 'updated'}) |
| 59 | + |
| 60 | + assert read_yaml_file(_FILENAME) == {'cycode_client_id': 'original'} |
| 61 | + |
| 62 | + |
| 63 | +def test_write_leaves_no_temporary_files_behind(fs: FakeFilesystem) -> None: |
| 64 | + fs.create_dir(_DIRECTORY) |
| 65 | + write_yaml_file(_FILENAME, {'cycode_client_id': _CLIENT_ID}) |
| 66 | + |
| 67 | + assert [path.name for path in Path(_DIRECTORY).iterdir()] == ['credentials.yaml'] |
| 68 | + |
| 69 | + |
| 70 | +# Stands in for a concurrent writer stopping partway through: the existing file must survive untouched, |
| 71 | +# which is what truncating it up front could never guarantee. |
| 72 | +def test_a_failed_write_leaves_the_existing_file_untouched(fs: FakeFilesystem, mocker: 'MockerFixture') -> None: |
| 73 | + fs.create_dir(_DIRECTORY) |
| 74 | + write_yaml_file(_FILENAME, {'cycode_client_id': 'original'}) |
| 75 | + mocker.patch('yaml.safe_dump', side_effect=OSError('no space left on device')) |
| 76 | + |
| 77 | + with pytest.raises(OSError, match='no space left on device'): |
| 78 | + write_yaml_file(_FILENAME, {'cycode_client_id': 'updated'}) |
| 79 | + |
| 80 | + mocker.stopall() |
| 81 | + assert read_yaml_file(_FILENAME) == {'cycode_client_id': 'original'} |
| 82 | + assert [path.name for path in Path(_DIRECTORY).iterdir()] == ['credentials.yaml'] |
0 commit comments