Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
"""$toString arity and field path syntax tests."""

from datetime import datetime, timezone

import pytest
from bson import Binary, Code, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp

from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501
ExpressionTestCase,
)
from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import (
assert_expression_result,
execute_expression,
)
from documentdb_tests.framework.error_codes import (
CONVERSION_FAILURE_ERROR,
FAILED_TO_PARSE_ERROR,
INVALID_DOLLAR_FIELD_PATH,
TO_TYPE_ARITY_ERROR,
)
from documentdb_tests.framework.parametrize import pytest_params
from documentdb_tests.framework.test_constants import DECIMAL128_ONE_AND_HALF

# Property [Arity]: $toString unwraps single-element literal arrays and rejects empty or
# multi-element arrays. Non-convertible types unwrapped from a single-element array are
# rejected with a conversion failure.
TOSTRING_ARITY_TESTS: list[ExpressionTestCase] = [
ExpressionTestCase(
"single_null",
msg="Single-element literal array wrapping null unwraps and returns null",
expression={"$toString": [None]},
expected=None,
),
ExpressionTestCase(
"single_bool",
msg="Single-element literal array wrapping bool unwraps and converts",
expression={"$toString": [True]},
expected="true",
),
ExpressionTestCase(
"single_int32",
msg="Single-element literal array wrapping int32 unwraps and converts",
expression={"$toString": [42]},
expected="42",
),
ExpressionTestCase(
"single_int64",
msg="Single-element literal array wrapping int64 unwraps and converts",
expression={"$toString": [Int64(99)]},
expected="99",
),
ExpressionTestCase(
"single_double",
msg="Single-element literal array wrapping double unwraps and converts",
expression={"$toString": [3.14]},
expected="3.14",
),
ExpressionTestCase(
"single_decimal128",
msg="Single-element literal array wrapping Decimal128 unwraps and converts",
expression={"$toString": [DECIMAL128_ONE_AND_HALF]},
expected="1.5",
),
ExpressionTestCase(
"single_string",
msg="Single-element literal array wrapping string unwraps and returns it",
expression={"$toString": ["hello"]},
expected="hello",
),
ExpressionTestCase(
"single_objectid",
msg="Single-element literal array wrapping ObjectId unwraps and converts",
expression={"$toString": [ObjectId("507f1f77bcf86cd799439011")]},
expected="507f1f77bcf86cd799439011",
),
ExpressionTestCase(
"single_datetime",
msg="Single-element literal array wrapping datetime unwraps and converts",
expression={"$toString": [datetime(2024, 1, 1, tzinfo=timezone.utc)]},
expected="2024-01-01T00:00:00.000Z",
),
ExpressionTestCase(
"single_binary",
msg="Single-element literal array wrapping Binary unwraps and converts",
expression={"$toString": [Binary(b"hi", 0)]},
expected="aGk=",
),
ExpressionTestCase(
"single_minkey",
msg="Single-element array with MinKey unwraps then rejects with conversion failure",
expression={"$toString": [MinKey()]},
error_code=CONVERSION_FAILURE_ERROR,
),
ExpressionTestCase(
"single_maxkey",
msg="Single-element array with MaxKey unwraps then rejects with conversion failure",
expression={"$toString": [MaxKey()]},
error_code=CONVERSION_FAILURE_ERROR,
),
ExpressionTestCase(
"single_timestamp",
msg="Single-element array with Timestamp unwraps then rejects with conversion failure",
expression={"$toString": [Timestamp(1, 1)]},
error_code=CONVERSION_FAILURE_ERROR,
),
ExpressionTestCase(
"single_regex",
msg="Single-element array with Regex unwraps then rejects with conversion failure",
expression={"$toString": [Regex("abc")]},
error_code=CONVERSION_FAILURE_ERROR,
),
ExpressionTestCase(
"single_code",
msg="Single-element array with Code unwraps then rejects with conversion failure",
expression={"$toString": [Code("x")]},
error_code=CONVERSION_FAILURE_ERROR,
),
ExpressionTestCase(
"empty_array",
msg="Empty literal array is an arity error",
expression={"$toString": []},
error_code=TO_TYPE_ARITY_ERROR,
),
ExpressionTestCase(
"two_elements",
msg="Two-element literal array is an arity error",
expression={"$toString": ["a", "b"]},
error_code=TO_TYPE_ARITY_ERROR,
),
ExpressionTestCase(
"large_array",
msg="Large literal array is an arity error",
expression={"$toString": list(range(100))},
error_code=TO_TYPE_ARITY_ERROR,
),
]

