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 d5f4fc9b86
commit 1859d14f78
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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.
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
^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -92,6 +92,7 @@ class Migrations(LazyLogging):
list[Migration]: list of found migrations
"""
migrations: list[Migration] = []
package_dir = Path(__file__).resolve().parent
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
# 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 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.api.swagger import SwaggerView
from ahriman.web.views.index import IndexView
from ahriman.web.views import v1, v2
from ahriman.web.views.base import BaseView
__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:
"""
setup all defined routes
@ -37,30 +109,8 @@ def setup_routes(application: Application, static_path: Path) -> None:
application(Application): web application instance
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_view("/api/v1/service/add", v1.AddView)
application.router.add_view("/api/v1/service/pgp", v1.PGPView)
application.router.add_view("/api/v1/service/rebuild", v1.RebuildView)
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)
views = Path(__file__).parent / "views"
for route, view in _dynamic_routes(views).items():
application.router.add_view(route, view)

View File

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

View File

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

View File

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

View File

@ -44,6 +44,7 @@ class IndexView(BaseView):
"""
GET_PERMISSION = UserAccess.Unauthorized
ROUTES = ["/", "/index.html"]
@aiohttp_jinja2.template("build-status.jinja2")
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
# 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
ROUTES = ["/api/v1/service/add"]
@aiohttp_apispec.docs(
tags=["Actions"],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -39,6 +39,7 @@ class UploadView(BaseView):
"""
POST_PERMISSION = UserAccess.Full
ROUTES = ["/api/v1/service/upload"]
@staticmethod
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
GET_PERMISSION = UserAccess.Reporter
ROUTES = ["/api/v1/packages/{package}/logs"]
@aiohttp_apispec.docs(
tags=["Packages"],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,4 +17,3 @@
# You should have received a copy of the GNU General Public License
# 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
ROUTES = ["/api/v2/packages/{package}/logs"]
@aiohttp_apispec.docs(
tags=["Packages"],

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