def test_cli_quiet(self): """Test copying of a password without echoing the entry's text.""" # Generate a password and some additional text for a dummy password store entry. a_password = random_string() additional_text = random_string() raw_entry = a_password + "\n\n" + additional_text # Prepare a mock method to test that the password is copied, # but without actually invoking the `pass' program. copy_password_method = MagicMock() # Some voodoo to mock methods in classes that # have yet to be instantiated follows :-). mocked_class = type("TestPasswordEntry", (PasswordEntry, ), dict(text=raw_entry)) setattr(mocked_class, "copy_password", copy_password_method) with PatchedAttribute(qpass, "PasswordEntry", mocked_class): with PatchedAttribute(cli, "is_clipboard_supported", lambda: True): with TemporaryDirectory() as directory: touch(os.path.join(directory, "foo.gpg")) returncode, output = run_cli( main, "--password-store=%s" % directory, "--quiet", "foo") # Make sure the command succeeded. assert returncode == 0 # Make sure the password was copied to the clipboard. assert copy_password_method.called # Make sure no output was generated. assert not output.strip()
def test_natural_order(self): """Verify the natural order sorting of the snippets in the configuration file.""" first = "This should be the first line.\n" middle = "This should appear in the middle.\n" last = "This should be the last line.\n" with TemporaryDirectory() as temporary_directory: filename = os.path.join(temporary_directory, 'config') directory = '%s.d' % filename # Create the configuration file and directory. write_file(filename) os.makedirs(directory) # Create the files with configuration snippets. for number, contents in (1, first), (5, middle), (10, last): write_file(os.path.join(directory, '%i.conf' % number), contents) # Use the command line interface to update the configuration file. returncode, output = run_cli(main, filename) assert returncode == 0 # Make sure the configuration file was updated. assert os.path.isfile(filename) assert os.path.getsize(filename) > 0 with open(filename) as handle: lines = handle.readlines() # Make sure all of the expected lines are present. assert first in lines assert middle in lines assert last in lines # Check the order of the lines (natural order instead of lexicographical). assert lines.index(first) < lines.index(middle) assert lines.index(middle) < lines.index(last)
def test_filename_patterns(self): """Test support for filename patterns in configuration files.""" with TemporaryDirectory(prefix='rotate-backups-', suffix='-test-suite') as root: for subdirectory in 'laptop', 'vps': os.makedirs(os.path.join(root, subdirectory)) config_file = os.path.join(root, 'rotate-backups.ini') parser = configparser.RawConfigParser() pattern = os.path.join(root, '*') parser.add_section(pattern) parser.set(pattern, 'daily', '7') parser.set(pattern, 'weekly', '4') parser.set(pattern, 'monthly', 'always') with open(config_file, 'w') as handle: parser.write(handle) # Check that the configured rotation scheme is applied. default_scheme = dict(monthly='always') program = RotateBackups(config_file=config_file, rotation_scheme=default_scheme) program.load_config_file(os.path.join(root, 'laptop')) assert program.rotation_scheme != default_scheme # Check that the available locations are matched. available_locations = [ location for location, rotation_scheme, options in load_config_file(config_file) ] assert len(available_locations) == 2 assert any(location.directory == os.path.join(root, 'laptop') for location in available_locations) assert any(location.directory == os.path.join(root, 'vps') for location in available_locations)
def test_exclude_list(self): """Test that ``rsync-system-backup --exclude`` works as intended.""" with TemporaryDirectory() as temporary_directory: source = os.path.join(temporary_directory, 'source') destination = os.path.join(temporary_directory, 'destination') latest_directory = os.path.join(destination, 'latest') # Create a source directory with two files. os.makedirs(source) with open(os.path.join(source, 'included.txt'), 'w') as handle: handle.write("This file should be included.\n") with open(os.path.join(source, 'excluded.txt'), 'w') as handle: handle.write("This file should be excluded.\n") # Run the program through the command line interface. exit_code, output = run_cli( main, '--backup', '--exclude=excluded.txt', '--no-sudo', '--disable-notifications', source, latest_directory, ) assert exit_code == 0 # Make sure one of the files was copied and the other wasn't. assert os.path.isfile( os.path.join(latest_directory, 'included.txt')) assert not os.path.exists( os.path.join(latest_directory, 'excluded.txt'))
def test_missing_password_store_error(self): """Test the MissingPasswordStoreError exception.""" with TemporaryDirectory() as directory: missing = os.path.join(directory, 'missing') program = PasswordStore(directory=missing) self.assertRaises(MissingPasswordStoreError, program.ensure_directory_exists)
def test_no_matching_password_error(self): """Test the NoMatchingPasswordError exception.""" with TemporaryDirectory() as directory: touch(os.path.join(directory, 'Whatever.gpg')) program = PasswordStore(directory=directory) self.assertRaises(NoMatchingPasswordError, program.smart_search, 'x')
def test_show_entry(self): """Test showing of an entry on the terminal.""" password = random_string() # Some voodoo to mock methods in classes that # have yet to be instantiated follows :-). mocked_class = type( 'TestPasswordEntry', (PasswordEntry, ), dict(text=password), ) with PatchedAttribute(qpass, 'PasswordEntry', mocked_class): with TemporaryDirectory() as directory: name = 'some/random/password' touch(os.path.join(directory, '%s.gpg' % name)) returncode, output = run_cli( main, '--password-store=%s' % directory, '--no-clipboard', name, ) assert returncode == 0 assert dedent(output) == dedent( """ {title} Password: {password} """, title=name.replace('/', ' / '), password=password, )
def test_argument_validation(self): """Test argument validation.""" # Test that an invalid ionice scheduling class causes an error to be reported. returncode, output = run_cli(main, '--ionice=unsupported-class') assert returncode != 0 # Test that an invalid rotation scheme causes an error to be reported. returncode, output = run_cli(main, '--hourly=not-a-number') assert returncode != 0 # Argument validation tests that require an empty directory. with TemporaryDirectory(prefix='rotate-backups-', suffix='-test-suite') as root: # Test that non-existing directories cause an error to be reported. returncode, output = run_cli(main, os.path.join(root, 'does-not-exist')) assert returncode != 0 # Test that loading of a custom configuration file raises an # exception when the configuration file cannot be loaded. self.assertRaises( ValueError, lambda: list( load_config_file(os.path.join(root, 'rotate-backups.ini'))) ) # Test that an empty rotation scheme raises an exception. self.create_sample_backup_set(root) self.assertRaises( ValueError, lambda: RotateBackups(rotation_scheme={}).rotate_backups(root)) # Argument validation tests that assume the current user isn't root. if os.getuid() != 0: # I'm being lazy and will assume that this test suite will only be # run on systems where users other than root do not have access to # /root. returncode, output = run_cli(main, '-n', '/root') assert returncode != 0
def test_encrypted_backup(self): """ Test a backup to an encrypted filesystem. To make this test work you need to make the following additions to system files (and create ``/mnt/rsync-system-backup``): .. code-block:: sh $ grep rsync-system-backup /etc/fstab /dev/mapper/rsync-system-backup /mnt/rsync-system-backup ext4 noauto 0 0 $ grep rsync-system-backup /etc/crypttab rsync-system-backup /tmp/rsync-system-backup.img /tmp/rsync-system-backup.key luks,noauto $ sudo cat /etc/sudoers.d/rsync-system-backup peter ALL=NOPASSWD:/usr/sbin/cryptdisks_start rsync-system-backup peter ALL=NOPASSWD:/usr/sbin/cryptdisks_stop rsync-system-backup peter ALL=NOPASSWD:/bin/mount /mnt/rsync-system-backup peter ALL=NOPASSWD:/bin/umount /mnt/rsync-system-backup peter ALL=NOPASSWD:/sbin/mkfs.ext4 /dev/mapper/rsync-system-backup peter ALL=NOPASSWD:/usr/bin/test -e /dev/mapper/rsync-system-backup peter ALL=NOPASSWD:/bin/mountpoint /mnt/rsync-system-backup peter ALL=NOPASSWD:/usr/bin/test -d /mnt/rsync-system-backup/latest peter ALL=NOPASSWD:/bin/mkdir -p /mnt/rsync-system-backup/latest peter ALL=NOPASSWD:/usr/bin/rsync * /tmp/* /mnt/rsync-system-backup/latest/ peter ALL=NOPASSWD:/bin/cp --archive --link /mnt/rsync-system-backup/latest /mnt/rsync-system-backup/* peter ALL=NOPASSWD:/usr/bin/test -d /mnt/rsync-system-backup peter ALL=NOPASSWD:/usr/bin/test -r /mnt/rsync-system-backup peter ALL=NOPASSWD:/usr/bin/find /mnt/rsync-system-backup * peter ALL=NOPASSWD:/usr/bin/test -w /mnt/rsync-system-backup peter ALL=NOPASSWD:/bin/rm --recursive /mnt/rsync-system-backup/latest Of course you should change ``/etc/sudoers.d/rsync-system-backup`` to replace ``peter`` with your actual username :-). """ if not os.path.isdir(MOUNT_POINT): return self.skipTest("Skipping test because %s doesn't exist!", MOUNT_POINT) with TemporaryDirectory() as source: destination = os.path.join(MOUNT_POINT, 'latest') with prepared_image_file(): # Create a source for testing. self.create_source(source) # Run the program through the command line interface. self.create_encrypted_backup(source, destination) # Unlock the encrypted image file. with unlocked_device(CRYPTO_NAME): # Mount the encrypted filesystem. with active_mountpoint(MOUNT_POINT): # Verify that the backup was successful. self.verify_destination(destination) # Invoke rsync-system-backup using the same command line # arguments, but this time while the encrypted device is # already unlocked and the filesystem is already mounted. self.create_encrypted_backup(source, destination) # Verify that the backup was successful. self.verify_destination(destination) # Invoke rsync-system-backup using the same command line # arguments, but this time while the encrypted device is # already unlocked although the filesystem isn't mounted. self.create_encrypted_backup(source, destination) # Verify that the backup was successful. with active_mountpoint(MOUNT_POINT): self.verify_destination(destination)
def test_temporary_directory(self): """Test :class:`humanfriendly.testing.TemporaryDirectory`.""" with TemporaryDirectory() as directory: assert os.path.isdir(directory) temporary_file = os.path.join(directory, 'some-file') with open(temporary_file, 'w') as handle: handle.write("Hello world!") assert not os.path.exists(temporary_file) assert not os.path.exists(directory)
def test_removal_command(self): """Test that the removal command can be customized.""" with TemporaryDirectory(prefix='rotate-backups-', suffix='-test-suite') as root: today = datetime.datetime.now() for date in today, (today - datetime.timedelta(hours=24)): os.mkdir(os.path.join(root, date.strftime('%Y-%m-%d'))) program = RotateBackups(removal_command=['rmdir'], rotation_scheme=dict(monthly='always')) commands = program.rotate_backups(root, prepare=True) assert any(cmd.command_line[0] == 'rmdir' for cmd in commands)
def test_rsync_module_path_as_destination(self): """Test that destination defaults to ``$RSYNC_MODULE_PATH``.""" with TemporaryDirectory() as temporary_directory: try: os.environ['RSYNC_MODULE_PATH'] = temporary_directory program = RsyncSystemBackup() assert program.destination.directory == temporary_directory assert not program.destination.hostname assert not program.destination.username assert not program.destination.module finally: os.environ.pop('RSYNC_MODULE_PATH')
def test_select_entry(self): """Test password selection.""" with TemporaryDirectory() as directory: touch(os.path.join(directory, "foo.gpg")) touch(os.path.join(directory, "bar.gpg")) touch(os.path.join(directory, "baz.gpg")) program = PasswordStore(directory=directory) # Substring search. entry = program.select_entry("fo") assert entry.name == "foo" # Fuzzy search. entry = program.select_entry("bz") assert entry.name == "baz"
def test_write_contents_create(self): """Test write_contents().""" expected_contents = u"Hello world!" with TemporaryDirectory() as directory: # Create the file. filename = os.path.join(directory, 'file-to-create') assert not os.path.exists(filename) write_contents(filename, expected_contents) # Make sure the file exists. assert os.path.exists(filename) # Validate the file's contents. with codecs.open(filename, 'r', 'UTF-8') as handle: assert handle.read() == expected_contents
def test_select_entry_interactive(self): """Test interactive password selection.""" with TemporaryDirectory() as directory: touch(os.path.join(directory, 'foo.gpg')) touch(os.path.join(directory, 'bar.gpg')) touch(os.path.join(directory, 'baz.gpg')) # Select entries using the command line filter 'a' and then use # interactive selection to narrow the choice down to 'baz' by # specifying the unique substring 'z'. program = PasswordStore(directory=directory) with CaptureOutput(input='z'): entry = program.select_entry('a') assert entry.name == 'baz'
def test_password_discovery(self): """Test password discovery.""" with TemporaryDirectory() as directory: touch(os.path.join(directory, 'foo.gpg')) touch(os.path.join(directory, 'foo/bar.gpg')) touch(os.path.join(directory, 'foo/bar/baz.gpg')) touch(os.path.join(directory, 'Also with spaces.gpg')) program = PasswordStore(directory=directory) assert len(program.entries) == 4 assert program.entries[0].name == 'Also with spaces' assert program.entries[1].name == 'foo' assert program.entries[2].name == 'foo/bar' assert program.entries[3].name == 'foo/bar/baz'
def test_select_entry(self): """Test password selection.""" with TemporaryDirectory() as directory: touch(os.path.join(directory, 'foo.gpg')) touch(os.path.join(directory, 'bar.gpg')) touch(os.path.join(directory, 'baz.gpg')) program = PasswordStore(directory=directory) # Substring search. entry = program.select_entry('fo') assert entry.name == 'foo' # Fuzzy search. entry = program.select_entry('bz') assert entry.name == 'baz'
def test_touch(self): """Test :func:`humanfriendly.testing.touch()`.""" with TemporaryDirectory() as directory: # Create a file in the temporary directory. filename = os.path.join(directory, random_string()) assert not os.path.isfile(filename) touch(filename) assert os.path.isfile(filename) # Create a file in a subdirectory. filename = os.path.join(directory, random_string(), random_string()) assert not os.path.isfile(filename) touch(filename) assert os.path.isfile(filename)
def test_cli_list(self): """Test the output of ``qpass --list``.""" with TemporaryDirectory() as directory: touch(os.path.join(directory, 'foo.gpg')) touch(os.path.join(directory, 'foo/bar.gpg')) touch(os.path.join(directory, 'Also with spaces.gpg')) returncode, output = run_cli(main, '--password-store=%s' % directory, '--list') assert returncode == 0 entries = output.splitlines() assert 'foo' in entries assert 'foo/bar' in entries assert 'Also with spaces' in entries
def test_exclude_list(self): """Test exclude list logic.""" # These are the backups expected to be preserved. After each backup # I've noted which rotation scheme it falls in and the number of # preserved backups within that rotation scheme (counting up as we # progress through the backups sorted by date). expected_to_be_preserved = set([ '2013-10-10@20:07', # monthly (1), yearly (1) '2013-11-01@20:06', # monthly (2) '2013-12-01@20:07', # monthly (3) '2014-01-01@20:07', # monthly (4), yearly (2) '2014-02-01@20:05', # monthly (5) '2014-03-01@20:04', # monthly (6) '2014-04-01@20:03', # monthly (7) '2014-05-01@20:06', # monthly (8) '2014-05-19@20:02', # weekly (1) '2014-05-26@20:05', # weekly (2) '2014-06-01@20:01', # monthly (9) '2014-06-09@20:01', # weekly (3) '2014-06-16@20:02', # weekly (4) '2014-06-23@20:04', # weekly (5) '2014-06-26@20:04', # daily (1) '2014-06-27@20:02', # daily (2) '2014-06-28@20:02', # daily (3) '2014-06-29@20:01', # daily (4) '2014-06-30@20:03', # daily (5), weekly (6) '2014-07-01@20:02', # daily (6), monthly (10) '2014-07-02@20:03', # hourly (1), daily (7) 'some-random-directory', # no recognizable time stamp, should definitely be preserved ]) for name in SAMPLE_BACKUP_SET: if name.startswith('2014-05-'): expected_to_be_preserved.add(name) with TemporaryDirectory(prefix='rotate-backups-', suffix='-test-suite') as root: self.create_sample_backup_set(root) run_cli( main, '--verbose', '--ionice=idle', '--hourly=24', '--daily=7', '--weekly=4', '--monthly=12', '--yearly=always', '--exclude=2014-05-*', root, ) backups_that_were_preserved = set(os.listdir(root)) assert backups_that_were_preserved == expected_to_be_preserved
def test_prefer_new(self): """Test the alternative preference for the newest backup in each time slot.""" with TemporaryDirectory(prefix='rotate-backups-', suffix='-test-suite') as root: os.mkdir(os.path.join(root, 'backup-2016-01-10_21-15-00')) os.mkdir(os.path.join(root, 'backup-2016-01-10_21-30-00')) os.mkdir(os.path.join(root, 'backup-2016-01-10_21-45-00')) run_cli(main, '--hourly=1', '--prefer-recent', root) assert not os.path.exists( os.path.join(root, 'backup-2016-01-10_21-15-00')) assert not os.path.exists( os.path.join(root, 'backup-2016-01-10_21-30-00')) assert os.path.exists( os.path.join(root, 'backup-2016-01-10_21-45-00'))
def test_minutely_rotation(self): """Test rotation with multiple backups per hour.""" with TemporaryDirectory(prefix='rotate-backups-', suffix='-test-suite') as root: os.mkdir(os.path.join(root, 'backup-2016-01-10_21-15-00')) os.mkdir(os.path.join(root, 'backup-2016-01-10_21-30-00')) os.mkdir(os.path.join(root, 'backup-2016-01-10_21-45-00')) run_cli(main, '--prefer-recent', '--relaxed', '--minutely=2', root) assert not os.path.exists( os.path.join(root, 'backup-2016-01-10_21-15-00')) assert os.path.exists( os.path.join(root, 'backup-2016-01-10_21-30-00')) assert os.path.exists( os.path.join(root, 'backup-2016-01-10_21-45-00'))
def test_cli_exclude(self): """Test the output of ``qpass --exclude=... --list``.""" with TemporaryDirectory() as directory: touch(os.path.join(directory, "foo.gpg")) touch(os.path.join(directory, "foo/bar.gpg")) touch(os.path.join(directory, "Also with spaces.gpg")) returncode, output = run_cli(main, "--password-store=%s" % directory, "--exclude=*bar*", "--list") assert returncode == 0 entries = output.splitlines() assert "foo" in entries assert "foo/bar" not in entries assert "Also with spaces" in entries
def test_make_dirs(self): """Test make_dirs().""" with TemporaryDirectory() as directory: subdirectory = os.path.join(directory, 'a', 'b', 'c') make_dirs(subdirectory) # Make sure the subdirectory was created. assert os.path.isdir(subdirectory) # Make sure existing directories don't raise an exception. make_dirs(subdirectory) # Make sure that errors other than EEXIST aren't swallowed. For the # purpose of this test we assume that /proc is the Linux `process # information pseudo-file system' whose top level directories # aren't writable (with or without superuser privileges). self.assertRaises(OSError, make_dirs, '/proc/linux-utils-test')
def test_invalid_dates(self): """Make sure filenames with invalid dates don't cause an exception.""" with TemporaryDirectory(prefix='rotate-backups-', suffix='-test-suite') as root: file_with_valid_date = os.path.join( root, 'snapshot-201808030034.tar.gz') file_with_invalid_date = os.path.join( root, 'snapshot-180731150101.tar.gz') for filename in file_with_valid_date, file_with_invalid_date: touch(filename) program = RotateBackups(rotation_scheme=dict(monthly='always')) backups = program.collect_backups(root) assert len(backups) == 1 assert backups[0].pathname == file_with_valid_date
def test_touch(self): """Test touch().""" expected_contents = u"Hello world!" with TemporaryDirectory() as directory: # Test that touch() creates files. filename = os.path.join(directory, 'file-to-touch') touch(filename) assert os.path.isfile(filename) # Test that touch() doesn't change a file's contents. with open(filename, 'w') as handle: handle.write(expected_contents) touch(filename) with open(filename) as handle: assert handle.read() == expected_contents
def test_smart_search(self): """Test smart searching.""" with TemporaryDirectory() as directory: touch(os.path.join(directory, 'abcdef.gpg')) touch(os.path.join(directory, 'aabbccddeeff.gpg')) touch(os.path.join(directory, 'Google.gpg')) program = PasswordStore(directory=directory) # Test a substring match that avoids fuzzy matching. matches = program.smart_search('abc') assert len(matches) == 1 assert matches[0].name == 'abcdef' # Test a fuzzy match to confirm that the fall back works. matches = program.smart_search('gg') assert len(matches) == 1 assert matches[0].name == 'Google'
def test_refuse_to_overwrite(self): """Test that local modifications are not overwritten.""" with TemporaryDirectory() as temporary_directory: filename = os.path.join(temporary_directory, 'config') # Create the configuration file and directory. write_file(filename, "Original content.\n") # Use the command line interface to initialize the directory. returncode, output = run_cli(main, filename) assert returncode == 0 # Modify the generated configuration file. write_file(filename, "Not the same thing.\n") # Use the command line interface to update the configuration file. returncode, output = run_cli(main, filename, merged=True) assert returncode != 0 assert "refusing to overwrite" in output
def test_rotate_backups(self): """Test the :func:`.rotate_backups()` function.""" # These are the backups expected to be preserved. After each backup # I've noted which rotation scheme it falls in and the number of # preserved backups within that rotation scheme (counting up as we # progress through the backups sorted by date). expected_to_be_preserved = set([ '2013-10-10@20:07', # monthly (1), yearly (1) '2013-11-01@20:06', # monthly (2) '2013-12-01@20:07', # monthly (3) '2014-01-01@20:07', # monthly (4), yearly (2) '2014-02-01@20:05', # monthly (5) '2014-03-01@20:04', # monthly (6) '2014-04-01@20:03', # monthly (7) '2014-05-01@20:06', # monthly (8) '2014-06-01@20:01', # monthly (9) '2014-06-09@20:01', # weekly (1) '2014-06-16@20:02', # weekly (2) '2014-06-23@20:04', # weekly (3) '2014-06-26@20:04', # daily (1) '2014-06-27@20:02', # daily (2) '2014-06-28@20:02', # daily (3) '2014-06-29@20:01', # daily (4) '2014-06-30@20:03', # daily (5), weekly (4) '2014-07-01@20:02', # daily (6), monthly (10) '2014-07-02@20:03', # hourly (1), daily (7) 'some-random-directory', # no recognizable time stamp, should definitely be preserved 'rotate-backups.ini', # no recognizable time stamp, should definitely be preserved ]) with TemporaryDirectory(prefix='rotate-backups-', suffix='-test-suite') as root: # Specify the rotation scheme and options through a configuration file. config_file = os.path.join(root, 'rotate-backups.ini') parser = configparser.RawConfigParser() parser.add_section(root) parser.set(root, 'hourly', '24') parser.set(root, 'daily', '7') parser.set(root, 'weekly', '4') parser.set(root, 'monthly', '12') parser.set(root, 'yearly', 'always') parser.set(root, 'ionice', 'idle') with open(config_file, 'w') as handle: parser.write(handle) self.create_sample_backup_set(root) run_cli(main, '--verbose', '--config=%s' % config_file) backups_that_were_preserved = set(os.listdir(root)) assert backups_that_were_preserved == expected_to_be_preserved
def test_simple_search(self): """Test simple substring searching.""" with TemporaryDirectory() as directory: touch(os.path.join(directory, 'foo.gpg')) touch(os.path.join(directory, 'bar.gpg')) touch(os.path.join(directory, 'baz.gpg')) program = PasswordStore(directory=directory) matches = program.simple_search('fo') assert len(matches) == 1 assert matches[0].name == 'foo' matches = program.simple_search('a') assert len(matches) == 2 assert matches[0].name == 'bar' assert matches[1].name == 'baz' matches = program.simple_search('b', 'z') assert len(matches) == 1 assert matches[0].name == 'baz'