extract schemas automatically from views

This commit is contained in:
Evgenii Alekseev 2023-04-04 13:45:18 +03:00
parent 8f4a2547e8
commit e08ab2db10
15 changed files with 84 additions and 75 deletions

View File

@ -213,9 +213,9 @@ class Configuration(configparser.RawConfigParser):
if self.has_section(full_section):
return full_section, section
# okay lets just use section as type
if not self.has_section(section):
raise configparser.NoSectionError(section)
return section, section
if self.has_section(section):
return section, section
raise configparser.NoSectionError(section)
def load(self, path: Path) -> None:
"""

View File

@ -173,7 +173,7 @@ class BaseView(View, CorsViewMixin):
Raises:
HTTPMethodNotAllowed: in case if there is no GET method implemented
"""
get_method: Optional[Callable[[], Awaitable[StreamResponse]]] = getattr(self, "get", None)
get_method: Optional[Callable[..., Awaitable[StreamResponse]]] = getattr(self, "get", None)
# using if/else in order to suppress mypy warning which doesn't know that
# ``_raise_allowed_methods`` raises exception
if get_method is not None:

View File

@ -3,8 +3,9 @@ import pytest
from asyncio import BaseEventLoop
from aiohttp.web import Application, Resource, UrlMappingMatchInfo
from aiohttp.test_utils import TestClient
from marshmallow import Schema
from pytest_mock import MockerFixture
from typing import Any, Dict, Optional
from typing import Any, Awaitable, Callable, Dict, Optional
from unittest.mock import MagicMock
import ahriman.core.auth.helpers
@ -54,6 +55,41 @@ def request(application: Application, path: str, method: str, json: Any = None,
return request_mock
@pytest.helpers.register
def schema_request(handler: Callable[..., Awaitable[Any]], *, location: str = "json") -> Schema:
"""
extract request schema from docs
Args:
handler(Callable[[], Awaitable[Any]]): request handler
location(str, optional): location of the request (Default value = "json")
Returns:
Schema: request schema as set by the decorators
"""
schemas: List[Dict[str, Any]] = handler.__schemas__ # type: ignore
return next(schema["schema"] for schema in schemas if schema["put_into"] == location)
@pytest.helpers.register
def schema_response(handler: Callable[..., Awaitable[Any]], *, code: int = 200) -> Schema:
"""
extract response schema from docs
Args:
handler(Callable[[], Awaitable[Any]]): request handler
code(int, optional): return code of the request (Default value = 200)
Returns:
Schema: response schema as set by the decorators
"""
schemas: Dict[int, Any] = handler.__apispec__["responses"] # type: ignore
schema = schemas[code]["schema"]
if callable(schema):
schema = schema()
return schema
@pytest.fixture
def application(configuration: Configuration, spawner: Spawn, database: SQLite, repository: Repository,
mocker: MockerFixture) -> Application:

View File

@ -4,8 +4,6 @@ from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas.error_schema import ErrorSchema
from ahriman.web.schemas.package_names_schema import PackageNamesSchema
from ahriman.web.views.service.add import AddView
@ -23,7 +21,7 @@ async def test_post(client: TestClient, mocker: MockerFixture) -> None:
must call post request correctly
"""
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add")
request_schema = PackageNamesSchema()
request_schema = pytest.helpers.schema_request(AddView.post)
payload = {"packages": ["ahriman"]}
assert not request_schema.validate(payload)
@ -37,7 +35,7 @@ async def test_post_empty(client: TestClient, mocker: MockerFixture) -> None:
must call raise 400 on empty request
"""
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add")
response_schema = ErrorSchema()
response_schema = pytest.helpers.schema_response(AddView.post, code=400)
response = await client.post("/api/v1/service/add", json={"packages": [""]})
assert response.status == 400

View File

@ -4,9 +4,6 @@ from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas.error_schema import ErrorSchema
from ahriman.web.schemas.pgp_key_id_schema import PGPKeyIdSchema
from ahriman.web.schemas.pgp_key_schema import PGPKeySchema
from ahriman.web.views.service.pgp import PGPView
@ -27,8 +24,8 @@ async def test_get(client: TestClient, mocker: MockerFixture) -> None:
must retrieve key from the keyserver
"""
import_mock = mocker.patch("ahriman.core.sign.gpg.GPG.key_download", return_value="imported")
request_schema = PGPKeyIdSchema()
response_schema = PGPKeySchema()
request_schema = pytest.helpers.schema_request(PGPView.get, location="querystring")
response_schema = pytest.helpers.schema_response(PGPView.get)
payload = {"key": "0xdeadbeaf", "server": "keyserver.ubuntu.com"}
assert not request_schema.validate(payload)
@ -44,7 +41,7 @@ async def test_get_empty(client: TestClient, mocker: MockerFixture) -> None:
must raise 400 on missing parameters
"""
import_mock = mocker.patch("ahriman.core.sign.gpg.GPG.key_download")
response_schema = ErrorSchema()
response_schema = pytest.helpers.schema_response(PGPView.get, code=400)
response = await client.get("/api/v1/service/pgp")
assert response.status == 400
@ -57,7 +54,7 @@ async def test_get_process_exception(client: TestClient, mocker: MockerFixture)
must raise 404 on invalid PGP server response
"""
import_mock = mocker.patch("ahriman.core.sign.gpg.GPG.key_download", side_effect=Exception())
response_schema = ErrorSchema()
response_schema = pytest.helpers.schema_response(PGPView.get, code=400)
response = await client.get("/api/v1/service/pgp", params={"key": "0xdeadbeaf", "server": "keyserver.ubuntu.com"})
assert response.status == 404
@ -70,7 +67,7 @@ async def test_post(client: TestClient, mocker: MockerFixture) -> None:
must call post request correctly
"""
import_mock = mocker.patch("ahriman.core.spawn.Spawn.key_import")
request_schema = PGPKeyIdSchema()
request_schema = pytest.helpers.schema_request(PGPView.post)
payload = {"key": "0xdeadbeaf", "server": "keyserver.ubuntu.com"}
assert not request_schema.validate(payload)
@ -84,7 +81,7 @@ async def test_post_exception(client: TestClient, mocker: MockerFixture) -> None
must raise exception on missing key payload
"""
import_mock = mocker.patch("ahriman.core.spawn.Spawn.key_import")
response_schema = ErrorSchema()
response_schema = pytest.helpers.schema_response(PGPView.post, code=400)
response = await client.post("/api/v1/service/pgp")
assert response.status == 400

View File

@ -4,8 +4,6 @@ from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas.error_schema import ErrorSchema
from ahriman.web.schemas.package_names_schema import PackageNamesSchema
from ahriman.web.views.service.rebuild import RebuildView
@ -23,7 +21,7 @@ async def test_post(client: TestClient, mocker: MockerFixture) -> None:
must call post request correctly
"""
rebuild_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_rebuild")
request_schema = PackageNamesSchema()
request_schema = pytest.helpers.schema_request(RebuildView.post)
payload = {"packages": ["python", "ahriman"]}
assert not request_schema.validate(payload)
@ -37,7 +35,7 @@ async def test_post_exception(client: TestClient, mocker: MockerFixture) -> None
must raise exception on missing packages payload
"""
rebuild_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_rebuild")
response_schema = ErrorSchema()
response_schema = pytest.helpers.schema_response(RebuildView.post, code=400)
response = await client.post("/api/v1/service/rebuild")
assert response.status == 400

View File

@ -4,8 +4,6 @@ from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas.error_schema import ErrorSchema
from ahriman.web.schemas.package_names_schema import PackageNamesSchema
from ahriman.web.views.service.remove import RemoveView
@ -23,7 +21,7 @@ async def test_post(client: TestClient, mocker: MockerFixture) -> None:
must call post request correctly
"""
remove_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_remove")
request_schema = PackageNamesSchema()
request_schema = pytest.helpers.schema_request(RemoveView.post)
payload = {"packages": ["ahriman"]}
assert not request_schema.validate(payload)
@ -37,7 +35,7 @@ async def test_post_exception(client: TestClient, mocker: MockerFixture) -> None
must raise exception on missing packages payload
"""
remove_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_remove")
response_schema = ErrorSchema()
response_schema = pytest.helpers.schema_response(RemoveView.post, code=400)
response = await client.post("/api/v1/service/remove")
assert response.status == 400

View File

@ -4,8 +4,6 @@ from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas.error_schema import ErrorSchema
from ahriman.web.schemas.package_names_schema import PackageNamesSchema
from ahriman.web.views.service.request import RequestView
@ -23,7 +21,7 @@ async def test_post(client: TestClient, mocker: MockerFixture) -> None:
must call post request correctly
"""
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add")
request_schema = PackageNamesSchema()
request_schema = pytest.helpers.schema_request(RequestView.post)
payload = {"packages": ["ahriman"]}
assert not request_schema.validate(payload)
@ -37,7 +35,7 @@ async def test_post_exception(client: TestClient, mocker: MockerFixture) -> None
must raise exception on missing packages payload
"""
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add")
response_schema = ErrorSchema()
response_schema = pytest.helpers.schema_response(RequestView.post, code=400)
response = await client.post("/api/v1/service/request")
assert response.status == 400

View File

@ -5,9 +5,6 @@ from pytest_mock import MockerFixture
from ahriman.models.aur_package import AURPackage
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas.aur_package_schema import AURPackageSchema
from ahriman.web.schemas.error_schema import ErrorSchema
from ahriman.web.schemas.search_schema import SearchSchema
from ahriman.web.views.service.search import SearchView
@ -25,8 +22,8 @@ async def test_get(client: TestClient, aur_package_ahriman: AURPackage, mocker:
must call get request correctly
"""
mocker.patch("ahriman.core.alpm.remote.AUR.multisearch", return_value=[aur_package_ahriman])
request_schema = SearchSchema()
response_schema = AURPackageSchema()
request_schema = pytest.helpers.schema_request(SearchView.get, location="querystring")
response_schema = pytest.helpers.schema_response(SearchView.get)
payload = {"for": ["ahriman"]}
assert not request_schema.validate(payload)
@ -42,7 +39,7 @@ async def test_get_exception(client: TestClient, mocker: MockerFixture) -> None:
must raise 400 on empty search string
"""
search_mock = mocker.patch("ahriman.core.alpm.remote.AUR.multisearch")
response_schema = ErrorSchema()
response_schema = pytest.helpers.schema_response(SearchView.get, code=400)
response = await client.get("/api/v1/service/search")
assert response.status == 400
@ -55,7 +52,7 @@ async def test_get_empty(client: TestClient, mocker: MockerFixture) -> None:
must raise 404 on empty search result
"""
mocker.patch("ahriman.core.alpm.remote.AUR.multisearch", return_value=[])
response_schema = ErrorSchema()
response_schema = pytest.helpers.schema_response(SearchView.get, code=404)
response = await client.get("/api/v1/service/search", params={"for": ["ahriman"]})
assert response.status == 404
@ -67,7 +64,7 @@ async def test_get_join(client: TestClient, mocker: MockerFixture) -> None:
must join search args with space
"""
search_mock = mocker.patch("ahriman.core.alpm.remote.AUR.multisearch")
request_schema = SearchSchema()
request_schema = pytest.helpers.schema_request(SearchView.get, location="querystring")
payload = {"for": ["ahriman", "maybe"]}
assert not request_schema.validate(payload)

View File

@ -5,9 +5,6 @@ from aiohttp.test_utils import TestClient
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.package import Package
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas.error_schema import ErrorSchema
from ahriman.web.schemas.log_schema import LogSchema
from ahriman.web.schemas.logs_schema import LogsSchema
from ahriman.web.views.status.logs import LogsView
@ -57,7 +54,7 @@ async def test_get(client: TestClient, package_ahriman: Package) -> None:
json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()})
await client.post(f"/api/v1/packages/{package_ahriman.base}/logs",
json={"created": 42.0, "message": "message", "process_id": 42})
response_schema = LogsSchema()
response_schema = pytest.helpers.schema_response(LogsView.get)
response = await client.get(f"/api/v1/packages/{package_ahriman.base}/logs")
assert response.status == 200
@ -71,7 +68,7 @@ async def test_get_not_found(client: TestClient, package_ahriman: Package) -> No
"""
must return not found for missing package
"""
response_schema = ErrorSchema()
response_schema = pytest.helpers.schema_response(LogsView.get, code=404)
response = await client.get(f"/api/v1/packages/{package_ahriman.base}/logs")
assert response.status == 404
@ -84,7 +81,7 @@ async def test_post(client: TestClient, package_ahriman: Package) -> None:
"""
await client.post(f"/api/v1/packages/{package_ahriman.base}",
json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()})
request_schema = LogSchema()
request_schema = pytest.helpers.schema_request(LogsView.post)
payload = {"created": 42.0, "message": "message", "process_id": 42}
assert not request_schema.validate(payload)
@ -100,7 +97,7 @@ async def test_post_exception(client: TestClient, package_ahriman: Package) -> N
"""
must raise exception on invalid payload
"""
response_schema = ErrorSchema()
response_schema = pytest.helpers.schema_response(LogsView.post, code=400)
response = await client.post(f"/api/v1/packages/{package_ahriman.base}/logs", json={})
assert response.status == 400

