Example #1
0
    def test_sync_bookshelf_removes_loans_not_present_on_remote(self):
        loans_data, json_loans = self.sample_json(
            "shelf_with_some_checked_out_books.json")
        holds_data, json_holds = self.sample_json("no_holds.json")

        overdrive = MockOverdriveAPI(self._db)
        overdrive.queue_response(200, content=loans_data)
        overdrive.queue_response(200, content=holds_data)

        # Create a loan not present in the sample data.
        patron = self._patron()
        overdrive_edition, new = self._edition(
            data_source_name=DataSource.OVERDRIVE, with_license_pool=True)
        overdrive_loan, new = overdrive_edition.license_pool.loan_to(patron)
        yesterday = datetime.utcnow() - timedelta(days=1)
        overdrive_loan.start = yesterday

        # Sync with Overdrive, and the loan not present in the sample
        # data is removed.
        circulation = CirculationAPI(self._db, overdrive=overdrive)
        loans, holds = circulation.sync_bookshelf(patron, "dummy pin")

        eq_(4, len(loans))
        eq_(loans, patron.loans)
        assert overdrive_loan not in patron.loans
Example #2
0
    def test_place_hold_looks_up_notification_address(self):
        edition, pool = self._edition(identifier_type=Identifier.OVERDRIVE_ID,
                                      data_source_name=DataSource.OVERDRIVE,
                                      with_license_pool=True)

        # The first request we make will be to get patron info,
        # so that we know that the most recent email address used
        # to put a book on hold is [email protected].
        ignore, patron_with_email = self.sample_json("patron_info.json")

        # The second request we make will be to put a book on hold,
        # and when we do so we will ask for the notification to be
        # sent to [email protected].
        ignore, successful_hold = self.sample_json("successful_hold.json")

        api = MockOverdriveAPI(self._db)
        api.queue_response(200, content=patron_with_email)
        api.queue_response(200, content=successful_hold)
        with temp_config() as config:
            config[
                'default_notification_email_address'] = "*****@*****.**"
            hold = api.place_hold(self._patron(),
                                  'pin',
                                  pool,
                                  notification_email_address=None)

        # The book was placed on hold.
        eq_(1, hold.hold_position)
        eq_(pool.identifier.identifier, hold.identifier)

        # And when we placed it on hold, we passed in [email protected]
        # as the email address -- not [email protected].
        url, positional_args, kwargs = api.requests[-1]
        headers, body = positional_args
        assert '{"name": "emailAddress", "value": "*****@*****.**"}' in body
Example #3
0
    def test_sync_bookshelf_creates_local_loans(self):
        loans_data, json_loans = self.sample_json(
            "shelf_with_some_checked_out_books.json")
        holds_data, json_holds = self.sample_json("no_holds.json")

        overdrive = MockOverdriveAPI(self._db)
        overdrive.queue_response(200, content=loans_data)
        overdrive.queue_response(200, content=holds_data)

        patron = self._patron()
        circulation = CirculationAPI(self._db, overdrive=overdrive)
        loans, holds = circulation.sync_bookshelf(patron, "dummy pin")

        # All four loans in the sample data were created.
        eq_(4, len(loans))
        eq_(loans, patron.loans)

        eq_([], holds)

        # Running the sync again leaves all four loans in place.
        overdrive.queue_response(200, content=loans_data)
        overdrive.queue_response(200, content=holds_data)
        loans, holds = circulation.sync_bookshelf(patron, "dummy pin")
        eq_(4, len(loans))
        eq_(loans, patron.loans)
Example #4
0
    def test_refresh_patron_access_token(self):
        """Verify that patron information is included in the request
        when refreshing a patron access token.
        """
        api = MockOverdriveAPI(self._db)
        patron = self._patron()
        patron.authorization_identifier = 'barcode'
        credential = self._credential(patron=patron)

        data, raw = self.sample_json("patron_token.json")
        api.queue_response(200, content=raw)

        # Try to refresh the patron access token with a PIN, and
        # then without a PIN.
        api.refresh_patron_access_token(credential, patron, "a pin")

        api.refresh_patron_access_token(credential, patron, None)

        # Verify that the requests that were made correspond to what
        # Overdrive is expecting.
        initial, with_pin, without_pin = api.access_token_requests
        url, payload, headers, kwargs = with_pin
        eq_("https://oauth-patron.overdrive.com/patrontoken", url)
        eq_("barcode", payload['username'])
        eq_("websiteid:d authorizationname:default", payload['scope'])
        eq_("a pin", payload['password'])
        assert not 'password_required' in payload

        url, payload, headers, kwargs = without_pin
        eq_("https://oauth-patron.overdrive.com/patrontoken", url)
        eq_("barcode", payload['username'])
        eq_("websiteid:d authorizationname:default", payload['scope'])
        eq_("false", payload['password_required'])
        eq_("[ignore]", payload['password'])
