erase logs based on current package version

Old implementation has used process id instead, but it leads to log
removal in case of remote process trigger
This commit is contained in:
Evgenii Alekseev 2023-08-18 14:56:49 +03:00
parent 50775c3f0a
commit 479f0db572
23 changed files with 156 additions and 86 deletions

View File

@ -0,0 +1,36 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
__all__ = ["steps"]
steps = [
"""
drop index logs_package_base_process_id
""",
"""
alter table logs drop column process_id
""",
"""
alter table logs add column version text not null default ''
""",
"""
create index logs_package_base_version on logs (package_base, version)
""",
]

View File

@ -66,13 +66,13 @@ class LogsOperations(Operations):
connection.execute(
"""
insert into logs
(package_base, process_id, created, record)
(package_base, created, version, record)
values
(:package_base, :process_id, :created, :record)
(:package_base, :created, :version, :record)
""",
{
"package_base": log_record_id.package_base,
"process_id": log_record_id.process_id,
"version": log_record_id.version,
"created": created,
"record": record,
}
@ -80,22 +80,22 @@ class LogsOperations(Operations):
return self.with_connection(run, commit=True)
def logs_remove(self, package_base: str, current_process_id: int | None) -> None:
def logs_remove(self, package_base: str, version: str | None) -> None:
"""
remove log records for the specified package
Args:
package_base(str): package base to remove logs
current_process_id(int | None): current process id. If set it will remove only logs belonging to another
process
version(str): package version. If set it will remove only logs belonging to another
version
"""
def run(connection: Connection) -> None:
connection.execute(
"""
delete from logs
where package_base = :package_base and (:process_id is null or process_id <> :process_id)
where package_base = :package_base and (:version is null or version <> :version)
""",
{"package_base": package_base, "process_id": current_process_id}
{"package_base": package_base, "version": version}
)
return self.with_connection(run, commit=True)

View File

@ -77,8 +77,8 @@ class HttpLogHandler(logging.Handler):
Args:
record(logging.LogRecord): log record to log
"""
package_base = getattr(record, "package_base", None)
if package_base is None:
log_record_id = getattr(record, "package_id", None)
if log_record_id is None:
return # in case if no package base supplied we need just skip log message
self.reporter.package_logs(package_base, record)
self.reporter.package_logs(log_record_id, record)

View File

@ -24,6 +24,8 @@ from collections.abc import Generator
from functools import cached_property
from typing import Any
from ahriman.models.log_record_id import LogRecordId
class LazyLogging:
"""
@ -60,38 +62,40 @@ class LazyLogging:
logging.setLogRecordFactory(logging.LogRecord)
@staticmethod
def _package_logger_set(package_base: str) -> None:
def _package_logger_set(package_base: str, version: str | None) -> None:
"""
set package base as extra info to the logger
Args:
package_base(str): package base
version(str | None): package version if available
"""
current_factory = logging.getLogRecordFactory()
def package_record_factory(*args: Any, **kwargs: Any) -> logging.LogRecord:
record = current_factory(*args, **kwargs)
record.package_base = package_base
record.package_id = LogRecordId(package_base, version or "")
return record
logging.setLogRecordFactory(package_record_factory)
@contextlib.contextmanager
def in_package_context(self, package_base: str) -> Generator[None, None, None]:
def in_package_context(self, package_base: str, version: str | None) -> Generator[None, None, None]:
"""
execute function while setting package context
Args:
package_base(str): package base to set context in
version(str | None): package version if available
Examples:
This function is designed to be called as context manager with ``package_base`` argument, e.g.:
>>> with self.in_package_context(package.base):
>>> with self.in_package_context(package.base, package.version):
>>> build_package(package)
"""
try:
self._package_logger_set(package_base)
self._package_logger_set(package_base, version)
yield
finally:
self._package_logger_reset()

View File

@ -93,7 +93,8 @@ class Executor(Cleaner):
result = Result()
for single in updates:
with self.in_package_context(single.base), TemporaryDirectory(ignore_cleanup_errors=True) as dir_name:
with self.in_package_context(single.base, local_versions.get(single.base)), \
TemporaryDirectory(ignore_cleanup_errors=True) as dir_name:
try:
packager = self.packager(packagers, single.base)
build_single(single, Path(dir_name), packager.packager_id)
@ -201,14 +202,16 @@ class Executor(Cleaner):
package_path = self.paths.repository / safe_filename(name)
self.repo.add(package_path)
current_packages = self.packages()
current_packages = {package.base: package for package in self.packages()}
local_versions = {package_base: package.version for package_base, package in current_packages.items()}
removed_packages: list[str] = [] # list of packages which have been removed from the base
updates = self.load_archives(packages)
packagers = packagers or Packagers()
result = Result()
for local in updates:
with self.in_package_context(local.base):
with self.in_package_context(local.base, local_versions.get(local.base)):
try:
packager = self.packager(packagers, local.base)
@ -218,12 +221,9 @@ class Executor(Cleaner):
self.reporter.set_success(local)
result.add_success(local)
current_package_archives = {
package
for current in current_packages
if current.base == local.base
for package in current.packages
}
current_package_archives: set[str] = set()
if local.base in current_packages:
current_package_archives = set(current_packages[local.base].packages.keys())
removed_packages.extend(current_package_archives.difference(local.packages))
except Exception:
self.reporter.set_failed(local.base)

View File

@ -66,10 +66,11 @@ class UpdateHandler(Cleaner):
continue
raise UnknownPackageError(package.base)
result: list[Package] = []
local_versions = {package.base: package.version for package in self.packages()}
result: list[Package] = []
for local in self.packages():
with self.in_package_context(local.base):
with self.in_package_context(local.base, local_versions.get(local.base)):
if not local.remote.is_remote:
continue # avoid checking local packages
if local.base in self.ignore_list:
@ -102,11 +103,12 @@ class UpdateHandler(Cleaner):
Returns:
list[Package]: list of local packages which are out-of-dated
"""
result: list[Package] = []
packages = {local.base: local for local in self.packages()}
local_versions = {package_base: package.version for package_base, package in packages.items()}
result: list[Package] = []
for cache_dir in self.paths.cache.iterdir():
with self.in_package_context(cache_dir.name):
with self.in_package_context(cache_dir.name, local_versions.get(cache_dir.name)):
try:
source = RemoteSource(
source=PackageSource.Local,

View File

@ -24,6 +24,7 @@ import logging
from ahriman.core.configuration import Configuration
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
@ -82,12 +83,12 @@ class Client:
del package_base
return []
def package_logs(self, package_base: str, record: logging.LogRecord) -> None:
def package_logs(self, log_record_id: LogRecordId, record: logging.LogRecord) -> None:
"""
post log record
Args:
package_base(str) package base
log_record_id(LogRecordId): log record id
record(logging.LogRecord): log record to post to api
"""

View File

@ -17,8 +17,6 @@
# 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 os
from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.exceptions import UnknownPackageError
@ -59,7 +57,7 @@ class Watcher(LazyLogging):
self.status = BuildStatus()
# special variables for updating logs
self._last_log_record_id = LogRecordId("", os.getpid())
self._last_log_record_id = LogRecordId("", "")
@property
def packages(self) -> list[tuple[Package, BuildStatus]]:
@ -99,15 +97,15 @@ class Watcher(LazyLogging):
"""
return self.database.logs_get(package_base)
def logs_remove(self, package_base: str, current_process_id: int | None) -> None:
def logs_remove(self, package_base: str, version: str | None) -> None:
"""
remove package related logs
Args:
package_base(str): package base
current_process_id(int | None): current process id
version(str): package versio
"""
self.database.logs_remove(package_base, current_process_id)
self.database.logs_remove(package_base, version)
def logs_update(self, log_record_id: LogRecordId, created: float, record: str) -> None:
"""
@ -120,7 +118,7 @@ class Watcher(LazyLogging):
"""
if self._last_log_record_id != log_record_id:
# there is new log record, so we remove old ones
self.logs_remove(log_record_id.package_base, log_record_id.process_id)
self.logs_remove(log_record_id.package_base, log_record_id.version)
self._last_log_record_id = log_record_id
self.database.logs_insert(log_record_id, created, record)

View File

@ -33,6 +33,7 @@ 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.internal_status import InternalStatus
from ahriman.models.log_record_id import LogRecordId
from ahriman.models.package import Package
from ahriman.models.user import User
@ -253,20 +254,20 @@ class WebClient(Client, LazyLogging):
for package in response_json
]
def package_logs(self, package_base: str, record: logging.LogRecord) -> None:
def package_logs(self, log_record_id: LogRecordId, record: logging.LogRecord) -> None:
"""
post log record
Args:
package_base(str) package base
log_record_id(LogRecordId): log record id
record(logging.LogRecord): log record to post to api
"""
payload = {
"created": record.created,
"message": record.getMessage(),
"process_id": record.process,
"version": log_record_id.version,
}
self.make_request("POST", self._logs_url(package_base), json=payload)
self.make_request("POST", self._logs_url(log_record_id.package_base), json=payload)
def package_remove(self, package_base: str) -> None:
"""

View File

@ -27,8 +27,8 @@ class LogRecordId:
Attributes:
package_base(str): package base for which log record belongs
process_id(int): process id from which log record was emitted
version(str): package version for which log record belongs
"""
package_base: str
process_id: int
version: str

