feat: load http views dynamically (#113)

This commit is contained in:
Evgenii Alekseev 2023-09-30 01:24:04 +03:00 committed by GitHub
parent bc9682373d
commit 9fe760efdf
45 changed files with 352 additions and 74 deletions

View File

@ -1303,6 +1303,18 @@ It is possible to customize html templates. In order to do so, create files some
In addition, default html templates supports style customization out-of-box. In order to customize style, just put file named ``user-style.jinja2`` to the templates directory. In addition, default html templates supports style customization out-of-box. In order to customize style, just put file named ``user-style.jinja2`` to the templates directory.
Web API extension
^^^^^^^^^^^^^^^^^
The application loads web views dynamically, so it is possible relatively easy extend its API. In order to do so:
#. Create view class which is derived from ``ahriman.web.views.base.BaseView`` class.
#. Create implementation for this class.
#. Put file into ``ahriman.web.views`` package.
#. Restart application.
For more details about implementation and possibilities, kindly refer to module documentation and source code and `aiohttp documentation <https://docs.aiohttp.org/en/stable/>`_.
I did not find my question I did not find my question
^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -92,6 +92,7 @@ class Migrations(LazyLogging):
list[Migration]: list of found migrations list[Migration]: list of found migrations
""" """
migrations: list[Migration] = [] migrations: list[Migration] = []
package_dir = Path(__file__).resolve().parent package_dir = Path(__file__).resolve().parent
modules = [module_name for (_, module_name, _) in iter_modules([str(package_dir)])] modules = [module_name for (_, module_name, _) in iter_modules([str(package_dir)])]

View File

@ -17,18 +17,90 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from aiohttp.web import Application from aiohttp.web import Application, View
from collections.abc import Generator
from importlib.machinery import SourceFileLoader
from pathlib import Path from pathlib import Path
from pkgutil import ModuleInfo, iter_modules
from types import ModuleType
from typing import Any, Type, TypeGuard
from ahriman.web.views.api.docs import DocsView from ahriman.web.views.base import BaseView
from ahriman.web.views.api.swagger import SwaggerView
from ahriman.web.views.index import IndexView
from ahriman.web.views import v1, v2
__all__ = ["setup_routes"] __all__ = ["setup_routes"]
def _dynamic_routes(module_root: Path) -> dict[str, Type[View]]:
"""
extract dynamic routes based on views
Args:
module_root(Path): root module path with views
Returns:
dict[str, Type[View]]: map of the route to its view
"""
def is_base_view(clz: Any) -> TypeGuard[Type[BaseView]]:
return isinstance(clz, type) and issubclass(clz, BaseView)
routes: dict[str, Type[View]] = {}
for module_info in _modules(module_root):
module = _module(module_info)
for attribute_name in dir(module):
view = getattr(module, attribute_name)
if not is_base_view(view):
continue
routes.update([(route, view) for route in view.ROUTES])
return routes
def _module(module_info: ModuleInfo) -> ModuleType:
"""
load module from its info
Args:
module_info(ModuleInfo): module info descriptor
Returns:
ModuleType: loaded module
Raises:
ValueError: if loader is not an instance of ``SourceFileLoader``
"""
module_spec = module_info.module_finder.find_spec(module_info.name, None)
if module_spec is None:
raise ValueError(f"Module specification of {module_info.name} is empty")
loader = module_spec.loader
if not isinstance(loader, SourceFileLoader):
raise ValueError(f"Module {module_info.name} loader is not an instance of SourceFileLoader")
module = ModuleType(loader.name)
loader.exec_module(module)
return module
def _modules(module_root: Path) -> Generator[ModuleInfo, None, None]:
"""
extract available modules from package
Args:
module_root(Path): module root path
Yields:
ModuleInfo: module information each available module
"""
for module_info in iter_modules([str(module_root)]):
if module_info.ispkg:
yield from _modules(module_root / module_info.name)
else:
yield module_info
def setup_routes(application: Application, static_path: Path) -> None: def setup_routes(application: Application, static_path: Path) -> None:
""" """
setup all defined routes setup all defined routes
@ -37,30 +109,8 @@ def setup_routes(application: Application, static_path: Path) -> None:
application(Application): web application instance application(Application): web application instance
static_path(Path): path to static files directory static_path(Path): path to static files directory
""" """
application.router.add_view("/", IndexView)
application.router.add_view("/index.html", IndexView)
application.router.add_view("/api-docs", DocsView)
application.router.add_view("/api-docs/swagger.json", SwaggerView)
application.router.add_static("/static", static_path, follow_symlinks=True) application.router.add_static("/static", static_path, follow_symlinks=True)
application.router.add_view("/api/v1/service/add", v1.AddView) views = Path(__file__).parent / "views"
application.router.add_view("/api/v1/service/pgp", v1.PGPView) for route, view in _dynamic_routes(views).items():
application.router.add_view("/api/v1/service/rebuild", v1.RebuildView) application.router.add_view(route, view)
application.router.add_view("/api/v1/service/process/{process_id}", v1.ProcessView)
application.router.add_view("/api/v1/service/remove", v1.RemoveView)
application.router.add_view("/api/v1/service/request", v1.RequestView)
application.router.add_view("/api/v1/service/search", v1.SearchView)
application.router.add_view("/api/v1/service/update", v1.UpdateView)
application.router.add_view("/api/v1/service/upload", v1.UploadView)
application.router.add_view("/api/v1/packages", v1.PackagesView)
application.router.add_view("/api/v1/packages/{package}", v1.PackageView)
application.router.add_view("/api/v1/packages/{package}/logs", v1.LogsView)
application.router.add_view("/api/v2/packages/{package}/logs", v2.LogsView)
application.router.add_view("/api/v1/status", v1.StatusView)
application.router.add_view("/api/v1/login", v1.LoginView)
application.router.add_view("/api/v1/logout", v1.LogoutView)

View File

@ -34,6 +34,7 @@ class DocsView(BaseView):
""" """
GET_PERMISSION = UserAccess.Unauthorized GET_PERMISSION = UserAccess.Unauthorized
ROUTES = ["/api-docs"]
@aiohttp_jinja2.template("api.jinja2") @aiohttp_jinja2.template("api.jinja2")
async def get(self) -> dict[str, Any]: async def get(self) -> dict[str, Any]:

View File

@ -34,6 +34,7 @@ class SwaggerView(BaseView):
""" """
GET_PERMISSION = UserAccess.Unauthorized GET_PERMISSION = UserAccess.Unauthorized
ROUTES = ["/api-docs/swagger.json"]
async def get(self) -> Response: async def get(self) -> Response:
""" """

View File

@ -38,9 +38,11 @@ class BaseView(View, CorsViewMixin):
Attributes: Attributes:
OPTIONS_PERMISSION(UserAccess): (class attribute) options permissions of self OPTIONS_PERMISSION(UserAccess): (class attribute) options permissions of self
ROUTES(list[str]): (class attribute) list of supported routes
""" """
OPTIONS_PERMISSION = UserAccess.Unauthorized OPTIONS_PERMISSION = UserAccess.Unauthorized
ROUTES: list[str] = []
@property @property
def configuration(self) -> Configuration: def configuration(self) -> Configuration:

View File

@ -44,6 +44,7 @@ class IndexView(BaseView):
""" """
GET_PERMISSION = UserAccess.Unauthorized GET_PERMISSION = UserAccess.Unauthorized
ROUTES = ["/", "/index.html"]
@aiohttp_jinja2.template("build-status.jinja2") @aiohttp_jinja2.template("build-status.jinja2")
async def get(self) -> dict[str, Any]: async def get(self) -> dict[str, Any]:

View File

@ -17,20 +17,3 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from ahriman.web.views.v1.service.add import AddView
from ahriman.web.views.v1.service.pgp import PGPView
from ahriman.web.views.v1.service.process import ProcessView
from ahriman.web.views.v1.service.rebuild import RebuildView
from ahriman.web.views.v1.service.remove import RemoveView
from ahriman.web.views.v1.service.request import RequestView
from ahriman.web.views.v1.service.search import SearchView
from ahriman.web.views.v1.service.update import UpdateView
from ahriman.web.views.v1.service.upload import UploadView
from ahriman.web.views.v1.status.logs import LogsView
from ahriman.web.views.v1.status.package import PackageView
from ahriman.web.views.v1.status.packages import PackagesView
from ahriman.web.views.v1.status.status import StatusView
from ahriman.web.views.v1.user.login import LoginView
from ahriman.web.views.v1.user.logout import LogoutView

View File

@ -35,6 +35,7 @@ class AddView(BaseView):
""" """
POST_PERMISSION = UserAccess.Full POST_PERMISSION = UserAccess.Full
ROUTES = ["/api/v1/service/add"]
@aiohttp_apispec.docs( @aiohttp_apispec.docs(
tags=["Actions"], tags=["Actions"],

View File

@ -35,8 +35,9 @@ class PGPView(BaseView):
POST_PERMISSION(UserAccess): (class attribute) post permissions of self POST_PERMISSION(UserAccess): (class attribute) post permissions of self
""" """
POST_PERMISSION = UserAccess.Full
GET_PERMISSION = UserAccess.Reporter GET_PERMISSION = UserAccess.Reporter
POST_PERMISSION = UserAccess.Full
ROUTES = ["/api/v1/service/pgp"]
@aiohttp_apispec.docs( @aiohttp_apispec.docs(
tags=["Actions"], tags=["Actions"],

View File

@ -35,6 +35,7 @@ class ProcessView(BaseView):
""" """
GET_PERMISSION = UserAccess.Reporter GET_PERMISSION = UserAccess.Reporter
ROUTES = ["/api/v1/service/process/{process_id}"]
@aiohttp_apispec.docs( @aiohttp_apispec.docs(
tags=["Actions"], tags=["Actions"],

View File

@ -35,6 +35,7 @@ class RebuildView(BaseView):
""" """
POST_PERMISSION = UserAccess.Full POST_PERMISSION = UserAccess.Full
ROUTES = ["/api/v1/service/rebuild"]
@aiohttp_apispec.docs( @aiohttp_apispec.docs(
tags=["Actions"], tags=["Actions"],

View File

@ -35,6 +35,7 @@ class RemoveView(BaseView):
""" """
POST_PERMISSION = UserAccess.Full POST_PERMISSION = UserAccess.Full
ROUTES = ["/api/v1/service/remove"]
@aiohttp_apispec.docs( @aiohttp_apispec.docs(
tags=["Actions"], tags=["Actions"],

View File

@ -35,6 +35,7 @@ class RequestView(BaseView):
""" """
POST_PERMISSION = UserAccess.Reporter POST_PERMISSION = UserAccess.Reporter
ROUTES = ["/api/v1/service/request"]
@aiohttp_apispec.docs( @aiohttp_apispec.docs(
tags=["Actions"], tags=["Actions"],

View File

@ -38,6 +38,7 @@ class SearchView(BaseView):
""" """
GET_PERMISSION = UserAccess.Reporter GET_PERMISSION = UserAccess.Reporter
ROUTES = ["/api/v1/service/search"]
@aiohttp_apispec.docs( @aiohttp_apispec.docs(
tags=["Actions"], tags=["Actions"],

View File

@ -35,6 +35,7 @@ class UpdateView(BaseView):
""" """
POST_PERMISSION = UserAccess.Full POST_PERMISSION = UserAccess.Full
ROUTES = ["/api/v1/service/update"]
@aiohttp_apispec.docs( @aiohttp_apispec.docs(
tags=["Actions"], tags=["Actions"],

View File

@ -39,6 +39,7 @@ class UploadView(BaseView):
""" """
POST_PERMISSION = UserAccess.Full POST_PERMISSION = UserAccess.Full
ROUTES = ["/api/v1/service/upload"]
@staticmethod @staticmethod
async def save_file(part: BodyPartReader, target: Path, *, max_body_size: int | None = None) -> tuple[str, Path]: async def save_file(part: BodyPartReader, target: Path, *, max_body_size: int | None = None) -> tuple[str, Path]:

View File

@ -41,6 +41,7 @@ class LogsView(BaseView):
DELETE_PERMISSION = POST_PERMISSION = UserAccess.Full DELETE_PERMISSION = POST_PERMISSION = UserAccess.Full
GET_PERMISSION = UserAccess.Reporter GET_PERMISSION = UserAccess.Reporter
ROUTES = ["/api/v1/packages/{package}/logs"]
@aiohttp_apispec.docs( @aiohttp_apispec.docs(
tags=["Packages"], tags=["Packages"],

View File

@ -41,6 +41,7 @@ class PackageView(BaseView):
DELETE_PERMISSION = POST_PERMISSION = UserAccess.Full DELETE_PERMISSION = POST_PERMISSION = UserAccess.Full
GET_PERMISSION = UserAccess.Read GET_PERMISSION = UserAccess.Read
ROUTES = ["/api/v1/packages/{package}"]
@aiohttp_apispec.docs( @aiohttp_apispec.docs(
tags=["Packages"], tags=["Packages"],

View File

@ -41,6 +41,7 @@ class PackagesView(BaseView):
GET_PERMISSION = UserAccess.Read GET_PERMISSION = UserAccess.Read
POST_PERMISSION = UserAccess.Full POST_PERMISSION = UserAccess.Full
ROUTES = ["/api/v1/packages"]
@aiohttp_apispec.docs( @aiohttp_apispec.docs(
tags=["Packages"], tags=["Packages"],

View File

@ -41,6 +41,7 @@ class StatusView(BaseView):
GET_PERMISSION = UserAccess.Read GET_PERMISSION = UserAccess.Read
POST_PERMISSION = UserAccess.Full POST_PERMISSION = UserAccess.Full
ROUTES = ["/api/v1/status"]
@aiohttp_apispec.docs( @aiohttp_apispec.docs(
tags=["Status"], tags=["Status"],

View File

@ -37,6 +37,7 @@ class LoginView(BaseView):
""" """
GET_PERMISSION = POST_PERMISSION = UserAccess.Unauthorized GET_PERMISSION = POST_PERMISSION = UserAccess.Unauthorized
ROUTES = ["/api/v1/login"]
@aiohttp_apispec.docs( @aiohttp_apispec.docs(
tags=["Login"], tags=["Login"],

View File

@ -36,6 +36,7 @@ class LogoutView(BaseView):
""" """
POST_PERMISSION = UserAccess.Unauthorized POST_PERMISSION = UserAccess.Unauthorized
ROUTES = ["/api/v1/logout"]
@aiohttp_apispec.docs( @aiohttp_apispec.docs(
tags=["Login"], tags=["Login"],

View File

@ -17,4 +17,3 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from ahriman.web.views.v2.status.logs import LogsView

View File

@ -37,6 +37,7 @@ class LogsView(BaseView):
""" """
GET_PERMISSION = UserAccess.Reporter GET_PERMISSION = UserAccess.Reporter
ROUTES = ["/api/v2/packages/{package}/logs"]
@aiohttp_apispec.docs( @aiohttp_apispec.docs(
tags=["Packages"], tags=["Packages"],

View File

@ -7,7 +7,7 @@ from collections.abc import Awaitable, Callable
from marshmallow import Schema from marshmallow import Schema
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from typing import Any from typing import Any
from unittest.mock import MagicMock from unittest.mock import MagicMock, Mock
import ahriman.core.auth.helpers import ahriman.core.auth.helpers
@ -20,6 +20,26 @@ from ahriman.models.user import User
from ahriman.web.web import setup_service 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 @pytest.helpers.register
def request(application: Application, path: str, method: str, params: Any = None, json: Any = None, data: Any = None, 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: 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 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.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: 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 from ahriman.web.views.base import BaseView
def test_routes() -> None:
"""
must return correct routes
"""
assert BaseView.ROUTES == []
def test_configuration(base: BaseView) -> None: def test_configuration(base: BaseView) -> None:
""" """
must return configuration must return configuration

View File

@ -15,6 +15,13 @@ async def test_get_permission() -> None:
assert await IndexView.get_permission(request) == UserAccess.Unauthorized 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: async def test_get(client_with_auth: TestClient) -> None:
""" """
must generate status page correctly (/) must generate status page correctly (/)

View File

@ -5,7 +5,7 @@ from pytest_mock import MockerFixture
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
from ahriman.models.user_access import UserAccess 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: async def test_get_permission() -> None:
@ -17,6 +17,13 @@ async def test_get_permission() -> None:
assert await AddView.get_permission(request) == UserAccess.Full 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: async def test_post(client: TestClient, mocker: MockerFixture) -> None:
""" """
must call post request correctly must call post request correctly

View File

@ -4,7 +4,7 @@ from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from ahriman.models.user_access import UserAccess 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: async def test_get_permission() -> None:
@ -19,6 +19,13 @@ async def test_get_permission() -> None:
assert await PGPView.get_permission(request) == UserAccess.Full 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: async def test_get(client: TestClient, mocker: MockerFixture) -> None:
""" """
must retrieve key from the keyserver must retrieve key from the keyserver

View File

@ -4,7 +4,7 @@ from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from ahriman.models.user_access import UserAccess 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: async def test_get_permission() -> None:
@ -16,6 +16,13 @@ async def test_get_permission() -> None:
assert await ProcessView.get_permission(request) == UserAccess.Reporter 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: async def test_get(client: TestClient, mocker: MockerFixture) -> None:
""" """
must call post request correctly must call post request correctly

View File

@ -5,7 +5,7 @@ from pytest_mock import MockerFixture
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
from ahriman.models.user_access import UserAccess 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: async def test_get_permission() -> None:
@ -17,6 +17,13 @@ async def test_get_permission() -> None:
assert await RebuildView.get_permission(request) == UserAccess.Full 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: async def test_post(client: TestClient, mocker: MockerFixture) -> None:
""" """
must call post request correctly must call post request correctly

View File

@ -4,7 +4,7 @@ from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from ahriman.models.user_access import UserAccess 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: async def test_get_permission() -> None:
@ -16,6 +16,13 @@ async def test_get_permission() -> None:
assert await RemoveView.get_permission(request) == UserAccess.Full 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: async def test_post(client: TestClient, mocker: MockerFixture) -> None:
""" """
must call post request correctly must call post request correctly

View File

@ -5,7 +5,7 @@ from pytest_mock import MockerFixture
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
from ahriman.models.user_access import UserAccess 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: async def test_get_permission() -> None:
@ -17,6 +17,13 @@ async def test_get_permission() -> None:
assert await RequestView.get_permission(request) == UserAccess.Reporter 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: async def test_post(client: TestClient, mocker: MockerFixture) -> None:
""" """
must call post request correctly 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.aur_package import AURPackage
from ahriman.models.user_access import UserAccess 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: async def test_get_permission() -> None:
@ -17,6 +17,13 @@ async def test_get_permission() -> None:
assert await SearchView.get_permission(request) == UserAccess.Reporter 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: async def test_get(client: TestClient, aur_package_ahriman: AURPackage, mocker: MockerFixture) -> None:
""" """
must call get request correctly must call get request correctly

View File

@ -5,7 +5,7 @@ from pytest_mock import MockerFixture
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
from ahriman.models.user_access import UserAccess 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: async def test_get_permission() -> None:
@ -17,6 +17,13 @@ async def test_get_permission() -> None:
assert await UpdateView.get_permission(request) == UserAccess.Full 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: async def test_post(client: TestClient, mocker: MockerFixture) -> None:
""" """
must call post request correctly for alias 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.repository_paths import RepositoryPaths
from ahriman.models.user_access import UserAccess 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: async def test_get_permission() -> None:
@ -22,6 +22,13 @@ async def test_get_permission() -> None:
assert await UploadView.get_permission(request) == UserAccess.Full 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: async def test_save_file(mocker: MockerFixture) -> None:
""" """
must correctly save file must correctly save file
@ -84,8 +91,8 @@ async def test_post(client: TestClient, repository_paths: RepositoryPaths, mocke
must process file upload via http must process file upload via http
""" """
local = Path("local") local = Path("local")
save_mock = mocker.patch("ahriman.web.views.v1.UploadView.save_file", save_mock = pytest.helpers.patch_view(client.app, "save_file",
side_effect=AsyncMock(return_value=("filename", local / ".filename"))) AsyncMock(return_value=("filename", local / ".filename")))
rename_mock = mocker.patch("pathlib.Path.rename") rename_mock = mocker.patch("pathlib.Path.rename")
# no content validation here because it has invalid schema # 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 must process file upload with signature via http
""" """
local = Path("local") local = Path("local")
save_mock = mocker.patch("ahriman.web.views.v1.UploadView.save_file", save_mock = pytest.helpers.patch_view(client.app, "save_file",
side_effect=AsyncMock(side_effect=[ AsyncMock(side_effect=[
("filename", local / ".filename"), ("filename", local / ".filename"),
("filename.sig", local / ".filename.sig"), ("filename.sig", local / ".filename.sig"),
])) ]))
rename_mock = mocker.patch("pathlib.Path.rename") rename_mock = mocker.patch("pathlib.Path.rename")
# no content validation here because it has invalid schema # 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.build_status import BuildStatusEnum
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.user_access import UserAccess 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: async def test_get_permission() -> None:
@ -20,6 +20,13 @@ async def test_get_permission() -> None:
assert await LogsView.get_permission(request) == UserAccess.Full 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: async def test_delete(client: TestClient, package_ahriman: Package, package_python_schedule: Package) -> None:
""" """
must delete logs for package 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.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.user_access import UserAccess 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: async def test_get_permission() -> None:
@ -20,6 +20,13 @@ async def test_get_permission() -> None:
assert await PackageView.get_permission(request) == UserAccess.Full 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: async def test_delete(client: TestClient, package_ahriman: Package, package_python_schedule: Package) -> None:
""" """
must delete single base 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.build_status import BuildStatusEnum
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.user_access import UserAccess 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: async def test_get_permission() -> None:
@ -21,6 +21,13 @@ async def test_get_permission() -> None:
assert await PackagesView.get_permission(request) == UserAccess.Full 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: async def test_get(client: TestClient, package_ahriman: Package, package_python_schedule: Package) -> None:
""" """
must return status for all packages 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.internal_status import InternalStatus
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.user_access import UserAccess 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: async def test_get_permission() -> None:
@ -23,6 +23,13 @@ async def test_get_permission() -> None:
assert await StatusView.get_permission(request) == UserAccess.Full 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: async def test_get(client: TestClient, package_ahriman: Package) -> None:
""" """
must generate web service status correctly 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 import User
from ahriman.models.user_access import UserAccess 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: async def test_get_permission() -> None:
@ -17,6 +17,13 @@ async def test_get_permission() -> None:
assert await LoginView.get_permission(request) == UserAccess.Unauthorized 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: async def test_get_default_validator(client_with_auth: TestClient) -> None:
""" """
must return 405 in case if no OAuth enabled 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 pytest_mock import MockerFixture
from ahriman.models.user_access import UserAccess 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: async def test_get_permission() -> None:
@ -17,6 +17,13 @@ async def test_get_permission() -> None:
assert await LogoutView.get_permission(request) == UserAccess.Unauthorized 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: async def test_post(client_with_auth: TestClient, mocker: MockerFixture) -> None:
""" """
must log out user correctly 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.build_status import BuildStatusEnum
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.user_access import UserAccess 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: async def test_get_permission() -> None:
@ -17,6 +17,13 @@ async def test_get_permission() -> None:
assert await LogsView.get_permission(request) == UserAccess.Reporter 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: async def test_get(client: TestClient, package_ahriman: Package) -> None:
""" """
must get logs for package must get logs for package