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

* add external process spawner and update test cases

* pass no_report to handlers

* provide service api endpoints

* do not spawn process for single architecture run

* pass no report to handlers

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

* move update under add

* implement actions from web page

* clear logging & improve l&f
This commit is contained in:
Evgenii Alekseev 2021-09-10 00:33:35 +03:00 committed by GitHub
parent 214f319123
commit 11c03a9041
101 changed files with 1417 additions and 295 deletions

View File

@ -2,7 +2,7 @@
keys = root,builder,build_details,http keys = root,builder,build_details,http
[handlers] [handlers]
keys = console_handler,build_file_handler,file_handler,http_handler,syslog_handler keys = console_handler,syslog_handler
[formatters] [formatters]
keys = generic_format,syslog_format keys = generic_format,syslog_format
@ -13,24 +13,6 @@ level = DEBUG
formatter = generic_format formatter = generic_format
args = (sys.stderr,) args = (sys.stderr,)
[handler_file_handler]
class = logging.handlers.RotatingFileHandler
level = DEBUG
formatter = generic_format
args = ("/var/log/ahriman/ahriman.log", "a", 20971520, 20)
[handler_build_file_handler]
class = logging.handlers.RotatingFileHandler
level = DEBUG
formatter = generic_format
args = ("/var/log/ahriman/build.log", "a", 20971520, 20)
[handler_http_handler]
class = logging.handlers.RotatingFileHandler
level = DEBUG
formatter = generic_format
args = ("/var/log/ahriman/http.log", "a", 20971520, 20)
[handler_syslog_handler] [handler_syslog_handler]
class = logging.handlers.SysLogHandler class = logging.handlers.SysLogHandler
level = DEBUG level = DEBUG

View File

@ -5,11 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://kit.fontawesome.com/0d6d6d5226.js" crossorigin="anonymous"></script> {% include "utils/style.jinja2" %}
<link href="https://unpkg.com/bootstrap-table@1.18.3/dist/bootstrap-table.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous">
{% include "style.jinja2" %}
</head> </head>
<body> <body>
@ -26,20 +22,46 @@
</div> </div>
<div class="container"> <div class="container">
<table id="packages" class="table table-striped table-hover" cellspacing="0" <div id="toolbar">
data-toggle="table" {% if not auth_enabled or auth_username is not none %}
data-pagination="true" <button id="add" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addForm">
data-page-siz="10" <i class="fa fa-plus"></i> Add
</button>
<button id="update" class="btn btn-secondary" onclick="updatePackages()" disabled>
<i class="fa fa-play"></i> Update
</button>
<button id="remove" class="btn btn-danger" onclick="removePackages()" disabled>
<i class="fa fa-trash"></i> Remove
</button>
{% endif %}
</div>
<table id="packages" class="table table-striped table-hover"
data-click-to-select="true"
data-export-options='{"fileName": "packages"}'
data-page-list="[10, 25, 50, 100, all]" data-page-list="[10, 25, 50, 100, all]"
data-page-size="10"
data-pagination="true"
data-resizable="true"
data-search="true" data-search="true"
data-show-columns="true" data-show-columns="true"
data-show-columns-search="true"
data-show-columns-toggle-all="true"
data-show-export="true" data-show-export="true"
data-sortable="true"> data-show-fullscreen="true"
data-show-search-clear-button="true"
data-sortable="true"
data-sort-reset="true"
data-toggle="table"
data-toolbar="#toolbar">
<thead class="table-primary"> <thead class="table-primary">
<tr> <tr>
<th data-sortable="true">package base</th> <th data-checkbox="true"></th>
<th data-sortable="true">packages</th> <th data-sortable="true" data-switchable="false">package base</th>
<th data-sortable="true">version</th> <th data-sortable="true">version</th>
<th data-sortable="true">packages</th>
<th data-sortable="true" data-visible="false">groups</th>
<th data-sortable="true" data-visible="false">licenses</th>
<th data-sortable="true">last update</th> <th data-sortable="true">last update</th>
<th data-sortable="true">status</th> <th data-sortable="true">status</th>
</tr> </tr>
@ -48,10 +70,13 @@
<tbody> <tbody>
{% if authorized %} {% if authorized %}
{% for package in packages %} {% for package in packages %}
<tr> <tr data-package-base="{{ package.base }}">
<td data-checkbox="true"></td>
<td><a href="{{ package.web_url }}" title="{{ package.base }}">{{ package.base }}</a></td> <td><a href="{{ package.web_url }}" title="{{ package.base }}">{{ package.base }}</a></td>
<td>{{ package.packages|join("<br>"|safe) }}</td>
<td>{{ package.version }}</td> <td>{{ package.version }}</td>
<td>{{ package.packages|join("<br>"|safe) }}</td>
<td>{{ package.groups|join("<br>"|safe) }}</td>
<td>{{ package.licenses|join("<br>"|safe) }}</td>
<td>{{ package.timestamp }}</td> <td>{{ package.timestamp }}</td>
<td class="table-{{ package.status_color }}">{{ package.status }}</td> <td class="table-{{ package.status_color }}">{{ package.status }}</td>
</tr> </tr>
@ -77,45 +102,23 @@
{% if auth_username is none %} {% if auth_username is none %}
<button type="button" class="btn btn-link" data-bs-toggle="modal" data-bs-target="#loginForm" style="text-decoration: none">login</button> <button type="button" class="btn btn-link" data-bs-toggle="modal" data-bs-target="#loginForm" style="text-decoration: none">login</button>
{% else %} {% else %}
<form action="/logout" method="post"> <form action="/user-api/v1/logout" method="post">
<button type="submit" class="btn btn-link" style="text-decoration: none">logout ({{ auth_username }})</button> <button class="btn btn-link" style="text-decoration: none">logout ({{ auth_username }})</button>
</form> </form>
{% endif %} {% endif %}
{% endif %} {% endif %}
</footer> </footer>
</div> </div>
<div id="loginForm" tabindex="-1" role="dialog" class="modal fade"> {% if auth_enabled %}
<div class="modal-dialog modal-login"> {% include "build-status/login-modal.jinja2" %}
<div class="modal-content"> {% endif %}
<form action="/login" method="post">
<div class="modal-header">
<h4 class="modal-title">login</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="username" class="col-form-label">username</label>
<input id="username" type="text" class="form-control" placeholder="enter username" name="username" required>
</div>
<div class="form-group">
<label for="password" class="col-form-label">password</label>
<input id="password" type="password" class="form-control" placeholder="enter username" name="password" required>
</div>
</div>
<div class="modal-footer">
<input type="submit" class="btn btn-primary" value="Login">
</div>
</form>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script> {% include "build-status/package-actions-modals.jinja2" %}
<script src="https://unpkg.com/tableexport.jquery.plugin/tableExport.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-U1DAWAznBHeqEIlVSCgzq+c9gqGAJn5c/t99JyeKa9xxaYpSvHU5awsuZVVFIhvj" crossorigin="anonymous"></script> {% include "utils/bootstrap-scripts.jinja2" %}
<script src="https://unpkg.com/bootstrap-table@1.18.3/dist/bootstrap-table.min.js"></script>
<script src="https://unpkg.com/bootstrap-table@1.18.3/dist/extensions/export/bootstrap-table-export.min.js"></script> {% include "build-status/package-actions-script.jinja2" %}
</body> </body>

