From afbd956fe29c0c7b2583e084764b50c6f152c257 Mon Sep 17 00:00:00 2001 From: Evgeniy Alekseev Date: Sat, 27 Mar 2021 17:28:42 +0300 Subject: [PATCH] complete status tests --- src/ahriman/application/lock.py | 4 +- src/ahriman/core/exceptions.py | 9 + src/ahriman/core/repository/properties.py | 2 +- .../core/{watcher => status}/__init__.py | 0 .../core/{watcher => status}/client.py | 2 +- .../core/{watcher => status}/watcher.py | 19 +- .../core/{watcher => status}/web_client.py | 6 +- src/ahriman/web/views/base.py | 2 +- src/ahriman/web/views/package.py | 5 +- src/ahriman/web/web.py | 2 +- .../ahriman/core/repository/test_executor.py | 32 +-- .../core/repository/test_update_handler.py | 20 +- tests/ahriman/core/status/conftest.py | 39 ++++ tests/ahriman/core/status/test_client.py | 116 ++++++++++ tests/ahriman/core/status/test_watcher.py | 219 ++++++++++++++++++ tests/ahriman/core/status/test_web_client.py | 163 +++++++++++++ 16 files changed, 599 insertions(+), 41 deletions(-) rename src/ahriman/core/{watcher => status}/__init__.py (100%) rename src/ahriman/core/{watcher => status}/client.py (98%) rename src/ahriman/core/{watcher => status}/watcher.py (90%) rename src/ahriman/core/{watcher => status}/web_client.py (99%) create mode 100644 tests/ahriman/core/status/conftest.py create mode 100644 tests/ahriman/core/status/test_client.py create mode 100644 tests/ahriman/core/status/test_watcher.py create mode 100644 tests/ahriman/core/status/test_web_client.py diff --git a/src/ahriman/application/lock.py b/src/ahriman/application/lock.py index 78aba4e7..4dcaf336 100644 --- a/src/ahriman/application/lock.py +++ b/src/ahriman/application/lock.py @@ -28,7 +28,7 @@ from typing import Literal, Optional, Type from ahriman.core.configuration import Configuration from ahriman.core.exceptions import DuplicateRun, UnsafeRun -from ahriman.core.watcher.client import Client +from ahriman.core.status.client import Client from ahriman.models.build_status import BuildStatusEnum @@ -105,7 +105,7 @@ class Lock: if self.path is None: return try: - self.path.touch() + self.path.touch(exist_ok=False) except FileExistsError: raise DuplicateRun() diff --git a/src/ahriman/core/exceptions.py b/src/ahriman/core/exceptions.py index c866734f..9ea4d48a 100644 --- a/src/ahriman/core/exceptions.py +++ b/src/ahriman/core/exceptions.py @@ -107,6 +107,15 @@ class SyncFailed(Exception): Exception.__init__(self, "Sync failed") +class UnknownPackage(Exception): + """ + exception for status watcher which will be thrown on unknown package + """ + + def __init__(self, base: str) -> None: + Exception.__init__(self, f"Package base {base} is unknown") + + class UnsafeRun(Exception): """ exception which will be raised in case if user is not owner of repository diff --git a/src/ahriman/core/repository/properties.py b/src/ahriman/core/repository/properties.py index ecedea2b..09676964 100644 --- a/src/ahriman/core/repository/properties.py +++ b/src/ahriman/core/repository/properties.py @@ -25,7 +25,7 @@ from ahriman.core.alpm.pacman import Pacman from ahriman.core.alpm.repo import Repo from ahriman.core.configuration import Configuration from ahriman.core.sign.gpg import GPG -from ahriman.core.watcher.client import Client +from ahriman.core.status.client import Client from ahriman.models.repository_paths import RepositoryPaths diff --git a/src/ahriman/core/watcher/__init__.py b/src/ahriman/core/status/__init__.py similarity index 100% rename from src/ahriman/core/watcher/__init__.py rename to src/ahriman/core/status/__init__.py diff --git a/src/ahriman/core/watcher/client.py b/src/ahriman/core/status/client.py similarity index 98% rename from src/ahriman/core/watcher/client.py rename to src/ahriman/core/status/client.py index 95193cd3..69adefa5 100644 --- a/src/ahriman/core/watcher/client.py +++ b/src/ahriman/core/status/client.py @@ -124,5 +124,5 @@ class Client: if host is None or port is None: return Client() - from ahriman.core.watcher.web_client import WebClient + from ahriman.core.status.web_client import WebClient return WebClient(host, port) diff --git a/src/ahriman/core/watcher/watcher.py b/src/ahriman/core/status/watcher.py similarity index 90% rename from src/ahriman/core/watcher/watcher.py rename to src/ahriman/core/status/watcher.py index dc418774..aa9c19fd 100644 --- a/src/ahriman/core/watcher/watcher.py +++ b/src/ahriman/core/status/watcher.py @@ -24,6 +24,7 @@ from pathlib import Path from typing import Any, Dict, List, Optional, Tuple from ahriman.core.configuration import Configuration +from ahriman.core.exceptions import UnknownPackage from ahriman.core.repository.repository import Repository from ahriman.models.build_status import BuildStatus, BuildStatusEnum from ahriman.models.package import Package @@ -80,8 +81,12 @@ class Watcher: if not self.cache_path.is_file(): return with self.cache_path.open() as cache: - dump = json.load(cache) - for item in dump["packages"]: + try: + dump = json.load(cache) + except Exception: + self.logger.exception("cannot parse json from file") + dump = {} + for item in dump.get("packages", []): try: parse_single(item) except Exception: @@ -110,7 +115,10 @@ class Watcher: get current package base build status :return: package and its status """ - return self.known[base] + try: + return self.known[base] + except KeyError: + raise UnknownPackage(base) def load(self) -> None: """ @@ -142,7 +150,10 @@ class Watcher: :param package: optional new package description. In case if not set current properties will be used """ if package is None: - package, _ = self.known[base] + try: + package, _ = self.known[base] + except KeyError: + raise UnknownPackage(base) full_status = BuildStatus(status) self.known[base] = (package, full_status) self._cache_save() diff --git a/src/ahriman/core/watcher/web_client.py b/src/ahriman/core/status/web_client.py similarity index 99% rename from src/ahriman/core/watcher/web_client.py rename to src/ahriman/core/status/web_client.py index d4280464..1686ddaf 100644 --- a/src/ahriman/core/watcher/web_client.py +++ b/src/ahriman/core/status/web_client.py @@ -18,11 +18,11 @@ # along with this program. If not, see . # import logging -from typing import List, Optional, Tuple - import requests -from ahriman.core.watcher.client import Client +from typing import List, Optional, Tuple + +from ahriman.core.status.client import Client from ahriman.models.build_status import BuildStatusEnum, BuildStatus from ahriman.models.package import Package diff --git a/src/ahriman/web/views/base.py b/src/ahriman/web/views/base.py index 1767a508..2d108c40 100644 --- a/src/ahriman/web/views/base.py +++ b/src/ahriman/web/views/base.py @@ -19,7 +19,7 @@ # from aiohttp.web import View -from ahriman.core.watcher.watcher import Watcher +from ahriman.core.status.watcher import Watcher class BaseView(View): diff --git a/src/ahriman/web/views/package.py b/src/ahriman/web/views/package.py index d578d93d..f153a8c4 100644 --- a/src/ahriman/web/views/package.py +++ b/src/ahriman/web/views/package.py @@ -19,6 +19,7 @@ # from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response, json_response +from ahriman.core.exceptions import UnknownPackage from ahriman.models.build_status import BuildStatusEnum from ahriman.models.package import Package from ahriman.web.views.base import BaseView @@ -38,7 +39,7 @@ class PackageView(BaseView): try: package, status = self.service.get(base) - except KeyError: + except UnknownPackage: raise HTTPNotFound() response = [ @@ -83,7 +84,7 @@ class PackageView(BaseView): try: self.service.update(base, status, package) - except KeyError: + except UnknownPackage: raise HTTPBadRequest(text=f"Package {base} is unknown, but no package body set") return HTTPNoContent() diff --git a/src/ahriman/web/web.py b/src/ahriman/web/web.py index 4e5af577..c22ca141 100644 --- a/src/ahriman/web/web.py +++ b/src/ahriman/web/web.py @@ -25,7 +25,7 @@ from aiohttp import web from ahriman.core.configuration import Configuration from ahriman.core.exceptions import InitializeException -from ahriman.core.watcher.watcher import Watcher +from ahriman.core.status.watcher import Watcher from ahriman.web.middlewares.exception_handler import exception_handler from ahriman.web.routes import setup_routes diff --git a/tests/ahriman/core/repository/test_executor.py b/tests/ahriman/core/repository/test_executor.py index a96f67d3..524bd58d 100644 --- a/tests/ahriman/core/repository/test_executor.py +++ b/tests/ahriman/core/repository/test_executor.py @@ -14,14 +14,14 @@ def test_process_build(executor: Executor, package_ahriman: Package, mocker: Moc mocker.patch("ahriman.core.build_tools.task.Task.build", return_value=[Path(package_ahriman.base)]) mocker.patch("ahriman.core.build_tools.task.Task.init") move_mock = mocker.patch("shutil.move") - watcher_client_mock = mocker.patch("ahriman.core.watcher.client.Client.set_building") + status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_building") # must return list of built packages assert executor.process_build([package_ahriman]) == [package_ahriman] # must move files (once) move_mock.assert_called_once() # must update status - watcher_client_mock.assert_called_once() + status_client_mock.assert_called_once() # must clear directory from ahriman.core.repository.cleaner import Cleaner Cleaner.clear_build.assert_called_once() @@ -35,10 +35,10 @@ def test_process_build_failure(executor: Executor, package_ahriman: Package, moc mocker.patch("ahriman.core.build_tools.task.Task.build", return_value=[Path(package_ahriman.base)]) mocker.patch("ahriman.core.build_tools.task.Task.init") mocker.patch("shutil.move", side_effect=Exception()) - watcher_client_mock = mocker.patch("ahriman.core.watcher.client.Client.set_failed") + status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_failed") executor.process_build([package_ahriman]) - watcher_client_mock.assert_called_once() + status_client_mock.assert_called_once() def test_process_remove_base(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None: @@ -47,13 +47,13 @@ def test_process_remove_base(executor: Executor, package_ahriman: Package, mocke """ mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman]) repo_remove_mock = mocker.patch("ahriman.core.alpm.repo.Repo.remove") - watcher_client_mock = mocker.patch("ahriman.core.watcher.client.Client.remove") + status_client_mock = mocker.patch("ahriman.core.status.client.Client.remove") executor.process_remove([package_ahriman.base]) # must remove via alpm wrapper repo_remove_mock.assert_called_once() # must update status - watcher_client_mock.assert_called_once() + status_client_mock.assert_called_once() def test_process_remove_base_multiple(executor: Executor, package_python_schedule: Package, @@ -63,7 +63,7 @@ def test_process_remove_base_multiple(executor: Executor, package_python_schedul """ mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_python_schedule]) repo_remove_mock = mocker.patch("ahriman.core.alpm.repo.Repo.remove") - watcher_client_mock = mocker.patch("ahriman.core.watcher.client.Client.remove") + status_client_mock = mocker.patch("ahriman.core.status.client.Client.remove") executor.process_remove([package_python_schedule.base]) # must remove via alpm wrapper @@ -72,7 +72,7 @@ def test_process_remove_base_multiple(executor: Executor, package_python_schedul for package, props in package_python_schedule.packages.items() ], any_order=True) # must update status - watcher_client_mock.assert_called_once() + status_client_mock.assert_called_once() def test_process_remove_base_single(executor: Executor, package_python_schedule: Package, @@ -82,13 +82,13 @@ def test_process_remove_base_single(executor: Executor, package_python_schedule: """ mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_python_schedule]) repo_remove_mock = mocker.patch("ahriman.core.alpm.repo.Repo.remove") - watcher_client_mock = mocker.patch("ahriman.core.watcher.client.Client.remove") + status_client_mock = mocker.patch("ahriman.core.status.client.Client.remove") executor.process_remove(["python2-schedule"]) # must remove via alpm wrapper repo_remove_mock.assert_called_once() # must not update status - watcher_client_mock.assert_not_called() + status_client_mock.assert_not_called() def test_process_remove_nothing(executor: Executor, package_ahriman: Package, package_python_schedule: Package, @@ -131,7 +131,7 @@ def test_process_update(executor: Executor, package_ahriman: Package, mocker: Mo move_mock = mocker.patch("shutil.move") repo_add_mock = mocker.patch("ahriman.core.alpm.repo.Repo.add") sign_package_mock = mocker.patch("ahriman.core.sign.gpg.GPG.sign_package", side_effect=lambda fn, _: [fn]) - watcher_client_mock = mocker.patch("ahriman.core.watcher.client.Client.set_success") + status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_success") # must return complete assert executor.process_update([Path(package.filename) for package in package_ahriman.packages.values()]) @@ -142,7 +142,7 @@ def test_process_update(executor: Executor, package_ahriman: Package, mocker: Mo # must add package repo_add_mock.assert_called_once() # must update status - watcher_client_mock.assert_called_once() + status_client_mock.assert_called_once() # must clear directory from ahriman.core.repository.cleaner import Cleaner Cleaner.clear_packages.assert_called_once() @@ -156,14 +156,14 @@ def test_process_update_group(executor: Executor, package_python_schedule: Packa mocker.patch("shutil.move") mocker.patch("ahriman.models.package.Package.load", return_value=package_python_schedule) repo_add_mock = mocker.patch("ahriman.core.alpm.repo.Repo.add") - watcher_client_mock = mocker.patch("ahriman.core.watcher.client.Client.set_success") + status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_success") executor.process_update([Path(package.filename) for package in package_python_schedule.packages.values()]) repo_add_mock.assert_has_calls([ mock.call(executor.paths.repository / package.filename) for package in package_python_schedule.packages.values() ], any_order=True) - watcher_client_mock.assert_called_with(package_python_schedule) + status_client_mock.assert_called_with(package_python_schedule) def test_process_update_failed(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None: @@ -172,10 +172,10 @@ def test_process_update_failed(executor: Executor, package_ahriman: Package, moc """ mocker.patch("shutil.move", side_effect=Exception()) mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman) - watcher_client_mock = mocker.patch("ahriman.core.watcher.client.Client.set_failed") + status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_failed") executor.process_update([Path(package.filename) for package in package_ahriman.packages.values()]) - watcher_client_mock.assert_called_once() + status_client_mock.assert_called_once() def test_process_update_failed_on_load(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None: diff --git a/tests/ahriman/core/repository/test_update_handler.py b/tests/ahriman/core/repository/test_update_handler.py index 6b959d86..f896a6a0 100644 --- a/tests/ahriman/core/repository/test_update_handler.py +++ b/tests/ahriman/core/repository/test_update_handler.py @@ -7,28 +7,28 @@ from ahriman.models.package import Package def test_updates_aur(update_handler: UpdateHandler, package_ahriman: Package, mocker: MockerFixture) -> None: """ - must provide updates with status watcher updates + must provide updates with status updates """ mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman]) mocker.patch("ahriman.models.package.Package.is_outdated", return_value=True) mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman) - watcher_client_mock = mocker.patch("ahriman.core.watcher.client.Client.set_pending") + status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_pending") assert update_handler.updates_aur([], False) == [package_ahriman] - watcher_client_mock.assert_called_once() + status_client_mock.assert_called_once() def test_updates_aur_failed(update_handler: UpdateHandler, package_ahriman: Package, mocker: MockerFixture) -> None: """ - must update status watcher via client for failed load + must update status via client for failed load """ mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman]) mocker.patch("ahriman.models.package.Package.load", side_effect=Exception()) - watcher_client_mock = mocker.patch("ahriman.core.watcher.client.Client.set_failed") + status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_failed") update_handler.updates_aur([], False) - watcher_client_mock.assert_called_once() + status_client_mock.assert_called_once() def test_updates_aur_filter(update_handler: UpdateHandler, package_ahriman: Package, package_python_schedule: Package, @@ -92,10 +92,10 @@ def test_updates_manual_status_known(update_handler: UpdateHandler, package_ahri mocker.patch("pathlib.Path.iterdir", return_value=[package_ahriman.base]) mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman]) mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman) - watcher_client_mock = mocker.patch("ahriman.core.watcher.client.Client.set_pending") + status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_pending") update_handler.updates_manual() - watcher_client_mock.assert_called_once() + status_client_mock.assert_called_once() def test_updates_manual_status_unknown(update_handler: UpdateHandler, package_ahriman: Package, @@ -106,10 +106,10 @@ def test_updates_manual_status_unknown(update_handler: UpdateHandler, package_ah mocker.patch("pathlib.Path.iterdir", return_value=[package_ahriman.base]) mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[]) mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman) - watcher_client_mock = mocker.patch("ahriman.core.watcher.client.Client.set_unknown") + status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_unknown") update_handler.updates_manual() - watcher_client_mock.assert_called_once() + status_client_mock.assert_called_once() def test_updates_manual_with_failures(update_handler: UpdateHandler, package_ahriman: Package, diff --git a/tests/ahriman/core/status/conftest.py b/tests/ahriman/core/status/conftest.py new file mode 100644 index 00000000..6a31f6d6 --- /dev/null +++ b/tests/ahriman/core/status/conftest.py @@ -0,0 +1,39 @@ +import pytest + +from pytest_mock import MockerFixture +from typing import Any, Dict + +from ahriman.core.configuration import Configuration +from ahriman.core.status.client import Client +from ahriman.core.status.watcher import Watcher +from ahriman.core.status.web_client import WebClient +from ahriman.models.build_status import BuildStatus, BuildStatusEnum +from ahriman.models.package import Package + + +# helpers +@pytest.helpers.register +def get_package_status(package: Package) -> Dict[str, Any]: + return {"status": BuildStatusEnum.Unknown.value, "package": package.view()} + + +@pytest.helpers.register +def get_package_status_extended(package: Package) -> Dict[str, Any]: + return {"status": BuildStatus().view(), "package": package.view()} + + +# fixtures +@pytest.fixture +def client() -> Client: + return Client() + + +@pytest.fixture +def watcher(configuration: Configuration, mocker: MockerFixture) -> Watcher: + mocker.patch("pathlib.Path.mkdir") + return Watcher("x86_64", configuration) + + +@pytest.fixture +def web_client() -> WebClient: + return WebClient("localhost", 8080) diff --git a/tests/ahriman/core/status/test_client.py b/tests/ahriman/core/status/test_client.py new file mode 100644 index 00000000..c3c7d2e4 --- /dev/null +++ b/tests/ahriman/core/status/test_client.py @@ -0,0 +1,116 @@ +from pytest_mock import MockerFixture + +from ahriman.core.configuration import Configuration +from ahriman.core.status.client import Client +from ahriman.core.status.web_client import WebClient +from ahriman.models.build_status import BuildStatusEnum +from ahriman.models.package import Package + + +def test_add(client: Client, package_ahriman: Package) -> None: + """ + must process package addition without errors + """ + client.add(package_ahriman, BuildStatusEnum.Unknown) + + +def test_get(client: Client, package_ahriman: Package) -> None: + """ + must return empty package list + """ + assert client.get(package_ahriman.base) == [] + assert client.get(None) == [] + + +def test_get_self(client: Client) -> None: + """ + must return unknown status for service + """ + assert client.get_self().status == BuildStatusEnum.Unknown + + +def test_remove(client: Client, package_ahriman: Package) -> None: + """ + must process remove without errors + """ + client.remove(package_ahriman.base) + + +def test_update(client: Client, package_ahriman: Package) -> None: + """ + must update package status without errors + """ + client.update(package_ahriman.base, BuildStatusEnum.Unknown) + + +def test_update_self(client: Client) -> None: + """ + must update self status without errors + """ + client.update_self(BuildStatusEnum.Unknown) + + +def test_set_building(client: Client, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must set building status to the package + """ + update_mock = mocker.patch("ahriman.core.status.client.Client.update") + client.set_building(package_ahriman.base) + + update_mock.assert_called_with(package_ahriman.base, BuildStatusEnum.Building) + + +def test_set_failed(client: Client, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must set failed status to the package + """ + update_mock = mocker.patch("ahriman.core.status.client.Client.update") + client.set_failed(package_ahriman.base) + + update_mock.assert_called_with(package_ahriman.base, BuildStatusEnum.Failed) + + +def test_set_pending(client: Client, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must set building status to the package + """ + update_mock = mocker.patch("ahriman.core.status.client.Client.update") + client.set_pending(package_ahriman.base) + + update_mock.assert_called_with(package_ahriman.base, BuildStatusEnum.Pending) + + +def test_set_success(client: Client, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must set success status to the package + """ + add_mock = mocker.patch("ahriman.core.status.client.Client.add") + client.set_success(package_ahriman) + + add_mock.assert_called_with(package_ahriman, BuildStatusEnum.Success) + + +def test_set_unknown(client: Client, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must add new package with unknown status + """ + add_mock = mocker.patch("ahriman.core.status.client.Client.add") + client.set_unknown(package_ahriman) + + add_mock.assert_called_with(package_ahriman, BuildStatusEnum.Unknown) + + +def test_load_dummy_client(configuration: Configuration) -> None: + """ + must load dummy client if no settings set + """ + assert isinstance(Client.load("x86_64", configuration), Client) + + +def test_load_full_client(configuration: Configuration) -> None: + """ + must load full client if no settings set + """ + configuration.set("web", "host", "localhost") + configuration.set("web", "port", "8080") + assert isinstance(Client.load("x86_64", configuration), WebClient) diff --git a/tests/ahriman/core/status/test_watcher.py b/tests/ahriman/core/status/test_watcher.py new file mode 100644 index 00000000..114c2489 --- /dev/null +++ b/tests/ahriman/core/status/test_watcher.py @@ -0,0 +1,219 @@ +import pytest +import tempfile + +from pathlib import Path +from pytest_mock import MockerFixture +from unittest.mock import PropertyMock + +from ahriman.core.exceptions import UnknownPackage +from ahriman.core.status.watcher import Watcher +from ahriman.models.build_status import BuildStatus, BuildStatusEnum +from ahriman.models.package import Package + + +def test_cache_load(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must load state from cache + """ + response = {"packages": [pytest.helpers.get_package_status_extended(package_ahriman)]} + + mocker.patch("pathlib.Path.is_file", return_value=True) + mocker.patch("pathlib.Path.open") + mocker.patch("json.load", return_value=response) + + watcher.known = {package_ahriman.base: (None, None)} + watcher._cache_load() + + package, status = watcher.known[package_ahriman.base] + assert package == package_ahriman + assert status.status == BuildStatusEnum.Unknown + + +def test_cache_load_json_error(watcher: Watcher, mocker: MockerFixture) -> None: + """ + must not fail on json errors + """ + mocker.patch("pathlib.Path.is_file", return_value=True) + mocker.patch("pathlib.Path.open") + mocker.patch("json.load", side_effect=Exception()) + + watcher._cache_load() + assert not watcher.known + + +def test_cache_load_no_file(watcher: Watcher, mocker: MockerFixture) -> None: + """ + must not fail on missing file + """ + mocker.patch("pathlib.Path.is_file", return_value=False) + + watcher._cache_load() + assert not watcher.known + + +def test_cache_load_unknown(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must not load unknown package + """ + response = {"packages": [pytest.helpers.get_package_status_extended(package_ahriman)]} + + mocker.patch("pathlib.Path.is_file", return_value=True) + mocker.patch("pathlib.Path.open") + mocker.patch("json.load", return_value=response) + + watcher._cache_load() + assert not watcher.known + + +def test_cache_save(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must save state to cache + """ + mocker.patch("pathlib.Path.open") + json_mock = mocker.patch("json.dump") + + watcher.known = {package_ahriman.base: (package_ahriman, BuildStatus())} + watcher._cache_save() + json_mock.assert_called_once() + + +def test_cache_save_failed(watcher: Watcher, mocker: MockerFixture) -> None: + """ + must not fail on dumping packages + """ + mocker.patch("pathlib.Path.open") + mocker.patch("json.dump", side_effect=Exception()) + + watcher._cache_save() + + +def test_cache_save_load(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must save state to cache which can be loaded later + """ + dump_file = Path(tempfile.mktemp()) + mocker.patch("ahriman.core.status.watcher.Watcher.cache_path", + new_callable=PropertyMock, return_value=dump_file) + known_current = {package_ahriman.base: (package_ahriman, BuildStatus())} + + watcher.known = known_current + watcher._cache_save() + + watcher.known = {package_ahriman.base: (None, None)} + watcher._cache_load() + assert watcher.known == known_current + + dump_file.unlink() + + +def test_get(watcher: Watcher, package_ahriman: Package) -> None: + """ + must return package status + """ + watcher.known = {package_ahriman.base: (package_ahriman, BuildStatus())} + package, status = watcher.get(package_ahriman.base) + assert package == package_ahriman + assert status.status == BuildStatusEnum.Unknown + + +def test_get_failed(watcher: Watcher, package_ahriman: Package) -> None: + """ + must fail on unknown package + """ + with pytest.raises(UnknownPackage): + watcher.get(package_ahriman.base) + + +def test_load(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must correctly load packages + """ + mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[package_ahriman]) + cache_mock = mocker.patch("ahriman.core.status.watcher.Watcher._cache_load") + + watcher.load() + cache_mock.assert_called_once() + package, status = watcher.known[package_ahriman.base] + assert package == package_ahriman + assert status.status == BuildStatusEnum.Unknown + + +def test_load_known(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must correctly load packages with known statuses + """ + mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[package_ahriman]) + mocker.patch("ahriman.core.status.watcher.Watcher._cache_load") + watcher.known = {package_ahriman.base: (package_ahriman, BuildStatus(BuildStatusEnum.Success))} + + watcher.load() + _, status = watcher.known[package_ahriman.base] + assert status.status == BuildStatusEnum.Success + + +def test_remove(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must remove package base + """ + cache_mock = mocker.patch("ahriman.core.status.watcher.Watcher._cache_save") + watcher.known = {package_ahriman.base: (package_ahriman, BuildStatus())} + + watcher.remove(package_ahriman.base) + assert not watcher.known + cache_mock.assert_called_once() + + +def test_remove_unknown(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must not fail on unknown base removal + """ + cache_mock = mocker.patch("ahriman.core.status.watcher.Watcher._cache_save") + + watcher.remove(package_ahriman.base) + cache_mock.assert_called_once() + + +def test_update(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must update package status + """ + cache_mock = mocker.patch("ahriman.core.status.watcher.Watcher._cache_save") + + watcher.update(package_ahriman.base, BuildStatusEnum.Unknown, package_ahriman) + cache_mock.assert_called_once() + package, status = watcher.known[package_ahriman.base] + assert package == package_ahriman + assert status.status == BuildStatusEnum.Unknown + + +def test_update_ping(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must update package status only for known package + """ + cache_mock = mocker.patch("ahriman.core.status.watcher.Watcher._cache_save") + watcher.known = {package_ahriman.base: (package_ahriman, BuildStatus())} + + watcher.update(package_ahriman.base, BuildStatusEnum.Success, None) + cache_mock.assert_called_once() + package, status = watcher.known[package_ahriman.base] + assert package == package_ahriman + assert status.status == BuildStatusEnum.Success + + +def test_update_unknown(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must fail on unknown package status update only + """ + cache_mock = mocker.patch("ahriman.core.status.watcher.Watcher._cache_save") + + with pytest.raises(UnknownPackage): + watcher.update(package_ahriman.base, BuildStatusEnum.Unknown, None) + cache_mock.assert_called_once() + + +def test_update_self(watcher: Watcher) -> None: + """ + must update service status + """ + watcher.update_self(BuildStatusEnum.Success) + assert watcher.status.status == BuildStatusEnum.Success diff --git a/tests/ahriman/core/status/test_web_client.py b/tests/ahriman/core/status/test_web_client.py new file mode 100644 index 00000000..8d5cc2e7 --- /dev/null +++ b/tests/ahriman/core/status/test_web_client.py @@ -0,0 +1,163 @@ +import json +import pytest + +from pytest_mock import MockerFixture +from requests import Response + +from ahriman.core.status.web_client import WebClient +from ahriman.models.build_status import BuildStatus, BuildStatusEnum +from ahriman.models.package import Package + + +def test_ahriman_url(web_client: WebClient) -> None: + """ + must generate service status url correctly + """ + assert web_client._ahriman_url().startswith(f"http://{web_client.host}:{web_client.port}") + assert web_client._ahriman_url().endswith("/api/v1/ahriman") + + +def test_package_url(web_client: WebClient, package_ahriman: Package) -> None: + """ + must generate package status correctly + """ + assert web_client._package_url(package_ahriman.base).startswith(f"http://{web_client.host}:{web_client.port}") + assert web_client._package_url(package_ahriman.base).endswith(f"/api/v1/packages/{package_ahriman.base}") + + +def test_add(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must process package addition + """ + requests_mock = mocker.patch("requests.post") + payload = pytest.helpers.get_package_status(package_ahriman) + + web_client.add(package_ahriman, BuildStatusEnum.Unknown) + requests_mock.assert_called_with(pytest.helpers.anyvar(str, True), json=payload) + + +def test_add_failed(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must suppress any exception happened during addition + """ + mocker.patch("requests.post", side_effect=Exception()) + web_client.add(package_ahriman, BuildStatusEnum.Unknown) + + +def test_get_all(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must return all packages status + """ + response = [pytest.helpers.get_package_status_extended(package_ahriman)] + response_obj = Response() + response_obj._content = json.dumps(response).encode("utf8") + response_obj.status_code = 200 + + requests_mock = mocker.patch("requests.get", return_value=response_obj) + + result = web_client.get(None) + requests_mock.assert_called_once() + assert len(result) == len(response) + assert (package_ahriman, BuildStatusEnum.Unknown) in [(package, status.status) for package, status in result] + + +def test_get_failed(web_client: WebClient, mocker: MockerFixture) -> None: + """ + must suppress any exception happened during status getting + """ + mocker.patch("requests.get", side_effect=Exception()) + assert web_client.get(None) == [] + + +def test_get_single(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must return single package status + """ + response = [pytest.helpers.get_package_status_extended(package_ahriman)] + response_obj = Response() + response_obj._content = json.dumps(response).encode("utf8") + response_obj.status_code = 200 + + requests_mock = mocker.patch("requests.get", return_value=response_obj) + + result = web_client.get(package_ahriman.base) + requests_mock.assert_called_once() + assert len(result) == len(response) + assert (package_ahriman, BuildStatusEnum.Unknown) in [(package, status.status) for package, status in result] + + +def test_get_self(web_client: WebClient, mocker: MockerFixture) -> None: + """ + must return service status + """ + response_obj = Response() + response_obj._content = json.dumps(BuildStatus().view()).encode("utf8") + response_obj.status_code = 200 + + requests_mock = mocker.patch("requests.get", return_value=response_obj) + + result = web_client.get_self() + requests_mock.assert_called_once() + assert result.status == BuildStatusEnum.Unknown + + +def test_get_self_failed(web_client: WebClient, mocker: MockerFixture) -> None: + """ + must suppress any exception happened during service status getting + """ + mocker.patch("requests.get", side_effect=Exception()) + assert web_client.get_self().status == BuildStatusEnum.Unknown + + +def test_remove(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must process package removal + """ + requests_mock = mocker.patch("requests.delete") + + web_client.remove(package_ahriman.base) + requests_mock.assert_called_with(pytest.helpers.anyvar(str, True)) + + +def test_remove_failed(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must suppress any exception happened during removal + """ + mocker.patch("requests.delete", side_effect=Exception()) + web_client.remove(package_ahriman.base) + + +def test_update(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must process package update + """ + requests_mock = mocker.patch("requests.post") + + web_client.update(package_ahriman.base, BuildStatusEnum.Unknown) + requests_mock.assert_called_with(pytest.helpers.anyvar(str, True), json={"status": BuildStatusEnum.Unknown.value}) + + +def test_update_failed(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must suppress any exception happened during update + """ + mocker.patch("requests.post", side_effect=Exception()) + web_client.update(package_ahriman.base, BuildStatusEnum.Unknown) + + +def test_update_self(web_client: WebClient, mocker: MockerFixture) -> None: + """ + must process service update + """ + requests_mock = mocker.patch("requests.post") + + web_client.update_self(BuildStatusEnum.Unknown) + requests_mock.assert_called_with(pytest.helpers.anyvar(str, True), json={"status": BuildStatusEnum.Unknown.value}) + + +def test_update_self_failed(web_client: WebClient, mocker: MockerFixture) -> None: + """ + must suppress any exception happened during service update + """ + mocker.patch("requests.post", side_effect=Exception()) + web_client.update_self(BuildStatusEnum.Unknown)