use http client class for all http requests

This commit is contained in:
Evgenii Alekseev 2023-08-23 03:27:42 +03:00
parent 6dfe1b92f2
commit 33e68a59e2
40 changed files with 603 additions and 570 deletions

View File

@ -114,6 +114,7 @@ Web server settings. If any of ``host``/``port`` is not set, web integration wil
* ``port`` - port to bind, int, optional. * ``port`` - port to bind, int, optional.
* ``static_path`` - path to directory with static files, string, required. * ``static_path`` - path to directory with static files, string, required.
* ``templates`` - path to templates directory, string, required. * ``templates`` - path to templates directory, string, required.
* ``timeout`` - HTTP request timeout in seconds, int, optional, default is ``30``.
* ``unix_socket`` - path to the listening unix socket, string, optional. If set, server will create the socket on the specified address which can (and will) be used by application. Note, that unlike usual host/port configuration, unix socket allows to perform requests without authorization. * ``unix_socket`` - path to the listening unix socket, string, optional. If set, server will create the socket on the specified address which can (and will) be used by application. Note, that unlike usual host/port configuration, unix socket allows to perform requests without authorization.
* ``unix_socket_unsafe`` - set unsafe (o+w) permissions to unix socket, boolean, optional, default ``yes``. This option is enabled by default, because it is supposed that unix socket is created in safe environment (only web service is supposed to be used in unsafe), but it can be disabled by configuration. * ``unix_socket_unsafe`` - set unsafe (o+w) permissions to unix socket, boolean, optional, default ``yes``. This option is enabled by default, because it is supposed that unix socket is created in safe environment (only web service is supposed to be used in unsafe), but it can be disabled by configuration.
* ``username`` - username to authorize in web service in order to update service status, string, required in case if authorization enabled. * ``username`` - username to authorize in web service in order to update service status, string, required in case if authorization enabled.
@ -311,6 +312,7 @@ This feature requires Github key creation (see below). Section name must be eith
Section name must be either ``remote-service`` (plus optional architecture name, e.g. ``remote-service:x86_64``) or random name with ``type`` set. Section name must be either ``remote-service`` (plus optional architecture name, e.g. ``remote-service:x86_64``) or random name with ``type`` set.
* ``type`` - type of the report, string, optional, must be set to ``remote-service`` if exists. * ``type`` - type of the report, string, optional, must be set to ``remote-service`` if exists.
* ``timeout`` - HTTP request timeout in seconds, int, optional, default is ``30``.
``rsync`` type ``rsync`` type
^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^

View File

@ -17,14 +17,11 @@
# 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 requests
from typing import Any from typing import Any
from ahriman.core.alpm.pacman import Pacman from ahriman.core.alpm.pacman import Pacman
from ahriman.core.alpm.remote import Remote from ahriman.core.alpm.remote import Remote
from ahriman.core.exceptions import PackageInfoError, UnknownPackageError from ahriman.core.exceptions import PackageInfoError, UnknownPackageError
from ahriman.core.util import exception_response_text
from ahriman.models.aur_package import AURPackage from ahriman.models.aur_package import AURPackage
@ -36,13 +33,11 @@ class AUR(Remote):
DEFAULT_AUR_URL(str): (class attribute) default AUR url DEFAULT_AUR_URL(str): (class attribute) default AUR url
DEFAULT_RPC_URL(str): (class attribute) default AUR RPC url DEFAULT_RPC_URL(str): (class attribute) default AUR RPC url
DEFAULT_RPC_VERSION(str): (class attribute) default AUR RPC version DEFAULT_RPC_VERSION(str): (class attribute) default AUR RPC version
DEFAULT_TIMEOUT(int): (class attribute) HTTP request timeout in seconds
""" """
DEFAULT_AUR_URL = "https://aur.archlinux.org" DEFAULT_AUR_URL = "https://aur.archlinux.org"
DEFAULT_RPC_URL = f"{DEFAULT_AUR_URL}/rpc" DEFAULT_RPC_URL = f"{DEFAULT_AUR_URL}/rpc"
DEFAULT_RPC_VERSION = "5" DEFAULT_RPC_VERSION = "5"
DEFAULT_TIMEOUT = 30
@classmethod @classmethod
def remote_git_url(cls, package_base: str, repository: str) -> str: def remote_git_url(cls, package_base: str, repository: str) -> str:
@ -91,7 +86,7 @@ class AUR(Remote):
raise PackageInfoError(error_details) raise PackageInfoError(error_details)
return [AURPackage.from_json(package) for package in response["results"]] return [AURPackage.from_json(package) for package in response["results"]]
def make_request(self, request_type: str, *args: str, **kwargs: str) -> list[AURPackage]: def aur_request(self, request_type: str, *args: str, **kwargs: str) -> list[AURPackage]:
""" """
perform request to AUR RPC perform request to AUR RPC
@ -103,34 +98,20 @@ class AUR(Remote):
Returns: Returns:
list[AURPackage]: response parsed to package list list[AURPackage]: response parsed to package list
""" """
query: dict[str, Any] = { query: list[tuple[str, str]] = [
"type": request_type, ("type", request_type),
"v": self.DEFAULT_RPC_VERSION ("v", self.DEFAULT_RPC_VERSION),
} ]
arg_query = "arg[]" if len(args) > 1 else "arg" arg_query = "arg[]" if len(args) > 1 else "arg"
query[arg_query] = list(args) for arg in args:
query.append((arg_query, arg))
for key, value in kwargs.items(): for key, value in kwargs.items():
query[key] = value query.append((key, value))
try: response = self.make_request("GET", self.DEFAULT_RPC_URL, params=query)
response = requests.get( return self.parse_response(response.json())
self.DEFAULT_RPC_URL,
params=query,
headers={"User-Agent": self.DEFAULT_USER_AGENT},
timeout=self.DEFAULT_TIMEOUT)
response.raise_for_status()
return self.parse_response(response.json())
except requests.HTTPError as e:
self.logger.exception(
"could not perform request by using type %s: %s",
request_type,
exception_response_text(e))
raise
except Exception:
self.logger.exception("could not perform request by using type %s", request_type)
raise
def package_info(self, package_name: str, *, pacman: Pacman) -> AURPackage: def package_info(self, package_name: str, *, pacman: Pacman) -> AURPackage:
""" """
@ -146,7 +127,7 @@ class AUR(Remote):
Raises: Raises:
UnknownPackageError: package doesn't exist UnknownPackageError: package doesn't exist
""" """
packages = self.make_request("info", package_name) packages = self.aur_request("info", package_name)
try: try:
return next(package for package in packages if package.name == package_name) return next(package for package in packages if package.name == package_name)
except StopIteration: except StopIteration:
@ -163,4 +144,4 @@ class AUR(Remote):
Returns: Returns:
list[AURPackage]: list of packages which match the criteria list[AURPackage]: list of packages which match the criteria
""" """
return self.make_request("search", *keywords, by="name-desc") return self.aur_request("search", *keywords, by="name-desc")

View File

@ -17,14 +17,11 @@
# 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 requests
from typing import Any from typing import Any
from ahriman.core.alpm.pacman import Pacman from ahriman.core.alpm.pacman import Pacman
from ahriman.core.alpm.remote import Remote from ahriman.core.alpm.remote import Remote
from ahriman.core.exceptions import PackageInfoError, UnknownPackageError from ahriman.core.exceptions import PackageInfoError, UnknownPackageError
from ahriman.core.util import exception_response_text
from ahriman.models.aur_package import AURPackage from ahriman.models.aur_package import AURPackage
@ -37,14 +34,12 @@ class Official(Remote):
DEFAULT_ARCHLINUX_GIT_URL(str): (class attribute) default url for git packages DEFAULT_ARCHLINUX_GIT_URL(str): (class attribute) default url for git packages
DEFAULT_SEARCH_REPOSITORIES(list[str]): (class attribute) default list of repositories to search DEFAULT_SEARCH_REPOSITORIES(list[str]): (class attribute) default list of repositories to search
DEFAULT_RPC_URL(str): (class attribute) default archlinux repositories RPC url DEFAULT_RPC_URL(str): (class attribute) default archlinux repositories RPC url
DEFAULT_TIMEOUT(int): (class attribute) HTTP request timeout in seconds
""" """
DEFAULT_ARCHLINUX_GIT_URL = "https://gitlab.archlinux.org" DEFAULT_ARCHLINUX_GIT_URL = "https://gitlab.archlinux.org"
DEFAULT_ARCHLINUX_URL = "https://archlinux.org" DEFAULT_ARCHLINUX_URL = "https://archlinux.org"
DEFAULT_SEARCH_REPOSITORIES = ["Core", "Extra", "Multilib"] DEFAULT_SEARCH_REPOSITORIES = ["Core", "Extra", "Multilib"]
DEFAULT_RPC_URL = "https://archlinux.org/packages/search/json" DEFAULT_RPC_URL = "https://archlinux.org/packages/search/json"
DEFAULT_TIMEOUT = 30
@classmethod @classmethod
def remote_git_url(cls, package_base: str, repository: str) -> str: def remote_git_url(cls, package_base: str, repository: str) -> str:
@ -91,7 +86,7 @@ class Official(Remote):
raise PackageInfoError("API validation error") raise PackageInfoError("API validation error")
return [AURPackage.from_repo(package) for package in response["results"]] return [AURPackage.from_repo(package) for package in response["results"]]
def make_request(self, *args: str, by: str) -> list[AURPackage]: def arch_request(self, *args: str, by: str) -> list[AURPackage]:
""" """
perform request to official repositories RPC perform request to official repositories RPC
@ -102,20 +97,15 @@ class Official(Remote):
Returns: Returns:
list[AURPackage]: response parsed to package list list[AURPackage]: response parsed to package list
""" """
try: query: list[tuple[str, str]] = [
response = requests.get( ("repo", repository)
self.DEFAULT_RPC_URL, for repository in self.DEFAULT_SEARCH_REPOSITORIES
params={by: args, "repo": self.DEFAULT_SEARCH_REPOSITORIES}, ]
headers={"User-Agent": self.DEFAULT_USER_AGENT}, for arg in args:
timeout=self.DEFAULT_TIMEOUT) query.append((by, arg))
response.raise_for_status()
return self.parse_response(response.json()) response = self.make_request("GET", self.DEFAULT_RPC_URL, params=query)
except requests.HTTPError as e: return self.parse_response(response.json())
self.logger.exception("could not perform request: %s", exception_response_text(e))
raise
except Exception:
self.logger.exception("could not perform request")
raise
def package_info(self, package_name: str, *, pacman: Pacman) -> AURPackage: def package_info(self, package_name: str, *, pacman: Pacman) -> AURPackage:
""" """
@ -131,7 +121,7 @@ class Official(Remote):
Raises: Raises:
UnknownPackageError: package doesn't exist UnknownPackageError: package doesn't exist
""" """
packages = self.make_request(package_name, by="name") packages = self.arch_request(package_name, by="name")
try: try:
return next(package for package in packages if package.name == package_name) return next(package for package in packages if package.name == package_name)
except StopIteration: except StopIteration:
@ -148,4 +138,4 @@ class Official(Remote):
Returns: Returns:
list[AURPackage]: list of packages which match the criteria list[AURPackage]: list of packages which match the criteria
""" """
return self.make_request(*keywords, by="q") return self.arch_request(*keywords, by="q")

View File

@ -17,19 +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/>.
# #
from ahriman import __version__
from ahriman.core.alpm.pacman import Pacman from ahriman.core.alpm.pacman import Pacman
from ahriman.core.log import LazyLogging from ahriman.core.http import SyncHttpClient
from ahriman.models.aur_package import AURPackage from ahriman.models.aur_package import AURPackage
class Remote(LazyLogging): class Remote(SyncHttpClient):
""" """
base class for remote package search base class for remote package search
Attributes:
DEFAULT_USER_AGENT(str): (class attribute) default user agent
Examples: Examples:
These classes are designed to be used without instancing. In order to achieve it several class methods are These classes are designed to be used without instancing. In order to achieve it several class methods are
provided: ``info``, ``multisearch`` and ``search``. Thus, the basic flow is the following:: provided: ``info``, ``multisearch`` and ``search``. Thus, the basic flow is the following::
@ -43,8 +39,6 @@ class Remote(LazyLogging):
directly, whereas ``multisearch`` splits search one by one and finds intersection between search results. directly, whereas ``multisearch`` splits search one by one and finds intersection between search results.
""" """
DEFAULT_USER_AGENT = f"ahriman/{__version__}"
@classmethod @classmethod
def info(cls, package_name: str, *, pacman: Pacman) -> AURPackage: def info(cls, package_name: str, *, pacman: Pacman) -> AURPackage:
""" """

