gabriel / musehub public
test_escape_like.py python
139 lines 5.0 KB
Raw
sha256:73f24973678ceefd56628ee17c6d0535d86e33533828af6c63348f50941515ad Merge branch 'fix/smoke-test-tag-label' into dev Human 14 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 12 commits
sha256:cfefc25a166c3c3eed8ea3529aee19ea350bc05f2954d007420e924133b7d8ce chore: pivot to nightly channel — bump version to 0.2.0.dev… Sonnet 5 patch 12 days ago
sha256:d035733f21ccff27735fddebfbbe0ed24565a32a22db8de5885402262671ecd2 chore: bump version to 0.2.0rc15 for musehub#113 fix release Sonnet 4.6 patch 14 days ago
sha256:0032d6cfa33bc3c8367436ad768e7dd0e339b4332153160247da8266cb5fa352 Merge branch 'task/version-tags-phase3-server' into dev Human 17 days ago
sha256:4669620efda9ff41c55bdefd1f7bfe1c239d468428744c84ead9957e5a003a53 merge: rescue snapshot-recovery hardening (c00aa21d) into d… Opus 4.8 minor 29 days ago
sha256:a59da49c4611b970fc4b6ae48678ce4943261c213a07ddbd73ce9201df869b4a fix: remove false-positive proposal_comments index drop fro… Sonnet 4.6 patch 34 days ago
sha256:0a240d6dbff234f07d98a28a4a9a68db702f3f9ff9260196f24219bdb1c0b6f3 feat: render markdown mists as HTML with heading anchor links Sonnet 4.6 patch 34 days ago
sha256:24a7d47486ebc4ebd1832830580e177ec6f877b48dced8c000e198cdec4ce9d6 Merge 'task/bump-version-rc12' into 'dev' — proposal: Bump … Human 35 days ago
sha256:b9ff931d147e0114a1f17060f415b89ed551c170a91ff226c70437aa5c85f9ee Merge 'task/bump-version-rc12' into 'dev' — proposal: Bump … Human 35 days ago
sha256:d1122d21e73471879b460037b22c0b50fded7c423444a176f248428f75dac39c Merge 'task/fix-issue-pagination-cursor' into 'dev' — propo… Human 35 days ago
sha256:01e18975e73d2b3cd5b6db7929c895bef9aa6e0d4391dc5b2adfc548b41318dd Merge 'feat/adding-debug-logs-to-staging' into 'dev' — prop… Human 36 days ago
sha256:6b1949fc2797ca4c1936a637a4cbfec828ef56cf52398a2e74ca3c4f494e728f fix: use wire_bytes not mpack_bytes_raw in compute_object_b… Sonnet 4.6 patch 48 days ago
sha256:b99f2455dc346966d040133f5203297e6e3ef5803a93728a2c30568d0a0f7583 rename: delta_add → delta_upsert across wire format, models… Sonnet 4.6 patch 50 days ago