Esempio n. 1
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)
Esempio n. 2
0
    def test_sync_bookshelf(self):
        patron = self._patron()
        circulation = CirculationAPI(
            self._db,
            self._default_library,
            api_map={self.collection.protocol: MockBibliothecaAPI})

        api = circulation.api_for_collection[self.collection.id]
        api.queue_response(200, content=self.sample_data("checkouts.xml"))
        circulation.sync_bookshelf(patron, "dummy pin")

        # The patron should have two loans and two holds.
        l1, l2 = patron.loans
        h1, h2 = patron.holds

        eq_(datetime.datetime(2015, 3, 20, 18, 50, 22), l1.start)
        eq_(datetime.datetime(2015, 4, 10, 18, 50, 22), l1.end)

        eq_(datetime.datetime(2015, 3, 13, 13, 38, 19), l2.start)
        eq_(datetime.datetime(2015, 4, 3, 13, 38, 19), l2.end)

        # This hold has no end date because there's no decision to be
        # made. The patron is fourth in line.
        eq_(datetime.datetime(2015, 3, 24, 15, 6, 56), h1.start)
        eq_(None, h1.end)
        eq_(4, h1.position)

        # The hold has an end date. It's time for the patron to decide
        # whether or not to check out this book.
        eq_(datetime.datetime(2015, 5, 25, 17, 5, 34), h2.start)
        eq_(datetime.datetime(2015, 5, 27, 17, 5, 34), h2.end)
        eq_(0, h2.position)
    def basic_test(self):
        patron = self._patron()        
        api = DummyThreeMAPI(self._db)
        api.queue_response(content=self.sample_data("checkouts.xml"))
        circulation = CirculationAPI(self._db, threem=api)
        circulation.sync_bookshelf(patron, "dummy pin")

        # The patron should have two loans and two holds.
        l1, l2 = patron.loans
        h1, h2 = patron.holds

        eq_(datetime.datetime(2015, 3, 20, 18, 50, 22), l1.start)
        eq_(datetime.datetime(2015, 4, 10, 18, 50, 22), l1.end)

        eq_(datetime.datetime(2015, 3, 13, 13, 38, 19), l2.start)
        eq_(datetime.datetime(2015, 4, 3, 13, 38, 19), l2.end)

        # This hold has no end date because there's no decision to be
        # made. The patron is fourth in line.
        eq_(datetime.datetime(2015, 3, 24, 15, 6, 56), h1.start)
        eq_(None, h1.end)
        eq_(4, h1.position)

        # The hold has an end date. It's time for the patron to decide
        # whether or not to check out this book.
        eq_(datetime.datetime(2015, 5, 25, 17, 5, 34), h2.start)
        eq_(datetime.datetime(2015, 5, 27, 17, 5, 34), h2.end)
        eq_(0, h2.position)
