port part of settings to database (#54)

This commit is contained in:
Evgenii Alekseev 2022-03-31 01:48:06 +03:00 committed by GitHub
parent d4eadf0013
commit 83931f5cf4
117 changed files with 2768 additions and 1044 deletions

4
.gitignore vendored
View File

@ -94,4 +94,6 @@ ENV/
.venv/ .venv/
*.tar.xz *.tar.xz
status_cache.json status_cache.json
*.db

View File

@ -4,6 +4,7 @@ FROM archlinux:base-devel
ENV AHRIMAN_ARCHITECTURE="x86_64" ENV AHRIMAN_ARCHITECTURE="x86_64"
ENV AHRIMAN_DEBUG="" ENV AHRIMAN_DEBUG=""
ENV AHRIMAN_FORCE_ROOT="" ENV AHRIMAN_FORCE_ROOT=""
ENV AHRIMAN_HOST="0.0.0.0"
ENV AHRIMAN_OUTPUT="syslog" ENV AHRIMAN_OUTPUT="syslog"
ENV AHRIMAN_PACKAGER="ahriman bot <ahriman@example.com>" ENV AHRIMAN_PACKAGER="ahriman bot <ahriman@example.com>"
ENV AHRIMAN_PORT="" ENV AHRIMAN_PORT=""

View File

@ -5,7 +5,8 @@ set -e
# configuration tune # configuration tune
sed -i "s|root = /var/lib/ahriman|root = $AHRIMAN_REPOSITORY_ROOT|g" "/etc/ahriman.ini" sed -i "s|root = /var/lib/ahriman|root = $AHRIMAN_REPOSITORY_ROOT|g" "/etc/ahriman.ini"
sed -i "s|host = 127.0.0.1|host = 0.0.0.0|g" "/etc/ahriman.ini" sed -i "s|database = /var/lib/ahriman/ahriman.db|database = $AHRIMAN_REPOSITORY_ROOT/ahriman.db|g" "/etc/ahriman.ini"
sed -i "s|host = 127.0.0.1|host = $AHRIMAN_HOST|g" "/etc/ahriman.ini"
sed -i "s|handlers = syslog_handler|handlers = ${AHRIMAN_OUTPUT}_handler|g" "/etc/ahriman.ini.d/logging.ini" sed -i "s|handlers = syslog_handler|handlers = ${AHRIMAN_OUTPUT}_handler|g" "/etc/ahriman.ini.d/logging.ini"
AHRIMAN_DEFAULT_ARGS=("-a" "$AHRIMAN_ARCHITECTURE") AHRIMAN_DEFAULT_ARGS=("-a" "$AHRIMAN_ARCHITECTURE")
@ -40,9 +41,9 @@ systemd-machine-id-setup &> /dev/null
# otherwise we prepend executable by sudo command # otherwise we prepend executable by sudo command
if [ -n "$AHRIMAN_FORCE_ROOT" ]; then if [ -n "$AHRIMAN_FORCE_ROOT" ]; then
AHRIMAN_EXECUTABLE=("ahriman") AHRIMAN_EXECUTABLE=("ahriman")
elif ahriman help-commands-unsafe | grep -Fxq "$1"; then elif ahriman help-commands-unsafe --command="$*" &> /dev/null; then
AHRIMAN_EXECUTABLE=("ahriman")
else
AHRIMAN_EXECUTABLE=("sudo" "-u" "$AHRIMAN_USER" "--" "ahriman") AHRIMAN_EXECUTABLE=("sudo" "-u" "$AHRIMAN_USER" "--" "ahriman")
else
AHRIMAN_EXECUTABLE=("ahriman")
fi fi
exec "${AHRIMAN_EXECUTABLE[@]}" "${AHRIMAN_DEFAULT_ARGS[@]}" "$@" exec "${AHRIMAN_EXECUTABLE[@]}" "${AHRIMAN_DEFAULT_ARGS[@]}" "$@"

View File

@ -228,6 +228,7 @@ The following environment variables are supported:
* `AHRIMAN_ARCHITECTURE` - architecture of the repository, default is `x86_64`. * `AHRIMAN_ARCHITECTURE` - architecture of the repository, default is `x86_64`.
* `AHRIMAN_DEBUG` - if set all commands will be logged to console. * `AHRIMAN_DEBUG` - if set all commands will be logged to console.
* `AHRIMAN_FORCE_ROOT` - force run ahriman as root instead of guessing by subcommand. * `AHRIMAN_FORCE_ROOT` - force run ahriman as root instead of guessing by subcommand.
* `AHRIMAN_HOST` - host for the web interface, default is `0.0.0.0`.
* `AHRIMAN_OUTPUT` - controls logging handler, e.g. `syslog`, `console`. The name must be found in logging configuration. Note that if `syslog` (the default) handler is used you will need to mount `/dev/log` inside container because it is not available there. * `AHRIMAN_OUTPUT` - controls logging handler, e.g. `syslog`, `console`. The name must be found in logging configuration. Note that if `syslog` (the default) handler is used you will need to mount `/dev/log` inside container because it is not available there.
* `AHRIMAN_PACKAGER` - packager name from which packages will be built, default is `ahriman bot <ahriman@example.com>`. * `AHRIMAN_PACKAGER` - packager name from which packages will be built, default is `ahriman bot <ahriman@example.com>`.
* `AHRIMAN_PORT` - HTTP server port if any, default is empty. * `AHRIMAN_PORT` - HTTP server port if any, default is empty.

View File

@ -1,6 +1,7 @@
[settings] [settings]
include = ahriman.ini.d include = ahriman.ini.d
logging = ahriman.ini.d/logging.ini logging = ahriman.ini.d/logging.ini
database = /var/lib/ahriman/ahriman.db
[alpm] [alpm]
aur_url = https://aur.archlinux.org aur_url = https://aur.archlinux.org

View File

@ -1,5 +1,5 @@
[loggers] [loggers]
keys = root,build_details,http,stderr,boto3,botocore,nose,s3transfer keys = root,build_details,database,http,stderr,boto3,botocore,nose,s3transfer
[handlers] [handlers]
keys = console_handler,syslog_handler keys = console_handler,syslog_handler
@ -38,6 +38,12 @@ handlers = syslog_handler
qualname = build_details qualname = build_details
propagate = 0 propagate = 0
[logger_database]
level = DEBUG
handlers = syslog_handler
qualname = database
propagate = 0
[logger_http] [logger_http]
level = DEBUG level = DEBUG
handlers = syslog_handler handlers = syslog_handler

View File

@ -94,6 +94,7 @@ def _parser() -> argparse.ArgumentParser:
_set_repo_sync_parser(subparsers) _set_repo_sync_parser(subparsers)
_set_repo_update_parser(subparsers) _set_repo_update_parser(subparsers)
_set_user_add_parser(subparsers) _set_user_add_parser(subparsers)
_set_user_list_parser(subparsers)
_set_user_remove_parser(subparsers) _set_user_remove_parser(subparsers)
_set_web_parser(subparsers) _set_web_parser(subparsers)
@ -126,6 +127,8 @@ def _set_help_commands_unsafe(root: SubParserAction) -> argparse.ArgumentParser:
""" """
parser = root.add_parser("help-commands-unsafe", help="list unsafe commands", parser = root.add_parser("help-commands-unsafe", help="list unsafe commands",
description="list unsafe commands as defined in default args", formatter_class=_formatter) description="list unsafe commands as defined in default args", formatter_class=_formatter)
parser.add_argument("--command", help="instead of showing commands, just test command line for unsafe subcommand "
"and return 0 in case if command is safe and 1 otherwise")
parser.set_defaults(handler=handlers.UnsafeCommands, architecture=[""], lock=None, no_report=True, quiet=True, parser.set_defaults(handler=handlers.UnsafeCommands, architecture=[""], lock=None, no_report=True, quiet=True,
unsafe=True, parser=_parser) unsafe=True, parser=_parser)
return parser return parser
@ -273,7 +276,7 @@ def _set_patch_list_parser(root: SubParserAction) -> argparse.ArgumentParser:
""" """
parser = root.add_parser("patch-list", help="list patch sets", parser = root.add_parser("patch-list", help="list patch sets",
description="list available patches for the package", formatter_class=_formatter) description="list available patches for the package", formatter_class=_formatter)
parser.add_argument("package", help="package base") parser.add_argument("package", help="package base", nargs="?")
parser.set_defaults(handler=handlers.Patch, action=Action.List, architecture=[""], lock=None, no_report=True) parser.set_defaults(handler=handlers.Patch, action=Action.List, architecture=[""], lock=None, no_report=True)
return parser return parser
@ -318,12 +321,10 @@ def _set_repo_clean_parser(root: SubParserAction) -> argparse.ArgumentParser:
"you should not run this command manually. Also in case if you are going to clear " "you should not run this command manually. Also in case if you are going to clear "
"the chroot directories you will need root privileges.", "the chroot directories you will need root privileges.",
formatter_class=_formatter) formatter_class=_formatter)
parser.add_argument("--build", help="clear directory with package sources", action="store_true")
parser.add_argument("--cache", help="clear directory with package caches", action="store_true") parser.add_argument("--cache", help="clear directory with package caches", action="store_true")
parser.add_argument("--chroot", help="clear build chroot", action="store_true") parser.add_argument("--chroot", help="clear build chroot", action="store_true")
parser.add_argument("--manual", help="clear directory with manually added packages", action="store_true") parser.add_argument("--manual", help="clear manually added packages queue", action="store_true")
parser.add_argument("--packages", help="clear directory with built packages", action="store_true") parser.add_argument("--packages", help="clear directory with built packages", action="store_true")
parser.add_argument("--patches", help="clear directory with patches", action="store_true")
parser.set_defaults(handler=handlers.Clean, quiet=True, unsafe=True) parser.set_defaults(handler=handlers.Clean, quiet=True, unsafe=True)
return parser return parser
@ -487,7 +488,6 @@ def _set_user_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
formatter_class=_formatter) formatter_class=_formatter)
parser.add_argument("username", help="username for web service") parser.add_argument("username", help="username for web service")
parser.add_argument("--as-service", help="add user as service user", action="store_true") parser.add_argument("--as-service", help="add user as service user", action="store_true")
parser.add_argument("--no-reload", help="do not reload authentication module", action="store_true")
parser.add_argument("-p", "--password", help="user password. Blank password will be treated as empty password, " parser.add_argument("-p", "--password", help="user password. Blank password will be treated as empty password, "
"which is in particular must be used for OAuth2 authorization type.") "which is in particular must be used for OAuth2 authorization type.")
parser.add_argument("-r", "--role", help="user access level", parser.add_argument("-r", "--role", help="user access level",
@ -498,6 +498,22 @@ def _set_user_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
return parser return parser
def _set_user_list_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for user list subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("user-list", help="user known users and their access",
description="list users from the user mapping and their roles",
formatter_class=_formatter)
parser.add_argument("username", help="filter users by username", nargs="?")
parser.add_argument("-r", "--role", help="filter users by role", type=UserAccess, choices=UserAccess)
parser.set_defaults(handler=handlers.User, action=Action.List, architecture=[""], lock=None, no_report=True, # nosec
password="", quiet=True, role=UserAccess.Read, unsafe=True)
return parser
def _set_user_remove_parser(root: SubParserAction) -> argparse.ArgumentParser: def _set_user_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
""" """
add parser for user removal subcommand add parser for user removal subcommand
@ -508,7 +524,6 @@ def _set_user_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
description="remove user from the user mapping and update the configuration", description="remove user from the user mapping and update the configuration",
formatter_class=_formatter) formatter_class=_formatter)
parser.add_argument("username", help="username for web service") parser.add_argument("username", help="username for web service")
parser.add_argument("--no-reload", help="do not reload authentication module", action="store_true")
parser.add_argument("-s", "--secure", help="set file permissions to user-only", action="store_true") parser.add_argument("-s", "--secure", help="set file permissions to user-only", action="store_true")
parser.set_defaults(handler=handlers.User, action=Action.Remove, architecture=[""], lock=None, no_report=True, # nosec parser.set_defaults(handler=handlers.User, action=Action.Remove, architecture=[""], lock=None, no_report=True, # nosec
password="", quiet=True, role=UserAccess.Read, unsafe=True) password="", quiet=True, role=UserAccess.Read, unsafe=True)

View File

@ -25,7 +25,7 @@ from typing import Any, Iterable, Set
from ahriman.application.application.properties import Properties from ahriman.application.application.properties import Properties
from ahriman.core.build_tools.sources import Sources from ahriman.core.build_tools.sources import Sources
from ahriman.core.util import package_like from ahriman.core.util import package_like, tmpdir
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource from ahriman.models.package_source import PackageSource
from ahriman.models.result import Result from ahriman.models.result import Result
@ -67,10 +67,11 @@ class Packages(Properties):
:param without_dependencies: if set, dependency check will be disabled :param without_dependencies: if set, dependency check will be disabled
""" """
package = Package.load(source, PackageSource.AUR, self.repository.pacman, self.repository.aur_url) package = Package.load(source, PackageSource.AUR, self.repository.pacman, self.repository.aur_url)
local_path = self.repository.paths.manual_for(package.base) self.repository.database.build_queue_insert(package)
Sources.load(local_path, package.git_url, self.repository.paths.patches_for(package.base)) with tmpdir() as local_path:
self._process_dependencies(local_path, known_packages, without_dependencies) Sources.load(local_path, package.git_url, self.database.patches_get(package.base))
self._process_dependencies(local_path, known_packages, without_dependencies)
def _add_directory(self, source: str, *_: Any) -> None: def _add_directory(self, source: str, *_: Any) -> None:
""" """
@ -92,10 +93,9 @@ class Packages(Properties):
cache_dir = self.repository.paths.cache_for(package.base) cache_dir = self.repository.paths.cache_for(package.base)
shutil.copytree(Path(source), cache_dir) # copy package to store in caches shutil.copytree(Path(source), cache_dir) # copy package to store in caches
Sources.init(cache_dir) # we need to run init command in directory where we do have permissions Sources.init(cache_dir) # we need to run init command in directory where we do have permissions
self.repository.database.build_queue_insert(package)
dst = self.repository.paths.manual_for(package.base) self._process_dependencies(cache_dir, known_packages, without_dependencies)
shutil.copytree(cache_dir, dst) # copy package for the build
self._process_dependencies(dst, known_packages, without_dependencies)
def _add_remote(self, source: str, *_: Any) -> None: def _add_remote(self, source: str, *_: Any) -> None:
""" """

View File

@ -20,6 +20,7 @@
import logging import logging
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
from ahriman.core.repository import Repository from ahriman.core.repository import Repository
@ -28,6 +29,7 @@ class Properties:
application base properties class application base properties class
:ivar architecture: repository architecture :ivar architecture: repository architecture
:ivar configuration: configuration instance :ivar configuration: configuration instance
:ivar database: database instance
:ivar logger: application logger :ivar logger: application logger
:ivar repository: repository instance :ivar repository: repository instance
""" """
@ -43,4 +45,5 @@ class Properties:
self.logger = logging.getLogger("root") self.logger = logging.getLogger("root")
self.configuration = configuration self.configuration = configuration
self.architecture = architecture self.architecture = architecture
self.repository = Repository(architecture, configuration, no_report, unsafe) self.database = SQLite.load(configuration)
self.repository = Repository(architecture, configuration, self.database, no_report, unsafe)

View File

@ -42,28 +42,22 @@ class Repository(Properties):
""" """
raise NotImplementedError raise NotImplementedError
def clean(self, build: bool, cache: bool, chroot: bool, manual: bool, packages: bool, patches: bool) -> None: def clean(self, cache: bool, chroot: bool, manual: bool, packages: bool) -> None:
""" """
run all clean methods. Warning: some functions might not be available under non-root run all clean methods. Warning: some functions might not be available under non-root
:param build: clear directory with package sources
:param cache: clear directory with package caches :param cache: clear directory with package caches
:param chroot: clear build chroot :param chroot: clear build chroot
:param manual: clear directory with manually added packages :param manual: clear directory with manually added packages
:param packages: clear directory with built packages :param packages: clear directory with built packages
:param patches: clear directory with patches
""" """
if build:
self.repository.clear_build()
if cache: if cache:
self.repository.clear_cache() self.repository.clear_cache()
if chroot: if chroot:
self.repository.clear_chroot() self.repository.clear_chroot()
if manual: if manual:
self.repository.clear_manual() self.repository.clear_queue()
if packages: if packages:
self.repository.clear_packages() self.repository.clear_packages()
if patches:
self.repository.clear_patches()
def report(self, target: Iterable[str], result: Result) -> None: def report(self, target: Iterable[str], result: Result) -> None:
""" """
@ -154,7 +148,7 @@ class Repository(Properties):
process_update(packages, Result()) process_update(packages, Result())
# process manual packages # process manual packages
tree = Tree.load(updates, self.repository.paths) tree = Tree.load(updates, self.database)
for num, level in enumerate(tree.levels()): for num, level in enumerate(tree.levels()):
self.logger.info("processing level #%i %s", num, [package.base for package in level]) self.logger.info("processing level #%i %s", num, [package.base for package in level])
build_result = self.repository.process_build(level) build_result = self.repository.process_build(level)

View File

@ -43,4 +43,4 @@ class Clean(Handler):
:param unsafe: if set no user check will be performed before path creation :param unsafe: if set no user check will be performed before path creation
""" """
Application(architecture, configuration, no_report, unsafe).clean( Application(architecture, configuration, no_report, unsafe).clean(
args.build, args.cache, args.chroot, args.manual, args.packages, args.patches) args.cache, args.chroot, args.manual, args.packages)

View File

@ -27,7 +27,7 @@ from typing import List, Type
from ahriman.application.lock import Lock from ahriman.application.lock import Lock
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import MissingArchitecture, MultipleArchitectures from ahriman.core.exceptions import ExitCode, MissingArchitecture, MultipleArchitectures
from ahriman.models.repository_paths import RepositoryPaths from ahriman.models.repository_paths import RepositoryPaths
@ -78,6 +78,8 @@ class Handler:
with Lock(args, architecture, configuration): with Lock(args, architecture, configuration):
cls.run(args, architecture, configuration, args.no_report, args.unsafe) cls.run(args, architecture, configuration, args.no_report, args.unsafe)
return True return True
except ExitCode:
return False
except Exception: except Exception:
# we are basically always want to print error to stderr instead of default logger # we are basically always want to print error to stderr instead of default logger
logging.getLogger("stderr").exception("process exception") logging.getLogger("stderr").exception("process exception")

View File

@ -18,15 +18,15 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
import argparse import argparse
import shutil
from pathlib import Path from pathlib import Path
from typing import List, Type from typing import List, Optional, Type
from ahriman.application.application import Application from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler from ahriman.application.handlers.handler import Handler
from ahriman.core.build_tools.sources import Sources from ahriman.core.build_tools.sources import Sources
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.formatters.string_printer import StringPrinter
from ahriman.models.action import Action from ahriman.models.action import Action
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource from ahriman.models.package_source import PackageSource
@ -37,8 +37,6 @@ class Patch(Handler):
patch control handler patch control handler
""" """
_print = print
@classmethod @classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool, unsafe: bool) -> None: configuration: Configuration, no_report: bool, unsafe: bool) -> None:
@ -69,25 +67,20 @@ class Patch(Handler):
""" """
package = Package.load(sources_dir, PackageSource.Local, application.repository.pacman, package = Package.load(sources_dir, PackageSource.Local, application.repository.pacman,
application.repository.aur_url) application.repository.aur_url)
patch_dir = application.repository.paths.patches_for(package.base) patch = Sources.patch_create(Path(sources_dir), *track)
application.database.patches_insert(package.base, patch)
Patch.patch_set_remove(application, package.base) # remove old patches
patch_dir.mkdir(mode=0o755, parents=True)
Sources.patch_create(Path(sources_dir), patch_dir / "00-main.patch", *track)
@staticmethod @staticmethod
def patch_set_list(application: Application, package_base: str) -> None: def patch_set_list(application: Application, package_base: Optional[str]) -> None:
""" """
list patches available for the package base list patches available for the package base
:param application: application instance :param application: application instance
:param package_base: package base :param package_base: package base
""" """
patch_dir = application.repository.paths.patches_for(package_base) patches = application.database.patches_list(package_base)
if not patch_dir.is_dir(): for base, patch in patches.items():
return content = base if package_base is None else patch
for patch_path in sorted(patch_dir.glob("*.patch")): StringPrinter(content).print(verbose=True)
Patch._print(patch_path.name)
@staticmethod @staticmethod
def patch_set_remove(application: Application, package_base: str) -> None: def patch_set_remove(application: Application, package_base: str) -> None:
@ -96,5 +89,4 @@ class Patch(Handler):
:param application: application instance :param application: application instance
:param package_base: package base :param package_base: package base
""" """
patch_dir = application.repository.paths.patches_for(package_base) application.database.patches_remove(package_base)
shutil.rmtree(patch_dir, ignore_errors=True)

View File

@ -18,11 +18,13 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
import argparse import argparse
import shlex
from typing import List, Type from typing import List, Type
from ahriman.application.handlers.handler import Handler from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import ExitCode
from ahriman.core.formatters.string_printer import StringPrinter from ahriman.core.formatters.string_printer import StringPrinter
@ -44,9 +46,25 @@ class UnsafeCommands(Handler):
:param no_report: force disable reporting :param no_report: force disable reporting
:param unsafe: if set no user check will be performed before path creation :param unsafe: if set no user check will be performed before path creation
""" """
unsafe_commands = UnsafeCommands.get_unsafe_commands(args.parser()) parser = args.parser()
for command in unsafe_commands: unsafe_commands = UnsafeCommands.get_unsafe_commands(parser)
StringPrinter(command).print(verbose=True) if args.command is None:
for command in unsafe_commands:
StringPrinter(command).print(verbose=True)
else:
UnsafeCommands.check_unsafe(args.command, unsafe_commands, parser)
@staticmethod
def check_unsafe(command: str, unsafe_commands: List[str], parser: argparse.ArgumentParser) -> None:
"""
check if command is unsafe
:param command: command to check
:param unsafe_commands: list of unsafe commands
:param parser: generated argument parser
"""
args = parser.parse_args(shlex.split(command))
if args.command in unsafe_commands:
raise ExitCode()
@staticmethod @staticmethod
def get_unsafe_commands(parser: argparse.ArgumentParser) -> List[str]: def get_unsafe_commands(parser: argparse.ArgumentParser) -> List[str]:

View File

