diff --git a/docs/ahriman.core.http.rst b/docs/ahriman.core.http.rst index e2df58a4..15b35d3f 100644 --- a/docs/ahriman.core.http.rst +++ b/docs/ahriman.core.http.rst @@ -4,6 +4,14 @@ ahriman.core.http package Submodules ---------- +ahriman.core.http.sync\_ahriman\_client module +---------------------------------------------- + +.. automodule:: ahriman.core.http.sync_ahriman_client + :members: + :no-undoc-members: + :show-inheritance: + ahriman.core.http.sync\_http\_client module ------------------------------------------- diff --git a/src/ahriman/core/http/__init__.py b/src/ahriman/core/http/__init__.py index 3e476dd1..02b72d30 100644 --- a/src/ahriman/core/http/__init__.py +++ b/src/ahriman/core/http/__init__.py @@ -17,4 +17,5 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # +from ahriman.core.http.sync_ahriman_client import SyncAhrimanClient from ahriman.core.http.sync_http_client import MultipartType, SyncHttpClient diff --git a/src/ahriman/core/http/sync_ahriman_client.py b/src/ahriman/core/http/sync_ahriman_client.py new file mode 100644 index 00000000..982d633e --- /dev/null +++ b/src/ahriman/core/http/sync_ahriman_client.py @@ -0,0 +1,85 @@ +# +# Copyright (c) 2021-2023 ahriman team. +# +# This file is part of ahriman +# (see https://github.com/arcan1s/ahriman). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +import contextlib +import requests + +from functools import cached_property +from urllib.parse import urlparse + +from ahriman import __version__ +from ahriman.core.http.sync_http_client import SyncHttpClient + + +class SyncAhrimanClient(SyncHttpClient): + """ + wrapper for ahriman web service + + Attributes: + address(str): address of the web service + """ + + address: str + + @cached_property + def session(self) -> requests.Session: + """ + get or create session + + Returns: + request.Session: created session object + """ + if urlparse(self.address).scheme == "http+unix": + import requests_unixsocket # type: ignore[import-untyped] + session: requests.Session = requests_unixsocket.Session() + session.headers["User-Agent"] = f"ahriman/{__version__}" + return session + + session = requests.Session() + session.headers["User-Agent"] = f"ahriman/{__version__}" + self._login(session) + + return session + + def _login(self, session: requests.Session) -> None: + """ + process login to the service + + Args: + session(requests.Session): request session to login + """ + if self.auth is None: + return # no auth configured + + username, password = self.auth + payload = { + "username": username, + "password": password, + } + with contextlib.suppress(Exception): + self.make_request("POST", self._login_url(), json=payload, session=session) + + def _login_url(self) -> str: + """ + get url for the login api + + Returns: + str: full url for web service to log in + """ + return f"{self.address}/api/v1/login" diff --git a/src/ahriman/core/status/web_client.py b/src/ahriman/core/status/web_client.py index a5683c06..0978fdc8 100644 --- a/src/ahriman/core/status/web_client.py +++ b/src/ahriman/core/status/web_client.py @@ -19,14 +19,11 @@ # import contextlib import logging -import requests -from functools import cached_property -from urllib.parse import quote_plus as urlencode, urlparse +from urllib.parse import quote_plus as urlencode -from ahriman import __version__ from ahriman.core.configuration import Configuration -from ahriman.core.http import SyncHttpClient +from ahriman.core.http import SyncAhrimanClient from ahriman.core.status.client import Client from ahriman.models.build_status import BuildStatus, BuildStatusEnum from ahriman.models.internal_status import InternalStatus @@ -35,12 +32,11 @@ from ahriman.models.package import Package from ahriman.models.repository_id import RepositoryId -class WebClient(Client, SyncHttpClient): +class WebClient(Client, SyncAhrimanClient): """ build status reporter web client Attributes: - address(str): address of the web service repository_id(RepositoryId): repository unique identifier """ @@ -56,30 +52,10 @@ class WebClient(Client, SyncHttpClient): suppress_errors = configuration.getboolean( # read old-style first and then fallback to new style "settings", "suppress_http_log_errors", fallback=configuration.getboolean("status", "suppress_http_log_errors", fallback=False)) - SyncHttpClient.__init__(self, configuration, section, suppress_errors=suppress_errors) + SyncAhrimanClient.__init__(self, configuration, section, suppress_errors=suppress_errors) self.repository_id = repository_id - @cached_property - def session(self) -> requests.Session: - """ - get or create session - - Returns: - request.Session: created session object - """ - if urlparse(self.address).scheme == "http+unix": - import requests_unixsocket # type: ignore[import-untyped] - session: requests.Session = requests_unixsocket.Session() - session.headers["User-Agent"] = f"ahriman/{__version__}" - return session - - session = requests.Session() - session.headers["User-Agent"] = f"ahriman/{__version__}" - self._login(session) - - return session - @staticmethod def parse_address(configuration: Configuration) -> tuple[str, str]: """ @@ -107,33 +83,6 @@ class WebClient(Client, SyncHttpClient): address = f"http://{host}:{port}" return "web", address - def _login(self, session: requests.Session) -> None: - """ - process login to the service - - Args: - session(requests.Session): request session to login - """ - if self.auth is None: - return # no auth configured - - username, password = self.auth - payload = { - "username": username, - "password": password, - } - with contextlib.suppress(Exception): - self.make_request("POST", self._login_url(), json=payload, session=session) - - def _login_url(self) -> str: - """ - get url for the login api - - Returns: - str: full url for web service to log in - """ - return f"{self.address}/api/v1/login" - def _logs_url(self, package_base: str) -> str: """ get url for the logs api diff --git a/tests/ahriman/core/http/conftest.py b/tests/ahriman/core/http/conftest.py new file mode 100644 index 00000000..16ef43df --- /dev/null +++ b/tests/ahriman/core/http/conftest.py @@ -0,0 +1,21 @@ +import pytest + +from ahriman.core.configuration import Configuration +from ahriman.core.http import SyncAhrimanClient +from ahriman.core.status.web_client import WebClient + + +@pytest.fixture +def ahriman_client(configuration: Configuration) -> SyncAhrimanClient: + """ + ahriman web client fixture + + Args: + configuration(Configuration): configuration fixture + + Returns: + SyncAhrimanClient: ahriman web client test instance + """ + configuration.set("web", "port", "8080") + _, repository_id = configuration.check_loaded() + return WebClient(repository_id, configuration) diff --git a/tests/ahriman/core/http/test_sync_ahriman_client.py b/tests/ahriman/core/http/test_sync_ahriman_client.py new file mode 100644 index 00000000..2a83bcac --- /dev/null +++ b/tests/ahriman/core/http/test_sync_ahriman_client.py @@ -0,0 +1,81 @@ +import pytest +import requests +import requests_unixsocket + +from pytest_mock import MockerFixture + +from ahriman.core.http import SyncAhrimanClient +from ahriman.models.user import User + + +def test_session(ahriman_client: SyncAhrimanClient, mocker: MockerFixture) -> None: + """ + must create normal requests session + """ + login_mock = mocker.patch("ahriman.core.http.SyncAhrimanClient._login") + + assert isinstance(ahriman_client.session, requests.Session) + assert not isinstance(ahriman_client.session, requests_unixsocket.Session) + login_mock.assert_called_once_with(pytest.helpers.anyvar(int)) + + +def test_session_unix_socket(ahriman_client: SyncAhrimanClient, mocker: MockerFixture) -> None: + """ + must create unix socket session + """ + login_mock = mocker.patch("ahriman.core.http.SyncAhrimanClient._login") + ahriman_client.address = "http+unix://path" + + assert isinstance(ahriman_client.session, requests_unixsocket.Session) + login_mock.assert_not_called() + + +def test_login(ahriman_client: SyncAhrimanClient, user: User, mocker: MockerFixture) -> None: + """ + must login user + """ + ahriman_client.auth = (user.username, user.password) + requests_mock = mocker.patch("ahriman.core.http.SyncAhrimanClient.make_request") + payload = { + "username": user.username, + "password": user.password + } + session = requests.Session() + + ahriman_client._login(session) + requests_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True), json=payload, session=session) + + +def test_login_failed(ahriman_client: SyncAhrimanClient, user: User, mocker: MockerFixture) -> None: + """ + must suppress any exception happened during login + """ + ahriman_client.user = user + mocker.patch("requests.Session.request", side_effect=Exception()) + ahriman_client._login(requests.Session()) + + +def test_login_failed_http_error(ahriman_client: SyncAhrimanClient, user: User, mocker: MockerFixture) -> None: + """ + must suppress HTTP exception happened during login + """ + ahriman_client.user = user + mocker.patch("requests.Session.request", side_effect=requests.HTTPError()) + ahriman_client._login(requests.Session()) + + +def test_login_skip(ahriman_client: SyncAhrimanClient, mocker: MockerFixture) -> None: + """ + must skip login if no user set + """ + requests_mock = mocker.patch("requests.Session.request") + ahriman_client._login(requests.Session()) + requests_mock.assert_not_called() + + +def test_login_url(ahriman_client: SyncAhrimanClient) -> None: + """ + must generate login url correctly + """ + assert ahriman_client._login_url().startswith(ahriman_client.address) + assert ahriman_client._login_url().endswith("/api/v1/login") diff --git a/tests/ahriman/core/status/test_web_client.py b/tests/ahriman/core/status/test_web_client.py index 0c5fcdff..08cb0f4f 100644 --- a/tests/ahriman/core/status/test_web_client.py +++ b/tests/ahriman/core/status/test_web_client.py @@ -2,7 +2,6 @@ import json import logging import pytest import requests -import requests_unixsocket from pytest_mock import MockerFixture @@ -12,29 +11,6 @@ from ahriman.models.build_status import BuildStatus, BuildStatusEnum from ahriman.models.internal_status import InternalStatus from ahriman.models.log_record_id import LogRecordId from ahriman.models.package import Package -from ahriman.models.user import User - - -def test_session(web_client: WebClient, mocker: MockerFixture) -> None: - """ - must create normal requests session - """ - login_mock = mocker.patch("ahriman.core.status.web_client.WebClient._login") - - assert isinstance(web_client.session, requests.Session) - assert not isinstance(web_client.session, requests_unixsocket.Session) - login_mock.assert_called_once_with(pytest.helpers.anyvar(int)) - - -def test_session_unix_socket(web_client: WebClient, mocker: MockerFixture) -> None: - """ - must create unix socket session - """ - login_mock = mocker.patch("ahriman.core.status.web_client.WebClient._login") - web_client.address = "http+unix://path" - - assert isinstance(web_client.session, requests_unixsocket.Session) - login_mock.assert_not_called() def test_parse_address(configuration: Configuration) -> None: @@ -55,57 +31,6 @@ def test_parse_address(configuration: Configuration) -> None: assert WebClient.parse_address(configuration) == ("status", "http://localhost:8082") -def test_login(web_client: WebClient, user: User, mocker: MockerFixture) -> None: - """ - must login user - """ - web_client.auth = (user.username, user.password) - requests_mock = mocker.patch("ahriman.core.status.web_client.WebClient.make_request") - payload = { - "username": user.username, - "password": user.password - } - session = requests.Session() - - web_client._login(session) - requests_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True), json=payload, session=session) - - -def test_login_failed(web_client: WebClient, user: User, mocker: MockerFixture) -> None: - """ - must suppress any exception happened during login - """ - web_client.user = user - mocker.patch("requests.Session.request", side_effect=Exception()) - web_client._login(requests.Session()) - - -def test_login_failed_http_error(web_client: WebClient, user: User, mocker: MockerFixture) -> None: - """ - must suppress HTTP exception happened during login - """ - web_client.user = user - mocker.patch("requests.Session.request", side_effect=requests.HTTPError()) - web_client._login(requests.Session()) - - -def test_login_skip(web_client: WebClient, mocker: MockerFixture) -> None: - """ - must skip login if no user set - """ - requests_mock = mocker.patch("requests.Session.request") - web_client._login(requests.Session()) - requests_mock.assert_not_called() - - -def test_login_url(web_client: WebClient) -> None: - """ - must generate login url correctly - """ - assert web_client._login_url().startswith(web_client.address) - assert web_client._login_url().endswith("/api/v1/login") - - def test_status_url(web_client: WebClient) -> None: """ must generate package status url correctly @@ -377,11 +302,13 @@ def test_status_update(web_client: WebClient, mocker: MockerFixture) -> None: requests_mock = mocker.patch("ahriman.core.status.web_client.WebClient.make_request") web_client.status_update(BuildStatusEnum.Unknown) - requests_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True), - params=web_client.repository_id.query(), - json={ - "status": BuildStatusEnum.Unknown.value, - }) + requests_mock.assert_called_once_with( + "POST", pytest.helpers.anyvar(str, True), + params=web_client.repository_id.query(), + json={ + "status": BuildStatusEnum.Unknown.value, + } + ) def test_status_update_self_failed(web_client: WebClient, mocker: MockerFixture) -> None: