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, )
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)
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)
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)
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)
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
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
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
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)
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)
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"
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)
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
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)
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)
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
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])
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()
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