class TestCollectionController(DatabaseTest): def setup(self): super(TestCollectionController, self).setup() from app import app self.app = app self.controller = CollectionController(self._db) self.collection = self._collection() self.valid_auth = 'Basic ' + base64.b64encode('abc:def') 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 test_authenticated_collection_required(self): # Returns collection if authentication is valid. with self.app.test_request_context('/', headers=dict(Authorization=self.valid_auth)): result = self.controller.authenticated_collection_from_request() eq_(result, self.collection) # Returns error if authentication is invalid. invalid_auth = 'Basic ' + base64.b64encode('abc:defg') with self.app.test_request_context('/', headers=dict(Authorization=invalid_auth)): result = self.controller.authenticated_collection_from_request() eq_(True, isinstance(result, ProblemDetail)) eq_(HTTP_UNAUTHORIZED, result.status_code) # Returns errors without authentication. with self.app.test_request_context('/'): result = self.controller.authenticated_collection_from_request() eq_(True, isinstance(result, ProblemDetail)) def test_authenticated_collection_optional(self): # Returns collection of authentication is valid. with self.app.test_request_context('/', headers=dict(Authorization=self.valid_auth)): result = self.controller.authenticated_collection_from_request(required=False) eq_(result, self.collection) # Returns error if attempted authentication is invalid. invalid_auth = 'Basic ' + base64.b64encode('abc:defg') with self.app.test_request_context('/', headers=dict(Authorization=invalid_auth)): result = self.controller.authenticated_collection_from_request(required=False) eq_(True, isinstance(result, ProblemDetail)) eq_(HTTP_UNAUTHORIZED, result.status_code) # Returns none if no authentication. with self.app.test_request_context('/'): result = self.controller.authenticated_collection_from_request(required=False) eq_(None, result) 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=dict(Authorization=self.valid_auth)): response = self.controller.updates_feed() # The collection's updates feed is returned. eq_(HTTP_OK, response.status_code) feed = feedparser.parse(response.get_data()) eq_(feed['feed']['title'],"%s Updates" % self.collection.name) # The feed has the collection'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=dict(Authorization=self.valid_auth)): response = self.controller.updates_feed() eq_(HTTP_OK, response.status_code) feed = feedparser.parse(response.get_data()) eq_(feed['feed']['title'],"%s Updates" % self.collection.name) # 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=dict(Authorization=self.valid_auth)): response = self.controller.updates_feed() 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=dict(Authorization=self.valid_auth)): response = self.controller.updates_feed() 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=dict(Authorization=self.valid_auth)): response = self.controller.updates_feed() 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_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) parser = OPDSXMLParser() message_path = '/atom:feed/simplified:message' with self.app.test_request_context( '/?urn=%s&urn=%s' % (catalogued_id.urn, uncatalogued_id.urn), headers=dict(Authorization=self.valid_auth)): # The uncatalogued identifier doesn't raise or return an error. response = self.controller.remove_items() eq_(HTTP_OK, response.status_code) # It sends two <simplified:message> tags. root = etree.parse(StringIO(response.data)) catalogued, uncatalogued = parser._xpath(root, message_path) eq_("http://www.gutenberg.org/ebooks/2013", parser._xpath(catalogued, 'atom:id')[0].text) eq_(str(HTTP_OK), parser._xpath(catalogued, 'simplified:status_code')[0].text) eq_("Successfully removed", parser._xpath(catalogued, 'schema:description')[0].text) eq_("http://www.gutenberg.org/ebooks/2014", parser._xpath(uncatalogued, 'atom:id')[0].text) eq_(str(HTTP_NOT_FOUND), parser._xpath(uncatalogued, 'simplified:status_code')[0].text) eq_("Not in collection catalog", parser._xpath(uncatalogued, 'schema:description')[0].text) # It sends no <entry> tags. eq_([], parser._xpath(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=dict(Authorization=self.valid_auth)): response = self.controller.remove_items() eq_(HTTP_OK, int(response.status_code)) # Once again we get two <simplified:message> tags. root = etree.parse(StringIO(response.data)) catalogued, uncatalogued = parser._xpath(root, message_path) eq_(invalid_urn, parser._xpath(catalogued, 'atom:id')[0].text) eq_("400", parser._xpath(catalogued, 'simplified:status_code')[0].text) eq_("Could not parse identifier.", parser._xpath(catalogued, 'schema:description')[0].text) eq_("http://www.gutenberg.org/ebooks/2013", parser._xpath(uncatalogued, 'atom:id')[0].text) eq_("200", parser._xpath(uncatalogued, 'simplified:status_code')[0].text) eq_("Successfully removed", parser._xpath(uncatalogued, 'schema:description')[0].text) # We have no <entry> tags. eq_([], parser._xpath(root, "//atom:entry")) # The catalogued identifier is still removed. assert catalogued_id not in self.collection.catalog