# Property [Invalid Field Path]: $toString rejects malformed field path syntax.
TOSTRING_INVALID_FIELD_PATH_TESTS: list[ExpressionTestCase] = [
ExpressionTestCase(
"bare_dollar",
msg="Bare '$' is an invalid field path",
expression={"$toString": "$"},
error_code=INVALID_DOLLAR_FIELD_PATH,
),
ExpressionTestCase(
"double_dollar",
msg="'$$' is rejected as an empty variable name",
expression={"$toString": "$$"},
error_code=FAILED_TO_PARSE_ERROR,
),
]


@pytest.mark.parametrize(
"test", pytest_params(TOSTRING_ARITY_TESTS + TOSTRING_INVALID_FIELD_PATH_TESTS)
)
def test_toString_arity(collection, test: ExpressionTestCase):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think every test in these files was originally parameterized over $convert too. These $toX operators are essentially an alias for $convert: {input, to}.

I missed this in the context of #684 but we should probably fix it there too as it's a significant coverage gap. The $convert tests rely on these split coverage.

"""$toString literal array arguments are unwrapped or rejected based on arity."""
result = execute_expression(collection, test.expression)
assert_expression_result(
result, expected=test.expected, error_code=test.error_code, msg=test.msg
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
"""$toString Binary conversion tests: base64 encoding, UUID format, and subtype handling."""

import pytest
from bson import Binary

from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501
ExpressionTestCase,
)
from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import (
assert_expression_result,
execute_expression,
)
from documentdb_tests.framework.parametrize import pytest_params

