mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-05-01 10:47:17 +00:00
* add support of remote task tracking * add remote call trigger implementation * docs update * add cross-service upload * add notes about user * add more ability to control upload * multipart upload with signatures as well as safe file save * configuration reference update * rename watcher methods * 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 * add --server flag for setup command * restore behavior of the httploghandler
180 lines
6.7 KiB
Python
180 lines
6.7 KiB
Python
import pytest
|
|
|
|
from aiohttp import FormData
|
|
from aiohttp.test_utils import TestClient
|
|
from aiohttp.web import HTTPBadRequest
|
|
from io import BytesIO
|
|
from pathlib import Path
|
|
from pytest_mock import MockerFixture
|
|
from unittest.mock import AsyncMock, MagicMock, call as MockCall
|
|
|
|
from ahriman.models.repository_paths import RepositoryPaths
|
|
from ahriman.models.user_access import UserAccess
|
|
from ahriman.web.views.service.upload import UploadView
|
|
|
|
|
|
async def test_get_permission() -> None:
|
|
"""
|
|
must return correct permission for the request
|
|
"""
|
|
for method in ("POST",):
|
|
request = pytest.helpers.request("", "", method)
|
|
assert await UploadView.get_permission(request) == UserAccess.Full
|
|
|
|
|
|
async def test_save_file(mocker: MockerFixture) -> None:
|
|
"""
|
|
must correctly save file
|
|
"""
|
|
part_mock = MagicMock()
|
|
part_mock.filename = "filename"
|
|
part_mock.read_chunk = AsyncMock(side_effect=[b"content", None])
|
|
|
|
tempfile_mock = mocker.patch("ahriman.web.views.service.upload.NamedTemporaryFile")
|
|
file_mock = MagicMock()
|
|
tempfile_mock.return_value.__enter__.return_value = file_mock
|
|
|
|
open_mock = mocker.patch("pathlib.Path.open")
|
|
copy_mock = mocker.patch("shutil.copyfileobj")
|
|
local = Path("local")
|
|
|
|
assert await UploadView.save_file(part_mock, local, max_body_size=None) == \
|
|
(part_mock.filename, local / f".{part_mock.filename}")
|
|
file_mock.write.assert_called_once_with(b"content")
|
|
open_mock.assert_called_once_with("wb")
|
|
copy_mock.assert_called_once_with(pytest.helpers.anyvar(int), pytest.helpers.anyvar(int))
|
|
|
|
|
|
async def test_save_file_no_filename() -> None:
|
|
"""
|
|
must raise exception on missing filename
|
|
"""
|
|
part_mock = MagicMock()
|
|
part_mock.filename = None
|
|
|
|
with pytest.raises(HTTPBadRequest):
|
|
await UploadView.save_file(part_mock, Path("local"), max_body_size=None)
|
|
|
|
|
|
async def test_save_file_invalid_filename() -> None:
|
|
"""
|
|
must raise exception on invalid filename
|
|
"""
|
|
part_mock = MagicMock()
|
|
part_mock.filename = ".."
|
|
|
|
with pytest.raises(HTTPBadRequest):
|
|
await UploadView.save_file(part_mock, Path("local"), max_body_size=None)
|
|
|
|
|
|
async def test_save_file_too_big() -> None:
|
|
"""
|
|
must raise exception on too big file
|
|
"""
|
|
part_mock = MagicMock()
|
|
part_mock.filename = "filename"
|
|
part_mock.read_chunk = AsyncMock(side_effect=[b"content", None])
|
|
|
|
with pytest.raises(HTTPBadRequest):
|
|
await UploadView.save_file(part_mock, Path("local"), max_body_size=0)
|
|
|
|
|
|
async def test_post(client: TestClient, repository_paths: RepositoryPaths, mocker: MockerFixture) -> None:
|
|
"""
|
|
must process file upload via http
|
|
"""
|
|
local = Path("local")
|
|
save_mock = mocker.patch("ahriman.web.views.service.upload.UploadView.save_file",
|
|
side_effect=AsyncMock(return_value=("filename", local / ".filename")))
|
|
rename_mock = mocker.patch("pathlib.Path.rename")
|
|
# no content validation here because it has invalid schema
|
|
|
|
data = FormData()
|
|
data.add_field("package", BytesIO(b"content"), filename="filename", content_type="application/octet-stream")
|
|
|
|
response = await client.post("/api/v1/service/upload", data=data)
|
|
assert response.ok
|
|
save_mock.assert_called_once_with(pytest.helpers.anyvar(int), repository_paths.packages, max_body_size=None)
|
|
rename_mock.assert_called_once_with(local / "filename")
|
|
|
|
|
|
async def test_post_with_sig(client: TestClient, repository_paths: RepositoryPaths, mocker: MockerFixture) -> None:
|
|
"""
|
|
must process file upload with signature via http
|
|
"""
|
|
local = Path("local")
|
|
save_mock = mocker.patch("ahriman.web.views.service.upload.UploadView.save_file",
|
|
side_effect=AsyncMock(side_effect=[
|
|
("filename", local / ".filename"),
|
|
("filename.sig", local / ".filename.sig"),
|
|
]))
|
|
rename_mock = mocker.patch("pathlib.Path.rename")
|
|
# no content validation here because it has invalid schema
|
|
|
|
data = FormData()
|
|
data.add_field("package", BytesIO(b"content"), filename="filename")
|
|
data.add_field("signature", BytesIO(b"sig"), filename="filename.sig")
|
|
|
|
response = await client.post("/api/v1/service/upload", data=data)
|
|
assert response.ok
|
|
save_mock.assert_has_calls([
|
|
MockCall(pytest.helpers.anyvar(int), repository_paths.packages, max_body_size=None),
|
|
MockCall(pytest.helpers.anyvar(int), repository_paths.packages, max_body_size=None),
|
|
])
|
|
rename_mock.assert_has_calls([
|
|
MockCall(local / "filename"),
|
|
MockCall(local / "filename.sig"),
|
|
])
|
|
|
|
|
|
async def test_post_not_found(client: TestClient, mocker: MockerFixture) -> None:
|
|
"""
|
|
must return 404 if request was disabled
|
|
"""
|
|
mocker.patch("ahriman.core.configuration.Configuration.getboolean", return_value=False)
|
|
data = FormData()
|
|
data.add_field("package", BytesIO(b"content"), filename="filename", content_type="application/octet-stream")
|
|
response_schema = pytest.helpers.schema_response(UploadView.post, code=404)
|
|
|
|
response = await client.post("/api/v1/service/upload", data=data)
|
|
assert response.status == 404
|
|
assert not response_schema.validate(await response.json())
|
|
|
|
|
|
async def test_post_not_multipart(client: TestClient) -> None:
|
|
"""
|
|
must return 400 on invalid payload
|
|
"""
|
|
response_schema = pytest.helpers.schema_response(UploadView.post, code=400)
|
|
|
|
response = await client.post("/api/v1/service/upload")
|
|
assert response.status == 400
|
|
assert not response_schema.validate(await response.json())
|
|
|
|
|
|
async def test_post_not_body_part(client: TestClient, mocker: MockerFixture) -> None:
|
|
"""
|
|
must return 400 on invalid iterator in multipart
|
|
"""
|
|
response_schema = pytest.helpers.schema_response(UploadView.post, code=400)
|
|
mocker.patch("aiohttp.MultipartReader.next", return_value=42) # surprise, motherfucker
|
|
data = FormData()
|
|
data.add_field("package", BytesIO(b"content"), filename="filename", content_type="application/octet-stream")
|
|
|
|
response = await client.post("/api/v1/service/upload", data=data)
|
|
assert response.status == 400
|
|
assert not response_schema.validate(await response.json())
|
|
|
|
|
|
async def test_post_not_package(client: TestClient) -> None:
|
|
"""
|
|
must return 400 on invalid multipart key
|
|
"""
|
|
response_schema = pytest.helpers.schema_response(UploadView.post, code=400)
|
|
data = FormData()
|
|
data.add_field("random", BytesIO(b"content"), filename="filename", content_type="application/octet-stream")
|
|
|
|
response = await client.post("/api/v1/service/upload", data=data)
|
|
assert response.status == 400
|
|
assert not response_schema.validate(await response.json())
|