diff --git a/docs/faq.rst b/docs/faq.rst
index 578808be..094e8f66 100644
--- a/docs/faq.rst
+++ b/docs/faq.rst
@@ -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 `_.
+
I did not find my question
^^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/src/ahriman/core/database/migrations/__init__.py b/src/ahriman/core/database/migrations/__init__.py
index 72df8146..3179eef8 100644
--- a/src/ahriman/core/database/migrations/__init__.py
+++ b/src/ahriman/core/database/migrations/__init__.py
@@ -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)])]
diff --git a/src/ahriman/web/routes.py b/src/ahriman/web/routes.py
index ed04d7e4..1c080676 100644
--- a/src/ahriman/web/routes.py
+++ b/src/ahriman/web/routes.py
@@ -17,18 +17,90 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-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)
diff --git a/src/ahriman/web/views/api/docs.py b/src/ahriman/web/views/api/docs.py
index 24a6bfd5..bde988e1 100644
--- a/src/ahriman/web/views/api/docs.py
+++ b/src/ahriman/web/views/api/docs.py
@@ -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]:
diff --git a/src/ahriman/web/views/api/swagger.py b/src/ahriman/web/views/api/swagger.py
index e3517a07..c663f209 100644
--- a/src/ahriman/web/views/api/swagger.py
+++ b/src/ahriman/web/views/api/swagger.py
@@ -34,6 +34,7 @@ class SwaggerView(BaseView):
"""
GET_PERMISSION = UserAccess.Unauthorized
+ ROUTES = ["/api-docs/swagger.json"]
async def get(self) -> Response:
"""
diff --git a/src/ahriman/web/views/base.py b/src/ahriman/web/views/base.py
index de4b6cab..dca09e5a 100644
--- a/src/ahriman/web/views/base.py
+++ b/src/ahriman/web/views/base.py
@@ -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:
diff --git a/src/ahriman/web/views/index.py b/src/ahriman/web/views/index.py
index b2df4a48..63d5adb4 100644
--- a/src/ahriman/web/views/index.py
+++ b/src/ahriman/web/views/index.py
@@ -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]:
diff --git a/src/ahriman/web/views/v1/__init__.py b/src/ahriman/web/views/v1/__init__.py
index a7057660..8fc622e9 100644
--- a/src/ahriman/web/views/v1/__init__.py
+++ b/src/ahriman/web/views/v1/__init__.py
@@ -17,20 +17,3 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-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
diff --git a/src/ahriman/web/views/v1/service/add.py b/src/ahriman/web/views/v1/service/add.py
index 68dbe0c6..6a13f362 100644
--- a/src/ahriman/web/views/v1/service/add.py
+++ b/src/ahriman/web/views/v1/service/add.py
@@ -35,6 +35,7 @@ class AddView(BaseView):
"""
POST_PERMISSION = UserAccess.Full
+ ROUTES = ["/api/v1/service/add"]
@aiohttp_apispec.docs(
tags=["Actions"],
diff --git a/src/ahriman/web/views/v1/service/pgp.py b/src/ahriman/web/views/v1/service/pgp.py
index cfb56d88..2ccf4465 100644
--- a/src/ahriman/web/views/v1/service/pgp.py
+++ b/src/ahriman/web/views/v1/service/pgp.py
@@ -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"],
diff --git a/src/ahriman/web/views/v1/service/process.py b/src/ahriman/web/views/v1/service/process.py
index c27057f8..849088fa 100644
--- a/src/ahriman/web/views/v1/service/process.py
+++ b/src/ahriman/web/views/v1/service/process.py
@@ -35,6 +35,7 @@ class ProcessView(BaseView):
"""
GET_PERMISSION = UserAccess.Reporter
+ ROUTES = ["/api/v1/service/process/{process_id}"]
@aiohttp_apispec.docs(
tags=["Actions"],
diff --git a/src/ahriman/web/views/v1/service/rebuild.py b/src/ahriman/web/views/v1/service/rebuild.py
index bf32beeb..728c4484 100644
--- a/src/ahriman/web/views/v1/service/rebuild.py
+++ b/src/ahriman/web/views/v1/service/rebuild.py
@@ -35,6 +35,7 @@ class RebuildView(BaseView):
"""
POST_PERMISSION = UserAccess.Full
+ ROUTES = ["/api/v1/service/rebuild"]
@aiohttp_apispec.docs(
tags=["Actions"],
diff --git a/src/ahriman/web/views/v1/service/remove.py b/src/ahriman/web/views/v1/service/remove.py
index b241cd0d..2f0fc73e 100644
--- a/src/ahriman/web/views/v1/service/remove.py
+++ b/src/ahriman/web/views/v1/service/remove.py
@@ -35,6 +35,7 @@ class RemoveView(BaseView):
"""
POST_PERMISSION = UserAccess.Full
+ ROUTES = ["/api/v1/service/remove"]
@aiohttp_apispec.docs(
tags=["Actions"],
diff --git a/src/ahriman/web/views/v1/service/request.py b/src/ahriman/web/views/v1/service/request.py
index f6dbfede..e1220a8a 100644
--- a/src/ahriman/web/views/v1/service/request.py
+++ b/src/ahriman/web/views/v1/service/request.py
@@ -35,6 +35,7 @@ class RequestView(BaseView):
"""
POST_PERMISSION = UserAccess.Reporter
+ ROUTES = ["/api/v1/service/request"]
@aiohttp_apispec.docs(
tags=["Actions"],
diff --git a/src/ahriman/web/views/v1/service/search.py b/src/ahriman/web/views/v1/service/search.py
index 4c75c3fd..0718cdcf 100644
--- a/src/ahriman/web/views/v1/service/search.py
+++ b/src/ahriman/web/views/v1/service/search.py
@@ -38,6 +38,7 @@ class SearchView(BaseView):
"""
GET_PERMISSION = UserAccess.Reporter
+ ROUTES = ["/api/v1/service/search"]
@aiohttp_apispec.docs(
tags=["Actions"],
diff --git a/src/ahriman/web/views/v1/service/update.py b/src/ahriman/web/views/v1/service/update.py
index 944c04e2..67108374 100644
--- a/src/ahriman/web/views/v1/service/update.py
+++ b/src/ahriman/web/views/v1/service/update.py
@@ -35,6 +35,7 @@ class UpdateView(BaseView):
"""
POST_PERMISSION = UserAccess.Full
+ ROUTES = ["/api/v1/service/update"]
@aiohttp_apispec.docs(
tags=["Actions"],
diff --git a/src/ahriman/web/views/v1/service/upload.py b/src/ahriman/web/views/v1/service/upload.py
index 686e2c85..96a9dbd5 100644
--- a/src/ahriman/web/views/v1/service/upload.py
+++ b/src/ahriman/web/views/v1/service/upload.py
@@ -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]:
diff --git a/src/ahriman/web/views/v1/status/logs.py b/src/ahriman/web/views/v1/status/logs.py
index 2ab07fcb..4d9a0b99 100644
--- a/src/ahriman/web/views/v1/status/logs.py
+++ b/src/ahriman/web/views/v1/status/logs.py
@@ -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"],
diff --git a/src/ahriman/web/views/v1/status/package.py b/src/ahriman/web/views/v1/status/package.py
index 68c1da49..cf9556fa 100644
--- a/src/ahriman/web/views/v1/status/package.py
+++ b/src/ahriman/web/views/v1/status/package.py
@@ -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"],
diff --git a/src/ahriman/web/views/v1/status/packages.py b/src/ahriman/web/views/v1/status/packages.py
index 604352d0..cb01c688 100644
--- a/src/ahriman/web/views/v1/status/packages.py
+++ b/src/ahriman/web/views/v1/status/packages.py
@@ -41,6 +41,7 @@ class PackagesView(BaseView):
GET_PERMISSION = UserAccess.Read
POST_PERMISSION = UserAccess.Full
+ ROUTES = ["/api/v1/packages"]
@aiohttp_apispec.docs(
tags=["Packages"],
diff --git a/src/ahriman/web/views/v1/status/status.py b/src/ahriman/web/views/v1/status/status.py
index 8bd4feef..bb625a36 100644
--- a/src/ahriman/web/views/v1/status/status.py
+++ b/src/ahriman/web/views/v1/status/status.py
@@ -41,6 +41,7 @@ class StatusView(BaseView):
GET_PERMISSION = UserAccess.Read
POST_PERMISSION = UserAccess.Full
+ ROUTES = ["/api/v1/status"]
@aiohttp_apispec.docs(
tags=["Status"],
diff --git a/src/ahriman/web/views/v1/user/login.py b/src/ahriman/web/views/v1/user/login.py
index bf78399f..28a05c56 100644
--- a/src/ahriman/web/views/v1/user/login.py
+++ b/src/ahriman/web/views/v1/user/login.py
@@ -37,6 +37,7 @@ class LoginView(BaseView):
"""
GET_PERMISSION = POST_PERMISSION = UserAccess.Unauthorized
+ ROUTES = ["/api/v1/login"]
@aiohttp_apispec.docs(
tags=["Login"],
diff --git a/src/ahriman/web/views/v1/user/logout.py b/src/ahriman/web/views/v1/user/logout.py
index 3db4aaf2..be9fe0bc 100644
--- a/src/ahriman/web/views/v1/user/logout.py
+++ b/src/ahriman/web/views/v1/user/logout.py
@@ -36,6 +36,7 @@ class LogoutView(BaseView):
"""
POST_PERMISSION = UserAccess.Unauthorized
+ ROUTES = ["/api/v1/logout"]
@aiohttp_apispec.docs(
tags=["Login"],
diff --git a/src/ahriman/web/views/v2/__init__.py b/src/ahriman/web/views/v2/__init__.py
index c67945b4..8fc622e9 100644
--- a/src/ahriman/web/views/v2/__init__.py
+++ b/src/ahriman/web/views/v2/__init__.py
@@ -17,4 +17,3 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-from ahriman.web.views.v2.status.logs import LogsView
diff --git a/src/ahriman/web/views/v2/status/logs.py b/src/ahriman/web/views/v2/status/logs.py
index 45ed66f5..a9e2fbbb 100644
--- a/src/ahriman/web/views/v2/status/logs.py
+++ b/src/ahriman/web/views/v2/status/logs.py
@@ -37,6 +37,7 @@ class LogsView(BaseView):
"""
GET_PERMISSION = UserAccess.Reporter
+ ROUTES = ["/api/v2/packages/{package}/logs"]
@aiohttp_apispec.docs(
tags=["Packages"],
diff --git a/tests/ahriman/web/conftest.py b/tests/ahriman/web/conftest.py
index 05b522ee..7110b846 100644
--- a/tests/ahriman/web/conftest.py
+++ b/tests/ahriman/web/conftest.py
@@ -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:
diff --git a/tests/ahriman/web/test_routes.py b/tests/ahriman/web/test_routes.py
index 48c139b5..12f54a88 100644
--- a/tests/ahriman/web/test_routes.py
+++ b/tests/ahriman/web/test_routes.py
@@ -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:
diff --git a/tests/ahriman/web/views/test_view_base.py b/tests/ahriman/web/views/test_view_base.py
index 4433dc18..3b040e19 100644
--- a/tests/ahriman/web/views/test_view_base.py
+++ b/tests/ahriman/web/views/test_view_base.py
@@ -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
diff --git a/tests/ahriman/web/views/test_view_index.py b/tests/ahriman/web/views/test_view_index.py
index d73ccbde..cf75dd27 100644
--- a/tests/ahriman/web/views/test_view_index.py
+++ b/tests/ahriman/web/views/test_view_index.py
@@ -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 (/)
diff --git a/tests/ahriman/web/views/v1/service/test_view_v1_service_add.py b/tests/ahriman/web/views/v1/service/test_view_v1_service_add.py
index f6948906..87a1c64b 100644
--- a/tests/ahriman/web/views/v1/service/test_view_v1_service_add.py
+++ b/tests/ahriman/web/views/v1/service/test_view_v1_service_add.py
@@ -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
diff --git a/tests/ahriman/web/views/v1/service/test_view_v1_service_pgp.py b/tests/ahriman/web/views/v1/service/test_view_v1_service_pgp.py
index 9cb95dbb..fd577a83 100644
--- a/tests/ahriman/web/views/v1/service/test_view_v1_service_pgp.py
+++ b/tests/ahriman/web/views/v1/service/test_view_v1_service_pgp.py
@@ -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
diff --git a/tests/ahriman/web/views/v1/service/test_view_v1_service_process.py b/tests/ahriman/web/views/v1/service/test_view_v1_service_process.py
index 1fa16cae..339972e2 100644
--- a/tests/ahriman/web/views/v1/service/test_view_v1_service_process.py
+++ b/tests/ahriman/web/views/v1/service/test_view_v1_service_process.py
@@ -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
diff --git a/tests/ahriman/web/views/v1/service/test_view_v1_service_rebuild.py b/tests/ahriman/web/views/v1/service/test_view_v1_service_rebuild.py
index 66bf475d..a092e0c7 100644
--- a/tests/ahriman/web/views/v1/service/test_view_v1_service_rebuild.py
+++ b/tests/ahriman/web/views/v1/service/test_view_v1_service_rebuild.py
@@ -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
diff --git a/tests/ahriman/web/views/v1/service/test_view_v1_service_remove.py b/tests/ahriman/web/views/v1/service/test_view_v1_service_remove.py
index dc1feaf0..1489a6c8 100644
--- a/tests/ahriman/web/views/v1/service/test_view_v1_service_remove.py
+++ b/tests/ahriman/web/views/v1/service/test_view_v1_service_remove.py
@@ -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
diff --git a/tests/ahriman/web/views/v1/service/test_view_v1_service_request.py b/tests/ahriman/web/views/v1/service/test_view_v1_service_request.py
index 4966eecf..904f444f 100644
--- a/tests/ahriman/web/views/v1/service/test_view_v1_service_request.py
+++ b/tests/ahriman/web/views/v1/service/test_view_v1_service_request.py
@@ -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
diff --git a/tests/ahriman/web/views/v1/service/test_view_v1_service_search.py b/tests/ahriman/web/views/v1/service/test_view_v1_service_search.py
index 533f8fc0..8e288a0d 100644
--- a/tests/ahriman/web/views/v1/service/test_view_v1_service_search.py
+++ b/tests/ahriman/web/views/v1/service/test_view_v1_service_search.py
@@ -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
diff --git a/tests/ahriman/web/views/v1/service/test_view_v1_service_update.py b/tests/ahriman/web/views/v1/service/test_view_v1_service_update.py
index d29f4b9d..8c185a27 100644
--- a/tests/ahriman/web/views/v1/service/test_view_v1_service_update.py
+++ b/tests/ahriman/web/views/v1/service/test_view_v1_service_update.py
@@ -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
diff --git a/tests/ahriman/web/views/v1/service/test_view_v1_service_upload.py b/tests/ahriman/web/views/v1/service/test_view_v1_service_upload.py
index a472651a..8e4ed26b 100644
--- a/tests/ahriman/web/views/v1/service/test_view_v1_service_upload.py
+++ b/tests/ahriman/web/views/v1/service/test_view_v1_service_upload.py
@@ -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
diff --git a/tests/ahriman/web/views/v1/status/test_view_v1_status_logs.py b/tests/ahriman/web/views/v1/status/test_view_v1_status_logs.py
index 1a73a010..8759b53f 100644
--- a/tests/ahriman/web/views/v1/status/test_view_v1_status_logs.py
+++ b/tests/ahriman/web/views/v1/status/test_view_v1_status_logs.py
@@ -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
diff --git a/tests/ahriman/web/views/v1/status/test_view_v1_status_package.py b/tests/ahriman/web/views/v1/status/test_view_v1_status_package.py
index 8559f7fd..d8c2efad 100644
--- a/tests/ahriman/web/views/v1/status/test_view_v1_status_package.py
+++ b/tests/ahriman/web/views/v1/status/test_view_v1_status_package.py
@@ -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
diff --git a/tests/ahriman/web/views/v1/status/test_view_v1_status_packages.py b/tests/ahriman/web/views/v1/status/test_view_v1_status_packages.py
index fabde948..1718cddb 100644
--- a/tests/ahriman/web/views/v1/status/test_view_v1_status_packages.py
+++ b/tests/ahriman/web/views/v1/status/test_view_v1_status_packages.py
@@ -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
diff --git a/tests/ahriman/web/views/v1/status/test_view_v1_status_status.py b/tests/ahriman/web/views/v1/status/test_view_v1_status_status.py
index fbb1035d..76bea32d 100644
--- a/tests/ahriman/web/views/v1/status/test_view_v1_status_status.py
+++ b/tests/ahriman/web/views/v1/status/test_view_v1_status_status.py
@@ -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
diff --git a/tests/ahriman/web/views/v1/user/test_view_v1_user_login.py b/tests/ahriman/web/views/v1/user/test_view_v1_user_login.py
index c8f35f67..9a47e250 100644
--- a/tests/ahriman/web/views/v1/user/test_view_v1_user_login.py
+++ b/tests/ahriman/web/views/v1/user/test_view_v1_user_login.py
@@ -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
diff --git a/tests/ahriman/web/views/v1/user/test_view_v1_user_logout.py b/tests/ahriman/web/views/v1/user/test_view_v1_user_logout.py
index 651a38a6..8859be3e 100644
--- a/tests/ahriman/web/views/v1/user/test_view_v1_user_logout.py
+++ b/tests/ahriman/web/views/v1/user/test_view_v1_user_logout.py
@@ -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
diff --git a/tests/ahriman/web/views/v2/status/test_view_v2_status_logs.py b/tests/ahriman/web/views/v2/status/test_view_v2_status_logs.py
index c9a31ef7..73466318 100644
--- a/tests/ahriman/web/views/v2/status/test_view_v2_status_logs.py
+++ b/tests/ahriman/web/views/v2/status/test_view_v2_status_logs.py
@@ -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