mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-09-28 22:06:05 +01:00
Review follow-ups to #1255, all in the code that PR touched. Drop the dead user_id from the Prowlarr retry path. ProwlarrSource.search never reads plan.languages, and _refresh_release builds a synthetic book with no titles_by_language, so the title variants came out identical with and without it. It also should not language-filter: it re-finds one exact release by its guid. Pin the tab move in tests. BOOK_LANGUAGE moved from the General tab to Search Mode with no migration, which only works because both tabs persist into the same settings.json. Nothing asserted that, so splitting the files later would silently reset every install to ["en"]. Covers the stored value, a fresh install, and ENV precedence. Stop the UI inventing a default language. An empty BOOK_LANGUAGE is a deliberate "no default filter" that the backend preserves, but the two frontend call sites replaced it with the first supported language, so the filter said English where the server filtered nothing. resolveDefaultLanguageCodes now falls back only when the value is absent. Keep the normalized value for every validated search key. validate_user_settings gated the write-back on a hand-maintained subset of the keys the search validator recognises, so METADATA_PROVIDER_COMBINED, SHOW_COMBINED_SELECTOR and FORCE_COMBINED_SEARCH were validated and then stored raw -- a padded provider name was accepted and persisted with its padding. Reuse the validator's own key set instead. Skip blank language entries rather than rejecting them, so "" and "en," mean the same as [] and ["en"] instead of erroring on an unnamed language. Extract resolveListOverride for the list-override detection that was copy-pasted between the two user-settings sections, and mention languages in the Search Preferences section description.
475 lines
16 KiB
Python
475 lines
16 KiB
Python
"""Tests for users/request settings registration."""
|
|
|
|
import shelfmark.config.users_settings # noqa: F401
|
|
from shelfmark.config import users_settings as users_settings_module
|
|
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, language, 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"]
|
|
)
|