Example #5
0
 def test_update_licensepool_error(self):
     # Create an identifier.
     identifier = self._identifier(identifier_type=Identifier.OVERDRIVE_ID)
     ignore, availability = self.sample_json(
         "overdrive_availability_information.json")
     api = MockOverdriveAPI(self._db)
     api.queue_response(500, content="An error occured.")
     book = dict(id=identifier.identifier, availability_link=self._url)
     pool, was_new, changed = api.update_licensepool(book)
     eq_(None, pool)
Example #6
0
    def test_circulation_lookup(self):
        """Test the method that actually looks up Overdrive circulation
        information.
        """
        api = MockOverdriveAPI(self._db)
        api.queue_response(200, content="foo")

        book, (status_code, headers,
               content) = api.circulation_lookup("an identifier")
        eq_(dict(id="an identifier"), book)
        eq_(200, status_code)
        eq_("foo", content)
Example #7
0
    def test_place_hold_raises_exception_if_patron_over_hold_limit(self):
        over_hold_limit = self.error_message(
            "PatronExceededHoldLimit",
            "Patron cannot place any more holds, already has maximum holds placed."
        )

        edition, pool = self._edition(identifier_type=Identifier.OVERDRIVE_ID,
                                      data_source_name=DataSource.OVERDRIVE,
                                      with_license_pool=True)
        api = MockOverdriveAPI(self._db)
        api.queue_response(400, content=over_hold_limit)
        assert_raises(PatronHoldLimitReached,
                      api.place_hold,
                      self._patron(),
                      'pin',
                      pool,
                      notification_email_address='*****@*****.**')
Example #8
0
    def test_update_availability(self):
        """Test the Overdrive implementation of the update_availability
        method defined by the CirculationAPI interface.
        """

        # Create a LicensePool that needs updating.
        edition, pool = self._edition(
            identifier_type=Identifier.OVERDRIVE_ID,
            # TODO: If this line is commented out, we get an error later
            # on which might or might not be worrisome.
            data_source_name=DataSource.OVERDRIVE,
            with_license_pool=True)

        # We have never checked the circulation information for this
        # LicensePool. Put some random junk in the pool to make sure
        # it gets replaced.
        pool.licenses_owned = 10
        pool.licenses_available = 4
        pool.patrons_in_hold_queue = 3
        eq_(None, pool.last_checked)

        # Prepare availability information.
        ignore, availability = self.sample_json(
            "overdrive_availability_information.json")
        # Since this is the first time we've seen this book,
        # we'll also be updating the bibliographic information.
        ignore, bibliographic = self.sample_json(
            "bibliographic_information.json")

        # To avoid a mismatch, make it look like the information is
        # for the new pool's Identifier.
        availability['id'] = pool.identifier.identifier
        bibliographic['id'] = pool.identifier.identifier

        api = MockOverdriveAPI(self._db)
        api.queue_response(200, content=availability)
        api.queue_response(200, content=bibliographic)

        api.update_availability(pool)

        # The availability information has been udpated, as has the
        # date the availability information was last checked.
        eq_(5, pool.licenses_owned)
        eq_(5, pool.licenses_available)
        eq_(0, pool.patrons_in_hold_queue)
        assert pool.last_checked is not None
Example #9
0
    def test_get_fulfillment_link_from_download_link(self):
        patron = self._patron()

        ignore, streaming_fulfill_link = self.sample_json(
            "streaming_fulfill_link_response.json")

        api = MockOverdriveAPI(self._db)
        api.queue_response(200, content=streaming_fulfill_link)

        href, type = api.get_fulfillment_link_from_download_link(
            patron,
            '1234',
            "http://download-link",
            fulfill_url="http://fulfill")
        eq_(
            "https://fulfill.contentreserve.com/PerfectLife9780345530967.epub-sample.overdrive.com?RetailerID=nypl&Expires=1469825647&Token=dd0e19b4-eb70-439d-8c50-a65201060f4c&Signature=asl67/G154KeeUsL1mHPwEbZfgc=",
            href)
        eq_("text/html", type)