View File

@ -0,0 +1,29 @@
<div id="loginForm" tabindex="-1" role="dialog" class="modal fade">
<div class="modal-dialog" role="document">
<div class="modal-content">
<form action="/user-api/v1/login" method="post">
<div class="modal-header">
<h4 class="modal-title">Login</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
</div>
<div class="modal-body">
<div class="form-group row">
<label for="username" class="col-sm-2 col-form-label">Username</label>
<div class="col-sm-10">
<input id="username" type="text" class="form-control" placeholder="enter username" name="username" required>
</div>
</div>
<div class="form-group row">
<label for="password" class="col-sm-2 col-form-label">Password</label>
<div class="col-sm-10">
<input id="password" type="password" class="form-control" placeholder="enter password" name="password" required>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary">Login</button>
</div>
</form>
</div>
</div>
</div>

View File

@ -0,0 +1,57 @@
<div id="addForm" tabindex="-1" role="dialog" class="modal fade">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Add new packages</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
</div>
<div class="modal-body">
<div class="form-group row">
<label for="package" class="col-sm-2 col-form-label">Package</label>
<div class="col-sm-10">
<input id="package" type="text" list="knownPackages" class="form-control" placeholder="AUR package" name="package" required>
<datalist id="knownPackages"></datalist>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="addPackages()">Add</button>
</div>
</div>
</div>
</div>
<div id="failedForm" tabindex="-1" role="dialog" class="modal fade">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header bg-danger">
<h4 class="modal-title">Failed</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
</div>
<div class="modal-body">
<p>Packages update has failed.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div id="successForm" tabindex="-1" role="dialog" class="modal fade">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header bg-success">
<h4 class="modal-title">Success</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
</div>
<div class="modal-body">
<p>Packages update has been run.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,76 @@
<script>
const $remove = $("#remove")
const $update = $("#update")
const $table = $("#packages")
$table.on("check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table",
function () {
$remove.prop("disabled", !$table.bootstrapTable("getSelections").length)
$update.prop("disabled", !$table.bootstrapTable("getSelections").length)
})
const $successForm = $("#successForm")
$successForm.on("hidden.bs.modal", function() { window.location.reload(); })
const $failedForm = $("#failedForm")
$failedForm.on("hidden.bs.modal", function() { window.location.reload(); })
const $package = $("#package")
const $knownPackages = $("#knownPackages")
$package.keyup(function () {
const $this = $(this);
clearTimeout($this.data("timeout"));
$this.data("timeout", setTimeout($.proxy(function () {
const $value = $package.val();
$.ajax({
url: "/service-api/v1/search",
data: {"for": $value},
type: "GET",
dataType: "json",
success: function (resp) {
const $options = resp.map(function (pkg) {
const $option = document.createElement("option");
$option.value = pkg;
return $option;
});
$knownPackages.empty().append($options);
$this.focus()
},
})
}, this), 500));
})
function doPackageAction($uri, $packages) {
if ($packages.length === 0)
return;
$.ajax({
url: $uri,
data: JSON.stringify({packages: $packages}),
type: "POST",
contentType: "application/json",
success: function (_) { $successForm.modal("show"); },
error: function (_) { $failedForm.modal("show"); },
})
}
function getSelection() {
return $.map($table.bootstrapTable("getSelections"), function(row) {
return row._data["package-base"];
})
}
function addPackages() {
const $packages = [$package.val()]
doPackageAction("/service-api/v1/add", $packages);
}
function removePackages() { doPackageAction("/service-api/v1/remove", getSelection()); }
function updatePackages() { doPackageAction("/service-api/v1/add", getSelection()); }
$(function () {
$table.bootstrapTable("uncheckAll");
})
</script>

View File

@ -6,15 +6,13 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous"> {% include "utils/style.jinja2" %}
{% include "style.jinja2" %}
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<table id="packages" class="table table-striped" cellspacing="0"> <table id="packages" class="table table-striped">
<thead class="table-primary"> <thead class="table-primary">
<tr> <tr>
<th>package</th> <th>package</th>

View File

