From f234f88dc89d0e16aacc2100eddbd5c70498582d Mon Sep 17 00:00:00 2001 From: Jonathan Green Date: Thu, 27 Jun 2024 16:36:21 -0300 Subject: [PATCH] Fix up tests a bit more --- src/palace/manager/core/opds_schema.py | 2 +- .../schema_acquisition-object.schema.json | 20 +++ .../schema_properties.schema.json | 123 ++++++++++++++++++ ...mental_presentation_properties.schema.json | 31 +++++ ...a_extensions_divina_properties.schema.json | 13 ++ ...tensions_encryption_properties.schema.json | 40 ++++++ ...ema_extensions_epub_properties.schema.json | 32 +++++ .../{opds/odl2_feed.json => odl2/feed2.json} | 0 .../bad_feed.json} | 0 .../opds2_feed.json => opds2/feed2.json} | 0 tests/fixtures/api_odl.py | 15 +-- tests/fixtures/files.py | 26 ++++ tests/fixtures/odl.py | 4 +- tests/manager/api/test_odl2.py | 8 +- tests/manager/core/test_opds2_import.py | 15 +-- tests/manager/core/test_opds_validate.py | 70 +++++----- 16 files changed, 327 insertions(+), 72 deletions(-) create mode 100644 src/palace/manager/resources/opds2_schema/cached/drafts.opds.io/schema_acquisition-object.schema.json create mode 100644 src/palace/manager/resources/opds2_schema/cached/drafts.opds.io/schema_properties.schema.json create mode 100644 src/palace/manager/resources/opds2_schema/cached/readium.org/webpub-manifest_schema_experimental_presentation_properties.schema.json create mode 100644 src/palace/manager/resources/opds2_schema/cached/readium.org/webpub-manifest_schema_extensions_divina_properties.schema.json create mode 100644 src/palace/manager/resources/opds2_schema/cached/readium.org/webpub-manifest_schema_extensions_encryption_properties.schema.json create mode 100644 src/palace/manager/resources/opds2_schema/cached/readium.org/webpub-manifest_schema_extensions_epub_properties.schema.json rename tests/files/{opds/odl2_feed.json => odl2/feed2.json} (100%) rename tests/files/{opds/opds2_bad_feed.json => opds2/bad_feed.json} (100%) rename tests/files/{opds/opds2_feed.json => opds2/feed2.json} (100%) diff --git a/src/palace/manager/core/opds_schema.py b/src/palace/manager/core/opds_schema.py index 9223dd0389..83facc221c 100644 --- a/src/palace/manager/core/opds_schema.py +++ b/src/palace/manager/core/opds_schema.py @@ -42,7 +42,7 @@ def opds2_cached_retrieve(uri: str) -> str: filename = parsed.path.removeprefix("/").replace("/", "_") package_file = resources / "cached" / netloc_dir / filename # if not package_file.is_file(): - # cached_dir = cast(Path, resources / "cached" / netloc_dir) + # cached_dir = resources / "cached" / netloc_dir # cached_dir.mkdir(parents=True, exist_ok=True) # (cached_dir / filename).write_text(requests.get(uri).text) diff --git a/src/palace/manager/resources/opds2_schema/cached/drafts.opds.io/schema_acquisition-object.schema.json b/src/palace/manager/resources/opds2_schema/cached/drafts.opds.io/schema_acquisition-object.schema.json new file mode 100644 index 0000000000..1a05a4efce --- /dev/null +++ b/src/palace/manager/resources/opds2_schema/cached/drafts.opds.io/schema_acquisition-object.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://drafts.opds.io/schema/acquisition-object.schema.json", + "title": "OPDS Acquisition Object", + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "child": { + "type": "array", + "items": { + "$ref": "acquisition-object.schema.json" + } + } + }, + "required": [ + "type" + ] +} diff --git a/src/palace/manager/resources/opds2_schema/cached/drafts.opds.io/schema_properties.schema.json b/src/palace/manager/resources/opds2_schema/cached/drafts.opds.io/schema_properties.schema.json new file mode 100644 index 0000000000..a3fc15a124 --- /dev/null +++ b/src/palace/manager/resources/opds2_schema/cached/drafts.opds.io/schema_properties.schema.json @@ -0,0 +1,123 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://drafts.opds.io/schema/properties.schema.json", + "title": "OPDS Link Properties", + "type": "object", + "properties": { + "numberOfItems": { + "description": "Provide a hint about the expected number of items returned", + "type": "integer", + "minimum": 0 + }, + "price": { + "description": "The price of a publication is tied to its acquisition link", + "type": "object", + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "currency": { + "type": "string", + "enum": [ + "AED", "AFN", "ALL", "AMD", "ANG", "AOA", "ARS", "AUD", "AWG", "AZN", "BAM", "BBD", "BDT", + "BGN", "BHD", "BIF", "BMD", "BND", "BOB", "BOV", "BRL", "BSD", "BTN", "BWP", "BYN", "BZD", + "CAD", "CDF", "CHE", "CHF", "CHW", "CLF", "CLP", "CNY", "COP", "COU", "CRC", "CUC", "CUP", + "CVE", "CZK", "DJF", "DKK", "DOP", "DZD", "EGP", "ERN", "ETB", "EUR", "FJD", "FKP", "GBP", + "GEL", "GHS", "GIP", "GMD", "GNF", "GTQ", "GYD", "HKD", "HNL", "HRK", "HTG", "HUF", "IDR", + "ILS", "INR", "IQD", "IRR", "ISK", "JMD", "JOD", "JPY", "KES", "KGS", "KHR", "KMF", "KPW", + "KRW", "KWD", "KYD", "KZT", "LAK", "LBP", "LKR", "LRD", "LSL", "LYD", "MAD", "MDL", "MGA", + "MKD", "MMK", "MNT", "MOP", "MRU", "MUR", "MVR", "MWK", "MXN", "MXV", "MYR", "MZN", "NAD", + "NGN", "NIO", "NOK", "NPR", "NZD", "OMR", "PAB", "PEN", "PGK", "PHP", "PKR", "PLN", "PYG", + "QAR", "RON", "RSD", "RUB", "RWF", "SAR", "SBD", "SCR", "SDG", "SEK", "SGD", "SHP", "SLL", + "SOS", "SRD", "SSP", "STN", "SVC", "SYP", "SZL", "THB", "TJS", "TMT", "TND", "TOP", "TRY", + "TTD", "TWD", "TZS", "UAH", "UGX", "USD", "USN", "UYI", "UYU", "UZS", "VEF", "VES", "VND", + "VUV", "WST", "XAF", "XAG", "XAU", "XBA", "XBB", "XBC", "XBD", "XCD", "XDR", "XOF", "XPD", + "XPF", "XPT", "XSU", "XTS", "XUA", "XXX", "YER", "ZAR", "ZMW", "ZWL" + ] + } + }, + "required": [ + "currency", + "value" + ] + }, + "indirectAcquisition": { + "description": "Indirect acquisition provides a hint for the expected media type that will be acquired after additional steps", + "type": "array", + "items": { + "$ref": "acquisition-object.schema.json" + } + }, + "holds": { + "description": "Library-specific feature for unavailable books that support a hold list", + "type": "object", + "properties": { + "total": { + "type": "integer", + "minimum": 0 + }, + "position": { + "type": "integer", + "minimum": 0 + } + } + }, + "copies": { + "description": "Library-specific feature that contains information about the copies that a library has acquired", + "type": "object", + "properties": { + "total": { + "type": "integer", + "minimum": 0 + }, + "available": { + "type": "integer", + "minimum": 0 + } + } + }, + "availability": { + "description": "Indicates the availability of a given resource", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "available", + "unavailable", + "reserved", + "ready" + ] + }, + "since": { + "description": "Timestamp for the previous state change", + "type": "string", + "anyOf": [ + { + "format": "date" + }, + { + "format": "date-time" + } + ] + }, + "until": { + "description": "Timestamp for the next state change", + "type": "string", + "anyOf": [ + { + "format": "date" + }, + { + "format": "date-time" + } + ] + } + }, + "required": [ + "state" + ] + } + } +} diff --git a/src/palace/manager/resources/opds2_schema/cached/readium.org/webpub-manifest_schema_experimental_presentation_properties.schema.json b/src/palace/manager/resources/opds2_schema/cached/readium.org/webpub-manifest_schema_experimental_presentation_properties.schema.json new file mode 100644 index 0000000000..2a25dfafde --- /dev/null +++ b/src/palace/manager/resources/opds2_schema/cached/readium.org/webpub-manifest_schema_experimental_presentation_properties.schema.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://readium.org/webpub-manifest/schema/experimental/presentation/properties.schema.json", + "title": "Presentation Hints - Link Properties", + "type": "object", + "properties": { + "clipped": { + "description": "Specifies whether or not the parts of a linked resource that flow out of the viewport are clipped.", + "type": "boolean" + }, + "fit": { + "description": "Specifies constraints for the presentation of a linked resource within the viewport.", + "type": "string", + "enum": [ + "contain", + "cover", + "width", + "height" + ] + }, + "orientation": { + "description": "Suggested orientation for the device when displaying the linked resource.", + "type": "string", + "enum": [ + "auto", + "landscape", + "portrait" + ] + } + } +} diff --git a/src/palace/manager/resources/opds2_schema/cached/readium.org/webpub-manifest_schema_extensions_divina_properties.schema.json b/src/palace/manager/resources/opds2_schema/cached/readium.org/webpub-manifest_schema_extensions_divina_properties.schema.json new file mode 100644 index 0000000000..da4c9c9836 --- /dev/null +++ b/src/palace/manager/resources/opds2_schema/cached/readium.org/webpub-manifest_schema_extensions_divina_properties.schema.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://readium.org/webpub-manifest/schema/extensions/divina/properties.schema.json", + "title": "Divina Profile - Link Properties", + "type": "object", + "properties": { + "break-scroll-before": { + "description": "Specifies that an item in the reading order should break the current continuous scroll and start a new one.", + "type": "boolean", + "default": false + } + } +} diff --git a/src/palace/manager/resources/opds2_schema/cached/readium.org/webpub-manifest_schema_extensions_encryption_properties.schema.json b/src/palace/manager/resources/opds2_schema/cached/readium.org/webpub-manifest_schema_extensions_encryption_properties.schema.json new file mode 100644 index 0000000000..cb5f55a10a --- /dev/null +++ b/src/palace/manager/resources/opds2_schema/cached/readium.org/webpub-manifest_schema_extensions_encryption_properties.schema.json @@ -0,0 +1,40 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://readium.org/webpub-manifest/schema/extensions/encryption/properties.schema.json", + "title": "Encryption Module - Link Properties", + "type": "object", + "properties": { + "encrypted": { + "description": "Indicates that a resource is encrypted/obfuscated and provides relevant information for decryption", + "type": "object", + "properties": { + "algorithm": { + "description": "Identifies the algorithm used to encrypt the resource", + "type": "string", + "format": "uri" + }, + "compression": { + "description": "Compression method used on the resource", + "type": "string" + }, + "originalLength": { + "description": "Original length of the resource in bytes before compression and/or encryption", + "type": "integer" + }, + "profile": { + "description": "Identifies the encryption profile used to encrypt the resource", + "type": "string", + "format": "uri" + }, + "scheme": { + "description": "Identifies the encryption scheme used to encrypt the resource", + "type": "string", + "format": "uri" + } + }, + "required": [ + "algorithm" + ] + } + } +} diff --git a/src/palace/manager/resources/opds2_schema/cached/readium.org/webpub-manifest_schema_extensions_epub_properties.schema.json b/src/palace/manager/resources/opds2_schema/cached/readium.org/webpub-manifest_schema_extensions_epub_properties.schema.json new file mode 100644 index 0000000000..2a9c6f5fca --- /dev/null +++ b/src/palace/manager/resources/opds2_schema/cached/readium.org/webpub-manifest_schema_extensions_epub_properties.schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://readium.org/webpub-manifest/schema/extensions/epub/properties.schema.json", + "title": "EPUB Profile - Link Properties", + "type": "object", + "properties": { + "contains": { + "description": "Identifies content contained in the linked resource, that cannot be strictly identified using a media type.", + "type": "array", + "items": { + "type": "string", + "enum": [ + "mathml", + "onix", + "remote-resources", + "js", + "svg", + "xmp" + ] + }, + "uniqueItems": true + }, + "layout": { + "description": "Hints how the layout of a specific resource should be presented", + "type": "string", + "enum": [ + "fixed", + "reflowable" + ] + } + } +} diff --git a/tests/files/opds/odl2_feed.json b/tests/files/odl2/feed2.json similarity index 100% rename from tests/files/opds/odl2_feed.json rename to tests/files/odl2/feed2.json diff --git a/tests/files/opds/opds2_bad_feed.json b/tests/files/opds2/bad_feed.json similarity index 100% rename from tests/files/opds/opds2_bad_feed.json rename to tests/files/opds2/bad_feed.json diff --git a/tests/files/opds/opds2_feed.json b/tests/files/opds2/feed2.json similarity index 100% rename from tests/files/opds/opds2_feed.json rename to tests/files/opds2/feed2.json diff --git a/tests/fixtures/api_odl.py b/tests/fixtures/api_odl.py index 822b8b6924..588290688c 100644 --- a/tests/fixtures/api_odl.py +++ b/tests/fixtures/api_odl.py @@ -15,7 +15,7 @@ from palace.manager.sqlalchemy.model.licensing import LicensePool from palace.manager.sqlalchemy.model.resource import HttpResponseTuple from palace.manager.sqlalchemy.model.work import Work -from tests.fixtures.files import FilesFixture +from tests.fixtures.files import FilesFixture, ODL2APIFilesFixture if TYPE_CHECKING: from tests.fixtures.database import DatabaseTransactionFixture @@ -95,19 +95,6 @@ def api_odl_files_fixture() -> ODLAPIFilesFixture: return ODLAPIFilesFixture() -class ODL2APIFilesFixture(FilesFixture): - """A fixture providing access to ODL2 files.""" - - def __init__(self): - super().__init__("odl2") - - -@pytest.fixture() -def api_odl2_files_fixture() -> ODL2APIFilesFixture: - """A fixture providing access to ODL2 files.""" - return ODL2APIFilesFixture() - - class MockGet: def __init__(self): self.responses = [] diff --git a/tests/fixtures/files.py b/tests/fixtures/files.py index c5b0040f1f..1554917d93 100644 --- a/tests/fixtures/files.py +++ b/tests/fixtures/files.py @@ -56,6 +56,32 @@ def opds_files_fixture() -> OPDSFilesFixture: return OPDSFilesFixture() +class OPDS2FilesFixture(FilesFixture): + """A fixture providing access to OPDS2 files.""" + + def __init__(self): + super().__init__("opds2") + + +@pytest.fixture() +def opds2_files_fixture() -> OPDS2FilesFixture: + """A fixture providing access to OPDS2 files.""" + return OPDS2FilesFixture() + + +class ODL2APIFilesFixture(FilesFixture): + """A fixture providing access to ODL2 files.""" + + def __init__(self): + super().__init__("odl2") + + +@pytest.fixture() +def api_odl2_files_fixture() -> ODL2APIFilesFixture: + """A fixture providing access to ODL2 files.""" + return ODL2APIFilesFixture() + + class SampleCoversFixture(FilesFixture): """A fixture providing access to sample cover images.""" diff --git a/tests/fixtures/odl.py b/tests/fixtures/odl.py index 57e01585f3..719e3c2df7 100644 --- a/tests/fixtures/odl.py +++ b/tests/fixtures/odl.py @@ -16,9 +16,9 @@ from palace.manager.sqlalchemy.model.resource import Representation from palace.manager.sqlalchemy.model.work import Work from palace.manager.util.http import HTTP -from tests.fixtures.api_odl import ODL2APIFilesFixture, ODLAPIFilesFixture +from tests.fixtures.api_odl import ODLAPIFilesFixture from tests.fixtures.database import DatabaseTransactionFixture -from tests.fixtures.files import FilesFixture +from tests.fixtures.files import FilesFixture, ODL2APIFilesFixture from tests.mocks.mock import MockRequestsResponse diff --git a/tests/manager/api/test_odl2.py b/tests/manager/api/test_odl2.py index ed601215fc..e97f2ebf40 100644 --- a/tests/manager/api/test_odl2.py +++ b/tests/manager/api/test_odl2.py @@ -39,13 +39,9 @@ from palace.manager.sqlalchemy.model.work import Work from palace.manager.sqlalchemy.util import create from palace.manager.util.datetime_helpers import utc_now -from tests.fixtures.api_odl import ( - LicenseHelper, - LicenseInfoHelper, - MockGet, - ODL2APIFilesFixture, -) +from tests.fixtures.api_odl import LicenseHelper, LicenseInfoHelper, MockGet from tests.fixtures.database import DatabaseTransactionFixture +from tests.fixtures.files import ODL2APIFilesFixture from tests.fixtures.odl import ODL2APITestFixture, ODL2TestFixture diff --git a/tests/manager/core/test_opds2_import.py b/tests/manager/core/test_opds2_import.py index 6b46047dc0..9bfcc9fa54 100644 --- a/tests/manager/core/test_opds2_import.py +++ b/tests/manager/core/test_opds2_import.py @@ -30,7 +30,7 @@ from palace.manager.sqlalchemy.model.work import Work from palace.manager.util.datetime_helpers import utc_now from tests.fixtures.database import DatabaseTransactionFixture -from tests.fixtures.files import FilesFixture +from tests.fixtures.files import OPDS2FilesFixture class OPDS2Test: @@ -91,19 +91,6 @@ class TestOPDS2ImporterFixture: library: Library -class OPDS2FilesFixture(FilesFixture): - """A fixture providing access to OPDS2 files.""" - - def __init__(self): - super().__init__("opds2") - - -@pytest.fixture() -def opds2_files_fixture() -> OPDS2FilesFixture: - """A fixture providing access to OPDS2 files.""" - return OPDS2FilesFixture() - - @pytest.fixture def opds2_importer_fixture( db: DatabaseTransactionFixture, diff --git a/tests/manager/core/test_opds_validate.py b/tests/manager/core/test_opds_validate.py index 4f87dc7818..0a25da45d0 100644 --- a/tests/manager/core/test_opds_validate.py +++ b/tests/manager/core/test_opds_validate.py @@ -1,4 +1,5 @@ import json +from contextlib import nullcontext import pytest from jsonschema.exceptions import ValidationError @@ -10,15 +11,24 @@ from palace.manager.core.opds_schema import ODL2SchemaValidation, OPDS2SchemaValidation from palace.manager.sqlalchemy.model.datasource import DataSource from tests.fixtures.database import DatabaseTransactionFixture -from tests.fixtures.files import OPDSFilesFixture -from tests.manager.core.test_opds2_import import OPDS2Test +from tests.fixtures.files import ODL2APIFilesFixture, OPDS2FilesFixture -class TestOPDS2Validation(OPDS2Test): +class TestOPDS2Validation: + @pytest.mark.parametrize( + "feed_name, fail", + [ + ("feed.json", False), + ("feed2.json", False), + ("bad_feed.json", True), + ], + ) def test_opds2_schema( self, + feed_name: str, + fail: bool, db: DatabaseTransactionFixture, - opds_files_fixture: OPDSFilesFixture, + opds2_files_fixture: OPDS2FilesFixture, ): collection = db.collection( protocol=OPDS2API.label(), @@ -34,40 +44,27 @@ def test_opds2_schema( parser=RWPMManifestParser(OPDS2FeedParserFactory()), ) - bookshelf_opds2 = json.loads(opds_files_fixture.sample_text("opds2_feed.json")) - validator.import_one_feed(bookshelf_opds2) + context = pytest.raises(ValidationError) if fail else nullcontext() - def test_opds2_schema_fail( - self, - db: DatabaseTransactionFixture, - opds_files_fixture: OPDSFilesFixture, - ): - collection = db.collection( - protocol=OPDS2API.label(), - data_source_name=DataSource.FEEDBOOKS, - settings={ - "external_account_id": "http://example.com/feed", - }, - ) - validator = OPDS2SchemaValidation( - db.session, - collection=collection, - import_class=OPDS2Importer, - parser=RWPMManifestParser(OPDS2FeedParserFactory()), - ) - - bookshelf_opds2 = json.loads( - opds_files_fixture.sample_text("opds2_bad_feed.json") - ) - with pytest.raises(ValidationError): - validator.import_one_feed(bookshelf_opds2) + feed = json.loads(opds2_files_fixture.sample_text(feed_name)) + with context: + validator.import_one_feed(feed) -class TestODL2Validation(OPDS2Test): +class TestODL2Validation: + @pytest.mark.parametrize( + "feed_name, fail", + [ + ("feed.json", True), + ("feed2.json", False), + ], + ) def test_odl2_schema( self, + feed_name: str, + fail: bool, db: DatabaseTransactionFixture, - opds_files_fixture: OPDSFilesFixture, + api_odl2_files_fixture: ODL2APIFilesFixture, ): collection = db.collection( protocol=ODL2API.label(), @@ -85,6 +82,9 @@ def test_odl2_schema( parser=RWPMManifestParser(ODLFeedParserFactory()), ) - bookshelf_odl2 = opds_files_fixture.sample_text("odl2_feed.json") - imported, failures = validator.import_one_feed(bookshelf_odl2) - assert (len(imported), len(failures)) == (0, 0) + context = pytest.raises(ValidationError) if fail else nullcontext() + + feed = api_odl2_files_fixture.sample_text(feed_name) + with context: + imported, failures = validator.import_one_feed(feed) + assert (len(imported), len(failures)) == (0, 0)