Example #10
0
    def test_update_formats(self):
        # Create a LicensePool with an inaccurate delivery mechanism.
        edition, pool = self._edition(identifier_type=Identifier.OVERDRIVE_ID,
                                      with_license_pool=True)

        # Add the bad delivery mechanism.
        pool.set_delivery_mechanism(Representation.PDF_MEDIA_TYPE,
                                    DeliveryMechanism.ADOBE_DRM,
                                    RightsStatus.IN_COPYRIGHT, None)

        # Prepare the bibliographic information.
        ignore, bibliographic = self.sample_json(
            "bibliographic_information.json")

        # To avoid a mismatch, make it look like the information is
        # for the new pool's Identifier.
        bibliographic['id'] = pool.identifier.identifier

        api = MockOverdriveAPI(self._db)
        api.queue_response(200, content=bibliographic)

        api.update_formats(pool)

        # The delivery mechanisms have been updated.
        eq_(3, len(pool.delivery_mechanisms))
        eq_(
            set([
                Representation.EPUB_MEDIA_TYPE,
                DeliveryMechanism.KINDLE_CONTENT_TYPE,
                DeliveryMechanism.STREAMING_TEXT_CONTENT_TYPE
            ]),
            set([
                lpdm.delivery_mechanism.content_type
                for lpdm in pool.delivery_mechanisms
            ]))
        eq_(
            set([
                DeliveryMechanism.ADOBE_DRM, DeliveryMechanism.KINDLE_DRM,
                DeliveryMechanism.OVERDRIVE_DRM
            ]),
            set([
                lpdm.delivery_mechanism.drm_scheme
                for lpdm in pool.delivery_mechanisms
            ]))
Example #11
0
    def test_sync_bookshelf_ignores_holds_from_other_sources(self):
        loans_data, json_loans = self.sample_json("no_loans.json")
        holds_data, json_holds = self.sample_json("holds.json")

        patron = self._patron()
        threem, new = self._edition(data_source_name=DataSource.THREEM,
                                    with_license_pool=True)
        threem_hold, new = threem.license_pool.on_hold_to(patron)

        overdrive = MockOverdriveAPI(self._db)
        overdrive.queue_response(200, content=loans_data)
        overdrive.queue_response(200, content=holds_data)

        # Overdrive doesn't know about the 3M hold, but it was
        # not destroyed, because it came from another source.
        circulation = CirculationAPI(self._db, overdrive=overdrive)
        loans, holds = circulation.sync_bookshelf(patron, "dummy pin")
        eq_(5, len(patron.holds))
        assert threem_hold in patron.holds
Example #12
0
    def test_sync_bookshelf_removes_holds_not_present_on_remote(self):
        loans_data, json_loans = self.sample_json("no_loans.json")
        holds_data, json_holds = self.sample_json("holds.json")

        patron = self._patron()
        overdrive_edition, new = self._edition(
            data_source_name=DataSource.OVERDRIVE, with_license_pool=True)
        overdrive_hold, new = overdrive_edition.license_pool.on_hold_to(patron)

        overdrive = MockOverdriveAPI(self._db)
        overdrive.queue_response(200, content=loans_data)
        overdrive.queue_response(200, content=holds_data)

        # The hold not present in the sample data has been removed
        circulation = CirculationAPI(self._db, overdrive=overdrive)
        loans, holds = circulation.sync_bookshelf(patron, "dummy pin")
        eq_(4, len(holds))
        eq_(holds, patron.holds)
        assert overdrive_hold not in patron.loans
Example #13
0
    def test_sync_bookshelf_ignores_loans_from_other_sources(self):
        patron = self._patron()
        gutenberg, new = self._edition(data_source_name=DataSource.GUTENBERG,
                                       with_license_pool=True)
        gutenberg_loan, new = gutenberg.license_pool.loan_to(patron)
        loans_data, json_loans = self.sample_json(
            "shelf_with_some_checked_out_books.json")
        holds_data, json_holds = self.sample_json("no_holds.json")

        # Overdrive doesn't know about the Gutenberg loan, but it was
        # not destroyed, because it came from another source.
        overdrive = MockOverdriveAPI(self._db)
        overdrive.queue_response(200, content=loans_data)
        overdrive.queue_response(200, content=holds_data)
        circulation = CirculationAPI(self._db, overdrive=overdrive)

        loans, holds = circulation.sync_bookshelf(patron, "dummy pin")
        eq_(5, len(patron.loans))
        assert gutenberg_loan in patron.loans
