# -*- coding: utf-8; -*-
#
# test/test_crypto.py
# Part of ‘dput’, a Debian package upload toolkit.
#
# This is free software, and you are welcome to redistribute it under
# certain conditions; see the end of this file for copyright
# information, grant of license, and disclaimer of warranty.

""" Unit tests for ‘crypto’ module. """

from __future__ import (absolute_import, unicode_literals)

import doctest
import functools
import operator
import sys
import textwrap

import gpgme
import testscenarios
import testtools

import dput.crypto

from .helper import (
        mock,
        patch_system_interfaces,
        set_fake_file_scenario,
        setup_fake_file_fixtures,
        )


def make_gpgme_signature_scenarios():
    """ Make a collection of scenarios for `gpgme.Signature` instances. """

    scenarios = [
            ('signature-good validity-unknown', {
                'signature': mock.MagicMock(
                    gpgme.Signature,
                    fpr="BADBEEF2FACEDCADF00DBEEFDECAFBAD",
                    status=gpgme.ERR_NO_ERROR,
                    summary=functools.reduce(
                        operator.ior, [gpgme.SIGSUM_GREEN]),
                    validity=gpgme.VALIDITY_UNKNOWN),
                'expected_character': "good",
                'expected_description': (
                    "Good signature from F00DBEEFDECAFBAD"),
                }),
            ('signature-good validity-never', {
                'signature': mock.MagicMock(
                    gpgme.Signature,
                    fpr="BADBEEF2FACEDCADF00DBEEFDECAFBAD",
                    status=gpgme.ERR_NO_ERROR,
                    summary=functools.reduce(
                        operator.ior, [gpgme.SIGSUM_GREEN]),
                    validity=gpgme.VALIDITY_NEVER),
                'expected_character': "good",
                'expected_description': (
                    "Good signature from F00DBEEFDECAFBAD"),
                }),
            ('signature-good validity-full key-expired', {
                'signature': mock.MagicMock(
                    gpgme.Signature,
                    fpr="BADBEEF2FACEDCADF00DBEEFDECAFBAD",
                    status=gpgme.ERR_NO_ERROR,
                    summary=functools.reduce(operator.ior, [
                        gpgme.SIGSUM_GREEN, gpgme.SIGSUM_KEY_EXPIRED]),
                    validity=gpgme.VALIDITY_FULL),
                'expected_character': "good",
                'expected_description': (
                    "Good signature from F00DBEEFDECAFBAD"),
                }),
            ('signature-good validity-full', {
                'signature': mock.MagicMock(
                    gpgme.Signature,
                    fpr="BADBEEF2FACEDCADF00DBEEFDECAFBAD",
                    status=gpgme.ERR_NO_ERROR,
                    summary=functools.reduce(operator.ior, [
                        gpgme.SIGSUM_VALID, gpgme.SIGSUM_GREEN]),
                    validity=gpgme.VALIDITY_FULL),
                'expected_character': "valid",
                'expected_description': (
                    "Valid signature from F00DBEEFDECAFBAD"),
                }),
            ('signature-bad', {
                'signature': mock.MagicMock(
                    gpgme.Signature,
                    fpr="BADBEEF2FACEDCADF00DBEEFDECAFBAD",
                    status=gpgme.ERR_BAD_SIGNATURE,
                    summary=functools.reduce(
                        operator.ior, [gpgme.SIGSUM_RED]),
                    validity=gpgme.VALIDITY_FULL),
                'expected_character': "bad",
                'expected_description': (
                    "Bad signature from F00DBEEFDECAFBAD"),
                }),
            ]

    return scenarios


class characterise_signature_TestCase(
        testscenarios.WithScenarios,
        testtools.TestCase):
    """ Test cases for function `characterise_signature`. """

    scenarios = make_gpgme_signature_scenarios()

    def test_returns_expected_character(self):
        """ Should return expected character for signature. """
        result = dput.crypto.characterise_signature(self.signature)
        self.assertEqual(result, self.expected_character)


class describe_signature_TestCase(
        testscenarios.WithScenarios,
        testtools.TestCase):
    """ Test cases for function `describe_signature`. """

    scenarios = make_gpgme_signature_scenarios()

    def test_returns_expected_character(self):
        """ Should return expected character for signature. """
        result = dput.crypto.describe_signature(self.signature)
        self.assertEqual(result, self.expected_description)


def make_gpgme_verify_scenarios():
    """ Make a collection of scenarios for ‘Context.verify’ method.

        :return: A collection of scenarios for tests.

        The collection is a mapping from scenario name to a dictionary of
        scenario attributes.

        """

    signatures_by_name = {
            name: scenario['signature']
            for (name, scenario) in make_gpgme_signature_scenarios()}

    scenarios_by_name = {
            'goodsig': {
                'result': [
                    signatures_by_name['signature-good validity-unknown'],
                    ],
                },
            'validsig': {
                'result': [
                    signatures_by_name['signature-good validity-full'],
                    ],
                },
            'badsig': {
                'exception': gpgme.GpgmeError(
                    gpgme.ERR_SOURCE_GPGME, gpgme.ERR_BAD_SIGNATURE,
                    "Bad signature"),
                },
            'errsig': {
                'exception': gpgme.GpgmeError(
                    gpgme.ERR_SOURCE_GPGME, gpgme.ERR_SIG_EXPIRED,
                    "Signature expired"),
                },
            'nodata': {
                'exception': gpgme.GpgmeError(
                    gpgme.ERR_SOURCE_GPGME, gpgme.ERR_NO_DATA,
                    "No data"),
                },
            'bogus': {
                'exception': ValueError,
                },
            }

    scenarios = {
            'default': scenarios_by_name['goodsig'],
            }
    scenarios.update(
            (name, scenario)
            for (name, scenario) in scenarios_by_name.items())

    return scenarios