Esempio n. 4
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
Esempio n. 5
0
    def test_do_run(self):
        library1 = self._default_library
        library2 = self._library(name="library2")
        out = StringIO()

        class MockParsed(object):
            pass

        class MockScript(RunSelfTestsScript):
            tested = []

            def parse_command_line(self, *args, **kwargs):
                parsed = MockParsed()
                parsed.libraries = [library1, library2]
                return parsed

            def test_collection(self, collection, api_map):
                self.tested.append((collection, api_map))

        script = MockScript(self._db, out)
        script.do_run()
        # Both libraries were tested.
        assert out.getvalue() == "Testing %s\nTesting %s\n" % (
            library1.name,
            library2.name,
        )

        # The default library is the only one with a collection;
        # test_collection() was called on that collection.
        [(collection, api_map)] = script.tested
        assert [collection] == library1.collections

        # The API lookup map passed into test_collection() is based on
        # CirculationAPI's default API map.
        default_api_map = CirculationAPI(self._db,
                                         self._default_library).default_api_map
        for k, v in list(default_api_map.items()):
            assert api_map[k] == v

        # But a couple things were added to the map that are not in
        # CirculationAPI.
        assert api_map[ExternalIntegration.OPDS_IMPORT] == OPDSImportMonitor
        assert api_map[ExternalIntegration.FEEDBOOKS] == FeedbooksImportMonitor

        # If test_collection raises an exception, the exception is recorded,
        # and we move on.
        class MockScript2(MockScript):
            def test_collection(self, collection, api_map):
                raise Exception("blah")

        out = StringIO()
        script = MockScript2(self._db, out)
        script.do_run()
        assert (
            out.getvalue() ==
            "Testing %s\n  Exception while running self-test: 'blah'\nTesting %s\n"
            % (library1.name, library2.name))
    def test_do_run(self):
        library1 = self._default_library
        library2 = self._library(name="library2")
        out = StringIO()

        class MockParsed(object):
            pass

        class MockScript(RunSelfTestsScript):
            tested = []
            def parse_command_line(self, *args, **kwargs):
                parsed = MockParsed()
                parsed.libraries = [library1, library2]
                return parsed

            def test_collection(self, collection, api_map):
                self.tested.append((collection, api_map))

        script = MockScript(self._db, out)
        script.do_run()
        # Both libraries were tested.
        eq_(out.getvalue(),
            "Testing %s\nTesting %s\n" % (library1.name, library2.name))

        # The default library is the only one with a collection;
        # test_collection() was called on that collection.
        [(collection, api_map)] = script.tested
        eq_([collection], library1.collections)

        # The API lookup map passed into test_collection() is based on
        # CirculationAPI's default API map.
        default_api_map = CirculationAPI(
            self._db, self._default_library
        ).default_api_map
        for k, v in default_api_map.items():
            eq_(api_map[k], v)

        # But a couple things were added to the map that are not in
        # CirculationAPI.
        eq_(api_map[ExternalIntegration.OPDS_IMPORT], OPDSImportMonitor)
        eq_(api_map[ExternalIntegration.FEEDBOOKS], FeedbooksImportMonitor)

        # If test_collection raises an exception, the exception is recorded,
        # and we move on.
        class MockScript2(MockScript):
            def test_collection(self, collection, api_map):
                raise Exception("blah")
        out = StringIO()
        script = MockScript2(self._db, out)
        script.do_run()
        eq_(out.getvalue(),
            "Testing %s\n  Exception while running self-test: Exception('blah',)\nTesting %s\n" % (library1.name, library2.name)
        )
 def setup(self):
     super(OverdriveAPITest, self).setup()
     library = self._default_library
     self.collection = MockOverdriveAPI.mock_collection(self._db)
     self.circulation = CirculationAPI(
         self._db, library, api_map={ExternalIntegration.OVERDRIVE:MockOverdriveAPI}
     )
     self.api = self.circulation.api_for_collection[self.collection.id]
    def test_can_fulfill_without_loan(self):
        """Can a title can be fulfilled without an active loan?  It depends on
        the BaseCirculationAPI implementation for that title's colelction.
        """
        class Mock(BaseCirculationAPI):
            def can_fulfill_without_loan(self, patron, pool, lpdm):
                return "yep"

        pool = self._licensepool(None)
        circulation = CirculationAPI(self._db, self._default_library)
        circulation.api_for_collection[pool.collection.id] = Mock()
        eq_(
            "yep",
            circulation.can_fulfill_without_loan(None, pool, object())
        )

        # If format data is missing or the BaseCirculationAPI cannot
        # be found, we assume the title cannot be fulfilled.
        eq_(False, circulation.can_fulfill_without_loan(None, pool, None))
        eq_(False, circulation.can_fulfill_without_loan(None, None, object()))

        circulation.api_for_collection = {}
        eq_(False, circulation.can_fulfill_without_loan(None, pool, None))

        # An open access pool can be fulfilled even without the BaseCirculationAPI.
        pool.open_access = True
        eq_(True, circulation.can_fulfill_without_loan(None, pool, object()))
