# -*- coding: utf-8 -*-
#
# Copyright 2009-2012 Canonical Ltd.
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 3, as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranties of
# MERCHANTABILITY, SATISFACTORY QUALITY, 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/>.
#
# In addition, as a special exception, the copyright holders give
# permission to link the code of portions of this program with the
# OpenSSL library under certain conditions as described in each
# individual source file, and distribute linked combinations
# including the two.
# You must obey the GNU General Public License in all respects
# for all of the code used other than OpenSSL.  If you modify
# file(s) with this exception, you may extend this exception to your
# version of the file(s), but you are not obligated to do so.  If you
# do not wish to do so, delete this exception statement from your
# version.  If you delete this exception statement from all source
# files in the program, then also delete it here.
"""Tests for the main SSO client code."""

from __future__ import unicode_literals

import logging

from functools import partial

from twisted.internet import defer
from ubuntuone.devtools.handlers import MementoHandler

from ubuntu_kylin_sso import main
from ubuntu_kylin_sso.main import (
    CredentialsManagement,
    except_to_errdict,
    SSOLogin,
    TIMEOUT_INTERVAL,
    UbuntuSSOService,
)
from ubuntu_kylin_sso.main import (
    HELP_TEXT_KEY,
    PING_URL_KEY,
    POLICY_URL_KEY,
    TC_URL_KEY,
    UI_EXECUTABLE_KEY,
    WINDOW_ID_KEY,
)
from ubuntu_kylin_sso.main.tests import FakedCredentials
from ubuntu_kylin_sso.tests import (
    APP_NAME,
    CAPTCHA_ID,
    CAPTCHA_SOLUTION,
    EMAIL,
    EMAIL_TOKEN,
    HELP_TEXT,
    NAME,
    PASSWORD,
    PING_URL,
    POLICY_URL,
    Recorder,
    TC_URL,
    TestCase,
    TOKEN,
    WINDOW_ID,
)


# Access to a protected member 'yyy' of a client class
# pylint: disable=W0212


class SampleException(Exception):
    """A sample exception."""


class FakedProxy(Recorder):
    """A faked multiplatform proxy."""


class FakedAccount(Recorder):
    """A faked Account processor."""


class FakedCredentialsFactory(Recorder):
    """A very dummy Credentials object."""

    credentials = FakedCredentials()

    # pylint: disable=C0103

    def Credentials(self, *a, **kw):
        """Return always the same Credentials instance."""
        return self.credentials

    # pylint: enable=C0103


class TestExceptToErrdictException(Exception):
    """A dummy exception for the following testcase."""


class ExceptToErrdictTestCase(TestCase):
    """Tests for the except_to_errdict function."""

    def test_first_arg_is_dict(self):
        """If the first arg is a dict, use it as the base dict."""
        sample_dict = {
            "errorcode1": "error message 1",
            "errorcode2": "error message 2",
            "errorcode3": "error message 3",
        }
        e = TestExceptToErrdictException(sample_dict)
        result = except_to_errdict(e)

        self.assertEqual(result["errtype"], e.__class__.__name__)
        for k in sample_dict.keys():
            self.assertIn(k, result)
            self.assertEqual(result[k], sample_dict[k])

    def test_first_arg_is_str(self):
        """If the first arg is a str, use it as the message."""
        sample_string = "a sample string"
        e = TestExceptToErrdictException(sample_string)
        result = except_to_errdict(e)
        self.assertEqual(result["errtype"], e.__class__.__name__)
        self.assertEqual(result["message"], sample_string)

    def test_first_arg_is_exception(self):
        """If the first arg is an Exception, use it's message attribute'."""

        class MyException(Exception):
            """Custom Exception."""

        message = 'My custom error for ♥ Ubuntu'
        my_exc = MyException(message)
        result = except_to_errdict(my_exc)
        self.assertEqual(result["errtype"], my_exc.__class__.__name__)
        self.assertEqual(result["message"], message)

    def test_first_arg_is_unicode(self):
        """If the first arg is a unicode, use it as the message."""
        sample_string = "a sample string"
        e = TestExceptToErrdictException(sample_string)
        result = except_to_errdict(e)
        self.assertEqual(result["errtype"], e.__class__.__name__)
        self.assertEqual(result["message"], sample_string)

    def test_no_args_at_all(self):
        """If there are no args, use the class docstring."""
        e = TestExceptToErrdictException()
        result = except_to_errdict(e)
        self.assertEqual(result["errtype"], e.__class__.__name__)
        self.assertEqual(result["message"], e.__class__.__doc__)

    def test_some_other_thing_as_first_arg(self):
        """If first arg is not basestring nor dict, then repr all args."""
        sample_args = (None, "unicode2\ufffd", "errorcode3")
        e = TestExceptToErrdictException(*sample_args)
        result = except_to_errdict(e)
        self.assertEqual(result["errtype"], e.__class__.__name__)


