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) client.queue_response(self.end_session_response) 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) client.queue_response(self.end_session_response) 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' 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)
def test_login_failure_interrupts_other_request(self): sip = MockSIPClient(login_user_id="user_id", login_password="******") sip.queue_response("940") # We don't even get a chance to make the patron information request # because our login attempt fails. pytest.raises(IOError, sip.patron_information, "patron_identifier")
def test_login_failure_interrupts_other_request(self): sip = MockSIPClient('user_id', 'password') sip.queue_response('940') # We don't even get a chance to make the patron information request # because our login attempt fails. assert_raises(IOError, sip.patron_information, 'patron_identifier')
def test_different_separator(self): """When you create the SIPClient you get to specify which character to use as the field separator. """ sip = MockSIPClient(separator='^') sip.queue_response("64Y 201610050000114734 AOnypl ^AA240^AENo Name^BLN^AFYour library card number cannot be located.^AY1AZC9DE") response = sip.patron_information('identifier') eq_('240', response['patron_identifier'])
def test_different_separator(self): """When you create the SIPClient you get to specify which character to use as the field separator. """ sip = MockSIPClient(separator="^") sip.queue_response( "64Y 201610050000114734 AOnypl ^AA240^AENo Name^BLN^AFYour library card number cannot be located.^AY1AZC9DE" ) response = sip.patron_information("identifier") assert "240" == response["patron_identifier"]
def test_login_happens_implicitly_when_user_id_and_password_specified( self): sip = MockSIPClient('user_id', 'password') # We're not logged in, and we must log in before sending a real # message. eq_(False, sip.logged_in) eq_(True, sip.must_log_in) sip.queue_response('941') sip.queue_response( '64Y 201610050000114734 AOnypl |AA12345|AENo Name|BLN|AFYour library card number cannot be located. Please see a staff member for assistance.|AY1AZC9DE' ) response = sip.patron_information('patron_identifier') # Two requests were made. eq_(2, len(sip.requests)) eq_(2, sip.sequence_number) # We're logged in. eq_(True, sip.logged_in) # We ended up with the right data. eq_('12345', response['patron_identifier']) # If we reset the connection, we stop being logged in. sip.connect() eq_(False, sip.logged_in) eq_(0, sip.sequence_number)
def test_sequence_number_increment(self): sip = MockSIPClient(login_user_id="user_id", login_password="******") sip.sequence_number = 0 sip.queue_response("941") response = sip.login() assert 1 == sip.sequence_number # Test wraparound from 9 to 0 sip.sequence_number = 9 sip.queue_response("941") response = sip.login() assert 0 == sip.sequence_number
def test_sequence_number_increment(self): sip = MockSIPClient() sip.sequence_number = 0 sip.queue_response('941') response = sip.login('user_id', 'password') eq_(1, sip.sequence_number) # Test wraparound from 9 to 0 sip.sequence_number = 9 sip.queue_response('941') response = sip.login('user_id', 'password') eq_(0, sip.sequence_number)
def test_login_does_not_happen_implicitly_when_user_id_and_password_not_specified(self): sip = MockSIPClient() # We're implicitly logged in. eq_(False, sip.must_log_in) sip.queue_response('64Y 201610050000114734 AOnypl |AA12345|AENo Name|BLN|AFYour library card number cannot be located. Please see a staff member for assistance.|AY1AZC9DE') response = sip.patron_information('patron_identifier') # One request was made. eq_(1, len(sip.requests)) eq_(1, sip.sequence_number) # We ended up with the right data. eq_('12345', response['patron_identifier'])
def test_sequence_number_increment(self): sip = MockSIPClient(login_user_id='user_id', login_password='******') sip.sequence_number=0 sip.queue_response('941') response = sip.login() eq_(1, sip.sequence_number) # Test wraparound from 9 to 0 sip.sequence_number=9 sip.queue_response('941') response = sip.login() eq_(0, sip.sequence_number)
def test_login_does_not_happen_implicitly_when_user_id_and_password_not_specified( self): sip = MockSIPClient() # We're implicitly logged in. eq_(False, sip.must_log_in) sip.queue_response( '64Y 201610050000114734 AOnypl |AA12345|AENo Name|BLN|AFYour library card number cannot be located. Please see a staff member for assistance.|AY1AZC9DE' ) response = sip.patron_information('patron_identifier') # One request was made. eq_(1, len(sip.requests)) eq_(1, sip.sequence_number) # We ended up with the right data. eq_('12345', response['patron_identifier'])
def test_login_does_not_happen_implicitly_when_user_id_and_password_not_specified( self, ): sip = MockSIPClient() # We're implicitly logged in. assert False == sip.must_log_in sip.queue_response( "64Y 201610050000114734 AOnypl |AA12345|AENo Name|BLN|AFYour library card number cannot be located. Please see a staff member for assistance.|AY1AZC9DE" ) response = sip.patron_information("patron_identifier") # One request was made. assert 1 == len(sip.requests) assert 1 == sip.sequence_number # We ended up with the right data. assert "12345" == response["patron_identifier"]
def test_maximum_resend(self): sip = MockSIPClient(login_user_id='user_id', login_password='******') # We will keep sending retry messages until we reach the maximum sip.queue_response('96') sip.queue_response('96') sip.queue_response('96') sip.queue_response('96') sip.queue_response('96') # After reaching the maximum the client should give an IOError assert_raises(IOError, sip.login) # We should send as many requests as we are allowed retries eq_(sip.MAXIMUM_RETRIES, len(sip.requests))
def test_maximum_resend(self): sip = MockSIPClient(login_user_id="user_id", login_password="******") # We will keep sending retry messages until we reach the maximum sip.queue_response("96") sip.queue_response("96") sip.queue_response("96") sip.queue_response("96") sip.queue_response("96") # After reaching the maximum the client should give an IOError pytest.raises(IOError, sip.login) # We should send as many requests as we are allowed retries assert sip.MAXIMUM_RETRIES == len(sip.requests)
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)
def test_login_happens_when_user_id_and_password_specified(self): sip = MockSIPClient(login_user_id="user_id", login_password="******") # We're not logged in, and we must log in before sending a real # message. assert True == sip.must_log_in sip.queue_response("941") sip.queue_response( "64Y 201610050000114734 AOnypl |AA12345|AENo Name|BLN|AFYour library card number cannot be located. Please see a staff member for assistance.|AY1AZC9DE" ) sip.login() response = sip.patron_information("patron_identifier") # Two requests were made. assert 2 == len(sip.requests) assert 2 == sip.sequence_number # We ended up with the right data. assert "12345" == response["patron_identifier"]
def test_variant_encoding(self): response_unicode = "64 000201610210000142637000000000000000000000000AOnypl |AA12345|AELE CARRÉ, JOHN|BZ0030|CA0050|CB0050|BLY|CQY|BV0|CC15.00|[email protected]|AY1AZD1B7\r" # By default, we expect data from a SIP2 server to be encoded # as CP850. assert "cp850" == self.sip.encoding self.sip.queue_response(response_unicode.encode("cp850")) response = self.sip.patron_information("identifier") assert "LE CARRÉ, JOHN" == response["personal_name"] # But a SIP2 server may send some other encoding, such as # UTF-8. This can cause odd results if the circulation manager # tries to parse the data as CP850. self.sip.queue_response(response_unicode.encode("utf-8")) response = self.sip.patron_information("identifier") assert "LE CARR├ë, JOHN" == response["personal_name"] # Giving SIPClient the right encoding means the data is # converted correctly. sip = MockSIPClient(encoding="utf-8") assert "utf-8" == sip.encoding sip.queue_response(response_unicode.encode("utf-8")) response = sip.patron_information("identifier") assert "LE CARRÉ, JOHN" == response["personal_name"]
def test_login_happens_when_user_id_and_password_specified(self): sip = MockSIPClient('user_id', 'password') # We're not logged in, and we must log in before sending a real # message. eq_(True, sip.must_log_in) sip.queue_response('941') sip.queue_response('64Y 201610050000114734 AOnypl |AA12345|AENo Name|BLN|AFYour library card number cannot be located. Please see a staff member for assistance.|AY1AZC9DE') sip.login() response = sip.patron_information('patron_identifier') # Two requests were made. eq_(2, len(sip.requests)) eq_(2, sip.sequence_number) # We ended up with the right data. eq_('12345', response['patron_identifier'])
def test_resend(self): sip = MockSIPClient() # The first response will be a request to resend the original message. sip.queue_response('96') # The second response will indicate a successful login. sip.queue_response('941') response = sip.login('user_id', 'password') # We made two requests for a single login command. req1, req2 = sip.requests # The first request includes a sequence ID field, "AY", with # the value "0". eq_('9300CNuser_id|COpassword|AY0AZF556\r', req1) # The second request does not include a sequence ID field. As # a consequence its checksum is different. eq_('9300CNuser_id|COpassword|AZF620\r', req2) # The login request eventually succeeded. eq_({'login_ok': '1', '_status': '94'}, response)
def test_resend(self): sip = MockSIPClient(login_user_id="user_id", login_password="******") # The first response will be a request to resend the original message. sip.queue_response("96") # The second response will indicate a successful login. sip.queue_response("941") response = sip.login() # We made two requests for a single login command. req1, req2 = sip.requests # The first request includes a sequence ID field, "AY", with # the value "0". assert b"9300CNuser_id|COpassword|AY0AZF556\r" == req1 # The second request does not include a sequence ID field. As # a consequence its checksum is different. assert b"9300CNuser_id|COpassword|AZF620\r" == req2 # The login request eventually succeeded. assert {"login_ok": "1", "_status": "94"} == response
class TestClientDialects(object): def setup_method(self): self.sip = MockSIPClient() def test_generic_dialect(self): # Generic ILS should send end_session message self.sip.dialect = GenericILS self.sip.queue_response( "36Y201610210000142637AO3|AA25891000331441|AF|AG") self.sip.end_session("username", "password") assert self.sip.read_count == 1 assert self.sip.write_count == 1 def test_ag_dialect(self): # AG VERSO ILS shouldn't end_session message self.sip.dialect = AutoGraphicsVerso self.sip.end_session("username", "password") assert self.sip.read_count == 0 assert self.sip.write_count == 0
def test_resend(self): sip = MockSIPClient(login_user_id='user_id', login_password='******') # The first response will be a request to resend the original message. sip.queue_response('96') # The second response will indicate a successful login. sip.queue_response('941') response = sip.login() # We made two requests for a single login command. req1, req2 = sip.requests # The first request includes a sequence ID field, "AY", with # the value "0". eq_('9300CNuser_id|COpassword|AY0AZF556\r', req1) # The second request does not include a sequence ID field. As # a consequence its checksum is different. eq_('9300CNuser_id|COpassword|AZF620\r', req2) # The login request eventually succeeded. eq_({'login_ok': '1', '_status': '94'}, response)
class TestPatronResponse(object): def setup_method(self): self.sip = MockSIPClient() def test_incorrect_card_number(self): self.sip.queue_response( "64Y 201610050000114734 AOnypl |AA240|AENo Name|BLN|AFYour library card number cannot be located.|AY1AZC9DE" ) response = self.sip.patron_information("identifier") # Test some of the basic fields. assert response["institution_id"] == "nypl " assert response["personal_name"] == "No Name" assert response["screen_message"] == [ "Your library card number cannot be located." ] assert response["valid_patron"] == "N" assert response["patron_status"] == "Y " parsed = response["patron_status_parsed"] assert True == parsed["charge privileges denied"] assert False == parsed["too many items charged"] def test_hold_items(self): "A patron has multiple items on hold." self.sip.queue_response( "64 000201610050000114837000300020002000000000000AOnypl |AA233|AEBAR, FOO|BZ0030|CA0050|CB0050|BLY|CQY|BV0|CC15.00|AS123|AS456|AS789|[email protected]|AY1AZC848" ) response = self.sip.patron_information("identifier") assert "0003" == response["hold_items_count"] assert ["123", "456", "789"] == response["hold_items"] def test_multiple_screen_messages(self): self.sip.queue_response( "64Y YYYYYYYYYYY000201610050000115040000000000000000000000000AOnypl |AA233|AESHELDON, ALICE|BZ0030|CA0050|CB0050|BLY|CQN|BV0|CC15.00|AFInvalid PIN entered. Please try again or see a staff member for assistance.|AFThere are unresolved issues with your account. Please see a staff member for assistance.|AY2AZ9B64" ) response = self.sip.patron_information("identifier") assert 2 == len(response["screen_message"]) def test_extension_field_captured(self): """This SIP2 message includes an extension field with the code XI.""" self.sip.queue_response( "64 Y 00020161005 122942000000000000000000000000AA240|AEBooth Active Test|BHUSD|BDAdult Circ Desk 1 Newtown, CT USA 06470|AQNEWTWN|BLY|CQN|PA20191004|PCAdult|PIAllowed|XI86371|AOBiblioTest|ZZfoo|AY2AZ0000" ) response = self.sip.patron_information("identifier") # The Evergreen XI field is a known extension and is picked up # as sipserver_internal_id. assert "86371" == response["sipserver_internal_id"] # The ZZ field is an unknown extension and is captured under # its SIP code. assert ["foo"] == response["ZZ"] def test_variant_encoding(self): response_unicode = "64 000201610210000142637000000000000000000000000AOnypl |AA12345|AELE CARRÉ, JOHN|BZ0030|CA0050|CB0050|BLY|CQY|BV0|CC15.00|[email protected]|AY1AZD1B7\r" # By default, we expect data from a SIP2 server to be encoded # as CP850. assert "cp850" == self.sip.encoding self.sip.queue_response(response_unicode.encode("cp850")) response = self.sip.patron_information("identifier") assert "LE CARRÉ, JOHN" == response["personal_name"] # But a SIP2 server may send some other encoding, such as # UTF-8. This can cause odd results if the circulation manager # tries to parse the data as CP850. self.sip.queue_response(response_unicode.encode("utf-8")) response = self.sip.patron_information("identifier") assert "LE CARR├ë, JOHN" == response["personal_name"] # Giving SIPClient the right encoding means the data is # converted correctly. sip = MockSIPClient(encoding="utf-8") assert "utf-8" == sip.encoding sip.queue_response(response_unicode.encode("utf-8")) response = sip.patron_information("identifier") assert "LE CARRÉ, JOHN" == response["personal_name"] def test_embedded_pipe(self): """In most cases we can handle data even if it contains embedded instances of the separator character. """ self.sip.queue_response( "64 000201610050000134405000000000000000000000000AOnypl |AA12345|AERICHARDSON, LEONARD|BZ0030|CA0050|CB0050|BLY|CQY|BV0|CC15.00|BEleona|rdr@|bar.com|AY1AZD1BB\r" ) response = self.sip.patron_information("identifier") assert "leona|rdr@|bar.com" == response["email_address"] def test_different_separator(self): """When you create the SIPClient you get to specify which character to use as the field separator. """ sip = MockSIPClient(separator="^") sip.queue_response( "64Y 201610050000114734 AOnypl ^AA240^AENo Name^BLN^AFYour library card number cannot be located.^AY1AZC9DE" ) response = sip.patron_information("identifier") assert "240" == response["patron_identifier"] def test_location_code_is_optional(self): """You can specify a location_code when logging in, or not.""" without_code = self.sip.login_message("login_id", "login_password") assert without_code.endswith("COlogin_password") with_code = self.sip.login_message("login_id", "login_password", "location_code") assert with_code.endswith("COlogin_password|CPlocation_code") def test_institution_id_field_is_always_provided(self): without_institution_arg = self.sip.patron_information_request( "patron_identifier", "patron_password") assert without_institution_arg.startswith("AO|", 33) def test_institution_id_field_value_provided(self): # Fake value retrieved from DB sip = MockSIPClient(institution_id="MAIN") with_institution_provided = sip.patron_information_request( "patron_identifier", "patron_password") assert with_institution_provided.startswith("AOMAIN|", 33) def test_patron_password_is_optional(self): without_password = self.sip.patron_information_request( "patron_identifier") assert without_password.endswith("AApatron_identifier|AC") with_password = self.sip.patron_information_request( "patron_identifier", "patron_password") assert with_password.endswith( "AApatron_identifier|AC|ADpatron_password") def test_parse_patron_status(self): m = MockSIPClient.parse_patron_status pytest.raises(ValueError, m, None) pytest.raises(ValueError, m, "") pytest.raises(ValueError, m, " " * 20) parsed = m("Y Y Y Y Y Y Y ") for yes in [ "charge privileges denied", #'renewal privileges denied', "recall privileges denied", #'hold privileges denied', "card reported lost", #'too many items charged', "too many items overdue", #'too many renewals', "too many claims of items returned", #'too many items lost', "excessive outstanding fines", #'excessive outstanding fees', "recall overdue", #'too many items billed', ]: assert parsed[yes] == True for no in [ #'charge privileges denied', "renewal privileges denied", #'recall privileges denied', "hold privileges denied", #'card reported lost', "too many items charged", #'too many items overdue', "too many renewals", #'too many claims of items returned', "too many items lost", #'excessive outstanding fines', "excessive outstanding fees", #'recall overdue', "too many items billed", ]: assert parsed[no] == False
def setup(self): self.sip = MockSIPClient()
def test_login_password_is_optional(self): """You can specify a login_id without specifying a login_password.""" sip = MockSIPClient('user_id') sip.queue_response('941') response = sip.login() eq_({'login_ok': '1', '_status': '94'}, response)
def test_login_success(self): sip = MockSIPClient('user_id', 'password') sip.queue_response('941') response = sip.login() eq_({'login_ok': '1', '_status': '94'}, response)
def test_login_failure(self): sip = MockSIPClient(login_user_id="user_id", login_password="******") sip.queue_response("940") pytest.raises(IOError, sip.login)
def test_login_password_is_optional(self): """You can specify a login_id without specifying a login_password.""" sip = MockSIPClient(login_user_id="user_id") sip.queue_response("941") response = sip.login() assert {"login_ok": "1", "_status": "94"} == response
def setup_method(self): self.sip = MockSIPClient()
def test_institution_id_field_value_provided(self): # Fake value retrieved from DB sip = MockSIPClient(institution_id="MAIN") with_institution_provided = sip.patron_information_request( "patron_identifier", "patron_password") assert with_institution_provided.startswith("AOMAIN|", 33)
def test_append_checksum(self): sip = MockSIPClient() sip.sequence_number = 7 data = "some data" new_data = sip.append_checksum(data) assert "some data|AY7AZFAAA" == new_data
def test_login_message(self): sip = MockSIPClient() message = sip.login_message("user_id", "password") assert "9300CNuser_id|COpassword" == message
def test_login_success(self): sip = MockSIPClient() sip.queue_response('941') response = sip.login('user_id', 'password') eq_({'login_ok': '1', '_status': '94'}, response)
def test_login_failure(self): sip = MockSIPClient('user_id', 'password') sip.queue_response('940') assert_raises(IOError, sip.login)
class TestPatronResponse(object): def setup(self): self.sip = MockSIPClient() def test_incorrect_card_number(self): self.sip.queue_response("64Y 201610050000114734 AOnypl |AA240|AENo Name|BLN|AFYour library card number cannot be located.|AY1AZC9DE") response = self.sip.patron_information('identifier') # Test some of the basic fields. eq_(response['institution_id'], 'nypl ') eq_(response['personal_name'], 'No Name') eq_(response['screen_message'], ['Your library card number cannot be located.']) eq_(response['valid_patron'], 'N') eq_(response['patron_status'], 'Y ') parsed = response['patron_status_parsed'] eq_(True, parsed['charge privileges denied']) eq_(False, parsed['too many items charged']) def test_hold_items(self): "A patron has multiple items on hold." self.sip.queue_response("64 000201610050000114837000300020002000000000000AOnypl |AA233|AEBAR, FOO|BZ0030|CA0050|CB0050|BLY|CQY|BV0|CC15.00|AS123|AS456|AS789|[email protected]|AY1AZC848") response = self.sip.patron_information('identifier') eq_('0003', response['hold_items_count']) eq_(['123', '456', '789'], response['hold_items']) def test_multiple_screen_messages(self): self.sip.queue_response("64Y YYYYYYYYYYY000201610050000115040000000000000000000000000AOnypl |AA233|AESHELDON, ALICE|BZ0030|CA0050|CB0050|BLY|CQN|BV0|CC15.00|AFInvalid PIN entered. Please try again or see a staff member for assistance.|AFThere are unresolved issues with your account. Please see a staff member for assistance.|AY2AZ9B64") response = self.sip.patron_information('identifier') eq_(2, len(response['screen_message'])) def test_extension_field_captured(self): """This SIP2 message includes an extension field with the code XI. """ self.sip.queue_response("64 Y 00020161005 122942000000000000000000000000AA240|AEBooth Active Test|BHUSD|BDAdult Circ Desk 1 Newtown, CT USA 06470|AQNEWTWN|BLY|CQN|PA20191004|PCAdult|PIAllowed|XI86371|AOBiblioTest|ZZfoo|AY2AZ0000") response = self.sip.patron_information('identifier') # The Evergreen XI field is a known extension and is picked up # as sipserver_internal_id. eq_("86371", response['sipserver_internal_id']) # The ZZ field is an unknown extension and is captured under # its SIP code. eq_(["foo"], response['ZZ']) def test_embedded_pipe(self): """In most cases we can handle data even if it contains embedded instances of the separator character. """ self.sip.queue_response('64 000201610050000134405000000000000000000000000AOnypl |AA12345|AERICHARDSON, LEONARD|BZ0030|CA0050|CB0050|BLY|CQY|BV0|CC15.00|BEleona|rdr@|bar.com|AY1AZD1BB\r') response = self.sip.patron_information('identifier') eq_("leona|rdr@|bar.com", response['email_address']) def test_different_separator(self): """When you create the SIPClient you get to specify which character to use as the field separator. """ sip = MockSIPClient(separator='^') sip.queue_response("64Y 201610050000114734 AOnypl ^AA240^AENo Name^BLN^AFYour library card number cannot be located.^AY1AZC9DE") response = sip.patron_information('identifier') eq_('240', response['patron_identifier']) def test_location_code_is_optional(self): """You can specify a location_code when logging in, or not.""" without_code = self.sip.login_message( "login_id", "login_password" ) assert without_code.endswith("COlogin_password") with_code = self.sip.login_message( "login_id", "login_password", "location_code" ) assert with_code.endswith("COlogin_password|CPlocation_code") def test_patron_password_is_optional(self): without_password = self.sip.patron_information_request( "patron_identifier" ) assert without_password.endswith('AApatron_identifier|AC') with_password = self.sip.patron_information_request( "patron_identifier", "patron_password" ) assert with_password.endswith( 'AApatron_identifier|AC|ADpatron_password' ) def test_parse_patron_status(self): m = MockSIPClient.parse_patron_status assert_raises(ValueError, m, None) assert_raises(ValueError, m, "") assert_raises(ValueError, m, " " * 20) parsed = m("Y Y Y Y Y Y Y ") for yes in [ 'charge privileges denied', #'renewal privileges denied', 'recall privileges denied', #'hold privileges denied', 'card reported lost', #'too many items charged', 'too many items overdue', #'too many renewals', 'too many claims of items returned', #'too many items lost', 'excessive outstanding fines', #'excessive outstanding fees', 'recall overdue', #'too many items billed', ]: eq_(parsed[yes], True) for no in [ #'charge privileges denied', 'renewal privileges denied', #'recall privileges denied', 'hold privileges denied', #'card reported lost', 'too many items charged', #'too many items overdue', 'too many renewals', #'too many claims of items returned', 'too many items lost', #'excessive outstanding fines', 'excessive outstanding fees', #'recall overdue', 'too many items billed', ]: eq_(parsed[no], False)
def test_login_success(self): sip = MockSIPClient(login_user_id="user_id", login_password="******") sip.queue_response("941") response = sip.login() assert {"login_ok": "1", "_status": "94"} == response
def test_append_checksum(self): sip = MockSIPClient() sip.sequence_number=7 data = "some data" new_data = sip.append_checksum(data) eq_("some data|AY7AZFAAA", new_data)
class TestPatronResponse(object): def setup(self): self.sip = MockSIPClient() def test_incorrect_card_number(self): self.sip.queue_response( "64Y 201610050000114734 AOnypl |AA240|AENo Name|BLN|AFYour library card number cannot be located.|AY1AZC9DE" ) response = self.sip.patron_information('identifier') # Test some of the basic fields. response['institution_id'] = 'nypl ' response['peronal_name'] = 'No Name' response['screen_message'] = [ 'Your library card number cannot be located.' ] response['valid_patron'] = 'N' response['patron_status'] = 'Y ' def test_hold_items(self): "A patron has multiple items on hold." self.sip.queue_response( "64 000201610050000114837000300020002000000000000AOnypl |AA233|AEBAR, FOO|BZ0030|CA0050|CB0050|BLY|CQY|BV0|CC15.00|AS123|AS456|AS789|[email protected]|AY1AZC848" ) response = self.sip.patron_information('identifier') eq_('0003', response['hold_items_count']) eq_(['123', '456', '789'], response['hold_items']) def test_multiple_screen_messages(self): self.sip.queue_response( "64Y YYYYYYYYYYY000201610050000115040000000000000000000000000AOnypl |AA233|AESHELDON, ALICE|BZ0030|CA0050|CB0050|BLY|CQN|BV0|CC15.00|AFInvalid PIN entered. Please try again or see a staff member for assistance.|AFThere are unresolved issues with your account. Please see a staff member for assistance.|AY2AZ9B64" ) response = self.sip.patron_information('identifier') eq_(2, len(response['screen_message'])) def test_extension_field_captured(self): """This SIP2 message includes an extension field with the code XI. """ self.sip.queue_response( "64 Y 00020161005 122942000000000000000000000000AA240|AEBooth Active Test|BHUSD|BDAdult Circ Desk 1 Newtown, CT USA 06470|AQNEWTWN|BLY|CQN|PA20191004|PCAdult|PIAllowed|XI86371|AOBiblioTest|ZZfoo|AY2AZ0000" ) response = self.sip.patron_information('identifier') # The Evergreen XI field is a known extension and is picked up # as sipserver_internal_id. eq_("86371", response['sipserver_internal_id']) # The ZZ field is an unknown extension and is captured under # its SIP code. eq_(["foo"], response['ZZ']) def test_embedded_pipe(self): """In most cases we can handle data even if it contains embedded instances of the separator character. """ self.sip.queue_response( '64 000201610050000134405000000000000000000000000AOnypl |AA12345|AERICHARDSON, LEONARD|BZ0030|CA0050|CB0050|BLY|CQY|BV0|CC15.00|BEleona|rdr@|bar.com|AY1AZD1BB\r' ) response = self.sip.patron_information('identifier') eq_("leona|rdr@|bar.com", response['email_address']) def test_different_separator(self): """When you create the SIPClient you get to specify which character to use as the field separator. """ sip = MockSIPClient(separator='^') sip.queue_response( "64Y 201610050000114734 AOnypl ^AA240^AENo Name^BLN^AFYour library card number cannot be located.^AY1AZC9DE" ) response = sip.patron_information('identifier') eq_('240', response['patron_identifier']) def test_location_code_is_optional(self): """You can specify a location_code when logging in, or not.""" without_code = self.sip.login_message("login_id", "login_password") assert without_code.endswith("COlogin_password") with_code = self.sip.login_message("login_id", "login_password", "location_code") assert with_code.endswith("COlogin_password|CPlocation_code") def test_patron_password_is_optional(self): without_password = self.sip.patron_information_request( "patron_identifier") assert without_password.endswith('AApatron_identifier|AC') with_password = self.sip.patron_information_request( "patron_identifier", "patron_password") assert with_password.endswith( 'AApatron_identifier|AC|ADpatron_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_login_message(self): sip = MockSIPClient() message = sip.login_message('user_id', 'password') eq_('9300CNuser_id|COpassword', message)