mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-07-23 18:59:56 +00:00
Add ability to trigger updates from the web (#31)
* add external process spawner and update test cases * pass no_report to handlers * provide service api endpoints * do not spawn process for single architecture run * pass no report to handlers * make _call method of handlers public and also simplify process spawn * move update under add * implement actions from web page * clear logging & improve l&f
This commit is contained in:
@ -1,41 +1,64 @@
|
||||
import pytest
|
||||
|
||||
from aiohttp import web
|
||||
from collections import namedtuple
|
||||
from pytest_mock import MockerFixture
|
||||
from typing import Any
|
||||
|
||||
import ahriman.core.auth.helpers
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.spawn import Spawn
|
||||
from ahriman.models.user import User
|
||||
from ahriman.web.web import setup_service
|
||||
|
||||
|
||||
_request = namedtuple("_request", ["app", "path", "method", "json", "post"])
|
||||
|
||||
|
||||
@pytest.helpers.register
|
||||
def request(app: web.Application, path: str, method: str, json: Any = None, data: Any = None) -> _request:
|
||||
"""
|
||||
request generator helper
|
||||
:param app: application fixture
|
||||
:param path: path for the request
|
||||
:param method: method for the request
|
||||
:param json: json payload of the request
|
||||
:param data: form data payload of the request
|
||||
:return: dummy request object
|
||||
"""
|
||||
return _request(app, path, method, json, data)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def application(configuration: Configuration, mocker: MockerFixture) -> web.Application:
|
||||
def application(configuration: Configuration, spawner: Spawn, mocker: MockerFixture) -> web.Application:
|
||||
"""
|
||||
application fixture
|
||||
:param configuration: configuration fixture
|
||||
:param spawner: spawner fixture
|
||||
:param mocker: mocker object
|
||||
:return: application test instance
|
||||
"""
|
||||
mocker.patch.object(ahriman.core.auth.helpers, "_has_aiohttp_security", False)
|
||||
mocker.patch("pathlib.Path.mkdir")
|
||||
return setup_service("x86_64", configuration)
|
||||
return setup_service("x86_64", configuration, spawner)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def application_with_auth(configuration: Configuration, user: User, mocker: MockerFixture) -> web.Application:
|
||||
def application_with_auth(configuration: Configuration, user: User, spawner: Spawn,
|
||||
mocker: MockerFixture) -> web.Application:
|
||||
"""
|
||||
application fixture with auth enabled
|
||||
:param configuration: configuration fixture
|
||||
:param user: user descriptor fixture
|
||||
:param spawner: spawner fixture
|
||||
:param mocker: mocker object
|
||||
:return: application test instance
|
||||
"""
|
||||
configuration.set_option("auth", "target", "configuration")
|
||||
mocker.patch.object(ahriman.core.auth.helpers, "_has_aiohttp_security", True)
|
||||
mocker.patch("pathlib.Path.mkdir")
|
||||
application = setup_service("x86_64", configuration)
|
||||
application = setup_service("x86_64", configuration, spawner)
|
||||
|
||||
generated = User(user.username, user.hash_password(application["validator"].salt), user.access)
|
||||
application["validator"]._users[generated.username] = generated
|
||||
|
@ -1,23 +1,10 @@
|
||||
import pytest
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
from ahriman.core.auth.auth import Auth
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.models.user import User
|
||||
from ahriman.web.middlewares.auth_handler import AuthorizationPolicy
|
||||
|
||||
_request = namedtuple("_request", ["path", "method"])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def aiohttp_request() -> _request:
|
||||
"""
|
||||
fixture for aiohttp like object
|
||||
:return: aiohttp like request test instance
|
||||
"""
|
||||
return _request("path", "GET")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def authorization_policy(configuration: Configuration, user: User) -> AuthorizationPolicy:
|
||||
|
@ -1,6 +1,7 @@
|
||||
import pytest
|
||||
|
||||
from aiohttp import web
|
||||
from pytest_mock import MockerFixture
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from ahriman.core.auth.auth import Auth
|
||||
@ -29,40 +30,40 @@ async def test_permits(authorization_policy: AuthorizationPolicy, user: User) ->
|
||||
authorization_policy.validator.verify_access.assert_called_with(user.username, user.access, "/endpoint")
|
||||
|
||||
|
||||
async def test_auth_handler_api(aiohttp_request: Any, auth: Auth, mocker: MockerFixture) -> None:
|
||||
async def test_auth_handler_api(auth: Auth, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must ask for status permission for api calls
|
||||
"""
|
||||
aiohttp_request = aiohttp_request._replace(path="/status-api")
|
||||
aiohttp_request = pytest.helpers.request("", "/status-api", "GET")
|
||||
request_handler = AsyncMock()
|
||||
mocker.patch("ahriman.core.auth.auth.Auth.is_safe_request", return_value=False)
|
||||
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
|
||||
|
||||
handler = auth_handler(auth)
|
||||
await handler(aiohttp_request, request_handler)
|
||||
check_permission_mock.assert_called_with(aiohttp_request, UserAccess.Status, aiohttp_request.path)
|
||||
check_permission_mock.assert_called_with(aiohttp_request, UserAccess.Read, aiohttp_request.path)
|
||||
|
||||
|
||||
async def test_auth_handler_api_post(aiohttp_request: Any, auth: Auth, mocker: MockerFixture) -> None:
|
||||
async def test_auth_handler_api_post(auth: Auth, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must ask for status permission for api calls with POST
|
||||
"""
|
||||
aiohttp_request = aiohttp_request._replace(path="/status-api", method="POST")
|
||||
aiohttp_request = pytest.helpers.request("", "/status-api", "POST")
|
||||
request_handler = AsyncMock()
|
||||
mocker.patch("ahriman.core.auth.auth.Auth.is_safe_request", return_value=False)
|
||||
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
|
||||
|
||||
handler = auth_handler(auth)
|
||||
await handler(aiohttp_request, request_handler)
|
||||
check_permission_mock.assert_called_with(aiohttp_request, UserAccess.Status, aiohttp_request.path)
|
||||
check_permission_mock.assert_called_with(aiohttp_request, UserAccess.Write, aiohttp_request.path)
|
||||
|
||||
|
||||
async def test_auth_handler_read(aiohttp_request: Any, auth: Auth, mocker: MockerFixture) -> None:
|
||||
async def test_auth_handler_read(auth: Auth, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must ask for read permission for api calls with GET
|
||||
"""
|
||||
for method in ("GET", "HEAD", "OPTIONS"):
|
||||
aiohttp_request = aiohttp_request._replace(method=method)
|
||||
aiohttp_request = pytest.helpers.request("", "", method)
|
||||
request_handler = AsyncMock()
|
||||
mocker.patch("ahriman.core.auth.auth.Auth.is_safe_request", return_value=False)
|
||||
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
|
||||
@ -72,12 +73,12 @@ async def test_auth_handler_read(aiohttp_request: Any, auth: Auth, mocker: Mocke
|
||||
check_permission_mock.assert_called_with(aiohttp_request, UserAccess.Read, aiohttp_request.path)
|
||||
|
||||
|
||||
async def test_auth_handler_write(aiohttp_request: Any, auth: Auth, mocker: MockerFixture) -> None:
|
||||
async def test_auth_handler_write(auth: Auth, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must ask for read permission for api calls with POST
|
||||
"""
|
||||
for method in ("CONNECT", "DELETE", "PATCH", "POST", "PUT", "TRACE"):
|
||||
aiohttp_request = aiohttp_request._replace(method=method)
|
||||
aiohttp_request = pytest.helpers.request("", "", method)
|
||||
request_handler = AsyncMock()
|
||||
mocker.patch("ahriman.core.auth.auth.Auth.is_safe_request", return_value=False)
|
||||
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
|
||||
|
@ -3,45 +3,47 @@ import pytest
|
||||
|
||||
from aiohttp.web_exceptions import HTTPBadRequest
|
||||
from pytest_mock import MockerFixture
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from ahriman.web.middlewares.exception_handler import exception_handler
|
||||
|
||||
|
||||
async def test_exception_handler(aiohttp_request: Any, mocker: MockerFixture) -> None:
|
||||
async def test_exception_handler(mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must pass success response
|
||||
"""
|
||||
request = pytest.helpers.request("", "", "")
|
||||
request_handler = AsyncMock()
|
||||
logging_mock = mocker.patch("logging.Logger.exception")
|
||||
|
||||
handler = exception_handler(logging.getLogger())
|
||||
await handler(aiohttp_request, request_handler)
|
||||
await handler(request, request_handler)
|
||||
logging_mock.assert_not_called()
|
||||
|
||||
|
||||
async def test_exception_handler_client_error(aiohttp_request: Any, mocker: MockerFixture) -> None:
|
||||
async def test_exception_handler_client_error(mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must pass client exception
|
||||
"""
|
||||
request = pytest.helpers.request("", "", "")
|
||||
request_handler = AsyncMock(side_effect=HTTPBadRequest())
|
||||
logging_mock = mocker.patch("logging.Logger.exception")
|
||||
|
||||
handler = exception_handler(logging.getLogger())
|
||||
with pytest.raises(HTTPBadRequest):
|
||||
await handler(aiohttp_request, request_handler)
|
||||
await handler(request, request_handler)
|
||||
logging_mock.assert_not_called()
|
||||
|
||||
|
||||
async def test_exception_handler_server_error(aiohttp_request: Any, mocker: MockerFixture) -> None:
|
||||
async def test_exception_handler_server_error(mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must log server exception and re-raise it
|
||||
"""
|
||||
request = pytest.helpers.request("", "", "")
|
||||
request_handler = AsyncMock(side_effect=Exception())
|
||||
logging_mock = mocker.patch("logging.Logger.exception")
|
||||
|
||||
handler = exception_handler(logging.getLogger())
|
||||
with pytest.raises(Exception):
|
||||
await handler(aiohttp_request, request_handler)
|
||||
await handler(request, request_handler)
|
||||
logging_mock.assert_called_once()
|
||||
|
@ -6,6 +6,18 @@ from pytest_aiohttp import TestClient
|
||||
from pytest_mock import MockerFixture
|
||||
from typing import Any
|
||||
|
||||
from ahriman.web.views.base import BaseView
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def base(application: web.Application) -> BaseView:
|
||||
"""
|
||||
base view fixture
|
||||
:param application: application fixture
|
||||
:return: generated base view fixture
|
||||
"""
|
||||
return BaseView(pytest.helpers.request(application, "", ""))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(application: web.Application, loop: BaseEventLoop,
|
||||
|
46
tests/ahriman/web/views/service/test_views_service_add.py
Normal file
46
tests/ahriman/web/views/service/test_views_service_add.py
Normal file
@ -0,0 +1,46 @@
|
||||
from aiohttp.test_utils import TestClient
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
|
||||
async def test_post(client: TestClient, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must call post request correctly
|
||||
"""
|
||||
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add")
|
||||
response = await client.post("/service-api/v1/add", json={"packages": ["ahriman"]})
|
||||
|
||||
assert response.status == 200
|
||||
add_mock.assert_called_with(["ahriman"], True)
|
||||
|
||||
|
||||
async def test_post_now(client: TestClient, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must call post and run build
|
||||
"""
|
||||
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add")
|
||||
response = await client.post("/service-api/v1/add", json={"packages": ["ahriman"], "build_now": False})
|
||||
|
||||
assert response.status == 200
|
||||
add_mock.assert_called_with(["ahriman"], False)
|
||||
|
||||
|
||||
async def test_post_exception(client: TestClient, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must raise exception on missing packages payload
|
||||
"""
|
||||
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add")
|
||||
response = await client.post("/service-api/v1/add")
|
||||
|
||||
assert response.status == 400
|
||||
add_mock.assert_not_called()
|
||||
|
||||
|
||||
async def test_post_update(client: TestClient, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must call post request correctly for alias
|
||||
"""
|
||||
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add")
|
||||
response = await client.post("/service-api/v1/update", json={"packages": ["ahriman"]})
|
||||
|
||||
assert response.status == 200
|
||||
add_mock.assert_called_with(["ahriman"], True)
|
24
tests/ahriman/web/views/service/test_views_service_remove.py
Normal file
24
tests/ahriman/web/views/service/test_views_service_remove.py
Normal file
@ -0,0 +1,24 @@
|
||||
from aiohttp.test_utils import TestClient
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
|
||||
async def test_post(client: TestClient, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must call post request correctly
|
||||
"""
|
||||
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_remove")
|
||||
response = await client.post("/service-api/v1/remove", json={"packages": ["ahriman"]})
|
||||
|
||||
assert response.status == 200
|
||||
add_mock.assert_called_with(["ahriman"])
|
||||
|
||||
|
||||
async def test_post_exception(client: TestClient, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must raise exception on missing packages payload
|
||||
"""
|
||||
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_remove")
|
||||
response = await client.post("/service-api/v1/remove")
|
||||
|
||||
assert response.status == 400
|
||||
add_mock.assert_not_called()
|
59
tests/ahriman/web/views/service/test_views_service_search.py
Normal file
59
tests/ahriman/web/views/service/test_views_service_search.py
Normal file
@ -0,0 +1,59 @@
|
||||
import aur
|
||||
|
||||
from aiohttp.test_utils import TestClient
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
|
||||
async def test_get(client: TestClient, aur_package_ahriman: aur.Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must call get request correctly
|
||||
"""
|
||||
mocker.patch("aur.search", return_value=[aur_package_ahriman])
|
||||
response = await client.get("/service-api/v1/search", params={"for": "ahriman"})
|
||||
|
||||
assert response.status == 200
|
||||
assert await response.json() == ["ahriman"]
|
||||
|
||||
|
||||
async def test_get_exception(client: TestClient, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must raise 400 on empty search string
|
||||
"""
|
||||
search_mock = mocker.patch("aur.search")
|
||||
response = await client.get("/service-api/v1/search")
|
||||
|
||||
assert response.status == 400
|
||||
search_mock.assert_not_called()
|
||||
|
||||
|
||||
async def test_get_join(client: TestClient, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must join search args with space
|
||||
"""
|
||||
search_mock = mocker.patch("aur.search")
|
||||
response = await client.get("/service-api/v1/search", params=[("for", "ahriman"), ("for", "maybe")])
|
||||
|
||||
assert response.status == 200
|
||||
search_mock.assert_called_with("ahriman maybe")
|
||||
|
||||
|
||||
async def test_get_join_filter(client: TestClient, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must filter search parameters with less than 3 symbols
|
||||
"""
|
||||
search_mock = mocker.patch("aur.search")
|
||||
response = await client.get("/service-api/v1/search", params=[("for", "ah"), ("for", "maybe")])
|
||||
|
||||
assert response.status == 200
|
||||
search_mock.assert_called_with("maybe")
|
||||
|
||||
|
||||
async def test_get_join_filter_empty(client: TestClient, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must filter search parameters with less than 3 symbols (empty result)
|
||||
"""
|
||||
search_mock = mocker.patch("aur.search")
|
||||
response = await client.get("/service-api/v1/search", params=[("for", "ah"), ("for", "ma")])
|
||||
|
||||
assert response.status == 400
|
||||
search_mock.assert_not_called()
|
88
tests/ahriman/web/views/test_views_base.py
Normal file
88
tests/ahriman/web/views/test_views_base.py
Normal file
@ -0,0 +1,88 @@
|
||||
import pytest
|
||||
|
||||
from multidict import MultiDict
|
||||
|
||||
from ahriman.web.views.base import BaseView
|
||||
|
||||
|
||||
def test_service(base: BaseView) -> None:
|
||||
"""
|
||||
must return service
|
||||
"""
|
||||
assert base.service
|
||||
|
||||
|
||||
def test_spawn(base: BaseView) -> None:
|
||||
"""
|
||||
must return spawn thread
|
||||
"""
|
||||
assert base.spawner
|
||||
|
||||
|
||||
def test_validator(base: BaseView) -> None:
|
||||
"""
|
||||
must return service
|
||||
"""
|
||||
assert base.validator
|
||||
|
||||
|
||||
async def test_extract_data_json(base: BaseView) -> None:
|
||||
"""
|
||||
must parse and return json
|
||||
"""
|
||||
json = {"key1": "value1", "key2": "value2"}
|
||||
|
||||
async def get_json():
|
||||
return json
|
||||
|
||||
base._request = pytest.helpers.request(base.request.app, "", "", json=get_json)
|
||||
assert await base.extract_data() == json
|
||||
|
||||
|
||||
async def test_extract_data_post(base: BaseView) -> None:
|
||||
"""
|
||||
must parse and return form data
|
||||
"""
|
||||
json = {"key1": "value1", "key2": "value2"}
|
||||
|
||||
async def get_json():
|
||||
raise ValueError()
|
||||
|
||||
async def get_data():
|
||||
return json
|
||||
|
||||
base._request = pytest.helpers.request(base.request.app, "", "", json=get_json, data=get_data)
|
||||
assert await base.extract_data() == json
|
||||
|
||||
|
||||
async def test_data_as_json(base: BaseView) -> None:
|
||||
"""
|
||||
must parse multi value form payload
|
||||
"""
|
||||
json = {"key1": "value1", "key2": ["value2", "value3"], "key3": ["value4", "value5", "value6"]}
|
||||
|
||||
async def get_data():
|
||||
result = MultiDict()
|
||||
for key, values in json.items():
|
||||
if isinstance(values, list):
|
||||
for value in values:
|
||||
result.add(key, value)
|
||||
else:
|
||||
result.add(key, values)
|
||||
return result
|
||||
|
||||
base._request = pytest.helpers.request(base.request.app, "", "", data=get_data)
|
||||
assert await base.data_as_json([]) == json
|
||||
|
||||
|
||||
async def test_data_as_json_with_list_keys(base: BaseView) -> None:
|
||||
"""
|
||||
must parse multi value form payload with forced list
|
||||
"""
|
||||
json = {"key1": "value1"}
|
||||
|
||||
async def get_data():
|
||||
return json
|
||||
|
||||
base._request = pytest.helpers.request(base.request.app, "", "", data=get_data)
|
||||
assert await base.data_as_json(["key1"]) == {"key1": ["value1"]}
|
@ -11,10 +11,10 @@ async def test_post(client_with_auth: TestClient, user: User, mocker: MockerFixt
|
||||
payload = {"username": user.username, "password": user.password}
|
||||
remember_mock = mocker.patch("aiohttp_security.remember")
|
||||
|
||||
post_response = await client_with_auth.post("/login", json=payload)
|
||||
post_response = await client_with_auth.post("/user-api/v1/login", json=payload)
|
||||
assert post_response.status == 200
|
||||
|
||||
post_response = await client_with_auth.post("/login", data=payload)
|
||||
post_response = await client_with_auth.post("/user-api/v1/login", data=payload)
|
||||
assert post_response.status == 200
|
||||
|
||||
remember_mock.assert_called()
|
||||
@ -25,7 +25,7 @@ async def test_post_skip(client: TestClient, user: User) -> None:
|
||||
must process if no auth configured
|
||||
"""
|
||||
payload = {"username": user.username, "password": user.password}
|
||||
post_response = await client.post("/login", json=payload)
|
||||
post_response = await client.post("/user-api/v1/login", json=payload)
|
||||
assert post_response.status == 200
|
||||
|
||||
|
||||
@ -36,6 +36,6 @@ async def test_post_unauthorized(client_with_auth: TestClient, user: User, mocke
|
||||
payload = {"username": user.username, "password": ""}
|
||||
remember_mock = mocker.patch("aiohttp_security.remember")
|
||||
|
||||
post_response = await client_with_auth.post("/login", json=payload)
|
||||
post_response = await client_with_auth.post("/user-api/v1/login", json=payload)
|
||||
assert post_response.status == 401
|
||||
remember_mock.assert_not_called()
|
@ -10,7 +10,7 @@ async def test_post(client_with_auth: TestClient, mocker: MockerFixture) -> None
|
||||
mocker.patch("aiohttp_security.check_authorized")
|
||||
forget_mock = mocker.patch("aiohttp_security.forget")
|
||||
|
||||
post_response = await client_with_auth.post("/logout")
|
||||
post_response = await client_with_auth.post("/user-api/v1/logout")
|
||||
assert post_response.status == 200
|
||||
forget_mock.assert_called_once()
|
||||
|
||||
@ -22,7 +22,7 @@ async def test_post_unauthorized(client_with_auth: TestClient, mocker: MockerFix
|
||||
mocker.patch("aiohttp_security.check_authorized", side_effect=HTTPUnauthorized())
|
||||
forget_mock = mocker.patch("aiohttp_security.forget")
|
||||
|
||||
post_response = await client_with_auth.post("/logout")
|
||||
post_response = await client_with_auth.post("/user-api/v1/logout")
|
||||
assert post_response.status == 401
|
||||
forget_mock.assert_not_called()
|
||||
|
||||
@ -31,5 +31,5 @@ async def test_post_disabled(client: TestClient) -> None:
|
||||
"""
|
||||
must raise exception if auth is disabled
|
||||
"""
|
||||
post_response = await client.post("/logout")
|
||||
post_response = await client.post("/user-api/v1/logout")
|
||||
assert post_response.status == 200
|
Reference in New Issue
Block a user