View File

@ -5,8 +5,6 @@ from aiohttp.test_utils import TestClient
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.package import Package
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas.error_schema import ErrorSchema
from ahriman.web.schemas.package_status_schema import PackageStatusSchema, PackageStatusSimplifiedSchema
from ahriman.web.views.status.package import PackageView
@ -66,7 +64,7 @@ async def test_get(client: TestClient, package_ahriman: Package, package_python_
json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()})
await client.post(f"/api/v1/packages/{package_python_schedule.base}",
json={"status": BuildStatusEnum.Success.value, "package": package_python_schedule.view()})
response_schema = PackageStatusSchema()
response_schema = pytest.helpers.schema_response(PackageView.get)
response = await client.get(f"/api/v1/packages/{package_ahriman.base}")
assert response.ok
@ -82,7 +80,7 @@ async def test_get_not_found(client: TestClient, package_ahriman: Package) -> No
"""
must return Not Found for unknown package
"""
response_schema = ErrorSchema()
response_schema = pytest.helpers.schema_response(PackageView.get, code=404)
response = await client.get(f"/api/v1/packages/{package_ahriman.base}")
assert response.status == 404
@ -93,7 +91,7 @@ async def test_post(client: TestClient, package_ahriman: Package) -> None:
"""
must update package status
"""
request_schema = PackageStatusSimplifiedSchema()
request_schema = pytest.helpers.schema_request(PackageView.post)
payload = {"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()}
assert not request_schema.validate(payload)
@ -108,7 +106,7 @@ async def test_post_exception(client: TestClient, package_ahriman: Package) -> N
"""
must raise exception on invalid payload
"""
response_schema = ErrorSchema()
response_schema = pytest.helpers.schema_response(PackageView.post, code=400)
response = await client.post(f"/api/v1/packages/{package_ahriman.base}", json={})
assert response.status == 400
@ -119,7 +117,7 @@ async def test_post_light(client: TestClient, package_ahriman: Package) -> None:
"""
must update package status only
"""
request_schema = PackageStatusSimplifiedSchema()
request_schema = pytest.helpers.schema_request(PackageView.post)
payload = {"status": BuildStatusEnum.Unknown.value, "package": package_ahriman.view()}
assert not request_schema.validate(payload)
@ -144,8 +142,8 @@ async def test_post_not_found(client: TestClient, package_ahriman: Package) -> N
"""
must raise exception on status update for unknown package
"""
request_schema = PackageStatusSimplifiedSchema()
response_schema = ErrorSchema()
request_schema = pytest.helpers.schema_request(PackageView.post)
response_schema = pytest.helpers.schema_response(PackageView.post, code=400)
payload = {"status": BuildStatusEnum.Success.value}
assert not request_schema.validate(payload)

View File

@ -6,7 +6,6 @@ from pytest_mock import MockerFixture
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.package import Package
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas.package_status_schema import PackageStatusSchema
from ahriman.web.views.status.packages import PackagesView
@ -30,7 +29,7 @@ async def test_get(client: TestClient, package_ahriman: Package, package_python_
json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()})
await client.post(f"/api/v1/packages/{package_python_schedule.base}",
json={"status": BuildStatusEnum.Success.value, "package": package_python_schedule.view()})
response_schema = PackageStatusSchema()
response_schema = pytest.helpers.schema_response(PackagesView.get)
response = await client.get("/api/v1/packages")
assert response.ok