@ -23,12 +23,12 @@ import getpass
from pathlib import Path from pathlib import Path
from typing import Type from typing import Type
from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
from ahriman.core.formatters.user_printer import UserPrinter
from ahriman.models.action import Action from ahriman.models.action import Action
from ahriman.models.user import User as MUser from ahriman.models.user import User as MUser
from ahriman.models.user_access import UserAccess
class User(Handler): class User(Handler):
@ -51,33 +51,35 @@ class User(Handler):
""" """
salt = User.get_salt(configuration) salt = User.get_salt(configuration)
user = User.user_create(args) user = User.user_create(args)
auth_configuration = User.configuration_get(configuration.include) auth_configuration = User.configuration_get(configuration.include)
database = SQLite.load(configuration)
User.user_clear(auth_configuration, user) if args.action == Action.List:
if args.action == Action.Update: for found_user in database.user_list(user.username, user.access):
User.configuration_create(auth_configuration, user, salt, args.as_service) UserPrinter(found_user).print(verbose=True)
User.configuration_write(auth_configuration, args.secure) elif args.action == Action.Remove:
database.user_remove(user.username)
if not args.no_reload: elif args.action == Action.Update:
client = Application(architecture, configuration, no_report=False, unsafe=unsafe).repository.reporter User.configuration_create(auth_configuration, user, salt, args.as_service, args.secure)
client.reload_auth() database.user_update(user.hash_password(salt))
@staticmethod @staticmethod
def configuration_create(configuration: Configuration, user: MUser, salt: str, as_service_user: bool) -> None: def configuration_create(configuration: Configuration, user: MUser, salt: str,
as_service_user: bool, secure: bool) -> None:
""" """
put new user to configuration enable configuration if it has been disabled
:param configuration: configuration instance :param configuration: configuration instance
:param user: user descriptor :param user: user descriptor
:param salt: password hash salt :param salt: password hash salt
:param as_service_user: add user as service user, also set password and user to configuration :param as_service_user: add user as service user, also set password and user to configuration
:param secure: if true then set file permissions to 0o600
""" """
section = Configuration.section_name("auth", user.access.value)
configuration.set_option("auth", "salt", salt) configuration.set_option("auth", "salt", salt)
configuration.set_option(section, user.username, user.hash_password(salt))
if as_service_user: if as_service_user:
configuration.set_option("web", "username", user.username) configuration.set_option("web", "username", user.username)
configuration.set_option("web", "password", user.password) configuration.set_option("web", "password", user.password)
User.configuration_write(configuration, secure)
@staticmethod @staticmethod
def configuration_get(include_path: Path) -> Configuration: def configuration_get(include_path: Path) -> Configuration:
@ -90,6 +92,8 @@ class User(Handler):
configuration = Configuration() configuration = Configuration()
configuration.load(target) configuration.load(target)
configuration.architecture = "" # not user anyway
return configuration return configuration
@staticmethod @staticmethod
@ -99,12 +103,11 @@ class User(Handler):
:param configuration: configuration instance :param configuration: configuration instance
:param secure: if true then set file permissions to 0o600 :param secure: if true then set file permissions to 0o600
""" """
if configuration.path is None: path, _ = configuration.check_loaded()
return # should never happen actually with path.open("w") as ahriman_configuration:
with configuration.path.open("w") as ahriman_configuration:
configuration.write(ahriman_configuration) configuration.write(ahriman_configuration)
if secure: if secure:
configuration.path.chmod(0o600) path.chmod(0o600)
@staticmethod @staticmethod
def get_salt(configuration: Configuration, salt_length: int = 20) -> str: def get_salt(configuration: Configuration, salt_length: int = 20) -> str:
@ -118,19 +121,6 @@ class User(Handler):
return salt return salt
return MUser.generate_password(salt_length) return MUser.generate_password(salt_length)
@staticmethod
def user_clear(configuration: Configuration, user: MUser) -> None:
"""
remove user user from configuration file in case if it exists
:param configuration: configuration instance
:param user: user descriptor
"""
for role in UserAccess:
section = Configuration.section_name("auth", role.value)
if not configuration.has_option(section, user.username):
continue
configuration.remove_option(section, user.username)
@staticmethod @staticmethod
def user_create(args: argparse.Namespace) -> MUser: def user_create(args: argparse.Namespace) -> MUser:
""" """

View File

@ -32,7 +32,6 @@ from ahriman.core.exceptions import DuplicateRun
from ahriman.core.status.client import Client from ahriman.core.status.client import Client
from ahriman.core.util import check_user from ahriman.core.util import check_user
from ahriman.models.build_status import BuildStatusEnum from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.repository_paths import RepositoryPaths
class Lock: class Lock:
@ -56,7 +55,7 @@ class Lock:
self.force = args.force self.force = args.force
self.unsafe = args.unsafe self.unsafe = args.unsafe
self.paths = RepositoryPaths(configuration.getpath("repository", "root"), architecture) self.paths = configuration.repository_paths
self.reporter = Client() if args.no_report else Client.load(configuration) self.reporter = Client() if args.no_report else Client.load(configuration)
def __enter__(self) -> Lock: def __enter__(self) -> Lock:

View File

@ -21,12 +21,11 @@ from __future__ import annotations
import logging import logging
from typing import Dict, Optional, Type from typing import Optional, Type
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import DuplicateUser from ahriman.core.database.sqlite import SQLite
from ahriman.models.auth_settings import AuthSettings from ahriman.models.auth_settings import AuthSettings
from ahriman.models.user import User
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
@ -63,40 +62,22 @@ class Auth:
return """<button type="button" class="btn btn-link" data-bs-toggle="modal" data-bs-target="#loginForm" style="text-decoration: none">login</button>""" return """<button type="button" class="btn btn-link" data-bs-toggle="modal" data-bs-target="#loginForm" style="text-decoration: none">login</button>"""
@classmethod @classmethod
def load(cls: Type[Auth], configuration: Configuration) -> Auth: def load(cls: Type[Auth], configuration: Configuration, database: SQLite) -> Auth:
""" """
load authorization module from settings load authorization module from settings
:param configuration: configuration instance :param configuration: configuration instance
:param database: database instance
:return: authorization module according to current settings :return: authorization module according to current settings
""" """
provider = AuthSettings.from_option(configuration.get("auth", "target", fallback="disabled")) provider = AuthSettings.from_option(configuration.get("auth", "target", fallback="disabled"))
if provider == AuthSettings.Configuration: if provider == AuthSettings.Configuration:
from ahriman.core.auth.mapping import Mapping from ahriman.core.auth.mapping import Mapping
return Mapping(configuration) return Mapping(configuration, database)
if provider == AuthSettings.OAuth: if provider == AuthSettings.OAuth:
from ahriman.core.auth.oauth import OAuth from ahriman.core.auth.oauth import OAuth
return OAuth(configuration) return OAuth(configuration, database)
return cls(configuration) return cls(configuration)
@staticmethod
def get_users(configuration: Configuration) -> Dict[str, User]:
"""
load users from settings
:param configuration: configuration instance
:return: map of username to its descriptor
"""
users: Dict[str, User] = {}
for role in UserAccess:
section = configuration.section_name("auth", role.value)
if not configuration.has_section(section):
continue
for user, password in configuration[section].items():
normalized_user = user.lower()
if normalized_user in users:
raise DuplicateUser(normalized_user)
users[normalized_user] = User(normalized_user, password, role)
return users
async def check_credentials(self, username: Optional[str], password: Optional[str]) -> bool: # pylint: disable=no-self-use async def check_credentials(self, username: Optional[str], password: Optional[str]) -> bool: # pylint: disable=no-self-use
""" """
validate user password validate user password

View File

@ -20,7 +20,9 @@
from typing import Optional from typing import Optional
from ahriman.core.auth.auth import Auth from ahriman.core.auth.auth import Auth
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
from ahriman.models.auth_settings import AuthSettings from ahriman.models.auth_settings import AuthSettings
from ahriman.models.user import User from ahriman.models.user import User
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
@ -30,18 +32,20 @@ class Mapping(Auth):
""" """
user authorization based on mapping from configuration file user authorization based on mapping from configuration file
:ivar salt: random generated string to salt passwords :ivar salt: random generated string to salt passwords
:ivar _users: map of username to its descriptor :ivar database: database instance
""" """
def __init__(self, configuration: Configuration, provider: AuthSettings = AuthSettings.Configuration) -> None: def __init__(self, configuration: Configuration, database: SQLite,
provider: AuthSettings = AuthSettings.Configuration) -> None:
""" """
default constructor default constructor
:param configuration: configuration instance :param configuration: configuration instance
:param database: database instance
:param provider: authorization type definition :param provider: authorization type definition
""" """
Auth.__init__(self, configuration, provider) Auth.__init__(self, configuration, provider)
self.database = database
self.salt = configuration.get("auth", "salt") self.salt = configuration.get("auth", "salt")
self._users = self.get_users(configuration)
async def check_credentials(self, username: Optional[str], password: Optional[str]) -> bool: async def check_credentials(self, username: Optional[str], password: Optional[str]) -> bool:
""" """
@ -61,8 +65,7 @@ class Mapping(Auth):
:param username: username :param username: username
:return: user descriptor if username is known and None otherwise :return: user descriptor if username is known and None otherwise
""" """
normalized_user = username.lower() return self.database.user_get(username)
return self._users.get(normalized_user)
async def known_username(self, username: Optional[str]) -> bool: async def known_username(self, username: Optional[str]) -> bool:
""" """

View File

@ -23,6 +23,7 @@ from typing import Optional, Type
from ahriman.core.auth.mapping import Mapping from ahriman.core.auth.mapping import Mapping
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
from ahriman.core.exceptions import InvalidOption from ahriman.core.exceptions import InvalidOption
from ahriman.models.auth_settings import AuthSettings from ahriman.models.auth_settings import AuthSettings
@ -38,13 +39,15 @@ class OAuth(Mapping):
:ivar scopes: list of scopes required by the application :ivar scopes: list of scopes required by the application
""" """
def __init__(self, configuration: Configuration, provider: AuthSettings = AuthSettings.OAuth) -> None: def __init__(self, configuration: Configuration, database: SQLite,
provider: AuthSettings = AuthSettings.OAuth) -> None:
""" """
default constructor default constructor
:param configuration: configuration instance :param configuration: configuration instance
:param database: database instance
:param provider: authorization type definition :param provider: authorization type definition
""" """
Mapping.__init__(self, configuration, provider) Mapping.__init__(self, configuration, database, provider)
self.client_id = configuration.get("auth", "client_id") self.client_id = configuration.get("auth", "client_id")
self.client_secret = configuration.get("auth", "client_secret") self.client_secret = configuration.get("auth", "client_secret")
# in order to use OAuth feature the service must be publicity available # in order to use OAuth feature the service must be publicity available

View File

@ -47,6 +47,8 @@ class Sources:
found_files: List[Path] = [] found_files: List[Path] = []
for glob in pattern: for glob in pattern:
found_files.extend(sources_dir.glob(glob)) found_files.extend(sources_dir.glob(glob))
if not found_files:
return # no additional files found
Sources.logger.info("found matching files %s", found_files) Sources.logger.info("found matching files %s", found_files)
# add them to index # add them to index
Sources._check_output("git", "add", "--intent-to-add", Sources._check_output("git", "add", "--intent-to-add",
@ -54,14 +56,13 @@ class Sources:
exception=None, cwd=sources_dir, logger=Sources.logger) exception=None, cwd=sources_dir, logger=Sources.logger)
@staticmethod @staticmethod
def diff(sources_dir: Path, patch_path: Path) -> None: def diff(sources_dir: Path) -> str:
""" """
generate diff from the current version and write it to the output file generate diff from the current version and write it to the output file
:param sources_dir: local path to git repository :param sources_dir: local path to git repository
:param patch_path: path to result patch :return: patch as plain string
""" """
patch = Sources._check_output("git", "diff", exception=None, cwd=sources_dir, logger=Sources.logger) return Sources._check_output("git", "diff", exception=None, cwd=sources_dir, logger=Sources.logger)
patch_path.write_text(patch)
@staticmethod @staticmethod
def fetch(sources_dir: Path, remote: Optional[str]) -> None: def fetch(sources_dir: Path, remote: Optional[str]) -> None:
@ -112,41 +113,39 @@ class Sources:
exception=None, cwd=sources_dir, logger=Sources.logger) exception=None, cwd=sources_dir, logger=Sources.logger)
@staticmethod @staticmethod
def load(sources_dir: Path, remote: str, patch_dir: Path) -> None: def load(sources_dir: Path, remote: str, patch: Optional[str]) -> None:
""" """
fetch sources from remote and apply patches fetch sources from remote and apply patches
:param sources_dir: local path to fetch :param sources_dir: local path to fetch
:param remote: remote target (from where to fetch) :param remote: remote target (from where to fetch)
:param patch_dir: path to directory with package patches :param patch: optional patch to be applied
""" """
Sources.fetch(sources_dir, remote) Sources.fetch(sources_dir, remote)
Sources.patch_apply(sources_dir, patch_dir) if patch is None:
Sources.logger.info("no patches found")
return
Sources.patch_apply(sources_dir, patch)
@staticmethod @staticmethod
def patch_apply(sources_dir: Path, patch_dir: Path) -> None: def patch_apply(sources_dir: Path, patch: str) -> None:
""" """
apply patches if any apply patches if any
:param sources_dir: local path to directory with git sources :param sources_dir: local path to directory with git sources
:param patch_dir: path to directory with package patches :param patch: patch to be applied
""" """
# check if even there are patches # create patch
if not patch_dir.is_dir(): Sources.logger.info("apply patch from database")
return # no patches provided Sources._check_output("git", "apply", "--ignore-space-change", "--ignore-whitespace",
# find everything that looks like patch and sort it exception=None, cwd=sources_dir, input_data=patch, logger=Sources.logger)
patches = sorted(patch_dir.glob("*.patch"))
Sources.logger.info("found %s patches", patches)
for patch in patches:
Sources.logger.info("apply patch %s", patch.name)
Sources._check_output("git", "apply", "--ignore-space-change", "--ignore-whitespace", str(patch),
exception=None, cwd=sources_dir, logger=Sources.logger)
@staticmethod @staticmethod
def patch_create(sources_dir: Path, patch_path: Path, *pattern: str) -> None: def patch_create(sources_dir: Path, *pattern: str) -> str:
""" """
create patch set for the specified local path create patch set for the specified local path
:param sources_dir: local path to git repository :param sources_dir: local path to git repository
:param patch_path: path to result patch
:param pattern: glob patterns :param pattern: glob patterns
:return: patch as plain text
""" """
Sources.add(sources_dir, *pattern) Sources.add(sources_dir, *pattern)
Sources.diff(sources_dir, patch_path) diff = Sources.diff(sources_dir)
return f"{diff}\n" # otherwise, patch will be broken

View File

@ -21,10 +21,11 @@ import logging
import shutil import shutil
from pathlib import Path from pathlib import Path
from typing import List, Optional from typing import List
from ahriman.core.build_tools.sources import Sources from ahriman.core.build_tools.sources import Sources
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
from ahriman.core.exceptions import BuildFailed from ahriman.core.exceptions import BuildFailed
from ahriman.core.util import check_output from ahriman.core.util import check_output
from ahriman.models.package import Package from ahriman.models.package import Package
@ -61,9 +62,10 @@ class Task:
self.makepkg_flags = configuration.getlist("build", "makepkg_flags", fallback=[]) self.makepkg_flags = configuration.getlist("build", "makepkg_flags", fallback=[])
self.makechrootpkg_flags = configuration.getlist("build", "makechrootpkg_flags", fallback=[]) self.makechrootpkg_flags = configuration.getlist("build", "makechrootpkg_flags", fallback=[])
def build(self) -> List[Path]: def build(self, sources_path: Path) -> List[Path]:
""" """
run package build run package build
:param sources_path: path to where sources are
:return: paths of produced packages :return: paths of produced packages
""" """
command = [self.build_command, "-r", str(self.paths.chroot)] command = [self.build_command, "-r", str(self.paths.chroot)]
@ -75,24 +77,24 @@ class Task:
Task._check_output( Task._check_output(
*command, *command,
exception=BuildFailed(self.package.base), exception=BuildFailed(self.package.base),
cwd=self.paths.sources_for(self.package.base), cwd=sources_path,
logger=self.build_logger, logger=self.build_logger,
user=self.uid) user=self.uid)
# well it is not actually correct, but we can deal with it # well it is not actually correct, but we can deal with it
packages = Task._check_output("makepkg", "--packagelist", packages = Task._check_output("makepkg", "--packagelist",
exception=BuildFailed(self.package.base), exception=BuildFailed(self.package.base),
cwd=self.paths.sources_for(self.package.base), cwd=sources_path,
logger=self.build_logger).splitlines() logger=self.build_logger).splitlines()
return [Path(package) for package in packages] return [Path(package) for package in packages]
def init(self, path: Optional[Path] = None) -> None: def init(self, path: Path, database: SQLite) -> None:
""" """
fetch package from git fetch package from git
:param path: optional local path to fetch. If not set default path will be used :param path: local path to fetch
:param database: database instance
""" """
git_path = path or self.paths.sources_for(self.package.base)
if self.paths.cache_for(self.package.base).is_dir(): if self.paths.cache_for(self.package.base).is_dir():
# no need to clone whole repository, just copy from cache first # no need to clone whole repository, just copy from cache first
shutil.copytree(self.paths.cache_for(self.package.base), git_path) shutil.copytree(self.paths.cache_for(self.package.base), path, dirs_exist_ok=True)
Sources.load(git_path, self.package.git_url, self.paths.patches_for(self.package.base)) Sources.load(path, self.package.git_url, database.patches_get(self.package.base))

View File

@ -28,6 +28,7 @@ from pathlib import Path
from typing import Any, Dict, Generator, List, Optional, Tuple, Type from typing import Any, Dict, Generator, List, Optional, Tuple, Type
from ahriman.core.exceptions import InitializeException from ahriman.core.exceptions import InitializeException
from ahriman.models.repository_paths import RepositoryPaths
class Configuration(configparser.RawConfigParser): class Configuration(configparser.RawConfigParser):
@ -72,6 +73,14 @@ class Configuration(configparser.RawConfigParser):
""" """
return self.getpath("settings", "logging") return self.getpath("settings", "logging")
@property
def repository_paths(self) -> RepositoryPaths:
"""
:return: repository paths instance
"""
_, architecture = self.check_loaded()
return RepositoryPaths(self.getpath("repository", "root"), architecture)
@classmethod @classmethod
def from_path(cls: Type[Configuration], path: Path, architecture: str, quiet: bool) -> Configuration: def from_path(cls: Type[Configuration], path: Path, architecture: str, quiet: bool) -> Configuration:
""" """
@ -134,6 +143,15 @@ class Configuration(configparser.RawConfigParser):
return path return path
return self.path.parent / path return self.path.parent / path
def check_loaded(self) -> Tuple[Path, str]:
"""
check if service was actually loaded
:return: configuration root path and architecture if loaded
"""
if self.path is None or self.architecture is None:
raise InitializeException("Configuration path and/or architecture are not set")
return self.path, self.architecture
def dump(self) -> Dict[str, Dict[str, str]]: def dump(self) -> Dict[str, Dict[str, str]]:
""" """
dump configuration to dictionary dump configuration to dictionary
@ -233,12 +251,11 @@ class Configuration(configparser.RawConfigParser):
""" """
reload configuration if possible or raise exception otherwise reload configuration if possible or raise exception otherwise
""" """
if self.path is None or self.architecture is None: path, architecture = self.check_loaded()
raise InitializeException("Configuration path and/or architecture are not set")
for section in self.sections(): # clear current content for section in self.sections(): # clear current content
self.remove_section(section) self.remove_section(section)
self.load(self.path) self.load(path)
self.merge_sections(self.architecture) self.merge_sections(architecture)
def set_option(self, section: str, option: str, value: Optional[str]) -> None: def set_option(self, section: str, option: str, value: Optional[str]) -> None:
""" """

View File

@ -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 <http://www.gnu.org/licenses/>.
#

View File

@ -0,0 +1,43 @@
#
# 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 <http://www.gnu.org/licenses/>.
#
from sqlite3 import Connection
from ahriman.core.configuration import Configuration
from ahriman.core.database.data.patches import migrate_patches
from ahriman.core.database.data.users import migrate_users_data
from ahriman.core.database.data.package_statuses import migrate_package_statuses
from ahriman.models.migration_result import MigrationResult
from ahriman.models.repository_paths import RepositoryPaths
def migrate_data(result: MigrationResult, connection: Connection,
configuration: Configuration, paths: RepositoryPaths) -> None:
"""
perform data migration
:param result: result of the schema migration
:param connection: database connection
:param configuration: configuration instance
:param paths: repository paths instance
"""
# initial data migration
if result.old_version == 0:
migrate_package_statuses(connection, paths)
migrate_users_data(connection, configuration)
migrate_patches(connection, paths)

View File

@ -0,0 +1,80 @@
#
# 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 <http://www.gnu.org/licenses/>.
#
import json
from sqlite3 import Connection
from ahriman.models.build_status import BuildStatus
from ahriman.models.package import Package
from ahriman.models.repository_paths import RepositoryPaths
def migrate_package_statuses(connection: Connection, paths: RepositoryPaths) -> None:
"""
perform migration for package statuses
:param connection: database connection
:param paths: repository paths instance
"""
def insert_base(metadata: Package, last_status: BuildStatus) -> None:
connection.execute(
"""
insert into package_bases
(package_base, version, aur_url)
values
(:package_base, :version, :aur_url)
""",
dict(package_base=metadata.base, version=metadata.version, aur_url=metadata.aur_url))
connection.execute(
"""
insert into package_statuses
(package_base, status, last_updated)
values
(:package_base, :status, :last_updated)""",
dict(package_base=metadata.base, status=last_status.status.value, last_updated=last_status.timestamp))
def insert_packages(metadata: Package) -> None:
package_list = []
for name, description in metadata.packages.items():
package_list.append(dict(package=name, package_base=metadata.base, **description.view()))
connection.executemany(
"""
insert into packages
(package, package_base, architecture, archive_size, build_date, depends, description,
filename, "groups", installed_size, licenses, provides, url)
values
(:package, :package_base, :architecture, :archive_size, :build_date, :depends, :description,
:filename, :groups, :installed_size, :licenses, :provides, :url)
""",
package_list)
cache_path = paths.root / "status_cache.json"
if not cache_path.is_file():
return # no file found
with cache_path.open() as cache:
dump = json.load(cache)
for item in dump.get("packages", []):
package = Package.from_json(item["package"])
status = BuildStatus.from_json(item["status"])
insert_base(package, status)
insert_packages(package)
connection.commit()
cache_path.unlink()

View File

@ -0,0 +1,44 @@
#
# 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 <http://www.gnu.org/licenses/>.
#
from sqlite3 import Connection
from ahriman.models.repository_paths import RepositoryPaths
def migrate_patches(connection: Connection, paths: RepositoryPaths) -> None:
"""
perform migration for patches
:param connection: database connection
:param paths: repository paths instance
"""
root = paths.root / "patches"
if not root.is_dir():
return # no directory found
for package in root.iterdir():
patch_path = package / "00-main.patch"
if not patch_path.is_file():
continue # not exist
content = patch_path.read_text(encoding="utf8")
connection.execute(
"""insert into patches (package_base, patch) values (:package_base, :patch)""",
{"package_base": package.name, "patch": content})
connection.commit()

View File

@ -0,0 +1,40 @@
#
# 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 <http://www.gnu.org/licenses/>.
#
from sqlite3 import Connection
from ahriman.core.configuration import Configuration
def migrate_users_data(connection: Connection, configuration: Configuration) -> None:
"""
perform migration for users
:param connection: database connection
:param configuration: configuration instance
"""
for section in configuration.sections():
for option, value in configuration[section].items():
if not section.startswith("auth:"):
continue
permission = section[5:]
connection.execute(
"""insert into users (username, permission, password) values (:username, :permission, :password)""",
{"username": option.lower(), "permission": permission, "password": value})
connection.commit()

View File

