diff options
Diffstat (limited to 'tests/commands/test_envelope.py')
-rw-r--r-- | tests/commands/test_envelope.py | 386 |
1 files changed, 386 insertions, 0 deletions
diff --git a/tests/commands/test_envelope.py b/tests/commands/test_envelope.py new file mode 100644 index 00000000..76aa7e88 --- /dev/null +++ b/tests/commands/test_envelope.py @@ -0,0 +1,386 @@ +# encoding=utf-8 +# Copyright © 2017-2018 Dylan Baker + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +"""Tests for the alot.commands.envelope module.""" + +import email +import os +import tempfile +import textwrap +import unittest + +import mock + +from alot.commands import envelope +from alot.db.envelope import Envelope +from alot.errors import GPGProblem +from alot.settings.errors import NoMatchingAccount +from alot.settings.manager import SettingsManager +from alot.account import Account + +from .. import utilities + +# When using an assert from a mock a TestCase method might not use self. That's +# okay. +# pylint: disable=no-self-use + + +class TestAttachCommand(unittest.TestCase): + """Tests for the AttachCommaned class.""" + + def test_single_path(self): + """A test for an existing single path.""" + ui = utilities.make_ui() + + with tempfile.TemporaryDirectory() as d: + testfile = os.path.join(d, 'foo') + with open(testfile, 'w') as f: + f.write('foo') + + cmd = envelope.AttachCommand(path=testfile) + cmd.apply(ui) + ui.current_buffer.envelope.attach.assert_called_with(testfile) + + def test_user(self): + """A test for an existing single path prefaced with ~/.""" + ui = utilities.make_ui() + + with tempfile.TemporaryDirectory() as d: + # This mock replaces expanduser to replace "~/" with a path to the + # temporary directory. This is easier and more reliable than + # relying on changing an environment variable (like HOME), since it + # doesn't rely on CPython implementation details. + with mock.patch('alot.commands.os.path.expanduser', + lambda x: os.path.join(d, x[2:])): + testfile = os.path.join(d, 'foo') + with open(testfile, 'w') as f: + f.write('foo') + + cmd = envelope.AttachCommand(path='~/foo') + cmd.apply(ui) + ui.current_buffer.envelope.attach.assert_called_with(testfile) + + def test_glob(self): + """A test using a glob.""" + ui = utilities.make_ui() + + with tempfile.TemporaryDirectory() as d: + testfile1 = os.path.join(d, 'foo') + testfile2 = os.path.join(d, 'far') + for t in [testfile1, testfile2]: + with open(t, 'w') as f: + f.write('foo') + + cmd = envelope.AttachCommand(path=os.path.join(d, '*')) + cmd.apply(ui) + ui.current_buffer.envelope.attach.assert_has_calls( + [mock.call(testfile1), mock.call(testfile2)], any_order=True) + + def test_no_match(self): + """A test for a file that doesn't exist.""" + ui = utilities.make_ui() + + with tempfile.TemporaryDirectory() as d: + cmd = envelope.AttachCommand(path=os.path.join(d, 'doesnt-exist')) + cmd.apply(ui) + ui.notify.assert_called() + + +class TestTagCommands(unittest.TestCase): + + def _test(self, tagstring, action, expected): + """Common steps for envelope.TagCommand tests + + :param tagstring: the string to pass to the TagCommand + :type tagstring: str + :param action: the action to pass to the TagCommand + :type action: str + :param expected: the expected output to assert in the test + :type expected: list(str) + """ + env = Envelope(tags=['one', 'two', 'three']) + ui = utilities.make_ui() + ui.current_buffer = mock.Mock() + ui.current_buffer.envelope = env + cmd = envelope.TagCommand(tags=tagstring, action=action) + cmd.apply(ui) + actual = env.tags + self.assertListEqual(sorted(actual), sorted(expected)) + + def test_add_new_tags(self): + self._test(u'four', 'add', ['one', 'two', 'three', 'four']) + + def test_adding_existing_tags_has_no_effect(self): + self._test(u'one', 'add', ['one', 'two', 'three']) + + def test_remove_existing_tags(self): + self._test(u'one', 'remove', ['two', 'three']) + + def test_remove_non_existing_tags_has_no_effect(self): + self._test(u'four', 'remove', ['one', 'two', 'three']) + + def test_set_tags(self): + self._test(u'a,b,c', 'set', ['a', 'b', 'c']) + + def test_toggle_will_remove_existing_tags(self): + self._test(u'one', 'toggle', ['two', 'three']) + + def test_toggle_will_add_new_tags(self): + self._test(u'four', 'toggle', ['one', 'two', 'three', 'four']) + + def test_toggle_can_remove_and_add_in_one_run(self): + self._test(u'one,four', 'toggle', ['two', 'three', 'four']) + + +class TestSignCommand(unittest.TestCase): + + """Tests for the SignCommand class.""" + + @staticmethod + def _make_ui_mock(): + """Create a mock for the ui and envelope and return them.""" + envelope = Envelope() + envelope['From'] = 'foo <foo@example.com>' + envelope.sign = mock.sentinel.default + envelope.sign_key = mock.sentinel.default + ui = utilities.make_ui(current_buffer=mock.Mock(envelope=envelope)) + return envelope, ui + + @mock.patch('alot.commands.envelope.crypto.get_key', + mock.Mock(return_value=mock.sentinel.keyid)) + def test_apply_keyid_success(self): + """If there is a valid keyid then key and to sign should be set. + """ + env, ui = self._make_ui_mock() + # The actual keyid doesn't matter, since it'll be mocked anyway + cmd = envelope.SignCommand(action='sign', keyid=['a']) + cmd.apply(ui) + + self.assertTrue(env.sign) + self.assertEqual(env.sign_key, mock.sentinel.keyid) + + @mock.patch('alot.commands.envelope.crypto.get_key', + mock.Mock(side_effect=GPGProblem('sentinel', 0))) + def test_apply_keyid_gpgproblem(self): + """If there is an invalid keyid then the signing key and to sign should + be set to false and default. + """ + env, ui = self._make_ui_mock() + # The actual keyid doesn't matter, since it'll be mocked anyway + cmd = envelope.SignCommand(action='sign', keyid=['a']) + cmd.apply(ui) + self.assertFalse(env.sign) + self.assertEqual(env.sign_key, mock.sentinel.default) + ui.notify.assert_called_once() + + @mock.patch('alot.commands.envelope.settings.account_matching_address', + mock.Mock(side_effect=NoMatchingAccount)) + def test_apply_no_keyid_nomatchingaccount(self): + """If there is a nokeyid and no account can be found to match the From, + then the envelope should not be marked to sign. + """ + env, ui = self._make_ui_mock() + # The actual keyid doesn't matter, since it'll be mocked anyway + cmd = envelope.SignCommand(action='sign', keyid=None) + cmd.apply(ui) + + self.assertFalse(env.sign) + self.assertEqual(env.sign_key, mock.sentinel.default) + ui.notify.assert_called_once() + + def test_apply_no_keyid_no_gpg_key(self): + """If there is a nokeyid and the account has no gpg key then the + signing key and to sign should be set to false and default. + """ + env, ui = self._make_ui_mock() + env.account = mock.Mock(gpg_key=None) + + cmd = envelope.SignCommand(action='sign', keyid=None) + cmd.apply(ui) + + self.assertFalse(env.sign) + self.assertEqual(env.sign_key, mock.sentinel.default) + ui.notify.assert_called_once() + + def test_apply_no_keyid_default(self): + """If there is no keyid and the account has a gpg key, then that should + be used. + """ + env, ui = self._make_ui_mock() + env.account = mock.Mock(gpg_key='sentinel') + + cmd = envelope.SignCommand(action='sign', keyid=None) + cmd.apply(ui) + + self.assertTrue(env.sign) + self.assertEqual(env.sign_key, 'sentinel') + + @mock.patch('alot.commands.envelope.crypto.get_key', + mock.Mock(return_value=mock.sentinel.keyid)) + def test_apply_no_sign(self): + """If signing with a valid keyid and valid key then set sign and + sign_key. + """ + env, ui = self._make_ui_mock() + # The actual keyid doesn't matter, since it'll be mocked anyway + cmd = envelope.SignCommand(action='sign', keyid=['a']) + cmd.apply(ui) + + self.assertTrue(env.sign) + self.assertEqual(env.sign_key, mock.sentinel.keyid) + + @mock.patch('alot.commands.envelope.crypto.get_key', + mock.Mock(return_value=mock.sentinel.keyid)) + def test_apply_unsign(self): + """Test that settingun sign sets the sign to False if all other + conditions allow for it. + """ + env, ui = self._make_ui_mock() + env.sign = True + env.sign_key = mock.sentinel + # The actual keyid doesn't matter, since it'll be mocked anyway + cmd = envelope.SignCommand(action='unsign', keyid=['a']) + cmd.apply(ui) + + self.assertFalse(env.sign) + self.assertIs(env.sign_key, None) + + @mock.patch('alot.commands.envelope.crypto.get_key', + mock.Mock(return_value=mock.sentinel.keyid)) + def test_apply_togglesign(self): + """Test that toggling changes the sign and sign_key as approriate if + other condtiions allow for it + """ + env, ui = self._make_ui_mock() + env.sign = True + env.sign_key = mock.sentinel.keyid + + # The actual keyid doesn't matter, since it'll be mocked anyway + # Test that togling from true to false works + cmd = envelope.SignCommand(action='toggle', keyid=['a']) + cmd.apply(ui) + self.assertFalse(env.sign) + self.assertIs(env.sign_key, None) + + # Test that toggling back to True works + cmd.apply(ui) + self.assertTrue(env.sign) + self.assertIs(env.sign_key, mock.sentinel.keyid) + + def _make_local_settings(self): + config = textwrap.dedent("""\ + [accounts] + [[default]] + realname = foo + address = foo@example.com + sendmail_command = /bin/true + """) + + # Allow settings.reload to work by not deleting the file until the end + with tempfile.NamedTemporaryFile(mode='w+', delete=False) as f: + f.write(config) + self.addCleanup(os.unlink, f.name) + + # Set the gpg_key separately to avoid validation failures + manager = SettingsManager() + manager.read_config(f.name) + manager.get_accounts()[0].gpg_key = mock.sentinel.gpg_key + return manager + + def test_apply_from_email_only(self): + """Test that a key can be derived using a 'From' header that contains + only an email. + + If the from header is in the form "foo@example.com" and a key exists it + should be used. + """ + manager = self._make_local_settings() + env, ui = self._make_ui_mock() + env.headers = {'From': ['foo@example.com']} + + cmd = envelope.SignCommand(action='sign') + with mock.patch('alot.commands.envelope.settings', manager): + cmd.apply(ui) + + self.assertTrue(env.sign) + self.assertIs(env.sign_key, mock.sentinel.gpg_key) + + def test_apply_from_user_and_email(self): + """This tests that a gpg key can be derived using a 'From' header that + contains a realname-email combo. + + If the header is in the form "Foo <foo@example.com>", a key should be + derived. + + See issue #1113 + """ + manager = self._make_local_settings() + env, ui = self._make_ui_mock() + + cmd = envelope.SignCommand(action='sign') + with mock.patch('alot.commands.envelope.settings', manager): + cmd.apply(ui) + + self.assertTrue(env.sign) + self.assertIs(env.sign_key, mock.sentinel.gpg_key) + + +class TestSendCommand(unittest.TestCase): + + """Tests for the SendCommand class.""" + + mail = textwrap.dedent("""\ + From: foo@example.com + To: bar@example.com + Subject: FooBar + + Foo Bar Baz + """) + + class MockedAccount(Account): + + def __init__(self): + super().__init__('foo@example.com') + + async def send_mail(self, mail): + pass + + @utilities.async_test + async def test_account_matching_address_with_str(self): + cmd = envelope.SendCommand(mail=self.mail) + account = mock.Mock(wraps=self.MockedAccount()) + with mock.patch( + 'alot.commands.envelope.settings.account_matching_address', + mock.Mock(return_value=account)) as account_matching_address: + await cmd.apply(mock.Mock()) + account_matching_address.assert_called_once_with('foo@example.com', + return_default=True) + # check that the apply did run through till the end. + account.send_mail.assert_called_once_with(self.mail) + + @utilities.async_test + async def test_account_matching_address_with_email_message(self): + mail = email.message_from_string(self.mail) + cmd = envelope.SendCommand(mail=mail) + account = mock.Mock(wraps=self.MockedAccount()) + with mock.patch( + 'alot.commands.envelope.settings.account_matching_address', + mock.Mock(return_value=account)) as account_matching_address: + await cmd.apply(mock.Mock()) + account_matching_address.assert_called_once_with('foo@example.com', + return_default=True) + # check that the apply did run through till the end. + account.send_mail.assert_called_once_with(mail) |