mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-04-24 07:17:17 +00:00
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:
parent
214f319123
commit
11c03a9041
@ -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
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
29
package/share/ahriman/build-status/login-modal.jinja2
Normal file
29
package/share/ahriman/build-status/login-modal.jinja2
Normal 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>
|
@ -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>
|
@ -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>
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -1 +0,0 @@
|
|||||||
<style></style>
|
|
12
package/share/ahriman/utils/bootstrap-scripts.jinja2
Normal file
12
package/share/ahriman/utils/bootstrap-scripts.jinja2
Normal 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>
|
9
package/share/ahriman/utils/style.jinja2
Normal file
9
package/share/ahriman/utils/style.jinja2
Normal 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>
|
10
setup.py
10
setup.py
@ -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",
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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:
|
||||||
"""
|
"""
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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()):
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
|
@ -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:
|
||||||
|
@ -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, [])
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
|
@ -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}")
|
||||||
|
@ -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
|
||||||
|
@ -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, [])
|
||||||
|
@ -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:
|
||||||
|
@ -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)
|
||||||
|
@ -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:
|
||||||
"""
|
"""
|
||||||
|
@ -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
|
||||||
|
@ -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
137
src/ahriman/core/spawn.py
Normal 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()
|
@ -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()
|
||||||
|
@ -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:
|
||||||
|
@ -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:
|
||||||
|
@ -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"
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
19
src/ahriman/web/views/service/__init__.py
Normal file
19
src/ahriman/web/views/service/__init__.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
52
src/ahriman/web/views/service/add.py
Normal file
52
src/ahriman/web/views/service/add.py
Normal 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("/")
|
50
src/ahriman/web/views/service/remove.py
Normal file
50
src/ahriman/web/views/service/remove.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from 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("/")
|
48
src/ahriman/web/views/service/search.py
Normal file
48
src/ahriman/web/views/service/search.py
Normal 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))
|
19
src/ahriman/web/views/status/__init__.py
Normal file
19
src/ahriman/web/views/status/__init__.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
@ -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)
|
||||||
|
|
@ -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()
|
19
src/ahriman/web/views/user/__init__.py
Normal file
19
src/ahriman/web/views/user/__init__.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
@ -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:
|
||||||
|
@ -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:
|
||||||
"""
|
"""
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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])
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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")
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
111
tests/ahriman/core/test_spawn.py
Normal file
111
tests/ahriman/core/test_spawn.py
Normal 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
|
@ -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()))
|
||||||
|
@ -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)
|
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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")
|
||||||
|
@ -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()
|
||||||
|
@ -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,
|
||||||
|
46
tests/ahriman/web/views/service/test_views_service_add.py
Normal file
46
tests/ahriman/web/views/service/test_views_service_add.py
Normal 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)
|
24
tests/ahriman/web/views/service/test_views_service_remove.py
Normal file
24
tests/ahriman/web/views/service/test_views_service_remove.py
Normal 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()
|
59
tests/ahriman/web/views/service/test_views_service_search.py
Normal file
59
tests/ahriman/web/views/service/test_views_service_search.py
Normal 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()
|
88
tests/ahriman/web/views/test_views_base.py
Normal file
88
tests/ahriman/web/views/test_views_base.py
Normal 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"]}
|
@ -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
Loading…
Reference in New Issue
Block a user