@ -0,0 +1,125 @@
#
# 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 <http://www.gnu.org/licenses/>.
#
from __future__ import annotations
import logging
from importlib import import_module
from pathlib import Path
from pkgutil import iter_modules
from sqlite3 import Connection
from typing import List, Type
from ahriman.models.migration import Migration
from ahriman.models.migration_result import MigrationResult
class Migrations:
"""
simple migration wrapper for the sqlite
idea comes from https://www.ash.dev/blog/simple-migration-system-in-sqlite/
:ivar connection: database connection
:ivar logger: class logger
"""
def __init__(self, connection: Connection) -> None:
"""
default constructor
:param connection: database connection
"""
self.connection = connection
self.logger = logging.getLogger("database")
@classmethod
def migrate(cls: Type[Migrations], connection: Connection) -> MigrationResult:
"""
perform migrations implicitly
:param connection: database connection
:return: current schema version
"""
return cls(connection).run()
def migrations(self) -> List[Migration]:
"""
extract all migrations from the current package
idea comes from https://julienharbulot.com/python-dynamical-import.html
"""
migrations: List[Migration] = []
package_dir = Path(__file__).resolve().parent
modules = [module_name for (_, module_name, _) in iter_modules([str(package_dir)])]
for index, module_name in enumerate(sorted(modules)):
module = import_module(f"{__name__}.{module_name}")
steps: List[str] = getattr(module, "steps", [])
self.logger.debug("found migration %s at index %s with steps count %s", module_name, index, len(steps))
migrations.append(Migration(index, module_name, steps))
return migrations
def run(self) -> MigrationResult:
"""
perform migrations
:return: current schema version
"""
migrations = self.migrations()
current_version = self.user_version()
expected_version = len(migrations)
result = MigrationResult(current_version, expected_version)
if not result.is_outdated:
self.logger.info("no migrations required")
return result
previous_isolation = self.connection.isolation_level
try:
self.connection.isolation_level = None
cursor = self.connection.cursor()
try:
cursor.execute("begin exclusive")
for migration in migrations[current_version:]:
self.logger.info("applying migration %s at index %s", migration.name, migration.index)
for statement in migration.steps:
cursor.execute(statement)
self.logger.info("migration %s at index %s has been applied", migration.name, migration.index)
cursor.execute(f"pragma user_version = {expected_version}") # no support for ? placeholders
except Exception:
self.logger.exception("migration failed with exception")
cursor.execute("rollback")
raise
else:
cursor.execute("commit")
finally:
cursor.close()
finally:
self.connection.isolation_level = previous_isolation
self.logger.info("migrations have been performed from version %s to %s", result.old_version, result.new_version)
return result
def user_version(self) -> int:
"""
get schema version from sqlite database
;return: current schema version
"""
cursor = self.connection.execute("pragma user_version")
current_version: int = cursor.fetchone()["user_version"]
return current_version

View File

@ -0,0 +1,73 @@
#
# 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 <http://www.gnu.org/licenses/>.
#
steps = [
"""
create table build_queue (
package_base text not null unique,
properties json not null
)
""",
"""
create table package_bases (
package_base text not null unique,
version text not null,
aur_url text not null
)
""",
"""
create table package_statuses (
package_base text not null unique,
status text not null,
last_updated integer
)
""",
"""
create table packages (
package text not null,
package_base text not null,
architecture text,
archive_size integer,
build_date integer,
depends json,
description text,
filename text,
"groups" json,
installed_size integer,
licenses json,
provides json,
url text,
unique (package, architecture)
)
""",
"""
create table patches (
package_base text not null unique,
patch blob not null
)
""",
"""
create table users (
username text not null unique,
access text not null,
password text
)
""",
]

View File

@ -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 <http://www.gnu.org/licenses/>.
#

View File

@ -0,0 +1,93 @@
#
# 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 <http://www.gnu.org/licenses/>.
#
from __future__ import annotations
from sqlite3 import Connection
from typing import List, Optional
from ahriman.core.database.operations.operations import Operations
from ahriman.models.user import User
from ahriman.models.user_access import UserAccess
class AuthOperations(Operations):
"""
authorization operations
"""
def user_get(self, username: str) -> Optional[User]:
"""
get user by username
:param username: username
:return: user if it was found
"""
return next(iter(self.user_list(username, None)), None)
def user_list(self, username: Optional[str], access: Optional[UserAccess]) -> List[User]:
"""
get users by filter
:param username: optional filter by username
:param access: optional filter by role
:return: list of users who match criteria
"""
username_filter = username.lower() if username is not None else username
access_filter = access.value if access is not None else access
def run(connection: Connection) -> List[User]:
return [
User(cursor["username"], cursor["password"], UserAccess(cursor["access"]))
for cursor in connection.execute(
"""
select * from users
where (:username is null or username = :username) and (:access is null or access = :access)
""",
{"username": username_filter, "access": access_filter})
]
return self.with_connection(run)
def user_remove(self, username: str) -> None:
"""
remove user from storage
:param username: username
"""
def run(connection: Connection) -> None:
connection.execute("""delete from users where username = :username""", {"username": username.lower()})
return self.with_connection(run, commit=True)
def user_update(self, user: User) -> None:
"""
get user by username
:param user: user descriptor
"""
def run(connection: Connection) -> None:
connection.execute(
"""
insert into users
(username, access, password)
values
(:username, :access, :password)
on conflict (username) do update set
access = :access, password = :password
""",
{"username": user.username.lower(), "access": user.access.value, "password": user.password})
self.with_connection(run, commit=True)

View File

@ -0,0 +1,77 @@
#
# 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 <http://www.gnu.org/licenses/>.
#
from sqlite3 import Connection
from typing import List, Optional
from ahriman.core.database.operations.operations import Operations
from ahriman.models.package import Package
class BuildOperations(Operations):
"""
operations for main functions
"""
def build_queue_clear(self, package_base: Optional[str]) -> None:
"""
remove packages from build queue
:param package_base: optional filter by package base
"""
def run(connection: Connection) -> None:
connection.execute(
"""
delete from build_queue
where :package_base is null or package_base = :package_base
""",
{"package_base": package_base})
return self.with_connection(run, commit=True)
def build_queue_get(self) -> List[Package]:
"""
retrieve packages from build queue
:return: list of packages to be built
"""
def run(connection: Connection) -> List[Package]:
return [
Package.from_json(row["properties"])
for row in connection.execute("""select * from build_queue""")
]
return self.with_connection(run)
def build_queue_insert(self, package: Package) -> None:
"""
insert packages to build queue
:param package: package to be inserted
"""
def run(connection: Connection) -> None:
connection.execute(
"""
insert into build_queue
(package_base, properties)
values
(:package_base, :properties)
on conflict (package_base) do update set
properties = :properties
""",
{"package_base": package.base, "properties": package.view()})
return self.with_connection(run, commit=True)

View File

@ -0,0 +1,71 @@
#
# 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 <http://www.gnu.org/licenses/>.
#
import logging
import sqlite3
from pathlib import Path
from sqlite3 import Connection, Cursor
from typing import Any, Dict, Tuple, TypeVar, Callable
T = TypeVar("T")
class Operations:
"""
base operation class
:ivar logger: class logger
:ivar path: path to the database file
"""
def __init__(self, path: Path) -> None:
"""
default constructor
:param path: path to the database file
"""
self.path = path
self.logger = logging.getLogger("database")
@staticmethod
def factory(cursor: Cursor, row: Tuple[Any, ...]) -> Dict[str, Any]:
"""
dictionary factory based on official documentation
:param cursor: cursor descriptor
:param row: fetched row
:return: row converted to dictionary
"""
result = {}
for index, column in enumerate(cursor.description):
result[column[0]] = row[index]
return result
def with_connection(self, query: Callable[[Connection], T], commit: bool = False) -> T:
"""
perform operation in connection
:param query: function to be called with connection
:param commit: if True commit() will be called on success
:return: result of the `query` call
"""
with sqlite3.connect(self.path, detect_types=sqlite3.PARSE_DECLTYPES) as connection:
connection.row_factory = self.factory
result = query(connection)
if commit:
connection.commit()
return result

View File

@ -0,0 +1,199 @@
#
# 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 <http://www.gnu.org/licenses/>.
#
from sqlite3 import Connection
from typing import Dict, Generator, Iterable, List, Tuple
from ahriman.core.database.operations.operations import Operations
from ahriman.models.build_status import BuildStatus
from ahriman.models.package import Package
from ahriman.models.package_description import PackageDescription
class PackageOperations(Operations):
"""
package operations
"""
@staticmethod
def _package_remove_package_base(connection: Connection, package_base: str) -> None:
"""
remove package base information
:param connection: database connection
:param package_base: package base name
"""
connection.execute("""delete from package_statuses where package_base = :package_base""",
{"package_base": package_base})
connection.execute("""delete from package_bases where package_base = :package_base""",
{"package_base": package_base})
@staticmethod
def _package_remove_packages(connection: Connection, package_base: str, current_packages: Iterable[str]) -> None:
"""
remove packages belong to the package base
:param connection: database connection
:param package_base: package base name
:param current_packages: current packages list which has to be left in database
"""
packages = [
package
for package in connection.execute(
"""select package from packages where package_base = :package_base""", {"package_base": package_base})
if package["package"] not in current_packages
]
connection.executemany("""delete from packages where package = :package""", packages)
@staticmethod
def _package_update_insert_base(connection: Connection, package: Package) -> None:
"""
insert base package into table
:param connection: database connection
:param package: package properties
"""
connection.execute(
"""
insert into package_bases
(package_base, version, aur_url)
values
(:package_base, :version, :aur_url)
on conflict (package_base) do update set
version = :version, aur_url = :aur_url
""",
dict(package_base=package.base, version=package.version, aur_url=package.aur_url))
@staticmethod
def _package_update_insert_packages(connection: Connection, package: Package) -> None:
"""
insert packages into table
:param connection: database connection
:param package: package properties
"""
package_list = []
for name, description in package.packages.items():
package_list.append(dict(package=name, package_base=package.base, **description.view()))
connection.executemany(
"""
insert into packages
(package, package_base, architecture, archive_size,
build_date, depends, description, filename,
"groups", installed_size, licenses, provides, url)
values
(:package, :package_base, :architecture, :archive_size,
:build_date, :depends, :description, :filename,
:groups, :installed_size, :licenses, :provides, :url)
on conflict (package, architecture) do update set
package_base = :package_base, archive_size = :archive_size,
build_date = :build_date, depends = :depends, description = :description, filename = :filename,
"groups" = :groups, installed_size = :installed_size, licenses = :licenses, provides = :provides, url = :url
""",
package_list)
@staticmethod
def _package_update_insert_status(connection: Connection, package_base: str, status: BuildStatus) -> None:
"""
insert base package status into table
:param connection: database connection
:param package_base: package base name
:param status: new build status
"""
connection.execute(
"""
insert into package_statuses (package_base, status, last_updated)
values
(:package_base, :status, :last_updated)
on conflict (package_base) do update set
status = :status, last_updated = :last_updated
""",
dict(package_base=package_base, status=status.status.value, last_updated=status.timestamp))
@staticmethod
def _packages_get_select_package_bases(connection: Connection) -> Dict[str, Package]:
"""
select package bases from the table
:param connection: database connection
:return: map of the package base to its descriptor (without packages themselves)
"""
return {
row["package_base"]: Package(row["package_base"], row["version"], row["aur_url"], {})
for row in connection.execute("""select * from package_bases""")
}
@staticmethod
def _packages_get_select_packages(connection: Connection, packages: Dict[str, Package]) -> Dict[str, Package]:
"""
select packages from the table
:param connection: database connection
:param packages: packages descriptor map
:return: map of the package base to its descriptor including individual packages
"""
for row in connection.execute("""select * from packages"""):
if row["package_base"] not in packages:
continue # normally must never happen though
packages[row["package_base"]].packages[row["package"]] = PackageDescription.from_json(row)
return packages
@staticmethod
def _packages_get_select_statuses(connection: Connection) -> Dict[str, BuildStatus]:
"""
select package build statuses from the table
:param connection: database connection
:return: map of the package base to its status
"""
return {
row["package_base"]: BuildStatus.from_json({"status": row["status"], "timestamp": row["last_updated"]})
for row in connection.execute("""select * from package_statuses""")
}
def package_remove(self, package_base: str) -> None:
"""
remove package from database
:param package_base: package base name
"""
def run(connection: Connection) -> None:
self._package_remove_packages(connection, package_base, [])
self._package_remove_package_base(connection, package_base)
return self.with_connection(run, commit=True)
def package_update(self, package: Package, status: BuildStatus) -> None:
"""
update package status
:param package: package properties
:param status: new build status
"""
def run(connection: Connection) -> None:
self._package_update_insert_base(connection, package)
self._package_update_insert_status(connection, package.base, status)
self._package_update_insert_packages(connection, package)
self._package_remove_packages(connection, package.base, package.packages.keys())
return self.with_connection(run, commit=True)
def packages_get(self) -> List[Tuple[Package, BuildStatus]]:
"""
get package list and their build statuses from database
:return: list of package properties and their statuses
"""
def run(connection: Connection) -> Generator[Tuple[Package, BuildStatus], None, None]:
packages = self._packages_get_select_package_bases(connection)
statuses = self._packages_get_select_statuses(connection)
for package_base, package in self._packages_get_select_packages(connection, packages).items():
yield package, statuses.get(package_base, BuildStatus())
return self.with_connection(lambda connection: list(run(connection)))

View File

@ -0,0 +1,85 @@
#
# 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 <http://www.gnu.org/licenses/>.
#
from sqlite3 import Connection
from typing import Dict, Optional
from ahriman.core.database.operations.operations import Operations
class PatchOperations(Operations):
"""
operations for patches
"""
def patches_get(self, package_base: str) -> Optional[str]:
"""
retrieve patches for the package
:param package_base: package base to search for patches
:return: plain text patch for the package
"""
return self.patches_list(package_base).get(package_base)
def patches_insert(self, package_base: str, patch: str) -> None:
"""
insert or update patch in database
:param package_base: package base to insert
:param patch: patch content
"""
def run(connection: Connection) -> None:
connection.execute(
"""
insert into patches
(package_base, patch)
values
(:package_base, :patch)
on conflict (package_base) do update set
patch = :patch
""",
{"package_base": package_base, "patch": patch})
return self.with_connection(run, commit=True)
def patches_list(self, package_base: Optional[str]) -> Dict[str, str]:
"""
extract all patches
:param package_base: optional filter by package base
:return: map of package base to patch content
"""
def run(connection: Connection) -> Dict[str, str]:
return {
cursor["package_base"]: cursor["patch"]
for cursor in connection.execute(
"""select * from patches where :package_base is null or package_base = :package_base""",
{"package_base": package_base})
}
return self.with_connection(run)
def patches_remove(self, package_base: str) -> None:
"""
remove patch set
:param package_base: package base to clear patches
"""
def run(connection: Connection) -> None:
connection.execute(
"""delete from patches where package_base = :package_base""",
{"package_base": package_base})
return self.with_connection(run, commit=True)

View File

@ -0,0 +1,70 @@
#
# 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 <http://www.gnu.org/licenses/>.
#
from __future__ import annotations
import json
import sqlite3
from sqlite3 import Connection
from typing import Type
from ahriman.core.configuration import Configuration
from ahriman.core.database.data import migrate_data
from ahriman.core.database.migrations import Migrations
from ahriman.core.database.operations.auth_operations import AuthOperations
from ahriman.core.database.operations.build_operations import BuildOperations
from ahriman.core.database.operations.package_operations import PackageOperations
from ahriman.core.database.operations.patch_operations import PatchOperations
class SQLite(AuthOperations, BuildOperations, PackageOperations, PatchOperations):
"""
wrapper for sqlite3 database
"""
@classmethod
def load(cls: Type[SQLite], configuration: Configuration) -> SQLite:
"""
construct instance from configuration
:param configuration: configuration instance
:return: fully initialized instance of the database
"""
database = cls(configuration.getpath("settings", "database"))
database.init(configuration)
return database
def init(self, configuration: Configuration) -> None:
"""
perform database migrations
:param configuration: configuration instance
"""
# custom types support
sqlite3.register_adapter(dict, json.dumps)
sqlite3.register_adapter(list, json.dumps)
sqlite3.register_converter("json", json.loads)
paths = configuration.repository_paths
def run(connection: Connection) -> None:
result = Migrations.migrate(connection)
migrate_data(result, connection, configuration, paths)
self.with_connection(run)
paths.chown(self.path)

View File

@ -26,12 +26,12 @@ class BuildFailed(RuntimeError):
base exception for failed builds base exception for failed builds
""" """
def __init__(self, package: str) -> None: def __init__(self, package_base: str) -> None:
""" """
default constructor default constructor
:param package: package base raised exception :param package_base: package base raised exception
""" """
RuntimeError.__init__(self, f"Package {package} build failed, check logs for details") RuntimeError.__init__(self, f"Package {package_base} build failed, check logs for details")
class DuplicateRun(RuntimeError): class DuplicateRun(RuntimeError):
@ -47,18 +47,11 @@ class DuplicateRun(RuntimeError):
self, "Another application instance is run. This error can be suppressed by using --force flag.") self, "Another application instance is run. This error can be suppressed by using --force flag.")
class DuplicateUser(ValueError): class ExitCode(RuntimeError):
""" """
exception which will be thrown in case if there are two users with different settings special exception which has to be thrown to return non-zero status without error message
""" """
def __init__(self, username: str) -> None:
"""
default constructor
:param username: username with duplicates
"""
ValueError.__init__(self, f"Found duplicate user with username {username}")
class InitializeException(RuntimeError): class InitializeException(RuntimeError):
""" """
@ -113,6 +106,19 @@ class InvalidPackageInfo(RuntimeError):
RuntimeError.__init__(self, f"There are errors during reading package information: `{details}`") RuntimeError.__init__(self, f"There are errors during reading package information: `{details}`")
class MigrationError(RuntimeError):
"""
exception which will be raised on migration error
"""
def __init__(self, details: str) -> None:
"""
default constructor
:param details: error details
"""
RuntimeError.__init__(self, details)
class MissingArchitecture(ValueError): class MissingArchitecture(ValueError):
""" """
exception which will be raised if architecture is required, but missing exception which will be raised if architecture is required, but missing

View File

@ -0,0 +1,46 @@
#
# 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 <http://www.gnu.org/licenses/>.
#
from typing import List
from ahriman.core.formatters.string_printer import StringPrinter
from ahriman.models.property import Property
from ahriman.models.user import User
class UserPrinter(StringPrinter):
"""
print properties of user
:ivar user: stored user
"""
def __init__(self, user: User) -> None:
"""
default constructor
:param user: user to print
"""
StringPrinter.__init__(self, user.username)
self.user = user
def properties(self) -> List[Property]:
"""
convert content into printable data
:return: list of content properties
"""
return [Property("role", self.user.access.value, is_required=True)]

View File

@ -37,14 +37,6 @@ class Cleaner(Properties):
""" """
raise NotImplementedError raise NotImplementedError
def clear_build(self) -> None:
"""
clear sources directory
"""
self.logger.info("clear package sources directory")
for package in self.paths.sources.iterdir():
shutil.rmtree(package)
def clear_cache(self) -> None: def clear_cache(self) -> None:
""" """
clear cache directory clear cache directory
@ -61,14 +53,6 @@ class Cleaner(Properties):
for chroot in self.paths.chroot.iterdir(): for chroot in self.paths.chroot.iterdir():
shutil.rmtree(chroot) shutil.rmtree(chroot)
def clear_manual(self) -> None:
"""
clear directory with manual package updates
"""
self.logger.info("clear manual packages")
for package in self.paths.manual.iterdir():
shutil.rmtree(package)
def clear_packages(self) -> None: def clear_packages(self) -> None:
""" """
clear directory with built packages (NOT repository itself) clear directory with built packages (NOT repository itself)
@ -77,10 +61,9 @@ class Cleaner(Properties):
for package in self.packages_built(): for package in self.packages_built():
package.unlink() package.unlink()
def clear_patches(self) -> None: def clear_queue(self) -> None:
""" """
clear directory with patches clear packages which were queued for the update
""" """
self.logger.info("clear patches directory") self.logger.info("clear build queue")
for package in self.paths.patches.iterdir(): self.database.build_queue_clear(None)
shutil.rmtree(package)

View File

@ -26,6 +26,7 @@ from ahriman.core.build_tools.task import Task
from ahriman.core.report.report import Report from ahriman.core.report.report import Report
from ahriman.core.repository.cleaner import Cleaner from ahriman.core.repository.cleaner import Cleaner
from ahriman.core.upload.upload import Upload from ahriman.core.upload.upload import Upload
from ahriman.core.util import tmpdir
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.result import Result from ahriman.models.result import Result
@ -56,25 +57,25 @@ class Executor(Cleaner):
:param updates: list of packages properties to build :param updates: list of packages properties to build
:return: `packages_built` :return: `packages_built`
""" """
def build_single(package: Package) -> None: def build_single(package: Package, local_path: Path) -> None:
self.reporter.set_building(package.base) self.reporter.set_building(package.base)
task = Task(package, self.configuration, self.paths) task = Task(package, self.configuration, self.paths)
task.init() task.init(local_path, self.database)
built = task.build() built = task.build(local_path)
for src in built: for src in built:
dst = self.paths.packages / src.name dst = self.paths.packages / src.name
shutil.move(src, dst) shutil.move(src, dst)
result = Result() result = Result()
for single in updates: for single in updates:
try: with tmpdir() as build_dir:
build_single(single) try:
result.add_success(single) build_single(single, build_dir)
except Exception: result.add_success(single)
self.reporter.set_failed(single.base) except Exception:
result.add_failed(single) self.reporter.set_failed(single.base)
self.logger.exception("%s (%s) build exception", single.base, self.architecture) result.add_failed(single)
self.clear_build() self.logger.exception("%s (%s) build exception", single.base, self.architecture)
return result return result
@ -87,6 +88,8 @@ class Executor(Cleaner):
def remove_base(package_base: str) -> None: def remove_base(package_base: str) -> None:
try: try:
self.paths.tree_clear(package_base) # remove all internal files self.paths.tree_clear(package_base) # remove all internal files
self.database.build_queue_clear(package_base)
self.database.patches_remove(package_base)
self.reporter.remove(package_base) # we only update status page in case of base removal self.reporter.remove(package_base) # we only update status page in case of base removal
except Exception: except Exception:
self.logger.exception("could not remove base %s", package_base) self.logger.exception("could not remove base %s", package_base)

View File

@ -22,11 +22,11 @@ import logging
from ahriman.core.alpm.pacman import Pacman from ahriman.core.alpm.pacman import Pacman
from ahriman.core.alpm.repo import Repo from ahriman.core.alpm.repo import Repo
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
from ahriman.core.exceptions import UnsafeRun from ahriman.core.exceptions import UnsafeRun
from ahriman.core.sign.gpg import GPG from ahriman.core.sign.gpg import GPG
from ahriman.core.status.client import Client from ahriman.core.status.client import Client
from ahriman.core.util import check_user from ahriman.core.util import check_user
from ahriman.models.repository_paths import RepositoryPaths
class Properties: class Properties:
@ -35,6 +35,7 @@ class Properties:
:ivar architecture: repository architecture :ivar architecture: repository architecture
:ivar aur_url: base AUR url :ivar aur_url: base AUR url
:ivar configuration: configuration instance :ivar configuration: configuration instance
:ivar database: database instance
:ivar ignore_list: package bases which will be ignored during auto updates :ivar ignore_list: package bases which will be ignored during auto updates
:ivar logger: class logger :ivar logger: class logger
:ivar name: repository name :ivar name: repository name
@ -45,22 +46,25 @@ class Properties:
:ivar sign: GPG wrapper instance :ivar sign: GPG wrapper instance
""" """
def __init__(self, architecture: str, configuration: Configuration, no_report: bool, unsafe: bool) -> None: def __init__(self, architecture: str, configuration: Configuration, database: SQLite,
no_report: bool, unsafe: bool) -> None:
""" """
default constructor default constructor
:param architecture: repository architecture :param architecture: repository architecture
:param configuration: configuration instance :param configuration: configuration instance
:param database: database instance
:param no_report: force disable reporting :param no_report: force disable reporting
:param unsafe: if set no user check will be performed before path creation :param unsafe: if set no user check will be performed before path creation
""" """
self.logger = logging.getLogger("root") self.logger = logging.getLogger("root")
self.architecture = architecture self.architecture = architecture
self.configuration = configuration self.configuration = configuration
self.database = database
self.aur_url = configuration.get("alpm", "aur_url") self.aur_url = configuration.get("alpm", "aur_url")
self.name = configuration.get("repository", "name") self.name = configuration.get("repository", "name")
self.paths = RepositoryPaths(configuration.getpath("repository", "root"), architecture) self.paths = configuration.repository_paths
try: try:
check_user(self.paths, unsafe) check_user(self.paths, unsafe)
self.paths.tree_create() self.paths.tree_create()