class BaseTestCase(TestCase):
    """Base test case."""

    timeout = 2

    @defer.inlineCallbacks
    def setUp(self):
        yield super(BaseTestCase, self).setUp()
        self.proxy = FakedProxy()
        self.deferred = defer.Deferred()

    def assert_recorder_called(self, fake, method, *args, **kwargs):
        """Check that 'fake.method(*args, **kwargs)' was called."""
        self.assertEqual(fake._called[method], [(args, kwargs)])

    def callback_deferred(self, *a):
        """Callback self.deferred with a as result."""
        self.deferred.callback(a)

    def errback_deferred(self, signal_name, *a):
        """Errback self.deferred with an AssertionError as error."""
        msg = 'Received unexpected signal %r with params %r.'
        self.deferred.errback(AssertionError(msg % (signal_name, a)))


class SSOLoginBaseTestCase(BaseTestCase):
    """Base test case for testing the SSOLogin class."""

    @defer.inlineCallbacks
    def setUp(self):
        yield super(SSOLoginBaseTestCase, self).setUp()

        def ksc(keyring, k, val):
            """Assert over token and app_name."""
            self.assertEqual(k, APP_NAME)
            self.assertEqual(val, TOKEN)
            self.keyring_was_set = True
            return defer.succeed(None)

        self.patch(main.Keyring, "set_credentials", ksc)
        self.patch(main, 'Account', FakedAccount)
        self.keyring_was_set = False

        self.obj = SSOLogin(self.proxy)

    def test_creation(self):
        """Test that the object creation is successful."""
        self.assertIsInstance(self.obj.processor, main.Account)
        self.assertTrue(self.obj.proxy is self.proxy)


class SSOLoginTestCase(SSOLoginBaseTestCase):
    """Test the SSOLogin class."""

    @defer.inlineCallbacks
    def test_generate_captcha(self):
        """Test that the captcha method works ok."""
        filename = "sample filename"
        expected_result = "expected result"

        self.patch(self.obj, "CaptchaGenerated", self.callback_deferred)
        self.patch(self.obj, "CaptchaGenerationError",
                   partial(self.errback_deferred, "CaptchaGenerationError"))

        self.obj.processor._next_result = defer.succeed(expected_result)
        self.obj.generate_captcha(APP_NAME, filename)

        app_name, result = yield self.deferred
        self.assertEqual(result, expected_result)
        self.assertEqual(app_name, APP_NAME)
        self.assert_recorder_called(self.obj.processor, 'generate_captcha',
            filename)

    @defer.inlineCallbacks
    def test_register_user(self):
        """Test that the register_user method works ok."""
        expected_result = "expected result"

        self.patch(self.obj, "UserRegistered", self.callback_deferred)
        self.patch(self.obj, "UserRegistrationError",
                   partial(self.errback_deferred, "UserRegistrationError"))

        self.obj.processor._next_result = defer.succeed(expected_result)
        self.obj.register_user(APP_NAME, EMAIL, PASSWORD, NAME, CAPTCHA_ID,
                               CAPTCHA_SOLUTION)

        app_name, result = yield self.deferred
        self.assertEqual(result, expected_result)
        self.assertEqual(app_name, APP_NAME)
        self.assert_recorder_called(self.obj.processor, 'register_user',
            EMAIL, PASSWORD, NAME, CAPTCHA_ID, CAPTCHA_SOLUTION)

    @defer.inlineCallbacks
    def test_login(self):
        """Test that the login method works ok when used is validated."""
        self.patch(self.obj, "LoggedIn", self.callback_deferred)
        self.patch(self.obj, "LoginError",
                   partial(self.errback_deferred, "LoginError"))
        self.patch(self.obj, "UserNotValidated",
                   partial(self.errback_deferred, "UserNotValidated"))
        self.patch(self.obj.processor, 'is_validated', lambda _: True)

        self.obj.processor._next_result = TOKEN
        self.obj.login(APP_NAME, EMAIL, PASSWORD)

        app_name, result = yield self.deferred
        self.assertEqual(result, EMAIL)
        self.assertEqual(app_name, APP_NAME)
        self.assertTrue(self.keyring_was_set, "The keyring should be set")
        self.assert_recorder_called(self.obj.processor, 'login',
            EMAIL, PASSWORD, main.get_token_name(APP_NAME))
        self.assert_recorder_called(self.obj.processor, 'is_validated',
            TOKEN)

    @defer.inlineCallbacks
    def test_login_is_not_validated(self):
        """Test that the login method works ok when user is not validated."""
        self.patch(self.obj, "LoggedIn",
                   partial(self.errback_deferred, "LoggedIn"))
        self.patch(self.obj, "LoginError",
                   partial(self.errback_deferred, "LoginError"))
        self.patch(self.obj, "UserNotValidated", self.callback_deferred)
        self.patch(self.obj.processor, 'is_validated', lambda _: False)

        self.obj.processor._next_result = TOKEN
        self.obj.login(APP_NAME, EMAIL, PASSWORD)

        app_name, result = yield self.deferred
        self.assertEqual(result, EMAIL)
        self.assertEqual(app_name, APP_NAME)
        self.assertFalse(self.keyring_was_set, "The keyring should not be set")
        self.assert_recorder_called(self.obj.processor, 'login',
            EMAIL, PASSWORD, main.get_token_name(APP_NAME))
        self.assert_recorder_called(self.obj.processor, 'is_validated',
            TOKEN)

    @defer.inlineCallbacks
    def test_validate_email(self):
        """Test that the validate_email method works ok."""
        self.patch(self.obj, "EmailValidated", self.callback_deferred)
        self.patch(self.obj, "EmailValidationError",
                   partial(self.errback_deferred, "EmailValidationError"))

        self.obj.processor._next_result = TOKEN
        self.obj.validate_email(APP_NAME, EMAIL, PASSWORD, EMAIL_TOKEN)

        app_name, result = yield self.deferred
        self.assertEqual(result, EMAIL)
        self.assertEqual(app_name, APP_NAME)
        self.assertTrue(self.keyring_was_set, "The keyring should be set")
        self.assert_recorder_called(self.obj.processor, 'validate_email',
            EMAIL, PASSWORD, EMAIL_TOKEN, main.get_token_name(APP_NAME))

    @defer.inlineCallbacks
    def test_request_password_reset_token(self):
        """Test that the request_password_reset_token method works ok."""
        self.patch(self.obj, "PasswordResetTokenSent", self.callback_deferred)
        self.patch(self.obj, "PasswordResetError",
                   partial(self.errback_deferred, "PasswordResetError"))

        self.obj.processor._next_result = EMAIL
        self.obj.request_password_reset_token(APP_NAME, EMAIL)

        app_name, result = yield self.deferred
        self.assertEqual(result, EMAIL)
        self.assertEqual(app_name, APP_NAME)
        self.assert_recorder_called(self.obj.processor,
            'request_password_reset_token', EMAIL)

    @defer.inlineCallbacks
    def test_set_new_password(self):
        """Test that the set_new_password method works ok."""
        self.patch(self.obj, "PasswordChanged", self.callback_deferred)
        self.patch(self.obj, "PasswordChangeError",
                   partial(self.errback_deferred, "PasswordChangeError"))

        self.obj.processor._next_result = EMAIL
        self.obj.set_new_password(APP_NAME, EMAIL, EMAIL_TOKEN, PASSWORD)

        app_name, result = yield self.deferred
        self.assertEqual(result, EMAIL)
        self.assertEqual(app_name, APP_NAME)
        self.assert_recorder_called(self.obj.processor,
            'set_new_password', EMAIL, EMAIL_TOKEN, PASSWORD)


