def test_libraries_get_with_geographic_info(self): # Delete any existing library created by the controller test setup. library = get_one(self._db, Library) if library: self._db.delete(library) test_library = self._library("Library 1", "L1") ConfigurationSetting.for_library( Configuration.LIBRARY_FOCUS_AREA, test_library).value = '{"CA": ["N3L"], "US": ["11235"]}' ConfigurationSetting.for_library( Configuration.LIBRARY_SERVICE_AREA, test_library).value = '{"CA": ["J2S"], "US": ["31415"]}' with self.request_context_with_admin("/"): response = self.manager.admin_library_settings_controller.process_get( ) library_settings = response.get("libraries")[0].get("settings") assert library_settings.get("focus_area") == { "CA": [{ "N3L": "Paris, Ontario" }], "US": [{ "11235": "Brooklyn, NY" }], } assert library_settings.get("service_area") == { "CA": [{ "J2S": "Saint-Hyacinthe Southwest, Quebec" }], "US": [{ "31415": "Savannah, GA" }], }
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_libraries_get_with_multiple_libraries(self): # Delete any existing library created by the controller test setup. library = get_one(self._db, Library) if library: self._db.delete(library) l1 = self._library("Library 1", "L1") l2 = self._library("Library 2", "L2") l3 = self._library("Library 3", "L3") # L2 has some additional library-wide settings. ConfigurationSetting.for_library(Configuration.FEATURED_LANE_SIZE, l2).value = 5 ConfigurationSetting.for_library( Configuration.DEFAULT_FACET_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME, l2, ).value = FacetConstants.ORDER_TITLE ConfigurationSetting.for_library( Configuration.ENABLED_FACETS_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME, l2, ).value = json.dumps( [FacetConstants.ORDER_TITLE, FacetConstants.ORDER_AUTHOR]) ConfigurationSetting.for_library( Configuration.LARGE_COLLECTION_LANGUAGES, l2).value = json.dumps(["French"]) # The admin only has access to L1 and L2. self.admin.remove_role(AdminRole.SYSTEM_ADMIN) self.admin.add_role(AdminRole.LIBRARIAN, l1) self.admin.add_role(AdminRole.LIBRARY_MANAGER, l2) with self.request_context_with_admin("/"): response = self.manager.admin_library_settings_controller.process_get( ) libraries = response.get("libraries") assert 2 == len(libraries) assert l1.uuid == libraries[0].get("uuid") assert l2.uuid == libraries[1].get("uuid") assert l1.name == libraries[0].get("name") assert l2.name == libraries[1].get("name") assert l1.short_name == libraries[0].get("short_name") assert l2.short_name == libraries[1].get("short_name") assert {} == libraries[0].get("settings") assert 4 == len(libraries[1].get("settings").keys()) settings = libraries[1].get("settings") assert "5" == settings.get(Configuration.FEATURED_LANE_SIZE) assert FacetConstants.ORDER_TITLE == settings.get( Configuration.DEFAULT_FACET_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME) assert [ FacetConstants.ORDER_TITLE, FacetConstants.ORDER_AUTHOR, ] == settings.get(Configuration.ENABLED_FACETS_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME) assert ["French" ] == settings.get(Configuration.LARGE_COLLECTION_LANGUAGES)
def test_estimate_language_collection_for_library(self): library = self._default_library # We thought we'd have big collections. old_settings = { Configuration.LARGE_COLLECTION_LANGUAGES : ["spa", "fre"], Configuration.SMALL_COLLECTION_LANGUAGES : ["chi"], Configuration.TINY_COLLECTION_LANGUAGES : ["rus"], } for key, value in old_settings.items(): ConfigurationSetting.for_library( key, library).value = json.dumps(value) # But there's nothing in our database, so when we call # Configuration.estimate_language_collections_for_library... Configuration.estimate_language_collections_for_library(library) # ...it gets reset to the default. eq_(["eng"], ConfigurationSetting.for_library( Configuration.LARGE_COLLECTION_LANGUAGES, library ).json_value) eq_([], ConfigurationSetting.for_library( Configuration.SMALL_COLLECTION_LANGUAGES, library ).json_value) eq_([], ConfigurationSetting.for_library( Configuration.TINY_COLLECTION_LANGUAGES, library ).json_value)
def process_get(self): libraries = [] for library in self._db.query(Library).order_by(Library.name): # Only include libraries this admin has librarian access to. if not flask.request.admin or not flask.request.admin.is_librarian( library): continue settings = dict() for setting in Configuration.LIBRARY_SETTINGS: if setting.get("type") == "list": value = ConfigurationSetting.for_library( setting.get("key"), library).json_value else: value = ConfigurationSetting.for_library( setting.get("key"), library).value if value: settings[setting.get("key")] = value libraries += [ dict( uuid=library.uuid, name=library.name, short_name=library.short_name, settings=settings, ) ] return dict(libraries=libraries, settings=Configuration.LIBRARY_SETTINGS)
def test_libraries_get_with_multiple_libraries(self): # Delete any existing library created by the controller test setup. library = get_one(self._db, Library) if library: self._db.delete(library) l1 = self._library("Library 1", "L1") l2 = self._library("Library 2", "L2") l3 = self._library("Library 3", "L3") # L2 has some additional library-wide settings. ConfigurationSetting.for_library(Configuration.FEATURED_LANE_SIZE, l2).value = 5 ConfigurationSetting.for_library( Configuration.DEFAULT_FACET_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME, l2 ).value = FacetConstants.ORDER_RANDOM ConfigurationSetting.for_library( Configuration.ENABLED_FACETS_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME, l2 ).value = json.dumps([FacetConstants.ORDER_TITLE, FacetConstants.ORDER_RANDOM]) ConfigurationSetting.for_library( Configuration.LARGE_COLLECTION_LANGUAGES, l2 ).value = json.dumps(["French"]) # The admin only has access to L1 and L2. self.admin.remove_role(AdminRole.SYSTEM_ADMIN) self.admin.add_role(AdminRole.LIBRARIAN, l1) self.admin.add_role(AdminRole.LIBRARY_MANAGER, l2) with self.request_context_with_admin("/"): response = self.manager.admin_library_settings_controller.process_get() libraries = response.get("libraries") eq_(2, len(libraries)) eq_(l1.uuid, libraries[0].get("uuid")) eq_(l2.uuid, libraries[1].get("uuid")) eq_(l1.name, libraries[0].get("name")) eq_(l2.name, libraries[1].get("name")) eq_(l1.short_name, libraries[0].get("short_name")) eq_(l2.short_name, libraries[1].get("short_name")) eq_({}, libraries[0].get("settings")) eq_(4, len(libraries[1].get("settings").keys())) settings = libraries[1].get("settings") eq_("5", settings.get(Configuration.FEATURED_LANE_SIZE)) eq_(FacetConstants.ORDER_RANDOM, settings.get(Configuration.DEFAULT_FACET_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME)) eq_([FacetConstants.ORDER_TITLE, FacetConstants.ORDER_RANDOM], settings.get(Configuration.ENABLED_FACETS_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME)) eq_(["French"], settings.get(Configuration.LARGE_COLLECTION_LANGUAGES))
def test_add_configuration_links(self): mock_feed = [] link_config = { CirculationManagerAnnotator.TERMS_OF_SERVICE: "http://terms/", CirculationManagerAnnotator.PRIVACY_POLICY: "http://privacy/", CirculationManagerAnnotator.COPYRIGHT: "http://copyright/", CirculationManagerAnnotator.ABOUT: "http://about/", CirculationManagerAnnotator.LICENSE: "http://license/", } # Set up configuration settings for links. for rel, value in link_config.iteritems(): ConfigurationSetting.for_library( rel, self._default_library).value = value self.annotator.add_configuration_links(mock_feed) # Five links were added to the "feed" eq_(5, len(mock_feed)) # They are the links we'd expect. links = {} for link in mock_feed: rel = link.attrib['rel'] href = link.attrib['href'] type = link.attrib['type'] eq_("text/html", type) # Check that the configuration value made it into the link. eq_(href, link_config[rel])
def test_borrow_with_outstanding_fines(self): # This checkout would succeed... now = datetime.now() loaninfo = LoanInfo( self.pool.collection, self.pool.data_source, self.pool.identifier.type, self.pool.identifier.identifier, now, now + timedelta(seconds=3600), ) self.remote.queue_checkout(loaninfo) # ...except the patron has too many fines. old_fines = self.patron.fines self.patron.fines = 1000 setting = ConfigurationSetting.for_library( Configuration.MAX_OUTSTANDING_FINES, self._default_library) setting.value = "$0.50" assert_raises(OutstandingFines, self.borrow) # Test the case where any amount of fines are too much. setting.value = "$0" assert_raises(OutstandingFines, self.borrow) # Remove the fine policy, and borrow succeeds. setting.value = None loan, i1, i2 = self.borrow() assert isinstance(loan, Loan) self.patron.fines = old_fines
def library_configuration_settings(self, library, validators_by_format, settings=None): """Validate and update a library's configuration settings based on incoming new values. :param library: A Library :param validators_by_format: A dictionary mapping the 'format' field from a setting configuration to a corresponding validator object. :param settings: A list of setting configurations to use in tests instead of Configuration.LIBRARY_SETTINGS """ settings = settings or Configuration.LIBRARY_SETTINGS for setting in settings: # Validate the incoming value. validator = None if "format" in setting: validator = validators_by_format.get(setting["format"]) elif "type" in setting: validator = validators_by_format.get(setting["type"]) validated_value = self._validate_setting(library, setting, validator) if isinstance(validated_value, ProblemDetail): # Validation failed -- return a ProblemDetail. return validated_value # Validation succeeded -- set the new value. ConfigurationSetting.for_library( setting["key"], library).value = self._format_validated_value( validated_value, validator)
def default_notification_email_address(self, patron, pin): """What email address should be used to notify this patron of changes? """ return ConfigurationSetting.for_library( Configuration.DEFAULT_NOTIFICATION_EMAIL_ADDRESS, patron.library).value
def max_outstanding_fines(cls, library): max_fines = ConfigurationSetting.for_library( cls.MAX_OUTSTANDING_FINES, library ) if max_fines.value is None: return None return MoneyUtility.parse(max_fines.value)
def test_default_notification_email_address(self): """Test the ability of the Overdrive API to detect an email address previously given by the patron to Overdrive for the purpose of notifications. """ ignore, patron_with_email = self.sample_json("patron_info.json") self.api.queue_response(200, content=patron_with_email) patron = self._patron() # If the patron has used a particular email address to put # books on hold, use that email address, not the site default. ConfigurationSetting.for_library( Configuration.DEFAULT_NOTIFICATION_EMAIL_ADDRESS, self._default_library).value = "*****@*****.**" eq_("*****@*****.**", self.api.default_notification_email_address(patron, 'pin')) # If the patron has never before put an Overdrive book on # hold, their JSON object has no `lastHoldEmail` key. In this # case we use the site default. patron_with_no_email = dict(patron_with_email) del patron_with_no_email['lastHoldEmail'] self.api.queue_response(200, content=patron_with_no_email) eq_("*****@*****.**", self.api.default_notification_email_address(patron, 'pin')) # If there's an error getting the information, use the # site default. self.api.queue_response(404) eq_("*****@*****.**", self.api.default_notification_email_address(patron, 'pin'))
def library_configuration_settings(self, library, validator): for setting in Configuration.LIBRARY_SETTINGS: if setting.get("format") == "geographic": locations = validator.validate_geographic_areas( self.list_setting(setting), self._db) if isinstance(locations, ProblemDetail): return locations value = locations or self.current_value(setting, library) elif setting.get("type") == "list": value = self.list_setting(setting) or self.current_value( setting, library) if setting.get("format") == "language-code": value = json.dumps([ LanguageCodes.string_to_alpha_3(language) for language in json.loads(value) ]) elif setting.get("type") == "image": value = self.image_setting(setting) or self.current_value( setting, library) else: default = setting.get('default') value = flask.request.form.get(setting['key'], default) ConfigurationSetting.for_library(setting['key'], library).value = value
def process_get(self): libraries = [] for library in self._db.query(Library).order_by(Library.name): # Only include libraries this admin has librarian access to. if not flask.request.admin or not flask.request.admin.is_librarian(library): continue settings = dict() for setting in Configuration.LIBRARY_SETTINGS: if setting.get("type") == "list": value = ConfigurationSetting.for_library(setting.get("key"), library).json_value if value and setting.get("format") == "geographic": value = self.get_extra_geographic_information(value) else: value = self.current_value(setting, library) if value: settings[setting.get("key")] = value libraries += [dict( uuid=library.uuid, name=library.name, short_name=library.short_name, settings=settings, )] return dict(libraries=libraries, settings=Configuration.LIBRARY_SETTINGS)
def test_default_notification_email_address(self): """Test the ability of the Overdrive API to detect an email address previously given by the patron to Overdrive for the purpose of notifications. """ ignore, patron_with_email = self.sample_json( "patron_info.json" ) self.api.queue_response(200, content=patron_with_email) patron = self._patron() # If the patron has used a particular email address to put # books on hold, use that email address, not the site default. ConfigurationSetting.for_library( Configuration.DEFAULT_NOTIFICATION_EMAIL_ADDRESS, self._default_library).value = "*****@*****.**" eq_("*****@*****.**", self.api.default_notification_email_address(patron, 'pin')) # If the patron has never before put an Overdrive book on # hold, their JSON object has no `lastHoldEmail` key. In this # case we use the site default. patron_with_no_email = dict(patron_with_email) del patron_with_no_email['lastHoldEmail'] self.api.queue_response(200, content=patron_with_no_email) eq_("*****@*****.**", self.api.default_notification_email_address(patron, 'pin')) # If there's an error getting the information, use the # site default. self.api.queue_response(404) eq_("*****@*****.**", self.api.default_notification_email_address(patron, 'pin'))
def test_borrow_with_outstanding_fines(self): # This checkout would succeed... now = datetime.now() loaninfo = LoanInfo( self.pool.collection, self.pool.data_source, self.pool.identifier.type, self.pool.identifier.identifier, now, now + timedelta(seconds=3600), ) self.remote.queue_checkout(loaninfo) # ...except the patron has too many fines. old_fines = self.patron.fines self.patron.fines = 1000 setting = ConfigurationSetting.for_library( Configuration.MAX_OUTSTANDING_FINES, self._default_library ) setting.value = "$0.50" assert_raises(OutstandingFines, self.borrow) # Test the case where any amount of fines are too much. setting.value = "$0" assert_raises(OutstandingFines, self.borrow) # Remove the fine policy, and borrow succeeds. setting.value = None loan, i1, i2 = self.borrow() assert isinstance(loan, Loan) self.patron.fines = old_fines
def test_has_borrowing_privileges(self): """Test the methods that encapsulate the determination of whether or not a patron can borrow books. """ now = datetime.datetime.utcnow() one_day_ago = now - datetime.timedelta(days=1) patron = self._patron() # Most patrons have borrowing privileges. eq_(True, PatronUtility.has_borrowing_privileges(patron)) PatronUtility.assert_borrowing_privileges(patron) # If your card expires you lose borrowing privileges. patron.authorization_expires = one_day_ago eq_(False, PatronUtility.has_borrowing_privileges(patron)) assert_raises(AuthorizationExpired, PatronUtility.assert_borrowing_privileges, patron) patron.authorization_expires = None # If you accrue excessive fines you lose borrowing privileges. setting = ConfigurationSetting.for_library( Configuration.MAX_OUTSTANDING_FINES, self._default_library) setting.value = "$0.50" patron.fines = 1 eq_(False, PatronUtility.has_borrowing_privileges(patron)) assert_raises(OutstandingFines, PatronUtility.assert_borrowing_privileges, patron) # Test the case where any amount of fines is too much. setting.value = "$0" eq_(False, PatronUtility.has_borrowing_privileges(patron)) assert_raises(OutstandingFines, PatronUtility.assert_borrowing_privileges, patron) setting.value = "$100" eq_(True, PatronUtility.has_borrowing_privileges(patron)) patron.fines = 0 eq_(True, PatronUtility.has_borrowing_privileges(patron)) # Even if the circulation manager is not configured to know # what "excessive fines" are, the authentication mechanism # might know, and might store that information in the # patron's block_reason. patron.block_reason = PatronData.EXCESSIVE_FINES assert_raises(OutstandingFines, PatronUtility.assert_borrowing_privileges, patron) # If your card is blocked for any reason you lose borrowing # privileges. patron.block_reason = "some reason" eq_(False, PatronUtility.has_borrowing_privileges(patron)) assert_raises(AuthorizationBlocked, PatronUtility.assert_borrowing_privileges, patron) patron.block_reason = None eq_(True, PatronUtility.has_borrowing_privileges(patron))
def _set_notification_address(self, library): """Set the default notification address for the given library. This is necessary to create RBdigital user accounts for its patrons. """ ConfigurationSetting.for_library( Configuration.DEFAULT_NOTIFICATION_EMAIL_ADDRESS, library ).value = '*****@*****.**'
def test_libraries_get_with_geographic_info(self): # Delete any existing library created by the controller test setup. library = get_one(self._db, Library) if library: self._db.delete(library) test_library = self._library("Library 1", "L1") ConfigurationSetting.for_library( Configuration.LIBRARY_FOCUS_AREA, test_library ).value = '{"CA": ["N3L"], "US": ["11235"]}' ConfigurationSetting.for_library( Configuration.LIBRARY_SERVICE_AREA, test_library ).value = '{"CA": ["J2S"], "US": ["31415"]}' with self.request_context_with_admin("/"): response = self.manager.admin_library_settings_controller.process_get() library_settings = response.get("libraries")[0].get("settings") eq_(library_settings.get("focus_area"), {u'CA': [{u'N3L': u'Paris, Ontario'}], u'US': [{u'11235': u'Brooklyn, NY'}]}) eq_(library_settings.get("service_area"), {u'CA': [{u'J2S': u'Saint-Hyacinthe Southwest, Quebec'}], u'US': [{u'31415': u'Savannah, GA'}]})
def checkout_status(self, identifier): """Times request rates related to checking out a book. Intended to be run with an identifier without license restrictions. """ status = dict() patron, password = self.test_patron license_pools = identifier.licensed_through if not license_pools: raise ValueError("No license pools for this identifier") for license_pool in license_pools: delivery_mechanism = None if license_pool.delivery_mechanisms: delivery_mechanism = license_pool.delivery_mechanisms[0] loans = [] service = "Checkout COLLECTION=%s IDENTIFIER=%r" % ( license_pool.collection.name, identifier) api = self.circulation.api_for_license_pool(license_pool) address = ConfigurationSetting.for_library( Configuration.DEFAULT_NOTIFICATION_EMAIL_ADDRESS, patron.library) def do_checkout(): loan, hold, is_new = api.borrow( patron, password, license_pool, delivery_mechanism, address, ) if loan: loans.append(loan) else: raise Exception("No loan created during checkout") self._add_timing(status, service, do_checkout) service = "Fulfill IDENTIFIER: %r" % identifier def do_fulfillment(): api.fulfill(patron, password, license_pool, delivery_mechanism) self._add_timing(status, service, do_fulfillment) service = "Checkin IDENTIFIER: %r" % identifier def do_checkin(): api.revoke_loan(patron, password, license_pool) self._add_timing(status, service, do_checkin) self.log_status(status) return status
def setup(self): super(TestLaneScript, self).setup() base_url_setting = ConfigurationSetting.sitewide( self._db, Configuration.BASE_URL_KEY) base_url_setting.value = u'http://test-circulation-manager/' for k, v in [(Configuration.LARGE_COLLECTION_LANGUAGES, []), (Configuration.SMALL_COLLECTION_LANGUAGES, []), (Configuration.TINY_COLLECTION_LANGUAGES, ['eng', 'fre'])]: ConfigurationSetting.for_library( k, self._default_library).value = json.dumps(v)
def test_collection_language_method_performs_estimate(self): C = Configuration library = self._default_library # We haven't set any of these values. for key in [ C.LARGE_COLLECTION_LANGUAGES, C.SMALL_COLLECTION_LANGUAGES, C.TINY_COLLECTION_LANGUAGES ]: eq_(None, ConfigurationSetting.for_library(key, library).value) # So how does this happen? eq_(["eng"], C.large_collection_languages(library)) eq_([], C.small_collection_languages(library)) eq_([], C.tiny_collection_languages(library)) # It happens because the first time we call one of those # *_collection_languages, it estimates values for all three # configuration settings, based on the library's current # holdings. large_setting = ConfigurationSetting.for_library( C.LARGE_COLLECTION_LANGUAGES, library) eq_(["eng"], large_setting.json_value) eq_([], ConfigurationSetting.for_library(C.SMALL_COLLECTION_LANGUAGES, library).json_value) eq_([], ConfigurationSetting.for_library(C.TINY_COLLECTION_LANGUAGES, library).json_value) # We can change these values. large_setting.value = json.dumps(["spa", "jpn"]) eq_(["spa", "jpn"], C.large_collection_languages(library)) # If we enter an invalid value, or a value that's not a list, # the estimate is re-calculated the next time we look. large_setting.value = "this isn't json" eq_(["eng"], C.large_collection_languages(library)) large_setting.value = '"this is json but it\'s not a list"' eq_(["eng"], C.large_collection_languages(library))
def setup(self): super(TestLaneScript, self).setup() base_url_setting = ConfigurationSetting.sitewide( self._db, Configuration.BASE_URL_KEY) base_url_setting.value = u'http://test-circulation-manager/' for k, v in [ (Configuration.LARGE_COLLECTION_LANGUAGES, []), (Configuration.SMALL_COLLECTION_LANGUAGES, []), (Configuration.TINY_COLLECTION_LANGUAGES, ['eng', 'fre']) ]: ConfigurationSetting.for_library( k, self._default_library).value = json.dumps(v)
def _email_uri_with_fallback(cls, library, key): """Try to find a certain email address configured for the given purpose. If not available, use the general patron support address. :param key: The specific email address to look for. """ for setting in [key, Configuration.HELP_EMAIL]: value = ConfigurationSetting.for_library(setting, library).value if not value: continue return cls._as_mailto(value)
def process_get(self): response = [] libraries = self._db.query(Library).order_by(Library.name).all() ConfigurationSetting.cache_warm(self._db) for library in libraries: # Only include libraries this admin has librarian access to. if not flask.request.admin or not flask.request.admin.is_librarian( library): continue settings = dict() for setting in Configuration.LIBRARY_SETTINGS: if setting.get("type") == "announcements": value = ConfigurationSetting.for_library( setting.get("key"), library).json_value if value: value = AnnouncementListValidator( ).validate_announcements(value) if setting.get("type") == "list": value = ConfigurationSetting.for_library( setting.get("key"), library).json_value if value and setting.get("format") == "geographic": value = self.get_extra_geographic_information(value) else: value = self.current_value(setting, library) if value: settings[setting.get("key")] = value response += [ dict( uuid=library.uuid, name=library.name, short_name=library.short_name, settings=settings, ) ] return dict(libraries=response, settings=Configuration.LIBRARY_SETTINGS)
def library_configuration_settings(self, library): for setting in Configuration.LIBRARY_SETTINGS: if setting.get("type") == "list": value = self.list_setting(setting) or self.current_value( setting, library) elif setting.get("type") == "image": value = self.image_setting(setting) or self.current_value( setting, library) else: default = setting.get('default') value = flask.request.form.get(setting['key'], default) ConfigurationSetting.for_library(setting['key'], library).value = value
def estimate_language_collections_for_library(cls, library): """Guess at appropriate values for the given library for LARGE_COLLECTION_LANGUAGES, SMALL_COLLECTION_LANGUAGES, and TINY_COLLECTION_LANGUAGES. Set configuration values appropriately, overriding any previous values. """ holdings = library.estimated_holdings_by_language() large, small, tiny = cls.classify_holdings(holdings) for setting, value in ( (cls.LARGE_COLLECTION_LANGUAGES, large), (cls.SMALL_COLLECTION_LANGUAGES, small), (cls.TINY_COLLECTION_LANGUAGES, tiny), ): ConfigurationSetting.for_library( setting, library).value = json.dumps(value)
def estimate_language_collections_for_library(cls, library): """Guess at appropriate values for the given library for LARGE_COLLECTION_LANGUAGES, SMALL_COLLECTION_LANGUAGES, and TINY_COLLECTION_LANGUAGES. Set configuration values appropriately, overriding any previous values. """ holdings = library.estimated_holdings_by_language() large, small, tiny = cls.classify_holdings(holdings) for setting, value in ( (cls.LARGE_COLLECTION_LANGUAGES, large), (cls.SMALL_COLLECTION_LANGUAGES, small), (cls.TINY_COLLECTION_LANGUAGES, tiny), ): ConfigurationSetting.for_library(setting, library).value = json.dumps(value)
def test_place_hold(self): edition, pool = self._edition(identifier_type=Identifier.AXIS_360_ID, data_source_name=DataSource.AXIS_360, with_license_pool=True) data = self.sample_data("place_hold_success.xml") self.api.queue_response(200, content=data) patron = self._patron() ConfigurationSetting.for_library( Configuration.DEFAULT_NOTIFICATION_EMAIL_ADDRESS, self._default_library).value = "*****@*****.**" response = self.api.place_hold(patron, 'pin', pool, None) eq_(1, response.hold_position) eq_(response.identifier_type, pool.identifier.type) eq_(response.identifier, pool.identifier.identifier) [request] = self.api.requests params = request[-1]['params'] eq_('*****@*****.**', params['email'])
def help_uris(cls, library): """Find all the URIs that might help patrons get help from this library. :yield: A sequence of 2-tuples (media type, URL) """ for name in cls.HELP_LINKS: setting = ConfigurationSetting.for_library(name, library) value = setting.value if not value: continue type = None if name == cls.HELP_EMAIL: value = cls._as_mailto(value) if name == cls.HELP_WEB: type = "text/html" yield type, value
def help_uris(cls, library): """Find all the URIs that might help patrons get help from this library. :yield: A sequence of 2-tuples (media type, URL) """ for name in cls.HELP_LINKS: setting = ConfigurationSetting.for_library(name, library) value = setting.value if not value: continue type = None if name == cls.HELP_EMAIL: value = cls._as_mailto(value) if name == cls.HELP_WEB: type = 'text/html' yield type, value
def library_configuration_settings(self, library, validator): for setting in Configuration.LIBRARY_SETTINGS: if setting.get("format") == "geographic": locations = validator.validate_geographic_areas(self.list_setting(setting), self._db) if isinstance(locations, ProblemDetail): return locations value = locations or self.current_value(setting, library) elif setting.get("type") == "list": value = self.list_setting(setting) or self.current_value(setting, library) if setting.get("format") == "language-code": value = json.dumps([LanguageCodes.string_to_alpha_3(language) for language in json.loads(value)]) elif setting.get("type") == "image": value = self.image_setting(setting) or self.current_value(setting, library) else: default = setting.get('default') value = flask.request.form.get(setting['key'], default) ConfigurationSetting.for_library(setting['key'], library).value = value
def test_has_excess_fines(self): # Test the has_excess_fines method. patron = self._patron() # If you accrue excessive fines you lose borrowing privileges. setting = ConfigurationSetting.for_library( Configuration.MAX_OUTSTANDING_FINES, self._default_library ) # Verify that all these tests work no matter what data type has been stored in # patron.fines. for patron_fines in ("1", "0.75", 1, 1.0, Decimal(1), MoneyUtility.parse("1")): patron.fines = patron_fines # Test cases where the patron's fines exceed a well-defined limit, # or when any amount of fines is too much. for max_fines in ["$0.50", "0.5", 0.5] + [ # well-defined limit "$0", "$0.00", "0", 0, ]: # any fines is too much setting.value = max_fines assert True == PatronUtility.has_excess_fines(patron) # Test cases where the patron's fines are below a # well-defined limit, or where fines are ignored # altogether. for max_fines in ["$100", 100] + [ # well-defined-limit None, "", ]: # fines ignored setting.value = max_fines assert False == PatronUtility.has_excess_fines(patron) # Test various cases where fines in any amount deny borrowing # privileges, but the patron has no fines. for patron_fines in ("0", "$0", 0, None, MoneyUtility.parse("$0")): patron.fines = patron_fines for max_fines in ["$0", "$0.00", "0", 0]: setting.value = max_fines assert False == PatronUtility.has_excess_fines(patron)
def _collection_languages(cls, library, key): """Look up a list of languages in a library configuration. If the value is not set, estimate a value (and all related values) by looking at the library's collection. """ setting = ConfigurationSetting.for_library(key, library) value = None try: value = setting.json_value if not isinstance(value, list): value = None except (TypeError, ValueError): pass if value is None: # We have no value or a bad value. Estimate a better value. cls.estimate_language_collections_for_library(library) value = setting.json_value return value
def test_max_outstanding_fines(self): m = Configuration.max_outstanding_fines # By default, fines are not enforced. eq_(None, m(self._default_library)) # The maximum fine value is determined by this # ConfigurationSetting. setting = ConfigurationSetting.for_library( Configuration.MAX_OUTSTANDING_FINES, self._default_library) # Any amount of fines is too much. setting.value = "$0" max_fines = m(self._default_library) eq_(0, max_fines.amount) # A more lenient approach. setting.value = "100" max_fines = m(self._default_library) eq_(100, max_fines.amount)
def test_borrow_with_fines_fails(self): # This checkout would succeed... now = datetime.now() loaninfo = LoanInfo( self.pool.collection, self.pool.data_source, self.pool.identifier.type, self.pool.identifier.identifier, now, now + timedelta(seconds=3600), ) self.remote.queue_checkout(loaninfo) # ...except the patron has too many fines. old_fines = self.patron.fines self.patron.fines = 1000 ConfigurationSetting.for_library(Configuration.MAX_OUTSTANDING_FINES, self._default_library).value = "$0.50" assert_raises(OutstandingFines, self.borrow) self.patron.fines = old_fines
def _set_public_key(self, key=None): """Set the public key for this library. This key will be published in the library's Authentication For OPDS document, allowing the remote registry to sign a shared secret for it. NOTE: This method commits to the database. :return: A Crypto.Cipher object that can be used to decrypt data encrypted with the public key. """ if not key: key = RSA.generate(2048) public_key = key.publickey().exportKey() encryptor = PKCS1_OAEP.new(key) ConfigurationSetting.for_library(Configuration.PUBLIC_KEY, self.library).value = public_key # Commit so the public key will be there when the registry gets the # OPDS Authentication document. self._db.commit() return encryptor
def test_add_configuration_links(self): mock_feed = [] link_config = { CirculationManagerAnnotator.TERMS_OF_SERVICE: "http://terms/", CirculationManagerAnnotator.PRIVACY_POLICY: "http://privacy/", CirculationManagerAnnotator.COPYRIGHT: "http://copyright/", CirculationManagerAnnotator.ABOUT: "http://about/", CirculationManagerAnnotator.LICENSE: "http://license/", Configuration.HELP_EMAIL: "help@me", Configuration.HELP_WEB: "http://help/", Configuration.HELP_URI: "uri:help", } # Set up configuration settings for links. for rel, value in link_config.iteritems(): ConfigurationSetting.for_library( rel, self._default_library).value = value self.annotator.add_configuration_links(mock_feed) # Eight links were added to the "feed" eq_(8, len(mock_feed)) # They are the links we'd expect. links = {} for link in mock_feed: rel = link.attrib['rel'] href = link.attrib['href'] if rel == 'help': continue # Tested below # Check that the configuration value made it into the link. eq_(href, link_config[rel]) eq_("text/html", link.attrib['type']) # There are three help links using different protocols. help_links = [ x.attrib['href'] for x in mock_feed if x.attrib['rel'] == 'help' ] eq_(set(["mailto:help@me", "http://help/", "uri:help"]), set(help_links))
def test_adobe_id_tags_when_vendor_id_configured(self): """When vendor ID delegation is configured, adobe_id_tags() returns a list containing a single tag. The tag contains the information necessary to get an Adobe ID and a link to the local DRM Device Management Protocol endpoint. """ self.initialize_adobe(self._default_library) patron_identifier = "patron identifier" [element] = self.annotator.adobe_id_tags(patron_identifier) eq_('{http://librarysimplified.org/terms/drm}licensor', element.tag) key = '{http://librarysimplified.org/terms/drm}vendor' eq_(self.adobe_vendor_id.username, element.attrib[key]) [token, device_management_link] = element.getchildren() eq_('{http://librarysimplified.org/terms/drm}clientToken', token.tag) # token.text is a token which we can decode, since we know # the secret. token = token.text authdata = AuthdataUtility.from_config(self._default_library) decoded = authdata.decode_short_client_token(token) expected_url = ConfigurationSetting.for_library( Configuration.WEBSITE_URL, self._default_library).value eq_((expected_url, patron_identifier), decoded) eq_("link", device_management_link.tag) eq_("http://librarysimplified.org/terms/drm/rel/devices", device_management_link.attrib['rel']) expect_url = self.annotator.url_for( 'adobe_drm_devices', library_short_name=self._default_library.short_name, _external=True) eq_(expect_url, device_management_link.attrib['href']) # If we call adobe_id_tags again we'll get a distinct tag # object that renders to the same XML. [same_tag] = self.annotator.adobe_id_tags(patron_identifier) assert same_tag is not element eq_(etree.tostring(element), etree.tostring(same_tag))
def _process_registration_result(self, catalog, encryptor, desired_stage): """We just sent out a registration request and got an OPDS catalog in return. Process that catalog. """ # Since we generated a public key, the catalog should have provided # credentials for future authenticated communication, # e.g. through Short Client Tokens or authenticated API # requests. if not isinstance(catalog, dict): return INTEGRATION_ERROR.detailed( _("Remote service served %(representation)r, which I can't make sense of as an OPDS document.", representation=catalog)) metadata = catalog.get("metadata", {}) short_name = metadata.get("short_name") shared_secret = metadata.get("shared_secret") if short_name: setting = self.setting(ExternalIntegration.USERNAME) setting.value = short_name if shared_secret: shared_secret = self._decrypt_shared_secret( encryptor, shared_secret) if isinstance(shared_secret, ProblemDetail): return shared_secret setting = self.setting(ExternalIntegration.PASSWORD) setting.value = shared_secret # We have successfully completed the registration. self.status_field.value = self.SUCCESS_STATUS # We're done with the library's public key, so remove the # setting. ConfigurationSetting.for_library(Configuration.PUBLIC_KEY, self.library).value = None # Our opinion about the proper stage of this library was succesfully # communicated to the registry. self.stage_field.value = desired_stage return True
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 val(x): return ConfigurationSetting.for_library(x, library).value
def test_has_borrowing_privileges(self): """Test the methods that encapsulate the determination of whether or not a patron can borrow books. """ now = datetime.datetime.utcnow() one_day_ago = now - datetime.timedelta(days=1) patron = self._patron() # Most patrons have borrowing privileges. eq_(True, PatronUtility.has_borrowing_privileges(patron)) PatronUtility.assert_borrowing_privileges(patron) # If your card expires you lose borrowing privileges. patron.authorization_expires = one_day_ago eq_(False, PatronUtility.has_borrowing_privileges(patron)) assert_raises( AuthorizationExpired, PatronUtility.assert_borrowing_privileges, patron ) patron.authorization_expires = None # If you accrue excessive fines you lose borrowing privileges. setting = ConfigurationSetting.for_library( Configuration.MAX_OUTSTANDING_FINES, self._default_library ) setting.value = "$0.50" patron.fines = 1 eq_(False, PatronUtility.has_borrowing_privileges(patron)) assert_raises( OutstandingFines, PatronUtility.assert_borrowing_privileges, patron ) # Test the case where any amount of fines is too much. setting.value = "$0" eq_(False, PatronUtility.has_borrowing_privileges(patron)) assert_raises( OutstandingFines, PatronUtility.assert_borrowing_privileges, patron ) setting.value = "$100" eq_(True, PatronUtility.has_borrowing_privileges(patron)) patron.fines = 0 eq_(True, PatronUtility.has_borrowing_privileges(patron)) # Even if the circulation manager is not configured to know # what "excessive fines" are, the authentication mechanism # might know, and might store that information in the # patron's block_reason. patron.block_reason = PatronData.EXCESSIVE_FINES assert_raises( OutstandingFines, PatronUtility.assert_borrowing_privileges, patron ) # If your card is blocked for any reason you lose borrowing # privileges. patron.block_reason = "some reason" eq_(False, PatronUtility.has_borrowing_privileges(patron)) assert_raises( AuthorizationBlocked, PatronUtility.assert_borrowing_privileges, patron ) patron.block_reason = None eq_(True, PatronUtility.has_borrowing_privileges(patron))
for library in LIBRARIES: short_name = library.library_registry_short_name short_name = short_name or adobe_conf.get('library_short_name') if short_name: ConfigurationSetting.for_library_and_externalintegration( _db, EI.USERNAME, library, integration ).value = short_name shared_secret = library.library_registry_shared_secret shared_secret = shared_secret or adobe_conf.get('authdata_secret') ConfigurationSetting.for_library_and_externalintegration( _db, EI.PASSWORD, library, integration ).value = shared_secret library_url = adobe_conf.get('library_uri') ConfigurationSetting.for_library( Configuration.WEBSITE_URL, library).value = library_url integration.libraries.append(library) # Import Google OAuth configuration. google_oauth_conf = Configuration.integration('Google OAuth') if google_oauth_conf: integration = EI(protocol=EI.GOOGLE_OAUTH, goal=EI.ADMIN_AUTH_GOAL) _db.add(integration) integration.url = google_oauth_conf.get("web", {}).get("auth_uri") integration.username = google_oauth_conf.get("web", {}).get("client_id") integration.password = google_oauth_conf.get("web", {}).get("client_secret") auth_domain = Configuration.policy('admin_authentication_domain') if auth_domain:
def push(self, stage, url_for, catalog_url=None, do_get=HTTP.debuggable_get, do_post=HTTP.debuggable_post): """Attempt to register a library with a RemoteRegistry. NOTE: This method is designed to be used in a controller. Other callers may use this method, but they must be able to render a ProblemDetail when there's a failure. NOTE: The application server must be running when this method is called, because part of the OPDS Directory Registration Protocol is the remote server retrieving the library's Authentication For OPDS document. :param stage: Either TESTING_STAGE or PRODUCTION_STAGE :param url_for: Flask url_for() or equivalent, used to generate URLs for the application server. :param do_get: Mockable method to make a GET request. :param do_post: Mockable method to make a POST request. :return: A ProblemDetail if there was a problem; otherwise True. """ # Assume that the registration will fail. # # TODO: If a registration has previously succeeded, failure to # re-register probably means a maintenance of the status quo, # not a change of success to failure. But we don't have any way # of being sure. self.status_field.value = self.FAILURE_STATUS if stage not in self.VALID_REGISTRATION_STAGES: return INVALID_INPUT.detailed( _("%r is not a valid registration stage") % stage ) # Verify that a public/private key pair exists for this library. # This key pair is created during initialization of the # LibraryAuthenticator, so this should always be present. # # We can't just create the key pair here because the process # of pushing a registration involves the other site making a # request to the circulation manager. This means the key pair # needs to be committed to the database _before_ the push # attempt starts. key_pair = ConfigurationSetting.for_library( Configuration.KEY_PAIR, self.library).json_value if not key_pair: # TODO: We could create the key pair _here_. The database # session will be committed at the end of this request, # so the push attempt would succeed if repeated. return SHARED_SECRET_DECRYPTION_ERROR.detailed( _("Library %(library)s has no key pair set.", library=self.library.short_name) ) public_key, private_key = key_pair cipher = Configuration.cipher(private_key) # Before we can start the registration protocol, we must fetch # the remote catalog's URL and extract the link to the # registration resource that kicks off the protocol. catalog_url = catalog_url or self.integration.url response = do_get(catalog_url) if isinstance(response, ProblemDetail): return response result = self._extract_catalog_information(response) if isinstance(result, ProblemDetail): return result register_url, vendor_id = result # Store the vendor id as a ConfigurationSetting on the integration # -- it'll be the same value for all libraries. if vendor_id: ConfigurationSetting.for_externalintegration( AuthdataUtility.VENDOR_ID_KEY, self.integration ).value = vendor_id # Build the document we'll be sending to the registration URL. payload = self._create_registration_payload(url_for, stage) if isinstance(payload, ProblemDetail): return payload headers = self._create_registration_headers() if isinstance(headers, ProblemDetail): return headers # Send the document. response = self._send_registration_request( register_url, headers, payload, do_post ) if isinstance(response, ProblemDetail): return response catalog = json.loads(response.content) # Process the result. return self._process_registration_result(catalog, cipher, stage)
def current_value(self, setting, library): return ConfigurationSetting.for_library(setting['key'], library).value
def test_libraries_post_create(self): class TestFileUpload(StringIO): headers = { "Content-Type": "image/png" } image_data = '\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x01\x03\x00\x00\x00%\xdbV\xca\x00\x00\x00\x06PLTE\xffM\x00\x01\x01\x01\x8e\x1e\xe5\x1b\x00\x00\x00\x01tRNS\xcc\xd24V\xfd\x00\x00\x00\nIDATx\x9cc`\x00\x00\x00\x02\x00\x01H\xaf\xa4q\x00\x00\x00\x00IEND\xaeB`\x82' original_validate = GeographicValidator().validate_geographic_areas class MockValidator(GeographicValidator): def __init__(self): self.was_called = False def validate_geographic_areas(self, values, db): self.was_called = True return original_validate(values, db) with self.request_context_with_admin("/", method="POST"): flask.request.form = MultiDict([ ("name", "The New York Public Library"), ("short_name", "nypl"), ("library_description", "Short description of library"), (Configuration.WEBSITE_URL, "https://library.library/"), (Configuration.TINY_COLLECTION_LANGUAGES, ['ger']), (Configuration.LIBRARY_SERVICE_AREA, ['06759', 'everywhere', 'MD', 'Boston, MA']), (Configuration.LIBRARY_FOCUS_AREA, ['Manitoba', 'Broward County, FL', 'QC']), (Configuration.DEFAULT_NOTIFICATION_EMAIL_ADDRESS, "*****@*****.**"), (Configuration.HELP_EMAIL, "*****@*****.**"), (Configuration.FEATURED_LANE_SIZE, "5"), (Configuration.DEFAULT_FACET_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME, FacetConstants.ORDER_RANDOM), (Configuration.ENABLED_FACETS_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME + "_" + FacetConstants.ORDER_TITLE, ''), (Configuration.ENABLED_FACETS_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME + "_" + FacetConstants.ORDER_RANDOM, ''), ]) flask.request.files = MultiDict([ (Configuration.LOGO, TestFileUpload(image_data)), ]) validator = MockValidator() response = self.manager.admin_library_settings_controller.process_post(validator) eq_(response.status_code, 201) library = get_one(self._db, Library, short_name="nypl") eq_(library.uuid, response.response[0]) eq_(library.name, "The New York Public Library") eq_(library.short_name, "nypl") eq_("5", ConfigurationSetting.for_library(Configuration.FEATURED_LANE_SIZE, library).value) eq_(FacetConstants.ORDER_RANDOM, ConfigurationSetting.for_library( Configuration.DEFAULT_FACET_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME, library).value) eq_(json.dumps([FacetConstants.ORDER_TITLE, FacetConstants.ORDER_RANDOM]), ConfigurationSetting.for_library( Configuration.ENABLED_FACETS_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME, library).value) eq_("data:image/png;base64,%s" % base64.b64encode(image_data), ConfigurationSetting.for_library(Configuration.LOGO, library).value) eq_(validator.was_called, True) eq_('{"CA": [], "US": ["06759", "everywhere", "MD", "Boston, MA"]}', ConfigurationSetting.for_library(Configuration.LIBRARY_SERVICE_AREA, library).value) eq_('{"CA": ["Manitoba", "Quebec"], "US": ["Broward County, FL"]}', ConfigurationSetting.for_library(Configuration.LIBRARY_FOCUS_AREA, library).value) # When the library was created, default lanes were also created # according to its language setup. This library has one tiny # collection (not a good choice for a real library), so only # two lanes were created: "Other Languages" and then "German" # underneath it. [german, other_languages] = sorted( library.lanes, key=lambda x: x.display_name ) eq_(None, other_languages.parent) eq_(['ger'], other_languages.languages) eq_(other_languages, german.parent) eq_(['ger'], german.languages)
def test_libraries_post_edit(self): # A library already exists. library = self._library("New York Public Library", "nypl") ConfigurationSetting.for_library(Configuration.FEATURED_LANE_SIZE, library).value = 5 ConfigurationSetting.for_library( Configuration.DEFAULT_FACET_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME, library ).value = FacetConstants.ORDER_RANDOM ConfigurationSetting.for_library( Configuration.ENABLED_FACETS_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME, library ).value = json.dumps([FacetConstants.ORDER_TITLE, FacetConstants.ORDER_RANDOM]) ConfigurationSetting.for_library( Configuration.LOGO, library ).value = "A tiny image" with self.request_context_with_admin("/", method="POST"): flask.request.form = MultiDict([ ("uuid", library.uuid), ("name", "The New York Public Library"), ("short_name", "nypl"), (Configuration.FEATURED_LANE_SIZE, "20"), (Configuration.MINIMUM_FEATURED_QUALITY, "0.9"), (Configuration.WEBSITE_URL, "https://library.library/"), (Configuration.DEFAULT_NOTIFICATION_EMAIL_ADDRESS, "*****@*****.**"), (Configuration.HELP_EMAIL, "*****@*****.**"), (Configuration.DEFAULT_FACET_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME, FacetConstants.ORDER_AUTHOR), (Configuration.ENABLED_FACETS_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME + "_" + FacetConstants.ORDER_AUTHOR, ''), (Configuration.ENABLED_FACETS_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME + "_" + FacetConstants.ORDER_RANDOM, ''), ]) flask.request.files = MultiDict([]) response = self.manager.admin_library_settings_controller.process_post() eq_(response.status_code, 200) library = get_one(self._db, Library, uuid=library.uuid) eq_(library.uuid, response.response[0]) eq_(library.name, "The New York Public Library") eq_(library.short_name, "nypl") # The library-wide settings were updated. def val(x): return ConfigurationSetting.for_library(x, library).value eq_("https://library.library/", val(Configuration.WEBSITE_URL)) eq_("*****@*****.**", val(Configuration.DEFAULT_NOTIFICATION_EMAIL_ADDRESS)) eq_("*****@*****.**", val(Configuration.HELP_EMAIL)) eq_("20", val(Configuration.FEATURED_LANE_SIZE)) eq_("0.9", val(Configuration.MINIMUM_FEATURED_QUALITY)) eq_(FacetConstants.ORDER_AUTHOR, val(Configuration.DEFAULT_FACET_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME) ) eq_(json.dumps([FacetConstants.ORDER_AUTHOR, FacetConstants.ORDER_RANDOM]), val(Configuration.ENABLED_FACETS_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME) ) # The library-wide logo was not updated and has been left alone. eq_("A tiny image", ConfigurationSetting.for_library(Configuration.LOGO, library).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 _create_registration_payload(self, url_for, stage): self.payload_ingredients = (url_for, stage) return dict(payload="this is it") def _create_registration_headers(self): self._create_registration_headers_called = True return dict(Header="Value") def _send_registration_request( self, register_url, headers, payload, do_post ): self._send_registration_request_called_with = ( register_url, headers, 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" # If there is no preexisting key pair set up for the library, # registration fails. (This normally won't happen because the # key pair is set up when the LibraryAuthenticator is # initialized. library = self._default_library registration = Mock(self.registry, library) stage = Registration.TESTING_STAGE url_for = object() catalog_url = "http://catalog/" do_post = object() def push(): return registration.push( stage, url_for, catalog_url, registration.mock_do_get, do_post ) result = push() expect = "Library %s has no key pair set." % library.short_name eq_(expect, result.detail) # When a key pair is present, registration is kicked off, and # in this case it succeeds. key_pair_setting = ConfigurationSetting.for_library( Configuration.KEY_PAIR, library ) public_key, private_key = Configuration.key_pair(key_pair_setting) result = registration.push( stage, url_for, catalog_url, registration.mock_do_get, do_post ) 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 ) # _create_registration_payload was called to create the body # of the registration request. eq_((url_for, stage), registration.payload_ingredients) # _create_registration_headers was called to create the headers # sent along with the request. eq_(True, registration._create_registration_headers_called) # 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", {"Header": "Value"}, 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 # a cipher created from the private key. (That cipher would be used # to decrypt anything the foreign site signed using this site's # public key.) results = registration._process_registration_result_called_with message, cipher, actual_stage = results eq_("you did it!", message) eq_(cipher._key.exportKey(), private_key) eq_(actual_stage, stage) # 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 ) 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. # The push() function will no longer push anything, so rename it. cause_problem = push 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 extract catalog information" ) registration._extract_catalog_information = fail problem = cause_problem() eq_("could not extract catalog information", problem.detail)