##
## test_authentication.py
## Login : <uli@pu.smp.net>
## Started on  Fri Aug 20 08:18:58 2010 Uli Fouquet
## $Id$
## 
## Copyright (C) 2010 Uli Fouquet
## 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 2 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, write to the Free Software
## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
##
import grok
import shutil
import tempfile
import unittest
from zope.app.testing.functional import FunctionalTestCase
from zope.authentication.interfaces import IAuthentication
from zope.component import provideAdapter, getUtility
from zope.component.hooks import setSite, clearSite
from zope.interface import verify
from zope.pluggableauth.interfaces import  IAuthenticatedPrincipalFactory
from zope.publisher.browser import TestRequest
from zope.publisher.interfaces import IRequest
from zope.session.interfaces import ISession
from zope.site import LocalSiteManager
from zope.testbrowser.testing import Browser
from zope.testing import cleanup
from waeup.sirp.testing import FunctionalLayer
from waeup.sirp.app import University
from waeup.sirp.applicants import  ApplicantsContainer, Applicant
from waeup.sirp.applicants.authentication import (
    ApplicantsAuthenticatorPlugin, WAeUPApplicantCredentialsPlugin,
    ApplicantCredentials, AuthenticatedApplicantPrincipalFactory,
    ApplicantPrincipalInfo, ApplicantPrincipal,)


class FakeBatch(dict):
    def getAccessCode(self, id):
        return self.get(id)

class FakeAccessCode(object):
    def __init__(self, repr, inv_date=None, disabled=False):
        self.used = inv_date is not None
        self.representation = repr
        self.disabled = disabled

class FakeInvAccessCode(object):
    invalidation_date = True

class FakeApplication(object):
    def __init__(self, ac=None):
        self.access_code = ac

def FakeSite():
    return {
        'applicants': {
            'APP': {
                'APP-12345': FakeApplication(u'APP-12345'),
                'APP-54321': FakeApplication(u'APP-54321'),
                'APP-22222': FakeApplication(u'APP-OTHER'),
                'APP-44444': FakeApplication(),
                'APP-55555': FakeApplication(u'APP-OTHER'),
                },
            'JAMB': {
                'JAMB1': FakeApplication(),
                'JAMB2': FakeApplication(u'APP-12345'),
                'JAMB3': FakeApplication(u'APP-54321'),
                'JAMB4': FakeApplication(u'APP-44444'),
                },
            },
        'accesscodes': {
            'APP': FakeBatch({
                    'APP-12345': FakeAccessCode('APP-12345'),
                    'APP-54321': FakeAccessCode('APP-54321', True),
                    'APP-11111': FakeAccessCode('APP-11111'),
                    'APP-22222': FakeAccessCode('APP-22222'),
                    'APP-33333': FakeAccessCode('APP-33333', True),
                    'APP-44444': FakeAccessCode('APP-44444', True),
                    'APP-55555': FakeAccessCode('APP-55555', True),
                    'APP-66666': FakeAccessCode('APP-66666', True, False),
                    'APP-77777': FakeAccessCode('APP-77777', False, False),
                    })
            }
        }

