diff --git a/package/etc/ahriman.ini.d/logging.ini b/package/etc/ahriman.ini.d/logging.ini
index e2e9a495..b4c9fa13 100644
--- a/package/etc/ahriman.ini.d/logging.ini
+++ b/package/etc/ahriman.ini.d/logging.ini
@@ -2,7 +2,7 @@
keys = root,builder,build_details,http
[handlers]
-keys = console_handler,build_file_handler,file_handler,http_handler,syslog_handler
+keys = console_handler,syslog_handler
[formatters]
keys = generic_format,syslog_format
@@ -13,24 +13,6 @@ level = DEBUG
formatter = generic_format
args = (sys.stderr,)
-[handler_file_handler]
-class = logging.handlers.RotatingFileHandler
-level = DEBUG
-formatter = generic_format
-args = ("/var/log/ahriman/ahriman.log", "a", 20971520, 20)
-
-[handler_build_file_handler]
-class = logging.handlers.RotatingFileHandler
-level = DEBUG
-formatter = generic_format
-args = ("/var/log/ahriman/build.log", "a", 20971520, 20)
-
-[handler_http_handler]
-class = logging.handlers.RotatingFileHandler
-level = DEBUG
-formatter = generic_format
-args = ("/var/log/ahriman/http.log", "a", 20971520, 20)
-
[handler_syslog_handler]
class = logging.handlers.SysLogHandler
level = DEBUG
diff --git a/package/share/ahriman/build-status.jinja2 b/package/share/ahriman/build-status.jinja2
index e9db6106..35520e01 100644
--- a/package/share/ahriman/build-status.jinja2
+++ b/package/share/ahriman/build-status.jinja2
@@ -5,11 +5,7 @@
-
-
-
-
- {% include "style.jinja2" %}
+ {% include "utils/style.jinja2" %}
@@ -26,20 +22,46 @@
-
+ {% if not auth_enabled or auth_username is not none %}
+
+ Add
+
+
+ Update
+
+
+ Remove
+
+ {% endif %}
+
+
+
+ data-show-fullscreen="true"
+ data-show-search-clear-button="true"
+ data-sortable="true"
+ data-sort-reset="true"
+ data-toggle="table"
+ data-toolbar="#toolbar">
- package base
- packages
+
+ package base
version
+ packages
+ groups
+ licenses
last update
status
@@ -48,10 +70,13 @@
{% if authorized %}
{% for package in packages %}
-
+
+
{{ package.base }}
- {{ package.packages|join(" "|safe) }}
{{ package.version }}
+ {{ package.packages|join(" "|safe) }}
+ {{ package.groups|join(" "|safe) }}
+ {{ package.licenses|join(" "|safe) }}
{{ package.timestamp }}
{{ package.status }}
@@ -77,45 +102,23 @@
{% if auth_username is none %}
login
{% else %}
-
{% endif %}
{% endif %}
-
+ {% if auth_enabled %}
+ {% include "build-status/login-modal.jinja2" %}
+ {% endif %}
-
-
-
-
-
+ {% include "build-status/package-actions-modals.jinja2" %}
+
+ {% include "utils/bootstrap-scripts.jinja2" %}
+
+ {% include "build-status/package-actions-script.jinja2" %}
diff --git a/package/share/ahriman/build-status/login-modal.jinja2 b/package/share/ahriman/build-status/login-modal.jinja2
new file mode 100644
index 00000000..26f25f8e
--- /dev/null
+++ b/package/share/ahriman/build-status/login-modal.jinja2
@@ -0,0 +1,29 @@
+
\ No newline at end of file
diff --git a/package/share/ahriman/build-status/package-actions-modals.jinja2 b/package/share/ahriman/build-status/package-actions-modals.jinja2
new file mode 100644
index 00000000..7faa73f1
--- /dev/null
+++ b/package/share/ahriman/build-status/package-actions-modals.jinja2
@@ -0,0 +1,57 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/package/share/ahriman/build-status/package-actions-script.jinja2 b/package/share/ahriman/build-status/package-actions-script.jinja2
new file mode 100644
index 00000000..3b77b5e1
--- /dev/null
+++ b/package/share/ahriman/build-status/package-actions-script.jinja2
@@ -0,0 +1,76 @@
+
\ No newline at end of file
diff --git a/package/share/ahriman/email-index.jinja2 b/package/share/ahriman/email-index.jinja2
index 87d3fd3e..1fc99d32 100644
--- a/package/share/ahriman/email-index.jinja2
+++ b/package/share/ahriman/email-index.jinja2
@@ -6,15 +6,13 @@
-
-
- {% include "style.jinja2" %}
+ {% include "utils/style.jinja2" %}
-
+
package
diff --git a/package/share/ahriman/repo-index.jinja2 b/package/share/ahriman/repo-index.jinja2
index e3b1ea13..c1a85691 100644
--- a/package/share/ahriman/repo-index.jinja2
+++ b/package/share/ahriman/repo-index.jinja2
@@ -5,11 +5,7 @@
-
-
-
-
- {% include "style.jinja2" %}
+ {% include "utils/style.jinja2" %}
@@ -30,19 +26,32 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
-
+ data-show-fullscreen="true"
+ data-show-search-clear-button="true"
+ data-sortable="true"
+ data-sort-reset="true"
+ data-toggle="table">
- package
+ package
version
+ architecture
+ description
+ upstream url
+ licenses
+ groups
+ depends
archive size
installed size
build date
@@ -54,6 +63,12 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
{{ package.name }}
{{ package.version }}
+ {{ package.architecture }}
+ {{ package.description }}
+ {{ package.url }}
+ {{ package.licenses|join(" "|safe) }}
+ {{ package.groups|join(" "|safe) }}
+ {{ package.depends|join(" "|safe) }}
{{ package.archive_size }}
{{ package.installed_size }}
{{ package.build_date }}
@@ -73,11 +88,7 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
-
-
-
-
-
+ {% include "utils/bootstrap-scripts.jinja2" %}
diff --git a/package/share/ahriman/style.jinja2 b/package/share/ahriman/style.jinja2
deleted file mode 100644
index 6f29e9e9..00000000
--- a/package/share/ahriman/style.jinja2
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/package/share/ahriman/utils/bootstrap-scripts.jinja2 b/package/share/ahriman/utils/bootstrap-scripts.jinja2
new file mode 100644
index 00000000..59991111
--- /dev/null
+++ b/package/share/ahriman/utils/bootstrap-scripts.jinja2
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/package/share/ahriman/utils/style.jinja2 b/package/share/ahriman/utils/style.jinja2
new file mode 100644
index 00000000..b2ccd979
--- /dev/null
+++ b/package/share/ahriman/utils/style.jinja2
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
diff --git a/setup.py b/setup.py
index 430ea257..a1ec1fef 100644
--- a/setup.py
+++ b/setup.py
@@ -68,7 +68,15 @@ setup(
"package/share/ahriman/build-status.jinja2",
"package/share/ahriman/email-index.jinja2",
"package/share/ahriman/repo-index.jinja2",
- "package/share/ahriman/style.jinja2",
+ ]),
+ ("share/ahriman/build-status", [
+ "package/share/ahriman/build-status/login-modal.jinja2",
+ "package/share/ahriman/build-status/package-actions-modals.jinja2",
+ "package/share/ahriman/build-status/package-actions-script.jinja2",
+ ]),
+ ("share/ahriman/utils", [
+ "package/share/ahriman/utils/bootstrap-scripts.jinja2",
+ "package/share/ahriman/utils/style.jinja2",
]),
],
diff --git a/src/ahriman/application/ahriman.py b/src/ahriman/application/ahriman.py
index 75480161..c9be419b 100644
--- a/src/ahriman/application/ahriman.py
+++ b/src/ahriman/application/ahriman.py
@@ -27,11 +27,10 @@ from ahriman import version
from ahriman.application import handlers
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.sign_settings import SignSettings
+from ahriman.models.user_access import UserAccess
# pylint thinks it is bad idea, but get the fuck off
-from ahriman.models.user_access import UserAccess
-
SubParserAction = argparse._SubParsersAction # pylint: disable=protected-access
@@ -367,7 +366,7 @@ def _set_web_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
parser = root.add_parser("web", help="start web server", description="start web server",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
- parser.set_defaults(handler=handlers.Web, lock=None, no_report=True)
+ parser.set_defaults(handler=handlers.Web, lock=None, no_report=True, parser=_parser)
return parser
diff --git a/src/ahriman/application/application.py b/src/ahriman/application/application.py
index 277fe884..0f9f9edd 100644
--- a/src/ahriman/application/application.py
+++ b/src/ahriman/application/application.py
@@ -40,16 +40,17 @@ class Application:
:ivar repository: repository instance
"""
- def __init__(self, architecture: str, configuration: Configuration) -> None:
+ def __init__(self, architecture: str, configuration: Configuration, no_report: bool) -> None:
"""
default constructor
:param architecture: repository architecture
:param configuration: configuration instance
+ :param no_report: force disable reporting
"""
self.logger = logging.getLogger("root")
self.configuration = configuration
self.architecture = architecture
- self.repository = Repository(architecture, configuration)
+ self.repository = Repository(architecture, configuration, no_report)
def _finalize(self, built_packages: Iterable[Package]) -> None:
"""
diff --git a/src/ahriman/application/handlers/add.py b/src/ahriman/application/handlers/add.py
index 4af8de55..34409de9 100644
--- a/src/ahriman/application/handlers/add.py
+++ b/src/ahriman/application/handlers/add.py
@@ -32,14 +32,16 @@ class Add(Handler):
"""
@classmethod
- def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
+ def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
+ configuration: Configuration, no_report: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
+ :param no_report: force disable reporting
"""
- application = Application(architecture, configuration)
+ application = Application(architecture, configuration, no_report)
application.add(args.package, args.without_dependencies)
if not args.now:
return
diff --git a/src/ahriman/application/handlers/clean.py b/src/ahriman/application/handlers/clean.py
index 8454ce91..9d6b8be9 100644
--- a/src/ahriman/application/handlers/clean.py
+++ b/src/ahriman/application/handlers/clean.py
@@ -32,12 +32,14 @@ class Clean(Handler):
"""
@classmethod
- def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
+ def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
+ configuration: Configuration, no_report: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
+ :param no_report: force disable reporting
"""
- Application(architecture, configuration).clean(args.no_build, args.no_cache, args.no_chroot,
- args.no_manual, args.no_packages)
+ Application(architecture, configuration, no_report).clean(args.no_build, args.no_cache, args.no_chroot,
+ args.no_manual, args.no_packages)
diff --git a/src/ahriman/application/handlers/create_user.py b/src/ahriman/application/handlers/create_user.py
index 6fc9aca6..4be5c92a 100644
--- a/src/ahriman/application/handlers/create_user.py
+++ b/src/ahriman/application/handlers/create_user.py
@@ -34,12 +34,14 @@ class CreateUser(Handler):
"""
@classmethod
- def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
+ def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
+ configuration: Configuration, no_report: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
+ :param no_report: force disable reporting
"""
salt = CreateUser.get_salt(configuration)
user = CreateUser.create_user(args)
diff --git a/src/ahriman/application/handlers/dump.py b/src/ahriman/application/handlers/dump.py
index 8a9151c8..c5f3a692 100644
--- a/src/ahriman/application/handlers/dump.py
+++ b/src/ahriman/application/handlers/dump.py
@@ -33,12 +33,14 @@ class Dump(Handler):
_print = print
@classmethod
- def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
+ def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
+ configuration: Configuration, no_report: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
+ :param no_report: force disable reporting
"""
dump = configuration.dump()
for section, values in sorted(dump.items()):
diff --git a/src/ahriman/application/handlers/handler.py b/src/ahriman/application/handlers/handler.py
index 23a582ba..85bd88ad 100644
--- a/src/ahriman/application/handlers/handler.py
+++ b/src/ahriman/application/handlers/handler.py
@@ -27,17 +27,20 @@ from typing import Set, Type
from ahriman.application.lock import Lock
from ahriman.core.configuration import Configuration
-from ahriman.core.exceptions import MissingArchitecture
+from ahriman.core.exceptions import MissingArchitecture, MultipleArchitecture
from ahriman.models.repository_paths import RepositoryPaths
class Handler:
"""
base handler class for command callbacks
+ :cvar ALLOW_MULTI_ARCHITECTURE_RUN: allow to run with multiple architectures
"""
+ ALLOW_MULTI_ARCHITECTURE_RUN = True
+
@classmethod
- def _call(cls: Type[Handler], args: argparse.Namespace, architecture: str) -> bool:
+ def call(cls: Type[Handler], args: argparse.Namespace, architecture: str) -> bool:
"""
additional function to wrap all calls for multiprocessing library
:param args: command line args
@@ -47,7 +50,7 @@ class Handler:
try:
configuration = Configuration.from_path(args.configuration, architecture, not args.no_log)
with Lock(args, architecture, configuration):
- cls.run(args, architecture, configuration)
+ cls.run(args, architecture, configuration, args.no_report)
return True
except Exception:
logging.getLogger("root").exception("process exception")
@@ -61,9 +64,18 @@ class Handler:
:return: 0 on success, 1 otherwise
"""
architectures = cls.extract_architectures(args)
- with Pool(len(architectures)) as pool:
- result = pool.starmap(
- cls._call, [(args, architecture) for architecture in architectures])
+
+ # actually we do not have to spawn another process if it is single-process application, do we?
+ if len(architectures) > 1:
+ if not cls.ALLOW_MULTI_ARCHITECTURE_RUN:
+ raise MultipleArchitecture(args.command)
+
+ with Pool(len(architectures)) as pool:
+ result = pool.starmap(
+ cls.call, [(args, architecture) for architecture in architectures])
+ else:
+ result = [cls.call(args, architectures.pop())]
+
return 0 if all(result) else 1
@classmethod
@@ -88,11 +100,13 @@ class Handler:
return architectures
@classmethod
- def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
+ def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
+ configuration: Configuration, no_report: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
+ :param no_report: force disable reporting
"""
raise NotImplementedError
diff --git a/src/ahriman/application/handlers/init.py b/src/ahriman/application/handlers/init.py
index beed55da..7d58c252 100644
--- a/src/ahriman/application/handlers/init.py
+++ b/src/ahriman/application/handlers/init.py
@@ -32,11 +32,13 @@ class Init(Handler):
"""
@classmethod
- def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
+ def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
+ configuration: Configuration, no_report: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
+ :param no_report: force disable reporting
"""
- Application(architecture, configuration).repository.repo.init()
+ Application(architecture, configuration, no_report).repository.repo.init()
diff --git a/src/ahriman/application/handlers/key_import.py b/src/ahriman/application/handlers/key_import.py
index 9b6b0216..18754a1e 100644
--- a/src/ahriman/application/handlers/key_import.py
+++ b/src/ahriman/application/handlers/key_import.py
@@ -32,11 +32,13 @@ class KeyImport(Handler):
"""
@classmethod
- def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
+ def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
+ configuration: Configuration, no_report: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
+ :param no_report: force disable reporting
"""
- Application(architecture, configuration).repository.sign.import_key(args.key_server, args.key)
+ Application(architecture, configuration, no_report).repository.sign.import_key(args.key_server, args.key)
diff --git a/src/ahriman/application/handlers/rebuild.py b/src/ahriman/application/handlers/rebuild.py
index e6613782..b318964a 100644
--- a/src/ahriman/application/handlers/rebuild.py
+++ b/src/ahriman/application/handlers/rebuild.py
@@ -32,16 +32,18 @@ class Rebuild(Handler):
"""
@classmethod
- def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
+ def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
+ configuration: Configuration, no_report: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
+ :param no_report: force disable reporting
"""
depends_on = set(args.depends_on) if args.depends_on else None
- application = Application(architecture, configuration)
+ application = Application(architecture, configuration, no_report)
packages = [
package
for package in application.repository.packages()
diff --git a/src/ahriman/application/handlers/remove.py b/src/ahriman/application/handlers/remove.py
index ba570095..7a403312 100644
--- a/src/ahriman/application/handlers/remove.py
+++ b/src/ahriman/application/handlers/remove.py
@@ -32,11 +32,13 @@ class Remove(Handler):
"""
@classmethod
- def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
+ def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
+ configuration: Configuration, no_report: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
+ :param no_report: force disable reporting
"""
- Application(architecture, configuration).remove(args.package)
+ Application(architecture, configuration, no_report).remove(args.package)
diff --git a/src/ahriman/application/handlers/remove_unknown.py b/src/ahriman/application/handlers/remove_unknown.py
index 1e19f79c..39cc2a60 100644
--- a/src/ahriman/application/handlers/remove_unknown.py
+++ b/src/ahriman/application/handlers/remove_unknown.py
@@ -33,14 +33,16 @@ class RemoveUnknown(Handler):
"""
@classmethod
- def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
+ def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
+ configuration: Configuration, no_report: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
+ :param no_report: force disable reporting
"""
- application = Application(architecture, configuration)
+ application = Application(architecture, configuration, no_report)
unknown_packages = application.unknown()
if args.dry_run:
for package in unknown_packages:
diff --git a/src/ahriman/application/handlers/report.py b/src/ahriman/application/handlers/report.py
index 79ce3c07..6bbb8bac 100644
--- a/src/ahriman/application/handlers/report.py
+++ b/src/ahriman/application/handlers/report.py
@@ -32,11 +32,13 @@ class Report(Handler):
"""
@classmethod
- def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
+ def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
+ configuration: Configuration, no_report: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
+ :param no_report: force disable reporting
"""
- Application(architecture, configuration).report(args.target, [])
+ Application(architecture, configuration, no_report).report(args.target, [])
diff --git a/src/ahriman/application/handlers/search.py b/src/ahriman/application/handlers/search.py
index ba026137..ce08a5e0 100644
--- a/src/ahriman/application/handlers/search.py
+++ b/src/ahriman/application/handlers/search.py
@@ -32,12 +32,14 @@ class Search(Handler):
"""
@classmethod
- def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
+ def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
+ configuration: Configuration, no_report: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
+ :param no_report: force disable reporting
"""
search = " ".join(args.search)
packages = aur.search(search)
diff --git a/src/ahriman/application/handlers/setup.py b/src/ahriman/application/handlers/setup.py
index 34121b2e..d2ac8d25 100644
--- a/src/ahriman/application/handlers/setup.py
+++ b/src/ahriman/application/handlers/setup.py
@@ -43,14 +43,16 @@ class Setup(Handler):
SUDOERS_PATH = Path("/etc/sudoers.d/ahriman")
@classmethod
- def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
+ def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
+ configuration: Configuration, no_report: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
+ :param no_report: force disable reporting
"""
- application = Application(architecture, configuration)
+ application = Application(architecture, configuration, no_report)
Setup.create_makepkg_configuration(args.packager, application.repository.paths)
Setup.create_executable(args.build_command, architecture)
Setup.create_devtools_configuration(args.build_command, architecture, args.from_configuration,
diff --git a/src/ahriman/application/handlers/sign.py b/src/ahriman/application/handlers/sign.py
index c5ed285e..66d1b957 100644
--- a/src/ahriman/application/handlers/sign.py
+++ b/src/ahriman/application/handlers/sign.py
@@ -32,11 +32,13 @@ class Sign(Handler):
"""
@classmethod
- def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
+ def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
+ configuration: Configuration, no_report: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
+ :param no_report: force disable reporting
"""
- Application(architecture, configuration).sign(args.package)
+ Application(architecture, configuration, no_report).sign(args.package)
diff --git a/src/ahriman/application/handlers/status.py b/src/ahriman/application/handlers/status.py
index ab790ae4..66e581b1 100644
--- a/src/ahriman/application/handlers/status.py
+++ b/src/ahriman/application/handlers/status.py
@@ -34,24 +34,27 @@ class Status(Handler):
"""
@classmethod
- def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
+ def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
+ configuration: Configuration, no_report: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
+ :param no_report: force disable reporting
"""
- application = Application(architecture, configuration)
+ # we are using reporter here
+ client = Application(architecture, configuration, no_report=False).repository.reporter
if args.ahriman:
- ahriman = application.repository.reporter.get_self()
+ ahriman = client.get_self()
print(ahriman.pretty_print())
print()
if args.package:
packages: Iterable[Tuple[Package, BuildStatus]] = sum(
- [application.repository.reporter.get(base) for base in args.package],
+ [client.get(base) for base in args.package],
start=[])
else:
- packages = application.repository.reporter.get(None)
+ packages = client.get(None)
for package, package_status in sorted(packages, key=lambda item: item[0].base):
print(package.pretty_print())
print(f"\t{package.version}")
diff --git a/src/ahriman/application/handlers/status_update.py b/src/ahriman/application/handlers/status_update.py
index 39647dc0..1b80ded4 100644
--- a/src/ahriman/application/handlers/status_update.py
+++ b/src/ahriman/application/handlers/status_update.py
@@ -32,14 +32,17 @@ class StatusUpdate(Handler):
"""
@classmethod
- def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
+ def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
+ configuration: Configuration, no_report: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
+ :param no_report: force disable reporting
"""
- client = Application(architecture, configuration).repository.reporter
+ # we are using reporter here
+ client = Application(architecture, configuration, no_report=False).repository.reporter
callback: Callable[[str], None] = lambda p: client.remove(p) if args.remove else client.update(p, args.status)
if args.package:
# update packages statuses
diff --git a/src/ahriman/application/handlers/sync.py b/src/ahriman/application/handlers/sync.py
index bedd0416..51af527e 100644
--- a/src/ahriman/application/handlers/sync.py
+++ b/src/ahriman/application/handlers/sync.py
@@ -32,11 +32,13 @@ class Sync(Handler):
"""
@classmethod
- def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
+ def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
+ configuration: Configuration, no_report: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
+ :param no_report: force disable reporting
"""
- Application(architecture, configuration).sync(args.target, [])
+ Application(architecture, configuration, no_report).sync(args.target, [])
diff --git a/src/ahriman/application/handlers/update.py b/src/ahriman/application/handlers/update.py
index 08febe66..c9bb3fab 100644
--- a/src/ahriman/application/handlers/update.py
+++ b/src/ahriman/application/handlers/update.py
@@ -32,14 +32,16 @@ class Update(Handler):
"""
@classmethod
- def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
+ def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
+ configuration: Configuration, no_report: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
+ :param no_report: force disable reporting
"""
- application = Application(architecture, configuration)
+ application = Application(architecture, configuration, no_report)
packages = application.get_updates(args.package, args.no_aur, args.no_manual, args.no_vcs,
Update.log_fn(application, args.dry_run))
if args.dry_run:
diff --git a/src/ahriman/application/handlers/web.py b/src/ahriman/application/handlers/web.py
index 837dce4e..29c6524d 100644
--- a/src/ahriman/application/handlers/web.py
+++ b/src/ahriman/application/handlers/web.py
@@ -23,6 +23,7 @@ from typing import Type
from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration
+from ahriman.core.spawn import Spawn
class Web(Handler):
@@ -30,14 +31,23 @@ class Web(Handler):
web server handler
"""
+ ALLOW_MULTI_ARCHITECTURE_RUN = False # required to be able to spawn external processes
+
@classmethod
- def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
+ def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
+ configuration: Configuration, no_report: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
+ :param no_report: force disable reporting
"""
+ # we are using local import for optional dependencies
from ahriman.web.web import run_server, setup_service
- application = setup_service(architecture, configuration)
+
+ spawner = Spawn(args.parser(), architecture, configuration)
+ spawner.start()
+
+ application = setup_service(architecture, configuration, spawner)
run_server(application)
diff --git a/src/ahriman/core/auth/auth.py b/src/ahriman/core/auth/auth.py
index 6ff0a8b8..70de402e 100644
--- a/src/ahriman/core/auth/auth.py
+++ b/src/ahriman/core/auth/auth.py
@@ -19,7 +19,7 @@
#
from __future__ import annotations
-from typing import Optional, Set, Type
+from typing import Optional, Type
from ahriman.core.configuration import Configuration
from ahriman.models.auth_settings import AuthSettings
@@ -36,8 +36,8 @@ class Auth:
:cvar ALLOWED_PATHS_GROUPS: URI paths prefixes which can be accessed without authorization, predefined
"""
- ALLOWED_PATHS = {"/", "/favicon.ico", "/index.html", "/login", "/logout"}
- ALLOWED_PATHS_GROUPS: Set[str] = set()
+ ALLOWED_PATHS = {"/", "/favicon.ico", "/index.html"}
+ ALLOWED_PATHS_GROUPS = {"/user-api"}
def __init__(self, configuration: Configuration, provider: AuthSettings = AuthSettings.Disabled) -> None:
"""
diff --git a/src/ahriman/core/exceptions.py b/src/ahriman/core/exceptions.py
index 1e452517..10a09989 100644
--- a/src/ahriman/core/exceptions.py
+++ b/src/ahriman/core/exceptions.py
@@ -109,6 +109,19 @@ class MissingArchitecture(Exception):
Exception.__init__(self, f"Architecture required for subcommand {command}, but missing")
+class MultipleArchitecture(Exception):
+ """
+ exception which will be raised if multiple architectures are not supported by the handler
+ """
+
+ def __init__(self, command: str) -> None:
+ """
+ default constructor
+ :param command: command name which throws exception
+ """
+ Exception.__init__(self, f"Multiple architectures are not supported by subcommand {command}")
+
+
class ReportFailed(Exception):
"""
report generation exception
diff --git a/src/ahriman/core/repository/properties.py b/src/ahriman/core/repository/properties.py
index e2b040b2..54e9ebc2 100644
--- a/src/ahriman/core/repository/properties.py
+++ b/src/ahriman/core/repository/properties.py
@@ -43,7 +43,13 @@ class Properties:
:ivar sign: GPG wrapper instance
"""
- def __init__(self, architecture: str, configuration: Configuration) -> None:
+ def __init__(self, architecture: str, configuration: Configuration, no_report: bool) -> None:
+ """
+ default constructor
+ :param architecture: repository architecture
+ :param configuration: configuration instance
+ :param no_report: force disable reporting
+ """
self.logger = logging.getLogger("builder")
self.architecture = architecture
self.configuration = configuration
@@ -58,4 +64,4 @@ class Properties:
self.pacman = Pacman(configuration)
self.sign = GPG(architecture, configuration)
self.repo = Repo(self.name, self.paths, self.sign.repository_sign_args)
- self.reporter = Client.load(configuration)
+ self.reporter = Client() if no_report else Client.load(configuration)
diff --git a/src/ahriman/core/spawn.py b/src/ahriman/core/spawn.py
new file mode 100644
index 00000000..eb094d54
--- /dev/null
+++ b/src/ahriman/core/spawn.py
@@ -0,0 +1,137 @@
+#
+# Copyright (c) 2021 ahriman team.
+#
+# This file is part of ahriman
+# (see https://github.com/arcan1s/ahriman).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+from __future__ import annotations
+
+import argparse
+import logging
+import uuid
+
+from multiprocessing import Process, Queue
+from threading import Lock, Thread
+from typing import Callable, Dict, Iterable, Tuple
+
+from ahriman.core.configuration import Configuration
+
+
+class Spawn(Thread):
+ """
+ helper to spawn external ahriman process
+ MUST NOT be used directly, the only one usage allowed is to spawn process from web services
+ :ivar active: map of active child processes required to avoid zombies
+ :ivar architecture: repository architecture
+ :ivar configuration: configuration instance
+ :ivar logger: spawner logger
+ :ivar queue: multiprocessing queue to read updates from processes
+ """
+
+ def __init__(self, args_parser: argparse.ArgumentParser, architecture: str, configuration: Configuration) -> None:
+ """
+ default constructor
+ :param args_parser: command line parser for the application
+ :param architecture: repository architecture
+ :param configuration: configuration instance
+ """
+ Thread.__init__(self, name="spawn")
+ self.architecture = architecture
+ self.args_parser = args_parser
+ self.configuration = configuration
+ self.logger = logging.getLogger("http")
+
+ self.lock = Lock()
+ self.active: Dict[str, Process] = {}
+ # stupid pylint does not know that it is possible
+ self.queue: Queue[Tuple[str, bool]] = Queue() # pylint: disable=unsubscriptable-object
+
+ @staticmethod
+ def process(callback: Callable[[argparse.Namespace, str], bool], args: argparse.Namespace, architecture: str,
+ process_id: str, queue: Queue[Tuple[str, bool]]) -> None: # pylint: disable=unsubscriptable-object
+ """
+ helper to run external process
+ :param callback: application run function (i.e. Handler.run method)
+ :param args: command line arguments
+ :param architecture: repository architecture
+ :param process_id: process unique identifier
+ :param queue: output queue
+ """
+ result = callback(args, architecture)
+ queue.put((process_id, result))
+
+ def packages_add(self, packages: Iterable[str], now: bool) -> None:
+ """
+ add packages
+ :param packages: packages list to add
+ :param now: build packages now
+ """
+ kwargs = {"now": ""} if now else {}
+ self.spawn_process("add", *packages, **kwargs)
+
+ def packages_remove(self, packages: Iterable[str]) -> None:
+ """
+ remove packages
+ :param packages: packages list to remove
+ """
+ self.spawn_process("remove", *packages)
+
+ def spawn_process(self, command: str, *args: str, **kwargs: str) -> None:
+ """
+ spawn external ahriman process with supplied arguments
+ :param command: subcommand to run
+ :param args: positional command arguments
+ :param kwargs: named command arguments
+ """
+ # default arguments
+ arguments = ["--architecture", self.architecture]
+ if self.configuration.path is not None:
+ arguments.extend(["--configuration", str(self.configuration.path)])
+ # positional command arguments
+ arguments.append(command)
+ arguments.extend(args)
+ # named command arguments
+ for argument, value in kwargs.items():
+ arguments.append(f"--{argument}")
+ if value:
+ arguments.append(value)
+
+ process_id = str(uuid.uuid4())
+ self.logger.info("full command line arguments of %s are %s", process_id, arguments)
+ parsed = self.args_parser.parse_args(arguments)
+
+ callback = parsed.handler.call
+ process = Process(target=self.process,
+ args=(callback, parsed, self.architecture, process_id, self.queue),
+ daemon=True)
+ process.start()
+
+ with self.lock:
+ self.active[process_id] = process
+
+ def run(self) -> None:
+ """
+ thread run method
+ """
+ for process_id, status in iter(self.queue.get, None):
+ self.logger.info("process %s has been terminated with status %s", process_id, status)
+
+ with self.lock:
+ process = self.active.pop(process_id, None)
+
+ if process is not None:
+ process.terminate() # make sure lol
+ process.join()
diff --git a/src/ahriman/core/status/watcher.py b/src/ahriman/core/status/watcher.py
index 7d8e3f2e..8d320853 100644
--- a/src/ahriman/core/status/watcher.py
+++ b/src/ahriman/core/status/watcher.py
@@ -49,7 +49,7 @@ class Watcher:
self.logger = logging.getLogger("http")
self.architecture = architecture
- self.repository = Repository(architecture, configuration)
+ self.repository = Repository(architecture, configuration, no_report=True)
self.known: Dict[str, Tuple[Package, BuildStatus]] = {}
self.status = BuildStatus()
diff --git a/src/ahriman/core/status/web_client.py b/src/ahriman/core/status/web_client.py
index badd9c52..122d8579 100644
--- a/src/ahriman/core/status/web_client.py
+++ b/src/ahriman/core/status/web_client.py
@@ -65,7 +65,7 @@ class WebClient(Client):
"""
:return: full url for web service to login
"""
- return f"{self.address}/login"
+ return f"{self.address}/user-api/v1/login"
@property
def _status_url(self) -> str:
diff --git a/src/ahriman/models/user.py b/src/ahriman/models/user.py
index 73e1f3b9..ef0c58bb 100644
--- a/src/ahriman/models/user.py
+++ b/src/ahriman/models/user.py
@@ -51,7 +51,7 @@ class User:
"""
if username is None or password is None:
return None
- return cls(username, password, UserAccess.Status)
+ return cls(username, password, UserAccess.Read)
@staticmethod
def generate_password(length: int) -> str:
diff --git a/src/ahriman/models/user_access.py b/src/ahriman/models/user_access.py
index b0a9f1fc..4eb78272 100644
--- a/src/ahriman/models/user_access.py
+++ b/src/ahriman/models/user_access.py
@@ -25,9 +25,7 @@ class UserAccess(Enum):
web user access enumeration
:cvar Read: user can read status page
:cvar Write: user can modify task and package list
- :cvar Status: user can update statuses via API
"""
Read = "read"
Write = "write"
- Status = "status"
diff --git a/src/ahriman/web/middlewares/auth_handler.py b/src/ahriman/web/middlewares/auth_handler.py
index 26da6725..b8b51fd1 100644
--- a/src/ahriman/web/middlewares/auth_handler.py
+++ b/src/ahriman/web/middlewares/auth_handler.py
@@ -73,9 +73,7 @@ def auth_handler(validator: Auth) -> MiddlewareType:
"""
@middleware
async def handle(request: Request, handler: HandlerType) -> StreamResponse:
- if request.path.startswith("/status-api"):
- permission = UserAccess.Status
- elif request.method in ("GET", "HEAD", "OPTIONS"):
+ if request.method in ("GET", "HEAD", "OPTIONS"):
permission = UserAccess.Read
else:
permission = UserAccess.Write
diff --git a/src/ahriman/web/routes.py b/src/ahriman/web/routes.py
index 0c6ba966..17c22d5c 100644
--- a/src/ahriman/web/routes.py
+++ b/src/ahriman/web/routes.py
@@ -19,13 +19,16 @@
#
from aiohttp.web import Application
-from ahriman.web.views.ahriman import AhrimanView
from ahriman.web.views.index import IndexView
-from ahriman.web.views.login import LoginView
-from ahriman.web.views.logout import LogoutView
-from ahriman.web.views.package import PackageView
-from ahriman.web.views.packages import PackagesView
-from ahriman.web.views.status import StatusView
+from ahriman.web.views.service.add import AddView
+from ahriman.web.views.service.remove import RemoveView
+from ahriman.web.views.service.search import SearchView
+from ahriman.web.views.status.ahriman import AhrimanView
+from ahriman.web.views.status.package import PackageView
+from ahriman.web.views.status.packages import PackagesView
+from ahriman.web.views.status.status import StatusView
+from ahriman.web.views.user.login import LoginView
+from ahriman.web.views.user.logout import LogoutView
def setup_routes(application: Application) -> None:
@@ -37,8 +40,13 @@ def setup_routes(application: Application) -> None:
GET / get build status page
GET /index.html same as above
- POST /login login to service
- POST /logout logout from service
+ POST /service-api/v1/add add new packages to repository
+
+ POST /service-api/v1/remove remove existing package from repository
+
+ POST /service-api/v1/update update packages in repository, actually it is just alias for add
+
+ GET /service-api/v1/search search for substring in AUR
GET /status-api/v1/ahriman get current service status
POST /status-api/v1/ahriman update service status
@@ -52,13 +60,21 @@ def setup_routes(application: Application) -> None:
GET /status-api/v1/status get web service status itself
+ POST /user-api/v1/login login to service
+ POST /user-api/v1/logout logout from service
+
:param application: web application instance
"""
application.router.add_get("/", IndexView, allow_head=True)
application.router.add_get("/index.html", IndexView, allow_head=True)
- application.router.add_post("/login", LoginView)
- application.router.add_post("/logout", LogoutView)
+ application.router.add_post("/service-api/v1/add", AddView)
+
+ application.router.add_post("/service-api/v1/remove", RemoveView)
+
+ application.router.add_get("/service-api/v1/search", SearchView, allow_head=False)
+
+ application.router.add_post("/service-api/v1/update", AddView)
application.router.add_get("/status-api/v1/ahriman", AhrimanView, allow_head=True)
application.router.add_post("/status-api/v1/ahriman", AhrimanView)
@@ -71,3 +87,6 @@ def setup_routes(application: Application) -> None:
application.router.add_post("/status-api/v1/packages/{package}", PackageView)
application.router.add_get("/status-api/v1/status", StatusView, allow_head=True)
+
+ application.router.add_post("/user-api/v1/login", LoginView)
+ application.router.add_post("/user-api/v1/logout", LogoutView)
diff --git a/src/ahriman/web/views/base.py b/src/ahriman/web/views/base.py
index 9b10ba68..78818e0a 100644
--- a/src/ahriman/web/views/base.py
+++ b/src/ahriman/web/views/base.py
@@ -18,9 +18,10 @@
# along with this program. If not, see .
#
from aiohttp.web import View
-from typing import Any, Dict
+from typing import Any, Dict, List, Optional
from ahriman.core.auth.auth import Auth
+from ahriman.core.spawn import Spawn
from ahriman.core.status.watcher import Watcher
@@ -37,6 +38,14 @@ class BaseView(View):
watcher: Watcher = self.request.app["watcher"]
return watcher
+ @property
+ def spawner(self) -> Spawn:
+ """
+ :return: external process spawner instance
+ """
+ spawner: Spawn = self.request.app["spawn"]
+ return spawner
+
@property
def validator(self) -> Auth:
"""
@@ -45,13 +54,33 @@ class BaseView(View):
validator: Auth = self.request.app["validator"]
return validator
- async def extract_data(self) -> Dict[str, Any]:
+ async def extract_data(self, list_keys: Optional[List[str]] = None) -> Dict[str, Any]:
"""
extract json data from either json or form data
+ :param list_keys: optional list of keys which must be forced to list from form data
:return: raw json object or form data converted to json
"""
try:
json: Dict[str, Any] = await self.request.json()
return json
except ValueError:
- return dict(await self.request.post())
+ return await self.data_as_json(list_keys or [])
+
+ async def data_as_json(self, list_keys: List[str]) -> Dict[str, Any]:
+ """
+ extract form data and convert it to json object
+ :param list_keys: list of keys which must be forced to list from form data
+ :return: form data converted to json. In case if a key is found multiple times it will be returned as list
+ """
+ raw = await self.request.post()
+ json: Dict[str, Any] = {}
+ for key, value in raw.items():
+ if key in json and isinstance(json[key], list):
+ json[key].append(value)
+ elif key in json:
+ json[key] = [json[key], value]
+ elif key in list_keys:
+ json[key] = [value]
+ else:
+ json[key] = value
+ return json
diff --git a/src/ahriman/web/views/service/__init__.py b/src/ahriman/web/views/service/__init__.py
new file mode 100644
index 00000000..fb32931e
--- /dev/null
+++ b/src/ahriman/web/views/service/__init__.py
@@ -0,0 +1,19 @@
+#
+# Copyright (c) 2021 ahriman team.
+#
+# This file is part of ahriman
+# (see https://github.com/arcan1s/ahriman).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
diff --git a/src/ahriman/web/views/service/add.py b/src/ahriman/web/views/service/add.py
new file mode 100644
index 00000000..19a3ba2c
--- /dev/null
+++ b/src/ahriman/web/views/service/add.py
@@ -0,0 +1,52 @@
+#
+# Copyright (c) 2021 ahriman team.
+#
+# This file is part of ahriman
+# (see https://github.com/arcan1s/ahriman).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+from aiohttp.web import HTTPFound, Response, json_response
+
+from ahriman.web.views.base import BaseView
+
+
+class AddView(BaseView):
+ """
+ add package web view
+ """
+
+ async def post(self) -> Response:
+ """
+ add new package
+
+ JSON body must be supplied, the following model is used:
+ {
+ "packages": "ahriman", # either list of packages or package name as in AUR
+ "build_now": true # optional flag which runs build
+ }
+
+ :return: redirect to main page on success
+ """
+ data = await self.extract_data(["packages"])
+
+ try:
+ now = data.get("build_now", True)
+ packages = data["packages"]
+ except Exception as e:
+ return json_response(text=str(e), status=400)
+
+ self.spawner.packages_add(packages, now)
+
+ return HTTPFound("/")
diff --git a/src/ahriman/web/views/service/remove.py b/src/ahriman/web/views/service/remove.py
new file mode 100644
index 00000000..403f15b2
--- /dev/null
+++ b/src/ahriman/web/views/service/remove.py
@@ -0,0 +1,50 @@
+#
+# Copyright (c) 2021 ahriman team.
+#
+# This file is part of ahriman
+# (see https://github.com/arcan1s/ahriman).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+from aiohttp.web import HTTPFound, Response, json_response
+
+from ahriman.web.views.base import BaseView
+
+
+class RemoveView(BaseView):
+ """
+ remove package web view
+ """
+
+ async def post(self) -> Response:
+ """
+ remove existing packages
+
+ JSON body must be supplied, the following model is used:
+ {
+ "packages": "ahriman", # either list of packages or package name
+ }
+
+ :return: redirect to main page on success
+ """
+ data = await self.extract_data(["packages"])
+
+ try:
+ packages = data["packages"]
+ except Exception as e:
+ return json_response(text=str(e), status=400)
+
+ self.spawner.packages_remove(packages)
+
+ return HTTPFound("/")
diff --git a/src/ahriman/web/views/service/search.py b/src/ahriman/web/views/service/search.py
new file mode 100644
index 00000000..b2e781a5
--- /dev/null
+++ b/src/ahriman/web/views/service/search.py
@@ -0,0 +1,48 @@
+#
+# Copyright (c) 2021 ahriman team.
+#
+# This file is part of ahriman
+# (see https://github.com/arcan1s/ahriman).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+import aur # type: ignore
+
+from aiohttp.web import Response, json_response
+from typing import Iterator
+
+from ahriman.web.views.base import BaseView
+
+
+class SearchView(BaseView):
+ """
+ AUR search web view
+ """
+
+ async def get(self) -> Response:
+ """
+ search packages in AUR
+
+ search string (non empty) must be supplied as `for` parameter
+
+ :return: 200 with found package bases sorted by name
+ """
+ search: Iterator[str] = filter(lambda s: len(s) > 3, self.request.query.getall("for", default=[]))
+ search_string = " ".join(search)
+
+ if not search_string:
+ return json_response(text="Search string must not be empty", status=400)
+ packages = aur.search(search_string)
+
+ return json_response(sorted(package.package_base for package in packages))
diff --git a/src/ahriman/web/views/status/__init__.py b/src/ahriman/web/views/status/__init__.py
new file mode 100644
index 00000000..fb32931e
--- /dev/null
+++ b/src/ahriman/web/views/status/__init__.py
@@ -0,0 +1,19 @@
+#
+# Copyright (c) 2021 ahriman team.
+#
+# This file is part of ahriman
+# (see https://github.com/arcan1s/ahriman).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
diff --git a/src/ahriman/web/views/ahriman.py b/src/ahriman/web/views/status/ahriman.py
similarity index 92%
rename from src/ahriman/web/views/ahriman.py
rename to src/ahriman/web/views/status/ahriman.py
index 42f85bc8..80a09b8e 100644
--- a/src/ahriman/web/views/ahriman.py
+++ b/src/ahriman/web/views/status/ahriman.py
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
+from aiohttp.web import HTTPNoContent, Response, json_response
from ahriman.models.build_status import BuildStatusEnum
from ahriman.web.views.base import BaseView
@@ -51,7 +51,7 @@ class AhrimanView(BaseView):
try:
status = BuildStatusEnum(data["status"])
except Exception as e:
- raise HTTPBadRequest(text=str(e))
+ return json_response(text=str(e), status=400)
self.service.update_self(status)
diff --git a/src/ahriman/web/views/package.py b/src/ahriman/web/views/status/package.py
similarity index 91%
rename from src/ahriman/web/views/package.py
rename to src/ahriman/web/views/status/package.py
index 3789a896..234bb5f8 100644
--- a/src/ahriman/web/views/package.py
+++ b/src/ahriman/web/views/status/package.py
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response, json_response
+from aiohttp.web import HTTPNoContent, HTTPNotFound, Response, json_response
from ahriman.core.exceptions import UnknownPackage
from ahriman.models.build_status import BuildStatusEnum
@@ -80,11 +80,11 @@ class PackageView(BaseView):
package = Package.from_json(data["package"]) if "package" in data else None
status = BuildStatusEnum(data["status"])
except Exception as e:
- raise HTTPBadRequest(text=str(e))
+ return json_response(text=str(e), status=400)
try:
self.service.update(base, status, package)
except UnknownPackage:
- raise HTTPBadRequest(text=f"Package {base} is unknown, but no package body set")
+ return json_response(text=f"Package {base} is unknown, but no package body set", status=400)
return HTTPNoContent()
diff --git a/src/ahriman/web/views/packages.py b/src/ahriman/web/views/status/packages.py
similarity index 100%
rename from src/ahriman/web/views/packages.py
rename to src/ahriman/web/views/status/packages.py
diff --git a/src/ahriman/web/views/status.py b/src/ahriman/web/views/status/status.py
similarity index 100%
rename from src/ahriman/web/views/status.py
rename to src/ahriman/web/views/status/status.py
diff --git a/src/ahriman/web/views/user/__init__.py b/src/ahriman/web/views/user/__init__.py
new file mode 100644
index 00000000..fb32931e
--- /dev/null
+++ b/src/ahriman/web/views/user/__init__.py
@@ -0,0 +1,19 @@
+#
+# Copyright (c) 2021 ahriman team.
+#
+# This file is part of ahriman
+# (see https://github.com/arcan1s/ahriman).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
diff --git a/src/ahriman/web/views/login.py b/src/ahriman/web/views/user/login.py
similarity index 100%
rename from src/ahriman/web/views/login.py
rename to src/ahriman/web/views/user/login.py
diff --git a/src/ahriman/web/views/logout.py b/src/ahriman/web/views/user/logout.py
similarity index 100%
rename from src/ahriman/web/views/logout.py
rename to src/ahriman/web/views/user/logout.py
diff --git a/src/ahriman/web/web.py b/src/ahriman/web/web.py
index 1cf98d3e..36a2b989 100644
--- a/src/ahriman/web/web.py
+++ b/src/ahriman/web/web.py
@@ -26,6 +26,7 @@ from aiohttp import web
from ahriman.core.auth.auth import Auth
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import InitializeException
+from ahriman.core.spawn import Spawn
from ahriman.core.status.watcher import Watcher
from ahriman.web.middlewares.exception_handler import exception_handler
from ahriman.web.routes import setup_routes
@@ -67,11 +68,12 @@ def run_server(application: web.Application) -> None:
access_log=logging.getLogger("http"))
-def setup_service(architecture: str, configuration: Configuration) -> web.Application:
+def setup_service(architecture: str, configuration: Configuration, spawner: Spawn) -> web.Application:
"""
create web application
:param architecture: repository architecture
:param configuration: configuration instance
+ :param spawner: spawner thread
:return: web application instance
"""
application = web.Application(logger=logging.getLogger("http"))
@@ -93,6 +95,9 @@ def setup_service(architecture: str, configuration: Configuration) -> web.Applic
application.logger.info("setup watcher")
application["watcher"] = Watcher(architecture, configuration)
+ application.logger.info("setup process spawner")
+ application["spawn"] = spawner
+
application.logger.info("setup authorization")
validator = application["validator"] = Auth.load(configuration)
if validator.enabled:
diff --git a/tests/ahriman/application/conftest.py b/tests/ahriman/application/conftest.py
index 2dc50ec4..872031d6 100644
--- a/tests/ahriman/application/conftest.py
+++ b/tests/ahriman/application/conftest.py
@@ -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:
"""
diff --git a/tests/ahriman/application/handlers/test_handler.py b/tests/ahriman/application/handlers/test_handler.py
index cd7c9efa..a8a770b0 100644
--- a/tests/ahriman/application/handlers/test_handler.py
+++ b/tests/ahriman/application/handlers/test_handler.py
@@ -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)
diff --git a/tests/ahriman/application/handlers/test_handler_add.py b/tests/ahriman/application/handlers/test_handler_add.py
index 25ab8d64..baad1abf 100644
--- a/tests/ahriman/application/handlers/test_handler_add.py
+++ b/tests/ahriman/application/handlers/test_handler_add.py
@@ -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()
diff --git a/tests/ahriman/application/handlers/test_handler_clean.py b/tests/ahriman/application/handlers/test_handler_clean.py
index ca1edf44..c0416934 100644
--- a/tests/ahriman/application/handlers/test_handler_clean.py
+++ b/tests/ahriman/application/handlers/test_handler_clean.py
@@ -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()
diff --git a/tests/ahriman/application/handlers/test_handler_create_user.py b/tests/ahriman/application/handlers/test_handler_create_user.py
index f251857d..cda99fdb 100644
--- a/tests/ahriman/application/handlers/test_handler_create_user.py
+++ b/tests/ahriman/application/handlers/test_handler_create_user.py
@@ -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()
diff --git a/tests/ahriman/application/handlers/test_handler_dump.py b/tests/ahriman/application/handlers/test_handler_dump.py
index 1864ae8f..f8962c15 100644
--- a/tests/ahriman/application/handlers/test_handler_dump.py
+++ b/tests/ahriman/application/handlers/test_handler_dump.py
@@ -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()
diff --git a/tests/ahriman/application/handlers/test_handler_init.py b/tests/ahriman/application/handlers/test_handler_init.py
index 4d70153c..82156a86 100644
--- a/tests/ahriman/application/handlers/test_handler_init.py
+++ b/tests/ahriman/application/handlers/test_handler_init.py
@@ -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()
diff --git a/tests/ahriman/application/handlers/test_handler_key_import.py b/tests/ahriman/application/handlers/test_handler_key_import.py
index 87279539..aede5841 100644
--- a/tests/ahriman/application/handlers/test_handler_key_import.py
+++ b/tests/ahriman/application/handlers/test_handler_key_import.py
@@ -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()
diff --git a/tests/ahriman/application/handlers/test_handler_rebuild.py b/tests/ahriman/application/handlers/test_handler_rebuild.py
index 32dfcc55..f77b70b1 100644
--- a/tests/ahriman/application/handlers/test_handler_rebuild.py
+++ b/tests/ahriman/application/handlers/test_handler_rebuild.py
@@ -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])
diff --git a/tests/ahriman/application/handlers/test_handler_remove.py b/tests/ahriman/application/handlers/test_handler_remove.py
index b64f9ccf..556cafc3 100644
--- a/tests/ahriman/application/handlers/test_handler_remove.py
+++ b/tests/ahriman/application/handlers/test_handler_remove.py
@@ -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()
diff --git a/tests/ahriman/application/handlers/test_handler_remove_unknown.py b/tests/ahriman/application/handlers/test_handler_remove_unknown.py
index 5d83ce47..861b6377 100644
--- a/tests/ahriman/application/handlers/test_handler_remove_unknown.py
+++ b/tests/ahriman/application/handlers/test_handler_remove_unknown.py
@@ -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)
diff --git a/tests/ahriman/application/handlers/test_handler_report.py b/tests/ahriman/application/handlers/test_handler_report.py
index 5f366038..c4b38340 100644
--- a/tests/ahriman/application/handlers/test_handler_report.py
+++ b/tests/ahriman/application/handlers/test_handler_report.py
@@ -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()
diff --git a/tests/ahriman/application/handlers/test_handler_search.py b/tests/ahriman/application/handlers/test_handler_search.py
index 68c432f7..891e6119 100644
--- a/tests/ahriman/application/handlers/test_handler_search.py
+++ b/tests/ahriman/application/handlers/test_handler_search.py
@@ -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
diff --git a/tests/ahriman/application/handlers/test_handler_setup.py b/tests/ahriman/application/handlers/test_handler_setup.py
index 8e5ca894..e51fa1fe 100644
--- a/tests/ahriman/application/handlers/test_handler_setup.py
+++ b/tests/ahriman/application/handlers/test_handler_setup.py
@@ -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()
diff --git a/tests/ahriman/application/handlers/test_handler_sign.py b/tests/ahriman/application/handlers/test_handler_sign.py
index bd8e73b6..f4500d96 100644
--- a/tests/ahriman/application/handlers/test_handler_sign.py
+++ b/tests/ahriman/application/handlers/test_handler_sign.py
@@ -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()
diff --git a/tests/ahriman/application/handlers/test_handler_status.py b/tests/ahriman/application/handlers/test_handler_status.py
index 143028ff..045705d6 100644
--- a/tests/ahriman/application/handlers/test_handler_status.py
+++ b/tests/ahriman/application/handlers/test_handler_status.py
@@ -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()
diff --git a/tests/ahriman/application/handlers/test_handler_status_update.py b/tests/ahriman/application/handlers/test_handler_status_update.py
index 4b4cc399..dc793577 100644
--- a/tests/ahriman/application/handlers/test_handler_status_update.py
+++ b/tests/ahriman/application/handlers/test_handler_status_update.py
@@ -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()
diff --git a/tests/ahriman/application/handlers/test_handler_sync.py b/tests/ahriman/application/handlers/test_handler_sync.py
index 660d71fb..91190da7 100644
--- a/tests/ahriman/application/handlers/test_handler_sync.py
+++ b/tests/ahriman/application/handlers/test_handler_sync.py
@@ -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()
diff --git a/tests/ahriman/application/handlers/test_handler_update.py b/tests/ahriman/application/handlers/test_handler_update.py
index 6861f034..acc93766 100644
--- a/tests/ahriman/application/handlers/test_handler_update.py
+++ b/tests/ahriman/application/handlers/test_handler_update.py
@@ -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()
diff --git a/tests/ahriman/application/handlers/test_handler_web.py b/tests/ahriman/application/handlers/test_handler_web.py
index 62f45f97..0f5e91e1 100644
--- a/tests/ahriman/application/handlers/test_handler_web.py
+++ b/tests/ahriman/application/handlers/test_handler_web.py
@@ -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
diff --git a/tests/ahriman/application/test_ahriman.py b/tests/ahriman/application/test_ahriman.py
index 91838703..a4df4bc9 100644
--- a/tests/ahriman/application/test_ahriman.py
+++ b/tests/ahriman/application/test_ahriman.py
@@ -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:
diff --git a/tests/ahriman/conftest.py b/tests/ahriman/conftest.py
index 99a600fe..91a0304c 100644
--- a/tests/ahriman/conftest.py
+++ b/tests/ahriman/conftest.py
@@ -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
diff --git a/tests/ahriman/core/auth/test_auth.py b/tests/ahriman/core/auth/test_auth.py
index 20d99218..5dda1f2a 100644
--- a/tests/ahriman/core/auth/test_auth.py
+++ b/tests/ahriman/core/auth/test_auth.py
@@ -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")
diff --git a/tests/ahriman/core/repository/conftest.py b/tests/ahriman/core/repository/conftest.py
index 2b8986d6..1e1057e4 100644
--- a/tests/ahriman/core/repository/conftest.py
+++ b/tests/ahriman/core/repository/conftest.py
@@ -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)
diff --git a/tests/ahriman/core/repository/test_properties.py b/tests/ahriman/core/repository/test_properties.py
index 760176c1..cda9ca4c 100644
--- a/tests/ahriman/core/repository/test_properties.py
+++ b/tests/ahriman/core/repository/test_properties.py
@@ -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()
diff --git a/tests/ahriman/core/status/test_watcher.py b/tests/ahriman/core/status/test_watcher.py
index a0dcc6ec..3a7cf764 100644
--- a/tests/ahriman/core/status/test_watcher.py
+++ b/tests/ahriman/core/status/test_watcher.py
@@ -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
diff --git a/tests/ahriman/core/test_spawn.py b/tests/ahriman/core/test_spawn.py
new file mode 100644
index 00000000..a64579d8
--- /dev/null
+++ b/tests/ahriman/core/test_spawn.py
@@ -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
diff --git a/tests/ahriman/core/upload/test_s3.py b/tests/ahriman/core/upload/test_s3.py
index 3d286ce5..c8b3af81 100644
--- a/tests/ahriman/core/upload/test_s3.py
+++ b/tests/ahriman/core/upload/test_s3.py
@@ -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()))
diff --git a/tests/ahriman/models/test_user.py b/tests/ahriman/models/test_user.py
index 3610e3a2..6acc2f3d 100644
--- a/tests/ahriman/models/test_user.py
+++ b/tests/ahriman/models/test_user.py
@@ -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)
diff --git a/tests/ahriman/web/conftest.py b/tests/ahriman/web/conftest.py
index f6919ed5..fc47d8e8 100644
--- a/tests/ahriman/web/conftest.py
+++ b/tests/ahriman/web/conftest.py
@@ -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
diff --git a/tests/ahriman/web/middlewares/conftest.py b/tests/ahriman/web/middlewares/conftest.py
index 716c29cd..a6b1a2df 100644
--- a/tests/ahriman/web/middlewares/conftest.py
+++ b/tests/ahriman/web/middlewares/conftest.py
@@ -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:
diff --git a/tests/ahriman/web/middlewares/test_auth_handler.py b/tests/ahriman/web/middlewares/test_auth_handler.py
index 96b09db8..3640889a 100644
--- a/tests/ahriman/web/middlewares/test_auth_handler.py
+++ b/tests/ahriman/web/middlewares/test_auth_handler.py
@@ -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")
diff --git a/tests/ahriman/web/middlewares/test_exception_handler.py b/tests/ahriman/web/middlewares/test_exception_handler.py
index 20435273..ca16d28e 100644
--- a/tests/ahriman/web/middlewares/test_exception_handler.py
+++ b/tests/ahriman/web/middlewares/test_exception_handler.py
@@ -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()
diff --git a/tests/ahriman/web/views/conftest.py b/tests/ahriman/web/views/conftest.py
index 8ae6bcc9..0ced73e5 100644
--- a/tests/ahriman/web/views/conftest.py
+++ b/tests/ahriman/web/views/conftest.py
@@ -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,
diff --git a/tests/ahriman/web/views/service/test_views_service_add.py b/tests/ahriman/web/views/service/test_views_service_add.py
new file mode 100644
index 00000000..24c750c9
--- /dev/null
+++ b/tests/ahriman/web/views/service/test_views_service_add.py
@@ -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)
diff --git a/tests/ahriman/web/views/service/test_views_service_remove.py b/tests/ahriman/web/views/service/test_views_service_remove.py
new file mode 100644
index 00000000..d7c45d80
--- /dev/null
+++ b/tests/ahriman/web/views/service/test_views_service_remove.py
@@ -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()
diff --git a/tests/ahriman/web/views/service/test_views_service_search.py b/tests/ahriman/web/views/service/test_views_service_search.py
new file mode 100644
index 00000000..bfd3158d
--- /dev/null
+++ b/tests/ahriman/web/views/service/test_views_service_search.py
@@ -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()
diff --git a/tests/ahriman/web/views/test_view_ahriman.py b/tests/ahriman/web/views/status/test_views_status_ahriman.py
similarity index 100%
rename from tests/ahriman/web/views/test_view_ahriman.py
rename to tests/ahriman/web/views/status/test_views_status_ahriman.py
diff --git a/tests/ahriman/web/views/test_view_package.py b/tests/ahriman/web/views/status/test_views_status_package.py
similarity index 100%
rename from tests/ahriman/web/views/test_view_package.py
rename to tests/ahriman/web/views/status/test_views_status_package.py
diff --git a/tests/ahriman/web/views/test_view_packages.py b/tests/ahriman/web/views/status/test_views_status_packages.py
similarity index 100%
rename from tests/ahriman/web/views/test_view_packages.py
rename to tests/ahriman/web/views/status/test_views_status_packages.py
diff --git a/tests/ahriman/web/views/test_view_status.py b/tests/ahriman/web/views/status/test_views_status_status.py
similarity index 100%
rename from tests/ahriman/web/views/test_view_status.py
rename to tests/ahriman/web/views/status/test_views_status_status.py
diff --git a/tests/ahriman/web/views/test_views_base.py b/tests/ahriman/web/views/test_views_base.py
new file mode 100644
index 00000000..e0ff3117
--- /dev/null
+++ b/tests/ahriman/web/views/test_views_base.py
@@ -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"]}
diff --git a/tests/ahriman/web/views/test_view_index.py b/tests/ahriman/web/views/test_views_index.py
similarity index 100%
rename from tests/ahriman/web/views/test_view_index.py
rename to tests/ahriman/web/views/test_views_index.py
diff --git a/tests/ahriman/web/views/test_view_login.py b/tests/ahriman/web/views/user/test_views_user_login.py
similarity index 77%
rename from tests/ahriman/web/views/test_view_login.py
rename to tests/ahriman/web/views/user/test_views_user_login.py
index 289b1fc2..8be422f1 100644
--- a/tests/ahriman/web/views/test_view_login.py
+++ b/tests/ahriman/web/views/user/test_views_user_login.py
@@ -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()
diff --git a/tests/ahriman/web/views/test_view_logout.py b/tests/ahriman/web/views/user/test_views_user_logout.py
similarity index 82%
rename from tests/ahriman/web/views/test_view_logout.py
rename to tests/ahriman/web/views/user/test_views_user_logout.py
index d9135c4a..3d287bb0 100644
--- a/tests/ahriman/web/views/test_view_logout.py
+++ b/tests/ahriman/web/views/user/test_views_user_logout.py
@@ -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