class SSOLoginWithErrorTestCase(SSOLoginTestCase):
    """Test the SSOLogin class."""

    def sample_fail(self, *args):
        """Fail predictably."""
        raise SampleException("Sample message")

    @defer.inlineCallbacks
    def test_generate_captcha(self):
        """Test that the captcha method fails as expected."""
        filename = "sample filename"

        self.patch(self.obj, "CaptchaGenerated",
                   partial(self.errback_deferred, "CaptchaGenerated"))
        self.patch(self.obj, "CaptchaGenerationError", self.callback_deferred)
        self.patch(self.obj.processor, "generate_captcha", self.sample_fail)
        self.obj.generate_captcha(APP_NAME, filename)

        app_name, error = yield self.deferred
        self.assertTrue(isinstance(error, SampleException))
        self.assertEqual(app_name, APP_NAME)

    @defer.inlineCallbacks
    def test_register_user(self):
        """Test that the register_user method fails as expected."""
        self.patch(self.obj, "UserRegistered",
                   partial(self.errback_deferred, "UserRegistered"))
        self.patch(self.obj, "UserRegistrationError", self.callback_deferred)
        self.patch(self.obj.processor, "register_user", self.sample_fail)
        self.obj.register_user(APP_NAME, EMAIL, PASSWORD, NAME, CAPTCHA_ID,
                             CAPTCHA_SOLUTION)

        app_name, error = yield self.deferred
        self.assertTrue(isinstance(error, SampleException))
        self.assertEqual(app_name, APP_NAME)

    @defer.inlineCallbacks
    def test_login(self):
        """Test that the login method works ok."""

        def fake_gtn(*args):
            """A fake get_token_name that fails."""
            raise SampleException()

        self.patch(main, "get_token_name", fake_gtn)
        self.patch(self.obj, "LoggedIn",
                   partial(self.errback_deferred, "LoggedIn"))
        self.patch(self.obj, "LoginError", self.callback_deferred)
        self.patch(self.obj, "UserNotValidated",
                   partial(self.errback_deferred, "UserNotValidated"))
        self.obj.login(APP_NAME, EMAIL, PASSWORD)

        app_name, error = yield self.deferred
        self.assertEqual(app_name, APP_NAME)
        self.assertTrue(isinstance(error, SampleException))
        self.assertFalse(self.keyring_was_set, "Keyring should not be set")

    @defer.inlineCallbacks
    def test_login_is_not_validated(self):
        """Test that the login method works ok."""

        def fake_validate(*args):
            """A fake get_token_name that fails."""
            raise SampleException()

        self.patch(self.obj, "LoggedIn",
                   partial(self.errback_deferred, "LoggedIn"))
        self.patch(self.obj, "LoginError", self.callback_deferred)
        self.patch(self.obj, "UserNotValidated",
                   partial(self.errback_deferred, "UserNotValidated"))
        self.patch(self.obj.processor, 'is_validated', fake_validate)
        self.obj.login(APP_NAME, EMAIL, PASSWORD)

        app_name, error = yield self.deferred
        self.assertEqual(app_name, APP_NAME)
        self.assertTrue(isinstance(error, SampleException))
        self.assertFalse(self.keyring_was_set, "Keyring should not be set")

    @defer.inlineCallbacks
    def test_login_set_credentials(self):
        """The login method fails as expected when set_credentials fails."""

        def fake_set_creds(*args):
            """A fake Keyring.set_credentials that fails."""
            return defer.fail(SampleException())

        self.patch(main.Keyring, "set_credentials", fake_set_creds)
        self.patch(self.obj, "LoggedIn",
                   partial(self.errback_deferred, "LoggedIn"))
        self.patch(self.obj, "LoginError", self.callback_deferred)
        self.patch(self.obj, "UserNotValidated",
                   partial(self.errback_deferred, "UserNotValidated"))
        self.patch(self.obj.processor, "login",
                                        lambda *args: defer.succeed(None))
        self.patch(self.obj.processor, "is_validated",
                                        lambda *args: defer.succeed(True))
        self.obj.login(APP_NAME, EMAIL, PASSWORD)

        app_name, error = yield self.deferred
        self.assertEqual(app_name, APP_NAME)
        self.assertTrue(isinstance(error, SampleException))
        self.assertFalse(self.keyring_was_set, "Keyring should not be set")

    @defer.inlineCallbacks
    def test_validate_email(self):
        """Test that the validate_email method fails as expected."""

        def fake_gtn(*args):
            """A fake get_token_name that fails."""
            raise SampleException()

        self.patch(main, "get_token_name", fake_gtn)

        self.patch(self.obj, "EmailValidated",
                   partial(self.errback_deferred, "EmailValidated"))
        self.patch(self.obj, "EmailValidationError", self.callback_deferred)
        self.obj.validate_email(APP_NAME, EMAIL, PASSWORD, EMAIL_TOKEN)

        app_name, error = yield self.deferred
        self.assertEqual(app_name, APP_NAME)
        self.assertTrue(isinstance(error, SampleException))
        self.assertFalse(self.keyring_was_set, "Keyring should not be set")

    @defer.inlineCallbacks
    def test_request_password_reset_token(self):
        """Test the request_password_reset_token method fails as expected."""
        self.patch(self.obj, "PasswordResetTokenSent",
                   partial(self.errback_deferred, "PasswordResetTokenSent"))
        self.patch(self.obj, "PasswordResetError", self.callback_deferred)
        self.patch(self.obj.processor, "request_password_reset_token",
                   lambda _: defer.fail(SampleException()))
        self.obj.request_password_reset_token(APP_NAME, EMAIL)

        app_name, error = yield self.deferred
        self.assertTrue(isinstance(error, SampleException))
        self.assertEqual(app_name, APP_NAME)

    @defer.inlineCallbacks
    def test_set_new_password(self):
        """Test that the set_new_password method fails as expected."""
        self.patch(self.obj, "PasswordChanged",
                   partial(self.errback_deferred, "PasswordChanged"))
        self.patch(self.obj, "PasswordChangeError", self.callback_deferred)
        self.patch(self.obj.processor, "set_new_password",
                   lambda *args: defer.fail(SampleException()))
        self.obj.set_new_password(APP_NAME, EMAIL, EMAIL_TOKEN, PASSWORD)

        app_name, error = yield self.deferred
        self.assertTrue(isinstance(error, SampleException))
        self.assertEqual(app_name, APP_NAME)


