Ejemplo n.º 1
0
    def test_duplicate(self):
        """You can't have two ConfigurationSettings for the same key,
        library, and external integration.

        (test_relationships shows that you can have two settings for the same
        key as long as library or integration is different.)
        """
        key = self._str
        integration, ignore = create(self._db,
                                     ExternalIntegration,
                                     goal=self._str,
                                     protocol=self._str)
        library = self._default_library
        setting = ConfigurationSetting.for_library_and_externalintegration(
            self._db, key, library, integration)
        setting2 = ConfigurationSetting.for_library_and_externalintegration(
            self._db, key, library, integration)
        assert setting.id == setting2.id
        pytest.raises(
            IntegrityError,
            create,
            self._db,
            ConfigurationSetting,
            key=key,
            library=library,
            external_integration=integration,
        )
Ejemplo n.º 2
0
    def test_disassociate_library(self):
        # Here's a Collection.
        collection = self._default_collection

        # It's associated with two different libraries.
        assert self._default_library in collection.libraries
        other_library = self._library()
        collection.libraries.append(other_library)

        # It has an ExternalIntegration, which has some settings.
        integration = collection.external_integration
        setting1 = integration.set_setting("integration setting", "value2")
        setting2 = ConfigurationSetting.for_library_and_externalintegration(
            self._db,
            "default_library+integration setting",
            self._default_library,
            integration,
        )
        setting2.value = "value2"
        setting3 = ConfigurationSetting.for_library_and_externalintegration(
            self._db,
            "other_library+integration setting",
            other_library,
            integration,
        )
        setting3.value = "value3"

        # Now, disassociate one of the libraries from the collection.
        collection.disassociate_library(self._default_library)

        # It's gone.
        assert self._default_library not in collection.libraries
        assert collection not in self._default_library.collections

        # Furthermore, ConfigurationSettings that configure that
        # Library's relationship to this Collection's
        # ExternalIntegration have been deleted.
        all_settings = self._db.query(ConfigurationSetting).all()
        assert setting2 not in all_settings

        # The other library is unaffected.
        assert other_library in collection.libraries
        assert collection in other_library.collections
        assert setting3 in all_settings

        # As is the library-independent configuration of this Collection's
        # ExternalIntegration.
        assert setting1 in all_settings

        # Calling disassociate_library again is a no-op.
        collection.disassociate_library(self._default_library)
        assert self._default_library not in collection.libraries

        # If you somehow manage to call disassociate_library on a Collection
        # that has no associated ExternalIntegration, an exception is raised.
        collection.external_integration_id = None
        with pytest.raises(ValueError) as excinfo:
            collection.disassociate_library(other_library)
        assert "No known external integration for collection" in str(excinfo.value)
Ejemplo n.º 3
0
 def test_duplicate_sitewide_setting(self):
     # You can't create two sitewide settings with the same key.
     c1 = ConfigurationSetting(key="key", value="value1")
     self._db.add(c1)
     self._db.flush()
     c2 = ConfigurationSetting(key="key", value="value2")
     self._db.add(c2)
     pytest.raises(IntegrityError, self._db.flush)
Ejemplo n.º 4
0
 def test_duplicate_library_setting(self):
     # A library can't have two settings with the same key.
     c1 = ConfigurationSetting(key="key",
                               value="value1",
                               library=self._default_library)
     self._db.add(c1)
     self._db.flush()
     c2 = ConfigurationSetting(key="key",
                               value="value2",
                               library=self._default_library)
     self._db.add(c2)
     pytest.raises(IntegrityError, self._db.flush)
Ejemplo n.º 5
0
 def test_duplicate_integration_setting(self):
     # An external integration can't have two settings with the
     # same key.
     integration = self._external_integration(self._str)
     c1 = ConfigurationSetting(key="key",
                               value="value1",
                               external_integration=integration)
     self._db.add(c1)
     self._db.flush()
     c2 = ConfigurationSetting(key="key",
                               value="value1",
                               external_integration=integration)
     self._db.add(c2)
     pytest.raises(IntegrityError, self._db.flush)