View File

@ -9,9 +9,6 @@ from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.internal_status import InternalStatus
from ahriman.models.package import Package
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas.error_schema import ErrorSchema
from ahriman.web.schemas.internal_status_schema import InternalStatusSchema
from ahriman.web.schemas.status_schema import StatusSchema
from ahriman.web.views.status.status import StatusView
@ -33,7 +30,7 @@ async def test_get(client: TestClient, package_ahriman: Package) -> None:
"""
await client.post(f"/api/v1/packages/{package_ahriman.base}",
json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()})
response_schema = InternalStatusSchema()
response_schema = pytest.helpers.schema_response(StatusView.get)
response = await client.get("/api/v1/status")
assert response.ok
@ -49,7 +46,7 @@ async def test_post(client: TestClient) -> None:
"""
must update service status correctly
"""
request_schema = StatusSchema()
request_schema = pytest.helpers.schema_request(StatusView.post)
payload = {"status": BuildStatusEnum.Success.value}
assert not request_schema.validate(payload)
@ -67,7 +64,7 @@ async def test_post_exception(client: TestClient) -> None:
"""
must raise exception on invalid payload
"""
response_schema = ErrorSchema()
response_schema = pytest.helpers.schema_response(StatusView.post, code=400)
response = await client.post("/api/v1/status", json={})
assert response.status == 400
@ -80,7 +77,7 @@ async def test_post_exception_inside(client: TestClient, mocker: MockerFixture)
"""
payload = {"status": BuildStatusEnum.Success.value}
mocker.patch("ahriman.core.status.watcher.Watcher.update_self", side_effect=Exception())
response_schema = ErrorSchema()
response_schema = pytest.helpers.schema_response(StatusView.post, code=500)
response = await client.post("/api/v1/status", json=payload)
assert response.status == 500

