import unittest


class AuthTicketTests(unittest.TestCase):

    def _getTargetClass(self):
        from repoze.who._auth_tkt import AuthTicket
        return AuthTicket

    def _makeOne(self, *args, **kw):
        return self._getTargetClass()(*args, **kw)

    def test_ctor_defaults(self):
        import hashlib
        from repoze.who import _auth_tkt
        with _Monkey(_auth_tkt, time_mod=_Timemod):
            tkt = self._makeOne('SEEKRIT', 'USERID', '1.2.3.4')
        self.assertEqual(tkt.secret, 'SEEKRIT')
        self.assertEqual(tkt.userid, 'USERID')
        self.assertEqual(tkt.ip, '1.2.3.4')
        self.assertEqual(tkt.tokens, '')
        self.assertEqual(tkt.user_data, '')
        self.assertEqual(tkt.time, _WHEN)
        self.assertEqual(tkt.cookie_name, 'auth_tkt')
        self.assertEqual(tkt.secure, False)
        self.assertEqual(tkt.digest_algo, hashlib.md5)

    def test_ctor_w_userid_w_embedded_bang(self):
        tokens = ('a,b',)  # cannot be safely round-tripped

        with self.assertRaises(ValueError) as exc:
            self._makeOne('SEEKRIT', 'USER!ID', '1.2.3.4')

        self.assertEqual(str(exc.exception), "'userid' may not contain '!'")

    def test_ctor_w_token_w_embedded_bang(self):
        tokens = ('a!b',)  # cannot be safely round-tripped

        with self.assertRaises(ValueError) as exc:
            self._makeOne('SEEKRIT', 'USERID', '1.2.3.4', tokens=tokens)

        self.assertEqual(
            str(exc.exception), "'token' values may not contain '!'"
        )

    def test_ctor_w_token_w_embedded_comma(self):
        tokens = ('a,b',)  # cannot be safely round-tripped

        with self.assertRaises(ValueError) as exc:
            self._makeOne('SEEKRIT', 'USERID', '1.2.3.4', tokens=tokens)

        self.assertEqual(
            str(exc.exception), "'token' values may not contain ','"
        )

    def test_ctor_w_user_data_w_embedded_bang(self):
        user_data = 'DATA!HERE' # cannot be safely round-tripped

        with self.assertRaises(ValueError) as exc:
            self._makeOne('SEEKRIT', 'USERID', '1.2.3.4', user_data=user_data)

        self.assertEqual(str(exc.exception), "'user_data' may not contain '!'")

    def test_ctor_explicit(self):
        import hashlib
        tkt = self._makeOne('SEEKRIT', 'USERID', '1.2.3.4', tokens=('a', 'b'),
                            user_data='DATA', time=_WHEN,
                            cookie_name='oatmeal', secure=True,
                            digest_algo=hashlib.sha512)
        self.assertEqual(tkt.secret, 'SEEKRIT')
        self.assertEqual(tkt.userid, 'USERID')
        self.assertEqual(tkt.ip, '1.2.3.4')
        self.assertEqual(tkt.tokens, 'a,b')
        self.assertEqual(tkt.user_data, 'DATA')
        self.assertEqual(tkt.time, _WHEN)
        self.assertEqual(tkt.cookie_name, 'oatmeal')
        self.assertEqual(tkt.secure, True)
        self.assertEqual(tkt.digest_algo, hashlib.sha512)

    def test_ctor_string_algorithm(self):
        import hashlib
        tkt = self._makeOne('SEEKRIT', 'USERID', '1.2.3.4', tokens=('a', 'b'),
                            user_data='DATA', time=_WHEN,
                            cookie_name='oatmeal', secure=True,
                            digest_algo='sha1')
        self.assertEqual(tkt.secret, 'SEEKRIT')
        self.assertEqual(tkt.userid, 'USERID')
        self.assertEqual(tkt.ip, '1.2.3.4')
        self.assertEqual(tkt.tokens, 'a,b')
        self.assertEqual(tkt.user_data, 'DATA')
        self.assertEqual(tkt.time, _WHEN)
        self.assertEqual(tkt.cookie_name, 'oatmeal')
        self.assertEqual(tkt.secure, True)
        self.assertEqual(tkt.digest_algo, hashlib.sha1)

    def test_digest(self):
        from repoze.who._auth_tkt import calculate_digest, hashlib
        tkt = self._makeOne('SEEKRIT', 'USERID', '1.2.3.4', tokens=('a', 'b'),
                            user_data='DATA', time=_WHEN,
                            cookie_name='oatmeal', secure=True)
        digest = calculate_digest('1.2.3.4', _WHEN, 'SEEKRIT', 'USERID',
                                  'a,b', 'DATA', hashlib.md5)
        self.assertEqual(tkt.digest(), digest)

    def test_cookie_value_wo_tokens_or_userdata(self):
        from repoze.who._auth_tkt import calculate_digest, hashlib
        tkt = self._makeOne('SEEKRIT', 'USERID', '1.2.3.4', time=_WHEN)
        digest = calculate_digest('1.2.3.4', _WHEN, 'SEEKRIT', 'USERID',
                                  '', '', hashlib.md5)
        self.assertEqual(tkt.cookie_value(),
                         '%s%08xUSERID!' % (digest, _WHEN))

    def test_cookie_value_w_tokens_and_userdata(self):
        from repoze.who._auth_tkt import calculate_digest, hashlib
        tkt = self._makeOne('SEEKRIT', 'USERID', '1.2.3.4', tokens=('a', 'b'),
                            user_data='DATA', time=_WHEN)
        digest = calculate_digest('1.2.3.4', _WHEN, 'SEEKRIT', 'USERID',
                                  'a,b', 'DATA', hashlib.md5)
        self.assertEqual(tkt.cookie_value(),
                         '%s%08xUSERID!a,b!DATA' % (digest, _WHEN))

    def test_cookie_not_secure_wo_tokens_or_userdata(self):
        from repoze.who._auth_tkt import calculate_digest, hashlib
        from repoze.who._helpers import encodestring
        tkt = self._makeOne('SEEKRIT', 'USERID', '1.2.3.4', time=_WHEN,
                            cookie_name='oatmeal')
        digest = calculate_digest('1.2.3.4', _WHEN, 'SEEKRIT', 'USERID',
                                  '', '', hashlib.md5)
        cookie = tkt.cookie()
        self.assertEqual(cookie['oatmeal'].value,
                         encodestring('%s%08xUSERID!' % (digest, _WHEN)
                                     ).strip())
        self.assertEqual(cookie['oatmeal']['path'], '/')
        self.assertEqual(cookie['oatmeal']['secure'], '')

    def test_cookie_secure_w_tokens_and_userdata(self):
        from repoze.who._auth_tkt import calculate_digest, hashlib
        from repoze.who._helpers import encodestring
        tkt = self._makeOne('SEEKRIT', 'USERID', '1.2.3.4', tokens=('a', 'b'),
                            user_data='DATA', time=_WHEN,
                            cookie_name='oatmeal', secure=True)
        digest = calculate_digest('1.2.3.4', _WHEN, 'SEEKRIT', 'USERID',
                                  'a,b', 'DATA', hashlib.md5)
        cookie = tkt.cookie()
        self.assertEqual(cookie['oatmeal'].value,
                         encodestring('%s%08xUSERID!a,b!DATA' % (digest, _WHEN)
                                     ).strip())
        self.assertEqual(cookie['oatmeal']['path'], '/')
        self.assertEqual(cookie['oatmeal']['secure'], 'true')