class AuthenticatorPluginTest(FunctionalTestCase):

    layer = FunctionalLayer

    def create_applicant(self, cname, aname, ac=None):
        # Create an applicant in an applicants container
        setSite(self.app)
        if not cname in self.app['applicants'].keys():
            container = ApplicantsContainer()
            self.app['applicants'][cname] = container
        applicant = Applicant()
        if ac is not None:
            applicant.access_code = ac
        self.app['applicants'][cname][aname] = applicant
        return

    def setUp(self):
        super(AuthenticatorPluginTest, self).setUp()

        # Setup a sample site for each test
        app = University()
        self.dc_root = tempfile.mkdtemp()
        app['datacenter'].setStoragePath(self.dc_root)

        # Prepopulate the ZODB...
        self.getRootFolder()['app'] = app
        self.app = self.getRootFolder()['app']

        fake_site = FakeSite()
        for ckey, fake_container in fake_site['applicants'].items():
            for akey, fake_appl in fake_container.items():
                self.create_applicant(ckey, akey, fake_appl.access_code)
        del self.app['accesscodes']
        self.app['accesscodes'] = fake_site['accesscodes']

        self.plugin = ApplicantsAuthenticatorPlugin()
        return

    def tearDown(self):
        super(AuthenticatorPluginTest, self).tearDown()
        shutil.rmtree(self.dc_root)
        return

    def test_invalid_credentials(self):
        result = self.plugin.authenticateCredentials('not-a-dict')
        assert result is None

        result = self.plugin.authenticateCredentials(
            dict(accesscode=None, foo='blah'))
        assert result is None

        result = self.plugin.authenticateCredentials(
            dict(accesscode='Nonsense',))
        assert result is None

        # Possible cases, where formal correct authentication
        # data is not valid:
        result = self.plugin.authenticateCredentials(
            dict(accesscode='APP-33333'))
        assert result is None

        result = self.plugin.authenticateCredentials(
            dict(accesscode='APP-55555'))
        assert result is None

        result = self.plugin.authenticateCredentials(
            dict(accesscode='APP-66666'))
        assert result is None

        result = self.plugin.authenticateCredentials(
            dict(accesscode='APP-77777'))
        assert result is None
        return

    def test_valid_credentials(self):
        """The six different cases where we allow login.

        All other combinations should be forbidden.
        """
        result = self.plugin.authenticateCredentials(
            dict(accesscode='APP-11111'))
        assert result is not None

        result = self.plugin.authenticateCredentials(
            dict(accesscode='APP-12345'))
        assert result is not None

        result = self.plugin.authenticateCredentials(
            dict(accesscode='APP-54321'))
        assert result is not None

        # check the `principalInfo` method of authenticator
        # plugin. This is only here to satisfy the coverage report.
        assert self.plugin.principalInfo('not-an-id') is None
        return

session_data = {
    'zope.pluggableauth.browserplugins': {}
    }

class FakeSession(dict):
    def __init__(self, request):
        pass

    def get(self, key, default=None):
        return self.__getitem__(key, default)

    def __getitem__(self, key, default=None):
        return session_data.get(key, default)

    def __setitem__(self, key, value):
        session_data[key] = value
        return

class CredentialsPluginTest(unittest.TestCase):

    def setUp(self):
        self.request = TestRequest()
        provideAdapter(FakeSession, (IRequest,), ISession)
        self.plugin = WAeUPApplicantCredentialsPlugin()
        self.full_request = TestRequest()
        session_data['zope.pluggableauth.browserplugins'] = {}
        return

    def tearDown(self):
        cleanup.tearDown()
        return

    def filled_request(self, form_dict):
        request = TestRequest()
        for key, value in form_dict.items():
            request.form[key] = value
        return request

    def test_extractCredentials_invalid(self):
        result = self.plugin.extractCredentials('not-a-request')
        assert result is None
        return

    def test_extractCredentials_empty(self):
        result = self.plugin.extractCredentials(self.request)
        assert result is None
        return

    def test_extractCredentials_full_set(self):
        request = self.filled_request({
                'form.ac_prefix': 'APP',
                'form.ac_series': '1',
                'form.ac_number': '1234567890',
                #'form.jamb_reg_no': 'JAMB_NUMBER',
                })
        result = self.plugin.extractCredentials(request)
        self.assertEqual(result, {'accesscode': 'APP-1-1234567890'})
        return

    def test_extractCredentials_accesscode_only(self):
        request = self.filled_request({
                'form.ac_prefix': 'APP',
                'form.ac_series': '1',
                'form.ac_number': '1234567890',
                })
        result = self.plugin.extractCredentials(request)
        self.assertEqual(result, {'accesscode': 'APP-1-1234567890'})
        return

    def test_extractCredentials_from_empty_session(self):
        session_data['zope.pluggableauth.browserplugins']['credentials'] = None
        result = self.plugin.extractCredentials(self.request)
        assert result is None
        return

    def test_extractCredentials_from_nonempty_session(self):
        credentials = ApplicantCredentials('APP-1-12345')
        session_data['zope.pluggableauth.browserplugins'][
            'credentials'] = credentials
        result = self.plugin.extractCredentials(self.request)
        self.assertEqual(result, {'accesscode': 'APP-1-12345'})
        return