# Property [Binary Conversion]: non-UUID Binary values are base64 encoded; subtype 4 with
# exactly 16 bytes converts to UUID string format; subtype 2 (old binary) includes the
# inner 4-byte length prefix in the base64 encoding.
TOSTRING_BINARY_TESTS: list[ExpressionTestCase] = [
ExpressionTestCase(
"binary_empty_sub0",
msg="Empty Binary (subtype 0) converts to empty string",
expression={"$toString": Binary(b"", 0)},
expected="",
),
ExpressionTestCase(
"binary_sub0",
msg="Subtype 0 Binary is base64 encoded",
expression={"$toString": Binary(b"hello", 0)},
expected="aGVsbG8=",
),
ExpressionTestCase(
"binary_sub1",
msg="Subtype 1 Binary is base64 encoded",
expression={"$toString": Binary(b"hello", 1)},
expected="aGVsbG8=",
),
ExpressionTestCase(
"binary_sub2_old_binary",
msg="Subtype 2 (old binary) includes the inner 4-byte length prefix in base64",
expression={"$toString": Binary(b"hello", 2)},
expected="BQAAAGhlbGxv",
),
ExpressionTestCase(
"binary_sub3",
msg="Subtype 3 Binary is base64 encoded",
expression={"$toString": Binary(b"hello", 3)},
expected="aGVsbG8=",
),
ExpressionTestCase(
"binary_sub5",
msg="Subtype 5 Binary is base64 encoded",
expression={"$toString": Binary(b"hello", 5)},
expected="aGVsbG8=",
),
ExpressionTestCase(
"binary_sub6",
msg="Subtype 6 (encrypted) Binary is base64 encoded",
expression={"$toString": Binary(b"hello", 6)},
expected="aGVsbG8=",
),
ExpressionTestCase(
"binary_sub8",
msg="Subtype 8 (sensitive) Binary is base64 encoded",
expression={"$toString": Binary(b"hello", 8)},
expected="aGVsbG8=",
),
ExpressionTestCase(
"binary_sub9",
msg="Subtype 9 (vector) Binary is base64 encoded",
expression={"$toString": Binary(b"hello", 9)},
expected="aGVsbG8=",
),
ExpressionTestCase(
"binary_sub128",
msg="Subtype 128 Binary is base64 encoded",
expression={"$toString": Binary(b"hello", 128)},
expected="aGVsbG8=",
),
ExpressionTestCase(
"binary_sub4_uuid",
msg="Subtype 4 Binary with exactly 16 bytes converts to UUID format",
expression={
"$toString": Binary(
b"\x12\x34\x56\x78\x12\x34\x12\x34\x12\x34\x12\x34\x56\x78\x90\x12",
4,
)
},
expected="12345678-1234-1234-1234-123456789012",
),
ExpressionTestCase(
"binary_sub4_15bytes",
msg="Subtype 4 with 15 bytes (not 16) falls back to base64",
expression={
"$toString": Binary(
b"\x12\x34\x56\x78\x12\x34\x12\x34\x12\x34\x12\x34\x56\x78\x90",
4,
)
},
expected="EjRWeBI0EjQSNBI0VniQ",
),
ExpressionTestCase(
"binary_sub4_17bytes",
msg="Subtype 4 with 17 bytes (not 16) falls back to base64",
expression={
"$toString": Binary(
b"\x12\x34\x56\x78\x12\x34\x12\x34\x12\x34\x12\x34\x56\x78\x90\x12\x99",
4,
)
},
expected="EjRWeBI0EjQSNBI0VniQEpk=",
),
ExpressionTestCase(
"binary_sub4_8bytes",
msg="Subtype 4 with 8 bytes (half of 16) falls back to base64",
expression={"$toString": Binary(b"\x12\x34\x56\x78\x12\x34\x56\x78", 4)},
expected="EjRWeBI0Vng=",
),
ExpressionTestCase(
"binary_sub4_32bytes",
msg="Subtype 4 with 32 bytes (double of 16) falls back to base64",
expression={"$toString": Binary(b"\x12\x34\x56\x78" * 8, 4)},
expected="EjRWeBI0VngSNFZ4EjRWeBI0VngSNFZ4EjRWeBI0Vng=",
),
ExpressionTestCase(
"binary_16bytes_sub0",
msg="16-byte subtype 0 Binary uses base64, not UUID format",
expression={
"$toString": Binary(
b"\x12\x34\x56\x78\x12\x34\x12\x34\x12\x34\x12\x34\x56\x78\x90\x12",
0,
)
},
expected="EjRWeBI0EjQSNBI0VniQEg==",
),
ExpressionTestCase(
"binary_16bytes_sub3",
msg="16-byte subtype 3 Binary uses base64, not UUID format",
expression={
"$toString": Binary(
b"\x12\x34\x56\x78\x12\x34\x12\x34\x12\x34\x12\x34\x56\x78\x90\x12",
3,
)
},
expected="EjRWeBI0EjQSNBI0VniQEg==",
),
ExpressionTestCase(
"binary_16bytes_sub5",
msg="16-byte subtype 5 Binary uses base64, not UUID format",
expression={
"$toString": Binary(
b"\x12\x34\x56\x78\x12\x34\x12\x34\x12\x34\x12\x34\x56\x78\x90\x12",
5,
)
},
expected="EjRWeBI0EjQSNBI0VniQEg==",
),
]


@pytest.mark.parametrize("test", pytest_params(TOSTRING_BINARY_TESTS))
def test_toString_binary(collection, test: ExpressionTestCase):
"""$toString converts Binary to base64 or UUID strings depending on subtype and length."""
result = execute_expression(collection, test.expression)
assert_expression_result(
result, expected=test.expected, error_code=test.error_code, msg=test.msg
)
Loading
Loading