class PingUrlOnNewToken(SSOLoginBaseTestCase):
    """The test case for the operations that pings a given url."""

    method = 'login'
    args = (APP_NAME, EMAIL, PASSWORD, PING_URL)
    success_signal = 'LoggedIn'
    error_signal = 'LoginError'
    unexpected_signals = ('UserNotValidated',)

    @defer.inlineCallbacks
    def setUp(self):
        yield super(PingUrlOnNewToken, self).setUp()
        self.patch(main.utils, 'ping_url', self.my_ping)
        self.patch(self.obj.processor, 'is_validated', lambda _: True)
        self.obj.processor._next_result = TOKEN
        self._next_ping_result = defer.succeed('foo')

        for i in self.unexpected_signals:
            self.patch(self.obj, i, partial(self.errback_deferred, i))

    def my_ping(self, *a):
        """Fake the ping_url method."""
        self._called = a
        return self._next_ping_result

    @defer.inlineCallbacks
    def test_url_is_pinged(self):
        """The method calls self.success_signal when ping was completed."""
        self.patch(self.obj, self.success_signal, self.callback_deferred)
        self.patch(self.obj, self.error_signal,
                   partial(self.errback_deferred, self.error_signal))

        getattr(self.obj, self.method)(*self.args)

        app_name, result = yield self.deferred

        # url was pinged with the utils.ping_url method
        self.assertEqual(self._called, (PING_URL, EMAIL, TOKEN))
        # token was stored in the keyring
        self.assertTrue(self.keyring_was_set, "The keyring should be set")

        self.assertEqual(app_name, APP_NAME)
        self.assertEqual(result, EMAIL)

    @defer.inlineCallbacks
    def test_ping_url_raises_exception(self):
        """Exception is handled if ping fails."""
        self.patch(self.obj, self.error_signal, self.callback_deferred)
        self.patch(self.obj, self.success_signal,
                   partial(self.errback_deferred, self.success_signal))

        error = 'Blu'
        self._next_ping_result = defer.fail(ValueError(error))
        getattr(self.obj, self.method)(*self.args)

        app_name, error = yield self.deferred

        # token was not stored in the keyring
        self.assertFalse(self.keyring_was_set, "The keyring should not be set")
        self.assertEqual(app_name, APP_NAME)
        self.assertIsInstance(error, ValueError)
        self.assertIn('Blu', error.message)


