Beispiel #1
0
    def test_remote_patron_lookup(self):
        #When the SIP authentication provider needs to look up a patron,
        #it calls patron_information on its SIP client and passes in None
        #for the password.
        patron = self._patron()
        patron.authorization_identifier = "1234"
        integration = self._external_integration(self._str)
        class Mock(MockSIPClient):
            def patron_information(self, identifier, password):
                self.patron_information = identifier
                self.password = password
                return self.patron_information_parser(TestSIP2AuthenticationProvider.polaris_wrong_pin)

        client = Mock()
        auth = SIP2AuthenticationProvider(
            self._default_library, integration, client=client
        )
        patron = auth._remote_patron_lookup(patron)
        eq_(patron.__class__, PatronData)
        eq_("25891000331441", patron.authorization_identifier)
        eq_("*****@*****.**", patron.email_address)
        eq_(9.25, patron.fines)
        eq_("Falk, Jen", patron.personal_name)
        eq_(datetime(2018, 6, 9, 23, 59, 59), patron.authorization_expires)
        eq_(client.patron_information, "1234")
        eq_(client.password, None)
Beispiel #2
0
    def test_info_to_patrondata_no_validate_password(self):
        integration = self._external_integration(self._str)
        integration.url = 'server.local'
        client = MockSIPClient()
        provider = SIP2AuthenticationProvider(
            self._default_library, integration, client=client
        )

        # Test with valid login, should return PatronData
        info = client.patron_information_parser(TestSIP2AuthenticationProvider.sierra_valid_login)
        patron = provider.info_to_patrondata(info, validate_password=False)
        eq_(patron.__class__, PatronData)
        eq_("12345", patron.authorization_identifier)
        eq_("*****@*****.**", patron.email_address)
        eq_("SHELDON, ALICE", patron.personal_name)
        eq_(0, patron.fines)
        eq_(None, patron.authorization_expires)
        eq_(None, patron.external_type)
        eq_(PatronData.NO_VALUE, patron.block_reason)

        # Test with invalid login, should return PatronData
        info = client.patron_information_parser(TestSIP2AuthenticationProvider.sierra_invalid_login)
        patron = provider.info_to_patrondata(info, validate_password=False)
        eq_(patron.__class__, PatronData)
        eq_("12345", patron.authorization_identifier)
        eq_("*****@*****.**", patron.email_address)
        eq_("SHELDON, ALICE", patron.personal_name)
        eq_(0, patron.fines)
        eq_(None, patron.authorization_expires)
        eq_(None, patron.external_type)
        eq_('no borrowing privileges', patron.block_reason)
Beispiel #3
0
    def test_remote_authenticate_no_password(self):

        integration = self._external_integration(self._str)
        p = SIP2AuthenticationProvider
        integration.setting(p.PASSWORD_KEYBOARD).value = p.NULL_KEYBOARD
        client = MockSIPClient()
        auth = SIP2AuthenticationProvider(
            self._default_library, integration, client=client
        )

        # This Evergreen instance doesn't use passwords.
        client.queue_response(self.evergreen_active_user)
        patrondata = auth.remote_authenticate("user", None)
        eq_("12345", patrondata.authorization_identifier)
        eq_("863715", patrondata.permanent_id)
        eq_("Booth Active Test", patrondata.personal_name)
        eq_(0, patrondata.fines)
        eq_(datetime(2019, 10, 4), patrondata.authorization_expires)
        eq_("Adult", patrondata.external_type)

        # If a password is specified, it is not sent over the wire.
        client.queue_response(self.evergreen_active_user)
        patrondata = auth.remote_authenticate("user2", "some password")
        eq_("12345", patrondata.authorization_identifier)
        request = client.requests[-1]
        assert 'user2' in request
        assert 'some password' not in request
    def test_info_to_patrondata_no_validate_password(self):
        integration = self._external_integration(self._str)
        integration.url = "server.local"
        provider = SIP2AuthenticationProvider(self._default_library,
                                              integration,
                                              client=MockSIPClientFactory())
        client = provider._client

        # Test with valid login, should return PatronData
        info = client.patron_information_parser(
            TestSIP2AuthenticationProvider.sierra_valid_login)
        patron = provider.info_to_patrondata(info, validate_password=False)
        assert patron.__class__ == PatronData
        assert "12345" == patron.authorization_identifier
        assert "*****@*****.**" == patron.email_address
        assert "LE CARRÉ, JOHN" == patron.personal_name
        assert 0 == patron.fines
        assert None == patron.authorization_expires
        assert None == patron.external_type
        assert PatronData.NO_VALUE == patron.block_reason

        # Test with invalid login, should return PatronData
        info = client.patron_information_parser(
            TestSIP2AuthenticationProvider.sierra_invalid_login)
        patron = provider.info_to_patrondata(info, validate_password=False)
        assert patron.__class__ == PatronData
        assert "12345" == patron.authorization_identifier
        assert "*****@*****.**" == patron.email_address
        assert "SHELDON, ALICE" == patron.personal_name
        assert 0 == patron.fines
        assert None == patron.authorization_expires
        assert None == patron.external_type
        assert "no borrowing privileges" == patron.block_reason
    def test_remote_patron_lookup(self):
        # When the SIP authentication provider needs to look up a patron,
        # it calls patron_information on its SIP client and passes in None
        # for the password.
        patron = self._patron()
        patron.authorization_identifier = "1234"
        integration = self._external_integration(self._str)

        class Mock(MockSIPClient):
            def patron_information(self, identifier, password):
                self.patron_information = identifier
                self.password = password
                return self.patron_information_parser(
                    TestSIP2AuthenticationProvider.polaris_wrong_pin)

        client = Mock()
        client.queue_response(self.end_session_response)
        auth = SIP2AuthenticationProvider(self._default_library,
                                          integration,
                                          client=client)
        patron = auth._remote_patron_lookup(patron)
        assert patron.__class__ == PatronData
        assert "25891000331441" == patron.authorization_identifier
        assert "*****@*****.**" == patron.email_address
        assert 9.25 == patron.fines
        assert "Falk, Jen" == patron.personal_name
        assert datetime(2018, 6, 9, 23, 59, 59) == patron.authorization_expires
        assert client.patron_information == "1234"
        assert client.password == None
    def test_patron_block_setting(self):
        integration_block = self._external_integration(
            self._str,
            settings={SIP2AuthenticationProvider.PATRON_STATUS_BLOCK: "true"})
        integration_noblock = self._external_integration(
            self._str,
            settings={SIP2AuthenticationProvider.PATRON_STATUS_BLOCK: "false"},
        )

        # Test with blocked patron, block should be set
        p = SIP2AuthenticationProvider(self._default_library,
                                       integration_block,
                                       client=MockSIPClientFactory())
        client = p._client
        info = client.patron_information_parser(
            TestSIP2AuthenticationProvider.evergreen_expired_card)
        patron = p.info_to_patrondata(info)
        assert patron.__class__ == PatronData
        assert "12345" == patron.authorization_identifier
        assert "863716" == patron.permanent_id
        assert "Booth Expired Test" == patron.personal_name
        assert 0 == patron.fines
        assert datetime(2008, 9, 7) == patron.authorization_expires
        assert PatronData.NO_BORROWING_PRIVILEGES == patron.block_reason

        # Test with blocked patron, block should not be set
        p = SIP2AuthenticationProvider(self._default_library,
                                       integration_noblock,
                                       client=MockSIPClientFactory())
        client = p._client
        info = client.patron_information_parser(
            TestSIP2AuthenticationProvider.evergreen_expired_card)
        patron = p.info_to_patrondata(info)
        assert patron.__class__ == PatronData
        assert "12345" == patron.authorization_identifier
        assert "863716" == patron.permanent_id
        assert "Booth Expired Test" == patron.personal_name
        assert 0 == patron.fines
        assert datetime(2008, 9, 7) == patron.authorization_expires
        assert PatronData.NO_VALUE == patron.block_reason
    def test_patron_block_setting(self):
        integration_block = self._external_integration(
            self._str,
            settings={SIP2AuthenticationProvider.PATRON_STATUS_BLOCK: "true"})
        integration_noblock = self._external_integration(
            self._str,
            settings={SIP2AuthenticationProvider.PATRON_STATUS_BLOCK: "false"})
        client = MockSIPClient()

        # Test with blocked patron, block should be set
        p = SIP2AuthenticationProvider(self._default_library,
                                       integration_block,
                                       client=client)
        info = client.patron_information_parser(
            TestSIP2AuthenticationProvider.evergreen_expired_card)
        patron = p.info_to_patrondata(info)
        eq_(patron.__class__, PatronData)
        eq_("12345", patron.authorization_identifier)
        eq_("863716", patron.permanent_id)
        eq_("Booth Expired Test", patron.personal_name)
        eq_(0, patron.fines)
        eq_(datetime(2008, 9, 7), patron.authorization_expires)
        eq_(PatronData.NO_BORROWING_PRIVILEGES, patron.block_reason)

        # Test with blocked patron, block should not be set
        p = SIP2AuthenticationProvider(self._default_library,
                                       integration_noblock,
                                       client=client)
        info = client.patron_information_parser(
            TestSIP2AuthenticationProvider.evergreen_expired_card)
        patron = p.info_to_patrondata(info)
        eq_(patron.__class__, PatronData)
        eq_("12345", patron.authorization_identifier)
        eq_("863716", patron.permanent_id)
        eq_("Booth Expired Test", patron.personal_name)
        eq_(0, patron.fines)
        eq_(datetime(2008, 9, 7), patron.authorization_expires)
        eq_(PatronData.NO_VALUE, patron.block_reason)
Beispiel #8
0
    def test_ioerror_during_connect_becomes_remoteintegrationexception(self):
        """If the IP of the circulation manager has not been whitelisted,
        we generally can't even connect to the server.
        """
        class CannotConnect(MockSIPClient):
            def connect(self):
                raise IOError("Doom!")

        client = CannotConnect()
        integration = self._external_integration(self._str)
        provider = SIP2AuthenticationProvider(self._default_library, integration, client=client)

        assert_raises_regexp(
            RemoteIntegrationException,
            "Error accessing unknown server: Doom!",
            provider.remote_authenticate,
            "username", "password",
        )
    def test_ioerror_during_send_becomes_remoteintegrationexception(self):
        """If there's an IOError communicating with the server,
        it becomes a RemoteIntegrationException.
        """
        class CannotSend(MockSIPClient):
            def do_send(self, data):
                raise IOError("Doom!")

        integration = self._external_integration(self._str)
        integration.url = "server.local"
        provider = SIP2AuthenticationProvider(self._default_library,
                                              integration,
                                              client=CannotSend)
        with pytest.raises(RemoteIntegrationException) as excinfo:
            provider.remote_authenticate(
                "username",
                "password",
            )
        assert "Error accessing server.local: Doom!" in str(excinfo.value)
 def test_ioerror_during_send_becomes_remoteintegrationexception(self):
     """If there's an IOError communicating with the server,
     it becomes a RemoteIntegrationException.
     """
     class CannotSend(MockSIPClient):
         def do_send(self, data):
             raise IOError("Doom!")
     client = CannotSend()
     client.target_server = 'server.local'
         
     provider = SIP2AuthenticationProvider(
         None, None, None, None, None, client=client
     )
     assert_raises_regexp(
         RemoteIntegrationException,
         "Error accessing server.local: Doom!",
         provider.remote_authenticate,
         "username", "password",
     )
    def test_remote_authenticate(self):
        integration = self._external_integration(self._str)
        client = MockSIPClient()
        auth = SIP2AuthenticationProvider(self._default_library,
                                          integration,
                                          client=client)

        # Some examples taken from a Sierra SIP API.
        client.queue_response(self.sierra_valid_login)
        patrondata = auth.remote_authenticate("user", "pass")
        eq_("12345", patrondata.authorization_identifier)
        eq_("*****@*****.**", patrondata.email_address)
        eq_("SHELDON, ALICE", patrondata.personal_name)
        eq_(0, patrondata.fines)
        eq_(None, patrondata.authorization_expires)
        eq_(None, patrondata.external_type)
        eq_(PatronData.NO_VALUE, patrondata.block_reason)

        client.queue_response(self.sierra_invalid_login)
        eq_(None, auth.remote_authenticate("user", "pass"))

        # Since Sierra provides both the patron's fine amount and the
        # maximum allowable amount, we can determine just by looking
        # at the SIP message that this patron is blocked for excessive
        # fines.
        client.queue_response(self.sierra_excessive_fines)
        patrondata = auth.remote_authenticate("user", "pass")
        eq_(PatronData.EXCESSIVE_FINES, patrondata.block_reason)

        # Some examples taken from an Evergreen instance that doesn't
        # use passwords.
        client.queue_response(self.evergreen_active_user)
        patrondata = auth.remote_authenticate("user", "pass")
        eq_("12345", patrondata.authorization_identifier)
        eq_("863715", patrondata.permanent_id)
        eq_("Booth Active Test", patrondata.personal_name)
        eq_(0, patrondata.fines)
        eq_(datetime(2019, 10, 4), patrondata.authorization_expires)
        eq_("Adult", patrondata.external_type)

        # A patron with an expired card.
        client.queue_response(self.evergreen_expired_card)
        patrondata = auth.remote_authenticate("user", "pass")
        eq_("12345", patrondata.authorization_identifier)
        # SIP extension field XI becomes sipserver_internal_id which
        # becomes PatronData.permanent_id.
        eq_("863716", patrondata.permanent_id)
        eq_("Booth Expired Test", patrondata.personal_name)
        eq_(0, patrondata.fines)
        eq_(datetime(2008, 9, 7), patrondata.authorization_expires)
        eq_(PatronData.NO_BORROWING_PRIVILEGES, patrondata.block_reason)

        # A patron with excessive fines
        client.queue_response(self.evergreen_excessive_fines)
        patrondata = auth.remote_authenticate("user", "pass")
        eq_("12345", patrondata.authorization_identifier)
        eq_("863718", patrondata.permanent_id)
        eq_("Booth Excessive Fines Test", patrondata.personal_name)
        eq_(100, patrondata.fines)
        eq_(datetime(2019, 10, 04), patrondata.authorization_expires)

        # We happen to know that this patron can't borrow books due to
        # excessive fines, but that information doesn't show up as a
        # block, because Evergreen doesn't also provide the
        # fine limit. This isn't a big deal -- we'll pick it up later
        # when we apply the site policy.
        #
        # This patron also has "Recall privileges denied" set, but
        # that's not a reason to block them.
        eq_(PatronData.NO_VALUE, patrondata.block_reason)

        # "Hold privileges denied" is not a block because you can
        # still borrow books.
        client.queue_response(self.evergreen_hold_privileges_denied)
        patrondata = auth.remote_authenticate("user", "pass")
        eq_(PatronData.NO_VALUE, patrondata.block_reason)

        client.queue_response(self.evergreen_card_reported_lost)
        patrondata = auth.remote_authenticate("user", "pass")
        eq_(PatronData.CARD_REPORTED_LOST, patrondata.block_reason)

        # Some examples taken from a Polaris instance.
        client.queue_response(self.polaris_valid_pin)
        patrondata = auth.remote_authenticate("user", "pass")
        eq_("25891000331441", patrondata.authorization_identifier)
        eq_("*****@*****.**", patrondata.email_address)
        eq_(9.25, patrondata.fines)
        eq_("Falk, Jen", patrondata.personal_name)
        eq_(datetime(2018, 6, 9, 23, 59, 59), patrondata.authorization_expires)

        client.queue_response(self.polaris_wrong_pin)
        patrondata = auth.remote_authenticate("user", "pass")
        eq_(None, patrondata)

        client.queue_response(self.polaris_no_such_patron)
        patrondata = auth.remote_authenticate("user", "pass")
        eq_(None, patrondata)

        client.queue_response(self.polaris_expired_card)
        patrondata = auth.remote_authenticate("user", "pass")
        eq_(datetime(2016, 10, 25, 23, 59, 59),
            patrondata.authorization_expires)

        client.queue_response(self.polaris_excess_fines)
        patrondata = auth.remote_authenticate("user", "pass")
        eq_(11.50, patrondata.fines)
    def test_remote_authenticate(self):
        integration = self._external_integration(self._str)
        client = MockSIPClient()
        auth = SIP2AuthenticationProvider(self._default_library,
                                          integration,
                                          client=client)

        # Some examples taken from a Sierra SIP API.
        client.queue_response(self.sierra_valid_login)
        client.queue_response(self.end_session_response)
        patrondata = auth.remote_authenticate("user", "pass")
        assert "12345" == patrondata.authorization_identifier
        assert "*****@*****.**" == patrondata.email_address
        assert "LE CARRÉ, JOHN" == patrondata.personal_name
        assert 0 == patrondata.fines
        assert None == patrondata.authorization_expires
        assert None == patrondata.external_type
        assert PatronData.NO_VALUE == patrondata.block_reason

        client.queue_response(self.sierra_invalid_login)
        client.queue_response(self.end_session_response)
        assert None == auth.remote_authenticate("user", "pass")

        # Since Sierra provides both the patron's fine amount and the
        # maximum allowable amount, we can determine just by looking
        # at the SIP message that this patron is blocked for excessive
        # fines.
        client.queue_response(self.sierra_excessive_fines)
        client.queue_response(self.end_session_response)
        patrondata = auth.remote_authenticate("user", "pass")
        assert PatronData.EXCESSIVE_FINES == patrondata.block_reason

        # A patron with an expired card.
        client.queue_response(self.evergreen_expired_card)
        client.queue_response(self.end_session_response)
        patrondata = auth.remote_authenticate("user", "pass")
        assert "12345" == patrondata.authorization_identifier
        # SIP extension field XI becomes sipserver_internal_id which
        # becomes PatronData.permanent_id.
        assert "863716" == patrondata.permanent_id
        assert "Booth Expired Test" == patrondata.personal_name
        assert 0 == patrondata.fines
        assert datetime(2008, 9, 7) == patrondata.authorization_expires
        assert PatronData.NO_BORROWING_PRIVILEGES == patrondata.block_reason

        # A patron with excessive fines
        client.queue_response(self.evergreen_excessive_fines)
        client.queue_response(self.end_session_response)
        patrondata = auth.remote_authenticate("user", "pass")
        assert "12345" == patrondata.authorization_identifier
        assert "863718" == patrondata.permanent_id
        assert "Booth Excessive Fines Test" == patrondata.personal_name
        assert 100 == patrondata.fines
        assert datetime(2019, 10, 4) == patrondata.authorization_expires

        # We happen to know that this patron can't borrow books due to
        # excessive fines, but that information doesn't show up as a
        # block, because Evergreen doesn't also provide the
        # fine limit. This isn't a big deal -- we'll pick it up later
        # when we apply the site policy.
        #
        # This patron also has "Recall privileges denied" set, but
        # that's not a reason to block them.
        assert PatronData.NO_VALUE == patrondata.block_reason

        # "Hold privileges denied" is not a block because you can
        # still borrow books.
        client.queue_response(self.evergreen_hold_privileges_denied)
        client.queue_response(self.end_session_response)
        patrondata = auth.remote_authenticate("user", "pass")
        assert PatronData.NO_VALUE == patrondata.block_reason

        client.queue_response(self.evergreen_card_reported_lost)
        client.queue_response(self.end_session_response)
        patrondata = auth.remote_authenticate("user", "pass")
        assert PatronData.CARD_REPORTED_LOST == patrondata.block_reason

        # Some examples taken from a Polaris instance.
        client.queue_response(self.polaris_valid_pin)
        client.queue_response(self.end_session_response)
        patrondata = auth.remote_authenticate("user", "pass")
        assert "25891000331441" == patrondata.authorization_identifier
        assert "*****@*****.**" == patrondata.email_address
        assert 9.25 == patrondata.fines
        assert "Falk, Jen" == patrondata.personal_name
        assert datetime(2018, 6, 9, 23, 59,
                        59) == patrondata.authorization_expires

        client.queue_response(self.polaris_wrong_pin)
        client.queue_response(self.end_session_response)
        patrondata = auth.remote_authenticate("user", "pass")
        assert None == patrondata

        client.queue_response(self.polaris_expired_card)
        client.queue_response(self.end_session_response)
        patrondata = auth.remote_authenticate("user", "pass")
        assert datetime(2016, 10, 25, 23, 59,
                        59) == patrondata.authorization_expires

        client.queue_response(self.polaris_excess_fines)
        client.queue_response(self.end_session_response)
        patrondata = auth.remote_authenticate("user", "pass")
        assert 11.50 == patrondata.fines

        # Two cases where the patron's authorization identifier was
        # just not recognized. One on an ILS that sets
        # valid_patron_password='******' when that happens.
        client.queue_response(self.polaris_no_such_patron)
        client.queue_response(self.end_session_response)
        patrondata = auth.remote_authenticate("user", "pass")
        assert None == patrondata

        # And once on an ILS that leaves valid_patron_password blank
        # when that happens.
        client.queue_response(self.tlc_no_such_patron)
        client.queue_response(self.end_session_response)
        patrondata = auth.remote_authenticate("user", "pass")
        assert None == patrondata
    def test_run_self_tests(self):
        integration = self._external_integration(self._str)
        integration.url = "server.com"

        class MockBadConnection(MockSIPClient):
            def connect(self):
                # probably a timeout if the server or port values are not valid
                raise IOError("Could not connect")

        class MockSIPLogin(MockSIPClient):
            def now(self):
                return datetime(2019, 1, 1).strftime("%Y%m%d0000%H%M%S")

            def login(self):
                if not self.login_user_id and not self.login_password:
                    raise IOError("Error logging in")

            def patron_information(self, username, password):
                return self.patron_information_parser(
                    TestSIP2AuthenticationProvider.sierra_valid_login)

        auth = SIP2AuthenticationProvider(self._default_library,
                                          integration,
                                          client=MockBadConnection)
        results = [r for r in auth._run_self_tests(self._db)]

        # If the connection doesn't work then don't bother running the other tests
        assert len(results) == 1
        assert results[0].name == "Test Connection"
        assert results[0].success == False
        assert isinstance(results[0].exception, IOError)
        assert results[0].exception.args == ("Could not connect", )

        auth = SIP2AuthenticationProvider(self._default_library,
                                          integration,
                                          client=MockSIPLogin)
        results = [x for x in auth._run_self_tests(self._db)]

        assert len(results) == 2
        assert results[0].name == "Test Connection"
        assert results[0].success == True

        assert results[
            1].name == "Test Login with username 'None' and password 'None'"
        assert results[1].success == False
        assert isinstance(results[1].exception, IOError)
        assert results[1].exception.args == ("Error logging in", )

        # Set the log in username and password
        integration.username = "******"
        integration.password = "******"
        goodLoginClient = MockSIPLogin(login_user_id="user1",
                                       login_password="******")
        auth = SIP2AuthenticationProvider(self._default_library,
                                          integration,
                                          client=goodLoginClient)
        results = [x for x in auth._run_self_tests(self._db)]

        assert len(results) == 3
        assert results[0].name == "Test Connection"
        assert results[0].success == True

        assert (results[1].name ==
                "Test Login with username 'user1' and password 'pass1'")
        assert results[1].success == True

        assert results[2].name == "Authenticating test patron"
        assert results[2].success == False
        assert isinstance(results[2].exception, CannotLoadConfiguration)
        assert results[2].exception.args == (
            "No test patron identifier is configured.", )

        # Now add the test patron credentials into the mocked client and SIP2 authenticator provider
        patronDataClient = MockSIPLogin(login_user_id="user1",
                                        login_password="******")
        valid_login_patron = patronDataClient.patron_information_parser(
            TestSIP2AuthenticationProvider.sierra_valid_login)

        class MockSIP2PatronInformation(SIP2AuthenticationProvider):
            def patron_information(self, username, password):
                return valid_login_patron

        auth = MockSIP2PatronInformation(self._default_library,
                                         integration,
                                         client=patronDataClient)
        # The actual test patron credentials
        auth.test_username = "******"
        auth.test_password = "******"
        results = [x for x in auth._run_self_tests(self._db)]

        assert len(results) == 6
        assert results[0].name == "Test Connection"
        assert results[0].success == True

        assert (results[1].name ==
                "Test Login with username 'user1' and password 'pass1'")
        assert results[1].success == True

        assert results[2].name == "Authenticating test patron"
        assert results[2].success == True

        # Since test patron authentication is true, we can now see self
        # test results for syncing metadata and the raw data from `patron_information`
        assert results[3].name == "Syncing patron metadata"
        assert results[3].success == True

        assert results[4].name == "Patron information request"
        assert results[4].success == True
        assert results[
            4].result == patronDataClient.patron_information_request(
                "usertest1", "userpassword1")

        assert results[5].name == "Raw test patron information"
        assert results[5].success == True
        assert results[5].result == json.dumps(valid_login_patron, indent=1)
    def test_remote_authenticate(self):
        client = MockSIPClient()
        auth = SIP2AuthenticationProvider(
            None, None, None, None, None, None, client=client
        )

        # Some examples taken from a Sierra SIP API.
        client.queue_response(self.sierra_valid_login)
        patrondata = auth.remote_authenticate("user", "pass")
        eq_("12345", patrondata.authorization_identifier)
        eq_("*****@*****.**", patrondata.email_address)
        eq_("SHELDON, ALICE", patrondata.personal_name)
        eq_(0, patrondata.fines)
        eq_(None, patrondata.authorization_expires)
        eq_(None, patrondata.external_type)
        
        client.queue_response(self.sierra_invalid_login)
        eq_(None, auth.remote_authenticate("user", "pass"))
        
        # Some examples taken from an Evergreen instance that doesn't
        # use passwords.
        client.queue_response(self.evergreen_active_user)
        patrondata = auth.remote_authenticate("user", "pass")
        eq_("12345", patrondata.authorization_identifier)
        eq_("863715", patrondata.permanent_id)
        eq_("Booth Active Test", patrondata.personal_name)
        eq_(0, patrondata.fines)
        eq_(datetime(2019, 10, 4), patrondata.authorization_expires)
        eq_("Adult", patrondata.external_type)
        
        client.queue_response(self.evergreen_expired_card)
        patrondata = auth.remote_authenticate("user", "pass")
        eq_("12345", patrondata.authorization_identifier)
        # SIP extension field XI becomes sipserver_internal_id which
        # becomes PatronData.permanent_id.
        eq_("863716", patrondata.permanent_id)
        eq_("Booth Expired Test", patrondata.personal_name)
        eq_(0, patrondata.fines)
        eq_(datetime(2008, 9, 7), patrondata.authorization_expires)

        client.queue_response(self.evergreen_excessive_fines)
        patrondata = auth.remote_authenticate("user", "pass")
        eq_("12345", patrondata.authorization_identifier)
        eq_("863718", patrondata.permanent_id)
        eq_("Booth Excessive Fines Test", patrondata.personal_name)
        eq_(100, patrondata.fines)
        eq_(datetime(2019, 10, 04), patrondata.authorization_expires)

        # Some examples taken from a Polaris instance.
        client.queue_response(self.polaris_valid_pin)
        patrondata = auth.remote_authenticate("user", "pass")
        eq_("25891000331441", patrondata.authorization_identifier)
        eq_("*****@*****.**", patrondata.email_address)
        eq_(9.25, patrondata.fines)
        eq_("Falk, Jen", patrondata.personal_name)
        eq_(datetime(2018, 6, 9, 23, 59, 59),
            patrondata.authorization_expires)

        client.queue_response(self.polaris_wrong_pin)
        patrondata = auth.remote_authenticate("user", "pass")
        eq_(None, patrondata)

        client.queue_response(self.polaris_no_such_patron)
        patrondata = auth.remote_authenticate("user", "pass")
        eq_(None, patrondata)
        
        client.queue_response(self.polaris_expired_card)
        patrondata = auth.remote_authenticate("user", "pass")
        eq_(datetime(2016, 10, 25, 23, 59, 59),
            patrondata.authorization_expires)
        
        client.queue_response(self.polaris_excess_fines)
        patrondata = auth.remote_authenticate("user", "pass")
        eq_(11.50, patrondata.fines)