#!/usr/bin/env python # -*- coding: utf-8 -*- import os from pathlib import Path import unittest.mock as mock import pytest # type: ignore from kittengroomer import FileBase, KittenGroomerBase from kittengroomer.helpers import ImplementationRequired skip = pytest.mark.skip xfail = pytest.mark.xfail fixture = pytest.fixture class TestFileBase: @fixture(scope='class') def src_dir_path(self, tmp_path_factory): return tmp_path_factory.mktemp('src') @fixture(scope='class') def dest_dir_path(self, tmp_path_factory): return tmp_path_factory.mktemp('dest') @fixture(scope='class') def tmpfile_path(self, src_dir_path): path_src_file = src_dir_path / 'test.txt' with path_src_file.open('w') as f: f.write('testing') return path_src_file @fixture(scope='class') def symlink_file_path(self, src_dir_path, tmpfile_path): symlink_path = src_dir_path / 'symlinked' symlink_path.symlink_to(tmpfile_path) return symlink_path @fixture def text_file(self, tmpfile_path, dest_dir_path): with mock.patch( 'kittengroomer.helpers.magic.from_file', return_value='text/plain' ): dst_path = dest_dir_path / 'test.txt' file = FileBase(tmpfile_path, dst_path) return file # Constructor behavior @mock.patch('kittengroomer.helpers.magic') def test_init_identify_filename(self, mock_libmagic): """Init should identify the filename correctly for src_path.""" src_path = Path('src/test.txt') dst_path = Path('dst/test.txt') file = FileBase(src_path, dst_path) assert file.filename == 'test.txt' @mock.patch('kittengroomer.helpers.magic') def test_init_identify_extension(self, mock_libmagic): """Init should identify the extension for src_path.""" src_path = Path('src/test.txt') dst_path = Path('dst/test.txt') file = FileBase(src_path, dst_path) assert file.extension == '.txt' @mock.patch('kittengroomer.helpers.magic') def test_init_uppercase_extension(self, mock_libmagic): """Init should coerce uppercase extension to lowercase""" src_path = Path('src/test.txt') dst_path = Path('dst/test.txt') file = FileBase(src_path, dst_path) assert file.extension == '.txt' @mock.patch('kittengroomer.helpers.magic') def test_has_extension_true(self, mock_libmagic): """If the file has an extension, has_extension should == True.""" src_path = Path('src/test.txt') dst_path = Path('dst/test.txt') file = FileBase(src_path, dst_path) assert file.has_extension is True @mock.patch('kittengroomer.helpers.magic') def test_has_extension_false(self, mock_libmagic): """If the file has no extension, has_extensions should == False.""" src_path = Path('src/test') dst_path = Path('dst/test') file = FileBase(src_path, dst_path) assert file.has_extension is False def test_init_file_doesnt_exist(self): """Init should raise an exception if the file doesn't exist.""" with pytest.raises(FileNotFoundError): FileBase(Path('non_existent'), Path('non_existent')) def test_init_srcpath_is_directory(self, tmpdir): """Init should raise an exception if given a path to a directory.""" with pytest.raises(IsADirectoryError): FileBase(Path(tmpdir.strpath), Path(tmpdir.strpath)) @mock.patch('kittengroomer.helpers.magic') def test_init_symlink(self, mock_libmagic, symlink_file_path, tmpdir): """Init should properly identify symlinks.""" file = FileBase(symlink_file_path, Path(tmpdir.strpath)) assert file.mimetype == 'inode/symlink' @mock.patch('kittengroomer.helpers.magic') def test_is_symlink_attribute(self, mock_libmagic, symlink_file_path, tmpdir): """If a file is a symlink, is_symlink should return True.""" file = FileBase(symlink_file_path, Path(tmpdir.strpath)) assert file.is_symlink is True def test_init_mimetype_attribute_assigned_correctly(self): """When libmagic returns a given mimetype, the mimetype should be assigned properly.""" with mock.patch('kittengroomer.helpers.magic.from_file', return_value='text/plain'): file = FileBase(Path('non_existent'), Path('non_existent')) assert file.mimetype == 'text/plain' def test_maintype_and_subtype_attributes(self): """If a file has a full mimetype, maintype and subtype should == the appropriate values.""" with mock.patch('kittengroomer.helpers.magic.from_file', return_value='text/plain'): file = FileBase(Path('non_existent'), Path('non_existent')) assert file.maintype == 'text' assert file.subtype == 'plain' def test_has_mimetype_no_full_type(self): """If a file doesn't have a full mimetype has_mimetype should == False.""" with mock.patch('kittengroomer.helpers.magic.from_file', return_value='data'): file = FileBase(Path('non_existent'), Path('non_existent')) assert file.has_mimetype is False def test_has_mimetype_mimetype_is_none(self): """If a file doesn't have a full mimetype has_mimetype should == False.""" with mock.patch('kittengroomer.helpers.FileBase._determine_mimetype', return_value=None): file = FileBase(Path('non_existent'), Path('non_existent')) assert file.has_mimetype is False # File properties def get_property_doesnt_exist(self, text_file): """Trying to get a property that doesn't exist should return None.""" assert text_file.get_property('thing') is None def get_property_builtin(self, text_file): """Getting a property that's been set should return that property.""" assert text_file.get_property('is_dangerous') is False def get_property_user_defined(self, text_file): """Getting a user defined property should return that property.""" text_file._user_defined = {'thing': True} assert text_file.get_property('thing') is True def set_property_user_defined(self, text_file): """Setting a non-default property should make it available for get_property.""" text_file.set_property('thing', True) assert text_file.get_property('thing') is True def set_property_builtin(self, text_file): """Setting a builtin property should assign that property.""" text_file.set_property('is_dangerous', True) assert text_file.get_property('is_dangerous') is True def test_add_new_description(self, text_file): """Adding a new description should add it to the list of description strings.""" text_file.add_description('thing') assert text_file.get_property('description_string') == 'thing' def test_add_description_exists(self, text_file): """Adding a description that already exists shouldn't duplicate it.""" text_file.add_description('thing') text_file.add_description('thing') assert text_file.get_property('description_string') == 'thing' def test_add_multiple_descriptions(self, text_file): text_file.add_description('thing') text_file.add_description('foo') assert text_file.get_property('description_string') == 'thing, foo' def test_add_description_not_string(self, text_file): """Adding a description that isn't a string should raise an error.""" with pytest.raises(TypeError): text_file.add_description(123) def test_add_new_error(self, text_file): """Adding a new error should add it to the dict of errors.""" text_file.add_error(Exception, 'thing') assert text_file.get_property('_errors') == {Exception: 'thing'} def test_normal_file_mark_dangerous(self, text_file): """Marking a file dangerous should identify it as dangerous.""" text_file.make_dangerous() assert text_file.is_dangerous is True def test_normal_file_mark_dangerous_filename_change(self, text_file): """Marking a file dangerous should mangle the filename.""" filename = text_file.filename text_file.make_dangerous() assert text_file.filename == 'DANGEROUS_{}_DANGEROUS'.format(filename) def test_normal_file_mark_dangerous_add_description(self, text_file): """Marking a file as dangerous and passing in a description should add that description to the file.""" text_file.make_dangerous('thing') assert text_file.get_property('description_string') == 'thing' def test_dangerous_file_mark_dangerous(self, text_file): """Marking a dangerous file as dangerous should do nothing, and the file should remain dangerous.""" text_file.make_dangerous() text_file.make_dangerous() assert text_file.is_dangerous is True def test_force_ext_change_filepath(self, text_file): """Force_ext should modify the path of the file to end in the new extension.""" text_file.force_ext('.test') assert text_file.dst_path.name.endswith('.test') def test_force_ext_add_dot(self, text_file): """Force_ext should add a dot to an extension given without one.""" text_file.force_ext('test') assert text_file.dst_path.name.endswith('.test') def test_force_ext_change_extension_attr(self, text_file): """Force_ext should only modify the extension of the destination file""" text_file.force_ext('.thing') assert text_file.extension == '.txt' def test_force_ext_no_change(self, text_file): """Force_ext should do nothing if the current extension is the same as the new extension.""" text_file.force_ext('.txt') assert text_file.extension == '.txt' assert '.txt.txt' not in text_file.dst_path.name def test_safe_copy_calls_copy(self, src_dir_path, dest_dir_path): """Calling safe_copy should copy the file from the correct path to the correct destination path.""" file_path = src_dir_path / 'test.txt' with open(file_path, 'w+') as file: file.write('') dst_path = dest_dir_path / 'test.txt' with mock.patch('kittengroomer.helpers.magic.from_file', return_value='text/plain'): file = FileBase(file_path, dst_path) with mock.patch('kittengroomer.helpers.shutil.copy') as mock_copy: file.safe_copy() mock_copy.assert_called_once_with(file_path, dst_path) def test_safe_copy_removes_exec_perms(self): """`safe_copy` should create a file that doesn't have any of the executable bits set.""" pass def test_safe_copy_makedir_doesnt_exist(self): """Calling safe_copy should create intermediate directories in the path if they don't exist.""" pass def test_safe_copy_makedir_exists(self): """Calling safe_copy when some intermediate directories exist should result in the creation of the full path and the file.""" pass def test_create_metadata_file_new(self): pass def test_create_metadata_file_already_exists(self): pass class TestLogging: def test_computehash(self): """Computehash should return the correct sha256 hash of a given file.""" pass class TestKittenGroomerBase: @fixture(scope='class') def src_dir_path(self, tmpdir_factory): return Path(tmpdir_factory.mktemp('src').strpath) @fixture(scope='class') def dest_dir_path(self, tmpdir_factory): return Path(tmpdir_factory.mktemp('dest').strpath) @fixture(scope='class') def tmpfile_path(self, src_dir_path): path_src_file = src_dir_path / 'test.txt' with path_src_file.open('w') as f: f.write('testing') return path_src_file @fixture def groomer(self, src_dir_path, dest_dir_path): return KittenGroomerBase(src_dir_path, dest_dir_path) def test_list_all_files_includes_file(self, src_dir_path, tmpfile_path, groomer): """Calling list_all_files should include files in the given path.""" files = groomer.list_all_files(src_dir_path) assert tmpfile_path in files def test_list_all_files_excludes_dir(self, tmpdir, groomer): """Calling list_all_files shouldn't include directories in the given path.""" testdir = tmpdir.join('testdir') os.mkdir(testdir.strpath) files = groomer.list_all_files(tmpdir.strpath) assert testdir.strpath not in files def test_safe_remove(self, groomer, src_dir_path): """Calling safe_remove should not raise an Exception if trying to remove a file that doesn't exist.""" groomer.safe_remove(src_dir_path / 'thing') def test_safe_mkdir_file_exists(self, groomer, dest_dir_path): """Calling safe_mkdir should not overwrite an existing directory.""" filepath = dest_dir_path / 'thing' filepath.mkdir() groomer.safe_mkdir(filepath) def test_processdir_not_implemented(self, groomer): """Calling processdir should raise an Implementation Required error.""" with pytest.raises(ImplementationRequired): groomer.processdir('.', '.')