View File

@ -19,6 +19,8 @@
#
from marshmallow import Schema, fields
from ahriman import __version__
class LogSchema(Schema):
"""
@ -29,9 +31,9 @@ class LogSchema(Schema):
"description": "Log record timestamp",
"example": 1680537091.233495,
})
process_id = fields.Integer(required=True, metadata={
"description": "Current process id",
"example": 42,
version = fields.Integer(required=True, metadata={
"description": "Package version to tag",
"example": __version__,
})
message = fields.String(required=True, metadata={
"description": "Log message",

View File

@ -19,11 +19,11 @@
#
import aiohttp_apispec # type: ignore[import]
import shutil
import tempfile
from aiohttp import BodyPartReader
from aiohttp.web import HTTPBadRequest, HTTPCreated, HTTPNotFound
from pathlib import Path
from tempfile import NamedTemporaryFile
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas import AuthSchema, ErrorSchema, FileSchema
@ -68,7 +68,7 @@ class UploadView(BaseView):
# in order to handle errors automatically we create temporary file for long operation (transfer)
# and then copy it to valid location
with tempfile.NamedTemporaryFile() as cache:
with NamedTemporaryFile() as cache:
while True:
chunk = await part.read_chunk()
if not chunk:

View File

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

View File

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

View File

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

View File

@ -8,11 +8,11 @@ def test_logs_insert_remove_process(database: SQLite, package_ahriman: Package,
"""
must clear process specific package logs
"""
database.logs_insert(LogRecordId(package_ahriman.base, 1), 42.0, "message 1")
database.logs_insert(LogRecordId(package_ahriman.base, 2), 43.0, "message 2")
database.logs_insert(LogRecordId(package_python_schedule.base, 1), 42.0, "message 3")
database.logs_insert(LogRecordId(package_ahriman.base, "1"), 42.0, "message 1")
database.logs_insert(LogRecordId(package_ahriman.base, "2"), 43.0, "message 2")
database.logs_insert(LogRecordId(package_python_schedule.base, "1"), 42.0, "message 3")
database.logs_remove(package_ahriman.base, 1)
database.logs_remove(package_ahriman.base, "1")
assert database.logs_get(package_ahriman.base) == "[1970-01-01 00:00:42] message 1"
assert database.logs_get(package_python_schedule.base) == "[1970-01-01 00:00:42] message 3"
@ -21,9 +21,9 @@ def test_logs_insert_remove_full(database: SQLite, package_ahriman: Package, pac
"""
must clear full package logs
"""
database.logs_insert(LogRecordId(package_ahriman.base, 1), 42.0, "message 1")
database.logs_insert(LogRecordId(package_ahriman.base, 2), 43.0, "message 2")
database.logs_insert(LogRecordId(package_python_schedule.base, 1), 42.0, "message 3")
database.logs_insert(LogRecordId(package_ahriman.base, "1"), 42.0, "message 1")
database.logs_insert(LogRecordId(package_ahriman.base, "2"), 43.0, "message 2")
database.logs_insert(LogRecordId(package_python_schedule.base, "1"), 42.0, "message 3")
database.logs_remove(package_ahriman.base, None)
assert not database.logs_get(package_ahriman.base)
@ -34,6 +34,6 @@ def test_logs_insert_get(database: SQLite, package_ahriman: Package) -> None:
"""
must insert and get package logs
"""
database.logs_insert(LogRecordId(package_ahriman.base, 1), 43.0, "message 2")
database.logs_insert(LogRecordId(package_ahriman.base, 1), 42.0, "message 1")
database.logs_insert(LogRecordId(package_ahriman.base, "1"), 43.0, "message 2")
database.logs_insert(LogRecordId(package_ahriman.base, "1"), 42.0, "message 1")
assert database.logs_get(package_ahriman.base) == "[1970-01-01 00:00:42] message 1\n[1970-01-01 00:00:43] message 2"

View File

@ -4,6 +4,7 @@ from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration
from ahriman.core.log.http_log_handler import HttpLogHandler
from ahriman.models.log_record_id import LogRecordId
from ahriman.models.package import Package
@ -39,13 +40,13 @@ def test_emit(configuration: Configuration, log_record: logging.LogRecord, packa
"""
must emit log record to reporter
"""
log_record.package_base = package_ahriman.base
log_record_id = log_record.package_id = LogRecordId(package_ahriman.base, package_ahriman.version)
log_mock = mocker.patch("ahriman.core.status.client.Client.package_logs")
handler = HttpLogHandler(configuration, report=False)
handler.emit(log_record)
log_mock.assert_called_once_with(package_ahriman.base, log_record)
log_mock.assert_called_once_with(log_record_id, log_record)
def test_emit_skip(configuration: Configuration, log_record: logging.LogRecord, mocker: MockerFixture) -> None:

View File

@ -5,6 +5,7 @@ from pytest_mock import MockerFixture
from ahriman.core.alpm.repo import Repo
from ahriman.core.database import SQLite
from ahriman.models.log_record_id import LogRecordId
from ahriman.models.package import Package
@ -20,16 +21,16 @@ def test_package_logger_set_reset(database: SQLite) -> None:
"""
must set and reset package base attribute
"""
package_base = "package base"
log_record_id = LogRecordId("base", "version")
database._package_logger_set(package_base)
database._package_logger_set(log_record_id.package_base, log_record_id.version)
record = logging.makeLogRecord({})
assert record.package_base == package_base
assert record.package_id == log_record_id
database._package_logger_reset()
record = logging.makeLogRecord({})
with pytest.raises(AttributeError):
record.package_base
record.package_id
def test_in_package_context(database: SQLite, package_ahriman: Package, mocker: MockerFixture) -> None:
@ -39,10 +40,24 @@ def test_in_package_context(database: SQLite, package_ahriman: Package, mocker:
set_mock = mocker.patch("ahriman.core.log.LazyLogging._package_logger_set")
reset_mock = mocker.patch("ahriman.core.log.LazyLogging._package_logger_reset")
with database.in_package_context(package_ahriman.base):
with database.in_package_context(package_ahriman.base, package_ahriman.version):
pass
set_mock.assert_called_once_with(package_ahriman.base)
set_mock.assert_called_once_with(package_ahriman.base, package_ahriman.version)
reset_mock.assert_called_once_with()
def test_in_package_context_empty_version(database: SQLite, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must set package log context
"""
set_mock = mocker.patch("ahriman.core.log.LazyLogging._package_logger_set")
reset_mock = mocker.patch("ahriman.core.log.LazyLogging._package_logger_reset")
with database.in_package_context(package_ahriman.base, None):
pass
set_mock.assert_called_once_with(package_ahriman.base, None)
reset_mock.assert_called_once_with()
@ -54,7 +69,7 @@ def test_in_package_context_failed(database: SQLite, package_ahriman: Package, m
reset_mock = mocker.patch("ahriman.core.log.LazyLogging._package_logger_reset")
with pytest.raises(Exception):
with database.in_package_context(package_ahriman.base):
with database.in_package_context(package_ahriman.base, ""):
raise Exception()
reset_mock.assert_called_once_with()

View File

@ -7,6 +7,7 @@ from ahriman.core.status.client import Client
from ahriman.core.status.web_client import WebClient
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
@ -66,11 +67,11 @@ def test_package_get(client: Client, package_ahriman: Package) -> None:
assert client.package_get(None) == []
def test_package_log(client: Client, package_ahriman: Package, log_record: logging.LogRecord) -> None:
def test_package_logs(client: Client, package_ahriman: Package, log_record: logging.LogRecord) -> None:
"""
must process log record without errors
"""
client.package_logs(package_ahriman.base, log_record)
client.package_logs(LogRecordId(package_ahriman.base, package_ahriman.version), log_record)
def test_package_remove(client: Client, package_ahriman: Package) -> None:

View File

@ -64,8 +64,8 @@ def test_logs_remove(watcher: Watcher, package_ahriman: Package, mocker: MockerF
must remove package logs
"""
logs_mock = mocker.patch("ahriman.core.database.SQLite.logs_remove")
watcher.logs_remove(package_ahriman.base, 42)
logs_mock.assert_called_once_with(package_ahriman.base, 42)
watcher.logs_remove(package_ahriman.base, "42")
logs_mock.assert_called_once_with(package_ahriman.base, "42")
def test_logs_update_new(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
@ -75,11 +75,11 @@ def test_logs_update_new(watcher: Watcher, package_ahriman: Package, mocker: Moc
delete_mock = mocker.patch("ahriman.core.status.watcher.Watcher.logs_remove")
insert_mock = mocker.patch("ahriman.core.database.SQLite.logs_insert")
log_record_id = LogRecordId(package_ahriman.base, watcher._last_log_record_id.process_id)
log_record_id = LogRecordId(package_ahriman.base, watcher._last_log_record_id.version)
assert watcher._last_log_record_id != log_record_id
watcher.logs_update(log_record_id, 42.01, "log record")
delete_mock.assert_called_once_with(package_ahriman.base, log_record_id.process_id)
delete_mock.assert_called_once_with(package_ahriman.base, log_record_id.version)
insert_mock.assert_called_once_with(log_record_id, 42.01, "log record")
assert watcher._last_log_record_id == log_record_id
@ -92,7 +92,7 @@ def test_logs_update_update(watcher: Watcher, package_ahriman: Package, mocker:
delete_mock = mocker.patch("ahriman.core.status.watcher.Watcher.logs_remove")
insert_mock = mocker.patch("ahriman.core.database.SQLite.logs_insert")
log_record_id = LogRecordId(package_ahriman.base, watcher._last_log_record_id.process_id)
log_record_id = LogRecordId(package_ahriman.base, watcher._last_log_record_id.version)
watcher._last_log_record_id = log_record_id
watcher.logs_update(log_record_id, 42.01, "log record")

View File

@ -11,6 +11,7 @@ from ahriman.core.configuration import Configuration
from ahriman.core.status.web_client import WebClient
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
@ -280,10 +281,10 @@ def test_package_logs(web_client: WebClient, log_record: logging.LogRecord, pack
payload = {
"created": log_record.created,
"message": log_record.getMessage(),
"process_id": log_record.process,
"version": package_ahriman.version,
}
web_client.package_logs(package_ahriman.base, 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),
params=None, json=payload, files=None)
@ -295,7 +296,7 @@ def test_package_logs_failed(web_client: WebClient, log_record: logging.LogRecor
"""
mocker.patch("requests.Session.request", side_effect=Exception())
log_record.package_base = package_ahriman.base
web_client.package_logs(package_ahriman.base, log_record)
web_client.package_logs(LogRecordId(package_ahriman.base, package_ahriman.version), log_record)
def test_package_logs_failed_http_error(web_client: WebClient, log_record: logging.LogRecord, package_ahriman: Package,
@ -305,7 +306,7 @@ def test_package_logs_failed_http_error(web_client: WebClient, log_record: loggi
"""
mocker.patch("requests.Session.request", side_effect=requests.exceptions.HTTPError())
log_record.package_base = package_ahriman.base
web_client.package_logs(package_ahriman.base, log_record)
web_client.package_logs(LogRecordId(package_ahriman.base, package_ahriman.version), log_record)
def test_package_remove(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:

View File

@ -30,7 +30,7 @@ async def test_save_file(mocker: MockerFixture) -> None:
part_mock.filename = "filename"
part_mock.read_chunk = AsyncMock(side_effect=[b"content", None])
tempfile_mock = mocker.patch("tempfile.NamedTemporaryFile")
tempfile_mock = mocker.patch("ahriman.web.views.service.upload.NamedTemporaryFile")
file_mock = MagicMock()
tempfile_mock.return_value.__enter__.return_value = file_mock

View File

@ -30,9 +30,9 @@ async def test_delete(client: TestClient, package_ahriman: Package, package_pyth
json={"status": BuildStatusEnum.Success.value, "package": package_python_schedule.view()})
await client.post(f"/api/v1/packages/{package_ahriman.base}/logs",
json={"created": 42.0, "message": "message", "process_id": 42})
json={"created": 42.0, "message": "message", "version": "42"})
await client.post(f"/api/v1/packages/{package_python_schedule.base}/logs",
json={"created": 42.0, "message": "message", "process_id": 42})
json={"created": 42.0, "message": "message", "version": "42"})
response = await client.delete(f"/api/v1/packages/{package_ahriman.base}/logs")
assert response.status == 204
@ -53,7 +53,7 @@ async def test_get(client: TestClient, package_ahriman: Package) -> None:
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_ahriman.base}/logs",
json={"created": 42.0, "message": "message", "process_id": 42})
json={"created": 42.0, "message": "message", "version": "42"})
response_schema = pytest.helpers.schema_response(LogsView.get)
response = await client.get(f"/api/v1/packages/{package_ahriman.base}/logs")
@ -83,7 +83,7 @@ async def test_post(client: TestClient, package_ahriman: Package) -> None:
json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()})
request_schema = pytest.helpers.schema_request(LogsView.post)
payload = {"created": 42.0, "message": "message", "process_id": 42}
payload = {"created": 42.0, "message": "message", "version": "42"}
assert not request_schema.validate(payload)
response = await client.post(f"/api/v1/packages/{package_ahriman.base}/logs", json=payload)
assert response.status == 204