View File

@ -0,0 +1,20 @@
#
# 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 ahriman.core.http.sync_http_client import MultipartType, SyncHttpClient

View File

@ -0,0 +1,137 @@
#
# 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 requests
from functools import cached_property
from typing import Any, IO, Literal
from ahriman import __version__
from ahriman.core.configuration import Configuration
from ahriman.core.log import LazyLogging
# filename, file, content-type, headers
MultipartType = tuple[str, IO[bytes], str, dict[str, str]]
class SyncHttpClient(LazyLogging):
"""
wrapper around requests library to reduce boilerplate
Attributes:
auth(tuple[str, str] | None): HTTP basic auth object if set
suppress_errors(bool): suppress logging of request errors
timeout(int): HTTP request timeout in seconds
"""
def __init__(self, section: str | None = None, configuration: Configuration | None = None, *,
suppress_errors: bool = False) -> None:
"""
default constructor
Args:
section(str, optional): settings section name (Default value = None)
configuration(Configuration | None): configuration instance (Default value = None)
suppress_errors(bool, optional): suppress logging of request errors (Default value = False)
"""
if configuration is None:
configuration = Configuration() # dummy configuration
if section is None:
section = configuration.default_section
username = configuration.get(section, "username", fallback=None)
password = configuration.get(section, "password", fallback=None)
self.auth = (username, password) if username and password else None
self.timeout = configuration.getint(section, "timeout", fallback=30)
self.suppress_errors = suppress_errors
@cached_property
def session(self) -> requests.Session:
"""
get or create session
Returns:
request.Session: created session object
"""
session = requests.Session()
session.headers["User-Agent"] = f"ahriman/{__version__}"
return session
@staticmethod
def exception_response_text(exception: requests.exceptions.RequestException) -> str:
"""
safe response exception text generation
Args:
exception(requests.exceptions.RequestException): exception raised
Returns:
str: text of the response if it is not None and empty string otherwise
"""
result: str = exception.response.text if exception.response is not None else ""
return result
def make_request(self, method: Literal["DELETE", "GET", "POST", "PUT"], url: str, *,
headers: dict[str, str] | None = None,
params: list[tuple[str, str]] | None = None,
data: Any | None = None,
json: dict[str, Any] | None = None,
files: dict[str, MultipartType] | None = None,
session: requests.Session | None = None,
suppress_errors: bool | None = None) -> requests.Response:
"""
perform request with specified parameters
Args:
method(Literal["DELETE", "GET", "POST", "PUT"]): HTTP method to call
url(str): remote url to call
headers(dict[str, str] | None, optional): request headers (Default value = None)
params(list[tuple[str, str]] | None, optional): request query parameters (Default value = None)
data(Any | None, optional): request raw data parameters (Default value = None)
json(dict[str, Any] | None, optional): request json parameters (Default value = None)
files(dict[str, MultipartType] | None, optional): multipart upload (Default value = None)
session(requests.Session | None, optional): session object if any (Default value = None)
suppress_errors(bool | None, optional): suppress logging errors (e.g. if no web server available). If none
set, the instance-wide value will be used (Default value = None)
Returns:
requests.Response: response object
"""
# defaults
if suppress_errors is None:
suppress_errors = self.suppress_errors
if session is None:
session = self.session
try:
response = session.request(method, url, params=params, data=data, headers=headers, files=files, json=json,
auth=self.auth, timeout=self.timeout)
response.raise_for_status()
return response
except requests.HTTPError as ex:
if not suppress_errors:
self.logger.exception("could not perform http request: %s", self.exception_response_text(ex))
raise
except Exception:
if not suppress_errors:
self.logger.exception("could not perform http request")
raise

View File

@ -81,8 +81,9 @@ class RemoteCall(Report):
""" """
try: try:
response = self.client.make_request("GET", f"/api/v1/service/process/{process_id}") response = self.client.make_request("GET", f"/api/v1/service/process/{process_id}")
except requests.RequestException as e: except requests.HTTPError as ex:
if e.response is not None and e.response.status_code == 404: status_code = ex.response.status_code if ex.response is not None else None
if status_code == 404:
return False return False
raise raise

View File

@ -17,17 +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 requests # technically we could use python-telegram-bot, but it is just a single request, c'mon
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.http import SyncHttpClient
from ahriman.core.report.jinja_template import JinjaTemplate from ahriman.core.report.jinja_template import JinjaTemplate
from ahriman.core.report.report import Report from ahriman.core.report.report import Report
from ahriman.core.util import exception_response_text
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.result import Result from ahriman.models.result import Result
class Telegram(Report, JinjaTemplate): class Telegram(Report, JinjaTemplate, SyncHttpClient):
""" """
telegram report generator telegram report generator
@ -38,7 +36,6 @@ class Telegram(Report, JinjaTemplate):
chat_id(str): chat id to post message, either string with @ or integer chat_id(str): chat id to post message, either string with @ or integer
template_path(Path): path to template for built packages template_path(Path): path to template for built packages
template_type(str): template message type to be used in parse mode, one of MarkdownV2, HTML, Markdown template_type(str): template message type to be used in parse mode, one of MarkdownV2, HTML, Markdown
timeout(int): HTTP request timeout in seconds
""" """
TELEGRAM_API_URL = "https://api.telegram.org" TELEGRAM_API_URL = "https://api.telegram.org"
@ -55,12 +52,12 @@ class Telegram(Report, JinjaTemplate):
""" """
Report.__init__(self, architecture, configuration) Report.__init__(self, architecture, configuration)
JinjaTemplate.__init__(self, section, configuration) JinjaTemplate.__init__(self, section, configuration)
SyncHttpClient.__init__(self, section, configuration)
self.api_key = configuration.get(section, "api_key") self.api_key = configuration.get(section, "api_key")
self.chat_id = configuration.get(section, "chat_id") self.chat_id = configuration.get(section, "chat_id")
self.template_path = configuration.getpath(section, "template_path") self.template_path = configuration.getpath(section, "template_path")
self.template_type = configuration.get(section, "template_type", fallback="HTML") self.template_type = configuration.get(section, "template_type", fallback="HTML")
self.timeout = configuration.getint(section, "timeout", fallback=30)
def _send(self, text: str) -> None: def _send(self, text: str) -> None:
""" """
@ -69,18 +66,8 @@ class Telegram(Report, JinjaTemplate):
Args: Args:
text(str): message body text text(str): message body text
""" """
try: self.make_request("POST", f"{self.TELEGRAM_API_URL}/bot{self.api_key}/sendMessage",
response = requests.post( data={"chat_id": self.chat_id, "text": text, "parse_mode": self.template_type})
f"{self.TELEGRAM_API_URL}/bot{self.api_key}/sendMessage",
data={"chat_id": self.chat_id, "text": text, "parse_mode": self.template_type},
timeout=self.timeout)
response.raise_for_status()
except requests.HTTPError as e:
self.logger.exception("could not perform request: %s", exception_response_text(e))
raise
except Exception:
self.logger.exception("could not perform request")
raise
def generate(self, packages: list[Package], result: Result) -> None: def generate(self, packages: list[Package], result: Result) -> None:
""" """

View File

@ -17,30 +17,26 @@
# 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 requests
from pathlib import Path from pathlib import Path
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import BuildError from ahriman.core.exceptions import BuildError
from ahriman.core.log import LazyLogging from ahriman.core.http import SyncHttpClient
from ahriman.core.util import check_output, exception_response_text from ahriman.core.util import check_output
from ahriman.models.sign_settings import SignSettings from ahriman.models.sign_settings import SignSettings
class GPG(LazyLogging): class GPG(SyncHttpClient):
""" """
gnupg wrapper gnupg wrapper
Attributes: Attributes:
DEFAULT_TIMEOUT(int): (class attribute) HTTP request timeout in seconds
configuration(Configuration): configuration instance configuration(Configuration): configuration instance
default_key(str | None): default PGP key ID to use default_key(str | None): default PGP key ID to use
targets(set[SignSettings]): list of targets to sign (repository, package etc) targets(set[SignSettings]): list of targets to sign (repository, package etc)
""" """
_check_output = check_output _check_output = check_output
DEFAULT_TIMEOUT = 30
def __init__(self, configuration: Configuration) -> None: def __init__(self, configuration: Configuration) -> None:
""" """
@ -49,6 +45,7 @@ class GPG(LazyLogging):
Args: Args:
configuration(Configuration): configuration instance configuration(Configuration): configuration instance
""" """
SyncHttpClient.__init__(self)
self.configuration = configuration self.configuration = configuration
self.targets, self.default_key = self.sign_options(configuration) self.targets, self.default_key = self.sign_options(configuration)
@ -126,16 +123,11 @@ class GPG(LazyLogging):
str: key as plain text str: key as plain text
""" """
key = key if key.startswith("0x") else f"0x{key}" key = key if key.startswith("0x") else f"0x{key}"
try: response = self.make_request("GET", f"https://{server}/pks/lookup", params=[
response = requests.get(f"https://{server}/pks/lookup", params={ ("op", "get"),
"op": "get", ("options", "mr"),
"options": "mr", ("search", key),
"search": key ])
}, timeout=self.DEFAULT_TIMEOUT)
response.raise_for_status()
except requests.exceptions.HTTPError as e:
self.logger.exception("could not download key %s from %s: %s", key, server, exception_response_text(e))
raise
return response.text return response.text
def key_export(self, key: str) -> str: def key_export(self, key: str) -> str:

View File

@ -22,39 +22,27 @@ import logging
import requests import requests
from functools import cached_property from functools import cached_property
from typing import Any, IO, Literal
from urllib.parse import quote_plus as urlencode from urllib.parse import quote_plus as urlencode
from ahriman import __version__ from ahriman import __version__
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.log import LazyLogging from ahriman.core.http import SyncHttpClient
from ahriman.core.status.client import Client from ahriman.core.status.client import Client
from ahriman.core.util import exception_response_text
from ahriman.models.build_status import BuildStatus, BuildStatusEnum from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.internal_status import InternalStatus from ahriman.models.internal_status import InternalStatus
from ahriman.models.log_record_id import LogRecordId from ahriman.models.log_record_id import LogRecordId
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.user import User
# filename, file, content-type, headers class WebClient(Client, SyncHttpClient):
MultipartType = tuple[str, IO[bytes], str, dict[str, str]]
class WebClient(Client, LazyLogging):
""" """
build status reporter web client build status reporter web client
Attributes: Attributes:
address(str): address of the web service address(str): address of the web service
suppress_errors(bool): suppress logging errors (e.g. if no web server available)
user(User | None): web service user descriptor
use_unix_socket(bool): use websocket or not use_unix_socket(bool): use websocket or not
""" """
_login_url = "/api/v1/login"
_status_url = "/api/v1/status"
def __init__(self, configuration: Configuration) -> None: def __init__(self, configuration: Configuration) -> None:
""" """
default constructor default constructor
@ -62,11 +50,10 @@ class WebClient(Client, LazyLogging):
Args: Args:
configuration(Configuration): configuration instance configuration(Configuration): configuration instance
""" """
suppress_errors = configuration.getboolean("settings", "suppress_http_log_errors", fallback=False)
SyncHttpClient.__init__(self, "web", configuration, suppress_errors=suppress_errors)
self.address, self.use_unix_socket = self.parse_address(configuration) self.address, self.use_unix_socket = self.parse_address(configuration)
self.user = User.from_option(
configuration.get("web", "username", fallback=None),
configuration.get("web", "password", fallback=None))
self.suppress_errors = configuration.getboolean("settings", "suppress_http_log_errors", fallback=False)
@cached_property @cached_property
def session(self) -> requests.Session: def session(self) -> requests.Session:
@ -78,34 +65,6 @@ class WebClient(Client, LazyLogging):
""" """
return self._create_session(use_unix_socket=self.use_unix_socket) return self._create_session(use_unix_socket=self.use_unix_socket)
@staticmethod
def _logs_url(package_base: str) -> str:
"""
get url for the logs api
Args:
package_base(str): package base
Returns:
str: full url for web service for logs
"""
return f"/api/v1/packages/{package_base}/logs"
@staticmethod
def _package_url(package_base: str = "") -> str:
"""
url generator
Args:
package_base(str, optional): package base to generate url (Default value = "")
Returns:
str: full url of web service for specific package base
"""
# in case if unix socket is used we need to normalize url
suffix = f"/{package_base}" if package_base else ""
return f"/api/v1/packages{suffix}"
@staticmethod @staticmethod
def parse_address(configuration: Configuration) -> tuple[str, bool]: def parse_address(configuration: Configuration) -> tuple[str, bool]:
""" """
@ -157,56 +116,60 @@ class WebClient(Client, LazyLogging):
Args: Args:
session(requests.Session): request session to login session(requests.Session): request session to login
""" """
if self.user is None: if self.auth is None:
return # no auth configured return # no auth configured
username, password = self.auth
payload = { payload = {
"username": self.user.username, "username": username,
"password": self.user.password "password": password,
} }
with contextlib.suppress(Exception): with contextlib.suppress(Exception):
self.make_request("POST", self._login_url, json=payload, session=session) self.make_request("POST", self._login_url(), json=payload, session=session)
def make_request(self, method: Literal["DELETE", "GET", "POST"], url: str, *, def _login_url(self) -> str:
params: list[tuple[str, str]] | None = None,
json: dict[str, Any] | None = None,
files: dict[str, MultipartType] | None = None,
session: requests.Session | None = None,
suppress_errors: bool | None = None) -> requests.Response:
""" """
perform request with specified parameters get url for the login api
Args:
method(Literal["DELETE", "GET", "POST"]): HTTP method to call
url(str): remote url to call
params(list[tuple[str, str]] | None, optional): request query parameters (Default value = None)
json(dict[str, Any] | None, optional): request json parameters (Default value = None)
files(dict[str, MultipartType] | None, optional): multipart upload (Default value = None)
session(requests.Session | None, optional): session object if any (Default value = None)
suppress_errors(bool | None, optional): suppress logging errors (e.g. if no web server available). If none
set, the instance-wide value will be used (Default value = None)
Returns: Returns:
requests.Response: response object str: full url for web service to log in
""" """
# defaults return f"{self.address}/api/v1/login"
if suppress_errors is None:
suppress_errors = self.suppress_errors
if session is None:
session = self.session
try: def _logs_url(self, package_base: str) -> str:
response = session.request(method, f"{self.address}{url}", params=params, json=json, files=files) """
response.raise_for_status() get url for the logs api
return response
except requests.RequestException as e: Args:
if not suppress_errors: package_base(str): package base
self.logger.exception("could not perform http request: %s", exception_response_text(e))
raise Returns:
except Exception: str: full url for web service for logs
if not suppress_errors: """
self.logger.exception("could not perform http request") return f"{self.address}/api/v1/packages/{package_base}/logs"
raise
def _package_url(self, package_base: str = "") -> str:
"""
url generator
Args:
package_base(str, optional): package base to generate url (Default value = "")
Returns:
str: full url of web service for specific package base
"""
# in case if unix socket is used we need to normalize url
suffix = f"/{package_base}" if package_base else ""
return f"{self.address}/api/v1/packages{suffix}"
def _status_url(self) -> str:
"""
get url for the status api
Returns:
str: full url for web service for status
"""
return f"{self.address}/api/v1/status"
def package_add(self, package: Package, status: BuildStatusEnum) -> None: def package_add(self, package: Package, status: BuildStatusEnum) -> None:
""" """
@ -293,7 +256,7 @@ class WebClient(Client, LazyLogging):
InternalStatus: current internal (web) service status InternalStatus: current internal (web) service status
""" """
with contextlib.suppress(Exception): with contextlib.suppress(Exception):
response = self.make_request("GET", self._status_url) response = self.make_request("GET", self._status_url())
response_json = response.json() response_json = response.json()
return InternalStatus.from_json(response_json) return InternalStatus.from_json(response_json)
@ -309,4 +272,4 @@ class WebClient(Client, LazyLogging):
""" """
payload = {"status": status.value} payload = {"status": status.value}
with contextlib.suppress(Exception): with contextlib.suppress(Exception):
self.make_request("POST", self._status_url, json=payload) self.make_request("POST", self._status_url(), json=payload)

View File

@ -61,7 +61,7 @@ class Github(HttpUpload):
""" """
try: try:
asset = next(asset for asset in release["assets"] if asset["name"] == name) asset = next(asset for asset in release["assets"] if asset["name"] == name)
self._request("DELETE", asset["url"]) self.make_request("DELETE", asset["url"])
except StopIteration: except StopIteration:
self.logger.info("no asset %s found in release %s", name, release["name"]) self.logger.info("no asset %s found in release %s", name, release["name"])
@ -81,7 +81,7 @@ class Github(HttpUpload):
headers = {"Content-Type": mime} if mime is not None else {"Content-Type": "application/octet-stream"} headers = {"Content-Type": mime} if mime is not None else {"Content-Type": "application/octet-stream"}
with path.open("rb") as archive: with path.open("rb") as archive:
self._request("POST", url, params={"name": path.name}, data=archive, headers=headers) self.make_request("POST", url, params=[("name", path.name)], data=archive, headers=headers)
def get_local_files(self, path: Path) -> dict[Path, str]: def get_local_files(self, path: Path) -> dict[Path, str]:
""" """
@ -136,7 +136,7 @@ class Github(HttpUpload):
dict[str, Any]: github API release object for the new release dict[str, Any]: github API release object for the new release
""" """
url = f"https://api.github.com/repos/{self.github_owner}/{self.github_repository}/releases" url = f"https://api.github.com/repos/{self.github_owner}/{self.github_repository}/releases"
response = self._request("POST", url, json={"tag_name": self.architecture, "name": self.architecture}) response = self.make_request("POST", url, json={"tag_name": self.architecture, "name": self.architecture})
release: dict[str, Any] = response.json() release: dict[str, Any] = response.json()
return release return release
@ -149,11 +149,11 @@ class Github(HttpUpload):
""" """
url = f"https://api.github.com/repos/{self.github_owner}/{self.github_repository}/releases/tags/{self.architecture}" url = f"https://api.github.com/repos/{self.github_owner}/{self.github_repository}/releases/tags/{self.architecture}"
try: try:
response = self._request("GET", url) response = self.make_request("GET", url)
release: dict[str, Any] = response.json() release: dict[str, Any] = response.json()
return release return release
except requests.HTTPError as e: except requests.HTTPError as ex:
status_code = e.response.status_code if e.response is not None else None status_code = ex.response.status_code if ex.response is not None else None
if status_code == 404: if status_code == 404:
return None return None
raise raise
@ -166,7 +166,7 @@ class Github(HttpUpload):
release(dict[str, Any]): release object release(dict[str, Any]): release object
body(str): new release body body(str): new release body
""" """
self._request("POST", release["url"], json={"body": body}) self.make_request("POST", release["url"], json={"body": body})
def sync(self, path: Path, built_packages: list[Package]) -> None: def sync(self, path: Path, built_packages: list[Package]) -> None:
""" """

View File

@ -18,24 +18,17 @@
# 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 hashlib import hashlib
import requests
from functools import cached_property
from pathlib import Path from pathlib import Path
from typing import Any
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.http import SyncHttpClient
from ahriman.core.upload.upload import Upload from ahriman.core.upload.upload import Upload
from ahriman.core.util import exception_response_text
class HttpUpload(Upload): class HttpUpload(Upload, SyncHttpClient):
""" """
helper for the http based uploads helper for the http based uploads
Attributes:
auth(tuple[str, str] | None): HTTP auth object if set
timeout(int): HTTP request timeout in seconds
""" """
def __init__(self, architecture: str, configuration: Configuration, section: str) -> None: def __init__(self, architecture: str, configuration: Configuration, section: str) -> None:
@ -48,20 +41,7 @@ class HttpUpload(Upload):
section(str): configuration section name section(str): configuration section name
""" """
Upload.__init__(self, architecture, configuration) Upload.__init__(self, architecture, configuration)
password = configuration.get(section, "password", fallback=None) SyncHttpClient.__init__(self, section, configuration)
username = configuration.get(section, "username", fallback=None)
self.auth = (password, username) if password and username else None
self.timeout = configuration.getint(section, "timeout", fallback=30)
@cached_property
def session(self) -> requests.Session:
"""
get or create session
Returns:
request.Session: created session object
"""
return requests.Session()
@staticmethod @staticmethod
def calculate_hash(path: Path) -> str: def calculate_hash(path: Path) -> str:
@ -107,23 +87,3 @@ class HttpUpload(Upload):
file, md5 = line.split() file, md5 = line.split()
files[file] = md5 files[file] = md5
return files return files
def _request(self, method: str, url: str, **kwargs: Any) -> requests.Response:
"""
request wrapper
Args:
method(str): request method
url(str): request url
**kwargs(Any): request parameters to be passed as is
Returns:
requests.Response: request response object
"""
try:
response = self.session.request(method, url, auth=self.auth, timeout=self.timeout, **kwargs)
response.raise_for_status()
except requests.HTTPError as e:
self.logger.exception("could not perform %s request to %s: %s", method, url, exception_response_text(e))
raise
return response

View File

@ -23,8 +23,9 @@ from functools import cached_property
from pathlib import Path from pathlib import Path
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.http import MultipartType
from ahriman.core.sign.gpg import GPG from ahriman.core.sign.gpg import GPG
from ahriman.core.status.web_client import MultipartType, WebClient from ahriman.core.status.web_client import WebClient
from ahriman.core.upload.http_upload import HttpUpload from ahriman.core.upload.http_upload import HttpUpload
from ahriman.models.package import Package from ahriman.models.package import Package
@ -77,7 +78,7 @@ class RemoteService(HttpUpload):
if signature_path is not None: if signature_path is not None:
files["signature"] = signature_path.name, signature_path.open("rb"), "application/octet-stream", {} files["signature"] = signature_path.name, signature_path.open("rb"), "application/octet-stream", {}
self._request("POST", f"{self.client.address}/api/v1/service/upload", files=files) self.make_request("POST", f"{self.client.address}/api/v1/service/upload", files=files)
finally: finally:
for _, fd, _, _ in files.values(): for _, fd, _, _ in files.values():
fd.close() fd.close()

View File

@ -51,8 +51,8 @@ class Upload(LazyLogging):
>>> try: >>> try:
>>> upload.sync(configuration.repository_paths.repository, []) >>> upload.sync(configuration.repository_paths.repository, [])
>>> except Exception as exception: >>> except Exception as ex:
>>> handle_exceptions(exception) >>> handle_exceptions(ex)
""" """
def __init__(self, architecture: str, configuration: Configuration) -> None: def __init__(self, architecture: str, configuration: Configuration) -> None:

View File