View File

@ -90,7 +90,7 @@ class UpdateHandler(Cleaner):
else: else:
self.reporter.set_success(local) self.reporter.set_success(local)
except Exception: except Exception:
self.logger.exception("could not procees package at %s", dirname) self.logger.exception("could not process package at %s", dirname)
return result return result
@ -102,16 +102,15 @@ class UpdateHandler(Cleaner):
result: List[Package] = [] result: List[Package] = []
known_bases = {package.base for package in self.packages()} known_bases = {package.base for package in self.packages()}
for dirname in self.paths.manual.iterdir(): try:
try: for local in self.database.build_queue_get():
local = Package.load(str(dirname), PackageSource.Local, self.pacman, self.aur_url)
result.append(local) result.append(local)
if local.base not in known_bases: if local.base not in known_bases:
self.reporter.set_unknown(local) self.reporter.set_unknown(local)
else: else:
self.reporter.set_pending(local.base) self.reporter.set_pending(local.base)
except Exception: except Exception:
self.logger.exception("could not add package from %s", dirname) self.logger.exception("could not load packages from database")
self.clear_manual() self.clear_queue()
return result return result

View File

@ -77,11 +77,6 @@ class Client:
""" """
return BuildStatus() return BuildStatus()
def reload_auth(self) -> None:
"""
reload authentication module call
"""
def remove(self, base: str) -> None: def remove(self, base: str) -> None:
""" """
remove packages from watcher remove packages from watcher

View File

@ -17,13 +17,12 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
import json
import logging import logging
from pathlib import Path from typing import Dict, List, Optional, Tuple
from typing import Any, Dict, List, Optional, Tuple
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
from ahriman.core.exceptions import UnknownPackage from ahriman.core.exceptions import UnknownPackage
from ahriman.core.repository import Repository from ahriman.core.repository import Repository
from ahriman.models.build_status import BuildStatus, BuildStatusEnum from ahriman.models.build_status import BuildStatus, BuildStatusEnum
@ -34,33 +33,29 @@ class Watcher:
""" """
package status watcher package status watcher
:ivar architecture: repository architecture :ivar architecture: repository architecture
:ivar database: database instance
:ivar known: list of known packages. For the most cases `packages` should be used instead :ivar known: list of known packages. For the most cases `packages` should be used instead
:ivar logger: class logger :ivar logger: class logger
:ivar repository: repository object :ivar repository: repository object
:ivar status: daemon status :ivar status: daemon status
""" """
def __init__(self, architecture: str, configuration: Configuration) -> None: def __init__(self, architecture: str, configuration: Configuration, database: SQLite) -> None:
""" """
default constructor default constructor
:param architecture: repository architecture :param architecture: repository architecture
:param configuration: configuration instance :param configuration: configuration instance
:param database: database instance
""" """
self.logger = logging.getLogger("http") self.logger = logging.getLogger("http")
self.architecture = architecture self.architecture = architecture
self.repository = Repository(architecture, configuration, no_report=True, unsafe=False) self.database = database
self.repository = Repository(architecture, configuration, database, no_report=True, unsafe=False)
self.known: Dict[str, Tuple[Package, BuildStatus]] = {} self.known: Dict[str, Tuple[Package, BuildStatus]] = {}
self.status = BuildStatus() self.status = BuildStatus()
@property
def cache_path(self) -> Path:
"""
:return: path to dump with json cache
"""
return self.repository.paths.root / "status_cache.json"
@property @property
def packages(self) -> List[Tuple[Package, BuildStatus]]: def packages(self) -> List[Tuple[Package, BuildStatus]]:
""" """
@ -68,48 +63,6 @@ class Watcher:
""" """
return list(self.known.values()) return list(self.known.values())
def _cache_load(self) -> None:
"""
update current state from cache
"""
def parse_single(properties: Dict[str, Any]) -> None:
package = Package.from_json(properties["package"])
status = BuildStatus.from_json(properties["status"])
if package.base in self.known:
self.known[package.base] = (package, status)
if not self.cache_path.is_file():
return
with self.cache_path.open() as cache:
try:
dump = json.load(cache)
except Exception:
self.logger.exception("cannot parse json from file")
dump = {}
for item in dump.get("packages", []):
try:
parse_single(item)
except Exception:
self.logger.exception("cannot parse item %s to package", item)
def _cache_save(self) -> None:
"""
dump current cache to filesystem
"""
dump = {
"packages": [
{
"package": package.view(),
"status": status.view()
} for package, status in self.packages
]
}
try:
with self.cache_path.open("w") as cache:
json.dump(dump, cache)
except Exception:
self.logger.exception("cannot dump cache")
def get(self, base: str) -> Tuple[Package, BuildStatus]: def get(self, base: str) -> Tuple[Package, BuildStatus]:
""" """
get current package base build status get current package base build status
@ -131,31 +84,34 @@ class Watcher:
else: else:
status = BuildStatus() status = BuildStatus()
self.known[package.base] = (package, status) self.known[package.base] = (package, status)
self._cache_load()
def remove(self, base: str) -> None: for package, status in self.database.packages_get():
if package.base in self.known:
self.known[package.base] = (package, status)
def remove(self, package_base: str) -> None:
""" """
remove package base from known list if any remove package base from known list if any
:param base: package base :param package_base: package base
""" """
self.known.pop(base, None) self.known.pop(package_base, None)
self._cache_save() self.database.package_remove(package_base)
def update(self, base: str, status: BuildStatusEnum, package: Optional[Package]) -> None: def update(self, package_base: str, status: BuildStatusEnum, package: Optional[Package]) -> None:
""" """
update package status and description update package status and description
:param base: package base to update :param package_base: package base to update
:param status: new build status :param status: new build status
:param package: optional new package description. In case if not set current properties will be used :param package: optional new package description. In case if not set current properties will be used
""" """
if package is None: if package is None:
try: try:
package, _ = self.known[base] package, _ = self.known[package_base]
except KeyError: except KeyError:
raise UnknownPackage(base) raise UnknownPackage(package_base)
full_status = BuildStatus(status) full_status = BuildStatus(status)
self.known[base] = (package, full_status) self.known[package_base] = (package, full_status)
self._cache_save() self.database.package_update(package, full_status)
def update_self(self, status: BuildStatusEnum) -> None: def update_self(self, status: BuildStatusEnum) -> None:
""" """

View File

@ -67,13 +67,6 @@ class WebClient(Client):
""" """
return f"{self.address}/user-api/v1/login" return f"{self.address}/user-api/v1/login"
@property
def _reload_auth_url(self) -> str:
"""
:return: full url for web service to reload authentication module
"""
return f"{self.address}/service-api/v1/reload-auth"
@property @property
def _status_url(self) -> str: def _status_url(self) -> str:
""" """
@ -198,18 +191,6 @@ class WebClient(Client):
self.logger.exception("could not get service status") self.logger.exception("could not get service status")
return BuildStatus() return BuildStatus()
def reload_auth(self) -> None:
"""
reload authentication module call
"""
try:
response = self.__session.post(self._reload_auth_url)
response.raise_for_status()
except requests.HTTPError as e:
self.logger.exception("could not reload auth module: %s", exception_response_text(e))
except Exception:
self.logger.exception("could not reload auth module")
def remove(self, base: str) -> None: def remove(self, base: str) -> None:
""" """
remove packages from watcher remove packages from watcher

View File

@ -26,8 +26,8 @@ from pathlib import Path
from typing import Iterable, List, Set, Type from typing import Iterable, List, Set, Type
from ahriman.core.build_tools.sources import Sources from ahriman.core.build_tools.sources import Sources
from ahriman.core.database.sqlite import SQLite
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.repository_paths import RepositoryPaths
class Leaf: class Leaf:
@ -54,16 +54,16 @@ class Leaf:
return self.package.packages.keys() return self.package.packages.keys()
@classmethod @classmethod
def load(cls: Type[Leaf], package: Package, paths: RepositoryPaths) -> Leaf: def load(cls: Type[Leaf], package: Package, database: SQLite) -> Leaf:
""" """
load leaf from package with dependencies load leaf from package with dependencies
:param package: package properties :param package: package properties
:param paths: repository paths instance :param database: database instance
:return: loaded class :return: loaded class
""" """
clone_dir = Path(tempfile.mkdtemp()) clone_dir = Path(tempfile.mkdtemp())
try: try:
Sources.load(clone_dir, package.git_url, paths.patches_for(package.base)) Sources.load(clone_dir, package.git_url, database.patches_get(package.base))
dependencies = Package.dependencies(clone_dir) dependencies = Package.dependencies(clone_dir)
finally: finally:
shutil.rmtree(clone_dir, ignore_errors=True) shutil.rmtree(clone_dir, ignore_errors=True)
@ -95,14 +95,14 @@ class Tree:
self.leaves = leaves self.leaves = leaves
@classmethod @classmethod
def load(cls: Type[Tree], packages: Iterable[Package], paths: RepositoryPaths) -> Tree: def load(cls: Type[Tree], packages: Iterable[Package], database: SQLite) -> Tree:
""" """
load tree from packages load tree from packages
:param packages: packages list :param packages: packages list
:param paths: repository paths instance :param database: database instance
:return: loaded class :return: loaded class
""" """
return cls([Leaf.load(package, paths) for package in packages]) return cls([Leaf.load(package, database) for package in packages])
def levels(self) -> List[List[Package]]: def levels(self) -> List[List[Package]]:
""" """

View File

@ -19,9 +19,12 @@
# #
import datetime import datetime
import os import os
import subprocess
import requests import requests
import shutil
import subprocess
import tempfile
from contextlib import contextmanager
from logging import Logger from logging import Logger
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Generator, Iterable, Optional, Union from typing import Any, Dict, Generator, Iterable, Optional, Union
@ -143,6 +146,19 @@ def pretty_size(size: Optional[float], level: int = 0) -> str:
return pretty_size(size / 1024, level + 1) return pretty_size(size / 1024, level + 1)
@contextmanager
def tmpdir() -> Generator[Path, None, None]:
"""
wrapper for tempfile to remove directory after all
:return: path to the created directory
"""
path = Path(tempfile.mkdtemp())
try:
yield path
finally:
shutil.rmtree(path, ignore_errors=True)
def walk(directory_path: Path) -> Generator[Path, None, None]: def walk(directory_path: Path) -> Generator[Path, None, None]:
""" """
list all file paths in given directory list all file paths in given directory

View File

@ -0,0 +1,35 @@
#
# 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 <http://www.gnu.org/licenses/>.
#
from dataclasses import dataclass
from typing import List
@dataclass
class Migration:
"""
migration implementation
:ivar index: migration position
:ivar name: migration name
:ivar steps: migration steps
"""
index: int
name: str
steps: List[str]

View File

@ -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 <http://www.gnu.org/licenses/>.
#
from dataclasses import dataclass
from ahriman.core.exceptions import MigrationError
@dataclass
class MigrationResult:
"""
migration result implementation model
:ivar old_version: old schema version before migrations
:ivar new_version: new schema version after migrations
"""
old_version: int
new_version: int
@property
def is_outdated(self) -> bool:
"""
:return: True in case if it requires migrations and False otherwise
"""
self.validate()
return self.new_version > self.old_version
def validate(self) -> None:
"""
perform version validation
"""
if self.old_version < 0 or self.old_version > self.new_version:
raise MigrationError(f"Invalid current schema version, expected less or equal to {self.new_version}, "
f"got {self.old_version}")

View File

@ -239,7 +239,7 @@ class Package:
from ahriman.core.build_tools.sources import Sources from ahriman.core.build_tools.sources import Sources
logger = logging.getLogger("build_details") logger = logging.getLogger("build_details")
Sources.load(paths.cache_for(self.base), self.git_url, paths.patches_for(self.base)) Sources.load(paths.cache_for(self.base), self.git_url, None)
try: try:
# update pkgver first # update pkgver first

View File

@ -19,7 +19,7 @@
# #
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass, field, fields from dataclasses import asdict, dataclass, field, fields
from pathlib import Path from pathlib import Path
from pyalpm import Package # type: ignore from pyalpm import Package # type: ignore
from typing import Any, Dict, List, Optional, Type from typing import Any, Dict, List, Optional, Type
@ -94,3 +94,10 @@ class PackageDescription:
licenses=package.licenses, licenses=package.licenses,
provides=package.provides, provides=package.provides,
url=package.url) url=package.url)
def view(self) -> Dict[str, Any]:
"""
generate json package view
:return: json-friendly dictionary
"""
return asdict(self)

View File

@ -52,16 +52,9 @@ class RepositoryPaths:
""" """
:return: directory for devtools chroot :return: directory for devtools chroot
""" """
# for the chroot directory devtools will create own tree and we don"t have to specify architecture here # for the chroot directory devtools will create own tree, and we don"t have to specify architecture here
return self.root / "chroot" return self.root / "chroot"
@property
def manual(self) -> Path:
"""
:return: directory for manual updates (i.e. from add command)
"""
return self.root / "manual" / self.architecture
@property @property
def packages(self) -> Path: def packages(self) -> Path:
""" """
@ -69,13 +62,6 @@ class RepositoryPaths:
""" """
return self.root / "packages" / self.architecture return self.root / "packages" / self.architecture
@property
def patches(self) -> Path:
"""
:return: directory for source patches
"""
return self.root / "patches"
@property @property
def repository(self) -> Path: def repository(self) -> Path:
""" """
@ -90,13 +76,6 @@ class RepositoryPaths:
""" """
return self.owner(self.root) return self.owner(self.root)
@property
def sources(self) -> Path:
"""
:return: directory for downloaded PKGBUILDs for current build
"""
return self.root / "sources" / self.architecture
@classmethod @classmethod
def known_architectures(cls: Type[RepositoryPaths], root: Path) -> Set[str]: def known_architectures(cls: Type[RepositoryPaths], root: Path) -> Set[str]:
""" """
@ -151,30 +130,6 @@ class RepositoryPaths:
set_owner(path) set_owner(path)
path = path.parent path = path.parent
def manual_for(self, package_base: str) -> Path:
"""
get manual path for specific package base
:param package_base: package base name
:return: full path to directory for specified package base manual updates
"""
return self.manual / package_base
def patches_for(self, package_base: str) -> Path:
"""
get patches path for specific package base
:param package_base: package base name
:return: full path to directory for specified package base patches
"""
return self.patches / package_base
def sources_for(self, package_base: str) -> Path:
"""
get path to directory from where build will start for the package base
:param package_base: package base name
:return: full path to directory for specified package base sources
"""
return self.sources / package_base
def tree_clear(self, package_base: str) -> None: def tree_clear(self, package_base: str) -> None:
""" """
clear package specific files clear package specific files
@ -182,9 +137,7 @@ class RepositoryPaths:
""" """
for directory in ( for directory in (
self.cache_for(package_base), self.cache_for(package_base),
self.manual_for(package_base), ):
self.patches_for(package_base),
self.sources_for(package_base)):
shutil.rmtree(directory, ignore_errors=True) shutil.rmtree(directory, ignore_errors=True)
def tree_create(self) -> None: def tree_create(self) -> None:
@ -194,10 +147,8 @@ class RepositoryPaths:
for directory in ( for directory in (
self.cache, self.cache,
self.chroot, self.chroot,
self.manual,
self.packages, self.packages,
self.patches,
self.repository, self.repository,
self.sources): ):
directory.mkdir(mode=0o755, parents=True, exist_ok=True) directory.mkdir(mode=0o755, parents=True, exist_ok=True)
self.chown(directory) self.chown(directory)

View File

@ -79,18 +79,18 @@ class User:
verified = False # the absence of evidence is not the evidence of absence (c) Gin Rummy verified = False # the absence of evidence is not the evidence of absence (c) Gin Rummy
return verified return verified
def hash_password(self, salt: str) -> str: def hash_password(self, salt: str) -> User:
""" """
generate hashed password from plain text generate hashed password from plain text
:param salt: salt for hashed password :param salt: salt for hashed password
:return: hashed string to store in configuration :return: user with hashed password to store in configuration
""" """
if not self.password: if not self.password:
# in case of empty password we leave it empty. This feature is used by any external (like OAuth) provider # in case of empty password we leave it empty. This feature is used by any external (like OAuth) provider
# when we do not store any password here # when we do not store any password here
return "" return self
password_hash: str = self._HASHER.hash(self.password + salt) password_hash: str = self._HASHER.hash(self.password + salt)
return password_hash return User(self.username, password_hash, self.access)
def verify_access(self, required: UserAccess) -> bool: def verify_access(self, required: UserAccess) -> bool:
""" """

View File

@ -22,7 +22,6 @@ from pathlib import Path
from ahriman.web.views.index import IndexView from ahriman.web.views.index import IndexView
from ahriman.web.views.service.add import AddView from ahriman.web.views.service.add import AddView
from ahriman.web.views.service.reload_auth import ReloadAuthView
from ahriman.web.views.service.remove import RemoveView from ahriman.web.views.service.remove import RemoveView
from ahriman.web.views.service.request import RequestView from ahriman.web.views.service.request import RequestView
from ahriman.web.views.service.search import SearchView from ahriman.web.views.service.search import SearchView
@ -45,8 +44,6 @@ def setup_routes(application: Application, static_path: Path) -> None:
POST /service-api/v1/add add new packages to repository POST /service-api/v1/add add new packages to repository
POST /service-api/v1/reload-auth reload authentication module
POST /service-api/v1/remove remove existing package from repository POST /service-api/v1/remove remove existing package from repository
POST /service-api/v1/request request to add new packages to repository POST /service-api/v1/request request to add new packages to repository
@ -81,8 +78,6 @@ def setup_routes(application: Application, static_path: Path) -> None:
application.router.add_post("/service-api/v1/add", AddView) application.router.add_post("/service-api/v1/add", AddView)
application.router.add_post("/service-api/v1/reload-auth", ReloadAuthView)
application.router.add_post("/service-api/v1/remove", RemoveView) application.router.add_post("/service-api/v1/remove", RemoveView)
application.router.add_post("/service-api/v1/request", RequestView) application.router.add_post("/service-api/v1/request", RequestView)

View File

@ -24,6 +24,7 @@ from typing import Any, Dict, List, Optional, Type
from ahriman.core.auth.auth import Auth from ahriman.core.auth.auth import Auth
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
from ahriman.core.spawn import Spawn from ahriman.core.spawn import Spawn
from ahriman.core.status.watcher import Watcher from ahriman.core.status.watcher import Watcher
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
@ -42,6 +43,14 @@ class BaseView(View):
configuration: Configuration = self.request.app["configuration"] configuration: Configuration = self.request.app["configuration"]
return configuration return configuration
@property
def database(self) -> SQLite:
"""
:return: database instance
"""
database: SQLite = self.request.app["database"]
return database
@property @property
def service(self) -> Watcher: def service(self) -> Watcher:
""" """

View File

@ -1,51 +0,0 @@
#
# 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 <http://www.gnu.org/licenses/>.
#
from aiohttp.web import HTTPNoContent, Response
from ahriman.core.auth.auth import Auth
from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView
class ReloadAuthView(BaseView):
"""
reload authentication module web view
:cvar POST_PERMISSION: post permissions of self
"""
POST_PERMISSION = UserAccess.Write
async def post(self) -> Response:
"""
reload authentication module. No parameters supported here
:return: 204 on success
"""
self.configuration.reload()
try:
import aiohttp_security # type: ignore
self.request.app[aiohttp_security.api.AUTZ_KEY].validator =\
self.request.app["validator"] =\
Auth.load(self.configuration)
except (ImportError, KeyError):
self.request.app.logger.warning("could not update authentication module validator", exc_info=True)
raise
raise HTTPNoContent()

View File

@ -58,7 +58,7 @@ class LoginView(BaseView):
identity = UserIdentity.from_username(username, self.validator.max_age) identity = UserIdentity.from_username(username, self.validator.max_age)
if identity is not None and await self.validator.known_username(username): if identity is not None and await self.validator.known_username(username):
await remember(self.request, response, identity.to_identity()) await remember(self.request, response, identity.to_identity())
return response raise response
raise HTTPUnauthorized() raise HTTPUnauthorized()
@ -81,6 +81,6 @@ class LoginView(BaseView):
identity = UserIdentity.from_username(username, self.validator.max_age) identity = UserIdentity.from_username(username, self.validator.max_age)
if identity is not None and await self.validator.check_credentials(username, data.get("password")): if identity is not None and await self.validator.check_credentials(username, data.get("password")):
await remember(self.request, response, identity.to_identity()) await remember(self.request, response, identity.to_identity())
return response raise response
raise HTTPUnauthorized() raise HTTPUnauthorized()

View File

@ -25,6 +25,7 @@ from aiohttp import web
from ahriman.core.auth.auth import Auth from ahriman.core.auth.auth import Auth
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
from ahriman.core.exceptions import InitializeException from ahriman.core.exceptions import InitializeException
from ahriman.core.spawn import Spawn from ahriman.core.spawn import Spawn
from ahriman.core.status.watcher import Watcher from ahriman.core.status.watcher import Watcher
@ -93,8 +94,11 @@ def setup_service(architecture: str, configuration: Configuration, spawner: Spaw
application.logger.info("setup configuration") application.logger.info("setup configuration")
application["configuration"] = configuration application["configuration"] = configuration
application.logger.info("setup database and perform migrations")
database = application["database"] = SQLite.load(configuration)
application.logger.info("setup watcher") application.logger.info("setup watcher")
application["watcher"] = Watcher(architecture, configuration) application["watcher"] = Watcher(architecture, configuration, database)
application.logger.info("setup process spawner") application.logger.info("setup process spawner")
application["spawn"] = spawner application["spawn"] = spawner
@ -108,7 +112,7 @@ def setup_service(architecture: str, configuration: Configuration, spawner: Spaw
check_host=configuration.getboolean("web", "debug_check_host", fallback=False)) check_host=configuration.getboolean("web", "debug_check_host", fallback=False))
application.logger.info("setup authorization") application.logger.info("setup authorization")
validator = application["validator"] = Auth.load(configuration) validator = application["validator"] = Auth.load(configuration, database)
if validator.enabled: if validator.enabled:
from ahriman.web.middlewares.auth_handler import setup_auth from ahriman.web.middlewares.auth_handler import setup_auth
setup_auth(application, validator) setup_auth(application, validator)

View File