Ejemplo n.º 6
0
    def test_explain(self):
        """Test that Library.explain gives all relevant information
        about a Library.
        """
        library = self._default_library
        library.uuid = "uuid"
        library.name = "The Library"
        library.short_name = "Short"
        library.library_registry_short_name = "SHORT"
        library.library_registry_shared_secret = "secret"

        integration = self._external_integration("protocol", "goal")
        integration.url = "http://url/"
        integration.username = "******"
        integration.password = "******"
        integration.setting("somesetting").value = "somevalue"

        # Different libraries specialize this integration differently.
        ConfigurationSetting.for_library_and_externalintegration(
            self._db, "library-specific", library,
            integration).value = "value for library1"

        library2 = self._library()
        ConfigurationSetting.for_library_and_externalintegration(
            self._db, "library-specific", library2,
            integration).value = "value for library2"

        library.integrations.append(integration)

        expect = ("""Library UUID: "uuid"
Name: "The Library"
Short name: "Short"
Short name (for library registry): "SHORT"

External integrations:
----------------------
ID: %s
Protocol/Goal: protocol/goal
library-specific='value for library1' (applies only to The Library)
somesetting='somevalue'
url='http://url/'
username='******'
""" % integration.id)
        actual = library.explain()
        assert expect == "\n".join(actual)

        with_secrets = library.explain(True)
        assert 'Shared secret (for library registry): "secret"' in with_secrets
        assert "password='******'" in with_secrets
Ejemplo n.º 7
0
    def test_is_secret(self):
        """Some configuration settings are considered secrets,
        and some are not.
        """
        m = ConfigurationSetting._is_secret
        assert True == m("secret")
        assert True == m("password")
        assert True == m("its_a_secret_to_everybody")
        assert True == m("the_password")
        assert True == m("password_for_the_account")
        assert False == m("public_information")

        assert True == ConfigurationSetting.sitewide(self._db,
                                                     "secret_key").is_secret
        assert False == ConfigurationSetting.sitewide(self._db,
                                                      "public_key").is_secret
Ejemplo n.º 8
0
    def test_explain(self):
        integration = self._external_integration("protocol", "goal")
        integration.name = "The Integration"
        integration.url = "http://url/"
        integration.username = "******"
        integration.password = "******"
        integration.setting("somesetting").value = "somevalue"

        # Two different libraries have slightly different
        # configurations for this integration.
        self._default_library.name = "First Library"
        self._default_library.integrations.append(integration)
        ConfigurationSetting.for_library_and_externalintegration(
            self._db, "library-specific", self._default_library,
            integration).value = "value1"

        library2 = self._library()
        library2.name = "Second Library"
        library2.integrations.append(integration)
        ConfigurationSetting.for_library_and_externalintegration(
            self._db, "library-specific", library2,
            integration).value = "value2"

        # If we decline to pass in a library, we get information about how
        # each library in the system configures this integration.

        expect = ("""ID: %s
Name: The Integration
Protocol/Goal: protocol/goal
library-specific='value1' (applies only to First Library)
library-specific='value2' (applies only to Second Library)
somesetting='somevalue'
url='http://url/'
username='******'""" % integration.id)
        actual = integration.explain()
        assert expect == "\n".join(actual)

        # If we pass in a library, we only get information about
        # how that specific library configures the integration.
        for_library_2 = "\n".join(integration.explain(library=library2))
        assert "applies only to First Library" not in for_library_2
        assert "applies only to Second Library" in for_library_2

        # If we pass in True for include_secrets, we see the passwords.
        with_secrets = integration.explain(include_secrets=True)
        assert "password='******'" in with_secrets
Ejemplo n.º 9
0
    def test_json_value(self):
        jsondata = ConfigurationSetting.sitewide(self._db, "json")
        assert None == jsondata.int_value

        jsondata.value = "[1,2]"
        assert [1, 2] == jsondata.json_value

        jsondata.value = "tra la la"
        pytest.raises(ValueError, lambda: jsondata.json_value)
