Add ability to trigger updates from the web (#31)

* add external process spawner and update test cases

* pass no_report to handlers

* provide service api endpoints

* do not spawn process for single architecture run

* pass no report to handlers

* make _call method of handlers public and also simplify process spawn

* move update under add

* implement actions from web page

* clear logging & improve l&f
This commit is contained in:
2021-09-10 00:33:35 +03:00
committed by GitHub
parent 18de70154e
commit 98eb93c27a
101 changed files with 1417 additions and 295 deletions

View File

@ -1,5 +1,4 @@
import argparse
import aur
import pytest
from pytest_mock import MockerFixture
@ -8,7 +7,6 @@ from ahriman.application.ahriman import _parser
from ahriman.application.application import Application
from ahriman.application.lock import Lock
from ahriman.core.configuration import Configuration
from ahriman.models.package import Package
@pytest.fixture
@ -20,7 +18,7 @@ def application(configuration: Configuration, mocker: MockerFixture) -> Applicat
:return: application test instance
"""
mocker.patch("pathlib.Path.mkdir")
return Application("x86_64", configuration)
return Application("x86_64", configuration, no_report=True)
@pytest.fixture
@ -32,31 +30,6 @@ def args() -> argparse.Namespace:
return argparse.Namespace(lock=None, force=False, unsafe=False, no_report=True)
@pytest.fixture
def aur_package_ahriman(package_ahriman: Package) -> aur.Package:
"""
fixture for AUR package
:param package_ahriman: package fixture
:return: AUR package test instance
"""
return aur.Package(
num_votes=None,
description=package_ahriman.packages[package_ahriman.base].description,
url_path=package_ahriman.web_url,
last_modified=None,
name=package_ahriman.base,
out_of_date=None,
id=None,
first_submitted=None,
maintainer=None,
version=package_ahriman.version,
license=package_ahriman.packages[package_ahriman.base].licenses,
url=None,
package_base=package_ahriman.base,
package_base_id=None,
category_id=None)
@pytest.fixture
def lock(args: argparse.Namespace, configuration: Configuration) -> Lock:
"""

View File

@ -6,7 +6,7 @@ from pytest_mock import MockerFixture
from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import MissingArchitecture
from ahriman.core.exceptions import MissingArchitecture, MultipleArchitecture
def test_call(args: argparse.Namespace, mocker: MockerFixture) -> None:
@ -20,7 +20,7 @@ def test_call(args: argparse.Namespace, mocker: MockerFixture) -> None:
enter_mock = mocker.patch("ahriman.application.lock.Lock.__enter__")
exit_mock = mocker.patch("ahriman.application.lock.Lock.__exit__")
assert Handler._call(args, "x86_64")
assert Handler.call(args, "x86_64")
enter_mock.assert_called_once()
exit_mock.assert_called_once()
@ -30,7 +30,7 @@ def test_call_exception(args: argparse.Namespace, mocker: MockerFixture) -> None
must process exception
"""
mocker.patch("ahriman.application.lock.Lock.__enter__", side_effect=Exception())
assert not Handler._call(args, "x86_64")
assert not Handler.call(args, "x86_64")
def test_execute(args: argparse.Namespace, mocker: MockerFixture) -> None:
@ -44,6 +44,29 @@ def test_execute(args: argparse.Namespace, mocker: MockerFixture) -> None:
starmap_mock.assert_called_once()
def test_execute_multiple_not_supported(args: argparse.Namespace, mocker: MockerFixture) -> None:
"""
must raise an exception if multiple architectures are not supported by the handler
"""
args.architecture = ["i686", "x86_64"]
args.command = "web"
mocker.patch.object(Handler, "ALLOW_MULTI_ARCHITECTURE_RUN", False)
with pytest.raises(MultipleArchitecture):
Handler.execute(args)
def test_execute_single(args: argparse.Namespace, mocker: MockerFixture) -> None:
"""
must run execution in current process if only one architecture supplied
"""
args.architecture = ["x86_64"]
starmap_mock = mocker.patch("multiprocessing.pool.Pool.starmap")
Handler.execute(args)
starmap_mock.assert_not_called()
def test_extract_architectures(args: argparse.Namespace, mocker: MockerFixture) -> None:
"""
must generate list of available architectures
@ -94,4 +117,4 @@ def test_run(args: argparse.Namespace, configuration: Configuration) -> None:
must raise NotImplemented for missing method
"""
with pytest.raises(NotImplementedError):
Handler.run(args, "x86_64", configuration)
Handler.run(args, "x86_64", configuration, True)

View File

@ -26,7 +26,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
mocker.patch("pathlib.Path.mkdir")
application_mock = mocker.patch("ahriman.application.application.Application.add")
Add.run(args, "x86_64", configuration)
Add.run(args, "x86_64", configuration, True)
application_mock.assert_called_once()
@ -41,6 +41,6 @@ def test_run_with_updates(args: argparse.Namespace, configuration: Configuration
application_mock = mocker.patch("ahriman.application.application.Application.update")
updates_mock = mocker.patch("ahriman.application.application.Application.get_updates")
Add.run(args, "x86_64", configuration)
Add.run(args, "x86_64", configuration, True)
application_mock.assert_called_once()
updates_mock.assert_called_once()

View File

@ -28,5 +28,5 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
mocker.patch("pathlib.Path.mkdir")
application_mock = mocker.patch("ahriman.application.application.Application.clean")
Clean.run(args, "x86_64", configuration)
Clean.run(args, "x86_64", configuration, True)
application_mock.assert_called_once()

View File

@ -19,7 +19,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
"""
args.username = "user"
args.password = "pa55w0rd"
args.role = UserAccess.Status
args.role = UserAccess.Read
args.as_service = False
return args
@ -34,7 +34,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
create_user = mocker.patch("ahriman.application.handlers.CreateUser.create_user")
get_salt_mock = mocker.patch("ahriman.application.handlers.CreateUser.get_salt")
CreateUser.run(args, "x86_64", configuration)
CreateUser.run(args, "x86_64", configuration, True)
get_auth_configuration_mock.assert_called_once()
create_configuration_mock.assert_called_once()
create_user.assert_called_once()

View File

@ -15,6 +15,6 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
application_mock = mocker.patch("ahriman.core.configuration.Configuration.dump",
return_value=configuration.dump())
Dump.run(args, "x86_64", configuration)
Dump.run(args, "x86_64", configuration, True)
application_mock.assert_called_once()
print_mock.assert_called()

View File

@ -13,6 +13,6 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
create_tree_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.create_tree")
init_mock = mocker.patch("ahriman.core.alpm.repo.Repo.init")
Init.run(args, "x86_64", configuration)
Init.run(args, "x86_64", configuration, True)
create_tree_mock.assert_called_once()
init_mock.assert_called_once()

View File

@ -25,5 +25,5 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
mocker.patch("pathlib.Path.mkdir")
application_mock = mocker.patch("ahriman.core.sign.gpg.GPG.import_key")
KeyImport.run(args, "x86_64", configuration)
KeyImport.run(args, "x86_64", configuration, True)
application_mock.assert_called_once()

View File

@ -26,7 +26,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
application_packages_mock = mocker.patch("ahriman.core.repository.repository.Repository.packages")
application_mock = mocker.patch("ahriman.application.application.Application.update")
Rebuild.run(args, "x86_64", configuration)
Rebuild.run(args, "x86_64", configuration, True)
application_packages_mock.assert_called_once()
application_mock.assert_called_once()
@ -44,7 +44,7 @@ def test_run_filter(args: argparse.Namespace, configuration: Configuration,
return_value=[package_ahriman, package_python_schedule])
application_mock = mocker.patch("ahriman.application.application.Application.update")
Rebuild.run(args, "x86_64", configuration)
Rebuild.run(args, "x86_64", configuration, True)
application_mock.assert_called_with([package_ahriman])
@ -60,5 +60,5 @@ def test_run_without_filter(args: argparse.Namespace, configuration: Configurati
return_value=[package_ahriman, package_python_schedule])
application_mock = mocker.patch("ahriman.application.application.Application.update")
Rebuild.run(args, "x86_64", configuration)
Rebuild.run(args, "x86_64", configuration, True)
application_mock.assert_called_with([package_ahriman, package_python_schedule])

View File

@ -24,5 +24,5 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
mocker.patch("pathlib.Path.mkdir")
application_mock = mocker.patch("ahriman.application.application.Application.remove")
Remove.run(args, "x86_64", configuration)
Remove.run(args, "x86_64", configuration, True)
application_mock.assert_called_once()

View File

@ -26,7 +26,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
application_mock = mocker.patch("ahriman.application.application.Application.unknown")
remove_mock = mocker.patch("ahriman.application.application.Application.remove")
RemoveUnknown.run(args, "x86_64", configuration)
RemoveUnknown.run(args, "x86_64", configuration, True)
application_mock.assert_called_once()
remove_mock.assert_called_once()
@ -44,7 +44,7 @@ def test_run_dry_run(args: argparse.Namespace, configuration: Configuration, pac
remove_mock = mocker.patch("ahriman.application.application.Application.remove")
log_fn_mock = mocker.patch("ahriman.application.handlers.remove_unknown.RemoveUnknown.log_fn")
RemoveUnknown.run(args, "x86_64", configuration)
RemoveUnknown.run(args, "x86_64", configuration, True)
application_mock.assert_called_once()
remove_mock.assert_not_called()
log_fn_mock.assert_called_with(package_ahriman)

View File

@ -24,5 +24,5 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
mocker.patch("pathlib.Path.mkdir")
application_mock = mocker.patch("ahriman.application.application.Application.report")
Report.run(args, "x86_64", configuration)
Report.run(args, "x86_64", configuration, True)
application_mock.assert_called_once()

View File

@ -26,7 +26,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, aur_package
mocker.patch("aur.search", return_value=[aur_package_ahriman])
log_mock = mocker.patch("ahriman.application.handlers.search.Search.log_fn")
Search.run(args, "x86_64", configuration)
Search.run(args, "x86_64", configuration, True)
log_mock.assert_called_once()
@ -38,7 +38,7 @@ def test_run_multiple_search(args: argparse.Namespace, configuration: Configurat
args.search = ["ahriman", "is", "cool"]
search_mock = mocker.patch("aur.search")
Search.run(args, "x86_64", configuration)
Search.run(args, "x86_64", configuration, True)
search_mock.assert_called_with(" ".join(args.search))
@ -51,5 +51,5 @@ def test_log_fn(args: argparse.Namespace, configuration: Configuration, aur_pack
mocker.patch("aur.search", return_value=[aur_package_ahriman])
print_mock = mocker.patch("builtins.print")
Search.run(args, "x86_64", configuration)
Search.run(args, "x86_64", configuration, True)
print_mock.assert_called() # we don't really care about call details tbh

View File

@ -39,7 +39,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
sudo_configuration_mock = mocker.patch("ahriman.application.handlers.setup.Setup.create_sudo_configuration")
executable_mock = mocker.patch("ahriman.application.handlers.setup.Setup.create_executable")
Setup.run(args, "x86_64", configuration)
Setup.run(args, "x86_64", configuration, True)
ahriman_configuration_mock.assert_called_once()
devtools_configuration_mock.assert_called_once()
makepkg_configuration_mock.assert_called_once()

View File

@ -24,5 +24,5 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
mocker.patch("pathlib.Path.mkdir")
application_mock = mocker.patch("ahriman.application.application.Application.sign")
Sign.run(args, "x86_64", configuration)
Sign.run(args, "x86_64", configuration, True)
application_mock.assert_called_once()

View File

@ -30,7 +30,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, package_ahr
packages_mock = mocker.patch("ahriman.core.status.client.Client.get",
return_value=[(package_ahriman, BuildStatus())])
Status.run(args, "x86_64", configuration)
Status.run(args, "x86_64", configuration, True)
application_mock.assert_called_once()
packages_mock.assert_called_once()
@ -46,5 +46,17 @@ def test_run_with_package_filter(args: argparse.Namespace, configuration: Config
packages_mock = mocker.patch("ahriman.core.status.client.Client.get",
return_value=[(package_ahriman, BuildStatus())])
Status.run(args, "x86_64", configuration)
Status.run(args, "x86_64", configuration, True)
packages_mock.assert_called_once()
def test_imply_with_report(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must create application object with native reporting
"""
args = _default_args(args)
mocker.patch("pathlib.Path.mkdir")
load_mock = mocker.patch("ahriman.core.status.client.Client.load")
Status.run(args, "x86_64", configuration, True)
load_mock.assert_called_once()

View File

@ -28,7 +28,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
mocker.patch("pathlib.Path.mkdir")
update_self_mock = mocker.patch("ahriman.core.status.client.Client.update_self")
StatusUpdate.run(args, "x86_64", configuration)
StatusUpdate.run(args, "x86_64", configuration, True)
update_self_mock.assert_called_once()
@ -42,7 +42,7 @@ def test_run_packages(args: argparse.Namespace, configuration: Configuration, pa
mocker.patch("pathlib.Path.mkdir")
update_mock = mocker.patch("ahriman.core.status.client.Client.update")
StatusUpdate.run(args, "x86_64", configuration)
StatusUpdate.run(args, "x86_64", configuration, True)
update_mock.assert_called_once()
@ -57,5 +57,17 @@ def test_run_remove(args: argparse.Namespace, configuration: Configuration, pack
mocker.patch("pathlib.Path.mkdir")
update_mock = mocker.patch("ahriman.core.status.client.Client.remove")
StatusUpdate.run(args, "x86_64", configuration)
StatusUpdate.run(args, "x86_64", configuration, True)
update_mock.assert_called_once()
def test_imply_with_report(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must create application object with native reporting
"""
args = _default_args(args)
mocker.patch("pathlib.Path.mkdir")
load_mock = mocker.patch("ahriman.core.status.client.Client.load")
StatusUpdate.run(args, "x86_64", configuration, True)
load_mock.assert_called_once()

View File

@ -24,5 +24,5 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
mocker.patch("pathlib.Path.mkdir")
application_mock = mocker.patch("ahriman.application.application.Application.sync")
Sync.run(args, "x86_64", configuration)
Sync.run(args, "x86_64", configuration, True)
application_mock.assert_called_once()

View File

@ -30,7 +30,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
application_mock = mocker.patch("ahriman.application.application.Application.update")
updates_mock = mocker.patch("ahriman.application.application.Application.get_updates")
Update.run(args, "x86_64", configuration)
Update.run(args, "x86_64", configuration, True)
application_mock.assert_called_once()
updates_mock.assert_called_once()
@ -44,7 +44,7 @@ def test_run_dry_run(args: argparse.Namespace, configuration: Configuration, moc
mocker.patch("pathlib.Path.mkdir")
updates_mock = mocker.patch("ahriman.application.application.Application.get_updates")
Update.run(args, "x86_64", configuration)
Update.run(args, "x86_64", configuration, True)
updates_mock.assert_called_once()

View File

@ -6,14 +6,33 @@ from ahriman.application.handlers import Web
from ahriman.core.configuration import Configuration
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
"""
default arguments for these test cases
:param args: command line arguments fixture
:return: generated arguments for these test cases
"""
args.parser = lambda: True
return args
def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run command
"""
args = _default_args(args)
mocker.patch("pathlib.Path.mkdir")
mocker.patch("ahriman.core.spawn.Spawn.start")
setup_mock = mocker.patch("ahriman.web.web.setup_service")
run_mock = mocker.patch("ahriman.web.web.run_server")
Web.run(args, "x86_64", configuration)
Web.run(args, "x86_64", configuration, True)
setup_mock.assert_called_once()
run_mock.assert_called_once()
def test_disallow_multi_architecture_run() -> None:
"""
must not allow multi architecture run
"""
assert not Web.ALLOW_MULTI_ARCHITECTURE_RUN

View File

@ -260,11 +260,12 @@ def test_subparsers_update(parser: argparse.ArgumentParser) -> None:
def test_subparsers_web(parser: argparse.ArgumentParser) -> None:
"""
web command must imply lock and no_report
web command must imply lock, no_report and parser
"""
args = parser.parse_args(["-a", "x86_64", "web"])
assert args.lock is None
assert args.no_report
assert args.parser is not None and args.parser()
def test_run(args: argparse.Namespace, mocker: MockerFixture) -> None:

View File

@ -1,11 +1,14 @@
import aur
import pytest
from pathlib import Path
from pytest_mock import MockerFixture
from typing import Any, Type, TypeVar
from unittest.mock import MagicMock
from ahriman.core.auth.auth import Auth
from ahriman.core.configuration import Configuration
from ahriman.core.spawn import Spawn
from ahriman.core.status.watcher import Watcher
from ahriman.models.package import Package
from ahriman.models.package_description import PackageDescription
@ -13,6 +16,7 @@ from ahriman.models.repository_paths import RepositoryPaths
from ahriman.models.user import User
from ahriman.models.user_access import UserAccess
T = TypeVar("T")
@ -43,10 +47,36 @@ def anyvar(cls: Type[T], strict: bool = False) -> T:
# generic fixtures
@pytest.fixture
def aur_package_ahriman(package_ahriman: Package) -> aur.Package:
"""
fixture for AUR package
:param package_ahriman: package fixture
:return: AUR package test instance
"""
return aur.Package(
num_votes=None,
description=package_ahriman.packages[package_ahriman.base].description,
url_path=package_ahriman.web_url,
last_modified=None,
name=package_ahriman.base,
out_of_date=None,
id=None,
first_submitted=None,
maintainer=None,
version=package_ahriman.version,
license=package_ahriman.packages[package_ahriman.base].licenses,
url=None,
package_base=package_ahriman.base,
package_base_id=None,
category_id=None)
@pytest.fixture
def auth(configuration: Configuration) -> Auth:
"""
auth provider fixture
:param configuration: configuration fixture
:return: auth service instance
"""
return Auth(configuration)
@ -160,6 +190,7 @@ def package_description_python2_schedule() -> PackageDescription:
def repository_paths(configuration: Configuration) -> RepositoryPaths:
"""
repository paths fixture
:param configuration: configuration fixture
:return: repository paths test instance
"""
return RepositoryPaths(
@ -167,13 +198,23 @@ def repository_paths(configuration: Configuration) -> RepositoryPaths:
root=configuration.getpath("repository", "root"))
@pytest.fixture
def spawner(configuration: Configuration) -> Spawn:
"""
spawner fixture
:param configuration: configuration fixture
:return: spawner fixture
"""
return Spawn(MagicMock(), "x86_64", configuration)
@pytest.fixture
def user() -> User:
"""
fixture for user descriptor
:return: user descriptor instance
"""
return User("user", "pa55w0rd", UserAccess.Status)
return User("user", "pa55w0rd", UserAccess.Read)
@pytest.fixture

View File

@ -46,8 +46,8 @@ def test_is_safe_request(auth: Auth) -> None:
must validate safe request
"""
# login and logout are always safe
assert auth.is_safe_request("/login", UserAccess.Write)
assert auth.is_safe_request("/logout", UserAccess.Write)
assert auth.is_safe_request("/user-api/v1/login", UserAccess.Write)
assert auth.is_safe_request("/user-api/v1/logout", UserAccess.Write)
auth.allowed_paths.add("/safe")
auth.allowed_paths_groups.add("/unsafe/safe")

View File

@ -19,7 +19,7 @@ def cleaner(configuration: Configuration, mocker: MockerFixture) -> Cleaner:
:return: cleaner test instance
"""
mocker.patch("pathlib.Path.mkdir")
return Cleaner("x86_64", configuration)
return Cleaner("x86_64", configuration, no_report=True)
@pytest.fixture
@ -36,7 +36,7 @@ def executor(configuration: Configuration, mocker: MockerFixture) -> Executor:
mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_chroot")
mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_manual")
mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_packages")
return Executor("x86_64", configuration)
return Executor("x86_64", configuration, no_report=True)
@pytest.fixture
@ -48,7 +48,7 @@ def repository(configuration: Configuration, mocker: MockerFixture) -> Repositor
:return: repository test instance
"""
mocker.patch("pathlib.Path.mkdir")
return Repository("x86_64", configuration)
return Repository("x86_64", configuration, no_report=True)
@pytest.fixture
@ -58,7 +58,7 @@ def properties(configuration: Configuration) -> Properties:
:param configuration: configuration fixture
:return: properties test instance
"""
return Properties("x86_64", configuration)
return Properties("x86_64", configuration, no_report=True)
@pytest.fixture
@ -75,4 +75,4 @@ def update_handler(configuration: Configuration, mocker: MockerFixture) -> Updat
mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_chroot")
mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_manual")
mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_packages")
return UpdateHandler("x86_64", configuration)
return UpdateHandler("x86_64", configuration, no_report=True)

View File

@ -2,6 +2,7 @@ from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration
from ahriman.core.repository.properties import Properties
from ahriman.core.status.web_client import WebClient
def test_create_tree_on_load(configuration: Configuration, mocker: MockerFixture) -> None:
@ -9,6 +10,29 @@ def test_create_tree_on_load(configuration: Configuration, mocker: MockerFixture
must create tree on load
"""
create_tree_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.create_tree")
Properties("x86_64", configuration)
Properties("x86_64", configuration, True)
create_tree_mock.assert_called_once()
def test_create_dummy_report_client(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must create dummy report client if report is disabled
"""
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.create_tree")
load_mock = mocker.patch("ahriman.core.status.client.Client.load")
properties = Properties("x86_64", configuration, True)
load_mock.assert_not_called()
assert not isinstance(properties.reporter, WebClient)
def test_create_full_report_client(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must create load report client if report is enabled
"""
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.create_tree")
load_mock = mocker.patch("ahriman.core.status.client.Client.load")
Properties("x86_64", configuration, False)
load_mock.assert_called_once()

View File

@ -5,12 +5,28 @@ from pathlib import Path
from pytest_mock import MockerFixture
from unittest.mock import PropertyMock
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import UnknownPackage
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
def test_force_no_report(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must force dummy report client
"""
configuration.set_option("web", "port", "8080")
mocker.patch("pathlib.Path.mkdir")
load_mock = mocker.patch("ahriman.core.status.client.Client.load")
watcher = Watcher("x86_64", configuration)
load_mock.assert_not_called()
assert not isinstance(watcher.repository.reporter, WebClient)
def test_cache_load(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must load state from cache

View File

@ -0,0 +1,111 @@
from pytest_mock import MockerFixture
from unittest.mock import MagicMock
from ahriman.core.spawn import Spawn
def test_process(spawner: Spawn) -> None:
"""
must process external process run correctly
"""
args = MagicMock()
callback = MagicMock()
callback.return_value = True
spawner.process(callback, args, spawner.architecture, "id", spawner.queue)
callback.assert_called_with(args, spawner.architecture)
(uuid, status) = spawner.queue.get()
assert uuid == "id"
assert status
assert spawner.queue.empty()
def test_process_error(spawner: Spawn) -> None:
"""
must process external run with error correctly
"""
callback = MagicMock()
callback.return_value = False
spawner.process(callback, MagicMock(), spawner.architecture, "id", spawner.queue)
(uuid, status) = spawner.queue.get()
assert uuid == "id"
assert not status
assert spawner.queue.empty()
def test_packages_add(spawner: Spawn, mocker: MockerFixture) -> None:
"""
must call package addition
"""
spawn_mock = mocker.patch("ahriman.core.spawn.Spawn.spawn_process")
spawner.packages_add(["ahriman", "linux"], now=False)
spawn_mock.assert_called_with("add", "ahriman", "linux")
def test_packages_add_with_build(spawner: Spawn, mocker: MockerFixture) -> None:
"""
must call package addition with update
"""
spawn_mock = mocker.patch("ahriman.core.spawn.Spawn.spawn_process")
spawner.packages_add(["ahriman", "linux"], now=True)
spawn_mock.assert_called_with("add", "ahriman", "linux", now="")
def test_packages_remove(spawner: Spawn, mocker: MockerFixture) -> None:
"""
must call package removal
"""
spawn_mock = mocker.patch("ahriman.core.spawn.Spawn.spawn_process")
spawner.packages_remove(["ahriman", "linux"])
spawn_mock.assert_called_with("remove", "ahriman", "linux")
def test_spawn_process(spawner: Spawn, mocker: MockerFixture) -> None:
"""
must correctly spawn child process
"""
start_mock = mocker.patch("multiprocessing.Process.start")
spawner.spawn_process("add", "ahriman", now="", maybe="?")
start_mock.assert_called_once()
spawner.args_parser.parse_args.assert_called_with([
"--architecture", spawner.architecture, "--configuration", str(spawner.configuration.path),
"add", "ahriman", "--now", "--maybe", "?"
])
def test_run(spawner: Spawn, mocker: MockerFixture) -> None:
"""
must implement run method
"""
logging_mock = mocker.patch("logging.Logger.info")
spawner.queue.put(("1", False))
spawner.queue.put(("2", True))
spawner.queue.put(None) # terminate
spawner.run()
logging_mock.assert_called()
def test_run_pop(spawner: Spawn) -> None:
"""
must pop and terminate child process
"""
first = spawner.active["1"] = MagicMock()
second = spawner.active["2"] = MagicMock()
spawner.queue.put(("1", False))
spawner.queue.put(("2", True))
spawner.queue.put(None) # terminate
spawner.run()
first.terminate.assert_called_once()
first.join.assert_called_once()
second.terminate.assert_called_once()
second.join.assert_called_once()
assert not spawner.active

View File

@ -59,10 +59,14 @@ def test_get_local_files(s3: S3, resource_path_root: Path) -> None:
Path("models/package_ahriman_srcinfo"),
Path("models/package_tpacpi-bat-git_srcinfo"),
Path("models/package_yay_srcinfo"),
Path("web/templates/build-status/login-modal.jinja2"),
Path("web/templates/build-status/package-actions-modals.jinja2"),
Path("web/templates/build-status/package-actions-script.jinja2"),
Path("web/templates/utils/bootstrap-scripts.jinja2"),
Path("web/templates/utils/style.jinja2"),
Path("web/templates/build-status.jinja2"),
Path("web/templates/email-index.jinja2"),
Path("web/templates/repo-index.jinja2"),
Path("web/templates/style.jinja2"),
])
local_files = list(sorted(s3.get_local_files(resource_path_root).keys()))

View File

@ -7,7 +7,7 @@ def test_from_option(user: User) -> None:
must generate user from options
"""
assert User.from_option(user.username, user.password) == user
# default is status access
# default is read access
user.access = UserAccess.Write
assert User.from_option(user.username, user.password) != user
@ -52,17 +52,6 @@ def test_verify_access_read(user: User) -> None:
user.access = UserAccess.Read
assert user.verify_access(UserAccess.Read)
assert not user.verify_access(UserAccess.Write)
assert not user.verify_access(UserAccess.Status)
def test_verify_access_status(user: User) -> None:
"""
user with status access must be able to only request status
"""
user.access = UserAccess.Status
assert not user.verify_access(UserAccess.Read)
assert not user.verify_access(UserAccess.Write)
assert user.verify_access(UserAccess.Status)
def test_verify_access_write(user: User) -> None:
@ -72,4 +61,3 @@ def test_verify_access_write(user: User) -> None:
user.access = UserAccess.Write
assert user.verify_access(UserAccess.Read)
assert user.verify_access(UserAccess.Write)
assert user.verify_access(UserAccess.Status)

View File

@ -1,41 +1,64 @@
import pytest
from aiohttp import web
from collections import namedtuple
from pytest_mock import MockerFixture
from typing import Any
import ahriman.core.auth.helpers
from ahriman.core.configuration import Configuration
from ahriman.core.spawn import Spawn
from ahriman.models.user import User
from ahriman.web.web import setup_service
_request = namedtuple("_request", ["app", "path", "method", "json", "post"])
@pytest.helpers.register
def request(app: web.Application, path: str, method: str, json: Any = None, data: Any = None) -> _request:
"""
request generator helper
:param app: application fixture
:param path: path for the request
:param method: method for the request
:param json: json payload of the request
:param data: form data payload of the request
:return: dummy request object
"""
return _request(app, path, method, json, data)
@pytest.fixture
def application(configuration: Configuration, mocker: MockerFixture) -> web.Application:
def application(configuration: Configuration, spawner: Spawn, mocker: MockerFixture) -> web.Application:
"""
application fixture
:param configuration: configuration fixture
:param spawner: spawner fixture
:param mocker: mocker object
:return: application test instance
"""
mocker.patch.object(ahriman.core.auth.helpers, "_has_aiohttp_security", False)
mocker.patch("pathlib.Path.mkdir")
return setup_service("x86_64", configuration)
return setup_service("x86_64", configuration, spawner)
@pytest.fixture
def application_with_auth(configuration: Configuration, user: User, mocker: MockerFixture) -> web.Application:
def application_with_auth(configuration: Configuration, user: User, spawner: Spawn,
mocker: MockerFixture) -> web.Application:
"""
application fixture with auth enabled
:param configuration: configuration fixture
:param user: user descriptor fixture
:param spawner: spawner fixture
:param mocker: mocker object
:return: application test instance
"""
configuration.set_option("auth", "target", "configuration")
mocker.patch.object(ahriman.core.auth.helpers, "_has_aiohttp_security", True)
mocker.patch("pathlib.Path.mkdir")
application = setup_service("x86_64", configuration)
application = setup_service("x86_64", configuration, spawner)
generated = User(user.username, user.hash_password(application["validator"].salt), user.access)
application["validator"]._users[generated.username] = generated

View File

@ -1,23 +1,10 @@
import pytest
from collections import namedtuple
from ahriman.core.auth.auth import Auth
from ahriman.core.configuration import Configuration
from ahriman.models.user import User
from ahriman.web.middlewares.auth_handler import AuthorizationPolicy
_request = namedtuple("_request", ["path", "method"])
@pytest.fixture
def aiohttp_request() -> _request:
"""
fixture for aiohttp like object
:return: aiohttp like request test instance
"""
return _request("path", "GET")
@pytest.fixture
def authorization_policy(configuration: Configuration, user: User) -> AuthorizationPolicy:

View File

@ -1,6 +1,7 @@
import pytest
from aiohttp import web
from pytest_mock import MockerFixture
from typing import Any
from unittest.mock import AsyncMock, MagicMock
from ahriman.core.auth.auth import Auth
@ -29,40 +30,40 @@ async def test_permits(authorization_policy: AuthorizationPolicy, user: User) ->
authorization_policy.validator.verify_access.assert_called_with(user.username, user.access, "/endpoint")
async def test_auth_handler_api(aiohttp_request: Any, auth: Auth, mocker: MockerFixture) -> None:
async def test_auth_handler_api(auth: Auth, mocker: MockerFixture) -> None:
"""
must ask for status permission for api calls
"""
aiohttp_request = aiohttp_request._replace(path="/status-api")
aiohttp_request = pytest.helpers.request("", "/status-api", "GET")
request_handler = AsyncMock()
mocker.patch("ahriman.core.auth.auth.Auth.is_safe_request", return_value=False)
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
handler = auth_handler(auth)
await handler(aiohttp_request, request_handler)
check_permission_mock.assert_called_with(aiohttp_request, UserAccess.Status, aiohttp_request.path)
check_permission_mock.assert_called_with(aiohttp_request, UserAccess.Read, aiohttp_request.path)
async def test_auth_handler_api_post(aiohttp_request: Any, auth: Auth, mocker: MockerFixture) -> None:
async def test_auth_handler_api_post(auth: Auth, mocker: MockerFixture) -> None:
"""
must ask for status permission for api calls with POST
"""
aiohttp_request = aiohttp_request._replace(path="/status-api", method="POST")
aiohttp_request = pytest.helpers.request("", "/status-api", "POST")
request_handler = AsyncMock()
mocker.patch("ahriman.core.auth.auth.Auth.is_safe_request", return_value=False)
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
handler = auth_handler(auth)
await handler(aiohttp_request, request_handler)
check_permission_mock.assert_called_with(aiohttp_request, UserAccess.Status, aiohttp_request.path)
check_permission_mock.assert_called_with(aiohttp_request, UserAccess.Write, aiohttp_request.path)
async def test_auth_handler_read(aiohttp_request: Any, auth: Auth, mocker: MockerFixture) -> None:
async def test_auth_handler_read(auth: Auth, mocker: MockerFixture) -> None:
"""
must ask for read permission for api calls with GET
"""
for method in ("GET", "HEAD", "OPTIONS"):
aiohttp_request = aiohttp_request._replace(method=method)
aiohttp_request = pytest.helpers.request("", "", method)
request_handler = AsyncMock()
mocker.patch("ahriman.core.auth.auth.Auth.is_safe_request", return_value=False)
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
@ -72,12 +73,12 @@ async def test_auth_handler_read(aiohttp_request: Any, auth: Auth, mocker: Mocke
check_permission_mock.assert_called_with(aiohttp_request, UserAccess.Read, aiohttp_request.path)
async def test_auth_handler_write(aiohttp_request: Any, auth: Auth, mocker: MockerFixture) -> None:
async def test_auth_handler_write(auth: Auth, mocker: MockerFixture) -> None:
"""
must ask for read permission for api calls with POST
"""
for method in ("CONNECT", "DELETE", "PATCH", "POST", "PUT", "TRACE"):
aiohttp_request = aiohttp_request._replace(method=method)
aiohttp_request = pytest.helpers.request("", "", method)
request_handler = AsyncMock()
mocker.patch("ahriman.core.auth.auth.Auth.is_safe_request", return_value=False)
check_permission_mock = mocker.patch("aiohttp_security.check_permission")

View File

@ -3,45 +3,47 @@ import pytest
from aiohttp.web_exceptions import HTTPBadRequest
from pytest_mock import MockerFixture
from typing import Any
from unittest.mock import AsyncMock
from ahriman.web.middlewares.exception_handler import exception_handler
async def test_exception_handler(aiohttp_request: Any, mocker: MockerFixture) -> None:
async def test_exception_handler(mocker: MockerFixture) -> None:
"""
must pass success response
"""
request = pytest.helpers.request("", "", "")
request_handler = AsyncMock()
logging_mock = mocker.patch("logging.Logger.exception")
handler = exception_handler(logging.getLogger())
await handler(aiohttp_request, request_handler)
await handler(request, request_handler)
logging_mock.assert_not_called()
async def test_exception_handler_client_error(aiohttp_request: Any, mocker: MockerFixture) -> None:
async def test_exception_handler_client_error(mocker: MockerFixture) -> None:
"""
must pass client exception
"""
request = pytest.helpers.request("", "", "")
request_handler = AsyncMock(side_effect=HTTPBadRequest())
logging_mock = mocker.patch("logging.Logger.exception")
handler = exception_handler(logging.getLogger())
with pytest.raises(HTTPBadRequest):
await handler(aiohttp_request, request_handler)
await handler(request, request_handler)
logging_mock.assert_not_called()
async def test_exception_handler_server_error(aiohttp_request: Any, mocker: MockerFixture) -> None:
async def test_exception_handler_server_error(mocker: MockerFixture) -> None:
"""
must log server exception and re-raise it
"""
request = pytest.helpers.request("", "", "")
request_handler = AsyncMock(side_effect=Exception())
logging_mock = mocker.patch("logging.Logger.exception")
handler = exception_handler(logging.getLogger())
with pytest.raises(Exception):
await handler(aiohttp_request, request_handler)
await handler(request, request_handler)
logging_mock.assert_called_once()

View File

@ -6,6 +6,18 @@ from pytest_aiohttp import TestClient
from pytest_mock import MockerFixture
from typing import Any
from ahriman.web.views.base import BaseView
@pytest.fixture
def base(application: web.Application) -> BaseView:
"""
base view fixture
:param application: application fixture
:return: generated base view fixture
"""
return BaseView(pytest.helpers.request(application, "", ""))
@pytest.fixture
def client(application: web.Application, loop: BaseEventLoop,

View File

@ -0,0 +1,46 @@
from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
async def test_post(client: TestClient, mocker: MockerFixture) -> None:
"""
must call post request correctly
"""
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add")
response = await client.post("/service-api/v1/add", json={"packages": ["ahriman"]})
assert response.status == 200
add_mock.assert_called_with(["ahriman"], True)
async def test_post_now(client: TestClient, mocker: MockerFixture) -> None:
"""
must call post and run build
"""
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add")
response = await client.post("/service-api/v1/add", json={"packages": ["ahriman"], "build_now": False})
assert response.status == 200
add_mock.assert_called_with(["ahriman"], False)
async def test_post_exception(client: TestClient, mocker: MockerFixture) -> None:
"""
must raise exception on missing packages payload
"""
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add")
response = await client.post("/service-api/v1/add")
assert response.status == 400
add_mock.assert_not_called()
async def test_post_update(client: TestClient, mocker: MockerFixture) -> None:
"""
must call post request correctly for alias
"""
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add")
response = await client.post("/service-api/v1/update", json={"packages": ["ahriman"]})
assert response.status == 200
add_mock.assert_called_with(["ahriman"], True)

View File

@ -0,0 +1,24 @@
from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
async def test_post(client: TestClient, mocker: MockerFixture) -> None:
"""
must call post request correctly
"""
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_remove")
response = await client.post("/service-api/v1/remove", json={"packages": ["ahriman"]})
assert response.status == 200
add_mock.assert_called_with(["ahriman"])
async def test_post_exception(client: TestClient, mocker: MockerFixture) -> None:
"""
must raise exception on missing packages payload
"""
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_remove")
response = await client.post("/service-api/v1/remove")
assert response.status == 400
add_mock.assert_not_called()

View File

@ -0,0 +1,59 @@
import aur
from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
async def test_get(client: TestClient, aur_package_ahriman: aur.Package, mocker: MockerFixture) -> None:
"""
must call get request correctly
"""
mocker.patch("aur.search", return_value=[aur_package_ahriman])
response = await client.get("/service-api/v1/search", params={"for": "ahriman"})
assert response.status == 200
assert await response.json() == ["ahriman"]
async def test_get_exception(client: TestClient, mocker: MockerFixture) -> None:
"""
must raise 400 on empty search string
"""
search_mock = mocker.patch("aur.search")
response = await client.get("/service-api/v1/search")
assert response.status == 400
search_mock.assert_not_called()
async def test_get_join(client: TestClient, mocker: MockerFixture) -> None:
"""
must join search args with space
"""
search_mock = mocker.patch("aur.search")
response = await client.get("/service-api/v1/search", params=[("for", "ahriman"), ("for", "maybe")])
assert response.status == 200
search_mock.assert_called_with("ahriman maybe")
async def test_get_join_filter(client: TestClient, mocker: MockerFixture) -> None:
"""
must filter search parameters with less than 3 symbols
"""
search_mock = mocker.patch("aur.search")
response = await client.get("/service-api/v1/search", params=[("for", "ah"), ("for", "maybe")])
assert response.status == 200
search_mock.assert_called_with("maybe")
async def test_get_join_filter_empty(client: TestClient, mocker: MockerFixture) -> None:
"""
must filter search parameters with less than 3 symbols (empty result)
"""
search_mock = mocker.patch("aur.search")
response = await client.get("/service-api/v1/search", params=[("for", "ah"), ("for", "ma")])
assert response.status == 400
search_mock.assert_not_called()

View File

@ -0,0 +1,88 @@
import pytest
from multidict import MultiDict
from ahriman.web.views.base import BaseView
def test_service(base: BaseView) -> None:
"""
must return service
"""
assert base.service
def test_spawn(base: BaseView) -> None:
"""
must return spawn thread
"""
assert base.spawner
def test_validator(base: BaseView) -> None:
"""
must return service
"""
assert base.validator
async def test_extract_data_json(base: BaseView) -> None:
"""
must parse and return json
"""
json = {"key1": "value1", "key2": "value2"}
async def get_json():
return json
base._request = pytest.helpers.request(base.request.app, "", "", json=get_json)
assert await base.extract_data() == json
async def test_extract_data_post(base: BaseView) -> None:
"""
must parse and return form data
"""
json = {"key1": "value1", "key2": "value2"}
async def get_json():
raise ValueError()
async def get_data():
return json
base._request = pytest.helpers.request(base.request.app, "", "", json=get_json, data=get_data)
assert await base.extract_data() == json
async def test_data_as_json(base: BaseView) -> None:
"""
must parse multi value form payload
"""
json = {"key1": "value1", "key2": ["value2", "value3"], "key3": ["value4", "value5", "value6"]}
async def get_data():
result = MultiDict()
for key, values in json.items():
if isinstance(values, list):
for value in values:
result.add(key, value)
else:
result.add(key, values)
return result
base._request = pytest.helpers.request(base.request.app, "", "", data=get_data)
assert await base.data_as_json([]) == json
async def test_data_as_json_with_list_keys(base: BaseView) -> None:
"""
must parse multi value form payload with forced list
"""
json = {"key1": "value1"}
async def get_data():
return json
base._request = pytest.helpers.request(base.request.app, "", "", data=get_data)
assert await base.data_as_json(["key1"]) == {"key1": ["value1"]}

View File

@ -11,10 +11,10 @@ async def test_post(client_with_auth: TestClient, user: User, mocker: MockerFixt
payload = {"username": user.username, "password": user.password}
remember_mock = mocker.patch("aiohttp_security.remember")
post_response = await client_with_auth.post("/login", json=payload)
post_response = await client_with_auth.post("/user-api/v1/login", json=payload)
assert post_response.status == 200
post_response = await client_with_auth.post("/login", data=payload)
post_response = await client_with_auth.post("/user-api/v1/login", data=payload)
assert post_response.status == 200
remember_mock.assert_called()
@ -25,7 +25,7 @@ async def test_post_skip(client: TestClient, user: User) -> None:
must process if no auth configured
"""
payload = {"username": user.username, "password": user.password}
post_response = await client.post("/login", json=payload)
post_response = await client.post("/user-api/v1/login", json=payload)
assert post_response.status == 200
@ -36,6 +36,6 @@ async def test_post_unauthorized(client_with_auth: TestClient, user: User, mocke
payload = {"username": user.username, "password": ""}
remember_mock = mocker.patch("aiohttp_security.remember")
post_response = await client_with_auth.post("/login", json=payload)
post_response = await client_with_auth.post("/user-api/v1/login", json=payload)
assert post_response.status == 401
remember_mock.assert_not_called()

View File

@ -10,7 +10,7 @@ async def test_post(client_with_auth: TestClient, mocker: MockerFixture) -> None
mocker.patch("aiohttp_security.check_authorized")
forget_mock = mocker.patch("aiohttp_security.forget")
post_response = await client_with_auth.post("/logout")
post_response = await client_with_auth.post("/user-api/v1/logout")
assert post_response.status == 200
forget_mock.assert_called_once()
@ -22,7 +22,7 @@ async def test_post_unauthorized(client_with_auth: TestClient, mocker: MockerFix
mocker.patch("aiohttp_security.check_authorized", side_effect=HTTPUnauthorized())
forget_mock = mocker.patch("aiohttp_security.forget")
post_response = await client_with_auth.post("/logout")
post_response = await client_with_auth.post("/user-api/v1/logout")
assert post_response.status == 401
forget_mock.assert_not_called()
@ -31,5 +31,5 @@ async def test_post_disabled(client: TestClient) -> None:
"""
must raise exception if auth is disabled
"""
post_response = await client.post("/logout")
post_response = await client.post("/user-api/v1/logout")
assert post_response.status == 200