@ -6,39 +6,46 @@ from ahriman.application.application.packages import Packages
from ahriman.application.application.properties import Properties from ahriman.application.application.properties import Properties
from ahriman.application.application.repository import Repository from ahriman.application.application.repository import Repository
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
@pytest.fixture @pytest.fixture
def application_packages(configuration: Configuration, mocker: MockerFixture) -> Packages: def application_packages(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> Packages:
""" """
fixture for application with package functions fixture for application with package functions
:param configuration: configuration fixture :param configuration: configuration fixture
:param database: database fixture
:param mocker: mocker object :param mocker: mocker object
:return: application test instance :return: application test instance
""" """
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
mocker.patch("ahriman.core.database.sqlite.SQLite.load", return_value=database)
return Packages("x86_64", configuration, no_report=True, unsafe=False) return Packages("x86_64", configuration, no_report=True, unsafe=False)
@pytest.fixture @pytest.fixture
def application_properties(configuration: Configuration, mocker: MockerFixture) -> Properties: def application_properties(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> Properties:
""" """
fixture for application with properties only fixture for application with properties only
:param configuration: configuration fixture :param configuration: configuration fixture
:param database: database fixture
:param mocker: mocker object :param mocker: mocker object
:return: application test instance :return: application test instance
""" """
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
mocker.patch("ahriman.core.database.sqlite.SQLite.load", return_value=database)
return Properties("x86_64", configuration, no_report=True, unsafe=False) return Properties("x86_64", configuration, no_report=True, unsafe=False)
@pytest.fixture @pytest.fixture
def application_repository(configuration: Configuration, mocker: MockerFixture) -> Repository: def application_repository(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> Repository:
""" """
fixture for application with repository functions fixture for application with repository functions
:param configuration: configuration fixture :param configuration: configuration fixture
:param database: database fixture
:param mocker: mocker object :param mocker: mocker object
:return: application test instance :return: application test instance
""" """
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
mocker.patch("ahriman.core.database.sqlite.SQLite.load", return_value=database)
return Repository("x86_64", configuration, no_report=True, unsafe=False) return Repository("x86_64", configuration, no_report=True, unsafe=False)

View File

@ -2,7 +2,6 @@ import pytest
from pathlib import Path from pathlib import Path
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from unittest import mock
from unittest.mock import MagicMock from unittest.mock import MagicMock
from ahriman.application.application.packages import Packages from ahriman.application.application.packages import Packages
@ -43,16 +42,17 @@ def test_add_aur(application_packages: Packages, package_ahriman: Package, mocke
must add package from AUR must add package from AUR
""" """
mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman) mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman)
insert_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.build_queue_insert")
load_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.load") load_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.load")
dependencies_mock = mocker.patch("ahriman.application.application.packages.Packages._process_dependencies") dependencies_mock = mocker.patch("ahriman.application.application.packages.Packages._process_dependencies")
application_packages._add_aur(package_ahriman.base, set(), False) application_packages._add_aur(package_ahriman.base, set(), False)
insert_mock.assert_called_once_with(package_ahriman)
load_mock.assert_called_once_with( load_mock.assert_called_once_with(
application_packages.repository.paths.manual_for(package_ahriman.base), pytest.helpers.anyvar(int),
package_ahriman.git_url, package_ahriman.git_url,
application_packages.repository.paths.patches_for(package_ahriman.base)) pytest.helpers.anyvar(int))
dependencies_mock.assert_called_once_with( dependencies_mock.assert_called_once_with(pytest.helpers.anyvar(int), set(), False)
application_packages.repository.paths.manual_for(package_ahriman.base), set(), False)
def test_add_directory(application_packages: Packages, package_ahriman: Package, mocker: MockerFixture) -> None: def test_add_directory(application_packages: Packages, package_ahriman: Package, mocker: MockerFixture) -> None:
@ -75,18 +75,16 @@ def test_add_local(application_packages: Packages, package_ahriman: Package, moc
""" """
mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman) mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman)
init_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.init") init_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.init")
insert_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.build_queue_insert")
copytree_mock = mocker.patch("shutil.copytree") copytree_mock = mocker.patch("shutil.copytree")
dependencies_mock = mocker.patch("ahriman.application.application.packages.Packages._process_dependencies") dependencies_mock = mocker.patch("ahriman.application.application.packages.Packages._process_dependencies")
application_packages._add_local(package_ahriman.base, set(), False) application_packages._add_local(package_ahriman.base, set(), False)
copytree_mock.assert_called_once_with(
Path(package_ahriman.base), application_packages.repository.paths.cache_for(package_ahriman.base))
init_mock.assert_called_once_with(application_packages.repository.paths.cache_for(package_ahriman.base)) init_mock.assert_called_once_with(application_packages.repository.paths.cache_for(package_ahriman.base))
copytree_mock.assert_has_calls([ insert_mock.assert_called_once_with(package_ahriman)
mock.call(Path(package_ahriman.base), application_packages.repository.paths.cache_for(package_ahriman.base)), dependencies_mock.assert_called_once_with(pytest.helpers.anyvar(int), set(), False)
mock.call(application_packages.repository.paths.cache_for(package_ahriman.base),
application_packages.repository.paths.manual_for(package_ahriman.base)),
])
dependencies_mock.assert_called_once_with(
application_packages.repository.paths.manual_for(package_ahriman.base), set(), False)
def test_add_remote(application_packages: Packages, package_description_ahriman: PackageDescription, def test_add_remote(application_packages: Packages, package_description_ahriman: PackageDescription,

View File

@ -17,21 +17,12 @@ def test_finalize(application_repository: Repository) -> None:
application_repository._finalize([]) application_repository._finalize([])
def test_clean_build(application_repository: Repository, mocker: MockerFixture) -> None:
"""
must clean build directory
"""
clear_mock = mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_build")
application_repository.clean(True, False, False, False, False, False)
clear_mock.assert_called_once_with()
def test_clean_cache(application_repository: Repository, mocker: MockerFixture) -> None: def test_clean_cache(application_repository: Repository, mocker: MockerFixture) -> None:
""" """
must clean cache directory must clean cache directory
""" """
clear_mock = mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_cache") clear_mock = mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_cache")
application_repository.clean(False, True, False, False, False, False) application_repository.clean(True, False, False, False)
clear_mock.assert_called_once_with() clear_mock.assert_called_once_with()
@ -40,7 +31,7 @@ def test_clean_chroot(application_repository: Repository, mocker: MockerFixture)
must clean chroot directory must clean chroot directory
""" """
clear_mock = mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_chroot") clear_mock = mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_chroot")
application_repository.clean(False, False, True, False, False, False) application_repository.clean(False, True, False, False)
clear_mock.assert_called_once_with() clear_mock.assert_called_once_with()
@ -48,8 +39,8 @@ def test_clean_manual(application_repository: Repository, mocker: MockerFixture)
""" """
must clean manual directory must clean manual directory
""" """
clear_mock = mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_manual") clear_mock = mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_queue")
application_repository.clean(False, False, False, True, False, False) application_repository.clean(False, False, True, False)
clear_mock.assert_called_once_with() clear_mock.assert_called_once_with()
@ -58,16 +49,7 @@ def test_clean_packages(application_repository: Repository, mocker: MockerFixtur
must clean packages directory must clean packages directory
""" """
clear_mock = mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_packages") clear_mock = mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_packages")
application_repository.clean(False, False, False, False, True, False) application_repository.clean(False, False, False, True)
clear_mock.assert_called_once_with()
def test_clean_patches(application_repository: Repository, mocker: MockerFixture) -> None:
"""
must clean packages directory
"""
clear_mock = mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_patches")
application_repository.clean(False, False, False, False, False, True)
clear_mock.assert_called_once_with() clear_mock.assert_called_once_with()

View File

