From 60246dd833ca2675b328ecb456c73c6cfcda7e60 Mon Sep 17 00:00:00 2001 From: Evgeniy Alekseev Date: Wed, 8 Sep 2021 20:00:41 +0300 Subject: [PATCH] provide service api endpoints --- src/ahriman/web/routes.py | 40 +++++++++---- src/ahriman/web/views/base.py | 12 ++-- src/ahriman/web/views/service/__init__.py | 19 ++++++ src/ahriman/web/views/service/add.py | 52 ++++++++++++++++ src/ahriman/web/views/service/remove.py | 50 ++++++++++++++++ src/ahriman/web/views/service/search.py | 48 +++++++++++++++ src/ahriman/web/views/service/update.py | 50 ++++++++++++++++ src/ahriman/web/views/status/__init__.py | 19 ++++++ src/ahriman/web/views/{ => status}/ahriman.py | 4 +- src/ahriman/web/views/{ => status}/package.py | 6 +- .../web/views/{ => status}/packages.py | 0 src/ahriman/web/views/{ => status}/status.py | 0 src/ahriman/web/views/user/__init__.py | 19 ++++++ src/ahriman/web/views/{ => user}/login.py | 0 src/ahriman/web/views/{ => user}/logout.py | 0 tests/ahriman/application/conftest.py | 27 --------- tests/ahriman/conftest.py | 26 ++++++++ .../views/service/test_views_service_add.py | 35 +++++++++++ .../service/test_views_service_remove.py | 24 ++++++++ .../service/test_views_service_search.py | 59 +++++++++++++++++++ .../service/test_views_service_update.py | 24 ++++++++ .../test_views_status_ahriman.py} | 0 .../test_views_status_package.py} | 0 .../test_views_status_packages.py} | 0 .../test_views_status_status.py} | 0 .../{test_view_base.py => test_views_base.py} | 20 +++++-- ...test_view_index.py => test_views_index.py} | 0 .../test_views_user_login.py} | 0 .../test_views_user_logout.py} | 0 29 files changed, 483 insertions(+), 51 deletions(-) create mode 100644 src/ahriman/web/views/service/__init__.py create mode 100644 src/ahriman/web/views/service/add.py create mode 100644 src/ahriman/web/views/service/remove.py create mode 100644 src/ahriman/web/views/service/search.py create mode 100644 src/ahriman/web/views/service/update.py create mode 100644 src/ahriman/web/views/status/__init__.py rename src/ahriman/web/views/{ => status}/ahriman.py (92%) rename src/ahriman/web/views/{ => status}/package.py (91%) rename src/ahriman/web/views/{ => status}/packages.py (100%) rename src/ahriman/web/views/{ => status}/status.py (100%) create mode 100644 src/ahriman/web/views/user/__init__.py rename src/ahriman/web/views/{ => user}/login.py (100%) rename src/ahriman/web/views/{ => user}/logout.py (100%) create mode 100644 tests/ahriman/web/views/service/test_views_service_add.py create mode 100644 tests/ahriman/web/views/service/test_views_service_remove.py create mode 100644 tests/ahriman/web/views/service/test_views_service_search.py create mode 100644 tests/ahriman/web/views/service/test_views_service_update.py rename tests/ahriman/web/views/{test_view_ahriman.py => status/test_views_status_ahriman.py} (100%) rename tests/ahriman/web/views/{test_view_package.py => status/test_views_status_package.py} (100%) rename tests/ahriman/web/views/{test_view_packages.py => status/test_views_status_packages.py} (100%) rename tests/ahriman/web/views/{test_view_status.py => status/test_views_status_status.py} (100%) rename tests/ahriman/web/views/{test_view_base.py => test_views_base.py} (79%) rename tests/ahriman/web/views/{test_view_index.py => test_views_index.py} (100%) rename tests/ahriman/web/views/{test_view_login.py => user/test_views_user_login.py} (100%) rename tests/ahriman/web/views/{test_view_logout.py => user/test_views_user_logout.py} (100%) 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