gabriel / musehub public
test_escape_like.py python
139 lines 5.0 KB
Raw
sha256:3c58668648c7323bb9f5c6881cfe6a3f14fc93fcb73b537d253732952a5bf8bf chore: bump version to 0.2.0rc12 Sonnet 4.6 patch 8 days ago
1 """Tests for checklist 2.2 — escape_like SQL utility.
2
3 Verifies that escape_like() correctly neutralises LIKE metacharacters ('%'
4 and '_') and the escape character itself ('\\') before they are embedded in a
5 LIKE / ILIKE pattern. All tests are synchronous and require no DB or async
6 infrastructure.
7 """
8 from __future__ import annotations
9
10 import fnmatch
11
12 import pytest
13
14 from musehub.db.utils import escape_like
15
16
17 # ---------------------------------------------------------------------------
18 # Single-character escaping
19 # ---------------------------------------------------------------------------
20
21 def test_percent_is_escaped() -> None:
22 assert escape_like("%") == "\\%"
23
24
25 def test_underscore_is_escaped() -> None:
26 assert escape_like("_") == "\\_"
27
28
29 def test_backslash_is_escaped() -> None:
30 # Backslash must be doubled; Python string "\"" is one backslash.
31 assert escape_like("\\") == "\\\\"
32
33
34 # ---------------------------------------------------------------------------
35 # Combined escaping
36 # ---------------------------------------------------------------------------
37
38 def test_combined_percent_and_underscore() -> None:
39 assert escape_like("100%_done") == "100\\%\\_done"
40
41
42 def test_no_special_chars_unchanged() -> None:
43 assert escape_like("normal text") == "normal text"
44
45
46 def test_empty_string_unchanged() -> None:
47 assert escape_like("") == ""
48
49
50 def test_percent_and_space() -> None:
51 assert escape_like("100% complete_now") == "100\\% complete\\_now"
52
53
54 def test_backslash_then_percent_then_underscore() -> None:
55 # Input: a\b%c_d
56 # Backslash is escaped first → a\\b%c_d
57 # Then percent → a\\b\%c_d
58 # Then underscore → a\\b\%c\_d
59 assert escape_like("a\\b%c_d") == "a\\\\b\\%c\\_d"
60
61
62 # ---------------------------------------------------------------------------
63 # Common injection strings
64 # ---------------------------------------------------------------------------
65
66 def test_injection_percent_admin_percent() -> None:
67 result = escape_like("%admin%")
68 assert result == "\\%admin\\%"
69
70
71 def test_injection_underscore_percent() -> None:
72 result = escape_like("_%")
73 assert result == "\\_\\%"
74
75
76 def test_injection_percent_underscore_percent() -> None:
77 result = escape_like("%_%")
78 assert result == "\\%\\_\\%"
79
80
81 # ---------------------------------------------------------------------------
82 # Motivating test — why escaping is necessary
83 # ---------------------------------------------------------------------------
84
85 def test_unescaped_percent_acts_as_wildcard() -> None:
86 """Demonstrate that a raw '%' in a LIKE-style pattern matches anything.
87
88 Python's fnmatch uses '*' for wildcard, but the principle is identical:
89 without escaping, the user-supplied metacharacter becomes a wildcard.
90 This test documents WHY escape_like exists.
91 """
92 # Simulate a LIKE pattern built without escaping — '%' matches any string
93 raw_pattern = "%" + "admin" + "%"
94 # In SQL, LIKE '%admin%' would match ANY string containing "admin".
95 # We simulate the wildcard behaviour with fnmatch ('*' = LIKE '%').
96 fnmatch_pattern = raw_pattern.replace("%", "*")
97 assert fnmatch.fnmatch("superadmin", fnmatch_pattern) # wildcard fires
98 assert fnmatch.fnmatch("admin123", fnmatch_pattern) # wildcard fires
99 assert fnmatch.fnmatch("anything_admin_suffix", fnmatch_pattern)
100
101 # After escaping, the literal string "\\%admin\\%" is NOT a wildcard pattern:
102 escaped = escape_like("%" + "admin" + "%")
103 # The escaped value is a plain string with no special fnmatch meaning.
104 assert escaped == "\\%admin\\%"
105 # It would only match the exact literal sequence in a database LIKE with escape="\\"
106 assert not fnmatch.fnmatch("superadmin", escaped)
107
108
109 def test_unescaped_underscore_acts_as_single_char_wildcard() -> None:
110 """Demonstrate that a raw '_' in a LIKE pattern matches any single character."""
111 # '_' in SQL LIKE matches exactly one character.
112 raw_user_input = "_"
113 # In fnmatch '?' is the single-char wildcard — analogous to SQL '_'.
114 fnmatch_pattern = raw_user_input.replace("_", "?")
115 assert fnmatch.fnmatch("a", fnmatch_pattern) # matches any single char
116 assert fnmatch.fnmatch("z", fnmatch_pattern)
117
118 # After escaping the input is no longer a wildcard
119 escaped = escape_like(raw_user_input)
120 assert escaped == "\\_"
121
122
123 # ---------------------------------------------------------------------------
124 # Multiple backslashes — ensure no double-escaping
125 # ---------------------------------------------------------------------------
126
127 def test_multiple_backslashes() -> None:
128 # Input: two backslashes "\\", each must be doubled independently.
129 assert escape_like("\\\\") == "\\\\\\\\"
130
131
132 def test_backslash_adjacent_to_percent() -> None:
133 # Input: \% — the backslash must be doubled before the percent is escaped
134 # so the output is \\\\\\% (four chars: \\, \, %, each escaped)
135 assert escape_like("\\%") == "\\\\\\%"
136
137
138 def test_backslash_adjacent_to_underscore() -> None:
139 assert escape_like("\\_") == "\\\\\\_"
File History 1 commit
sha256:35d76015db2541686c33edd44343ea2d9f751325b4a5556cc9c4c9c0f84edbbe chore: bump version to 0.2.0rc12 Sonnet 4.6 patch 6 days ago