class PingUrlOnNewTokenEmailValidated(PingUrlOnNewToken):
    """The test case for the operations that pings a given url."""

    method = 'validate_email'
    args = (APP_NAME, EMAIL, PASSWORD, EMAIL_TOKEN, PING_URL)
    success_signal = 'EmailValidated'
    error_signal = 'EmailValidationError'
    unexpected_signals = ()


class CredentialsManagementTestCase(BaseTestCase):
    """Tests for the CredentialsManagement DBus interface."""

    base_args = {
        HELP_TEXT_KEY: HELP_TEXT, PING_URL_KEY: PING_URL,
        POLICY_URL_KEY: POLICY_URL,
        TC_URL_KEY: TC_URL, WINDOW_ID_KEY: WINDOW_ID,
        UI_EXECUTABLE_KEY: 'super-ui',
    }

    @defer.inlineCallbacks
    def setUp(self):
        yield super(CredentialsManagementTestCase, self).setUp()

        self.factory = FakedCredentialsFactory()
        self.patch(main, 'Credentials', self.factory.Credentials)
        self.obj = CredentialsManagement(timeout_func=lambda *a: None,
                                         shutdown_func=lambda *a: None,
                                         proxy=self.proxy)
        self.args = {}
        self.cred_args = {}

        self.memento = MementoHandler()
        self.memento.setLevel(logging.DEBUG)
        main.logger.addHandler(self.memento)
        self.addCleanup(main.logger.removeHandler, self.memento)


