diff --git a/core/credentials/store.py b/core/credentials/store.py index 2d39fdf1..8cceb253 100644 --- a/core/credentials/store.py +++ b/core/credentials/store.py @@ -49,7 +49,13 @@ def validate_credential_payload(service: str, payload): return False, [] if not isinstance(payload, dict): return False, list(required) - missing = [k for k in required if not payload.get(k)] + + def _present(v): + # Whitespace-only strings count as missing — they'd otherwise save a + # blank secret that fails confusingly at the real service later. + return bool(v.strip()) if isinstance(v, str) else bool(v) + + missing = [k for k in required if not _present(payload.get(k))] return (not missing), missing diff --git a/tests/test_admin_gating.py b/tests/test_admin_gating.py index 6ab66818..06dc1adf 100644 --- a/tests/test_admin_gating.py +++ b/tests/test_admin_gating.py @@ -34,12 +34,14 @@ GATED = [ ('DELETE', '/api/library/album/123'), ('POST', '/api/library/tracks/delete-batch'), ('POST', '/api/database/update'), + ('POST', '/api/database/update/stop'), ('POST', '/api/database/backup'), ('DELETE', '/api/database/backups/x.db'), ('POST', '/api/database/backups/x.db/restore'), ('POST', '/api/database/maintenance/vacuum'), ('DELETE', '/api/metadata-cache/clear'), ('DELETE', '/api/metadata-cache/clear-musicbrainz'), + ('POST', '/api/metadata-cache/evict'), ] diff --git a/tests/test_service_credentials.py b/tests/test_service_credentials.py index aec802ec..64deae38 100644 --- a/tests/test_service_credentials.py +++ b/tests/test_service_credentials.py @@ -51,6 +51,13 @@ def test_validate_treats_empty_string_as_missing(): assert not ok and missing == ['username'] +def test_validate_treats_whitespace_only_as_missing(): + # A blank-but-spacey secret must be rejected, not saved to fail later. + ok, missing = validate_credential_payload('navidrome', + {'base_url': 'http://x', 'username': ' ', 'password': 'p'}) + assert not ok and missing == ['username'] + + # ── pure: active-set selection (stale-safe) ────────────────────────────────── def test_pick_active_credential_match_and_misses(): diff --git a/web_server.py b/web_server.py index 805ed158..8376c9b5 100644 --- a/web_server.py +++ b/web_server.py @@ -16105,6 +16105,7 @@ def get_database_update_status(): return jsonify(db_update_state) @app.route('/api/database/update/stop', methods=['POST']) +@admin_only def stop_database_update(): """Endpoint to stop the current database update.""" global db_update_worker @@ -16586,6 +16587,7 @@ def metadata_cache_clear(): return jsonify({"success": False, "error": str(e)}), 500 @app.route('/api/metadata-cache/evict', methods=['POST']) +@admin_only def metadata_cache_evict(): """Evict expired entries from the metadata cache.""" try: