mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-04-24 07:17:17 +00:00
use api generated docs instead of comments (#92)
This commit is contained in:
parent
d81e91e117
commit
fc01bf3d1c
2
.github/workflows/setup.sh
vendored
2
.github/workflows/setup.sh
vendored
@ -18,7 +18,7 @@ if [[ -z $MINIMAL_INSTALL ]]; then
|
|||||||
# VCS support
|
# VCS support
|
||||||
pacman --noconfirm -Sy breezy darcs mercurial subversion
|
pacman --noconfirm -Sy breezy darcs mercurial subversion
|
||||||
# web server
|
# web server
|
||||||
pacman --noconfirm -Sy python-aioauth-client python-aiohttp python-aiohttp-debugtoolbar python-aiohttp-jinja2 python-aiohttp-security python-aiohttp-session python-cryptography python-jinja
|
pacman --noconfirm -Sy python-aioauth-client python-aiohttp python-aiohttp-apispec-git python-aiohttp-cors python-aiohttp-debugtoolbar python-aiohttp-jinja2 python-aiohttp-security python-aiohttp-session python-cryptography python-jinja
|
||||||
# additional features
|
# additional features
|
||||||
pacman --noconfirm -Sy gnupg python-boto3 rsync
|
pacman --noconfirm -Sy gnupg python-boto3 rsync
|
||||||
fi
|
fi
|
||||||
|
@ -156,6 +156,52 @@ Again, the most checks can be performed by `make check` command, though some add
|
|||||||
* No global variable is allowed outside of `ahriman.version` module. `ahriman.core.context` is also special case.
|
* No global variable is allowed outside of `ahriman.version` module. `ahriman.core.context` is also special case.
|
||||||
* Single quotes are not allowed. The reason behind this restriction is the fact that docstrings must be written by using double quotes only, and we would like to make style consistent.
|
* Single quotes are not allowed. The reason behind this restriction is the fact that docstrings must be written by using double quotes only, and we would like to make style consistent.
|
||||||
* If your class writes anything to log, the `ahriman.core.log.LazyLogging` trait must be used.
|
* If your class writes anything to log, the `ahriman.core.log.LazyLogging` trait must be used.
|
||||||
|
* Web API methods must be documented by using `aiohttp_apispec` library. Schema testing mostly should be implemented in related view class tests. Recommended example for documentation (excluding comments):
|
||||||
|
|
||||||
|
```python
|
||||||
|
import aiohttp_apispec
|
||||||
|
|
||||||
|
from marshmallow import Schema, fields
|
||||||
|
|
||||||
|
from ahriman.web.schemas.auth_schema import AuthSchema
|
||||||
|
from ahriman.web.schemas.error_schema import ErrorSchema
|
||||||
|
from ahriman.web.schemas.package_name_schema import PackageNameSchema
|
||||||
|
from ahriman.web.views.base import BaseView
|
||||||
|
|
||||||
|
|
||||||
|
class RequestSchema(Schema):
|
||||||
|
|
||||||
|
field = fields.String(metadata={"description": "Field description", "example": "foo"})
|
||||||
|
|
||||||
|
|
||||||
|
class ResponseSchema(Schema):
|
||||||
|
|
||||||
|
field = fields.String(required=True, metadata={"description": "Field description"})
|
||||||
|
|
||||||
|
|
||||||
|
class Foo(BaseView):
|
||||||
|
|
||||||
|
POST_PERMISSION = ...
|
||||||
|
|
||||||
|
@aiohttp_apispec.docs(
|
||||||
|
tags=["Tag"],
|
||||||
|
summary="Do foo",
|
||||||
|
description="Extended description of the method which does foo",
|
||||||
|
responses={
|
||||||
|
200: {"description": "Success response", "schema": ResponseSchema},
|
||||||
|
204: {"description": "Success response"}, # example without json schema response
|
||||||
|
400: {"description": "Bad data is supplied", "schema": ErrorSchema}, # exception raised by this method
|
||||||
|
401: {"description": "Authorization required", "schema": ErrorSchema}, # should be always presented
|
||||||
|
403: {"description": "Access is forbidden", "schema": ErrorSchema}, # should be always presented
|
||||||
|
500: {"description": "Internal server error", "schema": ErrorSchema}, # should be always presented
|
||||||
|
},
|
||||||
|
security=[{"token": [POST_PERMISSION]}],
|
||||||
|
)
|
||||||
|
@aiohttp_apispec.cookies_schema(AuthSchema) # should be always presented
|
||||||
|
@aiohttp_apispec.match_info_schema(PackageNameSchema)
|
||||||
|
@aiohttp_apispec.json_schema(RequestSchema(many=True))
|
||||||
|
async def post(self) -> None: ...
|
||||||
|
```
|
||||||
|
|
||||||
### Other checks
|
### Other checks
|
||||||
|
|
||||||
|
@ -30,9 +30,9 @@ COPY "docker/install-aur-package.sh" "/usr/local/bin/install-aur-package"
|
|||||||
## darcs is not installed by reasons, because it requires a lot haskell packages which dramatically increase image size
|
## darcs is not installed by reasons, because it requires a lot haskell packages which dramatically increase image size
|
||||||
RUN pacman --noconfirm -Sy devtools git pyalpm python-cerberus python-inflection python-passlib python-requests python-setuptools python-srcinfo && \
|
RUN pacman --noconfirm -Sy devtools git pyalpm python-cerberus python-inflection python-passlib python-requests python-setuptools python-srcinfo && \
|
||||||
pacman --noconfirm -Sy python-build python-installer python-wheel && \
|
pacman --noconfirm -Sy python-build python-installer python-wheel && \
|
||||||
pacman --noconfirm -Sy breezy mercurial python-aiohttp python-boto3 python-cryptography python-jinja python-requests-unixsocket rsync subversion && \
|
pacman --noconfirm -Sy breezy mercurial python-aiohttp python-aiohttp-cors python-boto3 python-cryptography python-jinja python-requests-unixsocket rsync subversion && \
|
||||||
runuser -u build -- install-aur-package python-aioauth-client python-aiohttp-jinja2 python-aiohttp-debugtoolbar \
|
runuser -u build -- install-aur-package python-aioauth-client python-aiohttp-apispec-git python-aiohttp-jinja2 \
|
||||||
python-aiohttp-session python-aiohttp-security
|
python-aiohttp-debugtoolbar python-aiohttp-session python-aiohttp-security
|
||||||
|
|
||||||
# cleanup unused
|
# cleanup unused
|
||||||
RUN find "/var/cache/pacman/pkg" -type f -delete
|
RUN find "/var/cache/pacman/pkg" -type f -delete
|
||||||
|
@ -36,6 +36,6 @@ The application provides reasonable defaults which allow to use it out-of-box; h
|
|||||||
|
|
||||||
## Live demos
|
## Live demos
|
||||||
|
|
||||||
* [Build status page](https://ahriman-demo.arcanis.me). You can log in as `demo` user by using `demo` password. However, you will not be able to run tasks.
|
* [Build status page](https://ahriman-demo.arcanis.me). You can log in as `demo` user by using `demo` password. However, you will not be able to run tasks. [HTTP API documentation](https://ahriman-demo.arcanis.me/api-docs) is also available.
|
||||||
* [Repository index](http://repo.arcanis.me/x86_64/index.html).
|
* [Repository index](http://repo.arcanis.me/x86_64/index.html).
|
||||||
* [Telegram feed](https://t.me/arcanisrepo).
|
* [Telegram feed](https://t.me/arcanisrepo).
|
||||||
|
@ -269,6 +269,7 @@ Web application
|
|||||||
Web application requires the following python packages to be installed:
|
Web application requires the following python packages to be installed:
|
||||||
|
|
||||||
* Core part requires ``aiohttp`` (application itself), ``aiohttp_jinja2`` and ``Jinja2`` (HTML generation from templates).
|
* Core part requires ``aiohttp`` (application itself), ``aiohttp_jinja2`` and ``Jinja2`` (HTML generation from templates).
|
||||||
|
* Additional web features also require ``aiohttp-apispec`` (autogenerated documentation), ``aiohttp_cors`` (CORS support, required by documentation)
|
||||||
* In addition, ``aiohttp_debugtoolbar`` is required for debug panel. Please note that this option does not work together with authorization and basically must not be used in production.
|
* In addition, ``aiohttp_debugtoolbar`` is required for debug panel. Please note that this option does not work together with authorization and basically must not be used in production.
|
||||||
* In addition, authorization feature requires ``aiohttp_security``, ``aiohttp_session`` and ``cryptography``.
|
* In addition, authorization feature requires ``aiohttp_security``, ``aiohttp_session`` and ``cryptography``.
|
||||||
* In addition to base authorization dependencies, OAuth2 also requires ``aioauth-client`` library.
|
* In addition to base authorization dependencies, OAuth2 also requires ``aioauth-client`` library.
|
||||||
@ -279,6 +280,13 @@ Middlewares
|
|||||||
|
|
||||||
Service provides some custom middlewares, e.g. logging every exception (except for user ones) and user authorization.
|
Service provides some custom middlewares, e.g. logging every exception (except for user ones) and user authorization.
|
||||||
|
|
||||||
|
HEAD and OPTIONS requests
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
``HEAD`` request is automatically generated by ``ahriman.web.views.base.BaseView`` class. It just calls ``GET`` method, removes any data from body and returns the result. In case if no ``GET`` method available for this view, the ``aiohttp.web.HTTPMethodNotAllowed`` exception will be raised.
|
||||||
|
|
||||||
|
On the other side, ``OPTIONS`` method is implemented in the ``ahriman.web.middlewares.exception_handler.exception_handler`` middleware. In case if ``aiohttp.web.HTTPMethodNotAllowed`` exception is raised and original method was ``OPTIONS``, the middleware handles it, converts to valid request and returns response to user.
|
||||||
|
|
||||||
Web views
|
Web views
|
||||||
^^^^^^^^^
|
^^^^^^^^^
|
||||||
|
|
||||||
@ -288,6 +296,7 @@ REST API supports both form and JSON data, but the last one is recommended.
|
|||||||
|
|
||||||
Different APIs are separated into different packages:
|
Different APIs are separated into different packages:
|
||||||
|
|
||||||
|
* ``ahriman.web.views.api`` not a real API, but some views which provide OpenAPI support.
|
||||||
* ``ahriman.web.views.service`` provides views for application controls.
|
* ``ahriman.web.views.service`` provides views for application controls.
|
||||||
* ``ahriman.web.views.status`` package provides REST API for application reporting.
|
* ``ahriman.web.views.status`` package provides REST API for application reporting.
|
||||||
* ``ahriman.web.views.user`` package provides login and logout methods which can be called without authorization.
|
* ``ahriman.web.views.user`` package provides login and logout methods which can be called without authorization.
|
||||||
|
@ -878,6 +878,11 @@ How to enable OAuth authorization
|
|||||||
#.
|
#.
|
||||||
Restart web service ``systemctl restart ahriman-web@x86_64``.
|
Restart web service ``systemctl restart ahriman-web@x86_64``.
|
||||||
|
|
||||||
|
How to implement own interface
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
You can write your own interface by using API which is provided by the web service. Full autogenerated API documentation is available at ``http://localhost:8080/api-docs``.
|
||||||
|
|
||||||
Backup and restore
|
Backup and restore
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
|
@ -14,6 +14,8 @@ optdepends=('breezy: -bzr packages support'
|
|||||||
'mercurial: -hg packages support'
|
'mercurial: -hg packages support'
|
||||||
'python-aioauth-client: web server with OAuth2 authorization'
|
'python-aioauth-client: web server with OAuth2 authorization'
|
||||||
'python-aiohttp: web server'
|
'python-aiohttp: web server'
|
||||||
|
'python-aiohttp-apispec>=3.0.0: web server'
|
||||||
|
'python-aiohttp-cors: web server'
|
||||||
'python-aiohttp-debugtoolbar: web server with enabled debug panel'
|
'python-aiohttp-debugtoolbar: web server with enabled debug panel'
|
||||||
'python-aiohttp-jinja2: web server'
|
'python-aiohttp-jinja2: web server'
|
||||||
'python-aiohttp-security: web server with authorization'
|
'python-aiohttp-security: web server with authorization'
|
||||||
|
23
package/share/ahriman/templates/api.jinja2
Normal file
23
package/share/ahriman/templates/api.jinja2
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>ahriman API</title>
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<!-- Embed elements Elements via Web Component -->
|
||||||
|
<script src="https://unpkg.com/@stoplight/elements/web-components.min.js"></script>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/@stoplight/elements/styles.min.css" type="text/css">
|
||||||
|
|
||||||
|
<link rel="shortcut icon" href="/static/favicon.ico">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<elements-api
|
||||||
|
apiDescriptionUrl="/api-docs/swagger.json"
|
||||||
|
router="hash"
|
||||||
|
layout="sidebar"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
7
setup.py
7
setup.py
@ -68,6 +68,7 @@ setup(
|
|||||||
]),
|
]),
|
||||||
# templates
|
# templates
|
||||||
("share/ahriman/templates", [
|
("share/ahriman/templates", [
|
||||||
|
"package/share/ahriman/templates/api.jinja2",
|
||||||
"package/share/ahriman/templates/build-status.jinja2",
|
"package/share/ahriman/templates/build-status.jinja2",
|
||||||
"package/share/ahriman/templates/email-index.jinja2",
|
"package/share/ahriman/templates/email-index.jinja2",
|
||||||
"package/share/ahriman/templates/error.jinja2",
|
"package/share/ahriman/templates/error.jinja2",
|
||||||
@ -140,9 +141,11 @@ setup(
|
|||||||
],
|
],
|
||||||
"web": [
|
"web": [
|
||||||
"Jinja2",
|
"Jinja2",
|
||||||
"aiohttp",
|
|
||||||
"aiohttp_jinja2",
|
|
||||||
"aioauth-client",
|
"aioauth-client",
|
||||||
|
"aiohttp",
|
||||||
|
"aiohttp-apispec",
|
||||||
|
"aiohttp_cors",
|
||||||
|
"aiohttp_jinja2",
|
||||||
"aiohttp_debugtoolbar",
|
"aiohttp_debugtoolbar",
|
||||||
"aiohttp_session",
|
"aiohttp_session",
|
||||||
"aiohttp_security",
|
"aiohttp_security",
|
||||||
|
@ -27,7 +27,7 @@ from urllib.parse import urlparse
|
|||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
|
|
||||||
|
|
||||||
class Validator(RootValidator): # type: ignore
|
class Validator(RootValidator):
|
||||||
"""
|
"""
|
||||||
class which defines custom validation methods for the service configuration
|
class which defines custom validation methods for the service configuration
|
||||||
|
|
||||||
|
@ -19,10 +19,11 @@
|
|||||||
#
|
#
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import itertools
|
import functools
|
||||||
|
|
||||||
from typing import Callable, Iterable, List, Tuple
|
from typing import Callable, Iterable, List
|
||||||
|
|
||||||
|
from ahriman.core.util import partition
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
|
|
||||||
|
|
||||||
@ -149,13 +150,6 @@ class Tree:
|
|||||||
Returns:
|
Returns:
|
||||||
List[List[Package]]: sorted list of packages lists based on their dependencies
|
List[List[Package]]: sorted list of packages lists based on their dependencies
|
||||||
"""
|
"""
|
||||||
# https://docs.python.org/dev/library/itertools.html#itertools-recipes
|
|
||||||
def partition(source: List[Leaf]) -> Tuple[List[Leaf], Iterable[Leaf]]:
|
|
||||||
first_iter, second_iter = itertools.tee(source)
|
|
||||||
filter_fn: Callable[[Leaf], bool] = lambda leaf: leaf.is_dependency(next_level)
|
|
||||||
# materialize first list and leave second as iterator
|
|
||||||
return list(filter(filter_fn, first_iter)), itertools.filterfalse(filter_fn, second_iter)
|
|
||||||
|
|
||||||
unsorted: List[List[Leaf]] = []
|
unsorted: List[List[Leaf]] = []
|
||||||
|
|
||||||
# build initial tree
|
# build initial tree
|
||||||
@ -170,7 +164,9 @@ class Tree:
|
|||||||
next_level = unsorted[next_num]
|
next_level = unsorted[next_num]
|
||||||
|
|
||||||
# change lists inside the collection
|
# change lists inside the collection
|
||||||
unsorted[current_num], to_be_moved = partition(current_level)
|
# additional workaround with partial in order to hide cell-var-from-loop pylint warning
|
||||||
|
predicate = functools.partial(Leaf.is_dependency, packages=next_level)
|
||||||
|
unsorted[current_num], to_be_moved = partition(current_level, predicate)
|
||||||
unsorted[next_num].extend(to_be_moved)
|
unsorted[next_num].extend(to_be_moved)
|
||||||
|
|
||||||
comparator: Callable[[Package], str] = lambda package: package.base
|
comparator: Callable[[Package], str] = lambda package: package.base
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
#
|
#
|
||||||
import datetime
|
import datetime
|
||||||
import io
|
import io
|
||||||
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@ -28,14 +29,31 @@ import subprocess
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from pwd import getpwuid
|
from pwd import getpwuid
|
||||||
from typing import Any, Dict, Generator, IO, Iterable, List, Optional, Type, Union
|
from typing import Any, Callable, Dict, Generator, IO, Iterable, List, Optional, Type, TypeVar, Tuple, Union
|
||||||
|
|
||||||
from ahriman.core.exceptions import OptionError, UnsafeRunError
|
from ahriman.core.exceptions import OptionError, UnsafeRunError
|
||||||
from ahriman.models.repository_paths import RepositoryPaths
|
from ahriman.models.repository_paths import RepositoryPaths
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["check_output", "check_user", "enum_values", "exception_response_text", "filter_json", "full_version",
|
__all__ = [
|
||||||
"package_like", "pretty_datetime", "pretty_size", "safe_filename", "trim_package", "utcnow", "walk"]
|
"check_output",
|
||||||
|
"check_user",
|
||||||
|
"enum_values",
|
||||||
|
"exception_response_text",
|
||||||
|
"filter_json",
|
||||||
|
"full_version",
|
||||||
|
"package_like",
|
||||||
|
"partition",
|
||||||
|
"pretty_datetime",
|
||||||
|
"pretty_size",
|
||||||
|
"safe_filename",
|
||||||
|
"trim_package",
|
||||||
|
"utcnow",
|
||||||
|
"walk",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
def check_output(*args: str, exception: Optional[Exception] = None, cwd: Optional[Path] = None,
|
def check_output(*args: str, exception: Optional[Exception] = None, cwd: Optional[Path] = None,
|
||||||
@ -225,6 +243,21 @@ def package_like(filename: Path) -> bool:
|
|||||||
return ".pkg." in name and not name.endswith(".sig")
|
return ".pkg." in name and not name.endswith(".sig")
|
||||||
|
|
||||||
|
|
||||||
|
def partition(source: List[T], predicate: Callable[[T], bool]) -> Tuple[List[T], List[T]]:
|
||||||
|
"""
|
||||||
|
partition list into two based on predicate, based on # https://docs.python.org/dev/library/itertools.html#itertools-recipes
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source(List[T]): source list to be partitioned
|
||||||
|
predicate(Callable[[T], bool]): filter function
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[List[T], List[T]]: two lists, first is which ``predicate`` is ``True``, second is ``False``
|
||||||
|
"""
|
||||||
|
first_iter, second_iter = itertools.tee(source)
|
||||||
|
return list(filter(predicate, first_iter)), list(itertools.filterfalse(predicate, second_iter))
|
||||||
|
|
||||||
|
|
||||||
def pretty_datetime(timestamp: Optional[Union[datetime.datetime, float, int]]) -> str:
|
def pretty_datetime(timestamp: Optional[Union[datetime.datetime, float, int]]) -> str:
|
||||||
"""
|
"""
|
||||||
convert datetime object to string
|
convert datetime object to string
|
||||||
|
120
src/ahriman/web/apispec.py
Normal file
120
src/ahriman/web/apispec.py
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2023 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import aiohttp_apispec # type: ignore
|
||||||
|
|
||||||
|
from aiohttp.web import Application
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
from ahriman import version
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["setup_apispec"]
|
||||||
|
|
||||||
|
|
||||||
|
def _info() -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
create info object for swagger docs
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, Any]: info object as per openapi specification
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"title": "ahriman",
|
||||||
|
"description": """Wrapper for managing custom repository inspired by [repo-scripts](https://github.com/arcan1s/repo-scripts).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
* Install-configure-forget manager for the very own repository.
|
||||||
|
* Multi-architecture support.
|
||||||
|
* Dependency manager.
|
||||||
|
* VCS packages support.
|
||||||
|
* Official repository support.
|
||||||
|
* Ability to patch AUR packages and even create package from local PKGBUILDs.
|
||||||
|
* Sign support with gpg (repository, package, per package settings).
|
||||||
|
* Triggers for repository updates, e.g. synchronization to remote services (rsync, s3 and github) and report generation (email, html, telegram).
|
||||||
|
* Repository status interface with optional authorization and control options
|
||||||
|
|
||||||
|
<security-definitions />
|
||||||
|
""",
|
||||||
|
"license": {
|
||||||
|
"name": "GPL3",
|
||||||
|
"url": "https://raw.githubusercontent.com/arcan1s/ahriman/master/LICENSE",
|
||||||
|
},
|
||||||
|
"version": version.__version__,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _security() -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
get security definitions
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Dict[str, Any]]: generated security definition
|
||||||
|
"""
|
||||||
|
return [{
|
||||||
|
"token": {
|
||||||
|
"type": "apiKey", # as per specification we are using api key
|
||||||
|
"name": "API_SESSION",
|
||||||
|
"in": "cookie",
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
|
||||||
|
|
||||||
|
def _servers(application: Application) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
get list of defined addresses for server
|
||||||
|
|
||||||
|
Args:
|
||||||
|
application(Application): web application instance
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Dict[str, Any]]: list (actually only one) of defined web urls
|
||||||
|
"""
|
||||||
|
configuration: Configuration = application["configuration"]
|
||||||
|
address = configuration.get("web", "address", fallback=None)
|
||||||
|
if not address:
|
||||||
|
host = configuration.get("web", "host")
|
||||||
|
port = configuration.getint("web", "port")
|
||||||
|
address = f"http://{host}:{port}"
|
||||||
|
|
||||||
|
return [{
|
||||||
|
"url": address,
|
||||||
|
}]
|
||||||
|
|
||||||
|
|
||||||
|
def setup_apispec(application: Application) -> aiohttp_apispec.AiohttpApiSpec:
|
||||||
|
"""
|
||||||
|
setup swagger api specification
|
||||||
|
|
||||||
|
Args:
|
||||||
|
application(Application): web application instance
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
aiohttp_apispec.AiohttpApiSpec: created specification instance
|
||||||
|
"""
|
||||||
|
return aiohttp_apispec.setup_aiohttp_apispec(
|
||||||
|
application,
|
||||||
|
url="/api-docs/swagger.json",
|
||||||
|
openapi_version="3.0.2",
|
||||||
|
info=_info(),
|
||||||
|
servers=_servers(application),
|
||||||
|
security=_security(),
|
||||||
|
)
|
48
src/ahriman/web/cors.py
Normal file
48
src/ahriman/web/cors.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2023 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import aiohttp_cors # type: ignore
|
||||||
|
|
||||||
|
from aiohttp.web import Application
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["setup_cors"]
|
||||||
|
|
||||||
|
|
||||||
|
def setup_cors(application: Application) -> aiohttp_cors.CorsConfig:
|
||||||
|
"""
|
||||||
|
setup CORS for the web application
|
||||||
|
|
||||||
|
Args:
|
||||||
|
application(Application): web application instance
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
aiohttp_cors.CorsConfig: generated CORS configuration
|
||||||
|
"""
|
||||||
|
cors = aiohttp_cors.setup(application, defaults={
|
||||||
|
"*": aiohttp_cors.ResourceOptions(
|
||||||
|
expose_headers="*",
|
||||||
|
allow_headers="*",
|
||||||
|
allow_methods="*",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
for route in application.router.routes():
|
||||||
|
cors.add(route)
|
||||||
|
|
||||||
|
return cors
|
@ -17,8 +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 Request
|
from aiohttp.web import Request, StreamResponse
|
||||||
from aiohttp.web_response import StreamResponse
|
|
||||||
from typing import Awaitable, Callable
|
from typing import Awaitable, Callable
|
||||||
|
|
||||||
|
|
||||||
|
@ -21,10 +21,7 @@ import aiohttp_security # type: ignore
|
|||||||
import socket
|
import socket
|
||||||
import types
|
import types
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp.web import middleware, Application, Request, StaticResource, StreamResponse
|
||||||
from aiohttp.web import middleware, Request
|
|
||||||
from aiohttp.web_response import StreamResponse
|
|
||||||
from aiohttp.web_urldispatcher import StaticResource
|
|
||||||
from aiohttp_session import setup as setup_session
|
from aiohttp_session import setup as setup_session
|
||||||
from aiohttp_session.cookie_storage import EncryptedCookieStorage
|
from aiohttp_session.cookie_storage import EncryptedCookieStorage
|
||||||
from cryptography import fernet
|
from cryptography import fernet
|
||||||
@ -36,10 +33,10 @@ from ahriman.models.user_access import UserAccess
|
|||||||
from ahriman.web.middlewares import HandlerType, MiddlewareType
|
from ahriman.web.middlewares import HandlerType, MiddlewareType
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["AuthorizationPolicy", "auth_handler", "cookie_secret_key", "setup_auth"]
|
__all__ = ["setup_auth"]
|
||||||
|
|
||||||
|
|
||||||
class AuthorizationPolicy(aiohttp_security.AbstractAuthorizationPolicy): # type: ignore
|
class _AuthorizationPolicy(aiohttp_security.AbstractAuthorizationPolicy):
|
||||||
"""
|
"""
|
||||||
authorization policy implementation
|
authorization policy implementation
|
||||||
|
|
||||||
@ -83,7 +80,7 @@ class AuthorizationPolicy(aiohttp_security.AbstractAuthorizationPolicy): # type
|
|||||||
return await self.validator.verify_access(identity, permission, context)
|
return await self.validator.verify_access(identity, permission, context)
|
||||||
|
|
||||||
|
|
||||||
def auth_handler(allow_read_only: bool) -> MiddlewareType:
|
def _auth_handler(allow_read_only: bool) -> MiddlewareType:
|
||||||
"""
|
"""
|
||||||
authorization and authentication middleware
|
authorization and authentication middleware
|
||||||
|
|
||||||
@ -118,7 +115,7 @@ def auth_handler(allow_read_only: bool) -> MiddlewareType:
|
|||||||
return handle
|
return handle
|
||||||
|
|
||||||
|
|
||||||
def cookie_secret_key(configuration: Configuration) -> fernet.Fernet:
|
def _cookie_secret_key(configuration: Configuration) -> fernet.Fernet:
|
||||||
"""
|
"""
|
||||||
extract cookie secret key from configuration if set or generate new one
|
extract cookie secret key from configuration if set or generate new one
|
||||||
|
|
||||||
@ -135,26 +132,26 @@ def cookie_secret_key(configuration: Configuration) -> fernet.Fernet:
|
|||||||
return fernet.Fernet(secret_key)
|
return fernet.Fernet(secret_key)
|
||||||
|
|
||||||
|
|
||||||
def setup_auth(application: web.Application, configuration: Configuration, validator: Auth) -> web.Application:
|
def setup_auth(application: Application, configuration: Configuration, validator: Auth) -> Application:
|
||||||
"""
|
"""
|
||||||
setup authorization policies for the application
|
setup authorization policies for the application
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
application(web.Application): web application instance
|
application(Application): web application instance
|
||||||
configuration(Configuration): configuration instance
|
configuration(Configuration): configuration instance
|
||||||
validator(Auth): authorization module instance
|
validator(Auth): authorization module instance
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
web.Application: configured web application
|
Application: configured web application
|
||||||
"""
|
"""
|
||||||
secret_key = cookie_secret_key(configuration)
|
secret_key = _cookie_secret_key(configuration)
|
||||||
storage = EncryptedCookieStorage(secret_key, cookie_name="API_SESSION", max_age=validator.max_age)
|
storage = EncryptedCookieStorage(secret_key, cookie_name="API_SESSION", max_age=validator.max_age)
|
||||||
setup_session(application, storage)
|
setup_session(application, storage)
|
||||||
|
|
||||||
authorization_policy = AuthorizationPolicy(validator)
|
authorization_policy = _AuthorizationPolicy(validator)
|
||||||
identity_policy = aiohttp_security.SessionIdentityPolicy()
|
identity_policy = aiohttp_security.SessionIdentityPolicy()
|
||||||
|
|
||||||
aiohttp_security.setup(application, identity_policy, authorization_policy)
|
aiohttp_security.setup(application, identity_policy, authorization_policy)
|
||||||
application.middlewares.append(auth_handler(validator.allow_read_only))
|
application.middlewares.append(_auth_handler(validator.allow_read_only))
|
||||||
|
|
||||||
return application
|
return application
|
||||||
|
@ -20,8 +20,8 @@
|
|||||||
import aiohttp_jinja2
|
import aiohttp_jinja2
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from aiohttp.web import HTTPClientError, HTTPException, HTTPServerError, HTTPUnauthorized, Request, StreamResponse, \
|
from aiohttp.web import HTTPClientError, HTTPException, HTTPMethodNotAllowed, HTTPNoContent, HTTPServerError, \
|
||||||
json_response, middleware
|
HTTPUnauthorized, Request, StreamResponse, json_response, middleware
|
||||||
|
|
||||||
from ahriman.web.middlewares import HandlerType, MiddlewareType
|
from ahriman.web.middlewares import HandlerType, MiddlewareType
|
||||||
|
|
||||||
@ -29,6 +29,20 @@ from ahriman.web.middlewares import HandlerType, MiddlewareType
|
|||||||
__all__ = ["exception_handler"]
|
__all__ = ["exception_handler"]
|
||||||
|
|
||||||
|
|
||||||
|
def _is_templated_unauthorized(request: Request) -> bool:
|
||||||
|
"""
|
||||||
|
check if the request is eligible for rendering html template
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request(Request): source request to check
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True in case if response should be rendered as html and False otherwise
|
||||||
|
"""
|
||||||
|
return request.path in ("/api/v1/login", "/api/v1/logout") \
|
||||||
|
and "application/json" not in request.headers.getall("accept", [])
|
||||||
|
|
||||||
|
|
||||||
def exception_handler(logger: logging.Logger) -> MiddlewareType:
|
def exception_handler(logger: logging.Logger) -> MiddlewareType:
|
||||||
"""
|
"""
|
||||||
exception handler middleware. Just log any exception (except for client ones)
|
exception handler middleware. Just log any exception (except for client ones)
|
||||||
@ -44,10 +58,21 @@ def exception_handler(logger: logging.Logger) -> MiddlewareType:
|
|||||||
try:
|
try:
|
||||||
return await handler(request)
|
return await handler(request)
|
||||||
except HTTPUnauthorized as e:
|
except HTTPUnauthorized as e:
|
||||||
if is_templated_unauthorized(request):
|
if _is_templated_unauthorized(request):
|
||||||
context = {"code": e.status_code, "reason": e.reason}
|
context = {"code": e.status_code, "reason": e.reason}
|
||||||
return aiohttp_jinja2.render_template("error.jinja2", request, context, status=e.status_code)
|
return aiohttp_jinja2.render_template("error.jinja2", request, context, status=e.status_code)
|
||||||
return json_response(data={"error": e.reason}, status=e.status_code)
|
return json_response(data={"error": e.reason}, status=e.status_code)
|
||||||
|
except HTTPMethodNotAllowed as e:
|
||||||
|
if e.method == "OPTIONS":
|
||||||
|
# automatically handle OPTIONS method, idea comes from
|
||||||
|
# https://github.com/arcan1s/ffxivbis/blob/master/src/main/scala/me/arcanis/ffxivbis/http/api/v1/HttpHandler.scala#L32
|
||||||
|
raise HTTPNoContent(headers={"Allow": ",".join(sorted(e.allowed_methods))})
|
||||||
|
if e.method == "HEAD":
|
||||||
|
# since we have special autogenerated HEAD method, we need to remove it from list of available
|
||||||
|
e.allowed_methods = {method for method in e.allowed_methods if method != "HEAD"}
|
||||||
|
e.headers["Allow"] = ",".join(sorted(e.allowed_methods))
|
||||||
|
raise e
|
||||||
|
raise
|
||||||
except HTTPClientError as e:
|
except HTTPClientError as e:
|
||||||
return json_response(data={"error": e.reason}, status=e.status_code)
|
return json_response(data={"error": e.reason}, status=e.status_code)
|
||||||
except HTTPServerError as e:
|
except HTTPServerError as e:
|
||||||
@ -60,17 +85,3 @@ def exception_handler(logger: logging.Logger) -> MiddlewareType:
|
|||||||
return json_response(data={"error": str(e)}, status=500)
|
return json_response(data={"error": str(e)}, status=500)
|
||||||
|
|
||||||
return handle
|
return handle
|
||||||
|
|
||||||
|
|
||||||
def is_templated_unauthorized(request: Request) -> bool:
|
|
||||||
"""
|
|
||||||
check if the request is eligible for rendering html template
|
|
||||||
|
|
||||||
Args:
|
|
||||||
request(Request): source request to check
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True in case if response should be rendered as html and False otherwise
|
|
||||||
"""
|
|
||||||
return request.path in ("/api/v1/login", "/api/v1/logout") \
|
|
||||||
and "application/json" not in request.headers.getall("accept", [])
|
|
||||||
|
@ -20,6 +20,8 @@
|
|||||||
from aiohttp.web import Application
|
from aiohttp.web import Application
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from ahriman.web.views.api.docs import DocsView
|
||||||
|
from ahriman.web.views.api.swagger import SwaggerView
|
||||||
from ahriman.web.views.index import IndexView
|
from ahriman.web.views.index import IndexView
|
||||||
from ahriman.web.views.service.add import AddView
|
from ahriman.web.views.service.add import AddView
|
||||||
from ahriman.web.views.service.pgp import PGPView
|
from ahriman.web.views.service.pgp import PGPView
|
||||||
@ -43,82 +45,31 @@ def setup_routes(application: Application, static_path: Path) -> None:
|
|||||||
"""
|
"""
|
||||||
setup all defined routes
|
setup all defined routes
|
||||||
|
|
||||||
Available routes are:
|
|
||||||
|
|
||||||
* ``GET /`` get build status page
|
|
||||||
* ``GET /index.html`` same as above
|
|
||||||
|
|
||||||
* ``POST /api/v1/service/add`` add new packages to repository
|
|
||||||
|
|
||||||
* ``GET /api/v1/service/pgp`` fetch PGP key from the keyserver
|
|
||||||
* ``POST /api/v1/service/pgp`` import PGP key from the keyserver
|
|
||||||
|
|
||||||
* ``POST /api/v1/service/rebuild`` rebuild packages based on their dependency list
|
|
||||||
|
|
||||||
* ``POST /api/v1/service/remove`` remove existing package from repository
|
|
||||||
|
|
||||||
* ``POST /api/v1/service/request`` request to add new packages to repository
|
|
||||||
|
|
||||||
* ``GET /api/v1/service/search`` search for substring in AUR
|
|
||||||
|
|
||||||
* ``POST /api/v1/service/update`` update all packages in repository
|
|
||||||
|
|
||||||
* ``GET /api/v1/packages`` get all known packages
|
|
||||||
* ``POST /api/v1/packages`` force update every package from repository
|
|
||||||
|
|
||||||
* ``DELETE /api/v1/package/:base`` delete package base from status page
|
|
||||||
* ``GET /api/v1/package/:base`` get package base status
|
|
||||||
* ``POST /api/v1/package/:base`` update package base status
|
|
||||||
|
|
||||||
* ``DELETE /api/v1/packages/{package}/logs`` delete package related logs
|
|
||||||
* ``GET /api/v1/packages/{package}/logs`` create log record for the package
|
|
||||||
* ``POST /api/v1/packages/{package}/logs`` get last package logs
|
|
||||||
|
|
||||||
* ``GET /api/v1/status`` get service status itself
|
|
||||||
* ``POST /api/v1/status`` update service status itself
|
|
||||||
|
|
||||||
* ``GET /api/v1/login`` OAuth2 handler for login
|
|
||||||
* ``POST /api/v1/login`` login to service
|
|
||||||
* ``POST /api/v1/logout`` logout from service
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
application(Application): web application instance
|
application(Application): web application instance
|
||||||
static_path(Path): path to static files directory
|
static_path(Path): path to static files directory
|
||||||
"""
|
"""
|
||||||
application.router.add_get("/", IndexView, allow_head=True)
|
application.router.add_view("/", IndexView)
|
||||||
application.router.add_get("/index.html", IndexView, allow_head=True)
|
application.router.add_view("/index.html", IndexView)
|
||||||
|
|
||||||
|
application.router.add_view("/api-docs", DocsView)
|
||||||
|
application.router.add_view("/api-docs/swagger.json", SwaggerView)
|
||||||
|
|
||||||
application.router.add_static("/static", static_path, follow_symlinks=True)
|
application.router.add_static("/static", static_path, follow_symlinks=True)
|
||||||
|
|
||||||
application.router.add_post("/api/v1/service/add", AddView)
|
application.router.add_view("/api/v1/service/add", AddView)
|
||||||
|
application.router.add_view("/api/v1/service/pgp", PGPView)
|
||||||
|
application.router.add_view("/api/v1/service/rebuild", RebuildView)
|
||||||
|
application.router.add_view("/api/v1/service/remove", RemoveView)
|
||||||
|
application.router.add_view("/api/v1/service/request", RequestView)
|
||||||
|
application.router.add_view("/api/v1/service/search", SearchView)
|
||||||
|
application.router.add_view("/api/v1/service/update", UpdateView)
|
||||||
|
|
||||||
application.router.add_get("/api/v1/service/pgp", PGPView, allow_head=True)
|
application.router.add_view("/api/v1/packages", PackagesView)
|
||||||
application.router.add_post("/api/v1/service/pgp", PGPView)
|
application.router.add_view("/api/v1/packages/{package}", PackageView)
|
||||||
|
application.router.add_view("/api/v1/packages/{package}/logs", LogsView)
|
||||||
|
|
||||||
application.router.add_post("/api/v1/service/rebuild", RebuildView)
|
application.router.add_view("/api/v1/status", StatusView)
|
||||||
|
|
||||||
application.router.add_post("/api/v1/service/remove", RemoveView)
|
application.router.add_view("/api/v1/login", LoginView)
|
||||||
|
application.router.add_view("/api/v1/logout", LogoutView)
|
||||||
application.router.add_post("/api/v1/service/request", RequestView)
|
|
||||||
|
|
||||||
application.router.add_get("/api/v1/service/search", SearchView, allow_head=False)
|
|
||||||
|
|
||||||
application.router.add_post("/api/v1/service/update", UpdateView)
|
|
||||||
|
|
||||||
application.router.add_get("/api/v1/packages", PackagesView, allow_head=True)
|
|
||||||
application.router.add_post("/api/v1/packages", PackagesView)
|
|
||||||
|
|
||||||
application.router.add_delete("/api/v1/packages/{package}", PackageView)
|
|
||||||
application.router.add_get("/api/v1/packages/{package}", PackageView, allow_head=True)
|
|
||||||
application.router.add_post("/api/v1/packages/{package}", PackageView)
|
|
||||||
|
|
||||||
application.router.add_delete("/api/v1/packages/{package}/logs", LogsView)
|
|
||||||
application.router.add_get("/api/v1/packages/{package}/logs", LogsView, allow_head=True)
|
|
||||||
application.router.add_post("/api/v1/packages/{package}/logs", LogsView)
|
|
||||||
|
|
||||||
application.router.add_get("/api/v1/status", StatusView, allow_head=True)
|
|
||||||
application.router.add_post("/api/v1/status", StatusView)
|
|
||||||
|
|
||||||
application.router.add_get("/api/v1/login", LoginView)
|
|
||||||
application.router.add_post("/api/v1/login", LoginView)
|
|
||||||
application.router.add_post("/api/v1/logout", LogoutView)
|
|
||||||
|
19
src/ahriman/web/schemas/__init__.py
Normal file
19
src/ahriman/web/schemas/__init__.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2023 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
35
src/ahriman/web/schemas/aur_package_schema.py
Normal file
35
src/ahriman/web/schemas/aur_package_schema.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2023 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from marshmallow import Schema, fields
|
||||||
|
|
||||||
|
|
||||||
|
class AURPackageSchema(Schema):
|
||||||
|
"""
|
||||||
|
response AUR package schema
|
||||||
|
"""
|
||||||
|
|
||||||
|
package = fields.String(required=True, metadata={
|
||||||
|
"description": "Package base",
|
||||||
|
"example": "ahriman",
|
||||||
|
})
|
||||||
|
description = fields.String(required=True, metadata={
|
||||||
|
"description": "Package description",
|
||||||
|
"example": "ArcH linux ReposItory MANager",
|
||||||
|
})
|
30
src/ahriman/web/schemas/auth_schema.py
Normal file
30
src/ahriman/web/schemas/auth_schema.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2023 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from marshmallow import Schema, fields
|
||||||
|
|
||||||
|
|
||||||
|
class AuthSchema(Schema):
|
||||||
|
"""
|
||||||
|
request cookie authorization schema
|
||||||
|
"""
|
||||||
|
|
||||||
|
API_SESSION = fields.String(required=True, metadata={
|
||||||
|
"description": "API session key as returned from authorization",
|
||||||
|
})
|
51
src/ahriman/web/schemas/counters_schema.py
Normal file
51
src/ahriman/web/schemas/counters_schema.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2023 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from marshmallow import Schema, fields
|
||||||
|
|
||||||
|
|
||||||
|
class CountersSchema(Schema):
|
||||||
|
"""
|
||||||
|
response package counters schema
|
||||||
|
"""
|
||||||
|
|
||||||
|
total = fields.Integer(required=True, metadata={
|
||||||
|
"description": "Total amount of packages",
|
||||||
|
"example": 6,
|
||||||
|
})
|
||||||
|
_unknown = fields.Integer(data_key="unknown", required=True, metadata={
|
||||||
|
"description": "Amount of packages in unknown state",
|
||||||
|
"example": 0,
|
||||||
|
})
|
||||||
|
pending = fields.Integer(required=True, metadata={
|
||||||
|
"description": "Amount of packages in pending state",
|
||||||
|
"example": 2,
|
||||||
|
})
|
||||||
|
building = fields.Integer(required=True, metadata={
|
||||||
|
"description": "Amount of packages in building state",
|
||||||
|
"example": 1,
|
||||||
|
})
|
||||||
|
failed = fields.Integer(required=True, metadata={
|
||||||
|
"description": "Amount of packages in failed state",
|
||||||
|
"example": 1,
|
||||||
|
})
|
||||||
|
success = fields.Integer(required=True, metadata={
|
||||||
|
"description": "Amount of packages in success state",
|
||||||
|
"example": 3,
|
||||||
|
})
|
30
src/ahriman/web/schemas/error_schema.py
Normal file
30
src/ahriman/web/schemas/error_schema.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2023 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from marshmallow import Schema, fields
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorSchema(Schema):
|
||||||
|
"""
|
||||||
|
response error schema
|
||||||
|
"""
|
||||||
|
|
||||||
|
error = fields.String(required=True, metadata={
|
||||||
|
"description": "Error description",
|
||||||
|
})
|
49
src/ahriman/web/schemas/internal_status_schema.py
Normal file
49
src/ahriman/web/schemas/internal_status_schema.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2023 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from marshmallow import Schema, fields
|
||||||
|
|
||||||
|
from ahriman import version
|
||||||
|
from ahriman.web.schemas.counters_schema import CountersSchema
|
||||||
|
from ahriman.web.schemas.status_schema import StatusSchema
|
||||||
|
|
||||||
|
|
||||||
|
class InternalStatusSchema(Schema):
|
||||||
|
"""
|
||||||
|
response service status schema
|
||||||
|
"""
|
||||||
|
|
||||||
|
architecture = fields.String(required=True, metadata={
|
||||||
|
"description": "Repository architecture",
|
||||||
|
"example": "x86_64",
|
||||||
|
})
|
||||||
|
packages = fields.Nested(CountersSchema, required=True, metadata={
|
||||||
|
"description": "Repository package counters",
|
||||||
|
})
|
||||||
|
repository = fields.String(required=True, metadata={
|
||||||
|
"description": "Repository name",
|
||||||
|
"example": "repo-clone",
|
||||||
|
})
|
||||||
|
status = fields.Nested(StatusSchema, required=True, metadata={
|
||||||
|
"description": "Repository status as stored by web service",
|
||||||
|
})
|
||||||
|
version = fields.String(required=True, metadata={
|
||||||
|
"description": "Repository version",
|
||||||
|
"example": version.__version__,
|
||||||
|
})
|
38
src/ahriman/web/schemas/log_schema.py
Normal file
38
src/ahriman/web/schemas/log_schema.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2023 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from marshmallow import Schema, fields
|
||||||
|
|
||||||
|
|
||||||
|
class LogSchema(Schema):
|
||||||
|
"""
|
||||||
|
request package log schema
|
||||||
|
"""
|
||||||
|
|
||||||
|
created = fields.Float(required=True, metadata={
|
||||||
|
"description": "Log record timestamp",
|
||||||
|
"example": 1680537091.233495,
|
||||||
|
})
|
||||||
|
process_id = fields.Integer(required=True, metadata={
|
||||||
|
"description": "Current process id",
|
||||||
|
"example": 42,
|
||||||
|
})
|
||||||
|
message = fields.String(required=True, metadata={
|
||||||
|
"description": "Log message",
|
||||||
|
})
|
35
src/ahriman/web/schemas/login_schema.py
Normal file
35
src/ahriman/web/schemas/login_schema.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2023 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from marshmallow import Schema, fields
|
||||||
|
|
||||||
|
|
||||||
|
class LoginSchema(Schema):
|
||||||
|
"""
|
||||||
|
request login schema
|
||||||
|
"""
|
||||||
|
|
||||||
|
username = fields.String(required=True, metadata={
|
||||||
|
"description": "Login username",
|
||||||
|
"example": "user",
|
||||||
|
})
|
||||||
|
password = fields.String(required=True, metadata={
|
||||||
|
"description": "Login password",
|
||||||
|
"example": "pa55w0rd",
|
||||||
|
})
|
39
src/ahriman/web/schemas/logs_schema.py
Normal file
39
src/ahriman/web/schemas/logs_schema.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2023 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from marshmallow import Schema, fields
|
||||||
|
|
||||||
|
from ahriman.web.schemas.status_schema import StatusSchema
|
||||||
|
|
||||||
|
|
||||||
|
class LogsSchema(Schema):
|
||||||
|
"""
|
||||||
|
response package logs schema
|
||||||
|
"""
|
||||||
|
|
||||||
|
package_base = fields.String(required=True, metadata={
|
||||||
|
"description": "Package base name",
|
||||||
|
"example": "ahriman",
|
||||||
|
})
|
||||||
|
status = fields.Nested(StatusSchema, required=True, metadata={
|
||||||
|
"description": "Last package status",
|
||||||
|
})
|
||||||
|
logs = fields.String(required=True, metadata={
|
||||||
|
"description": "Full package log from the last build",
|
||||||
|
})
|
30
src/ahriman/web/schemas/oauth2_schema.py
Normal file
30
src/ahriman/web/schemas/oauth2_schema.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2023 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from marshmallow import Schema, fields
|
||||||
|
|
||||||
|
|
||||||
|
class OAuth2Schema(Schema):
|
||||||
|
"""
|
||||||
|
request OAuth2 authorization schema
|
||||||
|
"""
|
||||||
|
|
||||||
|
code = fields.String(metadata={
|
||||||
|
"description": "OAuth2 authorization code. In case if not set, the redirect to provider will be initiated",
|
||||||
|
})
|
31
src/ahriman/web/schemas/package_name_schema.py
Normal file
31
src/ahriman/web/schemas/package_name_schema.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2023 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from marshmallow import Schema, fields
|
||||||
|
|
||||||
|
|
||||||
|
class PackageNameSchema(Schema):
|
||||||
|
"""
|
||||||
|
request package name schema
|
||||||
|
"""
|
||||||
|
|
||||||
|
package = fields.String(required=True, metadata={
|
||||||
|
"description": "Package name",
|
||||||
|
"example": "ahriman",
|
||||||
|
})
|
31
src/ahriman/web/schemas/package_names_schema.py
Normal file
31
src/ahriman/web/schemas/package_names_schema.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2023 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from marshmallow import Schema, fields
|
||||||
|
|
||||||
|
|
||||||
|
class PackageNamesSchema(Schema):
|
||||||
|
"""
|
||||||
|
request package names schema
|
||||||
|
"""
|
||||||
|
|
||||||
|
packages = fields.List(fields.String(), required=True, metadata={
|
||||||
|
"description": "Package names",
|
||||||
|
"example": ["ahriman"],
|
||||||
|
})
|
79
src/ahriman/web/schemas/package_properties_schema.py
Normal file
79
src/ahriman/web/schemas/package_properties_schema.py
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2023 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from marshmallow import Schema, fields
|
||||||
|
|
||||||
|
|
||||||
|
class PackagePropertiesSchema(Schema):
|
||||||
|
"""
|
||||||
|
request and response package properties schema
|
||||||
|
"""
|
||||||
|
|
||||||
|
architecture = fields.String(metadata={
|
||||||
|
"description": "Package architecture",
|
||||||
|
"example": "x86_64",
|
||||||
|
})
|
||||||
|
archive_size = fields.Integer(metadata={
|
||||||
|
"description": "Archive size in bytes",
|
||||||
|
"example": 287989,
|
||||||
|
})
|
||||||
|
build_date = fields.Integer(metadata={
|
||||||
|
"description": "Package build timestamp",
|
||||||
|
"example": 1680537091,
|
||||||
|
})
|
||||||
|
depends = fields.List(fields.String(), metadata={
|
||||||
|
"description": "Package dependencies list",
|
||||||
|
"example": ["devtools"],
|
||||||
|
})
|
||||||
|
make_depends = fields.List(fields.String(), metadata={
|
||||||
|
"description": "Package make dependencies list",
|
||||||
|
"example": ["python-build"],
|
||||||
|
})
|
||||||
|
opt_depends = fields.List(fields.String(), metadata={
|
||||||
|
"description": "Package optional dependencies list",
|
||||||
|
"example": ["python-aiohttp"],
|
||||||
|
})
|
||||||
|
description = fields.String(metadata={
|
||||||
|
"description": "Package description",
|
||||||
|
"example": "ArcH linux ReposItory MANager",
|
||||||
|
})
|
||||||
|
filename = fields.String(metadata={
|
||||||
|
"description": "Package file name",
|
||||||
|
"example": "ahriman-2.7.1-1-any.pkg.tar.zst",
|
||||||
|
})
|
||||||
|
groups = fields.List(fields.String(), metadata={
|
||||||
|
"description": "Package groups",
|
||||||
|
"example": ["base-devel"],
|
||||||
|
})
|
||||||
|
installed_size = fields.Integer(metadata={
|
||||||
|
"description": "Installed package size in bytes",
|
||||||
|
"example": 2047658,
|
||||||
|
})
|
||||||
|
licenses = fields.List(fields.String(), metadata={
|
||||||
|
"description": "Package licenses",
|
||||||
|
"example": ["GPL3"],
|
||||||
|
})
|
||||||
|
provides = fields.List(fields.String(), metadata={
|
||||||
|
"description": "Package provides list",
|
||||||
|
"example": ["ahriman-git"],
|
||||||
|
})
|
||||||
|
url = fields.String(metadata={
|
||||||
|
"description": "Upstream url",
|
||||||
|
"example": "https://github.com/arcan1s/ahriman",
|
||||||
|
})
|
46
src/ahriman/web/schemas/package_schema.py
Normal file
46
src/ahriman/web/schemas/package_schema.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2023 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from marshmallow import Schema, fields
|
||||||
|
|
||||||
|
from ahriman import version
|
||||||
|
from ahriman.web.schemas.package_properties_schema import PackagePropertiesSchema
|
||||||
|
from ahriman.web.schemas.remote_schema import RemoteSchema
|
||||||
|
|
||||||
|
|
||||||
|
class PackageSchema(Schema):
|
||||||
|
"""
|
||||||
|
request and response package schema
|
||||||
|
"""
|
||||||
|
|
||||||
|
base = fields.String(required=True, metadata={
|
||||||
|
"description": "Package base",
|
||||||
|
"example": "ahriman",
|
||||||
|
})
|
||||||
|
version = fields.String(required=True, metadata={
|
||||||
|
"description": "Package version",
|
||||||
|
"example": version.__version__,
|
||||||
|
})
|
||||||
|
remote = fields.Nested(RemoteSchema, required=True, metadata={
|
||||||
|
"description": "Package remote properties",
|
||||||
|
})
|
||||||
|
packages = fields.Dict(
|
||||||
|
keys=fields.String(), values=fields.Nested(PackagePropertiesSchema), required=True, metadata={
|
||||||
|
"description": "Packages which belong to this base",
|
||||||
|
})
|
50
src/ahriman/web/schemas/package_status_schema.py
Normal file
50
src/ahriman/web/schemas/package_status_schema.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2023 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from marshmallow import Schema, fields
|
||||||
|
|
||||||
|
from ahriman.models.build_status import BuildStatusEnum
|
||||||
|
from ahriman.web.schemas.package_schema import PackageSchema
|
||||||
|
from ahriman.web.schemas.status_schema import StatusSchema
|
||||||
|
|
||||||
|
|
||||||
|
class PackageStatusSimplifiedSchema(Schema):
|
||||||
|
"""
|
||||||
|
special request package status schema
|
||||||
|
"""
|
||||||
|
|
||||||
|
package = fields.Nested(PackageSchema, metadata={
|
||||||
|
"description": "Package description",
|
||||||
|
})
|
||||||
|
status = fields.Enum(BuildStatusEnum, by_value=True, required=True, metadata={
|
||||||
|
"description": "Current status",
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class PackageStatusSchema(Schema):
|
||||||
|
"""
|
||||||
|
response package status schema
|
||||||
|
"""
|
||||||
|
|
||||||
|
package = fields.Nested(PackageSchema, required=True, metadata={
|
||||||
|
"description": "Package description",
|
||||||
|
})
|
||||||
|
status = fields.Nested(StatusSchema, required=True, metadata={
|
||||||
|
"description": "Last package status",
|
||||||
|
})
|
35
src/ahriman/web/schemas/pgp_key_id_schema.py
Normal file
35
src/ahriman/web/schemas/pgp_key_id_schema.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2023 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from marshmallow import Schema, fields
|
||||||
|
|
||||||
|
|
||||||
|
class PGPKeyIdSchema(Schema):
|
||||||
|
"""
|
||||||
|
request PGP key ID schema
|
||||||
|
"""
|
||||||
|
|
||||||
|
key = fields.String(required=True, metadata={
|
||||||
|
"description": "PGP key ID",
|
||||||
|
"example": "0xE989490C",
|
||||||
|
})
|
||||||
|
server = fields.String(required=True, metadata={
|
||||||
|
"description": "PGP key server",
|
||||||
|
"example": "keyserver.ubuntu.com",
|
||||||
|
})
|
30
src/ahriman/web/schemas/pgp_key_schema.py
Normal file
30
src/ahriman/web/schemas/pgp_key_schema.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2023 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from marshmallow import Schema, fields
|
||||||
|
|
||||||
|
|
||||||
|
class PGPKeySchema(Schema):
|
||||||
|
"""
|
||||||
|
response PGP key schema
|
||||||
|
"""
|
||||||
|
|
||||||
|
key = fields.String(required=True, metadata={
|
||||||
|
"description": "PGP key body",
|
||||||
|
})
|
48
src/ahriman/web/schemas/remote_schema.py
Normal file
48
src/ahriman/web/schemas/remote_schema.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2023 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from marshmallow import Schema, fields
|
||||||
|
|
||||||
|
from ahriman.models.package_source import PackageSource
|
||||||
|
|
||||||
|
|
||||||
|
class RemoteSchema(Schema):
|
||||||
|
"""
|
||||||
|
request and response package remote schema
|
||||||
|
"""
|
||||||
|
|
||||||
|
branch = fields.String(required=True, metadata={
|
||||||
|
"description": "Repository branch",
|
||||||
|
"example": "master",
|
||||||
|
})
|
||||||
|
git_url = fields.String(required=True, metadata={
|
||||||
|
"description": "Package git url",
|
||||||
|
"example": "https://aur.archlinux.org/ahriman.git",
|
||||||
|
})
|
||||||
|
path = fields.String(required=True, metadata={
|
||||||
|
"description": "Path to package sources in git repository",
|
||||||
|
"example": ".",
|
||||||
|
})
|
||||||
|
source = fields.Enum(PackageSource, by_value=True, required=True, metadata={
|
||||||
|
"description": "Pacakge source",
|
||||||
|
})
|
||||||
|
web_url = fields.String(required=True, metadata={
|
||||||
|
"description": "Package repository page",
|
||||||
|
"example": "https://aur.archlinux.org/packages/ahriman",
|
||||||
|
})
|
31
src/ahriman/web/schemas/search_schema.py
Normal file
31
src/ahriman/web/schemas/search_schema.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2023 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from marshmallow import Schema, fields
|
||||||
|
|
||||||
|
|
||||||
|
class SearchSchema(Schema):
|
||||||
|
"""
|
||||||
|
request package search schema
|
||||||
|
"""
|
||||||
|
|
||||||
|
_for = fields.List(fields.String(), data_key="for", required=True, metadata={
|
||||||
|
"description": "Keyword for search",
|
||||||
|
"example": ["ahriman"],
|
||||||
|
})
|
36
src/ahriman/web/schemas/status_schema.py
Normal file
36
src/ahriman/web/schemas/status_schema.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2023 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from marshmallow import Schema, fields
|
||||||
|
|
||||||
|
from ahriman.models.build_status import BuildStatusEnum
|
||||||
|
|
||||||
|
|
||||||
|
class StatusSchema(Schema):
|
||||||
|
"""
|
||||||
|
request and response status schema
|
||||||
|
"""
|
||||||
|
|
||||||
|
status = fields.Enum(BuildStatusEnum, by_value=True, required=True, metadata={
|
||||||
|
"description": "Current status",
|
||||||
|
})
|
||||||
|
timestamp = fields.Integer(metadata={
|
||||||
|
"description": "Last update timestamp",
|
||||||
|
"example": 1680537091,
|
||||||
|
})
|
19
src/ahriman/web/views/api/__init__.py
Normal file
19
src/ahriman/web/views/api/__init__.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2023 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
46
src/ahriman/web/views/api/docs.py
Normal file
46
src/ahriman/web/views/api/docs.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2023 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import aiohttp_jinja2
|
||||||
|
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from ahriman.models.user_access import UserAccess
|
||||||
|
from ahriman.web.views.base import BaseView
|
||||||
|
|
||||||
|
|
||||||
|
class DocsView(BaseView):
|
||||||
|
"""
|
||||||
|
api docs view
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
|
||||||
|
"""
|
||||||
|
|
||||||
|
GET_PERMISSION = UserAccess.Unauthorized
|
||||||
|
|
||||||
|
@aiohttp_jinja2.template("api.jinja2")
|
||||||
|
async def get(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
return static docs html
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, Any]: parameters for jinja template
|
||||||
|
"""
|
||||||
|
return {}
|
76
src/ahriman/web/views/api/swagger.py
Normal file
76
src/ahriman/web/views/api/swagger.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2023 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from aiohttp.web import Response, json_response
|
||||||
|
from typing import Callable, Dict
|
||||||
|
|
||||||
|
from ahriman.core.util import partition
|
||||||
|
from ahriman.models.user_access import UserAccess
|
||||||
|
from ahriman.web.views.base import BaseView
|
||||||
|
|
||||||
|
|
||||||
|
class SwaggerView(BaseView):
|
||||||
|
"""
|
||||||
|
api docs specification view
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
|
||||||
|
"""
|
||||||
|
|
||||||
|
GET_PERMISSION = UserAccess.Unauthorized
|
||||||
|
|
||||||
|
async def get(self) -> Response:
|
||||||
|
"""
|
||||||
|
get api specification
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: 200 with json api specification
|
||||||
|
"""
|
||||||
|
spec = self.request.app["swagger_dict"]
|
||||||
|
is_body_parameter: Callable[[Dict[str, str]], bool] = lambda p: p["in"] == "body"
|
||||||
|
|
||||||
|
# special workaround because it writes request body to parameters section
|
||||||
|
paths = spec["paths"]
|
||||||
|
for methods in paths.values():
|
||||||
|
for method in methods.values():
|
||||||
|
if "parameters" not in method:
|
||||||
|
continue
|
||||||
|
|
||||||
|
body, other = partition(method["parameters"], is_body_parameter)
|
||||||
|
if not body:
|
||||||
|
continue # there were no ``body`` parameters found
|
||||||
|
|
||||||
|
# there should be only one body parameters
|
||||||
|
method["requestBody"] = {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": next(iter(body))["schema"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
method["parameters"] = other
|
||||||
|
|
||||||
|
# inject security schema
|
||||||
|
spec["components"]["securitySchemes"] = {
|
||||||
|
key: value
|
||||||
|
for schema in spec["security"]
|
||||||
|
for key, value in schema.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
return json_response(spec)
|
@ -19,8 +19,9 @@
|
|||||||
#
|
#
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from aiohttp.web import Request, View
|
from aiohttp_cors import CorsViewMixin # type: ignore
|
||||||
from typing import Any, Callable, Dict, List, Optional, Type, TypeVar
|
from aiohttp.web import Request, StreamResponse, View
|
||||||
|
from typing import Any, Awaitable, Callable, Dict, List, Optional, Type, TypeVar
|
||||||
|
|
||||||
from ahriman.core.auth import Auth
|
from ahriman.core.auth import Auth
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
@ -28,15 +29,19 @@ from ahriman.core.spawn import Spawn
|
|||||||
from ahriman.core.status.watcher import Watcher
|
from ahriman.core.status.watcher import Watcher
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
|
|
||||||
|
|
||||||
T = TypeVar("T", str, List[str])
|
T = TypeVar("T", str, List[str])
|
||||||
|
|
||||||
|
|
||||||
class BaseView(View):
|
class BaseView(View, CorsViewMixin):
|
||||||
"""
|
"""
|
||||||
base web view to make things typed
|
base web view to make things typed
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
OPTIONS_PERMISSION(UserAccess): (class attribute) options permissions of self
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
OPTIONS_PERMISSION = UserAccess.Unauthorized
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def configuration(self) -> Configuration:
|
def configuration(self) -> Configuration:
|
||||||
"""
|
"""
|
||||||
@ -92,7 +97,8 @@ class BaseView(View):
|
|||||||
Returns:
|
Returns:
|
||||||
UserAccess: extracted permission
|
UserAccess: extracted permission
|
||||||
"""
|
"""
|
||||||
permission: UserAccess = getattr(cls, f"{request.method.upper()}_PERMISSION", UserAccess.Full)
|
method = "GET" if (other := request.method.upper()) == "HEAD" else other
|
||||||
|
permission: UserAccess = getattr(cls, f"{method}_PERMISSION", UserAccess.Full)
|
||||||
return permission
|
return permission
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -118,23 +124,6 @@ class BaseView(View):
|
|||||||
raise KeyError(f"Key {key} is missing or empty")
|
raise KeyError(f"Key {key} is missing or empty")
|
||||||
return value
|
return value
|
||||||
|
|
||||||
async def extract_data(self, list_keys: Optional[List[str]] = None) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
extract json data from either json or form data
|
|
||||||
|
|
||||||
Args:
|
|
||||||
list_keys(Optional[List[str]], optional): optional list of keys which must be forced to list from form data
|
|
||||||
(Default value = None)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict[str, Any]: raw json object or form data converted to json
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
json: Dict[str, Any] = await self.request.json()
|
|
||||||
return json
|
|
||||||
except ValueError:
|
|
||||||
return await self.data_as_json(list_keys or [])
|
|
||||||
|
|
||||||
async def data_as_json(self, list_keys: List[str]) -> Dict[str, Any]:
|
async def data_as_json(self, list_keys: List[str]) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
extract form data and convert it to json object
|
extract form data and convert it to json object
|
||||||
@ -158,3 +147,39 @@ class BaseView(View):
|
|||||||
else:
|
else:
|
||||||
json[key] = value
|
json[key] = value
|
||||||
return json
|
return json
|
||||||
|
|
||||||
|
async def extract_data(self, list_keys: Optional[List[str]] = None) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
extract json data from either json or form data
|
||||||
|
|
||||||
|
Args:
|
||||||
|
list_keys(Optional[List[str]], optional): optional list of keys which must be forced to list from form data
|
||||||
|
(Default value = None)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, Any]: raw json object or form data converted to json
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
json: Dict[str, Any] = await self.request.json()
|
||||||
|
return json
|
||||||
|
except ValueError:
|
||||||
|
return await self.data_as_json(list_keys or [])
|
||||||
|
|
||||||
|
# pylint: disable=not-callable,protected-access
|
||||||
|
async def head(self) -> StreamResponse: # type: ignore
|
||||||
|
"""
|
||||||
|
HEAD method implementation based on the result of GET method
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPMethodNotAllowed: in case if there is no GET method implemented
|
||||||
|
"""
|
||||||
|
get_method: Optional[Callable[[], Awaitable[StreamResponse]]] = getattr(self, "get", None)
|
||||||
|
# using if/else in order to suppress mypy warning which doesn't know that
|
||||||
|
# ``_raise_allowed_methods`` raises exception
|
||||||
|
if get_method is not None:
|
||||||
|
# there is a bug in pylint, see https://github.com/pylint-dev/pylint/issues/6005
|
||||||
|
response = await get_method()
|
||||||
|
response._body = b"" # type: ignore
|
||||||
|
return response
|
||||||
|
|
||||||
|
self._raise_allowed_methods()
|
||||||
|
@ -41,10 +41,9 @@ class IndexView(BaseView):
|
|||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
|
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
|
||||||
HEAD_PERMISSION(UserAccess): (class attribute) head permissions of self
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Unauthorized
|
GET_PERMISSION = UserAccess.Unauthorized
|
||||||
|
|
||||||
@aiohttp_jinja2.template("build-status.jinja2")
|
@aiohttp_jinja2.template("build-status.jinja2")
|
||||||
async def get(self) -> Dict[str, Any]:
|
async def get(self) -> Dict[str, Any]:
|
||||||
|
@ -17,9 +17,14 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
import aiohttp_apispec # type: ignore
|
||||||
|
|
||||||
from aiohttp.web import HTTPBadRequest, HTTPNoContent
|
from aiohttp.web import HTTPBadRequest, HTTPNoContent
|
||||||
|
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
|
from ahriman.web.schemas.auth_schema import AuthSchema
|
||||||
|
from ahriman.web.schemas.error_schema import ErrorSchema
|
||||||
|
from ahriman.web.schemas.package_names_schema import PackageNamesSchema
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
|
|
||||||
|
|
||||||
@ -33,34 +38,28 @@ class AddView(BaseView):
|
|||||||
|
|
||||||
POST_PERMISSION = UserAccess.Full
|
POST_PERMISSION = UserAccess.Full
|
||||||
|
|
||||||
|
@aiohttp_apispec.docs(
|
||||||
|
tags=["Actions"],
|
||||||
|
summary="Add new package",
|
||||||
|
description="Add new package(s) from AUR",
|
||||||
|
responses={
|
||||||
|
204: {"description": "Success response"},
|
||||||
|
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
|
||||||
|
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||||
|
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||||
|
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||||
|
},
|
||||||
|
security=[{"token": [POST_PERMISSION]}],
|
||||||
|
)
|
||||||
|
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||||
|
@aiohttp_apispec.json_schema(PackageNamesSchema)
|
||||||
async def post(self) -> None:
|
async def post(self) -> None:
|
||||||
"""
|
"""
|
||||||
add new package
|
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
|
|
||||||
}
|
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
|
HTTPBadRequest: if bad data is supplied
|
||||||
HTTPNoContent: in case of success response
|
HTTPNoContent: in case of success response
|
||||||
|
|
||||||
Examples:
|
|
||||||
Example of command by using curl::
|
|
||||||
|
|
||||||
$ curl -v -H 'Content-Type: application/json' 'http://example.com/api/v1/service/add' -d '{"packages": ["ahriman"]}'
|
|
||||||
> POST /api/v1/service/add HTTP/1.1
|
|
||||||
> Host: example.com
|
|
||||||
> User-Agent: curl/7.86.0
|
|
||||||
> Accept: */*
|
|
||||||
> Content-Type: application/json
|
|
||||||
> Content-Length: 25
|
|
||||||
>
|
|
||||||
< HTTP/1.1 204 No Content
|
|
||||||
< Date: Wed, 23 Nov 2022 18:44:21 GMT
|
|
||||||
< Server: Python/3.10 aiohttp/3.8.3
|
|
||||||
<
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
data = await self.extract_data(["packages"])
|
data = await self.extract_data(["packages"])
|
||||||
|
@ -17,9 +17,15 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
import aiohttp_apispec # type: ignore
|
||||||
|
|
||||||
from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response, json_response
|
from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response, json_response
|
||||||
|
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
|
from ahriman.web.schemas.auth_schema import AuthSchema
|
||||||
|
from ahriman.web.schemas.error_schema import ErrorSchema
|
||||||
|
from ahriman.web.schemas.pgp_key_id_schema import PGPKeyIdSchema
|
||||||
|
from ahriman.web.schemas.pgp_key_schema import PGPKeySchema
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
|
|
||||||
|
|
||||||
@ -29,17 +35,31 @@ class PGPView(BaseView):
|
|||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
|
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
|
||||||
HEAD_PERMISSION(UserAccess): (class attribute) head permissions of self
|
|
||||||
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
||||||
"""
|
"""
|
||||||
|
|
||||||
POST_PERMISSION = UserAccess.Full
|
POST_PERMISSION = UserAccess.Full
|
||||||
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Reporter
|
GET_PERMISSION = UserAccess.Reporter
|
||||||
|
|
||||||
|
@aiohttp_apispec.docs(
|
||||||
|
tags=["Actions"],
|
||||||
|
summary="Search for PGP key",
|
||||||
|
description="Search for PGP key and retrieve its body",
|
||||||
|
responses={
|
||||||
|
200: {"description": "Success response", "schema": PGPKeySchema},
|
||||||
|
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
|
||||||
|
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||||
|
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||||
|
404: {"description": "Package base is unknown", "schema": ErrorSchema},
|
||||||
|
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||||
|
},
|
||||||
|
security=[{"token": [GET_PERMISSION]}],
|
||||||
|
)
|
||||||
|
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||||
|
@aiohttp_apispec.querystring_schema(PGPKeyIdSchema)
|
||||||
async def get(self) -> Response:
|
async def get(self) -> Response:
|
||||||
"""
|
"""
|
||||||
retrieve key from the key server. It supports two query parameters: ``key`` - pgp key fingerprint and
|
retrieve key from the key server
|
||||||
``server`` which points to valid PGP key server
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Response: 200 with key body on success
|
Response: 200 with key body on success
|
||||||
@ -47,24 +67,7 @@ class PGPView(BaseView):
|
|||||||
Raises:
|
Raises:
|
||||||
HTTPBadRequest: if bad data is supplied
|
HTTPBadRequest: if bad data is supplied
|
||||||
HTTPNotFound: if key wasn't found or service was unable to fetch it
|
HTTPNotFound: if key wasn't found or service was unable to fetch it
|
||||||
|
"""
|
||||||
Examples:
|
|
||||||
Example of command by using curl::
|
|
||||||
|
|
||||||
$ curl -v -H 'Accept: application/json' 'http://example.com/api/v1/service/pgp?key=0xE989490C&server=keyserver.ubuntu.com'
|
|
||||||
> GET /api/v1/service/pgp?key=0xE989490C&server=keyserver.ubuntu.com HTTP/1.1
|
|
||||||
> Host: example.com
|
|
||||||
> User-Agent: curl/7.86.0
|
|
||||||
> Accept: application/json
|
|
||||||
>
|
|
||||||
< HTTP/1.1 200 OK
|
|
||||||
< Content-Type: application/json; charset=utf-8
|
|
||||||
< Content-Length: 3275
|
|
||||||
< Date: Fri, 25 Nov 2022 22:54:02 GMT
|
|
||||||
< Server: Python/3.10 aiohttp/3.8.3
|
|
||||||
<
|
|
||||||
{"key": "key"}
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
key = self.get_non_empty(self.request.query.getone, "key")
|
key = self.get_non_empty(self.request.query.getone, "key")
|
||||||
server = self.get_non_empty(self.request.query.getone, "server")
|
server = self.get_non_empty(self.request.query.getone, "server")
|
||||||
@ -78,36 +81,28 @@ class PGPView(BaseView):
|
|||||||
|
|
||||||
return json_response({"key": key})
|
return json_response({"key": key})
|
||||||
|
|
||||||
|
@aiohttp_apispec.docs(
|
||||||
|
tags=["Actions"],
|
||||||
|
summary="Fetch PGP key",
|
||||||
|
description="Fetch PGP key from the key server",
|
||||||
|
responses={
|
||||||
|
204: {"description": "Success response"},
|
||||||
|
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
|
||||||
|
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||||
|
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||||
|
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||||
|
},
|
||||||
|
security=[{"token": [POST_PERMISSION]}],
|
||||||
|
)
|
||||||
|
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||||
|
@aiohttp_apispec.json_schema(PGPKeyIdSchema)
|
||||||
async def post(self) -> None:
|
async def post(self) -> None:
|
||||||
"""
|
"""
|
||||||
store key to the local service environment
|
store key to the local service environment
|
||||||
|
|
||||||
JSON body must be supplied, the following model is used::
|
|
||||||
|
|
||||||
{
|
|
||||||
"key": "0x8BE91E5A773FB48AC05CC1EDBED105AED6246B39", # key fingerprint to import
|
|
||||||
"server": "keyserver.ubuntu.com" # optional pgp server address
|
|
||||||
}
|
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
HTTPBadRequest: if bad data is supplied
|
HTTPBadRequest: if bad data is supplied
|
||||||
HTTPNoContent: in case of success response
|
HTTPNoContent: in case of success response
|
||||||
|
|
||||||
Examples:
|
|
||||||
Example of command by using curl::
|
|
||||||
|
|
||||||
$ curl -v -H 'Content-Type: application/json' 'http://example.com/api/v1/service/pgp' -d '{"key": "0xE989490C"}'
|
|
||||||
> POST /api/v1/service/pgp HTTP/1.1
|
|
||||||
> Host: example.com
|
|
||||||
> User-Agent: curl/7.86.0
|
|
||||||
> Accept: */*
|
|
||||||
> Content-Type: application/json
|
|
||||||
> Content-Length: 21
|
|
||||||
>
|
|
||||||
< HTTP/1.1 204 No Content
|
|
||||||
< Date: Fri, 25 Nov 2022 22:55:56 GMT
|
|
||||||
< Server: Python/3.10 aiohttp/3.8.3
|
|
||||||
<
|
|
||||||
"""
|
"""
|
||||||
data = await self.extract_data()
|
data = await self.extract_data()
|
||||||
|
|
||||||
|
@ -17,9 +17,14 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
import aiohttp_apispec # type: ignore
|
||||||
|
|
||||||
from aiohttp.web import HTTPBadRequest, HTTPNoContent
|
from aiohttp.web import HTTPBadRequest, HTTPNoContent
|
||||||
|
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
|
from ahriman.web.schemas.auth_schema import AuthSchema
|
||||||
|
from ahriman.web.schemas.error_schema import ErrorSchema
|
||||||
|
from ahriman.web.schemas.package_names_schema import PackageNamesSchema
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
|
|
||||||
|
|
||||||
@ -33,40 +38,33 @@ class RebuildView(BaseView):
|
|||||||
|
|
||||||
POST_PERMISSION = UserAccess.Full
|
POST_PERMISSION = UserAccess.Full
|
||||||
|
|
||||||
|
@aiohttp_apispec.docs(
|
||||||
|
tags=["Actions"],
|
||||||
|
summary="Rebuild packages",
|
||||||
|
description="Rebuild packages which depend on specified one",
|
||||||
|
responses={
|
||||||
|
204: {"description": "Success response"},
|
||||||
|
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
|
||||||
|
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||||
|
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||||
|
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||||
|
},
|
||||||
|
security=[{"token": [POST_PERMISSION]}],
|
||||||
|
)
|
||||||
|
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||||
|
@aiohttp_apispec.json_schema(PackageNamesSchema)
|
||||||
async def post(self) -> None:
|
async def post(self) -> None:
|
||||||
"""
|
"""
|
||||||
rebuild packages based on their dependency
|
rebuild packages based on their dependency
|
||||||
|
|
||||||
JSON body must be supplied, the following model is used::
|
|
||||||
|
|
||||||
{
|
|
||||||
"packages": ["ahriman"] # either list of packages or package name of dependency
|
|
||||||
}
|
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
HTTPBadRequest: if bad data is supplied
|
HTTPBadRequest: if bad data is supplied
|
||||||
HTTPNoContent: in case of success response
|
HTTPNoContent: in case of success response
|
||||||
|
|
||||||
Examples:
|
|
||||||
Example of command by using curl::
|
|
||||||
|
|
||||||
$ curl -v -H 'Content-Type: application/json' 'http://example.com/api/v1/service/rebuild' -d '{"packages": ["python"]}'
|
|
||||||
> POST /api/v1/service/rebuild HTTP/1.1
|
|
||||||
> Host: example.com
|
|
||||||
> User-Agent: curl/7.86.0
|
|
||||||
> Accept: */*
|
|
||||||
> Content-Type: application/json
|
|
||||||
> Content-Length: 24
|
|
||||||
>
|
|
||||||
< HTTP/1.1 204 No Content
|
|
||||||
< Date: Sun, 27 Nov 2022 00:22:26 GMT
|
|
||||||
< Server: Python/3.10 aiohttp/3.8.3
|
|
||||||
<
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
data = await self.extract_data(["packages"])
|
data = await self.extract_data(["packages"])
|
||||||
packages = self.get_non_empty(lambda key: [package for package in data[key] if package], "packages")
|
packages = self.get_non_empty(lambda key: [package for package in data[key] if package], "packages")
|
||||||
depends_on = next(package for package in packages)
|
depends_on = next(iter(packages))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPBadRequest(reason=str(e))
|
raise HTTPBadRequest(reason=str(e))
|
||||||
|
|
||||||
|
@ -17,9 +17,14 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
import aiohttp_apispec # type: ignore
|
||||||
|
|
||||||
from aiohttp.web import HTTPBadRequest, HTTPNoContent
|
from aiohttp.web import HTTPBadRequest, HTTPNoContent
|
||||||
|
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
|
from ahriman.web.schemas.auth_schema import AuthSchema
|
||||||
|
from ahriman.web.schemas.error_schema import ErrorSchema
|
||||||
|
from ahriman.web.schemas.package_names_schema import PackageNamesSchema
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
|
|
||||||
|
|
||||||
@ -33,35 +38,28 @@ class RemoveView(BaseView):
|
|||||||
|
|
||||||
POST_PERMISSION = UserAccess.Full
|
POST_PERMISSION = UserAccess.Full
|
||||||
|
|
||||||
|
@aiohttp_apispec.docs(
|
||||||
|
tags=["Actions"],
|
||||||
|
summary="Remove packages",
|
||||||
|
description="Remove specified packages from the repository",
|
||||||
|
responses={
|
||||||
|
204: {"description": "Success response"},
|
||||||
|
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
|
||||||
|
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||||
|
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||||
|
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||||
|
},
|
||||||
|
security=[{"token": [POST_PERMISSION]}],
|
||||||
|
)
|
||||||
|
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||||
|
@aiohttp_apispec.json_schema(PackageNamesSchema)
|
||||||
async def post(self) -> None:
|
async def post(self) -> None:
|
||||||
"""
|
"""
|
||||||
remove existing packages
|
remove existing packages
|
||||||
|
|
||||||
JSON body must be supplied, the following model is used::
|
|
||||||
|
|
||||||
{
|
|
||||||
"packages": ["ahriman"] # either list of packages or package name
|
|
||||||
}
|
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
HTTPBadRequest: if bad data is supplied
|
HTTPBadRequest: if bad data is supplied
|
||||||
HTTPNoContent: in case of success response
|
HTTPNoContent: in case of success response
|
||||||
|
|
||||||
Examples:
|
|
||||||
Example of command by using curl::
|
|
||||||
|
|
||||||
$ curl -v -H 'Content-Type: application/json' 'http://example.com/api/v1/service/remove' -d '{"packages": ["ahriman"]}'
|
|
||||||
> POST /api/v1/service/remove HTTP/1.1
|
|
||||||
> Host: example.com
|
|
||||||
> User-Agent: curl/7.86.0
|
|
||||||
> Accept: */*
|
|
||||||
> Content-Type: application/json
|
|
||||||
> Content-Length: 25
|
|
||||||
>
|
|
||||||
< HTTP/1.1 204 No Content
|
|
||||||
< Date: Wed, 23 Nov 2022 18:57:56 GMT
|
|
||||||
< Server: Python/3.10 aiohttp/3.8.3
|
|
||||||
<
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
data = await self.extract_data(["packages"])
|
data = await self.extract_data(["packages"])
|
||||||
|
@ -17,9 +17,14 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
import aiohttp_apispec # type: ignore
|
||||||
|
|
||||||
from aiohttp.web import HTTPBadRequest, HTTPNoContent
|
from aiohttp.web import HTTPBadRequest, HTTPNoContent
|
||||||
|
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
|
from ahriman.web.schemas.auth_schema import AuthSchema
|
||||||
|
from ahriman.web.schemas.error_schema import ErrorSchema
|
||||||
|
from ahriman.web.schemas.package_names_schema import PackageNamesSchema
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
|
|
||||||
|
|
||||||
@ -33,35 +38,28 @@ class RequestView(BaseView):
|
|||||||
|
|
||||||
POST_PERMISSION = UserAccess.Reporter
|
POST_PERMISSION = UserAccess.Reporter
|
||||||
|
|
||||||
|
@aiohttp_apispec.docs(
|
||||||
|
tags=["Actions"],
|
||||||
|
summary="Request new package",
|
||||||
|
description="Request new package(s) to be added from AUR",
|
||||||
|
responses={
|
||||||
|
204: {"description": "Success response"},
|
||||||
|
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
|
||||||
|
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||||
|
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||||
|
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||||
|
},
|
||||||
|
security=[{"token": [POST_PERMISSION]}],
|
||||||
|
)
|
||||||
|
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||||
|
@aiohttp_apispec.json_schema(PackageNamesSchema)
|
||||||
async def post(self) -> None:
|
async def post(self) -> None:
|
||||||
"""
|
"""
|
||||||
request to add new package
|
request to 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
|
|
||||||
}
|
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
HTTPBadRequest: if bad data is supplied
|
HTTPBadRequest: if bad data is supplied
|
||||||
HTTPNoContent: in case of success response
|
HTTPNoContent: in case of success response
|
||||||
|
|
||||||
Examples:
|
|
||||||
Example of command by using curl::
|
|
||||||
|
|
||||||
$ curl -v -H 'Content-Type: application/json' 'http://example.com/api/v1/service/request' -d '{"packages": ["ahriman"]}'
|
|
||||||
> POST /api/v1/service/request HTTP/1.1
|
|
||||||
> Host: example.com
|
|
||||||
> User-Agent: curl/7.86.0
|
|
||||||
> Accept: */*
|
|
||||||
> Content-Type: application/json
|
|
||||||
> Content-Length: 25
|
|
||||||
>
|
|
||||||
< HTTP/1.1 204 No Content
|
|
||||||
< Date: Wed, 23 Nov 2022 18:59:32 GMT
|
|
||||||
< Server: Python/3.10 aiohttp/3.8.3
|
|
||||||
<
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
data = await self.extract_data(["packages"])
|
data = await self.extract_data(["packages"])
|
||||||
|
@ -17,12 +17,18 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
import aiohttp_apispec # type: ignore
|
||||||
|
|
||||||
from aiohttp.web import HTTPBadRequest, HTTPNotFound, Response, json_response
|
from aiohttp.web import HTTPBadRequest, HTTPNotFound, Response, json_response
|
||||||
from typing import Callable, List
|
from typing import Callable, List
|
||||||
|
|
||||||
from ahriman.core.alpm.remote import AUR
|
from ahriman.core.alpm.remote import AUR
|
||||||
from ahriman.models.aur_package import AURPackage
|
from ahriman.models.aur_package import AURPackage
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
|
from ahriman.web.schemas.aur_package_schema import AURPackageSchema
|
||||||
|
from ahriman.web.schemas.auth_schema import AuthSchema
|
||||||
|
from ahriman.web.schemas.error_schema import ErrorSchema
|
||||||
|
from ahriman.web.schemas.search_schema import SearchSchema
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
|
|
||||||
|
|
||||||
@ -32,14 +38,29 @@ class SearchView(BaseView):
|
|||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
|
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
|
||||||
HEAD_PERMISSION(UserAccess): (class attribute) head permissions of self
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Reporter
|
GET_PERMISSION = UserAccess.Reporter
|
||||||
|
|
||||||
|
@aiohttp_apispec.docs(
|
||||||
|
tags=["Actions"],
|
||||||
|
summary="Search for package",
|
||||||
|
description="Search for package in AUR",
|
||||||
|
responses={
|
||||||
|
200: {"description": "Success response", "schema": AURPackageSchema(many=True)},
|
||||||
|
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
|
||||||
|
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||||
|
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||||
|
404: {"description": "Package base is unknown", "schema": ErrorSchema},
|
||||||
|
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||||
|
},
|
||||||
|
security=[{"token": [GET_PERMISSION]}],
|
||||||
|
)
|
||||||
|
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||||
|
@aiohttp_apispec.querystring_schema(SearchSchema)
|
||||||
async def get(self) -> Response:
|
async def get(self) -> Response:
|
||||||
"""
|
"""
|
||||||
search packages in AUR. Search string (non-empty) must be supplied as ``for`` parameter
|
search packages in AUR
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Response: 200 with found package bases and descriptions sorted by base
|
Response: 200 with found package bases and descriptions sorted by base
|
||||||
@ -47,23 +68,6 @@ class SearchView(BaseView):
|
|||||||
Raises:
|
Raises:
|
||||||
HTTPBadRequest: in case if bad data is supplied
|
HTTPBadRequest: in case if bad data is supplied
|
||||||
HTTPNotFound: if no packages found
|
HTTPNotFound: if no packages found
|
||||||
|
|
||||||
Examples:
|
|
||||||
Example of command by using curl::
|
|
||||||
|
|
||||||
$ curl -v -H 'Accept: application/json' 'http://example.com/api/v1/service/search?for=ahriman'
|
|
||||||
> GET /api/v1/service/search?for=ahriman HTTP/1.1
|
|
||||||
> Host: example.com
|
|
||||||
> User-Agent: curl/7.86.0
|
|
||||||
> Accept: application/json
|
|
||||||
>
|
|
||||||
< HTTP/1.1 200 OK
|
|
||||||
< Content-Type: application/json; charset=utf-8
|
|
||||||
< Content-Length: 148
|
|
||||||
< Date: Wed, 23 Nov 2022 19:07:13 GMT
|
|
||||||
< Server: Python/3.10 aiohttp/3.8.3
|
|
||||||
<
|
|
||||||
[{"package": "ahriman", "description": "ArcH linux ReposItory MANager"}, {"package": "ahriman-git", "description": "ArcH Linux ReposItory MANager"}]
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
search: List[str] = self.get_non_empty(lambda key: self.request.query.getall(key, default=[]), "for")
|
search: List[str] = self.get_non_empty(lambda key: self.request.query.getall(key, default=[]), "for")
|
||||||
|
@ -17,9 +17,13 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
import aiohttp_apispec # type: ignore
|
||||||
|
|
||||||
from aiohttp.web import HTTPNoContent
|
from aiohttp.web import HTTPNoContent
|
||||||
|
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
|
from ahriman.web.schemas.auth_schema import AuthSchema
|
||||||
|
from ahriman.web.schemas.error_schema import ErrorSchema
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
|
|
||||||
|
|
||||||
@ -33,26 +37,25 @@ class UpdateView(BaseView):
|
|||||||
|
|
||||||
POST_PERMISSION = UserAccess.Full
|
POST_PERMISSION = UserAccess.Full
|
||||||
|
|
||||||
|
@aiohttp_apispec.docs(
|
||||||
|
tags=["Actions"],
|
||||||
|
summary="Update packages",
|
||||||
|
description="Run repository update process",
|
||||||
|
responses={
|
||||||
|
204: {"description": "Success response"},
|
||||||
|
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||||
|
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||||
|
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||||
|
},
|
||||||
|
security=[{"token": [POST_PERMISSION]}],
|
||||||
|
)
|
||||||
|
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||||
async def post(self) -> None:
|
async def post(self) -> None:
|
||||||
"""
|
"""
|
||||||
run repository update. No parameters supported here
|
run repository update. No parameters supported here
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
HTTPNoContent: in case of success response
|
HTTPNoContent: in case of success response
|
||||||
|
|
||||||
Examples:
|
|
||||||
Example of command by using curl::
|
|
||||||
|
|
||||||
$ curl -v -XPOST 'http://example.com/api/v1/service/update'
|
|
||||||
> POST /api/v1/service/update HTTP/1.1
|
|
||||||
> Host: example.com
|
|
||||||
> User-Agent: curl/7.86.0
|
|
||||||
> Accept: */*
|
|
||||||
>
|
|
||||||
< HTTP/1.1 204 No Content
|
|
||||||
< Date: Fri, 25 Nov 2022 22:57:56 GMT
|
|
||||||
< Server: Python/3.10 aiohttp/3.8.3
|
|
||||||
<
|
|
||||||
"""
|
"""
|
||||||
self.spawner.packages_update()
|
self.spawner.packages_update()
|
||||||
|
|
||||||
|
@ -17,11 +17,18 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
import aiohttp_apispec # type: ignore
|
||||||
|
|
||||||
from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response, json_response
|
from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response, json_response
|
||||||
|
|
||||||
from ahriman.core.exceptions import UnknownPackageError
|
from ahriman.core.exceptions import UnknownPackageError
|
||||||
from ahriman.models.log_record_id import LogRecordId
|
from ahriman.models.log_record_id import LogRecordId
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
|
from ahriman.web.schemas.auth_schema import AuthSchema
|
||||||
|
from ahriman.web.schemas.error_schema import ErrorSchema
|
||||||
|
from ahriman.web.schemas.log_schema import LogSchema
|
||||||
|
from ahriman.web.schemas.logs_schema import LogsSchema
|
||||||
|
from ahriman.web.schemas.package_name_schema import PackageNameSchema
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
|
|
||||||
|
|
||||||
@ -32,39 +39,53 @@ class LogsView(BaseView):
|
|||||||
Attributes:
|
Attributes:
|
||||||
DELETE_PERMISSION(UserAccess): (class attribute) delete permissions of self
|
DELETE_PERMISSION(UserAccess): (class attribute) delete permissions of self
|
||||||
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
|
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
|
||||||
HEAD_PERMISSION(UserAccess): (class attribute) head permissions of self
|
|
||||||
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
||||||
"""
|
"""
|
||||||
|
|
||||||
DELETE_PERMISSION = POST_PERMISSION = UserAccess.Full
|
DELETE_PERMISSION = POST_PERMISSION = UserAccess.Full
|
||||||
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Reporter
|
GET_PERMISSION = UserAccess.Reporter
|
||||||
|
|
||||||
|
@aiohttp_apispec.docs(
|
||||||
|
tags=["Packages"],
|
||||||
|
summary="Delete package logs",
|
||||||
|
description="Delete all logs which belong to the specified package",
|
||||||
|
responses={
|
||||||
|
204: {"description": "Success response"},
|
||||||
|
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||||
|
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||||
|
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||||
|
},
|
||||||
|
security=[{"token": [DELETE_PERMISSION]}],
|
||||||
|
)
|
||||||
|
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||||
|
@aiohttp_apispec.match_info_schema(PackageNameSchema)
|
||||||
async def delete(self) -> None:
|
async def delete(self) -> None:
|
||||||
"""
|
"""
|
||||||
delete package logs
|
delete package logs
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
HTTPNoContent: on success response
|
HTTPNoContent: on success response
|
||||||
|
|
||||||
Examples:
|
|
||||||
Example of command by using curl::
|
|
||||||
|
|
||||||
$ curl -v -XDELETE 'http://example.com/api/v1/packages/ahriman/logs'
|
|
||||||
> DELETE /api/v1/packages/ahriman/logs HTTP/1.1
|
|
||||||
> Host: example.com
|
|
||||||
> User-Agent: curl/7.86.0
|
|
||||||
> Accept: */*
|
|
||||||
>
|
|
||||||
< HTTP/1.1 204 No Content
|
|
||||||
< Date: Wed, 23 Nov 2022 19:26:40 GMT
|
|
||||||
< Server: Python/3.10 aiohttp/3.8.3
|
|
||||||
<
|
|
||||||
"""
|
"""
|
||||||
package_base = self.request.match_info["package"]
|
package_base = self.request.match_info["package"]
|
||||||
self.service.remove_logs(package_base, None)
|
self.service.remove_logs(package_base, None)
|
||||||
|
|
||||||
raise HTTPNoContent()
|
raise HTTPNoContent()
|
||||||
|
|
||||||
|
@aiohttp_apispec.docs(
|
||||||
|
tags=["Packages"],
|
||||||
|
summary="Get package logs",
|
||||||
|
description="Retrieve all package logs and the last package status",
|
||||||
|
responses={
|
||||||
|
200: {"description": "Success response", "schema": LogsSchema},
|
||||||
|
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||||
|
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||||
|
404: {"description": "Package base is unknown", "schema": ErrorSchema},
|
||||||
|
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||||
|
},
|
||||||
|
security=[{"token": [GET_PERMISSION]}],
|
||||||
|
)
|
||||||
|
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||||
|
@aiohttp_apispec.match_info_schema(PackageNameSchema)
|
||||||
async def get(self) -> Response:
|
async def get(self) -> Response:
|
||||||
"""
|
"""
|
||||||
get last package logs
|
get last package logs
|
||||||
@ -72,22 +93,8 @@ class LogsView(BaseView):
|
|||||||
Returns:
|
Returns:
|
||||||
Response: 200 with package logs on success
|
Response: 200 with package logs on success
|
||||||
|
|
||||||
Examples:
|
Raises:
|
||||||
Example of command by using curl::
|
HTTPNotFound: if package base is unknown
|
||||||
|
|
||||||
$ curl -v -H 'Accept: application/json' 'http://example.com/api/v1/packages/ahriman/logs'
|
|
||||||
> GET /api/v1/packages/ahriman/logs HTTP/1.1
|
|
||||||
> Host: example.com
|
|
||||||
> User-Agent: curl/7.86.0
|
|
||||||
> Accept: application/json
|
|
||||||
>
|
|
||||||
< HTTP/1.1 200 OK
|
|
||||||
< Content-Type: application/json; charset=utf-8
|
|
||||||
< Content-Length: 100112
|
|
||||||
< Date: Wed, 23 Nov 2022 19:24:14 GMT
|
|
||||||
< Server: Python/3.10 aiohttp/3.8.3
|
|
||||||
<
|
|
||||||
{"package_base": "ahriman", "status": {"status": "success", "timestamp": 1669231136}, "logs": "[2022-11-23 19:17:32] clone remote https://aur.archlinux.org/ahriman.git to /tmp/tmpy9j6fq9p using branch master"}
|
|
||||||
"""
|
"""
|
||||||
package_base = self.request.match_info["package"]
|
package_base = self.request.match_info["package"]
|
||||||
|
|
||||||
@ -104,37 +111,29 @@ class LogsView(BaseView):
|
|||||||
}
|
}
|
||||||
return json_response(response)
|
return json_response(response)
|
||||||
|
|
||||||
|
@aiohttp_apispec.docs(
|
||||||
|
tags=["Packages"],
|
||||||
|
summary="Add package logs",
|
||||||
|
description="Insert new package log record",
|
||||||
|
responses={
|
||||||
|
204: {"description": "Success response"},
|
||||||
|
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
|
||||||
|
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||||
|
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||||
|
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||||
|
},
|
||||||
|
security=[{"token": [POST_PERMISSION]}],
|
||||||
|
)
|
||||||
|
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||||
|
@aiohttp_apispec.match_info_schema(PackageNameSchema)
|
||||||
|
@aiohttp_apispec.json_schema(LogSchema)
|
||||||
async def post(self) -> None:
|
async def post(self) -> None:
|
||||||
"""
|
"""
|
||||||
create new package log record
|
create new package log record
|
||||||
|
|
||||||
JSON body must be supplied, the following model is used::
|
|
||||||
|
|
||||||
{
|
|
||||||
"created": 42.001, # log record created timestamp
|
|
||||||
"message": "log message", # log record
|
|
||||||
"process_id": 42 # process id from which log record was emitted
|
|
||||||
}
|
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
HTTPBadRequest: if bad data is supplied
|
HTTPBadRequest: if bad data is supplied
|
||||||
HTTPNoContent: in case of success response
|
HTTPNoContent: in case of success response
|
||||||
|
|
||||||
Examples:
|
|
||||||
Example of command by using curl::
|
|
||||||
|
|
||||||
$ curl -v -H 'Content-Type: application/json' 'http://example.com/api/v1/packages/ahriman/logs' -d '{"created": 1669231764.042444, "message": "my log message", "process_id": 1}'
|
|
||||||
> POST /api/v1/packages/ahriman/logs HTTP/1.1
|
|
||||||
> Host: example.com
|
|
||||||
> User-Agent: curl/7.86.0
|
|
||||||
> Accept: */*
|
|
||||||
> Content-Type: application/json
|
|
||||||
> Content-Length: 76
|
|
||||||
>
|
|
||||||
< HTTP/1.1 204 No Content
|
|
||||||
< Date: Wed, 23 Nov 2022 19:30:45 GMT
|
|
||||||
< Server: Python/3.10 aiohttp/3.8.3
|
|
||||||
<
|
|
||||||
"""
|
"""
|
||||||
package_base = self.request.match_info["package"]
|
package_base = self.request.match_info["package"]
|
||||||
data = await self.extract_data()
|
data = await self.extract_data()
|
||||||
|
@ -17,12 +17,18 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
import aiohttp_apispec # type: ignore
|
||||||
|
|
||||||
from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response, json_response
|
from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response, json_response
|
||||||
|
|
||||||
from ahriman.core.exceptions import UnknownPackageError
|
from ahriman.core.exceptions import UnknownPackageError
|
||||||
from ahriman.models.build_status import BuildStatusEnum
|
from ahriman.models.build_status import BuildStatusEnum
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
|
from ahriman.web.schemas.auth_schema import AuthSchema
|
||||||
|
from ahriman.web.schemas.error_schema import ErrorSchema
|
||||||
|
from ahriman.web.schemas.package_name_schema import PackageNameSchema
|
||||||
|
from ahriman.web.schemas.package_status_schema import PackageStatusSchema, PackageStatusSimplifiedSchema
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
|
|
||||||
|
|
||||||
@ -33,39 +39,53 @@ class PackageView(BaseView):
|
|||||||
Attributes:
|
Attributes:
|
||||||
DELETE_PERMISSION(UserAccess): (class attribute) delete permissions of self
|
DELETE_PERMISSION(UserAccess): (class attribute) delete permissions of self
|
||||||
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
|
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
|
||||||
HEAD_PERMISSION(UserAccess): (class attribute) head permissions of self
|
|
||||||
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
||||||
"""
|
"""
|
||||||
|
|
||||||
DELETE_PERMISSION = POST_PERMISSION = UserAccess.Full
|
DELETE_PERMISSION = POST_PERMISSION = UserAccess.Full
|
||||||
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Read
|
GET_PERMISSION = UserAccess.Read
|
||||||
|
|
||||||
|
@aiohttp_apispec.docs(
|
||||||
|
tags=["Packages"],
|
||||||
|
summary="Delete package",
|
||||||
|
description="Delete package and its status from service",
|
||||||
|
responses={
|
||||||
|
204: {"description": "Success response"},
|
||||||
|
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||||
|
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||||
|
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||||
|
},
|
||||||
|
security=[{"token": [DELETE_PERMISSION]}],
|
||||||
|
)
|
||||||
|
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||||
|
@aiohttp_apispec.match_info_schema(PackageNameSchema)
|
||||||
async def delete(self) -> None:
|
async def delete(self) -> None:
|
||||||
"""
|
"""
|
||||||
delete package base from status page
|
delete package base from status page
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
HTTPNoContent: on success response
|
HTTPNoContent: on success response
|
||||||
|
|
||||||
Examples:
|
|
||||||
Example of command by using curl::
|
|
||||||
|
|
||||||
$ curl -v -XDELETE 'http://example.com/api/v1/packages/ahriman'
|
|
||||||
> DELETE /api/v1/packages/ahriman HTTP/1.1
|
|
||||||
> Host: example.com
|
|
||||||
> User-Agent: curl/7.86.0
|
|
||||||
> Accept: */*
|
|
||||||
>
|
|
||||||
< HTTP/1.1 204 No Content
|
|
||||||
< Date: Wed, 23 Nov 2022 19:43:40 GMT
|
|
||||||
< Server: Python/3.10 aiohttp/3.8.3
|
|
||||||
<
|
|
||||||
"""
|
"""
|
||||||
package_base = self.request.match_info["package"]
|
package_base = self.request.match_info["package"]
|
||||||
self.service.remove(package_base)
|
self.service.remove(package_base)
|
||||||
|
|
||||||
raise HTTPNoContent()
|
raise HTTPNoContent()
|
||||||
|
|
||||||
|
@aiohttp_apispec.docs(
|
||||||
|
tags=["Packages"],
|
||||||
|
summary="Get package",
|
||||||
|
description="Retrieve packages and its descriptor",
|
||||||
|
responses={
|
||||||
|
200: {"description": "Success response", "schema": PackageStatusSchema(many=True)},
|
||||||
|
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||||
|
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||||
|
404: {"description": "Package base is unknown", "schema": ErrorSchema},
|
||||||
|
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||||
|
},
|
||||||
|
security=[{"token": [GET_PERMISSION]}],
|
||||||
|
)
|
||||||
|
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||||
|
@aiohttp_apispec.match_info_schema(PackageNameSchema)
|
||||||
async def get(self) -> Response:
|
async def get(self) -> Response:
|
||||||
"""
|
"""
|
||||||
get current package base status
|
get current package base status
|
||||||
@ -75,23 +95,6 @@ class PackageView(BaseView):
|
|||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
HTTPNotFound: if no package was found
|
HTTPNotFound: if no package was found
|
||||||
|
|
||||||
Examples:
|
|
||||||
Example of command by using curl::
|
|
||||||
|
|
||||||
$ curl -v -H 'Accept: application/json' 'http://example.com/api/v1/packages/ahriman'
|
|
||||||
> GET /api/v1/packages/ahriman HTTP/1.1
|
|
||||||
> Host: example.com
|
|
||||||
> User-Agent: curl/7.86.0
|
|
||||||
> Accept: application/json
|
|
||||||
>
|
|
||||||
< HTTP/1.1 200 OK
|
|
||||||
< Content-Type: application/json; charset=utf-8
|
|
||||||
< Content-Length: 743
|
|
||||||
< Date: Wed, 23 Nov 2022 19:41:01 GMT
|
|
||||||
< Server: Python/3.10 aiohttp/3.8.3
|
|
||||||
<
|
|
||||||
[{"package": {"base": "ahriman", "version": "2.3.0-1", "remote": {"git_url": "https://aur.archlinux.org/ahriman.git", "web_url": "https://aur.archlinux.org/packages/ahriman", "path": ".", "branch": "master", "source": "aur"}, "packages": {"ahriman": {"architecture": "any", "archive_size": 247573, "build_date": 1669231069, "depends": ["devtools", "git", "pyalpm", "python-inflection", "python-passlib", "python-requests", "python-setuptools", "python-srcinfo"], "description": "ArcH linux ReposItory MANager", "filename": "ahriman-2.3.0-1-any.pkg.tar.zst", "groups": [], "installed_size": 1676153, "licenses": ["GPL3"], "provides": [], "url": "https://github.com/arcan1s/ahriman"}}}, "status": {"status": "success", "timestamp": 1669231136}}]
|
|
||||||
"""
|
"""
|
||||||
package_base = self.request.match_info["package"]
|
package_base = self.request.match_info["package"]
|
||||||
|
|
||||||
@ -108,37 +111,29 @@ class PackageView(BaseView):
|
|||||||
]
|
]
|
||||||
return json_response(response)
|
return json_response(response)
|
||||||
|
|
||||||
|
@aiohttp_apispec.docs(
|
||||||
|
tags=["Packages"],
|
||||||
|
summary="Update package",
|
||||||
|
description="Update package status and set its descriptior optionally",
|
||||||
|
responses={
|
||||||
|
204: {"description": "Success response"},
|
||||||
|
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
|
||||||
|
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||||
|
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||||
|
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||||
|
},
|
||||||
|
security=[{"token": [POST_PERMISSION]}],
|
||||||
|
)
|
||||||
|
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||||
|
@aiohttp_apispec.match_info_schema(PackageNameSchema)
|
||||||
|
@aiohttp_apispec.json_schema(PackageStatusSimplifiedSchema)
|
||||||
async def post(self) -> None:
|
async def post(self) -> None:
|
||||||
"""
|
"""
|
||||||
update package build status
|
update package build status
|
||||||
|
|
||||||
JSON body must be supplied, the following model is used::
|
|
||||||
|
|
||||||
{
|
|
||||||
"status": "unknown", # package build status string, must be valid ``BuildStatusEnum``
|
|
||||||
"package": {} # package body (use ``dataclasses.asdict`` to generate one), optional.
|
|
||||||
# Must be supplied in case if package base is unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
HTTPBadRequest: if bad data is supplied
|
HTTPBadRequest: if bad data is supplied
|
||||||
HTTPNoContent: in case of success response
|
HTTPNoContent: in case of success response
|
||||||
|
|
||||||
Examples:
|
|
||||||
Example of command by using curl::
|
|
||||||
|
|
||||||
$ curl -v -H 'Content-Type: application/json' 'http://example.com/api/v1/packages/ahriman' -d '{"status": "success"}'
|
|
||||||
> POST /api/v1/packages/ahriman HTTP/1.1
|
|
||||||
> Host: example.com
|
|
||||||
> User-Agent: curl/7.86.0
|
|
||||||
> Accept: */*
|
|
||||||
> Content-Type: application/json
|
|
||||||
> Content-Length: 21
|
|
||||||
>
|
|
||||||
< HTTP/1.1 204 No Content
|
|
||||||
< Date: Wed, 23 Nov 2022 19:42:49 GMT
|
|
||||||
< Server: Python/3.10 aiohttp/3.8.3
|
|
||||||
<
|
|
||||||
"""
|
"""
|
||||||
package_base = self.request.match_info["package"]
|
package_base = self.request.match_info["package"]
|
||||||
data = await self.extract_data()
|
data = await self.extract_data()
|
||||||
|
@ -17,9 +17,14 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
import aiohttp_apispec # type: ignore
|
||||||
|
|
||||||
from aiohttp.web import HTTPNoContent, Response, json_response
|
from aiohttp.web import HTTPNoContent, Response, json_response
|
||||||
|
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
|
from ahriman.web.schemas.auth_schema import AuthSchema
|
||||||
|
from ahriman.web.schemas.error_schema import ErrorSchema
|
||||||
|
from ahriman.web.schemas.package_status_schema import PackageStatusSchema
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
|
|
||||||
|
|
||||||
@ -29,36 +34,31 @@ class PackagesView(BaseView):
|
|||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
|
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
|
||||||
HEAD_PERMISSION(UserAccess): (class attribute) head permissions of self
|
|
||||||
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
||||||
"""
|
"""
|
||||||
|
|
||||||
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Read
|
GET_PERMISSION = UserAccess.Read
|
||||||
POST_PERMISSION = UserAccess.Full
|
POST_PERMISSION = UserAccess.Full
|
||||||
|
|
||||||
|
@aiohttp_apispec.docs(
|
||||||
|
tags=["Packages"],
|
||||||
|
summary="Get packages list",
|
||||||
|
description="Retrieve all packages and their descriptors",
|
||||||
|
responses={
|
||||||
|
200: {"description": "Success response", "schema": PackageStatusSchema(many=True)},
|
||||||
|
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||||
|
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||||
|
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||||
|
},
|
||||||
|
security=[{"token": [GET_PERMISSION]}],
|
||||||
|
)
|
||||||
|
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||||
async def get(self) -> Response:
|
async def get(self) -> Response:
|
||||||
"""
|
"""
|
||||||
get current packages status
|
get current packages status
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Response: 200 with package description on success
|
Response: 200 with package description on success
|
||||||
|
|
||||||
Examples:
|
|
||||||
Example of command by using curl::
|
|
||||||
|
|
||||||
$ curl -v -H 'Accept: application/json' 'http://example.com/api/v1/packages'
|
|
||||||
> GET /api/v1/packages HTTP/1.1
|
|
||||||
> Host: example.com
|
|
||||||
> User-Agent: curl/7.86.0
|
|
||||||
> Accept: application/json
|
|
||||||
>
|
|
||||||
< HTTP/1.1 200 OK
|
|
||||||
< Content-Type: application/json; charset=utf-8
|
|
||||||
< Content-Length: 2687
|
|
||||||
< Date: Wed, 23 Nov 2022 19:35:24 GMT
|
|
||||||
< Server: Python/3.10 aiohttp/3.8.3
|
|
||||||
<
|
|
||||||
[{"package": {"base": "ahriman", "version": "2.3.0-1", "remote": {"git_url": "https://aur.archlinux.org/ahriman.git", "web_url": "https://aur.archlinux.org/packages/ahriman", "path": ".", "branch": "master", "source": "aur"}, "packages": {"ahriman": {"architecture": "any", "archive_size": 247573, "build_date": 1669231069, "depends": ["devtools", "git", "pyalpm", "python-inflection", "python-passlib", "python-requests", "python-setuptools", "python-srcinfo"], "description": "ArcH linux ReposItory MANager", "filename": "ahriman-2.3.0-1-any.pkg.tar.zst", "groups": [], "installed_size": 1676153, "licenses": ["GPL3"], "provides": [], "url": "https://github.com/arcan1s/ahriman"}}}, "status": {"status": "success", "timestamp": 1669231136}}]
|
|
||||||
"""
|
"""
|
||||||
response = [
|
response = [
|
||||||
{
|
{
|
||||||
@ -68,26 +68,25 @@ class PackagesView(BaseView):
|
|||||||
]
|
]
|
||||||
return json_response(response)
|
return json_response(response)
|
||||||
|
|
||||||
|
@aiohttp_apispec.docs(
|
||||||
|
tags=["Packages"],
|
||||||
|
summary="Load packages",
|
||||||
|
description="Load packages from cache",
|
||||||
|
responses={
|
||||||
|
204: {"description": "Success response"},
|
||||||
|
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||||
|
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||||
|
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||||
|
},
|
||||||
|
security=[{"token": [POST_PERMISSION]}],
|
||||||
|
)
|
||||||
|
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||||
async def post(self) -> None:
|
async def post(self) -> None:
|
||||||
"""
|
"""
|
||||||
reload all packages from repository. No parameters supported here
|
reload all packages from repository
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
HTTPNoContent: on success response
|
HTTPNoContent: on success response
|
||||||
|
|
||||||
Examples:
|
|
||||||
Example of command by using curl::
|
|
||||||
|
|
||||||
$ curl -v -XPOST 'http://example.com/api/v1/packages'
|
|
||||||
> POST /api/v1/packages HTTP/1.1
|
|
||||||
> Host: example.com
|
|
||||||
> User-Agent: curl/7.86.0
|
|
||||||
> Accept: */*
|
|
||||||
>
|
|
||||||
< HTTP/1.1 204 No Content
|
|
||||||
< Date: Wed, 23 Nov 2022 19:38:06 GMT
|
|
||||||
< Server: Python/3.10 aiohttp/3.8.3
|
|
||||||
<
|
|
||||||
"""
|
"""
|
||||||
self.service.load()
|
self.service.load()
|
||||||
|
|
||||||
|
@ -17,6 +17,8 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
import aiohttp_apispec # type: ignore
|
||||||
|
|
||||||
from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
|
from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
|
||||||
|
|
||||||
from ahriman import version
|
from ahriman import version
|
||||||
@ -24,6 +26,10 @@ from ahriman.models.build_status import BuildStatusEnum
|
|||||||
from ahriman.models.counters import Counters
|
from ahriman.models.counters import Counters
|
||||||
from ahriman.models.internal_status import InternalStatus
|
from ahriman.models.internal_status import InternalStatus
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
|
from ahriman.web.schemas.auth_schema import AuthSchema
|
||||||
|
from ahriman.web.schemas.error_schema import ErrorSchema
|
||||||
|
from ahriman.web.schemas.internal_status_schema import InternalStatusSchema
|
||||||
|
from ahriman.web.schemas.status_schema import StatusSchema
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
|
|
||||||
|
|
||||||
@ -33,36 +39,31 @@ class StatusView(BaseView):
|
|||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
|
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
|
||||||
HEAD_PERMISSION(UserAccess): (class attribute) head permissions of self
|
|
||||||
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
||||||
"""
|
"""
|
||||||
|
|
||||||
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Read
|
GET_PERMISSION = UserAccess.Read
|
||||||
POST_PERMISSION = UserAccess.Full
|
POST_PERMISSION = UserAccess.Full
|
||||||
|
|
||||||
|
@aiohttp_apispec.docs(
|
||||||
|
tags=["Status"],
|
||||||
|
summary="Web service status",
|
||||||
|
description="Get web service status counters",
|
||||||
|
responses={
|
||||||
|
200: {"description": "Success response", "schema": InternalStatusSchema},
|
||||||
|
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||||
|
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||||
|
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||||
|
},
|
||||||
|
security=[{"token": [GET_PERMISSION]}],
|
||||||
|
)
|
||||||
|
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||||
async def get(self) -> Response:
|
async def get(self) -> Response:
|
||||||
"""
|
"""
|
||||||
get current service status
|
get current service status
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Response: 200 with service status object
|
Response: 200 with service status object
|
||||||
|
|
||||||
Examples:
|
|
||||||
Example of command by using curl::
|
|
||||||
|
|
||||||
$ curl -v -H 'Accept: application/json' 'http://example.com/api/v1/status'
|
|
||||||
> GET /api/v1/status HTTP/1.1
|
|
||||||
> Host: example.com
|
|
||||||
> User-Agent: curl/7.86.0
|
|
||||||
> Accept: application/json
|
|
||||||
>
|
|
||||||
< HTTP/1.1 200 OK
|
|
||||||
< Content-Type: application/json; charset=utf-8
|
|
||||||
< Content-Length: 222
|
|
||||||
< Date: Wed, 23 Nov 2022 19:32:31 GMT
|
|
||||||
< Server: Python/3.10 aiohttp/3.8.3
|
|
||||||
<
|
|
||||||
{"status": {"status": "success", "timestamp": 1669231237}, "architecture": "x86_64", "packages": {"total": 4, "unknown": 0, "pending": 0, "building": 0, "failed": 0, "success": 4}, "repository": "repo", "version": "2.3.0"}
|
|
||||||
"""
|
"""
|
||||||
counters = Counters.from_packages(self.service.packages)
|
counters = Counters.from_packages(self.service.packages)
|
||||||
status = InternalStatus(
|
status = InternalStatus(
|
||||||
@ -74,35 +75,28 @@ class StatusView(BaseView):
|
|||||||
|
|
||||||
return json_response(status.view())
|
return json_response(status.view())
|
||||||
|
|
||||||
|
@aiohttp_apispec.docs(
|
||||||
|
tags=["Status"],
|
||||||
|
summary="Set web service status",
|
||||||
|
description="Update web service status. Counters will remain unchanged",
|
||||||
|
responses={
|
||||||
|
204: {"description": "Success response"},
|
||||||
|
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
|
||||||
|
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||||
|
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||||
|
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||||
|
},
|
||||||
|
security=[{"token": [POST_PERMISSION]}],
|
||||||
|
)
|
||||||
|
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||||
|
@aiohttp_apispec.json_schema(StatusSchema)
|
||||||
async def post(self) -> None:
|
async def post(self) -> None:
|
||||||
"""
|
"""
|
||||||
update service status
|
update service status
|
||||||
|
|
||||||
JSON body must be supplied, the following model is used::
|
|
||||||
|
|
||||||
{
|
|
||||||
"status": "unknown", # service status string, must be valid ``BuildStatusEnum``
|
|
||||||
}
|
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
HTTPBadRequest: if bad data is supplied
|
HTTPBadRequest: if bad data is supplied
|
||||||
HTTPNoContent: in case of success response
|
HTTPNoContent: in case of success response
|
||||||
|
|
||||||
Examples:
|
|
||||||
Example of command by using curl::
|
|
||||||
|
|
||||||
$ curl -v -H 'Content-Type: application/json' 'http://example.com/api/v1/status' -d '{"status": "success"}'
|
|
||||||
> POST /api/v1/status HTTP/1.1
|
|
||||||
> Host: example.com
|
|
||||||
> User-Agent: curl/7.86.0
|
|
||||||
> Accept: */*
|
|
||||||
> Content-Type: application/json
|
|
||||||
> Content-Length: 21
|
|
||||||
>
|
|
||||||
< HTTP/1.1 204 No Content
|
|
||||||
< Date: Wed, 23 Nov 2022 19:33:57 GMT
|
|
||||||
< Server: Python/3.10 aiohttp/3.8.3
|
|
||||||
<
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
data = await self.extract_data()
|
data = await self.extract_data()
|
||||||
|
@ -17,10 +17,15 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
import aiohttp_apispec # type: ignore
|
||||||
|
|
||||||
from aiohttp.web import HTTPFound, HTTPMethodNotAllowed, HTTPUnauthorized
|
from aiohttp.web import HTTPFound, HTTPMethodNotAllowed, HTTPUnauthorized
|
||||||
|
|
||||||
from ahriman.core.auth.helpers import remember
|
from ahriman.core.auth.helpers import remember
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
|
from ahriman.web.schemas.error_schema import ErrorSchema
|
||||||
|
from ahriman.web.schemas.login_schema import LoginSchema
|
||||||
|
from ahriman.web.schemas.oauth2_schema import OAuth2Schema
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
|
|
||||||
|
|
||||||
@ -35,6 +40,18 @@ class LoginView(BaseView):
|
|||||||
|
|
||||||
GET_PERMISSION = POST_PERMISSION = UserAccess.Unauthorized
|
GET_PERMISSION = POST_PERMISSION = UserAccess.Unauthorized
|
||||||
|
|
||||||
|
@aiohttp_apispec.docs(
|
||||||
|
tags=["Login"],
|
||||||
|
summary="Login via OAuth2",
|
||||||
|
description="Login by using OAuth2 authorization code. Only available if OAuth2 is enabled",
|
||||||
|
responses={
|
||||||
|
302: {"description": "Success response"},
|
||||||
|
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||||
|
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||||
|
},
|
||||||
|
security=[{"token": [GET_PERMISSION]}],
|
||||||
|
)
|
||||||
|
@aiohttp_apispec.querystring_schema(OAuth2Schema)
|
||||||
async def get(self) -> None:
|
async def get(self) -> None:
|
||||||
"""
|
"""
|
||||||
OAuth2 response handler
|
OAuth2 response handler
|
||||||
@ -48,9 +65,6 @@ class LoginView(BaseView):
|
|||||||
HTTPFound: on success response
|
HTTPFound: on success response
|
||||||
HTTPMethodNotAllowed: in case if method is used, but OAuth is disabled
|
HTTPMethodNotAllowed: in case if method is used, but OAuth is disabled
|
||||||
HTTPUnauthorized: if case of authorization error
|
HTTPUnauthorized: if case of authorization error
|
||||||
|
|
||||||
Examples:
|
|
||||||
This request must not be used directly.
|
|
||||||
"""
|
"""
|
||||||
from ahriman.core.auth.oauth import OAuth
|
from ahriman.core.auth.oauth import OAuth
|
||||||
|
|
||||||
@ -70,43 +84,25 @@ class LoginView(BaseView):
|
|||||||
|
|
||||||
raise HTTPUnauthorized()
|
raise HTTPUnauthorized()
|
||||||
|
|
||||||
|
@aiohttp_apispec.docs(
|
||||||
|
tags=["Login"],
|
||||||
|
summary="Login via basic authorization",
|
||||||
|
description="Login by using username and password",
|
||||||
|
responses={
|
||||||
|
302: {"description": "Success response"},
|
||||||
|
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||||
|
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||||
|
},
|
||||||
|
security=[{"token": [POST_PERMISSION]}],
|
||||||
|
)
|
||||||
|
@aiohttp_apispec.json_schema(LoginSchema)
|
||||||
async def post(self) -> None:
|
async def post(self) -> None:
|
||||||
"""
|
"""
|
||||||
login user to service
|
login user to service. The authentication session will be passed in ``Set-Cookie`` header.
|
||||||
|
|
||||||
either JSON body or form data must be supplied the following fields are required::
|
|
||||||
|
|
||||||
{
|
|
||||||
"username": "username", # username to use for login
|
|
||||||
"password": "pa55w0rd" # password to use for login
|
|
||||||
}
|
|
||||||
|
|
||||||
The authentication session will be passed in ``Set-Cookie`` header.
|
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
HTTPFound: on success response
|
HTTPFound: on success response
|
||||||
HTTPUnauthorized: if case of authorization error
|
HTTPUnauthorized: if case of authorization error
|
||||||
|
|
||||||
Examples:
|
|
||||||
Example of command by using curl::
|
|
||||||
|
|
||||||
$ curl -v -H 'Content-Type: application/json' 'http://example.com/api/v1/login' -d '{"username": "test", "password": "test"}'
|
|
||||||
> POST /api/v1/login HTTP/1.1
|
|
||||||
> Host: example.com
|
|
||||||
> User-Agent: curl/7.86.0
|
|
||||||
> Accept: */*
|
|
||||||
> Content-Type: application/json
|
|
||||||
> Content-Length: 40
|
|
||||||
>
|
|
||||||
< HTTP/1.1 302 Found
|
|
||||||
< Content-Type: text/plain; charset=utf-8
|
|
||||||
< Location: /
|
|
||||||
< Content-Length: 10
|
|
||||||
< Set-Cookie: ...
|
|
||||||
< Date: Wed, 23 Nov 2022 17:51:27 GMT
|
|
||||||
< Server: Python/3.10 aiohttp/3.8.3
|
|
||||||
<
|
|
||||||
302: Found
|
|
||||||
"""
|
"""
|
||||||
data = await self.extract_data()
|
data = await self.extract_data()
|
||||||
identity = data.get("username")
|
identity = data.get("username")
|
||||||
|
@ -17,10 +17,14 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
import aiohttp_apispec # type: ignore
|
||||||
|
|
||||||
from aiohttp.web import HTTPFound, HTTPUnauthorized
|
from aiohttp.web import HTTPFound, HTTPUnauthorized
|
||||||
|
|
||||||
from ahriman.core.auth.helpers import check_authorized, forget
|
from ahriman.core.auth.helpers import check_authorized, forget
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
|
from ahriman.web.schemas.auth_schema import AuthSchema
|
||||||
|
from ahriman.web.schemas.error_schema import ErrorSchema
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
|
|
||||||
|
|
||||||
@ -34,33 +38,26 @@ class LogoutView(BaseView):
|
|||||||
|
|
||||||
POST_PERMISSION = UserAccess.Unauthorized
|
POST_PERMISSION = UserAccess.Unauthorized
|
||||||
|
|
||||||
|
@aiohttp_apispec.docs(
|
||||||
|
tags=["Login"],
|
||||||
|
summary="Logout",
|
||||||
|
description="Logout user and remove authorization cookies",
|
||||||
|
responses={
|
||||||
|
302: {"description": "Success response"},
|
||||||
|
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||||
|
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||||
|
},
|
||||||
|
security=[{"token": [POST_PERMISSION]}],
|
||||||
|
)
|
||||||
|
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||||
async def post(self) -> None:
|
async def post(self) -> None:
|
||||||
"""
|
"""
|
||||||
logout user from the service. No parameters supported here.
|
logout user from the service
|
||||||
|
|
||||||
The server will respond with ``Set-Cookie`` header, in which API session cookie will be nullified.
|
The server will respond with ``Set-Cookie`` header, in which API session cookie will be nullified.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
HTTPFound: on success response
|
HTTPFound: on success response
|
||||||
|
|
||||||
Examples:
|
|
||||||
Example of command by using curl::
|
|
||||||
|
|
||||||
$ curl -v -XPOST 'http://example.com/api/v1/logout'
|
|
||||||
> POST /api/v1/logout HTTP/1.1
|
|
||||||
> Host: example.com
|
|
||||||
> User-Agent: curl/7.86.0
|
|
||||||
> Accept: */*
|
|
||||||
>
|
|
||||||
< HTTP/1.1 302 Found
|
|
||||||
< Content-Type: text/plain; charset=utf-8
|
|
||||||
< Location: /
|
|
||||||
< Content-Length: 10
|
|
||||||
< Set-Cookie: ...
|
|
||||||
< Date: Wed, 23 Nov 2022 19:10:51 GMT
|
|
||||||
< Server: Python/3.10 aiohttp/3.8.3
|
|
||||||
<
|
|
||||||
302: Found
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
await check_authorized(self.request)
|
await check_authorized(self.request)
|
||||||
|
@ -22,7 +22,7 @@ import jinja2
|
|||||||
import logging
|
import logging
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp.web import Application, normalize_path_middleware, run_app
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from ahriman.core.auth import Auth
|
from ahriman.core.auth import Auth
|
||||||
@ -32,20 +32,22 @@ from ahriman.core.exceptions import InitializeError
|
|||||||
from ahriman.core.log.filtered_access_logger import FilteredAccessLogger
|
from ahriman.core.log.filtered_access_logger import FilteredAccessLogger
|
||||||
from ahriman.core.spawn import Spawn
|
from ahriman.core.spawn import Spawn
|
||||||
from ahriman.core.status.watcher import Watcher
|
from ahriman.core.status.watcher import Watcher
|
||||||
|
from ahriman.web.apispec import setup_apispec
|
||||||
|
from ahriman.web.cors import setup_cors
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["create_socket", "on_shutdown", "on_startup", "run_server", "setup_service"]
|
__all__ = ["run_server", "setup_service"]
|
||||||
|
|
||||||
|
|
||||||
def create_socket(configuration: Configuration, application: web.Application) -> Optional[socket.socket]:
|
def _create_socket(configuration: Configuration, application: Application) -> Optional[socket.socket]:
|
||||||
"""
|
"""
|
||||||
create unix socket based on configuration option
|
create unix socket based on configuration option
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
configuration(Configuration): configuration instance
|
configuration(Configuration): configuration instance
|
||||||
application(web.Application): web application instance
|
application(Application): web application instance
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Optional[socket.socket]: unix socket object if set by option
|
Optional[socket.socket]: unix socket object if set by option
|
||||||
@ -64,7 +66,7 @@ def create_socket(configuration: Configuration, application: web.Application) ->
|
|||||||
unix_socket.chmod(0o666) # for the glory of satan of course
|
unix_socket.chmod(0o666) # for the glory of satan of course
|
||||||
|
|
||||||
# register socket removal
|
# register socket removal
|
||||||
async def remove_socket(_: web.Application) -> None:
|
async def remove_socket(_: Application) -> None:
|
||||||
unix_socket.unlink(missing_ok=True)
|
unix_socket.unlink(missing_ok=True)
|
||||||
|
|
||||||
application.on_shutdown.append(remove_socket)
|
application.on_shutdown.append(remove_socket)
|
||||||
@ -72,22 +74,22 @@ def create_socket(configuration: Configuration, application: web.Application) ->
|
|||||||
return sock
|
return sock
|
||||||
|
|
||||||
|
|
||||||
async def on_shutdown(application: web.Application) -> None:
|
async def _on_shutdown(application: Application) -> None:
|
||||||
"""
|
"""
|
||||||
web application shutdown handler
|
web application shutdown handler
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
application(web.Application): web application instance
|
application(Application): web application instance
|
||||||
"""
|
"""
|
||||||
application.logger.warning("server terminated")
|
application.logger.warning("server terminated")
|
||||||
|
|
||||||
|
|
||||||
async def on_startup(application: web.Application) -> None:
|
async def _on_startup(application: Application) -> None:
|
||||||
"""
|
"""
|
||||||
web application start handler
|
web application start handler
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
application(web.Application): web application instance
|
application(Application): web application instance
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
InitializeError: in case if matched could not be loaded
|
InitializeError: in case if matched could not be loaded
|
||||||
@ -101,25 +103,25 @@ async def on_startup(application: web.Application) -> None:
|
|||||||
raise InitializeError(message)
|
raise InitializeError(message)
|
||||||
|
|
||||||
|
|
||||||
def run_server(application: web.Application) -> None:
|
def run_server(application: Application) -> None:
|
||||||
"""
|
"""
|
||||||
run web application
|
run web application
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
application(web.Application): web application instance
|
application(Application): web application instance
|
||||||
"""
|
"""
|
||||||
application.logger.info("start server")
|
application.logger.info("start server")
|
||||||
|
|
||||||
configuration: Configuration = application["configuration"]
|
configuration: Configuration = application["configuration"]
|
||||||
host = configuration.get("web", "host")
|
host = configuration.get("web", "host")
|
||||||
port = configuration.getint("web", "port")
|
port = configuration.getint("web", "port")
|
||||||
unix_socket = create_socket(configuration, application)
|
unix_socket = _create_socket(configuration, application)
|
||||||
|
|
||||||
web.run_app(application, host=host, port=port, sock=unix_socket, handle_signals=True,
|
run_app(application, host=host, port=port, sock=unix_socket, handle_signals=True,
|
||||||
access_log=logging.getLogger("http"), access_log_class=FilteredAccessLogger)
|
access_log=logging.getLogger("http"), access_log_class=FilteredAccessLogger)
|
||||||
|
|
||||||
|
|
||||||
def setup_service(architecture: str, configuration: Configuration, spawner: Spawn) -> web.Application:
|
def setup_service(architecture: str, configuration: Configuration, spawner: Spawn) -> Application:
|
||||||
"""
|
"""
|
||||||
create web application
|
create web application
|
||||||
|
|
||||||
@ -129,18 +131,21 @@ def setup_service(architecture: str, configuration: Configuration, spawner: Spaw
|
|||||||
spawner(Spawn): spawner thread
|
spawner(Spawn): spawner thread
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
web.Application: web application instance
|
Application: web application instance
|
||||||
"""
|
"""
|
||||||
application = web.Application(logger=logging.getLogger(__name__))
|
application = Application(logger=logging.getLogger(__name__))
|
||||||
application.on_shutdown.append(on_shutdown)
|
application.on_shutdown.append(_on_shutdown)
|
||||||
application.on_startup.append(on_startup)
|
application.on_startup.append(_on_startup)
|
||||||
|
|
||||||
application.middlewares.append(web.normalize_path_middleware(append_slash=False, remove_slash=True))
|
application.middlewares.append(normalize_path_middleware(append_slash=False, remove_slash=True))
|
||||||
application.middlewares.append(exception_handler(application.logger))
|
application.middlewares.append(exception_handler(application.logger))
|
||||||
|
|
||||||
application.logger.info("setup routes")
|
application.logger.info("setup routes")
|
||||||
setup_routes(application, configuration.getpath("web", "static_path"))
|
setup_routes(application, configuration.getpath("web", "static_path"))
|
||||||
|
|
||||||
|
application.logger.info("setup CORS")
|
||||||
|
setup_cors(application)
|
||||||
|
|
||||||
application.logger.info("setup templates")
|
application.logger.info("setup templates")
|
||||||
aiohttp_jinja2.setup(application, loader=jinja2.FileSystemLoader(configuration.getpath("web", "templates")))
|
aiohttp_jinja2.setup(application, loader=jinja2.FileSystemLoader(configuration.getpath("web", "templates")))
|
||||||
|
|
||||||
@ -170,4 +175,7 @@ def setup_service(architecture: str, configuration: Configuration, spawner: Spaw
|
|||||||
from ahriman.web.middlewares.auth_handler import setup_auth
|
from ahriman.web.middlewares.auth_handler import setup_auth
|
||||||
setup_auth(application, configuration, validator)
|
setup_auth(application, configuration, validator)
|
||||||
|
|
||||||
|
application.logger.info("setup api docs")
|
||||||
|
setup_apispec(application)
|
||||||
|
|
||||||
return application
|
return application
|
||||||
|
@ -12,7 +12,7 @@ from unittest.mock import MagicMock
|
|||||||
|
|
||||||
from ahriman.core.exceptions import BuildError, OptionError, UnsafeRunError
|
from ahriman.core.exceptions import BuildError, OptionError, UnsafeRunError
|
||||||
from ahriman.core.util import check_output, check_user, enum_values, exception_response_text, filter_json, \
|
from ahriman.core.util import check_output, check_user, enum_values, exception_response_text, filter_json, \
|
||||||
full_version, package_like, pretty_datetime, pretty_size, safe_filename, trim_package, utcnow, walk
|
full_version, package_like, partition, pretty_datetime, pretty_size, safe_filename, trim_package, utcnow, walk
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
from ahriman.models.package_source import PackageSource
|
from ahriman.models.package_source import PackageSource
|
||||||
from ahriman.models.repository_paths import RepositoryPaths
|
from ahriman.models.repository_paths import RepositoryPaths
|
||||||
@ -228,6 +228,15 @@ def test_package_like_sig(package_ahriman: Package) -> None:
|
|||||||
assert not package_like(sig_file)
|
assert not package_like(sig_file)
|
||||||
|
|
||||||
|
|
||||||
|
def test_partition() -> None:
|
||||||
|
"""
|
||||||
|
must partition list based on predicate
|
||||||
|
"""
|
||||||
|
even, odd = partition([1, 4, 2, 1, 3, 4], lambda i: i % 2 == 0)
|
||||||
|
assert even == [4, 2, 4]
|
||||||
|
assert odd == [1, 1, 3]
|
||||||
|
|
||||||
|
|
||||||
def test_pretty_datetime() -> None:
|
def test_pretty_datetime() -> None:
|
||||||
"""
|
"""
|
||||||
must generate string from timestamp value
|
must generate string from timestamp value
|
||||||
@ -371,6 +380,7 @@ def test_walk(resource_path_root: Path) -> None:
|
|||||||
resource_path_root / "web" / "templates" / "static" / "favicon.ico",
|
resource_path_root / "web" / "templates" / "static" / "favicon.ico",
|
||||||
resource_path_root / "web" / "templates" / "utils" / "bootstrap-scripts.jinja2",
|
resource_path_root / "web" / "templates" / "utils" / "bootstrap-scripts.jinja2",
|
||||||
resource_path_root / "web" / "templates" / "utils" / "style.jinja2",
|
resource_path_root / "web" / "templates" / "utils" / "style.jinja2",
|
||||||
|
resource_path_root / "web" / "templates" / "api.jinja2",
|
||||||
resource_path_root / "web" / "templates" / "build-status.jinja2",
|
resource_path_root / "web" / "templates" / "build-status.jinja2",
|
||||||
resource_path_root / "web" / "templates" / "email-index.jinja2",
|
resource_path_root / "web" / "templates" / "email-index.jinja2",
|
||||||
resource_path_root / "web" / "templates" / "error.jinja2",
|
resource_path_root / "web" / "templates" / "error.jinja2",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from asyncio import BaseEventLoop
|
from asyncio import BaseEventLoop
|
||||||
from aiohttp import web
|
from aiohttp.web import Application, Resource, UrlMappingMatchInfo
|
||||||
from aiohttp.test_utils import TestClient
|
from aiohttp.test_utils import TestClient
|
||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
@ -19,29 +19,35 @@ from ahriman.web.web import setup_service
|
|||||||
|
|
||||||
|
|
||||||
@pytest.helpers.register
|
@pytest.helpers.register
|
||||||
def request(app: web.Application, path: str, method: str, json: Any = None, data: Any = None,
|
def request(application: Application, path: str, method: str, json: Any = None, data: Any = None,
|
||||||
extra: Optional[Dict[str, Any]] = None) -> MagicMock:
|
extra: Optional[Dict[str, Any]] = None, resource: Optional[Resource] = None) -> MagicMock:
|
||||||
"""
|
"""
|
||||||
request generator helper
|
request generator helper
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
app(web.Application): application fixture
|
application(Application): application fixture
|
||||||
path(str): path for the request
|
path(str): path for the request
|
||||||
method(str): method for the request
|
method(str): method for the request
|
||||||
json(Any, optional): json payload of the request (Default value = None)
|
json(Any, optional): json payload of the request (Default value = None)
|
||||||
data(Any, optional): form data payload of the request (Default value = None)
|
data(Any, optional): form data payload of the request (Default value = None)
|
||||||
extra(Optional[Dict[str, Any]], optional): extra info which will be injected for ``get_extra_info`` command
|
extra(Optional[Dict[str, Any]], optional): extra info which will be injected for ``get_extra_info`` command
|
||||||
|
resource(Optional[Resource], optional): optional web resource for the request (Default value = None)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
MagicMock: dummy request mock
|
MagicMock: dummy request mock
|
||||||
"""
|
"""
|
||||||
request_mock = MagicMock()
|
request_mock = MagicMock()
|
||||||
request_mock.app = app
|
request_mock.app = application
|
||||||
request_mock.path = path
|
request_mock.path = path
|
||||||
request_mock.method = method
|
request_mock.method = method
|
||||||
request_mock.json = json
|
request_mock.json = json
|
||||||
request_mock.post = data
|
request_mock.post = data
|
||||||
|
|
||||||
|
if resource is not None:
|
||||||
|
route_mock = MagicMock()
|
||||||
|
route_mock.resource = resource
|
||||||
|
request_mock.match_info = UrlMappingMatchInfo({}, route_mock)
|
||||||
|
|
||||||
extra = extra or {}
|
extra = extra or {}
|
||||||
request_mock.get_extra_info.side_effect = lambda key: extra.get(key)
|
request_mock.get_extra_info.side_effect = lambda key: extra.get(key)
|
||||||
|
|
||||||
@ -50,7 +56,7 @@ def request(app: web.Application, path: str, method: str, json: Any = None, data
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def application(configuration: Configuration, spawner: Spawn, database: SQLite, repository: Repository,
|
def application(configuration: Configuration, spawner: Spawn, database: SQLite, repository: Repository,
|
||||||
mocker: MockerFixture) -> web.Application:
|
mocker: MockerFixture) -> Application:
|
||||||
"""
|
"""
|
||||||
application fixture
|
application fixture
|
||||||
|
|
||||||
@ -62,17 +68,19 @@ def application(configuration: Configuration, spawner: Spawn, database: SQLite,
|
|||||||
mocker(MockerFixture): mocker object
|
mocker(MockerFixture): mocker object
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
web.Application: application test instance
|
Application: application test instance
|
||||||
"""
|
"""
|
||||||
|
configuration.set_option("web", "port", "8080")
|
||||||
mocker.patch("ahriman.core.database.SQLite.load", return_value=database)
|
mocker.patch("ahriman.core.database.SQLite.load", return_value=database)
|
||||||
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
|
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
|
||||||
|
mocker.patch("aiohttp_apispec.setup_aiohttp_apispec")
|
||||||
mocker.patch.object(ahriman.core.auth.helpers, "_has_aiohttp_security", False)
|
mocker.patch.object(ahriman.core.auth.helpers, "_has_aiohttp_security", False)
|
||||||
return setup_service("x86_64", configuration, spawner)
|
return setup_service("x86_64", configuration, spawner)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def application_with_auth(configuration: Configuration, user: User, spawner: Spawn, database: SQLite,
|
def application_with_auth(configuration: Configuration, user: User, spawner: Spawn, database: SQLite,
|
||||||
repository: Repository, mocker: MockerFixture) -> web.Application:
|
repository: Repository, mocker: MockerFixture) -> Application:
|
||||||
"""
|
"""
|
||||||
application fixture with auth enabled
|
application fixture with auth enabled
|
||||||
|
|
||||||
@ -85,11 +93,13 @@ def application_with_auth(configuration: Configuration, user: User, spawner: Spa
|
|||||||
mocker(MockerFixture): mocker object
|
mocker(MockerFixture): mocker object
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
web.Application: application test instance
|
Application: application test instance
|
||||||
"""
|
"""
|
||||||
configuration.set_option("auth", "target", "configuration")
|
configuration.set_option("auth", "target", "configuration")
|
||||||
|
configuration.set_option("web", "port", "8080")
|
||||||
mocker.patch("ahriman.core.database.SQLite.load", return_value=database)
|
mocker.patch("ahriman.core.database.SQLite.load", return_value=database)
|
||||||
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
|
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
|
||||||
|
mocker.patch("aiohttp_apispec.setup_aiohttp_apispec")
|
||||||
mocker.patch.object(ahriman.core.auth.helpers, "_has_aiohttp_security", True)
|
mocker.patch.object(ahriman.core.auth.helpers, "_has_aiohttp_security", True)
|
||||||
application = setup_service("x86_64", configuration, spawner)
|
application = setup_service("x86_64", configuration, spawner)
|
||||||
|
|
||||||
@ -101,7 +111,7 @@ def application_with_auth(configuration: Configuration, user: User, spawner: Spa
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def application_with_debug(configuration: Configuration, user: User, spawner: Spawn, database: SQLite,
|
def application_with_debug(configuration: Configuration, user: User, spawner: Spawn, database: SQLite,
|
||||||
repository: Repository, mocker: MockerFixture) -> web.Application:
|
repository: Repository, mocker: MockerFixture) -> Application:
|
||||||
"""
|
"""
|
||||||
application fixture with debug enabled
|
application fixture with debug enabled
|
||||||
|
|
||||||
@ -114,23 +124,25 @@ def application_with_debug(configuration: Configuration, user: User, spawner: Sp
|
|||||||
mocker(MockerFixture): mocker object
|
mocker(MockerFixture): mocker object
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
web.Application: application test instance
|
Application: application test instance
|
||||||
"""
|
"""
|
||||||
configuration.set_option("web", "debug", "yes")
|
configuration.set_option("web", "debug", "yes")
|
||||||
|
configuration.set_option("web", "port", "8080")
|
||||||
mocker.patch("ahriman.core.database.SQLite.load", return_value=database)
|
mocker.patch("ahriman.core.database.SQLite.load", return_value=database)
|
||||||
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
|
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
|
||||||
|
mocker.patch("aiohttp_apispec.setup_aiohttp_apispec")
|
||||||
mocker.patch.object(ahriman.core.auth.helpers, "_has_aiohttp_security", False)
|
mocker.patch.object(ahriman.core.auth.helpers, "_has_aiohttp_security", False)
|
||||||
return setup_service("x86_64", configuration, spawner)
|
return setup_service("x86_64", configuration, spawner)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def client(application: web.Application, event_loop: BaseEventLoop,
|
def client(application: Application, event_loop: BaseEventLoop,
|
||||||
aiohttp_client: Any, mocker: MockerFixture) -> TestClient:
|
aiohttp_client: Any, mocker: MockerFixture) -> TestClient:
|
||||||
"""
|
"""
|
||||||
web client fixture
|
web client fixture
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
application(web.Application): application fixture
|
application(Application): application fixture
|
||||||
event_loop(BaseEventLoop): context event loop
|
event_loop(BaseEventLoop): context event loop
|
||||||
aiohttp_client(Any): aiohttp client fixture
|
aiohttp_client(Any): aiohttp client fixture
|
||||||
mocker(MockerFixture): mocker object
|
mocker(MockerFixture): mocker object
|
||||||
@ -143,13 +155,13 @@ def client(application: web.Application, event_loop: BaseEventLoop,
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def client_with_auth(application_with_auth: web.Application, event_loop: BaseEventLoop,
|
def client_with_auth(application_with_auth: Application, event_loop: BaseEventLoop,
|
||||||
aiohttp_client: Any, mocker: MockerFixture) -> TestClient:
|
aiohttp_client: Any, mocker: MockerFixture) -> TestClient:
|
||||||
"""
|
"""
|
||||||
web client fixture with full authorization functions
|
web client fixture with full authorization functions
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
application_with_auth(web.Application): application fixture
|
application_with_auth(Application): application fixture
|
||||||
event_loop(BaseEventLoop): context event loop
|
event_loop(BaseEventLoop): context event loop
|
||||||
aiohttp_client(Any): aiohttp client fixture
|
aiohttp_client(Any): aiohttp client fixture
|
||||||
mocker(MockerFixture): mocker object
|
mocker(MockerFixture): mocker object
|
||||||
@ -162,13 +174,13 @@ def client_with_auth(application_with_auth: web.Application, event_loop: BaseEve
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def client_with_oauth_auth(application_with_auth: web.Application, event_loop: BaseEventLoop,
|
def client_with_oauth_auth(application_with_auth: Application, event_loop: BaseEventLoop,
|
||||||
aiohttp_client: Any, mocker: MockerFixture) -> TestClient:
|
aiohttp_client: Any, mocker: MockerFixture) -> TestClient:
|
||||||
"""
|
"""
|
||||||
web client fixture with full authorization functions
|
web client fixture with full authorization functions
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
application_with_auth(web.Application): application fixture
|
application_with_auth(Application): application fixture
|
||||||
event_loop(BaseEventLoop): context event loop
|
event_loop(BaseEventLoop): context event loop
|
||||||
aiohttp_client(Any): aiohttp client fixture
|
aiohttp_client(Any): aiohttp client fixture
|
||||||
mocker(MockerFixture): mocker object
|
mocker(MockerFixture): mocker object
|
||||||
|
@ -4,11 +4,11 @@ from ahriman.core.auth import Auth
|
|||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.database import SQLite
|
from ahriman.core.database import SQLite
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def authorization_policy(configuration: Configuration, database: SQLite, user: User) -> AuthorizationPolicy:
|
def authorization_policy(configuration: Configuration, database: SQLite, user: User) -> _AuthorizationPolicy:
|
||||||
"""
|
"""
|
||||||
fixture for authorization policy
|
fixture for authorization policy
|
||||||
|
|
||||||
@ -22,5 +22,5 @@ def authorization_policy(configuration: Configuration, database: SQLite, user: U
|
|||||||
"""
|
"""
|
||||||
configuration.set_option("auth", "target", "configuration")
|
configuration.set_option("auth", "target", "configuration")
|
||||||
validator = Auth.load(configuration, database)
|
validator = Auth.load(configuration, database)
|
||||||
policy = AuthorizationPolicy(validator)
|
policy = _AuthorizationPolicy(validator)
|
||||||
return policy
|
return policy
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import pytest
|
import pytest
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
from aiohttp import web
|
|
||||||
from aiohttp.test_utils import TestClient
|
from aiohttp.test_utils import TestClient
|
||||||
|
from aiohttp.web import Application
|
||||||
from cryptography import fernet
|
from cryptography import fernet
|
||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
from unittest.mock import AsyncMock, call as MockCall
|
from unittest.mock import AsyncMock, call as MockCall
|
||||||
@ -11,10 +11,10 @@ from ahriman.core.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.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
from ahriman.web.middlewares.auth_handler import AuthorizationPolicy, auth_handler, cookie_secret_key, setup_auth
|
from ahriman.web.middlewares.auth_handler import _AuthorizationPolicy, _auth_handler, _cookie_secret_key, setup_auth
|
||||||
|
|
||||||
|
|
||||||
async def test_authorized_userid(authorization_policy: AuthorizationPolicy, user: User, mocker: MockerFixture) -> None:
|
async def test_authorized_userid(authorization_policy: _AuthorizationPolicy, user: User, mocker: MockerFixture) -> None:
|
||||||
"""
|
"""
|
||||||
must return authorized user id
|
must return authorized user id
|
||||||
"""
|
"""
|
||||||
@ -22,7 +22,7 @@ async def test_authorized_userid(authorization_policy: AuthorizationPolicy, user
|
|||||||
assert await authorization_policy.authorized_userid(user.username) == user.username
|
assert await authorization_policy.authorized_userid(user.username) == user.username
|
||||||
|
|
||||||
|
|
||||||
async def test_authorized_userid_unknown(authorization_policy: AuthorizationPolicy, user: User) -> None:
|
async def test_authorized_userid_unknown(authorization_policy: _AuthorizationPolicy, user: User) -> None:
|
||||||
"""
|
"""
|
||||||
must not allow unknown user id for authorization
|
must not allow unknown user id for authorization
|
||||||
"""
|
"""
|
||||||
@ -30,7 +30,7 @@ async def test_authorized_userid_unknown(authorization_policy: AuthorizationPoli
|
|||||||
assert await authorization_policy.authorized_userid("somerandomname") is None
|
assert await authorization_policy.authorized_userid("somerandomname") is None
|
||||||
|
|
||||||
|
|
||||||
async def test_permits(authorization_policy: AuthorizationPolicy, user: User) -> None:
|
async def test_permits(authorization_policy: _AuthorizationPolicy, user: User) -> None:
|
||||||
"""
|
"""
|
||||||
must call validator check
|
must call validator check
|
||||||
"""
|
"""
|
||||||
@ -56,7 +56,7 @@ async def test_auth_handler_unix_socket(client_with_auth: TestClient, mocker: Mo
|
|||||||
request_handler.get_permission.return_value = UserAccess.Full
|
request_handler.get_permission.return_value = UserAccess.Full
|
||||||
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
|
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
|
||||||
|
|
||||||
handler = auth_handler(allow_read_only=False)
|
handler = _auth_handler(allow_read_only=False)
|
||||||
await handler(aiohttp_request, request_handler)
|
await handler(aiohttp_request, request_handler)
|
||||||
check_permission_mock.assert_not_called()
|
check_permission_mock.assert_not_called()
|
||||||
|
|
||||||
@ -70,7 +70,7 @@ async def test_auth_handler_api(mocker: MockerFixture) -> None:
|
|||||||
request_handler.get_permission.return_value = UserAccess.Read
|
request_handler.get_permission.return_value = UserAccess.Read
|
||||||
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
|
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
|
||||||
|
|
||||||
handler = auth_handler(allow_read_only=False)
|
handler = _auth_handler(allow_read_only=False)
|
||||||
await handler(aiohttp_request, request_handler)
|
await handler(aiohttp_request, request_handler)
|
||||||
check_permission_mock.assert_called_once_with(aiohttp_request, UserAccess.Read, aiohttp_request.path)
|
check_permission_mock.assert_called_once_with(aiohttp_request, UserAccess.Read, aiohttp_request.path)
|
||||||
|
|
||||||
@ -102,7 +102,7 @@ async def test_auth_handler_allow_read_only(mocker: MockerFixture) -> None:
|
|||||||
request_handler.get_permission.return_value = UserAccess.Read
|
request_handler.get_permission.return_value = UserAccess.Read
|
||||||
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
|
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
|
||||||
|
|
||||||
handler = auth_handler(allow_read_only=True)
|
handler = _auth_handler(allow_read_only=True)
|
||||||
await handler(aiohttp_request, request_handler)
|
await handler(aiohttp_request, request_handler)
|
||||||
check_permission_mock.assert_not_called()
|
check_permission_mock.assert_not_called()
|
||||||
|
|
||||||
@ -116,7 +116,7 @@ async def test_auth_handler_api_no_method(mocker: MockerFixture) -> None:
|
|||||||
request_handler.get_permission = None
|
request_handler.get_permission = None
|
||||||
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
|
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
|
||||||
|
|
||||||
handler = auth_handler(allow_read_only=False)
|
handler = _auth_handler(allow_read_only=False)
|
||||||
await handler(aiohttp_request, request_handler)
|
await handler(aiohttp_request, request_handler)
|
||||||
check_permission_mock.assert_called_once_with(aiohttp_request, UserAccess.Full, aiohttp_request.path)
|
check_permission_mock.assert_called_once_with(aiohttp_request, UserAccess.Full, aiohttp_request.path)
|
||||||
|
|
||||||
@ -130,7 +130,7 @@ async def test_auth_handler_api_post(mocker: MockerFixture) -> None:
|
|||||||
request_handler.get_permission.return_value = UserAccess.Full
|
request_handler.get_permission.return_value = UserAccess.Full
|
||||||
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
|
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
|
||||||
|
|
||||||
handler = auth_handler(allow_read_only=False)
|
handler = _auth_handler(allow_read_only=False)
|
||||||
await handler(aiohttp_request, request_handler)
|
await handler(aiohttp_request, request_handler)
|
||||||
check_permission_mock.assert_called_once_with(aiohttp_request, UserAccess.Full, aiohttp_request.path)
|
check_permission_mock.assert_called_once_with(aiohttp_request, UserAccess.Full, aiohttp_request.path)
|
||||||
|
|
||||||
@ -145,7 +145,7 @@ async def test_auth_handler_read(mocker: MockerFixture) -> None:
|
|||||||
request_handler.get_permission.return_value = UserAccess.Read
|
request_handler.get_permission.return_value = UserAccess.Read
|
||||||
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
|
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
|
||||||
|
|
||||||
handler = auth_handler(allow_read_only=False)
|
handler = _auth_handler(allow_read_only=False)
|
||||||
await handler(aiohttp_request, request_handler)
|
await handler(aiohttp_request, request_handler)
|
||||||
check_permission_mock.assert_called_once_with(aiohttp_request, UserAccess.Read, aiohttp_request.path)
|
check_permission_mock.assert_called_once_with(aiohttp_request, UserAccess.Read, aiohttp_request.path)
|
||||||
|
|
||||||
@ -160,7 +160,7 @@ async def test_auth_handler_write(mocker: MockerFixture) -> None:
|
|||||||
request_handler.get_permission.return_value = UserAccess.Full
|
request_handler.get_permission.return_value = UserAccess.Full
|
||||||
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
|
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
|
||||||
|
|
||||||
handler = auth_handler(allow_read_only=False)
|
handler = _auth_handler(allow_read_only=False)
|
||||||
await handler(aiohttp_request, request_handler)
|
await handler(aiohttp_request, request_handler)
|
||||||
check_permission_mock.assert_called_once_with(aiohttp_request, UserAccess.Full, aiohttp_request.path)
|
check_permission_mock.assert_called_once_with(aiohttp_request, UserAccess.Full, aiohttp_request.path)
|
||||||
|
|
||||||
@ -169,7 +169,7 @@ def test_cookie_secret_key(configuration: Configuration) -> None:
|
|||||||
"""
|
"""
|
||||||
must generate fernet key
|
must generate fernet key
|
||||||
"""
|
"""
|
||||||
secret_key = cookie_secret_key(configuration)
|
secret_key = _cookie_secret_key(configuration)
|
||||||
assert isinstance(secret_key, fernet.Fernet)
|
assert isinstance(secret_key, fernet.Fernet)
|
||||||
|
|
||||||
|
|
||||||
@ -178,10 +178,10 @@ def test_cookie_secret_key_cached(configuration: Configuration) -> None:
|
|||||||
must use cookie key as set by configuration
|
must use cookie key as set by configuration
|
||||||
"""
|
"""
|
||||||
configuration.set_option("auth", "cookie_secret_key", fernet.Fernet.generate_key().decode("utf8"))
|
configuration.set_option("auth", "cookie_secret_key", fernet.Fernet.generate_key().decode("utf8"))
|
||||||
assert cookie_secret_key(configuration) is not None
|
assert _cookie_secret_key(configuration) is not None
|
||||||
|
|
||||||
|
|
||||||
def test_setup_auth(application_with_auth: web.Application, configuration: Configuration, auth: Auth,
|
def test_setup_auth(application_with_auth: Application, configuration: Configuration, auth: Auth,
|
||||||
mocker: MockerFixture) -> None:
|
mocker: MockerFixture) -> None:
|
||||||
"""
|
"""
|
||||||
must set up authorization
|
must set up authorization
|
||||||
|
@ -2,12 +2,12 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from aiohttp.web import HTTPBadRequest, HTTPInternalServerError, HTTPNoContent, HTTPUnauthorized
|
from aiohttp.web import HTTPBadRequest, HTTPInternalServerError, HTTPMethodNotAllowed, HTTPNoContent, HTTPUnauthorized
|
||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import AsyncMock, MagicMock
|
from unittest.mock import AsyncMock, MagicMock
|
||||||
|
|
||||||
from ahriman.web.middlewares.exception_handler import exception_handler, is_templated_unauthorized
|
from ahriman.web.middlewares.exception_handler import _is_templated_unauthorized, exception_handler
|
||||||
|
|
||||||
|
|
||||||
def _extract_body(response: Any) -> Any:
|
def _extract_body(response: Any) -> Any:
|
||||||
@ -31,27 +31,27 @@ def test_is_templated_unauthorized() -> None:
|
|||||||
|
|
||||||
response_mock.path = "/api/v1/login"
|
response_mock.path = "/api/v1/login"
|
||||||
response_mock.headers.getall.return_value = ["*/*"]
|
response_mock.headers.getall.return_value = ["*/*"]
|
||||||
assert is_templated_unauthorized(response_mock)
|
assert _is_templated_unauthorized(response_mock)
|
||||||
|
|
||||||
response_mock.path = "/api/v1/login"
|
response_mock.path = "/api/v1/login"
|
||||||
response_mock.headers.getall.return_value = ["application/json"]
|
response_mock.headers.getall.return_value = ["application/json"]
|
||||||
assert not is_templated_unauthorized(response_mock)
|
assert not _is_templated_unauthorized(response_mock)
|
||||||
|
|
||||||
response_mock.path = "/api/v1/logout"
|
response_mock.path = "/api/v1/logout"
|
||||||
response_mock.headers.getall.return_value = ["*/*"]
|
response_mock.headers.getall.return_value = ["*/*"]
|
||||||
assert is_templated_unauthorized(response_mock)
|
assert _is_templated_unauthorized(response_mock)
|
||||||
|
|
||||||
response_mock.path = "/api/v1/logout"
|
response_mock.path = "/api/v1/logout"
|
||||||
response_mock.headers.getall.return_value = ["application/json"]
|
response_mock.headers.getall.return_value = ["application/json"]
|
||||||
assert not is_templated_unauthorized(response_mock)
|
assert not _is_templated_unauthorized(response_mock)
|
||||||
|
|
||||||
response_mock.path = "/api/v1/status"
|
response_mock.path = "/api/v1/status"
|
||||||
response_mock.headers.getall.return_value = ["*/*"]
|
response_mock.headers.getall.return_value = ["*/*"]
|
||||||
assert not is_templated_unauthorized(response_mock)
|
assert not _is_templated_unauthorized(response_mock)
|
||||||
|
|
||||||
response_mock.path = "/api/v1/status"
|
response_mock.path = "/api/v1/status"
|
||||||
response_mock.headers.getall.return_value = ["application/json"]
|
response_mock.headers.getall.return_value = ["application/json"]
|
||||||
assert not is_templated_unauthorized(response_mock)
|
assert not _is_templated_unauthorized(response_mock)
|
||||||
|
|
||||||
|
|
||||||
async def test_exception_handler(mocker: MockerFixture) -> None:
|
async def test_exception_handler(mocker: MockerFixture) -> None:
|
||||||
@ -87,7 +87,7 @@ async def test_exception_handler_unauthorized(mocker: MockerFixture) -> None:
|
|||||||
"""
|
"""
|
||||||
request = pytest.helpers.request("", "", "")
|
request = pytest.helpers.request("", "", "")
|
||||||
request_handler = AsyncMock(side_effect=HTTPUnauthorized())
|
request_handler = AsyncMock(side_effect=HTTPUnauthorized())
|
||||||
mocker.patch("ahriman.web.middlewares.exception_handler.is_templated_unauthorized", return_value=False)
|
mocker.patch("ahriman.web.middlewares.exception_handler._is_templated_unauthorized", return_value=False)
|
||||||
render_mock = mocker.patch("aiohttp_jinja2.render_template")
|
render_mock = mocker.patch("aiohttp_jinja2.render_template")
|
||||||
|
|
||||||
handler = exception_handler(logging.getLogger())
|
handler = exception_handler(logging.getLogger())
|
||||||
@ -102,7 +102,7 @@ async def test_exception_handler_unauthorized_templated(mocker: MockerFixture) -
|
|||||||
"""
|
"""
|
||||||
request = pytest.helpers.request("", "", "")
|
request = pytest.helpers.request("", "", "")
|
||||||
request_handler = AsyncMock(side_effect=HTTPUnauthorized())
|
request_handler = AsyncMock(side_effect=HTTPUnauthorized())
|
||||||
mocker.patch("ahriman.web.middlewares.exception_handler.is_templated_unauthorized", return_value=True)
|
mocker.patch("ahriman.web.middlewares.exception_handler._is_templated_unauthorized", return_value=True)
|
||||||
render_mock = mocker.patch("aiohttp_jinja2.render_template")
|
render_mock = mocker.patch("aiohttp_jinja2.render_template")
|
||||||
|
|
||||||
handler = exception_handler(logging.getLogger())
|
handler = exception_handler(logging.getLogger())
|
||||||
@ -111,6 +111,44 @@ async def test_exception_handler_unauthorized_templated(mocker: MockerFixture) -
|
|||||||
render_mock.assert_called_once_with("error.jinja2", request, context, status=HTTPUnauthorized.status_code)
|
render_mock.assert_called_once_with("error.jinja2", request, context, status=HTTPUnauthorized.status_code)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_exception_handler_options() -> None:
|
||||||
|
"""
|
||||||
|
must handle OPTIONS request
|
||||||
|
"""
|
||||||
|
request = pytest.helpers.request("", "", "OPTIONS")
|
||||||
|
request_handler = AsyncMock(side_effect=HTTPMethodNotAllowed("OPTIONS", ["GET"]))
|
||||||
|
|
||||||
|
handler = exception_handler(logging.getLogger())
|
||||||
|
with pytest.raises(HTTPNoContent) as response:
|
||||||
|
await handler(request, request_handler)
|
||||||
|
assert response.headers["Allow"] == "GET"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_exception_handler_head() -> None:
|
||||||
|
"""
|
||||||
|
must handle missing HEAD requests
|
||||||
|
"""
|
||||||
|
request = pytest.helpers.request("", "", "HEAD")
|
||||||
|
request_handler = AsyncMock(side_effect=HTTPMethodNotAllowed("HEAD", ["HEAD,GET"]))
|
||||||
|
|
||||||
|
handler = exception_handler(logging.getLogger())
|
||||||
|
with pytest.raises(HTTPMethodNotAllowed) as response:
|
||||||
|
await handler(request, request_handler)
|
||||||
|
assert response.headers["Allow"] == "GET"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_exception_handler_method_not_allowed() -> None:
|
||||||
|
"""
|
||||||
|
must handle not allowed methodss
|
||||||
|
"""
|
||||||
|
request = pytest.helpers.request("", "", "POST")
|
||||||
|
request_handler = AsyncMock(side_effect=HTTPMethodNotAllowed("POST", ["GET"]))
|
||||||
|
|
||||||
|
handler = exception_handler(logging.getLogger())
|
||||||
|
with pytest.raises(HTTPMethodNotAllowed):
|
||||||
|
await handler(request, request_handler)
|
||||||
|
|
||||||
|
|
||||||
async def test_exception_handler_client_error(mocker: MockerFixture) -> None:
|
async def test_exception_handler_client_error(mocker: MockerFixture) -> None:
|
||||||
"""
|
"""
|
||||||
must handle client exception
|
must handle client exception
|
||||||
|
1
tests/ahriman/web/schemas/test_aur_package_schema.py
Normal file
1
tests/ahriman/web/schemas/test_aur_package_schema.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# schema testing goes in view class tests
|
9
tests/ahriman/web/schemas/test_auth_schema.py
Normal file
9
tests/ahriman/web/schemas/test_auth_schema.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from ahriman.web.schemas.auth_schema import AuthSchema
|
||||||
|
|
||||||
|
|
||||||
|
def test_schema() -> None:
|
||||||
|
"""
|
||||||
|
must return valid schema
|
||||||
|
"""
|
||||||
|
schema = AuthSchema()
|
||||||
|
assert not schema.validate({"API_SESSION": "key"})
|
1
tests/ahriman/web/schemas/test_counters_schema.py
Normal file
1
tests/ahriman/web/schemas/test_counters_schema.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# schema testing goes in view class tests
|
1
tests/ahriman/web/schemas/test_error_schema.py
Normal file
1
tests/ahriman/web/schemas/test_error_schema.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# schema testing goes in view class tests
|
1
tests/ahriman/web/schemas/test_internal_status_schema.py
Normal file
1
tests/ahriman/web/schemas/test_internal_status_schema.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# schema testing goes in view class tests
|
1
tests/ahriman/web/schemas/test_log_schema.py
Normal file
1
tests/ahriman/web/schemas/test_log_schema.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# schema testing goes in view class tests
|
1
tests/ahriman/web/schemas/test_login_schema.py
Normal file
1
tests/ahriman/web/schemas/test_login_schema.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# schema testing goes in view class tests
|
1
tests/ahriman/web/schemas/test_logs_schema.py
Normal file
1
tests/ahriman/web/schemas/test_logs_schema.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# schema testing goes in view class tests
|
1
tests/ahriman/web/schemas/test_oauth2_schema.py
Normal file
1
tests/ahriman/web/schemas/test_oauth2_schema.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# schema testing goes in view class tests
|
10
tests/ahriman/web/schemas/test_package_name_schema.py
Normal file
10
tests/ahriman/web/schemas/test_package_name_schema.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from ahriman.models.package import Package
|
||||||
|
from ahriman.web.schemas.package_name_schema import PackageNameSchema
|
||||||
|
|
||||||
|
|
||||||
|
def test_schema(package_ahriman: Package) -> None:
|
||||||
|
"""
|
||||||
|
must return valid schema
|
||||||
|
"""
|
||||||
|
schema = PackageNameSchema()
|
||||||
|
assert not schema.validate({"package": package_ahriman.base})
|
1
tests/ahriman/web/schemas/test_package_names_schema.py
Normal file
1
tests/ahriman/web/schemas/test_package_names_schema.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# schema testing goes in view class tests
|
@ -0,0 +1 @@
|
|||||||
|
# schema testing goes in view class tests
|
1
tests/ahriman/web/schemas/test_package_schema.py
Normal file
1
tests/ahriman/web/schemas/test_package_schema.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# schema testing goes in view class tests
|
1
tests/ahriman/web/schemas/test_package_status_schema.py
Normal file
1
tests/ahriman/web/schemas/test_package_status_schema.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# schema testing goes in view class tests
|
1
tests/ahriman/web/schemas/test_pgp_key_id_schema.py
Normal file
1
tests/ahriman/web/schemas/test_pgp_key_id_schema.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# schema testing goes in view class tests
|
1
tests/ahriman/web/schemas/test_pgp_key_schema.py
Normal file
1
tests/ahriman/web/schemas/test_pgp_key_schema.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# schema testing goes in view class tests
|
1
tests/ahriman/web/schemas/test_remote_schema.py
Normal file
1
tests/ahriman/web/schemas/test_remote_schema.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# schema testing goes in view class tests
|
1
tests/ahriman/web/schemas/test_search_schema.py
Normal file
1
tests/ahriman/web/schemas/test_search_schema.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# schema testing goes in view class tests
|
1
tests/ahriman/web/schemas/test_status_schema.py
Normal file
1
tests/ahriman/web/schemas/test_status_schema.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# schema testing goes in view class tests
|
57
tests/ahriman/web/test_apispec.py
Normal file
57
tests/ahriman/web/test_apispec.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from aiohttp.web import Application
|
||||||
|
from pytest_mock import MockerFixture
|
||||||
|
|
||||||
|
from ahriman import version
|
||||||
|
from ahriman.web.apispec import _info, _security, _servers, setup_apispec
|
||||||
|
|
||||||
|
|
||||||
|
def test_info() -> None:
|
||||||
|
"""
|
||||||
|
must generate info object for swagger
|
||||||
|
"""
|
||||||
|
info = _info()
|
||||||
|
assert info["title"] == "ahriman"
|
||||||
|
assert info["version"] == version.__version__
|
||||||
|
|
||||||
|
|
||||||
|
def test_security() -> None:
|
||||||
|
"""
|
||||||
|
must generate security definitions for swagger
|
||||||
|
"""
|
||||||
|
token = next(iter(_security()))["token"]
|
||||||
|
assert token == {"type": "apiKey", "name": "API_SESSION", "in": "cookie"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_servers(application: Application) -> None:
|
||||||
|
"""
|
||||||
|
must generate servers definitions
|
||||||
|
"""
|
||||||
|
servers = _servers(application)
|
||||||
|
assert servers == [{"url": "http://127.0.0.1:8080"}]
|
||||||
|
|
||||||
|
|
||||||
|
def test_servers_address(application: Application) -> None:
|
||||||
|
"""
|
||||||
|
must generate servers definitions with address
|
||||||
|
"""
|
||||||
|
application["configuration"].set_option("web", "address", "https://example.com")
|
||||||
|
servers = _servers(application)
|
||||||
|
assert servers == [{"url": "https://example.com"}]
|
||||||
|
|
||||||
|
|
||||||
|
def test_setup_apispec(application: Application, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must set api specification
|
||||||
|
"""
|
||||||
|
apispec_mock = mocker.patch("aiohttp_apispec.setup_aiohttp_apispec")
|
||||||
|
setup_apispec(application)
|
||||||
|
apispec_mock.assert_called_once_with(
|
||||||
|
application,
|
||||||
|
url="/api-docs/swagger.json",
|
||||||
|
openapi_version="3.0.2",
|
||||||
|
info=pytest.helpers.anyvar(int),
|
||||||
|
servers=pytest.helpers.anyvar(int),
|
||||||
|
security=pytest.helpers.anyvar(int),
|
||||||
|
)
|
20
tests/ahriman/web/test_cors.py
Normal file
20
tests/ahriman/web/test_cors.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import aiohttp_cors
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from aiohttp.web import Application
|
||||||
|
|
||||||
|
|
||||||
|
def test_setup_cors(application: Application) -> None:
|
||||||
|
"""
|
||||||
|
must setup CORS
|
||||||
|
"""
|
||||||
|
cors: aiohttp_cors.CorsConfig = application[aiohttp_cors.APP_CONFIG_KEY]
|
||||||
|
# let's test here that it is enabled for all requests
|
||||||
|
for route in application.router.routes():
|
||||||
|
# we don't want to deal with match info here though
|
||||||
|
try:
|
||||||
|
url = route.url_for()
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
continue
|
||||||
|
request = pytest.helpers.request(application, url, route.method, resource=route.resource)
|
||||||
|
assert cors._cors_impl._router_adapter.is_cors_enabled_on_request(request)
|
@ -1,10 +1,10 @@
|
|||||||
from aiohttp import web
|
from aiohttp.web import Application
|
||||||
|
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.web.routes import setup_routes
|
from ahriman.web.routes import setup_routes
|
||||||
|
|
||||||
|
|
||||||
def test_setup_routes(application: web.Application, configuration: Configuration) -> None:
|
def test_setup_routes(application: Application, configuration: Configuration) -> None:
|
||||||
"""
|
"""
|
||||||
must generate non-empty list of routes
|
must generate non-empty list of routes
|
||||||
"""
|
"""
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
import pytest
|
import pytest
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp.web import Application
|
||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
from unittest.mock import call as MockCall
|
from unittest.mock import call as MockCall
|
||||||
|
|
||||||
from ahriman.core.exceptions import InitializeError
|
from ahriman.core.exceptions import InitializeError
|
||||||
from ahriman.core.log.filtered_access_logger import FilteredAccessLogger
|
from ahriman.core.log.filtered_access_logger import FilteredAccessLogger
|
||||||
from ahriman.core.status.watcher import Watcher
|
from ahriman.core.status.watcher import Watcher
|
||||||
from ahriman.web.web import create_socket, on_shutdown, on_startup, run_server
|
from ahriman.web.web import _create_socket, _on_shutdown, _on_startup, run_server
|
||||||
|
|
||||||
|
|
||||||
async def test_create_socket(application: web.Application, mocker: MockerFixture) -> None:
|
async def test_create_socket(application: Application, mocker: MockerFixture) -> None:
|
||||||
"""
|
"""
|
||||||
must create socket
|
must create socket
|
||||||
"""
|
"""
|
||||||
@ -23,7 +23,7 @@ async def test_create_socket(application: web.Application, mocker: MockerFixture
|
|||||||
chmod_mock = mocker.patch("pathlib.Path.chmod")
|
chmod_mock = mocker.patch("pathlib.Path.chmod")
|
||||||
unlink_mock = mocker.patch("pathlib.Path.unlink")
|
unlink_mock = mocker.patch("pathlib.Path.unlink")
|
||||||
|
|
||||||
sock = create_socket(application["configuration"], application)
|
sock = _create_socket(application["configuration"], application)
|
||||||
assert sock.family == socket.AF_UNIX
|
assert sock.family == socket.AF_UNIX
|
||||||
assert sock.type == socket.SOCK_STREAM
|
assert sock.type == socket.SOCK_STREAM
|
||||||
bind_mock.assert_called_once_with(str(path))
|
bind_mock.assert_called_once_with(str(path))
|
||||||
@ -35,14 +35,14 @@ async def test_create_socket(application: web.Application, mocker: MockerFixture
|
|||||||
unlink_mock.assert_has_calls([MockCall(missing_ok=True), MockCall(missing_ok=True)])
|
unlink_mock.assert_has_calls([MockCall(missing_ok=True), MockCall(missing_ok=True)])
|
||||||
|
|
||||||
|
|
||||||
def test_create_socket_empty(application: web.Application) -> None:
|
def test_create_socket_empty(application: Application) -> None:
|
||||||
"""
|
"""
|
||||||
must skip socket creation if not set by configuration
|
must skip socket creation if not set by configuration
|
||||||
"""
|
"""
|
||||||
assert create_socket(application["configuration"], application) is None
|
assert _create_socket(application["configuration"], application) is None
|
||||||
|
|
||||||
|
|
||||||
def test_create_socket_safe(application: web.Application, mocker: MockerFixture) -> None:
|
def test_create_socket_safe(application: Application, mocker: MockerFixture) -> None:
|
||||||
"""
|
"""
|
||||||
must create socket with default permission set
|
must create socket with default permission set
|
||||||
"""
|
"""
|
||||||
@ -54,32 +54,32 @@ def test_create_socket_safe(application: web.Application, mocker: MockerFixture)
|
|||||||
mocker.patch("pathlib.Path.unlink")
|
mocker.patch("pathlib.Path.unlink")
|
||||||
chmod_mock = mocker.patch("pathlib.Path.chmod")
|
chmod_mock = mocker.patch("pathlib.Path.chmod")
|
||||||
|
|
||||||
sock = create_socket(application["configuration"], application)
|
sock = _create_socket(application["configuration"], application)
|
||||||
assert sock is not None
|
assert sock is not None
|
||||||
chmod_mock.assert_not_called()
|
chmod_mock.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
async def test_on_shutdown(application: web.Application, mocker: MockerFixture) -> None:
|
async def test_on_shutdown(application: Application, mocker: MockerFixture) -> None:
|
||||||
"""
|
"""
|
||||||
must write information to log
|
must write information to log
|
||||||
"""
|
"""
|
||||||
logging_mock = mocker.patch("logging.Logger.warning")
|
logging_mock = mocker.patch("logging.Logger.warning")
|
||||||
await on_shutdown(application)
|
await _on_shutdown(application)
|
||||||
logging_mock.assert_called_once_with(pytest.helpers.anyvar(str, True))
|
logging_mock.assert_called_once_with(pytest.helpers.anyvar(str, True))
|
||||||
|
|
||||||
|
|
||||||
async def test_on_startup(application: web.Application, watcher: Watcher, mocker: MockerFixture) -> None:
|
async def test_on_startup(application: Application, watcher: Watcher, mocker: MockerFixture) -> None:
|
||||||
"""
|
"""
|
||||||
must call load method
|
must call load method
|
||||||
"""
|
"""
|
||||||
mocker.patch("aiohttp.web.Application.__getitem__", return_value=watcher)
|
mocker.patch("aiohttp.web.Application.__getitem__", return_value=watcher)
|
||||||
load_mock = mocker.patch("ahriman.core.status.watcher.Watcher.load")
|
load_mock = mocker.patch("ahriman.core.status.watcher.Watcher.load")
|
||||||
|
|
||||||
await on_startup(application)
|
await _on_startup(application)
|
||||||
load_mock.assert_called_once_with()
|
load_mock.assert_called_once_with()
|
||||||
|
|
||||||
|
|
||||||
async def test_on_startup_exception(application: web.Application, watcher: Watcher, mocker: MockerFixture) -> None:
|
async def test_on_startup_exception(application: Application, watcher: Watcher, mocker: MockerFixture) -> None:
|
||||||
"""
|
"""
|
||||||
must throw exception on load error
|
must throw exception on load error
|
||||||
"""
|
"""
|
||||||
@ -87,16 +87,16 @@ async def test_on_startup_exception(application: web.Application, watcher: Watch
|
|||||||
mocker.patch("ahriman.core.status.watcher.Watcher.load", side_effect=Exception())
|
mocker.patch("ahriman.core.status.watcher.Watcher.load", side_effect=Exception())
|
||||||
|
|
||||||
with pytest.raises(InitializeError):
|
with pytest.raises(InitializeError):
|
||||||
await on_startup(application)
|
await _on_startup(application)
|
||||||
|
|
||||||
|
|
||||||
def test_run(application: web.Application, mocker: MockerFixture) -> None:
|
def test_run(application: Application, mocker: MockerFixture) -> None:
|
||||||
"""
|
"""
|
||||||
must run application
|
must run application
|
||||||
"""
|
"""
|
||||||
port = 8080
|
port = 8080
|
||||||
application["configuration"].set_option("web", "port", str(port))
|
application["configuration"].set_option("web", "port", str(port))
|
||||||
run_application_mock = mocker.patch("aiohttp.web.run_app")
|
run_application_mock = mocker.patch("ahriman.web.web.run_app")
|
||||||
|
|
||||||
run_server(application)
|
run_server(application)
|
||||||
run_application_mock.assert_called_once_with(
|
run_application_mock.assert_called_once_with(
|
||||||
@ -105,13 +105,13 @@ def test_run(application: web.Application, mocker: MockerFixture) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_run_with_auth(application_with_auth: web.Application, mocker: MockerFixture) -> None:
|
def test_run_with_auth(application_with_auth: Application, mocker: MockerFixture) -> None:
|
||||||
"""
|
"""
|
||||||
must run application with enabled authorization
|
must run application with enabled authorization
|
||||||
"""
|
"""
|
||||||
port = 8080
|
port = 8080
|
||||||
application_with_auth["configuration"].set_option("web", "port", str(port))
|
application_with_auth["configuration"].set_option("web", "port", str(port))
|
||||||
run_application_mock = mocker.patch("aiohttp.web.run_app")
|
run_application_mock = mocker.patch("ahriman.web.web.run_app")
|
||||||
|
|
||||||
run_server(application_with_auth)
|
run_server(application_with_auth)
|
||||||
run_application_mock.assert_called_once_with(
|
run_application_mock.assert_called_once_with(
|
||||||
@ -120,13 +120,13 @@ def test_run_with_auth(application_with_auth: web.Application, mocker: MockerFix
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_run_with_debug(application_with_debug: web.Application, mocker: MockerFixture) -> None:
|
def test_run_with_debug(application_with_debug: Application, mocker: MockerFixture) -> None:
|
||||||
"""
|
"""
|
||||||
must run application with enabled debug panel
|
must run application with enabled debug panel
|
||||||
"""
|
"""
|
||||||
port = 8080
|
port = 8080
|
||||||
application_with_debug["configuration"].set_option("web", "port", str(port))
|
application_with_debug["configuration"].set_option("web", "port", str(port))
|
||||||
run_application_mock = mocker.patch("aiohttp.web.run_app")
|
run_application_mock = mocker.patch("ahriman.web.web.run_app")
|
||||||
|
|
||||||
run_server(application_with_debug)
|
run_server(application_with_debug)
|
||||||
run_application_mock.assert_called_once_with(
|
run_application_mock.assert_called_once_with(
|
||||||
@ -135,14 +135,14 @@ def test_run_with_debug(application_with_debug: web.Application, mocker: MockerF
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_run_with_socket(application: web.Application, mocker: MockerFixture) -> None:
|
def test_run_with_socket(application: Application, mocker: MockerFixture) -> None:
|
||||||
"""
|
"""
|
||||||
must run application
|
must run application
|
||||||
"""
|
"""
|
||||||
port = 8080
|
port = 8080
|
||||||
application["configuration"].set_option("web", "port", str(port))
|
application["configuration"].set_option("web", "port", str(port))
|
||||||
socket_mock = mocker.patch("ahriman.web.web.create_socket", return_value=42)
|
socket_mock = mocker.patch("ahriman.web.web._create_socket", return_value=42)
|
||||||
run_application_mock = mocker.patch("aiohttp.web.run_app")
|
run_application_mock = mocker.patch("ahriman.web.web.run_app")
|
||||||
|
|
||||||
run_server(application)
|
run_server(application)
|
||||||
socket_mock.assert_called_once_with(application["configuration"], application)
|
socket_mock.assert_called_once_with(application["configuration"], application)
|
||||||
|
24
tests/ahriman/web/views/api/test_views_api_docs.py
Normal file
24
tests/ahriman/web/views/api/test_views_api_docs.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from aiohttp.test_utils import TestClient
|
||||||
|
|
||||||
|
from ahriman.models.user_access import UserAccess
|
||||||
|
from ahriman.web.views.api.docs import DocsView
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_permission() -> None:
|
||||||
|
"""
|
||||||
|
must return correct permission for the request
|
||||||
|
"""
|
||||||
|
for method in ("GET",):
|
||||||
|
request = pytest.helpers.request("", "", method)
|
||||||
|
assert await DocsView.get_permission(request) == UserAccess.Unauthorized
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get(client: TestClient) -> None:
|
||||||
|
"""
|
||||||
|
must generate api-docs correctly
|
||||||
|
"""
|
||||||
|
response = await client.get("/api-docs")
|
||||||
|
assert response.ok
|
||||||
|
assert await response.text()
|
91
tests/ahriman/web/views/api/test_views_api_swagger.py
Normal file
91
tests/ahriman/web/views/api/test_views_api_swagger.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from aiohttp.test_utils import TestClient
|
||||||
|
|
||||||
|
from ahriman.models.user_access import UserAccess
|
||||||
|
from ahriman.web.views.api.swagger import SwaggerView
|
||||||
|
|
||||||
|
|
||||||
|
def _client(client: TestClient) -> TestClient:
|
||||||
|
"""
|
||||||
|
generate test client with docs
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client(TestClient): test client fixture
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
TestClient: test client fixture with additional properties
|
||||||
|
"""
|
||||||
|
client.app["swagger_dict"] = {
|
||||||
|
"paths": {
|
||||||
|
"/api/v1/logout": {
|
||||||
|
"get": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "cookie",
|
||||||
|
"name": "API_SESSION",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"head": {},
|
||||||
|
"post": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "cookie",
|
||||||
|
"name": "API_SESSION",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in": "body",
|
||||||
|
"name": "schema",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"components": {},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"token": {
|
||||||
|
"type": "apiKey",
|
||||||
|
"name": "API_SESSION",
|
||||||
|
"in": "cookie",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
return client
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_permission() -> None:
|
||||||
|
"""
|
||||||
|
must return correct permission for the request
|
||||||
|
"""
|
||||||
|
for method in ("GET",):
|
||||||
|
request = pytest.helpers.request("", "", method)
|
||||||
|
assert await SwaggerView.get_permission(request) == UserAccess.Unauthorized
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get(client: TestClient) -> None:
|
||||||
|
"""
|
||||||
|
must generate api-docs correctly
|
||||||
|
"""
|
||||||
|
client = _client(client)
|
||||||
|
response = await client.get("/api-docs/swagger.json")
|
||||||
|
assert response.ok
|
||||||
|
|
||||||
|
json = await response.json()
|
||||||
|
assert "securitySchemes" in json["components"]
|
||||||
|
assert not any(parameter["in"] == "body" for parameter in json["paths"]["/api/v1/logout"]["post"]["parameters"])
|
||||||
|
assert "requestBody" in json["paths"]["/api/v1/logout"]["post"]
|
||||||
|
assert "requestBody" not in json["paths"]["/api/v1/logout"]["get"]
|
||||||
|
assert "requestBody" not in json["paths"]["/api/v1/logout"]["head"]
|
@ -1,17 +1,17 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp.web import Application
|
||||||
|
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def base(application: web.Application) -> BaseView:
|
def base(application: Application) -> BaseView:
|
||||||
"""
|
"""
|
||||||
base view fixture
|
base view fixture
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
application(web.Application): application fixture
|
application(Application): application fixture
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
BaseView: generated base view fixture
|
BaseView: generated base view fixture
|
||||||
|
@ -4,6 +4,8 @@ from aiohttp.test_utils import TestClient
|
|||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
|
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
|
from ahriman.web.schemas.error_schema import ErrorSchema
|
||||||
|
from ahriman.web.schemas.package_names_schema import PackageNamesSchema
|
||||||
from ahriman.web.views.service.add import AddView
|
from ahriman.web.views.service.add import AddView
|
||||||
|
|
||||||
|
|
||||||
@ -21,8 +23,11 @@ async def test_post(client: TestClient, mocker: MockerFixture) -> None:
|
|||||||
must call post request correctly
|
must call post request correctly
|
||||||
"""
|
"""
|
||||||
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add")
|
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add")
|
||||||
|
request_schema = PackageNamesSchema()
|
||||||
|
|
||||||
response = await client.post("/api/v1/service/add", json={"packages": ["ahriman"]})
|
payload = {"packages": ["ahriman"]}
|
||||||
|
assert not request_schema.validate(payload)
|
||||||
|
response = await client.post("/api/v1/service/add", json=payload)
|
||||||
assert response.ok
|
assert response.ok
|
||||||
add_mock.assert_called_once_with(["ahriman"], now=True)
|
add_mock.assert_called_once_with(["ahriman"], now=True)
|
||||||
|
|
||||||
@ -32,15 +37,19 @@ async def test_post_empty(client: TestClient, mocker: MockerFixture) -> None:
|
|||||||
must call raise 400 on empty request
|
must call raise 400 on empty request
|
||||||
"""
|
"""
|
||||||
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add")
|
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add")
|
||||||
|
response_schema = ErrorSchema()
|
||||||
|
|
||||||
response = await client.post("/api/v1/service/add", json={"packages": [""]})
|
response = await client.post("/api/v1/service/add", json={"packages": [""]})
|
||||||
assert response.status == 400
|
assert response.status == 400
|
||||||
|
assert not response_schema.validate(await response.json())
|
||||||
add_mock.assert_not_called()
|
add_mock.assert_not_called()
|
||||||
|
|
||||||
response = await client.post("/api/v1/service/add", json={"packages": []})
|
response = await client.post("/api/v1/service/add", json={"packages": []})
|
||||||
assert response.status == 400
|
assert response.status == 400
|
||||||
|
assert not response_schema.validate(await response.json())
|
||||||
add_mock.assert_not_called()
|
add_mock.assert_not_called()
|
||||||
|
|
||||||
response = await client.post("/api/v1/service/add", json={})
|
response = await client.post("/api/v1/service/add", json={})
|
||||||
assert response.status == 400
|
assert response.status == 400
|
||||||
|
assert not response_schema.validate(await response.json())
|
||||||
add_mock.assert_not_called()
|
add_mock.assert_not_called()
|
||||||
|
@ -4,6 +4,9 @@ from aiohttp.test_utils import TestClient
|
|||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
|
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
|
from ahriman.web.schemas.error_schema import ErrorSchema
|
||||||
|
from ahriman.web.schemas.pgp_key_id_schema import PGPKeyIdSchema
|
||||||
|
from ahriman.web.schemas.pgp_key_schema import PGPKeySchema
|
||||||
from ahriman.web.views.service.pgp import PGPView
|
from ahriman.web.views.service.pgp import PGPView
|
||||||
|
|
||||||
|
|
||||||
@ -11,7 +14,7 @@ async def test_get_permission() -> None:
|
|||||||
"""
|
"""
|
||||||
must return correct permission for the request
|
must return correct permission for the request
|
||||||
"""
|
"""
|
||||||
for method in ("GET", "HEAD"):
|
for method in ("GET",):
|
||||||
request = pytest.helpers.request("", "", method)
|
request = pytest.helpers.request("", "", method)
|
||||||
assert await PGPView.get_permission(request) == UserAccess.Reporter
|
assert await PGPView.get_permission(request) == UserAccess.Reporter
|
||||||
for method in ("POST",):
|
for method in ("POST",):
|
||||||
@ -24,10 +27,15 @@ async def test_get(client: TestClient, mocker: MockerFixture) -> None:
|
|||||||
must retrieve key from the keyserver
|
must retrieve key from the keyserver
|
||||||
"""
|
"""
|
||||||
import_mock = mocker.patch("ahriman.core.sign.gpg.GPG.key_download", return_value="imported")
|
import_mock = mocker.patch("ahriman.core.sign.gpg.GPG.key_download", return_value="imported")
|
||||||
|
request_schema = PGPKeyIdSchema()
|
||||||
|
response_schema = PGPKeySchema()
|
||||||
|
|
||||||
response = await client.get("/api/v1/service/pgp", params={"key": "0xdeadbeaf", "server": "keyserver.ubuntu.com"})
|
payload = {"key": "0xdeadbeaf", "server": "keyserver.ubuntu.com"}
|
||||||
|
assert not request_schema.validate(payload)
|
||||||
|
response = await client.get("/api/v1/service/pgp", params=payload)
|
||||||
assert response.ok
|
assert response.ok
|
||||||
import_mock.assert_called_once_with("keyserver.ubuntu.com", "0xdeadbeaf")
|
import_mock.assert_called_once_with("keyserver.ubuntu.com", "0xdeadbeaf")
|
||||||
|
assert not response_schema.validate(await response.json())
|
||||||
assert await response.json() == {"key": "imported"}
|
assert await response.json() == {"key": "imported"}
|
||||||
|
|
||||||
|
|
||||||
@ -36,9 +44,11 @@ async def test_get_empty(client: TestClient, mocker: MockerFixture) -> None:
|
|||||||
must raise 400 on missing parameters
|
must raise 400 on missing parameters
|
||||||
"""
|
"""
|
||||||
import_mock = mocker.patch("ahriman.core.sign.gpg.GPG.key_download")
|
import_mock = mocker.patch("ahriman.core.sign.gpg.GPG.key_download")
|
||||||
|
response_schema = ErrorSchema()
|
||||||
|
|
||||||
response = await client.get("/api/v1/service/pgp")
|
response = await client.get("/api/v1/service/pgp")
|
||||||
assert response.status == 400
|
assert response.status == 400
|
||||||
|
assert not response_schema.validate(await response.json())
|
||||||
import_mock.assert_not_called()
|
import_mock.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
@ -47,9 +57,11 @@ async def test_get_process_exception(client: TestClient, mocker: MockerFixture)
|
|||||||
must raise 404 on invalid PGP server response
|
must raise 404 on invalid PGP server response
|
||||||
"""
|
"""
|
||||||
import_mock = mocker.patch("ahriman.core.sign.gpg.GPG.key_download", side_effect=Exception())
|
import_mock = mocker.patch("ahriman.core.sign.gpg.GPG.key_download", side_effect=Exception())
|
||||||
|
response_schema = ErrorSchema()
|
||||||
|
|
||||||
response = await client.get("/api/v1/service/pgp", params={"key": "0xdeadbeaf", "server": "keyserver.ubuntu.com"})
|
response = await client.get("/api/v1/service/pgp", params={"key": "0xdeadbeaf", "server": "keyserver.ubuntu.com"})
|
||||||
assert response.status == 404
|
assert response.status == 404
|
||||||
|
assert not response_schema.validate(await response.json())
|
||||||
import_mock.assert_called_once_with("keyserver.ubuntu.com", "0xdeadbeaf")
|
import_mock.assert_called_once_with("keyserver.ubuntu.com", "0xdeadbeaf")
|
||||||
|
|
||||||
|
|
||||||
@ -58,8 +70,11 @@ async def test_post(client: TestClient, mocker: MockerFixture) -> None:
|
|||||||
must call post request correctly
|
must call post request correctly
|
||||||
"""
|
"""
|
||||||
import_mock = mocker.patch("ahriman.core.spawn.Spawn.key_import")
|
import_mock = mocker.patch("ahriman.core.spawn.Spawn.key_import")
|
||||||
|
request_schema = PGPKeyIdSchema()
|
||||||
|
|
||||||
response = await client.post("/api/v1/service/pgp", json={"key": "0xdeadbeaf", "server": "keyserver.ubuntu.com"})
|
payload = {"key": "0xdeadbeaf", "server": "keyserver.ubuntu.com"}
|
||||||
|
assert not request_schema.validate(payload)
|
||||||
|
response = await client.post("/api/v1/service/pgp", json=payload)
|
||||||
assert response.ok
|
assert response.ok
|
||||||
import_mock.assert_called_once_with("0xdeadbeaf", "keyserver.ubuntu.com")
|
import_mock.assert_called_once_with("0xdeadbeaf", "keyserver.ubuntu.com")
|
||||||
|
|
||||||
@ -69,7 +84,9 @@ async def test_post_exception(client: TestClient, mocker: MockerFixture) -> None
|
|||||||
must raise exception on missing key payload
|
must raise exception on missing key payload
|
||||||
"""
|
"""
|
||||||
import_mock = mocker.patch("ahriman.core.spawn.Spawn.key_import")
|
import_mock = mocker.patch("ahriman.core.spawn.Spawn.key_import")
|
||||||
|
response_schema = ErrorSchema()
|
||||||
|
|
||||||
response = await client.post("/api/v1/service/pgp")
|
response = await client.post("/api/v1/service/pgp")
|
||||||
assert response.status == 400
|
assert response.status == 400
|
||||||
|
assert not response_schema.validate(await response.json())
|
||||||
import_mock.assert_not_called()
|
import_mock.assert_not_called()
|
||||||
|
@ -4,6 +4,8 @@ from aiohttp.test_utils import TestClient
|
|||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
|
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
|
from ahriman.web.schemas.error_schema import ErrorSchema
|
||||||
|
from ahriman.web.schemas.package_names_schema import PackageNamesSchema
|
||||||
from ahriman.web.views.service.rebuild import RebuildView
|
from ahriman.web.views.service.rebuild import RebuildView
|
||||||
|
|
||||||
|
|
||||||
@ -21,8 +23,11 @@ async def test_post(client: TestClient, mocker: MockerFixture) -> None:
|
|||||||
must call post request correctly
|
must call post request correctly
|
||||||
"""
|
"""
|
||||||
rebuild_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_rebuild")
|
rebuild_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_rebuild")
|
||||||
|
request_schema = PackageNamesSchema()
|
||||||
|
|
||||||
response = await client.post("/api/v1/service/rebuild", json={"packages": ["python", "ahriman"]})
|
payload = {"packages": ["python", "ahriman"]}
|
||||||
|
assert not request_schema.validate(payload)
|
||||||
|
response = await client.post("/api/v1/service/rebuild", json=payload)
|
||||||
assert response.ok
|
assert response.ok
|
||||||
rebuild_mock.assert_called_once_with("python")
|
rebuild_mock.assert_called_once_with("python")
|
||||||
|
|
||||||
@ -32,7 +37,9 @@ async def test_post_exception(client: TestClient, mocker: MockerFixture) -> None
|
|||||||
must raise exception on missing packages payload
|
must raise exception on missing packages payload
|
||||||
"""
|
"""
|
||||||
rebuild_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_rebuild")
|
rebuild_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_rebuild")
|
||||||
|
response_schema = ErrorSchema()
|
||||||
|
|
||||||
response = await client.post("/api/v1/service/rebuild")
|
response = await client.post("/api/v1/service/rebuild")
|
||||||
assert response.status == 400
|
assert response.status == 400
|
||||||
|
assert not response_schema.validate(await response.json())
|
||||||
rebuild_mock.assert_not_called()
|
rebuild_mock.assert_not_called()
|
||||||
|
@ -4,6 +4,8 @@ from aiohttp.test_utils import TestClient
|
|||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
|
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
|
from ahriman.web.schemas.error_schema import ErrorSchema
|
||||||
|
from ahriman.web.schemas.package_names_schema import PackageNamesSchema
|
||||||
from ahriman.web.views.service.remove import RemoveView
|
from ahriman.web.views.service.remove import RemoveView
|
||||||
|
|
||||||
|
|
||||||
@ -21,8 +23,11 @@ async def test_post(client: TestClient, mocker: MockerFixture) -> None:
|
|||||||
must call post request correctly
|
must call post request correctly
|
||||||
"""
|
"""
|
||||||
remove_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_remove")
|
remove_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_remove")
|
||||||
|
request_schema = PackageNamesSchema()
|
||||||
|
|
||||||
response = await client.post("/api/v1/service/remove", json={"packages": ["ahriman"]})
|
payload = {"packages": ["ahriman"]}
|
||||||
|
assert not request_schema.validate(payload)
|
||||||
|
response = await client.post("/api/v1/service/remove", json=payload)
|
||||||
assert response.ok
|
assert response.ok
|
||||||
remove_mock.assert_called_once_with(["ahriman"])
|
remove_mock.assert_called_once_with(["ahriman"])
|
||||||
|
|
||||||
@ -32,7 +37,9 @@ async def test_post_exception(client: TestClient, mocker: MockerFixture) -> None
|
|||||||
must raise exception on missing packages payload
|
must raise exception on missing packages payload
|
||||||
"""
|
"""
|
||||||
remove_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_remove")
|
remove_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_remove")
|
||||||
|
response_schema = ErrorSchema()
|
||||||
|
|
||||||
response = await client.post("/api/v1/service/remove")
|
response = await client.post("/api/v1/service/remove")
|
||||||
assert response.status == 400
|
assert response.status == 400
|
||||||
|
assert not response_schema.validate(await response.json())
|
||||||
remove_mock.assert_not_called()
|
remove_mock.assert_not_called()
|
||||||
|
@ -4,6 +4,8 @@ from aiohttp.test_utils import TestClient
|
|||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
|
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
|
from ahriman.web.schemas.error_schema import ErrorSchema
|
||||||
|
from ahriman.web.schemas.package_names_schema import PackageNamesSchema
|
||||||
from ahriman.web.views.service.request import RequestView
|
from ahriman.web.views.service.request import RequestView
|
||||||
|
|
||||||
|
|
||||||
@ -21,8 +23,11 @@ async def test_post(client: TestClient, mocker: MockerFixture) -> None:
|
|||||||
must call post request correctly
|
must call post request correctly
|
||||||
"""
|
"""
|
||||||
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add")
|
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add")
|
||||||
|
request_schema = PackageNamesSchema()
|
||||||
|
|
||||||
response = await client.post("/api/v1/service/request", json={"packages": ["ahriman"]})
|
payload = {"packages": ["ahriman"]}
|
||||||
|
assert not request_schema.validate(payload)
|
||||||
|
response = await client.post("/api/v1/service/request", json=payload)
|
||||||
assert response.ok
|
assert response.ok
|
||||||
add_mock.assert_called_once_with(["ahriman"], now=False)
|
add_mock.assert_called_once_with(["ahriman"], now=False)
|
||||||
|
|
||||||
@ -32,7 +37,9 @@ async def test_post_exception(client: TestClient, mocker: MockerFixture) -> None
|
|||||||
must raise exception on missing packages payload
|
must raise exception on missing packages payload
|
||||||
"""
|
"""
|
||||||
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add")
|
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add")
|
||||||
|
response_schema = ErrorSchema()
|
||||||
|
|
||||||
response = await client.post("/api/v1/service/request")
|
response = await client.post("/api/v1/service/request")
|
||||||
assert response.status == 400
|
assert response.status == 400
|
||||||
|
assert not response_schema.validate(await response.json())
|
||||||
add_mock.assert_not_called()
|
add_mock.assert_not_called()
|
||||||
|
@ -5,6 +5,9 @@ from pytest_mock import MockerFixture
|
|||||||
|
|
||||||
from ahriman.models.aur_package import AURPackage
|
from ahriman.models.aur_package import AURPackage
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
|
from ahriman.web.schemas.aur_package_schema import AURPackageSchema
|
||||||
|
from ahriman.web.schemas.error_schema import ErrorSchema
|
||||||
|
from ahriman.web.schemas.search_schema import SearchSchema
|
||||||
from ahriman.web.views.service.search import SearchView
|
from ahriman.web.views.service.search import SearchView
|
||||||
|
|
||||||
|
|
||||||
@ -12,7 +15,7 @@ async def test_get_permission() -> None:
|
|||||||
"""
|
"""
|
||||||
must return correct permission for the request
|
must return correct permission for the request
|
||||||
"""
|
"""
|
||||||
for method in ("GET", "HEAD"):
|
for method in ("GET",):
|
||||||
request = pytest.helpers.request("", "", method)
|
request = pytest.helpers.request("", "", method)
|
||||||
assert await SearchView.get_permission(request) == UserAccess.Reporter
|
assert await SearchView.get_permission(request) == UserAccess.Reporter
|
||||||
|
|
||||||
@ -22,11 +25,16 @@ async def test_get(client: TestClient, aur_package_ahriman: AURPackage, mocker:
|
|||||||
must call get request correctly
|
must call get request correctly
|
||||||
"""
|
"""
|
||||||
mocker.patch("ahriman.core.alpm.remote.AUR.multisearch", return_value=[aur_package_ahriman])
|
mocker.patch("ahriman.core.alpm.remote.AUR.multisearch", return_value=[aur_package_ahriman])
|
||||||
|
request_schema = SearchSchema()
|
||||||
|
response_schema = AURPackageSchema()
|
||||||
|
|
||||||
response = await client.get("/api/v1/service/search", params={"for": "ahriman"})
|
payload = {"for": ["ahriman"]}
|
||||||
|
assert not request_schema.validate(payload)
|
||||||
|
response = await client.get("/api/v1/service/search", params=payload)
|
||||||
assert response.ok
|
assert response.ok
|
||||||
assert await response.json() == [{"package": aur_package_ahriman.package_base,
|
assert await response.json() == [{"package": aur_package_ahriman.package_base,
|
||||||
"description": aur_package_ahriman.description}]
|
"description": aur_package_ahriman.description}]
|
||||||
|
assert not response_schema.validate(await response.json(), many=True)
|
||||||
|
|
||||||
|
|
||||||
async def test_get_exception(client: TestClient, mocker: MockerFixture) -> None:
|
async def test_get_exception(client: TestClient, mocker: MockerFixture) -> None:
|
||||||
@ -34,9 +42,11 @@ async def test_get_exception(client: TestClient, mocker: MockerFixture) -> None:
|
|||||||
must raise 400 on empty search string
|
must raise 400 on empty search string
|
||||||
"""
|
"""
|
||||||
search_mock = mocker.patch("ahriman.core.alpm.remote.AUR.multisearch")
|
search_mock = mocker.patch("ahriman.core.alpm.remote.AUR.multisearch")
|
||||||
|
response_schema = ErrorSchema()
|
||||||
|
|
||||||
response = await client.get("/api/v1/service/search")
|
response = await client.get("/api/v1/service/search")
|
||||||
assert response.status == 400
|
assert response.status == 400
|
||||||
|
assert not response_schema.validate(await response.json())
|
||||||
search_mock.assert_not_called()
|
search_mock.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
@ -45,8 +55,11 @@ async def test_get_empty(client: TestClient, mocker: MockerFixture) -> None:
|
|||||||
must raise 404 on empty search result
|
must raise 404 on empty search result
|
||||||
"""
|
"""
|
||||||
mocker.patch("ahriman.core.alpm.remote.AUR.multisearch", return_value=[])
|
mocker.patch("ahriman.core.alpm.remote.AUR.multisearch", return_value=[])
|
||||||
response = await client.get("/api/v1/service/search", params={"for": "ahriman"})
|
response_schema = ErrorSchema()
|
||||||
|
|
||||||
|
response = await client.get("/api/v1/service/search", params={"for": ["ahriman"]})
|
||||||
assert response.status == 404
|
assert response.status == 404
|
||||||
|
assert not response_schema.validate(await response.json())
|
||||||
|
|
||||||
|
|
||||||
async def test_get_join(client: TestClient, mocker: MockerFixture) -> None:
|
async def test_get_join(client: TestClient, mocker: MockerFixture) -> None:
|
||||||
@ -54,7 +67,10 @@ async def test_get_join(client: TestClient, mocker: MockerFixture) -> None:
|
|||||||
must join search args with space
|
must join search args with space
|
||||||
"""
|
"""
|
||||||
search_mock = mocker.patch("ahriman.core.alpm.remote.AUR.multisearch")
|
search_mock = mocker.patch("ahriman.core.alpm.remote.AUR.multisearch")
|
||||||
|
request_schema = SearchSchema()
|
||||||
|
|
||||||
response = await client.get("/api/v1/service/search", params=[("for", "ahriman"), ("for", "maybe")])
|
payload = {"for": ["ahriman", "maybe"]}
|
||||||
|
assert not request_schema.validate(payload)
|
||||||
|
response = await client.get("/api/v1/service/search", params=payload)
|
||||||
assert response.ok
|
assert response.ok
|
||||||
search_mock.assert_called_once_with("ahriman", "maybe", pacman=pytest.helpers.anyvar(int))
|
search_mock.assert_called_once_with("ahriman", "maybe", pacman=pytest.helpers.anyvar(int))
|
||||||
|
@ -5,6 +5,9 @@ from aiohttp.test_utils import TestClient
|
|||||||
from ahriman.models.build_status import BuildStatusEnum
|
from ahriman.models.build_status import BuildStatusEnum
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
|
from ahriman.web.schemas.error_schema import ErrorSchema
|
||||||
|
from ahriman.web.schemas.log_schema import LogSchema
|
||||||
|
from ahriman.web.schemas.logs_schema import LogsSchema
|
||||||
from ahriman.web.views.status.logs import LogsView
|
from ahriman.web.views.status.logs import LogsView
|
||||||
|
|
||||||
|
|
||||||
@ -12,7 +15,7 @@ async def test_get_permission() -> None:
|
|||||||
"""
|
"""
|
||||||
must return correct permission for the request
|
must return correct permission for the request
|
||||||
"""
|
"""
|
||||||
for method in ("GET", "HEAD"):
|
for method in ("GET",):
|
||||||
request = pytest.helpers.request("", "", method)
|
request = pytest.helpers.request("", "", method)
|
||||||
assert await LogsView.get_permission(request) == UserAccess.Reporter
|
assert await LogsView.get_permission(request) == UserAccess.Reporter
|
||||||
for method in ("DELETE", "POST"):
|
for method in ("DELETE", "POST"):
|
||||||
@ -54,11 +57,13 @@ async def test_get(client: TestClient, package_ahriman: Package) -> None:
|
|||||||
json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()})
|
json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()})
|
||||||
await client.post(f"/api/v1/packages/{package_ahriman.base}/logs",
|
await client.post(f"/api/v1/packages/{package_ahriman.base}/logs",
|
||||||
json={"created": 42.0, "message": "message", "process_id": 42})
|
json={"created": 42.0, "message": "message", "process_id": 42})
|
||||||
|
response_schema = LogsSchema()
|
||||||
|
|
||||||
response = await client.get(f"/api/v1/packages/{package_ahriman.base}/logs")
|
response = await client.get(f"/api/v1/packages/{package_ahriman.base}/logs")
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
|
|
||||||
logs = await response.json()
|
logs = await response.json()
|
||||||
|
assert not response_schema.validate(logs)
|
||||||
assert logs["logs"] == "[1970-01-01 00:00:42] message"
|
assert logs["logs"] == "[1970-01-01 00:00:42] message"
|
||||||
|
|
||||||
|
|
||||||
@ -66,8 +71,11 @@ async def test_get_not_found(client: TestClient, package_ahriman: Package) -> No
|
|||||||
"""
|
"""
|
||||||
must return not found for missing package
|
must return not found for missing package
|
||||||
"""
|
"""
|
||||||
|
response_schema = ErrorSchema()
|
||||||
|
|
||||||
response = await client.get(f"/api/v1/packages/{package_ahriman.base}/logs")
|
response = await client.get(f"/api/v1/packages/{package_ahriman.base}/logs")
|
||||||
assert response.status == 404
|
assert response.status == 404
|
||||||
|
assert not response_schema.validate(await response.json())
|
||||||
|
|
||||||
|
|
||||||
async def test_post(client: TestClient, package_ahriman: Package) -> None:
|
async def test_post(client: TestClient, package_ahriman: Package) -> None:
|
||||||
@ -76,10 +84,12 @@ async def test_post(client: TestClient, package_ahriman: Package) -> None:
|
|||||||
"""
|
"""
|
||||||
await client.post(f"/api/v1/packages/{package_ahriman.base}",
|
await client.post(f"/api/v1/packages/{package_ahriman.base}",
|
||||||
json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()})
|
json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()})
|
||||||
|
request_schema = LogSchema()
|
||||||
|
|
||||||
post_response = await client.post(f"/api/v1/packages/{package_ahriman.base}/logs",
|
payload = {"created": 42.0, "message": "message", "process_id": 42}
|
||||||
json={"created": 42.0, "message": "message", "process_id": 42})
|
assert not request_schema.validate(payload)
|
||||||
assert post_response.status == 204
|
response = await client.post(f"/api/v1/packages/{package_ahriman.base}/logs", json=payload)
|
||||||
|
assert response.status == 204
|
||||||
|
|
||||||
response = await client.get(f"/api/v1/packages/{package_ahriman.base}/logs")
|
response = await client.get(f"/api/v1/packages/{package_ahriman.base}/logs")
|
||||||
logs = await response.json()
|
logs = await response.json()
|
||||||
@ -90,5 +100,8 @@ async def test_post_exception(client: TestClient, package_ahriman: Package) -> N
|
|||||||
"""
|
"""
|
||||||
must raise exception on invalid payload
|
must raise exception on invalid payload
|
||||||
"""
|
"""
|
||||||
post_response = await client.post(f"/api/v1/packages/{package_ahriman.base}/logs", json={})
|
response_schema = ErrorSchema()
|
||||||
assert post_response.status == 400
|
|
||||||
|
response = await client.post(f"/api/v1/packages/{package_ahriman.base}/logs", json={})
|
||||||
|
assert response.status == 400
|
||||||
|
assert not response_schema.validate(await response.json())
|
||||||
|
@ -5,6 +5,8 @@ from aiohttp.test_utils import TestClient
|
|||||||
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
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
|
from ahriman.web.schemas.error_schema import ErrorSchema
|
||||||
|
from ahriman.web.schemas.package_status_schema import PackageStatusSchema, PackageStatusSimplifiedSchema
|
||||||
from ahriman.web.views.status.package import PackageView
|
from ahriman.web.views.status.package import PackageView
|
||||||
|
|
||||||
|
|
||||||
@ -12,7 +14,7 @@ async def test_get_permission() -> None:
|
|||||||
"""
|
"""
|
||||||
must return correct permission for the request
|
must return correct permission for the request
|
||||||
"""
|
"""
|
||||||
for method in ("GET", "HEAD"):
|
for method in ("GET",):
|
||||||
request = pytest.helpers.request("", "", method)
|
request = pytest.helpers.request("", "", method)
|
||||||
assert await PackageView.get_permission(request) == UserAccess.Read
|
assert await PackageView.get_permission(request) == UserAccess.Read
|
||||||
for method in ("DELETE", "POST"):
|
for method in ("DELETE", "POST"):
|
||||||
@ -64,11 +66,14 @@ async def test_get(client: TestClient, package_ahriman: Package, package_python_
|
|||||||
json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()})
|
json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()})
|
||||||
await client.post(f"/api/v1/packages/{package_python_schedule.base}",
|
await client.post(f"/api/v1/packages/{package_python_schedule.base}",
|
||||||
json={"status": BuildStatusEnum.Success.value, "package": package_python_schedule.view()})
|
json={"status": BuildStatusEnum.Success.value, "package": package_python_schedule.view()})
|
||||||
|
response_schema = PackageStatusSchema()
|
||||||
|
|
||||||
response = await client.get(f"/api/v1/packages/{package_ahriman.base}")
|
response = await client.get(f"/api/v1/packages/{package_ahriman.base}")
|
||||||
assert response.ok
|
assert response.ok
|
||||||
|
json = await response.json()
|
||||||
|
assert not response_schema.validate(json, many=True)
|
||||||
|
|
||||||
packages = [Package.from_json(item["package"]) for item in await response.json()]
|
packages = [Package.from_json(item["package"]) for item in json]
|
||||||
assert packages
|
assert packages
|
||||||
assert {package.base for package in packages} == {package_ahriman.base}
|
assert {package.base for package in packages} == {package_ahriman.base}
|
||||||
|
|
||||||
@ -77,18 +82,23 @@ async def test_get_not_found(client: TestClient, package_ahriman: Package) -> No
|
|||||||
"""
|
"""
|
||||||
must return Not Found for unknown package
|
must return Not Found for unknown package
|
||||||
"""
|
"""
|
||||||
|
response_schema = ErrorSchema()
|
||||||
|
|
||||||
response = await client.get(f"/api/v1/packages/{package_ahriman.base}")
|
response = await client.get(f"/api/v1/packages/{package_ahriman.base}")
|
||||||
assert response.status == 404
|
assert response.status == 404
|
||||||
|
assert not response_schema.validate(await response.json())
|
||||||
|
|
||||||
|
|
||||||
async def test_post(client: TestClient, package_ahriman: Package) -> None:
|
async def test_post(client: TestClient, package_ahriman: Package) -> None:
|
||||||
"""
|
"""
|
||||||
must update package status
|
must update package status
|
||||||
"""
|
"""
|
||||||
post_response = await client.post(
|
request_schema = PackageStatusSimplifiedSchema()
|
||||||
f"/api/v1/packages/{package_ahriman.base}",
|
|
||||||
json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()})
|
payload = {"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()}
|
||||||
assert post_response.status == 204
|
assert not request_schema.validate(payload)
|
||||||
|
response = await client.post(f"/api/v1/packages/{package_ahriman.base}", json=payload)
|
||||||
|
assert response.status == 204
|
||||||
|
|
||||||
response = await client.get(f"/api/v1/packages/{package_ahriman.base}")
|
response = await client.get(f"/api/v1/packages/{package_ahriman.base}")
|
||||||
assert response.ok
|
assert response.ok
|
||||||
@ -98,22 +108,28 @@ async def test_post_exception(client: TestClient, package_ahriman: Package) -> N
|
|||||||
"""
|
"""
|
||||||
must raise exception on invalid payload
|
must raise exception on invalid payload
|
||||||
"""
|
"""
|
||||||
post_response = await client.post(f"/api/v1/packages/{package_ahriman.base}", json={})
|
response_schema = ErrorSchema()
|
||||||
assert post_response.status == 400
|
|
||||||
|
response = await client.post(f"/api/v1/packages/{package_ahriman.base}", json={})
|
||||||
|
assert response.status == 400
|
||||||
|
assert not response_schema.validate(await response.json())
|
||||||
|
|
||||||
|
|
||||||
async def test_post_light(client: TestClient, package_ahriman: Package) -> None:
|
async def test_post_light(client: TestClient, package_ahriman: Package) -> None:
|
||||||
"""
|
"""
|
||||||
must update package status only
|
must update package status only
|
||||||
"""
|
"""
|
||||||
post_response = await client.post(
|
request_schema = PackageStatusSimplifiedSchema()
|
||||||
f"/api/v1/packages/{package_ahriman.base}",
|
|
||||||
json={"status": BuildStatusEnum.Unknown.value, "package": package_ahriman.view()})
|
|
||||||
assert post_response.status == 204
|
|
||||||
|
|
||||||
post_response = await client.post(
|
payload = {"status": BuildStatusEnum.Unknown.value, "package": package_ahriman.view()}
|
||||||
f"/api/v1/packages/{package_ahriman.base}", json={"status": BuildStatusEnum.Success.value})
|
assert not request_schema.validate(payload)
|
||||||
assert post_response.status == 204
|
response = await client.post(f"/api/v1/packages/{package_ahriman.base}", json=payload)
|
||||||
|
assert response.status == 204
|
||||||
|
|
||||||
|
payload = {"status": BuildStatusEnum.Success.value}
|
||||||
|
assert not request_schema.validate(payload)
|
||||||
|
response = await client.post(f"/api/v1/packages/{package_ahriman.base}", json=payload)
|
||||||
|
assert response.status == 204
|
||||||
|
|
||||||
response = await client.get(f"/api/v1/packages/{package_ahriman.base}")
|
response = await client.get(f"/api/v1/packages/{package_ahriman.base}")
|
||||||
assert response.ok
|
assert response.ok
|
||||||
@ -128,6 +144,11 @@ async def test_post_not_found(client: TestClient, package_ahriman: Package) -> N
|
|||||||
"""
|
"""
|
||||||
must raise exception on status update for unknown package
|
must raise exception on status update for unknown package
|
||||||
"""
|
"""
|
||||||
post_response = await client.post(
|
request_schema = PackageStatusSimplifiedSchema()
|
||||||
f"/api/v1/packages/{package_ahriman.base}", json={"status": BuildStatusEnum.Success.value})
|
response_schema = ErrorSchema()
|
||||||
assert post_response.status == 400
|
|
||||||
|
payload = {"status": BuildStatusEnum.Success.value}
|
||||||
|
assert not request_schema.validate(payload)
|
||||||
|
response = await client.post(f"/api/v1/packages/{package_ahriman.base}", json=payload)
|
||||||
|
assert response.status == 400
|
||||||
|
assert not response_schema.validate(await response.json())
|
||||||
|
@ -6,6 +6,7 @@ from pytest_mock import MockerFixture
|
|||||||
from ahriman.models.build_status import BuildStatusEnum
|
from ahriman.models.build_status import BuildStatusEnum
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
|
from ahriman.web.schemas.package_status_schema import PackageStatusSchema
|
||||||
from ahriman.web.views.status.packages import PackagesView
|
from ahriman.web.views.status.packages import PackagesView
|
||||||
|
|
||||||
|
|
||||||
@ -13,7 +14,7 @@ async def test_get_permission() -> None:
|
|||||||
"""
|
"""
|
||||||
must return correct permission for the request
|
must return correct permission for the request
|
||||||
"""
|
"""
|
||||||
for method in ("GET", "HEAD"):
|
for method in ("GET",):
|
||||||
request = pytest.helpers.request("", "", method)
|
request = pytest.helpers.request("", "", method)
|
||||||
assert await PackagesView.get_permission(request) == UserAccess.Read
|
assert await PackagesView.get_permission(request) == UserAccess.Read
|
||||||
for method in ("POST",):
|
for method in ("POST",):
|
||||||
@ -29,11 +30,14 @@ async def test_get(client: TestClient, package_ahriman: Package, package_python_
|
|||||||
json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()})
|
json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()})
|
||||||
await client.post(f"/api/v1/packages/{package_python_schedule.base}",
|
await client.post(f"/api/v1/packages/{package_python_schedule.base}",
|
||||||
json={"status": BuildStatusEnum.Success.value, "package": package_python_schedule.view()})
|
json={"status": BuildStatusEnum.Success.value, "package": package_python_schedule.view()})
|
||||||
|
response_schema = PackageStatusSchema()
|
||||||
|
|
||||||
response = await client.get("/api/v1/packages")
|
response = await client.get("/api/v1/packages")
|
||||||
assert response.ok
|
assert response.ok
|
||||||
|
json = await response.json()
|
||||||
|
assert not response_schema.validate(json, many=True)
|
||||||
|
|
||||||
packages = [Package.from_json(item["package"]) for item in await response.json()]
|
packages = [Package.from_json(item["package"]) for item in json]
|
||||||
assert packages
|
assert packages
|
||||||
assert {package.base for package in packages} == {package_ahriman.base, package_python_schedule.base}
|
assert {package.base for package in packages} == {package_ahriman.base, package_python_schedule.base}
|
||||||
|
|
||||||
@ -43,6 +47,7 @@ async def test_post(client: TestClient, mocker: MockerFixture) -> None:
|
|||||||
must be able to reload packages
|
must be able to reload packages
|
||||||
"""
|
"""
|
||||||
load_mock = mocker.patch("ahriman.core.status.watcher.Watcher.load")
|
load_mock = mocker.patch("ahriman.core.status.watcher.Watcher.load")
|
||||||
|
|
||||||
response = await client.post("/api/v1/packages")
|
response = await client.post("/api/v1/packages")
|
||||||
assert response.status == 204
|
assert response.status == 204
|
||||||
load_mock.assert_called_once_with()
|
load_mock.assert_called_once_with()
|
||||||
|
@ -9,6 +9,9 @@ from ahriman.models.build_status import BuildStatusEnum
|
|||||||
from ahriman.models.internal_status import InternalStatus
|
from ahriman.models.internal_status import InternalStatus
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
|
from ahriman.web.schemas.error_schema import ErrorSchema
|
||||||
|
from ahriman.web.schemas.internal_status_schema import InternalStatusSchema
|
||||||
|
from ahriman.web.schemas.status_schema import StatusSchema
|
||||||
from ahriman.web.views.status.status import StatusView
|
from ahriman.web.views.status.status import StatusView
|
||||||
|
|
||||||
|
|
||||||
@ -16,7 +19,7 @@ async def test_get_permission() -> None:
|
|||||||
"""
|
"""
|
||||||
must return correct permission for the request
|
must return correct permission for the request
|
||||||
"""
|
"""
|
||||||
for method in ("GET", "HEAD"):
|
for method in ("GET",):
|
||||||
request = pytest.helpers.request("", "", method)
|
request = pytest.helpers.request("", "", method)
|
||||||
assert await StatusView.get_permission(request) == UserAccess.Read
|
assert await StatusView.get_permission(request) == UserAccess.Read
|
||||||
for method in ("POST",):
|
for method in ("POST",):
|
||||||
@ -30,11 +33,13 @@ async def test_get(client: TestClient, package_ahriman: Package) -> None:
|
|||||||
"""
|
"""
|
||||||
await client.post(f"/api/v1/packages/{package_ahriman.base}",
|
await client.post(f"/api/v1/packages/{package_ahriman.base}",
|
||||||
json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()})
|
json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()})
|
||||||
|
response_schema = InternalStatusSchema()
|
||||||
|
|
||||||
response = await client.get("/api/v1/status")
|
response = await client.get("/api/v1/status")
|
||||||
assert response.ok
|
assert response.ok
|
||||||
|
|
||||||
json = await response.json()
|
json = await response.json()
|
||||||
|
assert not response_schema.validate(json)
|
||||||
|
|
||||||
assert json["version"] == version.__version__
|
assert json["version"] == version.__version__
|
||||||
assert json["packages"]
|
assert json["packages"]
|
||||||
assert json["packages"]["total"] == 1
|
assert json["packages"]["total"] == 1
|
||||||
@ -44,7 +49,10 @@ async def test_post(client: TestClient) -> None:
|
|||||||
"""
|
"""
|
||||||
must update service status correctly
|
must update service status correctly
|
||||||
"""
|
"""
|
||||||
|
request_schema = StatusSchema()
|
||||||
|
|
||||||
payload = {"status": BuildStatusEnum.Success.value}
|
payload = {"status": BuildStatusEnum.Success.value}
|
||||||
|
assert not request_schema.validate(payload)
|
||||||
post_response = await client.post("/api/v1/status", json=payload)
|
post_response = await client.post("/api/v1/status", json=payload)
|
||||||
assert post_response.status == 204
|
assert post_response.status == 204
|
||||||
|
|
||||||
@ -59,8 +67,11 @@ async def test_post_exception(client: TestClient) -> None:
|
|||||||
"""
|
"""
|
||||||
must raise exception on invalid payload
|
must raise exception on invalid payload
|
||||||
"""
|
"""
|
||||||
post_response = await client.post("/api/v1/status", json={})
|
response_schema = ErrorSchema()
|
||||||
assert post_response.status == 400
|
|
||||||
|
response = await client.post("/api/v1/status", json={})
|
||||||
|
assert response.status == 400
|
||||||
|
assert not response_schema.validate(await response.json())
|
||||||
|
|
||||||
|
|
||||||
async def test_post_exception_inside(client: TestClient, mocker: MockerFixture) -> None:
|
async def test_post_exception_inside(client: TestClient, mocker: MockerFixture) -> None:
|
||||||
@ -69,6 +80,8 @@ async def test_post_exception_inside(client: TestClient, mocker: MockerFixture)
|
|||||||
"""
|
"""
|
||||||
payload = {"status": BuildStatusEnum.Success.value}
|
payload = {"status": BuildStatusEnum.Success.value}
|
||||||
mocker.patch("ahriman.core.status.watcher.Watcher.update_self", side_effect=Exception())
|
mocker.patch("ahriman.core.status.watcher.Watcher.update_self", side_effect=Exception())
|
||||||
|
response_schema = ErrorSchema()
|
||||||
|
|
||||||
post_response = await client.post("/api/v1/status", json=payload)
|
response = await client.post("/api/v1/status", json=payload)
|
||||||
assert post_response.status == 500
|
assert response.status == 500
|
||||||
|
assert not response_schema.validate(await response.json())
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from multidict import MultiDict
|
from multidict import MultiDict
|
||||||
|
from aiohttp.test_utils import TestClient
|
||||||
|
|
||||||
|
from ahriman.models.user_access import UserAccess
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
|
|
||||||
|
|
||||||
@ -37,11 +39,17 @@ async def test_get_permission(base: BaseView) -> None:
|
|||||||
"""
|
"""
|
||||||
must search for permission attribute in class
|
must search for permission attribute in class
|
||||||
"""
|
"""
|
||||||
|
for method in ("DELETE", "GET", "POST"):
|
||||||
|
setattr(BaseView, f"{method.upper()}_PERMISSION", "permission")
|
||||||
|
|
||||||
for method in ("DELETE", "GET", "HEAD", "POST"):
|
for method in ("DELETE", "GET", "HEAD", "POST"):
|
||||||
request = pytest.helpers.request(base.request.app, "", method)
|
request = pytest.helpers.request(base.request.app, "", method)
|
||||||
setattr(BaseView, f"{method.upper()}_PERMISSION", "permission")
|
|
||||||
assert await base.get_permission(request) == "permission"
|
assert await base.get_permission(request) == "permission"
|
||||||
|
|
||||||
|
for method in ("OPTIONS",):
|
||||||
|
request = pytest.helpers.request(base.request.app, "", method)
|
||||||
|
assert await base.get_permission(request) == UserAccess.Unauthorized
|
||||||
|
|
||||||
|
|
||||||
def test_get_non_empty() -> None:
|
def test_get_non_empty() -> None:
|
||||||
"""
|
"""
|
||||||
@ -61,35 +69,6 @@ def test_get_non_empty() -> None:
|
|||||||
BaseView.get_non_empty(lambda k: [], "key")
|
BaseView.get_non_empty(lambda k: [], "key")
|
||||||
|
|
||||||
|
|
||||||
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:
|
async def test_data_as_json(base: BaseView) -> None:
|
||||||
"""
|
"""
|
||||||
must parse multi value form payload
|
must parse multi value form payload
|
||||||
@ -121,3 +100,49 @@ async def test_data_as_json_with_list_keys(base: BaseView) -> None:
|
|||||||
|
|
||||||
base._request = pytest.helpers.request(base.request.app, "", "", data=get_data)
|
base._request = pytest.helpers.request(base.request.app, "", "", data=get_data)
|
||||||
assert await base.data_as_json(["key1"]) == {"key1": ["value1"]}
|
assert await base.data_as_json(["key1"]) == {"key1": ["value1"]}
|
||||||
|
|
||||||
|
|
||||||
|
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_head(client: TestClient) -> None:
|
||||||
|
"""
|
||||||
|
must implement head as get method
|
||||||
|
"""
|
||||||
|
response = await client.head("/")
|
||||||
|
assert response.ok
|
||||||
|
assert await response.text() == ""
|
||||||
|
|
||||||
|
|
||||||
|
async def test_head_not_allowed(client: TestClient) -> None:
|
||||||
|
"""
|
||||||
|
must raise MethodNotAllowed in case if no get method was implemented
|
||||||
|
"""
|
||||||
|
response = await client.head("/api/v1/service/add")
|
||||||
|
assert response.status == 405
|
||||||
|
@ -10,7 +10,7 @@ async def test_get_permission() -> None:
|
|||||||
"""
|
"""
|
||||||
must return correct permission for the request
|
must return correct permission for the request
|
||||||
"""
|
"""
|
||||||
for method in ("GET", "HEAD"):
|
for method in ("GET",):
|
||||||
request = pytest.helpers.request("", "", method)
|
request = pytest.helpers.request("", "", method)
|
||||||
assert await IndexView.get_permission(request) == UserAccess.Unauthorized
|
assert await IndexView.get_permission(request) == UserAccess.Unauthorized
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user