"""Tests for JQL utility functions (reserved word quoting)."""
import pytest
from mcp_atlassian.jira.utils import (
quote_jql_identifier_if_needed,
sanitize_jql_reserved_words,
)
class TestJQLQuoting:
"""Tests for quote_jql_identifier_if_needed()."""
@pytest.mark.parametrize(
"identifier, expected",
[
# Reserved words get quoted
("IF", '"IF"'),
("AND", '"AND"'),
("OR", '"OR"'),
("NOT", '"NOT"'),
("ORDER", '"ORDER"'),
("IN", '"IN"'),
# Case-insensitive reserved word detection
("and", '"and"'),
("if", '"if"'),
("or", '"or"'),
# Normal identifiers pass through
("TEST", "TEST"),
("MYPROJECT", "MYPROJECT"),
("SCRUM", "SCRUM"),
# Starts with digit → quoted
("123P", '"123P"'),
("1ABC", '"1ABC"'),
# Internal quote escaping
('my"key', '"my\\"key"'),
# Internal backslash escaping
("my\\key", '"my\\\\key"'),
# Both backslash and double-quote (PR #949 regression)
('my\\"key', '"my\\\\\\"key"'),
],
ids=[
"reserved-IF",
"reserved-AND",
"reserved-OR",
"reserved-NOT",
"reserved-ORDER",
"reserved-IN",
"reserved-lower-and",
"reserved-lower-if",
"reserved-lower-or",
"normal-TEST",
"normal-MYPROJECT",
"normal-SCRUM",
"digit-start-123P",
"digit-start-1ABC",
"internal-quote",
"internal-backslash",
"internal-backslash-and-quote",
],
)
def test_quoting(self, identifier: str, expected: str) -> None:
assert quote_jql_identifier_if_needed(identifier) == expected
class TestSanitizeJQL:
"""Tests for sanitize_jql_reserved_words()."""
@pytest.mark.parametrize(
"input_jql, expected_jql",
[
# project = reserved word → quoted
(
"project = IF AND status = Open",
'project = "IF" AND status = Open',
),
# project IN with reserved words → only reserved words quoted
(
"project IN (IF, AND, TEST)",
'project IN ("IF", "AND", TEST)',
),
# Already quoted → unchanged
(
'project = "IF"',
'project = "IF"',
),
# Non-reserved word → unchanged
(
"project = TEST",
"project = TEST",
),
# Case-insensitive matching
(
"PROJECT = if",
'PROJECT = "if"',
),
# ORDER BY preserved
(
"project = IF ORDER BY created",
'project = "IF" ORDER BY created',
),
# No project clause → unchanged
(
"status = Open",
"status = Open",
),
# String literal containing project = IF → NOT modified
(
'summary ~ "project = IF"',
'summary ~ "project = IF"',
),
# None → None
(None, None),
# Empty string → empty string
("", ""),
# IN with extra whitespace
(
"project IN ( IF , AND )",
'project IN ( "IF" , "AND" )',
),
# Multiple project clauses
(
"project = IF OR project = OR",
'project = "IF" OR project = "OR"',
),
# project != reserved word (operator variation)
(
"project != IF AND status = Open",
'project != "IF" AND status = Open',
),
# project NOT IN with reserved words
(
"project NOT IN (IF, AND)",
'project NOT IN ("IF", "AND")',
),
# Already-quoted values in IN clause → preserved
(
'project IN ("IF", AND, TEST)',
'project IN ("IF", "AND", TEST)',
),
# Single-quoted string literal → NOT modified
(
"summary ~ 'project = IF'",
"summary ~ 'project = IF'",
),
# Single-quoted value in IN clause → preserved
(
"project IN ('IF', AND)",
"project IN ('IF', \"AND\")",
),
],
ids=[
"equals-reserved",
"in-mixed-reserved",
"already-quoted",
"non-reserved",
"case-insensitive",
"order-by-preserved",
"no-project-clause",
"string-literal-untouched",
"none-input",
"empty-string",
"in-extra-whitespace",
"multiple-project-clauses",
"not-equals-reserved",
"not-in-reserved",
"in-mixed-quoted-unquoted",
"single-quote-literal-untouched",
"single-quote-in-preserved",
],
)
def test_sanitize(self, input_jql: str | None, expected_jql: str | None) -> None:
assert sanitize_jql_reserved_words(input_jql) == expected_jql