feat: load http views dynamically (#113)

This commit is contained in:
2023-09-30 01:24:04 +03:00
committed by GitHub
parent d5f4fc9b86
commit 1859d14f78
45 changed files with 352 additions and 74 deletions

View File

@ -7,7 +7,7 @@ from collections.abc import Awaitable, Callable
from marshmallow import Schema
from pytest_mock import MockerFixture
from typing import Any
from unittest.mock import MagicMock
from unittest.mock import MagicMock, Mock
import ahriman.core.auth.helpers
@ -20,6 +20,26 @@ from ahriman.models.user import User
from ahriman.web.web import setup_service
@pytest.helpers.register
def patch_view(application: Application, attribute: str, mock: Mock) -> Mock:
"""
patch given attribute in views. This method is required because of dynamic load
Args:
application(Application): application fixture
attribute(str): attribute name to patch
mock(Mock): mock object
Returns:
Mock: mock set to object
"""
for route in application.router.routes():
if hasattr(route.handler, attribute):
setattr(route.handler, attribute, mock)
return mock
@pytest.helpers.register
def request(application: Application, path: str, method: str, params: Any = None, json: Any = None, data: Any = None,
extra: dict[str, Any] | None = None, resource: Resource | None = None) -> MagicMock:

View File

@ -1,7 +1,73 @@
import pytest
from aiohttp.web import Application
from importlib.machinery import ModuleSpec
from pathlib import Path
from pytest_mock import MockerFixture
from types import ModuleType
from ahriman.core.configuration import Configuration
from ahriman.web.routes import setup_routes
from ahriman.core.util import walk
from ahriman.web.routes import _dynamic_routes, _module, _modules, setup_routes
def test_dynamic_routes(resource_path_root: Path) -> None:
"""
must return all available routes
"""
views_root = resource_path_root / ".." / ".." / "src" / "ahriman" / "web" / "views"
expected_views = [
file
for file in walk(views_root)
if file.suffix == ".py" and file.name not in ("__init__.py", "base.py")
]
routes = _dynamic_routes(views_root)
assert all(isinstance(view, type) for view in routes.values())
assert len(set(routes.values())) == len(expected_views)
def test_module(mocker: MockerFixture) -> None:
"""
must load module
"""
exec_mock = mocker.patch("importlib.machinery.SourceFileLoader.exec_module")
module_info = next(_modules(Path(__file__).parent))
module = _module(module_info)
assert isinstance(module, ModuleType)
exec_mock.assert_called_once_with(pytest.helpers.anyvar(int))
def test_module_no_spec(mocker: MockerFixture) -> None:
"""
must raise ValueError if spec is not available
"""
mocker.patch("importlib.machinery.FileFinder.find_spec", return_value=None)
module_info = next(_modules(Path(__file__).parent))
with pytest.raises(ValueError):
_module(module_info)
def test_module_no_loader(mocker: MockerFixture) -> None:
"""
must raise ValueError if loader is not available
"""
mocker.patch("importlib.machinery.FileFinder.find_spec", return_value=ModuleSpec("name", None))
module_info = next(_modules(Path(__file__).parent))
with pytest.raises(ValueError):
_module(module_info)
def test_modules() -> None:
"""
must load modules
"""
modules = list(_modules(Path(__file__).parent.parent))
assert modules
assert all(not module.ispkg for module in modules)
def test_setup_routes(application: Application, configuration: Configuration) -> None:

View File

@ -10,6 +10,13 @@ from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView
def test_routes() -> None:
"""
must return correct routes
"""
assert BaseView.ROUTES == []
def test_configuration(base: BaseView) -> None:
"""
must return configuration

View File

@ -15,6 +15,13 @@ async def test_get_permission() -> None:
assert await IndexView.get_permission(request) == UserAccess.Unauthorized
def test_routes() -> None:
"""
must return correct routes
"""
assert IndexView.ROUTES == ["/", "/index.html"]
async def test_get(client_with_auth: TestClient) -> None:
"""
must generate status page correctly (/)

View File

@ -5,7 +5,7 @@ from pytest_mock import MockerFixture
from unittest.mock import AsyncMock
from ahriman.models.user_access import UserAccess
from ahriman.web.views.v1 import AddView
from ahriman.web.views.v1.service.add import AddView
async def test_get_permission() -> None:
@ -17,6 +17,13 @@ async def test_get_permission() -> None:
assert await AddView.get_permission(request) == UserAccess.Full
def test_routes() -> None:
"""
must return correct routes
"""
assert AddView.ROUTES == ["/api/v1/service/add"]
async def test_post(client: TestClient, mocker: MockerFixture) -> None:
"""
must call post request correctly

View File

@ -4,7 +4,7 @@ from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from ahriman.models.user_access import UserAccess
from ahriman.web.views.v1 import PGPView
from ahriman.web.views.v1.service.pgp import PGPView
async def test_get_permission() -> None:
@ -19,6 +19,13 @@ async def test_get_permission() -> None:
assert await PGPView.get_permission(request) == UserAccess.Full
def test_routes() -> None:
"""
must return correct routes
"""
assert PGPView.ROUTES == ["/api/v1/service/pgp"]
async def test_get(client: TestClient, mocker: MockerFixture) -> None:
"""
must retrieve key from the keyserver

View File

@ -4,7 +4,7 @@ from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from ahriman.models.user_access import UserAccess
from ahriman.web.views.v1 import ProcessView
from ahriman.web.views.v1.service.process import ProcessView
async def test_get_permission() -> None:
@ -16,6 +16,13 @@ async def test_get_permission() -> None:
assert await ProcessView.get_permission(request) == UserAccess.Reporter
def test_routes() -> None:
"""
must return correct routes
"""
assert ProcessView.ROUTES == ["/api/v1/service/process/{process_id}"]
async def test_get(client: TestClient, mocker: MockerFixture) -> None:
"""
must call post request correctly

View File

@ -5,7 +5,7 @@ from pytest_mock import MockerFixture
from unittest.mock import AsyncMock
from ahriman.models.user_access import UserAccess
from ahriman.web.views.v1 import RebuildView
from ahriman.web.views.v1.service.rebuild import RebuildView
async def test_get_permission() -> None:
@ -17,6 +17,13 @@ async def test_get_permission() -> None:
assert await RebuildView.get_permission(request) == UserAccess.Full
def test_routes() -> None:
"""
must return correct routes
"""
assert RebuildView.ROUTES == ["/api/v1/service/rebuild"]
async def test_post(client: TestClient, mocker: MockerFixture) -> None:
"""
must call post request correctly

View File

@ -4,7 +4,7 @@ from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from ahriman.models.user_access import UserAccess
from ahriman.web.views.v1 import RemoveView
from ahriman.web.views.v1.service.remove import RemoveView
async def test_get_permission() -> None:
@ -16,6 +16,13 @@ async def test_get_permission() -> None:
assert await RemoveView.get_permission(request) == UserAccess.Full
def test_routes() -> None:
"""
must return correct routes
"""
assert RemoveView.ROUTES == ["/api/v1/service/remove"]
async def test_post(client: TestClient, mocker: MockerFixture) -> None:
"""
must call post request correctly

View File

@ -5,7 +5,7 @@ from pytest_mock import MockerFixture
from unittest.mock import AsyncMock
from ahriman.models.user_access import UserAccess
from ahriman.web.views.v1 import RequestView
from ahriman.web.views.v1.service.request import RequestView
async def test_get_permission() -> None:
@ -17,6 +17,13 @@ async def test_get_permission() -> None:
assert await RequestView.get_permission(request) == UserAccess.Reporter
def test_routes() -> None:
"""
must return correct routes
"""
assert RequestView.ROUTES == ["/api/v1/service/request"]
async def test_post(client: TestClient, mocker: MockerFixture) -> None:
"""
must call post request correctly

View File

@ -5,7 +5,7 @@ from pytest_mock import MockerFixture
from ahriman.models.aur_package import AURPackage
from ahriman.models.user_access import UserAccess
from ahriman.web.views.v1 import SearchView
from ahriman.web.views.v1.service.search import SearchView
async def test_get_permission() -> None:
@ -17,6 +17,13 @@ async def test_get_permission() -> None:
assert await SearchView.get_permission(request) == UserAccess.Reporter
def test_routes() -> None:
"""
must return correct routes
"""
assert SearchView.ROUTES == ["/api/v1/service/search"]
async def test_get(client: TestClient, aur_package_ahriman: AURPackage, mocker: MockerFixture) -> None:
"""
must call get request correctly

View File

@ -5,7 +5,7 @@ from pytest_mock import MockerFixture
from unittest.mock import AsyncMock
from ahriman.models.user_access import UserAccess
from ahriman.web.views.v1 import UpdateView
from ahriman.web.views.v1.service.update import UpdateView
async def test_get_permission() -> None:
@ -17,6 +17,13 @@ async def test_get_permission() -> None:
assert await UpdateView.get_permission(request) == UserAccess.Full
def test_routes() -> None:
"""
must return correct routes
"""
assert UpdateView.ROUTES == ["/api/v1/service/update"]
async def test_post(client: TestClient, mocker: MockerFixture) -> None:
"""
must call post request correctly for alias

View File

@ -10,7 +10,7 @@ from unittest.mock import AsyncMock, MagicMock, call as MockCall
from ahriman.models.repository_paths import RepositoryPaths
from ahriman.models.user_access import UserAccess
from ahriman.web.views.v1 import UploadView
from ahriman.web.views.v1.service.upload import UploadView
async def test_get_permission() -> None:
@ -22,6 +22,13 @@ async def test_get_permission() -> None:
assert await UploadView.get_permission(request) == UserAccess.Full
def test_routes() -> None:
"""
must return correct routes
"""
assert UploadView.ROUTES == ["/api/v1/service/upload"]
async def test_save_file(mocker: MockerFixture) -> None:
"""
must correctly save file
@ -84,8 +91,8 @@ async def test_post(client: TestClient, repository_paths: RepositoryPaths, mocke
must process file upload via http
"""
local = Path("local")
save_mock = mocker.patch("ahriman.web.views.v1.UploadView.save_file",
side_effect=AsyncMock(return_value=("filename", local / ".filename")))
save_mock = pytest.helpers.patch_view(client.app, "save_file",
AsyncMock(return_value=("filename", local / ".filename")))
rename_mock = mocker.patch("pathlib.Path.rename")
# no content validation here because it has invalid schema
@ -103,11 +110,11 @@ async def test_post_with_sig(client: TestClient, repository_paths: RepositoryPat
must process file upload with signature via http
"""
local = Path("local")
save_mock = mocker.patch("ahriman.web.views.v1.UploadView.save_file",
side_effect=AsyncMock(side_effect=[
("filename", local / ".filename"),
("filename.sig", local / ".filename.sig"),
]))
save_mock = pytest.helpers.patch_view(client.app, "save_file",
AsyncMock(side_effect=[
("filename", local / ".filename"),
("filename.sig", local / ".filename.sig"),
]))
rename_mock = mocker.patch("pathlib.Path.rename")
# no content validation here because it has invalid schema

View File

@ -5,7 +5,7 @@ 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.views.v1 import LogsView
from ahriman.web.views.v1.status.logs import LogsView
async def test_get_permission() -> None:
@ -20,6 +20,13 @@ async def test_get_permission() -> None:
assert await LogsView.get_permission(request) == UserAccess.Full
def test_routes() -> None:
"""
must return correct routes
"""
assert LogsView.ROUTES == ["/api/v1/packages/{package}/logs"]
async def test_delete(client: TestClient, package_ahriman: Package, package_python_schedule: Package) -> None:
"""
must delete logs for package

View File

@ -5,7 +5,7 @@ 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.views.v1 import PackageView
from ahriman.web.views.v1.status.package import PackageView
async def test_get_permission() -> None:
@ -20,6 +20,13 @@ async def test_get_permission() -> None:
assert await PackageView.get_permission(request) == UserAccess.Full
def test_routes() -> None:
"""
must return correct routes
"""
assert PackageView.ROUTES == ["/api/v1/packages/{package}"]
async def test_delete(client: TestClient, package_ahriman: Package, package_python_schedule: Package) -> None:
"""
must delete single base

View File

@ -6,7 +6,7 @@ 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.views.v1 import PackagesView
from ahriman.web.views.v1.status.packages import (PackagesView)
async def test_get_permission() -> None:
@ -21,6 +21,13 @@ async def test_get_permission() -> None:
assert await PackagesView.get_permission(request) == UserAccess.Full
def test_routes() -> None:
"""
must return correct routes
"""
assert PackagesView.ROUTES == ["/api/v1/packages"]
async def test_get(client: TestClient, package_ahriman: Package, package_python_schedule: Package) -> None:
"""
must return status for all packages

View File

@ -8,7 +8,7 @@ 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.views.v1 import StatusView
from ahriman.web.views.v1.status.status import StatusView
async def test_get_permission() -> None:
@ -23,6 +23,13 @@ async def test_get_permission() -> None:
assert await StatusView.get_permission(request) == UserAccess.Full
def test_routes() -> None:
"""
must return correct routes
"""
assert StatusView.ROUTES == ["/api/v1/status"]
async def test_get(client: TestClient, package_ahriman: Package) -> None:
"""
must generate web service status correctly

View File

@ -5,7 +5,7 @@ from pytest_mock import MockerFixture
from ahriman.models.user import User
from ahriman.models.user_access import UserAccess
from ahriman.web.views.v1 import LoginView
from ahriman.web.views.v1.user.login import LoginView
async def test_get_permission() -> None:
@ -17,6 +17,13 @@ async def test_get_permission() -> None:
assert await LoginView.get_permission(request) == UserAccess.Unauthorized
def test_routes() -> None:
"""
must return correct routes
"""
assert LoginView.ROUTES == ["/api/v1/login"]
async def test_get_default_validator(client_with_auth: TestClient) -> None:
"""
must return 405 in case if no OAuth enabled

View File

@ -5,7 +5,7 @@ from aiohttp.web import HTTPUnauthorized
from pytest_mock import MockerFixture
from ahriman.models.user_access import UserAccess
from ahriman.web.views.v1 import LogoutView
from ahriman.web.views.v1.user.logout import LogoutView
async def test_get_permission() -> None:
@ -17,6 +17,13 @@ async def test_get_permission() -> None:
assert await LogoutView.get_permission(request) == UserAccess.Unauthorized
def test_routes() -> None:
"""
must return correct routes
"""
assert LogoutView.ROUTES == ["/api/v1/logout"]
async def test_post(client_with_auth: TestClient, mocker: MockerFixture) -> None:
"""
must log out user correctly

View File

@ -5,7 +5,7 @@ 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.views.v2 import LogsView
from ahriman.web.views.v2.status.logs import LogsView
async def test_get_permission() -> None:
@ -17,6 +17,13 @@ async def test_get_permission() -> None:
assert await LogsView.get_permission(request) == UserAccess.Reporter
def test_routes() -> None:
"""
must return correct routes
"""
assert LogsView.ROUTES == ["/api/v2/packages/{package}/logs"]
async def test_get(client: TestClient, package_ahriman: Package) -> None:
"""
must get logs for package