def setup_gpgme_verify_fixtures(testcase):
    """ Set up fixtures for GPGME interaction behaviour. """
    scenarios = make_gpgme_verify_scenarios()
    testcase.gpgme_verify_scenarios = scenarios


class check_file_signature_TestCase(testtools.TestCase):
    """ Test cases for `check_file_signature` function. """

    def setUp(self):
        """ Set up test fixtures. """
        super(check_file_signature_TestCase, self).setUp()
        patch_system_interfaces(self)

        setup_fake_file_fixtures(self)
        set_fake_file_scenario(self, 'exist-minimal')

        self.set_test_args()

        self.patch_gpgme_context()

        setup_gpgme_verify_fixtures(self)
        self.set_gpgme_verify_scenario('default')

    def set_test_args(self):
        """ Set the arguments for the test call to the function. """
        self.test_args = dict(
                infile=self.file_double.fake_file,
                )

    def patch_gpgme_context(self):
        """ Patch the ‘gpgme.Context’ class for this test case. """
        class_patcher = mock.patch.object(gpgme, 'Context')
        class_patcher.start()
        self.addCleanup(class_patcher.stop)

    def set_gpgme_verify_scenario(self, name):
        """ Set the status scenario for the ‘Context.verify’ call. """
        scenario = self.gpgme_verify_scenarios[name]
        mock_class = gpgme.Context
        self.mock_gpgme_context = mock_class.return_value
        mock_func = self.mock_gpgme_context.verify
        if 'exception' in scenario:
            mock_func.side_effect = scenario['exception']
        else:
            mock_func.return_value = scenario['result']

    def assert_stderr_contains_gpgme_error(self, code):
        """ Assert the `stderr` content contains the GPGME message. """
        expected_output = textwrap.dedent("""\
                gpgme: {path}: error {code}: ...
                """).format(
                    path=self.file_double.path, code=code)
        self.assertThat(
                sys.stderr.getvalue(),
                testtools.matchers.DocTestMatches(
                    expected_output, doctest.ELLIPSIS))

    def test_calls_gpgme_verify_with_expected_args(self):
        """ Should call `gpgme.Context.verify` with expected args. """
        dput.crypto.check_file_signature(**self.test_args)
        gpgme.Context.return_value.verify.assert_called_with(
            self.file_double.fake_file, None, None)

    def test_calls_sys_exit_if_gnupg_reports_bad_signature(self):
        """ Should call `sys.exit` if GnuPG reports bad signature. """
        self.set_gpgme_verify_scenario('badsig')
        with testtools.ExpectedException(gpgme.GpgmeError):
            dput.crypto.check_file_signature(**self.test_args)
        self.assert_stderr_contains_gpgme_error(gpgme.ERR_BAD_SIGNATURE)

    def test_calls_sys_exit_if_gnupg_reports_sig_expired(self):
        """ Should call `sys.exit` if GnuPG reports signature expired. """
        self.set_gpgme_verify_scenario('errsig')
        with testtools.ExpectedException(gpgme.GpgmeError):
            dput.crypto.check_file_signature(**self.test_args)
        self.assert_stderr_contains_gpgme_error(gpgme.ERR_SIG_EXPIRED)

    def test_calls_sys_exit_if_gnupg_reports_nodata(self):
        """ Should call `sys.exit` if GnuPG reports no data. """
        self.set_gpgme_verify_scenario('nodata')
        with testtools.ExpectedException(gpgme.GpgmeError):
            dput.crypto.check_file_signature(**self.test_args)
        self.assert_stderr_contains_gpgme_error(gpgme.ERR_NO_DATA)

    def test_outputs_message_if_gnupg_reports_goodsig(self):
        """ Should output a message if GnuPG reports a good signature. """
        self.set_gpgme_verify_scenario('goodsig')
        dput.crypto.check_file_signature(**self.test_args)
        expected_output = textwrap.dedent("""\
                gpgme: {path}: Good signature from ...
                """).format(path=self.file_double.path)
        self.assertThat(
                sys.stderr.getvalue(),
                testtools.matchers.DocTestMatches(
                    expected_output, doctest.ELLIPSIS))

    def test_outputs_message_if_gnupg_reports_validsig(self):
        """ Should output a message if GnuPG reports a valid signature. """
        self.set_gpgme_verify_scenario('validsig')
        dput.crypto.check_file_signature(**self.test_args)
        expected_output = textwrap.dedent("""\
                gpgme: {path}: Valid signature from ...
                """).format(path=self.file_double.path)
        self.assertThat(
                sys.stderr.getvalue(),
                testtools.matchers.DocTestMatches(
                    expected_output, doctest.ELLIPSIS))


# Copyright © 2015–2016 Ben Finney <bignose@debian.org>
#
# This is free software: you may copy, modify, and/or distribute this work
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; version 3 of that license or any later version.
# No warranty expressed or implied. See the file ‘LICENSE.GPL-3’ for details.


# Local variables:
# coding: utf-8
# mode: python
# End:
# vim: fileencoding=utf-8 filetype=python :
