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))
Beispiel #7
0
    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
Beispiel #11
0
 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)
Beispiel #12
0
    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))
Beispiel #18
0
    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'}]})
Beispiel #20
0
    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
Beispiel #21
0
 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)
Beispiel #22
0
    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)
Beispiel #24
0
    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)
Beispiel #25
0
    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
Beispiel #28
0
 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)
Beispiel #29
0
 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)
Beispiel #30
0
 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'])
Beispiel #31
0
    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
Beispiel #32
0
    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)
Beispiel #35
0
    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
Beispiel #36
0
    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
Beispiel #39
0
    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
Beispiel #40
0
    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))
Beispiel #41
0
    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))
Beispiel #42
0
    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:
Beispiel #47
0
    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)