feat: allow to use single web instance for all repositories (#114)

* Allow to use single web instance for any repository

* some improvements

* drop includes from user home directory, introduce new variables to docker

The old solution didn't actually work as expected, because devtools
configuration belongs to filesystem (as well as sudo one), so it was
still required to run setup command.

In order to handle additional repositories, the POSTSETUP and PRESETUP
commands variables have been introduced. FAQ has been updated as well

* raise 404 in case if repository is unknown
This commit is contained in:
2023-10-17 03:53:33 +03:00
parent 4eb187aead
commit 6bd1636bfa
141 changed files with 2037 additions and 917 deletions

View File

@ -14,10 +14,9 @@ import ahriman.core.auth.helpers
from ahriman.core.auth.oauth import OAuth
from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.repository import Repository
from ahriman.core.spawn import Spawn
from ahriman.models.user import User
from ahriman.web.web import setup_service
from ahriman.web.web import setup_server
@pytest.helpers.register
@ -114,8 +113,7 @@ def schema_response(handler: Callable[..., Awaitable[Any]], *, code: int = 200)
@pytest.fixture
def application(configuration: Configuration, spawner: Spawn, database: SQLite, repository: Repository,
mocker: MockerFixture) -> Application:
def application(configuration: Configuration, spawner: Spawn, database: SQLite, mocker: MockerFixture) -> Application:
"""
application fixture
@ -123,7 +121,6 @@ def application(configuration: Configuration, spawner: Spawn, database: SQLite,
configuration(Configuration): configuration fixture
spawner(Spawn): spawner fixture
database(SQLite): database fixture
repository(Repository): repository fixture
mocker(MockerFixture): mocker object
Returns:
@ -131,17 +128,16 @@ def application(configuration: Configuration, spawner: Spawn, database: SQLite,
"""
configuration.set_option("web", "port", "8080")
mocker.patch("ahriman.core.database.SQLite.load", return_value=database)
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
mocker.patch("aiohttp_apispec.setup_aiohttp_apispec")
mocker.patch.object(ahriman.core.auth.helpers, "_has_aiohttp_security", False)
_, repository_id = configuration.check_loaded()
return setup_service(repository_id, configuration, spawner)
return setup_server(configuration, spawner, [repository_id])
@pytest.fixture
def application_with_auth(configuration: Configuration, user: User, spawner: Spawn, database: SQLite,
repository: Repository, mocker: MockerFixture) -> Application:
mocker: MockerFixture) -> Application:
"""
application fixture with auth enabled
@ -150,7 +146,6 @@ def application_with_auth(configuration: Configuration, user: User, spawner: Spa
user(User): user descriptor fixture
spawner(Spawn): spawner fixture
database(SQLite): database fixture
repository(Repository): repository fixture
mocker(MockerFixture): mocker object
Returns:
@ -159,11 +154,10 @@ def application_with_auth(configuration: Configuration, user: User, spawner: Spa
configuration.set_option("auth", "target", "configuration")
configuration.set_option("web", "port", "8080")
mocker.patch("ahriman.core.database.SQLite.load", return_value=database)
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
mocker.patch("aiohttp_apispec.setup_aiohttp_apispec")
mocker.patch.object(ahriman.core.auth.helpers, "_has_aiohttp_security", True)
_, repository_id = configuration.check_loaded()
application = setup_service(repository_id, configuration, spawner)
application = setup_server(configuration, spawner, [repository_id])
generated = user.hash_password(application["validator"].salt)
mocker.patch("ahriman.core.database.SQLite.user_get", return_value=generated)
@ -173,7 +167,7 @@ def application_with_auth(configuration: Configuration, user: User, spawner: Spa
@pytest.fixture
def application_with_debug(configuration: Configuration, user: User, spawner: Spawn, database: SQLite,
repository: Repository, mocker: MockerFixture) -> Application:
mocker: MockerFixture) -> Application:
"""
application fixture with debug enabled
@ -182,7 +176,6 @@ def application_with_debug(configuration: Configuration, user: User, spawner: Sp
user(User): user descriptor fixture
spawner(Spawn): spawner fixture
database(SQLite): database fixture
repository(Repository): repository fixture
mocker(MockerFixture): mocker object
Returns:
@ -191,17 +184,16 @@ def application_with_debug(configuration: Configuration, user: User, spawner: Sp
configuration.set_option("web", "debug", "yes")
configuration.set_option("web", "port", "8080")
mocker.patch("ahriman.core.database.SQLite.load", return_value=database)
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
mocker.patch("aiohttp_apispec.setup_aiohttp_apispec")
mocker.patch.object(ahriman.core.auth.helpers, "_has_aiohttp_security", False)
_, repository_id = configuration.check_loaded()
return setup_service(repository_id, configuration, spawner)
return setup_server(configuration, spawner, [repository_id])
@pytest.fixture
def client(application: Application, event_loop: BaseEventLoop,
aiohttp_client: Any, mocker: MockerFixture) -> TestClient:
def client(application: Application, event_loop: BaseEventLoop, aiohttp_client: Any,
mocker: MockerFixture) -> TestClient:
"""
web client fixture
@ -219,8 +211,8 @@ def client(application: Application, event_loop: BaseEventLoop,
@pytest.fixture
def client_with_auth(application_with_auth: Application, event_loop: BaseEventLoop,
aiohttp_client: Any, mocker: MockerFixture) -> TestClient:
def client_with_auth(application_with_auth: Application, event_loop: BaseEventLoop, aiohttp_client: Any,
mocker: MockerFixture) -> TestClient:
"""
web client fixture with full authorization functions
@ -238,8 +230,8 @@ def client_with_auth(application_with_auth: Application, event_loop: BaseEventLo
@pytest.fixture
def client_with_oauth_auth(application_with_auth: Application, event_loop: BaseEventLoop,
aiohttp_client: Any, mocker: MockerFixture) -> TestClient:
def client_with_oauth_auth(application_with_auth: Application, event_loop: BaseEventLoop, aiohttp_client: Any,
mocker: MockerFixture) -> TestClient:
"""
web client fixture with full authorization functions

View File

@ -0,0 +1 @@
# schema testing goes in view class tests

View File

@ -5,10 +5,12 @@ from aiohttp.web import Application
from pytest_mock import MockerFixture
from unittest.mock import call as MockCall
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import InitializeError
from ahriman.core.log.filtered_access_logger import FilteredAccessLogger
from ahriman.core.spawn import Spawn
from ahriman.core.status.watcher import Watcher
from ahriman.web.web import _create_socket, _on_shutdown, _on_startup, run_server
from ahriman.web.web import _create_socket, _on_shutdown, _on_startup, run_server, setup_server
async def test_create_socket(application: Application, mocker: MockerFixture) -> None:
@ -72,7 +74,7 @@ async def test_on_startup(application: Application, watcher: Watcher, mocker: Mo
"""
must call load method
"""
mocker.patch("aiohttp.web.Application.__getitem__", return_value=watcher)
mocker.patch("aiohttp.web.Application.__getitem__", return_value={"": watcher})
load_mock = mocker.patch("ahriman.core.status.watcher.Watcher.load")
await _on_startup(application)
@ -83,7 +85,7 @@ async def test_on_startup_exception(application: Application, watcher: Watcher,
"""
must throw exception on load error
"""
mocker.patch("aiohttp.web.Application.__getitem__", return_value=watcher)
mocker.patch("aiohttp.web.Application.__getitem__", return_value={"": watcher})
mocker.patch("ahriman.core.status.watcher.Watcher.load", side_effect=Exception())
with pytest.raises(InitializeError):
@ -151,3 +153,11 @@ def test_run_with_socket(application: Application, mocker: MockerFixture) -> Non
application, host="127.0.0.1", port=port, sock=42, handle_signals=True,
access_log=pytest.helpers.anyvar(int), access_log_class=FilteredAccessLogger
)
def test_setup_no_repositories(configuration: Configuration, spawner: Spawn) -> None:
"""
must raise InitializeError if no repositories set
"""
with pytest.raises(InitializeError):
setup_server(configuration, spawner, [])

View File

@ -2,10 +2,11 @@ import pytest
from multidict import MultiDict
from aiohttp.test_utils import TestClient
from aiohttp.web import HTTPBadRequest
from aiohttp.web import HTTPBadRequest, HTTPNotFound
from pytest_mock import MockerFixture
from unittest.mock import AsyncMock
from ahriman.models.repository_id import RepositoryId
from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView
@ -24,11 +25,18 @@ def test_configuration(base: BaseView) -> None:
assert base.configuration
def test_service(base: BaseView) -> None:
def test_services(base: BaseView) -> None:
"""
must return service
must return services
"""
assert base.service
assert base.services
def test_sign(base: BaseView) -> None:
"""
must return GPP wrapper instance
"""
assert base.sign
def test_spawn(base: BaseView) -> None:
@ -193,6 +201,53 @@ def test_page_bad_request(base: BaseView) -> None:
base.page()
def test_repository_id(base: BaseView, repository_id: RepositoryId) -> None:
"""
must repository identifier from parameters
"""
base._request = pytest.helpers.request(base.request.app, "", "",
params=MultiDict(architecture="i686", repository="repo"))
assert base.repository_id() == RepositoryId("i686", "repo")
base._request = pytest.helpers.request(base.request.app, "", "", params=MultiDict(architecture="i686"))
assert base.repository_id() == repository_id
base._request = pytest.helpers.request(base.request.app, "", "", params=MultiDict(repository="repo"))
assert base.repository_id() == repository_id
base._request = pytest.helpers.request(base.request.app, "", "", params=MultiDict())
assert base.repository_id() == repository_id
def test_service(base: BaseView) -> None:
"""
must return service for repository
"""
repository_id = RepositoryId("i686", "repo")
base.request.app["watcher"] = {
repository_id: watcher
for watcher in base.request.app["watcher"].values()
}
assert base.service(repository_id) == base.services[repository_id]
def test_service_auto(base: BaseView, repository_id: RepositoryId, mocker: MockerFixture) -> None:
"""
must return service for repository if no parameters set
"""
mocker.patch("ahriman.web.views.base.BaseView.repository_id", return_value=repository_id)
assert base.service() == base.services[repository_id]
def test_service_not_found(base: BaseView) -> None:
"""
must raise HTTPNotFound if no repository found
"""
with pytest.raises(HTTPNotFound):
base.service(RepositoryId("", ""))
async def test_username(base: BaseView, mocker: MockerFixture) -> None:
"""
must return identity of logged-in user

View File

@ -4,6 +4,7 @@ from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from unittest.mock import AsyncMock
from ahriman.models.repository_id import RepositoryId
from ahriman.models.user_access import UserAccess
from ahriman.web.views.v1.service.add import AddView
@ -24,7 +25,7 @@ def test_routes() -> None:
assert AddView.ROUTES == ["/api/v1/service/add"]
async def test_post(client: TestClient, mocker: MockerFixture) -> None:
async def test_post(client: TestClient, repository_id: RepositoryId, mocker: MockerFixture) -> None:
"""
must call post request correctly
"""
@ -39,7 +40,7 @@ async def test_post(client: TestClient, mocker: MockerFixture) -> None:
assert not request_schema.validate(payload)
response = await client.post("/api/v1/service/add", json=payload)
assert response.ok
add_mock.assert_called_once_with(["ahriman"], "username", now=True)
add_mock.assert_called_once_with(repository_id, ["ahriman"], "username", now=True)
json = await response.json()
assert json["process_id"] == "abc"

View File

@ -4,6 +4,7 @@ from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from unittest.mock import AsyncMock
from ahriman.models.repository_id import RepositoryId
from ahriman.models.user_access import UserAccess
from ahriman.web.views.v1.service.rebuild import RebuildView
@ -24,7 +25,7 @@ def test_routes() -> None:
assert RebuildView.ROUTES == ["/api/v1/service/rebuild"]
async def test_post(client: TestClient, mocker: MockerFixture) -> None:
async def test_post(client: TestClient, repository_id: RepositoryId, mocker: MockerFixture) -> None:
"""
must call post request correctly
"""
@ -39,7 +40,7 @@ async def test_post(client: TestClient, mocker: MockerFixture) -> None:
assert not request_schema.validate(payload)
response = await client.post("/api/v1/service/rebuild", json=payload)
assert response.ok
rebuild_mock.assert_called_once_with("python", "username")
rebuild_mock.assert_called_once_with(repository_id, "python", "username")
json = await response.json()
assert json["process_id"] == "abc"

View File

@ -3,6 +3,7 @@ import pytest
from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from ahriman.models.repository_id import RepositoryId
from ahriman.models.user_access import UserAccess
from ahriman.web.views.v1.service.remove import RemoveView
@ -23,7 +24,7 @@ def test_routes() -> None:
assert RemoveView.ROUTES == ["/api/v1/service/remove"]
async def test_post(client: TestClient, mocker: MockerFixture) -> None:
async def test_post(client: TestClient, repository_id: RepositoryId, mocker: MockerFixture) -> None:
"""
must call post request correctly
"""
@ -35,7 +36,7 @@ async def test_post(client: TestClient, mocker: MockerFixture) -> None:
assert not request_schema.validate(payload)
response = await client.post("/api/v1/service/remove", json=payload)
assert response.ok
remove_mock.assert_called_once_with(["ahriman"])
remove_mock.assert_called_once_with(repository_id, ["ahriman"])
json = await response.json()
assert json["process_id"] == "abc"

View File

@ -4,6 +4,7 @@ from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from unittest.mock import AsyncMock
from ahriman.models.repository_id import RepositoryId
from ahriman.models.user_access import UserAccess
from ahriman.web.views.v1.service.request import RequestView
@ -24,7 +25,7 @@ def test_routes() -> None:
assert RequestView.ROUTES == ["/api/v1/service/request"]
async def test_post(client: TestClient, mocker: MockerFixture) -> None:
async def test_post(client: TestClient, repository_id: RepositoryId, mocker: MockerFixture) -> None:
"""
must call post request correctly
"""
@ -39,7 +40,7 @@ async def test_post(client: TestClient, mocker: MockerFixture) -> None:
assert not request_schema.validate(payload)
response = await client.post("/api/v1/service/request", json=payload)
assert response.ok
add_mock.assert_called_once_with(["ahriman"], "username", now=False)
add_mock.assert_called_once_with(repository_id, ["ahriman"], "username", now=False)
json = await response.json()
assert json["process_id"] == "abc"

View File

@ -77,4 +77,4 @@ async def test_get_join(client: TestClient, mocker: MockerFixture) -> None:
assert not request_schema.validate(payload)
response = await client.get("/api/v1/service/search", params=payload)
assert response.ok
search_mock.assert_called_once_with("ahriman", "maybe", pacman=pytest.helpers.anyvar(int))
search_mock.assert_called_once_with("ahriman", "maybe")

View File

@ -4,6 +4,7 @@ from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from unittest.mock import AsyncMock
from ahriman.models.repository_id import RepositoryId
from ahriman.models.user_access import UserAccess
from ahriman.web.views.v1.service.update import UpdateView
@ -24,7 +25,7 @@ def test_routes() -> None:
assert UpdateView.ROUTES == ["/api/v1/service/update"]
async def test_post(client: TestClient, mocker: MockerFixture) -> None:
async def test_post(client: TestClient, repository_id: RepositoryId, mocker: MockerFixture) -> None:
"""
must call post request correctly for alias
"""
@ -50,7 +51,7 @@ async def test_post(client: TestClient, mocker: MockerFixture) -> None:
assert not request_schema.validate(payload)
response = await client.post("/api/v1/service/update", json=payload)
assert response.ok
update_mock.assert_called_once_with("username", **(defaults | payload))
update_mock.assert_called_once_with(repository_id, "username", **(defaults | payload))
update_mock.reset_mock()
json = await response.json()

View File

@ -4,6 +4,7 @@ from aiohttp.test_utils import TestClient
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.package import Package
from ahriman.models.repository_id import RepositoryId
from ahriman.models.user_access import UserAccess
from ahriman.web.views.v1.status.package import PackageView
@ -94,13 +95,17 @@ async def test_get_not_found(client: TestClient, package_ahriman: Package) -> No
assert not response_schema.validate(await response.json())
async def test_post(client: TestClient, package_ahriman: Package) -> None:
async def test_post(client: TestClient, package_ahriman: Package, repository_id: RepositoryId) -> None:
"""
must update package status
"""
request_schema = pytest.helpers.schema_request(PackageView.post)
payload = {"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()}
payload = {
"status": BuildStatusEnum.Success.value,
"package": package_ahriman.view(),
"repository": repository_id.view(),
}
assert not request_schema.validate(payload)
response = await client.post(f"/api/v1/packages/{package_ahriman.base}", json=payload)
assert response.status == 204
@ -120,18 +125,25 @@ async def test_post_exception(client: TestClient, package_ahriman: Package) -> N
assert not response_schema.validate(await response.json())
async def test_post_light(client: TestClient, package_ahriman: Package) -> None:
async def test_post_light(client: TestClient, package_ahriman: Package, repository_id: RepositoryId) -> None:
"""
must update package status only
"""
request_schema = pytest.helpers.schema_request(PackageView.post)
payload = {"status": BuildStatusEnum.Unknown.value, "package": package_ahriman.view()}
payload = {
"status": BuildStatusEnum.Success.value,
"package": package_ahriman.view(),
"repository": repository_id.view(),
}
assert not request_schema.validate(payload)
response = await client.post(f"/api/v1/packages/{package_ahriman.base}", json=payload)
assert response.status == 204
payload = {"status": BuildStatusEnum.Success.value}
payload = {
"status": BuildStatusEnum.Success.value,
"repository": repository_id.view(),
}
assert not request_schema.validate(payload)
response = await client.post(f"/api/v1/packages/{package_ahriman.base}", json=payload)
assert response.status == 204
@ -145,14 +157,17 @@ async def test_post_light(client: TestClient, package_ahriman: Package) -> None:
assert statuses[package_ahriman.base].status == BuildStatusEnum.Success
async def test_post_not_found(client: TestClient, package_ahriman: Package) -> None:
async def test_post_not_found(client: TestClient, package_ahriman: Package, repository_id: RepositoryId) -> None:
"""
must raise exception on status update for unknown package
"""
request_schema = pytest.helpers.schema_request(PackageView.post)
response_schema = pytest.helpers.schema_response(PackageView.post, code=400)
payload = {"status": BuildStatusEnum.Success.value}
payload = {
"status": BuildStatusEnum.Success.value,
"repository": repository_id.view(),
}
assert not request_schema.validate(payload)
response = await client.post(f"/api/v1/packages/{package_ahriman.base}", json=payload)
assert response.status == 400

View File

@ -0,0 +1,37 @@
import pytest
from aiohttp.test_utils import TestClient
from ahriman.models.repository_id import RepositoryId
from ahriman.models.user_access import UserAccess
from ahriman.web.views.v1.status.repositories import RepositoriesView
async def test_get_permission() -> None:
"""
must return correct permission for the request
"""
for method in ("GET",):
request = pytest.helpers.request("", "", method)
assert await RepositoriesView.get_permission(request) == UserAccess.Read
def test_routes() -> None:
"""
must return correct routes
"""
assert RepositoriesView.ROUTES == ["/api/v1/repositories"]
async def test_get(client: TestClient, repository_id: RepositoryId) -> None:
"""
must return status for specific package
"""
response_schema = pytest.helpers.schema_response(RepositoriesView.get)
response = await client.get(f"/api/v1/repositories")
assert response.ok
json = await response.json()
assert not response_schema.validate(json, many=True)
assert json == [repository_id.view()]