def setup(self): super(TestCatalogController, self).setup() self.controller = CatalogController(self._db) # The collection as it exists on the circulation manager. remote_collection = self._collection(username='******', external_account_id=self._url) # The collection as it is recorded / catalogued here. self.collection = self._collection( name=remote_collection.metadata_identifier, protocol=remote_collection.protocol) self.work1 = self._work(with_license_pool=True, with_open_access_download=True) self.work2 = self._work(with_license_pool=True, with_open_access_download=True)
def register(): return CatalogController(app._db).register()
def remove(collection_metadata_identifier): return CatalogController(app._db).remove_items( collection_details=collection_metadata_identifier)
def updates(collection_metadata_identifier): return CatalogController(app._db).updates_feed( collection_details=collection_metadata_identifier)
def metadata_needed_for(collection_metadata_identifier): return CatalogController(app._db).metadata_needed_for( collection_details=collection_metadata_identifier)
def add_with_metadata(collection_metadata_identifier): return CatalogController(app._db).add_with_metadata( collection_details=collection_metadata_identifier)
def update_url(): return CatalogController(Conf.db).update_client_url()
def add(collection_metadata_identifier): return CatalogController(Conf.db).add_items( collection_details=collection_metadata_identifier )
class TestCatalogController(ControllerTest): XML_PARSE = OPDSXMLParser()._xpath def setup(self): super(TestCatalogController, self).setup() self.controller = CatalogController(self._db) # The collection as it exists on the circulation manager. remote_collection = self._collection(username='******', external_account_id=self._url) # The collection as it is recorded / catalogued here. self.collection = self._collection( name=remote_collection.metadata_identifier, protocol=remote_collection.protocol) self.work1 = self._work(with_license_pool=True, with_open_access_download=True) self.work2 = self._work(with_license_pool=True, with_open_access_download=True) def xml_value(self, message, tag): return self.XML_PARSE(message, tag)[0].text def test_updates_feed(self): identifier = self.work1.license_pools[0].identifier self.collection.catalog_identifier(self._db, identifier) with self.app.test_request_context('/', headers=self.valid_auth): response = self.controller.updates_feed(self.collection.name) # The catalog's updates feed is returned. eq_(HTTP_OK, response.status_code) feed = feedparser.parse(response.get_data()) eq_( feed.feed.title, u"%s Collection Updates for %s" % (self.collection.protocol, self.client.url)) # The feed has the catalog's catalog. eq_(1, len(feed['entries'])) [entry] = feed['entries'] eq_(self.work1.title, entry['title']) eq_(identifier.urn, entry['id']) # A time can be passed. time = datetime.utcnow() timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ") for record in self.work1.coverage_records: # Set back the clock on all of work1's time records record.timestamp = time - timedelta(days=1) with self.app.test_request_context('/?last_update_time=%s' % timestamp, headers=self.valid_auth): response = self.controller.updates_feed(self.collection.name) eq_(HTTP_OK, response.status_code) feed = feedparser.parse(response.get_data()) eq_( feed.feed.title, u"%s Collection Updates for %s" % (self.collection.protocol, self.client.url)) # The timestamp is included in the url. linkified_timestamp = time.strftime("%Y-%m-%d+%H:%M:%S").replace( ":", "%3A") assert feed['feed']['id'].endswith(linkified_timestamp) # And only works updated since the timestamp are returned. eq_(0, len(feed['entries'])) # Works updated since the timestamp are returned self.work1.coverage_records[0].timestamp = datetime.utcnow() with self.app.test_request_context('/?last_update_time=%s' % timestamp, headers=self.valid_auth): response = self.controller.updates_feed(self.collection.name) feed = feedparser.parse(response.get_data()) eq_(1, len(feed['entries'])) [entry] = feed['entries'] eq_(self.work1.title, entry['title']) eq_(identifier.urn, entry['id']) def test_updates_feed_is_paginated(self): for work in [self.work1, self.work2]: self.collection.catalog_identifier( self._db, work.license_pools[0].identifier) with self.app.test_request_context('/?size=1', headers=self.valid_auth): response = self.controller.updates_feed(self.collection.name) links = feedparser.parse(response.get_data())['feed']['links'] assert any([link['rel'] == 'next' for link in links]) assert not any([link['rel'] == 'previous' for link in links]) assert not any([link['rel'] == 'first' for l in links]) with self.app.test_request_context('/?size=1&after=1', headers=self.valid_auth): response = self.controller.updates_feed(self.collection.name) links = feedparser.parse(response.get_data())['feed']['links'] assert any([link['rel'] == 'previous' for link in links]) assert any([link['rel'] == 'first' for link in links]) assert not any([link['rel'] == 'next' for link in links]) def test_add_items(self): invalid_urn = "FAKE AS I WANNA BE" catalogued_id = self._identifier() uncatalogued_id = self._identifier() self.collection.catalog_identifier(self._db, catalogued_id) parser = OPDSXMLParser() message_path = '/atom:feed/simplified:message' with self.app.test_request_context( '/?urn=%s&urn=%s&urn=%s' % (catalogued_id.urn, uncatalogued_id.urn, invalid_urn), headers=self.valid_auth): response = self.controller.add_items(self.collection.name) # None of the identifiers raise or return an error. eq_(HTTP_OK, response.status_code) # It sends three messages. root = etree.parse(StringIO(response.data)) catalogued, uncatalogued, invalid = self.XML_PARSE(root, message_path) # The uncatalogued identifier is now in the catalog. assert uncatalogued_id in self.collection.catalog # It has an accurate response message. eq_(uncatalogued_id.urn, self.xml_value(uncatalogued, 'atom:id')) eq_('201', self.xml_value(uncatalogued, 'simplified:status_code')) eq_('Successfully added', self.xml_value(uncatalogued, 'schema:description')) # The catalogued identifier is still in the catalog. assert catalogued_id in self.collection.catalog # And even though it responds 'OK', the message tells you it # was already there. eq_(catalogued_id.urn, self.xml_value(catalogued, 'atom:id')) eq_('200', self.xml_value(catalogued, 'simplified:status_code')) eq_('Already in catalog', self.xml_value(catalogued, 'schema:description')) # Invalid identifier return 400 errors. eq_(invalid_urn, self.xml_value(invalid, 'atom:id')) eq_('400', self.xml_value(invalid, 'simplified:status_code')) eq_('Could not parse identifier.', self.xml_value(invalid, 'schema:description')) def test_remove_items(self): invalid_urn = "FAKE AS I WANNA BE" catalogued_id = self._identifier() uncatalogued_id = self._identifier() self.collection.catalog_identifier(self._db, catalogued_id) message_path = '/atom:feed/simplified:message' with self.app.test_request_context( '/?urn=%s&urn=%s' % (catalogued_id.urn, uncatalogued_id.urn), headers=self.valid_auth): # The uncatalogued identifier doesn't raise or return an error. response = self.controller.remove_items(self.collection.name) eq_(HTTP_OK, response.status_code) # It sends two <simplified:message> tags. root = etree.parse(StringIO(response.data)) catalogued, uncatalogued = self.XML_PARSE(root, message_path) eq_(catalogued_id.urn, self.xml_value(catalogued, 'atom:id')) eq_(str(HTTP_OK), self.xml_value(catalogued, 'simplified:status_code')) eq_("Successfully removed", self.xml_value(catalogued, 'schema:description')) eq_(uncatalogued_id.urn, self.xml_value(uncatalogued, 'atom:id')) eq_(str(HTTP_NOT_FOUND), self.xml_value(uncatalogued, 'simplified:status_code')) eq_("Not in catalog", self.xml_value(uncatalogued, 'schema:description')) # It sends no <entry> tags. eq_([], self.XML_PARSE(root, "//atom:entry")) # The catalogued identifier isn't in the catalog. assert catalogued_id not in self.collection.catalog # But it's still in the database. eq_( catalogued_id, self._db.query(Identifier).filter_by( id=catalogued_id.id).one()) # Try again, this time including an invalid URN. self.collection.catalog_identifier(self._db, catalogued_id) with self.app.test_request_context('/?urn=%s&urn=%s' % (invalid_urn, catalogued_id.urn), headers=self.valid_auth): response = self.controller.remove_items(self.collection.name) eq_(HTTP_OK, int(response.status_code)) # Once again we get two <simplified:message> tags. root = etree.parse(StringIO(response.data)) invalid, catalogued = self.XML_PARSE(root, message_path) eq_(invalid_urn, self.xml_value(invalid, 'atom:id')) eq_("400", self.xml_value(invalid, 'simplified:status_code')) eq_("Could not parse identifier.", self.xml_value(invalid, 'schema:description')) eq_(catalogued_id.urn, self.xml_value(catalogued, 'atom:id')) eq_("200", self.xml_value(catalogued, 'simplified:status_code')) eq_("Successfully removed", self.xml_value(catalogued, 'schema:description')) # We have no <entry> tags. eq_([], self.XML_PARSE(root, "//atom:entry")) # The catalogued identifier is still removed. assert catalogued_id not in self.collection.catalog def test_update_client_url(self): url = urllib.quote('https://try-me.fake.us/') with self.app.test_request_context('/'): # Without authentication a ProblemDetail is returned. response = self.controller.update_client_url() eq_(True, isinstance(response, ProblemDetail)) eq_(INVALID_CREDENTIALS, response) with self.app.test_request_context('/', headers=self.valid_auth): # When a URL isn't provided, a ProblemDetail is returned. response = self.controller.update_client_url() eq_(True, isinstance(response, ProblemDetail)) eq_(400, response.status_code) eq_(INVALID_INPUT.uri, response.uri) assert 'client_url' in response.detail with self.app.test_request_context('/?client_url=%s' % url, headers=self.valid_auth): response = self.controller.update_client_url() # The request was successful. eq_(HTTP_OK, response.status_code) # The IntegrationClient's URL has been changed. self.client.url = 'try-me.fake.us'