diff --git a/src/ahriman/web/routes.py b/src/ahriman/web/routes.py
index 900609c4..214b1911 100644
--- a/src/ahriman/web/routes.py
+++ b/src/ahriman/web/routes.py
@@ -19,13 +19,17 @@
#
from aiohttp.web import Application
-from ahriman.web.views.ahriman import AhrimanView
from ahriman.web.views.index import IndexView
-from ahriman.web.views.login import LoginView
-from ahriman.web.views.logout import LogoutView
-from ahriman.web.views.package import PackageView
-from ahriman.web.views.packages import PackagesView
-from ahriman.web.views.status import StatusView
+from ahriman.web.views.service.add import AddView
+from ahriman.web.views.service.remove import RemoveView
+from ahriman.web.views.service.search import SearchView
+from ahriman.web.views.service.update import UpdateView
+from ahriman.web.views.status.ahriman import AhrimanView
+from ahriman.web.views.status.package import PackageView
+from ahriman.web.views.status.packages import PackagesView
+from ahriman.web.views.status.status import StatusView
+from ahriman.web.views.user.login import LoginView
+from ahriman.web.views.user.logout import LogoutView
def setup_routes(application: Application) -> None:
@@ -37,8 +41,13 @@ def setup_routes(application: Application) -> None:
GET / get build status page
GET /index.html same as above
- POST /user-api/v1/login login to service
- POST /user-api/v1/logout logout from service
+ POST /service-api/v1/add add new packages to repository
+
+ POST /service-api/v1/remove remove existing package from repository
+
+ GET /service-api/v1/search search for substring in AUR
+
+ POST /service-api/v1/update update existing package in repository
GET /status-api/v1/ahriman get current service status
POST /status-api/v1/ahriman update service status
@@ -52,13 +61,21 @@ def setup_routes(application: Application) -> None:
GET /status-api/v1/status get web service status itself
+ POST /user-api/v1/login login to service
+ POST /user-api/v1/logout logout from service
+
:param application: web application instance
"""
application.router.add_get("/", IndexView, allow_head=True)
application.router.add_get("/index.html", IndexView, allow_head=True)
- application.router.add_post("/user-api/v1/login", LoginView)
- application.router.add_post("/user-api/v1/logout", LogoutView)
+ application.router.add_post("/service-api/v1/add", AddView)
+
+ application.router.add_post("/service-api/v1/remove", RemoveView)
+
+ application.router.add_get("/service-api/v1/search", SearchView, allow_head=False)
+
+ application.router.add_post("/service-api/v1/update", UpdateView)
application.router.add_get("/status-api/v1/ahriman", AhrimanView, allow_head=True)
application.router.add_post("/status-api/v1/ahriman", AhrimanView)
@@ -71,3 +88,6 @@ def setup_routes(application: Application) -> None:
application.router.add_post("/status-api/v1/packages/{package}", PackageView)
application.router.add_get("/status-api/v1/status", StatusView, allow_head=True)
+
+ application.router.add_post("/user-api/v1/login", LoginView)
+ application.router.add_post("/user-api/v1/logout", LogoutView)
diff --git a/src/ahriman/web/views/base.py b/src/ahriman/web/views/base.py
index 963c8019..78818e0a 100644
--- a/src/ahriman/web/views/base.py
+++ b/src/ahriman/web/views/base.py
@@ -18,7 +18,7 @@
# along with this program. If not, see .
#
from aiohttp.web import View
-from typing import Any, Dict
+from typing import Any, Dict, List, Optional
from ahriman.core.auth.auth import Auth
from ahriman.core.spawn import Spawn
@@ -54,20 +54,22 @@ class BaseView(View):
validator: Auth = self.request.app["validator"]
return validator
- async def extract_data(self) -> Dict[str, Any]:
+ async def extract_data(self, list_keys: Optional[List[str]] = None) -> Dict[str, Any]:
"""
extract json data from either json or form data
+ :param list_keys: optional list of keys which must be forced to list from form data
:return: raw json object or form data converted to json
"""
try:
json: Dict[str, Any] = await self.request.json()
return json
except ValueError:
- return await self.data_as_json()
+ return await self.data_as_json(list_keys or [])
- async def data_as_json(self) -> Dict[str, Any]:
+ async def data_as_json(self, list_keys: List[str]) -> Dict[str, Any]:
"""
extract form data and convert it to json object
+ :param list_keys: list of keys which must be forced to list from form data
:return: form data converted to json. In case if a key is found multiple times it will be returned as list
"""
raw = await self.request.post()
@@ -77,6 +79,8 @@ class BaseView(View):
json[key].append(value)
elif key in json:
json[key] = [json[key], value]
+ elif key in list_keys:
+ json[key] = [value]
else:
json[key] = value
return json
diff --git a/src/ahriman/web/views/service/__init__.py b/src/ahriman/web/views/service/__init__.py
new file mode 100644
index 00000000..fb32931e
--- /dev/null
+++ b/src/ahriman/web/views/service/__init__.py
@@ -0,0 +1,19 @@
+#
+# Copyright (c) 2021 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 .
+#
diff --git a/src/ahriman/web/views/service/add.py b/src/ahriman/web/views/service/add.py
new file mode 100644
index 00000000..0527c3ca
--- /dev/null
+++ b/src/ahriman/web/views/service/add.py
@@ -0,0 +1,52 @@
+#
+# Copyright (c) 2021 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 .
+#
+from aiohttp.web import HTTPFound, Response, json_response
+
+from ahriman.web.views.base import BaseView
+
+
+class AddView(BaseView):
+ """
+ add package web view
+ """
+
+ async def post(self) -> Response:
+ """
+ add new package
+
+ JSON body must be supplied, the following model is used:
+ {
+ "packages": "ahriman", # either list of packages or package name as in AUR
+ "build_now": true # optional flag which runs build
+ }
+
+ :return: redirect to main page on success
+ """
+ data = await self.extract_data(["packages"])
+
+ try:
+ now = data.get("build_now") or False
+ packages = data["packages"]
+ except Exception as e:
+ return json_response(text=str(e), status=400)
+
+ self.spawner.packages_add(packages, now)
+
+ return HTTPFound("/")
diff --git a/src/ahriman/web/views/service/remove.py b/src/ahriman/web/views/service/remove.py
new file mode 100644
index 00000000..403f15b2
--- /dev/null
+++ b/src/ahriman/web/views/service/remove.py
@@ -0,0 +1,50 @@
+#
+# Copyright (c) 2021 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 .
+#
+from aiohttp.web import HTTPFound, Response, json_response
+
+from ahriman.web.views.base import BaseView
+
+
+class RemoveView(BaseView):
+ """
+ remove package web view
+ """
+
+ async def post(self) -> Response:
+ """
+ remove existing packages
+
+ JSON body must be supplied, the following model is used:
+ {
+ "packages": "ahriman", # either list of packages or package name
+ }
+
+ :return: redirect to main page on success
+ """
+ data = await self.extract_data(["packages"])
+
+ try:
+ packages = data["packages"]
+ except Exception as e:
+ return json_response(text=str(e), status=400)
+
+ self.spawner.packages_remove(packages)
+
+ return HTTPFound("/")
diff --git a/src/ahriman/web/views/service/search.py b/src/ahriman/web/views/service/search.py
new file mode 100644
index 00000000..b2e781a5
--- /dev/null
+++ b/src/ahriman/web/views/service/search.py
@@ -0,0 +1,48 @@
+#
+# Copyright (c) 2021 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 .
+#
+import aur # type: ignore
+
+from aiohttp.web import Response, json_response
+from typing import Iterator
+
+from ahriman.web.views.base import BaseView
+
+
+class SearchView(BaseView):
+ """
+ AUR search web view
+ """
+
+ async def get(self) -> Response:
+ """
+ search packages in AUR
+
+ search string (non empty) must be supplied as `for` parameter
+
+ :return: 200 with found package bases sorted by name
+ """
+ search: Iterator[str] = filter(lambda s: len(s) > 3, self.request.query.getall("for", default=[]))
+ search_string = " ".join(search)
+
+ if not search_string:
+ return json_response(text="Search string must not be empty", status=400)
+ packages = aur.search(search_string)
+
+ return json_response(sorted(package.package_base for package in packages))
diff --git a/src/ahriman/web/views/service/update.py b/src/ahriman/web/views/service/update.py
new file mode 100644
index 00000000..92aa16ec
--- /dev/null
+++ b/src/ahriman/web/views/service/update.py
@@ -0,0 +1,50 @@
+#
+# Copyright (c) 2021 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 .
+#
+from aiohttp.web import HTTPFound, Response, json_response
+
+from ahriman.web.views.base import BaseView
+
+
+class UpdateView(BaseView):
+ """
+ update package web view
+ """
+
+ async def post(self) -> Response:
+ """
+ update existing packages
+
+ JSON body must be supplied, the following model is used:
+ {
+ "packages": "ahriman", # either list of packages or package name
+ }
+
+ :return: redirect to main page on success
+ """
+ data = await self.extract_data(["packages"])
+
+ try:
+ packages = data["packages"]
+ except Exception as e:
+ return json_response(text=str(e), status=400)
+
+ self.spawner.packages_update(packages)
+
+ return HTTPFound("/")
diff --git a/src/ahriman/web/views/status/__init__.py b/src/ahriman/web/views/status/__init__.py
new file mode 100644
index 00000000..fb32931e
--- /dev/null
+++ b/src/ahriman/web/views/status/__init__.py
@@ -0,0 +1,19 @@
+#
+# Copyright (c) 2021 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 .
+#
diff --git a/src/ahriman/web/views/ahriman.py b/src/ahriman/web/views/status/ahriman.py
similarity index 92%
rename from src/ahriman/web/views/ahriman.py
rename to src/ahriman/web/views/status/ahriman.py
index 42f85bc8..80a09b8e 100644
--- a/src/ahriman/web/views/ahriman.py
+++ b/src/ahriman/web/views/status/ahriman.py
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
+from aiohttp.web import HTTPNoContent, Response, json_response
from ahriman.models.build_status import BuildStatusEnum
from ahriman.web.views.base import BaseView
@@ -51,7 +51,7 @@ class AhrimanView(BaseView):
try:
status = BuildStatusEnum(data["status"])
except Exception as e:
- raise HTTPBadRequest(text=str(e))
+ return json_response(text=str(e), status=400)
self.service.update_self(status)
diff --git a/src/ahriman/web/views/package.py b/src/ahriman/web/views/status/package.py
similarity index 91%
rename from src/ahriman/web/views/package.py
rename to src/ahriman/web/views/status/package.py
index 3789a896..234bb5f8 100644
--- a/src/ahriman/web/views/package.py
+++ b/src/ahriman/web/views/status/package.py
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response, json_response
+from aiohttp.web import HTTPNoContent, HTTPNotFound, Response, json_response
from ahriman.core.exceptions import UnknownPackage
from ahriman.models.build_status import BuildStatusEnum
@@ -80,11 +80,11 @@ class PackageView(BaseView):
package = Package.from_json(data["package"]) if "package" in data else None
status = BuildStatusEnum(data["status"])
except Exception as e:
- raise HTTPBadRequest(text=str(e))
+ return json_response(text=str(e), status=400)
try:
self.service.update(base, status, package)
except UnknownPackage:
- raise HTTPBadRequest(text=f"Package {base} is unknown, but no package body set")
+ return json_response(text=f"Package {base} is unknown, but no package body set", status=400)
return HTTPNoContent()
diff --git a/src/ahriman/web/views/packages.py b/src/ahriman/web/views/status/packages.py
similarity index 100%
rename from src/ahriman/web/views/packages.py
rename to src/ahriman/web/views/status/packages.py
diff --git a/src/ahriman/web/views/status.py b/src/ahriman/web/views/status/status.py
similarity index 100%
rename from src/ahriman/web/views/status.py
rename to src/ahriman/web/views/status/status.py
diff --git a/src/ahriman/web/views/user/__init__.py b/src/ahriman/web/views/user/__init__.py
new file mode 100644
index 00000000..fb32931e
--- /dev/null
+++ b/src/ahriman/web/views/user/__init__.py
@@ -0,0 +1,19 @@
+#
+# Copyright (c) 2021 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 .
+#
diff --git a/src/ahriman/web/views/login.py b/src/ahriman/web/views/user/login.py
similarity index 100%
rename from src/ahriman/web/views/login.py
rename to src/ahriman/web/views/user/login.py
diff --git a/src/ahriman/web/views/logout.py b/src/ahriman/web/views/user/logout.py
similarity index 100%
rename from src/ahriman/web/views/logout.py
rename to src/ahriman/web/views/user/logout.py
diff --git a/tests/ahriman/application/conftest.py b/tests/ahriman/application/conftest.py
index bafd1f3e..872031d6 100644
--- a/tests/ahriman/application/conftest.py
+++ b/tests/ahriman/application/conftest.py
@@ -1,5 +1,4 @@
import argparse
-import aur
import pytest
from pytest_mock import MockerFixture
@@ -8,7 +7,6 @@ from ahriman.application.ahriman import _parser
from ahriman.application.application import Application
from ahriman.application.lock import Lock
from ahriman.core.configuration import Configuration
-from ahriman.models.package import Package
@pytest.fixture
@@ -32,31 +30,6 @@ def args() -> argparse.Namespace:
return argparse.Namespace(lock=None, force=False, unsafe=False, no_report=True)
-@pytest.fixture
-def aur_package_ahriman(package_ahriman: Package) -> aur.Package:
- """
- fixture for AUR package
- :param package_ahriman: package fixture
- :return: AUR package test instance
- """
- return aur.Package(
- num_votes=None,
- description=package_ahriman.packages[package_ahriman.base].description,
- url_path=package_ahriman.web_url,
- last_modified=None,
- name=package_ahriman.base,
- out_of_date=None,
- id=None,
- first_submitted=None,
- maintainer=None,
- version=package_ahriman.version,
- license=package_ahriman.packages[package_ahriman.base].licenses,
- url=None,
- package_base=package_ahriman.base,
- package_base_id=None,
- category_id=None)
-
-
@pytest.fixture
def lock(args: argparse.Namespace, configuration: Configuration) -> Lock:
"""
diff --git a/tests/ahriman/conftest.py b/tests/ahriman/conftest.py
index b82f80c1..91a0304c 100644
--- a/tests/ahriman/conftest.py
+++ b/tests/ahriman/conftest.py
@@ -1,3 +1,4 @@
+import aur
import pytest
from pathlib import Path
@@ -46,6 +47,31 @@ def anyvar(cls: Type[T], strict: bool = False) -> T:
# generic fixtures
+@pytest.fixture
+def aur_package_ahriman(package_ahriman: Package) -> aur.Package:
+ """
+ fixture for AUR package
+ :param package_ahriman: package fixture
+ :return: AUR package test instance
+ """
+ return aur.Package(
+ num_votes=None,
+ description=package_ahriman.packages[package_ahriman.base].description,
+ url_path=package_ahriman.web_url,
+ last_modified=None,
+ name=package_ahriman.base,
+ out_of_date=None,
+ id=None,
+ first_submitted=None,
+ maintainer=None,
+ version=package_ahriman.version,
+ license=package_ahriman.packages[package_ahriman.base].licenses,
+ url=None,
+ package_base=package_ahriman.base,
+ package_base_id=None,
+ category_id=None)
+
+
@pytest.fixture
def auth(configuration: Configuration) -> Auth:
"""
diff --git a/tests/ahriman/web/views/service/test_views_service_add.py b/tests/ahriman/web/views/service/test_views_service_add.py
new file mode 100644
index 00000000..4a24c6b9
--- /dev/null
+++ b/tests/ahriman/web/views/service/test_views_service_add.py
@@ -0,0 +1,35 @@
+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"], False)
+
+
+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": True})
+
+ assert response.status == 200
+ add_mock.assert_called_with(["ahriman"], True)
+
+
+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()
diff --git a/tests/ahriman/web/views/service/test_views_service_remove.py b/tests/ahriman/web/views/service/test_views_service_remove.py
new file mode 100644
index 00000000..d7c45d80
--- /dev/null
+++ b/tests/ahriman/web/views/service/test_views_service_remove.py
@@ -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()
diff --git a/tests/ahriman/web/views/service/test_views_service_search.py b/tests/ahriman/web/views/service/test_views_service_search.py
new file mode 100644
index 00000000..bfd3158d
--- /dev/null
+++ b/tests/ahriman/web/views/service/test_views_service_search.py
@@ -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()
diff --git a/tests/ahriman/web/views/service/test_views_service_update.py b/tests/ahriman/web/views/service/test_views_service_update.py
new file mode 100644
index 00000000..6e9a3b7b
--- /dev/null
+++ b/tests/ahriman/web/views/service/test_views_service_update.py
@@ -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_update")
+ response = await client.post("/service-api/v1/update", 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_update")
+ response = await client.post("/service-api/v1/update")
+
+ assert response.status == 400
+ add_mock.assert_not_called()
diff --git a/tests/ahriman/web/views/test_view_ahriman.py b/tests/ahriman/web/views/status/test_views_status_ahriman.py
similarity index 100%
rename from tests/ahriman/web/views/test_view_ahriman.py
rename to tests/ahriman/web/views/status/test_views_status_ahriman.py
diff --git a/tests/ahriman/web/views/test_view_package.py b/tests/ahriman/web/views/status/test_views_status_package.py
similarity index 100%
rename from tests/ahriman/web/views/test_view_package.py
rename to tests/ahriman/web/views/status/test_views_status_package.py
diff --git a/tests/ahriman/web/views/test_view_packages.py b/tests/ahriman/web/views/status/test_views_status_packages.py
similarity index 100%
rename from tests/ahriman/web/views/test_view_packages.py
rename to tests/ahriman/web/views/status/test_views_status_packages.py
diff --git a/tests/ahriman/web/views/test_view_status.py b/tests/ahriman/web/views/status/test_views_status_status.py
similarity index 100%
rename from tests/ahriman/web/views/test_view_status.py
rename to tests/ahriman/web/views/status/test_views_status_status.py
diff --git a/tests/ahriman/web/views/test_view_base.py b/tests/ahriman/web/views/test_views_base.py
similarity index 79%
rename from tests/ahriman/web/views/test_view_base.py
rename to tests/ahriman/web/views/test_views_base.py
index 74ae0dba..e0ff3117 100644
--- a/tests/ahriman/web/views/test_view_base.py
+++ b/tests/ahriman/web/views/test_views_base.py
@@ -61,9 +61,6 @@ async def test_data_as_json(base: BaseView) -> None:
"""
json = {"key1": "value1", "key2": ["value2", "value3"], "key3": ["value4", "value5", "value6"]}
- async def get_json():
- raise ValueError()
-
async def get_data():
result = MultiDict()
for key, values in json.items():
@@ -74,5 +71,18 @@ async def test_data_as_json(base: BaseView) -> None:
result.add(key, values)
return result
- base._request = pytest.helpers.request(base.request.app, "", "", json=get_json, data=get_data)
- assert await base.data_as_json() == json
+ 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"]}
diff --git a/tests/ahriman/web/views/test_view_index.py b/tests/ahriman/web/views/test_views_index.py
similarity index 100%
rename from tests/ahriman/web/views/test_view_index.py
rename to tests/ahriman/web/views/test_views_index.py
diff --git a/tests/ahriman/web/views/test_view_login.py b/tests/ahriman/web/views/user/test_views_user_login.py
similarity index 100%
rename from tests/ahriman/web/views/test_view_login.py
rename to tests/ahriman/web/views/user/test_views_user_login.py
diff --git a/tests/ahriman/web/views/test_view_logout.py b/tests/ahriman/web/views/user/test_views_user_logout.py
similarity index 100%
rename from tests/ahriman/web/views/test_view_logout.py
rename to tests/ahriman/web/views/user/test_views_user_logout.py