mplemet log storage at backend

This commit is contained in:
2022-11-20 03:51:42 +02:00
parent 8a6854c867
commit 12f6bb0aaf
69 changed files with 1134 additions and 235 deletions

View File

@ -51,18 +51,22 @@ def test_architectures_extract_specified(args: argparse.Namespace) -> None:
assert Handler.architectures_extract(args) == sorted(set(architectures))
def test_call(args: argparse.Namespace, mocker: MockerFixture) -> None:
def test_call(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must call inside lock
"""
args.configuration = Path("")
args.quiet = False
args.report = False
mocker.patch("ahriman.application.handlers.Handler.run")
mocker.patch("ahriman.core.configuration.Configuration.from_path")
configuration_mock = mocker.patch("ahriman.core.configuration.Configuration.from_path", return_value=configuration)
log_load_mock = mocker.patch("ahriman.core.log.Log.load")
enter_mock = mocker.patch("ahriman.application.lock.Lock.__enter__")
exit_mock = mocker.patch("ahriman.application.lock.Lock.__exit__")
assert Handler.call(args, "x86_64")
configuration_mock.assert_called_once_with(args.configuration, "x86_64")
log_load_mock.assert_called_once_with(configuration, quiet=args.quiet, report=args.report)
enter_mock.assert_called_once_with()
exit_mock.assert_called_once_with(None, None, None)

View File

@ -120,7 +120,7 @@ def test_imply_with_report(args: argparse.Namespace, configuration: Configuratio
load_mock = mocker.patch("ahriman.core.status.client.Client.load")
Status.run(args, "x86_64", configuration, report=False, unsafe=False)
load_mock.assert_called_once_with(configuration)
load_mock.assert_called_once_with(configuration, report=True)
def test_disallow_auto_architecture_run() -> None:

View File

@ -75,7 +75,7 @@ def test_imply_with_report(args: argparse.Namespace, configuration: Configuratio
load_mock = mocker.patch("ahriman.core.status.client.Client.load")
StatusUpdate.run(args, "x86_64", configuration, report=False, unsafe=False)
load_mock.assert_called_once_with(configuration)
load_mock.assert_called_once_with(configuration, report=True)
def test_disallow_auto_architecture_run() -> None:

View File

@ -215,7 +215,7 @@ def configuration(resource_path_root: Path) -> Configuration:
Configuration: configuration test instance
"""
path = resource_path_root / "core" / "ahriman.ini"
return Configuration.from_path(path=path, architecture="x86_64", quiet=False)
return Configuration.from_path(path=path, architecture="x86_64")
@pytest.fixture

View File

@ -1,3 +1,4 @@
import logging
import pytest
from ahriman.core.alpm.repo import Repo
@ -36,6 +37,17 @@ def leaf_python_schedule(package_python_schedule: Package) -> Leaf:
return Leaf(package_python_schedule, set())
@pytest.fixture
def log_record() -> logging.LogRecord:
"""
fixture for log record object
Returns:
logging.LogRecord: log record test instance
"""
return logging.LogRecord("record", logging.INFO, "path", 42, "log message", args=(), exc_info=None)
@pytest.fixture
def repo(configuration: Configuration, repository_paths: RepositoryPaths) -> Repo:
"""

View File

@ -1,7 +1,7 @@
from ahriman.core.database.migrations.m002_user_access import steps
def test_migration_package_source() -> None:
def test_migration_user_access() -> None:
"""
migration must not be empty
"""

View File

@ -1,7 +1,7 @@
from ahriman.core.database.migrations.m003_patch_variables import steps
def test_migration_package_source() -> None:
def test_migration_patches() -> None:
"""
migration must not be empty
"""

View File

@ -0,0 +1,8 @@
from ahriman.core.database.migrations.m004_logs import steps
def test_migration_logs() -> None:
"""
migration must not be empty
"""
assert steps

View File

@ -35,7 +35,7 @@ def test_build_queue_insert_get(database: SQLite, package_ahriman: Package) -> N
def test_build_queue_insert(database: SQLite, package_ahriman: Package) -> None:
"""
must update user in the database
must update build queue in the database
"""
database.build_queue_insert(package_ahriman)
assert database.build_queue_get() == [package_ahriman]

View File

@ -0,0 +1,23 @@
from ahriman.core.database import SQLite
from ahriman.models.package import Package
def test_logs_insert_delete(database: SQLite, package_ahriman: Package, package_python_schedule: Package) -> None:
"""
must clear all packages
"""
database.logs_insert(package_ahriman.base, 0.001, "message 1")
database.logs_insert(package_python_schedule.base, 0.002, "message 2")
database.logs_delete(package_ahriman.base)
assert not database.logs_get(package_ahriman.base)
assert database.logs_get(package_python_schedule.base)
def test_logs_insert_get(database: SQLite, package_ahriman: Package) -> None:
"""
must insert and get package logs
"""
database.logs_insert(package_ahriman.base, 0.002, "message 2")
database.logs_insert(package_ahriman.base, 0.001, "message 1")
assert database.logs_get(package_ahriman.base) == "message 1\nmessage 2"

View File

@ -0,0 +1,56 @@
import logging
from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration
from ahriman.core.log.http_log_handler import HttpLogHandler
def test_load(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must load handler
"""
# because of test cases we need to reset handler list
root = logging.getLogger()
current_handler = next((handler for handler in root.handlers if isinstance(handler, HttpLogHandler)), None)
root.removeHandler(current_handler)
add_mock = mocker.patch("logging.Logger.addHandler")
load_mock = mocker.patch("ahriman.core.status.client.Client.load")
handler = HttpLogHandler.load(configuration, report=False)
assert handler
add_mock.assert_called_once_with(handler)
load_mock.assert_called_once_with(configuration, report=False)
def test_load_exist(configuration: Configuration) -> None:
"""
must not load handler if already set
"""
handler = HttpLogHandler.load(configuration, report=False)
new_handler = HttpLogHandler.load(configuration, report=False)
assert handler is new_handler
def test_emit(configuration: Configuration, log_record: logging.LogRecord, mocker: MockerFixture) -> None:
"""
must emit log record to reporter
"""
log_mock = mocker.patch("ahriman.core.status.client.Client.logs")
handler = HttpLogHandler(configuration, report=False)
handler.emit(log_record)
log_mock.assert_called_once_with(log_record)
def test_emit_failed(configuration: Configuration, log_record: logging.LogRecord, mocker: MockerFixture) -> None:
"""
must call handle error on exception
"""
mocker.patch("ahriman.core.status.client.Client.logs", side_effect=Exception())
handle_error_mock = mocker.patch("logging.Handler.handleError")
handler = HttpLogHandler(configuration, report=False)
handler.emit(log_record)
handle_error_mock.assert_called_once_with(log_record)

View File

@ -1,3 +1,4 @@
import logging
import pytest
from ahriman.core.alpm.repo import Repo
@ -26,3 +27,19 @@ def test_logger_name(database: SQLite, repo: Repo) -> None:
"""
assert database.logger_name == "ahriman.core.database.sqlite.SQLite"
assert repo.logger_name == "ahriman.core.alpm.repo.Repo"
def test_package_logger_set_reset(database: SQLite) -> None:
"""
must set and reset package base attribute
"""
package_base = "package base"
database.package_logger_set(package_base)
record = logging.makeLogRecord({})
assert record.package_base == package_base
database.package_logger_reset()
record = logging.makeLogRecord({})
with pytest.raises(AttributeError):
record.package_base

View File

@ -0,0 +1,35 @@
import logging
from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration
from ahriman.core.log import Log
def test_load(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must load logging
"""
logging_mock = mocker.patch("ahriman.core.log.log.fileConfig")
http_log_mock = mocker.patch("ahriman.core.log.http_log_handler.HttpLogHandler.load")
Log.load(configuration, quiet=False, report=False)
logging_mock.assert_called_once_with(configuration.logging_path)
http_log_mock.assert_called_once_with(configuration, report=False)
def test_load_fallback(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must fallback to stderr without errors
"""
mocker.patch("ahriman.core.log.log.fileConfig", side_effect=PermissionError())
Log.load(configuration, quiet=False, report=False)
def test_load_quiet(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must disable logging in case if quiet flag set
"""
disable_mock = mocker.patch("logging.disable")
Log.load(configuration, quiet=True, report=False)
disable_mock.assert_called_once_with(logging.WARNING)

View File

@ -4,7 +4,6 @@ from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.exceptions import UnsafeRunError
from ahriman.core.repository.repository_properties import RepositoryProperties
from ahriman.core.status.web_client import WebClient
def test_create_tree_on_load(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> None:
@ -27,26 +26,3 @@ def test_create_tree_on_load_unsafe(configuration: Configuration, database: SQLi
RepositoryProperties("x86_64", configuration, database, report=False, unsafe=False)
tree_create_mock.assert_not_called()
def test_create_dummy_report_client(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> None:
"""
must create dummy report client if report is disabled
"""
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
load_mock = mocker.patch("ahriman.core.status.client.Client.load")
properties = RepositoryProperties("x86_64", configuration, database, report=False, unsafe=False)
load_mock.assert_not_called()
assert not isinstance(properties.reporter, WebClient)
def test_create_full_report_client(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> None:
"""
must create load report client if report is enabled
"""
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
load_mock = mocker.patch("ahriman.core.status.client.Client.load")
RepositoryProperties("x86_64", configuration, database, report=True, unsafe=True)
load_mock.assert_called_once_with(configuration)

View File

@ -1,3 +1,5 @@
import logging
from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration
@ -12,7 +14,16 @@ def test_load_dummy_client(configuration: Configuration) -> None:
"""
must load dummy client if no settings set
"""
assert isinstance(Client.load(configuration), Client)
assert not isinstance(Client.load(configuration, report=True), WebClient)
def test_load_dummy_client_disabled(configuration: Configuration) -> None:
"""
must load dummy client if report is set to False
"""
configuration.set_option("web", "host", "localhost")
configuration.set_option("web", "port", "8080")
assert not isinstance(Client.load(configuration, report=False), WebClient)
def test_load_full_client(configuration: Configuration) -> None:
@ -21,7 +32,7 @@ def test_load_full_client(configuration: Configuration) -> None:
"""
configuration.set_option("web", "host", "localhost")
configuration.set_option("web", "port", "8080")
assert isinstance(Client.load(configuration), WebClient)
assert isinstance(Client.load(configuration, report=True), WebClient)
def test_load_full_client_from_address(configuration: Configuration) -> None:
@ -29,7 +40,7 @@ def test_load_full_client_from_address(configuration: Configuration) -> None:
must load full client by using address
"""
configuration.set_option("web", "address", "http://localhost:8080")
assert isinstance(Client.load(configuration), WebClient)
assert isinstance(Client.load(configuration, report=True), WebClient)
def test_add(client: Client, package_ahriman: Package) -> None:
@ -57,6 +68,13 @@ def test_get_internal(client: Client) -> None:
assert actual == expected
def test_log(client: Client, log_record: logging.LogRecord) -> None:
"""
must process log record without errors
"""
client.logs(log_record)
def test_remove(client: Client, package_ahriman: Package) -> None:
"""
must process remove without errors

View File

@ -8,6 +8,7 @@ from ahriman.core.exceptions import UnknownPackageError
from ahriman.core.status.watcher import Watcher
from ahriman.core.status.web_client import WebClient
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.log_record_id import LogRecordId
from ahriman.models.package import Package
@ -18,10 +19,7 @@ def test_force_no_report(configuration: Configuration, database: SQLite, mocker:
configuration.set_option("web", "port", "8080")
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
load_mock = mocker.patch("ahriman.core.status.client.Client.load")
watcher = Watcher("x86_64", configuration, database)
load_mock.assert_not_called()
assert not isinstance(watcher.repository.reporter, WebClient)
@ -43,6 +41,15 @@ def test_get_failed(watcher: Watcher, package_ahriman: Package) -> None:
watcher.get(package_ahriman.base)
def test_get_logs(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must return package logs
"""
logs_mock = mocker.patch("ahriman.core.database.SQLite.logs_get")
watcher.get_logs(package_ahriman.base)
logs_mock.assert_called_once_with(package_ahriman.base)
def test_load(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must correctly load packages
@ -83,6 +90,15 @@ def test_remove(watcher: Watcher, package_ahriman: Package, mocker: MockerFixtur
cache_mock.assert_called_once_with(package_ahriman.base)
def test_remove_logs(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must remove package logs
"""
logs_mock = mocker.patch("ahriman.core.database.SQLite.logs_delete")
watcher.remove_logs(package_ahriman.base)
logs_mock.assert_called_once_with(package_ahriman.base)
def test_remove_unknown(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must not fail on unknown base removal
@ -128,6 +144,38 @@ def test_update_unknown(watcher: Watcher, package_ahriman: Package) -> None:
watcher.update(package_ahriman.base, BuildStatusEnum.Unknown, None)
def test_update_logs_new(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must create package logs record for new package
"""
delete_mock = mocker.patch("ahriman.core.database.SQLite.logs_delete")
insert_mock = mocker.patch("ahriman.core.database.SQLite.logs_insert")
log_record_id = LogRecordId(package_ahriman.base, watcher._last_log_record_id.process_id)
assert watcher._last_log_record_id != log_record_id
watcher.update_logs(log_record_id, 42.01, "log record")
delete_mock.assert_called_once_with(package_ahriman.base)
insert_mock.assert_called_once_with(package_ahriman.base, 42.01, "log record")
assert watcher._last_log_record_id == log_record_id
def test_update_logs_update(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must create package logs record for current package
"""
delete_mock = mocker.patch("ahriman.core.database.SQLite.logs_delete")
insert_mock = mocker.patch("ahriman.core.database.SQLite.logs_insert")
log_record_id = LogRecordId(package_ahriman.base, watcher._last_log_record_id.process_id)
watcher._last_log_record_id = log_record_id
watcher.update_logs(log_record_id, 42.01, "log record")
delete_mock.assert_not_called()
insert_mock.assert_called_once_with(package_ahriman.base, 42.01, "log record")
def test_update_self(watcher: Watcher) -> None:
"""
must update service status

View File

@ -1,4 +1,5 @@
import json
import logging
import pytest
import requests
@ -13,6 +14,14 @@ from ahriman.models.package import Package
from ahriman.models.user import User
def test_status_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
@ -75,9 +84,17 @@ def test_login_skip(web_client: WebClient, mocker: MockerFixture) -> None:
requests_mock.assert_not_called()
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 correctly
must generate package status url correctly
"""
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}")
@ -192,6 +209,43 @@ def test_get_internal_failed_http_error(web_client: WebClient, mocker: MockerFix
assert web_client.get_internal().architecture is None
def test_logs(web_client: WebClient, log_record: logging.LogRecord, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must process log record
"""
requests_mock = mocker.patch("requests.Session.post")
log_record.package_base = package_ahriman.base
payload = {
"created": log_record.created,
"message": log_record.getMessage(),
"process_id": log_record.process,
}
web_client.logs(log_record)
requests_mock.assert_called_once_with(pytest.helpers.anyvar(str, True), json=payload)
def test_log_failed(web_client: WebClient, log_record: logging.LogRecord, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must pass exception during log post
"""
mocker.patch("requests.Session.post", side_effect=Exception())
log_record.package_base = package_ahriman.base
with pytest.raises(Exception):
web_client.logs(log_record)
def test_log_skip(web_client: WebClient, log_record: logging.LogRecord, mocker: MockerFixture) -> None:
"""
must skip log record posting if no package base set
"""
requests_mock = mocker.patch("requests.Session.post")
web_client.logs(log_record)
requests_mock.assert_not_called()
def test_remove(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must process package removal

View File

@ -1,5 +1,4 @@
import configparser
import logging
import pytest
from pathlib import Path
@ -25,14 +24,12 @@ def test_from_path(mocker: MockerFixture) -> None:
mocker.patch("pathlib.Path.is_file", return_value=True)
read_mock = mocker.patch("ahriman.core.configuration.Configuration.read")
load_includes_mock = mocker.patch("ahriman.core.configuration.Configuration.load_includes")
load_logging_mock = mocker.patch("ahriman.core.configuration.Configuration.load_logging")
path = Path("path")
configuration = Configuration.from_path(path, "x86_64", True)
configuration = Configuration.from_path(path, "x86_64")
assert configuration.path == path
read_mock.assert_called_once_with(path)
load_includes_mock.assert_called_once_with()
load_logging_mock.assert_called_once_with(True)
def test_from_path_file_missing(mocker: MockerFixture) -> None:
@ -41,10 +38,9 @@ def test_from_path_file_missing(mocker: MockerFixture) -> None:
"""
mocker.patch("pathlib.Path.is_file", return_value=False)
mocker.patch("ahriman.core.configuration.Configuration.load_includes")
mocker.patch("ahriman.core.configuration.Configuration.load_logging")
read_mock = mocker.patch("ahriman.core.configuration.Configuration.read")
configuration = Configuration.from_path(Path("path"), "x86_64", True)
configuration = Configuration.from_path(Path("path"), "x86_64")
read_mock.assert_called_once_with(configuration.SYSTEM_CONFIGURATION_PATH)
@ -263,23 +259,6 @@ def test_load_includes_no_section(configuration: Configuration) -> None:
configuration.load_includes()
def test_load_logging_fallback(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must fallback to stderr without errors
"""
mocker.patch("ahriman.core.configuration.fileConfig", side_effect=PermissionError())
configuration.load_logging(quiet=False)
def test_load_logging_quiet(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must disable logging in case if quiet flag set
"""
disable_mock = mocker.patch("logging.disable")
configuration.load_logging(quiet=True)
disable_mock.assert_called_once_with(logging.WARNING)
def test_merge_sections_missing(configuration: Configuration) -> None:
"""
must merge create section if not exists

View File

@ -0,0 +1,75 @@
import pytest
from aiohttp.test_utils import TestClient
from ahriman.models.package import Package
from ahriman.models.user_access import UserAccess
from ahriman.web.views.status.logs import LogsView
async def test_get_permission() -> None:
"""
must return correct permission for the request
"""
for method in ("GET", "HEAD"):
request = pytest.helpers.request("", "", method)
assert await LogsView.get_permission(request) == UserAccess.Read
for method in ("DELETE", "POST"):
request = pytest.helpers.request("", "", method)
assert await LogsView.get_permission(request) == UserAccess.Full
async def test_delete(client: TestClient, package_ahriman: Package, package_python_schedule: Package) -> None:
"""
must delete logs for package
"""
await client.post(f"/api/v1/packages/{package_ahriman.base}/logs",
json={"created": 0.001, "message": "message", "process_id": 42})
await client.post(f"/api/v1/packages/{package_python_schedule.base}/logs",
json={"created": 0.001, "message": "message", "process_id": 42})
response = await client.delete(f"/api/v1/packages/{package_ahriman.base}/logs")
assert response.status == 204
response = await client.get(f"/api/v1/packages/{package_ahriman.base}/logs")
logs = await response.json()
assert not logs["logs"]
response = await client.get(f"/api/v1/packages/{package_python_schedule.base}/logs")
logs = await response.json()
assert logs["logs"]
async def test_get(client: TestClient, package_ahriman: Package) -> None:
"""
must get logs for package
"""
await client.post(f"/api/v1/packages/{package_ahriman.base}/logs",
json={"created": 0.001, "message": "message", "process_id": 42})
response = await client.get(f"/api/v1/packages/{package_ahriman.base}/logs")
assert response.status == 200
logs = await response.json()
assert logs == {"package_base": package_ahriman.base, "logs": "message"}
async def test_post(client: TestClient, package_ahriman: Package) -> None:
"""
must create logs record
"""
post_response = await client.post(f"/api/v1/packages/{package_ahriman.base}/logs",
json={"created": 0.001, "message": "message", "process_id": 42})
assert post_response.status == 204
response = await client.get(f"/api/v1/packages/{package_ahriman.base}/logs")
logs = await response.json()
assert logs == {"package_base": package_ahriman.base, "logs": "message"}
async def test_post_exception(client: TestClient, package_ahriman: Package) -> None:
"""
must raise exception on invalid payload
"""
post_response = await client.post(f"/api/v1/packages/{package_ahriman.base}/logs", json={})
assert post_response.status == 400

View File

@ -20,31 +20,6 @@ async def test_get_permission() -> None:
assert await PackageView.get_permission(request) == UserAccess.Full
async def test_get(client: TestClient, package_ahriman: Package, package_python_schedule: Package) -> None:
"""
must return status for specific package
"""
await client.post(f"/api/v1/packages/{package_ahriman.base}",
json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()})
await client.post(f"/api/v1/packages/{package_python_schedule.base}",
json={"status": BuildStatusEnum.Success.value, "package": package_python_schedule.view()})
response = await client.get(f"/api/v1/packages/{package_ahriman.base}")
assert response.ok
packages = [Package.from_json(item["package"]) for item in await response.json()]
assert packages
assert {package.base for package in packages} == {package_ahriman.base}
async def test_get_not_found(client: TestClient, package_ahriman: Package) -> None:
"""
must return Not Found for unknown package
"""
response = await client.get(f"/api/v1/packages/{package_ahriman.base}")
assert response.status == 404
async def test_delete(client: TestClient, package_ahriman: Package, package_python_schedule: Package) -> None:
"""
must delete single base
@ -81,6 +56,31 @@ async def test_delete_unknown(client: TestClient, package_ahriman: Package, pack
assert response.ok
async def test_get(client: TestClient, package_ahriman: Package, package_python_schedule: Package) -> None:
"""
must return status for specific package
"""
await client.post(f"/api/v1/packages/{package_ahriman.base}",
json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()})
await client.post(f"/api/v1/packages/{package_python_schedule.base}",
json={"status": BuildStatusEnum.Success.value, "package": package_python_schedule.view()})
response = await client.get(f"/api/v1/packages/{package_ahriman.base}")
assert response.ok
packages = [Package.from_json(item["package"]) for item in await response.json()]
assert packages
assert {package.base for package in packages} == {package_ahriman.base}
async def test_get_not_found(client: TestClient, package_ahriman: Package) -> None:
"""
must return Not Found for unknown package
"""
response = await client.get(f"/api/v1/packages/{package_ahriman.base}")
assert response.status == 404
async def test_post(client: TestClient, package_ahriman: Package) -> None:
"""
must update package status