Skip to content

Commit 48e669a

Browse files
committed
more tests and remove latin1
1 parent bf675a7 commit 48e669a

File tree

2 files changed

+86
-13
lines changed

2 files changed

+86
-13
lines changed

mssql_python/connection.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1185,9 +1185,9 @@ def getinfo(self, info_type: int) -> Union[str, int, bool, None]:
11851185
# Make sure we use the correct amount of data based on length
11861186
actual_data = data[:length]
11871187

1188-
# SQLGetInfoW returns UTF-16LE encoded strings
1189-
# Try encodings in order: UTF-16LE (Windows), UTF-8, Latin-1
1190-
for encoding in ("utf-16-le", "utf-8", "latin1"):
1188+
# SQLGetInfoW returns UTF-16LE encoded strings (wide-character ODBC API)
1189+
# Try UTF-16LE first (expected), then UTF-8 as fallback
1190+
for encoding in ("utf-16-le", "utf-8"):
11911191
try:
11921192
return actual_data.decode(encoding).rstrip("\0")
11931193
except UnicodeDecodeError:
@@ -1196,8 +1196,9 @@ def getinfo(self, info_type: int) -> Union[str, int, bool, None]:
11961196
# All decodings failed
11971197
logger.debug(
11981198
"error",
1199-
"Failed to decode string in getinfo with any supported encoding. "
1199+
"Failed to decode string in getinfo (info_type=%d) with supported encodings. "
12001200
"Returning None to avoid silent corruption.",
1201+
info_type,
12011202
)
12021203
return None
12031204
else:

tests/test_003_connection.py

Lines changed: 81 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5187,31 +5187,103 @@ def test_getinfo_basic_driver_info(db_connection):
51875187

51885188
def test_getinfo_string_encoding_utf16(db_connection):
51895189
"""Test that string values from getinfo are properly decoded from UTF-16."""
5190-
5190+
51915191
# Test string info types that should not contain null bytes
51925192
string_info_types = [
51935193
("SQL_DRIVER_VER", sql_const.SQL_DRIVER_VER.value),
51945194
("SQL_DRIVER_NAME", sql_const.SQL_DRIVER_NAME.value),
51955195
("SQL_DRIVER_ODBC_VER", sql_const.SQL_DRIVER_ODBC_VER.value),
51965196
("SQL_SERVER_NAME", sql_const.SQL_SERVER_NAME.value),
51975197
]
5198-
5198+
51995199
for name, info_type in string_info_types:
52005200
result = db_connection.getinfo(info_type)
5201-
5201+
52025202
if result is not None:
52035203
# Verify it's a string
5204-
assert isinstance(result, str), \
5205-
f"{name}: Expected str, got {type(result).__name__}"
5206-
5204+
assert isinstance(result, str), f"{name}: Expected str, got {type(result).__name__}"
5205+
52075206
# Verify no null bytes (indicates UTF-16 decoded as UTF-8 bug)
5208-
assert '\x00' not in result, \
5209-
f"{name} contains null bytes, likely UTF-16/UTF-8 encoding mismatch: {repr(result)}"
5210-
5207+
assert (
5208+
"\x00" not in result
5209+
), f"{name} contains null bytes, likely UTF-16/UTF-8 encoding mismatch: {repr(result)}"
5210+
52115211
# Verify it's not empty (optional, but good sanity check)
52125212
assert len(result) > 0, f"{name} returned empty string"
52135213

52145214

5215+
def test_getinfo_string_decoding_utf8_fallback(db_connection):
5216+
"""Test that getinfo falls back to UTF-8 when UTF-16LE decoding fails.
5217+
5218+
This test verifies the fallback path in the encoding loop where
5219+
UTF-16LE fails but UTF-8 succeeds.
5220+
"""
5221+
from unittest.mock import patch
5222+
5223+
# UTF-8 encoded "Hello" - this is valid UTF-8 but NOT valid UTF-16LE
5224+
# (odd number of bytes would fail UTF-16LE decode)
5225+
utf8_data = "Hello".encode("utf-8") # b'Hello' - 5 bytes, odd length
5226+
5227+
mock_result = {"data": utf8_data, "length": len(utf8_data)}
5228+
5229+
# Use a string-type info_type (SQL_DRIVER_NAME = 6 is in string_type_constants)
5230+
info_type = sql_const.SQL_DRIVER_NAME.value
5231+
5232+
with patch.object(db_connection._conn, "get_info", return_value=mock_result):
5233+
result = db_connection.getinfo(info_type)
5234+
5235+
assert result == "Hello", f"Expected 'Hello', got {repr(result)}"
5236+
assert isinstance(result, str), f"Expected str, got {type(result).__name__}"
5237+
5238+
5239+
def test_getinfo_string_decoding_all_fail_returns_none(db_connection):
5240+
"""Test that getinfo returns None when all decoding attempts fail.
5241+
5242+
This test verifies that when both UTF-16LE and UTF-8 decoding fail,
5243+
the method returns None to avoid silent data corruption.
5244+
"""
5245+
from unittest.mock import patch
5246+
5247+
# Invalid byte sequence that cannot be decoded as UTF-16LE or UTF-8
5248+
# 0xFF 0xFE is a BOM, but followed by invalid continuation bytes for UTF-8
5249+
# and odd length makes it invalid UTF-16LE
5250+
invalid_data = bytes([0x80, 0x81, 0x82]) # Invalid for both encodings
5251+
5252+
mock_result = {"data": invalid_data, "length": len(invalid_data)}
5253+
5254+
# Use a string-type info_type (SQL_DRIVER_NAME = 6 is in string_type_constants)
5255+
info_type = sql_const.SQL_DRIVER_NAME.value
5256+
5257+
with patch.object(db_connection._conn, "get_info", return_value=mock_result):
5258+
result = db_connection.getinfo(info_type)
5259+
5260+
# Should return None when all decoding fails
5261+
assert result is None, f"Expected None for invalid encoding, got {repr(result)}"
5262+
5263+
5264+
def test_getinfo_string_encoding_utf16_primary(db_connection):
5265+
"""Test that getinfo correctly decodes valid UTF-16LE data.
5266+
5267+
This test verifies the primary (expected) encoding path where
5268+
UTF-16LE decoding succeeds on first try.
5269+
"""
5270+
from unittest.mock import patch
5271+
5272+
# Valid UTF-16LE encoded "Test" with null terminator
5273+
utf16_data = "Test".encode("utf-16-le") + b"\x00\x00"
5274+
5275+
mock_result = {"data": utf16_data, "length": len(utf16_data)}
5276+
5277+
# Use a string-type info_type
5278+
info_type = sql_const.SQL_DRIVER_NAME.value
5279+
5280+
with patch.object(db_connection._conn, "get_info", return_value=mock_result):
5281+
result = db_connection.getinfo(info_type)
5282+
5283+
assert result == "Test", f"Expected 'Test', got {repr(result)}"
5284+
assert "\x00" not in result, f"Result contains null bytes: {repr(result)}"
5285+
5286+
52155287
def test_getinfo_sql_support(db_connection):
52165288
"""Test SQL support and conformance info types."""
52175289

0 commit comments

Comments
 (0)