@ -5,11 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://kit.fontawesome.com/0d6d6d5226.js" crossorigin="anonymous"></script> {% include "utils/style.jinja2" %}
<link href="https://unpkg.com/bootstrap-table@1.18.3/dist/bootstrap-table.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous">
{% include "style.jinja2" %}
</head> </head>
<body> <body>
@ -30,19 +26,32 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
</div> </div>
<div class="container"> <div class="container">
<table id="packages" class="table table-striped table-hover" cellspacing="0" <table id="packages" class="table table-striped table-hover"
data-toggle="table" data-export-options='{"fileName": "packages"}'
data-pagination="true"
data-page-siz="10"
data-page-list="[10, 25, 50, 100, all]" data-page-list="[10, 25, 50, 100, all]"
data-page-size="10"
data-pagination="true"
data-resizable="true"
data-search="true" data-search="true"
data-show-columns="true" data-show-columns="true"
data-show-columns-search="true"
data-show-columns-toggle-all="true"
data-show-export="true" data-show-export="true"
data-sortable="true"> data-show-fullscreen="true"
data-show-search-clear-button="true"
data-sortable="true"
data-sort-reset="true"
data-toggle="table">
<thead class="table-primary"> <thead class="table-primary">
<tr> <tr>
<th data-sortable="true">package</th> <th data-sortable="true" data-switchable="false">package</th>
<th data-sortable="true">version</th> <th data-sortable="true">version</th>
<th data-sortable="true" data-visible="false">architecture</th>
<th data-sortable="true" data-visible="false">description</th>
<th data-sortable="true" data-visible="false">upstream url</th>
<th data-sortable="true" data-visible="false">licenses</th>
<th data-sortable="true" data-visible="false">groups</th>
<th data-sortable="true" data-visible="false">depends</th>
<th data-sortable="true">archive size</th> <th data-sortable="true">archive size</th>
<th data-sortable="true">installed size</th> <th data-sortable="true">installed size</th>
<th data-sortable="true">build date</th> <th data-sortable="true">build date</th>
@ -54,6 +63,12 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
<tr> <tr>
<td><a href="{{ link_path }}/{{ package.filename }}" title="{{ package.name }}">{{ package.name }}</a></td> <td><a href="{{ link_path }}/{{ package.filename }}" title="{{ package.name }}">{{ package.name }}</a></td>
<td>{{ package.version }}</td> <td>{{ package.version }}</td>
<td>{{ package.architecture }}</td>
<td>{{ package.description }}</td>
<td><a href="{{ package.url }}" title="{{ package.name }} upstream url">{{ package.url }}</a></td>
<td>{{ package.licenses|join("<br>"|safe) }}</td>
<td>{{ package.groups|join("<br>"|safe) }}</td>
<td>{{ package.depends|join("<br>"|safe) }}</td>
<td>{{ package.archive_size }}</td> <td>{{ package.archive_size }}</td>
<td>{{ package.installed_size }}</td> <td>{{ package.installed_size }}</td>
<td>{{ package.build_date }}</td> <td>{{ package.build_date }}</td>
@ -73,11 +88,7 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
</footer> </footer>
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script> {% include "utils/bootstrap-scripts.jinja2" %}
<script src="https://unpkg.com/tableexport.jquery.plugin/tableExport.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-U1DAWAznBHeqEIlVSCgzq+c9gqGAJn5c/t99JyeKa9xxaYpSvHU5awsuZVVFIhvj" crossorigin="anonymous"></script>
<script src="https://unpkg.com/bootstrap-table@1.18.3/dist/bootstrap-table.min.js"></script>
<script src="https://unpkg.com/bootstrap-table@1.18.3/dist/extensions/export/bootstrap-table-export.min.js"></script>
</body> </body>

View File

@ -1 +0,0 @@
<style></style>

View File

@ -0,0 +1,12 @@
<script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script>
<script src="https://unpkg.com/tableexport.jquery.plugin/tableExport.min.js"></script>
<script src="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-U1DAWAznBHeqEIlVSCgzq+c9gqGAJn5c/t99JyeKa9xxaYpSvHU5awsuZVVFIhvj" crossorigin="anonymous"></script>
<script src="https://unpkg.com/bootstrap-table@1.18.3/dist/bootstrap-table.min.js"></script>
<script src="https://unpkg.com/bootstrap-table@1.18.3/dist/extensions/export/bootstrap-table-export.min.js"></script>
<script src="https://unpkg.com/bootstrap-table@1.18.3/dist/extensions/resizable/bootstrap-table-resizable.js"></script>

View File

@ -0,0 +1,9 @@
<script src="https://kit.fontawesome.com/0d6d6d5226.js" crossorigin="anonymous"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous">
<link href="https://unpkg.com/bootstrap-table@1.18.3/dist/bootstrap-table.min.css" rel="stylesheet">
<link href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" rel="stylesheet">
<style>
</style>

View File

@ -68,7 +68,15 @@ setup(
"package/share/ahriman/build-status.jinja2", "package/share/ahriman/build-status.jinja2",
"package/share/ahriman/email-index.jinja2", "package/share/ahriman/email-index.jinja2",
"package/share/ahriman/repo-index.jinja2", "package/share/ahriman/repo-index.jinja2",
"package/share/ahriman/style.jinja2", ]),
("share/ahriman/build-status", [
"package/share/ahriman/build-status/login-modal.jinja2",
"package/share/ahriman/build-status/package-actions-modals.jinja2",
"package/share/ahriman/build-status/package-actions-script.jinja2",
]),
("share/ahriman/utils", [
"package/share/ahriman/utils/bootstrap-scripts.jinja2",
"package/share/ahriman/utils/style.jinja2",
]), ]),
], ],

View File