class CredentialsManagementRefCountingTestCase(CredentialsManagementTestCase):
    """Tests for the CredentialsManagement ref counting."""

    @defer.inlineCallbacks
    def assert_refcounter_increased(self, method_name, signal_name=None):
        """Assert that calling 'method_name' increases the ref counter."""
        if signal_name is not None:
            d = defer.Deferred()
            self.patch(self.obj, signal_name, lambda *a: d.callback(a))
        else:
            d = defer.succeed(None)

        getattr(self.obj, method_name)(APP_NAME, self.args)

        yield d
        self.assertEqual(self.obj.ref_count, 1)

    def assert_refcounter_decreased(self, signal_name, *a, **kw):
        """Assert that calling 'signal_name' decreases the ref counter."""
        self.obj.ref_count = 3

        getattr(self.obj, signal_name)(*a, **kw)

        self.assertEqual(self.obj.ref_count, 2)

    def assert_refcounter_negative(self, signal_name, *a, **kw):
        """Assert that calling 'signal_name' decreases the ref counter.

        If decreased value is negative, assert that a warning log was made.

        """
        self.obj._ref_count = -3  # overwrite internal to force a negative

        getattr(self.obj, signal_name)(*a, **kw)

        self.assertEqual(self.obj.ref_count, 0)
        msg = 'Attempting to decrease ref_count to a negative value (-4).'
        self.assertTrue(self.memento.check_warning(msg))

    def test_ref_counting(self):
        """Ref counting is in place."""
        self.assertEqual(self.obj.ref_count, 0)

    @defer.inlineCallbacks
    def test_find_credentials(self):
        """Keep proper track of ongoing requests."""
        yield self.assert_refcounter_increased('find_credentials',
                                               'CredentialsFound')

    @defer.inlineCallbacks
    def test_find_credentials_with_success_cb(self):
        """Keep proper track of ongoing requests."""
        d = defer.Deferred()
        counter = []

        def in_the_middle():
            """Store the ref_count value."""
            counter.append(self.obj.ref_count)
            return defer.succeed(TOKEN)

        self.patch(self.factory.credentials, 'find_credentials', in_the_middle)
        self.obj.find_credentials(APP_NAME, self.args,
                                  success_cb=d.callback, error_cb=d.errback)
        yield d
        self.assertEqual(counter, [1])
        self.assertEqual(self.obj.ref_count, 0)

    @defer.inlineCallbacks
    def test_find_credentials_with_error_cb(self):
        """Keep proper track of ongoing requests."""
        d = defer.Deferred()
        counter = []

        def in_the_middle():
            """Store the ref_count value."""
            counter.append(self.obj.ref_count)
            return defer.fail(SampleException('foo'))

        self.patch(self.factory.credentials, 'find_credentials', in_the_middle)
        self.obj.find_credentials(APP_NAME, self.args,
                                  success_cb=d.errback, error_cb=d.callback)

        yield d
        self.assertEqual(counter, [1])
        self.assertEqual(self.obj.ref_count, 0)

    @defer.inlineCallbacks
    def test_clear_credentials(self):
        """Keep proper track of ongoing requests."""
        yield self.assert_refcounter_increased('clear_credentials',
                                               'CredentialsCleared')

    @defer.inlineCallbacks
    def test_store_credentials(self):
        """Keep proper track of ongoing requests."""
        yield self.assert_refcounter_increased('store_credentials',
                                               'CredentialsStored')

    @defer.inlineCallbacks
    def test_register(self):
        """Keep proper track of ongoing requests."""
        yield self.assert_refcounter_increased('register',
                                               'CredentialsFound')

    @defer.inlineCallbacks
    def test_login(self):
        """Keep proper track of ongoing requests."""
        yield self.assert_refcounter_increased('login',
                                               'CredentialsFound')

    def test_several_requests(self):
        """Requests can be nested."""
        # by patching the CredentialsFound signal, the counter is not decreased
        self.patch(self.obj, 'CredentialsFound', lambda *a: None)

        self.obj.login(APP_NAME, self.args)
        self.obj.register(APP_NAME, self.args)
        self.obj.login(APP_NAME, self.args)
        self.obj.register(APP_NAME, self.args)
        self.obj.register(APP_NAME, self.args)

        self.assertEqual(self.obj.ref_count, 5)

    def test_credentials_found(self):
        """Ref counter is decreased when a signal is sent."""
        self.assert_refcounter_decreased('CredentialsFound', APP_NAME, TOKEN)

    def test_credentials_not_found(self):
        """Ref counter is decreased when a signal is sent."""
        self.assert_refcounter_decreased('CredentialsNotFound', APP_NAME)

    def test_credentials_cleared(self):
        """Ref counter is decreased when a signal is sent."""
        self.assert_refcounter_decreased('CredentialsCleared', APP_NAME)

    def test_credentials_stored(self):
        """Ref counter is decreased when a signal is sent."""
        self.assert_refcounter_decreased('CredentialsStored', APP_NAME)

    def test_credentials_error(self):
        """Ref counter is decreased when a signal is sent."""
        self.assert_refcounter_decreased('CredentialsError', APP_NAME,
                                         SampleException('test'))

    def test_authorization_denied(self):
        """Ref counter is decreased when a signal is sent."""
        self.assert_refcounter_decreased('AuthorizationDenied', APP_NAME)

    def test_credentials_found_when_ref_count_is_not_positive(self):
        """Ref counter is decreased when a signal is sent."""
        self.assert_refcounter_negative('CredentialsFound', APP_NAME, TOKEN)

    def test_credentials_not_found_when_ref_count_is_not_positive(self):
        """Ref counter is decreased when a signal is sent."""
        self.assert_refcounter_negative('CredentialsNotFound', APP_NAME)

    def test_credentials_cleared_when_ref_count_is_not_positive(self):
        """Ref counter is decreased when a signal is sent."""
        self.assert_refcounter_negative('CredentialsCleared', APP_NAME)

    def test_credentials_stored_when_ref_count_is_not_positive(self):
        """Ref counter is decreased when a signal is sent."""
        self.assert_refcounter_negative('CredentialsStored', APP_NAME)

    def test_credentials_error_when_ref_count_is_not_positive(self):
        """Ref counter is decreased when a signal is sent."""
        self.assert_refcounter_negative('CredentialsError', APP_NAME,
                                        SampleException('test'))

    def test_autorization_denied_when_ref_count_is_not_positive(self):
        """Ref counter is decreased when a signal is sent."""
        self.assert_refcounter_negative('AuthorizationDenied', APP_NAME)

    def test_on_zero_ref_count_shutdown(self):
        """When ref count reaches 0, queue shutdown op."""
        self.patch(self.obj, 'timeout_func', self._set_called)
        self.obj.login(APP_NAME, self.args)

        self.assertEqual(self._called,
                         ((TIMEOUT_INTERVAL, self.obj.shutdown), {}))

    def test_on_non_zero_ref_count_do_not_shutdown(self):
        """If ref count is not 0, do not queue shutdown op."""
        self.patch(self.obj, 'timeout_func', self._set_called)
        # by patching the CredentialsFound signal, the counter is not decreased
        self.patch(self.obj, 'CredentialsFound', lambda *a: None)
        self.obj.login(APP_NAME, self.args)

        self.assertEqual(self._called, False)

    def test_on_non_zero_ref_count_after_zero_do_not_shutdown(self):
        """If the shutdown was queued, do not quit if counter is not zero."""

        def fake_timeout_func(interval, func):
            """Start a new request when the timer is started."""
            # the counter is not decreased
            self.patch(self.obj, 'CredentialsFound', lambda *a: None)
            self.obj.register(APP_NAME, self.args)
            assert self.obj.ref_count > 0
            func()

        self.patch(self.obj, 'timeout_func', fake_timeout_func)
        self.patch(self.obj, 'shutdown_func', self._set_called)

        self.obj.login(APP_NAME, self.args)
        # counter reached 0, timeout_func was called and register was called

        self.assertEqual(self._called, False, 'shutdown_func was not called')

    def test_zero_ref_count_after_zero_do_shutdown(self):
        """If the shutdown was queued, do quit if counter is zero."""

        def fake_timeout_func(interval, func):
            """Start a new request when the timer is started."""
            assert self.obj.ref_count == 0
            func()

        self.patch(self.obj, 'timeout_func', fake_timeout_func)
        self.patch(self.obj, 'shutdown_func', self._set_called)

        self.obj.login(APP_NAME, self.args)
        # counter reached 0, timeout_func was called

        self.assertEqual(self._called, ((), {}), 'shutdown_func was called')


