diff --git a/docs/source/mock-api-reference.rst b/docs/source/mock-api-reference.rst index ee0215fe0..338855d2c 100644 --- a/docs/source/mock-api-reference.rst +++ b/docs/source/mock-api-reference.rst @@ -20,12 +20,19 @@ API Reference :undoc-members: :exclude-members: to_dict, get_target, from_dict, not_deleted_targets, active_targets, inactive_targets, failed_targets, processing_targets +.. autoclass:: mock_vws.database.VuMarkDatabase + :members: + :undoc-members: + :exclude-members: to_dict, from_dict, not_deleted_targets + .. autoenum:: mock_vws.states.States :members: :undoc-members: .. autoclass:: mock_vws.target.ImageTarget +.. autoclass:: mock_vws.target.VuMarkTarget + Image matchers -------------- diff --git a/src/mock_vws/_database_matchers.py b/src/mock_vws/_database_matchers.py index 0e6a8c76d..dae0253a7 100644 --- a/src/mock_vws/_database_matchers.py +++ b/src/mock_vws/_database_matchers.py @@ -5,7 +5,9 @@ from beartype import beartype from vws_auth_tools import authorization_header -from mock_vws.database import CloudDatabase +from mock_vws.database import CloudDatabase, VuMarkDatabase + +AnyDatabase = CloudDatabase | VuMarkDatabase @beartype @@ -58,14 +60,14 @@ def get_database_matching_client_keys( @beartype -def get_database_matching_server_keys( +def get_database_matching_server_keys[DatabaseT: AnyDatabase]( *, request_headers: Mapping[str, str], request_body: bytes | None, request_method: str, request_path: str, - databases: Iterable[CloudDatabase], -) -> CloudDatabase: + databases: Iterable[DatabaseT], +) -> DatabaseT: """Return the first of the given databases which is being accessed by the given server request. diff --git a/src/mock_vws/_flask_server/target_manager.py b/src/mock_vws/_flask_server/target_manager.py index 045ed0a84..1a308e4dd 100644 --- a/src/mock_vws/_flask_server/target_manager.py +++ b/src/mock_vws/_flask_server/target_manager.py @@ -12,9 +12,9 @@ from flask import Flask, Response, request from pydantic_settings import BaseSettings -from mock_vws.database import CloudDatabase +from mock_vws.database import CloudDatabase, VuMarkDatabase from mock_vws.states import States -from mock_vws.target import ImageTarget +from mock_vws.target import ImageTarget, VuMarkTarget from mock_vws.target_manager import TargetManager from mock_vws.target_raters import ( BrisqueTargetTrackingRater, @@ -80,6 +80,25 @@ def delete_cloud_database(database_name: str) -> Response: return Response(response="", status=HTTPStatus.OK) +@TARGET_MANAGER_FLASK_APP.route( + rule="/vumark_databases/", + methods=[HTTPMethod.DELETE], +) +@beartype +def delete_vumark_database(database_name: str) -> Response: + """Delete a VuMark database. + + :status 200: The VuMark database has been deleted. + """ + (matching_database,) = { + database + for database in TARGET_MANAGER.vumark_databases + if database_name == database.database_name + } + TARGET_MANAGER.remove_vumark_database(vumark_database=matching_database) + return Response(response="", status=HTTPStatus.OK) + + @TARGET_MANAGER_FLASK_APP.route( rule="/cloud_databases", methods=[HTTPMethod.GET] ) @@ -95,6 +114,22 @@ def get_cloud_databases() -> Response: ) +@TARGET_MANAGER_FLASK_APP.route( + rule="/vumark_databases", + methods=[HTTPMethod.GET], +) +@beartype +def get_vumark_databases() -> Response: + """Return a list of all VuMark databases.""" + databases = [ + database.to_dict() for database in TARGET_MANAGER.vumark_databases + ] + return Response( + response=json.dumps(obj=databases), + status=HTTPStatus.OK, + ) + + @TARGET_MANAGER_FLASK_APP.route( rule="/cloud_databases", methods=[HTTPMethod.POST] ) @@ -194,6 +229,47 @@ def create_cloud_database() -> Response: ) +@TARGET_MANAGER_FLASK_APP.route( + rule="/vumark_databases", + methods=[HTTPMethod.POST], +) +@beartype +def create_vumark_database() -> Response: + """Create a new VuMark database. + + :status 201: The database has been successfully created. + """ + request_json = json.loads(s=request.data) + random_vumark_database = VuMarkDatabase() + database = VuMarkDatabase( + server_access_key=request_json.get( + "server_access_key", + random_vumark_database.server_access_key, + ), + server_secret_key=request_json.get( + "server_secret_key", + random_vumark_database.server_secret_key, + ), + database_name=request_json.get( + "database_name", + random_vumark_database.database_name, + ), + ) + + try: + TARGET_MANAGER.add_vumark_database(vumark_database=database) + except ValueError as exc: + return Response( + response=str(object=exc), + status=HTTPStatus.CONFLICT, + ) + + return Response( + response=json.dumps(obj=database.to_dict()), + status=HTTPStatus.CREATED, + ) + + @TARGET_MANAGER_FLASK_APP.route( rule="/cloud_databases//targets", methods=[HTTPMethod.POST], @@ -230,6 +306,28 @@ def create_target(database_name: str) -> Response: ) +@TARGET_MANAGER_FLASK_APP.route( + rule="/vumark_databases//vumark_targets", + methods=[HTTPMethod.POST], +) +@beartype +def create_vumark_target(database_name: str) -> Response: + """Create a new VuMark target in a given database.""" + (database,) = ( + database + for database in TARGET_MANAGER.vumark_databases + if database.database_name == database_name + ) + request_json = json.loads(s=request.data) + target = VuMarkTarget.from_dict(target_dict=request_json) + database.vumark_targets.add(target) + + return Response( + response=json.dumps(obj=target.to_dict()), + status=HTTPStatus.CREATED, + ) + + @TARGET_MANAGER_FLASK_APP.route( rule="/cloud_databases//targets/", methods={HTTPMethod.DELETE}, diff --git a/src/mock_vws/_flask_server/vws.py b/src/mock_vws/_flask_server/vws.py index a11cc5a69..1c98d1336 100644 --- a/src/mock_vws/_flask_server/vws.py +++ b/src/mock_vws/_flask_server/vws.py @@ -36,7 +36,7 @@ TargetStatusProcessingError, ValidatorError, ) -from mock_vws.database import CloudDatabase +from mock_vws.database import CloudDatabase, VuMarkDatabase from mock_vws.image_matchers import ( ExactMatcher, ImageMatcher, @@ -100,6 +100,21 @@ def get_all_cloud_databases() -> set[CloudDatabase]: } +@beartype +def get_all_vumark_databases() -> set[VuMarkDatabase]: + """Get all VuMark database objects from the task manager back-end.""" + settings = VWSSettings.model_validate(obj={}) + timeout_seconds = 30 + response = requests.get( + url=f"{settings.target_manager_base_url}/vumark_databases", + timeout=timeout_seconds, + ) + return { + VuMarkDatabase.from_dict(database_dict=database_dict) + for database_dict in response.json() + } + + @VWS_FLASK_APP.before_request def set_terminate_wsgi_input() -> None: """We set ``wsgi.input_terminated`` to ``True`` when going through @@ -129,14 +144,19 @@ def set_terminate_wsgi_input() -> None: @VWS_FLASK_APP.before_request @beartype def validate_request() -> None: - """Run validators on the request.""" - databases = get_all_cloud_databases() + """Run validators on the request. + + The VuMark endpoint does its own validation because it needs to + authenticate against both cloud and VuMark databases. + """ + if request.endpoint == "generate_vumark_instance": + return run_services_validators( request_headers=dict(request.headers), request_body=request.data, request_method=request.method, request_path=request.path, - databases=databases, + databases=get_all_cloud_databases(), ) @@ -357,6 +377,20 @@ def generate_vumark_instance(target_id: str) -> Response: Fake implementation of https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api#generate-instance """ + cloud_databases = get_all_cloud_databases() + vumark_databases = get_all_vumark_databases() + all_databases: list[CloudDatabase | VuMarkDatabase] = [ + *cloud_databases, + *vumark_databases, + ] + run_services_validators( + request_headers=dict(request.headers), + request_body=request.data, + request_method=request.method, + request_path=request.path, + databases=all_databases, + ) + # ``target_id`` is validated by request validators. del target_id diff --git a/src/mock_vws/_requests_mock_server/decorators.py b/src/mock_vws/_requests_mock_server/decorators.py index 60c698dc0..35535217b 100644 --- a/src/mock_vws/_requests_mock_server/decorators.py +++ b/src/mock_vws/_requests_mock_server/decorators.py @@ -12,7 +12,7 @@ from requests import PreparedRequest from responses import RequestsMock -from mock_vws.database import CloudDatabase +from mock_vws.database import CloudDatabase, VuMarkDatabase from mock_vws.image_matchers import ( ImageMatcher, StructuralSimilarityMatcher, @@ -140,6 +140,20 @@ def add_cloud_database(self, cloud_database: CloudDatabase) -> None: cloud_database=cloud_database, ) + def add_vumark_database(self, vumark_database: VuMarkDatabase) -> None: + """Add a VuMark database. + + Args: + vumark_database: The VuMark database to add. + + Raises: + ValueError: One of the given database keys matches a key for + an existing database. + """ + self._target_manager.add_vumark_database( + vumark_database=vumark_database, + ) + @staticmethod def _wrap_callback( callback: _Callback, diff --git a/src/mock_vws/_requests_mock_server/mock_web_services_api.py b/src/mock_vws/_requests_mock_server/mock_web_services_api.py index bbd782c24..5a5b7da96 100644 --- a/src/mock_vws/_requests_mock_server/mock_web_services_api.py +++ b/src/mock_vws/_requests_mock_server/mock_web_services_api.py @@ -12,7 +12,7 @@ import uuid from collections.abc import Callable, Iterable, Mapping from http import HTTPMethod, HTTPStatus -from typing import Any, ParamSpec, Protocol, runtime_checkable +from typing import TYPE_CHECKING, Any, ParamSpec, Protocol, runtime_checkable from zoneinfo import ZoneInfo from beartype import BeartypeConf, beartype @@ -41,6 +41,9 @@ from mock_vws.target_manager import TargetManager from mock_vws.target_raters import TargetTrackingRater +if TYPE_CHECKING: + from mock_vws.database import CloudDatabase, VuMarkDatabase + _TARGET_ID_PATTERN = "[A-Za-z0-9]+" @@ -309,12 +312,16 @@ def generate_vumark_instance( "application/pdf": VUMARK_PDF, } try: + all_databases: list[CloudDatabase | VuMarkDatabase] = [ + *self._target_manager.cloud_databases, + *self._target_manager.vumark_databases, + ] run_services_validators( request_headers=request.headers, request_body=_body_bytes(request=request), request_method=request.method or "", request_path=request.path_url, - databases=self._target_manager.cloud_databases, + databases=all_databases, ) accept = dict(request.headers).get("Accept", "") diff --git a/src/mock_vws/_services_validators/__init__.py b/src/mock_vws/_services_validators/__init__.py index e37c28d84..7a2e742a3 100644 --- a/src/mock_vws/_services_validators/__init__.py +++ b/src/mock_vws/_services_validators/__init__.py @@ -2,7 +2,7 @@ from collections.abc import Iterable, Mapping -from mock_vws.database import CloudDatabase +from mock_vws._database_matchers import AnyDatabase from .active_flag_validators import validate_active_flag from .auth_validators import ( @@ -55,7 +55,7 @@ def run_services_validators( request_headers: Mapping[str, str], request_body: bytes, request_method: str, - databases: Iterable[CloudDatabase], + databases: Iterable[AnyDatabase], ) -> None: """Run all validators. diff --git a/src/mock_vws/_services_validators/auth_validators.py b/src/mock_vws/_services_validators/auth_validators.py index a5922b7ae..1117b4636 100644 --- a/src/mock_vws/_services_validators/auth_validators.py +++ b/src/mock_vws/_services_validators/auth_validators.py @@ -6,12 +6,14 @@ from beartype import beartype -from mock_vws._database_matchers import get_database_matching_server_keys +from mock_vws._database_matchers import ( + AnyDatabase, + get_database_matching_server_keys, +) from mock_vws._services_validators.exceptions import ( AuthenticationFailureError, FailError, ) -from mock_vws.database import CloudDatabase _LOGGER = logging.getLogger(name=__name__) @@ -36,7 +38,7 @@ def validate_auth_header_exists(*, request_headers: Mapping[str, str]) -> None: def validate_access_key_exists( *, request_headers: Mapping[str, str], - databases: Iterable[CloudDatabase], + databases: Iterable[AnyDatabase], ) -> None: """Validate the authorization header includes an access key for a database. @@ -92,7 +94,7 @@ def validate_authorization( request_headers: Mapping[str, str], request_body: bytes, request_method: str, - databases: Iterable[CloudDatabase], + databases: Iterable[AnyDatabase], ) -> None: """Validate the authorization header given to a VWS endpoint. diff --git a/src/mock_vws/_services_validators/name_validators.py b/src/mock_vws/_services_validators/name_validators.py index 04db14721..abf1532c0 100644 --- a/src/mock_vws/_services_validators/name_validators.py +++ b/src/mock_vws/_services_validators/name_validators.py @@ -7,12 +7,14 @@ from beartype import beartype -from mock_vws._database_matchers import get_database_matching_server_keys +from mock_vws._database_matchers import ( + AnyDatabase, + get_database_matching_server_keys, +) from mock_vws._services_validators.exceptions import ( FailError, TargetNameExistError, ) -from mock_vws.database import CloudDatabase _LOGGER = logging.getLogger(name=__name__) @@ -116,7 +118,7 @@ def validate_name_length(*, request_body: bytes) -> None: @beartype def validate_name_does_not_exist_new_target( *, - databases: Iterable[CloudDatabase], + databases: Iterable[AnyDatabase], request_body: bytes, request_headers: Mapping[str, str], request_method: str, @@ -176,7 +178,7 @@ def validate_name_does_not_exist_existing_target( request_body: bytes, request_method: str, request_path: str, - databases: Iterable[CloudDatabase], + databases: Iterable[AnyDatabase], ) -> None: """Validate that the name does not exist for any existing target apart from diff --git a/src/mock_vws/_services_validators/project_state_validators.py b/src/mock_vws/_services_validators/project_state_validators.py index d4b263392..ac1fe97d2 100644 --- a/src/mock_vws/_services_validators/project_state_validators.py +++ b/src/mock_vws/_services_validators/project_state_validators.py @@ -6,7 +6,10 @@ from beartype import beartype -from mock_vws._database_matchers import get_database_matching_server_keys +from mock_vws._database_matchers import ( + AnyDatabase, + get_database_matching_server_keys, +) from mock_vws._services_validators.exceptions import ProjectInactiveError from mock_vws.database import CloudDatabase from mock_vws.states import States @@ -21,7 +24,7 @@ def validate_project_state( request_headers: Mapping[str, str], request_body: bytes, request_method: str, - databases: Iterable[CloudDatabase], + databases: Iterable[AnyDatabase], ) -> None: """Validate the state of the project. @@ -44,6 +47,9 @@ def validate_project_state( databases=databases, ) + if not isinstance(database, CloudDatabase): + return + if database.state != States.PROJECT_INACTIVE: return diff --git a/src/mock_vws/_services_validators/target_validators.py b/src/mock_vws/_services_validators/target_validators.py index ca5077ef2..58f1da0d7 100644 --- a/src/mock_vws/_services_validators/target_validators.py +++ b/src/mock_vws/_services_validators/target_validators.py @@ -5,9 +5,11 @@ from beartype import beartype -from mock_vws._database_matchers import get_database_matching_server_keys +from mock_vws._database_matchers import ( + AnyDatabase, + get_database_matching_server_keys, +) from mock_vws._services_validators.exceptions import UnknownTargetError -from mock_vws.database import CloudDatabase _LOGGER = logging.getLogger(name=__name__) _TARGETS_WITH_INSTANCE_PATH_LENGTH = 4 @@ -20,7 +22,7 @@ def validate_target_id_exists( request_headers: Mapping[str, str], request_body: bytes, request_method: str, - databases: Iterable[CloudDatabase], + databases: Iterable[AnyDatabase], ) -> None: """Validate that if a target ID is given, it exists in the database matching the request. diff --git a/src/mock_vws/database.py b/src/mock_vws/database.py index 0fb6876bc..b030a9e4e 100644 --- a/src/mock_vws/database.py +++ b/src/mock_vws/database.py @@ -9,7 +9,12 @@ from mock_vws._constants import TargetStatuses from mock_vws.states import States -from mock_vws.target import ImageTarget, ImageTargetDict +from mock_vws.target import ( + ImageTarget, + ImageTargetDict, + VuMarkTarget, + VuMarkTargetDict, +) @beartype @@ -25,6 +30,16 @@ class CloudDatabaseDict(TypedDict): targets: Iterable[ImageTargetDict] +@beartype +class VuMarkDatabaseDict(TypedDict): + """A dictionary type which represents a VuMark database.""" + + database_name: str + server_access_key: str + server_secret_key: str + vumark_targets: Iterable[VuMarkTargetDict] + + @beartype def _random_hex() -> str: """Return a random hex value.""" @@ -152,3 +167,59 @@ def processing_targets(self) -> set[ImageTarget]: for target in self.not_deleted_targets if target.status == TargetStatuses.PROCESSING.value } + + +@beartype +@dataclass(eq=True, frozen=True) +class VuMarkDatabase: + """Credentials for the VuMark generation API. + + Args: + database_name: The name of a VWS target manager database name. Defaults + to a random string. + server_access_key: A VWS server access key. Defaults to a random + string. + server_secret_key: A VWS server secret key. Defaults to a random + string. + """ + + database_name: str = field(default_factory=_random_hex, repr=False) + server_access_key: str = field(default_factory=_random_hex, repr=False) + server_secret_key: str = field(default_factory=_random_hex, repr=False) + # We have ``vumark_targets`` as ``hash=False`` so that we can have the + # class as ``frozen=True`` while still being able to keep the interface + # we want. + vumark_targets: set[VuMarkTarget] = field( + default_factory=set[VuMarkTarget], + hash=False, + ) + + def to_dict(self) -> VuMarkDatabaseDict: + """Dump a VuMark database to a dictionary which can be loaded as + JSON. + """ + vumark_targets = [target.to_dict() for target in self.vumark_targets] + return { + "database_name": self.database_name, + "server_access_key": self.server_access_key, + "server_secret_key": self.server_secret_key, + "vumark_targets": vumark_targets, + } + + @classmethod + def from_dict(cls, database_dict: VuMarkDatabaseDict) -> Self: + """Load a VuMark database from a dictionary.""" + return cls( + database_name=database_dict["database_name"], + server_access_key=database_dict["server_access_key"], + server_secret_key=database_dict["server_secret_key"], + vumark_targets={ + VuMarkTarget.from_dict(target_dict=target_dict) + for target_dict in database_dict["vumark_targets"] + }, + ) + + @property + def not_deleted_targets(self) -> set[VuMarkTarget]: + """All VuMark targets.""" + return set(self.vumark_targets) diff --git a/src/mock_vws/target.py b/src/mock_vws/target.py index a17e8c140..d381884d8 100644 --- a/src/mock_vws/target.py +++ b/src/mock_vws/target.py @@ -19,6 +19,13 @@ ) +class VuMarkTargetDict(TypedDict): + """A dictionary type which represents a VuMark target.""" + + target_id: str + name: str + + class ImageTargetDict(TypedDict): """A dictionary type which represents a target.""" @@ -208,3 +215,35 @@ def to_dict(self) -> ImageTargetDict: "upload_date": self.upload_date.isoformat(), "tracking_rating": self.tracking_rating, } + + +@beartype(conf=BeartypeConf(is_pep484_tower=True)) +@dataclass(frozen=True, eq=True) +class VuMarkTarget: + """ + A VuMark target as managed in + https://developer.vuforia.com/target-manager. + + Unlike ImageTarget, VuMark targets do not require an image — they use a + VuMark template. + """ + + name: str + target_id: str = field(default_factory=_random_hex) + + @classmethod + def from_dict(cls, target_dict: VuMarkTargetDict) -> Self: + """Load a VuMark target from a dictionary.""" + return cls( + target_id=target_dict["target_id"], + name=target_dict["name"], + ) + + def to_dict(self) -> VuMarkTargetDict: + """Dump a VuMark target to a dictionary which can be loaded as + JSON. + """ + return { + "target_id": self.target_id, + "name": self.name, + } diff --git a/src/mock_vws/target_manager.py b/src/mock_vws/target_manager.py index f5900f9b2..14850df8e 100644 --- a/src/mock_vws/target_manager.py +++ b/src/mock_vws/target_manager.py @@ -4,10 +4,10 @@ from beartype import beartype -from mock_vws.database import CloudDatabase +from mock_vws.database import CloudDatabase, VuMarkDatabase if TYPE_CHECKING: - from collections.abc import Iterable + from mock_vws._database_matchers import AnyDatabase @beartype @@ -19,8 +19,19 @@ class TargetManager: """ def __init__(self) -> None: - """Create a target manager with no cloud databases.""" - self._cloud_databases: Iterable[CloudDatabase] = set() + """Create a target manager with no databases.""" + self._cloud_databases: set[CloudDatabase] = set() + self._vumark_databases: set[VuMarkDatabase] = set() + + @property + def cloud_databases(self) -> set[CloudDatabase]: + """All cloud databases.""" + return set(self._cloud_databases) + + @property + def vumark_databases(self) -> set[VuMarkDatabase]: + """All VuMark databases.""" + return set(self._vumark_databases) def remove_cloud_database(self, cloud_database: CloudDatabase) -> None: """Remove a cloud database. @@ -35,6 +46,16 @@ def remove_cloud_database(self, cloud_database: CloudDatabase) -> None: db for db in self._cloud_databases if db != cloud_database } + def remove_vumark_database(self, vumark_database: VuMarkDatabase) -> None: + """Remove a VuMark database. + + Args: + vumark_database: The VuMark database to remove. + """ + self._vumark_databases = { + db for db in self._vumark_databases if db != vumark_database + } + def add_cloud_database(self, cloud_database: CloudDatabase) -> None: """Add a cloud database. @@ -47,9 +68,13 @@ def add_cloud_database(self, cloud_database: CloudDatabase) -> None: """ message_fmt = ( "All {key_name}s must be unique. " - 'There is already a cloud database with the {key_name} "{value}".' + 'There is already a database with the {key_name} "{value}".' ) - for existing_db in self.cloud_databases: + all_databases: list[AnyDatabase] = [ + *self._cloud_databases, + *self._vumark_databases, + ] + for existing_db in all_databases: for existing, new, key_name in ( ( existing_db.server_access_key, @@ -62,18 +87,67 @@ def add_cloud_database(self, cloud_database: CloudDatabase) -> None: "server secret key", ), ( - existing_db.client_access_key, + existing_db.database_name, + cloud_database.database_name, + "name", + ), + ): + if existing == new: + message = message_fmt.format(key_name=key_name, value=new) + raise ValueError(message) + + for existing_cloud_db in self._cloud_databases: + for existing, new, key_name in ( + ( + existing_cloud_db.client_access_key, cloud_database.client_access_key, "client access key", ), ( - existing_db.client_secret_key, + existing_cloud_db.client_secret_key, cloud_database.client_secret_key, "client secret key", ), + ): + if existing == new: + message = message_fmt.format(key_name=key_name, value=new) + raise ValueError(message) + + self._cloud_databases = {*self._cloud_databases, cloud_database} + + def add_vumark_database(self, vumark_database: VuMarkDatabase) -> None: + """Add a VuMark database. + + Args: + vumark_database: The VuMark database to add. + + Raises: + ValueError: One of the given database keys matches a key for + an existing database. + """ + message_fmt = ( + "All {key_name}s must be unique. " + 'There is already a database with the {key_name} "{value}".' + ) + all_databases: list[AnyDatabase] = [ + *self._cloud_databases, + *self._vumark_databases, + ] + for existing_db in all_databases: + for existing, new, key_name in ( + ( + existing_db.server_access_key, + vumark_database.server_access_key, + "server access key", + ), + ( + existing_db.server_secret_key, + vumark_database.server_secret_key, + "server secret key", + ), ( existing_db.database_name, - cloud_database.database_name, + vumark_database.database_name, "name", ), ): @@ -81,9 +155,4 @@ def add_cloud_database(self, cloud_database: CloudDatabase) -> None: message = message_fmt.format(key_name=key_name, value=new) raise ValueError(message) - self._cloud_databases = {*self._cloud_databases, cloud_database} - - @property - def cloud_databases(self) -> set[CloudDatabase]: - """All cloud databases.""" - return set(self._cloud_databases) + self._vumark_databases = {*self._vumark_databases, vumark_database} diff --git a/tests/mock_vws/fixtures/vuforia_backends.py b/tests/mock_vws/fixtures/vuforia_backends.py index 7eae10e4e..6f5ea90ba 100644 --- a/tests/mock_vws/fixtures/vuforia_backends.py +++ b/tests/mock_vws/fixtures/vuforia_backends.py @@ -19,12 +19,10 @@ from mock_vws._flask_server.target_manager import TARGET_MANAGER_FLASK_APP from mock_vws._flask_server.vwq import CLOUDRECO_FLASK_APP from mock_vws._flask_server.vws import VWS_FLASK_APP -from mock_vws.database import CloudDatabase +from mock_vws.database import CloudDatabase, VuMarkDatabase from mock_vws.states import States -from mock_vws.target import ImageTarget -from mock_vws.target_raters import HardcodedTargetTrackingRater +from mock_vws.target import VuMarkTarget from tests.mock_vws.fixtures.credentials import VuMarkCloudDatabase -from tests.mock_vws.utils import make_image_file from tests.mock_vws.utils.retries import RETRY_ON_TOO_MANY_REQUESTS LOGGER = logging.getLogger(name=__name__) @@ -65,28 +63,19 @@ def _delete_all_targets(*, database_keys: CloudDatabase) -> None: def _vumark_database( *, vumark_vuforia_database: VuMarkCloudDatabase, -) -> CloudDatabase: - """Return a database with a target for VuMark instance generation.""" - vumark_target = ImageTarget( - active_flag=True, - application_metadata=None, - image_value=make_image_file( - file_format="PNG", - color_space="RGB", - width=8, - height=8, - ).getvalue(), +) -> VuMarkDatabase: + """Return a database with a VuMark target for VuMark instance + generation. + """ + vumark_target = VuMarkTarget( name="mock-vumark-target", - processing_time_seconds=0, - width=1, - target_tracking_rater=HardcodedTargetTrackingRater(rating=5), target_id=vumark_vuforia_database.target_id, ) - return CloudDatabase( + return VuMarkDatabase( database_name=vumark_vuforia_database.target_manager_database_name, server_access_key=vumark_vuforia_database.server_access_key, server_secret_key=vumark_vuforia_database.server_secret_key, - targets={vumark_target}, + vumark_targets={vumark_target}, ) @@ -139,7 +128,7 @@ def _enable_use_mock_vuforia( with MockVWS() as mock: mock.add_cloud_database(cloud_database=working_database) mock.add_cloud_database(cloud_database=inactive_database) - mock.add_cloud_database(cloud_database=vumark_database) + mock.add_vumark_database(vumark_database=vumark_database) yield @@ -175,7 +164,7 @@ def _enable_use_docker_in_memory( vumark_database = _vumark_database( vumark_vuforia_database=vumark_vuforia_database, ) - (vumark_target,) = vumark_database.targets + (vumark_target,) = vumark_database.vumark_targets with responses.RequestsMock(assert_all_requests_are_fired=False) as mock: add_flask_app_to_mock( @@ -196,32 +185,44 @@ def _enable_use_docker_in_memory( base_url=target_manager_base_url, ) - databases_url = target_manager_base_url + "/cloud_databases" - databases = requests.get(url=databases_url, timeout=30).json() - for database in databases: - database_name = database["database_name"] + cloud_databases_url = target_manager_base_url + "/cloud_databases" + vumark_databases_url = target_manager_base_url + "/vumark_databases" + + for database in requests.get( + url=cloud_databases_url, timeout=30 + ).json(): + requests.delete( + url=cloud_databases_url + "/" + database["database_name"], + timeout=30, + ) + for database in requests.get( + url=vumark_databases_url, timeout=30 + ).json(): requests.delete( - url=databases_url + "/" + database_name, + url=vumark_databases_url + "/" + database["database_name"], timeout=30, ) requests.post( - url=databases_url, + url=cloud_databases_url, json=working_database.to_dict(), timeout=30, ) requests.post( - url=databases_url, + url=cloud_databases_url, json=inactive_database.to_dict(), timeout=30, ) requests.post( - url=databases_url, + url=vumark_databases_url, json=vumark_database.to_dict(), timeout=30, ) requests.post( - url=(f"{databases_url}/{vumark_database.database_name}/targets"), + url=( + f"{vumark_databases_url}" + f"/{vumark_database.database_name}/vumark_targets" + ), json=vumark_target.to_dict(), timeout=30, ) diff --git a/tests/mock_vws/test_flask_app_usage.py b/tests/mock_vws/test_flask_app_usage.py index 49578e0b1..5ce6ad1aa 100644 --- a/tests/mock_vws/test_flask_app_usage.py +++ b/tests/mock_vws/test_flask_app_usage.py @@ -18,7 +18,7 @@ from mock_vws._flask_server.target_manager import TARGET_MANAGER_FLASK_APP from mock_vws._flask_server.vwq import CLOUDRECO_FLASK_APP from mock_vws._flask_server.vws import VWS_FLASK_APP -from mock_vws.database import CloudDatabase +from mock_vws.database import CloudDatabase, VuMarkDatabase from tests.mock_vws.utils.usage_test_helpers import ( processing_time_seconds, ) @@ -131,23 +131,23 @@ def test_duplicate_keys() -> None: server_access_key_conflict_error = ( "All server access keys must be unique. " - 'There is already a cloud database with the server access key "1".' + 'There is already a database with the server access key "1".' ) server_secret_key_conflict_error = ( "All server secret keys must be unique. " - 'There is already a cloud database with the server secret key "2".' + 'There is already a database with the server secret key "2".' ) client_access_key_conflict_error = ( "All client access keys must be unique. " - 'There is already a cloud database with the client access key "3".' + 'There is already a database with the client access key "3".' ) client_secret_key_conflict_error = ( "All client secret keys must be unique. " - 'There is already a cloud database with the client secret key "4".' + 'There is already a database with the client secret key "4".' ) database_name_conflict_error = ( "All names must be unique. " - 'There is already a cloud database with the name "5".' + 'There is already a database with the name "5".' ) databases_url = _EXAMPLE_URL_FOR_TARGET_MANAGER + "/cloud_databases" @@ -169,6 +169,52 @@ def test_duplicate_keys() -> None: assert response.status_code == HTTPStatus.CONFLICT assert response.text == expected_message + @staticmethod + def test_duplicate_vumark_keys() -> None: + """ + It is not possible to have multiple databases with matching + keys, including VuMark databases. + """ + database = VuMarkDatabase( + server_access_key="v1", + server_secret_key="v2", + database_name="v3", + ) + + bad_server_access_key_db = VuMarkDatabase(server_access_key="v1") + bad_server_secret_key_db = VuMarkDatabase(server_secret_key="v2") + bad_database_name_db = VuMarkDatabase(database_name="v3") + + server_access_key_conflict_error = ( + "All server access keys must be unique. " + 'There is already a database with the server access key "v1".' + ) + server_secret_key_conflict_error = ( + "All server secret keys must be unique. " + 'There is already a database with the server secret key "v2".' + ) + database_name_conflict_error = ( + "All names must be unique. " + 'There is already a database with the name "v3".' + ) + + databases_url = _EXAMPLE_URL_FOR_TARGET_MANAGER + "/vumark_databases" + requests.post(url=databases_url, json=database.to_dict(), timeout=30) + + for bad_database, expected_message in ( + (bad_server_access_key_db, server_access_key_conflict_error), + (bad_server_secret_key_db, server_secret_key_conflict_error), + (bad_database_name_db, database_name_conflict_error), + ): + response = requests.post( + url=databases_url, + json=bad_database.to_dict(), + timeout=30, + ) + + assert response.status_code == HTTPStatus.CONFLICT + assert response.text == expected_message + @staticmethod def test_give_no_details(high_quality_image: io.BytesIO) -> None: """It is possible to create a database without giving any data.""" diff --git a/tests/mock_vws/test_requests_mock_usage.py b/tests/mock_vws/test_requests_mock_usage.py index 37365bb49..cc27c8bf7 100644 --- a/tests/mock_vws/test_requests_mock_usage.py +++ b/tests/mock_vws/test_requests_mock_usage.py @@ -16,7 +16,7 @@ from vws_auth_tools import rfc_1123_date from mock_vws import MissingSchemeError, MockVWS -from mock_vws.database import CloudDatabase +from mock_vws.database import CloudDatabase, VuMarkDatabase from mock_vws.image_matchers import ExactMatcher, StructuralSimilarityMatcher from mock_vws.target import ImageTarget from tests.mock_vws.utils import Endpoint @@ -528,23 +528,23 @@ def test_duplicate_keys() -> None: server_access_key_conflict_error = ( "All server access keys must be unique. " - 'There is already a cloud database with the server access key "1".' + 'There is already a database with the server access key "1".' ) server_secret_key_conflict_error = ( "All server secret keys must be unique. " - 'There is already a cloud database with the server secret key "2".' + 'There is already a database with the server secret key "2".' ) client_access_key_conflict_error = ( "All client access keys must be unique. " - 'There is already a cloud database with the client access key "3".' + 'There is already a database with the client access key "3".' ) client_secret_key_conflict_error = ( "All client secret keys must be unique. " - 'There is already a cloud database with the client secret key "4".' + 'There is already a database with the client secret key "4".' ) database_name_conflict_error = ( "All names must be unique. " - 'There is already a cloud database with the name "5".' + 'There is already a database with the name "5".' ) with MockVWS() as mock: @@ -562,6 +562,48 @@ def test_duplicate_keys() -> None: ): mock.add_cloud_database(cloud_database=bad_database) + @staticmethod + def test_duplicate_vumark_keys() -> None: + """ + It is not possible to have multiple databases with matching + keys, including VuMark databases. + """ + database = VuMarkDatabase( + server_access_key="1", + server_secret_key="2", + database_name="3", + ) + + bad_server_access_key_db = VuMarkDatabase(server_access_key="1") + bad_server_secret_key_db = VuMarkDatabase(server_secret_key="2") + bad_database_name_db = VuMarkDatabase(database_name="3") + + server_access_key_conflict_error = ( + "All server access keys must be unique. " + 'There is already a database with the server access key "1".' + ) + server_secret_key_conflict_error = ( + "All server secret keys must be unique. " + 'There is already a database with the server secret key "2".' + ) + database_name_conflict_error = ( + "All names must be unique. " + 'There is already a database with the name "3".' + ) + + with MockVWS() as mock: + mock.add_vumark_database(vumark_database=database) + for bad_database, expected_message in ( + (bad_server_access_key_db, server_access_key_conflict_error), + (bad_server_secret_key_db, server_secret_key_conflict_error), + (bad_database_name_db, database_name_conflict_error), + ): + with pytest.raises( + expected_exception=ValueError, + match=expected_message + "$", + ): + mock.add_vumark_database(vumark_database=bad_database) + class TestQueryImageMatchers: """Tests for query image matchers."""