Skip to content

Commit

Permalink
OPDS2 + ODL Handle case where no licenses are available (PP-1716) (#2057
Browse files Browse the repository at this point in the history
)

* Fix handling of case where no copies are available.

* Fix case where there are no licenses when we think there are some.

* Updated

* Add another test case.
  • Loading branch information
jonathangreen authored Sep 14, 2024
1 parent 9d299db commit 96b10e6
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 5 deletions.
28 changes: 24 additions & 4 deletions src/palace/manager/api/odl/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,9 +266,7 @@ def get_license_status_document(self, loan: Loan) -> dict[str, Any]:
f"status code {response.status_code}. Expected 2XX. Response headers: {header_string}. "
f"Response content: {response_string}."
)
raise RemoteIntegrationException(
url, "License Status Document request failed."
) from e
raise
try:
status_doc = json.loads(response.content)
except ValueError as e:
Expand Down Expand Up @@ -424,7 +422,29 @@ def _checkout(
raise NoAvailableCopies()
loan, ignore = license.loan_to(patron)

doc = self.get_license_status_document(loan)
try:
doc = self.get_license_status_document(loan)
except BadResponseException as e:
_db.delete(loan)
response = e.response
# DeMarque sends "application/api-problem+json", but the ODL spec says we should
# expect "application/problem+json", so we need to check for both.
if response.headers.get("Content-Type") in [
"application/api-problem+json",
"application/problem+json",
]:
try:
json_response = response.json()
except ValueError:
json_response = {}

if (
json_response.get("type")
== "http://opds-spec.org/odl/error/checkout/unavailable"
):
raise NoAvailableCopies()
raise

status = doc.get("status")

if status not in [self.READY_STATUS, self.ACTIVE_STATUS]:
Expand Down
1 change: 1 addition & 0 deletions tests/files/odl/unavailable.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"type":"http://opds-spec.org/odl/error/checkout/unavailable","title":"the license has reached its concurrent checkouts limit","detail":"all 1 of 1 concurrent loans exceeded","status":400}
57 changes: 56 additions & 1 deletion tests/manager/api/odl/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import json
import urllib
import uuid
from functools import partial
from typing import Any
from unittest.mock import MagicMock
from urllib.parse import parse_qs, urlparse
Expand Down Expand Up @@ -50,7 +51,7 @@
from palace.manager.sqlalchemy.model.work import Work
from palace.manager.sqlalchemy.util import create
from palace.manager.util.datetime_helpers import datetime_utc, utc_now
from palace.manager.util.http import RemoteIntegrationException
from palace.manager.util.http import BadResponseException, RemoteIntegrationException
from tests.fixtures.database import DatabaseTransactionFixture
from tests.fixtures.odl import OPDS2WithODLApiFixture

Expand Down Expand Up @@ -714,6 +715,60 @@ def test_checkout_no_available_copies(

assert 0 == db.session.query(Loan).count()

@pytest.mark.parametrize(
"response_type",
["application/api-problem+json", "application/problem+json"],
)
def test_checkout_no_available_copies_unknown_to_us(
self,
db: DatabaseTransactionFixture,
opds2_with_odl_api_fixture: OPDS2WithODLApiFixture,
response_type: str,
) -> None:
"""
The title has no available copies, but we are out of sync with the distributor, so we think there
are copies available.
"""
checkout = partial(
opds2_with_odl_api_fixture.api.checkout,
opds2_with_odl_api_fixture.patron,
"pin",
opds2_with_odl_api_fixture.pool,
MagicMock(),
)

# We think there are copies available.
opds2_with_odl_api_fixture.setup_license(concurrency=1, available=1)

# But the distributor says there are no available copies.
opds2_with_odl_api_fixture.mock_http.queue_response(
400,
response_type,
content=opds2_with_odl_api_fixture.files.sample_text("unavailable.json"),
)

with pytest.raises(NoAvailableCopies):
checkout()

assert db.session.query(Loan).count() == 0

# Test the case where we get bad JSON back from the distributor.
opds2_with_odl_api_fixture.mock_http.queue_response(
400,
response_type,
content="hot garbage",
)

with pytest.raises(BadResponseException):
checkout()

# Test the case where we just get an unknown bad response.
opds2_with_odl_api_fixture.mock_http.queue_response(
500, "text/plain", content="halt and catch fire 🔥"
)
with pytest.raises(BadResponseException):
checkout()

def test_checkout_no_licenses(
self,
db: DatabaseTransactionFixture,
Expand Down

0 comments on commit 96b10e6

Please sign in to comment.