diff --git a/src/ahriman/application/handlers/search.py b/src/ahriman/application/handlers/search.py index a999dc53..96b1c3f4 100644 --- a/src/ahriman/application/handlers/search.py +++ b/src/ahriman/application/handlers/search.py @@ -20,12 +20,13 @@ import argparse import aur # type: ignore -from typing import Callable, Dict, Iterable, List, Tuple, Type +from typing import Callable, Iterable, List, Tuple, Type from ahriman.application.formatters.aur_printer import AurPrinter from ahriman.application.handlers.handler import Handler from ahriman.core.configuration import Configuration from ahriman.core.exceptions import InvalidOption +from ahriman.core.util import aur_search class Search(Handler): @@ -46,17 +47,7 @@ class Search(Handler): :param configuration: configuration instance :param no_report: force disable reporting """ - packages: Dict[str, aur.Package] = {} - # see https://bugs.archlinux.org/task/49133 - for search in args.search: - portion = aur.search(search) - packages = { - package.package_base: package - for package in portion - if package.package_base in packages or not packages - } - - packages_list = list(packages.values()) # explicit conversion for the tests + packages_list = aur_search(*args.search) for package in Search.sort(packages_list, args.sort_by): AurPrinter(package).print(args.info) diff --git a/src/ahriman/core/util.py b/src/ahriman/core/util.py index bed5aa65..0c5f928b 100644 --- a/src/ahriman/core/util.py +++ b/src/ahriman/core/util.py @@ -17,6 +17,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # +import aur # type: ignore import datetime import os import subprocess @@ -24,11 +25,29 @@ import requests from logging import Logger from pathlib import Path -from typing import Any, Dict, Generator, Iterable, Optional, Union +from typing import Any, Dict, Generator, Iterable, List, Optional, Union from ahriman.core.exceptions import InvalidOption, UnsafeRun +def aur_search(*terms: str) -> List[aur.Package]: + """ + search in AUR by using API with multiple words. This method is required in order to handle + https://bugs.archlinux.org/task/49133. In addition short words will be dropped + :param terms: search terms, e.g. "ahriman", "is", "cool" + :return: list of packages each of them matches all search terms + """ + packages: Dict[str, aur.Package] = {} + for term in filter(lambda word: len(word) > 3, terms): + portion = aur.search(term) + packages = { + package.package_base: package + for package in portion + if package.package_base in packages or not packages + } + return list(packages.values()) + + def check_output(*args: str, exception: Optional[Exception], cwd: Optional[Path] = None, input_data: Optional[str] = None, logger: Optional[Logger] = None) -> str: """ diff --git a/src/ahriman/web/views/service/search.py b/src/ahriman/web/views/service/search.py index 863f021d..1df3b9b8 100644 --- a/src/ahriman/web/views/service/search.py +++ b/src/ahriman/web/views/service/search.py @@ -19,9 +19,10 @@ # import aur # type: ignore -from aiohttp.web import HTTPBadRequest, Response, json_response -from typing import Callable, Iterator +from aiohttp.web import HTTPNotFound, Response, json_response +from typing import Callable, List +from ahriman.core.util import aur_search from ahriman.models.user_access import UserAccess from ahriman.web.views.base import BaseView @@ -43,12 +44,10 @@ class SearchView(BaseView): :return: 200 with found package bases and descriptions sorted by base """ - search: Iterator[str] = filter(lambda s: len(s) > 3, self.request.query.getall("for", default=[])) - search_string = " ".join(search) - - if not search_string: - raise HTTPBadRequest(reason="Search string must not be empty") - packages = aur.search(search_string) + search: List[str] = self.request.query.getall("for", default=[]) + packages = aur_search(*search) + if not packages: + raise HTTPNotFound(reason=f"No packages found for terms: {search}") comparator: Callable[[aur.Package], str] = lambda item: str(item.package_base) response = [ diff --git a/tests/ahriman/application/handlers/test_handler_search.py b/tests/ahriman/application/handlers/test_handler_search.py index fc836c86..8300e0b9 100644 --- a/tests/ahriman/application/handlers/test_handler_search.py +++ b/tests/ahriman/application/handlers/test_handler_search.py @@ -3,7 +3,6 @@ import aur import pytest from pytest_mock import MockerFixture -from unittest import mock from ahriman.application.handlers import Search from ahriman.core.configuration import Configuration @@ -28,33 +27,21 @@ def test_run(args: argparse.Namespace, configuration: Configuration, aur_package must run command """ args = _default_args(args) - mocker.patch("aur.search", return_value=[aur_package_ahriman]) + search_mock = mocker.patch("ahriman.application.handlers.search.aur_search", return_value=[aur_package_ahriman]) print_mock = mocker.patch("ahriman.application.formatters.printer.Printer.print") Search.run(args, "x86_64", configuration, True) + search_mock.assert_called_once_with("ahriman") print_mock.assert_called_once() -def test_run_multiple_search(args: argparse.Namespace, configuration: Configuration, aur_package_ahriman: aur.Package, - mocker: MockerFixture) -> None: - """ - must run command with multiple search arguments - """ - args = _default_args(args) - args.search = ["ahriman", "is", "cool"] - search_mock = mocker.patch("aur.search", return_value=[aur_package_ahriman]) - - Search.run(args, "x86_64", configuration, True) - search_mock.assert_has_calls([mock.call(term) for term in args.search]) - - def test_run_sort(args: argparse.Namespace, configuration: Configuration, aur_package_ahriman: aur.Package, mocker: MockerFixture) -> None: """ must run command with sorting """ args = _default_args(args) - mocker.patch("aur.search", return_value=[aur_package_ahriman]) + mocker.patch("ahriman.application.handlers.search.aur_search", return_value=[aur_package_ahriman]) sort_mock = mocker.patch("ahriman.application.handlers.search.Search.sort") Search.run(args, "x86_64", configuration, True) @@ -68,7 +55,7 @@ def test_run_sort_by(args: argparse.Namespace, configuration: Configuration, aur """ args = _default_args(args) args.sort_by = "field" - mocker.patch("aur.search", return_value=[aur_package_ahriman]) + mocker.patch("ahriman.application.handlers.search.aur_search", return_value=[aur_package_ahriman]) sort_mock = mocker.patch("ahriman.application.handlers.search.Search.sort") Search.run(args, "x86_64", configuration, True) diff --git a/tests/ahriman/core/test_util.py b/tests/ahriman/core/test_util.py index 432e4a7f..98b48d2f 100644 --- a/tests/ahriman/core/test_util.py +++ b/tests/ahriman/core/test_util.py @@ -1,3 +1,4 @@ +import aur import datetime import logging import pytest @@ -5,12 +6,45 @@ import subprocess from pathlib import Path from pytest_mock import MockerFixture +from unittest import mock from ahriman.core.exceptions import InvalidOption, UnsafeRun -from ahriman.core.util import check_output, check_user, filter_json, package_like, pretty_datetime, pretty_size, walk +from ahriman.core.util import aur_search, check_output, check_user, filter_json, package_like, pretty_datetime, \ + pretty_size, walk from ahriman.models.package import Package +def test_aur_search(aur_package_ahriman: aur.Package, mocker: MockerFixture) -> None: + """ + must search in AUR with multiple words + """ + terms = ["ahriman", "is", "cool"] + search_mock = mocker.patch("aur.search", return_value=[aur_package_ahriman]) + + assert aur_search(*terms) == [aur_package_ahriman] + search_mock.assert_has_calls([mock.call("ahriman"), mock.call("cool")]) + + +def test_aur_search_empty(mocker: MockerFixture) -> None: + """ + must return empty list if no long terms supplied + """ + terms = ["it", "is"] + search_mock = mocker.patch("aur.search") + + assert aur_search(*terms) == [] + search_mock.assert_not_called() + + +def test_aur_search_single(aur_package_ahriman: aur.Package, mocker: MockerFixture) -> None: + """ + must search in AUR with one word + """ + search_mock = mocker.patch("aur.search", return_value=[aur_package_ahriman]) + assert aur_search("ahriman") == [aur_package_ahriman] + search_mock.assert_called_once_with("ahriman") + + def test_check_output(mocker: MockerFixture) -> None: """ must run command and log result diff --git a/tests/ahriman/web/views/service/test_views_service_search.py b/tests/ahriman/web/views/service/test_views_service_search.py index 2296d91c..b2c37964 100644 --- a/tests/ahriman/web/views/service/test_views_service_search.py +++ b/tests/ahriman/web/views/service/test_views_service_search.py @@ -21,7 +21,7 @@ async def test_get(client: TestClient, aur_package_ahriman: aur.Package, mocker: """ must call get request correctly """ - mocker.patch("aur.search", return_value=[aur_package_ahriman]) + mocker.patch("ahriman.web.views.service.search.aur_search", return_value=[aur_package_ahriman]) response = await client.get("/service-api/v1/search", params={"for": "ahriman"}) assert response.ok @@ -33,41 +33,19 @@ async def test_get_exception(client: TestClient, mocker: MockerFixture) -> None: """ must raise 400 on empty search string """ - search_mock = mocker.patch("aur.search") + search_mock = mocker.patch("ahriman.web.views.service.search.aur_search", return_value=[]) response = await client.get("/service-api/v1/search") - assert response.status == 400 - search_mock.assert_not_called() + assert response.status == 404 + search_mock.assert_called_once_with() async def test_get_join(client: TestClient, mocker: MockerFixture) -> None: """ must join search args with space """ - search_mock = mocker.patch("aur.search") + search_mock = mocker.patch("ahriman.web.views.service.search.aur_search") response = await client.get("/service-api/v1/search", params=[("for", "ahriman"), ("for", "maybe")]) assert response.ok - search_mock.assert_called_once_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.ok - search_mock.assert_called_once_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() + search_mock.assert_called_once_with("ahriman", "maybe")