Skip to content

Commit af84830

Browse files
authored
Merge pull request #2153 from Jdubrick/add-data-model-saved-prompts
RHIDP-14305: add validation for saved prompts
1 parent 0a018ba commit af84830

2 files changed

Lines changed: 275 additions & 0 deletions

File tree

src/utils/saved_prompts.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
"""Validation helpers for saved prompts."""
2+
3+
4+
class SavedPromptValidationError(Exception):
5+
"""Invalid saved-prompt field values."""
6+
7+
8+
class SavedPromptLimitExceededError(SavedPromptValidationError):
9+
"""Per-user saved-prompt count would exceed the configured maximum."""
10+
11+
12+
def validate_saved_prompt_quota(
13+
current_count: int,
14+
max_prompts_per_user: int,
15+
) -> None:
16+
"""Raise if creating another saved prompt would exceed the per-user maximum.
17+
18+
The maximum is inclusive so a configured limit of N means the user may hold
19+
N prompts (create is rejected only when they already have N). Call when
20+
``current_count`` is the number of prompts the user already has.
21+
22+
Parameters:
23+
current_count: Number of saved prompts the user currently has.
24+
max_prompts_per_user: Configured maximum prompts allowed per user.
25+
26+
Raises:
27+
SavedPromptLimitExceededError: If ``current_count >= max_prompts_per_user``.
28+
"""
29+
if current_count >= max_prompts_per_user:
30+
raise SavedPromptLimitExceededError(
31+
f"Saved prompt limit exceeded: {current_count} existing prompts, "
32+
f"maximum is {max_prompts_per_user}"
33+
)
34+
35+
36+
def validate_saved_prompt_name(
37+
name: str,
38+
max_display_name_length: int,
39+
) -> str:
40+
"""Validate and normalize a saved-prompt name.
41+
42+
Strips leading/trailing whitespace before checks because names are labels:
43+
padding is not meaningful and would otherwise create near-duplicate names
44+
under uniqueness constraints. Requires a non-empty stripped value whose
45+
length does not exceed ``max_display_name_length``.
46+
47+
Parameters:
48+
name: Prompt display name.
49+
max_display_name_length: Maximum allowed length for the stripped name.
50+
51+
Returns:
52+
The stripped ``name``.
53+
54+
Raises:
55+
SavedPromptValidationError: If the name fails validation.
56+
"""
57+
stripped_name = name.strip()
58+
if not stripped_name:
59+
raise SavedPromptValidationError("Saved prompt name must not be empty")
60+
if len(stripped_name) > max_display_name_length:
61+
raise SavedPromptValidationError(
62+
f"Saved prompt name length {len(stripped_name)} exceeds maximum "
63+
f"{max_display_name_length}"
64+
)
65+
return stripped_name
66+
67+
68+
def validate_saved_prompt_content(
69+
content: str,
70+
max_content_length: int,
71+
) -> None:
72+
"""Validate a saved-prompt content body.
73+
74+
Uses ``strip()`` only to detect blank prompts (empty or whitespace-only).
75+
Non-blank content is left unchanged so intentional leading/trailing
76+
whitespace in the prompt body is preserved; length is measured on that
77+
original string.
78+
79+
Parameters:
80+
content: Prompt body.
81+
max_content_length: Maximum allowed length for the original content.
82+
83+
Raises:
84+
SavedPromptValidationError: If the content fails validation.
85+
"""
86+
if not content.strip():
87+
raise SavedPromptValidationError("Saved prompt content must not be empty")
88+
if len(content) > max_content_length:
89+
raise SavedPromptValidationError(
90+
f"Saved prompt content length {len(content)} exceeds maximum "
91+
f"{max_content_length}"
92+
)
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
"""Unit tests for saved prompt validation helpers."""
2+
3+
import pytest
4+
5+
from utils.saved_prompts import (
6+
SavedPromptLimitExceededError,
7+
SavedPromptValidationError,
8+
validate_saved_prompt_content,
9+
validate_saved_prompt_name,
10+
validate_saved_prompt_quota,
11+
)
12+
13+
14+
class TestValidateSavedPromptQuota:
15+
"""Test cases for validate_saved_prompt_quota."""
16+
17+
def test_allows_count_below_max(self) -> None:
18+
"""Test create is allowed when current count is below the inclusive max."""
19+
validate_saved_prompt_quota(49, 50)
20+
21+
def test_rejects_count_equal_to_max(self) -> None:
22+
"""Test create is rejected when current count equals the inclusive max."""
23+
with pytest.raises(
24+
SavedPromptLimitExceededError,
25+
match=(
26+
r"Saved prompt limit exceeded: 50 existing prompts, " r"maximum is 50"
27+
),
28+
):
29+
validate_saved_prompt_quota(50, 50)
30+
31+
def test_rejects_count_above_max(self) -> None:
32+
"""Test create is rejected when current count is above the max."""
33+
with pytest.raises(
34+
SavedPromptLimitExceededError,
35+
match=(
36+
r"Saved prompt limit exceeded: 51 existing prompts, " r"maximum is 50"
37+
),
38+
):
39+
validate_saved_prompt_quota(51, 50)
40+
41+
def test_rejects_when_max_is_zero(self) -> None:
42+
"""Test max_prompts_per_user of 0 rejects even a zero current count."""
43+
with pytest.raises(
44+
SavedPromptLimitExceededError,
45+
match=(
46+
r"Saved prompt limit exceeded: 0 existing prompts, " r"maximum is 0"
47+
),
48+
):
49+
validate_saved_prompt_quota(0, 0)
50+
51+
def test_limit_exceeded_is_validation_error_subclass(self) -> None:
52+
"""Test SavedPromptLimitExceededError subclasses SavedPromptValidationError."""
53+
assert issubclass(SavedPromptLimitExceededError, SavedPromptValidationError)
54+
55+
56+
class TestValidateSavedPromptName:
57+
"""Test cases for validate_saved_prompt_name."""
58+
59+
def test_valid_name_returns_stripped_value(self) -> None:
60+
"""Test valid name is accepted and returned stripped."""
61+
assert (
62+
validate_saved_prompt_name(" my prompt ", max_display_name_length=10)
63+
== "my prompt"
64+
)
65+
66+
def test_empty_name_rejected(self) -> None:
67+
"""Test empty name raises SavedPromptValidationError."""
68+
with pytest.raises(
69+
SavedPromptValidationError,
70+
match="Saved prompt name must not be empty",
71+
):
72+
validate_saved_prompt_name("", max_display_name_length=10)
73+
74+
def test_spaces_only_name_rejected(self) -> None:
75+
"""Test spaces-only name raises SavedPromptValidationError."""
76+
with pytest.raises(
77+
SavedPromptValidationError,
78+
match="Saved prompt name must not be empty",
79+
):
80+
validate_saved_prompt_name(" ", max_display_name_length=10)
81+
82+
def test_mixed_whitespace_only_name_rejected(self) -> None:
83+
"""Test name of only spaces/newlines/tabs is rejected."""
84+
with pytest.raises(
85+
SavedPromptValidationError,
86+
match="Saved prompt name must not be empty",
87+
):
88+
validate_saved_prompt_name(" \n\t ", max_display_name_length=10)
89+
90+
def test_name_at_exact_max_length_accepted(self) -> None:
91+
"""Test name of exactly max_display_name_length is accepted."""
92+
assert (
93+
validate_saved_prompt_name("abcdefghij", max_display_name_length=10)
94+
== "abcdefghij"
95+
)
96+
97+
def test_name_longer_than_max_rejected(self) -> None:
98+
"""Test name exceeding max_display_name_length is rejected."""
99+
with pytest.raises(
100+
SavedPromptValidationError,
101+
match="Saved prompt name length 11 exceeds maximum 10",
102+
):
103+
validate_saved_prompt_name("abcdefghijk", max_display_name_length=10)
104+
105+
def test_name_rejected_when_max_length_is_zero(self) -> None:
106+
"""Test max_display_name_length of 0 rejects any non-empty name."""
107+
with pytest.raises(
108+
SavedPromptValidationError,
109+
match="Saved prompt name length 1 exceeds maximum 0",
110+
):
111+
validate_saved_prompt_name("a", max_display_name_length=0)
112+
113+
def test_unicode_emoji_name_within_length_accepted(self) -> None:
114+
"""Test unicode/emoji name within length is accepted."""
115+
assert (
116+
validate_saved_prompt_name("🔥 tip", max_display_name_length=10) == "🔥 tip"
117+
)
118+
119+
120+
class TestValidateSavedPromptContent:
121+
"""Test cases for validate_saved_prompt_content."""
122+
123+
def test_valid_content_accepted(self) -> None:
124+
"""Test non-empty content within the max length is accepted."""
125+
validate_saved_prompt_content(
126+
"do something useful",
127+
max_content_length=50,
128+
)
129+
130+
def test_empty_content_rejected(self) -> None:
131+
"""Test empty content raises SavedPromptValidationError."""
132+
with pytest.raises(
133+
SavedPromptValidationError,
134+
match="Saved prompt content must not be empty",
135+
):
136+
validate_saved_prompt_content("", max_content_length=50)
137+
138+
def test_spaces_only_content_rejected(self) -> None:
139+
"""Test spaces-only content raises SavedPromptValidationError."""
140+
with pytest.raises(
141+
SavedPromptValidationError,
142+
match="Saved prompt content must not be empty",
143+
):
144+
validate_saved_prompt_content(" ", max_content_length=50)
145+
146+
def test_mixed_whitespace_only_content_rejected(self) -> None:
147+
"""Test content of only spaces/newlines/tabs is rejected."""
148+
with pytest.raises(
149+
SavedPromptValidationError,
150+
match="Saved prompt content must not be empty",
151+
):
152+
validate_saved_prompt_content(" \n\t ", max_content_length=50)
153+
154+
def test_non_blank_content_with_whitespace_accepted(self) -> None:
155+
"""Test content with leading/trailing whitespace is accepted when non-blank."""
156+
validate_saved_prompt_content(
157+
" keep my spaces ",
158+
max_content_length=50,
159+
)
160+
161+
def test_content_at_exact_max_length_accepted(self) -> None:
162+
"""Test content of exactly max_content_length is accepted."""
163+
validate_saved_prompt_content("123456789012", max_content_length=12)
164+
165+
def test_content_longer_than_max_rejected(self) -> None:
166+
"""Test content exceeding max_content_length is rejected on original length."""
167+
# Leading spaces count toward length; strip would be under max but original is not.
168+
with pytest.raises(
169+
SavedPromptValidationError,
170+
match="Saved prompt content length 14 exceeds maximum 12",
171+
):
172+
validate_saved_prompt_content(
173+
" 1234567890 ",
174+
max_content_length=12,
175+
)
176+
177+
def test_content_rejected_when_max_length_is_zero(self) -> None:
178+
"""Test max_content_length of 0 rejects any non-empty content."""
179+
with pytest.raises(
180+
SavedPromptValidationError,
181+
match="Saved prompt content length 1 exceeds maximum 0",
182+
):
183+
validate_saved_prompt_content("a", max_content_length=0)

0 commit comments

Comments
 (0)