mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-10-05 22:05:50 +01:00
Frontend update + Misc fixes (#735)
- Updated frontend CSS to Tailwind v4 - Reverted socket IO origin restriction - Fixed search queries not persisting after auth redirect - Move advanced search options to left UI selector - Unlock IRC source to be used for audiobook content_type - Tweaked security settings env var syncing to be prioritised - Fix AA "all languages" query generation - Added language-free AA query as second fallback in case of no results - Testing moving SeleniumBase scratch files to /tmp via symlink - Added enhanced logging for activity dismissals and other events - Removed iFrame restrictions
This commit is contained in:
@@ -347,13 +347,22 @@ class TestActivityRoutes:
|
||||
_set_session(client, user_id=user["username"], db_user_id=None, is_admin=False)
|
||||
|
||||
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
||||
response = client.post(
|
||||
"/api/activity/dismiss",
|
||||
json={"item_type": "download", "item_key": "download:test-task"},
|
||||
)
|
||||
with patch("shelfmark.core.activity_routes.logger.warning") as mock_warning:
|
||||
response = client.post(
|
||||
"/api/activity/dismiss",
|
||||
json={"item_type": "download", "item_key": "download:test-task"},
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json["code"] == "user_identity_unavailable"
|
||||
mock_warning.assert_called_once()
|
||||
log_message = mock_warning.call_args.args[0]
|
||||
assert "Activity dismiss rejected" in log_message
|
||||
assert "status=403" in log_message
|
||||
assert "reason=User identity unavailable for activity workflow" in log_message
|
||||
assert "path=/api/activity/dismiss" in log_message
|
||||
assert f"user={user['username']}" in log_message
|
||||
assert "db_user_id=-" in log_message
|
||||
|
||||
def test_dismiss_returns_404_when_download_history_row_is_missing(self, main_module, client):
|
||||
user = _create_user(main_module, prefix="reader")
|
||||
@@ -535,15 +544,16 @@ class TestActivityRoutes:
|
||||
)
|
||||
|
||||
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
||||
response = client.post(
|
||||
"/api/activity/dismiss-many",
|
||||
json={
|
||||
"items": [
|
||||
{"item_type": "download", "item_key": f"download:{existing_task_id}"},
|
||||
{"item_type": "download", "item_key": "download:missing-bulk-task"},
|
||||
]
|
||||
},
|
||||
)
|
||||
with patch("shelfmark.core.activity_routes.logger.warning") as mock_warning:
|
||||
response = client.post(
|
||||
"/api/activity/dismiss-many",
|
||||
json={
|
||||
"items": [
|
||||
{"item_type": "download", "item_key": f"download:{existing_task_id}"},
|
||||
{"item_type": "download", "item_key": "download:missing-bulk-task"},
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
assert response.json["error"] == "One or more activity items were not found"
|
||||
@@ -552,6 +562,14 @@ class TestActivityRoutes:
|
||||
main_module,
|
||||
viewer_scope=f"user:{user['id']}",
|
||||
)
|
||||
mock_warning.assert_called_once()
|
||||
log_message = mock_warning.call_args.args[0]
|
||||
assert "Activity dismiss_many rejected" in log_message
|
||||
assert "status=404" in log_message
|
||||
assert "reason=One or more activity items were not found" in log_message
|
||||
assert "path=/api/activity/dismiss-many" in log_message
|
||||
assert "item_count=2" in log_message
|
||||
assert "missing_item_keys=download:missing-bulk-task" in log_message
|
||||
|
||||
def test_no_auth_dismiss_many_and_history_use_shared_identity(self, main_module):
|
||||
task_id = f"no-auth-{uuid.uuid4().hex[:10]}"
|
||||
@@ -664,6 +682,25 @@ class TestActivityRoutes:
|
||||
assert response.status_code == 403
|
||||
assert response.json["code"] == "user_identity_unavailable"
|
||||
|
||||
def test_clear_history_logs_identity_failure(self, main_module, client):
|
||||
admin = _create_user(main_module, prefix="admin", role="admin")
|
||||
_set_session(client, user_id=admin["username"], db_user_id=None, is_admin=True)
|
||||
|
||||
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
||||
with patch("shelfmark.core.activity_routes.logger.warning") as mock_warning:
|
||||
response = client.delete("/api/activity/history")
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json["code"] == "user_identity_unavailable"
|
||||
mock_warning.assert_called_once()
|
||||
log_message = mock_warning.call_args.args[0]
|
||||
assert "Activity history_clear rejected" in log_message
|
||||
assert "status=403" in log_message
|
||||
assert "reason=User identity unavailable for activity workflow" in log_message
|
||||
assert "path=/api/activity/history" in log_message
|
||||
assert f"user={admin['username']}" in log_message
|
||||
assert "is_admin=True" in log_message
|
||||
|
||||
def test_snapshot_backfills_undismissed_terminal_download_from_download_history(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)
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
"""Tests for auth mode and admin policy helpers used by OIDC integration."""
|
||||
|
||||
import sqlite3
|
||||
|
||||
from shelfmark.core.auth_modes import (
|
||||
determine_auth_mode,
|
||||
get_settings_tab_from_path,
|
||||
get_auth_check_admin_status,
|
||||
is_settings_or_onboarding_path,
|
||||
load_active_auth_mode,
|
||||
requires_admin_for_settings_access,
|
||||
should_restrict_settings_to_admin,
|
||||
)
|
||||
@@ -60,6 +63,18 @@ class TestDetermineAuthMode:
|
||||
}
|
||||
assert determine_auth_mode(config, cwa_db_path=None, has_local_admin=False) == "none"
|
||||
|
||||
def test_load_active_auth_mode_reads_env_backed_cwa_setting(self, monkeypatch, tmp_path):
|
||||
monkeypatch.setenv("CONFIG_DIR", str(tmp_path))
|
||||
monkeypatch.setenv("AUTH_METHOD", "cwa")
|
||||
|
||||
cwa_db_path = tmp_path / "app.db"
|
||||
conn = sqlite3.connect(cwa_db_path)
|
||||
conn.execute("create table user (name text)")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
assert load_active_auth_mode(cwa_db_path) == "cwa"
|
||||
|
||||
|
||||
class TestSettingsRestrictionPolicy:
|
||||
def test_settings_path_detection(self):
|
||||
|
||||
@@ -134,6 +134,30 @@ class TestOIDCLoginEndpoint:
|
||||
assert resp.status_code == 500
|
||||
assert resp.get_json()["error"] == "OIDC not configured"
|
||||
|
||||
@patch("shelfmark.core.oidc_routes._get_oidc_client")
|
||||
def test_login_stores_valid_return_to_in_session(self, mock_get_client, client):
|
||||
fake_client = Mock()
|
||||
fake_client.authorize_redirect.return_value = redirect("https://auth.example.com/authorize")
|
||||
mock_get_client.return_value = (fake_client, MOCK_OIDC_CONFIG)
|
||||
|
||||
resp = client.get("/api/auth/oidc/login?return_to=%2F%3Fq%3DSanderson")
|
||||
|
||||
assert resp.status_code == 302
|
||||
with client.session_transaction() as sess:
|
||||
assert sess["oidc_return_to"] == "/?q=Sanderson"
|
||||
|
||||
@patch("shelfmark.core.oidc_routes._get_oidc_client")
|
||||
def test_login_ignores_unsafe_return_to(self, mock_get_client, client):
|
||||
fake_client = Mock()
|
||||
fake_client.authorize_redirect.return_value = redirect("https://auth.example.com/authorize")
|
||||
mock_get_client.return_value = (fake_client, MOCK_OIDC_CONFIG)
|
||||
|
||||
resp = client.get("/api/auth/oidc/login?return_to=https://evil.example.com/phish")
|
||||
|
||||
assert resp.status_code == 302
|
||||
with client.session_transaction() as sess:
|
||||
assert "oidc_return_to" not in sess
|
||||
|
||||
|
||||
class TestOIDCCallbackEndpoint:
|
||||
@patch("shelfmark.core.oidc_routes._get_oidc_client")
|
||||
@@ -158,6 +182,62 @@ class TestOIDCCallbackEndpoint:
|
||||
assert sess["user_id"] == "john"
|
||||
assert sess["db_user_id"] is not None
|
||||
|
||||
@patch("shelfmark.core.oidc_routes._get_oidc_client")
|
||||
def test_callback_redirects_to_original_url_with_query(self, mock_get_client, client):
|
||||
fake_client = Mock()
|
||||
fake_client.authorize_redirect.return_value = redirect("https://auth.example.com/authorize")
|
||||
fake_client.authorize_access_token.return_value = {
|
||||
"userinfo": {
|
||||
"sub": "user-123",
|
||||
"email": "john@example.com",
|
||||
"name": "John Doe",
|
||||
"preferred_username": "john",
|
||||
"groups": ["users"],
|
||||
}
|
||||
}
|
||||
mock_get_client.return_value = (fake_client, MOCK_OIDC_CONFIG)
|
||||
|
||||
login_resp = client.get("/api/auth/oidc/login?return_to=%2F%3Fq%3DSanderson")
|
||||
assert login_resp.status_code == 302
|
||||
|
||||
resp = client.get("/api/auth/oidc/callback?code=abc123&state=test-state")
|
||||
assert resp.status_code == 302
|
||||
|
||||
parsed = urlparse(resp.headers["Location"])
|
||||
assert parsed.path == "/"
|
||||
assert parse_qs(parsed.query) == {"q": ["Sanderson"]}
|
||||
|
||||
@patch("shelfmark.core.oidc_routes._get_oidc_client")
|
||||
def test_callback_redirects_to_original_url_with_script_root(self, mock_get_client, client):
|
||||
fake_client = Mock()
|
||||
fake_client.authorize_redirect.return_value = redirect("https://auth.example.com/authorize")
|
||||
fake_client.authorize_access_token.return_value = {
|
||||
"userinfo": {
|
||||
"sub": "user-123",
|
||||
"email": "john@example.com",
|
||||
"name": "John Doe",
|
||||
"preferred_username": "john",
|
||||
"groups": ["users"],
|
||||
}
|
||||
}
|
||||
mock_get_client.return_value = (fake_client, MOCK_OIDC_CONFIG)
|
||||
|
||||
login_resp = client.get(
|
||||
"/api/auth/oidc/login?return_to=%2Frequests%3Fq%3DSanderson",
|
||||
environ_overrides={"SCRIPT_NAME": "/shelfmark"},
|
||||
)
|
||||
assert login_resp.status_code == 302
|
||||
|
||||
resp = client.get(
|
||||
"/api/auth/oidc/callback?code=abc123&state=test-state",
|
||||
environ_overrides={"SCRIPT_NAME": "/shelfmark"},
|
||||
)
|
||||
assert resp.status_code == 302
|
||||
|
||||
parsed = urlparse(resp.headers["Location"])
|
||||
assert parsed.path == "/shelfmark/requests"
|
||||
assert parse_qs(parsed.query) == {"q": ["Sanderson"]}
|
||||
|
||||
@patch("shelfmark.core.oidc_routes._get_oidc_client")
|
||||
def test_callback_sets_admin_from_groups(self, mock_get_client, client):
|
||||
fake_client = Mock()
|
||||
|
||||
Reference in New Issue
Block a user