class BadTicketTests(unittest.TestCase):

    def _getTargetClass(self):
        from repoze.who._auth_tkt import BadTicket
        return BadTicket

    def _makeOne(self, *args, **kw):
        return self._getTargetClass()(*args, **kw)

    def test_wo_expected(self):
        exc = self._makeOne('message')
        self.assertEqual(exc.args, ('message',))
        self.assertEqual(exc.expected, None)

    def test_w_expected(self):
        exc = self._makeOne('message', 'foo')
        self.assertEqual(exc.args, ('message',))
        self.assertEqual(exc.expected, 'foo')


class Test_parse_ticket(unittest.TestCase):

    def _callFUT(self, secret='SEEKRIT', ticket=None,
                 ip='1.2.3.4', digest="md5"):
        from repoze.who._auth_tkt import parse_ticket
        return parse_ticket(secret, ticket, ip, digest)

    def test_bad_timestamp(self):
        from repoze.who._auth_tkt import BadTicket
        TICKET = '12345678901234567890123456789012XXXXXXXXuserid!'
        try:
            self._callFUT(ticket=TICKET)
        except BadTicket as e:
            self.assertTrue(e.args[0].startswith(
                            'Timestamp is not a hex integer:'))
        else:  # pragma: no cover
            self.fail('Did not raise')

    def test_no_bang_after_userid(self):
        from repoze.who._auth_tkt import BadTicket
        TICKET = '1234567890123456789012345678901201020304userid'
        try:
            self._callFUT(ticket=TICKET)
        except BadTicket as e:
            self.assertEqual(e.args[0], 'userid is not followed by !')
        else:  # pragma: no cover
            self.fail('Did not raise')

    def test_wo_tokens_or_data_bad_digest(self):
        from repoze.who._auth_tkt import BadTicket
        TICKET = '1234567890123456789012345678901201020304userid!'
        try:
            self._callFUT(ticket=TICKET)
        except BadTicket as e:
            self.assertEqual(e.args[0], 'Digest signature is not correct')
        else:  # pragma: no cover
            self.fail('Did not raise')

    def test_wo_tokens_or_data_ok_digest(self):
        from repoze.who._auth_tkt import calculate_digest, hashlib
        digest = calculate_digest('1.2.3.4', _WHEN, 'SEEKRIT', 'USERID',
                                  '', '', hashlib.md5)
        TICKET = '%s%08xUSERID!' % (digest, _WHEN)
        timestamp, userid, tokens, user_data = self._callFUT(ticket=TICKET)
        self.assertEqual(timestamp, _WHEN)
        self.assertEqual(userid, 'USERID')
        self.assertEqual(tokens, [''])
        self.assertEqual(user_data, '')

    def test_w_tokens_and_data_ok_digest(self):
        from repoze.who._auth_tkt import calculate_digest, hashlib
        digest = calculate_digest('1.2.3.4', _WHEN, 'SEEKRIT', 'USERID',
                                  'a,b', 'DATA', hashlib.md5)
        TICKET = '%s%08xUSERID!a,b!DATA' % (digest, _WHEN)
        timestamp, userid, tokens, user_data = self._callFUT(ticket=TICKET)
        self.assertEqual(timestamp, _WHEN)
        self.assertEqual(userid, 'USERID')
        self.assertEqual(tokens, ['a', 'b'])
        self.assertEqual(user_data, 'DATA')

    def test_w_tokens_and_data_ok_alternate_digest(self):
        from repoze.who._auth_tkt import calculate_digest, hashlib
        digest = calculate_digest('1.2.3.4', _WHEN, 'SEEKRIT', 'USERID',
                                  'a,b', 'DATA', hashlib.sha256)
        TICKET = '%s%08xUSERID!a,b!DATA' % (digest, _WHEN)
        timestamp, userid, tokens, user_data = self._callFUT(
            ticket=TICKET, digest=hashlib.sha256)
        self.assertEqual(timestamp, _WHEN)
        self.assertEqual(userid, 'USERID')
        self.assertEqual(tokens, ['a', 'b'])
        self.assertEqual(user_data, 'DATA')


