make apispec dependency optional

This commit is contained in:
2024-12-20 17:07:36 +02:00
parent 6738f9206d
commit 670d502a7c
80 changed files with 843 additions and 628 deletions

View File

@ -0,0 +1,24 @@
import importlib
import sys
from pytest_mock import MockerFixture
from ahriman.web import apispec
def test_import_apispec() -> None:
"""
must correctly import apispec
"""
assert apispec.aiohttp_apispec
def test_import_apispec_missing(mocker: MockerFixture) -> None:
"""
must correctly process missing module
"""
mocker.patch.dict(sys.modules, {"aiohttp_apispec": None})
importlib.reload(apispec)
assert apispec.aiohttp_apispec is None
assert apispec.Schema
assert apispec.fields("arg", kwargs=42)

View File

@ -0,0 +1,161 @@
from aiohttp.web import HTTPFound
from pytest_mock import MockerFixture
from unittest.mock import MagicMock
from ahriman.models.user_access import UserAccess
from ahriman.web.apispec import decorators
from ahriman.web.apispec.decorators import _response_schema, apidocs
from ahriman.web.schemas import LoginSchema
def test_response_schema() -> None:
"""
must generate response schema
"""
schema = _response_schema(None)
assert schema.pop(204)
assert schema.pop(401)
assert schema.pop(403)
assert schema.pop(500)
def test_response_schema_no_403() -> None:
"""
must generate response schema without 403 error
"""
schema = _response_schema(None, error_403_enabled=False)
assert 403 not in schema
def test_response_schema_400() -> None:
"""
must generate response schema with 400 error
"""
schema = _response_schema(None, error_400_enabled=True)
assert schema.pop(400)
def test_response_schema_404() -> None:
"""
must generate response schema with 404 error
"""
schema = _response_schema(None, error_404_description="description")
assert schema.pop(404)
def test_response_schema_200() -> None:
"""
must generate response schema with 200 response
"""
schema = _response_schema(LoginSchema)
response = schema.pop(200)
assert response["schema"] == LoginSchema
assert 204 not in schema
def test_response_schema_code() -> None:
"""
must override status code
"""
schema = _response_schema(None, response_code=HTTPFound)
assert schema.pop(302)
assert 204 not in schema
def test_apidocs() -> None:
"""
must return decorated function
"""
annotated = apidocs(
tags=["tags"],
summary="summary",
description="description",
permission=UserAccess.Unauthorized,
)(MagicMock())
assert annotated.__apispec__
def test_apidocs_authorization() -> None:
"""
must return decorated function with authorization details
"""
annotated = apidocs(
tags=["tags"],
summary="summary",
description="description",
permission=UserAccess.Full,
)(MagicMock())
assert any(schema["put_into"] == "cookies" for schema in annotated.__schemas__)
def test_apidocs_match() -> None:
"""
must return decorated function with match details
"""
annotated = apidocs(
tags=["tags"],
summary="summary",
description="description",
permission=UserAccess.Unauthorized,
match_schema=LoginSchema,
)(MagicMock())
assert any(schema["put_into"] == "match_info" for schema in annotated.__schemas__)
def test_apidocs_querystring() -> None:
"""
must return decorated function with query string details
"""
annotated = apidocs(
tags=["tags"],
summary="summary",
description="description",
permission=UserAccess.Unauthorized,
query_schema=LoginSchema,
)(MagicMock())
assert any(schema["put_into"] == "querystring" for schema in annotated.__schemas__)
def test_apidocs_json() -> None:
"""
must return decorated function with json details
"""
annotated = apidocs(
tags=["tags"],
summary="summary",
description="description",
permission=UserAccess.Unauthorized,
body_schema=LoginSchema,
)(MagicMock())
assert any(schema["put_into"] == "json" for schema in annotated.__schemas__)
def test_apidocs_form() -> None:
"""
must return decorated function with generic body details
"""
annotated = apidocs(
tags=["tags"],
summary="summary",
description="description",
permission=UserAccess.Unauthorized,
body_schema=LoginSchema,
body_location="form",
)(MagicMock())
assert any(schema["put_into"] == "form" for schema in annotated.__schemas__)
def test_apidocs_import_error(mocker: MockerFixture) -> None:
"""
must return same function if no apispec module available
"""
mocker.patch.object(decorators, "aiohttp_apispec", None)
mock = MagicMock()
annotated = apidocs(
tags=["tags"],
summary="summary",
description="description",
permission=UserAccess.Unauthorized,
)(mock)
assert annotated == mock

View File

