"""Tests for checklist 2.2 — escape_like SQL utility. Verifies that escape_like() correctly neutralises LIKE metacharacters ('%' and '_') and the escape character itself ('\\') before they are embedded in a LIKE / ILIKE pattern. All tests are synchronous and require no DB or async infrastructure. """ from __future__ import annotations import fnmatch import pytest from musehub.db.utils import escape_like # --------------------------------------------------------------------------- # Single-character escaping # --------------------------------------------------------------------------- def test_percent_is_escaped() -> None: assert escape_like("%") == "\\%" def test_underscore_is_escaped() -> None: assert escape_like("_") == "\\_" def test_backslash_is_escaped() -> None: # Backslash must be doubled; Python string "\"" is one backslash. assert escape_like("\\") == "\\\\" # --------------------------------------------------------------------------- # Combined escaping # --------------------------------------------------------------------------- def test_combined_percent_and_underscore() -> None: assert escape_like("100%_done") == "100\\%\\_done" def test_no_special_chars_unchanged() -> None: assert escape_like("normal text") == "normal text" def test_empty_string_unchanged() -> None: assert escape_like("") == "" def test_percent_and_space() -> None: assert escape_like("100% complete_now") == "100\\% complete\\_now" def test_backslash_then_percent_then_underscore() -> None: # Input: a\b%c_d # Backslash is escaped first → a\\b%c_d # Then percent → a\\b\%c_d # Then underscore → a\\b\%c\_d assert escape_like("a\\b%c_d") == "a\\\\b\\%c\\_d" # --------------------------------------------------------------------------- # Common injection strings # --------------------------------------------------------------------------- def test_injection_percent_admin_percent() -> None: result = escape_like("%admin%") assert result == "\\%admin\\%" def test_injection_underscore_percent() -> None: result = escape_like("_%") assert result == "\\_\\%" def test_injection_percent_underscore_percent() -> None: result = escape_like("%_%") assert result == "\\%\\_\\%" # --------------------------------------------------------------------------- # Motivating test — why escaping is necessary # --------------------------------------------------------------------------- def test_unescaped_percent_acts_as_wildcard() -> None: """Demonstrate that a raw '%' in a LIKE-style pattern matches anything. Python's fnmatch uses '*' for wildcard, but the principle is identical: without escaping, the user-supplied metacharacter becomes a wildcard. This test documents WHY escape_like exists. """ # Simulate a LIKE pattern built without escaping — '%' matches any string raw_pattern = "%" + "admin" + "%" # In SQL, LIKE '%admin%' would match ANY string containing "admin". # We simulate the wildcard behaviour with fnmatch ('*' = LIKE '%'). fnmatch_pattern = raw_pattern.replace("%", "*") assert fnmatch.fnmatch("superadmin", fnmatch_pattern) # wildcard fires assert fnmatch.fnmatch("admin123", fnmatch_pattern) # wildcard fires assert fnmatch.fnmatch("anything_admin_suffix", fnmatch_pattern) # After escaping, the literal string "\\%admin\\%" is NOT a wildcard pattern: escaped = escape_like("%" + "admin" + "%") # The escaped value is a plain string with no special fnmatch meaning. assert escaped == "\\%admin\\%" # It would only match the exact literal sequence in a database LIKE with escape="\\" assert not fnmatch.fnmatch("superadmin", escaped) def test_unescaped_underscore_acts_as_single_char_wildcard() -> None: """Demonstrate that a raw '_' in a LIKE pattern matches any single character.""" # '_' in SQL LIKE matches exactly one character. raw_user_input = "_" # In fnmatch '?' is the single-char wildcard — analogous to SQL '_'. fnmatch_pattern = raw_user_input.replace("_", "?") assert fnmatch.fnmatch("a", fnmatch_pattern) # matches any single char assert fnmatch.fnmatch("z", fnmatch_pattern) # After escaping the input is no longer a wildcard escaped = escape_like(raw_user_input) assert escaped == "\\_" # --------------------------------------------------------------------------- # Multiple backslashes — ensure no double-escaping # --------------------------------------------------------------------------- def test_multiple_backslashes() -> None: # Input: two backslashes "\\", each must be doubled independently. assert escape_like("\\\\") == "\\\\\\\\" def test_backslash_adjacent_to_percent() -> None: # Input: \% — the backslash must be doubled before the percent is escaped # so the output is \\\\\\% (four chars: \\, \, %, each escaped) assert escape_like("\\%") == "\\\\\\%" def test_backslash_adjacent_to_underscore() -> None: assert escape_like("\\_") == "\\\\\\_"