Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: OptionsFormValidationError web error #127

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
Open
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
16 changes: 12 additions & 4 deletions docs/qppe-server.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -106,17 +106,17 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/QuestionCreated"
400:
404:
description: Package not found.
422:
description: Validation error
headers:
Content-Language:
$ref: '#/components/headers/ContentLanguage'
content:
application/json:
schema:
type: object
404:
description: Package not found.
$ref: "#/components/schemas/OptionsFormValidationError"
500:
description: Error occurred.
content:
Expand Down Expand Up @@ -1067,6 +1067,14 @@ components:
description: Optional human-readable reason for the error.
required: [ error_code, temporary ]

OptionsFormValidationError:
allOf:
- $ref: "#/components/schemas/RequestError"
- type: object
properties:
errors:
type: object

QuestionStateMigrationError:
type: object
properties:
Expand Down
5 changes: 5 additions & 0 deletions questionpy_server/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ class RequestErrorCode(Enum):
INVALID_QUESTION_STATE = "INVALID_QUESTION_STATE"
INVALID_PACKAGE = "INVALID_PACKAGE"
INVALID_REQUEST = "INVALID_REQUEST"
INVALID_OPTIONS_FORM = "INVALID_OPTIONS_FORM"
PACKAGE_ERROR = "PACKAGE_ERROR"
PACKAGE_NOT_FOUND = "PACKAGE_NOT_FOUND"
CALLBACK_API_ERROR = "CALLBACK_API_ERROR"
Expand All @@ -113,6 +114,10 @@ class RequestError(BaseModel):
reason: str | None = None


class OptionsFormValidationError(RequestError):
errors: dict[str, str]


class QuestionStateMigrationErrorCode(Enum):
NOT_IMPLEMENTED = "NOT_IMPLEMENTED"
DOWNGRADE_NOT_POSSIBLE = "DOWNGRADE_NOT_POSSIBLE"
Expand Down
4 changes: 3 additions & 1 deletion questionpy_server/web/_middlewares.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from aiohttp.web_response import StreamResponse

import questionpy_server.web.errors as web_error
from questionpy_common.api.qtype import InvalidAttemptStateError, InvalidQuestionStateError
from questionpy_common.api.qtype import InvalidAttemptStateError, InvalidQuestionStateError, OptionsFormValidationError
from questionpy_common.error import QPyBaseError
from questionpy_server.worker.exception import (
StaticFileSizeMismatchError,
Expand Down Expand Up @@ -50,6 +50,8 @@ async def error_middleware(request: Request, handler: Handler) -> StreamResponse
except tuple(exception_map.keys()) as e:
exception = exception_map[type(e)]
return exception(reason=e.reason, temporary=e.temporary)
except OptionsFormValidationError as e:
return web_error.InvalidOptionsFormError(reason=e.reason, errors=e.errors)
except Exception: # noqa: BLE001
web_logger.exception("There was an unexpected error while processing the request.")
return web_error.ServerError(reason="unknown", temporary=True)
Expand Down
15 changes: 14 additions & 1 deletion questionpy_server/web/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from aiohttp import web
from aiohttp.log import web_logger

from questionpy_server.models import RequestError, RequestErrorCode
from questionpy_server.models import OptionsFormValidationError, RequestError, RequestErrorCode


class _ExceptionMixin(web.HTTPException):
Expand Down Expand Up @@ -95,6 +95,19 @@ def __init__(self, *, reason: str | None, **_: Any) -> None:
)


class InvalidOptionsFormError(web.HTTPUnprocessableEntity, _ExceptionMixin):
def __init__(self, *, reason: str | None, errors: dict[str, str], **_: Any) -> None:
super().__init__(
"Invalid form data was provided",
OptionsFormValidationError(
error_code=RequestErrorCode.INVALID_OPTIONS_FORM,
reason=reason,
temporary=False,
errors=errors,
),
)


class PackageError(web.HTTPInternalServerError, _ExceptionMixin):
def __init__(self, *, reason: str | None, temporary: bool) -> None:
super().__init__(
Expand Down
31 changes: 30 additions & 1 deletion tests/questionpy_server/web/test_error_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
from typing import Any, NoReturn

import pytest
from aiohttp import web
from aiohttp import MultipartWriter, web
from aiohttp.pytest_plugin import AiohttpClient
from aiohttp.test_utils import TestClient
from aiohttp.web_exceptions import HTTPBadRequest, HTTPException, HTTPMethodNotAllowed, HTTPNotFound

from questionpy_common.api.qtype import InvalidQuestionStateError
Expand All @@ -30,6 +31,7 @@
WorkerStartError,
)
from questionpy_server.worker.runtime.messages import WorkerMemoryLimitExceededError, WorkerUnknownError
from tests.conftest import PACKAGE


def error_server(error: Exception) -> web.Application:
Expand Down Expand Up @@ -156,3 +158,30 @@ async def test_unexpected_exception_should_return_server_error(
assert logger_name == "aiohttp.web"
assert "unexpected error" in message
assert log_level == logging.ERROR


async def test_invalid_options_form_data_error(client: TestClient) -> None:
with PACKAGE.path.open("rb") as package_fd, MultipartWriter("form-data") as writer:
part = writer.append(package_fd)
part.set_content_disposition("form-data", name="package")

part = writer.append_json({"form_data": {}})
part.set_content_disposition("form-data", name="main")

res = await client.post(
f"/packages/{PACKAGE.hash}/question",
data=writer,
)

assert res.status == 422
data = await res.json()
assert (
data.items()
>= {
"error_code": RequestErrorCode.INVALID_OPTIONS_FORM.value,
"temporary": False,
"reason": None,
}.items()
)

assert data["errors"].items() == {"my_hidden": "Field required", "my_repetition": "Field required"}.items()
Loading