Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions annif/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,19 @@ def create_cx_app(config_name: str | None = None) -> FlaskApp:
import connexion
from connexion.datastructures import MediaTypeDict
from connexion.middleware import MiddlewarePosition
from connexion.validators import FormDataValidator, MultiPartFormDataValidator
from connexion.validators import MultiPartFormDataValidator
from starlette.middleware.cors import CORSMiddleware

import annif.registry
from annif.openapi.validation import CustomRequestBodyValidator
from annif.openapi.validation import (
CustomFormDataValidator,
CustomRequestBodyValidator,
)

specdir = os.path.join(os.path.dirname(__file__), "openapi")
cxapp = connexion.FlaskApp(__name__, specification_dir=specdir)
cxapp = connexion.FlaskApp(
__name__, specification_dir=specdir, strict_validation=True
)
config_name = _get_config_name(config_name)
logger.debug(f"creating connexion app with configuration {config_name}")
cxapp.app.config.from_object(config_name)
Expand All @@ -54,7 +59,7 @@ def create_cx_app(config_name: str | None = None) -> FlaskApp:
"body": MediaTypeDict(
{
"*/*json": CustomRequestBodyValidator,
"application/x-www-form-urlencoded": FormDataValidator,
"application/x-www-form-urlencoded": CustomFormDataValidator,
"multipart/form-data": MultiPartFormDataValidator,
}
),
Expand Down
3 changes: 3 additions & 0 deletions annif/openapi/annif.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,8 @@ paths:
content:
application/json:
{}
"400":
$ref: '#/components/responses/BadRequest'
"403":
$ref: '#/components/responses/NotAllowed'
"404":
Expand Down Expand Up @@ -232,6 +234,7 @@ paths:
description: candidate languages as IETF BCP 47 codes
items:
type: string
format: bcp47
maxLength: 3
minLength: 2
example: en
Expand Down
24 changes: 22 additions & 2 deletions annif/openapi/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
import logging
from typing import Any

from connexion.exceptions import BadRequestProblem
from connexion.exceptions import BadRequestProblem, ExtraParameterProblem
from connexion.json_schema import format_error_with_path
from connexion.validators import JSONRequestBodyValidator
from connexion.validators import FormDataValidator, JSONRequestBodyValidator
from jsonschema.exceptions import ValidationError

logger = logging.getLogger("openapi.validation")
Expand Down Expand Up @@ -37,3 +37,23 @@ def _validate(self, body: Any) -> dict | None:
extra={"validator": "body"},
)
raise BadRequestProblem(detail=f"{exception.message}{error_path_msg}")


class CustomFormDataValidator(FormDataValidator):
"""Custom request body validator that allows additional metadata fields starting
with 'metadata_' in the request body while rejecting other fields."""

def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)

def _validate_params_strictly(self, data: dict) -> None:
form_params = data.keys()
spec_params = self._schema.get("properties", {}).keys()

reduced_form_params = [
fp for fp in form_params if not fp.startswith("metadata_")
]

errors = set(reduced_form_params).difference(set(spec_params))
if errors:
raise ExtraParameterProblem(param_type="formData", extra_params=errors)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ flake8 = "*"
bumpversion = "*"
black = "25.*"
isort = "*"
schemathesis = "3.*.*"
schemathesis = "4.*"

[project.urls]
homepage = "https://annif.org"
Expand Down
15 changes: 10 additions & 5 deletions tests/test_openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@

import pytest
import schemathesis
from hypothesis import settings
from hypothesis import HealthCheck, settings, strategies
from simplemma.strategies.dictionaries.dictionary_factory import SUPPORTED_LANGUAGES

import annif

bcp47_strategy = strategies.sampled_from(SUPPORTED_LANGUAGES)
schemathesis.openapi.format("bcp47", bcp47_strategy)


cxapp = annif.create_app(config_name="annif.default_config.TestingConfig")
schema = schemathesis.from_path("annif/openapi/annif.yaml", app=cxapp)
schema = schemathesis.openapi.from_asgi("/v1/openapi.json", app=cxapp)


@schemathesis.hook("filter_path_parameters")
Expand All @@ -20,14 +25,14 @@ def filter_path_parameters(context, path_parameters):


@schema.parametrize()
@settings(max_examples=10)
@settings(max_examples=10, suppress_health_check=[HealthCheck.filter_too_much])
def test_openapi_fuzzy(case):
case.call_and_validate()


@pytest.mark.slow
@schema.include(path_regex="/v1/projects/{project_id}").parametrize()
@settings(max_examples=50)
@schema.include(path_regex="projects/{project_id}").parametrize()
@settings(max_examples=50, suppress_health_check=[HealthCheck.filter_too_much])
def test_openapi_fuzzy_target_dummy_fi(case):
case.path_parameters = {"project_id": "dummy-fi"}
case.call_and_validate()
Expand Down
Loading