def setup(self): super(TestRegistration, self).setup() # Create a RemoteRegistry. self.integration = self._external_integration(protocol="some protocol", goal="some goal") self.registry = RemoteRegistry(self.integration) self.registration = Registration(self.registry, self._default_library)
def test_constructor(self): # The Registration constructor was called during setup to create # self.registration. reg = self.registration eq_(self.registry, reg.registry) eq_(self._default_library, reg.library) settings = [ x for x in reg.integration.settings if x.library is not None ] eq_(set([reg.status_field, reg.stage_field, reg.web_client_field]), set(settings)) eq_(Registration.FAILURE_STATUS, reg.status_field.value) eq_(Registration.TESTING_STAGE, reg.stage_field.value) eq_(None, reg.web_client_field.value) # The Library has been associated with the ExternalIntegration. eq_([self._default_library], self.integration.libraries) # Creating another Registration doesn't add the library to the # ExternalIntegration again or override existing values for the # settings. reg.status_field.value = "new status" reg.stage_field.value = "new stage" reg2 = Registration(self.registry, self._default_library) eq_([self._default_library], self.integration.libraries) eq_("new status", reg2.status_field.value) eq_("new stage", reg2.stage_field.value)
def test_do_run(self): class Mock(LibraryRegistrationScript): processed = [] def process_library(self, *args): self.processed.append(args) script = Mock(self._db) base_url_setting = ConfigurationSetting.sitewide( self._db, Configuration.BASE_URL_KEY) base_url_setting.value = u'http://test-circulation-manager/' library = self._default_library library2 = self._library() cmd_args = [ library.short_name, "--stage=testing", "--registry-url=http://registry/" ] app = script.do_run(cmd_args=cmd_args, in_unit_test=True) # One library was processed. (registration, stage, url_for) = script.processed.pop() eq_([], script.processed) eq_(library, registration.library) eq_(Registration.TESTING_STAGE, stage) # A new ExternalIntegration was created for the newly defined # registry at http://registry/. eq_("http://registry/", registration.integration.url) # An application environment was created and the url_for # implementation for that environment was passed into # process_library. eq_(url_for, app.manager.url_for) # Let's say the other library was earlier registered in production. registration_2 = Registration(registration.registry, library2) registration_2.stage_field.value = Registration.PRODUCTION_STAGE # Now run the script again without specifying a particular # library or the --stage argument. app = script.do_run(cmd_args=[], in_unit_test=True) # Every library was processed. eq_(set([library, library2]), set([x[0].library for x in script.processed])) for i in script.processed: # Since no stage was provided, each library was registered # using the stage already associated with it. eq_(i[0].stage_field.value, i[1]) # Every library was registered with the default # library registry. eq_(RemoteRegistry.DEFAULT_LIBRARY_REGISTRY_URL, x[0].integration.url)
def test_registrations(self): registry = RemoteRegistry(self.integration) # Associate the default library with the registry. Registration(registry, self._default_library) # Create another library not associated with the registry. library2 = self._library() # registrations() finds a single Registration. [registration] = list(registry.registrations) assert isinstance(registration, Registration) eq_(registry, registration.registry) eq_(self._default_library, registration.library)
def extract(document, type=Registration.OPDS_2_TYPE): response = MockRequestsResponse(200, {"Content-Type": type}, document) return Registration._extract_catalog_information(response)
def extract(document, type=Registration.OPDS_2_TYPE): response = MockRequestsResponse( 200, { "Content-Type" : type }, document ) return Registration._extract_catalog_information(response)
class TestRegistration(DatabaseTest): def setup(self): super(TestRegistration, self).setup() # Create a RemoteRegistry. self.integration = self._external_integration(protocol="some protocol", goal="some goal") self.registry = RemoteRegistry(self.integration) self.registration = Registration(self.registry, self._default_library) def test_constructor(self): # The Registration constructor was called during setup to create # self.registration. reg = self.registration eq_(self.registry, reg.registry) eq_(self._default_library, reg.library) settings = [ x for x in reg.integration.settings if x.library is not None ] eq_(set([reg.status_field, reg.stage_field]), set(settings)) eq_(Registration.FAILURE_STATUS, reg.status_field.value) eq_(Registration.TESTING_STAGE, reg.stage_field.value) # The Library has been associated with the ExternalIntegration. eq_([self._default_library], self.integration.libraries) # Creating another Registration doesn't add the library to the # ExternalIntegration again or override existing values for the # settings. reg.status_field.value = "new status" reg.stage_field.value = "new stage" reg2 = Registration(self.registry, self._default_library) eq_([self._default_library], self.integration.libraries) eq_("new status", reg2.status_field.value) eq_("new stage", reg2.stage_field.value) def test_setting(self): m = self.registration.setting def _find(key): """Find a ConfigurationSetting associated with the library. This is necessary because ConfigurationSetting.value creates _two_ ConfigurationSettings, one associated with the library and one not associated with any library, to store the default value. """ values = [ x for x in self.registration.integration.settings if x.library and x.key == key ] if len(values) == 1: return values[0] return None # Calling setting() creates a ConfigurationSetting object # associated with the library. setting = m("key") eq_("key", setting.key) eq_(None, setting.value) eq_(self._default_library, setting.library) eq_(setting, _find("key")) # You can specify a default value, which is used only if the # current value is None. setting2 = m("key", "default") eq_(setting, setting2) eq_("default", setting.value) setting3 = m("key", "default2") eq_(setting, setting3) eq_("default", setting.value) def test_push(self): """Test the other methods orchestrated by the push() method. """ class Mock(Registration): def _extract_catalog_information(self, response): self.initial_catalog_response = response return "register_url", "vendor_id" def _set_public_key(self, key): self._set_public_key_called_with = key return "an encryptor" def _create_registration_payload(self, url_for, stage): self.payload_ingredients = (url_for, stage) return dict(payload="this is it") def _send_registration_request(self, register_url, payload, do_post): self._send_registration_request_called_with = (register_url, payload, do_post) return MockRequestsResponse(200, content=json.dumps("you did it!")) def _process_registration_result(self, catalog, encryptor, stage): self._process_registration_result_called_with = (catalog, encryptor, stage) return "all done!" def mock_do_get(self, url): self.do_get_called_with = url return "A fake catalog" # First of all, test success. registration = Mock(self.registry, self._default_library) stage = Registration.TESTING_STAGE url_for = object() catalog_url = "http://catalog/" do_post = object() key = object() result = registration.push(stage, url_for, catalog_url, registration.mock_do_get, do_post, key) # Ultimately the push succeeded. eq_("all done!", result) # But there were many steps towards this result. # First, do_get was called on the catalog URL. eq_(catalog_url, registration.do_get_called_with) # Then, the catalog was passed into _extract_catalog_information. eq_("A fake catalog", registration.initial_catalog_response) # _extract_catalog_information returned a registration URL and # a vendor ID. The registration URL was used later on... # # The vendor ID was set as a ConfigurationSetting on # the ExternalIntegration associated with this registry. eq_( "vendor_id", ConfigurationSetting.for_externalintegration( AuthdataUtility.VENDOR_ID_KEY, self.integration).value) # _set_public_key() was called to create an encryptor object. # It returned an encryptor (here mocked as the string "an encryptor") # to be used later. eq_(key, registration._set_public_key_called_with) # _create_registration_payload was called to create the body # of the registration request. eq_((url_for, stage), registration.payload_ingredients) # Then _send_registration_request was called, POSTing the # payload to "register_url", the registration URL we got earlier. results = registration._send_registration_request_called_with eq_(("register_url", dict(payload="this is it"), do_post), results) # Finally, the return value of that method was loaded as JSON # and passed into _process_registration_result, along with # the encryptor obtained from _set_public_key() results = registration._process_registration_result_called_with eq_(("you did it!", "an encryptor", stage), results) # If a nonexistent stage is provided a ProblemDetail is the result. result = registration.push("no such stage", url_for, catalog_url, registration.mock_do_get, do_post, key) eq_(INVALID_INPUT.uri, result.uri) eq_("'no such stage' is not a valid registration stage", result.detail) # Now in reverse order, let's replace the mocked methods so # that they return ProblemDetail documents. This tests that if # there is a failure at any stage, the ProblemDetail is # propagated. def cause_problem(): """Try the same method call that worked before; it won't work anymore. """ return registration.push(stage, url_for, catalog_url, registration.mock_do_get, do_post, key) def fail(*args, **kwargs): return INVALID_REGISTRATION.detailed( "could not process registration result") registration._process_registration_result = fail problem = cause_problem() eq_("could not process registration result", problem.detail) def fail(*args, **kwargs): return INVALID_REGISTRATION.detailed( "could not send registration request") registration._send_registration_request = fail problem = cause_problem() eq_("could not send registration request", problem.detail) def fail(*args, **kwargs): return INVALID_REGISTRATION.detailed( "could not create registration payload") registration._create_registration_payload = fail problem = cause_problem() eq_("could not create registration payload", problem.detail) def fail(*args, **kwargs): return INVALID_REGISTRATION.detailed("could not set public key") registration._set_public_key = fail problem = cause_problem() eq_("could not set public key", problem.detail) def fail(*args, **kwargs): return INVALID_REGISTRATION.detailed( "could not extract catalog information") registration._extract_catalog_information = fail problem = cause_problem() eq_("could not extract catalog information", problem.detail) def test__extract_catalog_information(self): """Test our ability to extract a registration link and an Adobe Vendor ID from an OPDS 1 or OPDS 2 catalog. """ def extract(document, type=Registration.OPDS_2_TYPE): response = MockRequestsResponse(200, {"Content-Type": type}, document) return Registration._extract_catalog_information(response) def assert_no_link(*args, **kwargs): """Verify that calling _extract_catalog_information on the given feed fails because there is no link with rel="register" """ result = extract(*args, **kwargs) eq_(REMOTE_INTEGRATION_FAILED.uri, result.uri) eq_("The service at http://url/ did not provide a register link.", result.detail) # OPDS 2 feed with link and Adobe Vendor ID. link = {'rel': 'register', 'href': 'register url'} metadata = {'adobe_vendor_id': 'vendorid'} feed = json.dumps(dict(links=[link], metadata=metadata)) eq_(("register url", "vendorid"), extract(feed)) # OPDS 2 feed with link and no Adobe Vendor ID feed = json.dumps(dict(links=[link])) eq_(("register url", None), extract(feed)) # OPDS 2 feed with no link. feed = json.dumps(dict(metadata=metadata)) assert_no_link(feed) # OPDS 1 feed with link. feed = '<feed><link rel="register" href="register url"/></feed>' eq_(("register url", None), extract(feed, Registration.OPDS_1_PREFIX + ";foo")) # OPDS 1 feed with no link. feed = '<feed></feed>' assert_no_link(feed, Registration.OPDS_1_PREFIX + ";foo") # Non-OPDS document. result = extract("plain text here", "text/plain") eq_(REMOTE_INTEGRATION_FAILED.uri, result.uri) eq_("The service at http://url/ did not return OPDS.", result.detail) def test__set_public_key(self): """Test that _set_public_key creates a public key for a library.""" # First try with a specific key. key = RSA.generate(1024) public_key = key.publickey().exportKey() # The return value is a PKCS1_OAEP encryptor made from the keypair. encryptor = self.registration._set_public_key(key) assert isinstance(encryptor, type(PKCS1_OAEP.new(key))) eq_(key, encryptor._key) # The key is stored in a setting on the library. setting = ConfigurationSetting.for_library(Configuration.PUBLIC_KEY, self.registration.library) eq_(key.publickey().exportKey(), setting.value) # Now try again without specifying a key - a new one will # be generated. This is what will happen outside of tests. encryptor = self.registration._set_public_key() assert encryptor._key != key setting = ConfigurationSetting.for_library(Configuration.PUBLIC_KEY, self.registration.library) # The library setting has been changed. eq_(encryptor._key.publickey().exportKey(), setting.value) def test__create_registration_payload(self): m = self.registration._create_registration_payload # Mock url_for to create good-looking callback URLs. def url_for(controller, library_short_name): return "http://server/%s/%s" % (library_short_name, controller) # First, test with no configuration contact configured for the # library. stage = object() expect_url = url_for("authentication_document", self.registration.library.short_name) expect_payload = dict(url=expect_url, stage=stage) eq_(expect_payload, m(url_for, stage)) # If a contact is configured, it shows up in the payload. contact = "mailto:[email protected]" ConfigurationSetting.for_library( Configuration.CONFIGURATION_CONTACT_EMAIL, self.registration.library, ).value = contact expect_payload['contact'] = contact eq_(expect_payload, m(url_for, stage)) def test__send_registration_request(self): class Mock(object): def __init__(self, response): self.response = response def do_post(self, url, payload, **kwargs): self.called_with = (url, payload, kwargs) return self.response # If everything goes well, the return value of do_post is # passed through. mock = Mock(MockRequestsResponse(200, content="all good")) url = object() payload = object() m = Registration._send_registration_request result = m(url, payload, mock.do_post) eq_(mock.response, result) called_with = mock.called_with eq_(called_with, (url, payload, dict(timeout=60, allowed_response_codes=["2xx", "3xx", "400", "401"]))) # Most error handling is expected to be handled by do_post # raising an exception, but certain responses get special # treatment: # The remote sends a 401 response with a problem detail. mock = Mock( MockRequestsResponse( 401, {"Content-Type": PROBLEM_DETAIL_JSON_MEDIA_TYPE}, content=json.dumps(dict(detail="this is a problem detail")))) result = m(url, payload, mock.do_post) assert isinstance(result, ProblemDetail) eq_(REMOTE_INTEGRATION_FAILED.uri, result.uri) eq_('Remote service returned: "this is a problem detail"', result.detail) # The remote sends some other kind of 401 response. mock = Mock( MockRequestsResponse(401, {"Content-Type": "text/html"}, content="log in why don't you")) result = m(url, payload, mock.do_post) assert isinstance(result, ProblemDetail) eq_(REMOTE_INTEGRATION_FAILED.uri, result.uri) eq_('Remote service returned: "log in why don\'t you"', result.detail) def test__decrypt_shared_secret(self): key = RSA.generate(2048) encryptor = PKCS1_OAEP.new(key) key2 = RSA.generate(2048) encryptor2 = PKCS1_OAEP.new(key2) shared_secret = os.urandom(24).encode('hex') encrypted_secret = base64.b64encode(encryptor.encrypt(shared_secret)) # Success. m = Registration._decrypt_shared_secret eq_(shared_secret, m(encryptor, encrypted_secret)) # If we try to decrypt using the wrong key, a ProblemDetail is # returned explaining the problem. problem = m(encryptor2, encrypted_secret) assert isinstance(problem, ProblemDetail) eq_(SHARED_SECRET_DECRYPTION_ERROR.uri, problem.uri) assert encrypted_secret in problem.detail def test__process_registration_result(self): reg = self.registration m = reg._process_registration_result # Set up a public key just so it can be removed once # registration is successful. public_key = ConfigurationSetting.for_library(Configuration.PUBLIC_KEY, reg.library) public_key.value = "a key" # Result must be a dictionary. result = m("not a dictionary", None, None) eq_(INTEGRATION_ERROR.uri, result.uri) eq_( "Remote service served 'not a dictionary', which I can't make sense of as an OPDS document.", result.detail) # Since there was an immediate failure, the public key has not been # wiped. eq_("a key", public_key.value) # When the result is empty, the registration is marked as successful. new_stage = "new stage" encryptor = object() result = m(dict(), encryptor, new_stage) eq_(True, result) eq_(reg.SUCCESS_STATUS, reg.status_field.value) # The library's public key has been removed. eq_(None, public_key.value) # The stage field has been set to the requested value. eq_(new_stage, reg.stage_field.value) # Now try with a result that includes a short name and # a shared secret. class Mock(Registration): def _decrypt_shared_secret(self, encryptor, shared_secret): self._decrypt_shared_secret_called_with = (encryptor, shared_secret) return "cleartext" reg = Mock(self.registry, self._default_library) catalog = dict( metadata=dict(short_name="SHORT", shared_secret="ciphertext")) result = reg._process_registration_result(catalog, encryptor, "another new stage") eq_(True, result) # Short name is set. eq_("SHORT", reg.setting(ExternalIntegration.USERNAME).value) # Shared secret was decrypted and is set. eq_((encryptor, "ciphertext"), reg._decrypt_shared_secret_called_with) eq_("cleartext", reg.setting(ExternalIntegration.PASSWORD).value) eq_("another new stage", reg.stage_field.value) # Now simulate a problem decrypting the shared secret. class Mock(Registration): def _decrypt_shared_secret(self, encryptor, shared_secret): return SHARED_SECRET_DECRYPTION_ERROR reg = Mock(self.registry, self._default_library) result = reg._process_registration_result(catalog, encryptor, "another new stage") eq_(SHARED_SECRET_DECRYPTION_ERROR, result)