mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-09-28 19:55:06 +01:00
`POST /api/requests/batch` checks that `requests` is a non-empty list and then hands each element to the shared preparation helper, which calls `.get()` on it. A bare string, number or null in the list raises `AttributeError` and the caller gets a 500, while `POST /api/requests` answers 400 with a message for the same mistake. This validates the element type beside the existing list check. One bad item rejects the whole batch rather than being reported per item, which matches the endpoint's current contract: every other failure path already aborts the batch with a single error body, as `test_batch_create_requests_is_atomic` asserts. Responses for valid payloads are unchanged. ## Verification - `tests/core/test_request_routes_api.py`: a case per bad shape (int, str, null, list), plus a mixed valid and invalid batch that asserts nothing was created. All fail on current main with a 500 and pass here. - Full suite (3168), ruff, ruff format, basedpyright, vulture green.
3245 lines
138 KiB
Python
3245 lines
138 KiB
Python
"""API tests for request routes and policy enforcement guards."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
import uuid
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from shelfmark.core.notifications import NotificationEvent
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def main_module():
|
|
"""Import `shelfmark.main` with background startup disabled."""
|
|
with patch("shelfmark.download.orchestrator.start"):
|
|
import shelfmark.main as main
|
|
|
|
importlib.reload(main)
|
|
return main
|
|
|
|
|
|
@pytest.fixture
|
|
def client(main_module):
|
|
return main_module.app.test_client()
|
|
|
|
|
|
def _set_session(client, *, user_id: str, db_user_id: int | None, is_admin: bool) -> None:
|
|
with client.session_transaction() as sess:
|
|
sess["user_id"] = user_id
|
|
sess["is_admin"] = is_admin
|
|
if db_user_id is not None:
|
|
sess["db_user_id"] = db_user_id
|
|
elif "db_user_id" in sess:
|
|
del sess["db_user_id"]
|
|
|
|
|
|
def _create_user(main_module, *, prefix: str, role: str = "user") -> dict:
|
|
username = f"{prefix}-{uuid.uuid4().hex[:8]}"
|
|
return main_module.user_db.create_user(username=username, role=role)
|
|
|
|
|
|
def _policy(
|
|
*,
|
|
requests_enabled: bool = True,
|
|
default_ebook: str = "download",
|
|
default_audiobook: str = "download",
|
|
max_pending_requests_per_user: int = 20,
|
|
requests_allow_notes: bool = True,
|
|
rules: list[dict] | None = None,
|
|
) -> dict:
|
|
return {
|
|
"REQUESTS_ENABLED": requests_enabled,
|
|
"REQUEST_POLICY_DEFAULT_EBOOK": default_ebook,
|
|
"REQUEST_POLICY_DEFAULT_AUDIOBOOK": default_audiobook,
|
|
"MAX_PENDING_REQUESTS_PER_USER": max_pending_requests_per_user,
|
|
"REQUESTS_ALLOW_NOTES": requests_allow_notes,
|
|
"REQUEST_POLICY_RULES": rules or [],
|
|
}
|
|
|
|
|
|
def _assert_emit_call(mock_emit, index: int, event: str, payload: dict, room: str) -> None:
|
|
call = mock_emit.call_args_list[index]
|
|
assert call.args == (event, payload)
|
|
assert call.kwargs == {"to": room}
|
|
|
|
|
|
class TestDownloadPolicyGuards:
|
|
def test_release_download_endpoint_blocks_before_queue_when_policy_requires_request(
|
|
self, main_module, client
|
|
):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module,
|
|
"load_users_request_policy_settings",
|
|
return_value=_policy(default_ebook="request_release"),
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=_policy(default_ebook="request_release"),
|
|
):
|
|
with patch.object(main_module.backend, "queue_release") as mock_queue_release:
|
|
resp = client.post(
|
|
"/api/releases/download",
|
|
json={
|
|
"source": "direct_download",
|
|
"source_id": "book-123",
|
|
"search_mode": "direct",
|
|
},
|
|
)
|
|
|
|
assert resp.status_code == 403
|
|
assert resp.json["code"] == "policy_requires_request"
|
|
assert resp.json["required_mode"] == "request_release"
|
|
mock_queue_release.assert_not_called()
|
|
|
|
def test_release_download_endpoint_applies_policy_when_session_lacks_is_admin(
|
|
self, main_module, client
|
|
):
|
|
user = _create_user(main_module, prefix="reader")
|
|
with client.session_transaction() as sess:
|
|
sess["user_id"] = user["username"]
|
|
sess["db_user_id"] = user["id"]
|
|
sess.pop("is_admin", None)
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module,
|
|
"load_users_request_policy_settings",
|
|
return_value=_policy(default_ebook="request_release"),
|
|
):
|
|
with patch.object(
|
|
main_module.backend, "queue_release", return_value=(True, None)
|
|
) as mock_queue_release:
|
|
resp = client.post(
|
|
"/api/releases/download",
|
|
json={
|
|
"source": "direct_download",
|
|
"source_id": "book-123",
|
|
"search_mode": "direct",
|
|
},
|
|
)
|
|
|
|
assert resp.status_code == 403
|
|
assert resp.json["code"] == "policy_requires_request"
|
|
assert resp.json["required_mode"] == "request_release"
|
|
mock_queue_release.assert_not_called()
|
|
|
|
def test_release_download_endpoint_blocks_before_queue_when_policy_blocked(
|
|
self, main_module, client
|
|
):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module,
|
|
"load_users_request_policy_settings",
|
|
return_value=_policy(default_ebook="blocked"),
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=_policy(default_ebook="blocked"),
|
|
):
|
|
with patch.object(main_module.backend, "queue_release") as mock_queue_release:
|
|
resp = client.post(
|
|
"/api/releases/download",
|
|
json={
|
|
"source": "direct_download",
|
|
"source_id": "rel-1",
|
|
"content_type": "ebook",
|
|
},
|
|
)
|
|
|
|
assert resp.status_code == 403
|
|
assert resp.json["code"] == "policy_blocked"
|
|
assert resp.json["required_mode"] == "blocked"
|
|
mock_queue_release.assert_not_called()
|
|
|
|
def test_admin_bypasses_policy_guards(self, main_module, client):
|
|
admin = _create_user(main_module, prefix="admin", role="admin")
|
|
_set_session(client, user_id=admin["username"], db_user_id=admin["id"], is_admin=True)
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module,
|
|
"load_users_request_policy_settings",
|
|
return_value=_policy(default_ebook="blocked"),
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=_policy(default_ebook="blocked"),
|
|
):
|
|
with patch.object(
|
|
main_module.backend, "queue_release", return_value=(True, None)
|
|
) as mock_queue_release:
|
|
resp = client.post(
|
|
"/api/releases/download",
|
|
json={
|
|
"source": "direct_download",
|
|
"source_id": "book-123",
|
|
"search_mode": "direct",
|
|
},
|
|
)
|
|
|
|
assert resp.status_code == 200
|
|
assert resp.json["status"] == "queued"
|
|
mock_queue_release.assert_called_once()
|
|
|
|
def test_no_auth_mode_bypasses_policy_guards(self, main_module, client):
|
|
with patch.object(main_module, "get_auth_mode", return_value="none"):
|
|
with patch.object(
|
|
main_module,
|
|
"load_users_request_policy_settings",
|
|
return_value=_policy(default_ebook="blocked"),
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=_policy(default_ebook="blocked"),
|
|
):
|
|
with patch.object(
|
|
main_module.backend, "queue_release", return_value=(True, None)
|
|
) as mock_queue_release:
|
|
resp = client.post(
|
|
"/api/releases/download",
|
|
json={
|
|
"source": "direct_download",
|
|
"source_id": "book-123",
|
|
"search_mode": "direct",
|
|
},
|
|
)
|
|
|
|
assert resp.status_code == 200
|
|
assert resp.json["status"] == "queued"
|
|
mock_queue_release.assert_called_once()
|
|
|
|
|
|
class TestRequestRoutes:
|
|
def test_request_endpoints_are_unavailable_in_no_auth_mode(self, main_module, client):
|
|
with patch.object(main_module, "get_auth_mode", return_value="none"):
|
|
resp = client.get("/api/requests")
|
|
|
|
assert resp.status_code == 403
|
|
assert resp.json["code"] == "requests_unavailable"
|
|
|
|
def test_request_policy_endpoint_returns_effective_policy(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(default_ebook="request_release")
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
resp = client.get("/api/request-policy")
|
|
|
|
assert resp.status_code == 200
|
|
assert resp.json["requests_enabled"] is True
|
|
assert resp.json["defaults"]["ebook"] == "request_release"
|
|
assert "source_modes" in resp.json
|
|
|
|
def test_request_policy_endpoint_normalizes_direct_request_book_to_request_release(
|
|
self, main_module, client
|
|
):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(default_ebook="request_book")
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.get_source_content_type_capabilities",
|
|
return_value={"direct_download": {"ebook"}},
|
|
):
|
|
resp = client.get("/api/request-policy")
|
|
|
|
assert resp.status_code == 200
|
|
assert resp.json["defaults"]["ebook"] == "request_book"
|
|
assert resp.json["source_modes"][0]["source"] == "direct_download"
|
|
assert resp.json["source_modes"][0]["modes"]["ebook"] == "request_release"
|
|
|
|
def test_create_list_and_cancel_request(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(default_ebook="request_book")
|
|
|
|
payload = {
|
|
"book_data": {
|
|
"title": "The Pragmatic Programmer",
|
|
"author": "Andrew Hunt",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": "ol-1",
|
|
},
|
|
"context": {
|
|
"source": "direct_download",
|
|
"content_type": "ebook",
|
|
"request_level": "book",
|
|
},
|
|
"note": "Please add this",
|
|
}
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
create_resp = client.post("/api/requests", json=payload)
|
|
list_resp = client.get("/api/requests")
|
|
|
|
assert create_resp.status_code == 201
|
|
request_id = create_resp.json["id"]
|
|
assert create_resp.json["status"] == "pending"
|
|
assert any(item["id"] == request_id for item in list_resp.json)
|
|
|
|
cancel_resp = client.delete(f"/api/requests/{request_id}")
|
|
|
|
assert cancel_resp.status_code == 200
|
|
assert cancel_resp.json["status"] == "cancelled"
|
|
updated = main_module.user_db.get_request(request_id)
|
|
assert updated is not None
|
|
assert updated["user_id"] == user["id"]
|
|
assert updated["status"] == "cancelled"
|
|
|
|
def test_download_policy_queues_release_without_creating_request(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(default_ebook="download")
|
|
|
|
payload = {
|
|
"book_data": {
|
|
"title": "Policy Download",
|
|
"author": "Shelfmark",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": "policy-download-1",
|
|
},
|
|
"context": {
|
|
"source": "prowlarr",
|
|
"content_type": "ebook",
|
|
"request_level": "release",
|
|
},
|
|
"release_data": {
|
|
"source": "prowlarr",
|
|
"source_id": "policy-download-release-1",
|
|
"title": "Policy Download.epub",
|
|
},
|
|
}
|
|
|
|
captured: dict[str, object] = {}
|
|
|
|
def fake_queue_release(release_data, priority, user_id=None, username=None):
|
|
captured["release_data"] = release_data
|
|
captured["priority"] = priority
|
|
captured["user_id"] = user_id
|
|
captured["username"] = username
|
|
return True, None
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
with patch.object(
|
|
main_module.backend, "queue_release", side_effect=fake_queue_release
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.notify_admin"
|
|
) as mock_notify_admin:
|
|
with patch(
|
|
"shelfmark.core.request_routes.notify_user"
|
|
) as mock_notify_user:
|
|
resp = client.post("/api/requests", json=payload)
|
|
|
|
assert resp.status_code == 200
|
|
assert resp.json["kind"] == "download"
|
|
assert resp.json["status"] == "queued"
|
|
assert resp.json["title"] == "Policy Download"
|
|
assert resp.json["source"] == "prowlarr"
|
|
assert resp.json["source_id"] == "policy-download-release-1"
|
|
assert captured["priority"] == 0
|
|
assert captured["user_id"] == user["id"]
|
|
assert captured["username"] == user["username"]
|
|
assert captured["release_data"]["source_id"] == "policy-download-release-1"
|
|
assert main_module.user_db.list_requests(user_id=user["id"]) == []
|
|
mock_notify_admin.assert_not_called()
|
|
mock_notify_user.assert_not_called()
|
|
|
|
def test_download_policy_rejects_mismatched_context_and_release_source(
|
|
self, main_module, client
|
|
):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(
|
|
default_ebook="download",
|
|
rules=[{"source": "direct_download", "content_type": "*", "mode": "blocked"}],
|
|
)
|
|
|
|
payload = {
|
|
"book_data": {
|
|
"title": "Policy Source Mismatch",
|
|
"author": "Shelfmark",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": "policy-source-mismatch-1",
|
|
},
|
|
"context": {
|
|
"source": "prowlarr",
|
|
"content_type": "ebook",
|
|
"request_level": "release",
|
|
},
|
|
"release_data": {
|
|
"source": "direct_download",
|
|
"source_id": "blocked-release-1",
|
|
"title": "Blocked Release.epub",
|
|
},
|
|
}
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
with patch.object(main_module.backend, "queue_release") as mock_queue:
|
|
resp = client.post("/api/requests", json=payload)
|
|
|
|
assert resp.status_code == 400
|
|
assert resp.json["code"] == "policy_source_mismatch"
|
|
assert resp.json["error"] == "Policy context source must match release_data.source"
|
|
assert main_module.user_db.list_requests(user_id=user["id"]) == []
|
|
mock_queue.assert_not_called()
|
|
|
|
def test_release_result_source_rejects_mismatch_before_normalization(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(
|
|
default_ebook="download",
|
|
rules=[{"source": "prowlarr", "content_type": "*", "mode": "blocked"}],
|
|
)
|
|
|
|
payload = {
|
|
"book_data": {
|
|
"title": "Release Result Source Mismatch",
|
|
"author": "Shelfmark",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": "release-result-mismatch-1",
|
|
},
|
|
"context": {
|
|
"source": "direct_download",
|
|
"content_type": "ebook",
|
|
"request_level": "release",
|
|
},
|
|
"release_data": {
|
|
"source": "prowlarr",
|
|
"source_id": "blocked-prowlarr-release-1",
|
|
"title": "Blocked Prowlarr Release.epub",
|
|
},
|
|
}
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
with patch.object(main_module.backend, "queue_release") as mock_queue:
|
|
resp = client.post("/api/requests", json=payload)
|
|
|
|
assert resp.status_code == 400
|
|
assert resp.json["code"] == "policy_source_mismatch"
|
|
assert resp.json["error"] == "Policy context source must match release_data.source"
|
|
assert main_module.user_db.list_requests(user_id=user["id"]) == []
|
|
mock_queue.assert_not_called()
|
|
|
|
def test_batch_rejects_release_result_source_mismatch_before_creating_any_requests(
|
|
self, main_module, client
|
|
):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(
|
|
default_ebook="request_release",
|
|
rules=[{"source": "prowlarr", "content_type": "*", "mode": "blocked"}],
|
|
)
|
|
|
|
payloads = [
|
|
{
|
|
"book_data": {
|
|
"title": "Batch Valid Direct",
|
|
"author": "Shelfmark",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": "batch-valid-direct-1",
|
|
},
|
|
"context": {
|
|
"source": "direct_download",
|
|
"content_type": "ebook",
|
|
"request_level": "release",
|
|
},
|
|
"release_data": {
|
|
"source": "direct_download",
|
|
"source_id": "batch-valid-direct-release-1",
|
|
"title": "Batch Valid Direct.epub",
|
|
},
|
|
},
|
|
{
|
|
"book_data": {
|
|
"title": "Batch Release Result Mismatch",
|
|
"author": "Shelfmark",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": "batch-release-result-mismatch-1",
|
|
},
|
|
"context": {
|
|
"source": "direct_download",
|
|
"content_type": "ebook",
|
|
"request_level": "release",
|
|
},
|
|
"release_data": {
|
|
"source": "prowlarr",
|
|
"source_id": "batch-blocked-prowlarr-release-1",
|
|
"title": "Batch Blocked Prowlarr.epub",
|
|
},
|
|
},
|
|
]
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
with patch.object(main_module.backend, "queue_release") as mock_queue:
|
|
resp = client.post("/api/requests/batch", json={"requests": payloads})
|
|
|
|
assert resp.status_code == 400
|
|
assert resp.json["code"] == "policy_source_mismatch"
|
|
assert resp.json["error"] == "Policy context source must match release_data.source"
|
|
assert main_module.user_db.list_requests(user_id=user["id"]) == []
|
|
mock_queue.assert_not_called()
|
|
|
|
def test_batch_download_policy_queues_releases_without_creating_requests(
|
|
self, main_module, client
|
|
):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(default_ebook="download")
|
|
|
|
payloads = [
|
|
{
|
|
"book_data": {
|
|
"title": "Batch Download One",
|
|
"author": "Shelfmark",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": "batch-download-1",
|
|
},
|
|
"context": {
|
|
"source": "prowlarr",
|
|
"content_type": "ebook",
|
|
"request_level": "release",
|
|
},
|
|
"release_data": {
|
|
"source": "prowlarr",
|
|
"source_id": "batch-download-release-1",
|
|
"title": "Batch Download One.epub",
|
|
},
|
|
},
|
|
{
|
|
"book_data": {
|
|
"title": "Batch Download Two",
|
|
"author": "Shelfmark",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": "batch-download-2",
|
|
},
|
|
"context": {
|
|
"source": "prowlarr",
|
|
"content_type": "ebook",
|
|
"request_level": "release",
|
|
},
|
|
"release_data": {
|
|
"source": "prowlarr",
|
|
"source_id": "batch-download-release-2",
|
|
"title": "Batch Download Two.epub",
|
|
},
|
|
},
|
|
]
|
|
|
|
queued: list[tuple[str, int, int | None, str | None]] = []
|
|
|
|
def fake_queue_release(release_data, priority, user_id=None, username=None):
|
|
queued.append((release_data["source_id"], priority, user_id, username))
|
|
return True, None
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
with patch.object(
|
|
main_module.backend, "queue_release", side_effect=fake_queue_release
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.notify_admin"
|
|
) as mock_notify_admin:
|
|
resp = client.post("/api/requests/batch", json={"requests": payloads})
|
|
|
|
assert resp.status_code == 200
|
|
assert [row["kind"] for row in resp.json] == ["download", "download"]
|
|
assert [row["source_id"] for row in resp.json] == [
|
|
"batch-download-release-1",
|
|
"batch-download-release-2",
|
|
]
|
|
assert queued == [
|
|
("batch-download-release-1", 0, user["id"], user["username"]),
|
|
("batch-download-release-2", 0, user["id"], user["username"]),
|
|
]
|
|
assert main_module.user_db.list_requests(user_id=user["id"]) == []
|
|
mock_notify_admin.assert_not_called()
|
|
|
|
def test_admin_can_create_request_on_behalf_of_another_user(self, main_module, client):
|
|
admin = _create_user(main_module, prefix="admin", role="admin")
|
|
target_user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=admin["username"], db_user_id=admin["id"], is_admin=True)
|
|
policy = _policy(default_ebook="request_book")
|
|
|
|
payload = {
|
|
"book_data": {
|
|
"title": "Shared Request",
|
|
"author": "Shelfmark",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": "shared-1",
|
|
},
|
|
"context": {
|
|
"source": "*",
|
|
"content_type": "ebook",
|
|
"request_level": "book",
|
|
},
|
|
"on_behalf_of_user_id": target_user["id"],
|
|
}
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
resp = client.post("/api/requests", json=payload)
|
|
|
|
assert resp.status_code == 201
|
|
assert resp.json["user_id"] == target_user["id"]
|
|
created = main_module.user_db.get_request(resp.json["id"])
|
|
assert created is not None
|
|
assert created["user_id"] == target_user["id"]
|
|
|
|
def test_non_admin_cannot_create_request_on_behalf_of_another_user(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
target_user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(default_ebook="request_book")
|
|
|
|
payload = {
|
|
"book_data": {
|
|
"title": "Shared Request",
|
|
"author": "Shelfmark",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": "shared-2",
|
|
},
|
|
"context": {
|
|
"source": "*",
|
|
"content_type": "ebook",
|
|
"request_level": "book",
|
|
},
|
|
"on_behalf_of_user_id": target_user["id"],
|
|
}
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
resp = client.post("/api/requests", json=payload)
|
|
|
|
assert resp.status_code == 403
|
|
assert resp.json["error"] == "Admin required"
|
|
assert main_module.user_db.list_requests(user_id=user["id"]) == []
|
|
assert main_module.user_db.list_requests(user_id=target_user["id"]) == []
|
|
|
|
def test_admin_on_behalf_of_unknown_user_returns_404(self, main_module, client):
|
|
admin = _create_user(main_module, prefix="admin", role="admin")
|
|
_set_session(client, user_id=admin["username"], db_user_id=admin["id"], is_admin=True)
|
|
policy = _policy(default_ebook="request_book")
|
|
|
|
payload = {
|
|
"book_data": {
|
|
"title": "Missing Target",
|
|
"author": "Shelfmark",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": "missing-target",
|
|
},
|
|
"context": {
|
|
"source": "*",
|
|
"content_type": "ebook",
|
|
"request_level": "book",
|
|
},
|
|
"on_behalf_of_user_id": 9_999_999,
|
|
}
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
resp = client.post("/api/requests", json=payload)
|
|
|
|
assert resp.status_code == 404
|
|
assert resp.json["error"] == "User not found"
|
|
|
|
def test_batch_create_requests_is_atomic(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(default_ebook="request_book", default_audiobook="request_book")
|
|
|
|
duplicate_request = {
|
|
"book_data": {
|
|
"title": "Duplicate Title",
|
|
"author": "Same Author",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": "dup-1",
|
|
},
|
|
"context": {
|
|
"source": "*",
|
|
"content_type": "ebook",
|
|
"request_level": "book",
|
|
},
|
|
}
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
resp = client.post(
|
|
"/api/requests/batch",
|
|
json={"requests": [duplicate_request, duplicate_request]},
|
|
)
|
|
|
|
assert resp.status_code == 409
|
|
assert resp.json["code"] == "duplicate_pending_request"
|
|
assert main_module.user_db.list_requests(user_id=user["id"]) == []
|
|
|
|
@pytest.mark.parametrize("bad_item", [1, "book", None, ["x"]])
|
|
def test_batch_rejects_non_object_request_items(self, main_module, client, bad_item):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
resp = client.post("/api/requests/batch", json={"requests": [bad_item]})
|
|
|
|
assert resp.status_code == 400
|
|
assert "requests must contain objects" in resp.json["error"]
|
|
|
|
def test_batch_non_object_item_rejects_whole_batch(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(default_ebook="request_book")
|
|
|
|
valid_request = {
|
|
"book_data": {
|
|
"title": "Batch Valid Alongside Bad Item",
|
|
"author": "Shelfmark",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": "batch-valid-alongside-bad-1",
|
|
},
|
|
"context": {
|
|
"source": "*",
|
|
"content_type": "ebook",
|
|
"request_level": "book",
|
|
},
|
|
}
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
resp = client.post(
|
|
"/api/requests/batch",
|
|
json={"requests": [valid_request, "not-an-object"]},
|
|
)
|
|
|
|
assert resp.status_code == 400
|
|
assert "requests must contain objects" in resp.json["error"]
|
|
assert main_module.user_db.list_requests(user_id=user["id"]) == []
|
|
|
|
def test_create_request_emits_websocket_events(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(default_ebook="request_book")
|
|
|
|
payload = {
|
|
"book_data": {
|
|
"title": "Eventful Book",
|
|
"author": "Event Author",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": "ol-event",
|
|
},
|
|
"context": {
|
|
"source": "direct_download",
|
|
"content_type": "ebook",
|
|
"request_level": "book",
|
|
},
|
|
}
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
with patch.object(main_module.ws_manager, "is_enabled", return_value=True):
|
|
with patch.object(main_module.ws_manager.socketio, "emit") as mock_emit:
|
|
resp = client.post("/api/requests", json=payload)
|
|
|
|
assert resp.status_code == 201
|
|
request_id = resp.json["id"]
|
|
|
|
assert mock_emit.call_count == 2
|
|
expected_payload = {
|
|
"request_id": request_id,
|
|
"status": "pending",
|
|
"title": "Eventful Book",
|
|
}
|
|
_assert_emit_call(mock_emit, 0, "new_request", expected_payload, "admins")
|
|
_assert_emit_call(mock_emit, 1, "request_update", expected_payload, f"user_{user['id']}")
|
|
|
|
def test_create_request_triggers_admin_notification(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(default_ebook="request_book")
|
|
|
|
payload = {
|
|
"book_data": {
|
|
"title": "Notify Create Book",
|
|
"author": "Notify Create Author",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": "ol-notify-create",
|
|
},
|
|
"context": {
|
|
"source": "direct_download",
|
|
"content_type": "ebook",
|
|
"request_level": "book",
|
|
},
|
|
}
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
with patch("shelfmark.core.request_routes.notify_admin") as mock_notify:
|
|
with patch("shelfmark.core.request_routes.notify_user") as mock_notify_user:
|
|
resp = client.post("/api/requests", json=payload)
|
|
|
|
assert resp.status_code == 201
|
|
mock_notify.assert_called_once()
|
|
event, context = mock_notify.call_args.args
|
|
assert event == NotificationEvent.REQUEST_CREATED
|
|
assert context.title == "Notify Create Book"
|
|
assert context.author == "Notify Create Author"
|
|
assert context.username == user["username"]
|
|
mock_notify_user.assert_called_once()
|
|
user_id, user_event, user_context = mock_notify_user.call_args.args
|
|
assert user_id == user["id"]
|
|
assert user_event == NotificationEvent.REQUEST_CREATED
|
|
assert user_context.title == "Notify Create Book"
|
|
|
|
def test_create_request_succeeds_when_notification_dispatch_raises(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(default_ebook="request_book")
|
|
|
|
payload = {
|
|
"book_data": {
|
|
"title": "Resilient Notify Create Book",
|
|
"author": "Resilient Notify Create Author",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": "ol-notify-resilience",
|
|
},
|
|
"context": {
|
|
"source": "direct_download",
|
|
"content_type": "ebook",
|
|
"request_level": "book",
|
|
},
|
|
}
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.notify_admin",
|
|
side_effect=RuntimeError("admin notification unavailable"),
|
|
) as mock_notify_admin:
|
|
with patch(
|
|
"shelfmark.core.request_routes.notify_user",
|
|
side_effect=RuntimeError("user notification unavailable"),
|
|
) as mock_notify_user:
|
|
resp = client.post("/api/requests", json=payload)
|
|
|
|
assert resp.status_code == 201
|
|
assert resp.json["status"] == "pending"
|
|
mock_notify_admin.assert_called_once()
|
|
mock_notify_user.assert_called_once()
|
|
|
|
def test_cancel_request_emits_to_user_and_admin_rooms(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(default_ebook="request_book")
|
|
|
|
payload = {
|
|
"book_data": {
|
|
"title": "Cancelable Book",
|
|
"author": "Cancelable Author",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": "ol-cancel",
|
|
},
|
|
"context": {
|
|
"source": "direct_download",
|
|
"content_type": "ebook",
|
|
"request_level": "book",
|
|
},
|
|
}
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
with patch.object(main_module.ws_manager, "is_enabled", return_value=True):
|
|
with patch.object(main_module.ws_manager.socketio, "emit") as mock_emit:
|
|
create_resp = client.post("/api/requests", json=payload)
|
|
request_id = create_resp.json["id"]
|
|
|
|
mock_emit.reset_mock()
|
|
cancel_resp = client.delete(f"/api/requests/{request_id}")
|
|
|
|
assert create_resp.status_code == 201
|
|
assert cancel_resp.status_code == 200
|
|
assert cancel_resp.json["status"] == "cancelled"
|
|
|
|
assert mock_emit.call_count == 2
|
|
expected_payload = {
|
|
"request_id": request_id,
|
|
"status": "cancelled",
|
|
"title": "Cancelable Book",
|
|
}
|
|
_assert_emit_call(mock_emit, 0, "request_update", expected_payload, f"user_{user['id']}")
|
|
_assert_emit_call(mock_emit, 1, "request_update", expected_payload, "admins")
|
|
|
|
def test_create_request_level_payload_mismatch_returns_400(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(default_ebook="request_release")
|
|
|
|
payload = {
|
|
"book_data": {
|
|
"title": "Clean Code",
|
|
"author": "Robert Martin",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": "ol-2",
|
|
},
|
|
"context": {
|
|
"source": "prowlarr",
|
|
"content_type": "ebook",
|
|
"request_level": "book",
|
|
},
|
|
"release_data": {
|
|
"source": "prowlarr",
|
|
"source_id": "rel-2",
|
|
"title": "Clean Code.epub",
|
|
},
|
|
}
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
resp = client.post("/api/requests", json=payload)
|
|
|
|
assert resp.status_code == 400
|
|
assert "request_level=book requires null release_data" in resp.json["error"]
|
|
|
|
def test_duplicate_pending_request_returns_409(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(default_ebook="request_book")
|
|
|
|
payload = {
|
|
"book_data": {
|
|
"title": "Domain-Driven Design",
|
|
"author": "Eric Evans",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": "ol-3",
|
|
},
|
|
"context": {
|
|
"source": "direct_download",
|
|
"content_type": "ebook",
|
|
"request_level": "book",
|
|
},
|
|
}
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
first_resp = client.post("/api/requests", json=payload)
|
|
second_resp = client.post("/api/requests", json=payload)
|
|
|
|
assert first_resp.status_code == 201
|
|
assert second_resp.status_code == 409
|
|
assert second_resp.json["code"] == "duplicate_pending_request"
|
|
|
|
def test_create_request_enforces_max_pending_limit(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(default_ebook="request_book", max_pending_requests_per_user=1)
|
|
|
|
payload_1 = {
|
|
"book_data": {
|
|
"title": "Book A",
|
|
"author": "Author A",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": "ol-a",
|
|
},
|
|
"context": {
|
|
"source": "direct_download",
|
|
"content_type": "ebook",
|
|
"request_level": "book",
|
|
},
|
|
}
|
|
payload_2 = {
|
|
"book_data": {
|
|
"title": "Book B",
|
|
"author": "Author B",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": "ol-b",
|
|
},
|
|
"context": {
|
|
"source": "direct_download",
|
|
"content_type": "ebook",
|
|
"request_level": "book",
|
|
},
|
|
}
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
first_resp = client.post("/api/requests", json=payload_1)
|
|
second_resp = client.post("/api/requests", json=payload_2)
|
|
|
|
assert first_resp.status_code == 201
|
|
assert second_resp.status_code == 409
|
|
assert second_resp.json["code"] == "max_pending_reached"
|
|
|
|
def test_create_request_strips_note_when_notes_disabled(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(default_ebook="request_book", requests_allow_notes=False)
|
|
|
|
payload = {
|
|
"book_data": {
|
|
"title": "No Notes Book",
|
|
"author": "No Notes Author",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": "ol-nonote",
|
|
},
|
|
"context": {
|
|
"source": "direct_download",
|
|
"content_type": "ebook",
|
|
"request_level": "book",
|
|
},
|
|
"note": "This should be dropped",
|
|
}
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
resp = client.post("/api/requests", json=payload)
|
|
|
|
assert resp.status_code == 201
|
|
assert resp.json["note"] is None
|
|
|
|
def test_request_book_policy_requires_book_level_request(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(default_ebook="request_book")
|
|
|
|
payload = {
|
|
"book_data": {
|
|
"title": "Refactoring",
|
|
"author": "Martin Fowler",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": "ol-4",
|
|
},
|
|
"context": {
|
|
"source": "prowlarr",
|
|
"content_type": "ebook",
|
|
"request_level": "release",
|
|
},
|
|
"release_data": {
|
|
"source": "prowlarr",
|
|
"source_id": "rel-4",
|
|
"title": "Refactoring.epub",
|
|
},
|
|
}
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
resp = client.post("/api/requests", json=payload)
|
|
|
|
assert resp.status_code == 403
|
|
assert resp.json["code"] == "policy_requires_request"
|
|
assert resp.json["required_mode"] == "request_book"
|
|
|
|
def test_request_book_policy_allows_direct_release_level_request(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(default_ebook="request_book")
|
|
|
|
payload = {
|
|
"book_data": {
|
|
"title": "Direct Result",
|
|
"author": "Direct Author",
|
|
"content_type": "ebook",
|
|
"provider": "direct_download",
|
|
"provider_id": "dd-1",
|
|
},
|
|
"context": {
|
|
"source": "direct_download",
|
|
"content_type": "ebook",
|
|
"request_level": "release",
|
|
},
|
|
"release_data": {
|
|
"source": "direct_download",
|
|
"source_id": "dd-1",
|
|
"title": "Direct Result.epub",
|
|
"format": "epub",
|
|
"size": "2 MB",
|
|
},
|
|
}
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
resp = client.post("/api/requests", json=payload)
|
|
|
|
assert resp.status_code == 201
|
|
assert resp.json["request_level"] == "release"
|
|
assert resp.json["policy_mode"] == "request_release"
|
|
assert resp.json["release_data"]["source"] == "direct_download"
|
|
assert resp.json["release_data"]["source_id"] == "dd-1"
|
|
|
|
def test_non_admin_cannot_access_admin_request_routes(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
resp = client.get("/api/admin/requests")
|
|
|
|
assert resp.status_code == 403
|
|
assert resp.json["error"] == "Admin access required"
|
|
|
|
def test_admin_reject_and_terminal_conflict(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
admin = _create_user(main_module, prefix="admin", role="admin")
|
|
policy = _policy(default_ebook="request_book")
|
|
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
create_payload = {
|
|
"book_data": {
|
|
"title": "Working Effectively with Legacy Code",
|
|
"author": "Michael Feathers",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": "ol-5",
|
|
},
|
|
"context": {
|
|
"source": "direct_download",
|
|
"content_type": "ebook",
|
|
"request_level": "book",
|
|
},
|
|
}
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
create_resp = client.post("/api/requests", json=create_payload)
|
|
request_id = create_resp.json["id"]
|
|
|
|
_set_session(
|
|
client, user_id=admin["username"], db_user_id=admin["id"], is_admin=True
|
|
)
|
|
count_resp = client.get("/api/admin/requests/count")
|
|
reject_resp = client.post(
|
|
f"/api/admin/requests/{request_id}/reject",
|
|
json={"admin_note": "Declined"},
|
|
)
|
|
reject_again_resp = client.post(
|
|
f"/api/admin/requests/{request_id}/reject",
|
|
json={"admin_note": "Declined again"},
|
|
)
|
|
|
|
assert count_resp.status_code == 200
|
|
assert count_resp.json["pending"] >= 1
|
|
assert reject_resp.status_code == 200
|
|
assert reject_resp.json["status"] == "rejected"
|
|
assert reject_again_resp.status_code == 409
|
|
assert reject_again_resp.json["code"] == "stale_transition"
|
|
updated = main_module.user_db.get_request(request_id)
|
|
assert updated is not None
|
|
assert updated["user_id"] == user["id"]
|
|
assert updated["status"] == "rejected"
|
|
|
|
def test_admin_reject_emits_update_to_user_and_admin_rooms(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
admin = _create_user(main_module, prefix="admin", role="admin")
|
|
policy = _policy(default_ebook="request_book")
|
|
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
create_payload = {
|
|
"book_data": {
|
|
"title": "Reject Emit Book",
|
|
"author": "Reject Emit Author",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": "ol-reject-emit",
|
|
},
|
|
"context": {
|
|
"source": "direct_download",
|
|
"content_type": "ebook",
|
|
"request_level": "book",
|
|
},
|
|
}
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
create_resp = client.post("/api/requests", json=create_payload)
|
|
request_id = create_resp.json["id"]
|
|
|
|
_set_session(
|
|
client, user_id=admin["username"], db_user_id=admin["id"], is_admin=True
|
|
)
|
|
with patch.object(main_module.ws_manager, "is_enabled", return_value=True):
|
|
with patch.object(main_module.ws_manager.socketio, "emit") as mock_emit:
|
|
reject_resp = client.post(
|
|
f"/api/admin/requests/{request_id}/reject",
|
|
json={"admin_note": "Rejected with event fanout"},
|
|
)
|
|
|
|
assert create_resp.status_code == 201
|
|
assert reject_resp.status_code == 200
|
|
assert reject_resp.json["status"] == "rejected"
|
|
|
|
assert mock_emit.call_count == 2
|
|
expected_payload = {
|
|
"request_id": request_id,
|
|
"status": "rejected",
|
|
"title": "Reject Emit Book",
|
|
}
|
|
_assert_emit_call(mock_emit, 0, "request_update", expected_payload, f"user_{user['id']}")
|
|
_assert_emit_call(mock_emit, 1, "request_update", expected_payload, "admins")
|
|
|
|
def test_admin_reject_triggers_admin_notification(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
admin = _create_user(main_module, prefix="admin", role="admin")
|
|
policy = _policy(default_ebook="request_book")
|
|
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
create_payload = {
|
|
"book_data": {
|
|
"title": "Reject Notify Book",
|
|
"author": "Reject Notify Author",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": "ol-reject-notify",
|
|
},
|
|
"context": {
|
|
"source": "direct_download",
|
|
"content_type": "ebook",
|
|
"request_level": "book",
|
|
},
|
|
}
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
create_resp = client.post("/api/requests", json=create_payload)
|
|
request_id = create_resp.json["id"]
|
|
|
|
_set_session(
|
|
client, user_id=admin["username"], db_user_id=admin["id"], is_admin=True
|
|
)
|
|
with patch("shelfmark.core.request_routes.notify_admin") as mock_notify:
|
|
with patch("shelfmark.core.request_routes.notify_user") as mock_notify_user:
|
|
reject_resp = client.post(
|
|
f"/api/admin/requests/{request_id}/reject",
|
|
json={"admin_note": "Needs better metadata"},
|
|
)
|
|
|
|
assert create_resp.status_code == 201
|
|
assert reject_resp.status_code == 200
|
|
mock_notify.assert_called_once()
|
|
event, context = mock_notify.call_args.args
|
|
assert event == NotificationEvent.REQUEST_REJECTED
|
|
assert context.title == "Reject Notify Book"
|
|
assert context.admin_note == "Needs better metadata"
|
|
assert context.username == user["username"]
|
|
mock_notify_user.assert_called_once()
|
|
user_id, user_event, user_context = mock_notify_user.call_args.args
|
|
assert user_id == user["id"]
|
|
assert user_event == NotificationEvent.REQUEST_REJECTED
|
|
assert user_context.admin_note == "Needs better metadata"
|
|
|
|
def test_admin_fulfil_queues_for_requesting_user(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
admin = _create_user(main_module, prefix="admin", role="admin")
|
|
policy = _policy(default_ebook="request_release")
|
|
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
create_payload = {
|
|
"book_data": {
|
|
"title": "Patterns of Enterprise Application Architecture",
|
|
"author": "Martin Fowler",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": "ol-6",
|
|
},
|
|
"context": {
|
|
"source": "prowlarr",
|
|
"content_type": "ebook",
|
|
"request_level": "release",
|
|
},
|
|
"release_data": {
|
|
"source": "prowlarr",
|
|
"source_id": "rel-6",
|
|
"title": "POEAA.epub",
|
|
},
|
|
}
|
|
|
|
captured: dict[str, object] = {}
|
|
|
|
def fake_queue_release(release_data, priority, user_id=None, username=None):
|
|
captured["release_data"] = release_data
|
|
captured["priority"] = priority
|
|
captured["user_id"] = user_id
|
|
captured["username"] = username
|
|
return True, None
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
create_resp = client.post("/api/requests", json=create_payload)
|
|
request_id = create_resp.json["id"]
|
|
|
|
_set_session(
|
|
client, user_id=admin["username"], db_user_id=admin["id"], is_admin=True
|
|
)
|
|
with patch.object(
|
|
main_module.backend, "queue_release", side_effect=fake_queue_release
|
|
):
|
|
fulfil_resp = client.post(
|
|
f"/api/admin/requests/{request_id}/fulfil",
|
|
json={"admin_note": "Approved"},
|
|
)
|
|
|
|
assert fulfil_resp.status_code == 200
|
|
assert fulfil_resp.json["status"] == "fulfilled"
|
|
assert captured["priority"] == 0
|
|
assert captured["user_id"] == user["id"]
|
|
assert captured["username"] == user["username"]
|
|
|
|
def test_admin_fulfil_emits_update_to_user_and_admin_rooms(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
admin = _create_user(main_module, prefix="admin", role="admin")
|
|
policy = _policy(default_ebook="request_release")
|
|
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
create_payload = {
|
|
"book_data": {
|
|
"title": "Fulfil Emit Book",
|
|
"author": "Fulfil Emit Author",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": "ol-fulfil-emit",
|
|
},
|
|
"context": {
|
|
"source": "prowlarr",
|
|
"content_type": "ebook",
|
|
"request_level": "release",
|
|
},
|
|
"release_data": {
|
|
"source": "prowlarr",
|
|
"source_id": "rel-fulfil-emit",
|
|
"title": "Fulfil Emit Book.epub",
|
|
},
|
|
}
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
create_resp = client.post("/api/requests", json=create_payload)
|
|
request_id = create_resp.json["id"]
|
|
|
|
_set_session(
|
|
client, user_id=admin["username"], db_user_id=admin["id"], is_admin=True
|
|
)
|
|
with patch.object(
|
|
main_module.backend, "queue_release", return_value=(True, None)
|
|
):
|
|
with patch.object(main_module.ws_manager, "is_enabled", return_value=True):
|
|
with patch.object(main_module.ws_manager.socketio, "emit") as mock_emit:
|
|
fulfil_resp = client.post(
|
|
f"/api/admin/requests/{request_id}/fulfil",
|
|
json={"admin_note": "Approved with event fanout"},
|
|
)
|
|
|
|
assert create_resp.status_code == 201
|
|
assert fulfil_resp.status_code == 200
|
|
assert fulfil_resp.json["status"] == "fulfilled"
|
|
|
|
assert mock_emit.call_count == 2
|
|
expected_payload = {
|
|
"request_id": request_id,
|
|
"status": "fulfilled",
|
|
"title": "Fulfil Emit Book",
|
|
}
|
|
_assert_emit_call(mock_emit, 0, "request_update", expected_payload, f"user_{user['id']}")
|
|
_assert_emit_call(mock_emit, 1, "request_update", expected_payload, "admins")
|
|
|
|
def test_admin_fulfil_triggers_admin_notification(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
admin = _create_user(main_module, prefix="admin", role="admin")
|
|
policy = _policy(default_ebook="request_release")
|
|
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
create_payload = {
|
|
"book_data": {
|
|
"title": "Fulfil Notify Book",
|
|
"author": "Fulfil Notify Author",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": "ol-fulfil-notify",
|
|
},
|
|
"context": {
|
|
"source": "prowlarr",
|
|
"content_type": "ebook",
|
|
"request_level": "release",
|
|
},
|
|
"release_data": {
|
|
"source": "prowlarr",
|
|
"source_id": "rel-fulfil-notify",
|
|
"title": "Fulfil Notify Book.epub",
|
|
},
|
|
}
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
create_resp = client.post("/api/requests", json=create_payload)
|
|
request_id = create_resp.json["id"]
|
|
|
|
_set_session(
|
|
client, user_id=admin["username"], db_user_id=admin["id"], is_admin=True
|
|
)
|
|
with patch.object(
|
|
main_module.backend, "queue_release", return_value=(True, None)
|
|
):
|
|
with patch("shelfmark.core.request_routes.notify_admin") as mock_notify:
|
|
with patch(
|
|
"shelfmark.core.request_routes.notify_user"
|
|
) as mock_notify_user:
|
|
fulfil_resp = client.post(
|
|
f"/api/admin/requests/{request_id}/fulfil",
|
|
json={"admin_note": "Approved"},
|
|
)
|
|
|
|
assert create_resp.status_code == 201
|
|
assert fulfil_resp.status_code == 200
|
|
mock_notify.assert_called_once()
|
|
event, context = mock_notify.call_args.args
|
|
assert event == NotificationEvent.REQUEST_FULFILLED
|
|
assert context.title == "Fulfil Notify Book"
|
|
assert context.username == user["username"]
|
|
mock_notify_user.assert_called_once()
|
|
user_id, user_event, user_context = mock_notify_user.call_args.args
|
|
assert user_id == user["id"]
|
|
assert user_event == NotificationEvent.REQUEST_FULFILLED
|
|
assert user_context.title == "Fulfil Notify Book"
|
|
|
|
def test_admin_fulfil_book_level_request_requires_release_data(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
admin = _create_user(main_module, prefix="admin", role="admin")
|
|
policy = _policy(default_ebook="request_book")
|
|
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
create_payload = {
|
|
"book_data": {
|
|
"title": "Designing Data-Intensive Applications",
|
|
"author": "Martin Kleppmann",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": "ol-7",
|
|
},
|
|
"context": {
|
|
"source": "prowlarr",
|
|
"content_type": "ebook",
|
|
"request_level": "book",
|
|
},
|
|
}
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
create_resp = client.post("/api/requests", json=create_payload)
|
|
request_id = create_resp.json["id"]
|
|
|
|
_set_session(
|
|
client, user_id=admin["username"], db_user_id=admin["id"], is_admin=True
|
|
)
|
|
fulfil_resp = client.post(f"/api/admin/requests/{request_id}/fulfil", json={})
|
|
|
|
assert fulfil_resp.status_code == 400
|
|
assert "release_data is required to fulfil requests" in fulfil_resp.json["error"]
|
|
|
|
def test_admin_fulfil_book_level_request_manual_approval(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
admin = _create_user(main_module, prefix="admin", role="admin")
|
|
policy = _policy(default_ebook="request_book")
|
|
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
create_payload = {
|
|
"book_data": {
|
|
"title": "Manual Approval Book",
|
|
"author": "Manual Admin",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": "ol-manual-approval",
|
|
},
|
|
"context": {
|
|
"source": "prowlarr",
|
|
"content_type": "ebook",
|
|
"request_level": "book",
|
|
},
|
|
}
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
create_resp = client.post("/api/requests", json=create_payload)
|
|
request_id = create_resp.json["id"]
|
|
|
|
_set_session(
|
|
client, user_id=admin["username"], db_user_id=admin["id"], is_admin=True
|
|
)
|
|
with patch.object(
|
|
main_module.backend, "queue_release", return_value=(True, None)
|
|
) as mock_queue:
|
|
fulfil_resp = client.post(
|
|
f"/api/admin/requests/{request_id}/fulfil",
|
|
json={"manual_approval": True, "admin_note": "Added manually"},
|
|
)
|
|
|
|
assert create_resp.status_code == 201
|
|
assert fulfil_resp.status_code == 200
|
|
assert fulfil_resp.json["status"] == "fulfilled"
|
|
assert fulfil_resp.json["delivery_state"] == "complete"
|
|
assert fulfil_resp.json["release_data"] is None
|
|
assert fulfil_resp.json["admin_note"] == "Added manually"
|
|
mock_queue.assert_not_called()
|
|
|
|
def test_admin_fulfil_book_level_request_with_release_data(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
admin = _create_user(main_module, prefix="admin", role="admin")
|
|
policy = _policy(default_ebook="request_book")
|
|
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
create_payload = {
|
|
"book_data": {
|
|
"title": "Book Level Fulfil",
|
|
"author": "QA Author",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": "ol-book-fulfil",
|
|
},
|
|
"context": {
|
|
"source": "prowlarr",
|
|
"content_type": "ebook",
|
|
"request_level": "book",
|
|
},
|
|
}
|
|
|
|
captured: dict[str, object] = {}
|
|
|
|
def fake_queue_release(release_data, priority, user_id=None, username=None):
|
|
captured["release_data"] = release_data
|
|
captured["priority"] = priority
|
|
captured["user_id"] = user_id
|
|
captured["username"] = username
|
|
return True, None
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
create_resp = client.post("/api/requests", json=create_payload)
|
|
request_id = create_resp.json["id"]
|
|
|
|
_set_session(
|
|
client, user_id=admin["username"], db_user_id=admin["id"], is_admin=True
|
|
)
|
|
with patch.object(
|
|
main_module.backend, "queue_release", side_effect=fake_queue_release
|
|
):
|
|
fulfil_resp = client.post(
|
|
f"/api/admin/requests/{request_id}/fulfil",
|
|
json={
|
|
"release_data": {
|
|
"source": "prowlarr",
|
|
"source_id": "book-level-picked-release",
|
|
"title": "Book Level Fulfil.epub",
|
|
}
|
|
},
|
|
)
|
|
|
|
assert create_resp.status_code == 201
|
|
assert fulfil_resp.status_code == 200
|
|
assert fulfil_resp.json["status"] == "fulfilled"
|
|
assert fulfil_resp.json["request_level"] == "book"
|
|
assert fulfil_resp.json["release_data"]["source_id"] == "book-level-picked-release"
|
|
assert captured["release_data"]["source_id"] == "book-level-picked-release"
|
|
assert captured["priority"] == 0
|
|
assert captured["user_id"] == user["id"]
|
|
assert captured["username"] == user["username"]
|
|
|
|
def test_admin_fulfil_uses_real_queue_and_preserves_requesting_identity(
|
|
self, main_module, client
|
|
):
|
|
class AvailableSource:
|
|
display_name = "Direct Download"
|
|
|
|
def is_available(self):
|
|
return True
|
|
|
|
user = _create_user(main_module, prefix="reader")
|
|
other_user = _create_user(main_module, prefix="reader")
|
|
admin = _create_user(main_module, prefix="admin", role="admin")
|
|
policy = _policy(default_ebook="request_release")
|
|
source_id = f"real-queue-{uuid.uuid4().hex[:10]}"
|
|
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
create_payload = {
|
|
"book_data": {
|
|
"title": "Building Microservices",
|
|
"author": "Sam Newman",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": "ol-8",
|
|
},
|
|
"context": {
|
|
"source": "direct_download",
|
|
"content_type": "ebook",
|
|
"request_level": "release",
|
|
},
|
|
"release_data": {
|
|
"source": "direct_download",
|
|
"source_id": source_id,
|
|
"title": "Building Microservices.epub",
|
|
},
|
|
}
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
with patch.object(
|
|
main_module.backend,
|
|
"get_source",
|
|
return_value=AvailableSource(),
|
|
):
|
|
create_resp = client.post("/api/requests", json=create_payload)
|
|
request_id = create_resp.json["id"]
|
|
|
|
_set_session(
|
|
client,
|
|
user_id=admin["username"],
|
|
db_user_id=admin["id"],
|
|
is_admin=True,
|
|
)
|
|
fulfil_resp = client.post(
|
|
f"/api/admin/requests/{request_id}/fulfil", json={}
|
|
)
|
|
|
|
assert fulfil_resp.status_code == 200
|
|
assert fulfil_resp.json["status"] == "fulfilled"
|
|
|
|
user_status = main_module.backend.queue_status(user_id=user["id"])
|
|
assert source_id in user_status["queued"]
|
|
assert user_status["queued"][source_id]["username"] == user["username"]
|
|
|
|
other_status = main_module.backend.queue_status(user_id=other_user["id"])
|
|
assert source_id not in other_status["queued"]
|
|
|
|
|
|
class TestRequestCreationEdgeCases:
|
|
"""Edge cases for POST /api/requests."""
|
|
|
|
def test_no_json_body_returns_400(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(default_ebook="request_book")
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
resp = client.post("/api/requests", content_type="text/plain", data="garbage")
|
|
|
|
assert resp.status_code == 400
|
|
assert "No data provided" in resp.json["error"]
|
|
|
|
def test_missing_book_data_returns_400(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(default_ebook="request_book")
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
resp = client.post(
|
|
"/api/requests", json={"context": {"source": "direct_download"}}
|
|
)
|
|
|
|
assert resp.status_code == 400
|
|
assert "book_data must be an object" in resp.json["error"]
|
|
|
|
def test_non_dict_context_returns_400(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(default_ebook="request_book")
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
resp = client.post(
|
|
"/api/requests",
|
|
json={
|
|
"context": "not-a-dict",
|
|
"book_data": {
|
|
"title": "X",
|
|
"author": "Y",
|
|
"provider": "z",
|
|
"provider_id": "1",
|
|
},
|
|
},
|
|
)
|
|
|
|
assert resp.status_code == 400
|
|
assert "context must be an object" in resp.json["error"]
|
|
|
|
def test_book_data_missing_required_fields_returns_400(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(default_ebook="request_book")
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
resp = client.post(
|
|
"/api/requests",
|
|
json={
|
|
"book_data": {"title": "Only a title"},
|
|
"context": {
|
|
"source": "direct_download",
|
|
"content_type": "ebook",
|
|
"request_level": "book",
|
|
},
|
|
},
|
|
)
|
|
|
|
assert resp.status_code == 400
|
|
assert "missing required field" in resp.json["error"]
|
|
|
|
def test_book_data_payload_too_large_returns_400(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(default_ebook="request_book")
|
|
|
|
payload = {
|
|
"book_data": {
|
|
"title": "Oversized Book",
|
|
"author": "Big Payload",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": "ol-big",
|
|
"description": "x" * 12000,
|
|
},
|
|
"context": {
|
|
"source": "direct_download",
|
|
"content_type": "ebook",
|
|
"request_level": "book",
|
|
},
|
|
}
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
resp = client.post("/api/requests", json=payload)
|
|
|
|
assert resp.status_code == 400
|
|
assert "book_data must be <= 10240 bytes" in resp.json["error"]
|
|
|
|
def test_disabled_requests_returns_403(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(requests_enabled=False, default_ebook="request_book")
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
resp = client.post(
|
|
"/api/requests",
|
|
json={
|
|
"book_data": {
|
|
"title": "T",
|
|
"author": "A",
|
|
"provider": "p",
|
|
"provider_id": "1",
|
|
},
|
|
"context": {
|
|
"source": "direct_download",
|
|
"content_type": "ebook",
|
|
"request_level": "book",
|
|
},
|
|
},
|
|
)
|
|
|
|
assert resp.status_code == 403
|
|
assert resp.json["code"] == "requests_unavailable"
|
|
|
|
def test_download_policy_without_concrete_release_returns_400(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(default_ebook="download")
|
|
|
|
payload = {
|
|
"book_data": {
|
|
"title": "Needs Release Selection",
|
|
"author": "Shelfmark",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": "needs-release-selection",
|
|
},
|
|
"context": {
|
|
"source": "*",
|
|
"content_type": "ebook",
|
|
"request_level": "book",
|
|
},
|
|
}
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
with patch.object(main_module.backend, "queue_release") as mock_queue_release:
|
|
resp = client.post("/api/requests", json=payload)
|
|
|
|
assert resp.status_code == 400
|
|
assert resp.json["code"] == "policy_requires_download"
|
|
assert resp.json["required_mode"] == "download"
|
|
mock_queue_release.assert_not_called()
|
|
assert main_module.user_db.list_requests(user_id=user["id"]) == []
|
|
|
|
def test_blocked_policy_returns_403(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(default_ebook="blocked")
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
resp = client.post(
|
|
"/api/requests",
|
|
json={
|
|
"book_data": {
|
|
"title": "T",
|
|
"author": "A",
|
|
"provider": "p",
|
|
"provider_id": "1",
|
|
"content_type": "ebook",
|
|
},
|
|
"context": {
|
|
"source": "direct_download",
|
|
"content_type": "ebook",
|
|
"request_level": "book",
|
|
},
|
|
},
|
|
)
|
|
|
|
assert resp.status_code == 403
|
|
assert resp.json["code"] == "policy_blocked"
|
|
assert resp.json["required_mode"] == "blocked"
|
|
|
|
def test_direct_requests_are_forced_to_release_level(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(default_ebook="request_book")
|
|
|
|
payload = {
|
|
"book_data": {
|
|
"title": "Auto Infer Book",
|
|
"author": "Test Author",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": "ol-auto-1",
|
|
},
|
|
"context": {"source": "direct_download", "content_type": "ebook"},
|
|
}
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
resp = client.post("/api/requests", json=payload)
|
|
|
|
assert resp.status_code == 201
|
|
assert resp.json["request_level"] == "release"
|
|
assert resp.json["policy_mode"] == "request_release"
|
|
assert resp.json["release_data"]["source"] == "direct_download"
|
|
assert resp.json["release_data"]["source_id"] == "ol-auto-1"
|
|
assert resp.json["release_data"]["search_mode"] == "direct"
|
|
|
|
def test_auto_infers_release_level_when_release_data_present(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(default_ebook="request_release")
|
|
|
|
payload = {
|
|
"book_data": {
|
|
"title": "Auto Infer Release",
|
|
"author": "Test Author",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": "ol-auto-2",
|
|
},
|
|
"context": {"source": "prowlarr", "content_type": "ebook"},
|
|
"release_data": {"source": "prowlarr", "source_id": "auto-r", "title": "File.epub"},
|
|
}
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
resp = client.post("/api/requests", json=payload)
|
|
|
|
assert resp.status_code == 201
|
|
assert resp.json["request_level"] == "release"
|
|
assert resp.json["release_data"]["source_id"] == "auto-r"
|
|
|
|
def test_release_level_request_with_request_release_policy(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(default_ebook="request_release")
|
|
|
|
payload = {
|
|
"book_data": {
|
|
"title": "Release Level Test",
|
|
"author": "RLT Author",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": "ol-rl",
|
|
},
|
|
"context": {"source": "prowlarr", "content_type": "ebook", "request_level": "release"},
|
|
"release_data": {"source": "prowlarr", "source_id": "rl-1", "title": "RL.epub"},
|
|
}
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
resp = client.post("/api/requests", json=payload)
|
|
|
|
assert resp.status_code == 201
|
|
assert resp.json["policy_mode"] == "request_release"
|
|
assert resp.json["request_level"] == "release"
|
|
|
|
def test_without_db_user_id_returns_403(self, main_module, client):
|
|
with client.session_transaction() as sess:
|
|
sess["user_id"] = "some-user"
|
|
sess["is_admin"] = False
|
|
if "db_user_id" in sess:
|
|
del sess["db_user_id"]
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
resp = client.post(
|
|
"/api/requests",
|
|
json={
|
|
"book_data": {"title": "T", "author": "A", "provider": "p", "provider_id": "1"},
|
|
"context": {"source": "direct_download"},
|
|
},
|
|
)
|
|
|
|
assert resp.status_code == 403
|
|
assert resp.json["code"] == "user_identity_unavailable"
|
|
|
|
def test_audiobook_content_type_request(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(default_audiobook="request_release")
|
|
|
|
payload = {
|
|
"book_data": {
|
|
"title": "Audiobook Test",
|
|
"author": "AB Author",
|
|
"content_type": "audiobook",
|
|
"provider": "hardcover",
|
|
"provider_id": "hc-ab",
|
|
},
|
|
"context": {
|
|
"source": "prowlarr",
|
|
"content_type": "audiobook",
|
|
"request_level": "release",
|
|
},
|
|
"release_data": {"source": "prowlarr", "source_id": "ab-1", "title": "AB.m4b"},
|
|
}
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
resp = client.post("/api/requests", json=payload)
|
|
|
|
assert resp.status_code == 201
|
|
assert resp.json["content_type"] == "audiobook"
|
|
|
|
|
|
class TestRequestListAndFilterEdgeCases:
|
|
"""Edge cases for GET /api/requests and GET /api/admin/requests."""
|
|
|
|
def _seed_requests(self, main_module, client, user, policy, count=3):
|
|
"""Create multiple requests and return their IDs."""
|
|
ids = []
|
|
for i in range(count):
|
|
payload = {
|
|
"book_data": {
|
|
"title": f"Seed Book {uuid.uuid4().hex[:6]}",
|
|
"author": f"Author {i}",
|
|
"content_type": "ebook",
|
|
"provider": "openlibrary",
|
|
"provider_id": f"ol-seed-{uuid.uuid4().hex[:6]}",
|
|
},
|
|
"context": {
|
|
"source": "direct_download",
|
|
"content_type": "ebook",
|
|
"request_level": "book",
|
|
},
|
|
}
|
|
resp = client.post("/api/requests", json=payload)
|
|
assert resp.status_code == 201
|
|
ids.append(resp.json["id"])
|
|
return ids
|
|
|
|
def test_list_requests_empty_result(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
resp = client.get("/api/requests")
|
|
|
|
assert resp.status_code == 200
|
|
assert resp.json == []
|
|
|
|
def test_list_requests_with_status_filter(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_create_user(main_module, prefix="admin", role="admin")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(default_ebook="request_book")
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
ids = self._seed_requests(main_module, client, user, policy, count=3)
|
|
|
|
# Cancel the first request.
|
|
client.delete(f"/api/requests/{ids[0]}")
|
|
|
|
# Filter: only pending.
|
|
pending_resp = client.get("/api/requests?status=pending")
|
|
cancelled_resp = client.get("/api/requests?status=cancelled")
|
|
|
|
assert pending_resp.status_code == 200
|
|
pending_ids = {r["id"] for r in pending_resp.json}
|
|
assert ids[0] not in pending_ids
|
|
assert ids[1] in pending_ids
|
|
|
|
assert cancelled_resp.status_code == 200
|
|
cancelled_ids = {r["id"] for r in cancelled_resp.json}
|
|
assert ids[0] in cancelled_ids
|
|
|
|
def test_list_requests_with_pagination(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(default_ebook="request_book")
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
self._seed_requests(main_module, client, user, policy, count=5)
|
|
|
|
page1 = client.get("/api/requests?limit=2&offset=0")
|
|
page2 = client.get("/api/requests?limit=2&offset=2")
|
|
|
|
assert page1.status_code == 200
|
|
assert len(page1.json) == 2
|
|
|
|
assert page2.status_code == 200
|
|
assert len(page2.json) == 2
|
|
|
|
# Pages should not overlap.
|
|
page1_ids = {r["id"] for r in page1.json}
|
|
page2_ids = {r["id"] for r in page2.json}
|
|
assert page1_ids.isdisjoint(page2_ids)
|
|
|
|
def test_user_only_sees_own_requests(self, main_module, client):
|
|
alice = _create_user(main_module, prefix="alice")
|
|
bob = _create_user(main_module, prefix="bob")
|
|
policy = _policy(default_ebook="request_book")
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
# Alice creates a request.
|
|
_set_session(
|
|
client, user_id=alice["username"], db_user_id=alice["id"], is_admin=False
|
|
)
|
|
client.post(
|
|
"/api/requests",
|
|
json={
|
|
"book_data": {
|
|
"title": "Alice Book",
|
|
"author": "A",
|
|
"provider": "p",
|
|
"provider_id": "a1",
|
|
"content_type": "ebook",
|
|
},
|
|
"context": {
|
|
"source": "direct_download",
|
|
"content_type": "ebook",
|
|
"request_level": "book",
|
|
},
|
|
},
|
|
)
|
|
|
|
# Bob creates a request.
|
|
_set_session(
|
|
client, user_id=bob["username"], db_user_id=bob["id"], is_admin=False
|
|
)
|
|
client.post(
|
|
"/api/requests",
|
|
json={
|
|
"book_data": {
|
|
"title": "Bob Book",
|
|
"author": "B",
|
|
"provider": "p",
|
|
"provider_id": "b1",
|
|
"content_type": "ebook",
|
|
},
|
|
"context": {
|
|
"source": "direct_download",
|
|
"content_type": "ebook",
|
|
"request_level": "book",
|
|
},
|
|
},
|
|
)
|
|
|
|
# Bob lists — should only see his.
|
|
bob_list = client.get("/api/requests")
|
|
|
|
# Alice lists — should only see hers.
|
|
_set_session(
|
|
client, user_id=alice["username"], db_user_id=alice["id"], is_admin=False
|
|
)
|
|
alice_list = client.get("/api/requests")
|
|
|
|
assert len(bob_list.json) == 1
|
|
assert bob_list.json[0]["book_data"]["title"] == "Bob Book"
|
|
|
|
assert len(alice_list.json) == 1
|
|
assert alice_list.json[0]["book_data"]["title"] == "Alice Book"
|
|
|
|
def test_admin_list_includes_username(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
admin = _create_user(main_module, prefix="admin", role="admin")
|
|
policy = _policy(default_ebook="request_book")
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
_set_session(
|
|
client, user_id=user["username"], db_user_id=user["id"], is_admin=False
|
|
)
|
|
create_resp = client.post(
|
|
"/api/requests",
|
|
json={
|
|
"book_data": {
|
|
"title": "Admin View",
|
|
"author": "AV",
|
|
"provider": "p",
|
|
"provider_id": "av1",
|
|
"content_type": "ebook",
|
|
},
|
|
"context": {
|
|
"source": "direct_download",
|
|
"content_type": "ebook",
|
|
"request_level": "book",
|
|
},
|
|
},
|
|
)
|
|
request_id = create_resp.json["id"]
|
|
|
|
_set_session(
|
|
client, user_id=admin["username"], db_user_id=admin["id"], is_admin=True
|
|
)
|
|
resp = client.get("/api/admin/requests")
|
|
|
|
assert resp.status_code == 200
|
|
matching = [r for r in resp.json if r["id"] == request_id]
|
|
assert len(matching) == 1
|
|
assert matching[0]["username"] == user["username"]
|
|
|
|
def test_admin_list_with_status_filter(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
admin = _create_user(main_module, prefix="admin", role="admin")
|
|
policy = _policy(default_ebook="request_book")
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
_set_session(
|
|
client, user_id=user["username"], db_user_id=user["id"], is_admin=False
|
|
)
|
|
create_resp = client.post(
|
|
"/api/requests",
|
|
json={
|
|
"book_data": {
|
|
"title": f"FilterTest-{uuid.uuid4().hex[:6]}",
|
|
"author": "FT",
|
|
"provider": "p",
|
|
"provider_id": f"ft-{uuid.uuid4().hex[:6]}",
|
|
"content_type": "ebook",
|
|
},
|
|
"context": {
|
|
"source": "direct_download",
|
|
"content_type": "ebook",
|
|
"request_level": "book",
|
|
},
|
|
},
|
|
)
|
|
request_id = create_resp.json["id"]
|
|
|
|
_set_session(
|
|
client, user_id=admin["username"], db_user_id=admin["id"], is_admin=True
|
|
)
|
|
client.post(f"/api/admin/requests/{request_id}/reject", json={})
|
|
|
|
pending_resp = client.get("/api/admin/requests?status=pending")
|
|
rejected_resp = client.get("/api/admin/requests?status=rejected")
|
|
|
|
pending_ids = {r["id"] for r in pending_resp.json}
|
|
rejected_ids = {r["id"] for r in rejected_resp.json}
|
|
assert request_id not in pending_ids
|
|
assert request_id in rejected_ids
|
|
|
|
|
|
class TestCancelEdgeCases:
|
|
"""Edge cases for DELETE /api/requests/<id>."""
|
|
|
|
def test_cancel_nonexistent_request_returns_404(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
resp = client.delete("/api/requests/99999")
|
|
|
|
assert resp.status_code == 404
|
|
|
|
def test_cancel_other_users_request_returns_403(self, main_module, client):
|
|
alice = _create_user(main_module, prefix="alice")
|
|
bob = _create_user(main_module, prefix="bob")
|
|
policy = _policy(default_ebook="request_book")
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
_set_session(
|
|
client, user_id=alice["username"], db_user_id=alice["id"], is_admin=False
|
|
)
|
|
create_resp = client.post(
|
|
"/api/requests",
|
|
json={
|
|
"book_data": {
|
|
"title": "Alice Only",
|
|
"author": "A",
|
|
"provider": "p",
|
|
"provider_id": "ao1",
|
|
"content_type": "ebook",
|
|
},
|
|
"context": {
|
|
"source": "direct_download",
|
|
"content_type": "ebook",
|
|
"request_level": "book",
|
|
},
|
|
},
|
|
)
|
|
request_id = create_resp.json["id"]
|
|
|
|
_set_session(
|
|
client, user_id=bob["username"], db_user_id=bob["id"], is_admin=False
|
|
)
|
|
cancel_resp = client.delete(f"/api/requests/{request_id}")
|
|
|
|
assert cancel_resp.status_code == 403
|
|
|
|
def test_cancel_already_cancelled_returns_409(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(default_ebook="request_book")
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
create_resp = client.post(
|
|
"/api/requests",
|
|
json={
|
|
"book_data": {
|
|
"title": "Cancel Twice",
|
|
"author": "CT",
|
|
"provider": "p",
|
|
"provider_id": "ct1",
|
|
"content_type": "ebook",
|
|
},
|
|
"context": {
|
|
"source": "direct_download",
|
|
"content_type": "ebook",
|
|
"request_level": "book",
|
|
},
|
|
},
|
|
)
|
|
request_id = create_resp.json["id"]
|
|
|
|
first = client.delete(f"/api/requests/{request_id}")
|
|
second = client.delete(f"/api/requests/{request_id}")
|
|
|
|
assert first.status_code == 200
|
|
assert second.status_code == 409
|
|
assert second.json["code"] == "stale_transition"
|
|
|
|
|
|
class TestAdminFulfilEdgeCases:
|
|
"""Edge cases for POST /api/admin/requests/<id>/fulfil."""
|
|
|
|
def test_fulfil_nonexistent_request_returns_404(self, main_module, client):
|
|
admin = _create_user(main_module, prefix="admin", role="admin")
|
|
_set_session(client, user_id=admin["username"], db_user_id=admin["id"], is_admin=True)
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
resp = client.post(
|
|
"/api/admin/requests/99999/fulfil",
|
|
json={
|
|
"release_data": {"source": "dd", "source_id": "r1", "title": "f.epub"},
|
|
},
|
|
)
|
|
|
|
assert resp.status_code == 404
|
|
|
|
def test_fulfil_with_queue_failure_returns_409(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
admin = _create_user(main_module, prefix="admin", role="admin")
|
|
policy = _policy(default_ebook="request_release")
|
|
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
create_resp = client.post(
|
|
"/api/requests",
|
|
json={
|
|
"book_data": {
|
|
"title": "Queue Fail",
|
|
"author": "QF",
|
|
"provider": "p",
|
|
"provider_id": "qf1",
|
|
"content_type": "ebook",
|
|
},
|
|
"context": {
|
|
"source": "prowlarr",
|
|
"content_type": "ebook",
|
|
"request_level": "release",
|
|
},
|
|
"release_data": {
|
|
"source": "prowlarr",
|
|
"source_id": "qf-r",
|
|
"title": "QF.epub",
|
|
},
|
|
},
|
|
)
|
|
request_id = create_resp.json["id"]
|
|
|
|
_set_session(
|
|
client, user_id=admin["username"], db_user_id=admin["id"], is_admin=True
|
|
)
|
|
with patch.object(
|
|
main_module.backend, "queue_release", return_value=(False, "Client offline")
|
|
):
|
|
fulfil_resp = client.post(
|
|
f"/api/admin/requests/{request_id}/fulfil", json={}
|
|
)
|
|
|
|
assert fulfil_resp.status_code == 409
|
|
assert fulfil_resp.json["code"] == "queue_failed"
|
|
|
|
def test_fulfil_with_admin_override_release_data(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
admin = _create_user(main_module, prefix="admin", role="admin")
|
|
policy = _policy(default_ebook="request_release")
|
|
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
|
|
captured = {}
|
|
|
|
def capture_queue(release_data, priority, **kwargs):
|
|
captured["release_data"] = release_data
|
|
return True, None
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
create_resp = client.post(
|
|
"/api/requests",
|
|
json={
|
|
"book_data": {
|
|
"title": "Override RD",
|
|
"author": "OR",
|
|
"provider": "p",
|
|
"provider_id": "or1",
|
|
"content_type": "ebook",
|
|
},
|
|
"context": {
|
|
"source": "prowlarr",
|
|
"content_type": "ebook",
|
|
"request_level": "release",
|
|
},
|
|
"release_data": {
|
|
"source": "prowlarr",
|
|
"source_id": "original-r",
|
|
"title": "Original.epub",
|
|
},
|
|
},
|
|
)
|
|
request_id = create_resp.json["id"]
|
|
|
|
_set_session(
|
|
client, user_id=admin["username"], db_user_id=admin["id"], is_admin=True
|
|
)
|
|
with patch.object(
|
|
main_module.backend, "queue_release", side_effect=capture_queue
|
|
):
|
|
fulfil_resp = client.post(
|
|
f"/api/admin/requests/{request_id}/fulfil",
|
|
json={
|
|
"release_data": {
|
|
"source": "direct_download",
|
|
"source_id": "better-r",
|
|
"title": "Better.epub",
|
|
},
|
|
},
|
|
)
|
|
|
|
assert fulfil_resp.status_code == 200
|
|
assert captured["release_data"]["source_id"] == "better-r"
|
|
assert fulfil_resp.json["release_data"]["source_id"] == "better-r"
|
|
|
|
def test_admin_without_db_user_id_returns_403(self, main_module, client):
|
|
with client.session_transaction() as sess:
|
|
sess["user_id"] = "admin-user"
|
|
sess["is_admin"] = True
|
|
if "db_user_id" in sess:
|
|
del sess["db_user_id"]
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
resp = client.post("/api/admin/requests/1/fulfil", json={})
|
|
|
|
assert resp.status_code == 403
|
|
assert "Admin user identity unavailable" in resp.json["error"]
|
|
|
|
def test_fulfil_with_non_boolean_manual_approval_returns_400(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
admin = _create_user(main_module, prefix="admin", role="admin")
|
|
policy = _policy(default_ebook="request_release")
|
|
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
create_resp = client.post(
|
|
"/api/requests",
|
|
json={
|
|
"book_data": {
|
|
"title": "Manual Flag Validation",
|
|
"author": "QA",
|
|
"provider": "p",
|
|
"provider_id": "mf1",
|
|
"content_type": "ebook",
|
|
},
|
|
"context": {
|
|
"source": "prowlarr",
|
|
"content_type": "ebook",
|
|
"request_level": "release",
|
|
},
|
|
"release_data": {
|
|
"source": "prowlarr",
|
|
"source_id": "mf-r",
|
|
"title": "MF.epub",
|
|
},
|
|
},
|
|
)
|
|
request_id = create_resp.json["id"]
|
|
|
|
_set_session(
|
|
client, user_id=admin["username"], db_user_id=admin["id"], is_admin=True
|
|
)
|
|
fulfil_resp = client.post(
|
|
f"/api/admin/requests/{request_id}/fulfil",
|
|
json={"manual_approval": "yes"},
|
|
)
|
|
|
|
assert create_resp.status_code == 201
|
|
assert fulfil_resp.status_code == 400
|
|
assert "manual_approval must be a boolean" in fulfil_resp.json["error"]
|
|
|
|
|
|
class TestAdminRejectEdgeCases:
|
|
"""Edge cases for POST /api/admin/requests/<id>/reject."""
|
|
|
|
def test_reject_nonexistent_request_returns_404(self, main_module, client):
|
|
admin = _create_user(main_module, prefix="admin", role="admin")
|
|
_set_session(client, user_id=admin["username"], db_user_id=admin["id"], is_admin=True)
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
resp = client.post("/api/admin/requests/99999/reject", json={})
|
|
|
|
assert resp.status_code == 404
|
|
|
|
def test_reject_already_fulfilled_returns_409(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
admin = _create_user(main_module, prefix="admin", role="admin")
|
|
policy = _policy(default_ebook="request_release")
|
|
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
create_resp = client.post(
|
|
"/api/requests",
|
|
json={
|
|
"book_data": {
|
|
"title": "Rej After Ful",
|
|
"author": "RAF",
|
|
"provider": "p",
|
|
"provider_id": "raf1",
|
|
"content_type": "ebook",
|
|
},
|
|
"context": {
|
|
"source": "prowlarr",
|
|
"content_type": "ebook",
|
|
"request_level": "release",
|
|
},
|
|
"release_data": {
|
|
"source": "prowlarr",
|
|
"source_id": "raf-r",
|
|
"title": "RAF.epub",
|
|
},
|
|
},
|
|
)
|
|
request_id = create_resp.json["id"]
|
|
|
|
_set_session(
|
|
client, user_id=admin["username"], db_user_id=admin["id"], is_admin=True
|
|
)
|
|
with patch.object(
|
|
main_module.backend, "queue_release", return_value=(True, None)
|
|
):
|
|
client.post(f"/api/admin/requests/{request_id}/fulfil", json={})
|
|
|
|
reject_resp = client.post(f"/api/admin/requests/{request_id}/reject", json={})
|
|
|
|
assert reject_resp.status_code == 409
|
|
assert reject_resp.json["code"] == "stale_transition"
|
|
|
|
|
|
class TestAdminCountEdgeCases:
|
|
"""Edge cases for GET /api/admin/requests/count."""
|
|
|
|
def test_count_reflects_all_statuses(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
admin = _create_user(main_module, prefix="admin", role="admin")
|
|
policy = _policy(default_ebook="request_book")
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
_set_session(
|
|
client, user_id=user["username"], db_user_id=user["id"], is_admin=False
|
|
)
|
|
|
|
# Create 3 requests.
|
|
ids = []
|
|
for _i in range(3):
|
|
resp = client.post(
|
|
"/api/requests",
|
|
json={
|
|
"book_data": {
|
|
"title": f"Count Test {uuid.uuid4().hex[:6]}",
|
|
"author": "CT",
|
|
"provider": "p",
|
|
"provider_id": f"ct-{uuid.uuid4().hex[:6]}",
|
|
"content_type": "ebook",
|
|
},
|
|
"context": {
|
|
"source": "direct_download",
|
|
"content_type": "ebook",
|
|
"request_level": "book",
|
|
},
|
|
},
|
|
)
|
|
ids.append(resp.json["id"])
|
|
|
|
# Cancel one.
|
|
client.delete(f"/api/requests/{ids[0]}")
|
|
|
|
# Admin rejects one.
|
|
_set_session(
|
|
client, user_id=admin["username"], db_user_id=admin["id"], is_admin=True
|
|
)
|
|
client.post(f"/api/admin/requests/{ids[1]}/reject", json={})
|
|
|
|
count_resp = client.get("/api/admin/requests/count")
|
|
|
|
assert count_resp.status_code == 200
|
|
by_status = count_resp.json["by_status"]
|
|
# At least 1 of each status we created.
|
|
assert by_status["cancelled"] >= 1
|
|
assert by_status["rejected"] >= 1
|
|
assert by_status["pending"] >= 1
|
|
assert count_resp.json["pending"] == by_status["pending"]
|
|
assert count_resp.json["total"] == sum(by_status.values())
|
|
|
|
|
|
class TestPolicyEndpointEdgeCases:
|
|
"""Edge cases for GET /api/request-policy."""
|
|
|
|
def test_admin_view_shows_is_admin_true(self, main_module, client):
|
|
admin = _create_user(main_module, prefix="admin", role="admin")
|
|
_set_session(client, user_id=admin["username"], db_user_id=admin["id"], is_admin=True)
|
|
policy = _policy(default_ebook="download")
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
resp = client.get("/api/request-policy")
|
|
|
|
assert resp.status_code == 200
|
|
assert resp.json["is_admin"] is True
|
|
|
|
def test_policy_endpoint_reflects_per_user_overrides(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
|
|
# Global says download, but user override sets request_release for ebook.
|
|
global_policy = _policy(default_ebook="download", default_audiobook="download")
|
|
main_module.user_db.set_user_settings(
|
|
user["id"], {"REQUEST_POLICY_DEFAULT_EBOOK": "request_release"}
|
|
)
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=global_policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=global_policy,
|
|
):
|
|
resp = client.get("/api/request-policy")
|
|
|
|
assert resp.status_code == 200
|
|
assert resp.json["defaults"]["ebook"] == "request_release"
|
|
assert resp.json["defaults"]["audiobook"] == "download"
|
|
|
|
def test_policy_endpoint_without_session_returns_401(self, main_module, client):
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
resp = client.get("/api/request-policy")
|
|
|
|
assert resp.status_code == 401
|
|
|
|
def test_policy_endpoint_includes_allow_notes_from_effective_settings(
|
|
self, main_module, client
|
|
):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(default_ebook="download", requests_allow_notes=False)
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
resp = client.get("/api/request-policy")
|
|
|
|
assert resp.status_code == 200
|
|
assert resp.json["allow_notes"] is False
|
|
|
|
def test_policy_endpoint_allow_notes_reflects_per_user_override(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
|
|
global_policy = _policy(default_ebook="download", requests_allow_notes=False)
|
|
main_module.user_db.set_user_settings(user["id"], {"REQUESTS_ALLOW_NOTES": True})
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=global_policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=global_policy,
|
|
):
|
|
resp = client.get("/api/request-policy")
|
|
|
|
assert resp.status_code == 200
|
|
assert resp.json["allow_notes"] is True
|
|
|
|
|
|
class TestDownloadPolicyGuardsExtended:
|
|
"""Extended policy enforcement tests for download endpoints."""
|
|
|
|
def test_download_allowed_when_requests_disabled(self, main_module, client):
|
|
"""When REQUESTS_ENABLED is false, policy is not enforced — downloads pass through."""
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
|
|
# Even though default is blocked, requests are disabled so policy doesn't apply.
|
|
policy = _policy(requests_enabled=False, default_ebook="blocked")
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
with patch.object(
|
|
main_module.backend, "queue_release", return_value=(True, None)
|
|
):
|
|
resp = client.post(
|
|
"/api/releases/download",
|
|
json={
|
|
"source": "direct_download",
|
|
"source_id": "book-pass",
|
|
"search_mode": "direct",
|
|
},
|
|
)
|
|
|
|
assert resp.status_code == 200
|
|
assert resp.json["status"] == "queued"
|
|
|
|
def test_download_allowed_when_policy_mode_is_download(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(default_ebook="download")
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
with patch.object(
|
|
main_module.backend, "queue_release", return_value=(True, None)
|
|
):
|
|
resp = client.post(
|
|
"/api/releases/download",
|
|
json={
|
|
"source": "direct_download",
|
|
"source_id": "book-free",
|
|
"search_mode": "direct",
|
|
},
|
|
)
|
|
|
|
assert resp.status_code == 200
|
|
assert resp.json["status"] == "queued"
|
|
|
|
def test_release_download_blocks_with_request_release_policy(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(default_ebook="request_release")
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
with patch.object(main_module.backend, "queue_release") as mock_queue:
|
|
resp = client.post(
|
|
"/api/releases/download",
|
|
json={
|
|
"source": "prowlarr",
|
|
"source_id": "rel-blocked",
|
|
"content_type": "ebook",
|
|
},
|
|
)
|
|
|
|
assert resp.status_code == 403
|
|
assert resp.json["code"] == "policy_requires_request"
|
|
assert resp.json["required_mode"] == "request_release"
|
|
mock_queue.assert_not_called()
|
|
|
|
def test_release_download_infers_audiobook_type_from_format_when_content_type_missing(
|
|
self, main_module, client
|
|
):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(default_ebook="download", default_audiobook="blocked")
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
with patch.object(main_module.backend, "queue_release") as mock_queue:
|
|
resp = client.post(
|
|
"/api/releases/download",
|
|
json={
|
|
"source": "prowlarr",
|
|
"source_id": "audio-rel",
|
|
"title": "Some Audio [m4b]",
|
|
"format": "m4b",
|
|
},
|
|
)
|
|
|
|
assert resp.status_code == 403
|
|
assert resp.json["code"] == "policy_blocked"
|
|
assert resp.json["required_mode"] == "blocked"
|
|
mock_queue.assert_not_called()
|
|
|
|
def test_release_download_blocks_with_request_book_policy(self, main_module, client):
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(default_ebook="request_book")
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
with patch.object(main_module.backend, "queue_release") as mock_queue:
|
|
resp = client.post(
|
|
"/api/releases/download",
|
|
json={
|
|
"source": "direct_download",
|
|
"source_id": "rel-rbook",
|
|
"content_type": "ebook",
|
|
},
|
|
)
|
|
|
|
assert resp.status_code == 403
|
|
assert resp.json["code"] == "policy_requires_request"
|
|
assert resp.json["required_mode"] == "request_release"
|
|
mock_queue.assert_not_called()
|
|
|
|
def test_release_download_with_per_source_matrix_rule(self, main_module, client):
|
|
"""Prowlarr blocked by matrix rule, but DD still allowed."""
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
policy = _policy(
|
|
default_ebook="download",
|
|
rules=[{"source": "prowlarr", "content_type": "*", "mode": "blocked"}],
|
|
)
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=policy,
|
|
):
|
|
with patch.object(main_module.backend, "queue_release") as mock_queue:
|
|
# Prowlarr should be blocked.
|
|
prowlarr_resp = client.post(
|
|
"/api/releases/download",
|
|
json={
|
|
"source": "prowlarr",
|
|
"source_id": "prowlarr-rel",
|
|
"content_type": "ebook",
|
|
},
|
|
)
|
|
|
|
with patch.object(
|
|
main_module.backend, "queue_release", return_value=(True, None)
|
|
) as mock_queue_dd:
|
|
# DD should still be allowed.
|
|
dd_resp = client.post(
|
|
"/api/releases/download",
|
|
json={
|
|
"source": "direct_download",
|
|
"source_id": "dd-rel",
|
|
"content_type": "ebook",
|
|
},
|
|
)
|
|
|
|
assert prowlarr_resp.status_code == 403
|
|
assert prowlarr_resp.json["code"] == "policy_blocked"
|
|
mock_queue.assert_not_called()
|
|
|
|
assert dd_resp.status_code == 200
|
|
mock_queue_dd.assert_called_once()
|
|
|
|
def test_per_user_override_unlocks_blocked_source(self, main_module, client):
|
|
"""Global blocks prowlarr, per-user override unlocks it."""
|
|
user = _create_user(main_module, prefix="reader")
|
|
_set_session(client, user_id=user["username"], db_user_id=user["id"], is_admin=False)
|
|
|
|
global_policy = _policy(
|
|
default_ebook="download",
|
|
rules=[{"source": "prowlarr", "content_type": "*", "mode": "blocked"}],
|
|
)
|
|
# User override: unblock prowlarr.
|
|
main_module.user_db.set_user_settings(
|
|
user["id"],
|
|
{
|
|
"REQUEST_POLICY_RULES": [
|
|
{"source": "prowlarr", "content_type": "*", "mode": "download"},
|
|
],
|
|
},
|
|
)
|
|
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
with patch.object(
|
|
main_module, "load_users_request_policy_settings", return_value=global_policy
|
|
):
|
|
with patch(
|
|
"shelfmark.core.request_routes.load_users_request_policy_settings",
|
|
return_value=global_policy,
|
|
):
|
|
with patch.object(
|
|
main_module.backend, "queue_release", return_value=(True, None)
|
|
):
|
|
resp = client.post(
|
|
"/api/releases/download",
|
|
json={
|
|
"source": "prowlarr",
|
|
"source_id": "prowlarr-unlocked",
|
|
"content_type": "ebook",
|
|
},
|
|
)
|
|
|
|
assert resp.status_code == 200
|