Example #14
0
    def test_update_licensepool_provides_bibliographic_coverage(self):
        # Create an identifier.
        identifier = self._identifier(identifier_type=Identifier.OVERDRIVE_ID)

        # Prepare bibliographic and availability information
        # for this identifier.
        ignore, availability = self.sample_json(
            "overdrive_availability_information.json")
        ignore, bibliographic = self.sample_json(
            "bibliographic_information.json")

        # To avoid a mismatch, make it look like the information is
        # for the newly created Identifier.
        availability['id'] = identifier.identifier
        bibliographic['id'] = identifier.identifier

        api = MockOverdriveAPI(self._db)
        api.queue_response(200, content=availability)
        api.queue_response(200, content=bibliographic)

        # Now we're ready. When we call update_licensepool, the
        # OverdriveAPI will retrieve the availability information,
        # then the bibliographic information. It will then trigger the
        # OverdriveBibliographicCoverageProvider, which will
        # create an Edition and a presentation-ready Work.
        pool, was_new, changed = api.update_licensepool(identifier.identifier)
        eq_(True, was_new)
        eq_(availability['copiesOwned'], pool.licenses_owned)

        edition = pool.presentation_edition
        eq_("Ancillary Justice", edition.title)

        eq_(True, pool.work.presentation_ready)
        assert pool.work.cover_thumbnail_url.startswith(
            'http://images.contentreserve.com/')

        # The book has been run through the bibliographic coverage
        # provider.
        coverage = [
            x for x in identifier.coverage_records if x.operation is None
            and x.data_source.name == DataSource.OVERDRIVE
        ]
        eq_(1, len(coverage))
Example #15
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")
        api = MockOverdriveAPI(self._db)
        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.
        with temp_config() as config:
            config[
                'default_notification_email_address'] = "*****@*****.**"
            eq_("*****@*****.**",
                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']
        api.queue_response(200, content=patron_with_no_email)
        with temp_config() as config:
            config[
                'default_notification_email_address'] = "*****@*****.**"
            eq_("*****@*****.**",
                api.default_notification_email_address(patron, 'pin'))

            # If there's an error getting the information, use the
            # site default.
            api.queue_response(404)
            eq_("*****@*****.**",
                api.default_notification_email_address(patron, 'pin'))
Example #16
0
    def test_fulfill_raises_exception_and_updates_formats_for_outdated_format(
            self):
        edition, pool = self._edition(identifier_type=Identifier.OVERDRIVE_ID,
                                      data_source_name=DataSource.OVERDRIVE,
                                      with_license_pool=True)

        # This pool has a format that's no longer available from overdrive.
        pool.set_delivery_mechanism(Representation.PDF_MEDIA_TYPE,
                                    DeliveryMechanism.ADOBE_DRM,
                                    RightsStatus.IN_COPYRIGHT, None)

        ignore, loan = self.sample_json("single_loan.json")

        ignore, lock_in_format_not_available = self.sample_json(
            "lock_in_format_not_available.json")

        # We will get the loan, try to lock in the format, and fail.
        api = MockOverdriveAPI(self._db)
        api.queue_response(200, content=loan)
        api.queue_response(400, content=lock_in_format_not_available)

        # Trying to get a fulfillment link raises an exception.
        assert_raises(FormatNotAvailable, api.get_fulfillment_link,
                      self._patron(), 'pin', pool.identifier.identifier,
                      'ebook-epub-adobe')

        # Fulfill will also update the formats.
        ignore, bibliographic = self.sample_json(
            "bibliographic_information.json")

        # If we have the LicensePool available (as opposed to just the
        # identifier), we will get the loan, try to lock in the
        # format, fail, and then update the bibliographic information.
        api.queue_response(200, content=loan)
        api.queue_response(400, content=lock_in_format_not_available)
        api.queue_response(200, content=bibliographic)

        assert_raises(FormatNotAvailable, api.fulfill, self._patron(), 'pin',
                      pool, 'ebook-epub-adobe')

        # The delivery mechanisms have been updated.
        eq_(3, len(pool.delivery_mechanisms))
        eq_(
            set([
                Representation.EPUB_MEDIA_TYPE,
                DeliveryMechanism.KINDLE_CONTENT_TYPE,
                DeliveryMechanism.STREAMING_TEXT_CONTENT_TYPE
            ]),
            set([
                lpdm.delivery_mechanism.content_type
                for lpdm in pool.delivery_mechanisms
            ]))
        eq_(
            set([
                DeliveryMechanism.ADOBE_DRM, DeliveryMechanism.KINDLE_DRM,
                DeliveryMechanism.OVERDRIVE_DRM
            ]),
            set([
                lpdm.delivery_mechanism.drm_scheme
                for lpdm in pool.delivery_mechanisms
            ]))