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:
Alex
2026-03-11 18:16:34 +00:00
committed by GitHub
parent a2a5a22324
commit c59ea46540
99 changed files with 3222 additions and 2167 deletions
+50 -13
View File
@@ -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)
+15
View File
@@ -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):
+80
View File
@@ -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()