mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-04-24 07:17:17 +00:00
port part of settings to database (#54)
This commit is contained in:
parent
d4eadf0013
commit
83931f5cf4
2
.gitignore
vendored
2
.gitignore
vendored
@ -95,3 +95,5 @@ ENV/
|
||||
|
||||
*.tar.xz
|
||||
status_cache.json
|
||||
|
||||
*.db
|
||||
|
@ -4,6 +4,7 @@ FROM archlinux:base-devel
|
||||
ENV AHRIMAN_ARCHITECTURE="x86_64"
|
||||
ENV AHRIMAN_DEBUG=""
|
||||
ENV AHRIMAN_FORCE_ROOT=""
|
||||
ENV AHRIMAN_HOST="0.0.0.0"
|
||||
ENV AHRIMAN_OUTPUT="syslog"
|
||||
ENV AHRIMAN_PACKAGER="ahriman bot <ahriman@example.com>"
|
||||
ENV AHRIMAN_PORT=""
|
||||
|
@ -5,7 +5,8 @@ set -e
|
||||
|
||||
# configuration tune
|
||||
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"
|
||||
|
||||
AHRIMAN_DEFAULT_ARGS=("-a" "$AHRIMAN_ARCHITECTURE")
|
||||
@ -40,9 +41,9 @@ systemd-machine-id-setup &> /dev/null
|
||||
# otherwise we prepend executable by sudo command
|
||||
if [ -n "$AHRIMAN_FORCE_ROOT" ]; then
|
||||
AHRIMAN_EXECUTABLE=("ahriman")
|
||||
elif ahriman help-commands-unsafe | grep -Fxq "$1"; then
|
||||
AHRIMAN_EXECUTABLE=("ahriman")
|
||||
else
|
||||
elif ahriman help-commands-unsafe --command="$*" &> /dev/null; then
|
||||
AHRIMAN_EXECUTABLE=("sudo" "-u" "$AHRIMAN_USER" "--" "ahriman")
|
||||
else
|
||||
AHRIMAN_EXECUTABLE=("ahriman")
|
||||
fi
|
||||
exec "${AHRIMAN_EXECUTABLE[@]}" "${AHRIMAN_DEFAULT_ARGS[@]}" "$@"
|
||||
|
@ -228,6 +228,7 @@ The following environment variables are supported:
|
||||
* `AHRIMAN_ARCHITECTURE` - architecture of the repository, default is `x86_64`.
|
||||
* `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_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_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.
|
||||
|
@ -1,6 +1,7 @@
|
||||
[settings]
|
||||
include = ahriman.ini.d
|
||||
logging = ahriman.ini.d/logging.ini
|
||||
database = /var/lib/ahriman/ahriman.db
|
||||
|
||||
[alpm]
|
||||
aur_url = https://aur.archlinux.org
|
||||
|
@ -1,5 +1,5 @@
|
||||
[loggers]
|
||||
keys = root,build_details,http,stderr,boto3,botocore,nose,s3transfer
|
||||
keys = root,build_details,database,http,stderr,boto3,botocore,nose,s3transfer
|
||||
|
||||
[handlers]
|
||||
keys = console_handler,syslog_handler
|
||||
@ -38,6 +38,12 @@ handlers = syslog_handler
|
||||
qualname = build_details
|
||||
propagate = 0
|
||||
|
||||
[logger_database]
|
||||
level = DEBUG
|
||||
handlers = syslog_handler
|
||||
qualname = database
|
||||
propagate = 0
|
||||
|
||||
[logger_http]
|
||||
level = DEBUG
|
||||
handlers = syslog_handler
|
||||
|
@ -94,6 +94,7 @@ def _parser() -> argparse.ArgumentParser:
|
||||
_set_repo_sync_parser(subparsers)
|
||||
_set_repo_update_parser(subparsers)
|
||||
_set_user_add_parser(subparsers)
|
||||
_set_user_list_parser(subparsers)
|
||||
_set_user_remove_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",
|
||||
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,
|
||||
unsafe=True, parser=_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",
|
||||
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)
|
||||
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 "
|
||||
"the chroot directories you will need root privileges.",
|
||||
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("--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("--patches", help="clear directory with patches", action="store_true")
|
||||
parser.set_defaults(handler=handlers.Clean, quiet=True, unsafe=True)
|
||||
return parser
|
||||
|
||||
@ -487,7 +488,6 @@ def _set_user_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
formatter_class=_formatter)
|
||||
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("--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, "
|
||||
"which is in particular must be used for OAuth2 authorization type.")
|
||||
parser.add_argument("-r", "--role", help="user access level",
|
||||
@ -498,6 +498,22 @@ def _set_user_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
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:
|
||||
"""
|
||||
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",
|
||||
formatter_class=_formatter)
|
||||
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.set_defaults(handler=handlers.User, action=Action.Remove, architecture=[""], lock=None, no_report=True, # nosec
|
||||
password="", quiet=True, role=UserAccess.Read, unsafe=True)
|
||||
|
@ -25,7 +25,7 @@ from typing import Any, Iterable, Set
|
||||
|
||||
from ahriman.application.application.properties import Properties
|
||||
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_source import PackageSource
|
||||
from ahriman.models.result import Result
|
||||
@ -67,10 +67,11 @@ class Packages(Properties):
|
||||
:param without_dependencies: if set, dependency check will be disabled
|
||||
"""
|
||||
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))
|
||||
self._process_dependencies(local_path, known_packages, without_dependencies)
|
||||
with tmpdir() as local_path:
|
||||
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:
|
||||
"""
|
||||
@ -92,10 +93,9 @@ class Packages(Properties):
|
||||
cache_dir = self.repository.paths.cache_for(package.base)
|
||||
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
|
||||
self.repository.database.build_queue_insert(package)
|
||||
|
||||
dst = self.repository.paths.manual_for(package.base)
|
||||
shutil.copytree(cache_dir, dst) # copy package for the build
|
||||
self._process_dependencies(dst, known_packages, without_dependencies)
|
||||
self._process_dependencies(cache_dir, known_packages, without_dependencies)
|
||||
|
||||
def _add_remote(self, source: str, *_: Any) -> None:
|
||||
"""
|
||||
|
@ -20,6 +20,7 @@
|
||||
import logging
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database.sqlite import SQLite
|
||||
from ahriman.core.repository import Repository
|
||||
|
||||
|
||||
@ -28,6 +29,7 @@ class Properties:
|
||||
application base properties class
|
||||
:ivar architecture: repository architecture
|
||||
:ivar configuration: configuration instance
|
||||
:ivar database: database instance
|
||||
:ivar logger: application logger
|
||||
:ivar repository: repository instance
|
||||
"""
|
||||
@ -43,4 +45,5 @@ class Properties:
|
||||
self.logger = logging.getLogger("root")
|
||||
self.configuration = configuration
|
||||
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)
|
||||
|
@ -42,28 +42,22 @@ class Repository(Properties):
|
||||
"""
|
||||
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
|
||||
:param build: clear directory with package sources
|
||||
:param cache: clear directory with package caches
|
||||
:param chroot: clear build chroot
|
||||
:param manual: clear directory with manually added packages
|
||||
:param packages: clear directory with built packages
|
||||
:param patches: clear directory with patches
|
||||
"""
|
||||
if build:
|
||||
self.repository.clear_build()
|
||||
if cache:
|
||||
self.repository.clear_cache()
|
||||
if chroot:
|
||||
self.repository.clear_chroot()
|
||||
if manual:
|
||||
self.repository.clear_manual()
|
||||
self.repository.clear_queue()
|
||||
if packages:
|
||||
self.repository.clear_packages()
|
||||
if patches:
|
||||
self.repository.clear_patches()
|
||||
|
||||
def report(self, target: Iterable[str], result: Result) -> None:
|
||||
"""
|
||||
@ -154,7 +148,7 @@ class Repository(Properties):
|
||||
process_update(packages, Result())
|
||||
|
||||
# process manual packages
|
||||
tree = Tree.load(updates, self.repository.paths)
|
||||
tree = Tree.load(updates, self.database)
|
||||
for num, level in enumerate(tree.levels()):
|
||||
self.logger.info("processing level #%i %s", num, [package.base for package in level])
|
||||
build_result = self.repository.process_build(level)
|
||||
|
@ -43,4 +43,4 @@ class Clean(Handler):
|
||||
:param unsafe: if set no user check will be performed before path creation
|
||||
"""
|
||||
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)
|
||||
|
@ -27,7 +27,7 @@ from typing import List, Type
|
||||
|
||||
from ahriman.application.lock import Lock
|
||||
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
|
||||
|
||||
|
||||
@ -78,6 +78,8 @@ class Handler:
|
||||
with Lock(args, architecture, configuration):
|
||||
cls.run(args, architecture, configuration, args.no_report, args.unsafe)
|
||||
return True
|
||||
except ExitCode:
|
||||
return False
|
||||
except Exception:
|
||||
# we are basically always want to print error to stderr instead of default logger
|
||||
logging.getLogger("stderr").exception("process exception")
|
||||
|
@ -18,15 +18,15 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import argparse
|
||||
import shutil
|
||||
|
||||
from pathlib import Path
|
||||
from typing import List, Type
|
||||
from typing import List, Optional, Type
|
||||
|
||||
from ahriman.application.application import Application
|
||||
from ahriman.application.handlers.handler import Handler
|
||||
from ahriman.core.build_tools.sources import Sources
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.formatters.string_printer import StringPrinter
|
||||
from ahriman.models.action import Action
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.package_source import PackageSource
|
||||
@ -37,8 +37,6 @@ class Patch(Handler):
|
||||
patch control handler
|
||||
"""
|
||||
|
||||
_print = print
|
||||
|
||||
@classmethod
|
||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
|
||||
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,
|
||||
application.repository.aur_url)
|
||||
patch_dir = application.repository.paths.patches_for(package.base)
|
||||
|
||||
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)
|
||||
patch = Sources.patch_create(Path(sources_dir), *track)
|
||||
application.database.patches_insert(package.base, patch)
|
||||
|
||||
@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
|
||||
:param application: application instance
|
||||
:param package_base: package base
|
||||
"""
|
||||
patch_dir = application.repository.paths.patches_for(package_base)
|
||||
if not patch_dir.is_dir():
|
||||
return
|
||||
for patch_path in sorted(patch_dir.glob("*.patch")):
|
||||
Patch._print(patch_path.name)
|
||||
patches = application.database.patches_list(package_base)
|
||||
for base, patch in patches.items():
|
||||
content = base if package_base is None else patch
|
||||
StringPrinter(content).print(verbose=True)
|
||||
|
||||
@staticmethod
|
||||
def patch_set_remove(application: Application, package_base: str) -> None:
|
||||
@ -96,5 +89,4 @@ class Patch(Handler):
|
||||
:param application: application instance
|
||||
:param package_base: package base
|
||||
"""
|
||||
patch_dir = application.repository.paths.patches_for(package_base)
|
||||
shutil.rmtree(patch_dir, ignore_errors=True)
|
||||
application.database.patches_remove(package_base)
|
||||
|
@ -18,11 +18,13 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import argparse
|
||||
import shlex
|
||||
|
||||
from typing import List, Type
|
||||
|
||||
from ahriman.application.handlers.handler import Handler
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.exceptions import ExitCode
|
||||
from ahriman.core.formatters.string_printer import StringPrinter
|
||||
|
||||
|
||||
@ -44,9 +46,25 @@ class UnsafeCommands(Handler):
|
||||
:param no_report: force disable reporting
|
||||
:param unsafe: if set no user check will be performed before path creation
|
||||
"""
|
||||
unsafe_commands = UnsafeCommands.get_unsafe_commands(args.parser())
|
||||
for command in unsafe_commands:
|
||||
StringPrinter(command).print(verbose=True)
|
||||
parser = args.parser()
|
||||
unsafe_commands = UnsafeCommands.get_unsafe_commands(parser)
|
||||
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
|
||||
def get_unsafe_commands(parser: argparse.ArgumentParser) -> List[str]:
|
||||
|
@ -23,12 +23,12 @@ import getpass
|
||||
from pathlib import Path
|
||||
from typing import Type
|
||||
|
||||
from ahriman.application.application import Application
|
||||
from ahriman.application.handlers.handler import Handler
|
||||
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.user import User as MUser
|
||||
from ahriman.models.user_access import UserAccess
|
||||
|
||||
|
||||
class User(Handler):
|
||||
@ -51,33 +51,35 @@ class User(Handler):
|
||||
"""
|
||||
salt = User.get_salt(configuration)
|
||||
user = User.user_create(args)
|
||||
|
||||
auth_configuration = User.configuration_get(configuration.include)
|
||||
database = SQLite.load(configuration)
|
||||
|
||||
User.user_clear(auth_configuration, user)
|
||||
if args.action == Action.Update:
|
||||
User.configuration_create(auth_configuration, user, salt, args.as_service)
|
||||
User.configuration_write(auth_configuration, args.secure)
|
||||
|
||||
if not args.no_reload:
|
||||
client = Application(architecture, configuration, no_report=False, unsafe=unsafe).repository.reporter
|
||||
client.reload_auth()
|
||||
if args.action == Action.List:
|
||||
for found_user in database.user_list(user.username, user.access):
|
||||
UserPrinter(found_user).print(verbose=True)
|
||||
elif args.action == Action.Remove:
|
||||
database.user_remove(user.username)
|
||||
elif args.action == Action.Update:
|
||||
User.configuration_create(auth_configuration, user, salt, args.as_service, args.secure)
|
||||
database.user_update(user.hash_password(salt))
|
||||
|
||||
@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 user: user descriptor
|
||||
:param salt: password hash salt
|
||||
: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(section, user.username, user.hash_password(salt))
|
||||
|
||||
if as_service_user:
|
||||
configuration.set_option("web", "username", user.username)
|
||||
configuration.set_option("web", "password", user.password)
|
||||
User.configuration_write(configuration, secure)
|
||||
|
||||
@staticmethod
|
||||
def configuration_get(include_path: Path) -> Configuration:
|
||||
@ -90,6 +92,8 @@ class User(Handler):
|
||||
configuration = Configuration()
|
||||
configuration.load(target)
|
||||
|
||||
configuration.architecture = "" # not user anyway
|
||||
|
||||
return configuration
|
||||
|
||||
@staticmethod
|
||||
@ -99,12 +103,11 @@ class User(Handler):
|
||||
:param configuration: configuration instance
|
||||
:param secure: if true then set file permissions to 0o600
|
||||
"""
|
||||
if configuration.path is None:
|
||||
return # should never happen actually
|
||||
with configuration.path.open("w") as ahriman_configuration:
|
||||
path, _ = configuration.check_loaded()
|
||||
with path.open("w") as ahriman_configuration:
|
||||
configuration.write(ahriman_configuration)
|
||||
if secure:
|
||||
configuration.path.chmod(0o600)
|
||||
path.chmod(0o600)
|
||||
|
||||
@staticmethod
|
||||
def get_salt(configuration: Configuration, salt_length: int = 20) -> str:
|
||||
@ -118,19 +121,6 @@ class User(Handler):
|
||||
return salt
|
||||
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
|
||||
def user_create(args: argparse.Namespace) -> MUser:
|
||||
"""
|
||||
|
@ -32,7 +32,6 @@ from ahriman.core.exceptions import DuplicateRun
|
||||
from ahriman.core.status.client import Client
|
||||
from ahriman.core.util import check_user
|
||||
from ahriman.models.build_status import BuildStatusEnum
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
|
||||
|
||||
class Lock:
|
||||
@ -56,7 +55,7 @@ class Lock:
|
||||
self.force = args.force
|
||||
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)
|
||||
|
||||
def __enter__(self) -> Lock:
|
||||
|
@ -21,12 +21,11 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from typing import Dict, Optional, Type
|
||||
from typing import Optional, Type
|
||||
|
||||
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.user import User
|
||||
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>"""
|
||||
|
||||
@classmethod
|
||||
def load(cls: Type[Auth], configuration: Configuration) -> Auth:
|
||||
def load(cls: Type[Auth], configuration: Configuration, database: SQLite) -> Auth:
|
||||
"""
|
||||
load authorization module from settings
|
||||
:param configuration: configuration instance
|
||||
:param database: database instance
|
||||
:return: authorization module according to current settings
|
||||
"""
|
||||
provider = AuthSettings.from_option(configuration.get("auth", "target", fallback="disabled"))
|
||||
if provider == AuthSettings.Configuration:
|
||||
from ahriman.core.auth.mapping import Mapping
|
||||
return Mapping(configuration)
|
||||
return Mapping(configuration, database)
|
||||
if provider == AuthSettings.OAuth:
|
||||
from ahriman.core.auth.oauth import OAuth
|
||||
return OAuth(configuration)
|
||||
return OAuth(configuration, database)
|
||||
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
|
||||
"""
|
||||
validate user password
|
||||
|
@ -20,7 +20,9 @@
|
||||
from typing import Optional
|
||||
|
||||
from ahriman.core.auth.auth import Auth
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database.sqlite import SQLite
|
||||
from ahriman.models.auth_settings import AuthSettings
|
||||
from ahriman.models.user import User
|
||||
from ahriman.models.user_access import UserAccess
|
||||
@ -30,18 +32,20 @@ class Mapping(Auth):
|
||||
"""
|
||||
user authorization based on mapping from configuration file
|
||||
: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
|
||||
:param configuration: configuration instance
|
||||
:param database: database instance
|
||||
:param provider: authorization type definition
|
||||
"""
|
||||
Auth.__init__(self, configuration, provider)
|
||||
self.database = database
|
||||
self.salt = configuration.get("auth", "salt")
|
||||
self._users = self.get_users(configuration)
|
||||
|
||||
async def check_credentials(self, username: Optional[str], password: Optional[str]) -> bool:
|
||||
"""
|
||||
@ -61,8 +65,7 @@ class Mapping(Auth):
|
||||
:param username: username
|
||||
:return: user descriptor if username is known and None otherwise
|
||||
"""
|
||||
normalized_user = username.lower()
|
||||
return self._users.get(normalized_user)
|
||||
return self.database.user_get(username)
|
||||
|
||||
async def known_username(self, username: Optional[str]) -> bool:
|
||||
"""
|
||||
|
@ -23,6 +23,7 @@ from typing import Optional, Type
|
||||
|
||||
from ahriman.core.auth.mapping import Mapping
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database.sqlite import SQLite
|
||||
from ahriman.core.exceptions import InvalidOption
|
||||
from ahriman.models.auth_settings import AuthSettings
|
||||
|
||||
@ -38,13 +39,15 @@ class OAuth(Mapping):
|
||||
: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
|
||||
:param configuration: configuration instance
|
||||
:param database: database instance
|
||||
: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_secret = configuration.get("auth", "client_secret")
|
||||
# in order to use OAuth feature the service must be publicity available
|
||||
|
@ -47,6 +47,8 @@ class Sources:
|
||||
found_files: List[Path] = []
|
||||
for glob in pattern:
|
||||
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)
|
||||
# add them to index
|
||||
Sources._check_output("git", "add", "--intent-to-add",
|
||||
@ -54,14 +56,13 @@ class Sources:
|
||||
exception=None, cwd=sources_dir, logger=Sources.logger)
|
||||
|
||||
@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
|
||||
: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)
|
||||
patch_path.write_text(patch)
|
||||
return Sources._check_output("git", "diff", exception=None, cwd=sources_dir, logger=Sources.logger)
|
||||
|
||||
@staticmethod
|
||||
def fetch(sources_dir: Path, remote: Optional[str]) -> None:
|
||||
@ -112,41 +113,39 @@ class Sources:
|
||||
exception=None, cwd=sources_dir, logger=Sources.logger)
|
||||
|
||||
@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
|
||||
:param sources_dir: local path 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.patch_apply(sources_dir, patch_dir)
|
||||
if patch is None:
|
||||
Sources.logger.info("no patches found")
|
||||
return
|
||||
Sources.patch_apply(sources_dir, patch)
|
||||
|
||||
@staticmethod
|
||||
def patch_apply(sources_dir: Path, patch_dir: Path) -> None:
|
||||
def patch_apply(sources_dir: Path, patch: str) -> None:
|
||||
"""
|
||||
apply patches if any
|
||||
: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
|
||||
if not patch_dir.is_dir():
|
||||
return # no patches provided
|
||||
# find everything that looks like patch and sort it
|
||||
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)
|
||||
# create patch
|
||||
Sources.logger.info("apply patch from database")
|
||||
Sources._check_output("git", "apply", "--ignore-space-change", "--ignore-whitespace",
|
||||
exception=None, cwd=sources_dir, input_data=patch, logger=Sources.logger)
|
||||
|
||||
@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
|
||||
:param sources_dir: local path to git repository
|
||||
:param patch_path: path to result patch
|
||||
:param pattern: glob patterns
|
||||
:return: patch as plain text
|
||||
"""
|
||||
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
|
||||
|
@ -21,10 +21,11 @@ import logging
|
||||
import shutil
|
||||
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
from typing import List
|
||||
|
||||
from ahriman.core.build_tools.sources import Sources
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database.sqlite import SQLite
|
||||
from ahriman.core.exceptions import BuildFailed
|
||||
from ahriman.core.util import check_output
|
||||
from ahriman.models.package import Package
|
||||
@ -61,9 +62,10 @@ class Task:
|
||||
self.makepkg_flags = configuration.getlist("build", "makepkg_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
|
||||
:param sources_path: path to where sources are
|
||||
:return: paths of produced packages
|
||||
"""
|
||||
command = [self.build_command, "-r", str(self.paths.chroot)]
|
||||
@ -75,24 +77,24 @@ class Task:
|
||||
Task._check_output(
|
||||
*command,
|
||||
exception=BuildFailed(self.package.base),
|
||||
cwd=self.paths.sources_for(self.package.base),
|
||||
cwd=sources_path,
|
||||
logger=self.build_logger,
|
||||
user=self.uid)
|
||||
|
||||
# well it is not actually correct, but we can deal with it
|
||||
packages = Task._check_output("makepkg", "--packagelist",
|
||||
exception=BuildFailed(self.package.base),
|
||||
cwd=self.paths.sources_for(self.package.base),
|
||||
cwd=sources_path,
|
||||
logger=self.build_logger).splitlines()
|
||||
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
|
||||
: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():
|
||||
# no need to clone whole repository, just copy from cache first
|
||||
shutil.copytree(self.paths.cache_for(self.package.base), git_path)
|
||||
Sources.load(git_path, self.package.git_url, self.paths.patches_for(self.package.base))
|
||||
shutil.copytree(self.paths.cache_for(self.package.base), path, dirs_exist_ok=True)
|
||||
Sources.load(path, self.package.git_url, database.patches_get(self.package.base))
|
||||
|
@ -28,6 +28,7 @@ from pathlib import Path
|
||||
from typing import Any, Dict, Generator, List, Optional, Tuple, Type
|
||||
|
||||
from ahriman.core.exceptions import InitializeException
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
|
||||
|
||||
class Configuration(configparser.RawConfigParser):
|
||||
@ -72,6 +73,14 @@ class Configuration(configparser.RawConfigParser):
|
||||
"""
|
||||
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
|
||||
def from_path(cls: Type[Configuration], path: Path, architecture: str, quiet: bool) -> Configuration:
|
||||
"""
|
||||
@ -134,6 +143,15 @@ class Configuration(configparser.RawConfigParser):
|
||||
return 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]]:
|
||||
"""
|
||||
dump configuration to dictionary
|
||||
@ -233,12 +251,11 @@ class Configuration(configparser.RawConfigParser):
|
||||
"""
|
||||
reload configuration if possible or raise exception otherwise
|
||||
"""
|
||||
if self.path is None or self.architecture is None:
|
||||
raise InitializeException("Configuration path and/or architecture are not set")
|
||||
path, architecture = self.check_loaded()
|
||||
for section in self.sections(): # clear current content
|
||||
self.remove_section(section)
|
||||
self.load(self.path)
|
||||
self.merge_sections(self.architecture)
|
||||
self.load(path)
|
||||
self.merge_sections(architecture)
|
||||
|
||||
def set_option(self, section: str, option: str, value: Optional[str]) -> None:
|
||||
"""
|
||||
|
19
src/ahriman/core/database/__init__.py
Normal file
19
src/ahriman/core/database/__init__.py
Normal 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/>.
|
||||
#
|
43
src/ahriman/core/database/data/__init__.py
Normal file
43
src/ahriman/core/database/data/__init__.py
Normal 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)
|
80
src/ahriman/core/database/data/package_statuses.py
Normal file
80
src/ahriman/core/database/data/package_statuses.py
Normal 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()
|
44
src/ahriman/core/database/data/patches.py
Normal file
44
src/ahriman/core/database/data/patches.py
Normal 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()
|
40
src/ahriman/core/database/data/users.py
Normal file
40
src/ahriman/core/database/data/users.py
Normal 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()
|
125
src/ahriman/core/database/migrations/__init__.py
Normal file
125
src/ahriman/core/database/migrations/__init__.py
Normal 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
|
73
src/ahriman/core/database/migrations/m000_initial.py
Normal file
73
src/ahriman/core/database/migrations/m000_initial.py
Normal 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
|
||||
)
|
||||
""",
|
||||
]
|
19
src/ahriman/core/database/operations/__init__.py
Normal file
19
src/ahriman/core/database/operations/__init__.py
Normal 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/>.
|
||||
#
|
93
src/ahriman/core/database/operations/auth_operations.py
Normal file
93
src/ahriman/core/database/operations/auth_operations.py
Normal 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)
|
77
src/ahriman/core/database/operations/build_operations.py
Normal file
77
src/ahriman/core/database/operations/build_operations.py
Normal 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)
|
71
src/ahriman/core/database/operations/operations.py
Normal file
71
src/ahriman/core/database/operations/operations.py
Normal 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
|
199
src/ahriman/core/database/operations/package_operations.py
Normal file
199
src/ahriman/core/database/operations/package_operations.py
Normal 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)))
|
85
src/ahriman/core/database/operations/patch_operations.py
Normal file
85
src/ahriman/core/database/operations/patch_operations.py
Normal 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)
|
70
src/ahriman/core/database/sqlite.py
Normal file
70
src/ahriman/core/database/sqlite.py
Normal 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)
|
@ -26,12 +26,12 @@ class BuildFailed(RuntimeError):
|
||||
base exception for failed builds
|
||||
"""
|
||||
|
||||
def __init__(self, package: str) -> None:
|
||||
def __init__(self, package_base: str) -> None:
|
||||
"""
|
||||
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):
|
||||
@ -47,18 +47,11 @@ class DuplicateRun(RuntimeError):
|
||||
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):
|
||||
"""
|
||||
@ -113,6 +106,19 @@ class InvalidPackageInfo(RuntimeError):
|
||||
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):
|
||||
"""
|
||||
exception which will be raised if architecture is required, but missing
|
||||
|
46
src/ahriman/core/formatters/user_printer.py
Normal file
46
src/ahriman/core/formatters/user_printer.py
Normal 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)]
|
@ -37,14 +37,6 @@ class Cleaner(Properties):
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
clear cache directory
|
||||
@ -61,14 +53,6 @@ class Cleaner(Properties):
|
||||
for chroot in self.paths.chroot.iterdir():
|
||||
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:
|
||||
"""
|
||||
clear directory with built packages (NOT repository itself)
|
||||
@ -77,10 +61,9 @@ class Cleaner(Properties):
|
||||
for package in self.packages_built():
|
||||
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")
|
||||
for package in self.paths.patches.iterdir():
|
||||
shutil.rmtree(package)
|
||||
self.logger.info("clear build queue")
|
||||
self.database.build_queue_clear(None)
|
||||
|
@ -26,6 +26,7 @@ from ahriman.core.build_tools.task import Task
|
||||
from ahriman.core.report.report import Report
|
||||
from ahriman.core.repository.cleaner import Cleaner
|
||||
from ahriman.core.upload.upload import Upload
|
||||
from ahriman.core.util import tmpdir
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.result import Result
|
||||
|
||||
@ -56,25 +57,25 @@ class Executor(Cleaner):
|
||||
:param updates: list of packages properties to build
|
||||
:return: `packages_built`
|
||||
"""
|
||||
def build_single(package: Package) -> None:
|
||||
def build_single(package: Package, local_path: Path) -> None:
|
||||
self.reporter.set_building(package.base)
|
||||
task = Task(package, self.configuration, self.paths)
|
||||
task.init()
|
||||
built = task.build()
|
||||
task.init(local_path, self.database)
|
||||
built = task.build(local_path)
|
||||
for src in built:
|
||||
dst = self.paths.packages / src.name
|
||||
shutil.move(src, dst)
|
||||
|
||||
result = Result()
|
||||
for single in updates:
|
||||
try:
|
||||
build_single(single)
|
||||
result.add_success(single)
|
||||
except Exception:
|
||||
self.reporter.set_failed(single.base)
|
||||
result.add_failed(single)
|
||||
self.logger.exception("%s (%s) build exception", single.base, self.architecture)
|
||||
self.clear_build()
|
||||
with tmpdir() as build_dir:
|
||||
try:
|
||||
build_single(single, build_dir)
|
||||
result.add_success(single)
|
||||
except Exception:
|
||||
self.reporter.set_failed(single.base)
|
||||
result.add_failed(single)
|
||||
self.logger.exception("%s (%s) build exception", single.base, self.architecture)
|
||||
|
||||
return result
|
||||
|
||||
@ -87,6 +88,8 @@ class Executor(Cleaner):
|
||||
def remove_base(package_base: str) -> None:
|
||||
try:
|
||||
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
|
||||
except Exception:
|
||||
self.logger.exception("could not remove base %s", package_base)
|
||||
|
@ -22,11 +22,11 @@ import logging
|
||||
from ahriman.core.alpm.pacman import Pacman
|
||||
from ahriman.core.alpm.repo import Repo
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database.sqlite import SQLite
|
||||
from ahriman.core.exceptions import UnsafeRun
|
||||
from ahriman.core.sign.gpg import GPG
|
||||
from ahriman.core.status.client import Client
|
||||
from ahriman.core.util import check_user
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
|
||||
|
||||
class Properties:
|
||||
@ -35,6 +35,7 @@ class Properties:
|
||||
:ivar architecture: repository architecture
|
||||
:ivar aur_url: base AUR url
|
||||
:ivar configuration: configuration instance
|
||||
:ivar database: database instance
|
||||
:ivar ignore_list: package bases which will be ignored during auto updates
|
||||
:ivar logger: class logger
|
||||
:ivar name: repository name
|
||||
@ -45,22 +46,25 @@ class Properties:
|
||||
: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
|
||||
:param architecture: repository architecture
|
||||
:param configuration: configuration instance
|
||||
:param database: database instance
|
||||
:param no_report: force disable reporting
|
||||
:param unsafe: if set no user check will be performed before path creation
|
||||
"""
|
||||
self.logger = logging.getLogger("root")
|
||||
self.architecture = architecture
|
||||
self.configuration = configuration
|
||||
self.database = database
|
||||
|
||||
self.aur_url = configuration.get("alpm", "aur_url")
|
||||
self.name = configuration.get("repository", "name")
|
||||
|
||||
self.paths = RepositoryPaths(configuration.getpath("repository", "root"), architecture)
|
||||
self.paths = configuration.repository_paths
|
||||
try:
|
||||
check_user(self.paths, unsafe)
|
||||
self.paths.tree_create()
|
||||
|
@ -90,7 +90,7 @@ class UpdateHandler(Cleaner):
|
||||
else:
|
||||
self.reporter.set_success(local)
|
||||
except Exception:
|
||||
self.logger.exception("could not procees package at %s", dirname)
|
||||
self.logger.exception("could not process package at %s", dirname)
|
||||
|
||||
return result
|
||||
|
||||
@ -102,16 +102,15 @@ class UpdateHandler(Cleaner):
|
||||
result: List[Package] = []
|
||||
known_bases = {package.base for package in self.packages()}
|
||||
|
||||
for dirname in self.paths.manual.iterdir():
|
||||
try:
|
||||
local = Package.load(str(dirname), PackageSource.Local, self.pacman, self.aur_url)
|
||||
try:
|
||||
for local in self.database.build_queue_get():
|
||||
result.append(local)
|
||||
if local.base not in known_bases:
|
||||
self.reporter.set_unknown(local)
|
||||
else:
|
||||
self.reporter.set_pending(local.base)
|
||||
except Exception:
|
||||
self.logger.exception("could not add package from %s", dirname)
|
||||
self.clear_manual()
|
||||
except Exception:
|
||||
self.logger.exception("could not load packages from database")
|
||||
self.clear_queue()
|
||||
|
||||
return result
|
||||
|
@ -77,11 +77,6 @@ class Client:
|
||||
"""
|
||||
return BuildStatus()
|
||||
|
||||
def reload_auth(self) -> None:
|
||||
"""
|
||||
reload authentication module call
|
||||
"""
|
||||
|
||||
def remove(self, base: str) -> None:
|
||||
"""
|
||||
remove packages from watcher
|
||||
|
@ -17,13 +17,12 @@
|
||||
# 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
|
||||
import logging
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database.sqlite import SQLite
|
||||
from ahriman.core.exceptions import UnknownPackage
|
||||
from ahriman.core.repository import Repository
|
||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||
@ -34,33 +33,29 @@ class Watcher:
|
||||
"""
|
||||
package status watcher
|
||||
:ivar architecture: repository architecture
|
||||
:ivar database: database instance
|
||||
:ivar known: list of known packages. For the most cases `packages` should be used instead
|
||||
:ivar logger: class logger
|
||||
:ivar repository: repository object
|
||||
:ivar status: daemon status
|
||||
"""
|
||||
|
||||
def __init__(self, architecture: str, configuration: Configuration) -> None:
|
||||
def __init__(self, architecture: str, configuration: Configuration, database: SQLite) -> None:
|
||||
"""
|
||||
default constructor
|
||||
:param architecture: repository architecture
|
||||
:param configuration: configuration instance
|
||||
:param database: database instance
|
||||
"""
|
||||
self.logger = logging.getLogger("http")
|
||||
|
||||
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.status = BuildStatus()
|
||||
|
||||
@property
|
||||
def cache_path(self) -> Path:
|
||||
"""
|
||||
:return: path to dump with json cache
|
||||
"""
|
||||
return self.repository.paths.root / "status_cache.json"
|
||||
|
||||
@property
|
||||
def packages(self) -> List[Tuple[Package, BuildStatus]]:
|
||||
"""
|
||||
@ -68,48 +63,6 @@ class Watcher:
|
||||
"""
|
||||
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]:
|
||||
"""
|
||||
get current package base build status
|
||||
@ -131,31 +84,34 @@ class Watcher:
|
||||
else:
|
||||
status = BuildStatus()
|
||||
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
|
||||
:param base: package base
|
||||
:param package_base: package base
|
||||
"""
|
||||
self.known.pop(base, None)
|
||||
self._cache_save()
|
||||
self.known.pop(package_base, None)
|
||||
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
|
||||
:param base: package base to update
|
||||
:param package_base: package base to update
|
||||
:param status: new build status
|
||||
:param package: optional new package description. In case if not set current properties will be used
|
||||
"""
|
||||
if package is None:
|
||||
try:
|
||||
package, _ = self.known[base]
|
||||
package, _ = self.known[package_base]
|
||||
except KeyError:
|
||||
raise UnknownPackage(base)
|
||||
raise UnknownPackage(package_base)
|
||||
full_status = BuildStatus(status)
|
||||
self.known[base] = (package, full_status)
|
||||
self._cache_save()
|
||||
self.known[package_base] = (package, full_status)
|
||||
self.database.package_update(package, full_status)
|
||||
|
||||
def update_self(self, status: BuildStatusEnum) -> None:
|
||||
"""
|
||||
|
@ -67,13 +67,6 @@ class WebClient(Client):
|
||||
"""
|
||||
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
|
||||
def _status_url(self) -> str:
|
||||
"""
|
||||
@ -198,18 +191,6 @@ class WebClient(Client):
|
||||
self.logger.exception("could not get service status")
|
||||
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:
|
||||
"""
|
||||
remove packages from watcher
|
||||
|
@ -26,8 +26,8 @@ from pathlib import Path
|
||||
from typing import Iterable, List, Set, Type
|
||||
|
||||
from ahriman.core.build_tools.sources import Sources
|
||||
from ahriman.core.database.sqlite import SQLite
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
|
||||
|
||||
class Leaf:
|
||||
@ -54,16 +54,16 @@ class Leaf:
|
||||
return self.package.packages.keys()
|
||||
|
||||
@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
|
||||
:param package: package properties
|
||||
:param paths: repository paths instance
|
||||
:param database: database instance
|
||||
:return: loaded class
|
||||
"""
|
||||
clone_dir = Path(tempfile.mkdtemp())
|
||||
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)
|
||||
finally:
|
||||
shutil.rmtree(clone_dir, ignore_errors=True)
|
||||
@ -95,14 +95,14 @@ class Tree:
|
||||
self.leaves = leaves
|
||||
|
||||
@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
|
||||
:param packages: packages list
|
||||
:param paths: repository paths instance
|
||||
:param database: database instance
|
||||
: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]]:
|
||||
"""
|
||||
|
@ -19,9 +19,12 @@
|
||||
#
|
||||
import datetime
|
||||
import os
|
||||
import subprocess
|
||||
import requests
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
from contextlib import contextmanager
|
||||
from logging import Logger
|
||||
from pathlib import Path
|
||||
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)
|
||||
|
||||
|
||||
@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]:
|
||||
"""
|
||||
list all file paths in given directory
|
||||
|
35
src/ahriman/models/migration.py
Normal file
35
src/ahriman/models/migration.py
Normal 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]
|
50
src/ahriman/models/migration_result.py
Normal file
50
src/ahriman/models/migration_result.py
Normal 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}")
|
@ -239,7 +239,7 @@ class Package:
|
||||
from ahriman.core.build_tools.sources import Sources
|
||||
|
||||
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:
|
||||
# update pkgver first
|
||||
|
@ -19,7 +19,7 @@
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field, fields
|
||||
from dataclasses import asdict, dataclass, field, fields
|
||||
from pathlib import Path
|
||||
from pyalpm import Package # type: ignore
|
||||
from typing import Any, Dict, List, Optional, Type
|
||||
@ -94,3 +94,10 @@ class PackageDescription:
|
||||
licenses=package.licenses,
|
||||
provides=package.provides,
|
||||
url=package.url)
|
||||
|
||||
def view(self) -> Dict[str, Any]:
|
||||
"""
|
||||
generate json package view
|
||||
:return: json-friendly dictionary
|
||||
"""
|
||||
return asdict(self)
|
||||
|
@ -52,16 +52,9 @@ class RepositoryPaths:
|
||||
"""
|
||||
: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"
|
||||
|
||||
@property
|
||||
def manual(self) -> Path:
|
||||
"""
|
||||
:return: directory for manual updates (i.e. from add command)
|
||||
"""
|
||||
return self.root / "manual" / self.architecture
|
||||
|
||||
@property
|
||||
def packages(self) -> Path:
|
||||
"""
|
||||
@ -69,13 +62,6 @@ class RepositoryPaths:
|
||||
"""
|
||||
return self.root / "packages" / self.architecture
|
||||
|
||||
@property
|
||||
def patches(self) -> Path:
|
||||
"""
|
||||
:return: directory for source patches
|
||||
"""
|
||||
return self.root / "patches"
|
||||
|
||||
@property
|
||||
def repository(self) -> Path:
|
||||
"""
|
||||
@ -90,13 +76,6 @@ class RepositoryPaths:
|
||||
"""
|
||||
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
|
||||
def known_architectures(cls: Type[RepositoryPaths], root: Path) -> Set[str]:
|
||||
"""
|
||||
@ -151,30 +130,6 @@ class RepositoryPaths:
|
||||
set_owner(path)
|
||||
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:
|
||||
"""
|
||||
clear package specific files
|
||||
@ -182,9 +137,7 @@ class RepositoryPaths:
|
||||
"""
|
||||
for directory in (
|
||||
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)
|
||||
|
||||
def tree_create(self) -> None:
|
||||
@ -194,10 +147,8 @@ class RepositoryPaths:
|
||||
for directory in (
|
||||
self.cache,
|
||||
self.chroot,
|
||||
self.manual,
|
||||
self.packages,
|
||||
self.patches,
|
||||
self.repository,
|
||||
self.sources):
|
||||
):
|
||||
directory.mkdir(mode=0o755, parents=True, exist_ok=True)
|
||||
self.chown(directory)
|
||||
|
@ -79,18 +79,18 @@ class User:
|
||||
verified = False # the absence of evidence is not the evidence of absence (c) Gin Rummy
|
||||
return verified
|
||||
|
||||
def hash_password(self, salt: str) -> str:
|
||||
def hash_password(self, salt: str) -> User:
|
||||
"""
|
||||
generate hashed password from plain text
|
||||
: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:
|
||||
# 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
|
||||
return ""
|
||||
return self
|
||||
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:
|
||||
"""
|
||||
|
@ -22,7 +22,6 @@ from pathlib import Path
|
||||
|
||||
from ahriman.web.views.index import IndexView
|
||||
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.request import RequestView
|
||||
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/reload-auth reload authentication module
|
||||
|
||||
POST /service-api/v1/remove remove existing package from 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/reload-auth", ReloadAuthView)
|
||||
|
||||
application.router.add_post("/service-api/v1/remove", RemoveView)
|
||||
|
||||
application.router.add_post("/service-api/v1/request", RequestView)
|
||||
|
@ -24,6 +24,7 @@ from typing import Any, Dict, List, Optional, Type
|
||||
|
||||
from ahriman.core.auth.auth import Auth
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database.sqlite import SQLite
|
||||
from ahriman.core.spawn import Spawn
|
||||
from ahriman.core.status.watcher import Watcher
|
||||
from ahriman.models.user_access import UserAccess
|
||||
@ -42,6 +43,14 @@ class BaseView(View):
|
||||
configuration: Configuration = self.request.app["configuration"]
|
||||
return configuration
|
||||
|
||||
@property
|
||||
def database(self) -> SQLite:
|
||||
"""
|
||||
:return: database instance
|
||||
"""
|
||||
database: SQLite = self.request.app["database"]
|
||||
return database
|
||||
|
||||
@property
|
||||
def service(self) -> Watcher:
|
||||
"""
|
||||
|
@ -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()
|
@ -58,7 +58,7 @@ class LoginView(BaseView):
|
||||
identity = UserIdentity.from_username(username, self.validator.max_age)
|
||||
if identity is not None and await self.validator.known_username(username):
|
||||
await remember(self.request, response, identity.to_identity())
|
||||
return response
|
||||
raise response
|
||||
|
||||
raise HTTPUnauthorized()
|
||||
|
||||
@ -81,6 +81,6 @@ class LoginView(BaseView):
|
||||
identity = UserIdentity.from_username(username, self.validator.max_age)
|
||||
if identity is not None and await self.validator.check_credentials(username, data.get("password")):
|
||||
await remember(self.request, response, identity.to_identity())
|
||||
return response
|
||||
raise response
|
||||
|
||||
raise HTTPUnauthorized()
|
||||
|
@ -25,6 +25,7 @@ from aiohttp import web
|
||||
|
||||
from ahriman.core.auth.auth import Auth
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database.sqlite import SQLite
|
||||
from ahriman.core.exceptions import InitializeException
|
||||
from ahriman.core.spawn import Spawn
|
||||
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["configuration"] = configuration
|
||||
|
||||
application.logger.info("setup database and perform migrations")
|
||||
database = application["database"] = SQLite.load(configuration)
|
||||
|
||||
application.logger.info("setup watcher")
|
||||
application["watcher"] = Watcher(architecture, configuration)
|
||||
application["watcher"] = Watcher(architecture, configuration, database)
|
||||
|
||||
application.logger.info("setup process 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))
|
||||
|
||||
application.logger.info("setup authorization")
|
||||
validator = application["validator"] = Auth.load(configuration)
|
||||
validator = application["validator"] = Auth.load(configuration, database)
|
||||
if validator.enabled:
|
||||
from ahriman.web.middlewares.auth_handler import setup_auth
|
||||
setup_auth(application, validator)
|
||||
|
@ -6,39 +6,46 @@ from ahriman.application.application.packages import Packages
|
||||
from ahriman.application.application.properties import Properties
|
||||
from ahriman.application.application.repository import Repository
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database.sqlite import SQLite
|
||||
|
||||
|
||||
@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
|
||||
:param configuration: configuration fixture
|
||||
:param database: database fixture
|
||||
:param mocker: mocker object
|
||||
:return: application test instance
|
||||
"""
|
||||
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)
|
||||
|
||||
|
||||
@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
|
||||
:param configuration: configuration fixture
|
||||
:param database: database fixture
|
||||
:param mocker: mocker object
|
||||
:return: application test instance
|
||||
"""
|
||||
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)
|
||||
|
||||
|
||||
@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
|
||||
:param configuration: configuration fixture
|
||||
:param database: database fixture
|
||||
:param mocker: mocker object
|
||||
:return: application test instance
|
||||
"""
|
||||
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)
|
||||
|
@ -2,7 +2,6 @@ import pytest
|
||||
|
||||
from pathlib import Path
|
||||
from pytest_mock import MockerFixture
|
||||
from unittest import mock
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
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
|
||||
"""
|
||||
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")
|
||||
dependencies_mock = mocker.patch("ahriman.application.application.packages.Packages._process_dependencies")
|
||||
|
||||
application_packages._add_aur(package_ahriman.base, set(), False)
|
||||
insert_mock.assert_called_once_with(package_ahriman)
|
||||
load_mock.assert_called_once_with(
|
||||
application_packages.repository.paths.manual_for(package_ahriman.base),
|
||||
pytest.helpers.anyvar(int),
|
||||
package_ahriman.git_url,
|
||||
application_packages.repository.paths.patches_for(package_ahriman.base))
|
||||
dependencies_mock.assert_called_once_with(
|
||||
application_packages.repository.paths.manual_for(package_ahriman.base), set(), False)
|
||||
pytest.helpers.anyvar(int))
|
||||
dependencies_mock.assert_called_once_with(pytest.helpers.anyvar(int), set(), False)
|
||||
|
||||
|
||||
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)
|
||||
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")
|
||||
dependencies_mock = mocker.patch("ahriman.application.application.packages.Packages._process_dependencies")
|
||||
|
||||
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))
|
||||
copytree_mock.assert_has_calls([
|
||||
mock.call(Path(package_ahriman.base), application_packages.repository.paths.cache_for(package_ahriman.base)),
|
||||
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)
|
||||
insert_mock.assert_called_once_with(package_ahriman)
|
||||
dependencies_mock.assert_called_once_with(pytest.helpers.anyvar(int), set(), False)
|
||||
|
||||
|
||||
def test_add_remote(application_packages: Packages, package_description_ahriman: PackageDescription,
|
||||
|
@ -17,21 +17,12 @@ def test_finalize(application_repository: Repository) -> None:
|
||||
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:
|
||||
"""
|
||||
must clean cache directory
|
||||
"""
|
||||
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()
|
||||
|
||||
|
||||
@ -40,7 +31,7 @@ def test_clean_chroot(application_repository: Repository, mocker: MockerFixture)
|
||||
must clean chroot directory
|
||||
"""
|
||||
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()
|
||||
|
||||
|
||||
@ -48,8 +39,8 @@ def test_clean_manual(application_repository: Repository, mocker: MockerFixture)
|
||||
"""
|
||||
must clean manual directory
|
||||
"""
|
||||
clear_mock = mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_manual")
|
||||
application_repository.clean(False, False, False, True, False, False)
|
||||
clear_mock = mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_queue")
|
||||
application_repository.clean(False, False, True, False)
|
||||
clear_mock.assert_called_once_with()
|
||||
|
||||
|
||||
@ -58,16 +49,7 @@ def test_clean_packages(application_repository: Repository, mocker: MockerFixtur
|
||||
must clean packages directory
|
||||
"""
|
||||
clear_mock = mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_packages")
|
||||
application_repository.clean(False, False, False, False, True, False)
|
||||
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)
|
||||
application_repository.clean(False, False, False, True)
|
||||
clear_mock.assert_called_once_with()
|
||||
|
||||
|
||||
|
@ -7,17 +7,20 @@ from ahriman.application.ahriman import _parser
|
||||
from ahriman.application.application import Application
|
||||
from ahriman.application.lock import Lock
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database.sqlite import SQLite
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def application(configuration: Configuration, mocker: MockerFixture) -> Application:
|
||||
def application(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> Application:
|
||||
"""
|
||||
fixture for application
|
||||
:param configuration: configuration fixture
|
||||
:param database: database fixture
|
||||
:param mocker: mocker object
|
||||
:return: application test instance
|
||||
"""
|
||||
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)
|
||||
|
||||
|
||||
|
@ -6,7 +6,7 @@ from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman.application.handlers import Handler
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.exceptions import MissingArchitecture, MultipleArchitectures
|
||||
from ahriman.core.exceptions import ExitCode, MissingArchitecture, MultipleArchitectures
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
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")
|
||||
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:
|
||||
@ -98,11 +116,14 @@ def test_execute_multiple_not_supported(args: argparse.Namespace, mocker: Mocker
|
||||
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
|
||||
"""
|
||||
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")
|
||||
|
||||
Handler.execute(args)
|
||||
|
@ -12,12 +12,10 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
||||
:param args: command line arguments fixture
|
||||
:return: generated arguments for these test cases
|
||||
"""
|
||||
args.build = False
|
||||
args.cache = False
|
||||
args.chroot = False
|
||||
args.manual = False
|
||||
args.packages = False
|
||||
args.patches = False
|
||||
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")
|
||||
|
||||
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)
|
||||
|
@ -67,24 +67,23 @@ def test_patch_set_list(application: Application, mocker: MockerFixture) -> None
|
||||
must list available patches for the command
|
||||
"""
|
||||
mocker.patch("pathlib.Path.is_dir", return_value=True)
|
||||
glob_mock = mocker.patch("pathlib.Path.glob", return_value=[Path("local")])
|
||||
print_mock = mocker.patch("ahriman.application.handlers.patch.Patch._print")
|
||||
get_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.patches_list", return_value={"ahriman": "patch"})
|
||||
print_mock = mocker.patch("ahriman.core.formatters.printer.Printer.print")
|
||||
|
||||
Patch.patch_set_list(application, "ahriman")
|
||||
glob_mock.assert_called_once_with("*.patch")
|
||||
print_mock.assert_called()
|
||||
get_mock.assert_called_once_with("ahriman")
|
||||
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
|
||||
"""
|
||||
mocker.patch("pathlib.Path.is_dir", return_value=False)
|
||||
glob_mock = mocker.patch("pathlib.Path.glob")
|
||||
print_mock = mocker.patch("ahriman.application.handlers.patch.Patch._print")
|
||||
mocker.patch("ahriman.core.database.sqlite.SQLite.patches_get", return_value=None)
|
||||
print_mock = mocker.patch("ahriman.core.formatters.printer.Printer.print")
|
||||
|
||||
Patch.patch_set_list(application, "ahriman")
|
||||
glob_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("ahriman.models.package.Package.load", return_value=package_ahriman)
|
||||
remove_mock = mocker.patch("ahriman.application.handlers.patch.Patch.patch_set_remove")
|
||||
create_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.patch_create")
|
||||
patch_dir = application.repository.paths.patches_for(package_ahriman.base)
|
||||
mocker.patch("ahriman.core.build_tools.sources.Sources.patch_create", return_value="patch")
|
||||
create_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.patches_insert")
|
||||
|
||||
Patch.patch_set_create(application, Path("path"), ["*.patch"])
|
||||
remove_mock.assert_called_once_with(application, package_ahriman.base)
|
||||
create_mock.assert_called_once_with(Path("path"), patch_dir / "00-main.patch", "*.patch")
|
||||
Patch.patch_set_create(application, "path", ["*.patch"])
|
||||
create_mock.assert_called_once_with(package_ahriman.base, "patch")
|
||||
|
||||
|
||||
def test_patch_set_remove(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must remove patch set for the package
|
||||
"""
|
||||
remove_mock = mocker.patch("shutil.rmtree")
|
||||
patch_dir = application.repository.paths.patches_for(package_ahriman.base)
|
||||
|
||||
remove_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.patches_remove")
|
||||
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)
|
||||
|
@ -6,13 +6,25 @@ from pytest_mock import MockerFixture
|
||||
from ahriman.application.ahriman import _parser
|
||||
from ahriman.application.handlers import UnsafeCommands
|
||||
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:
|
||||
"""
|
||||
must run command
|
||||
"""
|
||||
args.parser = _parser
|
||||
args = _default_args(args)
|
||||
commands_mock = mocker.patch("ahriman.application.handlers.UnsafeCommands.get_unsafe_commands",
|
||||
return_value=["command"])
|
||||
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)
|
||||
|
||||
|
||||
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:
|
||||
"""
|
||||
must return unsafe commands
|
||||
|
@ -3,10 +3,11 @@ import pytest
|
||||
|
||||
from pathlib import Path
|
||||
from pytest_mock import MockerFixture
|
||||
from unittest import mock
|
||||
|
||||
from ahriman.application.handlers import User
|
||||
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.user import User as MUser
|
||||
from ahriman.models.user_access import UserAccess
|
||||
@ -21,96 +22,78 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
||||
args.username = "user"
|
||||
args.action = Action.Update
|
||||
args.as_service = False
|
||||
args.no_reload = False
|
||||
args.password = "pa55w0rd"
|
||||
args.role = UserAccess.Read
|
||||
args.secure = False
|
||||
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
|
||||
"""
|
||||
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")
|
||||
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")
|
||||
get_salt_mock = mocker.patch("ahriman.application.handlers.User.get_salt")
|
||||
reload_mock = mocker.patch("ahriman.core.status.client.Client.reload_auth")
|
||||
create_user_mock = mocker.patch("ahriman.application.handlers.User.user_create", return_value=user)
|
||||
get_salt_mock = mocker.patch("ahriman.application.handlers.User.get_salt", return_value="salt")
|
||||
update_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.user_update")
|
||||
|
||||
User.run(args, "x86_64", configuration, True, False)
|
||||
get_auth_configuration_mock.assert_called_once_with(configuration.include)
|
||||
create_configuration_mock.assert_called_once_with(
|
||||
pytest.helpers.anyvar(int), pytest.helpers.anyvar(int), pytest.helpers.anyvar(int), args.as_service)
|
||||
create_configuration_mock.assert_called_once_with(pytest.helpers.anyvar(int), pytest.helpers.anyvar(int),
|
||||
pytest.helpers.anyvar(int), args.as_service, args.secure)
|
||||
create_user_mock.assert_called_once_with(args)
|
||||
get_salt_mock.assert_called_once_with(configuration)
|
||||
write_configuration_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.secure)
|
||||
reload_mock.assert_called_once_with()
|
||||
update_mock.assert_called_once_with(user)
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
args = _default_args(args)
|
||||
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")
|
||||
create_configuration_mock = mocker.patch("ahriman.application.handlers.User.configuration_create")
|
||||
write_configuration_mock = mocker.patch("ahriman.application.handlers.User.configuration_write")
|
||||
reload_mock = mocker.patch("ahriman.core.status.client.Client.reload_auth")
|
||||
remove_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.user_remove")
|
||||
|
||||
User.run(args, "x86_64", configuration, True, False)
|
||||
get_auth_configuration_mock.assert_called_once_with(configuration.include)
|
||||
create_configuration_mock.assert_not_called()
|
||||
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()
|
||||
remove_mock.assert_called_once_with(args.username)
|
||||
|
||||
|
||||
def test_configuration_create(configuration: Configuration, user: MUser, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must correctly create configuration file
|
||||
"""
|
||||
section = Configuration.section_name("auth", user.access.value)
|
||||
mocker.patch("pathlib.Path.open")
|
||||
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)
|
||||
set_mock.assert_has_calls([
|
||||
mock.call("auth", "salt", pytest.helpers.anyvar(int)),
|
||||
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"))
|
||||
User.configuration_create(configuration, user, "salt", False, False)
|
||||
set_mock.assert_called_once_with("auth", "salt", pytest.helpers.anyvar(int))
|
||||
write_mock.assert_called_once_with(configuration, False)
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
section = Configuration.section_name("auth", user.access.value)
|
||||
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"))
|
||||
assert generated.username == service.username
|
||||
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
|
||||
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)
|
||||
write_mock.assert_not_called()
|
||||
chmod_mock.assert_not_called()
|
||||
with pytest.raises(InitializeException):
|
||||
User.configuration_write(configuration, secure=True)
|
||||
|
||||
|
||||
def test_get_salt_read(configuration: Configuration) -> None:
|
||||
@ -200,31 +179,6 @@ def test_get_salt_generate(configuration: Configuration) -> None:
|
||||
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:
|
||||
"""
|
||||
must create user
|
||||
|
@ -438,6 +438,38 @@ def test_subparsers_user_add_option_role(parser: argparse.ArgumentParser) -> Non
|
||||
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:
|
||||
"""
|
||||
user-remove command must imply action, architecture, lock, no-report, password, quiet, role and unsafe
|
||||
|
@ -3,14 +3,16 @@ import pytest
|
||||
|
||||
from pathlib import Path
|
||||
from pytest_mock import MockerFixture
|
||||
from typing import Any, Type, TypeVar
|
||||
from typing import Any, Dict, Type, TypeVar
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from ahriman.core.auth.auth import Auth
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database.sqlite import SQLite
|
||||
from ahriman.core.spawn import Spawn
|
||||
from ahriman.core.status.watcher import Watcher
|
||||
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_description import PackageDescription
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
@ -48,6 +50,26 @@ def anyvar(cls: Type[T], strict: bool = False) -> T:
|
||||
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
|
||||
@pytest.fixture
|
||||
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)
|
||||
|
||||
|
||||
@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
|
||||
def package_ahriman(package_description_ahriman: PackageDescription) -> Package:
|
||||
"""
|
||||
@ -267,12 +301,13 @@ def user() -> User:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def watcher(configuration: Configuration, mocker: MockerFixture) -> Watcher:
|
||||
def watcher(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> Watcher:
|
||||
"""
|
||||
package status watcher fixture
|
||||
:param configuration: configuration fixture
|
||||
:param database: database fixture
|
||||
:param mocker: mocker object
|
||||
:return: package status watcher test instance
|
||||
"""
|
||||
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
|
||||
return Watcher("x86_64", configuration)
|
||||
return Watcher("x86_64", configuration, database)
|
||||
|
@ -3,24 +3,27 @@ import pytest
|
||||
from ahriman.core.auth.mapping import Mapping
|
||||
from ahriman.core.auth.oauth import OAuth
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database.sqlite import SQLite
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mapping(configuration: Configuration) -> Mapping:
|
||||
def mapping(configuration: Configuration, database: SQLite) -> Mapping:
|
||||
"""
|
||||
auth provider fixture
|
||||
:param configuration: configuration fixture
|
||||
:param database: database fixture
|
||||
:return: auth service instance
|
||||
"""
|
||||
return Mapping(configuration)
|
||||
return Mapping(configuration, database)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def oauth(configuration: Configuration) -> OAuth:
|
||||
def oauth(configuration: Configuration, database: SQLite) -> OAuth:
|
||||
"""
|
||||
OAuth provider fixture
|
||||
:param configuration: configuration fixture
|
||||
:param database: database fixture
|
||||
:return: OAuth2 service instance
|
||||
"""
|
||||
configuration.set("web", "address", "https://example.com")
|
||||
return OAuth(configuration)
|
||||
return OAuth(configuration, database)
|
||||
|
@ -1,10 +1,8 @@
|
||||
import pytest
|
||||
|
||||
from ahriman.core.auth.auth import Auth
|
||||
from ahriman.core.auth.mapping import Mapping
|
||||
from ahriman.core.auth.oauth import OAuth
|
||||
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_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
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
configuration.set_option("auth", "target", "disabled")
|
||||
auth = Auth.load(configuration)
|
||||
auth = Auth.load(configuration, database)
|
||||
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
|
||||
"""
|
||||
auth = Auth.load(configuration)
|
||||
auth = Auth.load(configuration, database)
|
||||
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
|
||||
"""
|
||||
configuration.set_option("auth", "target", "configuration")
|
||||
auth = Auth.load(configuration)
|
||||
auth = Auth.load(configuration, database)
|
||||
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
|
||||
"""
|
||||
configuration.set_option("auth", "target", "oauth")
|
||||
configuration.set_option("web", "address", "https://example.com")
|
||||
auth = Auth.load(configuration)
|
||||
auth = Auth.load(configuration, database)
|
||||
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:
|
||||
"""
|
||||
must pass any credentials
|
||||
|
@ -1,15 +1,17 @@
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman.core.auth.mapping import Mapping
|
||||
from ahriman.models.user import User
|
||||
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
|
||||
"""
|
||||
current_password = user.password
|
||||
user.password = user.hash_password(mapping.salt)
|
||||
mapping._users[user.username] = user
|
||||
user = user.hash_password(mapping.salt)
|
||||
mocker.patch("ahriman.core.database.sqlite.SQLite.user_get", return_value=user)
|
||||
assert await mapping.check_credentials(user.username, current_password)
|
||||
# here password is hashed so it is invalid
|
||||
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)
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
mapping._users[user.username] = user
|
||||
mocker.patch("ahriman.core.database.sqlite.SQLite.user_get", return_value=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
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
@ -54,20 +56,27 @@ def test_get_user_unknown(mapping: Mapping, user: User) -> 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
|
||||
"""
|
||||
mapping._users[user.username] = user
|
||||
mocker.patch("ahriman.core.database.sqlite.SQLite.user_get", return_value=user)
|
||||
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)
|
||||
mocker.patch("ahriman.core.database.sqlite.SQLite.user_get", return_value=None)
|
||||
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
|
||||
"""
|
||||
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 not await mapping.verify_access(user.username, UserAccess.Write, None)
|
||||
|
@ -22,16 +22,25 @@ def test_add(mocker: MockerFixture) -> None:
|
||||
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:
|
||||
"""
|
||||
must calculate diff
|
||||
"""
|
||||
write_mock = mocker.patch("pathlib.Path.write_text")
|
||||
check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output")
|
||||
|
||||
local = Path("local")
|
||||
Sources.diff(local, Path("patch"))
|
||||
write_mock.assert_called_once_with(pytest.helpers.anyvar(int))
|
||||
assert Sources.diff(local)
|
||||
check_output_mock.assert_called_once_with("git", "diff",
|
||||
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")
|
||||
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")
|
||||
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:
|
||||
"""
|
||||
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")
|
||||
|
||||
local = Path("local")
|
||||
Sources.patch_apply(local, Path("patches"))
|
||||
glob_mock.assert_called_once_with("*.patch")
|
||||
check_output_mock.assert_has_calls([
|
||||
mock.call("git", "apply", "--ignore-space-change", "--ignore-whitespace", "01.patch",
|
||||
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()
|
||||
Sources.patch_apply(local, "patches")
|
||||
check_output_mock.assert_called_once_with(
|
||||
"git", "apply", "--ignore-space-change", "--ignore-whitespace",
|
||||
exception=None, cwd=local, input_data="patches", logger=pytest.helpers.anyvar(int)
|
||||
)
|
||||
|
||||
|
||||
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")
|
||||
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")
|
||||
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")
|
||||
|
@ -1,6 +1,8 @@
|
||||
from pathlib import Path
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman.core.build_tools.task import Task
|
||||
from ahriman.core.database.sqlite import SQLite
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
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()
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
@ -20,5 +22,5 @@ def test_init_with_cache(task_ahriman: Task, mocker: MockerFixture) -> None:
|
||||
mocker.patch("ahriman.core.build_tools.sources.Sources.load")
|
||||
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
|
||||
|
13
tests/ahriman/core/database/conftest.py
Normal file
13
tests/ahriman/core/database/conftest.py
Normal 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()
|
33
tests/ahriman/core/database/data/test_data_init.py
Normal file
33
tests/ahriman/core/database/data/test_data_init.py
Normal 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()
|
43
tests/ahriman/core/database/data/test_package_statuses.py
Normal file
43
tests/ahriman/core/database/data/test_package_statuses.py
Normal 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()
|
53
tests/ahriman/core/database/data/test_patches.py
Normal file
53
tests/ahriman/core/database/data/test_patches.py
Normal 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()
|
22
tests/ahriman/core/database/data/test_users.py
Normal file
22
tests/ahriman/core/database/data/test_users.py
Normal 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()
|
15
tests/ahriman/core/database/migrations/conftest.py
Normal file
15
tests/ahriman/core/database/migrations/conftest.py
Normal 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)
|
@ -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
|
109
tests/ahriman/core/database/migrations/test_migrations_init.py
Normal file
109
tests/ahriman/core/database/migrations/test_migrations_init.py
Normal 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
|
@ -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
|
@ -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]
|
39
tests/ahriman/core/database/operations/test_operations.py
Normal file
39
tests/ahriman/core/database/operations/test_operations.py
Normal 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()
|
@ -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
|
@ -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"
|
28
tests/ahriman/core/database/test_sqlite.py
Normal file
28
tests/ahriman/core/database/test_sqlite.py
Normal 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)
|
@ -6,9 +6,11 @@ from ahriman.core.formatters.package_printer import PackagePrinter
|
||||
from ahriman.core.formatters.status_printer import StatusPrinter
|
||||
from ahriman.core.formatters.string_printer import StringPrinter
|
||||
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.build_status import BuildStatus
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.user import User
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -65,3 +67,13 @@ def update_printer(package_ahriman: Package) -> UpdatePrinter:
|
||||
:return: build status printer test instance
|
||||
"""
|
||||
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)
|
||||
|
15
tests/ahriman/core/formatters/test_user_printer.py
Normal file
15
tests/ahriman/core/formatters/test_user_printer.py
Normal 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
|
@ -3,6 +3,7 @@ import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database.sqlite import SQLite
|
||||
from ahriman.core.repository import Repository
|
||||
from ahriman.core.repository.cleaner import Cleaner
|
||||
from ahriman.core.repository.executor import Executor
|
||||
@ -11,68 +12,71 @@ from ahriman.core.repository.update_handler import UpdateHandler
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cleaner(configuration: Configuration, mocker: MockerFixture) -> Cleaner:
|
||||
def cleaner(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> Cleaner:
|
||||
"""
|
||||
fixture for cleaner
|
||||
:param configuration: configuration fixture
|
||||
:param database: database fixture
|
||||
:param mocker: mocker object
|
||||
:return: cleaner test instance
|
||||
"""
|
||||
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
|
||||
def executor(configuration: Configuration, mocker: MockerFixture) -> Executor:
|
||||
def executor(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> Executor:
|
||||
"""
|
||||
fixture for executor
|
||||
:param configuration: configuration fixture
|
||||
:param database: database fixture
|
||||
:param mocker: mocker object
|
||||
: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_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_queue")
|
||||
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
|
||||
def repository(configuration: Configuration, mocker: MockerFixture) -> Repository:
|
||||
def repository(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> Repository:
|
||||
"""
|
||||
fixture for repository
|
||||
:param configuration: configuration fixture
|
||||
:param database: database fixture
|
||||
:param mocker: mocker object
|
||||
:return: repository test instance
|
||||
"""
|
||||
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
|
||||
def properties(configuration: Configuration) -> Properties:
|
||||
def properties(configuration: Configuration, database: SQLite) -> Properties:
|
||||
"""
|
||||
fixture for properties
|
||||
:param configuration: configuration fixture
|
||||
:param database: database fixture
|
||||
: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
|
||||
def update_handler(configuration: Configuration, mocker: MockerFixture) -> UpdateHandler:
|
||||
def update_handler(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> UpdateHandler:
|
||||
"""
|
||||
fixture for update handler
|
||||
:param configuration: configuration fixture
|
||||
:param database: database fixture
|
||||
:param mocker: mocker object
|
||||
: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_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_queue")
|
||||
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)
|
||||
|
@ -36,15 +36,6 @@ def test_packages_built(cleaner: Cleaner) -> None:
|
||||
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:
|
||||
"""
|
||||
must remove every cached sources
|
||||
@ -63,15 +54,6 @@ def test_clear_chroot(cleaner: Cleaner, mocker: MockerFixture) -> None:
|
||||
_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:
|
||||
"""
|
||||
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()])
|
||||
|
||||
|
||||
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)
|
||||
cleaner.clear_patches()
|
||||
_mock_clear_check()
|
||||
clear_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.build_queue_clear")
|
||||
cleaner.clear_queue()
|
||||
clear_mock.assert_called_once_with(None)
|
||||
|
@ -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)
|
||||
# must update status
|
||||
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:
|
||||
|
@ -1,51 +1,52 @@
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database.sqlite import SQLite
|
||||
from ahriman.core.exceptions import UnsafeRun
|
||||
from ahriman.core.repository.properties import Properties
|
||||
from ahriman.core.status.web_client import WebClient
|
||||
|
||||
|
||||
def test_create_tree_on_load(configuration: Configuration, mocker: MockerFixture) -> None:
|
||||
def test_create_tree_on_load(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must create tree on load
|
||||
"""
|
||||
mocker.patch("ahriman.core.repository.properties.check_user")
|
||||
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()
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
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")
|
||||
Properties("x86_64", configuration, True, False)
|
||||
Properties("x86_64", configuration, database, True, False)
|
||||
|
||||
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
|
||||
"""
|
||||
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
|
||||
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()
|
||||
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
|
||||
"""
|
||||
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
|
||||
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)
|
||||
|
@ -168,7 +168,7 @@ def test_updates_manual_clear(update_handler: UpdateHandler, mocker: MockerFixtu
|
||||
update_handler.updates_manual()
|
||||
|
||||
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,
|
||||
@ -176,9 +176,8 @@ def test_updates_manual_status_known(update_handler: UpdateHandler, package_ahri
|
||||
"""
|
||||
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.models.package.Package.load", return_value=package_ahriman)
|
||||
status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_pending")
|
||||
|
||||
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
|
||||
"""
|
||||
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.models.package.Package.load", return_value=package_ahriman)
|
||||
status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_unknown")
|
||||
|
||||
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
|
||||
"""
|
||||
mocker.patch("pathlib.Path.iterdir", return_value=[package_ahriman.base])
|
||||
mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[])
|
||||
mocker.patch("ahriman.models.package.Package.load", side_effect=Exception())
|
||||
|
||||
mocker.patch("ahriman.core.database.sqlite.SQLite.build_queue_get", side_effect=Exception())
|
||||
mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman])
|
||||
assert update_handler.updates_manual() == []
|
||||
|
@ -1,33 +1,8 @@
|
||||
import pytest
|
||||
|
||||
from typing import Any, Dict
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.status.client import Client
|
||||
from ahriman.core.status.web_client import WebClient
|
||||
from ahriman.models.build_status import 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
|
||||
@ -47,5 +22,5 @@ def web_client(configuration: Configuration) -> WebClient:
|
||||
:param configuration: configuration fixture
|
||||
:return: web client test instance
|
||||
"""
|
||||
configuration.set("web", "port", 8080)
|
||||
configuration.set("web", "port", "8080")
|
||||
return WebClient(configuration)
|
||||
|
@ -61,13 +61,6 @@ def test_get_self(client: Client) -> None:
|
||||
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:
|
||||
"""
|
||||
must process remove without errors
|
||||
|
@ -6,6 +6,7 @@ from pytest_mock import MockerFixture
|
||||
from unittest.mock import PropertyMock
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database.sqlite import SQLite
|
||||
from ahriman.core.exceptions import UnknownPackage
|
||||
from ahriman.core.status.watcher import Watcher
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
@ -21,122 +22,12 @@ def test_force_no_report(configuration: Configuration, mocker: MockerFixture) ->
|
||||
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
|
||||
|
||||
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()
|
||||
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:
|
||||
"""
|
||||
must return package status
|
||||
@ -160,7 +51,7 @@ def test_load(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture)
|
||||
must correctly load packages
|
||||
"""
|
||||
mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[package_ahriman])
|
||||
cache_mock = mocker.patch("ahriman.core.status.watcher.Watcher._cache_load")
|
||||
cache_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.packages_get")
|
||||
|
||||
watcher.load()
|
||||
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
|
||||
"""
|
||||
status = BuildStatus(BuildStatusEnum.Success)
|
||||
mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[package_ahriman])
|
||||
mocker.patch("ahriman.core.status.watcher.Watcher._cache_load")
|
||||
watcher.known = {package_ahriman.base: (package_ahriman, BuildStatus(BuildStatusEnum.Success))}
|
||||
mocker.patch("ahriman.core.database.sqlite.SQLite.packages_get", return_value=[(package_ahriman, status)])
|
||||
watcher.known = {package_ahriman.base: (package_ahriman, status)}
|
||||
|
||||
watcher.load()
|
||||
_, status = watcher.known[package_ahriman.base]
|
||||
@ -186,32 +78,32 @@ def test_remove(watcher: Watcher, package_ahriman: Package, mocker: MockerFixtur
|
||||
"""
|
||||
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.remove(package_ahriman.base)
|
||||
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:
|
||||
"""
|
||||
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)
|
||||
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:
|
||||
"""
|
||||
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)
|
||||
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]
|
||||
assert package == package_ahriman
|
||||
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
|
||||
"""
|
||||
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.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]
|
||||
assert package == package_ahriman
|
||||
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
|
||||
"""
|
||||
cache_mock = mocker.patch("ahriman.core.status.watcher.Watcher._cache_save")
|
||||
|
||||
with pytest.raises(UnknownPackage):
|
||||
watcher.update(package_ahriman.base, BuildStatusEnum.Unknown, None)
|
||||
cache_mock.assert_called_once_with()
|
||||
|
||||
|
||||
def test_update_self(watcher: Watcher) -> None:
|
||||
|
@ -230,32 +230,6 @@ def test_get_self_failed_http_error(web_client: WebClient, mocker: MockerFixture
|
||||
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:
|
||||
"""
|
||||
must process package removal
|
||||
|
@ -8,6 +8,14 @@ from unittest import mock
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
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:
|
||||
@ -40,6 +48,33 @@ def test_from_path_file_missing(mocker: MockerFixture) -> None:
|
||||
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:
|
||||
"""
|
||||
dump must not be empty
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user