View File

@ -5,9 +5,6 @@ from pytest_mock import MockerFixture
from ahriman.models.user import User
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas.error_schema import ErrorSchema
from ahriman.web.schemas.login_schema import LoginSchema
from ahriman.web.schemas.oauth2_schema import OAuth2Schema
from ahriman.web.views.user.login import LoginView
@ -34,7 +31,7 @@ async def test_get_redirect_to_oauth(client_with_oauth_auth: TestClient) -> None
"""
oauth = client_with_oauth_auth.app["validator"]
oauth.get_oauth_url.return_value = "https://httpbin.org"
request_schema = OAuth2Schema()
request_schema = pytest.helpers.schema_request(LoginView.get, location="querystring")
payload = {}
assert not request_schema.validate(payload)
@ -49,7 +46,7 @@ async def test_get_redirect_to_oauth_empty_code(client_with_oauth_auth: TestClie
"""
oauth = client_with_oauth_auth.app["validator"]
oauth.get_oauth_url.return_value = "https://httpbin.org"
request_schema = OAuth2Schema()
request_schema = pytest.helpers.schema_request(LoginView.get, location="querystring")
payload = {"code": ""}
assert not request_schema.validate(payload)
@ -68,7 +65,7 @@ async def test_get(client_with_oauth_auth: TestClient, mocker: MockerFixture) ->
oauth.enabled = False # lol
oauth.max_age = 60
remember_mock = mocker.patch("aiohttp_security.remember")
request_schema = OAuth2Schema()
request_schema = pytest.helpers.schema_request(LoginView.get, location="querystring")
payload = {"code": "code"}
assert not request_schema.validate(payload)
@ -89,7 +86,7 @@ async def test_get_unauthorized(client_with_oauth_auth: TestClient, mocker: Mock
oauth.known_username.return_value = False
oauth.max_age = 60
remember_mock = mocker.patch("aiohttp_security.remember")
response_schema = ErrorSchema()
response_schema = pytest.helpers.schema_response(LoginView.post, code=401)
response = await client_with_oauth_auth.get(
"/api/v1/login", params={"code": "code"}, headers={"accept": "application/json"})
@ -105,7 +102,7 @@ async def test_post(client_with_auth: TestClient, user: User, mocker: MockerFixt
"""
payload = {"username": user.username, "password": user.password}
remember_mock = mocker.patch("aiohttp_security.remember")
request_schema = LoginSchema()
request_schema = pytest.helpers.schema_request(LoginView.post)
assert not request_schema.validate(payload)
@ -122,7 +119,7 @@ async def test_post_skip(client: TestClient, user: User) -> None:
"""
must process if no auth configured
"""
request_schema = LoginSchema()
request_schema = pytest.helpers.schema_request(LoginView.post)
payload = {"username": user.username, "password": user.password}
assert not request_schema.validate(payload)
@ -134,7 +131,7 @@ async def test_post_unauthorized(client_with_auth: TestClient, user: User, mocke
"""
must return unauthorized on invalid auth
"""
response_schema = ErrorSchema()
response_schema = pytest.helpers.schema_response(LoginView.post, code=401)
payload = {"username": user.username, "password": ""}
remember_mock = mocker.patch("aiohttp_security.remember")

View File

@ -5,7 +5,6 @@ from aiohttp.web import HTTPUnauthorized
from pytest_mock import MockerFixture
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas.error_schema import ErrorSchema
from ahriman.web.views.user.logout import LogoutView
@ -36,7 +35,7 @@ async def test_post_unauthorized(client_with_auth: TestClient, mocker: MockerFix
"""
mocker.patch("aiohttp_security.check_authorized", side_effect=HTTPUnauthorized())
forget_mock = mocker.patch("aiohttp_security.forget")
response_schema = ErrorSchema()
response_schema = pytest.helpers.schema_response(LogoutView.post, code=401)
response = await client_with_auth.post("/api/v1/logout", headers={"accept": "application/json"})
assert response.status == 401