diff --git a/invenio_records_resources/services/base/__init__.py b/invenio_records_resources/services/base/__init__.py index 0f12b73d..54e025e9 100644 --- a/invenio_records_resources/services/base/__init__.py +++ b/invenio_records_resources/services/base/__init__.py @@ -10,13 +10,14 @@ """Base Service API.""" from .config import ServiceConfig -from .links import ConditionalLink, Link, LinksTemplate, NestedLinks +from .links import ConditionalLink, Link, Link2, LinksTemplate, NestedLinks from .results import ServiceItemResult, ServiceListResult from .service import Service __all__ = ( "ConditionalLink", "Link", + "Link2", "LinksTemplate", "Service", "ServiceConfig", diff --git a/invenio_records_resources/services/base/links.py b/invenio_records_resources/services/base/links.py index 114b39f0..447a8d85 100644 --- a/invenio_records_resources/services/base/links.py +++ b/invenio_records_resources/services/base/links.py @@ -12,6 +12,7 @@ from copy import deepcopy from flask import current_app +from invenio_base import invenio_url_for from invenio_records.dictutils import dict_lookup from uritemplate import URITemplate from werkzeug.datastructures import MultiDict @@ -95,7 +96,7 @@ def expand(self, identity, obj): class Link: - """Utility class for keeping track of and resolve links.""" + """Deprecated utility class for keeping track of and resolve links.""" def __init__(self, uritemplate, when=None, vars=None): """Constructor.""" @@ -125,6 +126,63 @@ def expand(self, obj, context): return self._uritemplate.expand(**vars) +class Link2: + """Encapsulation of the rendering of an endpoint URL. + + Is interface-compatible with Link for ease of initial adoption. + """ + + def __init__(self, endpoint, when=None, vars=None, params=None): + """Constructor. + + :param endpoint: str. endpoint of the URL + :param when: fn(obj, dict) -> bool, when the URL should be rendered + :param vars: fn(ob, dict), mutate dict in preparation for expansion + :param params: list, parameters (excluding querystrings) used for expansion + """ + self._endpoint = endpoint + self._when_func = when + self._vars_func = vars + self._params = params or [] + + def should_render(self, obj, context): + """Determine if the link should be rendered.""" + if self._when_func: + return bool(self._when_func(obj, context)) + return True + + @staticmethod + def vars(obj, vars): + """Dynamically update vars used to expand the link. + + Subclasses should overwrite this method. + """ + pass + + def expand(self, obj, context): + """Expand the endpoint. + + Note: "args" key in generated values for expansion has special meaning. + It is used for querystring parameters. + """ + vars = {} + vars.update(deepcopy(context)) + self.vars(obj, vars) + if self._vars_func: + self._vars_func(obj, vars) + + # Construct final values dict. + # Because invenio_url_for renders on the URL all arguments given to it, + # filtering for expandable ones must be done. + values = {k: v for k, v in vars.items() if k in self._params} + # The "args" key in the final values dict is where + # querystrings are passed through. + # Assumes no clash between URL params and querystrings + values.update(vars.get("args", {})) + values = dict(sorted(values.items())) # keep sorted interface + return invenio_url_for(self._endpoint, **values) + + class ConditionalLink: """Conditional link.""" diff --git a/invenio_records_resources/services/files/config.py b/invenio_records_resources/services/files/config.py index 8b665618..f10c4a2e 100644 --- a/invenio_records_resources/services/files/config.py +++ b/invenio_records_resources/services/files/config.py @@ -10,13 +10,11 @@ """Record Service API.""" from ..base import ServiceConfig -from ..records.links import RecordLink from .components import ( FileContentComponent, FileMetadataComponent, FileProcessorComponent, ) -from .links import FileLink from .processors import ImageMetadataExtractor from .results import FileItem, FileList from .schema import FileSchema @@ -41,14 +39,10 @@ class FileServiceConfig(ServiceConfig): max_files_count = 100 - file_links_list = { - "self": RecordLink("{+api}/records/{id}/files"), - } - - file_links_item = { - "self": FileLink("{+api}/records/{id}/files/{+key}"), - "content": FileLink("{+api}/records/{id}/files/{+key}/content"), - } + # Inheriting resource config should define these + # TODO: Edit rdm-records such that it doesn't rely on this first + file_links_list = {} + file_links_item = {} components = [ FileMetadataComponent, diff --git a/invenio_records_resources/services/files/links.py b/invenio_records_resources/services/files/links.py index 43ff559a..88cb2f3d 100644 --- a/invenio_records_resources/services/files/links.py +++ b/invenio_records_resources/services/files/links.py @@ -1,18 +1,18 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2020-2021 CERN. -# Copyright (C) 2020-2021 Northwestern University. +# Copyright (C) 2020-2025 Northwestern University. # # Flask-Resources is free software; you can redistribute it and/or modify it # under the terms of the MIT License; see LICENSE file for more details. """Utility for rendering URI template links.""" -from ..base import Link +from ..base import Link, Link2 class FileLink(Link): - """Short cut for writing record links.""" + """Deprecated shortcut for writing file links.""" @staticmethod def vars(file_record, vars): @@ -22,3 +22,12 @@ def vars(file_record, vars): "key": file_record.key, } ) + + +class FileLink2(Link2): + """Rendering of a file link with specific vars expansion.""" + + @staticmethod + def vars(file_record, vars): + """Variables for the endpoint expansion.""" + vars.update({"key": file_record.key}) diff --git a/invenio_records_resources/services/files/service.py b/invenio_records_resources/services/files/service.py index 373f314f..c261cb88 100644 --- a/invenio_records_resources/services/files/service.py +++ b/invenio_records_resources/services/files/service.py @@ -40,11 +40,21 @@ def file_result_list(self, *args, **kwargs): def file_links_list_tpl(self, id_): """Return a link template for list results.""" - return LinksTemplate(self.config.file_links_list, context={"id": id_}) + return LinksTemplate( + # Until all modules have transitioned to using invenio_url_for, + # we have to keep `id` in context for URL expansion + self.config.file_links_list, + context={"id": id_, "pid_value": id_}, + ) def file_links_item_tpl(self, id_): """Return a link template for item results.""" - return LinksTemplate(self.config.file_links_item, context={"id": id_}) + return LinksTemplate( + # Until all modules have transitioned to using invenio_url_for, + # we have to keep `id` in context for URL expansion + self.config.file_links_item, + context={"id": id_, "pid_value": id_}, + ) def check_permission(self, identity, action_name, **kwargs): """Check a permission against the identity.""" diff --git a/invenio_records_resources/services/records/config.py b/invenio_records_resources/services/records/config.py index 0b4c8665..b8157432 100644 --- a/invenio_records_resources/services/records/config.py +++ b/invenio_records_resources/services/records/config.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2020-2024 CERN. -# Copyright (C) 2020 Northwestern University. +# Copyright (C) 2020-2025 Northwestern University. # Copyright (C) 2023 Graz University of Technology. # # Invenio-Records-Resources is free software; you can redistribute it and/or @@ -18,7 +18,7 @@ from ...records import Record from ..base import ServiceConfig from .components import MetadataComponent -from .links import RecordLink, pagination_links +from .links import RecordLink, RecordLink2, pagination_links from .params import FacetsParam, PaginationParam, QueryParser, QueryStrParam, SortParam from .results import RecordBulkItem, RecordBulkList, RecordItem, RecordList @@ -75,11 +75,9 @@ class RecordServiceConfig(ServiceConfig): # Service schema schema = None # Needs to be defined on concrete record service config - links_item = { - "self": RecordLink("{+api}/records/{id}"), - } - - links_search = pagination_links("{+api}/records{?args*}") + # Definition of those is left up to implementations + links_item = {} + links_search = {} # Service components components = [ diff --git a/invenio_records_resources/services/records/links.py b/invenio_records_resources/services/records/links.py index 7a6e706c..78ac88ce 100644 --- a/invenio_records_resources/services/records/links.py +++ b/invenio_records_resources/services/records/links.py @@ -1,18 +1,18 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2020-2021 CERN. -# Copyright (C) 2020-2021 Northwestern University. +# Copyright (C) 2020-2025 Northwestern University. # # Flask-Resources is free software; you can redistribute it and/or modify it # under the terms of the MIT License; see LICENSE file for more details. """Utility for rendering URI template links.""" -from ..base import Link +from ..base import Link, Link2 class RecordLink(Link): - """Short cut for writing record links.""" + """Deprecated shortcut for writing record links.""" @staticmethod def vars(record, vars): @@ -23,8 +23,20 @@ def vars(record, vars): vars.update({"id": record.pid.pid_value}) +class RecordLink2(Link2): + """Rendering of a record link with specific vars expansion.""" + + @staticmethod + def vars(record, vars): + """Variables for the endpoint expansion.""" + # Some records don't have record.pid.pid_value yet (e.g. drafts) + pid_value = getattr(record.pid, "pid_value", None) + if pid_value: + vars.update({"pid_value": record.pid.pid_value}) + + def pagination_links(tpl): - """Create pagination links (prev/selv/next) from the same template.""" + """Create pagination links (prev/self/next) from the same template.""" return { "prev": Link( tpl, @@ -42,3 +54,26 @@ def pagination_links(tpl): ), ), } + + +def pagination_links2(endpoint, params=None): + """Create pagination links (prev/self/next) from the same template.""" + return { + "prev": Link2( + endpoint, + when=lambda pagination, ctx: pagination.has_prev, + vars=lambda pagination, vars: vars["args"].update( # + {"page": pagination.prev_page.page} + ), + params=params, + ), + "self": Link2(endpoint, params=params), + "next": Link2( + endpoint, + when=lambda pagination, ctx: pagination.has_next, + vars=lambda pagination, vars: vars["args"].update( # ["args"] + {"page": pagination.next_page.page} + ), + params=params, + ), + } diff --git a/setup.cfg b/setup.cfg index 5b619621..802b52ae 100644 --- a/setup.cfg +++ b/setup.cfg @@ -51,7 +51,7 @@ install_requires = [options.extras_require] tests = pytest-black-ng>=0.4.0 - invenio-app>=2.0.0,<3.0.0 + invenio-app>=2.1.0,<3.0.0 invenio-db[postgresql,mysql,versioning]>=2.0.0,<3.0.0 pytest-invenio>=3.0.0,<4.0.0 pytest-mock>=1.6.0 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..eaccea01 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2025 Northwestern University. +# +# Invenio-RDM-Records is free software; you can redistribute it and/or modify +# it under the terms of the MIT License; see LICENSE file for more details. + +"""Tests.""" diff --git a/tests/conftest.py b/tests/conftest.py index f1ecd8d3..b6e2f622 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,9 +16,9 @@ import pytest from flask_principal import Identity, Need, UserNeed from invenio_app.factory import create_api as _create_api -from mock_module.config import MockFileServiceConfig, ServiceConfig from invenio_records_resources.services import FileService, RecordService +from tests.mock_module.config import FileServiceConfig, ServiceConfig pytest_plugins = ("celery.contrib.pytest",) @@ -48,6 +48,8 @@ def app_config(app_config): "invenio_jsonschemas.proxies.current_refresolver_store" ) + app_config["THEME_FRONTPAGE"] = False + return app_config @@ -56,13 +58,18 @@ def extra_entry_points(): """Extra entry points to load the mock_module features.""" return { "invenio_db.model": [ - "mock_module = mock_module.models", + "mock_module = tests.mock_module.models", ], "invenio_jsonschemas.schemas": [ - "mock_module = mock_module.jsonschemas", + "mock_module = tests.mock_module.jsonschemas", ], "invenio_search.mappings": [ - "records = mock_module.mappings", + "records = tests.mock_module.mappings", + ], + "invenio_base.api_blueprints": [ + "mock_module_mocks = tests.mock_module:create_mocks_bp", + # still present even though above doesn't support files + "mock_module_mocks_files = tests.mock_module:create_mocks_files_bp", ], } @@ -76,7 +83,7 @@ def create_app(instance_path, entry_points): @pytest.fixture(scope="module") def file_service(): """File service shared fixture.""" - return FileService(MockFileServiceConfig) + return FileService(FileServiceConfig) @pytest.fixture(scope="module") diff --git a/tests/factories/conftest.py b/tests/factories/conftest.py index 5acf1e62..41730c15 100644 --- a/tests/factories/conftest.py +++ b/tests/factories/conftest.py @@ -11,7 +11,6 @@ """Factories test configuration.""" import pytest -from flask_principal import Identity, Need, UserNeed from invenio_app.factory import create_api as _create_api @@ -21,13 +20,13 @@ def extra_entry_points(): return { # to be verified if needed, since the models are dynamically created "invenio_db.model": [ - "mock_module_factory = mock_module_factory.grant", + "mock_module_factory = tests.mock_module_factory.grant", ], "invenio_jsonschemas.schemas": [ - "mock_module_factory = mock_module_factory.jsonschemas", + "mock_module_factory = tests.mock_module_factory.jsonschemas", ], "invenio_search.mappings": [ - "grants = mock_module_factory.mappings", + "grants = tests.mock_module_factory.mappings", ], } diff --git a/tests/factories/test_factory.py b/tests/factories/test_factory.py index 79237bda..cbedec7d 100644 --- a/tests/factories/test_factory.py +++ b/tests/factories/test_factory.py @@ -11,13 +11,13 @@ """Factory tests.""" import pytest -from mock_module.schemas import RecordSchema from sqlalchemy.exc import InvalidRequestError from invenio_records_resources.factories.factory import RecordTypeFactory from invenio_records_resources.services import RecordServiceConfig, SearchOptions from invenio_records_resources.services.records.components import ServiceComponent from invenio_records_resources.services.records.facets import TermsFacet +from tests.mock_module.schemas import RecordSchema def test_model_class_create(): diff --git a/tests/factories/test_service.py b/tests/factories/test_service.py index fcd2584f..f42d03ad 100644 --- a/tests/factories/test_service.py +++ b/tests/factories/test_service.py @@ -2,7 +2,7 @@ # # This file is part of Invenio. # Copyright (C) 2020-2021 CERN. -# Copyright (C) 2020-2021 Northwestern University. +# Copyright (C) 2020-2025 Northwestern University. # # Invenio-Records-Resources is free software; you can redistribute it and/or # modify it under the terms of the MIT License; see LICENSE file for more @@ -10,9 +10,9 @@ from invenio_records_permissions import RecordPermissionPolicy from invenio_records_permissions.generators import AnyUser, Disable -from mock_module.schemas import RecordSchema from invenio_records_resources.factories.factory import RecordTypeFactory +from tests.mock_module.schemas import RecordSchema def test_simple_flow(app, identity_simple, db): diff --git a/tests/mock_module/__init__.py b/tests/mock_module/__init__.py index a0175c65..0bd56dfa 100644 --- a/tests/mock_module/__init__.py +++ b/tests/mock_module/__init__.py @@ -2,10 +2,60 @@ # # This file is part of Invenio. # Copyright (C) 2020 CERN. -# Copyright (C) 2020 Northwestern University. +# Copyright (C) 2020-2025 Northwestern University. # # Invenio-Records-Resources is free software; you can redistribute it and/or # modify it under the terms of the MIT License; see LICENSE file for more # details. """Mock test module.""" + +from invenio_records_resources.resources import FileResource, RecordResource +from invenio_records_resources.services import FileService, RecordService +from tests.mock_module.config import ( + FileServiceConfig, + ServiceConfig, + ServiceWithFilesConfig, +) +from tests.mock_module.resource import ( + CustomDisabledUploadFileResourceConfig, + CustomFileResourceConfig, + CustomRecordResourceConfig, +) + +service_for_records = RecordService(ServiceConfig) +service_for_records_w_files = RecordService(ServiceWithFilesConfig) +service_for_files = FileService(FileServiceConfig) + + +def create_mocks_bp(app): + """Create mocks Blueprint.""" + service = service_for_records + resource = RecordResource(CustomRecordResourceConfig, service) + return resource.as_blueprint() + + +def create_mocks_w_files_bp(app): + """Create mocks with files Blueprint. + + This blueprint and the one above cannot be registered together + as they use the same blueprint name. This is in keeping with + pre-existing tests. + """ + service = service_for_records_w_files + resource = RecordResource(CustomRecordResourceConfig, service) + return resource.as_blueprint() + + +def create_mocks_files_bp(app): + """Create mocks files Blueprint.""" + service = service_for_files + resource = FileResource(CustomFileResourceConfig, service) + return resource.as_blueprint() + + +def create_mocks_files_disabled_upload_bp(app): + """Create mocks disabled files Blueprint.""" + service = service_for_files + resource = FileResource(CustomDisabledUploadFileResourceConfig, service) + return resource.as_blueprint() diff --git a/tests/mock_module/config.py b/tests/mock_module/config.py index 1c15e5f7..17343a09 100644 --- a/tests/mock_module/config.py +++ b/tests/mock_module/config.py @@ -14,7 +14,7 @@ RecordServiceConfig, SearchOptions, ) -from invenio_records_resources.services.files.links import FileLink +from invenio_records_resources.services.files.links import FileLink2 from invenio_records_resources.services.records.components import FilesComponent from invenio_records_resources.services.records.config import SearchOptions from invenio_records_resources.services.records.facets import ( @@ -23,7 +23,8 @@ ) from invenio_records_resources.services.records.links import ( RecordLink, - pagination_links, + RecordLink2, + pagination_links2, ) from invenio_records_resources.services.records.params.querystr import ( SuggestQueryParser, @@ -70,12 +71,12 @@ class ServiceConfig(RecordServiceConfig): search = MockSearchOptions links_item = { - "self": RecordLink("{+api}/mocks/{id}"), - "files": RecordLink("{+api}/mocks/{id}/files"), - "files-archive": RecordLink("{+api}/mocks/{id}/files-archive"), + "self": RecordLink2("mocks.read", params=["pid_value"]), + "files": RecordLink2("mocks_files.search", params=["pid_value"]), + "files-archive": RecordLink2("mocks_files.read_archive", params=["pid_value"]), } - links_search = pagination_links("{+api}/mocks{?args*}") + links_search = pagination_links2("mocks.search") nested_links_item = None @@ -87,7 +88,7 @@ class ServiceWithFilesConfig(ServiceConfig): schema = RecordWithFilesSchema -class MockFileServiceConfig(FileServiceConfig): +class FileServiceConfig(FileServiceConfig): """File service configuration.""" service_id = "mock-files" @@ -95,12 +96,12 @@ class MockFileServiceConfig(FileServiceConfig): permission_policy_cls = PermissionPolicy file_links_list = { - "self": RecordLink("{+api}/mocks/{id}/files"), - "files-archive": RecordLink("{+api}/mocks/{id}/files-archive"), + "self": RecordLink2("mocks_files.search", params=["pid_value"]), + "files-archive": RecordLink2("mocks_files.read_archive", params=["pid_value"]), } file_links_item = { - "self": FileLink("{+api}/mocks/{id}/files/{key}"), - "content": FileLink("{+api}/mocks/{id}/files/{key}/content"), - "commit": FileLink("{+api}/mocks/{id}/files/{key}/commit"), + "self": FileLink2("mocks_files.read", params=["pid_value", "key"]), + "content": FileLink2("mocks_files.read_content", params=["pid_value", "key"]), + "commit": FileLink2("mocks_files.create_commit", params=["pid_value", "key"]), } diff --git a/tests/records/conftest.py b/tests/records/conftest.py index 32f58f01..fea3991f 100644 --- a/tests/records/conftest.py +++ b/tests/records/conftest.py @@ -13,7 +13,8 @@ import pytest from invenio_indexer.api import RecordIndexer -from mock_module.api import Record + +from tests.mock_module.api import Record @pytest.fixture() diff --git a/tests/records/test_api.py b/tests/records/test_api.py index 612b5532..2e9c8df7 100644 --- a/tests/records/test_api.py +++ b/tests/records/test_api.py @@ -12,7 +12,8 @@ import pytest from invenio_search import current_search_client from jsonschema import ValidationError -from mock_module.api import Record + +from tests.mock_module.api import Record # diff --git a/tests/records/test_dumpers.py b/tests/records/test_dumpers.py index a1df9881..6b0b0b0e 100644 --- a/tests/records/test_dumpers.py +++ b/tests/records/test_dumpers.py @@ -11,7 +11,6 @@ from copy import deepcopy import pytest -from mock_module.api import Record from invenio_records_resources.records.dumpers import CustomFieldsDumperExt from invenio_records_resources.services.custom_fields import ( @@ -20,6 +19,7 @@ KeywordCF, TextCF, ) +from tests.mock_module.api import Record @pytest.fixture(scope="module") diff --git a/tests/records/test_systemfield_files.py b/tests/records/test_systemfield_files.py index dd63889f..c90a741a 100644 --- a/tests/records/test_systemfield_files.py +++ b/tests/records/test_systemfield_files.py @@ -13,12 +13,12 @@ from invenio_files_rest.models import Bucket, FileInstance, ObjectVersion from invenio_records.systemfields import ConstantField, ModelField -from mock_module import models -from mock_module.api import FileRecord -from mock_module.api import Record as RecordBase from invenio_records_resources.records.dumpers import PartialFileDumper from invenio_records_resources.records.systemfields.files import FilesField +from tests.mock_module import models +from tests.mock_module.api import FileRecord +from tests.mock_module.api import Record as RecordBase # Define a files-enabled record class diff --git a/tests/records/test_systemfield_index.py b/tests/records/test_systemfield_index.py index 5510f378..52164cfc 100644 --- a/tests/records/test_systemfield_index.py +++ b/tests/records/test_systemfield_index.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2020 CERN. -# Copyright (C) 2020 Northwestern University. +# Copyright (C) 2020-2025 Northwestern University. # # Invenio-Records-Resources is free software; you can redistribute it and/or # modify it under the terms of the MIT License; see LICENSE file for more @@ -10,9 +10,8 @@ """Index field tests.""" from invenio_search.engine import dsl -from mock_module.api import Record -from invenio_records_resources.records.systemfields import IndexField +from tests.mock_module.api import Record def test_class_attribute_access(): diff --git a/tests/records/test_systemfield_modelpid.py b/tests/records/test_systemfield_modelpid.py index 0862410f..436a6b5d 100644 --- a/tests/records/test_systemfield_modelpid.py +++ b/tests/records/test_systemfield_modelpid.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2022 CERN. +# Copyright (C) 2025 Northwestern University. # # Invenio-Records-Resources is free software; you can redistribute it and/or # modify it under the terms of the MIT License; see LICENSE file for more @@ -8,13 +9,12 @@ """ModelPIDField tests.""" -import pytest from invenio_records.systemfields import ModelField -from mock_module.api import Record as RecordBase -from mock_module.models import RecordMetadataWithPID from invenio_records_resources.records.systemfields import ModelPIDField from invenio_records_resources.records.systemfields.pid import ModelPIDFieldContext +from tests.mock_module.api import Record as RecordBase +from tests.mock_module.models import RecordMetadataWithPID class Record(RecordBase): diff --git a/tests/records/test_systemfield_pid.py b/tests/records/test_systemfield_pid.py index 1c25d37f..2fd141f5 100644 --- a/tests/records/test_systemfield_pid.py +++ b/tests/records/test_systemfield_pid.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2020 CERN. -# Copyright (C) 2020 Northwestern University. +# Copyright (C) 2020-2025 Northwestern University. # # Invenio-Records-Resources is free software; you can redistribute it and/or # modify it under the terms of the MIT License; see LICENSE file for more @@ -9,16 +9,14 @@ """PIDField tests.""" -from datetime import datetime - from invenio_pidstore.providers.recordid_v2 import RecordIdProviderV2 -from mock_module.api import Record -from mock_module.models import RecordMetadata from sqlalchemy import inspect from invenio_records_resources.records.api import Record as RecordBase from invenio_records_resources.records.systemfields import PIDField from invenio_records_resources.records.systemfields.pid import PIDFieldContext +from tests.mock_module.api import Record +from tests.mock_module.models import RecordMetadata def test_class_attribute_access(): diff --git a/tests/records/test_systemfield_pidstatus.py b/tests/records/test_systemfield_pidstatus.py index 0fc1715a..38804bad 100644 --- a/tests/records/test_systemfield_pidstatus.py +++ b/tests/records/test_systemfield_pidstatus.py @@ -10,9 +10,9 @@ """PIDStatusCheckField tests.""" from invenio_pidstore.models import PIDStatus -from mock_module.api import Record from invenio_records_resources.records.systemfields import PIDStatusCheckField +from tests.mock_module.api import Record def test_class_attribute_access(): diff --git a/tests/resources/conftest.py b/tests/resources/conftest.py index 955fa775..95c8a7f4 100644 --- a/tests/resources/conftest.py +++ b/tests/resources/conftest.py @@ -14,11 +14,11 @@ """ import pytest -from mock_module.config import ServiceConfig -from mock_module.resource import CustomRecordResourceConfig from invenio_records_resources.resources import RecordResource from invenio_records_resources.services import RecordService +from tests.mock_module.config import ServiceConfig +from tests.mock_module.resource import CustomRecordResourceConfig @pytest.fixture(scope="module") @@ -33,11 +33,11 @@ def record_resource(service): return RecordResource(CustomRecordResourceConfig, service) -@pytest.fixture(scope="module") -def base_app(base_app, record_resource): - """Application factory fixture.""" - base_app.register_blueprint(record_resource.as_blueprint()) - yield base_app +# @pytest.fixture(scope="module") +# def base_app(base_app, record_resource): +# """Application factory fixture.""" +# base_app.register_blueprint(record_resource.as_blueprint()) +# yield base_app @pytest.fixture(scope="module") diff --git a/tests/resources/test_files_resource.py b/tests/resources/test_files_resource.py index b6831a2d..6eac7d5f 100644 --- a/tests/resources/test_files_resource.py +++ b/tests/resources/test_files_resource.py @@ -14,51 +14,40 @@ from unittest.mock import patch import pytest -from mock_module.config import ServiceWithFilesConfig -from mock_module.resource import ( - CustomDisabledUploadFileResourceConfig, - CustomFileResourceConfig, - CustomRecordResourceConfig, -) from zipstream import ZipStream -from invenio_records_resources.resources import FileResource, RecordResource -from invenio_records_resources.services import RecordService +from tests.mock_module import service_for_files, service_for_records_w_files @pytest.fixture(scope="module") -def service(): - return RecordService(ServiceWithFilesConfig) - - -@pytest.fixture(scope="module") -def record_resource(service): - """Record Resource.""" - return RecordResource(CustomRecordResourceConfig, service) - - -@pytest.fixture(scope="module") -def file_resource(file_service): - """File Resources.""" - return FileResource(CustomFileResourceConfig, file_service) - - -@pytest.fixture(scope="module") -def disabled_file_upload_resource(file_service): - """Disabled Upload File Resource.""" - return FileResource(CustomDisabledUploadFileResourceConfig, file_service) +def extra_entry_points(): + """Extra entry points to load the mock_module features.""" + return { + "invenio_db.model": [ + "mock_module = tests.mock_module.models", + ], + "invenio_jsonschemas.schemas": [ + "mock_module = tests.mock_module.jsonschemas", + ], + "invenio_search.mappings": [ + "records = tests.mock_module.mappings", + ], + "invenio_base.api_blueprints": [ + "mock_module_mocks = tests.mock_module:create_mocks_w_files_bp", + "mock_module_mocks_files = tests.mock_module:create_mocks_files_bp", + "mock_module_mocks_files_disabled_upload = tests.mock_module:create_mocks_files_disabled_upload_bp", + ], + } @pytest.fixture(scope="module") def base_app( - base_app, file_resource, disabled_file_upload_resource, service, file_service + base_app, ): """Application factory fixture.""" - base_app.register_blueprint(file_resource.as_blueprint()) - base_app.register_blueprint(disabled_file_upload_resource.as_blueprint()) registry = base_app.extensions["invenio-records-resources"].registry - registry.register(service, service_id="mock-records") - registry.register(file_service, service_id="mock-files") + registry.register(service_for_records_w_files, service_id="mock-records") + registry.register(service_for_files, service_id="mock-files") yield base_app @@ -75,7 +64,6 @@ def test_files_api_flow(client, search_clear, headers, input_data, location): assert res.status_code == 201 id_ = res.json["id"] assert res.json["links"]["files"].endswith(f"/api/mocks/{id_}/files") - # Initialize files upload res = client.post( f"/mocks/{id_}/files", diff --git a/tests/resources/test_resource_faceting.py b/tests/resources/test_resource_faceting.py index 787feb8d..b202edbc 100644 --- a/tests/resources/test_resource_faceting.py +++ b/tests/resources/test_resource_faceting.py @@ -11,10 +11,10 @@ import pytest -from mock_module.api import Record -from mock_module.config import ServiceConfig from invenio_records_resources.services import RecordService +from tests.mock_module.api import Record +from tests.mock_module.config import ServiceConfig # 2 things to test # 1- results are aggregated / post_filtered @@ -279,8 +279,7 @@ def test_links_keep_facets(client, headers, three_indexed_records): response_links = response.json["links"] expected_links = { "self": ( - "https://127.0.0.1:5000/api/mocks?" - "page=1&size=25&sort=newest&type=A%2A%2AB" + "https://127.0.0.1:5000/api/mocks?page=1&size=25&sort=newest&type=A**B" ), } for key, url in expected_links.items(): diff --git a/tests/resources/test_resource_pagination.py b/tests/resources/test_resource_pagination.py index ca16db23..285bb9d9 100644 --- a/tests/resources/test_resource_pagination.py +++ b/tests/resources/test_resource_pagination.py @@ -10,10 +10,10 @@ """Test pagination.""" import pytest -from mock_module.api import Record -from mock_module.config import ServiceConfig from invenio_records_resources.services import RecordService +from tests.mock_module.api import Record +from tests.mock_module.config import ServiceConfig # 2 things to test # 1- results are paginated @@ -192,15 +192,15 @@ def test_searchstring_is_preserved(client, headers, three_indexed_records): response_links = response.json["links"] expected_links = { "self": ( - "https://127.0.0.1:5000/api/mocks?page=2&q=test%20foo&size=1" + "https://127.0.0.1:5000/api/mocks?page=2&q=test+foo&size=1" "&sort=bestmatch" ), "prev": ( - "https://127.0.0.1:5000/api/mocks?page=1&q=test%20foo&size=1" + "https://127.0.0.1:5000/api/mocks?page=1&q=test+foo&size=1" "&sort=bestmatch" ), "next": ( - "https://127.0.0.1:5000/api/mocks?page=3&q=test%20foo&size=1" + "https://127.0.0.1:5000/api/mocks?page=3&q=test+foo&size=1" "&sort=bestmatch" ), } diff --git a/tests/resources/test_resource_sorting.py b/tests/resources/test_resource_sorting.py index a0ac66eb..f2042363 100644 --- a/tests/resources/test_resource_sorting.py +++ b/tests/resources/test_resource_sorting.py @@ -13,10 +13,10 @@ from copy import deepcopy import pytest -from mock_module.api import Record -from mock_module.config import ServiceConfig from invenio_records_resources.services import RecordService +from tests.mock_module.api import Record +from tests.mock_module.config import ServiceConfig # 3 things to test # 0- search options are configurable (see mock_module) @@ -126,8 +126,7 @@ def test_searchstring_is_preserved(client, headers, three_indexed_records): response_links = response.json["links"] expected_links = { "self": ( - "https://127.0.0.1:5000/api/mocks?page=1&q=the%20quick&size=25" - "&sort=newest" + "https://127.0.0.1:5000/api/mocks?page=1&q=the+quick&size=25&sort=newest" ), } for key, url in expected_links.items(): diff --git a/tests/resources/test_resources.py b/tests/resources/test_resources.py index 1f4438b1..d2f26bb7 100644 --- a/tests/resources/test_resources.py +++ b/tests/resources/test_resources.py @@ -11,7 +11,7 @@ import json -from mock_module.api import Record +from tests.mock_module.api import Record def test_simple_flow(app, client, input_data, headers): diff --git a/tests/services/conftest.py b/tests/services/conftest.py index b359f0bd..20dc4ce4 100644 --- a/tests/services/conftest.py +++ b/tests/services/conftest.py @@ -18,7 +18,8 @@ from invenio_cache import current_cache from kombu import Queue from kombu.compat import Consumer -from mock_module.api import Record, RecordWithFiles + +from tests.mock_module.api import Record, RecordWithFiles @pytest.fixture(scope="function") diff --git a/tests/services/files/conftest.py b/tests/services/files/conftest.py index ce14cf69..085c424e 100644 --- a/tests/services/files/conftest.py +++ b/tests/services/files/conftest.py @@ -15,10 +15,10 @@ import pytest from invenio_cache import current_cache -from mock_module.api import Record, RecordWithFiles -from mock_module.config import ServiceWithFilesConfig from invenio_records_resources.services import RecordService +from tests.mock_module.api import Record, RecordWithFiles +from tests.mock_module.config import ServiceWithFilesConfig @pytest.fixture(scope="module") diff --git a/tests/services/test_results_expand.py b/tests/services/test_results_expand.py index aad0d2e4..8aceb866 100644 --- a/tests/services/test_results_expand.py +++ b/tests/services/test_results_expand.py @@ -8,9 +8,8 @@ """Test expand referenced records Service layer RecordItem.""" -from mock_module.api import Record - from invenio_records_resources.services.records.results import ExpandableField +from tests.mock_module.api import Record MOCK_USER = {"id": 3, "profile": {"full_name": "John Doe"}} MOCK_ENTITY = {"id": "ABC", "metadata": {"title": "My title"}} diff --git a/tests/services/test_service.py b/tests/services/test_service.py index 6848142e..6997da47 100644 --- a/tests/services/test_service.py +++ b/tests/services/test_service.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2020 CERN. -# Copyright (C) 2020 Northwestern University. +# Copyright (C) 2020-2025 Northwestern University. # # Invenio-Records-Resources is free software; you can redistribute it and/or # modify it under the terms of the MIT License; see LICENSE file for more @@ -16,11 +16,9 @@ """ import pytest -from invenio_cache import current_cache from invenio_pidstore.errors import PIDDeletedError -from invenio_search import current_search, current_search_client -from marshmallow import ValidationError -from mock_module.api import Record + +from tests.mock_module.api import Record def test_simple_flow(app, consumer, service, identity_simple, input_data): @@ -100,7 +98,11 @@ def test_read_many_pid_values(app, search_clear, service, identity_simple, input Record.index.refresh() records = service.read_many( - identity_simple, ids=[item_one.id, item_two.id], fields=["metadata.type.type"] + identity_simple, + ids=[item_one.id, item_two.id], + # NOTE: "id" and "pid" are needed to produce the links in the result + # Previous link generation would silently produce incorrect links + fields=["id", "pid", "metadata.type.type"], ) assert records.total == 2 diff --git a/tests/services/test_service_facets.py b/tests/services/test_service_facets.py index 4e17147a..d437329f 100644 --- a/tests/services/test_service_facets.py +++ b/tests/services/test_service_facets.py @@ -11,7 +11,8 @@ """Facets tests.""" import pytest -from mock_module.api import Record + +from tests.mock_module.api import Record # diff --git a/tests/services/test_service_relation_propagation.py b/tests/services/test_service_relation_propagation.py index f6b11610..fff68689 100644 --- a/tests/services/test_service_relation_propagation.py +++ b/tests/services/test_service_relation_propagation.py @@ -13,8 +13,6 @@ import arrow import pytest -from mock_module.api import Record, RecordWithRelations -from mock_module.config import ServiceConfig as ServiceConfigBase from invenio_records_resources.proxies import ( current_notifications_registry, @@ -25,6 +23,8 @@ ChangeNotificationsComponent, RelationsComponent, ) +from tests.mock_module.api import Record, RecordWithRelations +from tests.mock_module.config import ServiceConfig as ServiceConfigBase @pytest.fixture(scope="module") diff --git a/tests/services/test_service_sort.py b/tests/services/test_service_sort.py index be5bfbcd..17d303cb 100644 --- a/tests/services/test_service_sort.py +++ b/tests/services/test_service_sort.py @@ -13,7 +13,8 @@ import pytest from marshmallow import ValidationError -from mock_module.api import Record + +from tests.mock_module.api import Record #