Files
shelfmark/tests/config/test_users_settings.py
Alex 3d72f9e258 Various requested small features (#741)
- Added torrent removal option
- Pass Prowlarr seedtimes to download clients (excluding rTorrent)
- Split default release source option by content type
- Split download to browser option by content type
- Add "hide links" option
2026-03-12 17:36:07 +00:00

449 lines
16 KiB
Python

"""Tests for users/request settings registration."""
from shelfmark.config import users_settings as users_settings_module
import shelfmark.config.users_settings # noqa: F401
from shelfmark.core import settings_registry
def _field_map(tab_name: str):
tab = settings_registry.get_settings_tab(tab_name)
assert tab is not None
return {field.key: field for field in tab.fields if hasattr(field, "key")}
def test_users_tab_is_renamed_to_users_and_requests():
tab = settings_registry.get_settings_tab("users")
assert tab is not None
assert tab.display_name == "Users & Requests"
def test_users_tab_registers_request_policy_fields():
fields = _field_map("users")
expected_keys = {
"users_management",
"VISIBLE_SELF_SETTINGS_SECTIONS",
"REQUESTS_ENABLED",
"request_policy_editor",
"MAX_PENDING_REQUESTS_PER_USER",
"REQUESTS_ALLOW_NOTES",
}
assert expected_keys.issubset(set(fields))
assert "REQUEST_POLICY_DEFAULT_EBOOK" not in fields
assert "REQUEST_POLICY_DEFAULT_AUDIOBOOK" not in fields
assert "REQUEST_POLICY_RULES" not in fields
def test_users_heading_contains_auth_mode_specific_descriptions():
fields = _field_map("users")
heading = fields["users_heading"]
assert heading.description_by_auth_mode["builtin"] == (
"Create and manage user accounts directly. Passwords are stored locally and users sign in "
"with their username and password."
)
assert heading.description_by_auth_mode["oidc"] == (
"Users sign in through your identity provider. New accounts can be created automatically on "
"first login when auto-provisioning is enabled, or you can pre-create users here and they\u2019ll "
"be linked by email on first sign-in."
)
assert heading.description_by_auth_mode["proxy"] == (
"Users are authenticated by your reverse proxy. Accounts are automatically created on first "
"sign-in. If a local user with a matching username already exists, it will be linked instead."
)
assert heading.description_by_auth_mode["cwa"] == (
"User accounts are synced from your Calibre-Web database. Users are matched by email, and new "
"accounts are created here when new CWA users are found."
)
assert heading.description_by_auth_mode["none"] == (
"Authentication is disabled. Anyone can access Shelfmark without signing in."
)
def test_request_policy_fields_are_user_overridable():
overridable_map = settings_registry.get_user_overridable_fields(tab_name="users")
expected_keys = {
"REQUESTS_ENABLED",
"REQUEST_POLICY_DEFAULT_EBOOK",
"REQUEST_POLICY_DEFAULT_AUDIOBOOK",
"REQUEST_POLICY_RULES",
"MAX_PENDING_REQUESTS_PER_USER",
"REQUESTS_ALLOW_NOTES",
}
assert expected_keys.issubset(set(overridable_map))
assert "RESTRICT_SETTINGS_TO_ADMIN" not in overridable_map
assert "VISIBLE_SELF_SETTINGS_SECTIONS" not in overridable_map
def test_visible_self_settings_sections_field_defaults_and_options():
fields = _field_map("users")
field = fields["VISIBLE_SELF_SETTINGS_SECTIONS"]
assert field.default == ["delivery", "search", "notifications"]
assert field.variant == "dropdown"
assert field.env_supported is False
assert field.options == [
{
"value": "delivery",
"label": "Delivery Preferences",
"description": "Show personal delivery output and destination settings.",
},
{
"value": "search",
"label": "Search Preferences",
"description": "Show personal search mode and provider settings.",
},
{
"value": "notifications",
"label": "Notifications",
"description": "Show personal notification route settings.",
},
]
def test_users_tab_registers_custom_components():
fields = _field_map("users")
users_management = fields["users_management"]
request_policy_editor = fields["request_policy_editor"]
assert users_management.get_field_type() == "CustomComponentField"
assert users_management.component == "users_management"
assert request_policy_editor.get_field_type() == "CustomComponentField"
assert request_policy_editor.component == "request_policy_grid"
assert request_policy_editor.wrap_in_field_wrapper is True
assert request_policy_editor.get_bind_keys() == [
"REQUEST_POLICY_DEFAULT_EBOOK",
"REQUEST_POLICY_DEFAULT_AUDIOBOOK",
"REQUEST_POLICY_RULES",
]
assert [field.key for field in request_policy_editor.value_fields] == [
"REQUEST_POLICY_DEFAULT_EBOOK",
"REQUEST_POLICY_DEFAULT_AUDIOBOOK",
"REQUEST_POLICY_RULES",
]
assert request_policy_editor.show_when == {"field": "REQUESTS_ENABLED", "value": True}
def test_request_policy_raw_fields_are_scoped_to_custom_component():
fields = _field_map("users")
request_policy_editor = fields["request_policy_editor"]
assert "REQUEST_POLICY_DEFAULT_EBOOK" not in fields
assert "REQUEST_POLICY_DEFAULT_AUDIOBOOK" not in fields
assert "REQUEST_POLICY_RULES" not in fields
assert [field.key for field in request_policy_editor.value_fields] == [
"REQUEST_POLICY_DEFAULT_EBOOK",
"REQUEST_POLICY_DEFAULT_AUDIOBOOK",
"REQUEST_POLICY_RULES",
]
def test_request_policy_rules_field_has_expected_columns():
fields = _field_map("users")
request_policy_editor = fields["request_policy_editor"]
rules_field = next(
field for field in request_policy_editor.value_fields if field.key == "REQUEST_POLICY_RULES"
)
columns = rules_field.columns() if callable(rules_field.columns) else rules_field.columns
column_keys = [column["key"] for column in columns]
assert column_keys == ["source", "content_type", "mode"]
def test_request_workflow_dependent_fields_are_gated_by_toggle():
fields = _field_map("users")
assert fields["MAX_PENDING_REQUESTS_PER_USER"].show_when == {
"field": "REQUESTS_ENABLED",
"value": True,
}
assert fields["REQUESTS_ALLOW_NOTES"].show_when == {
"field": "REQUESTS_ENABLED",
"value": True,
}
def test_users_tab_serialization_scopes_request_policy_to_bound_fields():
tab = settings_registry.get_settings_tab("users")
assert tab is not None
serialized_tab = settings_registry.serialize_tab(tab)
serialized_fields = {field["key"]: field for field in serialized_tab["fields"]}
assert "REQUEST_POLICY_DEFAULT_EBOOK" not in serialized_fields
assert "REQUEST_POLICY_DEFAULT_AUDIOBOOK" not in serialized_fields
assert "REQUEST_POLICY_RULES" not in serialized_fields
request_policy_editor = serialized_fields["request_policy_editor"]
bound_fields = request_policy_editor.get("boundFields", [])
assert [field["key"] for field in bound_fields] == [
"REQUEST_POLICY_DEFAULT_EBOOK",
"REQUEST_POLICY_DEFAULT_AUDIOBOOK",
"REQUEST_POLICY_RULES",
]
assert all(field.get("hiddenInUi") is True for field in bound_fields)
assert serialized_fields["users_heading"].get("descriptionByAuthMode", {}).get("builtin")
def test_request_policy_rules_source_options_are_dynamic(monkeypatch):
monkeypatch.setattr(
"shelfmark.release_sources.list_available_sources",
lambda: [
{
"name": "direct_download",
"display_name": "Direct Download",
"enabled": True,
"supported_content_types": ["ebook"],
},
{
"name": "prowlarr",
"display_name": "Prowlarr",
"enabled": True,
"supported_content_types": ["ebook", "audiobook"],
},
{
"name": "irc",
"display_name": "IRC",
"enabled": True,
"supported_content_types": ["ebook", "audiobook"],
},
],
)
columns = users_settings_module._get_request_policy_rule_columns()
source_options = columns[0]["options"]
assert source_options == [
{"value": "direct_download", "label": "Direct Download"},
{"value": "prowlarr", "label": "Prowlarr"},
{"value": "irc", "label": "IRC"},
]
content_type_column = columns[1]
content_type_options = content_type_column["options"]
assert content_type_column["filterByField"] == "source"
assert {"value": "ebook", "label": "Ebook", "childOf": "direct_download"} in content_type_options
assert {"value": "ebook", "label": "Ebook", "childOf": "prowlarr"} in content_type_options
assert {"value": "audiobook", "label": "Audiobook", "childOf": "prowlarr"} in content_type_options
assert {"value": "ebook", "label": "Ebook", "childOf": "irc"} in content_type_options
assert {"value": "audiobook", "label": "Audiobook", "childOf": "irc"} in content_type_options
assert {"value": "*", "label": "Any Type (*)", "childOf": "prowlarr"} not in content_type_options
assert {"value": "*", "label": "Any Type (*)", "childOf": "direct_download"} not in content_type_options
mode_options = columns[2]["options"]
# This test verifies dynamic source/content-type option wiring; keep mode-copy checks non-brittle.
assert mode_options[0]["value"] == "download"
assert mode_options[0]["label"] == "Download"
assert isinstance(mode_options[0].get("description"), str) and mode_options[0]["description"].strip()
assert {opt["value"] for opt in mode_options} == {"download", "request_release", "blocked"}
def test_on_save_users_rejects_unsupported_source_content_type_pair(monkeypatch):
monkeypatch.setattr(
"shelfmark.config.users_settings.validate_policy_rules",
lambda rules: (
[],
["Rule 1: source 'direct_download' does not support content_type 'audiobook'"],
),
)
result = users_settings_module._on_save_users(
{
"REQUEST_POLICY_RULES": [
{
"source": "direct_download",
"content_type": "audiobook",
"mode": "request_release",
}
]
}
)
assert result["error"] is True
assert "does not support content_type" in result["message"]
def test_on_save_users_rejects_blank_source_rule():
result = users_settings_module._on_save_users(
{
"REQUEST_POLICY_RULES": [
{
"source": "",
"content_type": "ebook",
"mode": "request_release",
}
]
}
)
assert result["error"] is True
assert "source is required" in result["message"]
def test_on_save_users_rejects_blank_content_type_rule():
result = users_settings_module._on_save_users(
{
"REQUEST_POLICY_RULES": [
{
"source": "direct_download",
"content_type": "",
"mode": "request_release",
}
]
}
)
assert result["error"] is True
assert "content_type is required" in result["message"]
def test_on_save_users_rejects_blank_mode_rule():
result = users_settings_module._on_save_users(
{
"REQUEST_POLICY_RULES": [
{
"source": "direct_download",
"content_type": "ebook",
"mode": "",
}
]
}
)
assert result["error"] is True
assert "mode is required" in result["message"]
def test_on_save_users_normalizes_rules(monkeypatch):
monkeypatch.setattr(
"shelfmark.config.users_settings.validate_policy_rules",
lambda rules: (
[
{"source": "direct_download", "content_type": "ebook", "mode": "request_release"},
],
[],
),
)
result = users_settings_module._on_save_users(
{
"REQUEST_POLICY_RULES": [
{
"source": "DIRECT_DOWNLOAD",
"content_type": "BOOK",
"mode": "REQUEST_RELEASE",
}
]
}
)
assert result["error"] is False
assert result["values"]["REQUEST_POLICY_RULES"] == [
{"source": "direct_download", "content_type": "ebook", "mode": "request_release"},
]
def test_on_save_users_normalizes_search_mode_override():
result = users_settings_module._on_save_users({"SEARCH_MODE": " UNIVERSAL "})
assert result["error"] is False
assert result["values"]["SEARCH_MODE"] == "universal"
def test_on_save_users_rejects_invalid_metadata_provider_override(monkeypatch):
monkeypatch.setattr(
"shelfmark.metadata_providers.is_provider_registered",
lambda provider_name: provider_name == "openlibrary",
)
result = users_settings_module._on_save_users({"METADATA_PROVIDER": "unknown-provider"})
assert result["error"] is True
assert "METADATA_PROVIDER must be a valid metadata provider name or empty" in result["message"]
def test_on_save_users_rejects_invalid_default_release_source_override(monkeypatch):
monkeypatch.setattr(
"shelfmark.release_sources.list_available_sources",
lambda: [
{
"name": "direct_download",
"display_name": "Direct Download",
"enabled": True,
"supported_content_types": ["ebook"],
},
{
"name": "prowlarr",
"display_name": "Prowlarr",
"enabled": True,
"supported_content_types": ["ebook", "audiobook"],
},
{
"name": "audiobookbay",
"display_name": "AudiobookBay",
"enabled": True,
"supported_content_types": ["audiobook"],
},
],
)
result = users_settings_module._on_save_users({"DEFAULT_RELEASE_SOURCE": "unknown-source"})
assert result["error"] is True
assert "DEFAULT_RELEASE_SOURCE must be a valid release source name or empty" in result["message"]
def test_on_save_users_rejects_audiobook_only_source_for_book_default(monkeypatch):
monkeypatch.setattr(
"shelfmark.release_sources.list_available_sources",
lambda: [
{
"name": "direct_download",
"display_name": "Direct Download",
"enabled": True,
"supported_content_types": ["ebook"],
},
{
"name": "audiobookbay",
"display_name": "AudiobookBay",
"enabled": True,
"supported_content_types": ["audiobook"],
},
],
)
result = users_settings_module._on_save_users({"DEFAULT_RELEASE_SOURCE": "audiobookbay"})
assert result["error"] is True
assert "DEFAULT_RELEASE_SOURCE must be a valid release source name or empty" in result["message"]
def test_on_save_users_rejects_book_only_source_for_audiobook_default(monkeypatch):
monkeypatch.setattr(
"shelfmark.release_sources.list_available_sources",
lambda: [
{
"name": "direct_download",
"display_name": "Direct Download",
"enabled": True,
"supported_content_types": ["ebook"],
},
{
"name": "audiobookbay",
"display_name": "AudiobookBay",
"enabled": True,
"supported_content_types": ["audiobook"],
},
],
)
result = users_settings_module._on_save_users(
{"DEFAULT_RELEASE_SOURCE_AUDIOBOOK": "direct_download"}
)
assert result["error"] is True
assert "DEFAULT_RELEASE_SOURCE_AUDIOBOOK must be a valid release source name or empty" in result["message"]