@ -4,7 +4,8 @@ from aiohttp.web import Application
from pytest_mock import MockerFixture
from ahriman import __version__
from ahriman.web.apispec import _info, _security, _servers, setup_apispec
from ahriman.web.apispec import info
from ahriman.web.apispec.info import _info, _security, _servers, setup_apispec
from ahriman.web.keys import ConfigurationKey
@ -47,7 +48,7 @@ def test_setup_apispec(application: Application, mocker: MockerFixture) -> None:
must set api specification
"""
apispec_mock = mocker.patch("aiohttp_apispec.setup_aiohttp_apispec")
setup_apispec(application)
assert setup_apispec(application)
apispec_mock.assert_called_once_with(
application,
url="/api-docs/swagger.json",
@ -56,3 +57,11 @@ def test_setup_apispec(application: Application, mocker: MockerFixture) -> None:
servers=pytest.helpers.anyvar(int),
security=pytest.helpers.anyvar(int),
)
def test_setup_apispec_import_error(application: Application, mocker: MockerFixture) -> None:
"""
must return none if apispec is not available
"""
mocker.patch.object(info, "aiohttp_apispec", None)
assert setup_apispec(application) is None

View File

@ -1,7 +1,9 @@
import pytest
from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration
from ahriman.models.user_access import UserAccess
from ahriman.web.views.api.docs import DocsView
@ -15,6 +17,28 @@ async def test_get_permission() -> None:
assert await DocsView.get_permission(request) == UserAccess.Unauthorized
def test_routes() -> None:
"""
must return correct routes
"""
assert DocsView.ROUTES == ["/api-docs"]
def test_routes_dynamic(configuration: Configuration) -> None:
"""
must correctly return docs route
"""
assert DocsView.ROUTES == DocsView.routes(configuration)
def test_routes_dynamic_not_found(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must disable docs route if no apispec package found
"""
mocker.patch("ahriman.web.views.api.docs.aiohttp_apispec", None)
assert DocsView.routes(configuration) == []
async def test_get(client: TestClient) -> None:
"""
must generate api-docs correctly

View File

@ -4,6 +4,7 @@ from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from typing import Any
from ahriman.core.configuration import Configuration
from ahriman.models.user_access import UserAccess
from ahriman.web.views.api.swagger import SwaggerView
@ -86,6 +87,28 @@ async def test_get_permission() -> None:
assert await SwaggerView.get_permission(request) == UserAccess.Unauthorized
def test_routes() -> None:
"""
must return correct routes
"""
assert SwaggerView.ROUTES == ["/api-docs/swagger.json"]
def test_routes_dynamic(configuration: Configuration) -> None:
"""
must correctly return openapi url
"""
assert SwaggerView.ROUTES == SwaggerView.routes(configuration)
def test_routes_dynamic_not_found(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must disable openapi route if no apispec package found
"""
mocker.patch("ahriman.web.views.api.swagger.aiohttp_apispec", None)
assert SwaggerView.routes(configuration) == []
async def test_get(client: TestClient, mocker: MockerFixture) -> None:
"""
must generate api-docs correctly

View File

@ -8,6 +8,7 @@ from pathlib import Path
from pytest_mock import MockerFixture
from unittest.mock import AsyncMock, MagicMock, call as MockCall
from ahriman.core.configuration import Configuration
from ahriman.models.repository_paths import RepositoryPaths
from ahriman.models.user_access import UserAccess
from ahriman.web.views.v1.service.upload import UploadView
@ -29,6 +30,21 @@ def test_routes() -> None:
assert UploadView.ROUTES == ["/api/v1/service/upload"]
def test_routes_dynamic(configuration: Configuration) -> None:
"""
must correctly return upload url
"""
assert UploadView.ROUTES == UploadView.routes(configuration)
def test_routes_dynamic_not_found(configuration: Configuration) -> None:
"""
must disable upload route if option is not set
"""
configuration.set_option("web", "enable_archive_upload", "no")
assert UploadView.routes(configuration) == []
async def test_save_file(mocker: MockerFixture) -> None:
"""
must correctly save file
@ -134,20 +150,6 @@ async def test_post_with_sig(client: TestClient, repository_paths: RepositoryPat
])
async def test_post_not_found(client: TestClient, mocker: MockerFixture) -> None:
"""
must return 404 if request was disabled
"""
mocker.patch("ahriman.core.configuration.Configuration.getboolean", return_value=False)
data = FormData()
data.add_field("package", BytesIO(b"content"), filename="filename", content_type="application/octet-stream")
response_schema = pytest.helpers.schema_response(UploadView.post, code=404)
response = await client.post("/api/v1/service/upload", data=data)
assert response.status == 404
assert not response_schema.validate(await response.json())
async def test_post_not_multipart(client: TestClient) -> None:
"""
must return 400 on invalid payload