class CredentialsManagementClearTestCase(CredentialsManagementTestCase):
    """Tests for the CredentialsManagement clear method."""

    method = 'clear_credentials'
    success_signal = 'CredentialsCleared'
    success_result = (APP_NAME,)
    others_should_errback = []

    @defer.inlineCallbacks
    def setUp(self):
        yield super(CredentialsManagementClearTestCase, self).setUp()
        self.call_method = getattr(self.obj, self.method)

    def test_backend_called(self):
        """The credentials backend is properly called."""
        self.call_method(APP_NAME, self.args)
        self.assert_recorder_called(self.factory, 'Credentials', APP_NAME)

    @defer.inlineCallbacks
    def test_does_not_block(self):
        """Calling 'method' does not block but return thru signals."""
        self.patch(self.obj, self.success_signal, self.callback_deferred)
        self.patch(self.obj, 'CredentialsError',
                   partial(self.errback_deferred, 'CredentialsError'))
        for signal in self.others_should_errback:
            self.patch(self.obj, signal,
                       partial(self.errback_deferred, signal))

        self.call_method(APP_NAME, self.args)

        result = yield self.deferred
        self.assertEqual(result, self.success_result)

    @defer.inlineCallbacks
    def test_handles_error(self):
        """If calling the backend fails, CredentialsError is sent."""
        self.patch(self.obj, self.success_signal,
                   partial(self.errback_deferred, self.success_signal))
        self.patch(self.obj, 'CredentialsError', self.callback_deferred)

        exc = SampleException('baz')
        self.patch(self.factory.credentials, self.method,
                   lambda *a: defer.fail(exc))
        self.call_method(APP_NAME, self.args)

        app_name, error = yield self.deferred
        self.assertEqual(error, exc)
        self.assertEqual(app_name, APP_NAME)


class CredentialsManagementStoreTestCase(CredentialsManagementClearTestCase):
    """Tests for the CredentialsManagement store method."""

    method = 'store_credentials'
    success_signal = 'CredentialsStored'


class CredentialsManagementFindTestCase(CredentialsManagementClearTestCase):
    """Tests for the CredentialsManagement find method."""

    method = 'find_credentials'
    success_signal = 'CredentialsFound'
    success_result = (APP_NAME, TOKEN)
    others_should_errback = ['CredentialsNotFound']

    @defer.inlineCallbacks
    def test_find_credentials_with_success_cb(self):
        """The credentials are asked and returned in a thread_execute call."""
        d = defer.Deferred()

        self.obj.find_credentials(APP_NAME, self.args,
                                  success_cb=d.callback, error_cb=d.errback)

        creds = yield d
        expected = yield self.factory.credentials.find_credentials()
        self.assertEqual(creds, expected)
        self.assert_recorder_called(self.factory, 'Credentials', APP_NAME)

    @defer.inlineCallbacks
    def test_find_credentials_with_error_cb(self):
        """If find_credentials fails, error_cb is called."""
        d = defer.Deferred()

        self.patch(self.factory.credentials, 'find_credentials',
                   lambda *a: defer.fail(SampleException('baz')))
        self.obj.find_credentials(APP_NAME, self.args,
                                  success_cb=d.errback, error_cb=d.callback)

        errdict = yield d
        self.assertEqual(errdict["errtype"], "SampleException")
        self.assert_recorder_called(self.factory, 'Credentials', APP_NAME)