Ejemplo n.º 10
0
    def test_float_value(self):
        number = ConfigurationSetting.sitewide(self._db, "number")
        assert None == number.int_value

        number.value = "1234.5"
        assert 1234.5 == number.float_value

        number.value = "tra la la"
        pytest.raises(ValueError, lambda: number.float_value)
Ejemplo n.º 11
0
    def test_stored_bytes_value(self):
        bytes_setting = ConfigurationSetting.sitewide(self._db,
                                                      "bytes_setting")
        assert bytes_setting.value is None

        bytes_setting.value = "1234 ☃".encode("utf8")
        assert "1234 ☃" == bytes_setting.value

        with pytest.raises(UnicodeDecodeError):
            bytes_setting.value = b"\x80"
Ejemplo n.º 12
0
 def test_duplicate_library_integration_setting(self):
     # A library can't configure an external integration two
     # different ways for the same key.
     integration = self._external_integration(self._str)
     c1 = ConfigurationSetting(
         key="key",
         value="value1",
         library=self._default_library,
         external_integration=integration,
     )
     self._db.add(c1)
     self._db.flush()
     c2 = ConfigurationSetting(
         key="key",
         value="value1",
         library=self._default_library,
         external_integration=integration,
     )
     self._db.add(c2)
     pytest.raises(IntegrityError, self._db.flush)
Ejemplo n.º 13
0
    def test_explain(self):
        """Test that ConfigurationSetting.explain gives information
        about all site-wide configuration settings.
        """
        ConfigurationSetting.sitewide(self._db, "a_secret").value = "1"
        ConfigurationSetting.sitewide(self._db,
                                      "nonsecret_setting").value = "2"

        integration = self._external_integration("a protocol", "a goal")

        actual = ConfigurationSetting.explain(self._db, include_secrets=True)
        expect = """Site-wide configuration settings:
---------------------------------
a_secret='1'
nonsecret_setting='2'"""
        assert expect == "\n".join(actual)

        without_secrets = "\n".join(
            ConfigurationSetting.explain(self._db, include_secrets=False))
        assert "a_secret" not in without_secrets
        assert "nonsecret_setting" in without_secrets
Ejemplo n.º 14
0
    def test_no_orphan_delete_cascade(self):
        # Disconnecting a ConfigurationSetting from a Library or
        # ExternalIntegration doesn't delete it, because it's fine for
        # a ConfigurationSetting to have no associated Library or
        # ExternalIntegration.

        library = self._default_library
        for_library = ConfigurationSetting.for_library(self._str, library)

        integration = self._external_integration(self._str)
        for_integration = ConfigurationSetting.for_externalintegration(
            self._str, integration)

        # Remove library and external_integration.
        for_library.library = None
        for_integration.external_integration = None
        self._db.commit()

        # That was a weird thing to do, but the ConfigurationSettings
        # are still in the database.
        for cs in for_library, for_integration:
            assert cs == get_one(self._db, ConfigurationSetting, id=cs.id)
Ejemplo n.º 15
0
 def test_excluded_audio_data_sources(self):
     # Get a handle on the underlying ConfigurationSetting
     setting = ConfigurationSetting.sitewide(
         self._db, Configuration.EXCLUDED_AUDIO_DATA_SOURCES)
     m = ConfigurationSetting.excluded_audio_data_sources
     # When no explicit value is set for the ConfigurationSetting,
     # the return value of the method is AUDIO_EXCLUSIONS -- whatever
     # the default is for the current version of the circulation manager.
     assert None == setting.value
     assert ConfigurationSetting.EXCLUDED_AUDIO_DATA_SOURCES_DEFAULT == m(
         self._db)
     # When an explicit value for the ConfigurationSetting, is set, that
     # value is interpreted as JSON and returned.
     setting.value = "[]"
     assert [] == m(self._db)
