From 67ed4a631bb271089a2bd1ea821ce84facdc8480 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Thu, 25 Jun 2026 16:07:26 -0700 Subject: [PATCH 01/11] Base Tests Signed-off-by: PatersonProjects --- .../commands/getClusterParameter/__init__.py | 1 + ...t_getClusterParameter_argument_handling.py | 285 ++++++++++++++++++ .../test_getClusterParameter_authorization.py | 44 +++ .../test_getClusterParameter_core_behavior.py | 91 ++++++ ...test_getClusterParameter_name_semantics.py | 158 ++++++++++ ..._getClusterParameter_response_structure.py | 124 ++++++++ .../test_getClusterParameter_round_trip.py | 110 +++++++ .../getClusterParameter/utils/__init__.py | 32 ++ 8 files changed, 845 insertions(+) create mode 100644 documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/__init__.py create mode 100644 documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_argument_handling.py create mode 100644 documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_authorization.py create mode 100644 documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_core_behavior.py create mode 100644 documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_name_semantics.py create mode 100644 documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_response_structure.py create mode 100644 documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_round_trip.py create mode 100644 documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/utils/__init__.py diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/__init__.py b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/__init__.py new file mode 100644 index 000000000..e07018905 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/__init__.py @@ -0,0 +1 @@ +"""getClusterParameter command tests.""" diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_argument_handling.py b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_argument_handling.py new file mode 100644 index 000000000..b936bf427 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_argument_handling.py @@ -0,0 +1,285 @@ +"""Tests for getClusterParameter argument and command-document handling. + +Covers the parameter argument's accepted/rejected BSON types, the three +documented argument forms (single name / array / wildcard), array-form edge +cases, null/empty-string coercion, and command-document quirks (comment field, +case-sensitive command key, unrecognized fields). + +Parameter values vary by deployment, so success cases assert on response +structure (ok, clusterParameters length/type) and never on exact values. +""" + +from dataclasses import dataclass +from typing import Any + +import pytest + +from documentdb_tests.compatibility.tests.system.administration.commands.getClusterParameter.utils import ( # noqa: E501 + valid_parameter_names, +) +from documentdb_tests.framework.assertions import ( + assertFailureCode, + assertProperties, + assertSuccessPartial, +) +from documentdb_tests.framework.bson_type_validator import ( + BsonTypeTestCase, + generate_bson_rejection_test_cases, +) +from documentdb_tests.framework.error_codes import ( + BAD_VALUE_ERROR, + COMMAND_NOT_FOUND_ERROR, + NO_SUCH_KEY_ERROR, + TYPE_MISMATCH_ERROR, +) +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.property_checks import Eq, Gt, IsType, Len +from documentdb_tests.framework.test_case import BaseTestCase +from documentdb_tests.framework.test_constants import BsonType + +pytestmark = pytest.mark.admin + + +# --------------------------------------------------------------------------- +# Category #1 / #3: argument BSON-type rejection +# The argument must be a string (single name or '*') or an array of strings. +# Every other BSON type is rejected with a type-mismatch error. NULL is handled +# separately in the coercion tests below. +# --------------------------------------------------------------------------- + +_ARGUMENT_TYPE_SPEC = [ + BsonTypeTestCase( + id="getClusterParameter_argument", + msg="getClusterParameter should reject non-string/non-array argument types", + keyword="getClusterParameter", + valid_types=[BsonType.STRING, BsonType.ARRAY], + default_error_code=TYPE_MISMATCH_ERROR, + skip_rejection_types=[BsonType.NULL], + ), +] + +_ARGUMENT_REJECTION_CASES = generate_bson_rejection_test_cases(_ARGUMENT_TYPE_SPEC) + + +@pytest.mark.parametrize("bson_type,sample_value,spec", _ARGUMENT_REJECTION_CASES) +def test_getClusterParameter_argument_rejects_type(collection, bson_type, sample_value, spec): + """Test getClusterParameter rejects non-string/non-array argument types.""" + result = execute_admin_command(collection, {"getClusterParameter": sample_value}) + assertFailureCode( + result, + spec.expected_code(bson_type), + msg=f"getClusterParameter should reject {bson_type.value} argument.", + ) + + +# --------------------------------------------------------------------------- +# Category #2 / #10: documented argument forms and array shapes +# --------------------------------------------------------------------------- + + +def test_getClusterParameter_single_name_returns_one(collection): + """Test a single parameter name returns exactly one entry.""" + (name,) = valid_parameter_names(collection, 1) + result = execute_admin_command(collection, {"getClusterParameter": name}) + assertProperties( + result, + {"ok": Eq(1.0), "clusterParameters": Len(1)}, + msg="Single name should return exactly one clusterParameters entry.", + raw_res=True, + ) + + +def test_getClusterParameter_array_single_name_returns_one(collection): + """Test a one-element array behaves like the single-name form.""" + names = valid_parameter_names(collection, 1) + result = execute_admin_command(collection, {"getClusterParameter": names}) + assertProperties( + result, + {"ok": Eq(1.0), "clusterParameters": Len(1)}, + msg="One-element array should return exactly one entry.", + raw_res=True, + ) + + +def test_getClusterParameter_array_two_names_returns_two(collection): + """Test an array of two valid names returns two entries.""" + names = valid_parameter_names(collection, 2) + result = execute_admin_command(collection, {"getClusterParameter": names}) + assertProperties( + result, + {"ok": Eq(1.0), "clusterParameters": Len(2)}, + msg="Array of two names should return two entries.", + raw_res=True, + ) + + +def test_getClusterParameter_array_three_names_returns_three(collection): + """Test an array of three valid names returns three entries.""" + names = valid_parameter_names(collection, 3) + result = execute_admin_command(collection, {"getClusterParameter": names}) + assertProperties( + result, + {"ok": Eq(1.0), "clusterParameters": Len(3)}, + msg="Array of three names should return three entries.", + raw_res=True, + ) + + +def test_getClusterParameter_wildcard_succeeds(collection): + """Test the '*' wildcard returns an array of parameters with ok:1.""" + result = execute_admin_command(collection, {"getClusterParameter": "*"}) + assertProperties( + result, + {"ok": Eq(1.0), "clusterParameters": IsType("array")}, + msg="'*' should return a clusterParameters array with ok:1.", + raw_res=True, + ) + + +def test_getClusterParameter_wildcard_is_non_empty(collection): + """Test the '*' wildcard returns at least one parameter.""" + result = execute_admin_command(collection, {"getClusterParameter": "*"}) + count = len(result["clusterParameters"]) + assertProperties( + {"count": count}, + {"count": Gt(0)}, + msg=f"'*' should return at least one parameter (got {count}).", + raw_res=True, + ) + + +def test_getClusterParameter_duplicate_names(collection): + """Test an array with a duplicated valid name is accepted.""" + names = valid_parameter_names(collection, 1) + result = execute_admin_command(collection, {"getClusterParameter": [names[0], names[0]]}) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="Array with a duplicated valid name should be accepted.", + ) + + +def test_getClusterParameter_many_names_no_truncation(collection): + """Test a large array of names is handled without truncation or failure.""" + names = valid_parameter_names(collection, 1) + big = [names[0]] * 100 + result = execute_admin_command(collection, {"getClusterParameter": big}) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="Large array of names should succeed without truncation.", + ) + + +# --------------------------------------------------------------------------- +# Argument / array-element error cases (parametrized) +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class ArgumentErrorCase(BaseTestCase): + """An argument value expected to produce a specific error code.""" + + argument: Any = None + + +_ARGUMENT_ERROR_CASES: list[ArgumentErrorCase] = [ + ArgumentErrorCase( + "array_with_int_element", + argument=["validNamePlaceholder", 123], + error_code=TYPE_MISMATCH_ERROR, + msg="Array with a non-string (int) element should be rejected.", + ), + ArgumentErrorCase( + "nested_array_element", + argument=[["validNamePlaceholder"]], + error_code=TYPE_MISMATCH_ERROR, + msg="Array with a nested-array element should be rejected.", + ), + ArgumentErrorCase( + "object_element", + argument=[{"x": 1}], + error_code=TYPE_MISMATCH_ERROR, + msg="Array with a document element should be rejected.", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(_ARGUMENT_ERROR_CASES)) +def test_getClusterParameter_array_element_rejects_type(collection, test): + """Test array elements that are not strings are rejected.""" + result = execute_admin_command(collection, {"getClusterParameter": test.argument}) + assertFailureCode(result, test.error_code, msg=test.msg) + + +# --------------------------------------------------------------------------- +# Category #6: null / empty-string coercion of the argument +# --------------------------------------------------------------------------- + + +def test_getClusterParameter_null_argument(collection): + """Test a null argument is rejected with a type-mismatch error.""" + result = execute_admin_command(collection, {"getClusterParameter": None}) + assertFailureCode( + result, + TYPE_MISMATCH_ERROR, + msg="Null argument should be rejected as a type mismatch.", + ) + + +def test_getClusterParameter_empty_string_argument(collection): + """Test an empty-string argument is treated as an unknown parameter name.""" + result = execute_admin_command(collection, {"getClusterParameter": ""}) + assertFailureCode( + result, + NO_SUCH_KEY_ERROR, + msg="Empty-string argument should be treated as an unknown parameter name.", + ) + + +def test_getClusterParameter_empty_array_argument(collection): + """Test an empty array does not return all parameters.""" + result = execute_admin_command(collection, {"getClusterParameter": []}) + assertFailureCode( + result, + BAD_VALUE_ERROR, + msg="Empty array should not return all parameters.", + ) + + +# --------------------------------------------------------------------------- +# Category #15: command-document quirks +# --------------------------------------------------------------------------- + + +def test_getClusterParameter_comment_accepted(collection): + """Test the optional comment field is accepted and does not affect success.""" + result = execute_admin_command( + collection, {"getClusterParameter": "*", "comment": "functional-test"} + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="comment field should be accepted.", + ) + + +def test_getClusterParameter_unrecognized_field_ignored(collection): + """Test an unrecognized top-level field is ignored rather than rejected.""" + result = execute_admin_command(collection, {"getClusterParameter": "*", "bogusField": 1}) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="Unrecognized top-level field should be ignored.", + ) + + +def test_getClusterParameter_wrong_case_command_key(collection): + """Test the command key is case-sensitive (getclusterparameter is unknown).""" + result = execute_admin_command(collection, {"getclusterparameter": "*"}) + assertFailureCode( + result, + COMMAND_NOT_FOUND_ERROR, + msg="Lower-case command key should be an unknown command.", + ) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_authorization.py b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_authorization.py new file mode 100644 index 000000000..feb0696f2 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_authorization.py @@ -0,0 +1,44 @@ +"""Tests for getClusterParameter authorization and namespace enforcement. + +getClusterParameter is an admin-only command guarded by the +``getClusterParameter`` privilege action. The cases that run without +authentication are covered here: the command is rejected against any +non-admin database (namespace delegation), and it succeeds on admin when auth +is disabled. + +DEFERRED (require an auth-enabled environment, not available on this target): +- A user holding the getClusterParameter action succeeds (#5, #7). +- A user lacking the action fails with an authorization error (#3, #5, #7). +- The read (getClusterParameter) action without the setClusterParameter action + still permits reads (#18). +- A role scoped to a non-admin database does not grant this cluster-scoped + action (#18). +""" + +import pytest + +from documentdb_tests.framework.assertions import assertFailureCode, assertSuccessPartial +from documentdb_tests.framework.error_codes import UNAUTHORIZED_ERROR +from documentdb_tests.framework.executor import execute_admin_command, execute_command + +pytestmark = [pytest.mark.admin, pytest.mark.rbac] + + +def test_getClusterParameter_rejected_on_non_admin_database(collection): + """Test getClusterParameter is rejected against a non-admin database.""" + result = execute_command(collection, {"getClusterParameter": "*"}) + assertFailureCode( + result, + UNAUTHORIZED_ERROR, + msg="getClusterParameter should be rejected on a non-admin database.", + ) + + +def test_getClusterParameter_succeeds_on_admin_without_auth(collection): + """Test getClusterParameter succeeds on admin when authentication is disabled.""" + result = execute_admin_command(collection, {"getClusterParameter": "*"}) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="getClusterParameter should succeed on admin with auth disabled.", + ) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_core_behavior.py b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_core_behavior.py new file mode 100644 index 000000000..27c3aad36 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_core_behavior.py @@ -0,0 +1,91 @@ +"""Tests for getClusterParameter core retrieval behavior. + +Covers read coherence and idempotency that hold on any single deployment: +repeated reads are stable, a single-name read agrees with the same parameter's +entry in the wildcard result, and every parameter advertised by '*' is +individually retrievable. + +DEFERRED (require a sharded cluster or controlled concurrency, not available on +a single mongod target): +- mongos durable value vs mongod cached value divergence and eventual + convergence (#13). On a sharded cluster mongos reads the config-server + durable value while mongod reads a cached snapshot; this needs a mongos + + shard topology to observe. +- Concurrent getClusterParameter during a setClusterParameter must observe a + coherent (old or new) snapshot, never partial (#14). Needs deterministic + interleaving with a writer and is inherently racy on a single node. +""" + +import pytest + +from documentdb_tests.compatibility.tests.system.administration.commands.getClusterParameter.utils import ( # noqa: E501 + all_cluster_parameters, + valid_parameter_names, +) +from documentdb_tests.framework.assertions import assertProperties +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.property_checks import Eq + +pytestmark = pytest.mark.admin + + +def test_getClusterParameter_repeated_wildcard_returns_same_parameter_set(collection): + """Test repeated '*' calls return the same set of parameter names (idempotent).""" + first = execute_admin_command(collection, {"getClusterParameter": "*"})["clusterParameters"] + second = execute_admin_command(collection, {"getClusterParameter": "*"})["clusterParameters"] + names_first = sorted(p["_id"] for p in first) + names_second = sorted(p["_id"] for p in second) + assertProperties( + {"names": names_second}, + {"names": Eq(names_first)}, + msg="Repeated '*' reads should return the same set of parameter names.", + raw_res=True, + ) + + +def test_getClusterParameter_repeated_single_read_is_consistent(collection): + """Test reading the same parameter twice returns an identical entry.""" + (name,) = valid_parameter_names(collection, 1) + first = execute_admin_command(collection, {"getClusterParameter": name})["clusterParameters"][0] + second = execute_admin_command(collection, {"getClusterParameter": name})["clusterParameters"][ + 0 + ] + assertProperties( + {"entry": second}, + {"entry": Eq(first)}, + msg=f"Repeated single read of '{name}' should be consistent.", + raw_res=True, + ) + + +def test_getClusterParameter_single_name_matches_wildcard_entry(collection): + """Test a single-name read agrees with that parameter's entry under '*'.""" + (name,) = valid_parameter_names(collection, 1) + single = execute_admin_command(collection, {"getClusterParameter": name})["clusterParameters"][ + 0 + ] + wildcard_entry = next(p for p in all_cluster_parameters(collection) if p["_id"] == name) + assertProperties( + {"entry": single}, + {"entry": Eq(wildcard_entry)}, + msg=f"Single read of '{name}' should match its '*' entry.", + raw_res=True, + ) + + +def test_getClusterParameter_wildcard_names_individually_retrievable(collection): + """Test parameters advertised by '*' are each retrievable by name.""" + names = [p["_id"] for p in all_cluster_parameters(collection)][:5] + retrieved = {} + for name in names: + params = execute_admin_command(collection, {"getClusterParameter": name})[ + "clusterParameters" + ] + retrieved[name] = [len(params), params[0]["_id"] if params else None] + expected = {name: [1, name] for name in names} + assertProperties( + {"retrieved": retrieved}, + {"retrieved": Eq(expected)}, + msg="Each parameter from '*' should be individually retrievable by name.", + raw_res=True, + ) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_name_semantics.py b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_name_semantics.py new file mode 100644 index 000000000..f07386aad --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_name_semantics.py @@ -0,0 +1,158 @@ +"""Tests for how getClusterParameter interprets parameter names. + +Parameter names are exact-match literals: there is no field-path traversal, +operator interpretation, globbing, or case folding. Any name that is not an +exact match for an existing cluster parameter is reported as unknown, and the +single-name and array forms report an unknown name identically. + +(The empty-string name case is covered in test_getClusterParameter_argument_handling.py.) +""" + +from dataclasses import dataclass +from typing import Any + +import pytest + +from documentdb_tests.compatibility.tests.system.administration.commands.getClusterParameter.utils import ( # noqa: E501 + valid_parameter_names, +) +from documentdb_tests.framework.assertions import assertFailureCode +from documentdb_tests.framework.error_codes import NO_SUCH_KEY_ERROR +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_case import BaseTestCase + +pytestmark = pytest.mark.admin + +_UNKNOWN_NAME = "definitelyNotAClusterParameterXYZ" + + +@dataclass(frozen=True) +class NameCase(BaseTestCase): + """A parameter-name argument expected to be reported as unknown.""" + + argument: Any = None + + +# --------------------------------------------------------------------------- +# Category #8: argument boundary probing — every odd name is a literal, +# unknown parameter name (no traversal/operator/glob interpretation). +# Category #9: wildcard edges — '*' is only special as the whole-string +# argument; inside an array or combined with other characters it is literal. +# Category #11: an unknown name reports identically as a string or in an array. +# --------------------------------------------------------------------------- + +_UNKNOWN_NAME_CASES: list[NameCase] = [ + # #8 boundary probing + NameCase( + "whitespace_only", + argument=" ", + error_code=NO_SUCH_KEY_ERROR, + msg="Whitespace-only name should be an unknown parameter.", + ), + NameCase( + "very_long_10k", + argument="x" * 10000, + error_code=NO_SUCH_KEY_ERROR, + msg="A 10k-character name should be an unknown parameter (no crash).", + ), + NameCase( + "dotted_name", + argument="a.b.c", + error_code=NO_SUCH_KEY_ERROR, + msg="Dotted name should be literal, not a field path -> unknown parameter.", + ), + NameCase( + "dollar_prefixed", + argument="$foo", + error_code=NO_SUCH_KEY_ERROR, + msg="$-prefixed name should be literal, not an operator -> unknown parameter.", + ), + NameCase( + "unicode_name", + argument="paramètre_café_\u6d4b\u8bd5", + error_code=NO_SUCH_KEY_ERROR, + msg="Unicode name should be an unknown parameter.", + ), + # #9 wildcard edges + NameCase( + "star_in_array", + argument=["*"], + error_code=NO_SUCH_KEY_ERROR, + msg="'*' inside an array should be a literal name, not expand-all.", + ), + NameCase( + "double_star", + argument="**", + error_code=NO_SUCH_KEY_ERROR, + msg="'**' should be a literal name (no globbing).", + ), + NameCase( + "star_prefix", + argument="*foo", + error_code=NO_SUCH_KEY_ERROR, + msg="'*foo' should be a literal name (no globbing).", + ), + # #11 unknown-name contract + NameCase( + "unknown_single_string", + argument=_UNKNOWN_NAME, + error_code=NO_SUCH_KEY_ERROR, + msg="An unknown single name should report NoSuchKey.", + ), + NameCase( + "unknown_in_array", + argument=[_UNKNOWN_NAME], + error_code=NO_SUCH_KEY_ERROR, + msg="An unknown name inside an array should report NoSuchKey.", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(_UNKNOWN_NAME_CASES)) +def test_getClusterParameter_unknown_name(collection, test): + """Test odd/unknown parameter names are reported as unknown.""" + result = execute_admin_command(collection, {"getClusterParameter": test.argument}) + assertFailureCode(result, test.error_code, msg=test.msg) + + +# --------------------------------------------------------------------------- +# Cases needing a deployment-derived valid name (built at runtime). +# --------------------------------------------------------------------------- + + +def test_getClusterParameter_name_is_case_sensitive(collection): + """Test a known name with altered case is treated as unknown (exact match).""" + (name,) = valid_parameter_names(collection, 1) + altered = name.upper() if name != name.upper() else name.lower() + result = execute_admin_command(collection, {"getClusterParameter": altered}) + assertFailureCode( + result, + NO_SUCH_KEY_ERROR, + msg=f"Case-altered name '{altered}' should not match known name '{name}'.", + ) + + +def test_getClusterParameter_star_with_valid_name_in_array(collection): + """Test '*' combined with a valid name in an array treats '*' literally.""" + (name,) = valid_parameter_names(collection, 1) + result = execute_admin_command(collection, {"getClusterParameter": ["*", name]}) + assertFailureCode( + result, + NO_SUCH_KEY_ERROR, + msg="'*' alongside a valid name in an array should be a literal unknown name.", + ) + + +def test_getClusterParameter_unknown_name_contract_consistent(collection): + """Test single-string and array forms report an unknown name identically.""" + single = execute_admin_command(collection, {"getClusterParameter": _UNKNOWN_NAME}) + array = execute_admin_command(collection, {"getClusterParameter": [_UNKNOWN_NAME]}) + single_code = getattr(single, "code", None) + array_code = getattr(array, "code", None) + assertFailureCode( + array, + single_code, + msg=f"Array unknown-name code ({array_code}) should match single " + f"unknown-name code ({single_code}).", + ) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_response_structure.py b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_response_structure.py new file mode 100644 index 000000000..2b12dd48c --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_response_structure.py @@ -0,0 +1,124 @@ +"""Tests for getClusterParameter response structure and value-type fidelity. + +The top-level shape ({ok:1, clusterParameters: }) is asserted in +test_getClusterParameter_argument_handling.py; this file covers element-level +structure and BSON-type fidelity: each element is an object keyed by ``_id``, +a single request isolates exactly the requested name, and parameter values +(nested documents, numbers, and clusterParameterTime) keep their BSON types +without coercion. +""" + +import pytest +from bson import Decimal128, Int64, Timestamp + +from documentdb_tests.framework.assertions import assertProperties +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.property_checks import Eq, Len + +pytestmark = pytest.mark.admin + +# Numeric BSON types that must survive a round trip without being coerced to +# string. bool is intentionally excluded (it is not a numeric value here). +_NUMERIC_TYPES = (int, float, Int64, Decimal128) + + +def test_getClusterParameter_each_element_is_object_with_string_id(collection): + """Test every clusterParameters element is an object with a string _id.""" + params = execute_admin_command(collection, {"getClusterParameter": "*"})["clusterParameters"] + summary = { + "all_objects": all(isinstance(e, dict) for e in params), + "all_ids_str": all(isinstance(e.get("_id"), str) for e in params), + } + assertProperties( + {"summary": summary}, + {"summary": Eq({"all_objects": True, "all_ids_str": True})}, + msg="Every clusterParameters element should be an object with a string _id.", + raw_res=True, + ) + + +def test_getClusterParameter_single_request_isolates_name(collection): + """Test requesting one name returns exactly that name and no others.""" + name = execute_admin_command(collection, {"getClusterParameter": "*"})["clusterParameters"][0][ + "_id" + ] + result = execute_admin_command(collection, {"getClusterParameter": name}) + assertProperties( + result, + {"clusterParameters": Len(1), "clusterParameters.0._id": Eq(name)}, + msg=f"Requesting '{name}' should return exactly that name.", + raw_res=True, + ) + + +def test_getClusterParameter_clusterParameterTime_is_timestamp_when_present(collection): + """Test any clusterParameterTime field is a BSON Timestamp.""" + params = execute_admin_command(collection, {"getClusterParameter": "*"})["clusterParameters"] + times = [e["clusterParameterTime"] for e in params if "clusterParameterTime" in e] + if not times: + pytest.skip("no parameter on this deployment carries clusterParameterTime") + all_timestamps = all(isinstance(t, Timestamp) for t in times) + assertProperties( + {"all_timestamps": all_timestamps}, + {"all_timestamps": Eq(True)}, + msg="clusterParameterTime should be a BSON Timestamp.", + raw_res=True, + ) + + +def test_getClusterParameter_document_valued_parameter_preserves_nested_object(collection): + """Test a document-valued parameter keeps its nested object structure.""" + params = execute_admin_command(collection, {"getClusterParameter": "*"})["clusterParameters"] + nested_is_object = None + for entry in params: + for key, value in entry.items(): + if key != "_id" and isinstance(value, dict): + nested_is_object = isinstance(value, dict) + break + if nested_is_object is not None: + break + if nested_is_object is None: + pytest.skip("no document-valued cluster parameter on this deployment") + assertProperties( + {"nested_is_object": nested_is_object}, + {"nested_is_object": Eq(True)}, + msg="A document-valued parameter should preserve its nested object structure.", + raw_res=True, + ) + + +def test_getClusterParameter_numeric_value_not_coerced(collection): + """Test numeric parameter values keep a numeric BSON type (not stringified).""" + params = execute_admin_command(collection, {"getClusterParameter": "*"})["clusterParameters"] + + def first_numeric_type(value): + if isinstance(value, bool): + return None + if isinstance(value, _NUMERIC_TYPES): + return type(value).__name__ + if isinstance(value, dict): + for v in value.values(): + found = first_numeric_type(v) + if found: + return found + return None + + numeric_type = None + for entry in params: + for key, value in entry.items(): + if key == "_id": + continue + numeric_type = first_numeric_type(value) + if numeric_type: + break + if numeric_type: + break + if numeric_type is None: + pytest.skip("no numeric-valued cluster parameter on this deployment") + is_numeric_type = numeric_type in {"int", "float", "Int64", "Decimal128"} + assertProperties( + {"is_numeric_type": is_numeric_type}, + {"is_numeric_type": Eq(True)}, + msg=f"Numeric parameter value kept numeric BSON type '{numeric_type}' (not coerced).", + raw_res=True, + ) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_round_trip.py b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_round_trip.py new file mode 100644 index 000000000..786426043 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_round_trip.py @@ -0,0 +1,110 @@ +"""Tests for getClusterParameter cross-command interactions. + +Round-trip (#12): a value written with setClusterParameter is read back by +getClusterParameter, and a never-set parameter returns its default without +error. + +Differential (#16): getClusterParameter and getParameter occupy distinct +namespaces (a cluster parameter is not retrievable via getParameter and a +server parameter is not a cluster parameter), and getDefaultRWConcern is a +separate facility (defaultRWConcern is not a cluster parameter). + +Marked no_parallel because the round-trip tests mutate a cluster parameter; +each restores the original value in a finally block. + +DEFERRED (require a sharded cluster, not this single target): +- After a set, mongos durable value and mongod cached value converge + eventually (#12). +- A settable-but-internal parameter's presence under '*' vs setClusterParameter + acceptance (#12) — needs an internal parameter inventory. +""" + +import pytest + +from documentdb_tests.framework.assertions import assertFailureCode, assertProperties +from documentdb_tests.framework.error_codes import INVALID_OPTIONS_ERROR +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.property_checks import Eq, IsType + +pytestmark = [pytest.mark.admin, pytest.mark.no_parallel] + + +def test_getClusterParameter_set_then_get_reflects_value(collection): + """Test a value written by setClusterParameter is read back (read-after-write).""" + get0 = execute_admin_command(collection, {"getClusterParameter": "defaultMaxTimeMS"}) + original = int(get0["clusterParameters"][0]["readOperations"]) + new_value = 4242 if original != 4242 else 1234 + try: + execute_admin_command( + collection, + {"setClusterParameter": {"defaultMaxTimeMS": {"readOperations": new_value}}}, + ) + got = execute_admin_command(collection, {"getClusterParameter": "defaultMaxTimeMS"}) + read_back = int(got["clusterParameters"][0]["readOperations"]) + assertProperties( + {"read_back": read_back}, + {"read_back": Eq(new_value)}, + msg="getClusterParameter should read back the value set by setClusterParameter.", + raw_res=True, + ) + finally: + execute_admin_command( + collection, + {"setClusterParameter": {"defaultMaxTimeMS": {"readOperations": original}}}, + ) + + +def test_getClusterParameter_never_set_returns_default(collection): + """Test a parameter that was not explicitly set still returns a default value.""" + result = execute_admin_command(collection, {"getClusterParameter": "defaultMaxTimeMS"}) + assertProperties( + result, + {"ok": Eq(1.0), "clusterParameters.0.readOperations": IsType("long")}, + msg="A never-set parameter should return its default without error.", + raw_res=True, + ) + + +def test_getClusterParameter_cluster_param_not_retrievable_via_getParameter(collection): + """Test a cluster parameter name is not retrievable through getParameter.""" + name = execute_admin_command(collection, {"getClusterParameter": "*"})["clusterParameters"][0][ + "_id" + ] + result = execute_admin_command(collection, {"getParameter": 1, name: 1}) + assertFailureCode( + result, + INVALID_OPTIONS_ERROR, + msg=f"Cluster parameter '{name}' should not be retrievable via getParameter.", + ) + + +def test_getClusterParameter_server_parameter_not_a_cluster_parameter(collection): + """Test a server parameter (logLevel) is not present in cluster parameters.""" + names = [ + p["_id"] + for p in execute_admin_command(collection, {"getClusterParameter": "*"})[ + "clusterParameters" + ] + ] + assertProperties( + {"has_logLevel": "logLevel" in names}, + {"has_logLevel": Eq(False)}, + msg="Server parameter 'logLevel' should not be a cluster parameter.", + raw_res=True, + ) + + +def test_getClusterParameter_defaultRWConcern_not_a_cluster_parameter(collection): + """Test defaultRWConcern is not a cluster parameter (distinct from getDefaultRWConcern).""" + names = [ + p["_id"] + for p in execute_admin_command(collection, {"getClusterParameter": "*"})[ + "clusterParameters" + ] + ] + assertProperties( + {"has_defaultRWConcern": "defaultRWConcern" in names}, + {"has_defaultRWConcern": Eq(False)}, + msg="defaultRWConcern should not appear among cluster parameters.", + raw_res=True, + ) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/utils/__init__.py b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/utils/__init__.py new file mode 100644 index 000000000..940510b19 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/utils/__init__.py @@ -0,0 +1,32 @@ +"""Shared helpers for getClusterParameter tests. + +Cluster parameter values and the exact set of available parameters vary by +deployment, so tests derive a valid parameter name at runtime from the +wildcard form rather than hard-coding one. +""" + +from documentdb_tests.framework.executor import execute_admin_command + + +def all_cluster_parameters(collection): + """Return the ``clusterParameters`` list from ``getClusterParameter: '*'``. + + Raises the underlying exception if the command did not succeed, so callers + fail loudly during setup rather than asserting against an error object. + """ + result = execute_admin_command(collection, {"getClusterParameter": "*"}) + if isinstance(result, Exception): + raise result + return result["clusterParameters"] + + +def valid_parameter_names(collection, count=1): + """Return up to ``count`` valid cluster parameter names for this deployment.""" + params = all_cluster_parameters(collection) + names = [p["_id"] for p in params] + if len(names) < count: + raise AssertionError( + f"deployment exposes only {len(names)} cluster parameter(s), " + f"need {count} for this test" + ) + return names[:count] From b3f1508f35b1b2522569a229a153d9bd579925e2 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Fri, 26 Jun 2026 11:12:03 -0700 Subject: [PATCH 02/11] Base Tests Signed-off-by: PatersonProjects --- .../commands/getClusterParameter/__init__.py | 0 ...t_getClusterParameter_argument_handling.py | 228 ++++++++++++++++++ .../test_getClusterParameter_authorization.py | 58 +++++ .../test_getClusterParameter_core_behavior.py | 111 +++++++++ ...test_getClusterParameter_name_semantics.py | 149 ++++++++++++ ..._getClusterParameter_response_structure.py | 101 ++++++++ .../test_getClusterParameter_round_trip.py | 92 +++++++ 7 files changed, 739 insertions(+) create mode 100644 documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/__init__.py create mode 100644 documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_argument_handling.py create mode 100644 documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_authorization.py create mode 100644 documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_core_behavior.py create mode 100644 documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_name_semantics.py create mode 100644 documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_response_structure.py create mode 100644 documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_round_trip.py diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/__init__.py b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_argument_handling.py b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_argument_handling.py new file mode 100644 index 000000000..188bd65c3 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_argument_handling.py @@ -0,0 +1,228 @@ +"""Tests for getClusterParameter argument handling. + +Covers BSON type rejection/acceptance for the argument, +argument forms (single/array/wildcard/empty), undocumented coercion +edge cases, array-form edge cases, and command-document quirks. + +Categories: #1, #2, #3 (type/field portions), #6, #10, #15 +""" + +from dataclasses import dataclass +from datetime import datetime, timezone +from typing import Any + +import pytest +from bson import Binary, Code, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp + +from documentdb_tests.framework.assertions import assertFailureCode, assertSuccessPartial +from documentdb_tests.framework.error_codes import ( + BAD_VALUE_ERROR, + COMMAND_NOT_FOUND_ERROR, + NO_SUCH_KEY_ERROR, + TYPE_MISMATCH_ERROR, +) +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_case import BaseTestCase + +pytestmark = pytest.mark.admin + +_VALID_PARAM = "changeStreamOptions" +_VALID_PARAM_2 = "changeStreams" + + +# --------------------------------------------------------------------------- +# §1 / §6 BSON type rejection for non-string / non-array argument types +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class TypeCase(BaseTestCase): + """Test case for type handling.""" + + arg: Any = None + + +_REJECT_TYPES: list[TypeCase] = [ + TypeCase("int", arg=1, error_code=TYPE_MISMATCH_ERROR, msg="Should reject int"), + TypeCase("int64", arg=Int64(1), error_code=TYPE_MISMATCH_ERROR, msg="Should reject Int64"), + TypeCase("double", arg=3.14, error_code=TYPE_MISMATCH_ERROR, msg="Should reject double"), + TypeCase( + "decimal128", + arg=Decimal128("1"), + error_code=TYPE_MISMATCH_ERROR, + msg="Should reject Decimal128", + ), + TypeCase("bool_true", arg=True, error_code=TYPE_MISMATCH_ERROR, msg="Should reject bool true"), + TypeCase( + "bool_false", arg=False, error_code=TYPE_MISMATCH_ERROR, msg="Should reject bool false" + ), + TypeCase("object", arg={"a": 1}, error_code=TYPE_MISMATCH_ERROR, msg="Should reject object"), + TypeCase( + "bindata", + arg=Binary(b"x"), + error_code=TYPE_MISMATCH_ERROR, + msg="Should reject binData", + ), + TypeCase( + "objectid", + arg=ObjectId("000000000000000000000000"), + error_code=TYPE_MISMATCH_ERROR, + msg="Should reject ObjectId", + ), + TypeCase( + "date", + arg=datetime(2024, 1, 1, tzinfo=timezone.utc), + error_code=TYPE_MISMATCH_ERROR, + msg="Should reject date", + ), + TypeCase("regex", arg=Regex("x"), error_code=TYPE_MISMATCH_ERROR, msg="Should reject regex"), + TypeCase( + "javascript", + arg=Code("function(){}"), + error_code=TYPE_MISMATCH_ERROR, + msg="Should reject JavaScript", + ), + TypeCase( + "timestamp", + arg=Timestamp(0, 1), + error_code=TYPE_MISMATCH_ERROR, + msg="Should reject Timestamp", + ), + TypeCase("minkey", arg=MinKey(), error_code=TYPE_MISMATCH_ERROR, msg="Should reject MinKey"), + TypeCase("maxkey", arg=MaxKey(), error_code=TYPE_MISMATCH_ERROR, msg="Should reject MaxKey"), + TypeCase("null", arg=None, error_code=TYPE_MISMATCH_ERROR, msg="Should reject null"), +] + + +@pytest.mark.parametrize("test", pytest_params(_REJECT_TYPES)) +def test_getClusterParameter_rejects_invalid_bson_type(collection, test): + """Test getClusterParameter rejects non-string/non-array argument types.""" + result = execute_admin_command(collection, {"getClusterParameter": test.arg}) + assertFailureCode(result, test.error_code, msg=test.msg) + + +# --------------------------------------------------------------------------- +# §2 Argument forms — accepted +# --------------------------------------------------------------------------- + + +def test_getClusterParameter_wildcard_returns_all(collection): + """Test '*' argument returns all available cluster parameters with ok:1.""" + result = execute_admin_command(collection, {"getClusterParameter": "*"}) + assertSuccessPartial(result, {"ok": 1.0}, msg="Wildcard '*' should return ok:1") + + +def test_getClusterParameter_single_name_returns_one(collection): + """Test single string name returns exactly one clusterParameters entry.""" + result = execute_admin_command(collection, {"getClusterParameter": _VALID_PARAM}) + from documentdb_tests.framework.assertions import assertProperties + from documentdb_tests.framework.property_checks import Eq, Len + + assertProperties( + result, + {"ok": Eq(1.0), "clusterParameters": Len(1)}, + msg="Single name should return ok:1 with one parameter", + raw_res=True, + ) + + +def test_getClusterParameter_array_two_names_returns_two(collection): + """Test array of two valid names returns two clusterParameters entries.""" + result = execute_admin_command( + collection, {"getClusterParameter": [_VALID_PARAM, _VALID_PARAM_2]} + ) + from documentdb_tests.framework.assertions import assertProperties + from documentdb_tests.framework.property_checks import Eq, Len + + assertProperties( + result, + {"ok": Eq(1.0), "clusterParameters": Len(2)}, + msg="Array of two names should return two parameters", + raw_res=True, + ) + + +def test_getClusterParameter_array_duplicate_names(collection): + """Test array with duplicate valid names succeeds.""" + result = execute_admin_command( + collection, {"getClusterParameter": [_VALID_PARAM, _VALID_PARAM]} + ) + assertSuccessPartial(result, {"ok": 1.0}, msg="Duplicate names in array should succeed") + + +# --------------------------------------------------------------------------- +# §2 Argument forms — rejected +# --------------------------------------------------------------------------- + + +def test_getClusterParameter_empty_array_errors(collection): + """Test empty array argument fails with BAD_VALUE_ERROR.""" + result = execute_admin_command(collection, {"getClusterParameter": []}) + assertFailureCode(result, BAD_VALUE_ERROR, msg="Empty array must supply at least one name") + + +def test_getClusterParameter_array_nonstring_element_rejects(collection): + """Test array containing a non-string element fails with TYPE_MISMATCH_ERROR.""" + result = execute_admin_command(collection, {"getClusterParameter": [_VALID_PARAM, 123]}) + assertFailureCode( + result, TYPE_MISMATCH_ERROR, msg="Non-string array element should be rejected" + ) + + +def test_getClusterParameter_unknown_name_errors(collection): + """Test an unknown parameter name fails with NO_SUCH_KEY_ERROR (code 4).""" + result = execute_admin_command(collection, {"getClusterParameter": "doesNotExist"}) + assertFailureCode(result, NO_SUCH_KEY_ERROR, msg="Unknown parameter name should fail") + + +def test_getClusterParameter_unrecognized_field_accepted(collection): + """Test extra comment field is accepted (MongoDB treats it as generic command field).""" + result = execute_admin_command(collection, {"getClusterParameter": "*", "comment": "test"}) + assertSuccessPartial(result, {"ok": 1.0}, msg="comment field should be accepted") + + +# --------------------------------------------------------------------------- +# §6 Undocumented coercion edge cases +# --------------------------------------------------------------------------- + + +def test_getClusterParameter_array_null_element_rejects(collection): + """Test array containing null element fails with TYPE_MISMATCH_ERROR.""" + result = execute_admin_command(collection, {"getClusterParameter": [None]}) + assertFailureCode(result, TYPE_MISMATCH_ERROR, msg="Null element in array should be rejected") + + +def test_getClusterParameter_array_doc_element_rejects(collection): + """Test array containing a document element fails with TYPE_MISMATCH_ERROR.""" + result = execute_admin_command(collection, {"getClusterParameter": [{"a": 1}]}) + assertFailureCode( + result, TYPE_MISMATCH_ERROR, msg="Document element in array should be rejected" + ) + + +def test_getClusterParameter_array_nested_array_rejects(collection): + """Test array containing a nested array element fails with TYPE_MISMATCH_ERROR.""" + result = execute_admin_command(collection, {"getClusterParameter": [[_VALID_PARAM]]}) + assertFailureCode(result, TYPE_MISMATCH_ERROR, msg="Nested array element should be rejected") + + +def test_getClusterParameter_array_mixed_valid_unknown_errors(collection): + """Test array with one valid and one unknown name fails with NO_SUCH_KEY_ERROR.""" + result = execute_admin_command( + collection, {"getClusterParameter": [_VALID_PARAM, "unknownParam"]} + ) + assertFailureCode(result, NO_SUCH_KEY_ERROR, msg="Unknown entry in mixed array should fail") + + +# --------------------------------------------------------------------------- +# §15 Command-document quirks +# --------------------------------------------------------------------------- + + +def test_getClusterParameter_wrong_case_command_key_rejected(collection): + """Test wrong-case command key 'getclusterparameter' is rejected as unknown command.""" + result = execute_admin_command(collection, {"getclusterparameter": "*"}) + assertFailureCode( + result, COMMAND_NOT_FOUND_ERROR, msg="Wrong-case command key should be rejected" + ) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_authorization.py b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_authorization.py new file mode 100644 index 000000000..feada379e --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_authorization.py @@ -0,0 +1,58 @@ +"""Tests for getClusterParameter authorization and namespace enforcement. + +Verifies admin-only restriction and access-control delegation: +non-admin databases are rejected with Unauthorized, and the command +succeeds when auth is disabled (the default test environment). + +Categories: #3 (non-admin-db + unauthorized), #5, #7 (auth), #18 +""" + +import pytest + +from documentdb_tests.compatibility.tests.system.diagnostic.utils.diagnostic_test_case import ( + DiagnosticTestCase, +) +from documentdb_tests.framework.assertions import assertFailureCode, assertSuccessPartial +from documentdb_tests.framework.error_codes import UNAUTHORIZED_ERROR +from documentdb_tests.framework.executor import execute_admin_command, execute_command +from documentdb_tests.framework.parametrize import pytest_params + +pytestmark = pytest.mark.admin + + +# --------------------------------------------------------------------------- +# §3 / §5 / §7 Admin-database restriction +# --------------------------------------------------------------------------- + +# Non-admin databases reject getClusterParameter with Unauthorized (code 13). +# We run via execute_command() (which uses the fixture's own non-admin database) +# to target a non-admin context without bypassing the executor restriction. +_NON_ADMIN_DB_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + id="user_db", + command={"getClusterParameter": "*"}, + use_admin=False, + error_code=UNAUTHORIZED_ERROR, + msg="getClusterParameter on a non-admin user database should fail with Unauthorized", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(_NON_ADMIN_DB_TESTS)) +def test_getClusterParameter_non_admin_db_rejected(collection, test): + """Test getClusterParameter fails with Unauthorized on non-admin databases.""" + result = execute_command(collection, test.command) + assertFailureCode(result, test.error_code, msg=test.msg) + + +# --------------------------------------------------------------------------- +# §5 / §7 Auth-disabled: admin succeeds (foundational delegation — one case) +# --------------------------------------------------------------------------- + + +def test_getClusterParameter_admin_succeeds_without_auth(collection): + """Test getClusterParameter on admin database succeeds when auth is disabled.""" + result = execute_admin_command(collection, {"getClusterParameter": "*"}) + assertSuccessPartial( + result, {"ok": 1.0}, msg="getClusterParameter on admin should succeed without auth" + ) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_core_behavior.py b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_core_behavior.py new file mode 100644 index 000000000..5d5ae95d3 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_core_behavior.py @@ -0,0 +1,111 @@ +"""Tests for getClusterParameter core retrieval behavior. + +Covers the three accepted argument forms, default values on a fresh +deployment, idempotency of repeated calls, and that requesting one name +does not return unrelated parameters. + +Categories: #4 (core behavior + deployment variants), #7, #14 +""" + +import pytest + +from documentdb_tests.framework.assertions import assertProperties, assertSuccessPartial +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.property_checks import Eq, Len + +pytestmark = pytest.mark.admin + +_VALID_PARAM = "changeStreamOptions" +_VALID_PARAM_2 = "changeStreams" + + +# --------------------------------------------------------------------------- +# §4 / §7 Core behavior — three argument forms +# --------------------------------------------------------------------------- + + +def test_getClusterParameter_single_name_succeeds(collection): + """Test single valid name returns ok:1 with clusterParameters length 1.""" + result = execute_admin_command(collection, {"getClusterParameter": _VALID_PARAM}) + assertProperties( + result, + {"ok": Eq(1.0), "clusterParameters": Len(1)}, + msg="Single valid name should return ok:1 with one parameter", + raw_res=True, + ) + + +def test_getClusterParameter_wildcard_returns_all_params(collection): + """Test '*' returns ok:1.""" + result = execute_admin_command(collection, {"getClusterParameter": "*"}) + assertSuccessPartial(result, {"ok": 1.0}, msg="Wildcard should return ok:1") + + +def test_getClusterParameter_wildcard_includes_known_param(collection): + """Test '*' result includes the known parameter 'changeStreamOptions'.""" + result = execute_admin_command(collection, {"getClusterParameter": "*"}) + ids = [p["_id"] for p in result["clusterParameters"]] + assertProperties( + {"found": 1 if _VALID_PARAM in ids else 0}, + {"found": Eq(1)}, + msg=f"Wildcard result should include '{_VALID_PARAM}'", + raw_res=True, + ) + + +def test_getClusterParameter_array_two_names_succeeds(collection): + """Test array of two valid names returns ok:1 with clusterParameters length 2.""" + result = execute_admin_command( + collection, {"getClusterParameter": [_VALID_PARAM, _VALID_PARAM_2]} + ) + assertProperties( + result, + {"ok": Eq(1.0), "clusterParameters": Len(2)}, + msg="Array of two names should return ok:1 with two parameters", + raw_res=True, + ) + + +# --------------------------------------------------------------------------- +# §4 / §7 Defaults available on any deployment +# --------------------------------------------------------------------------- + + +def test_getClusterParameter_wildcard_returns_defaults(collection): + """Test '*' returns defaults without error on any deployment.""" + result = execute_admin_command(collection, {"getClusterParameter": "*"}) + assertSuccessPartial(result, {"ok": 1.0}, msg="Defaults should be returned without prior set") + + +# --------------------------------------------------------------------------- +# §4 / §7 Requested name isolation +# --------------------------------------------------------------------------- + + +def test_getClusterParameter_single_name_id_equals_request(collection): + """Test requesting one name returns element with _id equal to requested name.""" + result = execute_admin_command(collection, {"getClusterParameter": _VALID_PARAM}) + assertProperties( + result, + {"clusterParameters.0._id": Eq(_VALID_PARAM)}, + msg=f"_id should equal the requested name '{_VALID_PARAM}'", + raw_res=True, + ) + + +# --------------------------------------------------------------------------- +# §14 Idempotency — repeated calls produce stable structure +# --------------------------------------------------------------------------- + + +def test_getClusterParameter_wildcard_idempotent(collection): + """Test repeated '*' calls return the same parameter count.""" + r1 = execute_admin_command(collection, {"getClusterParameter": "*"}) + r2 = execute_admin_command(collection, {"getClusterParameter": "*"}) + count1 = len(r1["clusterParameters"]) + assertProperties( + {"count": len(r2["clusterParameters"])}, + {"count": Eq(count1)}, + msg="Repeated wildcard calls should return stable parameter count", + raw_res=True, + ) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_name_semantics.py b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_name_semantics.py new file mode 100644 index 000000000..1185fb1ab --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_name_semantics.py @@ -0,0 +1,149 @@ +"""Tests for getClusterParameter parameter name semantics. + +Verifies that parameter names are treated as exact literal strings: +no field-path traversal, no globbing, case-sensitive matching. +Also pins the unknown-name contract (error, not silent omission) and +verifies wildcard edge interactions in array form. + +Categories: #8, #9, #11 +""" + +from dataclasses import dataclass +from typing import Any + +import pytest + +from documentdb_tests.framework.assertions import assertFailureCode +from documentdb_tests.framework.error_codes import NO_SUCH_KEY_ERROR +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_case import BaseTestCase + +pytestmark = pytest.mark.admin + +_VALID_PARAM = "changeStreamOptions" + + +# --------------------------------------------------------------------------- +# §8 Argument boundary probing — name strings are exact literals +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class NameCase(BaseTestCase): + """Test case for literal name semantics.""" + + name: Any = None + + +_BOUNDARY_NAMES: list[NameCase] = [ + NameCase( + "empty_string", + name="", + error_code=NO_SUCH_KEY_ERROR, + msg="Empty string should be treated as a literal no-match", + ), + NameCase( + "whitespace_only", + name=" ", + error_code=NO_SUCH_KEY_ERROR, + msg="Whitespace-only string should be treated as a literal no-match", + ), + NameCase( + "dotted_name", + name="a.b.c", + error_code=NO_SUCH_KEY_ERROR, + msg="Dotted name should not be traversed as a field path", + ), + NameCase( + "dollar_prefix", + name="$foo", + error_code=NO_SUCH_KEY_ERROR, + msg="Dollar-prefixed name should not be interpreted as an operator", + ), + NameCase( + "unicode_name", + name="paramétré", + error_code=NO_SUCH_KEY_ERROR, + msg="Unicode name should be treated as a literal no-match", + ), + NameCase( + "very_long_name", + name="x" * 10000, + error_code=NO_SUCH_KEY_ERROR, + msg="Very long name should not crash or timeout", + ), + NameCase( + "case_altered", + name="ChangeStreamOptions", + error_code=NO_SUCH_KEY_ERROR, + msg="Altered-case known name should not match (case-sensitive)", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(_BOUNDARY_NAMES)) +def test_getClusterParameter_name_is_literal_string(collection, test): + """Test that parameter names are treated as exact literal strings.""" + result = execute_admin_command(collection, {"getClusterParameter": test.name}) + assertFailureCode(result, test.error_code, msg=test.msg) + + +# --------------------------------------------------------------------------- +# §9 Wildcard edge interactions +# --------------------------------------------------------------------------- + + +def test_getClusterParameter_star_in_array_is_literal_name(collection): + """Test ['*'] treats '*' as a literal name (not expand-all) and errors.""" + result = execute_admin_command(collection, {"getClusterParameter": ["*"]}) + assertFailureCode( + result, NO_SUCH_KEY_ERROR, msg="'*' inside an array is a literal name, not a wildcard" + ) + + +def test_getClusterParameter_double_star_no_glob(collection): + """Test '**' is treated as a literal name, not a pattern.""" + result = execute_admin_command(collection, {"getClusterParameter": "**"}) + assertFailureCode(result, NO_SUCH_KEY_ERROR, msg="'**' should not be treated as a glob") + + +def test_getClusterParameter_star_prefix_no_glob(collection): + """Test '*foo' is treated as a literal name, not a pattern.""" + result = execute_admin_command(collection, {"getClusterParameter": "*foo"}) + assertFailureCode(result, NO_SUCH_KEY_ERROR, msg="'*foo' should not be treated as a glob") + + +def test_getClusterParameter_array_star_and_valid_name(collection): + """Test ['*', validName] treats '*' as a literal no-match and errors.""" + result = execute_admin_command(collection, {"getClusterParameter": ["*", _VALID_PARAM]}) + assertFailureCode( + result, + NO_SUCH_KEY_ERROR, + msg="'*' inside array with valid name should still error on '*' literal", + ) + + +# --------------------------------------------------------------------------- +# §11 Unknown-name contract consistency +# --------------------------------------------------------------------------- + + +def test_getClusterParameter_unknown_single_string_errors(collection): + """Test single unknown string fails with NO_SUCH_KEY_ERROR.""" + result = execute_admin_command(collection, {"getClusterParameter": "unknownParam"}) + assertFailureCode( + result, + NO_SUCH_KEY_ERROR, + msg="Single unknown name should fail with no-such-parameter error", + ) + + +def test_getClusterParameter_unknown_in_array_errors(collection): + """Test ['unknownParam'] fails with same error as 'unknownParam' string.""" + result = execute_admin_command(collection, {"getClusterParameter": ["unknownParam"]}) + assertFailureCode( + result, + NO_SUCH_KEY_ERROR, + msg="Array containing single unknown name should fail identically to string form", + ) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_response_structure.py b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_response_structure.py new file mode 100644 index 000000000..0767cbc0a --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_response_structure.py @@ -0,0 +1,101 @@ +"""Tests for getClusterParameter response structure. + +Verifies the shape and BSON types of the success response: +clusterParameters is an array, each element has _id equal to the +requested name, ok is 1, and single-name requests are isolated. +Also validates value type fidelity for the changeStreamOptions parameter. + +Categories: #4 (response structure), #17 +""" + +import pytest + +from documentdb_tests.framework.assertions import assertProperties +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.property_checks import Eq, IsType, Len + +pytestmark = pytest.mark.admin + +_VALID_PARAM = "changeStreamOptions" + + +# --------------------------------------------------------------------------- +# §4 Response shape +# --------------------------------------------------------------------------- + + +def test_getClusterParameter_response_has_ok_1(collection): + """Test success response includes ok:1.""" + result = execute_admin_command(collection, {"getClusterParameter": "*"}) + assertProperties(result, {"ok": Eq(1.0)}, msg="Response ok should be 1.0", raw_res=True) + + +def test_getClusterParameter_response_clusterParameters_is_array(collection): + """Test success response contains 'clusterParameters' field of type array.""" + result = execute_admin_command(collection, {"getClusterParameter": "*"}) + assertProperties( + result, + {"clusterParameters": IsType("array")}, + msg="clusterParameters should be an array", + raw_res=True, + ) + + +def test_getClusterParameter_single_name_length_is_one(collection): + """Test single-name request returns exactly one element in clusterParameters.""" + result = execute_admin_command(collection, {"getClusterParameter": _VALID_PARAM}) + assertProperties( + result, + {"clusterParameters": Len(1)}, + msg="Single-name request should return exactly one element", + raw_res=True, + ) + + +def test_getClusterParameter_element_has_id_field(collection): + """Test the clusterParameters element has a string '_id' field.""" + result = execute_admin_command(collection, {"getClusterParameter": _VALID_PARAM}) + assertProperties( + result, + {"clusterParameters.0._id": IsType("string")}, + msg="_id field should be a string", + raw_res=True, + ) + + +def test_getClusterParameter_single_name_id_matches_request(collection): + """Test requesting one name returns element with _id equal to requested name.""" + result = execute_admin_command(collection, {"getClusterParameter": _VALID_PARAM}) + assertProperties( + result, + {"clusterParameters.0._id": Eq(_VALID_PARAM)}, + msg=f"_id should equal requested name '{_VALID_PARAM}'", + raw_res=True, + ) + + +def test_getClusterParameter_element_has_nested_value_field(collection): + """Test document-valued parameter has a nested value sub-document.""" + result = execute_admin_command(collection, {"getClusterParameter": _VALID_PARAM}) + assertProperties( + result, + {"clusterParameters.0.preAndPostImages": IsType("object")}, + msg="changeStreamOptions should contain 'preAndPostImages' sub-document", + raw_res=True, + ) + + +# --------------------------------------------------------------------------- +# §17 Value type fidelity +# --------------------------------------------------------------------------- + + +def test_getClusterParameter_element_is_object_type(collection): + """Test the first clusterParameters element is a document (object type).""" + result = execute_admin_command(collection, {"getClusterParameter": _VALID_PARAM}) + assertProperties( + result, + {"clusterParameters.0": IsType("object")}, + msg="clusterParameters element should be an object", + raw_res=True, + ) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_round_trip.py b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_round_trip.py new file mode 100644 index 000000000..dbd523062 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_round_trip.py @@ -0,0 +1,92 @@ +"""Tests for getClusterParameter round-trip and differential behavior. + +Verifies read-after-write consistency with setClusterParameter, that +never-set parameters return their defaults, and that getClusterParameter +and getParameter operate on distinct namespaces. + +Categories: #12, #16 +""" + +import pytest +from bson import Int64 + +from documentdb_tests.framework.assertions import ( + assertFailureCode, + assertProperties, + assertSuccessPartial, +) +from documentdb_tests.framework.error_codes import INVALID_OPTIONS_ERROR +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.property_checks import Eq, Len + +pytestmark = [pytest.mark.admin, pytest.mark.no_parallel] + +_PARAM = "changeStreamOptions" +_NESTED_KEY = "preAndPostImages" +_VALUE_KEY = "expireAfterSeconds" + + +def _get_expire_after_seconds(collection): + """Read the current expireAfterSeconds value from changeStreamOptions.""" + r = execute_admin_command(collection, {"getClusterParameter": _PARAM}) + assertSuccessPartial(r, {"ok": 1.0}, msg="getClusterParameter should succeed in setup") + return r["clusterParameters"][0][_NESTED_KEY][_VALUE_KEY] + + +def _set_expire_after_seconds(collection, value): + """Set expireAfterSeconds on changeStreamOptions.""" + execute_admin_command( + collection, + {"setClusterParameter": {_PARAM: {_NESTED_KEY: {_VALUE_KEY: value}}}}, + ) + + +# --------------------------------------------------------------------------- +# §12 Round-trip with setClusterParameter +# --------------------------------------------------------------------------- + + +def test_getClusterParameter_reads_value_after_set(collection): + """Test getClusterParameter returns the value set by setClusterParameter.""" + original = _get_expire_after_seconds(collection) + # Server stores expireAfterSeconds as Int64; use same type for expected comparison. + new_value = Int64(7200) if int(original) != 7200 else Int64(3600) + try: + _set_expire_after_seconds(collection, int(new_value)) + result = execute_admin_command(collection, {"getClusterParameter": _PARAM}) + assertProperties( + result, + {"clusterParameters.0.preAndPostImages.expireAfterSeconds": Eq(new_value)}, + msg=f"expireAfterSeconds should equal {new_value} after set", + raw_res=True, + ) + finally: + _set_expire_after_seconds(collection, int(original)) + + +def test_getClusterParameter_never_set_returns_default(collection): + """Test a parameter that was never explicitly set returns a default without error.""" + result = execute_admin_command( + collection, {"getClusterParameter": "internalQueryCutoffForSampleFromRandomCursor"} + ) + assertProperties( + result, + {"ok": Eq(1.0), "clusterParameters": Len(1)}, + msg="Never-set parameter should return default without error", + raw_res=True, + ) + + +# --------------------------------------------------------------------------- +# §16 Differential: getClusterParameter vs getParameter have distinct namespaces +# --------------------------------------------------------------------------- + + +def test_getClusterParameter_name_not_retrievable_via_getParameter(collection): + """Test a cluster parameter name cannot be retrieved via getParameter.""" + result = execute_admin_command(collection, {"getParameter": _PARAM}) + assertFailureCode( + result, + INVALID_OPTIONS_ERROR, + msg="Cluster parameter name should not be retrievable via getParameter", + ) From 965a24d078652ad81fd2dca2d3c351ea4e689247 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Fri, 26 Jun 2026 14:51:53 -0700 Subject: [PATCH 03/11] Moved test cases Signed-off-by: PatersonProjects --- ...t_getClusterParameter_argument_handling.py | 154 +--------- .../test_getClusterParameter_authorization.py | 44 --- .../test_getClusterParameter_core_behavior.py | 26 -- .../test_getClusterParameter_errors.py | 262 ++++++++++++++++++ ...test_getClusterParameter_name_semantics.py | 141 ---------- ..._getClusterParameter_response_structure.py | 10 - .../test_getClusterParameter_round_trip.py | 27 +- .../getClusterParameter/utils/__init__.py | 32 --- 8 files changed, 268 insertions(+), 428 deletions(-) delete mode 100644 documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_authorization.py create mode 100644 documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_errors.py delete mode 100644 documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_name_semantics.py delete mode 100644 documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/utils/__init__.py diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_argument_handling.py b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_argument_handling.py index 825c06b05..81dcb7376 100644 --- a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_argument_handling.py +++ b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_argument_handling.py @@ -1,32 +1,16 @@ -"""Tests for getClusterParameter argument handling. +"""Tests for getClusterParameter accepted argument forms. -Covers BSON type rejection/acceptance for the argument, -argument forms (single/array/wildcard/empty), undocumented coercion -edge cases, array-form edge cases, and command-document quirks. +Covers the accepted argument forms: single string, wildcard, array of +strings, duplicate names, and the extra comment field. -Categories: #1, #2, #3 (type/field portions), #6, #10, #15 +Categories: #2, #10 """ import pytest -from documentdb_tests.framework.assertions import ( - assertFailureCode, - assertProperties, - assertSuccessPartial, -) -from documentdb_tests.framework.bson_type_validator import ( - BsonTypeTestCase, - generate_bson_rejection_test_cases, -) -from documentdb_tests.framework.error_codes import ( - BAD_VALUE_ERROR, - COMMAND_NOT_FOUND_ERROR, - NO_SUCH_KEY_ERROR, - TYPE_MISMATCH_ERROR, -) +from documentdb_tests.framework.assertions import assertProperties, assertSuccessPartial from documentdb_tests.framework.executor import execute_admin_command from documentdb_tests.framework.property_checks import Eq, Len -from documentdb_tests.framework.test_constants import BsonType pytestmark = pytest.mark.admin @@ -34,43 +18,6 @@ _VALID_PARAM_2 = "changeStreams" -# --------------------------------------------------------------------------- -# §1 / §6 BSON type rejection for non-string / non-array argument types -# The argument must be a string (single name or '*') or an array of strings. -# Every other BSON type is rejected with a type-mismatch error. NULL is handled -# separately in the coercion tests below. -# --------------------------------------------------------------------------- - -_ARGUMENT_TYPE_SPEC = [ - BsonTypeTestCase( - id="getClusterParameter_argument", - msg="getClusterParameter should reject non-string/non-array argument types", - keyword="getClusterParameter", - valid_types=[BsonType.STRING, BsonType.ARRAY], - default_error_code=TYPE_MISMATCH_ERROR, - skip_rejection_types=[BsonType.NULL], - ), -] - -_ARGUMENT_REJECTION_CASES = generate_bson_rejection_test_cases(_ARGUMENT_TYPE_SPEC) - - -@pytest.mark.parametrize("bson_type,sample_value,spec", _ARGUMENT_REJECTION_CASES) -def test_getClusterParameter_argument_rejects_type(collection, bson_type, sample_value, spec): - """Test getClusterParameter rejects non-string/non-array argument types.""" - result = execute_admin_command(collection, {"getClusterParameter": sample_value}) - assertFailureCode( - result, - spec.expected_code(bson_type), - msg=f"getClusterParameter should reject {bson_type.value} argument.", - ) - - -# --------------------------------------------------------------------------- -# §2 Argument forms — accepted -# --------------------------------------------------------------------------- - - def test_getClusterParameter_wildcard_returns_all(collection): """Test '*' argument returns all available cluster parameters with ok:1.""" result = execute_admin_command(collection, {"getClusterParameter": "*"}) @@ -109,98 +56,7 @@ def test_getClusterParameter_array_duplicate_names(collection): assertSuccessPartial(result, {"ok": 1.0}, msg="Duplicate names in array should succeed") -# --------------------------------------------------------------------------- -# §2 Argument forms — rejected -# --------------------------------------------------------------------------- - - -def test_getClusterParameter_empty_array_errors(collection): - """Test empty array argument fails with BAD_VALUE_ERROR.""" - result = execute_admin_command(collection, {"getClusterParameter": []}) - assertFailureCode(result, BAD_VALUE_ERROR, msg="Empty array must supply at least one name") - - -def test_getClusterParameter_array_nonstring_element_rejects(collection): - """Test array containing a non-string element fails with TYPE_MISMATCH_ERROR.""" - result = execute_admin_command(collection, {"getClusterParameter": [_VALID_PARAM, 123]}) - assertFailureCode( - result, TYPE_MISMATCH_ERROR, msg="Non-string array element should be rejected" - ) - - -def test_getClusterParameter_unknown_name_errors(collection): - """Test an unknown parameter name fails with NO_SUCH_KEY_ERROR (code 4).""" - result = execute_admin_command(collection, {"getClusterParameter": "doesNotExist"}) - assertFailureCode(result, NO_SUCH_KEY_ERROR, msg="Unknown parameter name should fail") - - def test_getClusterParameter_unrecognized_field_accepted(collection): """Test extra comment field is accepted (MongoDB treats it as generic command field).""" result = execute_admin_command(collection, {"getClusterParameter": "*", "comment": "test"}) assertSuccessPartial(result, {"ok": 1.0}, msg="comment field should be accepted") - - -# --------------------------------------------------------------------------- -# §6 Undocumented coercion edge cases -# --------------------------------------------------------------------------- - - -def test_getClusterParameter_null_argument(collection): - """Test a null argument is rejected with a type-mismatch error.""" - result = execute_admin_command(collection, {"getClusterParameter": None}) - assertFailureCode( - result, - TYPE_MISMATCH_ERROR, - msg="Null argument should be rejected as a type mismatch.", - ) - - -def test_getClusterParameter_empty_string_argument(collection): - """Test an empty-string argument is treated as an unknown parameter name.""" - result = execute_admin_command(collection, {"getClusterParameter": ""}) - assertFailureCode( - result, - NO_SUCH_KEY_ERROR, - msg="Empty-string argument should be treated as an unknown parameter name.", - ) - - -def test_getClusterParameter_array_null_element_rejects(collection): - """Test array containing null element fails with TYPE_MISMATCH_ERROR.""" - result = execute_admin_command(collection, {"getClusterParameter": [None]}) - assertFailureCode(result, TYPE_MISMATCH_ERROR, msg="Null element in array should be rejected") - - -def test_getClusterParameter_array_doc_element_rejects(collection): - """Test array containing a document element fails with TYPE_MISMATCH_ERROR.""" - result = execute_admin_command(collection, {"getClusterParameter": [{"a": 1}]}) - assertFailureCode( - result, TYPE_MISMATCH_ERROR, msg="Document element in array should be rejected" - ) - - -def test_getClusterParameter_array_nested_array_rejects(collection): - """Test array containing a nested array element fails with TYPE_MISMATCH_ERROR.""" - result = execute_admin_command(collection, {"getClusterParameter": [[_VALID_PARAM]]}) - assertFailureCode(result, TYPE_MISMATCH_ERROR, msg="Nested array element should be rejected") - - -def test_getClusterParameter_array_mixed_valid_unknown_errors(collection): - """Test array with one valid and one unknown name fails with NO_SUCH_KEY_ERROR.""" - result = execute_admin_command( - collection, {"getClusterParameter": [_VALID_PARAM, "unknownParam"]} - ) - assertFailureCode(result, NO_SUCH_KEY_ERROR, msg="Unknown entry in mixed array should fail") - - -# --------------------------------------------------------------------------- -# §15 Command-document quirks -# --------------------------------------------------------------------------- - - -def test_getClusterParameter_wrong_case_command_key_rejected(collection): - """Test wrong-case command key 'getclusterparameter' is rejected as unknown command.""" - result = execute_admin_command(collection, {"getclusterparameter": "*"}) - assertFailureCode( - result, COMMAND_NOT_FOUND_ERROR, msg="Wrong-case command key should be rejected" - ) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_authorization.py b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_authorization.py deleted file mode 100644 index 2be687b59..000000000 --- a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_authorization.py +++ /dev/null @@ -1,44 +0,0 @@ -"""Tests for getClusterParameter authorization and namespace enforcement. - -Verifies admin-only restriction and access-control delegation: -non-admin databases are rejected with Unauthorized, and the command -succeeds when auth is disabled (the default test environment). - -Categories: #3 (non-admin-db + unauthorized), #5, #7 (auth), #18 -""" - -import pytest - -from documentdb_tests.framework.assertions import assertFailureCode, assertSuccessPartial -from documentdb_tests.framework.error_codes import UNAUTHORIZED_ERROR -from documentdb_tests.framework.executor import execute_admin_command, execute_command - -pytestmark = [pytest.mark.admin, pytest.mark.rbac] - - -# --------------------------------------------------------------------------- -# §3 / §5 / §7 Admin-database restriction -# --------------------------------------------------------------------------- - - -def test_getClusterParameter_rejected_on_non_admin_database(collection): - """Test getClusterParameter is rejected against a non-admin database.""" - result = execute_command(collection, {"getClusterParameter": "*"}) - assertFailureCode( - result, - UNAUTHORIZED_ERROR, - msg="getClusterParameter should be rejected on a non-admin database.", - ) - - -# --------------------------------------------------------------------------- -# §5 / §7 Auth-disabled: admin succeeds (foundational delegation — one case) -# --------------------------------------------------------------------------- - - -def test_getClusterParameter_admin_succeeds_without_auth(collection): - """Test getClusterParameter on admin database succeeds when auth is disabled.""" - result = execute_admin_command(collection, {"getClusterParameter": "*"}) - assertSuccessPartial( - result, {"ok": 1.0}, msg="getClusterParameter on admin should succeed without auth" - ) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_core_behavior.py b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_core_behavior.py index 5d5ae95d3..246fe7a40 100644 --- a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_core_behavior.py +++ b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_core_behavior.py @@ -19,11 +19,6 @@ _VALID_PARAM_2 = "changeStreams" -# --------------------------------------------------------------------------- -# §4 / §7 Core behavior — three argument forms -# --------------------------------------------------------------------------- - - def test_getClusterParameter_single_name_succeeds(collection): """Test single valid name returns ok:1 with clusterParameters length 1.""" result = execute_admin_command(collection, {"getClusterParameter": _VALID_PARAM}) @@ -66,22 +61,6 @@ def test_getClusterParameter_array_two_names_succeeds(collection): ) -# --------------------------------------------------------------------------- -# §4 / §7 Defaults available on any deployment -# --------------------------------------------------------------------------- - - -def test_getClusterParameter_wildcard_returns_defaults(collection): - """Test '*' returns defaults without error on any deployment.""" - result = execute_admin_command(collection, {"getClusterParameter": "*"}) - assertSuccessPartial(result, {"ok": 1.0}, msg="Defaults should be returned without prior set") - - -# --------------------------------------------------------------------------- -# §4 / §7 Requested name isolation -# --------------------------------------------------------------------------- - - def test_getClusterParameter_single_name_id_equals_request(collection): """Test requesting one name returns element with _id equal to requested name.""" result = execute_admin_command(collection, {"getClusterParameter": _VALID_PARAM}) @@ -93,11 +72,6 @@ def test_getClusterParameter_single_name_id_equals_request(collection): ) -# --------------------------------------------------------------------------- -# §14 Idempotency — repeated calls produce stable structure -# --------------------------------------------------------------------------- - - def test_getClusterParameter_wildcard_idempotent(collection): """Test repeated '*' calls return the same parameter count.""" r1 = execute_admin_command(collection, {"getClusterParameter": "*"}) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_errors.py b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_errors.py new file mode 100644 index 000000000..4efa14ecc --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_errors.py @@ -0,0 +1,262 @@ +"""Tests for getClusterParameter error cases. + +Consolidates all error-producing inputs: BSON type rejection for the +argument field, unknown parameter names, invalid argument forms, null +coercion, array element type errors, namespace enforcement, and +command-key case sensitivity. + +Categories: #1, #2, #3, #6, #11, #15, #16, #18 +""" + +from dataclasses import dataclass +from typing import Any + +import pytest + +from documentdb_tests.framework.assertions import assertFailureCode +from documentdb_tests.framework.bson_type_validator import ( + BsonTypeTestCase, + generate_bson_rejection_test_cases, +) +from documentdb_tests.framework.error_codes import ( + BAD_VALUE_ERROR, + COMMAND_NOT_FOUND_ERROR, + INVALID_OPTIONS_ERROR, + NO_SUCH_KEY_ERROR, + TYPE_MISMATCH_ERROR, + UNAUTHORIZED_ERROR, +) +from documentdb_tests.framework.executor import execute_admin_command, execute_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_case import BaseTestCase +from documentdb_tests.framework.test_constants import BsonType + +pytestmark = pytest.mark.admin + +_VALID_PARAM = "changeStreamOptions" +_PARAM = "changeStreamOptions" + + +_ARGUMENT_TYPE_SPEC = [ + BsonTypeTestCase( + id="getClusterParameter_argument", + msg="getClusterParameter should reject non-string/non-array argument types", + keyword="getClusterParameter", + valid_types=[BsonType.STRING, BsonType.ARRAY], + default_error_code=TYPE_MISMATCH_ERROR, + skip_rejection_types=[BsonType.NULL], + ), +] + +_ARGUMENT_REJECTION_CASES = generate_bson_rejection_test_cases(_ARGUMENT_TYPE_SPEC) + + +@pytest.mark.parametrize("bson_type,sample_value,spec", _ARGUMENT_REJECTION_CASES) +def test_getClusterParameter_argument_rejects_type(collection, bson_type, sample_value, spec): + """Test getClusterParameter rejects non-string/non-array argument types.""" + result = execute_admin_command(collection, {"getClusterParameter": sample_value}) + assertFailureCode( + result, + spec.expected_code(bson_type), + msg=f"getClusterParameter should reject {bson_type.value} argument.", + ) + + +@dataclass(frozen=True) +class NameCase(BaseTestCase): + """Test case for literal name semantics.""" + + name: Any = None + + +_BOUNDARY_NAMES: list[NameCase] = [ + NameCase( + "whitespace_only", + name=" ", + error_code=NO_SUCH_KEY_ERROR, + msg="Whitespace-only string should be treated as a literal no-match", + ), + NameCase( + "dotted_name", + name="a.b.c", + error_code=NO_SUCH_KEY_ERROR, + msg="Dotted name should not be traversed as a field path", + ), + NameCase( + "dollar_prefix", + name="$foo", + error_code=NO_SUCH_KEY_ERROR, + msg="Dollar-prefixed name should not be interpreted as an operator", + ), + NameCase( + "unicode_name", + name="paramétré", + error_code=NO_SUCH_KEY_ERROR, + msg="Unicode name should be treated as a literal no-match", + ), + NameCase( + "very_long_name", + name="x" * 10000, + error_code=NO_SUCH_KEY_ERROR, + msg="Very long name should not crash or timeout", + ), + NameCase( + "case_altered", + name="ChangeStreamOptions", + error_code=NO_SUCH_KEY_ERROR, + msg="Altered-case known name should not match (case-sensitive)", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(_BOUNDARY_NAMES)) +def test_getClusterParameter_name_is_literal_string(collection, test): + """Test that parameter names are treated as exact literal strings.""" + result = execute_admin_command(collection, {"getClusterParameter": test.name}) + assertFailureCode(result, test.error_code, msg=test.msg) + + +def test_getClusterParameter_star_in_array_is_literal_name(collection): + """Test ['*'] treats '*' as a literal name (not expand-all) and errors.""" + result = execute_admin_command(collection, {"getClusterParameter": ["*"]}) + assertFailureCode( + result, NO_SUCH_KEY_ERROR, msg="'*' inside an array is a literal name, not a wildcard" + ) + + +def test_getClusterParameter_double_star_no_glob(collection): + """Test '**' is treated as a literal name, not a pattern.""" + result = execute_admin_command(collection, {"getClusterParameter": "**"}) + assertFailureCode(result, NO_SUCH_KEY_ERROR, msg="'**' should not be treated as a glob") + + +def test_getClusterParameter_star_prefix_no_glob(collection): + """Test '*foo' is treated as a literal name, not a pattern.""" + result = execute_admin_command(collection, {"getClusterParameter": "*foo"}) + assertFailureCode(result, NO_SUCH_KEY_ERROR, msg="'*foo' should not be treated as a glob") + + +def test_getClusterParameter_array_star_and_valid_name(collection): + """Test ['*', validName] treats '*' as a literal no-match and errors.""" + result = execute_admin_command(collection, {"getClusterParameter": ["*", _VALID_PARAM]}) + assertFailureCode( + result, + NO_SUCH_KEY_ERROR, + msg="'*' inside array with valid name should still error on '*' literal", + ) + + +def test_getClusterParameter_unknown_single_string_errors(collection): + """Test single unknown string fails with NO_SUCH_KEY_ERROR.""" + result = execute_admin_command(collection, {"getClusterParameter": "unknownParam"}) + assertFailureCode( + result, + NO_SUCH_KEY_ERROR, + msg="Single unknown name should fail with no-such-parameter error", + ) + + +def test_getClusterParameter_unknown_in_array_errors(collection): + """Test ['unknownParam'] fails with same error as 'unknownParam' string.""" + result = execute_admin_command(collection, {"getClusterParameter": ["unknownParam"]}) + assertFailureCode( + result, + NO_SUCH_KEY_ERROR, + msg="Array containing single unknown name should fail identically to string form", + ) + + +def test_getClusterParameter_empty_array_errors(collection): + """Test empty array argument fails with BAD_VALUE_ERROR.""" + result = execute_admin_command(collection, {"getClusterParameter": []}) + assertFailureCode(result, BAD_VALUE_ERROR, msg="Empty array must supply at least one name") + + +def test_getClusterParameter_array_nonstring_element_rejects(collection): + """Test array containing a non-string element fails with TYPE_MISMATCH_ERROR.""" + result = execute_admin_command(collection, {"getClusterParameter": [_VALID_PARAM, 123]}) + assertFailureCode( + result, TYPE_MISMATCH_ERROR, msg="Non-string array element should be rejected" + ) + + +def test_getClusterParameter_unknown_name_errors(collection): + """Test an unknown parameter name fails with NO_SUCH_KEY_ERROR (code 4).""" + result = execute_admin_command(collection, {"getClusterParameter": "doesNotExist"}) + assertFailureCode(result, NO_SUCH_KEY_ERROR, msg="Unknown parameter name should fail") + + +def test_getClusterParameter_null_argument(collection): + """Test a null argument is rejected with a type-mismatch error.""" + result = execute_admin_command(collection, {"getClusterParameter": None}) + assertFailureCode( + result, + TYPE_MISMATCH_ERROR, + msg="Null argument should be rejected as a type mismatch.", + ) + + +def test_getClusterParameter_empty_string_argument(collection): + """Test an empty-string argument is treated as an unknown parameter name.""" + result = execute_admin_command(collection, {"getClusterParameter": ""}) + assertFailureCode( + result, + NO_SUCH_KEY_ERROR, + msg="Empty-string argument should be treated as an unknown parameter name.", + ) + + +def test_getClusterParameter_array_null_element_rejects(collection): + """Test array containing null element fails with TYPE_MISMATCH_ERROR.""" + result = execute_admin_command(collection, {"getClusterParameter": [None]}) + assertFailureCode(result, TYPE_MISMATCH_ERROR, msg="Null element in array should be rejected") + + +def test_getClusterParameter_array_doc_element_rejects(collection): + """Test array containing a document element fails with TYPE_MISMATCH_ERROR.""" + result = execute_admin_command(collection, {"getClusterParameter": [{"a": 1}]}) + assertFailureCode( + result, TYPE_MISMATCH_ERROR, msg="Document element in array should be rejected" + ) + + +def test_getClusterParameter_array_nested_array_rejects(collection): + """Test array containing a nested array element fails with TYPE_MISMATCH_ERROR.""" + result = execute_admin_command(collection, {"getClusterParameter": [[_VALID_PARAM]]}) + assertFailureCode(result, TYPE_MISMATCH_ERROR, msg="Nested array element should be rejected") + + +def test_getClusterParameter_array_mixed_valid_unknown_errors(collection): + """Test array with one valid and one unknown name fails with NO_SUCH_KEY_ERROR.""" + result = execute_admin_command( + collection, {"getClusterParameter": [_VALID_PARAM, "unknownParam"]} + ) + assertFailureCode(result, NO_SUCH_KEY_ERROR, msg="Unknown entry in mixed array should fail") + + +def test_getClusterParameter_wrong_case_command_key_rejected(collection): + """Test wrong-case command key 'getclusterparameter' is rejected as unknown command.""" + result = execute_admin_command(collection, {"getclusterparameter": "*"}) + assertFailureCode( + result, COMMAND_NOT_FOUND_ERROR, msg="Wrong-case command key should be rejected" + ) + + +def test_getClusterParameter_rejected_on_non_admin_database(collection): + """Test getClusterParameter is rejected against a non-admin database.""" + result = execute_command(collection, {"getClusterParameter": "*"}) + assertFailureCode( + result, + UNAUTHORIZED_ERROR, + msg="getClusterParameter should be rejected on a non-admin database.", + ) + + +def test_getClusterParameter_name_not_retrievable_via_getParameter(collection): + """Test a cluster parameter name cannot be retrieved via getParameter.""" + result = execute_admin_command(collection, {"getParameter": 1, _PARAM: 1}) + assertFailureCode( + result, + INVALID_OPTIONS_ERROR, + msg="Cluster parameter name should not be retrievable via getParameter", + ) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_name_semantics.py b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_name_semantics.py deleted file mode 100644 index 8c695519f..000000000 --- a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_name_semantics.py +++ /dev/null @@ -1,141 +0,0 @@ -"""Tests for how getClusterParameter interprets parameter names. - -Parameter names are exact-match literals: there is no field-path traversal, -operator interpretation, globbing, or case folding. Any name that is not an -exact match for an existing cluster parameter is reported as unknown, and the -single-name and array forms report an unknown name identically. -""" - -from dataclasses import dataclass -from typing import Any - -import pytest - -from documentdb_tests.framework.assertions import assertFailureCode -from documentdb_tests.framework.error_codes import NO_SUCH_KEY_ERROR -from documentdb_tests.framework.executor import execute_admin_command -from documentdb_tests.framework.parametrize import pytest_params -from documentdb_tests.framework.test_case import BaseTestCase - -pytestmark = pytest.mark.admin - -_VALID_PARAM = "changeStreamOptions" - - -# --------------------------------------------------------------------------- -# §8 Argument boundary probing — name strings are exact literals -# --------------------------------------------------------------------------- - - -@dataclass(frozen=True) -class NameCase(BaseTestCase): - """Test case for literal name semantics.""" - - name: Any = None - - -_BOUNDARY_NAMES: list[NameCase] = [ - NameCase( - "whitespace_only", - name=" ", - error_code=NO_SUCH_KEY_ERROR, - msg="Whitespace-only string should be treated as a literal no-match", - ), - NameCase( - "dotted_name", - name="a.b.c", - error_code=NO_SUCH_KEY_ERROR, - msg="Dotted name should not be traversed as a field path", - ), - NameCase( - "dollar_prefix", - name="$foo", - error_code=NO_SUCH_KEY_ERROR, - msg="Dollar-prefixed name should not be interpreted as an operator", - ), - NameCase( - "unicode_name", - name="paramétré", - error_code=NO_SUCH_KEY_ERROR, - msg="Unicode name should be treated as a literal no-match", - ), - NameCase( - "very_long_name", - name="x" * 10000, - error_code=NO_SUCH_KEY_ERROR, - msg="Very long name should not crash or timeout", - ), - NameCase( - "case_altered", - name="ChangeStreamOptions", - error_code=NO_SUCH_KEY_ERROR, - msg="Altered-case known name should not match (case-sensitive)", - ), -] - - -@pytest.mark.parametrize("test", pytest_params(_BOUNDARY_NAMES)) -def test_getClusterParameter_name_is_literal_string(collection, test): - """Test that parameter names are treated as exact literal strings.""" - result = execute_admin_command(collection, {"getClusterParameter": test.name}) - assertFailureCode(result, test.error_code, msg=test.msg) - - -# --------------------------------------------------------------------------- -# §9 Wildcard edge interactions -# --------------------------------------------------------------------------- - - -def test_getClusterParameter_star_in_array_is_literal_name(collection): - """Test ['*'] treats '*' as a literal name (not expand-all) and errors.""" - result = execute_admin_command(collection, {"getClusterParameter": ["*"]}) - assertFailureCode( - result, NO_SUCH_KEY_ERROR, msg="'*' inside an array is a literal name, not a wildcard" - ) - - -def test_getClusterParameter_double_star_no_glob(collection): - """Test '**' is treated as a literal name, not a pattern.""" - result = execute_admin_command(collection, {"getClusterParameter": "**"}) - assertFailureCode(result, NO_SUCH_KEY_ERROR, msg="'**' should not be treated as a glob") - - -def test_getClusterParameter_star_prefix_no_glob(collection): - """Test '*foo' is treated as a literal name, not a pattern.""" - result = execute_admin_command(collection, {"getClusterParameter": "*foo"}) - assertFailureCode(result, NO_SUCH_KEY_ERROR, msg="'*foo' should not be treated as a glob") - - -def test_getClusterParameter_array_star_and_valid_name(collection): - """Test ['*', validName] treats '*' as a literal no-match and errors.""" - result = execute_admin_command(collection, {"getClusterParameter": ["*", _VALID_PARAM]}) - assertFailureCode( - result, - NO_SUCH_KEY_ERROR, - msg="'*' inside array with valid name should still error on '*' literal", - ) - - -# --------------------------------------------------------------------------- -# §11 Unknown-name contract consistency -# --------------------------------------------------------------------------- - - -def test_getClusterParameter_unknown_single_string_errors(collection): - """Test single unknown string fails with NO_SUCH_KEY_ERROR.""" - result = execute_admin_command(collection, {"getClusterParameter": "unknownParam"}) - assertFailureCode( - result, - NO_SUCH_KEY_ERROR, - msg="Single unknown name should fail with no-such-parameter error", - ) - - -def test_getClusterParameter_unknown_in_array_errors(collection): - """Test ['unknownParam'] fails with same error as 'unknownParam' string.""" - result = execute_admin_command(collection, {"getClusterParameter": ["unknownParam"]}) - assertFailureCode( - result, - NO_SUCH_KEY_ERROR, - msg="Array containing single unknown name should fail identically to string form", - ) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_response_structure.py b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_response_structure.py index f111bc0f7..d0ffee130 100644 --- a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_response_structure.py +++ b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_response_structure.py @@ -24,11 +24,6 @@ _NUMERIC_TYPES = (int, float, Int64, Decimal128) -# --------------------------------------------------------------------------- -# §4 Response shape -# --------------------------------------------------------------------------- - - def test_getClusterParameter_response_has_ok_1(collection): """Test success response includes ok:1.""" result = execute_admin_command(collection, {"getClusterParameter": "*"}) @@ -101,11 +96,6 @@ def test_getClusterParameter_element_is_object_type(collection): ) -# --------------------------------------------------------------------------- -# §17 Value type fidelity -# --------------------------------------------------------------------------- - - def test_getClusterParameter_clusterParameterTime_is_timestamp_when_present(collection): """Test any clusterParameterTime field is a BSON Timestamp.""" params = execute_admin_command(collection, {"getClusterParameter": "*"})["clusterParameters"] diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_round_trip.py b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_round_trip.py index edc2b294e..d64a3f8c4 100644 --- a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_round_trip.py +++ b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_round_trip.py @@ -10,12 +10,7 @@ import pytest from bson import Int64 -from documentdb_tests.framework.assertions import ( - assertFailureCode, - assertProperties, - assertSuccessPartial, -) -from documentdb_tests.framework.error_codes import INVALID_OPTIONS_ERROR +from documentdb_tests.framework.assertions import assertProperties, assertSuccessPartial from documentdb_tests.framework.executor import execute_admin_command from documentdb_tests.framework.property_checks import Eq, Len @@ -41,11 +36,6 @@ def _set_expire_after_seconds(collection, value): ) -# --------------------------------------------------------------------------- -# §12 Round-trip with setClusterParameter -# --------------------------------------------------------------------------- - - def test_getClusterParameter_reads_value_after_set(collection): """Test getClusterParameter returns the value set by setClusterParameter.""" original = _get_expire_after_seconds(collection) @@ -76,21 +66,6 @@ def test_getClusterParameter_never_set_returns_default(collection): ) -# --------------------------------------------------------------------------- -# §16 Differential: getClusterParameter vs getParameter have distinct namespaces -# --------------------------------------------------------------------------- - - -def test_getClusterParameter_name_not_retrievable_via_getParameter(collection): - """Test a cluster parameter name cannot be retrieved via getParameter.""" - result = execute_admin_command(collection, {"getParameter": 1, _PARAM: 1}) - assertFailureCode( - result, - INVALID_OPTIONS_ERROR, - msg="Cluster parameter name should not be retrievable via getParameter", - ) - - def test_getClusterParameter_server_parameter_not_a_cluster_parameter(collection): """Test a server parameter (logLevel) is not present in cluster parameters.""" names = [ diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/utils/__init__.py b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/utils/__init__.py deleted file mode 100644 index 940510b19..000000000 --- a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/utils/__init__.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Shared helpers for getClusterParameter tests. - -Cluster parameter values and the exact set of available parameters vary by -deployment, so tests derive a valid parameter name at runtime from the -wildcard form rather than hard-coding one. -""" - -from documentdb_tests.framework.executor import execute_admin_command - - -def all_cluster_parameters(collection): - """Return the ``clusterParameters`` list from ``getClusterParameter: '*'``. - - Raises the underlying exception if the command did not succeed, so callers - fail loudly during setup rather than asserting against an error object. - """ - result = execute_admin_command(collection, {"getClusterParameter": "*"}) - if isinstance(result, Exception): - raise result - return result["clusterParameters"] - - -def valid_parameter_names(collection, count=1): - """Return up to ``count`` valid cluster parameter names for this deployment.""" - params = all_cluster_parameters(collection) - names = [p["_id"] for p in params] - if len(names) < count: - raise AssertionError( - f"deployment exposes only {len(names)} cluster parameter(s), " - f"need {count} for this test" - ) - return names[:count] From 9ae0d1794f21281e2aaebfea58aae1535b244f85 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Mon, 29 Jun 2026 14:12:16 -0700 Subject: [PATCH 04/11] Parametrized test, removed duplicates and out of scope cases, added missing coverage Signed-off-by: PatersonProjects --- ...t_getClusterParameter_argument_handling.py | 122 ++++--- .../test_getClusterParameter_core_behavior.py | 96 ++---- .../test_getClusterParameter_errors.py | 302 +++++------------- ..._getClusterParameter_response_structure.py | 172 +++------- .../test_getClusterParameter_round_trip.py | 98 ------ .../system/administration/utils/__init__.py | 0 .../utils/administration_test_case.py | 50 +++ documentdb_tests/framework/property_checks.py | 19 ++ 8 files changed, 298 insertions(+), 561 deletions(-) delete mode 100644 documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_round_trip.py create mode 100644 documentdb_tests/compatibility/tests/system/administration/utils/__init__.py create mode 100644 documentdb_tests/compatibility/tests/system/administration/utils/administration_test_case.py diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_argument_handling.py b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_argument_handling.py index 81dcb7376..5fdde8516 100644 --- a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_argument_handling.py +++ b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_argument_handling.py @@ -1,62 +1,92 @@ -"""Tests for getClusterParameter accepted argument forms. +"""Tests for getClusterParameter argument handling. -Covers the accepted argument forms: single string, wildcard, array of -strings, duplicate names, and the extra comment field. - -Categories: #2, #10 +Covers accepted argument forms (single string, wildcard, array of +strings, duplicate names, unrecognized extra field) and BSON type +rejection for the command argument. """ import pytest -from documentdb_tests.framework.assertions import assertProperties, assertSuccessPartial +from documentdb_tests.compatibility.tests.system.administration.utils.administration_test_case import ( # noqa: E501 + AdministrationTestCase, +) +from documentdb_tests.framework.assertions import assertFailureCode, assertProperties +from documentdb_tests.framework.bson_type_validator import ( + BsonTypeTestCase, + generate_bson_rejection_test_cases, +) +from documentdb_tests.framework.error_codes import TYPE_MISMATCH_ERROR from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.property_checks import Eq, Len +from documentdb_tests.framework.test_constants import BsonType pytestmark = pytest.mark.admin _VALID_PARAM = "changeStreamOptions" -_VALID_PARAM_2 = "changeStreams" - - -def test_getClusterParameter_wildcard_returns_all(collection): - """Test '*' argument returns all available cluster parameters with ok:1.""" - result = execute_admin_command(collection, {"getClusterParameter": "*"}) - assertSuccessPartial(result, {"ok": 1.0}, msg="Wildcard '*' should return ok:1") - - -def test_getClusterParameter_single_name_returns_one(collection): - """Test single string name returns exactly one clusterParameters entry.""" - result = execute_admin_command(collection, {"getClusterParameter": _VALID_PARAM}) - assertProperties( +_VALID_PARAM_2 = "defaultMaxTimeMS" + +_ARGUMENT_TYPE_SPEC = [ + BsonTypeTestCase( + id="getClusterParameter_argument", + msg="getClusterParameter should reject non-string/non-array argument types", + keyword="getClusterParameter", + valid_types=[BsonType.STRING, BsonType.ARRAY], + default_error_code=TYPE_MISMATCH_ERROR, + skip_rejection_types=[BsonType.NULL], + ), +] + +_ARGUMENT_REJECTION_CASES = generate_bson_rejection_test_cases(_ARGUMENT_TYPE_SPEC) + + +@pytest.mark.parametrize("bson_type,sample_value,spec", _ARGUMENT_REJECTION_CASES) +def test_getClusterParameter_argument_rejects_type(collection, bson_type, sample_value, spec): + """Test getClusterParameter rejects non-string/non-array argument types.""" + result = execute_admin_command(collection, {"getClusterParameter": sample_value}) + assertFailureCode( result, - {"ok": Eq(1.0), "clusterParameters": Len(1)}, - msg="Single name should return ok:1 with one parameter", - raw_res=True, + spec.expected_code(bson_type), + msg=f"getClusterParameter should reject {bson_type.value} argument.", ) -def test_getClusterParameter_array_two_names_returns_two(collection): - """Test array of two valid names returns two clusterParameters entries.""" - result = execute_admin_command( - collection, {"getClusterParameter": [_VALID_PARAM, _VALID_PARAM_2]} - ) - assertProperties( - result, - {"ok": Eq(1.0), "clusterParameters": Len(2)}, +ARGUMENT_FORM_TESTS: list[AdministrationTestCase] = [ + AdministrationTestCase( + id="wildcard_returns_all", + command={"getClusterParameter": "*"}, + checks={"ok": Eq(1.0)}, + msg="Wildcard '*' should return ok:1", + ), + AdministrationTestCase( + id="single_name_returns_one", + command={"getClusterParameter": _VALID_PARAM}, + checks={"ok": Eq(1.0), "clusterParameters": Len(1)}, + msg="Single name should return ok:1 with one parameter", + ), + AdministrationTestCase( + id="array_two_names_returns_two", + command={"getClusterParameter": [_VALID_PARAM, _VALID_PARAM_2]}, + checks={"ok": Eq(1.0), "clusterParameters": Len(2)}, msg="Array of two names should return two parameters", - raw_res=True, - ) - - -def test_getClusterParameter_array_duplicate_names(collection): - """Test array with duplicate valid names succeeds.""" - result = execute_admin_command( - collection, {"getClusterParameter": [_VALID_PARAM, _VALID_PARAM]} - ) - assertSuccessPartial(result, {"ok": 1.0}, msg="Duplicate names in array should succeed") - - -def test_getClusterParameter_unrecognized_field_accepted(collection): - """Test extra comment field is accepted (MongoDB treats it as generic command field).""" - result = execute_admin_command(collection, {"getClusterParameter": "*", "comment": "test"}) - assertSuccessPartial(result, {"ok": 1.0}, msg="comment field should be accepted") + ), + AdministrationTestCase( + id="array_duplicate_names", + command={"getClusterParameter": [_VALID_PARAM, _VALID_PARAM]}, + checks={"ok": Eq(1.0)}, + msg="Duplicate names in array should succeed", + ), + AdministrationTestCase( + id="unrecognized_field_accepted", + command={"getClusterParameter": "*", "unknownField": "test"}, + checks={"ok": Eq(1.0)}, + msg="Unrecognized extra field should be accepted", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(ARGUMENT_FORM_TESTS)) +def test_getClusterParameter_argument_forms(collection, test): + """Test accepted argument forms each return ok:1 with expected clusterParameters length.""" + result = execute_admin_command(collection, test.build_command()) + assertProperties(result, test.build_checks(), msg=test.msg, raw_res=True) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_core_behavior.py b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_core_behavior.py index 246fe7a40..5e6cdaa96 100644 --- a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_core_behavior.py +++ b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_core_behavior.py @@ -1,85 +1,41 @@ """Tests for getClusterParameter core retrieval behavior. -Covers the three accepted argument forms, default values on a fresh -deployment, idempotency of repeated calls, and that requesting one name -does not return unrelated parameters. - -Categories: #4 (core behavior + deployment variants), #7, #14 +Verifies that the wildcard returns more than one cluster parameter and +that the result includes known parameters by name. """ import pytest -from documentdb_tests.framework.assertions import assertProperties, assertSuccessPartial +from documentdb_tests.compatibility.tests.system.administration.utils.administration_test_case import ( # noqa: E501 + AdministrationTestCase, +) +from documentdb_tests.framework.assertions import assertProperties from documentdb_tests.framework.executor import execute_admin_command -from documentdb_tests.framework.property_checks import Eq, Len +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.property_checks import Contains, LenGt pytestmark = pytest.mark.admin _VALID_PARAM = "changeStreamOptions" -_VALID_PARAM_2 = "changeStreams" - - -def test_getClusterParameter_single_name_succeeds(collection): - """Test single valid name returns ok:1 with clusterParameters length 1.""" - result = execute_admin_command(collection, {"getClusterParameter": _VALID_PARAM}) - assertProperties( - result, - {"ok": Eq(1.0), "clusterParameters": Len(1)}, - msg="Single valid name should return ok:1 with one parameter", - raw_res=True, - ) - - -def test_getClusterParameter_wildcard_returns_all_params(collection): - """Test '*' returns ok:1.""" - result = execute_admin_command(collection, {"getClusterParameter": "*"}) - assertSuccessPartial(result, {"ok": 1.0}, msg="Wildcard should return ok:1") - -def test_getClusterParameter_wildcard_includes_known_param(collection): - """Test '*' result includes the known parameter 'changeStreamOptions'.""" - result = execute_admin_command(collection, {"getClusterParameter": "*"}) - ids = [p["_id"] for p in result["clusterParameters"]] - assertProperties( - {"found": 1 if _VALID_PARAM in ids else 0}, - {"found": Eq(1)}, +CORE_BEHAVIOR_TESTS: list[AdministrationTestCase] = [ + AdministrationTestCase( + id="wildcard_returns_multiple_params", + command={"getClusterParameter": "*"}, + checks={"clusterParameters": LenGt(1)}, + msg="Wildcard should return more than one cluster parameter", + ), + AdministrationTestCase( + id="wildcard_includes_known_param", + command={"getClusterParameter": "*"}, + checks={"clusterParameters": Contains("_id", _VALID_PARAM)}, msg=f"Wildcard result should include '{_VALID_PARAM}'", - raw_res=True, - ) - - -def test_getClusterParameter_array_two_names_succeeds(collection): - """Test array of two valid names returns ok:1 with clusterParameters length 2.""" - result = execute_admin_command( - collection, {"getClusterParameter": [_VALID_PARAM, _VALID_PARAM_2]} - ) - assertProperties( - result, - {"ok": Eq(1.0), "clusterParameters": Len(2)}, - msg="Array of two names should return ok:1 with two parameters", - raw_res=True, - ) - - -def test_getClusterParameter_single_name_id_equals_request(collection): - """Test requesting one name returns element with _id equal to requested name.""" - result = execute_admin_command(collection, {"getClusterParameter": _VALID_PARAM}) - assertProperties( - result, - {"clusterParameters.0._id": Eq(_VALID_PARAM)}, - msg=f"_id should equal the requested name '{_VALID_PARAM}'", - raw_res=True, - ) + ), +] -def test_getClusterParameter_wildcard_idempotent(collection): - """Test repeated '*' calls return the same parameter count.""" - r1 = execute_admin_command(collection, {"getClusterParameter": "*"}) - r2 = execute_admin_command(collection, {"getClusterParameter": "*"}) - count1 = len(r1["clusterParameters"]) - assertProperties( - {"count": len(r2["clusterParameters"])}, - {"count": Eq(count1)}, - msg="Repeated wildcard calls should return stable parameter count", - raw_res=True, - ) +@pytest.mark.parametrize("test", pytest_params(CORE_BEHAVIOR_TESTS)) +def test_getClusterParameter_core_behavior(collection, test): + """Test core retrieval behavior: wildcard parameter count and inclusion.""" + result = execute_admin_command(collection, test.build_command()) + assertProperties(result, test.build_checks(), msg=test.msg, raw_res=True) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_errors.py b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_errors.py index 4efa14ecc..a478241a4 100644 --- a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_errors.py +++ b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_errors.py @@ -1,245 +1,129 @@ """Tests for getClusterParameter error cases. -Consolidates all error-producing inputs: BSON type rejection for the -argument field, unknown parameter names, invalid argument forms, null -coercion, array element type errors, namespace enforcement, and -command-key case sensitivity. - -Categories: #1, #2, #3, #6, #11, #15, #16, #18 +Covers error-producing inputs: unknown parameter names, empty and +null arguments, array element type errors, command-key case +sensitivity, and key ordering enforcement. Also verifies that the +command is rejected on non-admin databases. """ -from dataclasses import dataclass -from typing import Any - import pytest -from documentdb_tests.framework.assertions import assertFailureCode -from documentdb_tests.framework.bson_type_validator import ( - BsonTypeTestCase, - generate_bson_rejection_test_cases, +from documentdb_tests.compatibility.tests.system.administration.utils.administration_test_case import ( # noqa: E501 + AdministrationTestCase, ) +from documentdb_tests.framework.assertions import assertFailureCode from documentdb_tests.framework.error_codes import ( BAD_VALUE_ERROR, COMMAND_NOT_FOUND_ERROR, - INVALID_OPTIONS_ERROR, NO_SUCH_KEY_ERROR, TYPE_MISMATCH_ERROR, UNAUTHORIZED_ERROR, ) from documentdb_tests.framework.executor import execute_admin_command, execute_command from documentdb_tests.framework.parametrize import pytest_params -from documentdb_tests.framework.test_case import BaseTestCase -from documentdb_tests.framework.test_constants import BsonType pytestmark = pytest.mark.admin _VALID_PARAM = "changeStreamOptions" -_PARAM = "changeStreamOptions" - - -_ARGUMENT_TYPE_SPEC = [ - BsonTypeTestCase( - id="getClusterParameter_argument", - msg="getClusterParameter should reject non-string/non-array argument types", - keyword="getClusterParameter", - valid_types=[BsonType.STRING, BsonType.ARRAY], - default_error_code=TYPE_MISMATCH_ERROR, - skip_rejection_types=[BsonType.NULL], - ), -] - -_ARGUMENT_REJECTION_CASES = generate_bson_rejection_test_cases(_ARGUMENT_TYPE_SPEC) - - -@pytest.mark.parametrize("bson_type,sample_value,spec", _ARGUMENT_REJECTION_CASES) -def test_getClusterParameter_argument_rejects_type(collection, bson_type, sample_value, spec): - """Test getClusterParameter rejects non-string/non-array argument types.""" - result = execute_admin_command(collection, {"getClusterParameter": sample_value}) - assertFailureCode( - result, - spec.expected_code(bson_type), - msg=f"getClusterParameter should reject {bson_type.value} argument.", - ) - -@dataclass(frozen=True) -class NameCase(BaseTestCase): - """Test case for literal name semantics.""" - name: Any = None - - -_BOUNDARY_NAMES: list[NameCase] = [ - NameCase( - "whitespace_only", - name=" ", - error_code=NO_SUCH_KEY_ERROR, - msg="Whitespace-only string should be treated as a literal no-match", - ), - NameCase( - "dotted_name", - name="a.b.c", +_NO_SUCH_KEY_CASES: list[AdministrationTestCase] = [ + AdministrationTestCase( + id="unknown_single_string_errors", + command={"getClusterParameter": "unknownParam"}, error_code=NO_SUCH_KEY_ERROR, - msg="Dotted name should not be traversed as a field path", + msg="Single unknown name should fail with no-such-parameter error", ), - NameCase( - "dollar_prefix", - name="$foo", + AdministrationTestCase( + id="empty_string_argument", + command={"getClusterParameter": ""}, error_code=NO_SUCH_KEY_ERROR, - msg="Dollar-prefixed name should not be interpreted as an operator", + msg="Empty-string argument should be treated as an unknown parameter name", ), - NameCase( - "unicode_name", - name="paramétré", + AdministrationTestCase( + id="case_altered", + command={"getClusterParameter": "ChangeStreamOptions"}, error_code=NO_SUCH_KEY_ERROR, - msg="Unicode name should be treated as a literal no-match", + msg="Altered-case known name should not match (case-sensitive)", ), - NameCase( - "very_long_name", - name="x" * 10000, + AdministrationTestCase( + id="star_in_array_is_literal_name", + command={"getClusterParameter": ["*"]}, error_code=NO_SUCH_KEY_ERROR, - msg="Very long name should not crash or timeout", + msg="'*' inside an array is a literal name, not a wildcard", ), - NameCase( - "case_altered", - name="ChangeStreamOptions", + AdministrationTestCase( + id="array_mixed_valid_unknown_errors", + command={"getClusterParameter": [_VALID_PARAM, "unknownParam"]}, error_code=NO_SUCH_KEY_ERROR, - msg="Altered-case known name should not match (case-sensitive)", + msg="Unknown entry in mixed array should fail", ), ] +_TYPE_ERROR_CASES: list[AdministrationTestCase] = [ + AdministrationTestCase( + id="empty_array_errors", + command={"getClusterParameter": []}, + error_code=BAD_VALUE_ERROR, + msg="Empty array must supply at least one name", + ), + AdministrationTestCase( + id="array_nonstring_element_rejects", + command={"getClusterParameter": [_VALID_PARAM, 123]}, + error_code=TYPE_MISMATCH_ERROR, + msg="Non-string array element should be rejected", + ), + AdministrationTestCase( + id="null_argument", + command={"getClusterParameter": None}, + error_code=TYPE_MISMATCH_ERROR, + msg="Null argument should be rejected as a type mismatch", + ), + AdministrationTestCase( + id="array_null_element_rejects", + command={"getClusterParameter": [None]}, + error_code=TYPE_MISMATCH_ERROR, + msg="Null element in array should be rejected", + ), + AdministrationTestCase( + id="array_doc_element_rejects", + command={"getClusterParameter": [{"a": 1}]}, + error_code=TYPE_MISMATCH_ERROR, + msg="Document element in array should be rejected", + ), + AdministrationTestCase( + id="array_nested_array_rejects", + command={"getClusterParameter": [[_VALID_PARAM]]}, + error_code=TYPE_MISMATCH_ERROR, + msg="Nested array element should be rejected", + ), +] -@pytest.mark.parametrize("test", pytest_params(_BOUNDARY_NAMES)) -def test_getClusterParameter_name_is_literal_string(collection, test): - """Test that parameter names are treated as exact literal strings.""" - result = execute_admin_command(collection, {"getClusterParameter": test.name}) - assertFailureCode(result, test.error_code, msg=test.msg) - - -def test_getClusterParameter_star_in_array_is_literal_name(collection): - """Test ['*'] treats '*' as a literal name (not expand-all) and errors.""" - result = execute_admin_command(collection, {"getClusterParameter": ["*"]}) - assertFailureCode( - result, NO_SUCH_KEY_ERROR, msg="'*' inside an array is a literal name, not a wildcard" - ) - - -def test_getClusterParameter_double_star_no_glob(collection): - """Test '**' is treated as a literal name, not a pattern.""" - result = execute_admin_command(collection, {"getClusterParameter": "**"}) - assertFailureCode(result, NO_SUCH_KEY_ERROR, msg="'**' should not be treated as a glob") - - -def test_getClusterParameter_star_prefix_no_glob(collection): - """Test '*foo' is treated as a literal name, not a pattern.""" - result = execute_admin_command(collection, {"getClusterParameter": "*foo"}) - assertFailureCode(result, NO_SUCH_KEY_ERROR, msg="'*foo' should not be treated as a glob") - - -def test_getClusterParameter_array_star_and_valid_name(collection): - """Test ['*', validName] treats '*' as a literal no-match and errors.""" - result = execute_admin_command(collection, {"getClusterParameter": ["*", _VALID_PARAM]}) - assertFailureCode( - result, - NO_SUCH_KEY_ERROR, - msg="'*' inside array with valid name should still error on '*' literal", - ) - - -def test_getClusterParameter_unknown_single_string_errors(collection): - """Test single unknown string fails with NO_SUCH_KEY_ERROR.""" - result = execute_admin_command(collection, {"getClusterParameter": "unknownParam"}) - assertFailureCode( - result, - NO_SUCH_KEY_ERROR, - msg="Single unknown name should fail with no-such-parameter error", - ) - - -def test_getClusterParameter_unknown_in_array_errors(collection): - """Test ['unknownParam'] fails with same error as 'unknownParam' string.""" - result = execute_admin_command(collection, {"getClusterParameter": ["unknownParam"]}) - assertFailureCode( - result, - NO_SUCH_KEY_ERROR, - msg="Array containing single unknown name should fail identically to string form", - ) - - -def test_getClusterParameter_empty_array_errors(collection): - """Test empty array argument fails with BAD_VALUE_ERROR.""" - result = execute_admin_command(collection, {"getClusterParameter": []}) - assertFailureCode(result, BAD_VALUE_ERROR, msg="Empty array must supply at least one name") - - -def test_getClusterParameter_array_nonstring_element_rejects(collection): - """Test array containing a non-string element fails with TYPE_MISMATCH_ERROR.""" - result = execute_admin_command(collection, {"getClusterParameter": [_VALID_PARAM, 123]}) - assertFailureCode( - result, TYPE_MISMATCH_ERROR, msg="Non-string array element should be rejected" - ) - - -def test_getClusterParameter_unknown_name_errors(collection): - """Test an unknown parameter name fails with NO_SUCH_KEY_ERROR (code 4).""" - result = execute_admin_command(collection, {"getClusterParameter": "doesNotExist"}) - assertFailureCode(result, NO_SUCH_KEY_ERROR, msg="Unknown parameter name should fail") - - -def test_getClusterParameter_null_argument(collection): - """Test a null argument is rejected with a type-mismatch error.""" - result = execute_admin_command(collection, {"getClusterParameter": None}) - assertFailureCode( - result, - TYPE_MISMATCH_ERROR, - msg="Null argument should be rejected as a type mismatch.", - ) - - -def test_getClusterParameter_empty_string_argument(collection): - """Test an empty-string argument is treated as an unknown parameter name.""" - result = execute_admin_command(collection, {"getClusterParameter": ""}) - assertFailureCode( - result, - NO_SUCH_KEY_ERROR, - msg="Empty-string argument should be treated as an unknown parameter name.", - ) - - -def test_getClusterParameter_array_null_element_rejects(collection): - """Test array containing null element fails with TYPE_MISMATCH_ERROR.""" - result = execute_admin_command(collection, {"getClusterParameter": [None]}) - assertFailureCode(result, TYPE_MISMATCH_ERROR, msg="Null element in array should be rejected") - - -def test_getClusterParameter_array_doc_element_rejects(collection): - """Test array containing a document element fails with TYPE_MISMATCH_ERROR.""" - result = execute_admin_command(collection, {"getClusterParameter": [{"a": 1}]}) - assertFailureCode( - result, TYPE_MISMATCH_ERROR, msg="Document element in array should be rejected" - ) - - -def test_getClusterParameter_array_nested_array_rejects(collection): - """Test array containing a nested array element fails with TYPE_MISMATCH_ERROR.""" - result = execute_admin_command(collection, {"getClusterParameter": [[_VALID_PARAM]]}) - assertFailureCode(result, TYPE_MISMATCH_ERROR, msg="Nested array element should be rejected") - +_COMMAND_ROUTING_CASES: list[AdministrationTestCase] = [ + AdministrationTestCase( + id="wrong_case_command_key_rejected", + command={"getclusterparameter": "*"}, + error_code=COMMAND_NOT_FOUND_ERROR, + msg="Wrong-case command key should be rejected", + ), + AdministrationTestCase( + id="command_key_not_first_fails", + command={"comment": "test", "getClusterParameter": "*"}, + error_code=COMMAND_NOT_FOUND_ERROR, + msg="getClusterParameter must be the first key in the command document", + ), +] -def test_getClusterParameter_array_mixed_valid_unknown_errors(collection): - """Test array with one valid and one unknown name fails with NO_SUCH_KEY_ERROR.""" - result = execute_admin_command( - collection, {"getClusterParameter": [_VALID_PARAM, "unknownParam"]} - ) - assertFailureCode(result, NO_SUCH_KEY_ERROR, msg="Unknown entry in mixed array should fail") +_ERROR_CASES: list[AdministrationTestCase] = ( + _NO_SUCH_KEY_CASES + _TYPE_ERROR_CASES + _COMMAND_ROUTING_CASES +) -def test_getClusterParameter_wrong_case_command_key_rejected(collection): - """Test wrong-case command key 'getclusterparameter' is rejected as unknown command.""" - result = execute_admin_command(collection, {"getclusterparameter": "*"}) - assertFailureCode( - result, COMMAND_NOT_FOUND_ERROR, msg="Wrong-case command key should be rejected" - ) +@pytest.mark.parametrize("test", pytest_params(_ERROR_CASES)) +def test_getClusterParameter_errors(collection, test): + """Test all error-producing inputs: unknown names, type mismatches, and command routing.""" + result = execute_admin_command(collection, test.build_command()) + assertFailureCode(result, test.error_code, msg=test.msg) def test_getClusterParameter_rejected_on_non_admin_database(collection): @@ -250,13 +134,3 @@ def test_getClusterParameter_rejected_on_non_admin_database(collection): UNAUTHORIZED_ERROR, msg="getClusterParameter should be rejected on a non-admin database.", ) - - -def test_getClusterParameter_name_not_retrievable_via_getParameter(collection): - """Test a cluster parameter name cannot be retrieved via getParameter.""" - result = execute_admin_command(collection, {"getParameter": 1, _PARAM: 1}) - assertFailureCode( - result, - INVALID_OPTIONS_ERROR, - msg="Cluster parameter name should not be retrievable via getParameter", - ) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_response_structure.py b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_response_structure.py index d0ffee130..eced7ed4e 100644 --- a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_response_structure.py +++ b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_response_structure.py @@ -1,148 +1,54 @@ -"""Tests for getClusterParameter response structure and value-type fidelity. +"""Tests for getClusterParameter response structure. -Verifies the shape and BSON types of the success response: -clusterParameters is an array, each element has _id equal to the -requested name, ok is 1, and single-name requests are isolated. -Also validates BSON type fidelity (clusterParameterTime, numeric values). - -Categories: #4 (response structure), #17 +Verifies the top-level shape of the success response: ok is 1, +clusterParameters is an array, and a single-name request returns +exactly one element. """ import pytest -from bson import Decimal128, Int64, Timestamp +from documentdb_tests.compatibility.tests.system.administration.utils.administration_test_case import ( # noqa: E501 + AdministrationTestCase, +) from documentdb_tests.framework.assertions import assertProperties from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.property_checks import Eq, IsType, Len pytestmark = pytest.mark.admin _VALID_PARAM = "changeStreamOptions" -# Numeric BSON types that must survive a round trip without being coerced to -# string. bool is intentionally excluded (it is not a numeric value here). -_NUMERIC_TYPES = (int, float, Int64, Decimal128) - - -def test_getClusterParameter_response_has_ok_1(collection): - """Test success response includes ok:1.""" - result = execute_admin_command(collection, {"getClusterParameter": "*"}) - assertProperties(result, {"ok": Eq(1.0)}, msg="Response ok should be 1.0", raw_res=True) - - -def test_getClusterParameter_response_clusterParameters_is_array(collection): - """Test success response contains 'clusterParameters' field of type array.""" - result = execute_admin_command(collection, {"getClusterParameter": "*"}) - assertProperties( - result, - {"clusterParameters": IsType("array")}, +PROPERTY_TESTS: list[AdministrationTestCase] = [ + AdministrationTestCase( + id="ok_is_1", + command={"getClusterParameter": "*"}, + checks={"ok": Eq(1.0)}, + msg="Response ok should be 1.0", + ), + AdministrationTestCase( + id="clusterParameters_is_array", + command={"getClusterParameter": "*"}, + checks={"clusterParameters": IsType("array")}, msg="clusterParameters should be an array", - raw_res=True, - ) - - -def test_getClusterParameter_single_name_length_is_one(collection): - """Test single-name request returns exactly one element in clusterParameters.""" - result = execute_admin_command(collection, {"getClusterParameter": _VALID_PARAM}) - assertProperties( - result, - {"clusterParameters": Len(1)}, + ), + AdministrationTestCase( + id="single_name_length_is_one", + command={"getClusterParameter": _VALID_PARAM}, + checks={"clusterParameters": Len(1)}, msg="Single-name request should return exactly one element", - raw_res=True, - ) - - -def test_getClusterParameter_element_has_id_field(collection): - """Test the clusterParameters element has a string '_id' field.""" - result = execute_admin_command(collection, {"getClusterParameter": _VALID_PARAM}) - assertProperties( - result, - {"clusterParameters.0._id": IsType("string")}, - msg="_id field should be a string", - raw_res=True, - ) - - -def test_getClusterParameter_single_name_id_matches_request(collection): - """Test requesting one name returns element with _id equal to requested name.""" - result = execute_admin_command(collection, {"getClusterParameter": _VALID_PARAM}) - assertProperties( - result, - {"clusterParameters.0._id": Eq(_VALID_PARAM)}, - msg=f"_id should equal requested name '{_VALID_PARAM}'", - raw_res=True, - ) - - -def test_getClusterParameter_element_has_nested_value_field(collection): - """Test document-valued parameter has a nested value sub-document.""" - result = execute_admin_command(collection, {"getClusterParameter": _VALID_PARAM}) - assertProperties( - result, - {"clusterParameters.0.preAndPostImages": IsType("object")}, - msg="changeStreamOptions should contain 'preAndPostImages' sub-document", - raw_res=True, - ) - - -def test_getClusterParameter_element_is_object_type(collection): - """Test the first clusterParameters element is a document (object type).""" - result = execute_admin_command(collection, {"getClusterParameter": _VALID_PARAM}) - assertProperties( - result, - {"clusterParameters.0": IsType("object")}, - msg="clusterParameters element should be an object", - raw_res=True, - ) - - -def test_getClusterParameter_clusterParameterTime_is_timestamp_when_present(collection): - """Test any clusterParameterTime field is a BSON Timestamp.""" - params = execute_admin_command(collection, {"getClusterParameter": "*"})["clusterParameters"] - times = [e["clusterParameterTime"] for e in params if "clusterParameterTime" in e] - if not times: - pytest.skip("no parameter on this deployment carries clusterParameterTime") - all_timestamps = all(isinstance(t, Timestamp) for t in times) - assertProperties( - {"all_timestamps": all_timestamps}, - {"all_timestamps": Eq(True)}, - msg="clusterParameterTime should be a BSON Timestamp.", - raw_res=True, - ) - - -def test_getClusterParameter_numeric_value_not_coerced(collection): - """Test numeric parameter values keep a numeric BSON type (not stringified).""" - params = execute_admin_command(collection, {"getClusterParameter": "*"})["clusterParameters"] - - def first_numeric_type(value): - if isinstance(value, bool): - return None - if isinstance(value, _NUMERIC_TYPES): - return type(value).__name__ - if isinstance(value, dict): - for v in value.values(): - found = first_numeric_type(v) - if found: - return found - return None - - numeric_type = None - for entry in params: - for key, value in entry.items(): - if key == "_id": - continue - numeric_type = first_numeric_type(value) - if numeric_type: - break - if numeric_type: - break - if numeric_type is None: - pytest.skip("no numeric-valued cluster parameter on this deployment") - is_numeric_type = numeric_type in {"int", "float", "Int64", "Decimal128"} - assertProperties( - {"is_numeric_type": is_numeric_type}, - {"is_numeric_type": Eq(True)}, - msg=f"Numeric parameter value kept numeric BSON type '{numeric_type}' (not coerced).", - raw_res=True, - ) + ), + AdministrationTestCase( + id="element_id_matches_request", + command={"getClusterParameter": _VALID_PARAM}, + checks={"clusterParameters.0._id": Eq(_VALID_PARAM)}, + msg=f"Single-name request should return element with _id equal to '{_VALID_PARAM}'", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(PROPERTY_TESTS)) +def test_getClusterParameter_response_properties(collection, test): + """Verifies getClusterParameter response fields have expected types and values.""" + result = execute_admin_command(collection, test.build_command()) + assertProperties(result, test.build_checks(), msg=test.msg, raw_res=True) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_round_trip.py b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_round_trip.py deleted file mode 100644 index d64a3f8c4..000000000 --- a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_round_trip.py +++ /dev/null @@ -1,98 +0,0 @@ -"""Tests for getClusterParameter round-trip and differential behavior. - -Verifies read-after-write consistency with setClusterParameter, that -never-set parameters return their defaults, and that getClusterParameter -and getParameter operate on distinct namespaces. - -Categories: #12, #16 -""" - -import pytest -from bson import Int64 - -from documentdb_tests.framework.assertions import assertProperties, assertSuccessPartial -from documentdb_tests.framework.executor import execute_admin_command -from documentdb_tests.framework.property_checks import Eq, Len - -pytestmark = [pytest.mark.admin, pytest.mark.no_parallel] - -_PARAM = "changeStreamOptions" -_NESTED_KEY = "preAndPostImages" -_VALUE_KEY = "expireAfterSeconds" - - -def _get_expire_after_seconds(collection): - """Read the current expireAfterSeconds value from changeStreamOptions.""" - r = execute_admin_command(collection, {"getClusterParameter": _PARAM}) - assertSuccessPartial(r, {"ok": 1.0}, msg="getClusterParameter should succeed in setup") - return r["clusterParameters"][0][_NESTED_KEY][_VALUE_KEY] - - -def _set_expire_after_seconds(collection, value): - """Set expireAfterSeconds on changeStreamOptions.""" - execute_admin_command( - collection, - {"setClusterParameter": {_PARAM: {_NESTED_KEY: {_VALUE_KEY: value}}}}, - ) - - -def test_getClusterParameter_reads_value_after_set(collection): - """Test getClusterParameter returns the value set by setClusterParameter.""" - original = _get_expire_after_seconds(collection) - new_value = Int64(7200) if int(original) != 7200 else Int64(3600) - try: - _set_expire_after_seconds(collection, int(new_value)) - result = execute_admin_command(collection, {"getClusterParameter": _PARAM}) - assertProperties( - result, - {"clusterParameters.0.preAndPostImages.expireAfterSeconds": Eq(new_value)}, - msg=f"expireAfterSeconds should equal {new_value} after set", - raw_res=True, - ) - finally: - _set_expire_after_seconds(collection, int(original)) - - -def test_getClusterParameter_never_set_returns_default(collection): - """Test a parameter that was never explicitly set returns a default without error.""" - result = execute_admin_command( - collection, {"getClusterParameter": "internalQueryCutoffForSampleFromRandomCursor"} - ) - assertProperties( - result, - {"ok": Eq(1.0), "clusterParameters": Len(1)}, - msg="Never-set parameter should return default without error", - raw_res=True, - ) - - -def test_getClusterParameter_server_parameter_not_a_cluster_parameter(collection): - """Test a server parameter (logLevel) is not present in cluster parameters.""" - names = [ - p["_id"] - for p in execute_admin_command(collection, {"getClusterParameter": "*"})[ - "clusterParameters" - ] - ] - assertProperties( - {"has_logLevel": "logLevel" in names}, - {"has_logLevel": Eq(False)}, - msg="Server parameter 'logLevel' should not be a cluster parameter.", - raw_res=True, - ) - - -def test_getClusterParameter_defaultRWConcern_not_a_cluster_parameter(collection): - """Test defaultRWConcern is not a cluster parameter (distinct from getDefaultRWConcern).""" - names = [ - p["_id"] - for p in execute_admin_command(collection, {"getClusterParameter": "*"})[ - "clusterParameters" - ] - ] - assertProperties( - {"has_defaultRWConcern": "defaultRWConcern" in names}, - {"has_defaultRWConcern": Eq(False)}, - msg="defaultRWConcern should not appear among cluster parameters.", - raw_res=True, - ) diff --git a/documentdb_tests/compatibility/tests/system/administration/utils/__init__.py b/documentdb_tests/compatibility/tests/system/administration/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/system/administration/utils/administration_test_case.py b/documentdb_tests/compatibility/tests/system/administration/utils/administration_test_case.py new file mode 100644 index 000000000..0bb87e753 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/administration/utils/administration_test_case.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional + +from documentdb_tests.framework.test_case import BaseTestCase + + +@dataclass(frozen=True) +class AdministrationTestCase(BaseTestCase): + """Test case for administration command tests. + + Administration commands often require runtime-discovered values (e.g. a + valid cluster parameter name). Fields that need these values accept a + callable that receives the discovered value at execution time. + + Attributes: + setup: Commands to run before the test command to establish state. + command: A callable ``(param: str) -> dict`` for commands that need a + runtime-discovered value, or a plain dict. + checks: Mapping of dotted field paths to property check objects, or a + callable that receives the same runtime values as ``build_command`` + and returns such a mapping. + """ + + setup: List[Dict[str, Any]] = field(default_factory=list) + command: Optional[Dict[str, Any] | Callable[..., Dict[str, Any]]] = None + checks: Dict[str, Any] | Callable[..., Dict[str, Any]] = field(default_factory=dict) + + def build_command(self, *args: Any, **kwargs: Any) -> Dict[str, Any]: + """Resolve the command dict from a callable or plain dict. + + Pass any runtime-discovered values as positional or keyword arguments; + they are forwarded to the callable unchanged. + """ + if self.command is None: + raise ValueError(f"AdministrationTestCase '{self.id}' has no command defined") + if callable(self.command): + return self.command(*args, **kwargs) + return self.command + + def build_checks(self, *args: Any, **kwargs: Any) -> Dict[str, Any]: + """Resolve the checks dict from a callable or plain dict. + + Pass the same runtime-discovered values used for ``build_command``. + """ + if callable(self.checks): + return self.checks(*args, **kwargs) + return self.checks diff --git a/documentdb_tests/framework/property_checks.py b/documentdb_tests/framework/property_checks.py index 0f029cb54..9c7b1f112 100644 --- a/documentdb_tests/framework/property_checks.py +++ b/documentdb_tests/framework/property_checks.py @@ -165,6 +165,25 @@ def __repr__(self) -> str: return f"{type(self).__name__}({self.expected!r})" +class LenGt(Check): + """Assert that the field is a list with length strictly greater than a minimum.""" + + def __init__(self, minimum: int) -> None: + self.minimum = minimum + + def check(self, value: Any, path: str) -> str | None: + if value is _FIELD_ABSENT: + return f"expected '{path}' length > {self.minimum}, but field is missing" + if not isinstance(value, list): + return f"expected '{path}' to be a list, got {type(value).__name__}" + if len(value) <= self.minimum: + return f"expected '{path}' length > {self.minimum}, got {len(value)}" + return None + + def __repr__(self) -> str: + return f"{type(self).__name__}({self.minimum!r})" + + class Contains(Check): """Assert that a list contains a dict where ``key`` equals ``value``.""" From 5bb4075790af12a471277ecc0ac38c749f6a75e4 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Mon, 29 Jun 2026 14:43:57 -0700 Subject: [PATCH 05/11] Remove unused test class functions Signed-off-by: PatersonProjects --- ...t_getClusterParameter_argument_handling.py | 4 +- .../test_getClusterParameter_core_behavior.py | 4 +- .../test_getClusterParameter_errors.py | 2 +- ..._getClusterParameter_response_structure.py | 4 +- .../utils/administration_test_case.py | 41 +++---------------- 5 files changed, 12 insertions(+), 43 deletions(-) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_argument_handling.py b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_argument_handling.py index 5fdde8516..3251bfafb 100644 --- a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_argument_handling.py +++ b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_argument_handling.py @@ -88,5 +88,5 @@ def test_getClusterParameter_argument_rejects_type(collection, bson_type, sample @pytest.mark.parametrize("test", pytest_params(ARGUMENT_FORM_TESTS)) def test_getClusterParameter_argument_forms(collection, test): """Test accepted argument forms each return ok:1 with expected clusterParameters length.""" - result = execute_admin_command(collection, test.build_command()) - assertProperties(result, test.build_checks(), msg=test.msg, raw_res=True) + result = execute_admin_command(collection, test.command) + assertProperties(result, test.checks, msg=test.msg, raw_res=True) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_core_behavior.py b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_core_behavior.py index 5e6cdaa96..10b317b59 100644 --- a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_core_behavior.py +++ b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_core_behavior.py @@ -37,5 +37,5 @@ @pytest.mark.parametrize("test", pytest_params(CORE_BEHAVIOR_TESTS)) def test_getClusterParameter_core_behavior(collection, test): """Test core retrieval behavior: wildcard parameter count and inclusion.""" - result = execute_admin_command(collection, test.build_command()) - assertProperties(result, test.build_checks(), msg=test.msg, raw_res=True) + result = execute_admin_command(collection, test.command) + assertProperties(result, test.checks, msg=test.msg, raw_res=True) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_errors.py b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_errors.py index a478241a4..3d5992c4f 100644 --- a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_errors.py +++ b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_errors.py @@ -122,7 +122,7 @@ @pytest.mark.parametrize("test", pytest_params(_ERROR_CASES)) def test_getClusterParameter_errors(collection, test): """Test all error-producing inputs: unknown names, type mismatches, and command routing.""" - result = execute_admin_command(collection, test.build_command()) + result = execute_admin_command(collection, test.command) assertFailureCode(result, test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_response_structure.py b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_response_structure.py index eced7ed4e..2cd37a385 100644 --- a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_response_structure.py +++ b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_response_structure.py @@ -50,5 +50,5 @@ @pytest.mark.parametrize("test", pytest_params(PROPERTY_TESTS)) def test_getClusterParameter_response_properties(collection, test): """Verifies getClusterParameter response fields have expected types and values.""" - result = execute_admin_command(collection, test.build_command()) - assertProperties(result, test.build_checks(), msg=test.msg, raw_res=True) + result = execute_admin_command(collection, test.command) + assertProperties(result, test.checks, msg=test.msg, raw_res=True) diff --git a/documentdb_tests/compatibility/tests/system/administration/utils/administration_test_case.py b/documentdb_tests/compatibility/tests/system/administration/utils/administration_test_case.py index 0bb87e753..b9fbbd13c 100644 --- a/documentdb_tests/compatibility/tests/system/administration/utils/administration_test_case.py +++ b/documentdb_tests/compatibility/tests/system/administration/utils/administration_test_case.py @@ -1,8 +1,7 @@ from __future__ import annotations -from collections.abc import Callable from dataclasses import dataclass, field -from typing import Any, Dict, List, Optional +from typing import Any, Dict, Optional from documentdb_tests.framework.test_case import BaseTestCase @@ -11,40 +10,10 @@ class AdministrationTestCase(BaseTestCase): """Test case for administration command tests. - Administration commands often require runtime-discovered values (e.g. a - valid cluster parameter name). Fields that need these values accept a - callable that receives the discovered value at execution time. - Attributes: - setup: Commands to run before the test command to establish state. - command: A callable ``(param: str) -> dict`` for commands that need a - runtime-discovered value, or a plain dict. - checks: Mapping of dotted field paths to property check objects, or a - callable that receives the same runtime values as ``build_command`` - and returns such a mapping. + command: The command dict to execute. + checks: Mapping of dotted field paths to property check objects. """ - setup: List[Dict[str, Any]] = field(default_factory=list) - command: Optional[Dict[str, Any] | Callable[..., Dict[str, Any]]] = None - checks: Dict[str, Any] | Callable[..., Dict[str, Any]] = field(default_factory=dict) - - def build_command(self, *args: Any, **kwargs: Any) -> Dict[str, Any]: - """Resolve the command dict from a callable or plain dict. - - Pass any runtime-discovered values as positional or keyword arguments; - they are forwarded to the callable unchanged. - """ - if self.command is None: - raise ValueError(f"AdministrationTestCase '{self.id}' has no command defined") - if callable(self.command): - return self.command(*args, **kwargs) - return self.command - - def build_checks(self, *args: Any, **kwargs: Any) -> Dict[str, Any]: - """Resolve the checks dict from a callable or plain dict. - - Pass the same runtime-discovered values used for ``build_command``. - """ - if callable(self.checks): - return self.checks(*args, **kwargs) - return self.checks + command: Optional[Dict[str, Any]] = None + checks: Dict[str, Any] = field(default_factory=dict) From 51a482ab4db4faa743125c3ac8699d126a8fbf33 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Tue, 30 Jun 2026 13:32:35 -0700 Subject: [PATCH 06/11] Added tests Signed-off-by: PatersonProjects --- .../arithmetic/add/test_add_date.py | 162 +++++++++++ .../arithmetic/add/test_add_errors.py | 203 ++++++++++++++ .../arithmetic/add/test_add_input_forms.py | 47 ++++ .../arithmetic/add/test_add_non_finite.py | 154 +++++++++++ .../arithmetic/add/test_add_null.py | 86 ++++++ .../arithmetic/add/test_add_numeric.py | 259 ++++++++++++++++++ .../arithmetic/add/test_add_overflow.py | 95 +++++++ .../arithmetic/add/test_add_precision.py | 103 +++++++ .../arithmetic/add/test_add_return_type.py | 114 ++++++++ 9 files changed, 1223 insertions(+) create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py new file mode 100644 index 000000000..7cd0f09f2 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py @@ -0,0 +1,162 @@ +from datetime import datetime, timedelta, timezone + +import pytest +from bson import Decimal128, Int64 + +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_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Date Arithmetic]: $add accepts exactly one date operand and one or more numeric +# operands (in milliseconds). The date may appear in any position. +ADD_DATE_NUMERIC_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "date_int32", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 86400000}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 2, tzinfo=timezone.utc), + msg="$add should add int32 milliseconds to a date", + ), + ExpressionTestCase( + "date_int64", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": Int64(86400000)}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 2, tzinfo=timezone.utc), + msg="$add should add int64 milliseconds to a date", + ), + ExpressionTestCase( + "date_decimal", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": Decimal128("1.5")}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, 0, 0, 0, 2000, tzinfo=timezone.utc), + msg="$add should round a decimal128 fractional millisecond value when adding to a date", + ), + ExpressionTestCase( + "date_double_round_up", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 2.5}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, 0, 0, 0, 3000, tzinfo=timezone.utc), + msg="$add should round up a double fractional millisecond value (.5) when adding to a date", + ), + ExpressionTestCase( + "date_double_truncates", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 4.4}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, 0, 0, 0, 4000, tzinfo=timezone.utc), + msg="$add should truncate a double fractional millisecond value (<.5) when adding to a date", # noqa: E501 + ), +] + +# Property [Date Rounding Boundaries]: $add rounds fractional millisecond offsets using +# round-half-away-from-zero. Values with |frac| < 0.5 truncate toward zero; values with +# |frac| >= 0.5 round away from zero. +ADD_DATE_ROUNDING_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "date_double_0_1", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 0.1}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, tzinfo=timezone.utc), + msg="$add should truncate 0.1ms and leave the date unchanged", + ), + ExpressionTestCase( + "date_double_0_49", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 0.49}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, tzinfo=timezone.utc), + msg="$add should truncate 0.49ms and leave the date unchanged", + ), + ExpressionTestCase( + "date_double_0_51", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 0.51}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, 0, 0, 0, 1000, tzinfo=timezone.utc), + msg="$add should round up 0.51ms to 1ms when adding to a date", + ), + ExpressionTestCase( + "date_double_0_6", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 0.6}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, 0, 0, 0, 1000, tzinfo=timezone.utc), + msg="$add should round up 0.6ms to 1ms when adding to a date", + ), + ExpressionTestCase( + "date_double_neg_0_5", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": -0.5}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, tzinfo=timezone.utc) - timedelta(milliseconds=1), + msg="$add should round -0.5ms away from zero to -1ms when adding to a date", + ), + ExpressionTestCase( + "date_double_neg_0_51", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": -0.51}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, tzinfo=timezone.utc) - timedelta(milliseconds=1), + msg="$add should round -0.51ms away from zero to -1ms when adding to a date", + ), +] + +# Property [Date Operand Position]: the date operand may appear in any position among the +# operands; only one date is permitted. +ADD_DATE_POSITION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "number_then_date", + doc={"a": 86400000, "b": datetime(2026, 1, 1, tzinfo=timezone.utc)}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 2, tzinfo=timezone.utc), + msg="$add should add a date when the numeric operand appears before the date", + ), + ExpressionTestCase( + "date_in_middle", + doc={"a": 1, "b": datetime(2026, 1, 1, tzinfo=timezone.utc), "c": 1000}, + expression={"$add": ["$a", "$b", "$c"]}, + expected=datetime(2026, 1, 1, 0, 0, 1, 1000, tzinfo=timezone.utc), + msg="$add should add a date when it appears in the middle of the operand list", + ), +] + +# Property [Date Sign Handling]: adding zero or a negative number of milliseconds to a date +# returns the date unchanged or subtracted. +ADD_DATE_SIGN_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "date_negative", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": -86400000}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2025, 12, 31, tzinfo=timezone.utc), + msg="$add should subtract milliseconds from a date when adding a negative number", + ), + ExpressionTestCase( + "date_zero", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 0}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, tzinfo=timezone.utc), + msg="$add should return the same date when adding zero milliseconds", + ), + ExpressionTestCase( + "date_negative_zero", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": -0.0}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, tzinfo=timezone.utc), + msg="$add should return the same date when adding negative zero", + ), +] + +ADD_DATE_ALL_TESTS = ( + ADD_DATE_NUMERIC_TESTS + ADD_DATE_POSITION_TESTS + ADD_DATE_SIGN_TESTS + ADD_DATE_ROUNDING_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(ADD_DATE_ALL_TESTS)) +def test_add_date(collection, test_case: ExpressionTestCase): + """Test $add date arithmetic: numeric types, operand position, and sign handling.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py new file mode 100644 index 000000000..f5c817b3d --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py @@ -0,0 +1,203 @@ +from datetime import datetime, timezone + +import pytest +from bson import Binary, Code, 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_with_insert, +) +from documentdb_tests.framework.error_codes import ( + MORE_THAN_ONE_DATE_ERROR, + OVERFLOW_ERROR, + TYPE_MISMATCH_ERROR, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_INFINITY, + DECIMAL128_NAN, + FLOAT_INFINITY, + FLOAT_NAN, + INT64_MAX, +) + +# Property [Type Strictness]: $add rejects non-numeric, non-date operand types. +ADD_TYPE_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + f"type_{tid}", + doc={"a": 1, "b": val}, + expression={"$add": ["$a", "$b"]}, + error_code=TYPE_MISMATCH_ERROR, + msg=f"$add should reject a {tid} operand", + ) + for tid, val in [ + ("string", "string"), + ("bool", True), + ("array", [2, 3]), + ("object", {"a": 2}), + ("regex", Regex("abc")), + ("objectid", ObjectId("507f1f77bcf86cd799439011")), + ("binary", Binary(b"data")), + ("minkey", MinKey()), + ("maxkey", MaxKey()), + ("timestamp", Timestamp(1, 1)), + ("code", Code("function(){}")), + ] +] + +# Property [Mixed Valid and Invalid]: $add rejects an invalid operand when it appears among +# valid numeric operands. +ADD_MIXED_VALID_INVALID_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "mixed_valid_invalid", + doc={"a": 1, "b": 2, "c": "string"}, + expression={"$add": ["$a", "$b", "$c"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$add should error when a string appears among numeric operands", + ), +] + +# Property [Single Invalid Operand]: $add rejects a single operand of an invalid type. +ADD_SINGLE_TYPE_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "single_string", + doc={"a": "string"}, + expression={"$add": ["$a"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$add should reject a single string operand", + ), + ExpressionTestCase( + "single_boolean", + doc={"a": True}, + expression={"$add": ["$a"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$add should reject a single boolean operand", + ), + ExpressionTestCase( + "single_array", + doc={"a": [1, 2]}, + expression={"$add": ["$a"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$add should reject a single array operand", + ), + ExpressionTestCase( + "single_object", + doc={"a": {"x": 1}}, + expression={"$add": ["$a"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$add should reject a single object operand", + ), +] + +# Property [Multiple Dates]: $add rejects expressions with more than one date operand. +ADD_MULTIPLE_DATE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "add_two_identical_dates", + doc={ + "a": datetime(2026, 1, 1, tzinfo=timezone.utc), + "b": datetime(2026, 1, 1, tzinfo=timezone.utc), + }, + expression={"$add": ["$a", "$b"]}, + error_code=MORE_THAN_ONE_DATE_ERROR, + msg="$add should error when adding two identical date operands", + ), + ExpressionTestCase( + "two_different_dates", + doc={ + "a": datetime(2026, 1, 1, tzinfo=timezone.utc), + "b": datetime(2026, 1, 2, tzinfo=timezone.utc), + }, + expression={"$add": ["$a", "$b"]}, + error_code=MORE_THAN_ONE_DATE_ERROR, + msg="$add should error when adding two different date operands", + ), + ExpressionTestCase( + "two_dates_with_numbers", + doc={ + "a": datetime(2026, 1, 1, tzinfo=timezone.utc), + "b": datetime(2026, 1, 2, tzinfo=timezone.utc), + }, + expression={"$add": [1, 2, 3, "$a", "$b"]}, + error_code=MORE_THAN_ONE_DATE_ERROR, + msg="$add should error when two dates appear among numeric operands", + ), + ExpressionTestCase( + "dates_separated_by_number", + doc={ + "a": datetime(2026, 1, 1, tzinfo=timezone.utc), + "b": datetime(2026, 1, 2, tzinfo=timezone.utc), + }, + expression={"$add": ["$a", 1, "$b"]}, + error_code=MORE_THAN_ONE_DATE_ERROR, + msg="$add should error when two dates are separated by a numeric operand", + ), +] + +# Property [Date with Non-Finite]: $add rejects NaN and Infinity as numeric operands when a +# date is also present, since the resulting date would be non-representable. +ADD_DATE_NON_FINITE_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "date_nan", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": FLOAT_NAN}, + expression={"$add": ["$a", "$b"]}, + error_code=OVERFLOW_ERROR, + msg="$add should error when adding a date and float NaN", + ), + ExpressionTestCase( + "date_infinity", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": FLOAT_INFINITY}, + expression={"$add": ["$a", "$b"]}, + error_code=OVERFLOW_ERROR, + msg="$add should error when adding a date and float infinity", + ), + ExpressionTestCase( + "date_decimal_nan", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": DECIMAL128_NAN}, + expression={"$add": ["$a", "$b"]}, + error_code=OVERFLOW_ERROR, + msg="$add should error when adding a date and decimal128 NaN", + ), + ExpressionTestCase( + "date_decimal_infinity", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": DECIMAL128_INFINITY}, + expression={"$add": ["$a", "$b"]}, + error_code=OVERFLOW_ERROR, + msg="$add should error when adding a date and decimal128 infinity", + ), +] + +# Property [Date Overflow]: $add errors when the millisecond offset would push the date result +# beyond the representable date range. +ADD_DATE_OVERFLOW_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "date_int64_max", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": INT64_MAX}, + expression={"$add": ["$a", "$b"]}, + error_code=OVERFLOW_ERROR, + msg="$add should error when adding INT64_MAX milliseconds to a date overflows the date range", # noqa: E501 + ), +] + +ADD_ERROR_ALL_TESTS = ( + ADD_TYPE_ERROR_TESTS + + ADD_MIXED_VALID_INVALID_TESTS + + ADD_SINGLE_TYPE_ERROR_TESTS + + ADD_MULTIPLE_DATE_TESTS + + ADD_DATE_NON_FINITE_ERROR_TESTS + + ADD_DATE_OVERFLOW_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(ADD_ERROR_ALL_TESTS)) +def test_add_errors(collection, test_case: ExpressionTestCase): + """Test $add type, multiple-date, and date non-finite error cases.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py new file mode 100644 index 000000000..7019c1b0b --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py @@ -0,0 +1,47 @@ +import pytest + +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_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Expression Input]: $add evaluates a nested expression argument before summing. +ADD_EXPRESSION_INPUT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "nested_add", + doc={"a": 1, "b": 2}, + expression={"$add": [{"$add": ["$a", "$b"]}, 3]}, + expected=6, + msg="$add should evaluate a nested $add expression as an operand", + ), +] + +# Property [Mixed Literal and Field]: $add accepts a mix of field references and inline literals +# in the same operand list. +ADD_MIXED_INPUT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "mixed_literal_and_field", + doc={"a": 10}, + expression={"$add": ["$a", 5]}, + expected=15, + msg="$add should sum a field reference and an inline literal operand", + ), +] + +ADD_INPUT_FORM_ALL_TESTS = ADD_EXPRESSION_INPUT_TESTS + ADD_MIXED_INPUT_TESTS + + +@pytest.mark.parametrize("test_case", pytest_params(ADD_INPUT_FORM_ALL_TESTS)) +def test_add_input_forms(collection, test_case: ExpressionTestCase): + """Test $add literal, nested expression, and mixed input form cases.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py new file mode 100644 index 000000000..0343c3316 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py @@ -0,0 +1,154 @@ +import math + +import pytest + +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_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_INFINITY, + DECIMAL128_NAN, + DECIMAL128_NEGATIVE_INFINITY, + FLOAT_INFINITY, + FLOAT_NAN, + FLOAT_NEGATIVE_INFINITY, +) + +# Property [Infinity]: $add propagates infinity according to IEEE 754 rules. +ADD_INFINITY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "infinity", + doc={"a": FLOAT_INFINITY, "b": 1}, + expression={"$add": ["$a", "$b"]}, + expected=FLOAT_INFINITY, + msg="$add should return infinity when adding infinity and a finite number", + ), + ExpressionTestCase( + "negative_infinity", + doc={"a": FLOAT_NEGATIVE_INFINITY, "b": 1}, + expression={"$add": ["$a", "$b"]}, + expected=FLOAT_NEGATIVE_INFINITY, + msg="$add should return negative infinity when adding negative infinity and a finite number", # noqa: E501 + ), + ExpressionTestCase( + "single_infinity", + doc={"a": FLOAT_INFINITY}, + expression={"$add": ["$a"]}, + expected=FLOAT_INFINITY, + msg="$add should return infinity for a single infinity operand", + ), + ExpressionTestCase( + "inf_plus_inf", + doc={"a": FLOAT_INFINITY, "b": FLOAT_INFINITY}, + expression={"$add": ["$a", "$b"]}, + expected=FLOAT_INFINITY, + msg="$add should return infinity when adding two positive infinities", + ), + ExpressionTestCase( + "neg_inf_plus_neg_inf", + doc={"a": FLOAT_NEGATIVE_INFINITY, "b": FLOAT_NEGATIVE_INFINITY}, + expression={"$add": ["$a", "$b"]}, + expected=FLOAT_NEGATIVE_INFINITY, + msg="$add should return negative infinity when adding two negative infinities", + ), + ExpressionTestCase( + "inf_plus_zero", + doc={"a": FLOAT_INFINITY, "b": 0}, + expression={"$add": ["$a", "$b"]}, + expected=FLOAT_INFINITY, + msg="$add should return infinity when adding infinity and zero", + ), + ExpressionTestCase( + "neg_inf_plus_zero", + doc={"a": FLOAT_NEGATIVE_INFINITY, "b": 0}, + expression={"$add": ["$a", "$b"]}, + expected=FLOAT_NEGATIVE_INFINITY, + msg="$add should return negative infinity when adding negative infinity and zero", + ), + ExpressionTestCase( + "decimal_infinity", + doc={"a": DECIMAL128_INFINITY, "b": 1}, + expression={"$add": ["$a", "$b"]}, + expected=DECIMAL128_INFINITY, + msg="$add should return decimal128 infinity when adding decimal128 infinity and a number", + ), + ExpressionTestCase( + "decimal_negative_infinity", + doc={"a": DECIMAL128_NEGATIVE_INFINITY, "b": 1}, + expression={"$add": ["$a", "$b"]}, + expected=DECIMAL128_NEGATIVE_INFINITY, + msg="$add should return decimal128 negative infinity when adding decimal128 negative infinity and a number", # noqa: E501 + ), +] + +# Property [NaN]: $add propagates NaN according to IEEE 754 rules. +ADD_NAN_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "nan_add_one", + doc={"a": FLOAT_NAN, "b": 1}, + expression={"$add": ["$a", "$b"]}, + expected=pytest.approx(math.nan, nan_ok=True), + msg="$add should return NaN when adding float NaN and a finite number", + ), + ExpressionTestCase( + "inf_minus_inf", + doc={"a": FLOAT_INFINITY, "b": FLOAT_NEGATIVE_INFINITY}, + expression={"$add": ["$a", "$b"]}, + expected=pytest.approx(math.nan, nan_ok=True), + msg="$add should return NaN when adding float infinity and negative infinity", + ), + ExpressionTestCase( + "nan_plus_nan", + doc={"a": FLOAT_NAN, "b": FLOAT_NAN}, + expression={"$add": ["$a", "$b"]}, + expected=pytest.approx(math.nan, nan_ok=True), + msg="$add should return NaN when adding two float NaN values", + ), + ExpressionTestCase( + "nan_plus_inf", + doc={"a": FLOAT_NAN, "b": FLOAT_INFINITY}, + expression={"$add": ["$a", "$b"]}, + expected=pytest.approx(math.nan, nan_ok=True), + msg="$add should return NaN when adding float NaN and infinity", + ), + ExpressionTestCase( + "decimal_nan", + doc={"a": DECIMAL128_NAN, "b": 1}, + expression={"$add": ["$a", "$b"]}, + expected=DECIMAL128_NAN, + msg="$add should return decimal128 NaN when adding decimal128 NaN and a number", + ), + ExpressionTestCase( + "decimal_nan_plus_nan", + doc={"a": DECIMAL128_NAN, "b": DECIMAL128_NAN}, + expression={"$add": ["$a", "$b"]}, + expected=DECIMAL128_NAN, + msg="$add should return decimal128 NaN when adding two decimal128 NaN values", + ), + ExpressionTestCase( + "decimal_inf_minus_inf", + doc={"a": DECIMAL128_INFINITY, "b": DECIMAL128_NEGATIVE_INFINITY}, + expression={"$add": ["$a", "$b"]}, + expected=DECIMAL128_NAN, + msg="$add should return decimal128 NaN when adding decimal128 infinity and negative infinity", # noqa: E501 + ), +] + +ADD_NON_FINITE_ALL_TESTS = ADD_INFINITY_TESTS + ADD_NAN_TESTS + + +@pytest.mark.parametrize("test_case", pytest_params(ADD_NON_FINITE_ALL_TESTS)) +def test_add_non_finite(collection, test_case: ExpressionTestCase): + """Test $add infinity and NaN propagation cases.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py new file mode 100644 index 000000000..9651d52d3 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py @@ -0,0 +1,86 @@ +from datetime import datetime, timezone + +import pytest + +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_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import MISSING + +# Property [Null Propagation]: $add returns null if any operand is null or refers to a missing +# field. +ADD_NULL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "single_null", + doc={"a": None}, + expression={"$add": ["$a"]}, + expected=None, + msg="$add should return null for a single null operand", + ), + ExpressionTestCase( + "null_operand", + doc={"a": 1, "b": None}, + expression={"$add": ["$a", "$b"]}, + expected=None, + msg="$add should return null when any operand is null", + ), + ExpressionTestCase( + "missing_field", + doc={}, + expression={"$add": [1, MISSING]}, + expected=None, + msg="$add should return null when any operand is a missing field", + ), + ExpressionTestCase( + "null_with_multiple", + doc={"a": 1, "b": 2, "c": None}, + expression={"$add": ["$a", "$b", "$c"]}, + expected=None, + msg="$add should return null when null appears among multiple operands", + ), + ExpressionTestCase( + "null_in_middle", + doc={"a": 1, "b": 2, "c": 3, "e": 5}, + expression={"$add": ["$a", "$b", "$c", None, "$e"]}, + expected=None, + msg="$add should return null when null appears in the middle of operands", + ), + ExpressionTestCase( + "all_null", + doc={"a": None, "b": None}, + expression={"$add": ["$a", "$b"]}, + expected=None, + msg="$add should return null when all operands are null", + ), + ExpressionTestCase( + "all_missing", + doc={}, + expression={"$add": [MISSING, MISSING]}, + expected=None, + msg="$add should return null when all operands are missing fields", + ), + ExpressionTestCase( + "date_and_null", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": None}, + expression={"$add": ["$a", "$b"]}, + expected=None, + msg="$add should return null when a date is paired with a null operand", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(ADD_NULL_TESTS)) +def test_add_null(collection, test_case: ExpressionTestCase): + """Test $add null and missing field propagation cases.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py new file mode 100644 index 000000000..3c4d6f305 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py @@ -0,0 +1,259 @@ +import pytest +from bson import Decimal128, Int64 + +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_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Same-Type Addition]: $add of two values of the same numeric type returns a value of +# that type. +ADD_SAME_TYPE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "same_type_int32", + doc={"a": 1, "b": 2}, + expression={"$add": ["$a", "$b"]}, + expected=3, + msg="$add should add two int32 values", + ), + ExpressionTestCase( + "same_type_int64", + doc={"a": Int64(10), "b": Int64(20)}, + expression={"$add": ["$a", "$b"]}, + expected=Int64(30), + msg="$add should add two int64 values", + ), + ExpressionTestCase( + "same_type_double", + doc={"a": 1.5, "b": 2.5}, + expression={"$add": ["$a", "$b"]}, + expected=4.0, + msg="$add should add two double values", + ), + ExpressionTestCase( + "same_type_decimal", + doc={"a": Decimal128("10.5"), "b": Decimal128("20.5")}, + expression={"$add": ["$a", "$b"]}, + expected=Decimal128("31.0"), + msg="$add should add two decimal128 values", + ), +] + +# Property [Type Promotion]: $add promotes to the wider type when operands have different numeric +# types. Precedence: decimal128 > double > int64 > int32. +ADD_MIXED_TYPE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "int32_int64", + doc={"a": 1, "b": Int64(20)}, + expression={"$add": ["$a", "$b"]}, + expected=Int64(21), + msg="$add should return int64 when adding int32 and int64", + ), + ExpressionTestCase( + "int32_double", + doc={"a": 1, "b": 2.5}, + expression={"$add": ["$a", "$b"]}, + expected=3.5, + msg="$add should return double when adding int32 and double", + ), + ExpressionTestCase( + "int32_decimal", + doc={"a": 1, "b": Decimal128("2.5")}, + expression={"$add": ["$a", "$b"]}, + expected=Decimal128("3.5"), + msg="$add should return decimal128 when adding int32 and decimal128", + ), + ExpressionTestCase( + "int64_double", + doc={"a": Int64(10), "b": 2.5}, + expression={"$add": ["$a", "$b"]}, + expected=12.5, + msg="$add should return double when adding int64 and double", + ), + ExpressionTestCase( + "int64_decimal", + doc={"a": Int64(10), "b": Decimal128("2.5")}, + expression={"$add": ["$a", "$b"]}, + expected=Decimal128("12.5"), + msg="$add should return decimal128 when adding int64 and decimal128", + ), + ExpressionTestCase( + "double_decimal", + doc={"a": 1.5, "b": Decimal128("2.5")}, + expression={"$add": ["$a", "$b"]}, + expected=pytest.approx(Decimal128("4.00000000000000")), + msg="$add should return decimal128 when adding double and decimal128", + ), + ExpressionTestCase( + "three_mixed_types", + doc={"a": Decimal128("1.5"), "b": 2.5, "c": Int64(3)}, + expression={"$add": ["$a", "$b", "$c"]}, + expected=pytest.approx(Decimal128("7.00000000000000")), + msg="$add should return decimal128 when adding decimal128, double, and int64", + ), +] + +# Property [Multiple Operands]: $add correctly sums three or more operands. +ADD_MULTIPLE_OPERANDS_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "multiple_int32", + doc={"a": 1, "b": 2, "c": 3}, + expression={"$add": ["$a", "$b", "$c"]}, + expected=6, + msg="$add should add multiple int32 values", + ), + ExpressionTestCase( + "multiple_int64", + doc={"a": Int64(1), "b": Int64(2), "c": Int64(3)}, + expression={"$add": ["$a", "$b", "$c"]}, + expected=Int64(6), + msg="$add should add multiple int64 values", + ), + ExpressionTestCase( + "multiple_double", + doc={"a": 1.1, "b": 2.2, "c": 3.3}, + expression={"$add": ["$a", "$b", "$c"]}, + expected=pytest.approx(6.6), + msg="$add should add multiple double values", + ), + ExpressionTestCase( + "multiple_decimal", + doc={ + "a": Decimal128("1"), + "b": Decimal128("2"), + "c": Decimal128("3"), + "d": Decimal128("4"), + }, + expression={"$add": ["$a", "$b", "$c", "$d"]}, + expected=Decimal128("10"), + msg="$add should add multiple decimal128 values", + ), + ExpressionTestCase( + "five_operands", + doc={"a": 1, "b": 2, "c": 3, "d": 4, "e": 5}, + expression={"$add": ["$a", "$b", "$c", "$d", "$e"]}, + expected=15, + msg="$add should correctly sum five int32 operands", + ), + ExpressionTestCase( + "ten_operands", + doc={ + "a": 1, + "b": 2, + "c": 3, + "d": 4, + "e": 5, + "f": 6, + "g": 7, + "h": 8, + "i": 9, + "j": 10, + }, + expression={"$add": ["$a", "$b", "$c", "$d", "$e", "$f", "$g", "$h", "$i", "$j"]}, + expected=55, + msg="$add should correctly sum ten int32 operands", + ), +] + +# Property [Empty and Single Operand]: $add of zero operands returns 0; single operand returns +# that value unchanged. +ADD_SINGLE_AND_EMPTY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "empty", + doc={}, + expression={"$add": []}, + expected=0, + msg="$add should return 0 for empty operand list", + ), + ExpressionTestCase( + "single_int32", + doc={"a": 5}, + expression={"$add": ["$a"]}, + expected=5, + msg="$add should return the value for a single int32 operand", + ), + ExpressionTestCase( + "single_int64", + doc={"a": Int64(0)}, + expression={"$add": ["$a"]}, + expected=Int64(0), + msg="$add should return the value for a single int64 operand", + ), + ExpressionTestCase( + "single_double", + doc={"a": 0.0}, + expression={"$add": ["$a"]}, + expected=0.0, + msg="$add should return the value for a single double operand", + ), + ExpressionTestCase( + "single_decimal", + doc={"a": Decimal128("0")}, + expression={"$add": ["$a"]}, + expected=Decimal128("0"), + msg="$add should return the value for a single decimal128 operand", + ), +] + +# Property [Sign Handling]: $add handles negative values and zero correctly. +ADD_SIGN_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "negative_positive", + doc={"a": -5, "b": 3}, + expression={"$add": ["$a", "$b"]}, + expected=-2, + msg="$add should add a negative and a positive int32 value", + ), + ExpressionTestCase( + "both_negative", + doc={"a": -10, "b": -20}, + expression={"$add": ["$a", "$b"]}, + expected=-30, + msg="$add should add two negative int32 values", + ), + ExpressionTestCase( + "zeros", + doc={"a": 0, "b": 0}, + expression={"$add": ["$a", "$b"]}, + expected=0, + msg="$add should return 0 when adding two int32 zeros", + ), + ExpressionTestCase( + "zero_negative_zero", + doc={"a": 0, "b": -0.0}, + expression={"$add": ["$a", "$b"]}, + expected=0.0, + msg="$add should return 0.0 when adding int32 zero and negative zero double", + ), + ExpressionTestCase( + "sum_to_zero", + doc={"a": 1, "b": 0, "c": -1}, + expression={"$add": ["$a", "$b", "$c"]}, + expected=0, + msg="$add should return 0 when operands sum to zero", + ), +] + +ADD_NUMERIC_ALL_TESTS = ( + ADD_SAME_TYPE_TESTS + + ADD_MIXED_TYPE_TESTS + + ADD_MULTIPLE_OPERANDS_TESTS + + ADD_SINGLE_AND_EMPTY_TESTS + + ADD_SIGN_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(ADD_NUMERIC_ALL_TESTS)) +def test_add_numeric(collection, test_case: ExpressionTestCase): + """Test $add numeric type combinations, multiple operands, and sign handling.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py new file mode 100644 index 000000000..29a992905 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py @@ -0,0 +1,95 @@ +import pytest +from bson import Int64 + +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_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DOUBLE_FROM_INT64_MAX, + FLOAT_INFINITY, + FLOAT_NEGATIVE_INFINITY, + INT32_MAX, + INT32_MIN, + INT32_OVERFLOW, + INT32_UNDERFLOW, + INT64_MAX, + INT64_MIN, +) + +# Property [Int32 Overflow]: when an int32 result exceeds the int32 range, $add promotes to +# int64. +ADD_INT32_OVERFLOW_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "int32_overflow", + doc={"a": INT32_MAX, "b": 1}, + expression={"$add": ["$a", "$b"]}, + expected=Int64(INT32_OVERFLOW), + msg="$add should promote to int64 when the int32 result overflows INT32_MAX", + ), + ExpressionTestCase( + "int32_underflow", + doc={"a": INT32_MIN, "b": -1}, + expression={"$add": ["$a", "$b"]}, + expected=Int64(INT32_UNDERFLOW), + msg="$add should promote to int64 when the int32 result underflows INT32_MIN", + ), +] + +# Property [Int64 Overflow]: when an int64 result exceeds the int64 range, $add promotes to +# double. +ADD_INT64_OVERFLOW_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "int64_overflow", + doc={"a": INT64_MAX, "b": 1}, + expression={"$add": ["$a", "$b"]}, + expected=pytest.approx(DOUBLE_FROM_INT64_MAX), + msg="$add should promote to double when the int64 result overflows INT64_MAX", + ), + ExpressionTestCase( + "int64_underflow", + doc={"a": INT64_MIN, "b": -1}, + expression={"$add": ["$a", "$b"]}, + expected=pytest.approx(-DOUBLE_FROM_INT64_MAX), + msg="$add should promote to double when the int64 result underflows INT64_MIN", + ), +] + +# Property [Double Overflow]: when a double result exceeds the double range, $add returns +# infinity. +ADD_DOUBLE_OVERFLOW_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "double_overflow", + doc={"a": 1e308, "b": 1e308}, + expression={"$add": ["$a", "$b"]}, + expected=FLOAT_INFINITY, + msg="$add should return positive infinity on double overflow", + ), + ExpressionTestCase( + "double_underflow", + doc={"a": -1e308, "b": -1e308}, + expression={"$add": ["$a", "$b"]}, + expected=FLOAT_NEGATIVE_INFINITY, + msg="$add should return negative infinity on double underflow", + ), +] + +ADD_OVERFLOW_ALL_TESTS = ( + ADD_INT32_OVERFLOW_TESTS + ADD_INT64_OVERFLOW_TESTS + ADD_DOUBLE_OVERFLOW_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(ADD_OVERFLOW_ALL_TESTS)) +def test_add_overflow(collection, test_case: ExpressionTestCase): + """Test $add integer and double overflow and underflow cases.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py new file mode 100644 index 000000000..f0cdd34ab --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py @@ -0,0 +1,103 @@ +import pytest +from bson import Decimal128 + +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_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_INFINITY, + DECIMAL128_MAX, + DECIMAL128_MIN, + DECIMAL128_NEGATIVE_INFINITY, +) + +# Property [Decimal128 Precision]: $add preserves decimal128 precision, including exact +# representation of values that are inexact in double. +ADD_DECIMAL_PRECISION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "decimal_precision", + doc={"a": Decimal128("1.5"), "b": Decimal128("2.5")}, + expression={"$add": ["$a", "$b"]}, + expected=Decimal128("4.0"), + msg="$add should preserve decimal128 precision", + ), + ExpressionTestCase( + "decimal_precision_small", + doc={"a": Decimal128("0.1"), "b": Decimal128("0.2")}, + expression={"$add": ["$a", "$b"]}, + expected=Decimal128("0.3"), + msg="$add should exactly represent 0.1 + 0.2 with decimal128", + ), + ExpressionTestCase( + "decimal_large_precision", + doc={ + "a": Decimal128("999999999999999999999999999999999"), + "b": Decimal128("1"), + }, + expression={"$add": ["$a", "$b"]}, + expected=Decimal128("1000000000000000000000000000000000"), + msg="$add should handle large decimal128 addition with full precision", + ), + ExpressionTestCase( + "decimal_large_negative_precision", + doc={ + "a": Decimal128("-999999999999999999999999999999999"), + "b": Decimal128("-1"), + }, + expression={"$add": ["$a", "$b"]}, + expected=Decimal128("-1000000000000000000000000000000000"), + msg="$add should handle large negative decimal128 addition with full precision", + ), +] + +# Property [Decimal128 Boundaries]: $add at decimal128 boundary values promotes to infinity when +# the result overflows, and returns zero when max and min cancel. +ADD_DECIMAL_BOUNDARY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "decimal128_max_plus_zero", + doc={"a": DECIMAL128_MAX, "b": Decimal128("0")}, + expression={"$add": ["$a", "$b"]}, + expected=DECIMAL128_MAX, + msg="$add should return decimal128 max when adding zero to decimal128 max", + ), + ExpressionTestCase( + "decimal128_max_plus_max", + doc={"a": DECIMAL128_MAX, "b": DECIMAL128_MAX}, + expression={"$add": ["$a", "$b"]}, + expected=DECIMAL128_INFINITY, + msg="$add should return decimal128 infinity when adding two decimal128 max values", + ), + ExpressionTestCase( + "decimal128_min_plus_min", + doc={"a": DECIMAL128_MIN, "b": DECIMAL128_MIN}, + expression={"$add": ["$a", "$b"]}, + expected=DECIMAL128_NEGATIVE_INFINITY, + msg="$add should return decimal128 negative infinity when adding two decimal128 min values", + ), + ExpressionTestCase( + "decimal128_max_plus_min", + doc={"a": DECIMAL128_MAX, "b": DECIMAL128_MIN}, + expression={"$add": ["$a", "$b"]}, + expected=Decimal128("0E+6111"), + msg="$add should return zero when adding decimal128 max and decimal128 min", + ), +] + +ADD_PRECISION_ALL_TESTS = ADD_DECIMAL_PRECISION_TESTS + ADD_DECIMAL_BOUNDARY_TESTS + + +@pytest.mark.parametrize("test_case", pytest_params(ADD_PRECISION_ALL_TESTS)) +def test_add_precision(collection, test_case: ExpressionTestCase): + """Test $add decimal128 precision and boundary value cases.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py new file mode 100644 index 000000000..7af9e0b66 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py @@ -0,0 +1,114 @@ +from datetime import datetime, timezone + +import pytest +from bson import Decimal128, Int64 + +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_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Return Type]: $add follows numeric type promotion rules to determine the result type. +# Precedence: decimal128 > double > int64 > int32. Date + numeric always returns date. +ADD_RETURN_TYPE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "return_type_int_int", + doc={"a": 1, "b": 2}, + expression={"$type": {"$add": ["$a", "$b"]}}, + expected="int", + msg="$add should return int type when adding two int32 values", + ), + ExpressionTestCase( + "return_type_int_long", + doc={"a": 1, "b": Int64(2)}, + expression={"$type": {"$add": ["$a", "$b"]}}, + expected="long", + msg="$add should return long type when adding int32 and int64", + ), + ExpressionTestCase( + "return_type_int_double", + doc={"a": 1, "b": 2.0}, + expression={"$type": {"$add": ["$a", "$b"]}}, + expected="double", + msg="$add should return double type when adding int32 and double", + ), + ExpressionTestCase( + "return_type_int_decimal", + doc={"a": 1, "b": Decimal128("2")}, + expression={"$type": {"$add": ["$a", "$b"]}}, + expected="decimal", + msg="$add should return decimal type when adding int32 and decimal128", + ), + ExpressionTestCase( + "return_type_long_long", + doc={"a": Int64(1), "b": Int64(2)}, + expression={"$type": {"$add": ["$a", "$b"]}}, + expected="long", + msg="$add should return long type when adding two int64 values", + ), + ExpressionTestCase( + "return_type_long_double", + doc={"a": Int64(1), "b": 2.0}, + expression={"$type": {"$add": ["$a", "$b"]}}, + expected="double", + msg="$add should return double type when adding int64 and double", + ), + ExpressionTestCase( + "return_type_long_decimal", + doc={"a": Int64(1), "b": Decimal128("2")}, + expression={"$type": {"$add": ["$a", "$b"]}}, + expected="decimal", + msg="$add should return decimal type when adding int64 and decimal128", + ), + ExpressionTestCase( + "return_type_double_double", + doc={"a": 1.0, "b": 2.0}, + expression={"$type": {"$add": ["$a", "$b"]}}, + expected="double", + msg="$add should return double type when adding two double values", + ), + ExpressionTestCase( + "return_type_double_decimal", + doc={"a": 1.0, "b": Decimal128("2")}, + expression={"$type": {"$add": ["$a", "$b"]}}, + expected="decimal", + msg="$add should return decimal type when adding double and decimal128", + ), + ExpressionTestCase( + "return_type_decimal_decimal", + doc={"a": Decimal128("1"), "b": Decimal128("2")}, + expression={"$type": {"$add": ["$a", "$b"]}}, + expected="decimal", + msg="$add should return decimal type when adding two decimal128 values", + ), + ExpressionTestCase( + "return_type_date_int", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 1000}, + expression={"$type": {"$add": ["$a", "$b"]}}, + expected="date", + msg="$add should return date type when adding a date and an int32", + ), + ExpressionTestCase( + "return_type_empty", + doc={}, + expression={"$type": {"$add": []}}, + expected="int", + msg="$add should return int type for an empty operand list", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(ADD_RETURN_TYPE_TESTS)) +def test_add_return_type(collection, test_case: ExpressionTestCase): + """Test $add return type promotion rules for all numeric type combinations.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) From 014f2bceb8c06ba36a9e652a57ac7d81397bd67e Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Tue, 30 Jun 2026 13:40:50 -0700 Subject: [PATCH 07/11] Added docStrings to all files Signed-off-by: PatersonProjects --- .../core/operator/expressions/arithmetic/add/test_add_date.py | 4 ++++ .../operator/expressions/arithmetic/add/test_add_errors.py | 2 ++ .../expressions/arithmetic/add/test_add_input_forms.py | 2 ++ .../expressions/arithmetic/add/test_add_non_finite.py | 2 ++ .../core/operator/expressions/arithmetic/add/test_add_null.py | 2 ++ .../operator/expressions/arithmetic/add/test_add_numeric.py | 4 ++++ .../operator/expressions/arithmetic/add/test_add_overflow.py | 2 ++ .../operator/expressions/arithmetic/add/test_add_precision.py | 2 ++ .../expressions/arithmetic/add/test_add_return_type.py | 2 ++ 9 files changed, 22 insertions(+) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py index 7cd0f09f2..eff8f4af1 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py @@ -1,3 +1,7 @@ +"""Tests for $add date arithmetic including numeric offsets, rounding boundaries, operand +position, and sign handling. +""" + from datetime import datetime, timedelta, timezone import pytest diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py index f5c817b3d..127f0e5de 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py @@ -1,3 +1,5 @@ +"""Tests for $add error cases including invalid operand types, multiple dates, and date overflow.""" + from datetime import datetime, timezone import pytest diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py index 7019c1b0b..fa2f73df4 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py @@ -1,3 +1,5 @@ +"""Tests for $add input forms including nested expressions and mixed literal/field operands.""" + import pytest from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py index 0343c3316..958818e98 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py @@ -1,3 +1,5 @@ +"""Tests for $add infinity and NaN propagation.""" + import math import pytest diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py index 9651d52d3..3854d72d8 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py @@ -1,3 +1,5 @@ +"""Tests for $add null and missing field propagation.""" + from datetime import datetime, timezone import pytest diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py index 3c4d6f305..971b01581 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py @@ -1,3 +1,7 @@ +"""Tests for $add numeric operations including same-type and mixed-type addition, multiple +operands, empty/single operands, and sign handling. +""" + import pytest from bson import Decimal128, Int64 diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py index 29a992905..2da84306e 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py @@ -1,3 +1,5 @@ +"""Tests for $add integer and double overflow and underflow promotion.""" + import pytest from bson import Int64 diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py index f0cdd34ab..6658eb03a 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py @@ -1,3 +1,5 @@ +"""Tests for $add decimal128 precision and boundary value handling.""" + import pytest from bson import Decimal128 diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py index 7af9e0b66..d681c7fd2 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py @@ -1,3 +1,5 @@ +"""Tests for $add return type promotion rules across numeric and date operand combinations.""" + from datetime import datetime, timezone import pytest From 37407ae57c9912cf17ff31f5885fafea01f7db38 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Tue, 30 Jun 2026 13:47:01 -0700 Subject: [PATCH 08/11] Revert "Added docStrings to all files" This reverts commit 014f2bceb8c06ba36a9e652a57ac7d81397bd67e. Signed-off-by: PatersonProjects --- .../core/operator/expressions/arithmetic/add/test_add_date.py | 4 ---- .../operator/expressions/arithmetic/add/test_add_errors.py | 2 -- .../expressions/arithmetic/add/test_add_input_forms.py | 2 -- .../expressions/arithmetic/add/test_add_non_finite.py | 2 -- .../core/operator/expressions/arithmetic/add/test_add_null.py | 2 -- .../operator/expressions/arithmetic/add/test_add_numeric.py | 4 ---- .../operator/expressions/arithmetic/add/test_add_overflow.py | 2 -- .../operator/expressions/arithmetic/add/test_add_precision.py | 2 -- .../expressions/arithmetic/add/test_add_return_type.py | 2 -- 9 files changed, 22 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py index eff8f4af1..7cd0f09f2 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py @@ -1,7 +1,3 @@ -"""Tests for $add date arithmetic including numeric offsets, rounding boundaries, operand -position, and sign handling. -""" - from datetime import datetime, timedelta, timezone import pytest diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py index 127f0e5de..f5c817b3d 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py @@ -1,5 +1,3 @@ -"""Tests for $add error cases including invalid operand types, multiple dates, and date overflow.""" - from datetime import datetime, timezone import pytest diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py index fa2f73df4..7019c1b0b 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py @@ -1,5 +1,3 @@ -"""Tests for $add input forms including nested expressions and mixed literal/field operands.""" - import pytest from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py index 958818e98..0343c3316 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py @@ -1,5 +1,3 @@ -"""Tests for $add infinity and NaN propagation.""" - import math import pytest diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py index 3854d72d8..9651d52d3 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py @@ -1,5 +1,3 @@ -"""Tests for $add null and missing field propagation.""" - from datetime import datetime, timezone import pytest diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py index 971b01581..3c4d6f305 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py @@ -1,7 +1,3 @@ -"""Tests for $add numeric operations including same-type and mixed-type addition, multiple -operands, empty/single operands, and sign handling. -""" - import pytest from bson import Decimal128, Int64 diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py index 2da84306e..29a992905 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py @@ -1,5 +1,3 @@ -"""Tests for $add integer and double overflow and underflow promotion.""" - import pytest from bson import Int64 diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py index 6658eb03a..f0cdd34ab 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py @@ -1,5 +1,3 @@ -"""Tests for $add decimal128 precision and boundary value handling.""" - import pytest from bson import Decimal128 diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py index d681c7fd2..7af9e0b66 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py @@ -1,5 +1,3 @@ -"""Tests for $add return type promotion rules across numeric and date operand combinations.""" - from datetime import datetime, timezone import pytest From f851712aaa40362d3ada515b2c50fd52ffc9b3c4 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Tue, 30 Jun 2026 13:47:24 -0700 Subject: [PATCH 09/11] Revert "Added tests" This reverts commit 51a482ab4db4faa743125c3ac8699d126a8fbf33. Signed-off-by: PatersonProjects --- .../arithmetic/add/test_add_date.py | 162 ----------- .../arithmetic/add/test_add_errors.py | 203 -------------- .../arithmetic/add/test_add_input_forms.py | 47 ---- .../arithmetic/add/test_add_non_finite.py | 154 ----------- .../arithmetic/add/test_add_null.py | 86 ------ .../arithmetic/add/test_add_numeric.py | 259 ------------------ .../arithmetic/add/test_add_overflow.py | 95 ------- .../arithmetic/add/test_add_precision.py | 103 ------- .../arithmetic/add/test_add_return_type.py | 114 -------- 9 files changed, 1223 deletions(-) delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py deleted file mode 100644 index 7cd0f09f2..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py +++ /dev/null @@ -1,162 +0,0 @@ -from datetime import datetime, timedelta, timezone - -import pytest -from bson import Decimal128, Int64 - -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_with_insert, -) -from documentdb_tests.framework.parametrize import pytest_params - -# Property [Date Arithmetic]: $add accepts exactly one date operand and one or more numeric -# operands (in milliseconds). The date may appear in any position. -ADD_DATE_NUMERIC_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "date_int32", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 86400000}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 2, tzinfo=timezone.utc), - msg="$add should add int32 milliseconds to a date", - ), - ExpressionTestCase( - "date_int64", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": Int64(86400000)}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 2, tzinfo=timezone.utc), - msg="$add should add int64 milliseconds to a date", - ), - ExpressionTestCase( - "date_decimal", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": Decimal128("1.5")}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 1, 0, 0, 0, 2000, tzinfo=timezone.utc), - msg="$add should round a decimal128 fractional millisecond value when adding to a date", - ), - ExpressionTestCase( - "date_double_round_up", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 2.5}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 1, 0, 0, 0, 3000, tzinfo=timezone.utc), - msg="$add should round up a double fractional millisecond value (.5) when adding to a date", - ), - ExpressionTestCase( - "date_double_truncates", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 4.4}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 1, 0, 0, 0, 4000, tzinfo=timezone.utc), - msg="$add should truncate a double fractional millisecond value (<.5) when adding to a date", # noqa: E501 - ), -] - -# Property [Date Rounding Boundaries]: $add rounds fractional millisecond offsets using -# round-half-away-from-zero. Values with |frac| < 0.5 truncate toward zero; values with -# |frac| >= 0.5 round away from zero. -ADD_DATE_ROUNDING_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "date_double_0_1", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 0.1}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 1, tzinfo=timezone.utc), - msg="$add should truncate 0.1ms and leave the date unchanged", - ), - ExpressionTestCase( - "date_double_0_49", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 0.49}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 1, tzinfo=timezone.utc), - msg="$add should truncate 0.49ms and leave the date unchanged", - ), - ExpressionTestCase( - "date_double_0_51", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 0.51}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 1, 0, 0, 0, 1000, tzinfo=timezone.utc), - msg="$add should round up 0.51ms to 1ms when adding to a date", - ), - ExpressionTestCase( - "date_double_0_6", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 0.6}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 1, 0, 0, 0, 1000, tzinfo=timezone.utc), - msg="$add should round up 0.6ms to 1ms when adding to a date", - ), - ExpressionTestCase( - "date_double_neg_0_5", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": -0.5}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 1, tzinfo=timezone.utc) - timedelta(milliseconds=1), - msg="$add should round -0.5ms away from zero to -1ms when adding to a date", - ), - ExpressionTestCase( - "date_double_neg_0_51", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": -0.51}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 1, tzinfo=timezone.utc) - timedelta(milliseconds=1), - msg="$add should round -0.51ms away from zero to -1ms when adding to a date", - ), -] - -# Property [Date Operand Position]: the date operand may appear in any position among the -# operands; only one date is permitted. -ADD_DATE_POSITION_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "number_then_date", - doc={"a": 86400000, "b": datetime(2026, 1, 1, tzinfo=timezone.utc)}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 2, tzinfo=timezone.utc), - msg="$add should add a date when the numeric operand appears before the date", - ), - ExpressionTestCase( - "date_in_middle", - doc={"a": 1, "b": datetime(2026, 1, 1, tzinfo=timezone.utc), "c": 1000}, - expression={"$add": ["$a", "$b", "$c"]}, - expected=datetime(2026, 1, 1, 0, 0, 1, 1000, tzinfo=timezone.utc), - msg="$add should add a date when it appears in the middle of the operand list", - ), -] - -# Property [Date Sign Handling]: adding zero or a negative number of milliseconds to a date -# returns the date unchanged or subtracted. -ADD_DATE_SIGN_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "date_negative", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": -86400000}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2025, 12, 31, tzinfo=timezone.utc), - msg="$add should subtract milliseconds from a date when adding a negative number", - ), - ExpressionTestCase( - "date_zero", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 0}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 1, tzinfo=timezone.utc), - msg="$add should return the same date when adding zero milliseconds", - ), - ExpressionTestCase( - "date_negative_zero", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": -0.0}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 1, tzinfo=timezone.utc), - msg="$add should return the same date when adding negative zero", - ), -] - -ADD_DATE_ALL_TESTS = ( - ADD_DATE_NUMERIC_TESTS + ADD_DATE_POSITION_TESTS + ADD_DATE_SIGN_TESTS + ADD_DATE_ROUNDING_TESTS -) - - -@pytest.mark.parametrize("test_case", pytest_params(ADD_DATE_ALL_TESTS)) -def test_add_date(collection, test_case: ExpressionTestCase): - """Test $add date arithmetic: numeric types, operand position, and sign handling.""" - result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) - assert_expression_result( - result, - expected=test_case.expected, - error_code=test_case.error_code, - msg=test_case.msg, - ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py deleted file mode 100644 index f5c817b3d..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py +++ /dev/null @@ -1,203 +0,0 @@ -from datetime import datetime, timezone - -import pytest -from bson import Binary, Code, 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_with_insert, -) -from documentdb_tests.framework.error_codes import ( - MORE_THAN_ONE_DATE_ERROR, - OVERFLOW_ERROR, - TYPE_MISMATCH_ERROR, -) -from documentdb_tests.framework.parametrize import pytest_params -from documentdb_tests.framework.test_constants import ( - DECIMAL128_INFINITY, - DECIMAL128_NAN, - FLOAT_INFINITY, - FLOAT_NAN, - INT64_MAX, -) - -# Property [Type Strictness]: $add rejects non-numeric, non-date operand types. -ADD_TYPE_ERROR_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - f"type_{tid}", - doc={"a": 1, "b": val}, - expression={"$add": ["$a", "$b"]}, - error_code=TYPE_MISMATCH_ERROR, - msg=f"$add should reject a {tid} operand", - ) - for tid, val in [ - ("string", "string"), - ("bool", True), - ("array", [2, 3]), - ("object", {"a": 2}), - ("regex", Regex("abc")), - ("objectid", ObjectId("507f1f77bcf86cd799439011")), - ("binary", Binary(b"data")), - ("minkey", MinKey()), - ("maxkey", MaxKey()), - ("timestamp", Timestamp(1, 1)), - ("code", Code("function(){}")), - ] -] - -# Property [Mixed Valid and Invalid]: $add rejects an invalid operand when it appears among -# valid numeric operands. -ADD_MIXED_VALID_INVALID_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "mixed_valid_invalid", - doc={"a": 1, "b": 2, "c": "string"}, - expression={"$add": ["$a", "$b", "$c"]}, - error_code=TYPE_MISMATCH_ERROR, - msg="$add should error when a string appears among numeric operands", - ), -] - -# Property [Single Invalid Operand]: $add rejects a single operand of an invalid type. -ADD_SINGLE_TYPE_ERROR_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "single_string", - doc={"a": "string"}, - expression={"$add": ["$a"]}, - error_code=TYPE_MISMATCH_ERROR, - msg="$add should reject a single string operand", - ), - ExpressionTestCase( - "single_boolean", - doc={"a": True}, - expression={"$add": ["$a"]}, - error_code=TYPE_MISMATCH_ERROR, - msg="$add should reject a single boolean operand", - ), - ExpressionTestCase( - "single_array", - doc={"a": [1, 2]}, - expression={"$add": ["$a"]}, - error_code=TYPE_MISMATCH_ERROR, - msg="$add should reject a single array operand", - ), - ExpressionTestCase( - "single_object", - doc={"a": {"x": 1}}, - expression={"$add": ["$a"]}, - error_code=TYPE_MISMATCH_ERROR, - msg="$add should reject a single object operand", - ), -] - -# Property [Multiple Dates]: $add rejects expressions with more than one date operand. -ADD_MULTIPLE_DATE_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "add_two_identical_dates", - doc={ - "a": datetime(2026, 1, 1, tzinfo=timezone.utc), - "b": datetime(2026, 1, 1, tzinfo=timezone.utc), - }, - expression={"$add": ["$a", "$b"]}, - error_code=MORE_THAN_ONE_DATE_ERROR, - msg="$add should error when adding two identical date operands", - ), - ExpressionTestCase( - "two_different_dates", - doc={ - "a": datetime(2026, 1, 1, tzinfo=timezone.utc), - "b": datetime(2026, 1, 2, tzinfo=timezone.utc), - }, - expression={"$add": ["$a", "$b"]}, - error_code=MORE_THAN_ONE_DATE_ERROR, - msg="$add should error when adding two different date operands", - ), - ExpressionTestCase( - "two_dates_with_numbers", - doc={ - "a": datetime(2026, 1, 1, tzinfo=timezone.utc), - "b": datetime(2026, 1, 2, tzinfo=timezone.utc), - }, - expression={"$add": [1, 2, 3, "$a", "$b"]}, - error_code=MORE_THAN_ONE_DATE_ERROR, - msg="$add should error when two dates appear among numeric operands", - ), - ExpressionTestCase( - "dates_separated_by_number", - doc={ - "a": datetime(2026, 1, 1, tzinfo=timezone.utc), - "b": datetime(2026, 1, 2, tzinfo=timezone.utc), - }, - expression={"$add": ["$a", 1, "$b"]}, - error_code=MORE_THAN_ONE_DATE_ERROR, - msg="$add should error when two dates are separated by a numeric operand", - ), -] - -# Property [Date with Non-Finite]: $add rejects NaN and Infinity as numeric operands when a -# date is also present, since the resulting date would be non-representable. -ADD_DATE_NON_FINITE_ERROR_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "date_nan", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": FLOAT_NAN}, - expression={"$add": ["$a", "$b"]}, - error_code=OVERFLOW_ERROR, - msg="$add should error when adding a date and float NaN", - ), - ExpressionTestCase( - "date_infinity", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": FLOAT_INFINITY}, - expression={"$add": ["$a", "$b"]}, - error_code=OVERFLOW_ERROR, - msg="$add should error when adding a date and float infinity", - ), - ExpressionTestCase( - "date_decimal_nan", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": DECIMAL128_NAN}, - expression={"$add": ["$a", "$b"]}, - error_code=OVERFLOW_ERROR, - msg="$add should error when adding a date and decimal128 NaN", - ), - ExpressionTestCase( - "date_decimal_infinity", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": DECIMAL128_INFINITY}, - expression={"$add": ["$a", "$b"]}, - error_code=OVERFLOW_ERROR, - msg="$add should error when adding a date and decimal128 infinity", - ), -] - -# Property [Date Overflow]: $add errors when the millisecond offset would push the date result -# beyond the representable date range. -ADD_DATE_OVERFLOW_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "date_int64_max", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": INT64_MAX}, - expression={"$add": ["$a", "$b"]}, - error_code=OVERFLOW_ERROR, - msg="$add should error when adding INT64_MAX milliseconds to a date overflows the date range", # noqa: E501 - ), -] - -ADD_ERROR_ALL_TESTS = ( - ADD_TYPE_ERROR_TESTS - + ADD_MIXED_VALID_INVALID_TESTS - + ADD_SINGLE_TYPE_ERROR_TESTS - + ADD_MULTIPLE_DATE_TESTS - + ADD_DATE_NON_FINITE_ERROR_TESTS - + ADD_DATE_OVERFLOW_TESTS -) - - -@pytest.mark.parametrize("test_case", pytest_params(ADD_ERROR_ALL_TESTS)) -def test_add_errors(collection, test_case: ExpressionTestCase): - """Test $add type, multiple-date, and date non-finite error cases.""" - result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) - assert_expression_result( - result, - expected=test_case.expected, - error_code=test_case.error_code, - msg=test_case.msg, - ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py deleted file mode 100644 index 7019c1b0b..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py +++ /dev/null @@ -1,47 +0,0 @@ -import pytest - -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_with_insert, -) -from documentdb_tests.framework.parametrize import pytest_params - -# Property [Expression Input]: $add evaluates a nested expression argument before summing. -ADD_EXPRESSION_INPUT_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "nested_add", - doc={"a": 1, "b": 2}, - expression={"$add": [{"$add": ["$a", "$b"]}, 3]}, - expected=6, - msg="$add should evaluate a nested $add expression as an operand", - ), -] - -# Property [Mixed Literal and Field]: $add accepts a mix of field references and inline literals -# in the same operand list. -ADD_MIXED_INPUT_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "mixed_literal_and_field", - doc={"a": 10}, - expression={"$add": ["$a", 5]}, - expected=15, - msg="$add should sum a field reference and an inline literal operand", - ), -] - -ADD_INPUT_FORM_ALL_TESTS = ADD_EXPRESSION_INPUT_TESTS + ADD_MIXED_INPUT_TESTS - - -@pytest.mark.parametrize("test_case", pytest_params(ADD_INPUT_FORM_ALL_TESTS)) -def test_add_input_forms(collection, test_case: ExpressionTestCase): - """Test $add literal, nested expression, and mixed input form cases.""" - result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) - assert_expression_result( - result, - expected=test_case.expected, - error_code=test_case.error_code, - msg=test_case.msg, - ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py deleted file mode 100644 index 0343c3316..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py +++ /dev/null @@ -1,154 +0,0 @@ -import math - -import pytest - -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_with_insert, -) -from documentdb_tests.framework.parametrize import pytest_params -from documentdb_tests.framework.test_constants import ( - DECIMAL128_INFINITY, - DECIMAL128_NAN, - DECIMAL128_NEGATIVE_INFINITY, - FLOAT_INFINITY, - FLOAT_NAN, - FLOAT_NEGATIVE_INFINITY, -) - -# Property [Infinity]: $add propagates infinity according to IEEE 754 rules. -ADD_INFINITY_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "infinity", - doc={"a": FLOAT_INFINITY, "b": 1}, - expression={"$add": ["$a", "$b"]}, - expected=FLOAT_INFINITY, - msg="$add should return infinity when adding infinity and a finite number", - ), - ExpressionTestCase( - "negative_infinity", - doc={"a": FLOAT_NEGATIVE_INFINITY, "b": 1}, - expression={"$add": ["$a", "$b"]}, - expected=FLOAT_NEGATIVE_INFINITY, - msg="$add should return negative infinity when adding negative infinity and a finite number", # noqa: E501 - ), - ExpressionTestCase( - "single_infinity", - doc={"a": FLOAT_INFINITY}, - expression={"$add": ["$a"]}, - expected=FLOAT_INFINITY, - msg="$add should return infinity for a single infinity operand", - ), - ExpressionTestCase( - "inf_plus_inf", - doc={"a": FLOAT_INFINITY, "b": FLOAT_INFINITY}, - expression={"$add": ["$a", "$b"]}, - expected=FLOAT_INFINITY, - msg="$add should return infinity when adding two positive infinities", - ), - ExpressionTestCase( - "neg_inf_plus_neg_inf", - doc={"a": FLOAT_NEGATIVE_INFINITY, "b": FLOAT_NEGATIVE_INFINITY}, - expression={"$add": ["$a", "$b"]}, - expected=FLOAT_NEGATIVE_INFINITY, - msg="$add should return negative infinity when adding two negative infinities", - ), - ExpressionTestCase( - "inf_plus_zero", - doc={"a": FLOAT_INFINITY, "b": 0}, - expression={"$add": ["$a", "$b"]}, - expected=FLOAT_INFINITY, - msg="$add should return infinity when adding infinity and zero", - ), - ExpressionTestCase( - "neg_inf_plus_zero", - doc={"a": FLOAT_NEGATIVE_INFINITY, "b": 0}, - expression={"$add": ["$a", "$b"]}, - expected=FLOAT_NEGATIVE_INFINITY, - msg="$add should return negative infinity when adding negative infinity and zero", - ), - ExpressionTestCase( - "decimal_infinity", - doc={"a": DECIMAL128_INFINITY, "b": 1}, - expression={"$add": ["$a", "$b"]}, - expected=DECIMAL128_INFINITY, - msg="$add should return decimal128 infinity when adding decimal128 infinity and a number", - ), - ExpressionTestCase( - "decimal_negative_infinity", - doc={"a": DECIMAL128_NEGATIVE_INFINITY, "b": 1}, - expression={"$add": ["$a", "$b"]}, - expected=DECIMAL128_NEGATIVE_INFINITY, - msg="$add should return decimal128 negative infinity when adding decimal128 negative infinity and a number", # noqa: E501 - ), -] - -# Property [NaN]: $add propagates NaN according to IEEE 754 rules. -ADD_NAN_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "nan_add_one", - doc={"a": FLOAT_NAN, "b": 1}, - expression={"$add": ["$a", "$b"]}, - expected=pytest.approx(math.nan, nan_ok=True), - msg="$add should return NaN when adding float NaN and a finite number", - ), - ExpressionTestCase( - "inf_minus_inf", - doc={"a": FLOAT_INFINITY, "b": FLOAT_NEGATIVE_INFINITY}, - expression={"$add": ["$a", "$b"]}, - expected=pytest.approx(math.nan, nan_ok=True), - msg="$add should return NaN when adding float infinity and negative infinity", - ), - ExpressionTestCase( - "nan_plus_nan", - doc={"a": FLOAT_NAN, "b": FLOAT_NAN}, - expression={"$add": ["$a", "$b"]}, - expected=pytest.approx(math.nan, nan_ok=True), - msg="$add should return NaN when adding two float NaN values", - ), - ExpressionTestCase( - "nan_plus_inf", - doc={"a": FLOAT_NAN, "b": FLOAT_INFINITY}, - expression={"$add": ["$a", "$b"]}, - expected=pytest.approx(math.nan, nan_ok=True), - msg="$add should return NaN when adding float NaN and infinity", - ), - ExpressionTestCase( - "decimal_nan", - doc={"a": DECIMAL128_NAN, "b": 1}, - expression={"$add": ["$a", "$b"]}, - expected=DECIMAL128_NAN, - msg="$add should return decimal128 NaN when adding decimal128 NaN and a number", - ), - ExpressionTestCase( - "decimal_nan_plus_nan", - doc={"a": DECIMAL128_NAN, "b": DECIMAL128_NAN}, - expression={"$add": ["$a", "$b"]}, - expected=DECIMAL128_NAN, - msg="$add should return decimal128 NaN when adding two decimal128 NaN values", - ), - ExpressionTestCase( - "decimal_inf_minus_inf", - doc={"a": DECIMAL128_INFINITY, "b": DECIMAL128_NEGATIVE_INFINITY}, - expression={"$add": ["$a", "$b"]}, - expected=DECIMAL128_NAN, - msg="$add should return decimal128 NaN when adding decimal128 infinity and negative infinity", # noqa: E501 - ), -] - -ADD_NON_FINITE_ALL_TESTS = ADD_INFINITY_TESTS + ADD_NAN_TESTS - - -@pytest.mark.parametrize("test_case", pytest_params(ADD_NON_FINITE_ALL_TESTS)) -def test_add_non_finite(collection, test_case: ExpressionTestCase): - """Test $add infinity and NaN propagation cases.""" - result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) - assert_expression_result( - result, - expected=test_case.expected, - error_code=test_case.error_code, - msg=test_case.msg, - ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py deleted file mode 100644 index 9651d52d3..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py +++ /dev/null @@ -1,86 +0,0 @@ -from datetime import datetime, timezone - -import pytest - -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_with_insert, -) -from documentdb_tests.framework.parametrize import pytest_params -from documentdb_tests.framework.test_constants import MISSING - -# Property [Null Propagation]: $add returns null if any operand is null or refers to a missing -# field. -ADD_NULL_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "single_null", - doc={"a": None}, - expression={"$add": ["$a"]}, - expected=None, - msg="$add should return null for a single null operand", - ), - ExpressionTestCase( - "null_operand", - doc={"a": 1, "b": None}, - expression={"$add": ["$a", "$b"]}, - expected=None, - msg="$add should return null when any operand is null", - ), - ExpressionTestCase( - "missing_field", - doc={}, - expression={"$add": [1, MISSING]}, - expected=None, - msg="$add should return null when any operand is a missing field", - ), - ExpressionTestCase( - "null_with_multiple", - doc={"a": 1, "b": 2, "c": None}, - expression={"$add": ["$a", "$b", "$c"]}, - expected=None, - msg="$add should return null when null appears among multiple operands", - ), - ExpressionTestCase( - "null_in_middle", - doc={"a": 1, "b": 2, "c": 3, "e": 5}, - expression={"$add": ["$a", "$b", "$c", None, "$e"]}, - expected=None, - msg="$add should return null when null appears in the middle of operands", - ), - ExpressionTestCase( - "all_null", - doc={"a": None, "b": None}, - expression={"$add": ["$a", "$b"]}, - expected=None, - msg="$add should return null when all operands are null", - ), - ExpressionTestCase( - "all_missing", - doc={}, - expression={"$add": [MISSING, MISSING]}, - expected=None, - msg="$add should return null when all operands are missing fields", - ), - ExpressionTestCase( - "date_and_null", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": None}, - expression={"$add": ["$a", "$b"]}, - expected=None, - msg="$add should return null when a date is paired with a null operand", - ), -] - - -@pytest.mark.parametrize("test_case", pytest_params(ADD_NULL_TESTS)) -def test_add_null(collection, test_case: ExpressionTestCase): - """Test $add null and missing field propagation cases.""" - result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) - assert_expression_result( - result, - expected=test_case.expected, - error_code=test_case.error_code, - msg=test_case.msg, - ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py deleted file mode 100644 index 3c4d6f305..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py +++ /dev/null @@ -1,259 +0,0 @@ -import pytest -from bson import Decimal128, Int64 - -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_with_insert, -) -from documentdb_tests.framework.parametrize import pytest_params - -# Property [Same-Type Addition]: $add of two values of the same numeric type returns a value of -# that type. -ADD_SAME_TYPE_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "same_type_int32", - doc={"a": 1, "b": 2}, - expression={"$add": ["$a", "$b"]}, - expected=3, - msg="$add should add two int32 values", - ), - ExpressionTestCase( - "same_type_int64", - doc={"a": Int64(10), "b": Int64(20)}, - expression={"$add": ["$a", "$b"]}, - expected=Int64(30), - msg="$add should add two int64 values", - ), - ExpressionTestCase( - "same_type_double", - doc={"a": 1.5, "b": 2.5}, - expression={"$add": ["$a", "$b"]}, - expected=4.0, - msg="$add should add two double values", - ), - ExpressionTestCase( - "same_type_decimal", - doc={"a": Decimal128("10.5"), "b": Decimal128("20.5")}, - expression={"$add": ["$a", "$b"]}, - expected=Decimal128("31.0"), - msg="$add should add two decimal128 values", - ), -] - -# Property [Type Promotion]: $add promotes to the wider type when operands have different numeric -# types. Precedence: decimal128 > double > int64 > int32. -ADD_MIXED_TYPE_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "int32_int64", - doc={"a": 1, "b": Int64(20)}, - expression={"$add": ["$a", "$b"]}, - expected=Int64(21), - msg="$add should return int64 when adding int32 and int64", - ), - ExpressionTestCase( - "int32_double", - doc={"a": 1, "b": 2.5}, - expression={"$add": ["$a", "$b"]}, - expected=3.5, - msg="$add should return double when adding int32 and double", - ), - ExpressionTestCase( - "int32_decimal", - doc={"a": 1, "b": Decimal128("2.5")}, - expression={"$add": ["$a", "$b"]}, - expected=Decimal128("3.5"), - msg="$add should return decimal128 when adding int32 and decimal128", - ), - ExpressionTestCase( - "int64_double", - doc={"a": Int64(10), "b": 2.5}, - expression={"$add": ["$a", "$b"]}, - expected=12.5, - msg="$add should return double when adding int64 and double", - ), - ExpressionTestCase( - "int64_decimal", - doc={"a": Int64(10), "b": Decimal128("2.5")}, - expression={"$add": ["$a", "$b"]}, - expected=Decimal128("12.5"), - msg="$add should return decimal128 when adding int64 and decimal128", - ), - ExpressionTestCase( - "double_decimal", - doc={"a": 1.5, "b": Decimal128("2.5")}, - expression={"$add": ["$a", "$b"]}, - expected=pytest.approx(Decimal128("4.00000000000000")), - msg="$add should return decimal128 when adding double and decimal128", - ), - ExpressionTestCase( - "three_mixed_types", - doc={"a": Decimal128("1.5"), "b": 2.5, "c": Int64(3)}, - expression={"$add": ["$a", "$b", "$c"]}, - expected=pytest.approx(Decimal128("7.00000000000000")), - msg="$add should return decimal128 when adding decimal128, double, and int64", - ), -] - -# Property [Multiple Operands]: $add correctly sums three or more operands. -ADD_MULTIPLE_OPERANDS_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "multiple_int32", - doc={"a": 1, "b": 2, "c": 3}, - expression={"$add": ["$a", "$b", "$c"]}, - expected=6, - msg="$add should add multiple int32 values", - ), - ExpressionTestCase( - "multiple_int64", - doc={"a": Int64(1), "b": Int64(2), "c": Int64(3)}, - expression={"$add": ["$a", "$b", "$c"]}, - expected=Int64(6), - msg="$add should add multiple int64 values", - ), - ExpressionTestCase( - "multiple_double", - doc={"a": 1.1, "b": 2.2, "c": 3.3}, - expression={"$add": ["$a", "$b", "$c"]}, - expected=pytest.approx(6.6), - msg="$add should add multiple double values", - ), - ExpressionTestCase( - "multiple_decimal", - doc={ - "a": Decimal128("1"), - "b": Decimal128("2"), - "c": Decimal128("3"), - "d": Decimal128("4"), - }, - expression={"$add": ["$a", "$b", "$c", "$d"]}, - expected=Decimal128("10"), - msg="$add should add multiple decimal128 values", - ), - ExpressionTestCase( - "five_operands", - doc={"a": 1, "b": 2, "c": 3, "d": 4, "e": 5}, - expression={"$add": ["$a", "$b", "$c", "$d", "$e"]}, - expected=15, - msg="$add should correctly sum five int32 operands", - ), - ExpressionTestCase( - "ten_operands", - doc={ - "a": 1, - "b": 2, - "c": 3, - "d": 4, - "e": 5, - "f": 6, - "g": 7, - "h": 8, - "i": 9, - "j": 10, - }, - expression={"$add": ["$a", "$b", "$c", "$d", "$e", "$f", "$g", "$h", "$i", "$j"]}, - expected=55, - msg="$add should correctly sum ten int32 operands", - ), -] - -# Property [Empty and Single Operand]: $add of zero operands returns 0; single operand returns -# that value unchanged. -ADD_SINGLE_AND_EMPTY_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "empty", - doc={}, - expression={"$add": []}, - expected=0, - msg="$add should return 0 for empty operand list", - ), - ExpressionTestCase( - "single_int32", - doc={"a": 5}, - expression={"$add": ["$a"]}, - expected=5, - msg="$add should return the value for a single int32 operand", - ), - ExpressionTestCase( - "single_int64", - doc={"a": Int64(0)}, - expression={"$add": ["$a"]}, - expected=Int64(0), - msg="$add should return the value for a single int64 operand", - ), - ExpressionTestCase( - "single_double", - doc={"a": 0.0}, - expression={"$add": ["$a"]}, - expected=0.0, - msg="$add should return the value for a single double operand", - ), - ExpressionTestCase( - "single_decimal", - doc={"a": Decimal128("0")}, - expression={"$add": ["$a"]}, - expected=Decimal128("0"), - msg="$add should return the value for a single decimal128 operand", - ), -] - -# Property [Sign Handling]: $add handles negative values and zero correctly. -ADD_SIGN_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "negative_positive", - doc={"a": -5, "b": 3}, - expression={"$add": ["$a", "$b"]}, - expected=-2, - msg="$add should add a negative and a positive int32 value", - ), - ExpressionTestCase( - "both_negative", - doc={"a": -10, "b": -20}, - expression={"$add": ["$a", "$b"]}, - expected=-30, - msg="$add should add two negative int32 values", - ), - ExpressionTestCase( - "zeros", - doc={"a": 0, "b": 0}, - expression={"$add": ["$a", "$b"]}, - expected=0, - msg="$add should return 0 when adding two int32 zeros", - ), - ExpressionTestCase( - "zero_negative_zero", - doc={"a": 0, "b": -0.0}, - expression={"$add": ["$a", "$b"]}, - expected=0.0, - msg="$add should return 0.0 when adding int32 zero and negative zero double", - ), - ExpressionTestCase( - "sum_to_zero", - doc={"a": 1, "b": 0, "c": -1}, - expression={"$add": ["$a", "$b", "$c"]}, - expected=0, - msg="$add should return 0 when operands sum to zero", - ), -] - -ADD_NUMERIC_ALL_TESTS = ( - ADD_SAME_TYPE_TESTS - + ADD_MIXED_TYPE_TESTS - + ADD_MULTIPLE_OPERANDS_TESTS - + ADD_SINGLE_AND_EMPTY_TESTS - + ADD_SIGN_TESTS -) - - -@pytest.mark.parametrize("test_case", pytest_params(ADD_NUMERIC_ALL_TESTS)) -def test_add_numeric(collection, test_case: ExpressionTestCase): - """Test $add numeric type combinations, multiple operands, and sign handling.""" - result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) - assert_expression_result( - result, - expected=test_case.expected, - error_code=test_case.error_code, - msg=test_case.msg, - ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py deleted file mode 100644 index 29a992905..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py +++ /dev/null @@ -1,95 +0,0 @@ -import pytest -from bson import Int64 - -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_with_insert, -) -from documentdb_tests.framework.parametrize import pytest_params -from documentdb_tests.framework.test_constants import ( - DOUBLE_FROM_INT64_MAX, - FLOAT_INFINITY, - FLOAT_NEGATIVE_INFINITY, - INT32_MAX, - INT32_MIN, - INT32_OVERFLOW, - INT32_UNDERFLOW, - INT64_MAX, - INT64_MIN, -) - -# Property [Int32 Overflow]: when an int32 result exceeds the int32 range, $add promotes to -# int64. -ADD_INT32_OVERFLOW_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "int32_overflow", - doc={"a": INT32_MAX, "b": 1}, - expression={"$add": ["$a", "$b"]}, - expected=Int64(INT32_OVERFLOW), - msg="$add should promote to int64 when the int32 result overflows INT32_MAX", - ), - ExpressionTestCase( - "int32_underflow", - doc={"a": INT32_MIN, "b": -1}, - expression={"$add": ["$a", "$b"]}, - expected=Int64(INT32_UNDERFLOW), - msg="$add should promote to int64 when the int32 result underflows INT32_MIN", - ), -] - -# Property [Int64 Overflow]: when an int64 result exceeds the int64 range, $add promotes to -# double. -ADD_INT64_OVERFLOW_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "int64_overflow", - doc={"a": INT64_MAX, "b": 1}, - expression={"$add": ["$a", "$b"]}, - expected=pytest.approx(DOUBLE_FROM_INT64_MAX), - msg="$add should promote to double when the int64 result overflows INT64_MAX", - ), - ExpressionTestCase( - "int64_underflow", - doc={"a": INT64_MIN, "b": -1}, - expression={"$add": ["$a", "$b"]}, - expected=pytest.approx(-DOUBLE_FROM_INT64_MAX), - msg="$add should promote to double when the int64 result underflows INT64_MIN", - ), -] - -# Property [Double Overflow]: when a double result exceeds the double range, $add returns -# infinity. -ADD_DOUBLE_OVERFLOW_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "double_overflow", - doc={"a": 1e308, "b": 1e308}, - expression={"$add": ["$a", "$b"]}, - expected=FLOAT_INFINITY, - msg="$add should return positive infinity on double overflow", - ), - ExpressionTestCase( - "double_underflow", - doc={"a": -1e308, "b": -1e308}, - expression={"$add": ["$a", "$b"]}, - expected=FLOAT_NEGATIVE_INFINITY, - msg="$add should return negative infinity on double underflow", - ), -] - -ADD_OVERFLOW_ALL_TESTS = ( - ADD_INT32_OVERFLOW_TESTS + ADD_INT64_OVERFLOW_TESTS + ADD_DOUBLE_OVERFLOW_TESTS -) - - -@pytest.mark.parametrize("test_case", pytest_params(ADD_OVERFLOW_ALL_TESTS)) -def test_add_overflow(collection, test_case: ExpressionTestCase): - """Test $add integer and double overflow and underflow cases.""" - result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) - assert_expression_result( - result, - expected=test_case.expected, - error_code=test_case.error_code, - msg=test_case.msg, - ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py deleted file mode 100644 index f0cdd34ab..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py +++ /dev/null @@ -1,103 +0,0 @@ -import pytest -from bson import Decimal128 - -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_with_insert, -) -from documentdb_tests.framework.parametrize import pytest_params -from documentdb_tests.framework.test_constants import ( - DECIMAL128_INFINITY, - DECIMAL128_MAX, - DECIMAL128_MIN, - DECIMAL128_NEGATIVE_INFINITY, -) - -# Property [Decimal128 Precision]: $add preserves decimal128 precision, including exact -# representation of values that are inexact in double. -ADD_DECIMAL_PRECISION_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "decimal_precision", - doc={"a": Decimal128("1.5"), "b": Decimal128("2.5")}, - expression={"$add": ["$a", "$b"]}, - expected=Decimal128("4.0"), - msg="$add should preserve decimal128 precision", - ), - ExpressionTestCase( - "decimal_precision_small", - doc={"a": Decimal128("0.1"), "b": Decimal128("0.2")}, - expression={"$add": ["$a", "$b"]}, - expected=Decimal128("0.3"), - msg="$add should exactly represent 0.1 + 0.2 with decimal128", - ), - ExpressionTestCase( - "decimal_large_precision", - doc={ - "a": Decimal128("999999999999999999999999999999999"), - "b": Decimal128("1"), - }, - expression={"$add": ["$a", "$b"]}, - expected=Decimal128("1000000000000000000000000000000000"), - msg="$add should handle large decimal128 addition with full precision", - ), - ExpressionTestCase( - "decimal_large_negative_precision", - doc={ - "a": Decimal128("-999999999999999999999999999999999"), - "b": Decimal128("-1"), - }, - expression={"$add": ["$a", "$b"]}, - expected=Decimal128("-1000000000000000000000000000000000"), - msg="$add should handle large negative decimal128 addition with full precision", - ), -] - -# Property [Decimal128 Boundaries]: $add at decimal128 boundary values promotes to infinity when -# the result overflows, and returns zero when max and min cancel. -ADD_DECIMAL_BOUNDARY_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "decimal128_max_plus_zero", - doc={"a": DECIMAL128_MAX, "b": Decimal128("0")}, - expression={"$add": ["$a", "$b"]}, - expected=DECIMAL128_MAX, - msg="$add should return decimal128 max when adding zero to decimal128 max", - ), - ExpressionTestCase( - "decimal128_max_plus_max", - doc={"a": DECIMAL128_MAX, "b": DECIMAL128_MAX}, - expression={"$add": ["$a", "$b"]}, - expected=DECIMAL128_INFINITY, - msg="$add should return decimal128 infinity when adding two decimal128 max values", - ), - ExpressionTestCase( - "decimal128_min_plus_min", - doc={"a": DECIMAL128_MIN, "b": DECIMAL128_MIN}, - expression={"$add": ["$a", "$b"]}, - expected=DECIMAL128_NEGATIVE_INFINITY, - msg="$add should return decimal128 negative infinity when adding two decimal128 min values", - ), - ExpressionTestCase( - "decimal128_max_plus_min", - doc={"a": DECIMAL128_MAX, "b": DECIMAL128_MIN}, - expression={"$add": ["$a", "$b"]}, - expected=Decimal128("0E+6111"), - msg="$add should return zero when adding decimal128 max and decimal128 min", - ), -] - -ADD_PRECISION_ALL_TESTS = ADD_DECIMAL_PRECISION_TESTS + ADD_DECIMAL_BOUNDARY_TESTS - - -@pytest.mark.parametrize("test_case", pytest_params(ADD_PRECISION_ALL_TESTS)) -def test_add_precision(collection, test_case: ExpressionTestCase): - """Test $add decimal128 precision and boundary value cases.""" - result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) - assert_expression_result( - result, - expected=test_case.expected, - error_code=test_case.error_code, - msg=test_case.msg, - ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py deleted file mode 100644 index 7af9e0b66..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py +++ /dev/null @@ -1,114 +0,0 @@ -from datetime import datetime, timezone - -import pytest -from bson import Decimal128, Int64 - -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_with_insert, -) -from documentdb_tests.framework.parametrize import pytest_params - -# Property [Return Type]: $add follows numeric type promotion rules to determine the result type. -# Precedence: decimal128 > double > int64 > int32. Date + numeric always returns date. -ADD_RETURN_TYPE_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "return_type_int_int", - doc={"a": 1, "b": 2}, - expression={"$type": {"$add": ["$a", "$b"]}}, - expected="int", - msg="$add should return int type when adding two int32 values", - ), - ExpressionTestCase( - "return_type_int_long", - doc={"a": 1, "b": Int64(2)}, - expression={"$type": {"$add": ["$a", "$b"]}}, - expected="long", - msg="$add should return long type when adding int32 and int64", - ), - ExpressionTestCase( - "return_type_int_double", - doc={"a": 1, "b": 2.0}, - expression={"$type": {"$add": ["$a", "$b"]}}, - expected="double", - msg="$add should return double type when adding int32 and double", - ), - ExpressionTestCase( - "return_type_int_decimal", - doc={"a": 1, "b": Decimal128("2")}, - expression={"$type": {"$add": ["$a", "$b"]}}, - expected="decimal", - msg="$add should return decimal type when adding int32 and decimal128", - ), - ExpressionTestCase( - "return_type_long_long", - doc={"a": Int64(1), "b": Int64(2)}, - expression={"$type": {"$add": ["$a", "$b"]}}, - expected="long", - msg="$add should return long type when adding two int64 values", - ), - ExpressionTestCase( - "return_type_long_double", - doc={"a": Int64(1), "b": 2.0}, - expression={"$type": {"$add": ["$a", "$b"]}}, - expected="double", - msg="$add should return double type when adding int64 and double", - ), - ExpressionTestCase( - "return_type_long_decimal", - doc={"a": Int64(1), "b": Decimal128("2")}, - expression={"$type": {"$add": ["$a", "$b"]}}, - expected="decimal", - msg="$add should return decimal type when adding int64 and decimal128", - ), - ExpressionTestCase( - "return_type_double_double", - doc={"a": 1.0, "b": 2.0}, - expression={"$type": {"$add": ["$a", "$b"]}}, - expected="double", - msg="$add should return double type when adding two double values", - ), - ExpressionTestCase( - "return_type_double_decimal", - doc={"a": 1.0, "b": Decimal128("2")}, - expression={"$type": {"$add": ["$a", "$b"]}}, - expected="decimal", - msg="$add should return decimal type when adding double and decimal128", - ), - ExpressionTestCase( - "return_type_decimal_decimal", - doc={"a": Decimal128("1"), "b": Decimal128("2")}, - expression={"$type": {"$add": ["$a", "$b"]}}, - expected="decimal", - msg="$add should return decimal type when adding two decimal128 values", - ), - ExpressionTestCase( - "return_type_date_int", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 1000}, - expression={"$type": {"$add": ["$a", "$b"]}}, - expected="date", - msg="$add should return date type when adding a date and an int32", - ), - ExpressionTestCase( - "return_type_empty", - doc={}, - expression={"$type": {"$add": []}}, - expected="int", - msg="$add should return int type for an empty operand list", - ), -] - - -@pytest.mark.parametrize("test_case", pytest_params(ADD_RETURN_TYPE_TESTS)) -def test_add_return_type(collection, test_case: ExpressionTestCase): - """Test $add return type promotion rules for all numeric type combinations.""" - result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) - assert_expression_result( - result, - expected=test_case.expected, - error_code=test_case.error_code, - msg=test_case.msg, - ) From 4aa2ddc22486baeb4fdd8669b3bb9766c910803d Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Fri, 3 Jul 2026 09:47:18 -0700 Subject: [PATCH 10/11] Added tests for missing parameters Signed-off-by: PatersonProjects --- .../test_getClusterParameter_response_structure.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_response_structure.py b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_response_structure.py index 2cd37a385..1a8373e5c 100644 --- a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_response_structure.py +++ b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_response_structure.py @@ -13,7 +13,7 @@ from documentdb_tests.framework.assertions import assertProperties from documentdb_tests.framework.executor import execute_admin_command from documentdb_tests.framework.parametrize import pytest_params -from documentdb_tests.framework.property_checks import Eq, IsType, Len +from documentdb_tests.framework.property_checks import Contains, Eq, IsType, Len pytestmark = pytest.mark.admin @@ -44,6 +44,18 @@ checks={"clusterParameters.0._id": Eq(_VALID_PARAM)}, msg=f"Single-name request should return element with _id equal to '{_VALID_PARAM}'", ), + AdministrationTestCase( + id="wildcard_includes_auditConfig", + command={"getClusterParameter": "*"}, + checks={"clusterParameters": Contains("_id", "auditConfig")}, + msg="Wildcard result should include 'auditConfig'", + ), + AdministrationTestCase( + id="wildcard_includes_fleDisableSubstringPreviewParameterLimits", + command={"getClusterParameter": "*"}, + checks={"clusterParameters": Contains("_id", "fleDisableSubstringPreviewParameterLimits")}, + msg="Wildcard result should include 'fleDisableSubstringPreviewParameterLimits'", + ), ] From 1ff30ecc8494472b5e374af553f6115af9b0a2ff Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Fri, 3 Jul 2026 10:36:56 -0700 Subject: [PATCH 11/11] Removed auditConfig due to lack of parameter presence Signed-off-by: PatersonProjects --- .../test_getClusterParameter_response_structure.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_response_structure.py b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_response_structure.py index 1a8373e5c..1530b9a06 100644 --- a/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_response_structure.py +++ b/documentdb_tests/compatibility/tests/system/administration/commands/getClusterParameter/test_getClusterParameter_response_structure.py @@ -44,12 +44,6 @@ checks={"clusterParameters.0._id": Eq(_VALID_PARAM)}, msg=f"Single-name request should return element with _id equal to '{_VALID_PARAM}'", ), - AdministrationTestCase( - id="wildcard_includes_auditConfig", - command={"getClusterParameter": "*"}, - checks={"clusterParameters": Contains("_id", "auditConfig")}, - msg="Wildcard result should include 'auditConfig'", - ), AdministrationTestCase( id="wildcard_includes_fleDisableSubstringPreviewParameterLimits", command={"getClusterParameter": "*"},