class Test_helpers(unittest.TestCase):

    # calculate_digest is not very testable, fully exercised through callers.

    def test_ints_to_bytes(self):
        from struct import pack
        from repoze.who._auth_tkt import ints2bytes
        self.assertEqual(ints2bytes([1, 2, 3, 4]), pack('>BBBB', 1, 2, 3, 4))
        
    def test_encode_ip_timestamp(self):
        from struct import pack
        from repoze.who._auth_tkt import encode_ip_timestamp
        self.assertEqual(encode_ip_timestamp('1.2.3.4', _WHEN),
                         pack('>BBBBL', 1, 2, 3, 4, _WHEN))

    def test_maybe_encode_bytes(self):
        from repoze.who._auth_tkt import maybe_encode
        foo = b'foo'
        self.assertTrue(maybe_encode(foo) is foo)

    def test_maybe_encode_native_string(self):
        from repoze.who._auth_tkt import maybe_encode
        foo = 'foo'
        self.assertEqual(maybe_encode(foo), b'foo')

    def test_maybe_encode_unicode(self):
        from repoze.who._auth_tkt import maybe_encode
        foo = u'foo'
        self.assertEqual(maybe_encode(foo), b'foo')


_WHEN = 1234567
class _Timemod(object):
    @staticmethod
    def time():
        return _WHEN


class _Monkey(object):

    def __init__(self, module, **replacements):
        self.module = module
        self.orig = {}
        self.replacements = replacements
        
    def __enter__(self):
        for k, v in self.replacements.items():
            orig = getattr(self.module, k, self)
            if orig is not self:
                self.orig[k] = orig
            setattr(self.module, k, v)

    def __exit__(self, *exc_info):
        for k, v in self.replacements.items():
            if k in self.orig:
                setattr(self.module, k, self.orig[k])
            else: #pragma NO COVERSGE
                delattr(self.module, k)
