packagers support (#100)

This commit is contained in:
Evgenii Alekseev 2023-06-05 02:37:19 +03:00 committed by GitHub
parent d495163fdd
commit 4b984afb64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
89 changed files with 849 additions and 318 deletions

View File

@ -58,7 +58,7 @@ 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 --command="$*" &> /dev/null; then
elif ahriman help-commands-unsafe -- "$@" &> /dev/null; then
AHRIMAN_EXECUTABLE=("sudo" "-u" "$AHRIMAN_USER" "--" "ahriman")
else
AHRIMAN_EXECUTABLE=("ahriman")

View File

@ -1,4 +1,4 @@
.TH AHRIMAN "1" "2023\-05\-28" "ahriman" "Generated Python Manual"
.TH AHRIMAN "1" "2023\-06\-03" "ahriman" "Generated Python Manual"
.SH NAME
ahriman
.SH SYNOPSIS
@ -199,13 +199,12 @@ show help message for application or command and exit
show help message for specific command
.SH COMMAND \fI\,'ahriman help\-commands\-unsafe'\/\fR
usage: ahriman help\-commands\-unsafe [\-h] [\-\-command COMMAND]
usage: ahriman help\-commands\-unsafe [\-h] [command ...]
list unsafe commands as defined in default args
.SH OPTIONS \fI\,'ahriman help\-commands\-unsafe'\/\fR
.TP
\fB\-\-command\fR \fI\,COMMAND\/\fR
\fBcommand\fR
instead of showing commands, just test command line for unsafe subcommand and return 0 in case if command is safe and 1
otherwise
@ -226,7 +225,7 @@ print application and its dependencies versions
.SH COMMAND \fI\,'ahriman package\-add'\/\fR
usage: ahriman package\-add [\-h] [\-\-dependencies | \-\-no\-dependencies] [\-e] [\-n] [\-y]
[\-s {auto,archive,aur,directory,local,remote,repository}]
[\-s {auto,archive,aur,directory,local,remote,repository}] [\-u USERNAME]
package [package ...]
add existing or new package to the build queue
@ -256,6 +255,10 @@ download fresh package databases from the mirror before actions, \-yy to force r
\fB\-s\fR \fI\,{auto,archive,aur,directory,local,remote,repository}\/\fR, \fB\-\-source\fR \fI\,{auto,archive,aur,directory,local,remote,repository}\/\fR
explicitly specify the package source for this command
.TP
\fB\-u\fR \fI\,USERNAME\/\fR, \fB\-\-username\fR \fI\,USERNAME\/\fR
build as user
.SH COMMAND \fI\,'ahriman package\-remove'\/\fR
usage: ahriman package\-remove [\-h] package [package ...]
@ -457,7 +460,7 @@ download fresh package databases from the mirror before actions, \-yy to force r
.SH COMMAND \fI\,'ahriman repo\-rebuild'\/\fR
usage: ahriman repo\-rebuild [\-h] [\-\-depends\-on DEPENDS_ON] [\-\-dry\-run] [\-\-from\-database] [\-e]
[\-s {unknown,pending,building,failed,success}]
[\-s {unknown,pending,building,failed,success}] [\-u USERNAME]
force rebuild whole repository
@ -484,6 +487,10 @@ return non\-zero exit status if result is empty
\fB\-s\fR \fI\,{unknown,pending,building,failed,success}\/\fR, \fB\-\-status\fR \fI\,{unknown,pending,building,failed,success}\/\fR
filter packages by status. Requires \-\-from\-database to be set
.TP
\fB\-u\fR \fI\,USERNAME\/\fR, \fB\-\-username\fR \fI\,USERNAME\/\fR
build as user
.SH COMMAND \fI\,'ahriman repo\-remove\-unknown'\/\fR
usage: ahriman repo\-remove\-unknown [\-h] [\-\-dry\-run]
@ -553,7 +560,7 @@ instead of running all triggers as set by configuration, just process specified
.SH COMMAND \fI\,'ahriman repo\-update'\/\fR
usage: ahriman repo\-update [\-h] [\-\-aur | \-\-no\-aur] [\-\-dependencies | \-\-no\-dependencies] [\-\-dry\-run] [\-e]
[\-\-local | \-\-no\-local] [\-\-manual | \-\-no\-manual] [\-\-vcs | \-\-no\-vcs] [\-y]
[\-\-local | \-\-no\-local] [\-\-manual | \-\-no\-manual] [\-u USERNAME] [\-\-vcs | \-\-no\-vcs] [\-y]
[package ...]
check for packages updates and run build process if requested
@ -587,6 +594,10 @@ enable or disable checking of local packages for updates
\fB\-\-manual\fR, \fB\-\-no\-manual\fR
include or exclude manual updates
.TP
\fB\-u\fR \fI\,USERNAME\/\fR, \fB\-\-username\fR \fI\,USERNAME\/\fR
build as user
.TP
\fB\-\-vcs\fR, \fB\-\-no\-vcs\fR
fetch actual version of VCS packages
@ -724,7 +735,8 @@ drop into python shell while having created application
instead of dropping into shell, just execute the specified code
.SH COMMAND \fI\,'ahriman user\-add'\/\fR
usage: ahriman user\-add [\-h] [\-p PASSWORD] [\-r {unauthorized,read,reporter,full}] [\-s] username
usage: ahriman user\-add [\-h] [\-\-key KEY] [\-\-packager PACKAGER] [\-p PASSWORD] [\-r {unauthorized,read,reporter,full}] [\-s]
username
update user for web services with the given password and role. In case if password was not entered it will be asked interactively
@ -733,6 +745,14 @@ update user for web services with the given password and role. In case if passwo
username for web service
.SH OPTIONS \fI\,'ahriman user\-add'\/\fR
.TP
\fB\-\-key\fR \fI\,KEY\/\fR
optional PGP key used by this user. The private key must be imported
.TP
\fB\-\-packager\fR \fI\,PACKAGER\/\fR
optional packager id used for build process in form of `Name Surname <mail@example.com>`
.TP
\fB\-p\fR \fI\,PASSWORD\/\fR, \fB\-\-password\fR \fI\,PASSWORD\/\fR
user password. Blank password will be treated as empty password, which is in particular must be used for OAuth2

View File

@ -68,6 +68,14 @@ ahriman.core.database.migrations.m007\_check\_depends module
:no-undoc-members:
:show-inheritance:
ahriman.core.database.migrations.m008\_packagers module
-------------------------------------------------------
.. automodule:: ahriman.core.database.migrations.m008_packagers
:members:
:no-undoc-members:
:show-inheritance:
Module contents
---------------

View File

@ -116,6 +116,14 @@ ahriman.models.package\_source module
:no-undoc-members:
:show-inheritance:
ahriman.models.packagers module
-------------------------------
.. automodule:: ahriman.models.packagers
:members:
:no-undoc-members:
:show-inheritance:
ahriman.models.pacman\_synchronization module
---------------------------------------------

View File

@ -6,13 +6,13 @@ _shtab_ahriman_option_strings=('-h' '--help' '-a' '--architecture' '-c' '--confi
_shtab_ahriman_aur_search_option_strings=('-h' '--help' '-e' '--exit-code' '--info' '--no-info' '--sort-by')
_shtab_ahriman_search_option_strings=('-h' '--help' '-e' '--exit-code' '--info' '--no-info' '--sort-by')
_shtab_ahriman_help_option_strings=('-h' '--help')
_shtab_ahriman_help_commands_unsafe_option_strings=('-h' '--help' '--command')
_shtab_ahriman_help_commands_unsafe_option_strings=('-h' '--help')
_shtab_ahriman_help_updates_option_strings=('-h' '--help' '-e' '--exit-code')
_shtab_ahriman_help_version_option_strings=('-h' '--help')
_shtab_ahriman_version_option_strings=('-h' '--help')
_shtab_ahriman_package_add_option_strings=('-h' '--help' '--dependencies' '--no-dependencies' '-e' '--exit-code' '-n' '--now' '-y' '--refresh' '-s' '--source')
_shtab_ahriman_add_option_strings=('-h' '--help' '--dependencies' '--no-dependencies' '-e' '--exit-code' '-n' '--now' '-y' '--refresh' '-s' '--source')
_shtab_ahriman_package_update_option_strings=('-h' '--help' '--dependencies' '--no-dependencies' '-e' '--exit-code' '-n' '--now' '-y' '--refresh' '-s' '--source')
_shtab_ahriman_package_add_option_strings=('-h' '--help' '--dependencies' '--no-dependencies' '-e' '--exit-code' '-n' '--now' '-y' '--refresh' '-s' '--source' '-u' '--username')
_shtab_ahriman_add_option_strings=('-h' '--help' '--dependencies' '--no-dependencies' '-e' '--exit-code' '-n' '--now' '-y' '--refresh' '-s' '--source' '-u' '--username')
_shtab_ahriman_package_update_option_strings=('-h' '--help' '--dependencies' '--no-dependencies' '-e' '--exit-code' '-n' '--now' '-y' '--refresh' '-s' '--source' '-u' '--username')
_shtab_ahriman_package_remove_option_strings=('-h' '--help')
_shtab_ahriman_remove_option_strings=('-h' '--help')
_shtab_ahriman_package_status_option_strings=('-h' '--help' '--ahriman' '-e' '--exit-code' '--info' '--no-info' '-s' '--status')
@ -31,8 +31,8 @@ _shtab_ahriman_repo_create_keyring_option_strings=('-h' '--help')
_shtab_ahriman_repo_create_mirrorlist_option_strings=('-h' '--help')
_shtab_ahriman_repo_daemon_option_strings=('-h' '--help' '-i' '--interval' '--aur' '--no-aur' '--dependencies' '--no-dependencies' '--local' '--no-local' '--manual' '--no-manual' '--vcs' '--no-vcs' '-y' '--refresh')
_shtab_ahriman_daemon_option_strings=('-h' '--help' '-i' '--interval' '--aur' '--no-aur' '--dependencies' '--no-dependencies' '--local' '--no-local' '--manual' '--no-manual' '--vcs' '--no-vcs' '-y' '--refresh')
_shtab_ahriman_repo_rebuild_option_strings=('-h' '--help' '--depends-on' '--dry-run' '--from-database' '-e' '--exit-code' '-s' '--status')
_shtab_ahriman_rebuild_option_strings=('-h' '--help' '--depends-on' '--dry-run' '--from-database' '-e' '--exit-code' '-s' '--status')
_shtab_ahriman_repo_rebuild_option_strings=('-h' '--help' '--depends-on' '--dry-run' '--from-database' '-e' '--exit-code' '-s' '--status' '-u' '--username')
_shtab_ahriman_rebuild_option_strings=('-h' '--help' '--depends-on' '--dry-run' '--from-database' '-e' '--exit-code' '-s' '--status' '-u' '--username')
_shtab_ahriman_repo_remove_unknown_option_strings=('-h' '--help' '--dry-run')
_shtab_ahriman_remove_unknown_option_strings=('-h' '--help' '--dry-run')
_shtab_ahriman_repo_report_option_strings=('-h' '--help')
@ -45,8 +45,8 @@ _shtab_ahriman_repo_sync_option_strings=('-h' '--help')
_shtab_ahriman_sync_option_strings=('-h' '--help')
_shtab_ahriman_repo_tree_option_strings=('-h' '--help')
_shtab_ahriman_repo_triggers_option_strings=('-h' '--help')
_shtab_ahriman_repo_update_option_strings=('-h' '--help' '--aur' '--no-aur' '--dependencies' '--no-dependencies' '--dry-run' '-e' '--exit-code' '--local' '--no-local' '--manual' '--no-manual' '--vcs' '--no-vcs' '-y' '--refresh')
_shtab_ahriman_update_option_strings=('-h' '--help' '--aur' '--no-aur' '--dependencies' '--no-dependencies' '--dry-run' '-e' '--exit-code' '--local' '--no-local' '--manual' '--no-manual' '--vcs' '--no-vcs' '-y' '--refresh')
_shtab_ahriman_repo_update_option_strings=('-h' '--help' '--aur' '--no-aur' '--dependencies' '--no-dependencies' '--dry-run' '-e' '--exit-code' '--local' '--no-local' '--manual' '--no-manual' '-u' '--username' '--vcs' '--no-vcs' '-y' '--refresh')
_shtab_ahriman_update_option_strings=('-h' '--help' '--aur' '--no-aur' '--dependencies' '--no-dependencies' '--dry-run' '-e' '--exit-code' '--local' '--no-local' '--manual' '--no-manual' '-u' '--username' '--vcs' '--no-vcs' '-y' '--refresh')
_shtab_ahriman_service_clean_option_strings=('-h' '--help' '--cache' '--no-cache' '--chroot' '--no-chroot' '--manual' '--no-manual' '--packages' '--no-packages' '--pacman' '--no-pacman')
_shtab_ahriman_clean_option_strings=('-h' '--help' '--cache' '--no-cache' '--chroot' '--no-chroot' '--manual' '--no-manual' '--packages' '--no-packages' '--pacman' '--no-pacman')
_shtab_ahriman_repo_clean_option_strings=('-h' '--help' '--cache' '--no-cache' '--chroot' '--no-chroot' '--manual' '--no-manual' '--packages' '--no-packages' '--pacman' '--no-pacman')
@ -65,7 +65,7 @@ _shtab_ahriman_repo_setup_option_strings=('-h' '--help' '--build-as-user' '--bui
_shtab_ahriman_setup_option_strings=('-h' '--help' '--build-as-user' '--build-command' '--from-configuration' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--repository' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket')
_shtab_ahriman_service_shell_option_strings=('-h' '--help')
_shtab_ahriman_shell_option_strings=('-h' '--help')
_shtab_ahriman_user_add_option_strings=('-h' '--help' '-p' '--password' '-r' '--role' '-s' '--secure')
_shtab_ahriman_user_add_option_strings=('-h' '--help' '--key' '--packager' '-p' '--password' '-r' '--role' '-s' '--secure')
_shtab_ahriman_user_list_option_strings=('-h' '--help' '-e' '--exit-code' '-r' '--role')
_shtab_ahriman_user_remove_option_strings=('-h' '--help')
_shtab_ahriman_web_option_strings=('-h' '--help')
@ -133,6 +133,7 @@ _shtab_ahriman_search___info_nargs=0
_shtab_ahriman_search___no_info_nargs=0
_shtab_ahriman_help__h_nargs=0
_shtab_ahriman_help___help_nargs=0
_shtab_ahriman_help_commands_unsafe_pos_0_nargs=*
_shtab_ahriman_help_commands_unsafe__h_nargs=0
_shtab_ahriman_help_commands_unsafe___help_nargs=0
_shtab_ahriman_help_updates__h_nargs=0

View File

@ -95,6 +95,7 @@ _shtab_ahriman_add_options=(
{-n,--now}"[run update function after]"
"*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date]"
{-s,--source}"[explicitly specify the package source for this command]:source:(auto archive aur directory local remote repository)"
{-u,--username}"[build as user]:username:"
"(*):package source (base name, path to local files, remote URL):"
)
@ -151,7 +152,7 @@ _shtab_ahriman_help_options=(
_shtab_ahriman_help_commands_unsafe_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
"--command[instead of showing commands, just test command line for unsafe subcommand and return 0 in case if command is safe and 1 otherwise]:command:"
"(*)::instead of showing commands, just test command line for unsafe subcommand and return 0 in case if command is safe and 1 otherwise:"
)
_shtab_ahriman_help_updates_options=(
@ -192,6 +193,7 @@ _shtab_ahriman_package_add_options=(
{-n,--now}"[run update function after]"
"*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date]"
{-s,--source}"[explicitly specify the package source for this command]:source:(auto archive aur directory local remote repository)"
{-u,--username}"[build as user]:username:"
"(*):package source (base name, path to local files, remote URL):"
)
@ -227,6 +229,7 @@ _shtab_ahriman_package_update_options=(
{-n,--now}"[run update function after]"
"*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date]"
{-s,--source}"[explicitly specify the package source for this command]:source:(auto archive aur directory local remote repository)"
{-u,--username}"[build as user]:username:"
"(*):package source (base name, path to local files, remote URL):"
)
@ -263,6 +266,7 @@ _shtab_ahriman_rebuild_options=(
"--from-database[read packages from database instead of filesystem. This feature in particular is required in case if you would like to restore repository from another repository instance. Note, however, that in order to restore packages you need to have original ahriman instance run with web service and have run repo-update at least once.]"
{-e,--exit-code}"[return non-zero exit status if result is empty]"
{-s,--status}"[filter packages by status. Requires --from-database to be set]:status:(unknown pending building failed success)"
{-u,--username}"[build as user]:username:"
)
_shtab_ahriman_remove_options=(
@ -349,6 +353,7 @@ _shtab_ahriman_repo_rebuild_options=(
"--from-database[read packages from database instead of filesystem. This feature in particular is required in case if you would like to restore repository from another repository instance. Note, however, that in order to restore packages you need to have original ahriman instance run with web service and have run repo-update at least once.]"
{-e,--exit-code}"[return non-zero exit status if result is empty]"
{-s,--status}"[filter packages by status. Requires --from-database to be set]:status:(unknown pending building failed success)"
{-u,--username}"[build as user]:username:"
)
_shtab_ahriman_repo_remove_unknown_options=(
@ -413,6 +418,7 @@ _shtab_ahriman_repo_update_options=(
{-e,--exit-code}"[return non-zero exit status if result is empty]"
{--local,--no-local}"[enable or disable checking of local packages for updates]:local:"
{--manual,--no-manual}"[include or exclude manual updates]:manual:"
{-u,--username}"[build as user]:username:"
{--vcs,--no-vcs}"[fetch actual version of VCS packages]:vcs:"
"*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date]"
"(*)::filter check by package base:"
@ -529,6 +535,7 @@ _shtab_ahriman_update_options=(
{-e,--exit-code}"[return non-zero exit status if result is empty]"
{--local,--no-local}"[enable or disable checking of local packages for updates]:local:"
{--manual,--no-manual}"[include or exclude manual updates]:manual:"
{-u,--username}"[build as user]:username:"
{--vcs,--no-vcs}"[fetch actual version of VCS packages]:vcs:"
"*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date]"
"(*)::filter check by package base:"
@ -536,6 +543,8 @@ _shtab_ahriman_update_options=(
_shtab_ahriman_user_add_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
"--key[optional PGP key used by this user. The private key must be imported]:key:"
"--packager[optional packager id used for build process in form of \`Name Surname \<mail\@example.com\>\`]:packager:"
{-p,--password}"[user password. Blank password will be treated as empty password, which is in particular must be used for OAuth2 authorization type.]:password:"
{-r,--role}"[user access level]:role:(unauthorized read reporter full)"
{-s,--secure}"[set file permissions to user-only]"

View File

@ -87,7 +87,6 @@ Settings for signing packages or repository. Group name can refer to architectur
* ``target`` - configuration flag to enable signing, space separated list of strings, required. Allowed values are ``package`` (sign each package separately), ``repository`` (sign repository database file).
* ``key`` - default PGP key, string, required. This key will also be used for database signing if enabled.
* ``key_*`` settings - PGP key which will be used for specific packages, string, optional. For example, if there is ``key_yay`` option the specified key will be used for yay package and default key for others.
``web:*`` groups
----------------

View File

@ -64,7 +64,7 @@ Initial setup
.. code-block:: shell
echo 'Cmnd_Alias CARCHBUILD_CMD = /usr/local/bin/ahriman-x86_64-build *' | tee -a /etc/sudoers.d/ahriman
echo 'ahriman ALL=(ALL) NOPASSWD: CARCHBUILD_CMD' | tee -a /etc/sudoers.d/ahriman
echo 'ahriman ALL=(ALL) NOPASSWD:SETENV: CARCHBUILD_CMD' | tee -a /etc/sudoers.d/ahriman
chmod 400 /etc/sudoers.d/ahriman
This command supports several arguments, kindly refer to its help message.

View File

@ -87,6 +87,7 @@
<th data-sortable="true" data-field="packages">packages</th>
<th data-sortable="true" data-visible="false" data-field="groups">groups</th>
<th data-sortable="true" data-visible="false" data-field="licenses">licenses</th>
<th data-sortable="true" data-visible="false" data-field="packager">packager</th>
<th data-sortable="true" data-field="timestamp">last update</th>
<th data-sortable="true" data-cell-style="statusFormat" data-field="status">status</th>
</tr>

View File

@ -98,6 +98,7 @@
id: package_base,
base: web_url ? `<a href="${safe(web_url)}" title="${safe(package_base)}">${safe(package_base)}</a>` : safe(package_base),
version: safe(description.package.version),
packager: description.package.packager ? safe(description.package.packager) : "",
packages: listToTable(Object.keys(description.package.packages)),
groups: listToTable(extractListProperties(description.package, "groups")),
licenses: listToTable(extractListProperties(description.package, "licenses")),
@ -120,8 +121,8 @@
table.bootstrapTable("hideLoading");
} else {
// other errors
const messaga = error => { return `Could not load list of packages: ${error}`; };
showFailure("Load failure", messaga, jqXHR, errorThrown);
const message = error => { return `Could not load list of packages: ${error}`; };
showFailure("Load failure", message, jqXHR, errorThrown);
}
hideControls(true);
},

View File

@ -27,7 +27,7 @@ from typing import TypeVar
from ahriman import version
from ahriman.application import handlers
from ahriman.core.util import enum_values
from ahriman.core.util import enum_values, extract_user
from ahriman.models.action import Action
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.log_handler import LogHandler
@ -187,8 +187,8 @@ def _set_help_commands_unsafe_parser(root: SubParserAction) -> argparse.Argument
"""
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.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", nargs="*")
parser.set_defaults(handler=handlers.UnsafeCommands, architecture=[""], lock=None, report=False, quiet=True,
unsafe=True, parser=_parser)
return parser
@ -262,6 +262,7 @@ def _set_package_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
action="count", default=False)
parser.add_argument("-s", "--source", help="explicitly specify the package source for this command",
type=PackageSource, choices=enum_values(PackageSource), default=PackageSource.Auto)
parser.add_argument("-u", "--username", help="build as user", default=extract_user())
parser.set_defaults(handler=handlers.Add)
return parser
@ -481,7 +482,8 @@ def _set_repo_check_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("-y", "--refresh", help="download fresh package databases from the mirror before actions, "
"-yy to force refresh even if up to date",
action="count", default=False)
parser.set_defaults(handler=handlers.Update, dependencies=False, dry_run=True, aur=True, local=True, manual=False)
parser.set_defaults(handler=handlers.Update, dependencies=False, dry_run=True, aur=True, local=True, manual=False,
username=None)
return parser
@ -578,6 +580,7 @@ def _set_repo_rebuild_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
parser.add_argument("-s", "--status", help="filter packages by status. Requires --from-database to be set",
type=BuildStatusEnum, choices=enum_values(BuildStatusEnum))
parser.add_argument("-u", "--username", help="build as user", default=extract_user())
parser.set_defaults(handler=handlers.Rebuild)
return parser
@ -752,6 +755,7 @@ def _set_repo_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--manual", help="include or exclude manual updates",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("-u", "--username", help="build as user", default=extract_user())
parser.add_argument("--vcs", help="fetch actual version of VCS packages",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("-y", "--refresh", help="download fresh package databases from the mirror before actions, "
@ -923,6 +927,9 @@ def _set_user_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
"root privileges because it performs write to filesystem configuration.",
formatter_class=_formatter)
parser.add_argument("username", help="username for web service")
parser.add_argument("--key", help="optional PGP key used by this user. The private key must be imported")
parser.add_argument("--packager", help="optional packager id used for build process in form of "
"`Name Surname <mail@example.com>`")
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",
@ -949,8 +956,8 @@ def _set_user_list_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("username", help="filter users by username", nargs="?")
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
parser.add_argument("-r", "--role", help="filter users by role", type=UserAccess, choices=enum_values(UserAccess))
parser.set_defaults(handler=handlers.Users, action=Action.List, architecture=[""], lock=None, report=False, # nosec
password="", quiet=True, unsafe=True)
parser.set_defaults(handler=handlers.Users, action=Action.List, architecture=[""], lock=None, report=False,
quiet=True, unsafe=True)
return parser
@ -968,8 +975,8 @@ 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.set_defaults(handler=handlers.Users, action=Action.Remove, architecture=[""], lock=None, report=False, # nosec
password="", quiet=True)
parser.set_defaults(handler=handlers.Users, action=Action.Remove, architecture=[""], lock=None, report=False,
quiet=True)
return parser

View File

@ -39,7 +39,7 @@ class Application(ApplicationPackages, ApplicationRepository):
>>> configuration = Configuration()
>>> application = Application("x86_64", configuration, report=True, unsafe=False)
>>> # add packages to build queue
>>> application.add(["ahriman"], PackageSource.AUR, without_dependencies=False)
>>> application.add(["ahriman"], PackageSource.AUR)
>>>
>>> # check for updates
>>> updates = application.updates([], aur=True, local=True, manual=True, vcs=True, log_fn=print)
@ -96,21 +96,25 @@ class Application(ApplicationPackages, ApplicationRepository):
Args:
packages(list[Package]): list of source packages of which dependencies have to be processed
process_dependencies(bool): if no set, dependencies will not be processed
Returns:
list[Package]: updated packages list. Packager for dependencies will be copied from
original package
"""
def missing_dependencies(source: Iterable[Package]) -> set[str]:
# build initial list of dependencies
result = set()
for package in source:
result.update(package.depends_build)
def missing_dependencies(source: Iterable[Package]) -> dict[str, str | None]:
# append list of known packages with packages which are in current sources
satisfied_packages = known_packages | {
single
for package in source
for single in package.packages_full
}
# remove ones which are already well-known
result = result.difference(known_packages)
# remove ones which are in this list already
for package in source:
result = result.difference(package.packages_full)
return result
return {
dependency: package.packager
for package in source
for dependency in package.depends_build
if dependency not in satisfied_packages
}
if not process_dependencies or not packages:
return packages
@ -119,8 +123,8 @@ class Application(ApplicationPackages, ApplicationRepository):
with_dependencies = {package.base: package for package in packages}
while missing := missing_dependencies(with_dependencies.values()):
for package_name in missing:
package = Package.from_aur(package_name, self.repository.pacman)
for package_name, username in missing.items():
package = Package.from_aur(package_name, self.repository.pacman, username)
with_dependencies[package.base] = package
return list(with_dependencies.values())

View File

@ -55,15 +55,15 @@ class ApplicationPackages(ApplicationProperties):
dst = self.repository.paths.packages / local_path.name
shutil.copy(local_path, dst)
def _add_aur(self, source: str) -> None:
def _add_aur(self, source: str, username: str | None) -> None:
"""
add package from AUR
Args:
source(str): package base name
username(str | None): optional override of username for build process
"""
package = Package.from_aur(source, self.repository.pacman)
package = Package.from_aur(source, self.repository.pacman, username)
self.database.build_queue_insert(package)
self.database.remote_update(package)
@ -81,23 +81,24 @@ class ApplicationPackages(ApplicationProperties):
for full_path in filter(package_like, local_dir.iterdir()):
self._add_archive(str(full_path))
def _add_local(self, source: str) -> None:
def _add_local(self, source: str, username: str | None) -> None:
"""
add package from local PKGBUILDs
Args:
source(str): path to directory with local source files
username(str | None): optional override of username for build process
Raises:
UnknownPackageError: if specified package is unknown or doesn't exist
"""
if (source_dir := Path(source)).is_dir():
package = Package.from_build(source_dir, self.architecture)
package = Package.from_build(source_dir, self.architecture, username)
cache_dir = self.repository.paths.cache_for(package.base)
shutil.copytree(source_dir, 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
elif (source_dir := self.repository.paths.cache_for(source)).is_dir():
package = Package.from_build(source_dir, self.architecture)
package = Package.from_build(source_dir, self.architecture, username)
else:
raise UnknownPackageError(source)
@ -122,29 +123,31 @@ class ApplicationPackages(ApplicationProperties):
for chunk in response.iter_content(chunk_size=1024):
local_file.write(chunk)
def _add_repository(self, source: str, *_: Any) -> None:
def _add_repository(self, source: str, username: str | None) -> None:
"""
add package from official repository
Args:
source(str): package base name
username(str | None): optional override of username for build process
"""
package = Package.from_official(source, self.repository.pacman)
package = Package.from_official(source, self.repository.pacman, username)
self.database.build_queue_insert(package)
self.database.remote_update(package)
def add(self, names: Iterable[str], source: PackageSource) -> None:
def add(self, names: Iterable[str], source: PackageSource, username: str | None = None) -> None:
"""
add packages for the next build
Args:
names(Iterable[str]): list of package bases to add
source(PackageSource): package source to add
username(str | None, optional): optional override of username for build process (Default value = None)
"""
for name in names:
resolved_source = source.resolve(name)
fn = getattr(self, f"_add_{resolved_source.value}")
fn(name)
fn(name, username)
def on_result(self, result: Result) -> None:
"""

View File

@ -25,6 +25,7 @@ from ahriman.core.build_tools.sources import Sources
from ahriman.core.formatters import UpdatePrinter
from ahriman.core.tree import Tree
from ahriman.models.package import Package
from ahriman.models.packagers import Packagers
from ahriman.models.result import Result
@ -83,7 +84,7 @@ class ApplicationRepository(ApplicationProperties):
if archive.filepath is None:
self.logger.warning("filepath is empty for %s", package.base)
continue # avoid mypy warning
self.repository.sign.process_sign_package(archive.filepath, package.base)
self.repository.sign.process_sign_package(archive.filepath, None)
# sign repository database if set
self.repository.sign.process_sign_repository(self.repository.repo.repo_path)
# process triggers
@ -104,14 +105,14 @@ class ApplicationRepository(ApplicationProperties):
packages: list[str] = []
for single in probe.packages:
try:
_ = Package.from_aur(single, self.repository.pacman)
_ = Package.from_aur(single, self.repository.pacman, None)
except Exception:
packages.append(single)
return packages
def unknown_local(probe: Package) -> list[str]:
cache_dir = self.repository.paths.cache_for(probe.base)
local = Package.from_build(cache_dir, self.architecture)
local = Package.from_build(cache_dir, self.architecture, None)
packages = set(probe.packages.keys()).difference(local.packages.keys())
return list(packages)
@ -123,12 +124,14 @@ class ApplicationRepository(ApplicationProperties):
result.extend(unknown_aur(package)) # local package not found
return result
def update(self, updates: Iterable[Package]) -> Result:
def update(self, updates: Iterable[Package], packagers: Packagers | None = None) -> Result:
"""
run package updates
Args:
updates(Iterable[Package]): list of packages to update
packagers(Packagers | None, optional): optional override of username for build process
(Default value = None)
Returns:
Result: update result
@ -136,7 +139,7 @@ class ApplicationRepository(ApplicationProperties):
def process_update(paths: Iterable[Path], result: Result) -> None:
if not paths:
return # don't need to process if no update supplied
update_result = self.repository.process_update(paths)
update_result = self.repository.process_update(paths, packagers)
self.on_result(result.merge(update_result))
# process built packages
@ -148,7 +151,7 @@ class ApplicationRepository(ApplicationProperties):
tree = Tree.resolve(updates)
for num, level in enumerate(tree):
self.logger.info("processing level #%i %s", num, [package.base for package in level])
build_result = self.repository.process_build(level)
build_result = self.repository.process_build(level, packagers)
packages = self.repository.packages_built()
process_update(packages, build_result)

View File

@ -22,6 +22,7 @@ import argparse
from ahriman.application.application import Application
from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration
from ahriman.models.packagers import Packagers
class Add(Handler):
@ -45,12 +46,14 @@ class Add(Handler):
application = Application(architecture, configuration,
report=report, unsafe=unsafe, refresh_pacman_database=args.refresh)
application.on_start()
application.add(args.package, args.source)
application.add(args.package, args.source, args.username)
if not args.now:
return
packages = application.updates(args.package, aur=False, local=False, manual=True, vcs=False,
log_fn=application.logger.info)
packages = application.with_dependencies(packages, process_dependencies=args.dependencies)
result = application.update(packages)
packagers = Packagers(args.username, {package.base: package.packager for package in packages})
result = application.update(packages, packagers)
Add.check_if_empty(args.exit_code, result.is_empty)

View File

@ -78,7 +78,7 @@ class Patch(Handler):
tuple[str, PkgbuildPatch]: package base and created PKGBUILD patch based on the diff from master HEAD
to current files
"""
package = Package.from_build(sources_dir, architecture)
package = Package.from_build(sources_dir, architecture, None)
patch = Sources.patch_create(sources_dir, *track)
return package.base, PkgbuildPatch(None, patch)

View File

@ -57,7 +57,7 @@ class Rebuild(Handler):
UpdatePrinter(package, package.version).print(verbose=True)
return
result = application.update(updates)
result = application.update(updates, args.username)
Rebuild.check_if_empty(args.exit_code, result.is_empty)
@staticmethod

View File

@ -49,7 +49,7 @@ class ServiceUpdates(Handler):
"""
application = Application(architecture, configuration, report=report, unsafe=unsafe)
remote = Package.from_aur("ahriman", application.repository.pacman)
remote = Package.from_aur("ahriman", application.repository.pacman, None)
release = remote.version.rsplit("-", 1)[-1] # we don't store pkgrel locally, so we just append it
local_version = f"{version.__version__}-{release}"

View File

@ -213,7 +213,7 @@ class Setup(Handler):
"""
command = Setup.build_command(paths.root, prefix, architecture)
sudoers_file = Setup.build_command(Setup.SUDOERS_DIR_PATH, prefix, architecture)
sudoers_file.write_text(f"ahriman ALL=(ALL) NOPASSWD: {command} *\n", encoding="utf8")
sudoers_file.write_text(f"ahriman ALL=(ALL) NOPASSWD:SETENV: {command} *\n", encoding="utf8")
sudoers_file.chmod(0o400) # security!
@staticmethod

View File

@ -18,7 +18,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import argparse
import shlex
from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration
@ -47,14 +46,14 @@ class UnsafeCommands(Handler):
"""
parser = args.parser()
unsafe_commands = UnsafeCommands.get_unsafe_commands(parser)
if args.command is None:
if args.command:
UnsafeCommands.check_unsafe(args.command, unsafe_commands, parser)
else:
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:
def check_unsafe(command: list[str], unsafe_commands: list[str], parser: argparse.ArgumentParser) -> None:
"""
check if command is unsafe
@ -63,7 +62,7 @@ class UnsafeCommands(Handler):
unsafe_commands(list[str]): list of unsafe commands
parser(argparse.ArgumentParser): generated argument parser
"""
args = parser.parse_args(shlex.split(command))
args = parser.parse_args(command)
UnsafeCommands.check_if_empty(True, args.command in unsafe_commands)
@staticmethod

View File

@ -24,6 +24,7 @@ from collections.abc import Callable
from ahriman.application.application import Application
from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration
from ahriman.models.packagers import Packagers
class Update(Handler):
@ -54,7 +55,9 @@ class Update(Handler):
return
packages = application.with_dependencies(packages, process_dependencies=args.dependencies)
result = application.update(packages)
packagers = Packagers(args.username, {package.base: package.packager for package in packages})
result = application.update(packages, packagers)
Update.check_if_empty(args.exit_code, result.is_empty)
@staticmethod

View File

@ -156,4 +156,5 @@ class Users(Handler):
if password is None:
password = read_password()
return User(username=args.username, password=password, access=args.role)
return User(username=args.username, password=password, access=args.role,
packager_id=args.packager, key=args.key)

View File

@ -59,12 +59,13 @@ class Task(LazyLogging):
self.makepkg_flags = configuration.getlist("build", "makepkg_flags", fallback=[])
self.makechrootpkg_flags = configuration.getlist("build", "makechrootpkg_flags", fallback=[])
def build(self, sources_dir: Path) -> list[Path]:
def build(self, sources_dir: Path, packager: str | None = None) -> list[Path]:
"""
run package build
Args:
sources_dir(Path): path to where sources are
packager(str | None, optional): optional packager override (Default value = None)
Returns:
list[Path]: paths of produced packages
@ -75,12 +76,18 @@ class Task(LazyLogging):
command.extend(["--"] + self.makepkg_flags)
self.logger.info("using %s for %s", command, self.package.base)
environment: dict[str, str] = {}
if packager is not None:
environment["PACKAGER"] = packager
self.logger.info("using environment variables %s", environment)
Task._check_output(
*command,
exception=BuildError(self.package.base),
cwd=sources_dir,
logger=self.logger,
user=self.uid)
user=self.uid,
environment=environment)
# well it is not actually correct, but we can deal with it
packages = Task._check_output(

View File

@ -191,10 +191,6 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
"sign": {
"type": "dict",
"allow_unknown": True,
"keysrules": {
"type": "string",
"anyof_regex": ["^target$", "^key$", "^key_.*"],
},
"schema": {
"target": {
"type": "list",

View File

@ -0,0 +1,85 @@
#
# Copyright (c) 2021-2023 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.alpm.pacman import Pacman
from ahriman.core.configuration import Configuration
from ahriman.core.util import package_like
from ahriman.models.package import Package
from ahriman.models.pacman_synchronization import PacmanSynchronization
__all__ = ["migrate_data", "steps"]
steps = [
"""
alter table users add column packager_id
""",
"""
alter table users add column key_id
""",
"""
alter table package_bases add column packager
""",
]
def migrate_data(connection: Connection, configuration: Configuration) -> None:
"""
perform data migration
Args:
connection(Connection): database connection
configuration(Configuration): configuration instance
"""
migrate_package_base_packager(connection, configuration)
def migrate_package_base_packager(connection: Connection, configuration: Configuration) -> None:
"""
migrate package packager field
Args:
connection(Connection): database connection
configuration(Configuration): configuration instance
"""
if not configuration.repository_paths.repository.is_dir():
return
_, architecture = configuration.check_loaded()
pacman = Pacman(architecture, configuration, refresh_database=PacmanSynchronization.Disabled)
package_list = []
for full_path in filter(package_like, configuration.repository_paths.repository.iterdir()):
package = Package.from_archive(full_path, pacman, remote=None)
package_list.append({
"package_base": package.base,
"packager": package.packager,
})
connection.executemany(
"""
update package_bases set
packager = :packager
where package_base = :package_base
""",
package_list
)

View File

@ -57,8 +57,9 @@ class AuthOperations(Operations):
def run(connection: Connection) -> list[User]:
return [
User(username=cursor["username"], password=cursor["password"], access=UserAccess(cursor["access"]))
for cursor in connection.execute(
User(username=row["username"], password=row["password"], access=UserAccess(row["access"]),
packager_id=row["packager_id"], key=row["key_id"])
for row in connection.execute(
"""
select * from users
where (:username is null or username = :username) and (:access is null or access = :access)
@ -91,12 +92,13 @@ class AuthOperations(Operations):
connection.execute(
"""
insert into users
(username, access, password)
(username, access, password, packager_id, key_id)
values
(:username, :access, :password)
(:username, :access, :password, :packager_id, :key_id)
on conflict (username) do update set
access = :access, password = :password
access = :access, password = :password, packager_id = :packager_id, key_id = :key_id
""",
{"username": user.username.lower(), "access": user.access.value, "password": user.password})
{"username": user.username.lower(), "access": user.access.value, "password": user.password,
"packager_id": user.packager_id, "key_id": user.key})
self.with_connection(run, commit=True)

View File

@ -76,11 +76,12 @@ class PackageOperations(Operations):
connection.execute(
"""
insert into package_bases
(package_base, version, source, branch, git_url, path, web_url)
(package_base, version, source, branch, git_url, path, web_url, packager)
values
(:package_base, :version, :source, :branch, :git_url, :path, :web_url)
(:package_base, :version, :source, :branch, :git_url, :path, :web_url, :packager)
on conflict (package_base) do update set
version = :version, branch = :branch, git_url = :git_url, path = :path, web_url = :web_url, source = :source
version = :version, branch = :branch, git_url = :git_url, path = :path, web_url = :web_url,
source = :source, packager = :packager
""",
{
"package_base": package.base,
@ -90,6 +91,7 @@ class PackageOperations(Operations):
"path": package.remote.path if package.remote is not None else None,
"web_url": package.remote.web_url if package.remote is not None else None,
"source": package.remote.source.value if package.remote is not None else None,
"packager": package.packager,
}
)
@ -163,8 +165,9 @@ class PackageOperations(Operations):
base=row["package_base"],
version=row["version"],
remote=RemoteSource.from_json(row),
packages={})
for row in connection.execute("""select * from package_bases""")
packages={},
packager=row["packager"] or None,
) for row in connection.execute("""select * from package_bases""")
}
@staticmethod

View File

@ -77,8 +77,8 @@ class PatchOperations(Operations):
"""
def run(connection: Connection) -> list[tuple[str, PkgbuildPatch]]:
return [
(cursor["package_base"], PkgbuildPatch(cursor["variable"], cursor["patch"]))
for cursor in connection.execute(
(row["package_base"], PkgbuildPatch(row["variable"], row["patch"]))
for row in connection.execute(
"""select * from patches where :package_base is null or package_base = :package_base""",
{"package_base": package_base})
]

View File

@ -44,13 +44,13 @@ class RemotePush(LazyLogging):
remote_source(RemoteSource): repository remote source (remote pull url and branch)
"""
def __init__(self, configuration: Configuration, database: SQLite, section: str) -> None:
def __init__(self, database: SQLite, configuration: Configuration, section: str) -> None:
"""
default constructor
Args:
configuration(Configuration): configuration instance
database(SQLite): database instance
configuration(Configuration): configuration instance
section(str): settings section name
"""
self.database = database

View File

@ -105,5 +105,5 @@ class RemotePushTrigger(Trigger):
for target in self.targets:
section, _ = self.configuration.gettype(
target, self.architecture, fallback=self.CONFIGURATION_SCHEMA_FALLBACK)
runner = RemotePush(self.configuration, database, section)
runner = RemotePush(database, self.configuration, section)
runner.run(result)

View File

@ -28,6 +28,7 @@ from ahriman.core.repository.cleaner import Cleaner
from ahriman.core.util import safe_filename
from ahriman.models.package import Package
from ahriman.models.package_description import PackageDescription
from ahriman.models.packagers import Packagers
from ahriman.models.result import Result
@ -63,30 +64,35 @@ class Executor(Cleaner):
"""
raise NotImplementedError
def process_build(self, updates: Iterable[Package]) -> Result:
def process_build(self, updates: Iterable[Package], packagers: Packagers | None = None) -> Result:
"""
build packages
Args:
updates(Iterable[Package]): list of packages properties to build
packagers(Packagers | None, optional): optional override of username for build process
(Default value = None)
Returns:
Result: build result
"""
def build_single(package: Package, local_path: Path) -> None:
def build_single(package: Package, local_path: Path, packager_id: str | None) -> None:
self.reporter.set_building(package.base)
task = Task(package, self.configuration, self.paths)
task.init(local_path, self.database)
built = task.build(local_path)
built = task.build(local_path, packager_id)
for src in built:
dst = self.paths.packages / src.name
shutil.move(src, dst)
packagers = packagers or Packagers()
result = Result()
for single in updates:
with self.in_package_context(single.base), TemporaryDirectory(ignore_cleanup_errors=True) as dir_name:
try:
build_single(single, Path(dir_name))
packager = self.packager(packagers, single.base)
build_single(single, Path(dir_name), packager.packager_id)
result.add_success(single)
except Exception:
self.reporter.set_failed(single.base)
@ -158,12 +164,14 @@ class Executor(Cleaner):
return self.repo.repo_path
def process_update(self, packages: Iterable[Path]) -> Result:
def process_update(self, packages: Iterable[Path], packagers: Packagers | None = None) -> Result:
"""
sign packages, add them to repository and update repository database
Args:
packages(Iterable[Path]): list of filenames to run
packagers(Packagers | None, optional): optional override of username for build process
(Default value = None)
Returns:
Result: path to repository database
@ -176,13 +184,13 @@ class Executor(Cleaner):
shutil.move(self.paths.packages / archive.filename, self.paths.packages / safe)
archive.filename = safe
def update_single(name: str | None, package_base: str) -> None:
def update_single(name: str | None, package_base: str, packager_key: str | None) -> None:
if name is None:
self.logger.warning("received empty package name for base %s", package_base)
return # suppress type checking, it never can be none actually
# in theory, it might be NOT packages directory, but we suppose it is
full_path = self.paths.packages / name
files = self.sign.process_sign_package(full_path, package_base)
files = self.sign.process_sign_package(full_path, packager_key)
for src in files:
dst = self.paths.repository / safe_filename(src.name)
shutil.move(src, dst)
@ -192,14 +200,17 @@ class Executor(Cleaner):
current_packages = self.packages()
removed_packages: list[str] = [] # list of packages which have been removed from the base
updates = self.load_archives(packages)
packagers = packagers or Packagers()
result = Result()
for local in updates:
with self.in_package_context(local.base):
try:
packager = self.packager(packagers, local.base)
for description in local.packages.values():
rename(description, local.base)
update_single(description.filename, local.base)
update_single(description.filename, local.base, packager.key)
self.reporter.set_success(local)
result.add_success(local)

View File

@ -27,8 +27,11 @@ from ahriman.core.sign.gpg import GPG
from ahriman.core.status.client import Client
from ahriman.core.triggers import TriggerLoader
from ahriman.core.util import check_user
from ahriman.models.packagers import Packagers
from ahriman.models.pacman_synchronization import PacmanSynchronization
from ahriman.models.repository_paths import RepositoryPaths
from ahriman.models.user import User
from ahriman.models.user_access import UserAccess
class RepositoryProperties(LazyLogging):
@ -83,3 +86,23 @@ class RepositoryProperties(LazyLogging):
self.repo = Repo(self.name, self.paths, self.sign.repository_sign_args)
self.reporter = Client.load(configuration, report=report)
self.triggers = TriggerLoader.load(architecture, configuration)
def packager(self, packagers: Packagers, package_base: str) -> User:
"""
extract packager from configuration having username
Args:
packagers(Packagers): packagers override holder
package_base(str): package base to lookup
Returns:
User | None: user found in database if any and empty object otherwise
"""
username = packagers.for_base(package_base)
if username is None: # none to search
return User(username="", password="", access=UserAccess.Read, packager_id=None, key=None) # nosec
if (user := self.database.user_get(username)) is not None: # found user
return user
# empty user with the username
return User(username=username, password="", access=UserAccess.Read, packager_id=None, key=None) # nosec

View File

@ -65,9 +65,9 @@ class UpdateHandler(Cleaner):
try:
if source == PackageSource.Repository:
remote = Package.from_official(local.base, self.pacman)
remote = Package.from_official(local.base, self.pacman, None)
else:
remote = Package.from_aur(local.base, self.pacman)
remote = Package.from_aur(local.base, self.pacman, None)
if local.is_outdated(
remote, self.paths,
@ -98,7 +98,7 @@ class UpdateHandler(Cleaner):
with self.in_package_context(cache_dir.name):
try:
Sources.fetch(cache_dir, remote=None)
remote = Package.from_build(cache_dir, self.architecture)
remote = Package.from_build(cache_dir, self.architecture, None)
local = packages.get(remote.base)
if local is None:

View File

@ -19,7 +19,6 @@
#
import requests
from collections.abc import Generator
from pathlib import Path
from ahriman.core.configuration import Configuration
@ -165,21 +164,6 @@ class GPG(LazyLogging):
key_body = self.key_download(server, key)
GPG._check_output("gpg", "--import", input_data=key_body, logger=self.logger)
def keys(self) -> list[str]:
"""
extract list of keys described in configuration
Returns:
list[str]: list of unique keys which are set in configuration
"""
def generator() -> Generator[str, None, None]:
if self.default_key is not None:
yield self.default_key
for _, value in filter(lambda pair: pair[0].startswith("key_"), self.configuration["sign"].items()):
yield value
return sorted(set(generator()))
def process(self, path: Path, key: str) -> list[Path]:
"""
gpg command wrapper
@ -197,20 +181,21 @@ class GPG(LazyLogging):
logger=self.logger)
return [path, path.parent / f"{path.name}.sig"]
def process_sign_package(self, path: Path, package_base: str) -> list[Path]:
def process_sign_package(self, path: Path, packager_key: str | None) -> list[Path]:
"""
sign package if required by configuration
Args:
path(Path): path to file to sign
package_base(str): package base required to check for key overrides
packager_key(str | None): optional packager key to sign
Returns:
list[Path]: list of generated files including original file
"""
if SignSettings.Packages not in self.targets:
return [path]
key = self.configuration.get("sign", f"key_{package_base}", fallback=self.default_key)
key = packager_key or self.default_key
if key is None:
self.logger.error("no default key set, skip package %s sign", path)
return [path]

View File

@ -78,7 +78,7 @@ class Spawn(Thread, LazyLogging):
result = callback(args, architecture)
queue.put((process_id, result))
def _spawn_process(self, command: str, *args: str, **kwargs: str) -> None:
def _spawn_process(self, command: str, *args: str, **kwargs: str | None) -> None:
"""
spawn external ahriman process with supplied arguments
@ -94,6 +94,8 @@ class Spawn(Thread, LazyLogging):
arguments.extend(args)
# named command arguments
for argument, value in kwargs.items():
if value is None:
continue # skip null values
arguments.append(f"--{argument}")
if value:
arguments.append(value)
@ -122,27 +124,31 @@ class Spawn(Thread, LazyLogging):
kwargs = {} if server is None else {"key-server": server}
self._spawn_process("service-key-import", key, **kwargs)
def packages_add(self, packages: Iterable[str], *, now: bool) -> None:
def packages_add(self, packages: Iterable[str], username: str | None, *, now: bool) -> None:
"""
add packages
Args:
packages(Iterable[str]): packages list to add
username(str | None): optional override of username for build process
now(bool): build packages now
"""
kwargs = {"source": PackageSource.AUR.value} # avoid abusing by building non-aur packages
# avoid abusing by building non-aur packages
kwargs = {"source": PackageSource.AUR.value, "username": username}
if now:
kwargs["now"] = ""
self._spawn_process("package-add", *packages, **kwargs)
def packages_rebuild(self, depends_on: str) -> None:
def packages_rebuild(self, depends_on: str, username: str | None) -> None:
"""
rebuild packages which depend on the specified package
Args:
depends_on(str): packages dependency
username(str | None): optional override of username for build process
"""
self._spawn_process("repo-rebuild", **{"depends-on": depends_on})
kwargs = {"depends-on": depends_on, "username": username}
self._spawn_process("repo-rebuild", **kwargs)
def packages_remove(self, packages: Iterable[str]) -> None:
"""
@ -153,11 +159,15 @@ class Spawn(Thread, LazyLogging):
"""
self._spawn_process("package-remove", *packages)
def packages_update(self) -> None:
def packages_update(self, username: str | None) -> None:
"""
run full repository update
Args:
username(str | None): optional override of username for build process
"""
self._spawn_process("repo-update")
kwargs = {"username": username}
self._spawn_process("repo-update", **kwargs)
def run(self) -> None:
"""

View File

@ -19,6 +19,7 @@
#
from ahriman.core import context
from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.sign.gpg import GPG
from ahriman.core.support.package_creator import PackageCreator
from ahriman.core.support.pkgbuild.keyring_generator import KeyringGenerator
@ -107,8 +108,9 @@ class KeyringTrigger(Trigger):
"""
ctx = context.get()
sign = ctx.get(ContextKey("sign", GPG))
database = ctx.get(ContextKey("database", SQLite))
for target in self.targets:
generator = KeyringGenerator(sign, self.configuration, target)
generator = KeyringGenerator(database, sign, self.configuration, target)
runner = PackageCreator(self.configuration, generator)
runner.run()

View File

@ -67,5 +67,5 @@ class PackageCreator:
ctx = context.get()
database: SQLite = ctx.get(ContextKey("database", SQLite))
_, architecture = self.configuration.check_loaded()
package = Package.from_build(local_path, architecture)
package = Package.from_build(local_path, architecture, None)
database.package_update(package, BuildStatus())

View File

@ -21,6 +21,7 @@ from collections.abc import Callable
from pathlib import Path
from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.exceptions import PkgbuildGeneratorError
from ahriman.core.sign.gpg import GPG
from ahriman.core.support.pkgbuild.pkgbuild_generator import PkgbuildGenerator
@ -42,11 +43,12 @@ class KeyringGenerator(PkgbuildGenerator):
trusted(list[str]): lif of trusted PGP keys
"""
def __init__(self, sign: GPG, configuration: Configuration, section: str) -> None:
def __init__(self, database: SQLite, sign: GPG, configuration: Configuration, section: str) -> None:
"""
default constructor
Args:
database(SQLite): database instance
sign(GPG): GPG wrapper instance
configuration(Configuration): configuration instance
section(str): settings section name
@ -55,7 +57,8 @@ class KeyringGenerator(PkgbuildGenerator):
self.name = configuration.repository_name
# configuration fields
self.packagers = configuration.getlist(section, "packagers", fallback=sign.keys())
packager_keys = [packager.key for packager in database.user_list(None, None) if packager.key is not None]
self.packagers = configuration.getlist(section, "packagers", fallback=packager_keys)
self.revoked = configuration.getlist(section, "revoked", fallback=[])
self.trusted = configuration.getlist(
section, "trusted", fallback=[sign.default_key] if sign.default_key is not None else [])
@ -148,10 +151,10 @@ class KeyringGenerator(PkgbuildGenerator):
def install(self) -> str | None:
"""
content of the install functions
content of the .install functions
Returns:
str | None: content of the install functions if any
str | None: content of the .install functions if any
"""
# copy-paste from archlinux-keyring
return f"""post_upgrade() {{

View File

@ -98,10 +98,10 @@ class PkgbuildGenerator:
def install(self) -> str | None:
"""
content of the install functions
content of the .install functions
Returns:
str | None: content of the install functions if any
str | None: content of the .install functions if any
"""
def package(self) -> str:

View File

@ -28,6 +28,7 @@ import requests
import subprocess
from collections.abc import Callable, Generator, Iterable
from dataclasses import asdict
from enum import Enum
from pathlib import Path
from pwd import getpwuid
@ -40,8 +41,10 @@ from ahriman.models.repository_paths import RepositoryPaths
__all__ = [
"check_output",
"check_user",
"dataclass_view",
"enum_values",
"exception_response_text",
"extract_user",
"filter_json",
"full_version",
"package_like",
@ -61,7 +64,8 @@ T = TypeVar("T")
def check_output(*args: str, exception: Exception | None = None, cwd: Path | None = None, input_data: str | None = None,
logger: logging.Logger | None = None, user: int | None = None) -> str:
logger: logging.Logger | None = None, user: int | None = None,
environment: dict[str, str] | None = None) -> str:
"""
subprocess wrapper
@ -73,6 +77,7 @@ def check_output(*args: str, exception: Exception | None = None, cwd: Path | Non
input_data(str | None, optional): data which will be written to command stdin (Default value = None)
logger(logging.Logger | None, optional): logger to log command result if required (Default value = None)
user(int | None, optional): run process as specified user (Default value = None)
environment(dict[str, str] | None, optional): optional environment variables if any (Default value = None)
Returns:
str: command output
@ -106,7 +111,9 @@ def check_output(*args: str, exception: Exception | None = None, cwd: Path | Non
if logger is not None:
logger.debug(single)
environment = {"HOME": getpwuid(user).pw_dir} if user is not None else {}
environment = environment or {}
if user is not None:
environment["HOME"] = getpwuid(user).pw_dir
# FIXME additional workaround for linter and type check which do not know that user arg is supported
# pylint: disable=unexpected-keyword-arg
with subprocess.Popen(args, cwd=cwd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
@ -163,6 +170,19 @@ def check_user(paths: RepositoryPaths, *, unsafe: bool) -> None:
raise UnsafeRunError(current_uid, root_uid)
def dataclass_view(instance: Any) -> dict[str, Any]:
"""
convert dataclass instance to json object
Args:
instance(Any): dataclass instance
Returns:
dict[str, Any]: json representation of the dataclass with empty field removed
"""
return asdict(instance, dict_factory=lambda fields: {key: value for key, value in fields if value is not None})
def enum_values(enum: type[Enum]) -> list[str]:
"""
generate list of enumeration values from the source
@ -190,6 +210,17 @@ def exception_response_text(exception: requests.exceptions.RequestException) ->
return result
def extract_user() -> str | None:
"""
extract user from system environment
Returns:
str | None: SUDO_USER in case if set and USER otherwise. It can return None in case if environment has been
cleared before application start
"""
return os.getenv("SUDO_USER") or os.getenv("DOAS_USER") or os.getenv("USER")
def filter_json(source: dict[str, Any], known_fields: Iterable[str]) -> dict[str, Any]:
"""
filter json object by fields used for json-to-object conversion

View File

@ -17,9 +17,10 @@
# 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 asdict, dataclass, field
from dataclasses import dataclass, field
from typing import Any, Self
from ahriman.core.util import dataclass_view
from ahriman.models.build_status import BuildStatus
from ahriman.models.counters import Counters
@ -69,4 +70,4 @@ class InternalStatus:
Returns:
dict[str, Any]: json-friendly dictionary
"""
return asdict(self)
return dataclass_view(self)

View File

@ -23,7 +23,7 @@ from __future__ import annotations
import copy
from collections.abc import Callable, Generator, Iterable
from dataclasses import asdict, dataclass
from dataclasses import dataclass
from pathlib import Path
from pyalpm import vercmp # type: ignore[import]
from srcinfo.parse import parse_srcinfo # type: ignore[import]
@ -34,7 +34,7 @@ from ahriman.core.alpm.pacman import Pacman
from ahriman.core.alpm.remote import AUR, Official, OfficialSyncdb
from ahriman.core.exceptions import PackageInfoError
from ahriman.core.log import LazyLogging
from ahriman.core.util import check_output, full_version, srcinfo_property_list, utcnow
from ahriman.core.util import check_output, dataclass_view, full_version, srcinfo_property_list, utcnow
from ahriman.models.package_description import PackageDescription
from ahriman.models.package_source import PackageSource
from ahriman.models.remote_source import RemoteSource
@ -48,6 +48,7 @@ class Package(LazyLogging):
Attributes:
base(str): package base name
packager(str | None): package packager if available
packages(dict[str, PackageDescription): map of package names to their properties.
Filled only on load from archive
remote(RemoteSource | None): package remote source if applicable
@ -77,6 +78,7 @@ class Package(LazyLogging):
version: str
remote: RemoteSource | None
packages: dict[str, PackageDescription]
packager: str | None = None
_check_output = check_output
@ -204,16 +206,18 @@ class Package(LazyLogging):
"""
package = pacman.handle.load_pkg(str(path))
description = PackageDescription.from_package(package, path)
return cls(base=package.base, version=package.version, remote=remote, packages={package.name: description})
return cls(base=package.base, version=package.version, remote=remote, packages={package.name: description},
packager=package.packager)
@classmethod
def from_aur(cls, name: str, pacman: Pacman) -> Self:
def from_aur(cls, name: str, pacman: Pacman, packager: str | None = None) -> Self:
"""
construct package properties from AUR page
Args:
name(str): package name (either base or normal name)
pacman(Pacman): alpm wrapper instance
packager(str | None, optional): packager to be used for this build (Default value = None)
Returns:
Self: package properties
@ -224,16 +228,19 @@ class Package(LazyLogging):
base=package.package_base,
version=package.version,
remote=remote,
packages={package.name: PackageDescription.from_aur(package)})
packages={package.name: PackageDescription.from_aur(package)},
packager=packager,
)
@classmethod
def from_build(cls, path: Path, architecture: str) -> Self:
def from_build(cls, path: Path, architecture: str, packager: str | None = None) -> Self:
"""
construct package properties from sources directory
Args:
path(Path): path to package sources directory
architecture(str): load package for specific architecture
packager(str | None, optional): packager to be used for this build (Default value = None)
Returns:
Self: package properties
@ -265,7 +272,7 @@ class Package(LazyLogging):
source=PackageSource.Local,
)
return cls(base=srcinfo["pkgbase"], version=version, remote=remote, packages=packages)
return cls(base=srcinfo["pkgbase"], version=version, remote=remote, packages=packages, packager=packager)
@classmethod
def from_json(cls, dump: dict[str, Any]) -> Self:
@ -284,16 +291,18 @@ class Package(LazyLogging):
for key, value in packages_json.items()
}
remote = dump.get("remote") or {}
return cls(base=dump["base"], version=dump["version"], remote=RemoteSource.from_json(remote), packages=packages)
return cls(base=dump["base"], version=dump["version"], remote=RemoteSource.from_json(remote), packages=packages,
packager=dump.get("packager"))
@classmethod
def from_official(cls, name: str, pacman: Pacman, *, use_syncdb: bool = True) -> Self:
def from_official(cls, name: str, pacman: Pacman, packager: str | None = None, *, use_syncdb: bool = True) -> Self:
"""
construct package properties from official repository page
Args:
name(str): package name (either base or normal name)
pacman(Pacman): alpm wrapper instance
packager(str | None, optional): packager to be used for this build (Default value = None)
use_syncdb(bool, optional): use pacman databases instead of official repositories RPC (Default value = True)
Returns:
@ -305,7 +314,9 @@ class Package(LazyLogging):
base=package.package_base,
version=package.version,
remote=remote,
packages={package.name: PackageDescription.from_aur(package)})
packages={package.name: PackageDescription.from_aur(package)},
packager=packager,
)
@staticmethod
def local_files(path: Path) -> Generator[Path, None, None]:
@ -513,4 +524,4 @@ class Package(LazyLogging):
Returns:
dict[str, Any]: json-friendly dictionary
"""
return asdict(self)
return dataclass_view(self)

View File

@ -17,12 +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/>.
#
from dataclasses import asdict, dataclass, field, fields
from dataclasses import dataclass, field, fields
from pathlib import Path
from pyalpm import Package # type: ignore[import]
from typing import Any, Self
from ahriman.core.util import filter_json, trim_package
from ahriman.core.util import dataclass_view, filter_json, trim_package
from ahriman.models.aur_package import AURPackage
@ -172,4 +172,4 @@ class PackageDescription:
Returns:
dict[str, Any]: json-friendly dictionary
"""
return asdict(self)
return dataclass_view(self)

View File

@ -0,0 +1,46 @@
#
# Copyright (c) 2021-2023 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, field
@dataclass(frozen=True)
class Packagers:
"""
holder for packagers overrides
Attributes:
default(str | None): default packager username if any to be used if no override for the specified base was found
overrides: dict[str, str | None]: packager username override for specific package base
"""
default: str | None = None
overrides: dict[str, str | None] = field(default_factory=dict)
def for_base(self, package_base: str) -> str | None:
"""
extract username for the specified package base
Args:
package_base(str): package base to lookup
Returns:
str | None: package base override if set and default packager username otherwise
"""
return self.overrides.get(package_base) or self.default

View File

@ -17,11 +17,11 @@
# 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 asdict, dataclass, fields
from dataclasses import dataclass, fields
from pathlib import Path
from typing import Any, Self
from ahriman.core.util import filter_json
from ahriman.core.util import dataclass_view, filter_json
from ahriman.models.package_source import PackageSource
@ -118,4 +118,4 @@ class RemoteSource:
Returns:
dict[str, Any]: json-friendly dictionary
"""
return asdict(self)
return dataclass_view(self)

View File

@ -34,12 +34,14 @@ class User:
username(str): username
password(str): hashed user password with salt
access(UserAccess): user role
packager_id(str | None): packager id to be used. If not set, the default service packager will be used
key(str | None): personal packager key if any. If user id is empty, it is interpreted as default key
Examples:
Simply create user from database data and perform required validation::
>>> password = User.generate_password(24)
>>> user = User("ahriman", password, UserAccess.Full)
>>> user = User(username="ahriman", password=password, access=UserAccess.Full, packager_id=None, key=None)
Since the password supplied may be plain text, the ``hash_password`` method can be used to hash the password::
@ -61,9 +63,18 @@ class User:
username: str
password: str
access: UserAccess
packager_id: str | None
key: str | None
_HASHER = sha512_crypt
def __post_init__(self) -> None:
"""
remove empty fields
"""
object.__setattr__(self, "packager_id", self.packager_id or None)
object.__setattr__(self, "key", self.key or None)
@classmethod
def from_option(cls, username: str | None, password: str | None,
access: UserAccess = UserAccess.Read) -> Self | None:
@ -80,7 +91,7 @@ class User:
"""
if username is None or password is None:
return None
return cls(username=username, password=password, access=access)
return cls(username=username, password=password, access=access, packager_id=None, key=None)
@staticmethod
def generate_password(length: int) -> str:
@ -149,4 +160,4 @@ class User:
Returns:
str: unique string representation
"""
return f"User(username={self.username}, access={self.access})"
return f"User(username={self.username}, access={self.access}, packager_id={self.packager_id}, key={self.key})"

View File

@ -148,7 +148,7 @@ def setup_auth(application: Application, configuration: Configuration, validator
setup_session(application, storage)
authorization_policy = _AuthorizationPolicy(validator)
identity_policy = aiohttp_security.SessionIdentityPolicy()
identity_policy = application["identity"] = aiohttp_security.SessionIdentityPolicy()
aiohttp_security.setup(application, identity_policy, authorization_policy)
application.middlewares.append(_auth_handler(validator.allow_read_only))

View File

@ -44,3 +44,7 @@ class PackageSchema(Schema):
keys=fields.String(), values=fields.Nested(PackagePropertiesSchema()), required=True, metadata={
"description": "Packages which belong to this base",
})
packager = fields.String(metadata={
"description": "packager for the last success package build",
"example": "John Doe <john@doe.com>",
})

View File

@ -183,3 +183,16 @@ class BaseView(View, CorsViewMixin):
return response
self._raise_allowed_methods()
async def username(self) -> str | None:
"""
extract username from request if any
Returns:
str | None: authorized username if any and None otherwise (e.g. if authorization is disabled)
"""
policy = self.request.app.get("identity")
if policy is not None:
identity: str = await policy.identify(self.request)
return identity
return None

View File

@ -67,6 +67,7 @@ class AddView(BaseView):
except Exception as e:
raise HTTPBadRequest(reason=str(e))
self.spawner.packages_add(packages, now=True)
username = await self.username()
self.spawner.packages_add(packages, username, now=True)
raise HTTPNoContent()

View File

@ -68,6 +68,7 @@ class RebuildView(BaseView):
except Exception as e:
raise HTTPBadRequest(reason=str(e))
self.spawner.packages_rebuild(depends_on)
username = await self.username()
self.spawner.packages_rebuild(depends_on, username)
raise HTTPNoContent()

View File

@ -67,6 +67,7 @@ class RequestView(BaseView):
except Exception as e:
raise HTTPBadRequest(reason=str(e))
self.spawner.packages_add(packages, now=False)
username = await self.username()
self.spawner.packages_add(packages, username, now=False)
raise HTTPNoContent()

View File

@ -57,6 +57,7 @@ class UpdateView(BaseView):
Raises:
HTTPNoContent: in case of success response
"""
self.spawner.packages_update()
username = await self.username()
self.spawner.packages_update(username)
raise HTTPNoContent()

View File

@ -72,16 +72,16 @@ def test_with_dependencies(application: Application, package_ahriman: Package, p
"python-installer": create_package_mock("python-installer"),
}
package_mock = mocker.patch("ahriman.models.package.Package.from_aur", side_effect=lambda p, _: packages[p])
package_mock = mocker.patch("ahriman.models.package.Package.from_aur", side_effect=lambda *args: packages[args[0]])
packages_mock = mocker.patch("ahriman.application.application.Application._known_packages",
return_value=["devtools", "python-build", "python-pytest"])
return_value={"devtools", "python-build", "python-pytest"})
result = application.with_dependencies([package_ahriman], process_dependencies=True)
assert {package.base: package for package in result} == packages
package_mock.assert_has_calls([
MockCall(package_python_schedule.base, application.repository.pacman),
MockCall("python", application.repository.pacman),
MockCall("python-installer", application.repository.pacman),
MockCall(package_python_schedule.base, application.repository.pacman, package_ahriman.packager),
MockCall("python", application.repository.pacman, package_ahriman.packager),
MockCall("python-installer", application.repository.pacman, package_ahriman.packager),
], any_order=True)
packages_mock.assert_called_once_with()

View File

@ -43,7 +43,7 @@ def test_add_aur(application_packages: ApplicationPackages, package_ahriman: Pac
build_queue_mock = mocker.patch("ahriman.core.database.SQLite.build_queue_insert")
update_remote_mock = mocker.patch("ahriman.core.database.SQLite.remote_update")
application_packages._add_aur(package_ahriman.base)
application_packages._add_aur(package_ahriman.base, "packager")
build_queue_mock.assert_called_once_with(package_ahriman)
update_remote_mock.assert_called_once_with(package_ahriman)
@ -83,7 +83,7 @@ def test_add_local(application_packages: ApplicationPackages, package_ahriman: P
copytree_mock = mocker.patch("shutil.copytree")
build_queue_mock = mocker.patch("ahriman.core.database.SQLite.build_queue_insert")
application_packages._add_local(package_ahriman.base)
application_packages._add_local(package_ahriman.base, "packager")
is_dir_mock.assert_called_once_with()
copytree_mock.assert_called_once_with(
Path(package_ahriman.base), application_packages.repository.paths.cache_for(package_ahriman.base))
@ -103,7 +103,7 @@ def test_add_local_cache(application_packages: ApplicationPackages, package_ahri
copytree_mock = mocker.patch("shutil.copytree")
build_queue_mock = mocker.patch("ahriman.core.database.SQLite.build_queue_insert")
application_packages._add_local(package_ahriman.base)
application_packages._add_local(package_ahriman.base, "packager")
copytree_mock.assert_not_called()
init_mock.assert_not_called()
build_queue_mock.assert_called_once_with(package_ahriman)
@ -115,7 +115,7 @@ def test_add_local_missing(application_packages: ApplicationPackages, mocker: Mo
"""
mocker.patch("pathlib.Path.is_dir", return_value=False)
with pytest.raises(UnknownPackageError):
application_packages._add_local("package")
application_packages._add_local("package", "packager")
def test_add_remote(application_packages: ApplicationPackages, package_description_ahriman: PackageDescription,
@ -153,7 +153,7 @@ def test_add_repository(application_packages: ApplicationPackages, package_ahrim
build_queue_mock = mocker.patch("ahriman.core.database.SQLite.build_queue_insert")
update_remote_mock = mocker.patch("ahriman.core.database.SQLite.remote_update")
application_packages._add_repository(package_ahriman.base)
application_packages._add_repository(package_ahriman.base, "packager")
build_queue_mock.assert_called_once_with(package_ahriman)
update_remote_mock.assert_called_once_with(package_ahriman)
@ -165,8 +165,8 @@ def test_add_add_archive(application_packages: ApplicationPackages, package_ahri
"""
add_mock = mocker.patch("ahriman.application.application.application_packages.ApplicationPackages._add_archive")
application_packages.add([package_ahriman.base], PackageSource.Archive)
add_mock.assert_called_once_with(package_ahriman.base)
application_packages.add([package_ahriman.base], PackageSource.Archive, "packager")
add_mock.assert_called_once_with(package_ahriman.base, "packager")
def test_add_add_aur(application_packages: ApplicationPackages, package_ahriman: Package,
@ -176,8 +176,8 @@ def test_add_add_aur(application_packages: ApplicationPackages, package_ahriman:
"""
add_mock = mocker.patch("ahriman.application.application.application_packages.ApplicationPackages._add_aur")
application_packages.add([package_ahriman.base], PackageSource.AUR)
add_mock.assert_called_once_with(package_ahriman.base)
application_packages.add([package_ahriman.base], PackageSource.AUR, "packager")
add_mock.assert_called_once_with(package_ahriman.base, "packager")
def test_add_add_directory(application_packages: ApplicationPackages, package_ahriman: Package,
@ -187,8 +187,8 @@ def test_add_add_directory(application_packages: ApplicationPackages, package_ah
"""
add_mock = mocker.patch("ahriman.application.application.application_packages.ApplicationPackages._add_directory")
application_packages.add([package_ahriman.base], PackageSource.Directory)
add_mock.assert_called_once_with(package_ahriman.base)
application_packages.add([package_ahriman.base], PackageSource.Directory, "packager")
add_mock.assert_called_once_with(package_ahriman.base, "packager")
def test_add_add_local(application_packages: ApplicationPackages, package_ahriman: Package,
@ -198,8 +198,8 @@ def test_add_add_local(application_packages: ApplicationPackages, package_ahrima
"""
add_mock = mocker.patch("ahriman.application.application.application_packages.ApplicationPackages._add_local")
application_packages.add([package_ahriman.base], PackageSource.Local)
add_mock.assert_called_once_with(package_ahriman.base)
application_packages.add([package_ahriman.base], PackageSource.Local, "packager")
add_mock.assert_called_once_with(package_ahriman.base, "packager")
def test_add_add_remote(application_packages: ApplicationPackages, package_description_ahriman: PackageDescription,
@ -210,8 +210,8 @@ def test_add_add_remote(application_packages: ApplicationPackages, package_descr
add_mock = mocker.patch("ahriman.application.application.application_packages.ApplicationPackages._add_remote")
url = f"https://host/{package_description_ahriman.filename}"
application_packages.add([url], PackageSource.Remote)
add_mock.assert_called_once_with(url)
application_packages.add([url], PackageSource.Remote, "packager")
add_mock.assert_called_once_with(url, "packager")
def test_on_result(application_packages: ApplicationPackages) -> None:

View File

@ -76,9 +76,9 @@ def test_sign(application_repository: ApplicationRepository, package_ahriman: Pa
application_repository.sign([])
sign_package_mock.assert_has_calls([
MockCall(pytest.helpers.anyvar(int), package_ahriman.base),
MockCall(pytest.helpers.anyvar(int), package_python_schedule.base),
MockCall(pytest.helpers.anyvar(int), package_python_schedule.base),
MockCall(pytest.helpers.anyvar(int), None),
MockCall(pytest.helpers.anyvar(int), None),
MockCall(pytest.helpers.anyvar(int), None),
])
sign_repository_mock.assert_called_once_with(application_repository.repository.repo.repo_path)
on_result_mock.assert_called_once_with(Result())
@ -111,7 +111,7 @@ def test_sign_specific(application_repository: ApplicationRepository, package_ah
filename = package_ahriman.packages[package_ahriman.base].filepath
application_repository.sign([package_ahriman.base])
sign_package_mock.assert_called_once_with(filename, package_ahriman.base)
sign_package_mock.assert_called_once_with(filename, None)
sign_repository_mock.assert_called_once_with(application_repository.repository.repo.repo_path)
on_result_mock.assert_called_once_with(Result())
@ -170,9 +170,9 @@ def test_update(application_repository: ApplicationRepository, package_ahriman:
on_result_mock = mocker.patch(
"ahriman.application.application.application_repository.ApplicationRepository.on_result")
application_repository.update([package_ahriman])
build_mock.assert_called_once_with([package_ahriman])
update_mock.assert_has_calls([MockCall(paths), MockCall(paths)])
application_repository.update([package_ahriman], "username")
build_mock.assert_called_once_with([package_ahriman], "username")
update_mock.assert_has_calls([MockCall(paths, "username"), MockCall(paths, "username")])
on_result_mock.assert_has_calls([MockCall(result), MockCall(result)])

View File

@ -8,6 +8,7 @@ from ahriman.core.configuration import Configuration
from ahriman.core.repository import Repository
from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource
from ahriman.models.packagers import Packagers
from ahriman.models.result import Result
@ -27,6 +28,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
args.refresh = 0
args.source = PackageSource.Auto
args.dependencies = True
args.username = "username"
return args
@ -42,7 +44,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository:
on_start_mock = mocker.patch("ahriman.application.application.Application.on_start")
Add.run(args, "x86_64", configuration, report=False, unsafe=False)
application_mock.assert_called_once_with(args.package, args.source)
application_mock.assert_called_once_with(args.package, args.source, args.username)
dependencies_mock.assert_not_called()
on_start_mock.assert_called_once_with()
@ -67,7 +69,8 @@ def test_run_with_updates(args: argparse.Namespace, configuration: Configuration
Add.run(args, "x86_64", configuration, report=False, unsafe=False)
updates_mock.assert_called_once_with(args.package, aur=False, local=False, manual=True, vcs=False,
log_fn=pytest.helpers.anyvar(int))
application_mock.assert_called_once_with([package_ahriman])
application_mock.assert_called_once_with([package_ahriman],
Packagers(args.username, {package_ahriman.base: "packager"}))
dependencies_mock.assert_called_once_with([package_ahriman], process_dependencies=args.dependencies)
check_mock.assert_called_once_with(False, False)

View File

@ -109,7 +109,7 @@ def test_patch_create_from_diff(package_ahriman: Package, mocker: MockerFixture)
sources_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.patch_create", return_value=patch.value)
assert Patch.patch_create_from_diff(path, "x86_64", ["*.diff"]) == (package_ahriman.base, patch)
package_mock.assert_called_once_with(path, "x86_64")
package_mock.assert_called_once_with(path, "x86_64", None)
sources_mock.assert_called_once_with(path, "*.diff")

View File

@ -28,6 +28,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
args.from_database = False
args.exit_code = False
args.status = None
args.username = "username"
return args
@ -50,7 +51,7 @@ def test_run(args: argparse.Namespace, package_ahriman: Package, configuration:
Rebuild.run(args, "x86_64", configuration, report=False, unsafe=False)
extract_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.status, from_database=args.from_database)
application_packages_mock.assert_called_once_with([package_ahriman], None)
application_mock.assert_called_once_with([package_ahriman])
application_mock.assert_called_once_with([package_ahriman], args.username)
check_mock.assert_has_calls([MockCall(False, False), MockCall(False, False)])
on_start_mock.assert_called_once_with()

View File

@ -36,7 +36,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository:
check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty")
ServiceUpdates.run(args, "x86_64", configuration, report=False, unsafe=False)
package_mock.assert_called_once_with(package_ahriman.base, repository.pacman)
package_mock.assert_called_once_with(package_ahriman.base, repository.pacman, None)
application_mock.assert_called_once_with(verbose=True, separator=" -> ")
check_mock.assert_called_once_with(args.exit_code, True)

View File

@ -19,7 +19,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
argparse.Namespace: generated arguments for these test cases
"""
args.parser = _parser
args.command = None
args.command = []
return args
@ -42,14 +42,14 @@ def test_run_check(args: argparse.Namespace, configuration: Configuration, mocke
must run command and check if command is unsafe
"""
args = _default_args(args)
args.command = "clean"
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, report=False, unsafe=False)
commands_mock.assert_called_once_with(pytest.helpers.anyvar(int))
check_mock.assert_called_once_with("clean", ["command"], pytest.helpers.anyvar(int))
check_mock.assert_called_once_with(["clean"], ["command"], pytest.helpers.anyvar(int))
def test_check_unsafe(mocker: MockerFixture) -> None:
@ -57,7 +57,7 @@ def test_check_unsafe(mocker: MockerFixture) -> None:
must check if command is unsafe
"""
check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty")
UnsafeCommands.check_unsafe("service-clean", ["service-clean"], _parser())
UnsafeCommands.check_unsafe(["service-clean"], ["service-clean"], _parser())
check_mock.assert_called_once_with(True, True)
@ -66,7 +66,7 @@ def test_check_unsafe_safe(mocker: MockerFixture) -> None:
must check if command is safe
"""
check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty")
UnsafeCommands.check_unsafe("package-status", ["service-clean"], _parser())
UnsafeCommands.check_unsafe(["package-status"], ["service-clean"], _parser())
check_mock.assert_called_once_with(True, False)

View File

@ -9,6 +9,7 @@ from ahriman.application.handlers import Update
from ahriman.core.configuration import Configuration
from ahriman.core.repository import Repository
from ahriman.models.package import Package
from ahriman.models.packagers import Packagers
from ahriman.models.result import Result
@ -31,6 +32,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
args.manual = True
args.vcs = True
args.refresh = 0
args.username = "username"
return args
@ -51,7 +53,8 @@ def test_run(args: argparse.Namespace, package_ahriman: Package, configuration:
on_start_mock = mocker.patch("ahriman.application.application.Application.on_start")
Update.run(args, "x86_64", configuration, report=False, unsafe=False)
application_mock.assert_called_once_with([package_ahriman])
application_mock.assert_called_once_with([package_ahriman],
Packagers(args.username, {package_ahriman.base: "packager"}))
updates_mock.assert_called_once_with(args.package, aur=args.aur, local=args.local, manual=args.manual, vcs=args.vcs,
log_fn=pytest.helpers.anyvar(int))
dependencies_mock.assert_called_once_with([package_ahriman], process_dependencies=args.dependencies)

View File

@ -27,6 +27,8 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
args.username = "user"
args.action = Action.Update
args.exit_code = False
args.key = "key"
args.packager = "packager"
args.password = "pa55w0rd"
args.role = UserAccess.Reporter
args.secure = False
@ -38,7 +40,8 @@ def test_run(args: argparse.Namespace, configuration: Configuration, database: S
must run command
"""
args = _default_args(args)
user = User(username=args.username, password=args.password, access=args.role)
user = User(username=args.username, password=args.password, access=args.role,
packager_id=args.packager, key=args.key)
mocker.patch("ahriman.core.database.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.Users.configuration_get")
@ -61,7 +64,8 @@ def test_run_empty_salt(args: argparse.Namespace, configuration: Configuration,
must create configuration if salt was not set
"""
args = _default_args(args)
user = User(username=args.username, password=args.password, access=args.role)
user = User(username=args.username, password=args.password, access=args.role,
packager_id=args.packager, key=args.key)
mocker.patch("ahriman.core.database.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.Users.configuration_get")

View File

@ -351,13 +351,14 @@ def test_subparsers_repo_backup_architecture(parser: argparse.ArgumentParser) ->
def test_subparsers_repo_check(parser: argparse.ArgumentParser) -> None:
"""
repo-check command must imply dependencies, dry-run, aur and manual
repo-check command must imply dependencies, dry-run, aur, manual and username
"""
args = parser.parse_args(["repo-check"])
assert not args.dependencies
assert args.dry_run
assert args.aur
assert not args.manual
assert args.username is None
def test_subparsers_repo_check_architecture(parser: argparse.ArgumentParser) -> None:
@ -757,14 +758,13 @@ def test_subparsers_user_add_option_role(parser: argparse.ArgumentParser) -> Non
def test_subparsers_user_list(parser: argparse.ArgumentParser) -> None:
"""
user-list command must imply action, architecture, lock, report, password, quiet and unsafe
user-list command must imply action, architecture, lock, report, quiet and unsafe
"""
args = parser.parse_args(["user-list"])
assert args.action == Action.List
assert args.architecture == [""]
assert args.lock is None
assert not args.report
assert args.password is not None
assert args.quiet
assert args.unsafe
@ -787,14 +787,13 @@ def test_subparsers_user_list_option_role(parser: argparse.ArgumentParser) -> No
def test_subparsers_user_remove(parser: argparse.ArgumentParser) -> None:
"""
user-remove command must imply action, architecture, lock, report, password and quiet
user-remove command must imply action, architecture, lock, report and quiet
"""
args = parser.parse_args(["user-remove", "username"])
assert args.action == Action.Remove
assert args.architecture == [""]
assert args.lock is None
assert not args.report
assert args.password is not None
assert args.quiet

View File

@ -265,7 +265,8 @@ def package_ahriman(package_description_ahriman: PackageDescription, remote_sour
base="ahriman",
version="2.6.0-1",
remote=remote_source,
packages=packages)
packages=packages,
packager="packager")
@pytest.fixture
@ -499,7 +500,7 @@ def user() -> User:
Returns:
User: user descriptor instance
"""
return User(username="user", password="pa55w0rd", access=UserAccess.Reporter)
return User(username="user", password="pa55w0rd", access=UserAccess.Reporter, packager_id="packager", key="key")
@pytest.fixture

View File

@ -10,7 +10,7 @@ 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(Path("ahriman"))
task_ahriman.build(Path("ahriman"), "packager")
check_output_mock.assert_called()

View File

@ -27,7 +27,7 @@ def test_migrate_data(connection: Connection, configuration: Configuration, mock
def test_migrate_package_depends(connection: Connection, configuration: Configuration, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must update make and opt depends list
must update check depends list
"""
mocker.patch("pathlib.Path.is_dir", return_value=True)
mocker.patch("pathlib.Path.iterdir", return_value=[package_ahriman.packages[package_ahriman.base].filepath])
@ -45,7 +45,7 @@ def test_migrate_package_depends(connection: Connection, configuration: Configur
def test_migrate_package_depends_skip(connection: Connection, configuration: Configuration,
mocker: MockerFixture) -> None:
"""
must skip update make and opt depends list if no repository directory found
must skip update check depends list if no repository directory found
"""
mocker.patch("pathlib.Path.is_dir", return_value=False)
migrate_package_check_depends(connection, configuration)

View File

@ -0,0 +1,52 @@
import pytest
from pytest_mock import MockerFixture
from sqlite3 import Connection
from ahriman.core.configuration import Configuration
from ahriman.core.database.migrations.m008_packagers import migrate_data, migrate_package_base_packager, steps
from ahriman.models.package import Package
def test_migration_packagers() -> None:
"""
migration must not be empty
"""
assert steps
def test_migrate_data(connection: Connection, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must perform data migration
"""
depends_mock = mocker.patch("ahriman.core.database.migrations.m008_packagers.migrate_package_base_packager")
migrate_data(connection, configuration)
depends_mock.assert_called_once_with(connection, configuration)
def test_migrate_package_base_packager(connection: Connection, configuration: Configuration, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must update packagers
"""
mocker.patch("pathlib.Path.is_dir", return_value=True)
mocker.patch("pathlib.Path.iterdir", return_value=[package_ahriman.packages[package_ahriman.base].filepath])
package_mock = mocker.patch("ahriman.models.package.Package.from_archive", return_value=package_ahriman)
migrate_package_base_packager(connection, configuration)
package_mock.assert_called_once_with(
package_ahriman.packages[package_ahriman.base].filepath, pytest.helpers.anyvar(int), remote=None)
connection.executemany.assert_called_once_with(pytest.helpers.anyvar(str, strict=True), [{
"package_base": package_ahriman.base,
"packager": package_ahriman.packager,
}])
def test_migrate_package_depends_skip(connection: Connection, configuration: Configuration,
mocker: MockerFixture) -> None:
"""
must skip update packagers if no repository directory found
"""
mocker.patch("pathlib.Path.is_dir", return_value=False)
migrate_package_base_packager(connection, configuration)
connection.executemany.assert_not_called()

View File

@ -16,21 +16,22 @@ def test_user_list(database: SQLite, user: User) -> None:
must return all users
"""
database.user_update(user)
database.user_update(User(username=user.password, password=user.username, access=user.access))
second = User(username=user.password, password=user.username, access=user.access, packager_id=None, key=None)
database.user_update(second)
users = database.user_list(None, None)
assert len(users) == 2
assert user in users
assert User(username=user.password, password=user.username, access=user.access) in users
assert second in users
def test_user_list_filter_by_username(database: SQLite) -> None:
"""
must return users filtered by its id
"""
first = User(username="1", password="", access=UserAccess.Read)
second = User(username="2", password="", access=UserAccess.Full)
third = User(username="3", password="", access=UserAccess.Read)
first = User(username="1", password="", access=UserAccess.Read, packager_id=None, key=None)
second = User(username="2", password="", access=UserAccess.Full, packager_id=None, key=None)
third = User(username="3", password="", access=UserAccess.Read, packager_id=None, key=None)
database.user_update(first)
database.user_update(second)
@ -45,9 +46,9 @@ def test_user_list_filter_by_access(database: SQLite) -> None:
"""
must return users filtered by its access
"""
first = User(username="1", password="", access=UserAccess.Read)
second = User(username="2", password="", access=UserAccess.Full)
third = User(username="3", password="", access=UserAccess.Read)
first = User(username="1", password="", access=UserAccess.Read, packager_id=None, key=None)
second = User(username="2", password="", access=UserAccess.Full, packager_id=None, key=None)
third = User(username="3", password="", access=UserAccess.Read, packager_id=None, key=None)
database.user_update(first)
database.user_update(second)
@ -63,9 +64,9 @@ def test_user_list_filter_by_username_access(database: SQLite) -> None:
"""
must return users filtered by its access and username
"""
first = User(username="1", password="", access=UserAccess.Read)
second = User(username="2", password="", access=UserAccess.Full)
third = User(username="3", password="", access=UserAccess.Read)
first = User(username="1", password="", access=UserAccess.Read, packager_id=None, key=None)
second = User(username="2", password="", access=UserAccess.Full, packager_id=None, key=None)
third = User(username="3", password="", access=UserAccess.Read, packager_id=None, key=None)
database.user_update(first)
database.user_update(second)
@ -91,6 +92,7 @@ def test_user_update(database: SQLite, user: User) -> None:
database.user_update(user)
assert database.user_get(user.username) == user
new_user = User(username=user.username, password=user.hash_password("salt").password, access=UserAccess.Full)
new_user = User(username=user.username, password=user.hash_password("salt").password, access=UserAccess.Full,
packager_id=None, key="new key")
database.user_update(new_user)
assert database.user_get(new_user.username) == new_user

View File

@ -32,7 +32,7 @@ def test_package_update(database: SQLite, configuration: Configuration, package_
fetch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.fetch")
patches_mock = mocker.patch("ahriman.core.database.SQLite.patches_get", return_value=[patch1, patch2])
patches_write_mock = mocker.patch("ahriman.models.pkgbuild_patch.PkgbuildPatch.write")
runner = RemotePush(configuration, database, "gitremote")
runner = RemotePush(database, configuration, "gitremote")
assert runner.package_update(package_ahriman, local) == package_ahriman.base
glob_mock.assert_called_once_with(".git*")
@ -56,7 +56,7 @@ def test_packages_update(database: SQLite, configuration: Configuration, result:
"""
update_mock = mocker.patch("ahriman.core.gitremote.remote_push.RemotePush.package_update",
return_value=[package_ahriman.base])
runner = RemotePush(configuration, database, "gitremote")
runner = RemotePush(database, configuration, "gitremote")
local = Path("local")
assert list(runner.packages_update(result, local))
@ -71,7 +71,7 @@ def test_run(database: SQLite, configuration: Configuration, result: Result, pac
mocker.patch("ahriman.core.gitremote.remote_push.RemotePush.packages_update", return_value=[package_ahriman.base])
fetch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.fetch")
push_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.push")
runner = RemotePush(configuration, database, "gitremote")
runner = RemotePush(database, configuration, "gitremote")
runner.run(result)
fetch_mock.assert_called_once_with(pytest.helpers.anyvar(int), runner.remote_source)
@ -85,7 +85,7 @@ def test_run_failed(database: SQLite, configuration: Configuration, result: Resu
must reraise exception on error occurred
"""
mocker.patch("ahriman.core.build_tools.sources.Sources.fetch", side_effect=Exception())
runner = RemotePush(configuration, database, "gitremote")
runner = RemotePush(database, configuration, "gitremote")
with pytest.raises(GitRemoteError):
runner.run(result)

View File

@ -6,6 +6,8 @@ from unittest.mock import call as MockCall
from ahriman.core.repository.executor import Executor
from ahriman.models.package import Package
from ahriman.models.packagers import Packagers
from ahriman.models.user import User
def test_load_archives(executor: Executor) -> None:
@ -33,7 +35,7 @@ def test_process_build(executor: Executor, package_ahriman: Package, mocker: Moc
move_mock = mocker.patch("shutil.move")
status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_building")
executor.process_build([package_ahriman])
executor.process_build([package_ahriman], Packagers("packager"))
# must move files (once)
move_mock.assert_called_once_with(Path(package_ahriman.base), executor.paths.packages / package_ahriman.base)
# must update status
@ -157,7 +159,7 @@ def test_process_remove_unknown(executor: Executor, package_ahriman: Package, mo
status_client_mock.assert_called_once_with(package_ahriman.base)
def test_process_update(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
def test_process_update(executor: Executor, package_ahriman: Package, user: User, mocker: MockerFixture) -> None:
"""
must run update process
"""
@ -168,14 +170,16 @@ def test_process_update(executor: Executor, package_ahriman: Package, mocker: Mo
sign_package_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process_sign_package", side_effect=lambda fn, _: [fn])
status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_success")
remove_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_remove")
packager_mock = mocker.patch("ahriman.core.repository.executor.Executor.packager", return_value=user)
filepath = next(package.filepath for package in package_ahriman.packages.values())
# must return complete
assert executor.process_update([filepath])
assert executor.process_update([filepath], Packagers("packager"))
packager_mock.assert_called_once_with(Packagers("packager"), "ahriman")
# must move files (once)
move_mock.assert_called_once_with(executor.paths.packages / filepath, executor.paths.repository / filepath)
# must sign package
sign_package_mock.assert_called_once_with(executor.paths.packages / filepath, package_ahriman.base)
sign_package_mock.assert_called_once_with(executor.paths.packages / filepath, user.key)
# must add package
repo_add_mock.assert_called_once_with(executor.paths.repository / filepath)
# must update status

View File

@ -4,6 +4,10 @@ from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.exceptions import UnsafeRunError
from ahriman.core.repository.repository_properties import RepositoryProperties
from ahriman.models.packagers import Packagers
from ahriman.models.pacman_synchronization import PacmanSynchronization
from ahriman.models.user import User
from ahriman.models.user_access import UserAccess
def test_create_tree_on_load(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> None:
@ -12,7 +16,8 @@ def test_create_tree_on_load(configuration: Configuration, database: SQLite, moc
"""
mocker.patch("ahriman.core.repository.repository_properties.check_user")
tree_create_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
RepositoryProperties("x86_64", configuration, database, report=False, unsafe=False, refresh_pacman_database=0)
RepositoryProperties("x86_64", configuration, database, report=False, unsafe=False,
refresh_pacman_database=PacmanSynchronization.Disabled)
tree_create_mock.assert_called_once_with()
@ -23,6 +28,36 @@ def test_create_tree_on_load_unsafe(configuration: Configuration, database: SQLi
"""
mocker.patch("ahriman.core.repository.repository_properties.check_user", side_effect=UnsafeRunError(0, 1))
tree_create_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
RepositoryProperties("x86_64", configuration, database, report=False, unsafe=False, refresh_pacman_database=0)
RepositoryProperties("x86_64", configuration, database, report=False, unsafe=False,
refresh_pacman_database=PacmanSynchronization.Disabled)
tree_create_mock.assert_not_called()
def test_packager(repository: RepositoryProperties, mocker: MockerFixture) -> None:
"""
must extract packager
"""
database_mock = mocker.patch("ahriman.core.database.SQLite.user_get")
assert repository.packager(Packagers("username", {}), "base")
database_mock.assert_called_once_with("username")
def test_packager_empty(repository: RepositoryProperties, mocker: MockerFixture) -> None:
"""
must return empty user if username was not set
"""
database_mock = mocker.patch("ahriman.core.database.SQLite.user_get")
user = User(username="", password="", access=UserAccess.Read, packager_id=None, key=None)
assert repository.packager(Packagers(), "base") == user
database_mock.assert_not_called()
def test_packager_empty_result(repository: RepositoryProperties, mocker: MockerFixture) -> None:
"""
must return empty user if it wasn't found in database
"""
database_mock = mocker.patch("ahriman.core.database.SQLite.user_get", return_value=None)
user = User(username="username", password="", access=UserAccess.Read, packager_id=None, key=None)
assert repository.packager(Packagers(user.username), "base") == user
database_mock.assert_called_once_with(user.username)

View File

@ -74,7 +74,7 @@ def test_updates_aur_filter(update_handler: UpdateHandler, package_ahriman: Pack
package_load_mock = mocker.patch("ahriman.models.package.Package.from_aur", return_value=package_ahriman)
assert update_handler.updates_aur([package_ahriman.base], vcs=True) == [package_ahriman]
package_load_mock.assert_called_once_with(package_ahriman.base, update_handler.pacman)
package_load_mock.assert_called_once_with(package_ahriman.base, update_handler.pacman, None)
def test_updates_aur_ignore(update_handler: UpdateHandler, package_ahriman: Package,
@ -120,7 +120,7 @@ def test_updates_local(update_handler: UpdateHandler, package_ahriman: Package,
assert update_handler.updates_local(vcs=True) == [package_ahriman]
fetch_mock.assert_called_once_with(Path(package_ahriman.base), remote=None)
package_load_mock.assert_called_once_with(Path(package_ahriman.base), "x86_64")
package_load_mock.assert_called_once_with(Path(package_ahriman.base), "x86_64", None)
status_client_mock.assert_called_once_with(package_ahriman.base)
package_is_outdated_mock.assert_called_once_with(
package_ahriman, update_handler.paths,

View File

@ -135,21 +135,6 @@ def test_key_import(gpg: GPG, mocker: MockerFixture) -> None:
check_output_mock.assert_called_once_with("gpg", "--import", input_data="key", logger=pytest.helpers.anyvar(int))
def test_keys(gpg: GPG) -> None:
"""
must extract keys
"""
assert gpg.keys() == []
gpg.default_key = "key"
assert gpg.keys() == [gpg.default_key]
gpg.configuration.set_option("sign", "key_a", "key1")
gpg.configuration.set_option("sign", "key_b", "key1")
gpg.configuration.set_option("sign", "key_c", "key2")
assert gpg.keys() == ["key", "key1", "key2"]
def test_process(gpg_with_key: GPG, mocker: MockerFixture) -> None:
"""
must call process method correctly
@ -170,7 +155,7 @@ def test_process_sign_package_1(gpg_with_key: GPG, mocker: MockerFixture) -> Non
gpg_with_key.targets = {SignSettings.Packages}
assert gpg_with_key.process_sign_package(Path("a"), "a") == result
process_mock.assert_called_once_with(Path("a"), "key")
process_mock.assert_called_once_with(Path("a"), "a")
def test_process_sign_package_2(gpg_with_key: GPG, mocker: MockerFixture) -> None:
@ -182,7 +167,19 @@ def test_process_sign_package_2(gpg_with_key: GPG, mocker: MockerFixture) -> Non
gpg_with_key.targets = {SignSettings.Packages, SignSettings.Repository}
assert gpg_with_key.process_sign_package(Path("a"), "a") == result
process_mock.assert_called_once_with(Path("a"), "key")
process_mock.assert_called_once_with(Path("a"), "a")
def test_process_sign_package_3(gpg_with_key: GPG, mocker: MockerFixture) -> None:
"""
must sign package with default key if none passed
"""
result = [Path("a"), Path("a.sig")]
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process", return_value=result)
gpg_with_key.targets = {SignSettings.Packages}
assert gpg_with_key.process_sign_package(Path("a"), None) == result
process_mock.assert_called_once_with(Path("a"), gpg_with_key.default_key)
def test_process_sign_package_skip_1(gpg_with_key: GPG, mocker: MockerFixture) -> None:
@ -211,7 +208,7 @@ def test_process_sign_package_skip_3(gpg: GPG, mocker: MockerFixture) -> None:
"""
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process")
gpg.targets = {SignSettings.Packages}
gpg.process_sign_package(Path("a"), "a")
gpg.process_sign_package(Path("a"), None)
process_mock.assert_not_called()
@ -221,7 +218,7 @@ def test_process_sign_package_skip_4(gpg: GPG, mocker: MockerFixture) -> None:
"""
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process")
gpg.targets = {SignSettings.Packages, SignSettings.Repository}
gpg.process_sign_package(Path("a"), "a")
gpg.process_sign_package(Path("a"), None)
process_mock.assert_not_called()

View File

@ -1,24 +1,26 @@
import pytest
from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.sign.gpg import GPG
from ahriman.core.support.pkgbuild.keyring_generator import KeyringGenerator
from ahriman.core.support.pkgbuild.pkgbuild_generator import PkgbuildGenerator
@pytest.fixture
def keyring_generator(gpg: GPG, configuration: Configuration) -> KeyringGenerator:
def keyring_generator(database: SQLite, gpg: GPG, configuration: Configuration) -> KeyringGenerator:
"""
fixture for keyring pkgbuild generator
Args:
database(SQLite): database fixture
gpg(GPG): empty GPG fixture
configuration(Configuration): configuration fixture
Returns:
KeyringGenerator: keyring generator test instance
"""
return KeyringGenerator(gpg, configuration, "keyring")
return KeyringGenerator(database, gpg, configuration, "keyring")
@pytest.fixture

View File

@ -5,84 +5,87 @@ from pytest_mock import MockerFixture
from unittest.mock import MagicMock, call as MockCall
from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.exceptions import PkgbuildGeneratorError
from ahriman.core.sign.gpg import GPG
from ahriman.core.support.pkgbuild.keyring_generator import KeyringGenerator
from ahriman.models.user import User
def test_init_packagers(gpg: GPG, configuration: Configuration, mocker: MockerFixture) -> None:
def test_init_packagers(database: SQLite, gpg: GPG, configuration: Configuration, user: User,
mocker: MockerFixture) -> None:
"""
must extract packagers keys
"""
mocker.patch("ahriman.core.sign.gpg.GPG.keys", return_value=["key"])
mocker.patch("ahriman.core.database.SQLite.user_list", return_value=[user])
assert KeyringGenerator(gpg, configuration, "keyring").packagers == ["key"]
assert KeyringGenerator(database, gpg, configuration, "keyring").packagers == ["key"]
configuration.set_option("keyring", "packagers", "key1")
assert KeyringGenerator(gpg, configuration, "keyring").packagers == ["key1"]
assert KeyringGenerator(database, gpg, configuration, "keyring").packagers == ["key1"]
def test_init_revoked(gpg: GPG, configuration: Configuration) -> None:
def test_init_revoked(database: SQLite, gpg: GPG, configuration: Configuration) -> None:
"""
must extract revoked keys
"""
assert KeyringGenerator(gpg, configuration, "keyring").revoked == []
assert KeyringGenerator(database, gpg, configuration, "keyring").revoked == []
configuration.set_option("keyring", "revoked", "key1")
assert KeyringGenerator(gpg, configuration, "keyring").revoked == ["key1"]
assert KeyringGenerator(database, gpg, configuration, "keyring").revoked == ["key1"]
def test_init_trusted(gpg: GPG, configuration: Configuration) -> None:
def test_init_trusted(database: SQLite, gpg: GPG, configuration: Configuration) -> None:
"""
must extract trusted keys
"""
assert KeyringGenerator(gpg, configuration, "keyring").trusted == []
assert KeyringGenerator(database, gpg, configuration, "keyring").trusted == []
gpg.default_key = "key"
assert KeyringGenerator(gpg, configuration, "keyring").trusted == ["key"]
assert KeyringGenerator(database, gpg, configuration, "keyring").trusted == ["key"]
configuration.set_option("keyring", "trusted", "key1")
assert KeyringGenerator(gpg, configuration, "keyring").trusted == ["key1"]
assert KeyringGenerator(database, gpg, configuration, "keyring").trusted == ["key1"]
def test_license(gpg: GPG, configuration: Configuration) -> None:
def test_license(database: SQLite, gpg: GPG, configuration: Configuration) -> None:
"""
must generate correct licenses list
"""
assert KeyringGenerator(gpg, configuration, "keyring").license == ["Unlicense"]
assert KeyringGenerator(database, gpg, configuration, "keyring").license == ["Unlicense"]
configuration.set_option("keyring", "license", "GPL MPL")
assert KeyringGenerator(gpg, configuration, "keyring").license == ["GPL", "MPL"]
assert KeyringGenerator(database, gpg, configuration, "keyring").license == ["GPL", "MPL"]
def test_pkgdesc(gpg: GPG, configuration: Configuration) -> None:
def test_pkgdesc(database: SQLite, gpg: GPG, configuration: Configuration) -> None:
"""
must generate correct pkgdesc property
"""
assert KeyringGenerator(gpg, configuration, "keyring").pkgdesc == "aur-clone PGP keyring"
assert KeyringGenerator(database, gpg, configuration, "keyring").pkgdesc == "aur-clone PGP keyring"
configuration.set_option("keyring", "description", "description")
assert KeyringGenerator(gpg, configuration, "keyring").pkgdesc == "description"
assert KeyringGenerator(database, gpg, configuration, "keyring").pkgdesc == "description"
def test_pkgname(gpg: GPG, configuration: Configuration) -> None:
def test_pkgname(database: SQLite, gpg: GPG, configuration: Configuration) -> None:
"""
must generate correct pkgname property
"""
assert KeyringGenerator(gpg, configuration, "keyring").pkgname == "aur-clone-keyring"
assert KeyringGenerator(database, gpg, configuration, "keyring").pkgname == "aur-clone-keyring"
configuration.set_option("keyring", "package", "keyring")
assert KeyringGenerator(gpg, configuration, "keyring").pkgname == "keyring"
assert KeyringGenerator(database, gpg, configuration, "keyring").pkgname == "keyring"
def test_url(gpg: GPG, configuration: Configuration) -> None:
def test_url(database: SQLite, gpg: GPG, configuration: Configuration) -> None:
"""
must generate correct url property
"""
assert KeyringGenerator(gpg, configuration, "keyring").url == ""
assert KeyringGenerator(database, gpg, configuration, "keyring").url == ""
configuration.set_option("keyring", "homepage", "homepage")
assert KeyringGenerator(gpg, configuration, "keyring").url == "homepage"
assert KeyringGenerator(database, gpg, configuration, "keyring").url == "homepage"
def test_generate_gpg(keyring_generator: KeyringGenerator, mocker: MockerFixture) -> None:

View File

@ -1,6 +1,8 @@
from pytest_mock import MockerFixture
from unittest.mock import call as MockCall
from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.sign.gpg import GPG
from ahriman.core.support import KeyringTrigger
from ahriman.models.context_key import ContextKey
@ -21,10 +23,10 @@ def test_on_start(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run report for specified targets
"""
gpg_mock = mocker.patch("ahriman.core._Context.get")
context_mock = mocker.patch("ahriman.core._Context.get")
run_mock = mocker.patch("ahriman.core.support.package_creator.PackageCreator.run")
trigger = KeyringTrigger("x86_64", configuration)
trigger.on_start()
gpg_mock.assert_called_once_with(ContextKey("sign", GPG))
context_mock.assert_has_calls([MockCall(ContextKey("sign", GPG)), MockCall(ContextKey("database", SQLite))])
run_mock.assert_called_once_with()

View File

@ -35,6 +35,6 @@ def test_run(package_creator: PackageCreator, database: SQLite, mocker: MockerFi
write_mock.assert_called_once_with(local_path)
init_mock.assert_called_once_with(local_path)
package_mock.assert_called_once_with(local_path, "x86_64")
package_mock.assert_called_once_with(local_path, "x86_64", None)
database_mock.assert_called_once_with(ContextKey("database", SQLite))
insert_mock.assert_called_once_with(package, pytest.helpers.anyvar(int))

View File

@ -42,7 +42,7 @@ def test_spawn_process(spawner: Spawn, mocker: MockerFixture) -> None:
"""
start_mock = mocker.patch("multiprocessing.Process.start")
spawner._spawn_process("add", "ahriman", now="", maybe="?")
spawner._spawn_process("add", "ahriman", now="", maybe="?", none=None)
start_mock.assert_called_once_with()
spawner.args_parser.parse_args.assert_called_once_with(
spawner.command_arguments + [
@ -74,8 +74,8 @@ def test_packages_add(spawner: Spawn, mocker: MockerFixture) -> None:
must call package addition
"""
spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process")
spawner.packages_add(["ahriman", "linux"], now=False)
spawn_mock.assert_called_once_with("package-add", "ahriman", "linux", source="aur")
spawner.packages_add(["ahriman", "linux"], None, now=False)
spawn_mock.assert_called_once_with("package-add", "ahriman", "linux", source="aur", username=None)
def test_packages_add_with_build(spawner: Spawn, mocker: MockerFixture) -> None:
@ -83,8 +83,17 @@ def test_packages_add_with_build(spawner: Spawn, mocker: MockerFixture) -> None:
must call package addition with update
"""
spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process")
spawner.packages_add(["ahriman", "linux"], now=True)
spawn_mock.assert_called_once_with("package-add", "ahriman", "linux", source="aur", now="")
spawner.packages_add(["ahriman", "linux"], None, now=True)
spawn_mock.assert_called_once_with("package-add", "ahriman", "linux", source="aur", username=None, now="")
def test_packages_add_with_username(spawner: Spawn, mocker: MockerFixture) -> None:
"""
must call package addition with username
"""
spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process")
spawner.packages_add(["ahriman", "linux"], "username", now=False)
spawn_mock.assert_called_once_with("package-add", "ahriman", "linux", source="aur", username="username")
def test_packages_rebuild(spawner: Spawn, mocker: MockerFixture) -> None:
@ -92,8 +101,8 @@ def test_packages_rebuild(spawner: Spawn, mocker: MockerFixture) -> None:
must call package rebuild
"""
spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process")
spawner.packages_rebuild("python")
spawn_mock.assert_called_once_with("repo-rebuild", **{"depends-on": "python"})
spawner.packages_rebuild("python", "packager")
spawn_mock.assert_called_once_with("repo-rebuild", **{"depends-on": "python", "username": "packager"})
def test_packages_remove(spawner: Spawn, mocker: MockerFixture) -> None:
@ -110,8 +119,8 @@ def test_packages_update(spawner: Spawn, mocker: MockerFixture) -> None:
must call repo update
"""
spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process")
spawner.packages_update()
spawn_mock.assert_called_once_with("repo-update")
spawner.packages_update("packager")
spawn_mock.assert_called_once_with("repo-update", username="packager")
def test_run(spawner: Spawn, mocker: MockerFixture) -> None:

View File

@ -11,9 +11,9 @@ from typing import Any
from unittest.mock import MagicMock
from ahriman.core.exceptions import BuildError, OptionError, UnsafeRunError
from ahriman.core.util import check_output, check_user, enum_values, exception_response_text, filter_json, \
full_version, package_like, partition, pretty_datetime, pretty_size, safe_filename, srcinfo_property, \
srcinfo_property_list, trim_package, utcnow, walk
from ahriman.core.util import check_output, check_user, dataclass_view, enum_values, exception_response_text,\
extract_user, filter_json, full_version, package_like, partition, pretty_datetime, pretty_size, safe_filename, \
srcinfo_property, srcinfo_property_list, trim_package, utcnow, walk
from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource
from ahriman.models.repository_paths import RepositoryPaths
@ -91,6 +91,16 @@ def test_check_output_with_user(passwd: Any, mocker: MockerFixture) -> None:
getpwuid_mock.assert_called_once_with(user)
def test_check_output_with_user_and_environment(passwd: Any, mocker: MockerFixture) -> None:
"""
must run set environment if both environment and user are set
"""
mocker.patch("ahriman.core.util.getpwuid", return_value=passwd)
user = os.getuid()
assert check_output("python", "-c", """import os; print(os.getenv("HOME"), os.getenv("VAR"))""",
environment={"VAR": "VALUE"}, user=user) == f"{passwd.pw_dir} VALUE"
def test_check_output_failure(mocker: MockerFixture) -> None:
"""
must process exception correctly
@ -155,6 +165,23 @@ def test_check_user_unsafe(mocker: MockerFixture) -> None:
check_user(paths, unsafe=True)
def test_dataclass_view(package_ahriman: Package) -> None:
"""
must serialize dataclasses
"""
assert Package.from_json(dataclass_view(package_ahriman)) == package_ahriman
def test_dataclass_view_without_none(package_ahriman: Package) -> None:
"""
must serialize dataclasses with None fields removed
"""
package_ahriman.packager = None
result = dataclass_view(package_ahriman)
assert "packager" not in result
assert Package.from_json(result) == package_ahriman
def test_exception_response_text() -> None:
"""
must parse HTTP response to string
@ -174,6 +201,23 @@ def test_exception_response_text_empty() -> None:
assert exception_response_text(exception) == ""
def test_extract_user() -> None:
"""
must extract user from system environment
"""
os.environ["USER"] = "user"
assert extract_user() == "user"
os.environ["SUDO_USER"] = "sudo"
assert extract_user() == "sudo"
os.environ["DOAS_USER"] = "doas"
assert extract_user() == "sudo"
del os.environ["SUDO_USER"]
assert extract_user() == "doas"
def test_filter_json(package_ahriman: Package) -> None:
"""
must filter fields by known list

View File

@ -116,6 +116,7 @@ def pyalpm_package_ahriman(aur_package_ahriman: AURPackage) -> MagicMock:
type(mock).name = PropertyMock(return_value=aur_package_ahriman.name)
type(mock).optdepends = PropertyMock(return_value=aur_package_ahriman.opt_depends)
type(mock).checkdepends = PropertyMock(return_value=aur_package_ahriman.check_depends)
type(mock).packager = PropertyMock(return_value="packager")
type(mock).provides = PropertyMock(return_value=aur_package_ahriman.provides)
type(mock).version = PropertyMock(return_value=aur_package_ahriman.version)
type(mock).url = PropertyMock(return_value=aur_package_ahriman.url)

View File

@ -56,7 +56,7 @@ def test_depends_build_with_version_and_overlap(mocker: MockerFixture, resource_
srcinfo = (resource_path_root / "models" / "package_gcc10_srcinfo").read_text()
mocker.patch("ahriman.models.package.Package._check_output", return_value=srcinfo)
package_gcc10 = Package.from_build(Path("local"), "x86_64")
package_gcc10 = Package.from_build(Path("local"), "x86_64", None)
assert package_gcc10.depends_build == {
"glibc", "zstd", # depends
"doxygen", "binutils", "git", "libmpc", "python", # make depends
@ -168,10 +168,11 @@ def test_from_aur(package_ahriman: Package, aur_package_ahriman: AURPackage, pac
"""
mocker.patch("ahriman.core.alpm.remote.AUR.info", return_value=aur_package_ahriman)
package = Package.from_aur(package_ahriman.base, pacman)
package = Package.from_aur(package_ahriman.base, pacman, package_ahriman.packager)
assert package_ahriman.base == package.base
assert package_ahriman.version == package.version
assert package_ahriman.packages.keys() == package.packages.keys()
assert package_ahriman.packager == package.packager
def test_from_build(package_ahriman: Package, mocker: MockerFixture, resource_path_root: Path) -> None:
@ -181,7 +182,7 @@ def test_from_build(package_ahriman: Package, mocker: MockerFixture, resource_pa
srcinfo = (resource_path_root / "models" / "package_ahriman_srcinfo").read_text()
mocker.patch("ahriman.models.package.Package._check_output", return_value=srcinfo)
package = Package.from_build(Path("path"), "x86_64")
package = Package.from_build(Path("path"), "x86_64", "packager")
assert package_ahriman.packages.keys() == package.packages.keys()
package_ahriman.packages = package.packages # we are not going to test PackageDescription here
package_ahriman.remote = package.remote
@ -195,7 +196,7 @@ def test_from_build_multiple_packages(mocker: MockerFixture, resource_path_root:
srcinfo = (resource_path_root / "models" / "package_gcc10_srcinfo").read_text()
mocker.patch("ahriman.models.package.Package._check_output", return_value=srcinfo)
package = Package.from_build(Path("path"), "x86_64")
package = Package.from_build(Path("path"), "x86_64", None)
assert package.packages == {
"gcc10": PackageDescription(
depends=["gcc10-libs=10.3.0-2", "binutils>=2.28", "libmpc", "zstd"],
@ -225,7 +226,7 @@ def test_from_build_architecture(mocker: MockerFixture, resource_path_root: Path
srcinfo = (resource_path_root / "models" / "package_jellyfin-ffmpeg5-bin_srcinfo").read_text()
mocker.patch("ahriman.models.package.Package._check_output", return_value=srcinfo)
package = Package.from_build(Path("path"), "x86_64")
package = Package.from_build(Path("path"), "x86_64", None)
assert package.packages == {
"jellyfin-ffmpeg5-bin": PackageDescription(
depends=["glibc"],
@ -254,7 +255,7 @@ def test_from_build_failed(package_ahriman: Package, mocker: MockerFixture) -> N
mocker.patch("ahriman.models.package.parse_srcinfo", return_value=({"packages": {}}, ["an error"]))
with pytest.raises(PackageInfoError):
Package.from_build(Path("path"), "x86_64")
Package.from_build(Path("path"), "x86_64", None)
def test_from_json_view_1(package_ahriman: Package) -> None:
@ -285,10 +286,11 @@ def test_from_official(package_ahriman: Package, aur_package_ahriman: AURPackage
"""
mocker.patch("ahriman.core.alpm.remote.Official.info", return_value=aur_package_ahriman)
package = Package.from_official(package_ahriman.base, pacman)
package = Package.from_official(package_ahriman.base, pacman, package_ahriman.packager)
assert package_ahriman.base == package.base
assert package_ahriman.version == package.version
assert package_ahriman.packages.keys() == package.packages.keys()
assert package_ahriman.packager == package.packager
def test_local_files(mocker: MockerFixture, resource_path_root: Path) -> None:

View File

@ -0,0 +1,12 @@
from ahriman.models.package import Package
from ahriman.models.packagers import Packagers
def test_for_base(package_ahriman: Package) -> None:
"""
must return username used for base package
"""
assert Packagers(None, {package_ahriman.base: "packager"}).for_base(package_ahriman.base) == "packager"
assert Packagers("default", {package_ahriman.base: "packager"}).for_base("random") == "default"
assert Packagers("default").for_base(package_ahriman.base) == "default"
assert Packagers().for_base(package_ahriman.base) is None

View File

@ -8,7 +8,7 @@ def test_from_option(user: User) -> None:
"""
must generate user from options
"""
user = replace(user, access=UserAccess.Read)
user = replace(user, access=UserAccess.Read, packager_id=None, key=None)
assert User.from_option(user.username, user.password) == user
# default is read access
user = replace(user, access=UserAccess.Full)

View File

@ -2,6 +2,7 @@ import pytest
from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from unittest.mock import AsyncMock
from ahriman.models.user_access import UserAccess
from ahriman.web.views.service.add import AddView
@ -21,13 +22,16 @@ async def test_post(client: TestClient, mocker: MockerFixture) -> None:
must call post request correctly
"""
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add")
user_mock = AsyncMock()
user_mock.return_value = "username"
mocker.patch("ahriman.web.views.base.BaseView.username", side_effect=user_mock)
request_schema = pytest.helpers.schema_request(AddView.post)
payload = {"packages": ["ahriman"]}
assert not request_schema.validate(payload)
response = await client.post("/api/v1/service/add", json=payload)
assert response.ok
add_mock.assert_called_once_with(["ahriman"], now=True)
add_mock.assert_called_once_with(["ahriman"], "username", now=True)
async def test_post_empty(client: TestClient, mocker: MockerFixture) -> None:

View File

@ -2,6 +2,7 @@ import pytest
from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from unittest.mock import AsyncMock
from ahriman.models.user_access import UserAccess
from ahriman.web.views.service.rebuild import RebuildView
@ -21,13 +22,16 @@ async def test_post(client: TestClient, mocker: MockerFixture) -> None:
must call post request correctly
"""
rebuild_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_rebuild")
user_mock = AsyncMock()
user_mock.return_value = "username"
mocker.patch("ahriman.web.views.base.BaseView.username", side_effect=user_mock)
request_schema = pytest.helpers.schema_request(RebuildView.post)
payload = {"packages": ["python", "ahriman"]}
assert not request_schema.validate(payload)
response = await client.post("/api/v1/service/rebuild", json=payload)
assert response.ok
rebuild_mock.assert_called_once_with("python")
rebuild_mock.assert_called_once_with("python", "username")
async def test_post_exception(client: TestClient, mocker: MockerFixture) -> None:

View File

@ -2,6 +2,7 @@ import pytest
from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from unittest.mock import AsyncMock
from ahriman.models.user_access import UserAccess
from ahriman.web.views.service.request import RequestView
@ -21,13 +22,16 @@ async def test_post(client: TestClient, mocker: MockerFixture) -> None:
must call post request correctly
"""
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add")
user_mock = AsyncMock()
user_mock.return_value = "username"
mocker.patch("ahriman.web.views.base.BaseView.username", side_effect=user_mock)
request_schema = pytest.helpers.schema_request(RequestView.post)
payload = {"packages": ["ahriman"]}
assert not request_schema.validate(payload)
response = await client.post("/api/v1/service/request", json=payload)
assert response.ok
add_mock.assert_called_once_with(["ahriman"], now=False)
add_mock.assert_called_once_with(["ahriman"], "username", now=False)
async def test_post_exception(client: TestClient, mocker: MockerFixture) -> None:

View File

@ -1,5 +1,6 @@
from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from unittest.mock import AsyncMock
async def test_post_update(client: TestClient, mocker: MockerFixture) -> None:
@ -7,7 +8,10 @@ async def test_post_update(client: TestClient, mocker: MockerFixture) -> None:
must call post request correctly for alias
"""
update_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_update")
user_mock = AsyncMock()
user_mock.return_value = "username"
mocker.patch("ahriman.web.views.base.BaseView.username", side_effect=user_mock)
response = await client.post("/api/v1/service/update")
assert response.ok
update_mock.assert_called_once_with()
update_mock.assert_called_once_with("username")

View File

@ -2,6 +2,8 @@ import pytest
from multidict import MultiDict
from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from unittest.mock import AsyncMock
from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView
@ -146,3 +148,22 @@ async def test_head_not_allowed(client: TestClient) -> None:
"""
response = await client.head("/api/v1/service/add")
assert response.status == 405
async def test_username(base: BaseView, mocker: MockerFixture) -> None:
"""
must return identity of logged-in user
"""
policy = AsyncMock()
policy.identify.return_value = "identity"
mocker.patch("aiohttp.web.Application.get", return_value=policy)
assert await base.username() == "identity"
policy.identify.assert_called_once_with(base.request)
async def test_username_no_auth(base: BaseView) -> None:
"""
must return None in case if auth is disabled
"""
assert await base.username() is None