Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I have added a new type utility to allow each test to also run with $convert. It works a little differently than it originally did, so that the tests can keep using ExpressionTestCase, rather than needing their own classes, but should have the same end functionality.

"""$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,176 @@
"""$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.type.utils.convert_variants import ( # noqa: E501
with_convert_variants,
)
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(
with_convert_variants(
TOSTRING_BINARY_TESTS, "$toString", "string", extra_convert_fields={"format": "auto"}
)
),
)
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