Ejemplo n.º 16
0
    def test_delete(self):
        """Verify that Collection.delete will only operate on collections
        flagged for deletion, and that deletion cascades to all
        relevant related database objects.
        """

        # This collection is doomed.
        collection = self._default_collection

        # It's associated with a library.
        assert self._default_library in collection.libraries

        # It has an ExternalIntegration, which has some settings.
        integration = collection.external_integration
        setting1 = integration.set_setting("integration setting", "value2")
        setting2 = ConfigurationSetting.for_library_and_externalintegration(
            self._db,
            "library+integration setting",
            self._default_library,
            integration,
        )
        setting2.value = "value2"

        # Also it has links to another independent ExternalIntegration (S3 storage in this case).
        s3_storage = self._external_integration(
            ExternalIntegration.S3,
            ExternalIntegration.STORAGE_GOAL,
            libraries=[self._default_library],
        )
        link1 = self._external_integration_link(
            integration,
            self._default_library,
            s3_storage,
            ExternalIntegrationLink.PROTECTED_ACCESS_BOOKS,
        )
        link2 = self._external_integration_link(
            integration,
            self._default_library,
            s3_storage,
            ExternalIntegrationLink.COVERS,
        )

        integration.links.append(link1)
        integration.links.append(link2)

        # It's got a Work that has a LicensePool, which has a License,
        # which has a loan.
        work = self._work(with_license_pool=True)
        [pool] = work.license_pools
        license = self._license(pool)
        patron = self._patron()
        loan, is_new = license.loan_to(patron)

        # The LicensePool also has a hold.
        patron2 = self._patron()
        hold, is_new = pool.on_hold_to(patron2)

        # And a Complaint.
        complaint, is_new = Complaint.register(
            pool, list(Complaint.VALID_TYPES)[0], source=None, detail=None
        )

        # And a CirculationEvent.
        CirculationEvent.log(
            self._db, pool, CirculationEvent.DISTRIBUTOR_TITLE_ADD, 0, 1
        )

        # There's a second Work which has _two_ LicensePools from two
        # different Collections -- the one we're about to delete and
        # another Collection.
        work2 = self._work(with_license_pool=True)
        collection2 = self._collection()
        pool2 = self._licensepool(None, collection=collection2)
        work2.license_pools.append(pool2)

        # Finally, here's a mock ExternalSearchIndex so we can track when
        # Works are removed from the search index.
        class MockExternalSearchIndex(object):
            removed = []

            def remove_work(self, work):
                self.removed.append(work)

        index = MockExternalSearchIndex()

        # delete() will not work on a collection that's not marked for
        # deletion.
        with pytest.raises(Exception) as excinfo:
            collection.delete()
        assert (
            "Cannot delete %s: it is not marked for deletion." % collection.name
            in str(excinfo.value)
        )

        # Delete the collection.
        collection.marked_for_deletion = True
        collection.delete(search_index=index)

        # It's gone.
        assert collection not in self._db.query(Collection).all()

        # The default library now has no collections.
        assert [] == self._default_library.collections

        # The deletion of the Collection's sole LicensePool has
        # cascaded to Loan, Hold, Complaint, License, and
        # CirculationEvent.
        assert [] == patron.loans
        assert [] == patron2.holds
        for cls in (Loan, Hold, Complaint, License, CirculationEvent):
            assert [] == self._db.query(cls).all()

        # n.b. Annotations are associated with Identifier, not
        # LicensePool, so they can and should survive the deletion of
        # the Collection in which they were originally created.

        # The first Work has been deleted, since it lost all of its
        # LicensePools.
        assert [work2] == self._db.query(Work).all()

        # The second Work is still around, and it still has the other
        # LicensePool.
        assert [pool2] == work2.license_pools

        # Our search index was told to remove the first work (which no longer
        # has any LicensePools), but not the second.
        assert [work] == index.removed

        # The collection ExternalIntegration, its settings, and links to other integrations have been deleted.
        # The storage ExternalIntegration remains.
        external_integrations = self._db.query(ExternalIntegration).all()
        assert integration not in external_integrations
        assert s3_storage in external_integrations

        settings = self._db.query(ConfigurationSetting).all()
        for setting in (setting1, setting2):
            assert setting not in settings

        links = self._db.query(ExternalIntegrationLink).all()
        for link in (link1, link2):
            assert link not in links

        # If no search_index is passed into delete() (the default behavior),
        # we try to instantiate the normal ExternalSearchIndex object. Since
        # no search index is configured, this will raise an exception -- but
        # delete() will catch the exception and carry out the delete,
        # without trying to delete any Works from the search index.
        collection2.marked_for_deletion = True
        collection2.delete()

        # We've now deleted every LicensePool created for this test.
        assert 0 == self._db.query(LicensePool).count()
        assert [] == work2.license_pools