class CredentialsManagementNotFindTestCase(CredentialsManagementFindTestCase):
    """Tests for the CredentialsManagement find method."""

    success_signal = 'CredentialsNotFound'
    success_result = (APP_NAME,)
    others_should_errback = ['CredentialsFound']

    @defer.inlineCallbacks
    def setUp(self):
        yield super(CredentialsManagementNotFindTestCase, self).setUp()
        self.call_method = getattr(self.obj, self.method)
        self.patch(self.factory.credentials, self.method,
                   lambda: defer.succeed({}))


class CredentialsManagementOpsTestCase(CredentialsManagementTestCase):
    """Tests for the CredentialsManagement login/register methods."""

    @defer.inlineCallbacks
    def setUp(self):
        yield super(CredentialsManagementOpsTestCase, self).setUp()
        self.args = dict(self.base_args)
        self.args[WINDOW_ID_KEY] = str(self.args[WINDOW_ID_KEY])
        self.cred_args = self.base_args.copy()

    def test_register(self):
        """The registration is correct."""
        self.obj.register(APP_NAME, self.args)
        self.assert_recorder_called(self.factory, 'Credentials',
                                    APP_NAME, **self.cred_args)

    def test_login(self):
        """The login is correct."""
        self.obj.login(APP_NAME, self.args)
        self.assert_recorder_called(self.factory, 'Credentials',
                                    APP_NAME, **self.cred_args)

    def test_login_email_password(self):
        """The login_email_password is correct."""
        self.args['email'] = EMAIL
        self.args['password'] = PASSWORD
        self.obj.login_email_password(APP_NAME, self.args)
        self.assert_recorder_called(self.factory, 'Credentials',
                                    APP_NAME, **self.cred_args)


class CredentialsManagementParamsTestCase(CredentialsManagementOpsTestCase):
    """Tests for the CredentialsManagement extra parameters handling."""

    @defer.inlineCallbacks
    def setUp(self):
        yield super(CredentialsManagementParamsTestCase, self).setUp()
        self.args['dummy'] = 'nothing useful'


class CredentialsManagementSignalsTestCase(CredentialsManagementTestCase):
    """Tests for the CredentialsManagement DBus signals."""

    def assert_signal_logged(self, signal, *args):
        """Check that signal info was properly logged."""
        signal(APP_NAME, *args)
        msgs = [self.obj.__class__.__name__,
                signal.__name__, repr(APP_NAME)]
        self.assertTrue(self.memento.check_info(*msgs))

    def test_credentials_found(self):
        """The CredentialsFound signal."""
        self.assert_signal_logged(self.obj.CredentialsFound, TOKEN)

        msg = 'credentials must not be logged (found %r in log).'
        for val in TOKEN.itervalues():
            self.assertFalse(self.memento.check_info(val), msg % val)

    def test_credentials_not_found(self):
        """The CredentialsNotFound signal."""
        self.assert_signal_logged(self.obj.CredentialsNotFound)

    def test_credentials_cleared(self):
        """The CredentialsCleared signal."""
        self.assert_signal_logged(self.obj.CredentialsCleared)

    def test_credentials_stored(self):
        """The CredentialsStored signal."""
        self.assert_signal_logged(self.obj.CredentialsStored)

    def test_credentials_error(self):
        """The CredentialsError signal."""
        error = {'error_message': 'failed!', 'detailed error': 'yadda yadda'}
        self.obj.CredentialsError(APP_NAME, error)
        msgs = (self.obj.__class__.__name__,
                self.obj.CredentialsError.__name__,
                repr(APP_NAME), repr(error))
        self.assertTrue(self.memento.check_error(*msgs))

    def test_authorization_denied(self):
        """The AuthorizationDenied signal."""
        self.assert_signal_logged(self.obj.AuthorizationDenied)


class UbuntuSSOServiceTestCase(BaseTestCase):
    """Test suite for the UbuntuSSOService class."""

    @defer.inlineCallbacks
    def setUp(self):
        yield super(UbuntuSSOServiceTestCase, self).setUp()
        self.proxy.sso_login = object()
        self.proxy.cred_manager = object()
        self.params = []
        self.patch(main, 'UbuntuSSOProxy',
                   lambda _: self.params.append(_) or self.proxy)
        self.obj = UbuntuSSOService()
        yield self.obj.start()

    def test_creation(self):
        """Creation paremeter is properly used."""
        self.assertEqual(self.params, [self.obj])

    def test_sso_login(self):
        """Attributes has the expected value."""
        self.assertIsInstance(self.obj.sso_login, SSOLogin)
        self.assertTrue(self.obj.sso_login.proxy is self.proxy.sso_login)

    def test_cred_manager(self):
        """Attributes has the expected value."""
        self.assertIsInstance(self.obj.cred_manager, CredentialsManagement)
        self.assertTrue(self.obj.cred_manager.proxy is self.proxy.cred_manager)
