Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/fessctl/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,13 @@ def send_request(
response = httpx.delete(
url, headers=headers, params=params, timeout=self.timeout
)
elif action == Action.LIST or action == Action.GET:
elif action == Action.LIST:
# Fess admin list endpoints bind size, page and filters from the
# JSON request body and ignore the query string, so send both.
response = httpx.request(
"GET", url, headers=headers, params=params, json=params, timeout=self.timeout
)
elif action == Action.GET:
response = httpx.get(
url, headers=headers, params=params, timeout=self.timeout
)
Expand Down
26 changes: 26 additions & 0 deletions tests/commands/test_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,32 @@ def test_scheduler_crud_flow(runner, fess_service):
assert "failed to retrieve scheduler" in result.stdout.lower()


def test_scheduler_list_size_and_page(runner, fess_service):
"""
Tests that --size limits the rows returned and --page moves through them.
A fresh Fess ships with several built-in jobs, so there is more than one row.
"""
result = runner.invoke(
scheduler_app,
["list", "--size", "1", "--page", "1", "--output", "json"]
)
assert result.exit_code == 0, f"List page 1 failed: {result.stdout}"
page1 = json.loads(result.stdout)["response"]
assert page1.get("status") == 0
assert page1.get("total", 0) > 1, "Expected more than one scheduler on a fresh Fess"
assert len(page1["settings"]) == 1

result = runner.invoke(
scheduler_app,
["list", "--size", "1", "--page", "2", "--output", "json"]
)
assert result.exit_code == 0, f"List page 2 failed: {result.stdout}"
page2 = json.loads(result.stdout)["response"]
assert page2.get("status") == 0
assert len(page2["settings"]) == 1
assert page2["settings"][0]["id"] != page1["settings"][0]["id"]


def test_scheduler_start_stop_json_output(runner, fess_service):
"""
Tests the start and stop commands return valid JSON responses.
Expand Down
71 changes: 63 additions & 8 deletions tests/unit/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Unit tests for fessctl.api.client module.
"""
import json
from types import SimpleNamespace
from unittest.mock import Mock, patch, MagicMock

import httpx
Expand Down Expand Up @@ -201,16 +202,17 @@ def test_delete_uses_delete(self, mock_delete, client):

mock_delete.assert_called_once()

@patch("httpx.get")
def test_list_uses_get(self, mock_get, client):
@patch("httpx.request")
def test_list_uses_get(self, mock_request, client):
"""Test that LIST action uses GET method."""
mock_response = Mock()
mock_response.json.return_value = {"response": {"status": 0}}
mock_get.return_value = mock_response
mock_request.return_value = mock_response

client.send_request(Action.LIST, "http://test/api")

mock_get.assert_called_once()
mock_request.assert_called_once()
assert mock_request.call_args[0][0] == "GET"

@patch("httpx.get")
def test_get_uses_get(self, mock_get, client):
Expand Down Expand Up @@ -268,6 +270,59 @@ def test_stop_fess14_uses_post(self, mock_post, client_v14):
mock_post.assert_called_once()


LIST_METHODS = sorted(
name for name in dir(FessAPIClient) if name.startswith("list_"))


@pytest.fixture
def http():
"""Patch httpx.get and httpx.request, both returning a successful response."""
response = Mock()
response.json.return_value = {"response": {"status": 0}}
with patch("httpx.get", return_value=response) as mock_get, \
patch("httpx.request", return_value=response) as mock_request:
yield SimpleNamespace(get=mock_get, request=mock_request)


class TestListPaging:
"""Fess admin list endpoints bind size, page and filters from the JSON request body."""

def test_list_sends_params_in_json_body_and_query_string(self, client, http):
params = {"page": 2, "size": 1}

client.send_request(Action.LIST, "http://test/api", params=params)

http.get.assert_not_called()
http.request.assert_called_once()
assert http.request.call_args[0] == ("GET", "http://test/api")
call_kwargs = http.request.call_args[1]
assert call_kwargs["json"] == {"page": 2, "size": 1}
assert call_kwargs["params"] == {"page": 2, "size": 1}

def test_list_without_params_sends_no_body(self, client, http):
client.send_request(Action.LIST, "http://test/api")

call_kwargs = http.request.call_args[1]
assert call_kwargs["json"] is None
assert call_kwargs["params"] is None

def test_get_carries_no_body(self, client, http):
client.send_request(Action.GET, "http://test/api/role/abc")

http.request.assert_not_called()
http.get.assert_called_once()
assert "json" not in http.get.call_args[1]

@pytest.mark.parametrize("method", LIST_METHODS)
def test_every_list_method_puts_paging_in_body(self, client, http, method):
getattr(client, method)(page=2, size=1)

http.get.assert_not_called()
call_kwargs = http.request.call_args[1]
assert call_kwargs["json"] == {"page": 2, "size": 1}
assert call_kwargs["params"] == {"page": 2, "size": 1}


class TestSendRequestErrorHandling:
"""Tests for error handling in send_request."""

Expand Down Expand Up @@ -533,16 +588,16 @@ def test_get_role(self, mock_get, client):
call_url = mock_get.call_args[0][0]
assert "role-123" in call_url

@patch("httpx.get")
def test_list_roles_with_pagination(self, mock_get, client):
@patch("httpx.request")
def test_list_roles_with_pagination(self, mock_request, client):
"""Test listing roles with pagination."""
mock_response = Mock()
mock_response.json.return_value = {"response": {"status": 0, "settings": []}}
mock_get.return_value = mock_response
mock_request.return_value = mock_response

client.list_roles(page=2, size=50)

call_kwargs = mock_get.call_args[1]
call_kwargs = mock_request.call_args[1]
assert call_kwargs["params"]["page"] == 2
assert call_kwargs["params"]["size"] == 50

Expand Down
Loading