Ejemplo n.º 17
0
    def test_restrict_to_ready_deliverable_works(self):
        """A partial test of restrict_to_ready_deliverable_works.

        This test covers the following cases:
        1. The bit that excludes audiobooks from certain data sources.
        2. Makes sure that self-hosted books and books with unlimited access are not get filtered out that come.

        The other cases are tested indirectly in lane.py, but could use a more explicit test here.
        """
        # Create two audiobooks and one ebook.
        overdrive_audiobook = self._work(
            data_source_name=DataSource.OVERDRIVE,
            with_license_pool=True,
            title="Overdrive Audiobook",
        )
        overdrive_audiobook.presentation_edition.medium = Edition.AUDIO_MEDIUM
        overdrive_ebook = self._work(
            data_source_name=DataSource.OVERDRIVE,
            with_license_pool=True,
            title="Overdrive Ebook",
        )
        feedbooks_audiobook = self._work(
            data_source_name=DataSource.FEEDBOOKS,
            with_license_pool=True,
            title="Feedbooks Audiobook",
        )
        feedbooks_audiobook.presentation_edition.medium = Edition.AUDIO_MEDIUM

        DataSource.lookup(self._db, DataSource.LCP, autocreate=True)
        self_hosted_lcp_book = self._work(
            data_source_name=DataSource.LCP,
            title="Self-hosted LCP book",
            with_license_pool=True,
            self_hosted=True,
        )
        unlimited_access_book = self._work(
            data_source_name=DataSource.LCP,
            title="Self-hosted LCP book",
            with_license_pool=True,
            unlimited_access=True,
        )

        def expect(qu, works):
            """Modify the query `qu` by calling
            restrict_to_ready_deliverable_works(), then verify that
            the query returns the works expected by `works`.
            """
            restricted_query = Collection.restrict_to_ready_deliverable_works(qu)
            expect_ids = [x.id for x in works]
            actual_ids = [x.id for x in restricted_query]
            assert set(expect_ids) == set(actual_ids)

        # Here's the setting which controls which data sources should
        # have their audiobooks excluded.
        setting = ConfigurationSetting.sitewide(
            self._db, Configuration.EXCLUDED_AUDIO_DATA_SOURCES
        )
        qu = (
            self._db.query(Work)
            .join(Work.license_pools)
            .join(Work.presentation_edition)
        )
        # When its value is set to the empty list, every work shows
        # up.
        setting.value = json.dumps([])
        expect(
            qu,
            [
                overdrive_ebook,
                overdrive_audiobook,
                feedbooks_audiobook,
                self_hosted_lcp_book,
                unlimited_access_book,
            ],
        )
        # Putting a data source in the list excludes its audiobooks, but
        # not its ebooks.
        setting.value = json.dumps([DataSource.OVERDRIVE])
        expect(
            qu,
            [
                overdrive_ebook,
                feedbooks_audiobook,
                self_hosted_lcp_book,
                unlimited_access_book,
            ],
        )
        setting.value = json.dumps([DataSource.OVERDRIVE, DataSource.FEEDBOOKS])
        expect(qu, [overdrive_ebook, self_hosted_lcp_book, unlimited_access_book])
