From 839b241ec18a41f543ad92723196b59b20079f96 Mon Sep 17 00:00:00 2001 From: Evgeniy Alekseev Date: Sun, 28 Mar 2021 14:57:28 +0300 Subject: [PATCH] complete web tests --- setup.py | 2 +- src/ahriman/core/alpm/pacman.py | 4 +- src/ahriman/core/configuration.py | 28 ++-- src/ahriman/core/report/html.py | 5 +- src/ahriman/core/repository/properties.py | 4 +- src/ahriman/web/web.py | 3 +- tests/ahriman/conftest.py | 4 +- tests/ahriman/core/test_configuration.py | 6 +- tests/ahriman/web/test_web.py | 2 - tests/ahriman/web/views/conftest.py | 14 ++ tests/ahriman/web/views/test_view_ahriman.py | 37 +++++ tests/ahriman/web/views/test_view_index.py | 19 +++ tests/ahriman/web/views/test_view_package.py | 117 +++++++++++++++ tests/ahriman/web/views/test_view_packages.py | 32 +++++ tests/testresources/core/ahriman.ini | 4 +- .../web/templates/build-status.jinja2 | 54 +++++++ .../web/templates/repo-index.jinja2 | 62 ++++++++ .../web/templates/search-line.jinja2 | 3 + .../testresources/web/templates/search.jinja2 | 25 ++++ .../web/templates/sorttable.jinja2 | 1 + .../testresources/web/templates/style.jinja2 | 136 ++++++++++++++++++ 21 files changed, 530 insertions(+), 32 deletions(-) create mode 100644 tests/ahriman/web/views/conftest.py create mode 100644 tests/ahriman/web/views/test_view_ahriman.py create mode 100644 tests/ahriman/web/views/test_view_index.py create mode 100644 tests/ahriman/web/views/test_view_package.py create mode 100644 tests/ahriman/web/views/test_view_packages.py create mode 100644 tests/testresources/web/templates/build-status.jinja2 create mode 100644 tests/testresources/web/templates/repo-index.jinja2 create mode 100644 tests/testresources/web/templates/search-line.jinja2 create mode 100644 tests/testresources/web/templates/search.jinja2 create mode 100644 tests/testresources/web/templates/sorttable.jinja2 create mode 100644 tests/testresources/web/templates/style.jinja2 diff --git a/setup.py b/setup.py index 264cbe5c..54831b34 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ setup( ], tests_require=[ "pytest", - "pytest-asyncio", + "pytest-aiohttp", "pytest-cov", "pytest-helpers-namespace", "pytest-mock", diff --git a/src/ahriman/core/alpm/pacman.py b/src/ahriman/core/alpm/pacman.py index 4e52cf5f..619764ca 100644 --- a/src/ahriman/core/alpm/pacman.py +++ b/src/ahriman/core/alpm/pacman.py @@ -35,8 +35,8 @@ class Pacman: :param config: configuration instance """ root = config.get("alpm", "root") - pacman_root = config.get("alpm", "database") - self.handle = Handle(root, pacman_root) + pacman_root = config.getpath("alpm", "database") + self.handle = Handle(root, str(pacman_root)) for repository in config.getlist("alpm", "repositories"): self.handle.register_syncdb(repository, 0) # 0 is pgp_level diff --git a/src/ahriman/core/configuration.py b/src/ahriman/core/configuration.py index 1586d69b..d9b93929 100644 --- a/src/ahriman/core/configuration.py +++ b/src/ahriman/core/configuration.py @@ -55,8 +55,7 @@ class Configuration(configparser.RawConfigParser): """ :return: path to directory with configuration includes """ - value = Path(self.get("settings", "include")) - return self.absolute_path_for(value) + return self.getpath("settings", "include") @classmethod def from_path(cls: Type[Configuration], path: Path, logfile: bool) -> Configuration: @@ -71,16 +70,6 @@ class Configuration(configparser.RawConfigParser): config.load_logging(logfile) return config - def absolute_path_for(self, path_part: Path) -> Path: - """ - helper to generate absolute configuration path for relative settings value - :param path_part: path to generate - :return: absolute path according to current path configuration - """ - if self.path is None or path_part.is_absolute(): - return path_part - return self.path.parent / path_part - def dump(self, architecture: str) -> Dict[str, Dict[str, str]]: """ dump configuration to dictionary @@ -112,6 +101,18 @@ class Configuration(configparser.RawConfigParser): return [] return raw.split() + def getpath(self, section: str, key: str) -> Path: + """ + helper to generate absolute configuration path for relative settings value + :param section: section name + :param key: key name + :return: absolute path according to current path configuration + """ + value = Path(self.get(section, key)) + if self.path is None or value.is_absolute(): + return value + return self.path.parent / value + def get_section_name(self, prefix: str, suffix: str) -> str: """ check if there is `prefix`_`suffix` section and return it on success. Return `prefix` otherwise @@ -148,8 +149,7 @@ class Configuration(configparser.RawConfigParser): """ def file_logger() -> None: try: - value = Path(self.get("settings", "logging")) - config_path = self.absolute_path_for(value) + config_path = self.getpath("settings", "logging") fileConfig(config_path) except (FileNotFoundError, PermissionError): console_logger() diff --git a/src/ahriman/core/report/html.py b/src/ahriman/core/report/html.py index be036816..3bff8567 100644 --- a/src/ahriman/core/report/html.py +++ b/src/ahriman/core/report/html.py @@ -19,7 +19,6 @@ # import jinja2 -from pathlib import Path from typing import Callable, Dict, Iterable from ahriman.core.configuration import Configuration @@ -60,9 +59,9 @@ class HTML(Report): """ Report.__init__(self, architecture, config) section = config.get_section_name("html", architecture) - self.report_path = Path(config.get(section, "path")) + self.report_path = config.getpath(section, "path") self.link_path = config.get(section, "link_path") - self.template_path = Path(config.get(section, "template_path")) + self.template_path = config.getpath(section, "template_path") # base template vars self.homepage = config.get(section, "homepage", fallback=None) diff --git a/src/ahriman/core/repository/properties.py b/src/ahriman/core/repository/properties.py index d1879377..4db2f12f 100644 --- a/src/ahriman/core/repository/properties.py +++ b/src/ahriman/core/repository/properties.py @@ -19,8 +19,6 @@ # import logging -from pathlib import Path - from ahriman.core.alpm.pacman import Pacman from ahriman.core.alpm.repo import Repo from ahriman.core.configuration import Configuration @@ -52,7 +50,7 @@ class Properties: self.aur_url = config.get("alpm", "aur_url") self.name = config.get("repository", "name") - self.paths = RepositoryPaths(Path(config.get("repository", "root")), architecture) + self.paths = RepositoryPaths(config.getpath("repository", "root"), architecture) self.paths.create_tree() self.pacman = Pacman(config) diff --git a/src/ahriman/web/web.py b/src/ahriman/web/web.py index ab8a679e..888fa3c4 100644 --- a/src/ahriman/web/web.py +++ b/src/ahriman/web/web.py @@ -82,8 +82,9 @@ def setup_service(architecture: str, config: Configuration) -> web.Application: application.logger.info("setup routes") setup_routes(application) + application.logger.info("setup templates") - aiohttp_jinja2.setup(application, loader=jinja2.FileSystemLoader(config.get("web", "templates"))) + aiohttp_jinja2.setup(application, loader=jinja2.FileSystemLoader(config.getpath("web", "templates"))) application.logger.info("setup configuration") application["config"] = config diff --git a/tests/ahriman/conftest.py b/tests/ahriman/conftest.py index 7d5ebd84..ee5aa2d2 100644 --- a/tests/ahriman/conftest.py +++ b/tests/ahriman/conftest.py @@ -83,10 +83,10 @@ def package_description_python2_schedule() -> PackageDescription: @pytest.fixture -def repository_paths() -> RepositoryPaths: +def repository_paths(configuration: Configuration) -> RepositoryPaths: return RepositoryPaths( architecture="x86_64", - root=Path("/var/lib/ahriman")) + root=configuration.getpath("repository", "root")) @pytest.fixture diff --git a/tests/ahriman/core/test_configuration.py b/tests/ahriman/core/test_configuration.py index bb29eb58..e593b29c 100644 --- a/tests/ahriman/core/test_configuration.py +++ b/tests/ahriman/core/test_configuration.py @@ -26,7 +26,8 @@ def test_absolute_path_for_absolute(configuration: Configuration) -> None: must not change path for absolute path in settings """ path = Path("/a/b/c") - assert configuration.absolute_path_for(path) == path + configuration.set("build", "path", str(path)) + assert configuration.getpath("build", "path") == path def test_absolute_path_for_relative(configuration: Configuration) -> None: @@ -34,7 +35,8 @@ def test_absolute_path_for_relative(configuration: Configuration) -> None: must prepend root path to relative path """ path = Path("a") - result = configuration.absolute_path_for(path) + configuration.set("build", "path", str(path)) + result = configuration.getpath("build", "path") assert result.is_absolute() assert result.parent == configuration.path.parent assert result.name == path.name diff --git a/tests/ahriman/web/test_web.py b/tests/ahriman/web/test_web.py index 5e42af99..e5c77100 100644 --- a/tests/ahriman/web/test_web.py +++ b/tests/ahriman/web/test_web.py @@ -8,7 +8,6 @@ from ahriman.core.status.watcher import Watcher from ahriman.web.web import on_startup, run_server -@pytest.mark.asyncio async def test_on_startup(application: web.Application, watcher: Watcher, mocker: MockerFixture) -> None: """ must call load method @@ -20,7 +19,6 @@ async def test_on_startup(application: web.Application, watcher: Watcher, mocker load_mock.assert_called_once() -@pytest.mark.asyncio async def test_on_startup_exception(application: web.Application, watcher: Watcher, mocker: MockerFixture) -> None: """ must throw exception on load error diff --git a/tests/ahriman/web/views/conftest.py b/tests/ahriman/web/views/conftest.py new file mode 100644 index 00000000..31f3da77 --- /dev/null +++ b/tests/ahriman/web/views/conftest.py @@ -0,0 +1,14 @@ +import pytest + +from aiohttp import web +from asyncio import BaseEventLoop +from pytest_aiohttp import TestClient +from pytest_mock import MockerFixture +from typing import Any + + +@pytest.fixture +def client(application: web.Application, loop: BaseEventLoop, + aiohttp_client: Any, mocker: MockerFixture) -> TestClient: + mocker.patch("pathlib.Path.iterdir", return_value=[]) + return loop.run_until_complete(aiohttp_client(application)) diff --git a/tests/ahriman/web/views/test_view_ahriman.py b/tests/ahriman/web/views/test_view_ahriman.py new file mode 100644 index 00000000..250eb731 --- /dev/null +++ b/tests/ahriman/web/views/test_view_ahriman.py @@ -0,0 +1,37 @@ +from aiohttp.test_utils import TestClient + +from ahriman.models.build_status import BuildStatus, BuildStatusEnum + + +async def test_get(client: TestClient) -> None: + """ + must return valid service status + """ + response = await client.get("/api/v1/ahriman") + status = BuildStatus.from_json(await response.json()) + + assert response.status == 200 + assert status.status == BuildStatusEnum.Unknown + + +async def test_post(client: TestClient) -> None: + """ + must update service status correctly + """ + payload = {"status": BuildStatusEnum.Success.value} + post_response = await client.post("/api/v1/ahriman", json=payload) + assert post_response.status == 204 + + response = await client.get("/api/v1/ahriman") + status = BuildStatus.from_json(await response.json()) + + assert response.status == 200 + assert status.status == BuildStatusEnum.Success + + +async def test_post_exception(client: TestClient) -> None: + """ + must raise exception on invalid payload + """ + post_response = await client.post("/api/v1/ahriman", json={}) + assert post_response.status == 400 diff --git a/tests/ahriman/web/views/test_view_index.py b/tests/ahriman/web/views/test_view_index.py new file mode 100644 index 00000000..1d2099fa --- /dev/null +++ b/tests/ahriman/web/views/test_view_index.py @@ -0,0 +1,19 @@ +from pytest_aiohttp import TestClient + + +async def test_get(client: TestClient) -> None: + """ + must generate status page correctly (/) + """ + response = await client.get("/") + assert response.status == 200 + assert await response.text() + + +async def test_get_index(client: TestClient) -> None: + """ + must generate status page correctly (/index.html) + """ + response = await client.get("/index.html") + assert response.status == 200 + assert await response.text() diff --git a/tests/ahriman/web/views/test_view_package.py b/tests/ahriman/web/views/test_view_package.py new file mode 100644 index 00000000..d804a8a0 --- /dev/null +++ b/tests/ahriman/web/views/test_view_package.py @@ -0,0 +1,117 @@ +from pytest_aiohttp import TestClient + +from ahriman.models.build_status import BuildStatus, BuildStatusEnum +from ahriman.models.package import Package + + +async def test_get(client: TestClient, package_ahriman: Package, package_python_schedule: Package) -> None: + """ + must return status for specific package + """ + await client.post(f"/api/v1/packages/{package_ahriman.base}", + json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()}) + await client.post(f"/api/v1/packages/{package_python_schedule.base}", + json={"status": BuildStatusEnum.Success.value, "package": package_python_schedule.view()}) + + response = await client.get(f"/api/v1/packages/{package_ahriman.base}") + assert response.status == 200 + + packages = [Package.from_json(item["package"]) for item in await response.json()] + assert packages + assert {package.base for package in packages} == {package_ahriman.base} + + +async def test_get_not_found(client: TestClient, package_ahriman: Package) -> None: + """ + must return Not Found for unknown package + """ + response = await client.get(f"/api/v1/packages/{package_ahriman.base}") + assert response.status == 404 + + +async def test_delete(client: TestClient, package_ahriman: Package, package_python_schedule: Package) -> None: + """ + must delete single base + """ + await client.post(f"/api/v1/packages/{package_ahriman.base}", + json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()}) + await client.post(f"/api/v1/packages/{package_python_schedule.base}", + json={"status": BuildStatusEnum.Success.value, "package": package_python_schedule.view()}) + + response = await client.delete(f"/api/v1/packages/{package_ahriman.base}") + assert response.status == 204 + + response = await client.get(f"/api/v1/packages/{package_ahriman.base}") + assert response.status == 404 + + response = await client.get(f"/api/v1/packages/{package_python_schedule.base}") + assert response.status == 200 + + +async def test_delete_unknown(client: TestClient, package_ahriman: Package, package_python_schedule: Package) -> None: + """ + must suppress errors on unknown package deletion + """ + await client.post(f"/api/v1/packages/{package_python_schedule.base}", + json={"status": BuildStatusEnum.Success.value, "package": package_python_schedule.view()}) + + response = await client.delete(f"/api/v1/packages/{package_ahriman.base}") + assert response.status == 204 + + response = await client.get(f"/api/v1/packages/{package_ahriman.base}") + assert response.status == 404 + + response = await client.get(f"/api/v1/packages/{package_python_schedule.base}") + assert response.status == 200 + + +async def test_post(client: TestClient, package_ahriman: Package) -> None: + """ + must update package status + """ + post_response = await client.post( + f"/api/v1/packages/{package_ahriman.base}", + json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()}) + assert post_response.status == 204 + + response = await client.get(f"/api/v1/packages/{package_ahriman.base}") + assert response.status == 200 + + +async def test_post_exception(client: TestClient, package_ahriman: Package) -> None: + """ + must raise exception on invalid payload + """ + post_response = await client.post(f"/api/v1/packages/{package_ahriman.base}", json={}) + assert post_response.status == 400 + + +async def test_post_light(client: TestClient, package_ahriman: Package) -> None: + """ + must update package status only + """ + post_response = await client.post( + f"/api/v1/packages/{package_ahriman.base}", + json={"status": BuildStatusEnum.Unknown.value, "package": package_ahriman.view()}) + assert post_response.status == 204 + + post_response = await client.post( + f"/api/v1/packages/{package_ahriman.base}", json={"status": BuildStatusEnum.Success.value}) + assert post_response.status == 204 + + response = await client.get(f"/api/v1/packages/{package_ahriman.base}") + assert response.status == 200 + statuses = { + Package.from_json(item["package"]).base: BuildStatus.from_json(item["status"]) + for item in await response.json() + } + assert statuses[package_ahriman.base].status == BuildStatusEnum.Success + + +async def test_post_not_found(client: TestClient, package_ahriman: Package) -> None: + """ + must raise exception on status update for unknown package + """ + post_response = await client.post( + f"/api/v1/packages/{package_ahriman.base}", json={"status": BuildStatusEnum.Success.value}) + assert post_response.status == 400 diff --git a/tests/ahriman/web/views/test_view_packages.py b/tests/ahriman/web/views/test_view_packages.py new file mode 100644 index 00000000..a1691b65 --- /dev/null +++ b/tests/ahriman/web/views/test_view_packages.py @@ -0,0 +1,32 @@ +from pytest_aiohttp import TestClient +from pytest_mock import MockerFixture + +from ahriman.models.build_status import BuildStatusEnum +from ahriman.models.package import Package + + +async def test_get(client: TestClient, package_ahriman: Package, package_python_schedule: Package) -> None: + """ + must return status for all packages + """ + await client.post(f"/api/v1/packages/{package_ahriman.base}", + json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()}) + await client.post(f"/api/v1/packages/{package_python_schedule.base}", + json={"status": BuildStatusEnum.Success.value, "package": package_python_schedule.view()}) + + response = await client.get("/api/v1/packages") + assert response.status == 200 + + packages = [Package.from_json(item["package"]) for item in await response.json()] + assert packages + assert {package.base for package in packages} == {package_ahriman.base, package_python_schedule.base} + + +async def test_post(client: TestClient, mocker: MockerFixture) -> None: + """ + must be able to reload packages + """ + load_mock = mocker.patch("ahriman.core.status.watcher.Watcher.load") + response = await client.post("/api/v1/packages") + assert response.status == 204 + load_mock.assert_called_once() diff --git a/tests/testresources/core/ahriman.ini b/tests/testresources/core/ahriman.ini index 190835df..b918f520 100644 --- a/tests/testresources/core/ahriman.ini +++ b/tests/testresources/core/ahriman.ini @@ -29,7 +29,7 @@ target = path = homepage = link_path = -template_path = /usr/share/ahriman/repo-index.jinja2 +template_path = ../web/templates/repo-index.jinja2 [upload] target = @@ -41,4 +41,4 @@ remote = bucket = [web] -templates = /usr/share/ahriman \ No newline at end of file +templates = ../web/templates \ No newline at end of file diff --git a/tests/testresources/web/templates/build-status.jinja2 b/tests/testresources/web/templates/build-status.jinja2 new file mode 100644 index 00000000..3d36e9d1 --- /dev/null +++ b/tests/testresources/web/templates/build-status.jinja2 @@ -0,0 +1,54 @@ + + + + {{ repository|e }} + + {% include "style.jinja2" %} + + {% include "sorttable.jinja2" %} + {% include "search.jinja2" %} + + + +
+

ahriman + {{ version|e }} + {{ architecture|e }} + {{ service.status|e }} +

+ + {% include "search-line.jinja2" %} + +
+ + + + + + + + + + {% for package in packages %} + + + + + + + + {% endfor %} +
package basepackagesversionlast updatestatus
{{ package.version|e }}{{ package.timestamp|e }}{{ package.status|e }}
+
+ + +
+ + + diff --git a/tests/testresources/web/templates/repo-index.jinja2 b/tests/testresources/web/templates/repo-index.jinja2 new file mode 100644 index 00000000..3edbb86f --- /dev/null +++ b/tests/testresources/web/templates/repo-index.jinja2 @@ -0,0 +1,62 @@ + + + + {{ repository|e }} + + {% include "style.jinja2" %} + + {% include "sorttable.jinja2" %} + {% include "search.jinja2" %} + + + +
+

Archlinux user repository

+ +
+ {% if pgp_key is not none %} +

This repository is signed with {{ pgp_key|e }} by default.

+ {% endif %} + + + $ cat /etc/pacman.conf
+ [{{ repository|e }}]
+ Server = {{ link_path|e }}
+ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Package{% if has_package_signed %}Required{% else %}Never{% endif %} TrustedOnly +
+
+ + {% include "search-line.jinja2" %} + +
+ + + + + + + + + + {% for package in packages %} + + + + + + + + {% endfor %} +
packageversionarchive sizeinstalled sizebuild date
{{ package.version|e }}{{ package.archive_size|e }}{{ package.installed_size|e }}{{ package.build_date|e }}
+
+ +
+ +
+
+ + diff --git a/tests/testresources/web/templates/search-line.jinja2 b/tests/testresources/web/templates/search-line.jinja2 new file mode 100644 index 00000000..cb685672 --- /dev/null +++ b/tests/testresources/web/templates/search-line.jinja2 @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/tests/testresources/web/templates/search.jinja2 b/tests/testresources/web/templates/search.jinja2 new file mode 100644 index 00000000..a1c80092 --- /dev/null +++ b/tests/testresources/web/templates/search.jinja2 @@ -0,0 +1,25 @@ + \ No newline at end of file diff --git a/tests/testresources/web/templates/sorttable.jinja2 b/tests/testresources/web/templates/sorttable.jinja2 new file mode 100644 index 00000000..12a1f08a --- /dev/null +++ b/tests/testresources/web/templates/sorttable.jinja2 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/testresources/web/templates/style.jinja2 b/tests/testresources/web/templates/style.jinja2 new file mode 100644 index 00000000..26291537 --- /dev/null +++ b/tests/testresources/web/templates/style.jinja2 @@ -0,0 +1,136 @@ + \ No newline at end of file