class ApplicantCredentialsTest(unittest.TestCase):

    def setUp(self):
        self.credentials = ApplicantCredentials('SOME_ACCESSCODE')
        return

    def tearDown(self):
        return

    def test_methods(self):
        self.assertEqual(self.credentials.getAccessCode(), 'SOME_ACCESSCODE')
        assert self.credentials.getLogin() is None
        assert self.credentials.getPassword() is None
        return

class FakePluggableAuth(object):
    prefix = 'foo'

class PrincipalFactoryTest(unittest.TestCase):

    def setUp(self):
        self.info = ApplicantPrincipalInfo('APP-1-1234567890')
        return

    def tearDown(self):
        pass

    def test_principalFactory_interface(self):
        verify.verifyClass(IAuthenticatedPrincipalFactory,
                           AuthenticatedApplicantPrincipalFactory
                           )
        return

    def test_principalFactory_create(self):
        factory = AuthenticatedApplicantPrincipalFactory(self.info, None)

        assert factory.info is self.info
        assert factory.request is None
        return

    def test_principalFactory_call_w_prefix(self):
        factory = AuthenticatedApplicantPrincipalFactory(self.info, None)
        principal = factory(FakePluggableAuth())

        assert isinstance(principal, ApplicantPrincipal)
        self.assertEqual(principal.__repr__(),
                         "ApplicantPrincipal('foo.APP-1-1234567890')")
        self.assertEqual(principal.id, 'foo.APP-1-1234567890')
        return

    def test_principalFactory_call_wo_prefix(self):
        factory = AuthenticatedApplicantPrincipalFactory(self.info, None)
        fake_auth = FakePluggableAuth()
        fake_auth.prefix = None
        principal = factory(fake_auth)
        self.assertEqual(principal.id, 'APP-1-1234567890')
        return

class PAUSetupTest(FunctionalTestCase):
    # Check correct setup of authentication components in the
    # applicants subpackage.

    # When a university is created, we want by default have our local
    # authentication components (an authenticator plugin and a
    # credentials plugin) to be registered with the local PAU. Admins
    # can remove these components on the fly later-on if they wish.

    layer = FunctionalLayer

    def setUp(self):
        super(PAUSetupTest, self).setUp()

        # Setup a sample site for each test
        app = University()
        self.dc_root = tempfile.mkdtemp()
        app['datacenter'].setStoragePath(self.dc_root)

        # Prepopulate the ZODB...
        self.getRootFolder()['app'] = app
        self.app = self.getRootFolder()['app']
        self.browser = Browser()
        self.browser.handleErrors = False

    def tearDown(self):
        super(PAUSetupTest, self).tearDown()
        shutil.rmtree(self.dc_root)

    def test_extra_auth_plugins_installed(self):
        # Check whether the auth plugins defined in here are setup
        # automatically when a university is created

        # Get the PAU responsible for the local site ('app')
        pau = getUtility(IAuthentication, context=self.app)
        cred_plugins = pau.getCredentialsPlugins()
        auth_plugins = pau.getAuthenticatorPlugins()
        cred_names = [name for name, plugin in cred_plugins]
        auth_names = [name for name, plugin in auth_plugins]

        # Make sure our local ApplicantsAuthenticatorPlugin is registered...
        self.assertTrue('applicants' in auth_names)
        # Make sure our local WAeUPApplicantCredentialsPlugin is registered...
        self.assertTrue('applicant_credentials' in cred_names)
        return

def test_suite():
    suite = unittest.TestSuite()
    for testcase in [
        AuthenticatorPluginTest, CredentialsPluginTest,
        ApplicantCredentialsTest, PrincipalFactoryTest,
        PAUSetupTest,
        ]:
        suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
                testcase
                )
        )
    return suite