Ejemplo n.º 18
0
 def test_setter(self, _, set_to, expect):
     # Values are converted into Unicode strings on the way in to
     # the 'value' setter.
     setting = ConfigurationSetting.sitewide(self._db, "setting")
     setting.value = set_to
     assert setting.value == expect

def fix(value):
    result = format(value)
    formatted_info = None
    if result:
        formatted_info = json.dumps({"US": result})
    return formatted_info


expect = json.dumps({"US": ["Waterford, CT"]})
assert fix("Waterford, CT") == expect
assert fix(json.dumps("Waterford, CT")) == expect
assert fix(json.dumps(["Waterford, CT"])) == expect
# If the value is already in the correct format, fix() shouldn't return anything;
# there's no need to update the setting.
assert fix(expect) == None
for setting in area_settings:
    library = _db.query(Library).filter(
        Library.id == setting.library_id).first()
    formatted_info = fix(setting._value)
    if formatted_info:
        print "Changing %r to %s" % (setting._value, formatted_info)
        ConfigurationSetting.for_library_and_externalintegration(
            _db, setting.key, library, None).value = formatted_info
    else:
        print "Leaving %s alone" % (setting._value)

_db.commit()
_db.close()
Ejemplo n.º 20
0
    def test_value_inheritance(self):

        key = "SomeKey"

        # Here's a sitewide configuration setting.
        sitewide_conf = ConfigurationSetting.sitewide(self._db, key)

        # Its value is not set.
        assert None == sitewide_conf.value

        # Set it.
        sitewide_conf.value = "Sitewide value"
        assert "Sitewide value" == sitewide_conf.value

        # Here's an integration, let's say the SIP2 authentication mechanism
        sip, ignore = create(
            self._db,
            ExternalIntegration,
            goal=ExternalIntegration.PATRON_AUTH_GOAL,
            protocol="SIP2",
        )

        # It happens to a ConfigurationSetting for the same key used
        # in the sitewide configuration.
        sip_conf = ConfigurationSetting.for_externalintegration(key, sip)

        # But because the meaning of a configuration key differ so
        # widely across integrations, the SIP2 integration does not
        # inherit the sitewide value for the key.
        assert None == sip_conf.value
        sip_conf.value = "SIP2 value"

        # Here's a library which has a ConfigurationSetting for the same
        # key used in the sitewide configuration.
        library = self._default_library
        library_conf = ConfigurationSetting.for_library(key, library)

        # Since all libraries use a given ConfigurationSetting to mean
        # the same thing, a library _does_ inherit the sitewide value
        # for a configuration setting.
        assert "Sitewide value" == library_conf.value

        # Change the site-wide configuration, and the default also changes.
        sitewide_conf.value = "New site-wide value"
        assert "New site-wide value" == library_conf.value

        # The per-library value takes precedence over the site-wide
        # value.
        library_conf.value = "Per-library value"
        assert "Per-library value" == library_conf.value

        # Now let's consider a setting like the patron identifier
        # prefix.  This is set on the combination of a library and a
        # SIP2 integration.
        key = "patron_identifier_prefix"
        library_patron_prefix_conf = (
            ConfigurationSetting.for_library_and_externalintegration(
                self._db, key, library, sip))
        assert None == library_patron_prefix_conf.value

        # If the SIP2 integration has a value set for this
        # ConfigurationSetting, that value is inherited for every
        # individual library that uses the integration.
        generic_patron_prefix_conf = ConfigurationSetting.for_externalintegration(
            key, sip)
        assert None == generic_patron_prefix_conf.value
        generic_patron_prefix_conf.value = "Integration-specific value"
        assert "Integration-specific value" == library_patron_prefix_conf.value

        # Change the value on the integration, and the default changes
        # for each individual library.
        generic_patron_prefix_conf.value = "New integration-specific value"
        assert "New integration-specific value" == library_patron_prefix_conf.value

        # The library+integration setting takes precedence over the
        # integration setting.
        library_patron_prefix_conf.value = "Library-specific value"
        assert "Library-specific value" == library_patron_prefix_conf.value