Esempio n. 9
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
    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.default_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 = DummyOverdriveAPI(self._db)
        overdrive.queue_response(content=holds_data)
        overdrive.queue_response(content=loans_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
Esempio n. 11
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
  def test_sync_bookshelf_ignores_loans_from_other_sources(self):
      patron = self.default_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 = DummyOverdriveAPI(self._db)
      overdrive.queue_response(content=holds_data)
      overdrive.queue_response(content=loans_data)
      circulation = CirculationAPI(self._db, overdrive=overdrive)
      patron = self.default_patron
      
      loans, holds = circulation.sync_bookshelf(patron, "dummy pin")
      eq_(5, len(patron.loans))
      assert gutenberg_loan in patron.loans
Esempio n. 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
Esempio n. 14
0
 def test_test_patron(self):
     """Verify that test_patron() returns credentials determined
     by the basic auth provider.
     """
     auth = self.mock_auth
     provider = auth.basic_auth_provider
     api = CirculationAPI(self._db, self._default_library)
     status = ServiceStatus(api, auth=auth)
     patron, password = status.test_patron
     eq_(provider.test_username, patron.authorization_identifier)
     eq_(provider.test_password, password)
    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.default_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 = DummyOverdriveAPI(self._db)
        overdrive.queue_response(content=holds_data)
        overdrive.queue_response(content=loans_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
    def test_patron_activity(self):
        # Get a CirculationAPI that doesn't mock out its API's patron activity.
        circulation = CirculationAPI(
            self._db, self._default_library, api_map={
            ExternalIntegration.BIBLIOTHECA : MockBibliothecaAPI
        })
        mock_bibliotheca = circulation.api_for_collection[self.collection.id]

        data = sample_data("checkouts.xml", "bibliotheca")
        mock_bibliotheca.queue_response(200, content=data)

        loans, holds, complete = circulation.patron_activity(self.patron, "1234")
        eq_(2, len(loans))
        eq_(2, len(holds))
        eq_(True, complete)

        mock_bibliotheca.queue_response(500, content="Error")

        loans, holds, complete = circulation.patron_activity(self.patron, "1234")
        eq_(0, len(loans))
        eq_(0, len(holds))
        eq_(False, complete)
Esempio n. 17
0
    def test_patron_activity(self):
        threem = MockThreeMAPI(self._db)

        circulation = CirculationAPI(self._db, threem=threem)

        data = sample_data("checkouts.xml", "threem")

        threem.queue_response(200, content=data)

        loans, holds, complete = circulation.patron_activity(
            self.patron, "1234")
        eq_(2, len(loans))
        eq_(2, len(holds))
        eq_(True, complete)

        threem.queue_response(500, content="Error")

        loans, holds, complete = circulation.patron_activity(
            self.patron, "1234")
        eq_(0, len(loans))
        eq_(0, len(holds))
        eq_(False, complete)
    def test_sync_bookshelf_creates_local_holds(self):
        
        loans_data, json_loans = self.sample_json("no_loans.json")
        holds_data, json_holds = self.sample_json("holds.json")

        overdrive = DummyOverdriveAPI(self._db)
        overdrive.queue_response(content=holds_data)
        overdrive.queue_response(content=loans_data)
        circulation = CirculationAPI(self._db, overdrive=overdrive)
        patron = self.default_patron

        loans, holds = circulation.sync_bookshelf(patron, "dummy pin")
        # All four loans in the sample data were created.
        eq_(4, len(holds))
        eq_(holds, patron.holds)

        # Running the sync again leaves all four holds in place.
        overdrive.queue_response(content=holds_data)
        overdrive.queue_response(content=loans_data)
        circulation = CirculationAPI(self._db, overdrive=overdrive)
        loans, holds = circulation.sync_bookshelf(patron, "dummy pin")
        eq_(4, len(holds))
        eq_(holds, patron.holds)        
Esempio n. 19
0
 def test_init(self):
     # Test that ServiceStatus can create an Authenticator.
     integration = self._external_integration(
         "api.simple_authentication",
         goal=ExternalIntegration.PATRON_AUTH_GOAL)
     provider = SimpleAuthenticationProvider
     integration.setting(provider.TEST_IDENTIFIER).value = "validpatron"
     integration.setting(provider.TEST_PASSWORD).value = "password"
     self._default_library.integrations.append(integration)
     api = CirculationAPI(self._db, self._default_library)
     service_status = ServiceStatus(api)
     assert service_status.auth != None
     assert isinstance(service_status.auth.basic_auth_provider, provider)
     eq_(self._default_library, service_status.auth.library)
    def test_patron_activity(self):
        # Get a CirculationAPI that doesn't mock out its API's patron activity.
        circulation = CirculationAPI(
            self._db,
            self._default_library,
            api_map={ExternalIntegration.BIBLIOTHECA: MockBibliothecaAPI})
        mock_bibliotheca = circulation.api_for_collection[self.collection.id]

        data = sample_data("checkouts.xml", "bibliotheca")
        mock_bibliotheca.queue_response(200, content=data)

        loans, holds, complete = circulation.patron_activity(
            self.patron, "1234")
        eq_(2, len(loans))
        eq_(2, len(holds))
        eq_(True, complete)

        mock_bibliotheca.queue_response(500, content="Error")

        loans, holds, complete = circulation.patron_activity(
            self.patron, "1234")
        eq_(0, len(loans))
        eq_(0, len(holds))
        eq_(False, complete)
    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 = DummyOverdriveAPI(self._db)
        overdrive.queue_response(content=holds_data)
        overdrive.queue_response(content=loans_data)

        # Create a loan not present in the sample data.
        patron = self.default_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)

        # 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
Esempio n. 22
0
    def setup(self):
        super(OdiloAPITest, self).setup()
        library = self._default_library
        self.patron = self._patron()
        self.patron.authorization_identifier='0001000265'
        self.collection = MockOdiloAPI.mock_collection(self._db)
        self.circulation = CirculationAPI(
            self._db, library, api_map={ExternalIntegration.ODILO: MockOdiloAPI}
        )
        self.api = self.circulation.api_for_collection[self.collection.id]

        self.edition, self.licensepool = self._edition(
            data_source_name=DataSource.ODILO,
            identifier_type=Identifier.ODILO_ID,
            collection=self.collection,
            identifier_id=self.RECORD_ID,
            with_license_pool=True
        )
    def test_configuration_exception_is_stored(self):
        """If the initialization of an API object raises
        CannotLoadConfiguration, the exception is stored with the
        CirculationAPI rather than being propagated.
        """
        api_map = {self._default_collection.protocol: self.MisconfiguredAPI}
        circulation = CirculationAPI(self._db,
                                     self._default_library,
                                     api_map=api_map)

        # Although the CirculationAPI was created, it has no functioning
        # APIs.
        eq_({}, circulation.api_for_collection)

        # Instead, the CannotLoadConfiguration exception raised by the
        # constructor has been stored in initialization_exceptions.
        e = circulation.initialization_exceptions[self._default_collection.id]
        assert isinstance(e, CannotLoadConfiguration)
        eq_("doomed!", e.message)
Esempio n. 24
0
    def test_can_fulfill_without_loan(self):
        """Can a title can be fulfilled without an active loan?  It depends on
        the BaseCirculationAPI implementation for that title's colelction.
        """
        class Mock(BaseCirculationAPI):
            def can_fulfill_without_loan(self, patron, pool, lpdm):
                return "yep"

        pool = self._licensepool(None)
        circulation = CirculationAPI(self._db, self._default_library)
        circulation.api_for_collection[pool.collection.id] = Mock()
        eq_("yep", circulation.can_fulfill_without_loan(None, pool, object()))

        # If format data is missing or the BaseCirculationAPI cannot
        # be found, we assume the title cannot be fulfilled.
        eq_(False, circulation.can_fulfill_without_loan(None, pool, None))
        eq_(False, circulation.can_fulfill_without_loan(None, None, object()))

        circulation.api_for_collection = {}
        eq_(False, circulation.can_fulfill_without_loan(None, pool, None))

        # An open access pool can be fulfilled even without the BaseCirculationAPI.
        pool.open_access = True
        eq_(True, circulation.can_fulfill_without_loan(None, pool, object()))
Esempio n. 25
0
    def test_checkout_status(self):

        # Create a Collection to test.
        overdrive_collection = self._collection(
            protocol=ExternalIntegration.OVERDRIVE)
        edition, lp = self._edition(with_license_pool=True,
                                    collection=overdrive_collection)
        library = self._default_library
        library.collections.append(overdrive_collection)

        # Test a scenario where we get information for every
        # relevant collection in the library.
        class CheckoutSuccess(object):
            def __init__(self, *args, **kwargs):
                self.borrowed = False
                self.fulfilled = False
                self.revoked = False

            def borrow(self, patron, password, license_pool, *args, **kwargs):
                "Simulate a successful borrow."
                self.borrowed = True
                return object(), None, True

            def fulfill(self, *args, **kwargs):
                "Simulate a successful fulfillment."
                self.fulfilled = True

            def revoke_loan(self, *args, **kwargs):
                "Simulate a successful loan revocation."
                self.revoked = True

        everything_succeeds = {ExternalIntegration.OVERDRIVE: CheckoutSuccess}

        auth = self.mock_auth
        api = CirculationAPI(self._db, library, api_map=everything_succeeds)
        status = ServiceStatus(api, auth=auth)
        ConfigurationSetting.for_library(
            Configuration.DEFAULT_NOTIFICATION_EMAIL_ADDRESS,
            library).value = "a@b"
        response = status.checkout_status(lp.identifier)

        # The ServiceStatus object was able to run its test.
        for value in response.values():
            assert value.startswith('SUCCESS')

        # The mock Overdrive API had all its methods called.
        api = status.circulation.api_for_collection[overdrive_collection.id]
        eq_(True, api.borrowed)
        eq_(True, api.fulfilled)
        eq_(True, api.revoked)

        # Now try some failure conditions.

        # First: the 'borrow' operation succeeds on an API level but
        # it doesn't create a loan.
        class NoLoanCreated(CheckoutSuccess):
            def borrow(self, patron, password, license_pool, *args, **kwargs):
                "Oops! We put the book on hold instead of borrowing it."
                return None, object(), True

        no_loan_created = {ExternalIntegration.OVERDRIVE: NoLoanCreated}
        api = CirculationAPI(self._db, library, api_map=no_loan_created)
        status = ServiceStatus(api, auth=auth)
        response = status.checkout_status(lp.identifier)
        assert 'FAILURE: No loan created during checkout' in response.values()

        # Next: The 'revoke' operation fails on an API level.
        class RevokeFail(CheckoutSuccess):
            def revoke_loan(self, *args, **kwargs):
                "Simulate an error during loan revocation."
                raise Exception("Doomed to fail!")

        revoke_fail = {ExternalIntegration.OVERDRIVE: RevokeFail}
        api = CirculationAPI(self._db, library, api_map=revoke_fail)
        status = ServiceStatus(api, auth=auth)
        response = status.checkout_status(lp.identifier)
        assert 'FAILURE: Doomed to fail!' in response.values()

        # But at least we got through the borrow and fulfill steps.
        api = status.circulation.api_for_collection[overdrive_collection.id]
        eq_(True, api.borrowed)
        eq_(True, api.fulfilled)
        eq_(False, api.revoked)
Esempio n. 26
0
    def test_loans_status(self):
        auth = self.mock_auth

        class MockPatronActivitySuccess(object):
            def __init__(self, *args, **kwargs):
                pass

            def patron_activity(self, patron, pin):
                "Simulate a patron with nothing going on."
                return

        class MockPatronActivityFailure(object):
            def __init__(self, *args, **kwargs):
                pass

            def patron_activity(self, patron, pin):
                "Simulate an integration failure."
                raise ValueError("Doomed to fail!")

        # Create a variety of Collections for this library.
        overdrive_collection = self._collection(
            protocol=ExternalIntegration.OVERDRIVE)
        axis_collection = self._collection(
            protocol=ExternalIntegration.AXIS_360)
        self._default_library.collections.append(overdrive_collection)
        self._default_library.collections.append(axis_collection)

        # Test a scenario where we get information for every
        # relevant collection in the library.
        everything_succeeds = {
            ExternalIntegration.OVERDRIVE: MockPatronActivitySuccess,
            ExternalIntegration.AXIS_360: MockPatronActivitySuccess
        }

        api = CirculationAPI(self._db,
                             self._default_library,
                             api_map=everything_succeeds)
        status = ServiceStatus(api, auth=auth)
        response = status.loans_status(response=True)
        for value in response.values():
            assert value.startswith('SUCCESS')

        # Simulate a failure in one of the providers.
        overdrive_fails = {
            ExternalIntegration.OVERDRIVE: MockPatronActivityFailure,
            ExternalIntegration.AXIS_360: MockPatronActivitySuccess
        }
        api = CirculationAPI(self._db,
                             self._default_library,
                             api_map=overdrive_fails)
        status = ServiceStatus(api, auth=auth)
        response = status.loans_status(response=True)
        key = '%s patron account (Overdrive)' % overdrive_collection.name
        eq_("FAILURE: Doomed to fail!", response[key])

        # Simulate failures on the ILS level.
        def test_with_broken_basic_auth_provider(value):
            class BrokenBasicAuthProvider(object):
                def testing_patron(self, _db):
                    return value

            auth.basic_auth_provider = BrokenBasicAuthProvider()
            response = status.loans_status(response=True)
            eq_(
                {
                    'Patron authentication':
                    'Could not create patron with configured credentials.'
                }, response)

        # Test patron can't authenticate
        test_with_broken_basic_auth_provider(
            (None, "password that didn't work"))

        # Auth provider is just totally broken.
        test_with_broken_basic_auth_provider(None)

        # If the auth process returns a problem detail, the problem
        # detail is used as the basis for the error message.
        class ExpiredPatronProvider(object):
            def testing_patron(self, _db):
                return EXPIRED_CREDENTIALS, None

        auth.basic_auth_provider = ExpiredPatronProvider()
        response = status.loans_status(response=True)
        eq_({'Patron authentication': EXPIRED_CREDENTIALS.response[0]},
            response)