@ -27,11 +27,10 @@ from ahriman import version
from ahriman.application import handlers from ahriman.application import handlers
from ahriman.models.build_status import BuildStatusEnum from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.sign_settings import SignSettings from ahriman.models.sign_settings import SignSettings
from ahriman.models.user_access import UserAccess
# pylint thinks it is bad idea, but get the fuck off # pylint thinks it is bad idea, but get the fuck off
from ahriman.models.user_access import UserAccess
SubParserAction = argparse._SubParsersAction # pylint: disable=protected-access SubParserAction = argparse._SubParsersAction # pylint: disable=protected-access
@ -367,7 +366,7 @@ def _set_web_parser(root: SubParserAction) -> argparse.ArgumentParser:
""" """
parser = root.add_parser("web", help="start web server", description="start web server", parser = root.add_parser("web", help="start web server", description="start web server",
formatter_class=argparse.ArgumentDefaultsHelpFormatter) formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.set_defaults(handler=handlers.Web, lock=None, no_report=True) parser.set_defaults(handler=handlers.Web, lock=None, no_report=True, parser=_parser)
return parser return parser

View File

@ -40,16 +40,17 @@ class Application:
:ivar repository: repository instance :ivar repository: repository instance
""" """
def __init__(self, architecture: str, configuration: Configuration) -> None: def __init__(self, architecture: str, configuration: Configuration, no_report: bool) -> None:
""" """
default constructor default constructor
:param architecture: repository architecture :param architecture: repository architecture
:param configuration: configuration instance :param configuration: configuration instance
:param no_report: force disable reporting
""" """
self.logger = logging.getLogger("root") self.logger = logging.getLogger("root")
self.configuration = configuration self.configuration = configuration
self.architecture = architecture self.architecture = architecture
self.repository = Repository(architecture, configuration) self.repository = Repository(architecture, configuration, no_report)
def _finalize(self, built_packages: Iterable[Package]) -> None: def _finalize(self, built_packages: Iterable[Package]) -> None:
""" """

View File

@ -32,14 +32,16 @@ class Add(Handler):
""" """
@classmethod @classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None: def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
""" """
callback for command line callback for command line
:param args: command line args :param args: command line args
:param architecture: repository architecture :param architecture: repository architecture
:param configuration: configuration instance :param configuration: configuration instance
:param no_report: force disable reporting
""" """
application = Application(architecture, configuration) application = Application(architecture, configuration, no_report)
application.add(args.package, args.without_dependencies) application.add(args.package, args.without_dependencies)
if not args.now: if not args.now:
return return

View File

@ -32,12 +32,14 @@ class Clean(Handler):
""" """
@classmethod @classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None: def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
""" """
callback for command line callback for command line
:param args: command line args :param args: command line args
:param architecture: repository architecture :param architecture: repository architecture
:param configuration: configuration instance :param configuration: configuration instance
:param no_report: force disable reporting
""" """
Application(architecture, configuration).clean(args.no_build, args.no_cache, args.no_chroot, Application(architecture, configuration, no_report).clean(args.no_build, args.no_cache, args.no_chroot,
args.no_manual, args.no_packages) args.no_manual, args.no_packages)

View File

@ -34,12 +34,14 @@ class CreateUser(Handler):
""" """
@classmethod @classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None: def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
""" """
callback for command line callback for command line
:param args: command line args :param args: command line args
:param architecture: repository architecture :param architecture: repository architecture
:param configuration: configuration instance :param configuration: configuration instance
:param no_report: force disable reporting
""" """
salt = CreateUser.get_salt(configuration) salt = CreateUser.get_salt(configuration)
user = CreateUser.create_user(args) user = CreateUser.create_user(args)

View File

@ -33,12 +33,14 @@ class Dump(Handler):
_print = print _print = print
@classmethod @classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None: def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
""" """
callback for command line callback for command line
:param args: command line args :param args: command line args
:param architecture: repository architecture :param architecture: repository architecture
:param configuration: configuration instance :param configuration: configuration instance
:param no_report: force disable reporting
""" """
dump = configuration.dump() dump = configuration.dump()
for section, values in sorted(dump.items()): for section, values in sorted(dump.items()):

View File

@ -27,17 +27,20 @@ from typing import Set, Type
from ahriman.application.lock import Lock from ahriman.application.lock import Lock
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import MissingArchitecture from ahriman.core.exceptions import MissingArchitecture, MultipleArchitecture
from ahriman.models.repository_paths import RepositoryPaths from ahriman.models.repository_paths import RepositoryPaths
class Handler: class Handler:
""" """
base handler class for command callbacks base handler class for command callbacks
:cvar ALLOW_MULTI_ARCHITECTURE_RUN: allow to run with multiple architectures
""" """
ALLOW_MULTI_ARCHITECTURE_RUN = True
@classmethod @classmethod
def _call(cls: Type[Handler], args: argparse.Namespace, architecture: str) -> bool: def call(cls: Type[Handler], args: argparse.Namespace, architecture: str) -> bool:
""" """
additional function to wrap all calls for multiprocessing library additional function to wrap all calls for multiprocessing library
:param args: command line args :param args: command line args
@ -47,7 +50,7 @@ class Handler:
try: try:
configuration = Configuration.from_path(args.configuration, architecture, not args.no_log) configuration = Configuration.from_path(args.configuration, architecture, not args.no_log)
with Lock(args, architecture, configuration): with Lock(args, architecture, configuration):
cls.run(args, architecture, configuration) cls.run(args, architecture, configuration, args.no_report)
return True return True
except Exception: except Exception:
logging.getLogger("root").exception("process exception") logging.getLogger("root").exception("process exception")
@ -61,9 +64,18 @@ class Handler:
:return: 0 on success, 1 otherwise :return: 0 on success, 1 otherwise
""" """
architectures = cls.extract_architectures(args) architectures = cls.extract_architectures(args)
with Pool(len(architectures)) as pool:
result = pool.starmap( # actually we do not have to spawn another process if it is single-process application, do we?
cls._call, [(args, architecture) for architecture in architectures]) if len(architectures) > 1:
if not cls.ALLOW_MULTI_ARCHITECTURE_RUN:
raise MultipleArchitecture(args.command)
with Pool(len(architectures)) as pool:
result = pool.starmap(
cls.call, [(args, architecture) for architecture in architectures])
else:
result = [cls.call(args, architectures.pop())]
return 0 if all(result) else 1 return 0 if all(result) else 1
@classmethod @classmethod
@ -88,11 +100,13 @@ class Handler:
return architectures return architectures
@classmethod @classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None: def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
""" """
callback for command line callback for command line
:param args: command line args :param args: command line args
:param architecture: repository architecture :param architecture: repository architecture
:param configuration: configuration instance :param configuration: configuration instance
:param no_report: force disable reporting
""" """
raise NotImplementedError raise NotImplementedError

View File

@ -32,11 +32,13 @@ class Init(Handler):
""" """
@classmethod @classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None: def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
""" """
callback for command line callback for command line
:param args: command line args :param args: command line args
:param architecture: repository architecture :param architecture: repository architecture
:param configuration: configuration instance :param configuration: configuration instance
:param no_report: force disable reporting
""" """
Application(architecture, configuration).repository.repo.init() Application(architecture, configuration, no_report).repository.repo.init()

View File

@ -32,11 +32,13 @@ class KeyImport(Handler):
""" """
@classmethod @classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None: def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
""" """
callback for command line callback for command line
:param args: command line args :param args: command line args
:param architecture: repository architecture :param architecture: repository architecture
:param configuration: configuration instance :param configuration: configuration instance
:param no_report: force disable reporting
""" """
Application(architecture, configuration).repository.sign.import_key(args.key_server, args.key) Application(architecture, configuration, no_report).repository.sign.import_key(args.key_server, args.key)

View File

@ -32,16 +32,18 @@ class Rebuild(Handler):
""" """
@classmethod @classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None: def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
""" """
callback for command line callback for command line
:param args: command line args :param args: command line args
:param architecture: repository architecture :param architecture: repository architecture
:param configuration: configuration instance :param configuration: configuration instance
:param no_report: force disable reporting
""" """
depends_on = set(args.depends_on) if args.depends_on else None depends_on = set(args.depends_on) if args.depends_on else None
application = Application(architecture, configuration) application = Application(architecture, configuration, no_report)
packages = [ packages = [
package package
for package in application.repository.packages() for package in application.repository.packages()

View File

@ -32,11 +32,13 @@ class Remove(Handler):
""" """
@classmethod @classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None: def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
""" """
callback for command line callback for command line
:param args: command line args :param args: command line args
:param architecture: repository architecture :param architecture: repository architecture
:param configuration: configuration instance :param configuration: configuration instance
:param no_report: force disable reporting
""" """
Application(architecture, configuration).remove(args.package) Application(architecture, configuration, no_report).remove(args.package)

View File

@ -33,14 +33,16 @@ class RemoveUnknown(Handler):
""" """
@classmethod @classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None: def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
""" """
callback for command line callback for command line
:param args: command line args :param args: command line args
:param architecture: repository architecture :param architecture: repository architecture
:param configuration: configuration instance :param configuration: configuration instance
:param no_report: force disable reporting
""" """
application = Application(architecture, configuration) application = Application(architecture, configuration, no_report)
unknown_packages = application.unknown() unknown_packages = application.unknown()
if args.dry_run: if args.dry_run:
for package in unknown_packages: for package in unknown_packages:

View File

@ -32,11 +32,13 @@ class Report(Handler):
""" """
@classmethod @classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None: def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
""" """
callback for command line callback for command line
:param args: command line args :param args: command line args
:param architecture: repository architecture :param architecture: repository architecture
:param configuration: configuration instance :param configuration: configuration instance
:param no_report: force disable reporting
""" """
Application(architecture, configuration).report(args.target, []) Application(architecture, configuration, no_report).report(args.target, [])

View File

@ -32,12 +32,14 @@ class Search(Handler):
""" """
@classmethod @classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None: def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
""" """
callback for command line callback for command line
:param args: command line args :param args: command line args
:param architecture: repository architecture :param architecture: repository architecture
:param configuration: configuration instance :param configuration: configuration instance
:param no_report: force disable reporting
""" """
search = " ".join(args.search) search = " ".join(args.search)
packages = aur.search(search) packages = aur.search(search)

View File

@ -43,14 +43,16 @@ class Setup(Handler):
SUDOERS_PATH = Path("/etc/sudoers.d/ahriman") SUDOERS_PATH = Path("/etc/sudoers.d/ahriman")
@classmethod @classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None: def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
""" """
callback for command line callback for command line
:param args: command line args :param args: command line args
:param architecture: repository architecture :param architecture: repository architecture
:param configuration: configuration instance :param configuration: configuration instance
:param no_report: force disable reporting
""" """
application = Application(architecture, configuration) application = Application(architecture, configuration, no_report)
Setup.create_makepkg_configuration(args.packager, application.repository.paths) Setup.create_makepkg_configuration(args.packager, application.repository.paths)
Setup.create_executable(args.build_command, architecture) Setup.create_executable(args.build_command, architecture)
Setup.create_devtools_configuration(args.build_command, architecture, args.from_configuration, Setup.create_devtools_configuration(args.build_command, architecture, args.from_configuration,

View File

@ -32,11 +32,13 @@ class Sign(Handler):
""" """
@classmethod @classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None: def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
""" """
callback for command line callback for command line
:param args: command line args :param args: command line args
:param architecture: repository architecture :param architecture: repository architecture
:param configuration: configuration instance :param configuration: configuration instance
:param no_report: force disable reporting
""" """
Application(architecture, configuration).sign(args.package) Application(architecture, configuration, no_report).sign(args.package)

View File

@ -34,24 +34,27 @@ class Status(Handler):
""" """
@classmethod @classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None: def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
""" """
callback for command line callback for command line
:param args: command line args :param args: command line args
:param architecture: repository architecture :param architecture: repository architecture
:param configuration: configuration instance :param configuration: configuration instance
:param no_report: force disable reporting
""" """
application = Application(architecture, configuration) # we are using reporter here
client = Application(architecture, configuration, no_report=False).repository.reporter
if args.ahriman: if args.ahriman:
ahriman = application.repository.reporter.get_self() ahriman = client.get_self()
print(ahriman.pretty_print()) print(ahriman.pretty_print())
print() print()
if args.package: if args.package:
packages: Iterable[Tuple[Package, BuildStatus]] = sum( packages: Iterable[Tuple[Package, BuildStatus]] = sum(
[application.repository.reporter.get(base) for base in args.package], [client.get(base) for base in args.package],
start=[]) start=[])
else: else:
packages = application.repository.reporter.get(None) packages = client.get(None)
for package, package_status in sorted(packages, key=lambda item: item[0].base): for package, package_status in sorted(packages, key=lambda item: item[0].base):
print(package.pretty_print()) print(package.pretty_print())
print(f"\t{package.version}") print(f"\t{package.version}")

View File

@ -32,14 +32,17 @@ class StatusUpdate(Handler):
""" """
@classmethod @classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None: def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
""" """
callback for command line callback for command line
:param args: command line args :param args: command line args
:param architecture: repository architecture :param architecture: repository architecture
:param configuration: configuration instance :param configuration: configuration instance
:param no_report: force disable reporting
""" """
client = Application(architecture, configuration).repository.reporter # we are using reporter here
client = Application(architecture, configuration, no_report=False).repository.reporter
callback: Callable[[str], None] = lambda p: client.remove(p) if args.remove else client.update(p, args.status) callback: Callable[[str], None] = lambda p: client.remove(p) if args.remove else client.update(p, args.status)
if args.package: if args.package:
# update packages statuses # update packages statuses

View File

@ -32,11 +32,13 @@ class Sync(Handler):
""" """
@classmethod @classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None: def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
""" """
callback for command line callback for command line
:param args: command line args :param args: command line args
:param architecture: repository architecture :param architecture: repository architecture
:param configuration: configuration instance :param configuration: configuration instance
:param no_report: force disable reporting
""" """
Application(architecture, configuration).sync(args.target, []) Application(architecture, configuration, no_report).sync(args.target, [])

View File

@ -32,14 +32,16 @@ class Update(Handler):
""" """
@classmethod @classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None: def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
""" """
callback for command line callback for command line
:param args: command line args :param args: command line args
:param architecture: repository architecture :param architecture: repository architecture
:param configuration: configuration instance :param configuration: configuration instance
:param no_report: force disable reporting
""" """
application = Application(architecture, configuration) application = Application(architecture, configuration, no_report)
packages = application.get_updates(args.package, args.no_aur, args.no_manual, args.no_vcs, packages = application.get_updates(args.package, args.no_aur, args.no_manual, args.no_vcs,
Update.log_fn(application, args.dry_run)) Update.log_fn(application, args.dry_run))
if args.dry_run: if args.dry_run:

View File

@ -23,6 +23,7 @@ from typing import Type
from ahriman.application.handlers.handler import Handler from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.spawn import Spawn
class Web(Handler): class Web(Handler):
@ -30,14 +31,23 @@ class Web(Handler):
web server handler web server handler
""" """
ALLOW_MULTI_ARCHITECTURE_RUN = False # required to be able to spawn external processes
@classmethod @classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None: def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
""" """
callback for command line callback for command line
:param args: command line args :param args: command line args
:param architecture: repository architecture :param architecture: repository architecture
:param configuration: configuration instance :param configuration: configuration instance
:param no_report: force disable reporting
""" """
# we are using local import for optional dependencies
from ahriman.web.web import run_server, setup_service from ahriman.web.web import run_server, setup_service
application = setup_service(architecture, configuration)
spawner = Spawn(args.parser(), architecture, configuration)
spawner.start()
application = setup_service(architecture, configuration, spawner)
run_server(application) run_server(application)

View File

@ -19,7 +19,7 @@
# #
from __future__ import annotations from __future__ import annotations
from typing import Optional, Set, Type from typing import Optional, Type
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.models.auth_settings import AuthSettings from ahriman.models.auth_settings import AuthSettings
@ -36,8 +36,8 @@ class Auth:
:cvar ALLOWED_PATHS_GROUPS: URI paths prefixes which can be accessed without authorization, predefined :cvar ALLOWED_PATHS_GROUPS: URI paths prefixes which can be accessed without authorization, predefined
""" """
ALLOWED_PATHS = {"/", "/favicon.ico", "/index.html", "/login", "/logout"} ALLOWED_PATHS = {"/", "/favicon.ico", "/index.html"}
ALLOWED_PATHS_GROUPS: Set[str] = set() ALLOWED_PATHS_GROUPS = {"/user-api"}
def __init__(self, configuration: Configuration, provider: AuthSettings = AuthSettings.Disabled) -> None: def __init__(self, configuration: Configuration, provider: AuthSettings = AuthSettings.Disabled) -> None:
""" """

View File

@ -109,6 +109,19 @@ class MissingArchitecture(Exception):
Exception.__init__(self, f"Architecture required for subcommand {command}, but missing") Exception.__init__(self, f"Architecture required for subcommand {command}, but missing")
class MultipleArchitecture(Exception):
"""
exception which will be raised if multiple architectures are not supported by the handler
"""
def __init__(self, command: str) -> None:
"""
default constructor
:param command: command name which throws exception
"""
Exception.__init__(self, f"Multiple architectures are not supported by subcommand {command}")
class ReportFailed(Exception): class ReportFailed(Exception):
""" """
report generation exception report generation exception

View File

@ -43,7 +43,13 @@ class Properties:
:ivar sign: GPG wrapper instance :ivar sign: GPG wrapper instance
""" """
def __init__(self, architecture: str, configuration: Configuration) -> None: def __init__(self, architecture: str, configuration: Configuration, no_report: bool) -> None:
"""
default constructor
:param architecture: repository architecture
:param configuration: configuration instance
:param no_report: force disable reporting
"""
self.logger = logging.getLogger("builder") self.logger = logging.getLogger("builder")
self.architecture = architecture self.architecture = architecture
self.configuration = configuration self.configuration = configuration
@ -58,4 +64,4 @@ class Properties:
self.pacman = Pacman(configuration) self.pacman = Pacman(configuration)
self.sign = GPG(architecture, configuration) self.sign = GPG(architecture, configuration)
self.repo = Repo(self.name, self.paths, self.sign.repository_sign_args) self.repo = Repo(self.name, self.paths, self.sign.repository_sign_args)
self.reporter = Client.load(configuration) self.reporter = Client() if no_report else Client.load(configuration)

137
src/ahriman/core/spawn.py Normal file
View File

@ -0,0 +1,137 @@
#
# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from __future__ import annotations
import argparse
import logging
import uuid
from multiprocessing import Process, Queue
from threading import Lock, Thread
from typing import Callable, Dict, Iterable, Tuple
from ahriman.core.configuration import Configuration
class Spawn(Thread):
"""
helper to spawn external ahriman process
MUST NOT be used directly, the only one usage allowed is to spawn process from web services
:ivar active: map of active child processes required to avoid zombies
:ivar architecture: repository architecture
:ivar configuration: configuration instance
:ivar logger: spawner logger
:ivar queue: multiprocessing queue to read updates from processes
"""
def __init__(self, args_parser: argparse.ArgumentParser, architecture: str, configuration: Configuration) -> None:
"""
default constructor
:param args_parser: command line parser for the application
:param architecture: repository architecture
:param configuration: configuration instance
"""
Thread.__init__(self, name="spawn")
self.architecture = architecture
self.args_parser = args_parser
self.configuration = configuration
self.logger = logging.getLogger("http")
self.lock = Lock()
self.active: Dict[str, Process] = {}
# stupid pylint does not know that it is possible
self.queue: Queue[Tuple[str, bool]] = Queue() # pylint: disable=unsubscriptable-object
@staticmethod
def process(callback: Callable[[argparse.Namespace, str], bool], args: argparse.Namespace, architecture: str,
process_id: str, queue: Queue[Tuple[str, bool]]) -> None: # pylint: disable=unsubscriptable-object
"""
helper to run external process
:param callback: application run function (i.e. Handler.run method)
:param args: command line arguments
:param architecture: repository architecture
:param process_id: process unique identifier
:param queue: output queue
"""
result = callback(args, architecture)
queue.put((process_id, result))
def packages_add(self, packages: Iterable[str], now: bool) -> None:
"""
add packages
:param packages: packages list to add
:param now: build packages now
"""
kwargs = {"now": ""} if now else {}
self.spawn_process("add", *packages, **kwargs)
def packages_remove(self, packages: Iterable[str]) -> None:
"""
remove packages
:param packages: packages list to remove
"""
self.spawn_process("remove", *packages)
def spawn_process(self, command: str, *args: str, **kwargs: str) -> None:
"""
spawn external ahriman process with supplied arguments
:param command: subcommand to run
:param args: positional command arguments
:param kwargs: named command arguments
"""
# default arguments
arguments = ["--architecture", self.architecture]
if self.configuration.path is not None:
arguments.extend(["--configuration", str(self.configuration.path)])
# positional command arguments
arguments.append(command)
arguments.extend(args)
# named command arguments
for argument, value in kwargs.items():
arguments.append(f"--{argument}")
if value:
arguments.append(value)
process_id = str(uuid.uuid4())
self.logger.info("full command line arguments of %s are %s", process_id, arguments)
parsed = self.args_parser.parse_args(arguments)
callback = parsed.handler.call
process = Process(target=self.process,
args=(callback, parsed, self.architecture, process_id, self.queue),
daemon=True)
process.start()
with self.lock:
self.active[process_id] = process
def run(self) -> None:
"""
thread run method
"""
for process_id, status in iter(self.queue.get, None):
self.logger.info("process %s has been terminated with status %s", process_id, status)
with self.lock:
process = self.active.pop(process_id, None)
if process is not None:
process.terminate() # make sure lol
process.join()

View File

@ -49,7 +49,7 @@ class Watcher:
self.logger = logging.getLogger("http") self.logger = logging.getLogger("http")
self.architecture = architecture self.architecture = architecture
self.repository = Repository(architecture, configuration) self.repository = Repository(architecture, configuration, no_report=True)
self.known: Dict[str, Tuple[Package, BuildStatus]] = {} self.known: Dict[str, Tuple[Package, BuildStatus]] = {}
self.status = BuildStatus() self.status = BuildStatus()

View File

@ -65,7 +65,7 @@ class WebClient(Client):
""" """
:return: full url for web service to login :return: full url for web service to login
""" """
return f"{self.address}/login" return f"{self.address}/user-api/v1/login"
@property @property
def _status_url(self) -> str: def _status_url(self) -> str:

View File

@ -51,7 +51,7 @@ class User:
""" """
if username is None or password is None: if username is None or password is None:
return None return None
return cls(username, password, UserAccess.Status) return cls(username, password, UserAccess.Read)
@staticmethod @staticmethod
def generate_password(length: int) -> str: def generate_password(length: int) -> str:

View File

@ -25,9 +25,7 @@ class UserAccess(Enum):
web user access enumeration web user access enumeration
:cvar Read: user can read status page :cvar Read: user can read status page
:cvar Write: user can modify task and package list :cvar Write: user can modify task and package list
:cvar Status: user can update statuses via API
""" """
Read = "read" Read = "read"
Write = "write" Write = "write"
Status = "status"

View File

@ -73,9 +73,7 @@ def auth_handler(validator: Auth) -> MiddlewareType:
""" """
@middleware @middleware
async def handle(request: Request, handler: HandlerType) -> StreamResponse: async def handle(request: Request, handler: HandlerType) -> StreamResponse:
if request.path.startswith("/status-api"): if request.method in ("GET", "HEAD", "OPTIONS"):
permission = UserAccess.Status
elif request.method in ("GET", "HEAD", "OPTIONS"):
permission = UserAccess.Read permission = UserAccess.Read
else: else:
permission = UserAccess.Write permission = UserAccess.Write

View File

@ -19,13 +19,16 @@
# #
from aiohttp.web import Application from aiohttp.web import Application
from ahriman.web.views.ahriman import AhrimanView
from ahriman.web.views.index import IndexView from ahriman.web.views.index import IndexView
from ahriman.web.views.login import LoginView from ahriman.web.views.service.add import AddView
from ahriman.web.views.logout import LogoutView from ahriman.web.views.service.remove import RemoveView
from ahriman.web.views.package import PackageView from ahriman.web.views.service.search import SearchView
from ahriman.web.views.packages import PackagesView from ahriman.web.views.status.ahriman import AhrimanView
from ahriman.web.views.status import StatusView from ahriman.web.views.status.package import PackageView
from ahriman.web.views.status.packages import PackagesView
from ahriman.web.views.status.status import StatusView
from ahriman.web.views.user.login import LoginView
from ahriman.web.views.user.logout import LogoutView
def setup_routes(application: Application) -> None: def setup_routes(application: Application) -> None:
@ -37,8 +40,13 @@ def setup_routes(application: Application) -> None:
GET / get build status page GET / get build status page
GET /index.html same as above GET /index.html same as above
POST /login login to service POST /service-api/v1/add add new packages to repository
POST /logout logout from service
POST /service-api/v1/remove remove existing package from repository
POST /service-api/v1/update update packages in repository, actually it is just alias for add
GET /service-api/v1/search search for substring in AUR
GET /status-api/v1/ahriman get current service status GET /status-api/v1/ahriman get current service status
POST /status-api/v1/ahriman update service status POST /status-api/v1/ahriman update service status
@ -52,13 +60,21 @@ def setup_routes(application: Application) -> None:
GET /status-api/v1/status get web service status itself GET /status-api/v1/status get web service status itself
POST /user-api/v1/login login to service
POST /user-api/v1/logout logout from service
:param application: web application instance :param application: web application instance
""" """
application.router.add_get("/", IndexView, allow_head=True) application.router.add_get("/", IndexView, allow_head=True)
application.router.add_get("/index.html", IndexView, allow_head=True) application.router.add_get("/index.html", IndexView, allow_head=True)
application.router.add_post("/login", LoginView) application.router.add_post("/service-api/v1/add", AddView)
application.router.add_post("/logout", LogoutView)
application.router.add_post("/service-api/v1/remove", RemoveView)
application.router.add_get("/service-api/v1/search", SearchView, allow_head=False)
application.router.add_post("/service-api/v1/update", AddView)
application.router.add_get("/status-api/v1/ahriman", AhrimanView, allow_head=True) application.router.add_get("/status-api/v1/ahriman", AhrimanView, allow_head=True)
application.router.add_post("/status-api/v1/ahriman", AhrimanView) application.router.add_post("/status-api/v1/ahriman", AhrimanView)
@ -71,3 +87,6 @@ def setup_routes(application: Application) -> None:
application.router.add_post("/status-api/v1/packages/{package}", PackageView) application.router.add_post("/status-api/v1/packages/{package}", PackageView)
application.router.add_get("/status-api/v1/status", StatusView, allow_head=True) application.router.add_get("/status-api/v1/status", StatusView, allow_head=True)
application.router.add_post("/user-api/v1/login", LoginView)
application.router.add_post("/user-api/v1/logout", LogoutView)

View File

@ -18,9 +18,10 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from aiohttp.web import View from aiohttp.web import View
from typing import Any, Dict from typing import Any, Dict, List, Optional
from ahriman.core.auth.auth import Auth from ahriman.core.auth.auth import Auth
from ahriman.core.spawn import Spawn
from ahriman.core.status.watcher import Watcher from ahriman.core.status.watcher import Watcher
@ -37,6 +38,14 @@ class BaseView(View):
watcher: Watcher = self.request.app["watcher"] watcher: Watcher = self.request.app["watcher"]
return watcher return watcher
@property
def spawner(self) -> Spawn:
"""
:return: external process spawner instance
"""
spawner: Spawn = self.request.app["spawn"]
return spawner
@property @property
def validator(self) -> Auth: def validator(self) -> Auth:
""" """
@ -45,13 +54,33 @@ class BaseView(View):
validator: Auth = self.request.app["validator"] validator: Auth = self.request.app["validator"]
return validator return validator
async def extract_data(self) -> Dict[str, Any]: async def extract_data(self, list_keys: Optional[List[str]] = None) -> Dict[str, Any]:
""" """
extract json data from either json or form data extract json data from either json or form data
:param list_keys: optional list of keys which must be forced to list from form data
:return: raw json object or form data converted to json :return: raw json object or form data converted to json
""" """
try: try:
json: Dict[str, Any] = await self.request.json() json: Dict[str, Any] = await self.request.json()
return json return json
except ValueError: except ValueError:
return dict(await self.request.post()) return await self.data_as_json(list_keys or [])
async def data_as_json(self, list_keys: List[str]) -> Dict[str, Any]:
"""
extract form data and convert it to json object
:param list_keys: list of keys which must be forced to list from form data
:return: form data converted to json. In case if a key is found multiple times it will be returned as list
"""
raw = await self.request.post()
json: Dict[str, Any] = {}
for key, value in raw.items():
if key in json and isinstance(json[key], list):
json[key].append(value)
elif key in json:
json[key] = [json[key], value]
elif key in list_keys:
json[key] = [value]
else:
json[key] = value
return json

View File

@ -0,0 +1,19 @@
#
# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#

View File

@ -0,0 +1,52 @@
#
# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from aiohttp.web import HTTPFound, Response, json_response
from ahriman.web.views.base import BaseView
class AddView(BaseView):
"""
add package web view
"""
async def post(self) -> Response:
"""
add new package
JSON body must be supplied, the following model is used:
{
"packages": "ahriman", # either list of packages or package name as in AUR
"build_now": true # optional flag which runs build
}
:return: redirect to main page on success
"""
data = await self.extract_data(["packages"])
try:
now = data.get("build_now", True)
packages = data["packages"]
except Exception as e:
return json_response(text=str(e), status=400)
self.spawner.packages_add(packages, now)
return HTTPFound("/")

View File

@ -0,0 +1,50 @@
#
# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from aiohttp.web import HTTPFound, Response, json_response
from ahriman.web.views.base import BaseView
class RemoveView(BaseView):
"""
remove package web view
"""
async def post(self) -> Response:
"""
remove existing packages
JSON body must be supplied, the following model is used:
{
"packages": "ahriman", # either list of packages or package name
}
:return: redirect to main page on success
"""
data = await self.extract_data(["packages"])
try:
packages = data["packages"]
except Exception as e:
return json_response(text=str(e), status=400)
self.spawner.packages_remove(packages)
return HTTPFound("/")

View File

@ -0,0 +1,48 @@
#
# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import aur # type: ignore
from aiohttp.web import Response, json_response
from typing import Iterator
from ahriman.web.views.base import BaseView
class SearchView(BaseView):
"""
AUR search web view
"""
async def get(self) -> Response:
"""
search packages in AUR
search string (non empty) must be supplied as `for` parameter
:return: 200 with found package bases sorted by name
"""
search: Iterator[str] = filter(lambda s: len(s) > 3, self.request.query.getall("for", default=[]))
search_string = " ".join(search)
if not search_string:
return json_response(text="Search string must not be empty", status=400)
packages = aur.search(search_string)
return json_response(sorted(package.package_base for package in packages))

View File

@ -0,0 +1,19 @@
#
# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#

View File

@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response from aiohttp.web import HTTPNoContent, Response, json_response
from ahriman.models.build_status import BuildStatusEnum from ahriman.models.build_status import BuildStatusEnum
from ahriman.web.views.base import BaseView from ahriman.web.views.base import BaseView
@ -51,7 +51,7 @@ class AhrimanView(BaseView):
try: try:
status = BuildStatusEnum(data["status"]) status = BuildStatusEnum(data["status"])
except Exception as e: except Exception as e:
raise HTTPBadRequest(text=str(e)) return json_response(text=str(e), status=400)
self.service.update_self(status) self.service.update_self(status)

View File

@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response, json_response from aiohttp.web import HTTPNoContent, HTTPNotFound, Response, json_response
from ahriman.core.exceptions import UnknownPackage from ahriman.core.exceptions import UnknownPackage
from ahriman.models.build_status import BuildStatusEnum from ahriman.models.build_status import BuildStatusEnum
@ -80,11 +80,11 @@ class PackageView(BaseView):
package = Package.from_json(data["package"]) if "package" in data else None package = Package.from_json(data["package"]) if "package" in data else None
status = BuildStatusEnum(data["status"]) status = BuildStatusEnum(data["status"])
except Exception as e: except Exception as e:
raise HTTPBadRequest(text=str(e)) return json_response(text=str(e), status=400)
try: try:
self.service.update(base, status, package) self.service.update(base, status, package)
except UnknownPackage: except UnknownPackage:
raise HTTPBadRequest(text=f"Package {base} is unknown, but no package body set") return json_response(text=f"Package {base} is unknown, but no package body set", status=400)
return HTTPNoContent() return HTTPNoContent()

View File

@ -0,0 +1,19 @@
#
# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#

View File

@ -26,6 +26,7 @@ from aiohttp import web
from ahriman.core.auth.auth import Auth from ahriman.core.auth.auth import Auth
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import InitializeException from ahriman.core.exceptions import InitializeException
from ahriman.core.spawn import Spawn
from ahriman.core.status.watcher import Watcher from ahriman.core.status.watcher import Watcher
from ahriman.web.middlewares.exception_handler import exception_handler from ahriman.web.middlewares.exception_handler import exception_handler
from ahriman.web.routes import setup_routes from ahriman.web.routes import setup_routes
@ -67,11 +68,12 @@ def run_server(application: web.Application) -> None:
access_log=logging.getLogger("http")) access_log=logging.getLogger("http"))
def setup_service(architecture: str, configuration: Configuration) -> web.Application: def setup_service(architecture: str, configuration: Configuration, spawner: Spawn) -> web.Application:
""" """
create web application create web application
:param architecture: repository architecture :param architecture: repository architecture
:param configuration: configuration instance :param configuration: configuration instance
:param spawner: spawner thread
:return: web application instance :return: web application instance
""" """
application = web.Application(logger=logging.getLogger("http")) application = web.Application(logger=logging.getLogger("http"))
@ -93,6 +95,9 @@ def setup_service(architecture: str, configuration: Configuration) -> web.Applic
application.logger.info("setup watcher") application.logger.info("setup watcher")
application["watcher"] = Watcher(architecture, configuration) application["watcher"] = Watcher(architecture, configuration)
application.logger.info("setup process spawner")
application["spawn"] = spawner
application.logger.info("setup authorization") application.logger.info("setup authorization")
validator = application["validator"] = Auth.load(configuration) validator = application["validator"] = Auth.load(configuration)
if validator.enabled: if validator.enabled:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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