@ -24,7 +24,6 @@ import itertools
import logging import logging
import os import os
import re import re
import requests
import selectors import selectors
import subprocess import subprocess
@ -44,7 +43,6 @@ __all__ = [
"check_user", "check_user",
"dataclass_view", "dataclass_view",
"enum_values", "enum_values",
"exception_response_text",
"extract_user", "extract_user",
"filter_json", "filter_json",
"full_version", "full_version",
@ -214,20 +212,6 @@ def enum_values(enum: type[Enum]) -> list[str]:
return [str(key.value) for key in enum] # explicit str conversion for typing return [str(key.value) for key in enum] # explicit str conversion for typing
def exception_response_text(exception: requests.exceptions.RequestException) -> str:
"""
safe response exception text generation
Args:
exception(requests.exceptions.RequestException): exception raised
Returns:
str: text of the response if it is not None and empty string otherwise
"""
result: str = exception.response.text if exception.response is not None else ""
return result
def extract_user() -> str | None: def extract_user() -> str | None:
""" """
extract user from system environment extract user from system environment

View File

@ -75,24 +75,6 @@ class User:
object.__setattr__(self, "packager_id", self.packager_id or None) object.__setattr__(self, "packager_id", self.packager_id or None)
object.__setattr__(self, "key", self.key or None) object.__setattr__(self, "key", self.key or None)
@classmethod
def from_option(cls, username: str | None, password: str | None,
access: UserAccess = UserAccess.Read) -> Self | None:
"""
build user descriptor from configuration options
Args:
username(str | None): username
password(str | None): password as string
access(UserAccess, optional): optional user access (Default value = UserAccess.Read)
Returns:
Self | None: generated user descriptor if all options are supplied and None otherwise
"""
if username is None or password is None:
return None
return cls(username=username, password=password, access=access, packager_id=None, key=None)
@staticmethod @staticmethod
def generate_password(length: int) -> str: def generate_password(length: int) -> str:
""" """

View File

@ -60,31 +60,31 @@ def exception_handler(logger: logging.Logger) -> MiddlewareType:
async def handle(request: Request, handler: HandlerType) -> StreamResponse: async def handle(request: Request, handler: HandlerType) -> StreamResponse:
try: try:
return await handler(request) return await handler(request)
except HTTPUnauthorized as e: except HTTPUnauthorized as ex:
if _is_templated_unauthorized(request): if _is_templated_unauthorized(request):
context = {"code": e.status_code, "reason": e.reason} context = {"code": ex.status_code, "reason": ex.reason}
return aiohttp_jinja2.render_template("error.jinja2", request, context, status=e.status_code) return aiohttp_jinja2.render_template("error.jinja2", request, context, status=ex.status_code)
return json_response(data={"error": e.reason}, status=e.status_code) return json_response(data={"error": ex.reason}, status=ex.status_code)
except HTTPMethodNotAllowed as e: except HTTPMethodNotAllowed as ex:
if e.method == "OPTIONS": if ex.method == "OPTIONS":
# automatically handle OPTIONS method, idea comes from # 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 # 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))}) raise HTTPNoContent(headers={"Allow": ",".join(sorted(ex.allowed_methods))})
if e.method == "HEAD": if ex.method == "HEAD":
# since we have special autogenerated HEAD method, we need to remove it from list of available # 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"} ex.allowed_methods = {method for method in ex.allowed_methods if method != "HEAD"}
e.headers["Allow"] = ",".join(sorted(e.allowed_methods)) ex.headers["Allow"] = ",".join(sorted(ex.allowed_methods))
raise e raise ex
raise raise
except HTTPClientError as e: except HTTPClientError as ex:
return json_response(data={"error": e.reason}, status=e.status_code) return json_response(data={"error": ex.reason}, status=ex.status_code)
except HTTPServerError as e: except HTTPServerError as ex:
logger.exception("server exception during performing request to %s", request.path) logger.exception("server exception during performing request to %s", request.path)
return json_response(data={"error": e.reason}, status=e.status_code) return json_response(data={"error": ex.reason}, status=ex.status_code)
except HTTPException: # just raise 2xx and 3xx codes except HTTPException: # just raise 2xx and 3xx codes
raise raise
except Exception as e: except Exception as ex:
logger.exception("unknown exception during performing request to %s", request.path) logger.exception("unknown exception during performing request to %s", request.path)
return json_response(data={"error": str(e)}, status=500) return json_response(data={"error": str(ex)}, status=500)
return handle return handle

View File

@ -64,8 +64,8 @@ class AddView(BaseView):
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")
except Exception as e: except Exception as ex:
raise HTTPBadRequest(reason=str(e)) raise HTTPBadRequest(reason=str(ex))
username = await self.username() username = await self.username()
process_id = self.spawner.packages_add(packages, username, now=True) process_id = self.spawner.packages_add(packages, username, now=True)

View File

@ -68,8 +68,8 @@ class PGPView(BaseView):
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")
except Exception as e: except Exception as ex:
raise HTTPBadRequest(reason=str(e)) raise HTTPBadRequest(reason=str(ex))
try: try:
key = self.service.repository.sign.key_download(server, key) key = self.service.repository.sign.key_download(server, key)
@ -107,8 +107,8 @@ class PGPView(BaseView):
try: try:
key = self.get_non_empty(data.get, "key") key = self.get_non_empty(data.get, "key")
except Exception as e: except Exception as ex:
raise HTTPBadRequest(reason=str(e)) raise HTTPBadRequest(reason=str(ex))
process_id = self.spawner.key_import(key, data.get("server")) process_id = self.spawner.key_import(key, data.get("server"))

View File

@ -65,8 +65,8 @@ class RebuildView(BaseView):
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(iter(packages)) depends_on = next(iter(packages))
except Exception as e: except Exception as ex:
raise HTTPBadRequest(reason=str(e)) raise HTTPBadRequest(reason=str(ex))
username = await self.username() username = await self.username()
process_id = self.spawner.packages_rebuild(depends_on, username) process_id = self.spawner.packages_rebuild(depends_on, username)

View File

@ -64,8 +64,8 @@ class RemoveView(BaseView):
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")
except Exception as e: except Exception as ex:
raise HTTPBadRequest(reason=str(e)) raise HTTPBadRequest(reason=str(ex))
process_id = self.spawner.packages_remove(packages) process_id = self.spawner.packages_remove(packages)

View File

@ -64,8 +64,8 @@ class RequestView(BaseView):
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")
except Exception as e: except Exception as ex:
raise HTTPBadRequest(reason=str(e)) raise HTTPBadRequest(reason=str(ex))
username = await self.username() username = await self.username()
process_id = self.spawner.packages_add(packages, username, now=False) process_id = self.spawner.packages_add(packages, username, now=False)

View File

@ -69,8 +69,8 @@ class SearchView(BaseView):
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")
packages = AUR.multisearch(*search, pacman=self.service.repository.pacman) packages = AUR.multisearch(*search, pacman=self.service.repository.pacman)
except Exception as e: except Exception as ex:
raise HTTPBadRequest(reason=str(e)) raise HTTPBadRequest(reason=str(ex))
if not packages: if not packages:
raise HTTPNotFound(reason=f"No packages found for terms: {search}") raise HTTPNotFound(reason=f"No packages found for terms: {search}")

View File

@ -63,8 +63,8 @@ class UpdateView(BaseView):
""" """
try: try:
data = await self.extract_data() data = await self.extract_data()
except Exception as e: except Exception as ex:
raise HTTPBadRequest(reason=str(e)) raise HTTPBadRequest(reason=str(ex))
username = await self.username() username = await self.username()
process_id = self.spawner.packages_update( process_id = self.spawner.packages_update(

View File

@ -120,8 +120,8 @@ class UploadView(BaseView):
try: try:
reader = await self.request.multipart() reader = await self.request.multipart()
except Exception as e: except Exception as ex:
raise HTTPBadRequest(reason=str(e)) raise HTTPBadRequest(reason=str(ex))
max_body_size = self.configuration.getint("web", "max_body_size", fallback=None) max_body_size = self.configuration.getint("web", "max_body_size", fallback=None)
target = self.configuration.repository_paths.packages target = self.configuration.repository_paths.packages

View File

@ -138,8 +138,8 @@ class LogsView(BaseView):
created = data["created"] created = data["created"]
record = data["message"] record = data["message"]
version = data["version"] version = data["version"]
except Exception as e: except Exception as ex:
raise HTTPBadRequest(reason=str(e)) raise HTTPBadRequest(reason=str(ex))
self.service.logs_update(LogRecordId(package_base, version), created, record) self.service.logs_update(LogRecordId(package_base, version), created, record)

View File

@ -138,8 +138,8 @@ class PackageView(BaseView):
try: try:
package = Package.from_json(data["package"]) if "package" in data else None package = Package.from_json(data["package"]) if "package" in data else None
status = BuildStatusEnum(data["status"]) status = BuildStatusEnum(data["status"])
except Exception as e: except Exception as ex:
raise HTTPBadRequest(reason=str(e)) raise HTTPBadRequest(reason=str(ex))
try: try:
self.service.package_update(package_base, status, package) self.service.package_update(package_base, status, package)

View File

@ -99,8 +99,8 @@ class StatusView(BaseView):
try: try:
data = await self.extract_data() data = await self.extract_data()
status = BuildStatusEnum(data["status"]) status = BuildStatusEnum(data["status"])
except Exception as e: except Exception as ex:
raise HTTPBadRequest(reason=str(e)) raise HTTPBadRequest(reason=str(ex))
self.service.status_update(status) self.service.status_update(status)

View File

@ -67,80 +67,73 @@ def test_remote_web_url(aur_package_ahriman: AURPackage) -> None:
assert web_url.startswith(AUR.DEFAULT_AUR_URL) assert web_url.startswith(AUR.DEFAULT_AUR_URL)
def test_make_request(aur: AUR, aur_package_ahriman: AURPackage, def test_aur_request(aur: AUR, aur_package_ahriman: AURPackage,
mocker: MockerFixture, resource_path_root: Path) -> None: mocker: MockerFixture, resource_path_root: Path) -> None:
""" """
must perform request to AUR must perform request to AUR
""" """
response_mock = MagicMock() response_mock = MagicMock()
response_mock.json.return_value = json.loads(_get_response(resource_path_root)) response_mock.json.return_value = json.loads(_get_response(resource_path_root))
request_mock = mocker.patch("requests.get", return_value=response_mock) request_mock = mocker.patch("ahriman.core.alpm.remote.AUR.make_request", return_value=response_mock)
assert aur.make_request("info", "ahriman") == [aur_package_ahriman] assert aur.aur_request("info", "ahriman") == [aur_package_ahriman]
request_mock.assert_called_once_with( request_mock.assert_called_once_with(
"https://aur.archlinux.org/rpc", "GET", "https://aur.archlinux.org/rpc",
params={"v": "5", "type": "info", "arg": ["ahriman"]}, params=[("type", "info"), ("v", "5"), ("arg", "ahriman")])
headers={"User-Agent": AUR.DEFAULT_USER_AGENT},
timeout=aur.DEFAULT_TIMEOUT)
def test_make_request_multi_arg(aur: AUR, aur_package_ahriman: AURPackage, def test_aur_request_multi_arg(aur: AUR, aur_package_ahriman: AURPackage,
mocker: MockerFixture, resource_path_root: Path) -> None: mocker: MockerFixture, resource_path_root: Path) -> None:
""" """
must perform request to AUR with multiple args must perform request to AUR with multiple args
""" """
response_mock = MagicMock() response_mock = MagicMock()
response_mock.json.return_value = json.loads(_get_response(resource_path_root)) response_mock.json.return_value = json.loads(_get_response(resource_path_root))
request_mock = mocker.patch("requests.get", return_value=response_mock) request_mock = mocker.patch("ahriman.core.alpm.remote.AUR.make_request", return_value=response_mock)
assert aur.make_request("search", "ahriman", "is", "cool") == [aur_package_ahriman] assert aur.aur_request("search", "ahriman", "is", "cool") == [aur_package_ahriman]
request_mock.assert_called_once_with( request_mock.assert_called_once_with(
"https://aur.archlinux.org/rpc", "GET", "https://aur.archlinux.org/rpc",
params={"v": "5", "type": "search", "arg[]": ["ahriman", "is", "cool"]}, params=[("type", "search"), ("v", "5"), ("arg[]", "ahriman"), ("arg[]", "is"), ("arg[]", "cool")])
headers={"User-Agent": AUR.DEFAULT_USER_AGENT},
timeout=aur.DEFAULT_TIMEOUT)
def test_make_request_with_kwargs(aur: AUR, aur_package_ahriman: AURPackage, def test_aur_request_with_kwargs(aur: AUR, aur_package_ahriman: AURPackage,
mocker: MockerFixture, resource_path_root: Path) -> None: mocker: MockerFixture, resource_path_root: Path) -> None:
""" """
must perform request to AUR with named parameters must perform request to AUR with named parameters
""" """
response_mock = MagicMock() response_mock = MagicMock()
response_mock.json.return_value = json.loads(_get_response(resource_path_root)) response_mock.json.return_value = json.loads(_get_response(resource_path_root))
request_mock = mocker.patch("requests.get", return_value=response_mock) request_mock = mocker.patch("ahriman.core.alpm.remote.AUR.make_request", return_value=response_mock)
assert aur.make_request("search", "ahriman", by="name") == [aur_package_ahriman] assert aur.aur_request("search", "ahriman", by="name") == [aur_package_ahriman]
request_mock.assert_called_once_with( request_mock.assert_called_once_with(
"https://aur.archlinux.org/rpc", "GET", "https://aur.archlinux.org/rpc",
params={"v": "5", "type": "search", "arg": ["ahriman"], "by": "name"}, params=[("type", "search"), ("v", "5"), ("arg", "ahriman"), ("by", "name")])
headers={"User-Agent": AUR.DEFAULT_USER_AGENT},
timeout=aur.DEFAULT_TIMEOUT)
def test_make_request_failed(aur: AUR, mocker: MockerFixture) -> None: def test_aur_request_failed(aur: AUR, mocker: MockerFixture) -> None:
""" """
must reraise generic exception must reraise generic exception
""" """
mocker.patch("requests.get", side_effect=Exception()) mocker.patch("requests.Session.request", side_effect=Exception())
with pytest.raises(Exception): with pytest.raises(Exception):
aur.make_request("info", "ahriman") aur.aur_request("info", "ahriman")
def test_make_request_failed_http_error(aur: AUR, mocker: MockerFixture) -> None: def test_aur_request_failed_http_error(aur: AUR, mocker: MockerFixture) -> None:
""" must reraise http exception
""" """
must reraise http exception mocker.patch("requests.Session.request", side_effect=requests.HTTPError())
""" with pytest.raises(requests.HTTPError):
mocker.patch("requests.get", side_effect=requests.exceptions.HTTPError()) aur.aur_request("info", "ahriman")
with pytest.raises(requests.exceptions.HTTPError):
aur.make_request("info", "ahriman")
def test_package_info(aur: AUR, aur_package_ahriman: AURPackage, pacman: Pacman, mocker: MockerFixture) -> None: def test_package_info(aur: AUR, aur_package_ahriman: AURPackage, pacman: Pacman, mocker: MockerFixture) -> None:
""" """
must make request for info must make request for info
""" """
request_mock = mocker.patch("ahriman.core.alpm.remote.AUR.make_request", return_value=[aur_package_ahriman]) request_mock = mocker.patch("ahriman.core.alpm.remote.AUR.aur_request", return_value=[aur_package_ahriman])
assert aur.package_info(aur_package_ahriman.name, pacman=pacman) == aur_package_ahriman assert aur.package_info(aur_package_ahriman.name, pacman=pacman) == aur_package_ahriman
request_mock.assert_called_once_with("info", aur_package_ahriman.name) request_mock.assert_called_once_with("info", aur_package_ahriman.name)
@ -150,7 +143,7 @@ def test_package_info_not_found(aur: AUR, aur_package_ahriman: AURPackage, pacma
""" """
must raise UnknownPackage exception in case if no package was found must raise UnknownPackage exception in case if no package was found
""" """
mocker.patch("ahriman.core.alpm.remote.AUR.make_request", return_value=[]) mocker.patch("ahriman.core.alpm.remote.AUR.aur_request", return_value=[])
with pytest.raises(UnknownPackageError, match=aur_package_ahriman.name): with pytest.raises(UnknownPackageError, match=aur_package_ahriman.name):
assert aur.package_info(aur_package_ahriman.name, pacman=pacman) assert aur.package_info(aur_package_ahriman.name, pacman=pacman)
@ -159,6 +152,6 @@ def test_package_search(aur: AUR, aur_package_ahriman: AURPackage, pacman: Pacma
""" """
must make request for search must make request for search
""" """
request_mock = mocker.patch("ahriman.core.alpm.remote.AUR.make_request", return_value=[aur_package_ahriman]) request_mock = mocker.patch("ahriman.core.alpm.remote.AUR.aur_request", return_value=[aur_package_ahriman])
assert aur.package_search(aur_package_ahriman.name, pacman=pacman) == [aur_package_ahriman] assert aur.package_search(aur_package_ahriman.name, pacman=pacman) == [aur_package_ahriman]
request_mock.assert_called_once_with("search", aur_package_ahriman.name, by="name-desc") request_mock.assert_called_once_with("search", aur_package_ahriman.name, by="name-desc")

View File

@ -62,39 +62,37 @@ def test_remote_web_url(aur_package_akonadi: AURPackage) -> None:
assert web_url.startswith(Official.DEFAULT_ARCHLINUX_URL) assert web_url.startswith(Official.DEFAULT_ARCHLINUX_URL)
def test_make_request(official: Official, aur_package_akonadi: AURPackage, def test_arch_request(official: Official, aur_package_akonadi: AURPackage,
mocker: MockerFixture, resource_path_root: Path) -> None: mocker: MockerFixture, resource_path_root: Path) -> None:
""" """
must perform request to official repositories must perform request to official repositories
""" """
response_mock = MagicMock() response_mock = MagicMock()
response_mock.json.return_value = json.loads(_get_response(resource_path_root)) response_mock.json.return_value = json.loads(_get_response(resource_path_root))
request_mock = mocker.patch("requests.get", return_value=response_mock) request_mock = mocker.patch("ahriman.core.alpm.remote.Official.make_request", return_value=response_mock)
assert official.make_request("akonadi", by="q") == [aur_package_akonadi] assert official.arch_request("akonadi", by="q") == [aur_package_akonadi]
request_mock.assert_called_once_with( request_mock.assert_called_once_with(
"https://archlinux.org/packages/search/json", "GET", "https://archlinux.org/packages/search/json",
params={"q": ("akonadi",), "repo": Official.DEFAULT_SEARCH_REPOSITORIES}, params=[("repo", repository) for repository in Official.DEFAULT_SEARCH_REPOSITORIES] + [("q", "akonadi")])
headers={"User-Agent": Official.DEFAULT_USER_AGENT},
timeout=official.DEFAULT_TIMEOUT)
def test_make_request_failed(official: Official, mocker: MockerFixture) -> None: def test_arch_request_failed(official: Official, mocker: MockerFixture) -> None:
""" """
must reraise generic exception must reraise generic exception
""" """
mocker.patch("requests.get", side_effect=Exception()) mocker.patch("requests.Session.request", side_effect=Exception())
with pytest.raises(Exception): with pytest.raises(Exception):
official.make_request("akonadi", by="q") official.arch_request("akonadi", by="q")
def test_make_request_failed_http_error(official: Official, mocker: MockerFixture) -> None: def test_arch_request_failed_http_error(official: Official, mocker: MockerFixture) -> None:
""" """
must reraise http exception must reraise http exception
""" """
mocker.patch("requests.get", side_effect=requests.exceptions.HTTPError()) mocker.patch("requests.Session.request", side_effect=requests.HTTPError())
with pytest.raises(requests.exceptions.HTTPError): with pytest.raises(requests.HTTPError):
official.make_request("akonadi", by="q") official.arch_request("akonadi", by="q")
def test_package_info(official: Official, aur_package_akonadi: AURPackage, pacman: Pacman, def test_package_info(official: Official, aur_package_akonadi: AURPackage, pacman: Pacman,
@ -102,7 +100,7 @@ def test_package_info(official: Official, aur_package_akonadi: AURPackage, pacma
""" """
must make request for info must make request for info
""" """
request_mock = mocker.patch("ahriman.core.alpm.remote.Official.make_request", request_mock = mocker.patch("ahriman.core.alpm.remote.Official.arch_request",
return_value=[aur_package_akonadi]) return_value=[aur_package_akonadi])
assert official.package_info(aur_package_akonadi.name, pacman=pacman) == aur_package_akonadi assert official.package_info(aur_package_akonadi.name, pacman=pacman) == aur_package_akonadi
request_mock.assert_called_once_with(aur_package_akonadi.name, by="name") request_mock.assert_called_once_with(aur_package_akonadi.name, by="name")
@ -113,7 +111,7 @@ def test_package_info_not_found(official: Official, aur_package_ahriman: AURPack
""" """
must raise UnknownPackage exception in case if no package was found must raise UnknownPackage exception in case if no package was found
""" """
mocker.patch("ahriman.core.alpm.remote.Official.make_request", return_value=[]) mocker.patch("ahriman.core.alpm.remote.Official.arch_request", return_value=[])
with pytest.raises(UnknownPackageError, match=aur_package_ahriman.name): with pytest.raises(UnknownPackageError, match=aur_package_ahriman.name):
assert official.package_info(aur_package_ahriman.name, pacman=pacman) assert official.package_info(aur_package_ahriman.name, pacman=pacman)
@ -123,7 +121,7 @@ def test_package_search(official: Official, aur_package_akonadi: AURPackage, pac
""" """
must make request for search must make request for search
""" """
request_mock = mocker.patch("ahriman.core.alpm.remote.Official.make_request", request_mock = mocker.patch("ahriman.core.alpm.remote.Official.arch_request",
return_value=[aur_package_akonadi]) return_value=[aur_package_akonadi])
assert official.package_search(aur_package_akonadi.name, pacman=pacman) == [aur_package_akonadi] assert official.package_search(aur_package_akonadi.name, pacman=pacman) == [aur_package_akonadi]
request_mock.assert_called_once_with(aur_package_akonadi.name, by="q") request_mock.assert_called_once_with(aur_package_akonadi.name, by="q")

View File

@ -0,0 +1,154 @@
import pytest
import requests
from pytest_mock import MockerFixture
from unittest.mock import MagicMock, call as MockCall
from ahriman.core.configuration import Configuration
from ahriman.core.http import SyncHttpClient
def test_init() -> None:
"""
must init from empty parameters
"""
assert SyncHttpClient()
def test_init_auth(configuration: Configuration) -> None:
"""
must init with auth
"""
configuration.set_option("web", "username", "username")
configuration.set_option("web", "password", "password")
assert SyncHttpClient("web", configuration).auth == ("username", "password")
assert SyncHttpClient(configuration=configuration).auth is None
def test_init_auth_empty() -> None:
"""
must init with empty auth
"""
assert SyncHttpClient().auth is None
def test_session() -> None:
"""
must generate valid session
"""
session = SyncHttpClient().session
assert "User-Agent" in session.headers
def test_exception_response_text() -> None:
"""
must parse HTTP response to string
"""
response_mock = MagicMock()
response_mock.text = "hello"
exception = requests.exceptions.HTTPError(response=response_mock)
assert SyncHttpClient.exception_response_text(exception) == "hello"
def test_exception_response_text_empty() -> None:
"""
must parse HTTP exception with empty response to empty string
"""
exception = requests.exceptions.HTTPError(response=None)
assert SyncHttpClient.exception_response_text(exception) == ""
def test_make_request(mocker: MockerFixture) -> None:
"""
must make HTTP request
"""
request_mock = mocker.patch("requests.Session.request")
client = SyncHttpClient()
assert client.make_request("GET", "url1") is not None
assert client.make_request("GET", "url2", params=[("param", "value")]) is not None
assert client.make_request("POST", "url3") is not None
assert client.make_request("POST", "url4", json={"param": "value"}) is not None
assert client.make_request("POST", "url5", data={"param": "value"}) is not None
# we don't want to put full descriptor here
assert client.make_request("POST", "url6", files={"file": "tuple"}) is not None
assert client.make_request("DELETE", "url7") is not None
assert client.make_request("GET", "url8", headers={"user-agent": "ua"}) is not None
auth = client.auth = ("username", "password")
assert client.make_request("GET", "url9") is not None
request_mock.assert_has_calls([
MockCall("GET", "url1", params=None, data=None, headers=None, files=None, json=None,
auth=None, timeout=client.timeout),
MockCall().raise_for_status(),
MockCall("GET", "url2", params=[("param", "value")], data=None, headers=None, files=None, json=None,
auth=None, timeout=client.timeout),
MockCall().raise_for_status(),
MockCall("POST", "url3", params=None, data=None, headers=None, files=None, json=None,
auth=None, timeout=client.timeout),
MockCall().raise_for_status(),
MockCall("POST", "url4", params=None, data=None, headers=None, files=None, json={"param": "value"},
auth=None, timeout=client.timeout),
MockCall().raise_for_status(),
MockCall("POST", "url5", params=None, data={"param": "value"}, headers=None, files=None, json=None,
auth=None, timeout=client.timeout),
MockCall().raise_for_status(),
MockCall("POST", "url6", params=None, data=None, headers=None, files={"file": "tuple"}, json=None,
auth=None, timeout=client.timeout),
MockCall().raise_for_status(),
MockCall("DELETE", "url7", params=None, data=None, headers=None, files=None, json=None,
auth=None, timeout=client.timeout),
MockCall().raise_for_status(),
MockCall("GET", "url8", params=None, data=None, headers={"user-agent": "ua"}, files=None, json=None,
auth=None, timeout=client.timeout),
MockCall().raise_for_status(),
MockCall("GET", "url9", params=None, data=None, headers=None, files=None, json=None,
auth=auth, timeout=client.timeout),
MockCall().raise_for_status(),
])
def test_make_request_failed(mocker: MockerFixture) -> None:
"""
must process request errors
"""
mocker.patch("requests.Session.request", side_effect=Exception())
logging_mock = mocker.patch("logging.Logger.exception")
with pytest.raises(Exception):
SyncHttpClient().make_request("GET", "url")
logging_mock.assert_called_once() # we do not check logging arguments
def test_make_request_suppress_errors(mocker: MockerFixture) -> None:
"""
must suppress request errors correctly
"""
mocker.patch("requests.Session.request", side_effect=Exception())
logging_mock = mocker.patch("logging.Logger.exception")
with pytest.raises(Exception):
SyncHttpClient().make_request("GET", "url", suppress_errors=True)
with pytest.raises(Exception):
SyncHttpClient(suppress_errors=True).make_request("GET", "url")
logging_mock.assert_not_called()
def test_make_request_session() -> None:
"""
must use session from arguments
"""
session_mock = MagicMock()
client = SyncHttpClient()
client.make_request("GET", "url", session=session_mock)
session_mock.request.assert_called_once_with(
"GET", "url", params=None, data=None, headers=None, files=None, json=None,
auth=None, timeout=client.timeout)

View File

@ -40,7 +40,7 @@ def test_is_process_alive_unknown(remote_call: RemoteCall, mocker: MockerFixture
response = requests.Response() response = requests.Response()
response.status_code = 404 response.status_code = 404
mocker.patch("ahriman.core.status.web_client.WebClient.make_request", mocker.patch("ahriman.core.status.web_client.WebClient.make_request",
side_effect=requests.RequestException(response=response)) side_effect=requests.HTTPError(response=response))
assert not remote_call.is_process_alive("id") assert not remote_call.is_process_alive("id")
@ -62,9 +62,9 @@ def test_is_process_alive_http_error(remote_call: RemoteCall, mocker: MockerFixt
response = requests.Response() response = requests.Response()
response.status_code = 500 response.status_code = 500
mocker.patch("ahriman.core.status.web_client.WebClient.make_request", mocker.patch("ahriman.core.status.web_client.WebClient.make_request",
side_effect=requests.RequestException(response=response)) side_effect=requests.HTTPError(response=response))
with pytest.raises(requests.RequestException): with pytest.raises(requests.HTTPError):
remote_call.is_process_alive("id") remote_call.is_process_alive("id")

View File

@ -14,35 +14,35 @@ def test_send(configuration: Configuration, mocker: MockerFixture) -> None:
""" """
must send a message must send a message
""" """
request_mock = mocker.patch("requests.post") request_mock = mocker.patch("ahriman.core.report.telegram.Telegram.make_request")
report = Telegram("x86_64", configuration, "telegram") report = Telegram("x86_64", configuration, "telegram")
report._send("a text") report._send("a text")
request_mock.assert_called_once_with( request_mock.assert_called_once_with(
"POST",
pytest.helpers.anyvar(str, strict=True), pytest.helpers.anyvar(str, strict=True),
data={"chat_id": pytest.helpers.anyvar(str, strict=True), "text": "a text", "parse_mode": "HTML"}, data={"chat_id": pytest.helpers.anyvar(str, strict=True), "text": "a text", "parse_mode": "HTML"})
timeout=report.timeout)
def test_send_failed(configuration: Configuration, mocker: MockerFixture) -> None: def test_send_failed(configuration: Configuration, mocker: MockerFixture) -> None:
""" """
must reraise generic exception must reraise generic exception
""" """
mocker.patch("requests.post", side_effect=Exception()) mocker.patch("requests.Session.request", side_effect=Exception())
report = Telegram("x86_64", configuration, "telegram") report = Telegram("x86_64", configuration, "telegram")
with pytest.raises(Exception): with pytest.raises(Exception):
report._send("a text") report._send("a text")
def test_make_request_failed_http_error(configuration: Configuration, mocker: MockerFixture) -> None: def test_send_failed_http_error(configuration: Configuration, mocker: MockerFixture) -> None:
""" """
must reraise http exception must reraise http exception
""" """
mocker.patch("requests.post", side_effect=requests.exceptions.HTTPError()) mocker.patch("requests.Session.request", side_effect=requests.HTTPError())
report = Telegram("x86_64", configuration, "telegram") report = Telegram("x86_64", configuration, "telegram")
with pytest.raises(requests.exceptions.HTTPError): with pytest.raises(requests.HTTPError):
report._send("a text") report._send("a text")

View File

@ -87,20 +87,19 @@ def test_key_download(gpg: GPG, mocker: MockerFixture) -> None:
""" """
must download the key from public server must download the key from public server
""" """
requests_mock = mocker.patch("requests.get") requests_mock = mocker.patch("ahriman.core.sign.gpg.GPG.make_request")
gpg.key_download("keyserver.ubuntu.com", "0xE989490C") gpg.key_download("keyserver.ubuntu.com", "0xE989490C")
requests_mock.assert_called_once_with( requests_mock.assert_called_once_with(
"https://keyserver.ubuntu.com/pks/lookup", "GET", "https://keyserver.ubuntu.com/pks/lookup",
params={"op": "get", "options": "mr", "search": "0xE989490C"}, params=[("op", "get"), ("options", "mr"), ("search", "0xE989490C")])
timeout=gpg.DEFAULT_TIMEOUT)
def test_key_download_failure(gpg: GPG, mocker: MockerFixture) -> None: def test_key_download_failure(gpg: GPG, mocker: MockerFixture) -> None:
""" """
must download the key from public server and log error if any (and raise it again) must download the key from public server and log error if any (and raise it again)
""" """
mocker.patch("requests.get", side_effect=requests.exceptions.HTTPError()) mocker.patch("requests.Session.request", side_effect=requests.HTTPError())
with pytest.raises(requests.exceptions.HTTPError): with pytest.raises(requests.HTTPError):
gpg.key_download("keyserver.ubuntu.com", "0xE989490C") gpg.key_download("keyserver.ubuntu.com", "0xE989490C")

View File

@ -5,7 +5,6 @@ import requests
import requests_unixsocket import requests_unixsocket
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from unittest.mock import call as MockCall
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.status.web_client import WebClient from ahriman.core.status.web_client import WebClient
@ -16,35 +15,6 @@ from ahriman.models.package import Package
from ahriman.models.user import User from ahriman.models.user import User
def test_login_url(web_client: WebClient) -> None:
"""
must generate login url correctly
"""
assert web_client._login_url.endswith("/api/v1/login")
def test_status_url(web_client: WebClient) -> None:
"""
must generate package status url correctly
"""
assert web_client._status_url.endswith("/api/v1/status")
def test_logs_url(web_client: WebClient, package_ahriman: Package) -> None:
"""
must generate logs url correctly
"""
assert web_client._logs_url(package_ahriman.base).endswith(f"/api/v1/packages/{package_ahriman.base}/logs")
def test_package_url(web_client: WebClient, package_ahriman: Package) -> None:
"""
must generate package status url correctly
"""
assert web_client._package_url("").endswith("/api/v1/packages")
assert web_client._package_url(package_ahriman.base).endswith(f"/api/v1/packages/{package_ahriman.base}")
def test_parse_address(configuration: Configuration) -> None: def test_parse_address(configuration: Configuration) -> None:
""" """
must extract address correctly must extract address correctly
@ -87,16 +57,16 @@ def test_login(web_client: WebClient, user: User, mocker: MockerFixture) -> None
""" """
must login user must login user
""" """
web_client.user = user web_client.auth = (user.username, user.password)
requests_mock = mocker.patch("requests.Session.request") requests_mock = mocker.patch("ahriman.core.status.web_client.WebClient.make_request")
payload = { payload = {
"username": user.username, "username": user.username,
"password": user.password "password": user.password
} }
session = requests.Session()
web_client._login(requests.Session()) web_client._login(session)
requests_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True), requests_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True), json=payload, session=session)
params=None, json=payload, files=None)
def test_login_failed(web_client: WebClient, user: User, mocker: MockerFixture) -> None: def test_login_failed(web_client: WebClient, user: User, mocker: MockerFixture) -> None:
@ -113,7 +83,7 @@ def test_login_failed_http_error(web_client: WebClient, user: User, mocker: Mock
must suppress HTTP exception happened during login must suppress HTTP exception happened during login
""" """
web_client.user = user web_client.user = user
mocker.patch("requests.Session.request", side_effect=requests.exceptions.HTTPError()) mocker.patch("requests.Session.request", side_effect=requests.HTTPError())
web_client._login(requests.Session()) web_client._login(requests.Session())
@ -126,57 +96,50 @@ def test_login_skip(web_client: WebClient, mocker: MockerFixture) -> None:
requests_mock.assert_not_called() requests_mock.assert_not_called()
def test_make_request(web_client: WebClient, mocker: MockerFixture) -> None: def test_login_url(web_client: WebClient) -> None:
""" """
must make HTTP request must generate login url correctly
""" """
request_mock = mocker.patch("requests.Session.request") assert web_client._login_url().startswith(web_client.address)
assert web_client._login_url().endswith("/api/v1/login")
assert web_client.make_request("GET", "/url1") is not None
assert web_client.make_request("GET", "/url2", params=[("param", "value")]) is not None
assert web_client.make_request("POST", "/url3") is not None
assert web_client.make_request("POST", "/url4", json={"param": "value"}) is not None
# we don't want to put full descriptor here
assert web_client.make_request("POST", "/url5", files={"file": "tuple"}) is not None
assert web_client.make_request("DELETE", "/url6") is not None
request_mock.assert_has_calls([
MockCall("GET", f"{web_client.address}/url1", params=None, json=None, files=None),
MockCall().raise_for_status(),
MockCall("GET", f"{web_client.address}/url2", params=[("param", "value")], json=None, files=None),
MockCall().raise_for_status(),
MockCall("POST", f"{web_client.address}/url3", params=None, json=None, files=None),
MockCall().raise_for_status(),
MockCall("POST", f"{web_client.address}/url4", params=None, json={"param": "value"}, files=None),
MockCall().raise_for_status(),
MockCall("POST", f"{web_client.address}/url5", params=None, json=None, files={"file": "tuple"}),
MockCall().raise_for_status(),
MockCall("DELETE", f"{web_client.address}/url6", params=None, json=None, files=None),
MockCall().raise_for_status(),
])
def test_make_request_failed(web_client: WebClient, mocker: MockerFixture) -> None: def test_status_url(web_client: WebClient) -> None:
""" """
must make HTTP request must generate package status url correctly
""" """
mocker.patch("requests.Session.request", side_effect=Exception()) assert web_client._status_url().startswith(web_client.address)
with pytest.raises(Exception): assert web_client._status_url().endswith("/api/v1/status")
web_client.make_request("GET", "url")
def test_logs_url(web_client: WebClient, package_ahriman: Package) -> None:
"""
must generate logs url correctly
"""
assert web_client._logs_url(package_ahriman.base).startswith(web_client.address)
assert web_client._logs_url(package_ahriman.base).endswith(f"/api/v1/packages/{package_ahriman.base}/logs")
def test_package_url(web_client: WebClient, package_ahriman: Package) -> None:
"""
must generate package status url correctly
"""
assert web_client._package_url("").startswith(web_client.address)
assert web_client._package_url("").endswith("/api/v1/packages")
assert web_client._package_url(package_ahriman.base).startswith(web_client.address)
assert web_client._package_url(package_ahriman.base).endswith(f"/api/v1/packages/{package_ahriman.base}")
def test_package_add(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None: def test_package_add(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
""" """
must process package addition must process package addition
""" """
requests_mock = mocker.patch("requests.Session.request") requests_mock = mocker.patch("ahriman.core.status.web_client.WebClient.make_request")
payload = pytest.helpers.get_package_status(package_ahriman) payload = pytest.helpers.get_package_status(package_ahriman)
web_client.package_add(package_ahriman, BuildStatusEnum.Unknown) web_client.package_add(package_ahriman, BuildStatusEnum.Unknown)
requests_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True), requests_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True), json=payload)
params=None, json=payload, files=None)
def test_package_add_failed(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None: def test_package_add_failed(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
@ -191,7 +154,7 @@ def test_package_add_failed_http_error(web_client: WebClient, package_ahriman: P
""" """
must suppress HTTP exception happened during addition must suppress HTTP exception happened during addition
""" """
mocker.patch("requests.Session.request", side_effect=requests.exceptions.HTTPError()) mocker.patch("requests.Session.request", side_effect=requests.HTTPError())
web_client.package_add(package_ahriman, BuildStatusEnum.Unknown) web_client.package_add(package_ahriman, BuildStatusEnum.Unknown)
@ -213,7 +176,7 @@ def test_package_add_failed_http_error_suppress(web_client: WebClient, package_a
must suppress HTTP exception happened during addition and don't log must suppress HTTP exception happened during addition and don't log
""" """
web_client.suppress_errors = True web_client.suppress_errors = True
mocker.patch("requests.Session.request", side_effect=requests.exceptions.HTTPError()) mocker.patch("requests.Session.request", side_effect=requests.HTTPError())
logging_mock = mocker.patch("logging.exception") logging_mock = mocker.patch("logging.exception")
web_client.package_add(package_ahriman, BuildStatusEnum.Unknown) web_client.package_add(package_ahriman, BuildStatusEnum.Unknown)
@ -229,11 +192,11 @@ def test_package_get_all(web_client: WebClient, package_ahriman: Package, mocker
response_obj._content = json.dumps(response).encode("utf8") response_obj._content = json.dumps(response).encode("utf8")
response_obj.status_code = 200 response_obj.status_code = 200
requests_mock = mocker.patch("requests.Session.request", return_value=response_obj) requests_mock = mocker.patch("ahriman.core.status.web_client.WebClient.make_request",
return_value=response_obj)
result = web_client.package_get(None) result = web_client.package_get(None)
requests_mock.assert_called_once_with("GET", f"{web_client.address}{web_client._package_url()}", requests_mock.assert_called_once_with("GET", web_client._package_url())
params=None, json=None, files=None)
assert len(result) == len(response) assert len(result) == len(response)
assert (package_ahriman, BuildStatusEnum.Unknown) in [(package, status.status) for package, status in result] assert (package_ahriman, BuildStatusEnum.Unknown) in [(package, status.status) for package, status in result]
@ -250,7 +213,7 @@ def test_package_get_failed_http_error(web_client: WebClient, mocker: MockerFixt
""" """
must suppress HTTP exception happened during status getting must suppress HTTP exception happened during status getting
""" """
mocker.patch("requests.Session.request", side_effect=requests.exceptions.HTTPError()) mocker.patch("requests.Session.request", side_effect=requests.HTTPError())
assert web_client.package_get(None) == [] assert web_client.package_get(None) == []
@ -263,12 +226,11 @@ def test_package_get_single(web_client: WebClient, package_ahriman: Package, moc
response_obj._content = json.dumps(response).encode("utf8") response_obj._content = json.dumps(response).encode("utf8")
response_obj.status_code = 200 response_obj.status_code = 200
requests_mock = mocker.patch("requests.Session.request", return_value=response_obj) requests_mock = mocker.patch("ahriman.core.status.web_client.WebClient.make_request",
return_value=response_obj)
result = web_client.package_get(package_ahriman.base) result = web_client.package_get(package_ahriman.base)
requests_mock.assert_called_once_with("GET", requests_mock.assert_called_once_with("GET", web_client._package_url(package_ahriman.base))
f"{web_client.address}{web_client._package_url(package_ahriman.base)}",
params=None, json=None, files=None)
assert len(result) == len(response) assert len(result) == len(response)
assert (package_ahriman, BuildStatusEnum.Unknown) in [(package, status.status) for package, status in result] assert (package_ahriman, BuildStatusEnum.Unknown) in [(package, status.status) for package, status in result]
@ -278,7 +240,7 @@ def test_package_logs(web_client: WebClient, log_record: logging.LogRecord, pack
""" """
must process log record must process log record
""" """
requests_mock = mocker.patch("requests.Session.request") requests_mock = mocker.patch("ahriman.core.status.web_client.WebClient.make_request")
payload = { payload = {
"created": log_record.created, "created": log_record.created,
"message": log_record.getMessage(), "message": log_record.getMessage(),
@ -286,8 +248,8 @@ def test_package_logs(web_client: WebClient, log_record: logging.LogRecord, pack
} }
web_client.package_logs(LogRecordId(package_ahriman.base, package_ahriman.version), log_record) web_client.package_logs(LogRecordId(package_ahriman.base, package_ahriman.version), log_record)
requests_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True), requests_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True), json=payload,
params=None, json=payload, files=None) suppress_errors=True)
def test_package_logs_failed(web_client: WebClient, log_record: logging.LogRecord, package_ahriman: Package, def test_package_logs_failed(web_client: WebClient, log_record: logging.LogRecord, package_ahriman: Package,
@ -306,7 +268,7 @@ def test_package_logs_failed_http_error(web_client: WebClient, log_record: loggi
""" """
must pass exception during log post must pass exception during log post
""" """
mocker.patch("requests.Session.request", side_effect=requests.exceptions.HTTPError()) mocker.patch("requests.Session.request", side_effect=requests.HTTPError())
log_record.package_base = package_ahriman.base log_record.package_base = package_ahriman.base
with pytest.raises(Exception): with pytest.raises(Exception):
web_client.package_logs(LogRecordId(package_ahriman.base, package_ahriman.version), log_record) web_client.package_logs(LogRecordId(package_ahriman.base, package_ahriman.version), log_record)
@ -316,11 +278,10 @@ def test_package_remove(web_client: WebClient, package_ahriman: Package, mocker:
""" """
must process package removal must process package removal
""" """
requests_mock = mocker.patch("requests.Session.request") requests_mock = mocker.patch("ahriman.core.status.web_client.WebClient.make_request")
web_client.package_remove(package_ahriman.base) web_client.package_remove(package_ahriman.base)
requests_mock.assert_called_once_with("DELETE", pytest.helpers.anyvar(str, True), requests_mock.assert_called_once_with("DELETE", pytest.helpers.anyvar(str, True))
params=None, json=None, files=None)
def test_package_remove_failed(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None: def test_package_remove_failed(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
@ -336,7 +297,7 @@ def test_package_remove_failed_http_error(web_client: WebClient, package_ahriman
""" """
must suppress HTTP exception happened during removal must suppress HTTP exception happened during removal
""" """
mocker.patch("requests.Session.request", side_effect=requests.exceptions.HTTPError()) mocker.patch("requests.Session.request", side_effect=requests.HTTPError())
web_client.package_remove(package_ahriman.base) web_client.package_remove(package_ahriman.base)
@ -344,12 +305,12 @@ def test_package_update(web_client: WebClient, package_ahriman: Package, mocker:
""" """
must process package update must process package update
""" """
requests_mock = mocker.patch("requests.Session.request") requests_mock = mocker.patch("ahriman.core.status.web_client.WebClient.make_request")
web_client.package_update(package_ahriman.base, BuildStatusEnum.Unknown) web_client.package_update(package_ahriman.base, BuildStatusEnum.Unknown)
requests_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True), params=None, json={ requests_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True), json={
"status": BuildStatusEnum.Unknown.value "status": BuildStatusEnum.Unknown.value
}, files=None) })
def test_package_update_failed(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None: def test_package_update_failed(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
@ -365,7 +326,7 @@ def test_package_update_failed_http_error(web_client: WebClient, package_ahriman
""" """
must suppress HTTP exception happened during update must suppress HTTP exception happened during update
""" """
mocker.patch("requests.Session.request", side_effect=requests.exceptions.HTTPError()) mocker.patch("requests.Session.request", side_effect=requests.HTTPError())
web_client.package_update(package_ahriman.base, BuildStatusEnum.Unknown) web_client.package_update(package_ahriman.base, BuildStatusEnum.Unknown)
@ -378,11 +339,11 @@ def test_status_get(web_client: WebClient, mocker: MockerFixture) -> None:
response_obj._content = json.dumps(status.view()).encode("utf8") response_obj._content = json.dumps(status.view()).encode("utf8")
response_obj.status_code = 200 response_obj.status_code = 200
requests_mock = mocker.patch("requests.Session.request", return_value=response_obj) requests_mock = mocker.patch("ahriman.core.status.web_client.WebClient.make_request",
return_value=response_obj)
result = web_client.status_get() result = web_client.status_get()
requests_mock.assert_called_once_with("GET", f"{web_client.address}{web_client._status_url}", requests_mock.assert_called_once_with("GET", web_client._status_url())
params=None, json=None, files=None)
assert result.architecture == "x86_64" assert result.architecture == "x86_64"
@ -398,7 +359,7 @@ def test_status_get_failed_http_error(web_client: WebClient, mocker: MockerFixtu
""" """
must suppress HTTP exception happened during web service status getting must suppress HTTP exception happened during web service status getting
""" """
mocker.patch("requests.Session.request", side_effect=requests.exceptions.HTTPError()) mocker.patch("requests.Session.request", side_effect=requests.HTTPError())
assert web_client.status_get().architecture is None assert web_client.status_get().architecture is None
@ -406,12 +367,12 @@ def test_status_update(web_client: WebClient, mocker: MockerFixture) -> None:
""" """
must process service update must process service update
""" """
requests_mock = mocker.patch("requests.Session.request") requests_mock = mocker.patch("ahriman.core.status.web_client.WebClient.make_request")
web_client.status_update(BuildStatusEnum.Unknown) web_client.status_update(BuildStatusEnum.Unknown)
requests_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True), params=None, json={ requests_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True), json={
"status": BuildStatusEnum.Unknown.value "status": BuildStatusEnum.Unknown.value
}, files=None) })
def test_status_update_self_failed(web_client: WebClient, mocker: MockerFixture) -> None: def test_status_update_self_failed(web_client: WebClient, mocker: MockerFixture) -> None:
@ -426,5 +387,5 @@ def test_status_update_failed_http_error(web_client: WebClient, mocker: MockerFi
""" """
must suppress HTTP exception happened during service update must suppress HTTP exception happened during service update
""" """
mocker.patch("requests.Session.request", side_effect=requests.exceptions.HTTPError()) mocker.patch("requests.Session.request", side_effect=requests.HTTPError())
web_client.status_update(BuildStatusEnum.Unknown) web_client.status_update(BuildStatusEnum.Unknown)

View File

@ -10,9 +10,9 @@ from typing import Any
from unittest.mock import MagicMock, call as MockCall from unittest.mock import MagicMock, call as MockCall
from ahriman.core.exceptions import BuildError, CalledProcessError, OptionError, UnsafeRunError from ahriman.core.exceptions import BuildError, CalledProcessError, OptionError, UnsafeRunError
from ahriman.core.util import check_output, check_user, dataclass_view, enum_values, exception_response_text, \ from ahriman.core.util import check_output, check_user, dataclass_view, enum_values, extract_user, filter_json, \
extract_user, filter_json, full_version, package_like, parse_version, partition, pretty_datetime, pretty_size, \ full_version, package_like, parse_version, partition, pretty_datetime, pretty_size, safe_filename, \
safe_filename, srcinfo_property, srcinfo_property_list, trim_package, utcnow, walk srcinfo_property, srcinfo_property_list, 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
@ -204,25 +204,6 @@ def test_dataclass_view_without_none(package_ahriman: Package) -> None:
assert Package.from_json(result) == package_ahriman assert Package.from_json(result) == package_ahriman
def test_exception_response_text() -> None:
"""
must parse HTTP response to string
"""
response_mock = MagicMock()
response_mock.text = "hello"
exception = requests.exceptions.HTTPError(response=response_mock)
assert exception_response_text(exception) == "hello"
def test_exception_response_text_empty() -> None:
"""
must parse HTTP exception with empty response to empty string
"""
exception = requests.exceptions.HTTPError(response=None)
assert exception_response_text(exception) == ""
def test_extract_user() -> None: def test_extract_user() -> None:
""" """
must extract user from system environment must extract user from system environment

View File

@ -13,7 +13,7 @@ def test_asset_remove(github: Github, github_release: dict[str, Any], mocker: Mo
""" """
must remove asset from the release must remove asset from the release
""" """
request_mock = mocker.patch("ahriman.core.upload.github.Github._request") request_mock = mocker.patch("ahriman.core.upload.github.Github.make_request")
github.asset_remove(github_release, "asset_name") github.asset_remove(github_release, "asset_name")
request_mock.assert_called_once_with("DELETE", "asset_url") request_mock.assert_called_once_with("DELETE", "asset_url")
@ -22,7 +22,7 @@ def test_asset_remove_unknown(github: Github, github_release: dict[str, Any], mo
""" """
must not fail if no asset found must not fail if no asset found
""" """
request_mock = mocker.patch("ahriman.core.upload.github.Github._request") request_mock = mocker.patch("ahriman.core.upload.github.Github.make_request")
github.asset_remove(github_release, "unknown_asset_name") github.asset_remove(github_release, "unknown_asset_name")
request_mock.assert_not_called() request_mock.assert_not_called()
@ -32,11 +32,11 @@ def test_asset_upload(github: Github, github_release: dict[str, Any], mocker: Mo
must upload asset to the repository must upload asset to the repository
""" """
mocker.patch("pathlib.Path.open") mocker.patch("pathlib.Path.open")
request_mock = mocker.patch("ahriman.core.upload.github.Github._request") request_mock = mocker.patch("ahriman.core.upload.github.Github.make_request")
remove_mock = mocker.patch("ahriman.core.upload.github.Github.asset_remove") remove_mock = mocker.patch("ahriman.core.upload.github.Github.asset_remove")
github.asset_upload(github_release, Path("/root/new.tar.xz")) github.asset_upload(github_release, Path("/root/new.tar.xz"))
request_mock.assert_called_once_with("POST", "upload_url", params={"name": "new.tar.xz"}, request_mock.assert_called_once_with("POST", "upload_url", params=[("name", "new.tar.xz")],
data=pytest.helpers.anyvar(int), data=pytest.helpers.anyvar(int),
headers={"Content-Type": "application/x-tar"}) headers={"Content-Type": "application/x-tar"})
remove_mock.assert_not_called() remove_mock.assert_not_called()
@ -47,7 +47,7 @@ def test_asset_upload_with_removal(github: Github, github_release: dict[str, Any
must remove existing file before upload must remove existing file before upload
""" """
mocker.patch("pathlib.Path.open") mocker.patch("pathlib.Path.open")
mocker.patch("ahriman.core.upload.github.Github._request") mocker.patch("ahriman.core.upload.github.Github.make_request")
remove_mock = mocker.patch("ahriman.core.upload.github.Github.asset_remove") remove_mock = mocker.patch("ahriman.core.upload.github.Github.asset_remove")
github.asset_upload(github_release, Path("asset_name")) github.asset_upload(github_release, Path("asset_name"))
@ -65,10 +65,10 @@ def test_asset_upload_empty_mimetype(github: Github, github_release: dict[str, A
mocker.patch("pathlib.Path.open") mocker.patch("pathlib.Path.open")
mocker.patch("ahriman.core.upload.github.Github.asset_remove") mocker.patch("ahriman.core.upload.github.Github.asset_remove")
mocker.patch("mimetypes.guess_type", return_value=(None, None)) mocker.patch("mimetypes.guess_type", return_value=(None, None))
request_mock = mocker.patch("ahriman.core.upload.github.Github._request") request_mock = mocker.patch("ahriman.core.upload.github.Github.make_request")
github.asset_upload(github_release, Path("/root/new.tar.xz")) github.asset_upload(github_release, Path("/root/new.tar.xz"))
request_mock.assert_called_once_with("POST", "upload_url", params={"name": "new.tar.xz"}, request_mock.assert_called_once_with("POST", "upload_url", params=[("name", "new.tar.xz")],
data=pytest.helpers.anyvar(int), data=pytest.helpers.anyvar(int),
headers={"Content-Type": "application/octet-stream"}) headers={"Content-Type": "application/octet-stream"})
@ -125,7 +125,7 @@ def test_release_create(github: Github, mocker: MockerFixture) -> None:
""" """
must create release must create release
""" """
request_mock = mocker.patch("ahriman.core.upload.github.Github._request") request_mock = mocker.patch("ahriman.core.upload.github.Github.make_request")
github.release_create() github.release_create()
request_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True), request_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True),
json={"tag_name": github.architecture, "name": github.architecture}) json={"tag_name": github.architecture, "name": github.architecture})
@ -135,7 +135,7 @@ def test_release_get(github: Github, mocker: MockerFixture) -> None:
""" """
must get release must get release
""" """
request_mock = mocker.patch("ahriman.core.upload.github.Github._request") request_mock = mocker.patch("ahriman.core.upload.github.Github.make_request")
github.release_get() github.release_get()
request_mock.assert_called_once_with("GET", pytest.helpers.anyvar(str, True)) request_mock.assert_called_once_with("GET", pytest.helpers.anyvar(str, True))
@ -146,7 +146,8 @@ def test_release_get_empty(github: Github, mocker: MockerFixture) -> None:
""" """
response = requests.Response() response = requests.Response()
response.status_code = 404 response.status_code = 404
mocker.patch("ahriman.core.upload.github.Github._request", side_effect=requests.HTTPError(response=response)) mocker.patch("ahriman.core.upload.github.Github.make_request",
side_effect=requests.HTTPError(response=response))
assert github.release_get() is None assert github.release_get() is None
@ -154,7 +155,7 @@ def test_release_get_exception(github: Github, mocker: MockerFixture) -> None:
""" """
must re-raise non HTTPError exception must re-raise non HTTPError exception
""" """
mocker.patch("ahriman.core.upload.github.Github._request", side_effect=Exception()) mocker.patch("ahriman.core.upload.github.Github.make_request", side_effect=Exception())
with pytest.raises(Exception): with pytest.raises(Exception):
github.release_get() github.release_get()
@ -164,7 +165,7 @@ def test_release_get_exception_http_error(github: Github, mocker: MockerFixture)
must re-raise HTTPError exception with code differs from 404 must re-raise HTTPError exception with code differs from 404
""" """
exception = requests.HTTPError(response=requests.Response()) exception = requests.HTTPError(response=requests.Response())
mocker.patch("ahriman.core.upload.github.Github._request", side_effect=exception) mocker.patch("ahriman.core.upload.github.Github.make_request", side_effect=exception)
with pytest.raises(requests.HTTPError): with pytest.raises(requests.HTTPError):
github.release_get() github.release_get()
@ -173,7 +174,7 @@ def test_release_update(github: Github, github_release: dict[str, Any], mocker:
""" """
must update release must update release
""" """
request_mock = mocker.patch("ahriman.core.upload.github.Github._request") request_mock = mocker.patch("ahriman.core.upload.github.Github.make_request")
github.release_update(github_release, "body") github.release_update(github_release, "body")
request_mock.assert_called_once_with("POST", "release_url", json={"body": "body"}) request_mock.assert_called_once_with("POST", "release_url", json={"body": "body"})

View File

@ -1,11 +1,5 @@
import pytest
import requests
from pathlib import Path from pathlib import Path
from pytest_mock import MockerFixture
from unittest.mock import MagicMock
from ahriman.core.upload.github import Github
from ahriman.core.upload.http_upload import HttpUpload from ahriman.core.upload.http_upload import HttpUpload
@ -40,24 +34,3 @@ def test_get_hashes_empty() -> None:
must read empty body must read empty body
""" """
assert HttpUpload.get_hashes("") == {} assert HttpUpload.get_hashes("") == {}
def test_request(github: Github, mocker: MockerFixture) -> None:
"""
must call request method
"""
response_mock = MagicMock()
request_mock = mocker.patch("requests.Session.request", return_value=response_mock)
github._request("GET", "url", arg="arg")
request_mock.assert_called_once_with("GET", "url", auth=github.auth, timeout=github.timeout, arg="arg")
response_mock.raise_for_status.assert_called_once_with()
def test_request_exception(github: Github, mocker: MockerFixture) -> None:
"""
must call request method and log HTTPError exception
"""
mocker.patch("requests.Session.request", side_effect=requests.HTTPError())
with pytest.raises(requests.HTTPError):
github._request("GET", "url", arg="arg")

View File

@ -24,7 +24,7 @@ def test_package_upload(remote_service: RemoteService, package_ahriman: Package,
mocker.patch("pathlib.Path.is_file", return_value=False) mocker.patch("pathlib.Path.is_file", return_value=False)
file_mock = MagicMock() file_mock = MagicMock()
open_mock = mocker.patch("pathlib.Path.open", return_value=file_mock) open_mock = mocker.patch("pathlib.Path.open", return_value=file_mock)
upload_mock = mocker.patch("ahriman.core.upload.http_upload.HttpUpload._request") upload_mock = mocker.patch("ahriman.core.upload.http_upload.HttpUpload.make_request")
filename = package_ahriman.packages[package_ahriman.base].filename filename = package_ahriman.packages[package_ahriman.base].filename
remote_service.sync(Path("local"), [package_ahriman]) remote_service.sync(Path("local"), [package_ahriman])
@ -43,7 +43,7 @@ def test_package_upload_with_signature(remote_service: RemoteService, package_ah
mocker.patch("pathlib.Path.is_file", return_value=True) mocker.patch("pathlib.Path.is_file", return_value=True)
file_mock = MagicMock() file_mock = MagicMock()
open_mock = mocker.patch("pathlib.Path.open", return_value=file_mock) open_mock = mocker.patch("pathlib.Path.open", return_value=file_mock)
upload_mock = mocker.patch("ahriman.core.upload.http_upload.HttpUpload._request") upload_mock = mocker.patch("ahriman.core.upload.http_upload.HttpUpload.make_request")
filename = package_ahriman.packages[package_ahriman.base].filename filename = package_ahriman.packages[package_ahriman.base].filename
remote_service.sync(Path("local"), [package_ahriman]) remote_service.sync(Path("local"), [package_ahriman])

View File

@ -4,27 +4,6 @@ from ahriman.models.user import User
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
def test_from_option(user: User) -> None:
"""
must generate user from options
"""
user = replace(user, access=UserAccess.Read, packager_id=None, key=None)
assert User.from_option(user.username, user.password) == user
# default is read access
user = replace(user, access=UserAccess.Full)
assert User.from_option(user.username, user.password) != user
assert User.from_option(user.username, user.password, user.access) == user
def test_from_option_empty() -> None:
"""
must return nothing if settings are missed
"""
assert User.from_option(None, "") is None
assert User.from_option("", None) is None
assert User.from_option(None, None) is None
def test_check_credentials_hash_password(user: User) -> None: def test_check_credentials_hash_password(user: User) -> None:
""" """
must generate and validate user password must generate and validate user password