@ -7,17 +7,20 @@ from ahriman.application.ahriman import _parser
from ahriman.application.application import Application from ahriman.application.application import Application
from ahriman.application.lock import Lock from ahriman.application.lock import Lock
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
@pytest.fixture @pytest.fixture
def application(configuration: Configuration, mocker: MockerFixture) -> Application: def application(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> Application:
""" """
fixture for application fixture for application
:param configuration: configuration fixture :param configuration: configuration fixture
:param database: database fixture
:param mocker: mocker object :param mocker: mocker object
:return: application test instance :return: application test instance
""" """
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
mocker.patch("ahriman.core.database.sqlite.SQLite.load", return_value=database)
return Application("x86_64", configuration, no_report=True, unsafe=False) return Application("x86_64", configuration, no_report=True, unsafe=False)

View File

@ -6,7 +6,7 @@ from pytest_mock import MockerFixture
from ahriman.application.handlers import Handler from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import MissingArchitecture, MultipleArchitectures from ahriman.core.exceptions import ExitCode, MissingArchitecture, MultipleArchitectures
def test_architectures_extract(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: def test_architectures_extract(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
@ -71,8 +71,26 @@ def test_call_exception(args: argparse.Namespace, mocker: MockerFixture) -> None
""" """
must process exception must process exception
""" """
mocker.patch("ahriman.application.lock.Lock.__enter__", side_effect=Exception()) args.configuration = Path("")
args.quiet = False
mocker.patch("ahriman.core.configuration.Configuration.from_path", side_effect=Exception())
logging_mock = mocker.patch("logging.Logger.exception")
assert not Handler.call(args, "x86_64") assert not Handler.call(args, "x86_64")
logging_mock.assert_called_once_with(pytest.helpers.anyvar(str, strict=True))
def test_call_exit_code(args: argparse.Namespace, mocker: MockerFixture) -> None:
"""
must process exitcode exception
"""
args.configuration = Path("")
args.quiet = False
mocker.patch("ahriman.core.configuration.Configuration.from_path", side_effect=ExitCode())
logging_mock = mocker.patch("logging.Logger.exception")
assert not Handler.call(args, "x86_64")
logging_mock.assert_not_called()
def test_execute(args: argparse.Namespace, mocker: MockerFixture) -> None: def test_execute(args: argparse.Namespace, mocker: MockerFixture) -> None:
@ -98,11 +116,14 @@ def test_execute_multiple_not_supported(args: argparse.Namespace, mocker: Mocker
Handler.execute(args) Handler.execute(args)
def test_execute_single(args: argparse.Namespace, mocker: MockerFixture) -> None: def test_execute_single(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
""" """
must run execution in current process if only one architecture supplied must run execution in current process if only one architecture supplied
""" """
args.architecture = ["x86_64"] args.architecture = ["x86_64"]
args.configuration = Path("")
args.quiet = False
mocker.patch("ahriman.core.configuration.Configuration.from_path", return_value=configuration)
starmap_mock = mocker.patch("multiprocessing.pool.Pool.starmap") starmap_mock = mocker.patch("multiprocessing.pool.Pool.starmap")
Handler.execute(args) Handler.execute(args)

View File

@ -12,12 +12,10 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
:param args: command line arguments fixture :param args: command line arguments fixture
:return: generated arguments for these test cases :return: generated arguments for these test cases
""" """
args.build = False
args.cache = False args.cache = False
args.chroot = False args.chroot = False
args.manual = False args.manual = False
args.packages = False args.packages = False
args.patches = False
return args return args
@ -30,4 +28,4 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
application_mock = mocker.patch("ahriman.application.application.Application.clean") application_mock = mocker.patch("ahriman.application.application.Application.clean")
Clean.run(args, "x86_64", configuration, True, False) Clean.run(args, "x86_64", configuration, True, False)
application_mock.assert_called_once_with(False, False, False, False, False, False) application_mock.assert_called_once_with(False, False, False, False)

View File

@ -67,24 +67,23 @@ def test_patch_set_list(application: Application, mocker: MockerFixture) -> None
must list available patches for the command must list available patches for the command
""" """
mocker.patch("pathlib.Path.is_dir", return_value=True) mocker.patch("pathlib.Path.is_dir", return_value=True)
glob_mock = mocker.patch("pathlib.Path.glob", return_value=[Path("local")]) get_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.patches_list", return_value={"ahriman": "patch"})
print_mock = mocker.patch("ahriman.application.handlers.patch.Patch._print") print_mock = mocker.patch("ahriman.core.formatters.printer.Printer.print")
Patch.patch_set_list(application, "ahriman") Patch.patch_set_list(application, "ahriman")
glob_mock.assert_called_once_with("*.patch") get_mock.assert_called_once_with("ahriman")
print_mock.assert_called() print_mock.assert_called_once_with(verbose=True)
def test_patch_set_list_no_dir(application: Application, mocker: MockerFixture) -> None: def test_patch_set_list_no_patches(application: Application, mocker: MockerFixture) -> None:
""" """
must not fail if no patches directory found must not fail if no patches directory found
""" """
mocker.patch("pathlib.Path.is_dir", return_value=False) mocker.patch("pathlib.Path.is_dir", return_value=False)
glob_mock = mocker.patch("pathlib.Path.glob") mocker.patch("ahriman.core.database.sqlite.SQLite.patches_get", return_value=None)
print_mock = mocker.patch("ahriman.application.handlers.patch.Patch._print") print_mock = mocker.patch("ahriman.core.formatters.printer.Printer.print")
Patch.patch_set_list(application, "ahriman") Patch.patch_set_list(application, "ahriman")
glob_mock.assert_not_called()
print_mock.assert_not_called() print_mock.assert_not_called()
@ -94,21 +93,17 @@ def test_patch_set_create(application: Application, package_ahriman: Package, mo
""" """
mocker.patch("pathlib.Path.mkdir") mocker.patch("pathlib.Path.mkdir")
mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman) mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman)
remove_mock = mocker.patch("ahriman.application.handlers.patch.Patch.patch_set_remove") mocker.patch("ahriman.core.build_tools.sources.Sources.patch_create", return_value="patch")
create_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.patch_create") create_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.patches_insert")
patch_dir = application.repository.paths.patches_for(package_ahriman.base)
Patch.patch_set_create(application, Path("path"), ["*.patch"]) Patch.patch_set_create(application, "path", ["*.patch"])
remove_mock.assert_called_once_with(application, package_ahriman.base) create_mock.assert_called_once_with(package_ahriman.base, "patch")
create_mock.assert_called_once_with(Path("path"), patch_dir / "00-main.patch", "*.patch")
def test_patch_set_remove(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None: def test_patch_set_remove(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:
""" """
must remove patch set for the package must remove patch set for the package
""" """
remove_mock = mocker.patch("shutil.rmtree") remove_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.patches_remove")
patch_dir = application.repository.paths.patches_for(package_ahriman.base)
Patch.patch_set_remove(application, package_ahriman.base) Patch.patch_set_remove(application, package_ahriman.base)
remove_mock.assert_called_once_with(patch_dir, ignore_errors=True) remove_mock.assert_called_once_with(package_ahriman.base)

View File

@ -6,13 +6,25 @@ from pytest_mock import MockerFixture
from ahriman.application.ahriman import _parser from ahriman.application.ahriman import _parser
from ahriman.application.handlers import UnsafeCommands from ahriman.application.handlers import UnsafeCommands
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import ExitCode
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 = _parser
args.command = None
return args
def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
""" """
must run command must run command
""" """
args.parser = _parser args = _default_args(args)
commands_mock = mocker.patch("ahriman.application.handlers.UnsafeCommands.get_unsafe_commands", commands_mock = mocker.patch("ahriman.application.handlers.UnsafeCommands.get_unsafe_commands",
return_value=["command"]) return_value=["command"])
print_mock = mocker.patch("ahriman.core.formatters.printer.Printer.print") print_mock = mocker.patch("ahriman.core.formatters.printer.Printer.print")
@ -22,6 +34,36 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
print_mock.assert_called_once_with(verbose=True) print_mock.assert_called_once_with(verbose=True)
def test_run_check(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run command and check if command is unsafe
"""
args = _default_args(args)
args.command = "clean"
commands_mock = mocker.patch("ahriman.application.handlers.UnsafeCommands.get_unsafe_commands",
return_value=["command"])
check_mock = mocker.patch("ahriman.application.handlers.UnsafeCommands.check_unsafe")
UnsafeCommands.run(args, "x86_64", configuration, True, False)
commands_mock.assert_called_once_with(pytest.helpers.anyvar(int))
check_mock.assert_called_once_with("clean", ["command"], pytest.helpers.anyvar(int))
def test_check_unsafe() -> None:
"""
must check if command is unsafe
"""
with pytest.raises(ExitCode):
UnsafeCommands.check_unsafe("repo-clean", ["repo-clean"], _parser())
def test_check_unsafe_safe() -> None:
"""
must check if command is unsafe
"""
UnsafeCommands.check_unsafe("package-status", ["repo-clean"], _parser())
def test_get_unsafe_commands() -> None: def test_get_unsafe_commands() -> None:
""" """
must return unsafe commands must return unsafe commands

View File

@ -3,10 +3,11 @@ import pytest
from pathlib import Path from pathlib import Path
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from unittest import mock
from ahriman.application.handlers import User from ahriman.application.handlers import User
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
from ahriman.core.exceptions import InitializeException
from ahriman.models.action import Action from ahriman.models.action import Action
from ahriman.models.user import User as MUser from ahriman.models.user import User as MUser
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
@ -21,96 +22,78 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
args.username = "user" args.username = "user"
args.action = Action.Update args.action = Action.Update
args.as_service = False args.as_service = False
args.no_reload = False
args.password = "pa55w0rd" args.password = "pa55w0rd"
args.role = UserAccess.Read args.role = UserAccess.Read
args.secure = False args.secure = False
return args return args
def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: def test_run(args: argparse.Namespace, configuration: Configuration, database: SQLite, mocker: MockerFixture) -> None:
""" """
must run command must run command
""" """
args = _default_args(args) args = _default_args(args)
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") user = MUser(args.username, args.password, args.role)
mocker.patch("ahriman.core.database.sqlite.SQLite.load", return_value=database)
mocker.patch("ahriman.models.user.User.hash_password", return_value=user)
get_auth_configuration_mock = mocker.patch("ahriman.application.handlers.User.configuration_get") get_auth_configuration_mock = mocker.patch("ahriman.application.handlers.User.configuration_get")
create_configuration_mock = mocker.patch("ahriman.application.handlers.User.configuration_create") create_configuration_mock = mocker.patch("ahriman.application.handlers.User.configuration_create")
write_configuration_mock = mocker.patch("ahriman.application.handlers.User.configuration_write") create_user_mock = mocker.patch("ahriman.application.handlers.User.user_create", return_value=user)
create_user_mock = mocker.patch("ahriman.application.handlers.User.user_create") get_salt_mock = mocker.patch("ahriman.application.handlers.User.get_salt", return_value="salt")
get_salt_mock = mocker.patch("ahriman.application.handlers.User.get_salt") update_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.user_update")
reload_mock = mocker.patch("ahriman.core.status.client.Client.reload_auth")
User.run(args, "x86_64", configuration, True, False) User.run(args, "x86_64", configuration, True, False)
get_auth_configuration_mock.assert_called_once_with(configuration.include) get_auth_configuration_mock.assert_called_once_with(configuration.include)
create_configuration_mock.assert_called_once_with( create_configuration_mock.assert_called_once_with(pytest.helpers.anyvar(int), pytest.helpers.anyvar(int),
pytest.helpers.anyvar(int), pytest.helpers.anyvar(int), pytest.helpers.anyvar(int), args.as_service) pytest.helpers.anyvar(int), args.as_service, args.secure)
create_user_mock.assert_called_once_with(args) create_user_mock.assert_called_once_with(args)
get_salt_mock.assert_called_once_with(configuration) get_salt_mock.assert_called_once_with(configuration)
write_configuration_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.secure) update_mock.assert_called_once_with(user)
reload_mock.assert_called_once_with()
def test_run_remove(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: def test_run_list(args: argparse.Namespace, configuration: Configuration, database: SQLite, user: User,
mocker: MockerFixture) -> None:
"""
must list avaiable users
"""
args = _default_args(args)
args.action = Action.List
mocker.patch("ahriman.core.database.sqlite.SQLite.load", return_value=database)
get_auth_configuration_mock = mocker.patch("ahriman.application.handlers.User.configuration_get")
list_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.user_list", return_value=[user])
User.run(args, "x86_64", configuration, True, False)
get_auth_configuration_mock.assert_called_once_with(configuration.include)
list_mock.assert_called_once_with("user", UserAccess.Read)
def test_run_remove(args: argparse.Namespace, configuration: Configuration, database: SQLite,
mocker: MockerFixture) -> None:
""" """
must remove user if remove flag supplied must remove user if remove flag supplied
""" """
args = _default_args(args) args = _default_args(args)
args.action = Action.Remove args.action = Action.Remove
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") mocker.patch("ahriman.core.database.sqlite.SQLite.load", return_value=database)
get_auth_configuration_mock = mocker.patch("ahriman.application.handlers.User.configuration_get") get_auth_configuration_mock = mocker.patch("ahriman.application.handlers.User.configuration_get")
create_configuration_mock = mocker.patch("ahriman.application.handlers.User.configuration_create") remove_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.user_remove")
write_configuration_mock = mocker.patch("ahriman.application.handlers.User.configuration_write")
reload_mock = mocker.patch("ahriman.core.status.client.Client.reload_auth")
User.run(args, "x86_64", configuration, True, False) User.run(args, "x86_64", configuration, True, False)
get_auth_configuration_mock.assert_called_once_with(configuration.include) get_auth_configuration_mock.assert_called_once_with(configuration.include)
create_configuration_mock.assert_not_called() remove_mock.assert_called_once_with(args.username)
write_configuration_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.secure)
reload_mock.assert_called_once_with()
def test_run_no_reload(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run command with no reload
"""
args = _default_args(args)
args.no_reload = True
mocker.patch("ahriman.application.handlers.User.configuration_get")
mocker.patch("ahriman.application.handlers.User.configuration_create")
mocker.patch("ahriman.application.handlers.User.configuration_write")
reload_mock = mocker.patch("ahriman.core.status.client.Client.reload_auth")
User.run(args, "x86_64", configuration, True, False)
reload_mock.assert_not_called()
def test_configuration_create(configuration: Configuration, user: MUser, mocker: MockerFixture) -> None: def test_configuration_create(configuration: Configuration, user: MUser, mocker: MockerFixture) -> None:
""" """
must correctly create configuration file must correctly create configuration file
""" """
section = Configuration.section_name("auth", user.access.value)
mocker.patch("pathlib.Path.open") mocker.patch("pathlib.Path.open")
set_mock = mocker.patch("ahriman.core.configuration.Configuration.set_option") set_mock = mocker.patch("ahriman.core.configuration.Configuration.set_option")
write_mock = mocker.patch("ahriman.application.handlers.User.configuration_write")
User.configuration_create(configuration, user, "salt", False) User.configuration_create(configuration, user, "salt", False, False)
set_mock.assert_has_calls([ set_mock.assert_called_once_with("auth", "salt", pytest.helpers.anyvar(int))
mock.call("auth", "salt", pytest.helpers.anyvar(int)), write_mock.assert_called_once_with(configuration, False)
mock.call(section, user.username, pytest.helpers.anyvar(int))
])
def test_configuration_create_user_exists(configuration: Configuration, user: MUser, mocker: MockerFixture) -> None:
"""
must correctly update configuration file if user already exists
"""
section = Configuration.section_name("auth", user.access.value)
configuration.set_option(section, user.username, "")
mocker.patch("pathlib.Path.open")
User.configuration_create(configuration, user, "salt", False)
generated = MUser.from_option(user.username, configuration.get(section, user.username))
assert generated.check_credentials(user.password, configuration.get("auth", "salt"))
def test_configuration_create_with_plain_password( def test_configuration_create_with_plain_password(
@ -120,12 +103,11 @@ def test_configuration_create_with_plain_password(
""" """
must set plain text password and user for the service must set plain text password and user for the service
""" """
section = Configuration.section_name("auth", user.access.value)
mocker.patch("pathlib.Path.open") mocker.patch("pathlib.Path.open")
User.configuration_create(configuration, user, "salt", True) User.configuration_create(configuration, user, "salt", True, False)
generated = MUser.from_option(user.username, configuration.get(section, user.username)) generated = MUser.from_option(user.username, user.password).hash_password("salt")
service = MUser.from_option(configuration.get("web", "username"), configuration.get("web", "password")) service = MUser.from_option(configuration.get("web", "username"), configuration.get("web", "password"))
assert generated.username == service.username assert generated.username == service.username
assert generated.check_credentials(service.password, configuration.get("auth", "salt")) assert generated.check_credentials(service.password, configuration.get("auth", "salt"))
@ -174,12 +156,9 @@ def test_configuration_write_not_loaded(configuration: Configuration, mocker: Mo
""" """
configuration.path = None configuration.path = None
mocker.patch("pathlib.Path.open") mocker.patch("pathlib.Path.open")
write_mock = mocker.patch("ahriman.core.configuration.Configuration.write")
chmod_mock = mocker.patch("pathlib.Path.chmod")
User.configuration_write(configuration, secure=True) with pytest.raises(InitializeException):
write_mock.assert_not_called() User.configuration_write(configuration, secure=True)
chmod_mock.assert_not_called()
def test_get_salt_read(configuration: Configuration) -> None: def test_get_salt_read(configuration: Configuration) -> None:
@ -200,31 +179,6 @@ def test_get_salt_generate(configuration: Configuration) -> None:
assert len(salt) == 16 assert len(salt) == 16
def test_user_clear(configuration: Configuration, user: MUser) -> None:
"""
must clear user from configuration
"""
section = Configuration.section_name("auth", user.access.value)
configuration.set_option(section, user.username, user.password)
User.user_clear(configuration, user)
assert configuration.get(section, user.username, fallback=None) is None
def test_user_clear_multiple_sections(configuration: Configuration, user: MUser) -> None:
"""
must clear user from configuration from all sections
"""
for role in UserAccess:
section = Configuration.section_name("auth", role.value)
configuration.set_option(section, user.username, user.password)
User.user_clear(configuration, user)
for role in UserAccess:
section = Configuration.section_name("auth", role.value)
assert configuration.get(section, user.username, fallback=None) is None
def test_user_create(args: argparse.Namespace, user: MUser) -> None: def test_user_create(args: argparse.Namespace, user: MUser) -> None:
""" """
must create user must create user

View File

@ -438,6 +438,38 @@ def test_subparsers_user_add_option_role(parser: argparse.ArgumentParser) -> Non
assert isinstance(args.role, UserAccess) assert isinstance(args.role, UserAccess)
def test_subparsers_user_list(parser: argparse.ArgumentParser) -> None:
"""
user-list command must imply action, architecture, lock, no-report, password, quiet and unsafe
"""
args = parser.parse_args(["user-list"])
assert args.action == Action.List
assert args.architecture == [""]
assert args.lock is None
assert args.no_report
assert args.password is not None
assert args.quiet
assert args.unsafe
def test_subparsers_user_list_architecture(parser: argparse.ArgumentParser) -> None:
"""
user-list command must correctly parse architecture list
"""
args = parser.parse_args(["-a", "x86_64", "user-list"])
assert args.architecture == [""]
def test_subparsers_user_list_option_role(parser: argparse.ArgumentParser) -> None:
"""
user-list command must convert role option to useraccess instance
"""
args = parser.parse_args(["user-list"])
assert isinstance(args.role, UserAccess)
args = parser.parse_args(["user-list", "--role", "write"])
assert isinstance(args.role, UserAccess)
def test_subparsers_user_remove(parser: argparse.ArgumentParser) -> None: def test_subparsers_user_remove(parser: argparse.ArgumentParser) -> None:
""" """
user-remove command must imply action, architecture, lock, no-report, password, quiet, role and unsafe user-remove command must imply action, architecture, lock, no-report, password, quiet, role and unsafe

View File

@ -3,14 +3,16 @@ import pytest
from pathlib import Path from pathlib import Path
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from typing import Any, Type, TypeVar from typing import Any, Dict, Type, TypeVar
from unittest.mock import MagicMock from unittest.mock import MagicMock
from ahriman.core.auth.auth import Auth from ahriman.core.auth.auth import Auth
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
from ahriman.core.spawn import Spawn from ahriman.core.spawn import Spawn
from ahriman.core.status.watcher import Watcher from ahriman.core.status.watcher import Watcher
from ahriman.models.aur_package import AURPackage from ahriman.models.aur_package import AURPackage
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.package_description import PackageDescription from ahriman.models.package_description import PackageDescription
from ahriman.models.repository_paths import RepositoryPaths from ahriman.models.repository_paths import RepositoryPaths
@ -48,6 +50,26 @@ def anyvar(cls: Type[T], strict: bool = False) -> T:
return AnyVar() return AnyVar()
@pytest.helpers.register
def get_package_status(package: Package) -> Dict[str, Any]:
"""
helper to extract package status from package
:param package: package object
:return: simplified package status map (with only status and view)
"""
return {"status": BuildStatusEnum.Unknown.value, "package": package.view()}
@pytest.helpers.register
def get_package_status_extended(package: Package) -> Dict[str, Any]:
"""
helper to extract package status from package
:param package: package object
:return: full package status map (with timestamped build status and view)
"""
return {"status": BuildStatus().view(), "package": package.view()}
# generic fixtures # generic fixtures
@pytest.fixture @pytest.fixture
def aur_package_ahriman() -> AURPackage: def aur_package_ahriman() -> AURPackage:
@ -123,6 +145,18 @@ def configuration(resource_path_root: Path) -> Configuration:
return Configuration.from_path(path=path, architecture="x86_64", quiet=False) return Configuration.from_path(path=path, architecture="x86_64", quiet=False)
@pytest.fixture
def database(configuration: Configuration) -> SQLite:
"""
database fixture
:param: configuration: configuration fixture
:return: database test instance
"""
database = SQLite.load(configuration)
yield database
database.path.unlink()
@pytest.fixture @pytest.fixture
def package_ahriman(package_description_ahriman: PackageDescription) -> Package: def package_ahriman(package_description_ahriman: PackageDescription) -> Package:
""" """
@ -267,12 +301,13 @@ def user() -> User:
@pytest.fixture @pytest.fixture
def watcher(configuration: Configuration, mocker: MockerFixture) -> Watcher: def watcher(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> Watcher:
""" """
package status watcher fixture package status watcher fixture
:param configuration: configuration fixture :param configuration: configuration fixture
:param database: database fixture
:param mocker: mocker object :param mocker: mocker object
:return: package status watcher test instance :return: package status watcher test instance
""" """
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
return Watcher("x86_64", configuration) return Watcher("x86_64", configuration, database)

View File

@ -3,24 +3,27 @@ import pytest
from ahriman.core.auth.mapping import Mapping from ahriman.core.auth.mapping import Mapping
from ahriman.core.auth.oauth import OAuth from ahriman.core.auth.oauth import OAuth
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
@pytest.fixture @pytest.fixture
def mapping(configuration: Configuration) -> Mapping: def mapping(configuration: Configuration, database: SQLite) -> Mapping:
""" """
auth provider fixture auth provider fixture
:param configuration: configuration fixture :param configuration: configuration fixture
:param database: database fixture
:return: auth service instance :return: auth service instance
""" """
return Mapping(configuration) return Mapping(configuration, database)
@pytest.fixture @pytest.fixture
def oauth(configuration: Configuration) -> OAuth: def oauth(configuration: Configuration, database: SQLite) -> OAuth:
""" """
OAuth provider fixture OAuth provider fixture
:param configuration: configuration fixture :param configuration: configuration fixture
:param database: database fixture
:return: OAuth2 service instance :return: OAuth2 service instance
""" """
configuration.set("web", "address", "https://example.com") configuration.set("web", "address", "https://example.com")
return OAuth(configuration) return OAuth(configuration, database)

View File

@ -1,10 +1,8 @@
import pytest
from ahriman.core.auth.auth import Auth from ahriman.core.auth.auth import Auth
from ahriman.core.auth.mapping import Mapping from ahriman.core.auth.mapping import Mapping
from ahriman.core.auth.oauth import OAuth from ahriman.core.auth.oauth import OAuth
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import DuplicateUser from ahriman.core.database.sqlite import SQLite
from ahriman.models.user import User from ahriman.models.user import User
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
@ -17,88 +15,42 @@ def test_auth_control(auth: Auth) -> None:
assert "button" in auth.auth_control # I think it should be button assert "button" in auth.auth_control # I think it should be button
def test_load_dummy(configuration: Configuration) -> None: def test_load_dummy(configuration: Configuration, database: SQLite) -> None:
""" """
must load dummy validator if authorization is not enabled must load dummy validator if authorization is not enabled
""" """
configuration.set_option("auth", "target", "disabled") configuration.set_option("auth", "target", "disabled")
auth = Auth.load(configuration) auth = Auth.load(configuration, database)
assert isinstance(auth, Auth) assert isinstance(auth, Auth)
def test_load_dummy_empty(configuration: Configuration) -> None: def test_load_dummy_empty(configuration: Configuration, database: SQLite) -> None:
""" """
must load dummy validator if no option set must load dummy validator if no option set
""" """
auth = Auth.load(configuration) auth = Auth.load(configuration, database)
assert isinstance(auth, Auth) assert isinstance(auth, Auth)
def test_load_mapping(configuration: Configuration) -> None: def test_load_mapping(configuration: Configuration, database: SQLite) -> None:
""" """
must load mapping validator if option set must load mapping validator if option set
""" """
configuration.set_option("auth", "target", "configuration") configuration.set_option("auth", "target", "configuration")
auth = Auth.load(configuration) auth = Auth.load(configuration, database)
assert isinstance(auth, Mapping) assert isinstance(auth, Mapping)
def test_load_oauth(configuration: Configuration) -> None: def test_load_oauth(configuration: Configuration, database: SQLite) -> None:
""" """
must load OAuth2 validator if option set must load OAuth2 validator if option set
""" """
configuration.set_option("auth", "target", "oauth") configuration.set_option("auth", "target", "oauth")
configuration.set_option("web", "address", "https://example.com") configuration.set_option("web", "address", "https://example.com")
auth = Auth.load(configuration) auth = Auth.load(configuration, database)
assert isinstance(auth, OAuth) assert isinstance(auth, OAuth)
def test_get_users(mapping: Auth, configuration: Configuration) -> None:
"""
must return valid user list
"""
user_write = User("user_write", "pwd_write", UserAccess.Write)
write_section = Configuration.section_name("auth", user_write.access.value)
configuration.set_option(write_section, user_write.username, user_write.password)
user_read = User("user_read", "pwd_read", UserAccess.Read)
read_section = Configuration.section_name("auth", user_read.access.value)
configuration.set_option(read_section, user_read.username, user_read.password)
user_read = User("user_read", "pwd_read", UserAccess.Read)
read_section = Configuration.section_name("auth", user_read.access.value)
configuration.set_option(read_section, user_read.username, user_read.password)
users = mapping.get_users(configuration)
expected = {user_write.username: user_write, user_read.username: user_read}
assert users == expected
def test_get_users_normalized(mapping: Auth, configuration: Configuration) -> None:
"""
must return user list with normalized usernames in keys
"""
user = User("UsEr", "pwd_read", UserAccess.Read)
read_section = Configuration.section_name("auth", user.access.value)
configuration.set_option(read_section, user.username, user.password)
users = mapping.get_users(configuration)
expected = user.username.lower()
assert expected in users
assert users[expected].username == expected
def test_get_users_duplicate(mapping: Auth, configuration: Configuration, user: User) -> None:
"""
must raise exception on duplicate username
"""
write_section = Configuration.section_name("auth", UserAccess.Write.value)
configuration.set_option(write_section, user.username, user.password)
read_section = Configuration.section_name("auth", UserAccess.Read.value)
configuration.set_option(read_section, user.username, user.password)
with pytest.raises(DuplicateUser):
mapping.get_users(configuration)
async def test_check_credentials(auth: Auth, user: User) -> None: async def test_check_credentials(auth: Auth, user: User) -> None:
""" """
must pass any credentials must pass any credentials

View File

@ -1,15 +1,17 @@
from pytest_mock import MockerFixture
from ahriman.core.auth.mapping import Mapping from ahriman.core.auth.mapping import Mapping
from ahriman.models.user import User from ahriman.models.user import User
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
async def test_check_credentials(mapping: Mapping, user: User) -> None: async def test_check_credentials(mapping: Mapping, user: User, mocker: MockerFixture) -> None:
""" """
must return true for valid credentials must return true for valid credentials
""" """
current_password = user.password current_password = user.password
user.password = user.hash_password(mapping.salt) user = user.hash_password(mapping.salt)
mapping._users[user.username] = user mocker.patch("ahriman.core.database.sqlite.SQLite.user_get", return_value=user)
assert await mapping.check_credentials(user.username, current_password) assert await mapping.check_credentials(user.username, current_password)
# here password is hashed so it is invalid # here password is hashed so it is invalid
assert not await mapping.check_credentials(user.username, user.password) assert not await mapping.check_credentials(user.username, user.password)
@ -31,19 +33,19 @@ async def test_check_credentials_unknown(mapping: Mapping, user: User) -> None:
assert not await mapping.check_credentials(user.username, user.password) assert not await mapping.check_credentials(user.username, user.password)
def test_get_user(mapping: Mapping, user: User) -> None: def test_get_user(mapping: Mapping, user: User, mocker: MockerFixture) -> None:
""" """
must return user from storage by username must return user from storage by username
""" """
mapping._users[user.username] = user mocker.patch("ahriman.core.database.sqlite.SQLite.user_get", return_value=user)
assert mapping.get_user(user.username) == user assert mapping.get_user(user.username) == user
def test_get_user_normalized(mapping: Mapping, user: User) -> None: def test_get_user_normalized(mapping: Mapping, user: User, mocker: MockerFixture) -> None:
""" """
must return user from storage by username case-insensitive must return user from storage by username case-insensitive
""" """
mapping._users[user.username] = user mocker.patch("ahriman.core.database.sqlite.SQLite.user_get", return_value=user)
assert mapping.get_user(user.username.upper()) == user assert mapping.get_user(user.username.upper()) == user
@ -54,20 +56,27 @@ def test_get_user_unknown(mapping: Mapping, user: User) -> None:
assert mapping.get_user(user.username) is None assert mapping.get_user(user.username) is None
async def test_known_username(mapping: Mapping, user: User) -> None: async def test_known_username(mapping: Mapping, user: User, mocker: MockerFixture) -> None:
""" """
must allow only known users must allow only known users
""" """
mapping._users[user.username] = user mocker.patch("ahriman.core.database.sqlite.SQLite.user_get", return_value=user)
assert await mapping.known_username(user.username) assert await mapping.known_username(user.username)
async def test_known_username_unknown(mapping: Mapping, user: User, mocker: MockerFixture) -> None:
"""
must not allow only known users
"""
assert not await mapping.known_username(None) assert not await mapping.known_username(None)
mocker.patch("ahriman.core.database.sqlite.SQLite.user_get", return_value=None)
assert not await mapping.known_username(user.password) assert not await mapping.known_username(user.password)
async def test_verify_access(mapping: Mapping, user: User) -> None: async def test_verify_access(mapping: Mapping, user: User, mocker: MockerFixture) -> None:
""" """
must verify user access must verify user access
""" """
mapping._users[user.username] = user mocker.patch("ahriman.core.database.sqlite.SQLite.user_get", return_value=user)
assert await mapping.verify_access(user.username, user.access, None) assert await mapping.verify_access(user.username, user.access, None)
assert not await mapping.verify_access(user.username, UserAccess.Write, None) assert not await mapping.verify_access(user.username, UserAccess.Write, None)

View File

@ -22,16 +22,25 @@ def test_add(mocker: MockerFixture) -> None:
exception=None, cwd=local, logger=pytest.helpers.anyvar(int)) exception=None, cwd=local, logger=pytest.helpers.anyvar(int))
def test_add_skip(mocker: MockerFixture) -> None:
"""
must skip addition of files to index if no fiels found
"""
mocker.patch("pathlib.Path.glob", return_value=[])
check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output")
Sources.add(Path("local"), "pattern1")
check_output_mock.assert_not_called()
def test_diff(mocker: MockerFixture) -> None: def test_diff(mocker: MockerFixture) -> None:
""" """
must calculate diff must calculate diff
""" """
write_mock = mocker.patch("pathlib.Path.write_text")
check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output") check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output")
local = Path("local") local = Path("local")
Sources.diff(local, Path("patch")) assert Sources.diff(local)
write_mock.assert_called_once_with(pytest.helpers.anyvar(int))
check_output_mock.assert_called_once_with("git", "diff", check_output_mock.assert_called_once_with("git", "diff",
exception=None, cwd=local, logger=pytest.helpers.anyvar(int)) exception=None, cwd=local, logger=pytest.helpers.anyvar(int))
@ -142,51 +151,34 @@ def test_load(mocker: MockerFixture) -> None:
fetch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.fetch") fetch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.fetch")
patch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.patch_apply") patch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.patch_apply")
Sources.load(Path("local"), "remote", Path("patches")) Sources.load(Path("local"), "remote", "patch")
fetch_mock.assert_called_once_with(Path("local"), "remote") fetch_mock.assert_called_once_with(Path("local"), "remote")
patch_mock.assert_called_once_with(Path("local"), Path("patches")) patch_mock.assert_called_once_with(Path("local"), "patch")
def test_load_no_patch(mocker: MockerFixture) -> None:
"""
must load packages sources correctly without patches
"""
mocker.patch("ahriman.core.build_tools.sources.Sources.fetch")
patch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.patch_apply")
Sources.load(Path("local"), "remote", None)
patch_mock.assert_not_called()
def test_patch_apply(mocker: MockerFixture) -> None: def test_patch_apply(mocker: MockerFixture) -> None:
""" """
must apply patches if any must apply patches if any
""" """
mocker.patch("pathlib.Path.is_dir", return_value=True)
glob_mock = mocker.patch("pathlib.Path.glob", return_value=[Path("01.patch"), Path("02.patch")])
check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output") check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output")
local = Path("local") local = Path("local")
Sources.patch_apply(local, Path("patches")) Sources.patch_apply(local, "patches")
glob_mock.assert_called_once_with("*.patch") check_output_mock.assert_called_once_with(
check_output_mock.assert_has_calls([ "git", "apply", "--ignore-space-change", "--ignore-whitespace",
mock.call("git", "apply", "--ignore-space-change", "--ignore-whitespace", "01.patch", exception=None, cwd=local, input_data="patches", logger=pytest.helpers.anyvar(int)
exception=None, cwd=local, logger=pytest.helpers.anyvar(int)), )
mock.call("git", "apply", "--ignore-space-change", "--ignore-whitespace", "02.patch",
exception=None, cwd=local, logger=pytest.helpers.anyvar(int)),
])
def test_patch_apply_no_dir(mocker: MockerFixture) -> None:
"""
must not fail if no patches directory exists
"""
mocker.patch("pathlib.Path.is_dir", return_value=False)
glob_mock = mocker.patch("pathlib.Path.glob")
Sources.patch_apply(Path("local"), Path("patches"))
glob_mock.assert_not_called()
def test_patch_apply_no_patches(mocker: MockerFixture) -> None:
"""
must not fail if no patches exist
"""
mocker.patch("pathlib.Path.is_dir", return_value=True)
mocker.patch("pathlib.Path.glob", return_value=[])
check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output")
Sources.patch_apply(Path("local"), Path("patches"))
check_output_mock.assert_not_called()
def test_patch_create(mocker: MockerFixture) -> None: def test_patch_create(mocker: MockerFixture) -> None:
@ -196,6 +188,15 @@ def test_patch_create(mocker: MockerFixture) -> None:
add_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.add") add_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.add")
diff_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.diff") diff_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.diff")
Sources.patch_create(Path("local"), Path("patch"), "glob") Sources.patch_create(Path("local"), "glob")
add_mock.assert_called_once_with(Path("local"), "glob") add_mock.assert_called_once_with(Path("local"), "glob")
diff_mock.assert_called_once_with(Path("local"), Path("patch")) diff_mock.assert_called_once_with(Path("local"))
def test_patch_create_with_newline(mocker: MockerFixture) -> None:
"""
created patch must have new line at the end
"""
mocker.patch("ahriman.core.build_tools.sources.Sources.add")
mocker.patch("ahriman.core.build_tools.sources.Sources.diff", return_value="diff")
assert Sources.patch_create(Path("local"), "glob").endswith("\n")

View File

@ -1,6 +1,8 @@
from pathlib import Path
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from ahriman.core.build_tools.task import Task from ahriman.core.build_tools.task import Task
from ahriman.core.database.sqlite import SQLite
def test_build(task_ahriman: Task, mocker: MockerFixture) -> None: def test_build(task_ahriman: Task, mocker: MockerFixture) -> None:
@ -8,11 +10,11 @@ def test_build(task_ahriman: Task, mocker: MockerFixture) -> None:
must build package must build package
""" """
check_output_mock = mocker.patch("ahriman.core.build_tools.task.Task._check_output") check_output_mock = mocker.patch("ahriman.core.build_tools.task.Task._check_output")
task_ahriman.build() task_ahriman.build(Path("ahriman"))
check_output_mock.assert_called() check_output_mock.assert_called()
def test_init_with_cache(task_ahriman: Task, mocker: MockerFixture) -> None: def test_init_with_cache(task_ahriman: Task, database: SQLite, mocker: MockerFixture) -> None:
""" """
must copy tree instead of fetch must copy tree instead of fetch
""" """
@ -20,5 +22,5 @@ def test_init_with_cache(task_ahriman: Task, mocker: MockerFixture) -> None:
mocker.patch("ahriman.core.build_tools.sources.Sources.load") mocker.patch("ahriman.core.build_tools.sources.Sources.load")
copytree_mock = mocker.patch("shutil.copytree") copytree_mock = mocker.patch("shutil.copytree")
task_ahriman.init(None) task_ahriman.init(Path("ahriman"), database)
copytree_mock.assert_called_once() # we do not check full command here, sorry copytree_mock.assert_called_once() # we do not check full command here, sorry

View File

@ -0,0 +1,13 @@
import pytest
from sqlite3 import Connection
from unittest.mock import MagicMock
@pytest.fixture
def connection() -> Connection:
"""
mock object for sqlite3 connection
:return: sqlite3 connection test instance
"""
return MagicMock()

View File

@ -0,0 +1,33 @@
from pytest_mock import MockerFixture
from sqlite3 import Connection
from ahriman.core.configuration import Configuration
from ahriman.core.database.data import migrate_data
from ahriman.models.migration_result import MigrationResult
from ahriman.models.repository_paths import RepositoryPaths
def test_migrate_data_initial(connection: Connection, configuration: Configuration,
repository_paths: RepositoryPaths, mocker: MockerFixture) -> None:
"""
must perform initial migration
"""
packages = mocker.patch("ahriman.core.database.data.migrate_package_statuses")
users = mocker.patch("ahriman.core.database.data.migrate_users_data")
migrate_data(MigrationResult(old_version=0, new_version=900), connection, configuration, repository_paths)
packages.assert_called_once_with(connection, repository_paths)
users.assert_called_once_with(connection, configuration)
def test_migrate_data_skip(connection: Connection, configuration: Configuration,
repository_paths: RepositoryPaths, mocker: MockerFixture) -> None:
"""
must not migrate data if version is up-to-date
"""
packages = mocker.patch("ahriman.core.database.data.migrate_package_statuses")
users = mocker.patch("ahriman.core.database.data.migrate_users_data")
migrate_data(MigrationResult(old_version=900, new_version=900), connection, configuration, repository_paths)
packages.assert_not_called()
users.assert_not_called()

View File

@ -0,0 +1,43 @@
import pytest
from pytest_mock import MockerFixture
from sqlite3 import Connection
from unittest import mock
from ahriman.core.database.data import migrate_package_statuses
from ahriman.models.package import Package
from ahriman.models.repository_paths import RepositoryPaths
def test_migrate_package_statuses(connection: Connection, package_ahriman: Package, repository_paths: RepositoryPaths,
mocker: MockerFixture) -> None:
"""
must migrate packages to database
"""
response = {"packages": [pytest.helpers.get_package_status_extended(package_ahriman)]}
mocker.patch("pathlib.Path.is_file", return_value=True)
mocker.patch("pathlib.Path.open")
mocker.patch("json.load", return_value=response)
unlink_mock = mocker.patch("pathlib.Path.unlink")
migrate_package_statuses(connection, repository_paths)
unlink_mock.assert_called_once_with()
connection.execute.assert_has_calls([
mock.call(pytest.helpers.anyvar(str, strict=True), pytest.helpers.anyvar(int)),
mock.call(pytest.helpers.anyvar(str, strict=True), pytest.helpers.anyvar(int)),
])
connection.executemany.assert_has_calls([
mock.call(pytest.helpers.anyvar(str, strict=True), pytest.helpers.anyvar(int)),
])
connection.commit.assert_called_once_with()
def test_migrate_package_statuses_skip(connection: Connection, repository_paths: RepositoryPaths,
mocker: MockerFixture) -> None:
"""
must skip packages migration if no cache file found
"""
mocker.patch("pathlib.Path.is_file", return_value=False)
migrate_package_statuses(connection, repository_paths)
connection.commit.assert_not_called()

View File

@ -0,0 +1,53 @@
import pytest
from pathlib import Path
from pytest_mock import MockerFixture
from sqlite3 import Connection
from ahriman.core.database.data import migrate_patches
from ahriman.models.package import Package
from ahriman.models.repository_paths import RepositoryPaths
def test_migrate_patches(connection: Connection, repository_paths: RepositoryPaths,
package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must perform migration for patches
"""
mocker.patch("pathlib.Path.is_dir", return_value=True)
mocker.patch("pathlib.Path.is_file", return_value=True)
iterdir_mock = mocker.patch("pathlib.Path.iterdir", return_value=[Path(package_ahriman.base)])
read_mock = mocker.patch("pathlib.Path.read_text", return_value="patch")
migrate_patches(connection, repository_paths)
iterdir_mock.assert_called_once_with()
read_mock.assert_called_once_with(encoding="utf8")
connection.execute.assert_called_once_with(pytest.helpers.anyvar(str, strict=True), pytest.helpers.anyvar(int))
connection.commit.assert_called_once_with()
def test_migrate_patches_skip(connection: Connection, repository_paths: RepositoryPaths,
mocker: MockerFixture) -> None:
"""
must skip patches migration in case if no root found
"""
mocker.patch("pathlib.Path.is_dir", return_value=False)
iterdir_mock = mocker.patch("pathlib.Path.iterdir")
migrate_patches(connection, repository_paths)
iterdir_mock.assert_not_called()
def test_migrate_patches_no_patch(connection: Connection, repository_paths: RepositoryPaths,
package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must skip package if no match found
"""
mocker.patch("pathlib.Path.is_dir", return_value=True)
mocker.patch("pathlib.Path.is_file", return_value=False)
iterdir_mock = mocker.patch("pathlib.Path.iterdir", return_value=[Path(package_ahriman.base)])
read_mock = mocker.patch("pathlib.Path.read_text")
migrate_patches(connection, repository_paths)
iterdir_mock.assert_called_once_with()
read_mock.assert_not_called()

View File

@ -0,0 +1,22 @@
import pytest
from sqlite3 import Connection
from unittest import mock
from ahriman.core.configuration import Configuration
from ahriman.core.database.data import migrate_users_data
def test_migrate_users_data(connection: Connection, configuration: Configuration) -> None:
"""
must users to database
"""
configuration.set_option("auth:read", "user1", "password1")
configuration.set_option("auth:write", "user2", "password2")
migrate_users_data(connection, configuration)
connection.execute.assert_has_calls([
mock.call(pytest.helpers.anyvar(str, strict=True), pytest.helpers.anyvar(int)),
mock.call(pytest.helpers.anyvar(str, strict=True), pytest.helpers.anyvar(int)),
])
connection.commit.assert_called_once_with()

View File

@ -0,0 +1,15 @@
import pytest
from sqlite3 import Connection
from ahriman.core.database.migrations import Migrations
@pytest.fixture
def migrations(connection: Connection) -> Migrations:
"""
fixture for migrations object
:param connection: sqlite connection fixture
:return: migrations test instance
"""
return Migrations(connection)

View File

@ -0,0 +1,8 @@
from ahriman.core.database.migrations.m000_initial import steps
def test_migration_initial() -> None:
"""
migration must not be empty
"""
assert steps

View File

@ -0,0 +1,109 @@
import pytest
from pytest_mock import MockerFixture
from sqlite3 import Connection
from unittest import mock
from unittest.mock import MagicMock
from ahriman.core.database.migrations import Migrations
from ahriman.models.migration import Migration
from ahriman.models.migration_result import MigrationResult
def test_migrate(connection: Connection, mocker: MockerFixture) -> None:
"""
must perform migrations
"""
run_mock = mocker.patch("ahriman.core.database.migrations.Migrations.run")
Migrations.migrate(connection)
run_mock.assert_called_once_with()
def test_migrations(migrations: Migrations) -> None:
"""
must retrieve migrations
"""
assert migrations.migrations()
def test_run_skip(migrations: Migrations, mocker: MockerFixture) -> None:
"""
must skip migration if version is the same
"""
mocker.patch.object(MigrationResult, "is_outdated", False)
migrations.run()
migrations.connection.cursor.assert_not_called()
def test_run(migrations: Migrations, mocker: MockerFixture) -> None:
"""
must run migration
"""
cursor = MagicMock()
mocker.patch("ahriman.core.database.migrations.Migrations.user_version", return_value=0)
mocker.patch("ahriman.core.database.migrations.Migrations.migrations",
return_value=[Migration(0, "test", ["select 1"])])
migrations.connection.cursor.return_value = cursor
validate_mock = mocker.patch("ahriman.models.migration_result.MigrationResult.validate")
migrations.run()
validate_mock.assert_called_once_with()
cursor.execute.assert_has_calls([
mock.call("begin exclusive"),
mock.call("select 1"),
mock.call("pragma user_version = 1"),
mock.call("commit"),
])
cursor.close.assert_called_once_with()
def test_run_migration_exception(migrations: Migrations, mocker: MockerFixture) -> None:
"""
must rollback and close cursor on exception during migration
"""
cursor = MagicMock()
mocker.patch("logging.Logger.info", side_effect=Exception())
mocker.patch("ahriman.core.database.migrations.Migrations.user_version", return_value=0)
mocker.patch("ahriman.core.database.migrations.Migrations.migrations",
return_value=[Migration(0, "test", ["select 1"])])
mocker.patch("ahriman.models.migration_result.MigrationResult.validate")
migrations.connection.cursor.return_value = cursor
with pytest.raises(Exception):
migrations.run()
cursor.execute.assert_has_calls([
mock.call("begin exclusive"),
mock.call("rollback"),
])
cursor.close.assert_called_once_with()
def test_run_sql_exception(migrations: Migrations, mocker: MockerFixture) -> None:
"""
must close cursor on general migration error
"""
cursor = MagicMock()
cursor.execute.side_effect = Exception()
mocker.patch("ahriman.core.database.migrations.Migrations.user_version", return_value=0)
mocker.patch("ahriman.core.database.migrations.Migrations.migrations",
return_value=[Migration(0, "test", ["select 1"])])
mocker.patch("ahriman.models.migration_result.MigrationResult.validate")
migrations.connection.cursor.return_value = cursor
with pytest.raises(Exception):
migrations.run()
cursor.close.assert_called_once_with()
def test_user_version(migrations: Migrations) -> None:
"""
must correctly extract current migration version
"""
cursor = MagicMock()
cursor.fetchone.return_value = {"user_version": 42}
migrations.connection.execute.return_value = cursor
version = migrations.user_version()
migrations.connection.execute.assert_called_once_with("pragma user_version")
assert version == 42

View File

@ -0,0 +1,97 @@
from ahriman.core.database.sqlite import SQLite
from ahriman.models.user import User
from ahriman.models.user_access import UserAccess
def test_user_get_update(database: SQLite, user: User) -> None:
"""
must retrieve user from the database
"""
database.user_update(user)
assert database.user_get(user.username) == user
def test_user_list(database: SQLite, user: User) -> None:
"""
must return all users
"""
database.user_update(user)
database.user_update(User(user.password, user.username, user.access))
users = database.user_list(None, None)
assert len(users) == 2
assert user in users
assert User(user.password, user.username, user.access) in users
def test_user_list_filter_by_username(database: SQLite) -> None:
"""
must return users filtered by its id
"""
first = User("1", "", UserAccess.Read)
second = User("2", "", UserAccess.Write)
third = User("3", "", UserAccess.Read)
database.user_update(first)
database.user_update(second)
database.user_update(third)
assert database.user_list("1", None) == [first]
assert database.user_list("2", None) == [second]
assert database.user_list("3", None) == [third]
def test_user_list_filter_by_access(database: SQLite) -> None:
"""
must return users filtered by its access
"""
first = User("1", "", UserAccess.Read)
second = User("2", "", UserAccess.Write)
third = User("3", "", UserAccess.Read)
database.user_update(first)
database.user_update(second)
database.user_update(third)
users = database.user_list(None, UserAccess.Read)
assert len(users) == 2
assert first in users
assert third in users
def test_user_list_filter_by_username_access(database: SQLite) -> None:
"""
must return users filtered by its access and username
"""
first = User("1", "", UserAccess.Read)
second = User("2", "", UserAccess.Write)
third = User("3", "", UserAccess.Read)
database.user_update(first)
database.user_update(second)
database.user_update(third)
assert database.user_list("1", UserAccess.Read) == [first]
assert not database.user_list("1", UserAccess.Write)
def test_user_remove_update(database: SQLite, user: User) -> None:
"""
must remove user from the database
"""
database.user_update(user)
database.user_remove(user.username)
assert database.user_get(user.username) is None
def test_user_update(database: SQLite, user: User) -> None:
"""
must update user in the database
"""
database.user_update(user)
assert database.user_get(user.username) == user
new_user = user.hash_password("salt")
new_user.access = UserAccess.Write
database.user_update(new_user)
assert database.user_get(new_user.username) == new_user

View File

@ -0,0 +1,45 @@
from ahriman.core.database.sqlite import SQLite
from ahriman.models.package import Package
def test_build_queue_insert_clear(database: SQLite, package_ahriman: Package, package_python_schedule: Package) -> None:
"""
must clear all packages from queue
"""
database.build_queue_insert(package_ahriman)
database.build_queue_insert(package_python_schedule)
database.build_queue_clear(None)
assert not database.build_queue_get()
def test_build_queue_insert_clear_specific(database: SQLite, package_ahriman: Package,
package_python_schedule: Package) -> None:
"""
must remove only specified package from the queue
"""
database.build_queue_insert(package_ahriman)
database.build_queue_insert(package_python_schedule)
database.build_queue_clear(package_python_schedule.base)
assert database.build_queue_get() == [package_ahriman]
def test_build_queue_insert_get(database: SQLite, package_ahriman: Package) -> None:
"""
must insert and get package from the database
"""
database.build_queue_insert(package_ahriman)
assert database.build_queue_get() == [package_ahriman]
def test_build_queue_insert(database: SQLite, package_ahriman: Package) -> None:
"""
must update user in the database
"""
database.build_queue_insert(package_ahriman)
assert database.build_queue_get() == [package_ahriman]
package_ahriman.version = "42"
database.build_queue_insert(package_ahriman)
assert database.build_queue_get() == [package_ahriman]

View File

@ -0,0 +1,39 @@
import sqlite3
from pytest_mock import MockerFixture
from unittest.mock import MagicMock
from ahriman.core.database.sqlite import SQLite
def test_factory(database: SQLite) -> None:
"""
must convert response to dictionary
"""
result = database.with_connection(lambda conn: conn.execute("select 1 as result").fetchone())
assert isinstance(result, dict)
assert result["result"] == 1
def test_with_connection(database: SQLite, mocker: MockerFixture) -> None:
"""
must run query inside connection
"""
connection_mock = MagicMock()
connect_mock = mocker.patch("sqlite3.connect", return_value=connection_mock)
database.with_connection(lambda conn: conn.execute("select 1"))
connect_mock.assert_called_once_with(database.path, detect_types=sqlite3.PARSE_DECLTYPES)
connection_mock.__enter__().commit.assert_not_called()
def test_with_connection_with_commit(database: SQLite, mocker: MockerFixture) -> None:
"""
must run query inside connection and commit after
"""
connection_mock = MagicMock()
connection_mock.commit.return_value = 42
mocker.patch("sqlite3.connect", return_value=connection_mock)
database.with_connection(lambda conn: conn.execute("select 1"), commit=True)
connection_mock.__enter__().commit.assert_called_once_with()

View File

@ -0,0 +1,168 @@
import pytest
from pytest_mock import MockerFixture
from sqlite3 import Connection
from unittest import mock
from ahriman.core.database.sqlite import SQLite
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.package import Package
def test_package_remove_package_base(database: SQLite, connection: Connection) -> None:
"""
must remove package base
"""
database._package_remove_package_base(connection, "package")
connection.execute.assert_has_calls([
mock.call(pytest.helpers.anyvar(str, strict=True), {"package_base": "package"}),
mock.call(pytest.helpers.anyvar(str, strict=True), {"package_base": "package"}),
])
def test_package_remove_packages(database: SQLite, connection: Connection, package_ahriman: Package) -> None:
"""
must remove packages belong to base
"""
database._package_remove_packages(connection, package_ahriman.base, package_ahriman.packages.keys())
connection.execute.assert_called_once_with(
pytest.helpers.anyvar(str, strict=True), {"package_base": package_ahriman.base})
connection.executemany.assert_called_once_with(pytest.helpers.anyvar(str, strict=True), [])
def test_package_update_insert_base(database: SQLite, connection: Connection, package_ahriman: Package) -> None:
"""
must insert base package
"""
database._package_update_insert_base(connection, package_ahriman)
connection.execute.assert_called_once_with(pytest.helpers.anyvar(str, strict=True), pytest.helpers.anyvar(int))
def test_package_update_insert_packages(database: SQLite, connection: Connection, package_ahriman: Package) -> None:
"""
must insert single packages
"""
database._package_update_insert_packages(connection, package_ahriman)
connection.executemany(pytest.helpers.anyvar(str, strict=True), pytest.helpers.anyvar(int))
def test_package_update_insert_status(database: SQLite, connection: Connection, package_ahriman: Package) -> None:
"""
must insert single package status
"""
database._package_update_insert_status(connection, package_ahriman.base, BuildStatus())
connection.execute(pytest.helpers.anyvar(str, strict=True), pytest.helpers.anyvar(int))
def test_packages_get_select_package_bases(database: SQLite, connection: Connection) -> None:
"""
must select all bases
"""
database._packages_get_select_package_bases(connection)
connection.execute(pytest.helpers.anyvar(str, strict=True))
def test_packages_get_select_packages(database: SQLite, connection: Connection, package_ahriman: Package) -> None:
"""
must select all packages
"""
database._packages_get_select_packages(connection, {package_ahriman.base: package_ahriman})
connection.execute(pytest.helpers.anyvar(str, strict=True))
def test_packages_get_select_packages_skip(database: SQLite, connection: Connection, package_ahriman: Package) -> None:
"""
must skip unknown packages
"""
view = {"package_base": package_ahriman.base}
for package, properties in package_ahriman.packages.items():
view.update({"package": package})
view.update(properties.view())
connection.execute.return_value = [{"package_base": "random name"}, view]
database._packages_get_select_packages(connection, {package_ahriman.base: package_ahriman})
def test_packages_get_select_statuses(database: SQLite, connection: Connection) -> None:
"""
must select all statuses
"""
database._packages_get_select_statuses(connection)
connection.execute(pytest.helpers.anyvar(str, strict=True))
def test_package_remove(database: SQLite, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must totally remove package from the database
"""
remove_package_mock = mocker.patch("ahriman.core.database.sqlite.SQLite._package_remove_package_base")
remove_packages_mock = mocker.patch("ahriman.core.database.sqlite.SQLite._package_remove_packages")
database.package_remove(package_ahriman.base)
remove_package_mock.assert_called_once_with(pytest.helpers.anyvar(int), package_ahriman.base)
remove_packages_mock.assert_called_once_with(pytest.helpers.anyvar(int), package_ahriman.base, [])
def test_package_update(database: SQLite, package_ahriman: Package, mocker: MockerFixture):
"""
must update package status
"""
status = BuildStatus()
insert_base_mock = mocker.patch("ahriman.core.database.sqlite.SQLite._package_update_insert_base")
insert_status_mock = mocker.patch("ahriman.core.database.sqlite.SQLite._package_update_insert_status")
insert_packages_mock = mocker.patch("ahriman.core.database.sqlite.SQLite._package_update_insert_packages")
remove_packages_mock = mocker.patch("ahriman.core.database.sqlite.SQLite._package_remove_packages")
database.package_update(package_ahriman, status)
insert_base_mock.assert_called_once_with(pytest.helpers.anyvar(int), package_ahriman)
insert_status_mock.assert_called_once_with(pytest.helpers.anyvar(int), package_ahriman.base, status)
insert_packages_mock.assert_called_once_with(pytest.helpers.anyvar(int), package_ahriman)
remove_packages_mock.assert_called_once_with(
pytest.helpers.anyvar(int), package_ahriman.base, package_ahriman.packages.keys())
def test_packages_get(database: SQLite, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must return all packages
"""
select_bases_mock = mocker.patch("ahriman.core.database.sqlite.SQLite._packages_get_select_package_bases",
return_value={package_ahriman.base: package_ahriman})
select_packages_mock = mocker.patch("ahriman.core.database.sqlite.SQLite._packages_get_select_packages")
select_statuses_mock = mocker.patch("ahriman.core.database.sqlite.SQLite._packages_get_select_statuses")
database.packages_get()
select_bases_mock.assert_called_once_with(pytest.helpers.anyvar(int))
select_statuses_mock.assert_called_once_with(pytest.helpers.anyvar(int))
select_packages_mock.assert_called_once_with(pytest.helpers.anyvar(int), {package_ahriman.base: package_ahriman})
def test_package_update_get(database: SQLite, package_ahriman: Package) -> None:
"""
must insert and retrieve package
"""
status = BuildStatus()
database.package_update(package_ahriman, status)
assert next((db_package, db_status)
for db_package, db_status in database.packages_get()
if db_package.base == package_ahriman.base) == (package_ahriman, status)
def test_package_update_remove_get(database: SQLite, package_ahriman: Package) -> None:
"""
must insert, remove and retrieve package
"""
status = BuildStatus()
database.package_update(package_ahriman, status)
database.package_remove(package_ahriman.base)
assert not database.packages_get()
def test_package_update_update(database: SQLite, package_ahriman: Package) -> None:
"""
must perform update for existing package
"""
database.package_update(package_ahriman, BuildStatus())
database.package_update(package_ahriman, BuildStatus(BuildStatusEnum.Failed))
assert next(db_status.status
for db_package, db_status in database.packages_get()
if db_package.base == package_ahriman.base) == BuildStatusEnum.Failed

View File

@ -0,0 +1,55 @@
from ahriman.core.database.sqlite import SQLite
from ahriman.models.package import Package
def test_patches_get_insert(database: SQLite, package_ahriman: Package, package_python_schedule: Package) -> None:
"""
must insert patch to database
"""
database.patches_insert(package_ahriman.base, "patch_1")
database.patches_insert(package_python_schedule.base, "patch_2")
assert database.patches_get(package_ahriman.base) == "patch_1"
assert not database.build_queue_get()
def test_patches_list(database: SQLite, package_ahriman: Package, package_python_schedule: Package) -> None:
"""
must list all patches
"""
database.patches_insert(package_ahriman.base, "patch1")
database.patches_insert(package_python_schedule.base, "patch2")
assert database.patches_list(None) == {package_ahriman.base: "patch1", package_python_schedule.base: "patch2"}
def test_patches_list_filter(database: SQLite, package_ahriman: Package, package_python_schedule: Package) -> None:
"""
must list all patches filtered by package name (same as get)
"""
database.patches_insert(package_ahriman.base, "patch1")
database.patches_insert(package_python_schedule.base, "patch2")
assert database.patches_list(package_ahriman.base) == {package_ahriman.base: "patch1"}
assert database.patches_list(package_python_schedule.base) == {package_python_schedule.base: "patch2"}
def test_patches_insert_remove(database: SQLite, package_ahriman: Package, package_python_schedule: Package) -> None:
"""
must remove patch from database
"""
database.patches_insert(package_ahriman.base, "patch_1")
database.patches_insert(package_python_schedule.base, "patch_2")
database.patches_remove(package_ahriman.base)
assert database.patches_get(package_ahriman.base) is None
database.patches_insert(package_python_schedule.base, "patch_2")
def test_patches_insert_insert(database: SQLite, package_ahriman: Package) -> None:
"""
must update patch in database
"""
database.patches_insert(package_ahriman.base, "patch_1")
assert database.patches_get(package_ahriman.base) == "patch_1"
database.patches_insert(package_ahriman.base, "patch_2")
assert database.patches_get(package_ahriman.base) == "patch_2"

View File

@ -0,0 +1,28 @@
import pytest
from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
def test_load(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must correctly load instance
"""
init_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.init")
SQLite.load(configuration)
init_mock.assert_called_once_with(configuration)
def test_init(database: SQLite, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run migrations on init
"""
migrate_schema_mock = mocker.patch("ahriman.core.database.migrations.Migrations.migrate")
migrate_data_mock = mocker.patch("ahriman.core.database.sqlite.migrate_data")
database.init(configuration)
migrate_schema_mock.assert_called_once_with(pytest.helpers.anyvar(int))
migrate_data_mock.assert_called_once_with(
pytest.helpers.anyvar(int), pytest.helpers.anyvar(int), configuration, configuration.repository_paths)

View File

@ -6,9 +6,11 @@ from ahriman.core.formatters.package_printer import PackagePrinter
from ahriman.core.formatters.status_printer import StatusPrinter from ahriman.core.formatters.status_printer import StatusPrinter
from ahriman.core.formatters.string_printer import StringPrinter from ahriman.core.formatters.string_printer import StringPrinter
from ahriman.core.formatters.update_printer import UpdatePrinter from ahriman.core.formatters.update_printer import UpdatePrinter
from ahriman.core.formatters.user_printer import UserPrinter
from ahriman.models.aur_package import AURPackage from ahriman.models.aur_package import AURPackage
from ahriman.models.build_status import BuildStatus from ahriman.models.build_status import BuildStatus
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.user import User
@pytest.fixture @pytest.fixture
@ -65,3 +67,13 @@ def update_printer(package_ahriman: Package) -> UpdatePrinter:
:return: build status printer test instance :return: build status printer test instance
""" """
return UpdatePrinter(package_ahriman, None) return UpdatePrinter(package_ahriman, None)
@pytest.fixture
def user_printer(user: User) -> UserPrinter:
"""
fixture for user printer
:param user: user fixture
:return: user printer test instance
"""
return UserPrinter(user)

View File

@ -0,0 +1,15 @@
from ahriman.core.formatters.user_printer import UserPrinter
def test_properties(user_printer: UserPrinter) -> None:
"""
must return non empty properties list
"""
assert user_printer.properties()
def test_title(user_printer: UserPrinter) -> None:
"""
must return non empty title
"""
assert user_printer.title() is not None

View File

@ -3,6 +3,7 @@ import pytest
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
from ahriman.core.repository import Repository from ahriman.core.repository import Repository
from ahriman.core.repository.cleaner import Cleaner from ahriman.core.repository.cleaner import Cleaner
from ahriman.core.repository.executor import Executor from ahriman.core.repository.executor import Executor
@ -11,68 +12,71 @@ from ahriman.core.repository.update_handler import UpdateHandler
@pytest.fixture @pytest.fixture
def cleaner(configuration: Configuration, mocker: MockerFixture) -> Cleaner: def cleaner(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> Cleaner:
""" """
fixture for cleaner fixture for cleaner
:param configuration: configuration fixture :param configuration: configuration fixture
:param database: database fixture
:param mocker: mocker object :param mocker: mocker object
:return: cleaner test instance :return: cleaner test instance
""" """
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
return Cleaner("x86_64", configuration, no_report=True, unsafe=False) return Cleaner("x86_64", configuration, database, no_report=True, unsafe=False)
@pytest.fixture @pytest.fixture
def executor(configuration: Configuration, mocker: MockerFixture) -> Executor: def executor(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> Executor:
""" """
fixture for executor fixture for executor
:param configuration: configuration fixture :param configuration: configuration fixture
:param database: database fixture
:param mocker: mocker object :param mocker: mocker object
:return: executor test instance :return: executor test instance
""" """
mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_build")
mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_cache") mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_cache")
mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_chroot") 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") mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_packages")
mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_queue")
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
return Executor("x86_64", configuration, no_report=True, unsafe=False) return Executor("x86_64", configuration, database, no_report=True, unsafe=False)
@pytest.fixture @pytest.fixture
def repository(configuration: Configuration, mocker: MockerFixture) -> Repository: def repository(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> Repository:
""" """
fixture for repository fixture for repository
:param configuration: configuration fixture :param configuration: configuration fixture
:param database: database fixture
:param mocker: mocker object :param mocker: mocker object
:return: repository test instance :return: repository test instance
""" """
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
return Repository("x86_64", configuration, no_report=True, unsafe=False) return Repository("x86_64", configuration, database, no_report=True, unsafe=False)
@pytest.fixture @pytest.fixture
def properties(configuration: Configuration) -> Properties: def properties(configuration: Configuration, database: SQLite) -> Properties:
""" """
fixture for properties fixture for properties
:param configuration: configuration fixture :param configuration: configuration fixture
:param database: database fixture
:return: properties test instance :return: properties test instance
""" """
return Properties("x86_64", configuration, no_report=True, unsafe=False) return Properties("x86_64", configuration, database, no_report=True, unsafe=False)
@pytest.fixture @pytest.fixture
def update_handler(configuration: Configuration, mocker: MockerFixture) -> UpdateHandler: def update_handler(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> UpdateHandler:
""" """
fixture for update handler fixture for update handler
:param configuration: configuration fixture :param configuration: configuration fixture
:param database: database fixture
:param mocker: mocker object :param mocker: mocker object
:return: update handler test instance :return: update handler test instance
""" """
mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_build")
mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_cache") mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_cache")
mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_chroot") 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") mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_packages")
mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_queue")
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
return UpdateHandler("x86_64", configuration, no_report=True, unsafe=False) return UpdateHandler("x86_64", configuration, database, no_report=True, unsafe=False)

View File

@ -36,15 +36,6 @@ def test_packages_built(cleaner: Cleaner) -> None:
cleaner.packages_built() cleaner.packages_built()
def test_clear_build(cleaner: Cleaner, mocker: MockerFixture) -> None:
"""
must remove directories with sources
"""
_mock_clear(mocker)
cleaner.clear_build()
_mock_clear_check()
def test_clear_cache(cleaner: Cleaner, mocker: MockerFixture) -> None: def test_clear_cache(cleaner: Cleaner, mocker: MockerFixture) -> None:
""" """
must remove every cached sources must remove every cached sources
@ -63,15 +54,6 @@ def test_clear_chroot(cleaner: Cleaner, mocker: MockerFixture) -> None:
_mock_clear_check() _mock_clear_check()
def test_clear_manual(cleaner: Cleaner, mocker: MockerFixture) -> None:
"""
must clear directory with manual packages
"""
_mock_clear(mocker)
cleaner.clear_manual()
_mock_clear_check()
def test_clear_packages(cleaner: Cleaner, mocker: MockerFixture) -> None: def test_clear_packages(cleaner: Cleaner, mocker: MockerFixture) -> None:
""" """
must delete built packages must delete built packages
@ -84,10 +66,10 @@ def test_clear_packages(cleaner: Cleaner, mocker: MockerFixture) -> None:
Path.unlink.assert_has_calls([mock.call(), mock.call(), mock.call()]) Path.unlink.assert_has_calls([mock.call(), mock.call(), mock.call()])
def test_clear_patches(cleaner: Cleaner, mocker: MockerFixture) -> None: def test_clear_queue(cleaner: Cleaner, mocker: MockerFixture) -> None:
""" """
must clear directory with patches must clear queued packages from the database
""" """
_mock_clear(mocker) clear_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.build_queue_clear")
cleaner.clear_patches() cleaner.clear_queue()
_mock_clear_check() clear_mock.assert_called_once_with(None)

View File

@ -41,9 +41,6 @@ def test_process_build(executor: Executor, package_ahriman: Package, mocker: Moc
move_mock.assert_called_once_with(Path(package_ahriman.base), executor.paths.packages / package_ahriman.base) move_mock.assert_called_once_with(Path(package_ahriman.base), executor.paths.packages / package_ahriman.base)
# must update status # must update status
status_client_mock.assert_called_once_with(package_ahriman.base) status_client_mock.assert_called_once_with(package_ahriman.base)
# must clear directory
from ahriman.core.repository.cleaner import Cleaner
Cleaner.clear_build.assert_called_once_with()
def test_process_build_failure(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None: def test_process_build_failure(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:

View File

@ -1,51 +1,52 @@
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
from ahriman.core.exceptions import UnsafeRun from ahriman.core.exceptions import UnsafeRun
from ahriman.core.repository.properties import Properties from ahriman.core.repository.properties import Properties
from ahriman.core.status.web_client import WebClient from ahriman.core.status.web_client import WebClient
def test_create_tree_on_load(configuration: Configuration, mocker: MockerFixture) -> None: def test_create_tree_on_load(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> None:
""" """
must create tree on load must create tree on load
""" """
mocker.patch("ahriman.core.repository.properties.check_user") mocker.patch("ahriman.core.repository.properties.check_user")
tree_create_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") tree_create_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
Properties("x86_64", configuration, True, False) Properties("x86_64", configuration, database, True, False)
tree_create_mock.assert_called_once_with() tree_create_mock.assert_called_once_with()
def test_create_tree_on_load_unsafe(configuration: Configuration, mocker: MockerFixture) -> None: def test_create_tree_on_load_unsafe(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> None:
""" """
must not create tree on load in case if user differs from the root owner must not create tree on load in case if user differs from the root owner
""" """
mocker.patch("ahriman.core.repository.properties.check_user", side_effect=UnsafeRun(0, 1)) mocker.patch("ahriman.core.repository.properties.check_user", side_effect=UnsafeRun(0, 1))
tree_create_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") tree_create_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
Properties("x86_64", configuration, True, False) Properties("x86_64", configuration, database, True, False)
tree_create_mock.assert_not_called() tree_create_mock.assert_not_called()
def test_create_dummy_report_client(configuration: Configuration, mocker: MockerFixture) -> None: def test_create_dummy_report_client(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> None:
""" """
must create dummy report client if report is disabled must create dummy report client if report is disabled
""" """
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
load_mock = mocker.patch("ahriman.core.status.client.Client.load") load_mock = mocker.patch("ahriman.core.status.client.Client.load")
properties = Properties("x86_64", configuration, True, False) properties = Properties("x86_64", configuration, database, True, False)
load_mock.assert_not_called() load_mock.assert_not_called()
assert not isinstance(properties.reporter, WebClient) assert not isinstance(properties.reporter, WebClient)
def test_create_full_report_client(configuration: Configuration, mocker: MockerFixture) -> None: def test_create_full_report_client(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> None:
""" """
must create load report client if report is enabled must create load report client if report is enabled
""" """
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
load_mock = mocker.patch("ahriman.core.status.client.Client.load") load_mock = mocker.patch("ahriman.core.status.client.Client.load")
Properties("x86_64", configuration, False, False) Properties("x86_64", configuration, database, False, False)
load_mock.assert_called_once_with(configuration) load_mock.assert_called_once_with(configuration)

View File

@ -168,7 +168,7 @@ def test_updates_manual_clear(update_handler: UpdateHandler, mocker: MockerFixtu
update_handler.updates_manual() update_handler.updates_manual()
from ahriman.core.repository.cleaner import Cleaner from ahriman.core.repository.cleaner import Cleaner
Cleaner.clear_manual.assert_called_once_with() Cleaner.clear_queue.assert_called_once_with()
def test_updates_manual_status_known(update_handler: UpdateHandler, package_ahriman: Package, def test_updates_manual_status_known(update_handler: UpdateHandler, package_ahriman: Package,
@ -176,9 +176,8 @@ def test_updates_manual_status_known(update_handler: UpdateHandler, package_ahri
""" """
must create record for known package via reporter must create record for known package via reporter
""" """
mocker.patch("pathlib.Path.iterdir", return_value=[package_ahriman.base]) mocker.patch("ahriman.core.database.sqlite.SQLite.build_queue_get", return_value=[package_ahriman])
mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman]) mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman])
mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman)
status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_pending") status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_pending")
update_handler.updates_manual() update_handler.updates_manual()
@ -190,9 +189,8 @@ def test_updates_manual_status_unknown(update_handler: UpdateHandler, package_ah
""" """
must create record for unknown package via reporter must create record for unknown package via reporter
""" """
mocker.patch("pathlib.Path.iterdir", return_value=[package_ahriman.base]) mocker.patch("ahriman.core.database.sqlite.SQLite.build_queue_get", return_value=[package_ahriman])
mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[]) mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[])
mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman)
status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_unknown") status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_unknown")
update_handler.updates_manual() update_handler.updates_manual()
@ -204,8 +202,6 @@ def test_updates_manual_with_failures(update_handler: UpdateHandler, package_ahr
""" """
must process manual through the packages with failure must process manual through the packages with failure
""" """
mocker.patch("pathlib.Path.iterdir", return_value=[package_ahriman.base]) mocker.patch("ahriman.core.database.sqlite.SQLite.build_queue_get", side_effect=Exception())
mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[]) mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman])
mocker.patch("ahriman.models.package.Package.load", side_effect=Exception())
assert update_handler.updates_manual() == [] assert update_handler.updates_manual() == []

View File

@ -1,33 +1,8 @@
import pytest import pytest
from typing import Any, Dict
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.status.client import Client from ahriman.core.status.client import Client
from ahriman.core.status.web_client import WebClient from ahriman.core.status.web_client import WebClient
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.package import Package
# helpers
@pytest.helpers.register
def get_package_status(package: Package) -> Dict[str, Any]:
"""
helper to extract package status from package
:param package: package object
:return: simplified package status map (with only status and view)
"""
return {"status": BuildStatusEnum.Unknown.value, "package": package.view()}
@pytest.helpers.register
def get_package_status_extended(package: Package) -> Dict[str, Any]:
"""
helper to extract package status from package
:param package: package object
:return: full package status map (with timestamped build status and view)
"""
return {"status": BuildStatus().view(), "package": package.view()}
# fixtures # fixtures
@ -47,5 +22,5 @@ def web_client(configuration: Configuration) -> WebClient:
:param configuration: configuration fixture :param configuration: configuration fixture
:return: web client test instance :return: web client test instance
""" """
configuration.set("web", "port", 8080) configuration.set("web", "port", "8080")
return WebClient(configuration) return WebClient(configuration)

View File

@ -61,13 +61,6 @@ def test_get_self(client: Client) -> None:
assert client.get_self().status == BuildStatusEnum.Unknown assert client.get_self().status == BuildStatusEnum.Unknown
def test_reload_auth(client: Client) -> None:
"""
must process auth reload without errors
"""
client.reload_auth()
def test_remove(client: Client, package_ahriman: Package) -> None: def test_remove(client: Client, package_ahriman: Package) -> None:
""" """
must process remove without errors must process remove without errors

View File

@ -6,6 +6,7 @@ from pytest_mock import MockerFixture
from unittest.mock import PropertyMock from unittest.mock import PropertyMock
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.database.sqlite import SQLite
from ahriman.core.exceptions import UnknownPackage from ahriman.core.exceptions import UnknownPackage
from ahriman.core.status.watcher import Watcher from ahriman.core.status.watcher import Watcher
from ahriman.core.status.web_client import WebClient from ahriman.core.status.web_client import WebClient
@ -13,7 +14,7 @@ from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.package import Package from ahriman.models.package import Package
def test_force_no_report(configuration: Configuration, mocker: MockerFixture) -> None: def test_force_no_report(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> None:
""" """
must force dummy report client must force dummy report client
""" """
@ -21,122 +22,12 @@ def test_force_no_report(configuration: Configuration, mocker: MockerFixture) ->
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
load_mock = mocker.patch("ahriman.core.status.client.Client.load") load_mock = mocker.patch("ahriman.core.status.client.Client.load")
watcher = Watcher("x86_64", configuration) watcher = Watcher("x86_64", configuration, database)
load_mock.assert_not_called() load_mock.assert_not_called()
assert not isinstance(watcher.repository.reporter, WebClient) assert not isinstance(watcher.repository.reporter, WebClient)
def test_cache_load(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must load state from cache
"""
response = {"packages": [pytest.helpers.get_package_status_extended(package_ahriman)]}
mocker.patch("pathlib.Path.is_file", return_value=True)
mocker.patch("pathlib.Path.open")
mocker.patch("json.load", return_value=response)
watcher.known = {package_ahriman.base: (None, None)}
watcher._cache_load()
package, status = watcher.known[package_ahriman.base]
assert package == package_ahriman
assert status.status == BuildStatusEnum.Unknown
def test_cache_load_json_error(watcher: Watcher, mocker: MockerFixture) -> None:
"""
must not fail on json errors
"""
mocker.patch("pathlib.Path.is_file", return_value=True)
mocker.patch("pathlib.Path.open")
mocker.patch("json.load", side_effect=Exception())
watcher._cache_load()
assert not watcher.known
def test_cache_load_no_file(watcher: Watcher, mocker: MockerFixture) -> None:
"""
must not fail on missing file
"""
mocker.patch("pathlib.Path.is_file", return_value=False)
watcher._cache_load()
assert not watcher.known
def test_cache_load_package_load_error(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must not fail on json errors
"""
response = {"packages": [pytest.helpers.get_package_status_extended(package_ahriman)]}
mocker.patch("pathlib.Path.is_file", return_value=True)
mocker.patch("pathlib.Path.open")
mocker.patch("ahriman.models.package.Package.from_json", side_effect=Exception())
mocker.patch("json.load", return_value=response)
watcher._cache_load()
assert not watcher.known
def test_cache_load_unknown(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must not load unknown package
"""
response = {"packages": [pytest.helpers.get_package_status_extended(package_ahriman)]}
mocker.patch("pathlib.Path.is_file", return_value=True)
mocker.patch("pathlib.Path.open")
mocker.patch("json.load", return_value=response)
watcher._cache_load()
assert not watcher.known
def test_cache_save(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must save state to cache
"""
mocker.patch("pathlib.Path.open")
json_mock = mocker.patch("json.dump")
watcher.known = {package_ahriman.base: (package_ahriman, BuildStatus())}
watcher._cache_save()
json_mock.assert_called_once_with(pytest.helpers.anyvar(int), pytest.helpers.anyvar(int))
def test_cache_save_failed(watcher: Watcher, mocker: MockerFixture) -> None:
"""
must not fail on dumping packages
"""
mocker.patch("pathlib.Path.open")
mocker.patch("json.dump", side_effect=Exception())
watcher._cache_save()
def test_cache_save_load(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must save state to cache which can be loaded later
"""
dump_file = Path(tempfile.mktemp()) # nosec
mocker.patch("ahriman.core.status.watcher.Watcher.cache_path",
new_callable=PropertyMock, return_value=dump_file)
known_current = {package_ahriman.base: (package_ahriman, BuildStatus())}
watcher.known = known_current
watcher._cache_save()
watcher.known = {package_ahriman.base: (None, None)}
watcher._cache_load()
assert watcher.known == known_current
dump_file.unlink()
def test_get(watcher: Watcher, package_ahriman: Package) -> None: def test_get(watcher: Watcher, package_ahriman: Package) -> None:
""" """
must return package status must return package status
@ -160,7 +51,7 @@ def test_load(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture)
must correctly load packages must correctly load packages
""" """
mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[package_ahriman]) mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[package_ahriman])
cache_mock = mocker.patch("ahriman.core.status.watcher.Watcher._cache_load") cache_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.packages_get")
watcher.load() watcher.load()
cache_mock.assert_called_once_with() cache_mock.assert_called_once_with()
@ -173,9 +64,10 @@ def test_load_known(watcher: Watcher, package_ahriman: Package, mocker: MockerFi
""" """
must correctly load packages with known statuses must correctly load packages with known statuses
""" """
status = BuildStatus(BuildStatusEnum.Success)
mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[package_ahriman]) mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[package_ahriman])
mocker.patch("ahriman.core.status.watcher.Watcher._cache_load") mocker.patch("ahriman.core.database.sqlite.SQLite.packages_get", return_value=[(package_ahriman, status)])
watcher.known = {package_ahriman.base: (package_ahriman, BuildStatus(BuildStatusEnum.Success))} watcher.known = {package_ahriman.base: (package_ahriman, status)}
watcher.load() watcher.load()
_, status = watcher.known[package_ahriman.base] _, status = watcher.known[package_ahriman.base]
@ -186,32 +78,32 @@ def test_remove(watcher: Watcher, package_ahriman: Package, mocker: MockerFixtur
""" """
must remove package base must remove package base
""" """
cache_mock = mocker.patch("ahriman.core.status.watcher.Watcher._cache_save") cache_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.package_remove")
watcher.known = {package_ahriman.base: (package_ahriman, BuildStatus())} watcher.known = {package_ahriman.base: (package_ahriman, BuildStatus())}
watcher.remove(package_ahriman.base) watcher.remove(package_ahriman.base)
assert not watcher.known assert not watcher.known
cache_mock.assert_called_once_with() cache_mock.assert_called_once_with(package_ahriman.base)
def test_remove_unknown(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None: def test_remove_unknown(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
""" """
must not fail on unknown base removal must not fail on unknown base removal
""" """
cache_mock = mocker.patch("ahriman.core.status.watcher.Watcher._cache_save") cache_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.package_remove")
watcher.remove(package_ahriman.base) watcher.remove(package_ahriman.base)
cache_mock.assert_called_once_with() cache_mock.assert_called_once_with(package_ahriman.base)
def test_update(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None: def test_update(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
""" """
must update package status must update package status
""" """
cache_mock = mocker.patch("ahriman.core.status.watcher.Watcher._cache_save") cache_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.package_update")
watcher.update(package_ahriman.base, BuildStatusEnum.Unknown, package_ahriman) watcher.update(package_ahriman.base, BuildStatusEnum.Unknown, package_ahriman)
cache_mock.assert_called_once_with() cache_mock.assert_called_once_with(package_ahriman, pytest.helpers.anyvar(int))
package, status = watcher.known[package_ahriman.base] package, status = watcher.known[package_ahriman.base]
assert package == package_ahriman assert package == package_ahriman
assert status.status == BuildStatusEnum.Unknown assert status.status == BuildStatusEnum.Unknown
@ -221,25 +113,22 @@ def test_update_ping(watcher: Watcher, package_ahriman: Package, mocker: MockerF
""" """
must update package status only for known package must update package status only for known package
""" """
cache_mock = mocker.patch("ahriman.core.status.watcher.Watcher._cache_save") cache_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.package_update")
watcher.known = {package_ahriman.base: (package_ahriman, BuildStatus())} watcher.known = {package_ahriman.base: (package_ahriman, BuildStatus())}
watcher.update(package_ahriman.base, BuildStatusEnum.Success, None) watcher.update(package_ahriman.base, BuildStatusEnum.Success, None)
cache_mock.assert_called_once_with() cache_mock.assert_called_once_with(package_ahriman, pytest.helpers.anyvar(int))
package, status = watcher.known[package_ahriman.base] package, status = watcher.known[package_ahriman.base]
assert package == package_ahriman assert package == package_ahriman
assert status.status == BuildStatusEnum.Success assert status.status == BuildStatusEnum.Success
def test_update_unknown(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None: def test_update_unknown(watcher: Watcher, package_ahriman: Package) -> None:
""" """
must fail on unknown package status update only must fail on unknown package status update only
""" """
cache_mock = mocker.patch("ahriman.core.status.watcher.Watcher._cache_save")
with pytest.raises(UnknownPackage): with pytest.raises(UnknownPackage):
watcher.update(package_ahriman.base, BuildStatusEnum.Unknown, None) watcher.update(package_ahriman.base, BuildStatusEnum.Unknown, None)
cache_mock.assert_called_once_with()
def test_update_self(watcher: Watcher) -> None: def test_update_self(watcher: Watcher) -> None:

View File

@ -230,32 +230,6 @@ def test_get_self_failed_http_error(web_client: WebClient, mocker: MockerFixture
assert web_client.get_self().status == BuildStatusEnum.Unknown assert web_client.get_self().status == BuildStatusEnum.Unknown
def test_reload_auth(web_client: WebClient, mocker: MockerFixture) -> None:
"""
must process auth reload
"""
requests_mock = mocker.patch("requests.Session.post")
web_client.reload_auth()
requests_mock.assert_called_once_with(pytest.helpers.anyvar(str, True))
def test_reload_auth_failed(web_client: WebClient, mocker: MockerFixture) -> None:
"""
must suppress any exception happened during auth reload
"""
mocker.patch("requests.Session.post", side_effect=Exception())
web_client.reload_auth()
def test_reload_auth_failed_http_error(web_client: WebClient, mocker: MockerFixture) -> None:
"""
must suppress any exception happened during removal
"""
mocker.patch("requests.Session.post", side_effect=requests.exceptions.HTTPError())
web_client.reload_auth()
def test_remove(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None: def test_remove(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
""" """
must process package removal must process package removal

View File

@ -8,6 +8,14 @@ from unittest import mock
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import InitializeException from ahriman.core.exceptions import InitializeException
from ahriman.models.repository_paths import RepositoryPaths
def test_repository_paths(configuration: Configuration, repository_paths: RepositoryPaths) -> None:
"""
must return repository paths
"""
assert configuration.repository_paths == repository_paths
def test_from_path(mocker: MockerFixture) -> None: def test_from_path(mocker: MockerFixture) -> None:
@ -40,6 +48,33 @@ def test_from_path_file_missing(mocker: MockerFixture) -> None:
read_mock.assert_called_once_with(configuration.SYSTEM_CONFIGURATION_PATH) read_mock.assert_called_once_with(configuration.SYSTEM_CONFIGURATION_PATH)
def test_check_loaded(configuration: Configuration) -> None:
"""
must return valid path and architecture
"""
path, architecture = configuration.check_loaded()
assert path == configuration.path
assert architecture == configuration.architecture
def test_check_loaded_path(configuration: Configuration) -> None:
"""
must raise exception if path is none
"""
configuration.path = None
with pytest.raises(InitializeException):
configuration.check_loaded()
def test_check_loaded_architecture(configuration: Configuration) -> None:
"""
must raise exception if architecture is none
"""
configuration.architecture = None
with pytest.raises(InitializeException):
configuration.check_loaded()
def test_dump(configuration: Configuration) -> None: def test_dump(configuration: Configuration) -> None:
""" """
dump must not be empty dump must not be empty

Some files were not shown because too many files have changed in this diff Show More