Compare commits

..

8 Commits

26 changed files with 114 additions and 295 deletions

View File

@@ -158,9 +158,7 @@ Reporting to web service related settings. In most cases there is fallback to we
* ``enabled`` - enable reporting to web service, boolean, optional, default ``yes`` for backward compatibility.
* ``address`` - remote web service address with protocol, string, optional. In case of websocket, the ``http+unix`` scheme and URL encoded address (e.g. ``%2Fvar%2Flib%2Fahriman`` for ``/var/lib/ahriman``) must be used, e.g. ``http+unix://%2Fvar%2Flib%2Fahriman%2Fsocket``. In case if none set, it will be guessed from ``web`` section.
* ``max_retries`` - maximum amount of retries of HTTP requests, integer, optional, default ``0``.
* ``password`` - password to authorize in web service in order to update service status, string, required in case if authorization enabled.
* ``retry_backoff`` - retry exponential backoff, float, optional, default ``0.0``.
* ``suppress_http_log_errors`` - suppress HTTP log errors, boolean, optional, default ``no``. If set to ``yes``, any HTTP log errors (e.g. if web server is not available, but HTTP logging is enabled) will be suppressed.
* ``timeout`` - HTTP request timeout in seconds, integer, optional, default is ``30``.
* ``username`` - username to authorize in web service in order to update service status, string, required in case if authorization enabled.
@@ -369,8 +367,6 @@ Section name must be either ``telegram`` (plus optional architecture name, e.g.
* ``chat_id`` - telegram chat id, either string with ``@`` or integer value, required.
* ``homepage`` - link to homepage, string, optional.
* ``link_path`` - prefix for HTML links, string, required.
* ``max_retries`` - maximum amount of retries of HTTP requests, integer, optional, default ``0``.
* ``retry_backoff`` - retry exponential backoff, float, optional, default ``0.0``.
* ``rss_url`` - link to RSS feed, string, optional.
* ``template`` - Jinja2 template name, string, required.
* ``template_type`` - ``parse_mode`` to be passed to telegram API, one of ``MarkdownV2``, ``HTML``, ``Markdown``, string, optional, default ``HTML``.
@@ -396,7 +392,6 @@ Type will be read from several sources:
This feature requires GitHub key creation (see below). Section name must be either ``github`` (plus optional architecture name, e.g. ``github:x86_64``) or random name with ``type`` set.
* ``type`` - type of the upload, string, optional, must be set to ``github`` if exists.
* ``max_retries`` - maximum amount of retries of HTTP requests, integer, optional, default ``0``.
* ``owner`` - GitHub repository owner, string, required.
* ``password`` - created GitHub API key. In order to create it do the following:
@@ -406,7 +401,6 @@ This feature requires GitHub key creation (see below). Section name must be eith
#. Generate new token. Required scope is ``public_repo`` (or ``repo`` for private repository support).
* ``repository`` - GitHub repository name, string, required. Repository must be created before any action and must have active branch (e.g. with readme).
* ``retry_backoff`` - retry exponential backoff, float, optional, default ``0.0``.
* ``timeout`` - HTTP request timeout in seconds, integer, optional, default is ``30``.
* ``use_full_release_name`` - if set to ``yes``, the release will contain both repository name and architecture, and only architecture otherwise, boolean, optional, default ``no`` (legacy behavior).
* ``username`` - GitHub authorization user, string, required. Basically the same as ``owner``.
@@ -417,8 +411,6 @@ 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.
* ``type`` - type of the report, string, optional, must be set to ``remote-service`` if exists.
* ``max_retries`` - maximum amount of retries of HTTP requests, integer, optional, default ``0``.
* ``retry_backoff`` - retry exponential backoff, float, optional, default ``0.0``.
* ``timeout`` - HTTP request timeout in seconds, integer, optional, default is ``30``.
``rsync`` type

View File

@@ -73,12 +73,8 @@ enabled = yes
; In case if unix sockets are used, it might point to the valid socket with encoded path, e.g.:
; address = http+unix://%2Fvar%2Flib%2Fahriman%2Fsocket
;address = http://${web:host}:${web:port}
; Maximum amount of retries of HTTP requests.
;max_retries = 0
; Optional password for authentication (if enabled).
;password =
; Retry exponential backoff.
;retry_backoff = 0.0
; Do not log HTTP errors if occurs.
suppress_http_log_errors = yes
; HTTP request timeout in seconds.
@@ -220,10 +216,6 @@ templates[] = ${prefix}/share/ahriman/templates
;homepage=
; Prefix for packages links. Link to a package will be formed as link_path / filename.
;link_path =
; Maximum amount of retries of HTTP requests.
;max_retries = 0
; Retry exponential backoff.
;retry_backoff = 0.0
; Optional link to the RSS feed.
;rss_url =
; Template name to be used.
@@ -244,16 +236,12 @@ target =
[github]
; Trigger type name.
;type = github
; Maximum amount of retries of HTTP requests.
;max_retries = 0
; GitHub repository owner username.
;owner =
; GitHub API key. public_repo (repo) scope is required.
;password =
; GitHub repository name.
;repository =
; Retry exponential backoff.
;retry_backoff = 0.0
; HTTP request timeout in seconds.
;timeout = 30
; Include repository name to release name (recommended).
@@ -265,10 +253,6 @@ target =
[remote-service]
; Trigger type name.
;type = remote-service
; Maximum amount of retries of HTTP requests.
;max_retries = 0
; Retry exponential backoff.
;retry_backoff = 0.0
; HTTP request timeout in seconds.
;timeout = 30

View File

@@ -87,7 +87,7 @@
};
});
updateTable(table, payload, row => row.timestamp);
updateTable(table, payload);
table.bootstrapTable("hideLoading");
},
onFailure,

View File

@@ -195,19 +195,16 @@
return intervalId;
}
function updateTable(table, rows, rowChangedKey) {
function updateTable(table, rows) {
// instead of using load method here, we just update rows manually to avoid table reinitialization
const currentData = table.bootstrapTable("getData").reduce((accumulator, row) => {
accumulator[row.id] = {state: row["0"], key: rowChangedKey(row)};
accumulator[row.id] = row["0"];
return accumulator;
}, {});
// insert or update rows, skipping ones whose status hasn't changed
// insert or update rows
rows.forEach(row => {
if (Object.hasOwn(currentData, row.id)) {
if (rowChangedKey(row) === currentData[row.id].key) {
return;
}
row["0"] = currentData[row.id].state; // copy checkbox state
row["0"] = currentData[row.id]; // copy checkbox state
table.bootstrapTable("updateByUniqueId", {
id: row.id,
row: row,

View File

@@ -81,13 +81,11 @@ class Backup(Handler):
Returns:
set[Path]: map of the filesystem paths
"""
# configuration files
root, _ = configuration.check_loaded()
paths = set(configuration.includes)
paths.add(root)
paths = set(configuration.include.glob("*.ini"))
# database
paths.add(SQLite.database_path(configuration))
root, _ = configuration.check_loaded()
paths.add(root) # the configuration itself
paths.add(SQLite.database_path(configuration)) # database
# local caches
repository_paths = configuration.repository_paths

View File

@@ -47,7 +47,7 @@ class Restore(Handler):
report(bool): force enable or disable reporting
"""
with tarfile.open(args.path) as archive:
archive.extractall(path=args.output, filter="data")
archive.extractall(path=args.output) # nosec
@staticmethod
def _set_repo_restore_parser(root: SubParserAction) -> argparse.ArgumentParser:

View File

@@ -86,7 +86,7 @@ class OAuth(Mapping):
Raises:
OptionError: in case if invalid OAuth provider name supplied
"""
provider: type = getattr(aioauth_client, name, type(None))
provider: type[aioauth_client.OAuth2Client] = getattr(aioauth_client, name)
try:
is_oauth2_client = issubclass(provider, aioauth_client.OAuth2Client)
except TypeError: # what if it is random string?

View File

@@ -296,20 +296,10 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
"empty": False,
"is_url": [],
},
"max_retries": {
"type": "integer",
"coerce": "integer",
"min": 0,
},
"password": {
"type": "string",
"empty": False,
},
"retry_backoff": {
"type": "float",
"coerce": "float",
"min": 0,
},
"suppress_http_log_errors": {
"type": "boolean",
"coerce": "boolean",

View File

@@ -76,19 +76,6 @@ class Validator(RootValidator):
converted: bool = self.configuration._convert_to_boolean(value) # type: ignore[attr-defined]
return converted
def _normalize_coerce_float(self, value: str) -> float:
"""
extract float from string value
Args:
value(str): converting value
Returns:
float: value converted to float according to configuration rules
"""
del self
return float(value)
def _normalize_coerce_integer(self, value: str) -> int:
"""
extract integer from string value

View File

@@ -20,9 +20,10 @@
import contextlib
import requests
from requests.adapters import BaseAdapter
from functools import cached_property
from urllib.parse import urlparse
from ahriman import __version__
from ahriman.core.http.sync_http_client import SyncHttpClient
@@ -36,36 +37,32 @@ class SyncAhrimanClient(SyncHttpClient):
address: str
def _login_url(self) -> str:
@cached_property
def session(self) -> requests.Session:
"""
get url for the login api
get or create session
Returns:
str: full url for web service to log in
request.Session: created session object
"""
return f"{self.address}/api/v1/login"
if urlparse(self.address).scheme == "http+unix":
import requests_unixsocket
session: requests.Session = requests_unixsocket.Session() # type: ignore[no-untyped-call]
session.headers["User-Agent"] = f"ahriman/{__version__}"
return session
def adapters(self) -> dict[str, BaseAdapter]:
session = requests.Session()
session.headers["User-Agent"] = f"ahriman/{__version__}"
self._login(session)
return session
def _login(self, session: requests.Session) -> None:
"""
get registered adapters
Returns:
dict[str, BaseAdapter]: map of protocol and adapter used for this protocol
"""
adapters = SyncHttpClient.adapters(self)
if (scheme := urlparse(self.address).scheme) == "http+unix":
from requests_unixsocket.adapters import UnixAdapter
adapters[f"{scheme}://"] = UnixAdapter() # type: ignore[no-untyped-call]
return adapters
def start(self, session: requests.Session) -> None:
"""
method which will be called on session creation
process login to the service
Args:
session(requests.Session): created requests session
session(requests.Session): request session to login
"""
if self.auth is None:
return # no auth configured
@@ -77,3 +74,12 @@ class SyncAhrimanClient(SyncHttpClient):
}
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"

View File

@@ -21,9 +21,7 @@ import requests
import sys
from functools import cached_property
from requests.adapters import BaseAdapter, HTTPAdapter
from typing import Any, IO, Literal
from urllib3.util.retry import Retry
from ahriman import __version__
from ahriman.core.configuration import Configuration
@@ -64,16 +62,6 @@ class SyncHttpClient(LazyLogging):
self.timeout: int | None = configuration.getint(section, "timeout", fallback=30)
self.suppress_errors = suppress_errors
retries = configuration.getint(section, "max_retries", fallback=0)
self.retry = Retry(
total=retries,
connect=retries,
read=retries,
status=retries,
status_forcelist=[429, 500, 502, 503, 504],
backoff_factor=configuration.getfloat(section, "retry_backoff", fallback=0.0),
)
@cached_property
def session(self) -> requests.Session:
"""
@@ -83,17 +71,11 @@ class SyncHttpClient(LazyLogging):
request.Session: created session object
"""
session = requests.Session()
for protocol, adapter in self.adapters().items():
session.mount(protocol, adapter)
python_version = ".".join(map(str, sys.version_info[:3])) # just major.minor.patch
session.headers["User-Agent"] = f"ahriman/{__version__} " \
f"{requests.utils.default_user_agent()} " \
f"python/{python_version}"
self.start(session)
return session
@staticmethod
@@ -110,19 +92,6 @@ class SyncHttpClient(LazyLogging):
result: str = exception.response.text if exception.response is not None else ""
return result
def adapters(self) -> dict[str, BaseAdapter]:
"""
get registered adapters
Returns:
dict[str, BaseAdapter]: map of protocol and adapter used for this protocol
"""
adapter = HTTPAdapter(max_retries=self.retry)
return {
"http://": adapter,
"https://": adapter,
}
def make_request(self, method: Literal["DELETE", "GET", "HEAD", "POST", "PUT"], url: str, *,
headers: dict[str, str] | None = None,
params: list[tuple[str, str]] | None = None,
@@ -170,11 +139,3 @@ class SyncHttpClient(LazyLogging):
if not suppress_errors:
self.logger.exception("could not perform http request")
raise
def start(self, session: requests.Session) -> None:
"""
method which will be called on session creation
Args:
session(requests.Session): created requests session
"""

View File

@@ -74,18 +74,6 @@ class Email(Report, JinjaTemplate):
self.ssl = SmtpSSLSettings.from_option(configuration.get(section, "ssl", fallback="disabled"))
self.user = configuration.get(section, "user", fallback=None)
@property
def _smtp_session(self) -> type[smtplib.SMTP]:
"""
build SMTP session based on configuration settings
Returns:
type[smtplib.SMTP]: SMTP or SMTP_SSL session depending on whether SSL is enabled or not
"""
if self.ssl == SmtpSSLSettings.SSL:
return smtplib.SMTP_SSL
return smtplib.SMTP
def _send(self, text: str, attachment: dict[str, str]) -> None:
"""
send email callback
@@ -105,13 +93,16 @@ class Email(Report, JinjaTemplate):
attach.add_header("Content-Disposition", "attachment", filename=filename)
message.attach(attach)
with self._smtp_session(self.host, self.port) as session:
if self.ssl != SmtpSSLSettings.SSL:
session = smtplib.SMTP(self.host, self.port)
if self.ssl == SmtpSSLSettings.STARTTLS:
session.starttls()
if self.user is not None and self.password is not None:
session.login(self.user, self.password)
session.sendmail(self.sender, self.receivers, message.as_string())
else:
session = smtplib.SMTP_SSL(self.host, self.port)
if self.user is not None and self.password is not None:
session.login(self.user, self.password)
session.sendmail(self.sender, self.receivers, message.as_string())
session.quit()
def generate(self, packages: list[Package], result: Result) -> None:
"""

View File

@@ -302,16 +302,6 @@ class ReportTrigger(Trigger):
"empty": False,
"is_url": [],
},
"max_retries": {
"type": "integer",
"coerce": "integer",
"min": 0,
},
"retry_backoff": {
"type": "float",
"coerce": "float",
"min": 0,
},
"rss_url": {
"type": "string",
"empty": False,

View File

@@ -54,11 +54,6 @@ class UploadTrigger(Trigger):
"type": "string",
"allowed": ["github"],
},
"max_retries": {
"type": "integer",
"coerce": "integer",
"min": 0,
},
"owner": {
"type": "string",
"required": True,
@@ -73,11 +68,6 @@ class UploadTrigger(Trigger):
"required": True,
"empty": False,
},
"retry_backoff": {
"type": "float",
"coerce": "float",
"min": 0,
},
"timeout": {
"type": "integer",
"coerce": "integer",
@@ -100,16 +90,6 @@ class UploadTrigger(Trigger):
"type": "string",
"allowed": ["ahriman", "remote-service"],
},
"max_retries": {
"type": "integer",
"coerce": "integer",
"min": 0,
},
"retry_backoff": {
"type": "float",
"coerce": "float",
"min": 0,
},
"timeout": {
"type": "integer",
"coerce": "integer",

View File

@@ -164,11 +164,6 @@ def check_output(*args: str, exception: Exception | Callable[[int, list[str], st
if key in ("PATH",) # whitelisted variables only
} | environment
result: dict[str, list[str]] = {
"stdout": [],
"stderr": [],
}
with subprocess.Popen(args, cwd=cwd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
user=user, env=full_environment, text=True, encoding="utf8", errors="backslashreplace",
bufsize=1) as process:
@@ -177,27 +172,30 @@ def check_output(*args: str, exception: Exception | Callable[[int, list[str], st
input_channel.write(input_data)
input_channel.close()
with selectors.DefaultSelector() as selector:
selector.register(get_io(process, "stdout"), selectors.EVENT_READ, data="stdout")
selector.register(get_io(process, "stderr"), selectors.EVENT_READ, data="stderr")
selector = selectors.DefaultSelector()
selector.register(get_io(process, "stdout"), selectors.EVENT_READ, data="stdout")
selector.register(get_io(process, "stderr"), selectors.EVENT_READ, data="stderr")
while selector.get_map(): # while there are unread selectors, keep reading
for key_data, output in poll(selector):
result[key_data].append(output)
result: dict[str, list[str]] = {
"stdout": [],
"stderr": [],
}
while selector.get_map(): # while there are unread selectors, keep reading
for key_data, output in poll(selector):
result[key_data].append(output)
stdout = "\n".join(result["stdout"]).rstrip("\n") # remove newline at the end of any
stderr = "\n".join(result["stderr"]).rstrip("\n")
status_code = process.wait()
if status_code != 0:
if isinstance(exception, Exception):
raise exception
if callable(exception):
raise exception(status_code, list(args), stdout, stderr)
raise CalledProcessError(status_code, list(args), stderr)
stdout = "\n".join(result["stdout"]).rstrip("\n") # remove newline at the end of any
stderr = "\n".join(result["stderr"]).rstrip("\n")
if status_code != 0:
if isinstance(exception, Exception):
raise exception
if callable(exception):
raise exception(status_code, list(args), stdout, stderr)
raise CalledProcessError(status_code, list(args), stderr)
return stdout
return stdout
def check_user(root: Path, *, unsafe: bool) -> None:

View File

@@ -72,7 +72,7 @@ def _security() -> list[dict[str, Any]]:
return [{
"token": {
"type": "apiKey", # as per specification we are using api key
"name": "AHRIMAN",
"name": "API_SESSION",
"in": "cookie",
}
}]

View File

@@ -149,17 +149,11 @@ def setup_auth(application: Application, configuration: Configuration, validator
Application: configured web application
"""
secret_key = _cookie_secret_key(configuration)
storage = EncryptedCookieStorage(
secret_key,
cookie_name="AHRIMAN",
max_age=validator.max_age,
httponly=True,
samesite="Strict",
)
storage = EncryptedCookieStorage(secret_key, cookie_name="API_SESSION", max_age=validator.max_age)
setup_session(application, storage)
authorization_policy = _AuthorizationPolicy(validator)
identity_policy = aiohttp_security.SessionIdentityPolicy("SESSION")
identity_policy = aiohttp_security.SessionIdentityPolicy()
aiohttp_security.setup(application, identity_policy, authorization_policy)
application.middlewares.append(_auth_handler(validator.allow_read_only))

View File

@@ -25,6 +25,6 @@ class AuthSchema(Schema):
request cookie authorization schema
"""
AHRIMAN = fields.String(required=True, metadata={
API_SESSION = fields.String(required=True, metadata={
"description": "API session key as returned from authorization",
})

View File

@@ -34,7 +34,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
_, repository_id = configuration.check_loaded()
Restore.run(args, repository_id, configuration, report=False)
extract_mock.extractall.assert_called_once_with(path=args.output, filter="data")
extract_mock.extractall.assert_called_once_with(path=args.output)
def test_disallow_multi_architecture_run() -> None:

View File

@@ -33,14 +33,6 @@ def test_normalize_coerce_boolean(validator: Validator, mocker: MockerFixture) -
convert_mock.assert_called_once_with("1")
def test_normalize_coerce_float(validator: Validator) -> None:
"""
must convert string value to float by using configuration converters
"""
assert validator._normalize_coerce_float("1.5") == 1.5
assert validator._normalize_coerce_float("0.0") == 0.0
def test_normalize_coerce_integer(validator: Validator) -> None:
"""
must convert string value to integer by using configuration converters

View File

@@ -1,5 +1,6 @@
import pytest
import requests
import requests_unixsocket
from pytest_mock import MockerFixture
@@ -7,32 +8,31 @@ from ahriman.core.http import SyncAhrimanClient
from ahriman.models.user import User
def test_adapters(ahriman_client: SyncAhrimanClient) -> None:
def test_session(ahriman_client: SyncAhrimanClient, mocker: MockerFixture) -> None:
"""
must return native adapters
must create normal requests session
"""
assert "http+unix://" not in ahriman_client.adapters()
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_adapters_unix_socket(ahriman_client: SyncAhrimanClient) -> None:
def test_session_unix_socket(ahriman_client: SyncAhrimanClient, mocker: MockerFixture) -> None:
"""
must register unix socket adapter
must create unix socket session
"""
login_mock = mocker.patch("ahriman.core.http.SyncAhrimanClient._login")
ahriman_client.address = "http+unix://path"
assert "http+unix://" in ahriman_client.adapters()
assert isinstance(ahriman_client.session, requests_unixsocket.Session)
login_mock.assert_not_called()
def test_login_url(ahriman_client: SyncAhrimanClient) -> None:
def test_login(ahriman_client: SyncAhrimanClient, user: User, mocker: MockerFixture) -> None:
"""
must generate login url correctly
"""
assert ahriman_client._login_url().startswith(ahriman_client.address)
assert ahriman_client._login_url().endswith("/api/v1/login")
def test_start(ahriman_client: SyncAhrimanClient, user: User, mocker: MockerFixture) -> None:
"""
must log in user on start
must login user
"""
ahriman_client.auth = (user.username, user.password)
requests_mock = mocker.patch("ahriman.core.http.SyncAhrimanClient.make_request")
@@ -42,32 +42,40 @@ def test_start(ahriman_client: SyncAhrimanClient, user: User, mocker: MockerFixt
}
session = requests.Session()
ahriman_client.start(session)
ahriman_client._login(session)
requests_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True), json=payload, session=session)
def test_start_failed(ahriman_client: SyncAhrimanClient, user: User, mocker: MockerFixture) -> None:
def test_login_failed(ahriman_client: SyncAhrimanClient, user: User, mocker: MockerFixture) -> None:
"""
must suppress any exception happened during session start
must suppress any exception happened during login
"""
ahriman_client.user = user
mocker.patch("requests.Session.request", side_effect=Exception)
ahriman_client.start(requests.Session())
ahriman_client._login(requests.Session())
def test_start_failed_http_error(ahriman_client: SyncAhrimanClient, user: User, mocker: MockerFixture) -> None:
def test_login_failed_http_error(ahriman_client: SyncAhrimanClient, user: User, mocker: MockerFixture) -> None:
"""
must suppress HTTP exception happened during session start
must suppress HTTP exception happened during login
"""
ahriman_client.user = user
mocker.patch("requests.Session.request", side_effect=requests.HTTPError)
ahriman_client.start(requests.Session())
ahriman_client._login(requests.Session())
def test_start_skip(ahriman_client: SyncAhrimanClient, mocker: MockerFixture) -> None:
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.start(requests.Session())
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")

View File

@@ -33,15 +33,12 @@ def test_init_auth_empty() -> None:
assert SyncHttpClient().auth is None
def test_session(mocker: MockerFixture) -> None:
def test_session() -> None:
"""
must generate valid session
"""
start_mock = mocker.patch("ahriman.core.http.sync_http_client.SyncHttpClient.start")
session = SyncHttpClient().session
assert "User-Agent" in session.headers
start_mock.assert_called_once_with(pytest.helpers.anyvar(int))
def test_exception_response_text() -> None:
@@ -63,18 +60,6 @@ def test_exception_response_text_empty() -> None:
assert SyncHttpClient.exception_response_text(exception) == ""
def test_adapters() -> None:
"""
must create adapters with retry policy
"""
client = SyncHttpClient()
adapers = client.adapters()
assert "http://" in adapers
assert "https://" in adapers
assert all(adapter.max_retries == client.retry for adapter in adapers.values())
def test_make_request(mocker: MockerFixture) -> None:
"""
must make HTTP request
@@ -173,11 +158,3 @@ def test_make_request_session() -> None:
session_mock.request.assert_called_once_with(
"GET", "url", params=None, data=None, headers=None, files=None, json=None,
stream=None, auth=None, timeout=client.timeout)
def test_start() -> None:
"""
must do nothing on start
"""
client = SyncHttpClient()
client.start(client.session)

View File

@@ -1,5 +1,3 @@
import smtplib
import pytest
from pytest_mock import MockerFixture
@@ -8,7 +6,6 @@ from ahriman.core.configuration import Configuration
from ahriman.core.report.email import Email
from ahriman.models.package import Package
from ahriman.models.result import Result
from ahriman.models.smtp_ssl_settings import SmtpSSLSettings
def test_template(configuration: Configuration) -> None:
@@ -40,36 +37,17 @@ def test_template_full(configuration: Configuration) -> None:
assert Email(repository_id, configuration, "email").template_full == root.parent / template
def test_smtp_session(email: Email) -> None:
"""
must build normal SMTP session if SSL is disabled
"""
email.ssl = SmtpSSLSettings.Disabled
assert email._smtp_session == smtplib.SMTP
email.ssl = SmtpSSLSettings.STARTTLS
assert email._smtp_session == smtplib.SMTP
def test_smtp_session_ssl(email: Email) -> None:
"""
must build SMTP_SSL session if SSL is enabled
"""
email.ssl = SmtpSSLSettings.SSL
assert email._smtp_session == smtplib.SMTP_SSL
def test_send(email: Email, mocker: MockerFixture) -> None:
"""
must send an email with attachment
"""
smtp_mock = mocker.patch("smtplib.SMTP")
smtp_mock.return_value.__enter__.return_value = smtp_mock.return_value
email._send("a text", {"attachment.html": "an attachment"})
smtp_mock.return_value.starttls.assert_not_called()
smtp_mock.return_value.login.assert_not_called()
smtp_mock.return_value.sendmail.assert_called_once_with(email.sender, email.receivers, pytest.helpers.anyvar(int))
smtp_mock.return_value.quit.assert_called_once_with()
def test_send_auth(configuration: Configuration, mocker: MockerFixture) -> None:
@@ -79,7 +57,6 @@ def test_send_auth(configuration: Configuration, mocker: MockerFixture) -> None:
configuration.set_option("email", "user", "username")
configuration.set_option("email", "password", "password")
smtp_mock = mocker.patch("smtplib.SMTP")
smtp_mock.return_value.__enter__.return_value = smtp_mock.return_value
_, repository_id = configuration.check_loaded()
email = Email(repository_id, configuration, "email")
@@ -93,7 +70,6 @@ def test_send_auth_no_password(configuration: Configuration, mocker: MockerFixtu
"""
configuration.set_option("email", "user", "username")
smtp_mock = mocker.patch("smtplib.SMTP")
smtp_mock.return_value.__enter__.return_value = smtp_mock.return_value
_, repository_id = configuration.check_loaded()
email = Email(repository_id, configuration, "email")
@@ -107,7 +83,6 @@ def test_send_auth_no_user(configuration: Configuration, mocker: MockerFixture)
"""
configuration.set_option("email", "password", "password")
smtp_mock = mocker.patch("smtplib.SMTP")
smtp_mock.return_value.__enter__.return_value = smtp_mock.return_value
_, repository_id = configuration.check_loaded()
email = Email(repository_id, configuration, "email")
@@ -121,7 +96,6 @@ def test_send_ssl_tls(configuration: Configuration, mocker: MockerFixture) -> No
"""
configuration.set_option("email", "ssl", "ssl")
smtp_mock = mocker.patch("smtplib.SMTP_SSL")
smtp_mock.return_value.__enter__.return_value = smtp_mock.return_value
_, repository_id = configuration.check_loaded()
email = Email(repository_id, configuration, "email")
@@ -129,6 +103,7 @@ def test_send_ssl_tls(configuration: Configuration, mocker: MockerFixture) -> No
smtp_mock.return_value.starttls.assert_not_called()
smtp_mock.return_value.login.assert_not_called()
smtp_mock.return_value.sendmail.assert_called_once_with(email.sender, email.receivers, pytest.helpers.anyvar(int))
smtp_mock.return_value.quit.assert_called_once_with()
def test_send_starttls(configuration: Configuration, mocker: MockerFixture) -> None:
@@ -137,7 +112,6 @@ def test_send_starttls(configuration: Configuration, mocker: MockerFixture) -> N
"""
configuration.set_option("email", "ssl", "starttls")
smtp_mock = mocker.patch("smtplib.SMTP")
smtp_mock.return_value.__enter__.return_value = smtp_mock.return_value
_, repository_id = configuration.check_loaded()
email = Email(repository_id, configuration, "email")

View File

@@ -23,7 +23,7 @@ def test_security() -> None:
must generate security definitions for swagger
"""
token = next(iter(_security()))["token"]
assert token == {"type": "apiKey", "name": "AHRIMAN", "in": "cookie"}
assert token == {"type": "apiKey", "name": "API_SESSION", "in": "cookie"}
def test_servers(application: Application) -> None:

View File

@@ -6,4 +6,4 @@ def test_schema() -> None:
must return valid schema
"""
schema = AuthSchema()
assert not schema.validate({"AHRIMAN": "key"})
assert not schema.validate({"API_SESSION": "key"})

View File

@@ -27,7 +27,7 @@ def _client(client: TestClient, mocker: MockerFixture) -> TestClient:
"parameters": [
{
"in": "cookie",
"name": "AHRIMAN",
"name": "API_SESSION",
"schema": {
"type": "string",
},
@@ -39,7 +39,7 @@ def _client(client: TestClient, mocker: MockerFixture) -> TestClient:
"parameters": [
{
"in": "cookie",
"name": "AHRIMAN",
"name": "API_SESSION",
"schema": {
"type": "string",
},
@@ -60,7 +60,7 @@ def _client(client: TestClient, mocker: MockerFixture) -> TestClient:
{
"token": {
"type": "apiKey",
"name": "AHRIMAN",
"name": "API_SESSION",
"in": "cookie",
},
},