diff --git a/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/__init__.py b/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/test_listDatabases_authorized_databases.py b/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/test_listDatabases_authorized_databases.py new file mode 100644 index 00000000..2cf08b91 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/test_listDatabases_authorized_databases.py @@ -0,0 +1,168 @@ +"""Tests for listDatabases authorizedDatabases parameter.""" + +from __future__ import annotations + +import datetime + +import pytest +from bson import Binary, Code, MaxKey, MinKey, ObjectId, Regex, Timestamp + +from documentdb_tests.compatibility.tests.core.collections.commands.listDatabases.utils.listDatabases_common import ( # noqa: E501 + basic_success, +) +from documentdb_tests.compatibility.tests.core.collections.commands.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertResult +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.test_constants import DECIMAL128_ZERO, INT64_ZERO + +# Property [authorizedDatabases Success Behavior]: authorizedDatabases +# accepts true and false without changing the result set for a fully +# privileged user. +AUTH_DBS_SUCCESS_TESTS: list[CommandTestCase] = [ + CommandTestCase( + command={"listDatabases": 1, "authorizedDatabases": True}, + expected=basic_success, + msg="authorizedDatabases=true should succeed and include test database", + id="auth_dbs_true", + ), + CommandTestCase( + command={"listDatabases": 1, "authorizedDatabases": False}, + expected=basic_success, + msg="authorizedDatabases=false should succeed and include test database", + id="auth_dbs_false", + ), +] + +# Property [authorizedDatabases Type Strictness]: authorizedDatabases +# rejects all non-bool, non-null types with a TypeMismatch error, +# including numeric types that nameOnly would accept. +AUTH_DBS_TYPE_ERROR_TESTS: list[CommandTestCase] = [ + CommandTestCase( + command={"listDatabases": 1, "authorizedDatabases": 1}, + error_code=TYPE_MISMATCH_ERROR, + msg="authorizedDatabases with int32 should produce TypeMismatch", + id="auth_dbs_type_int32", + ), + CommandTestCase( + command={"listDatabases": 1, "authorizedDatabases": INT64_ZERO}, + error_code=TYPE_MISMATCH_ERROR, + msg="authorizedDatabases with Int64 should produce TypeMismatch", + id="auth_dbs_type_int64", + ), + CommandTestCase( + command={"listDatabases": 1, "authorizedDatabases": 1.0}, + error_code=TYPE_MISMATCH_ERROR, + msg="authorizedDatabases with double should produce TypeMismatch", + id="auth_dbs_type_double", + ), + CommandTestCase( + command={"listDatabases": 1, "authorizedDatabases": DECIMAL128_ZERO}, + error_code=TYPE_MISMATCH_ERROR, + msg="authorizedDatabases with Decimal128 should produce TypeMismatch", + id="auth_dbs_type_decimal128", + ), + CommandTestCase( + command={"listDatabases": 1, "authorizedDatabases": "true"}, + error_code=TYPE_MISMATCH_ERROR, + msg="authorizedDatabases with string should produce TypeMismatch", + id="auth_dbs_type_string", + ), + CommandTestCase( + command={"listDatabases": 1, "authorizedDatabases": []}, + error_code=TYPE_MISMATCH_ERROR, + msg="authorizedDatabases with array should produce TypeMismatch", + id="auth_dbs_type_array", + ), + CommandTestCase( + command={"listDatabases": 1, "authorizedDatabases": {}}, + error_code=TYPE_MISMATCH_ERROR, + msg="authorizedDatabases with object should produce TypeMismatch", + id="auth_dbs_type_object", + ), + CommandTestCase( + command={"listDatabases": 1, "authorizedDatabases": ObjectId()}, + error_code=TYPE_MISMATCH_ERROR, + msg="authorizedDatabases with ObjectId should produce TypeMismatch", + id="auth_dbs_type_objectid", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "authorizedDatabases": datetime.datetime(2024, 1, 1), + }, + error_code=TYPE_MISMATCH_ERROR, + msg="authorizedDatabases with datetime should produce TypeMismatch", + id="auth_dbs_type_datetime", + ), + CommandTestCase( + command={"listDatabases": 1, "authorizedDatabases": Timestamp(0, 0)}, + error_code=TYPE_MISMATCH_ERROR, + msg="authorizedDatabases with Timestamp should produce TypeMismatch", + id="auth_dbs_type_timestamp", + ), + CommandTestCase( + command={"listDatabases": 1, "authorizedDatabases": Binary(b"\x01")}, + error_code=TYPE_MISMATCH_ERROR, + msg="authorizedDatabases with Binary should produce TypeMismatch", + id="auth_dbs_type_binary", + ), + CommandTestCase( + command={"listDatabases": 1, "authorizedDatabases": Regex("^x")}, + error_code=TYPE_MISMATCH_ERROR, + msg="authorizedDatabases with Regex should produce TypeMismatch", + id="auth_dbs_type_regex", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "authorizedDatabases": Code("function(){}"), + }, + error_code=TYPE_MISMATCH_ERROR, + msg="authorizedDatabases with Code should produce TypeMismatch", + id="auth_dbs_type_code", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "authorizedDatabases": Code("function(){}", {"x": 1}), + }, + error_code=TYPE_MISMATCH_ERROR, + msg="authorizedDatabases with CodeWithScope should produce TypeMismatch", + id="auth_dbs_type_code_with_scope", + ), + CommandTestCase( + command={"listDatabases": 1, "authorizedDatabases": MinKey()}, + error_code=TYPE_MISMATCH_ERROR, + msg="authorizedDatabases with MinKey should produce TypeMismatch", + id="auth_dbs_type_minkey", + ), + CommandTestCase( + command={"listDatabases": 1, "authorizedDatabases": MaxKey()}, + error_code=TYPE_MISMATCH_ERROR, + msg="authorizedDatabases with MaxKey should produce TypeMismatch", + id="auth_dbs_type_maxkey", + ), +] + +AUTH_DBS_TESTS: list[CommandTestCase] = AUTH_DBS_SUCCESS_TESTS + AUTH_DBS_TYPE_ERROR_TESTS + + +@pytest.mark.collection_mgmt +@pytest.mark.parametrize("test", pytest_params(AUTH_DBS_TESTS)) +def test_listDatabases_authorized_databases(collection, test): + """Test listDatabases authorizedDatabases parameter behavior.""" + ctx = CommandContext.from_collection(collection) + collection.database.create_collection(collection.name) + result = execute_admin_command(collection, test.build_command(ctx)) + assertResult( + result, + expected=test.build_expected(ctx), + error_code=test.error_code, + msg=test.msg, + raw_res=True, + ) diff --git a/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/test_listDatabases_command_acceptance.py b/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/test_listDatabases_command_acceptance.py new file mode 100644 index 00000000..0ad6d41b --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/test_listDatabases_command_acceptance.py @@ -0,0 +1,409 @@ +"""Tests for listDatabases command field value acceptance.""" + +from __future__ import annotations + +import datetime + +import pytest +from bson import Binary, Code, Decimal128, MaxKey, MinKey, ObjectId, Regex, Timestamp + +from documentdb_tests.compatibility.tests.core.collections.commands.listDatabases.utils.listDatabases_common import ( # noqa: E501 + basic_success, +) +from documentdb_tests.compatibility.tests.core.collections.commands.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.error_codes import ( + API_VERSION_ERROR, + UNRECOGNIZED_COMMAND_FIELD_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 Contains, Eq +from documentdb_tests.framework.test_constants import ( + DECIMAL128_INFINITY, + DECIMAL128_MAX, + DECIMAL128_NAN, + DECIMAL128_NEGATIVE_INFINITY, + DECIMAL128_NEGATIVE_ZERO, + DOUBLE_MAX, + DOUBLE_MIN_SUBNORMAL, + DOUBLE_NEGATIVE_ZERO, + FLOAT_INFINITY, + FLOAT_NAN, + FLOAT_NEGATIVE_INFINITY, + INT32_MAX, + INT32_MIN, + INT64_MAX, + INT64_MIN, +) + +# Property [Null and Missing Behavior]: null literal and omitted +# parameters are accepted and produce a successful response. +NULL_MISSING_TESTS: list[CommandTestCase] = [ + CommandTestCase( + command={"listDatabases": None}, + expected=basic_success, + msg="Null command field value should be accepted", + id="command_value_null", + ), + CommandTestCase( + command={"listDatabases": 1, "filter": None}, + expected=basic_success, + msg="Null filter should be accepted", + id="filter_null", + ), + CommandTestCase( + command={"listDatabases": 1, "nameOnly": None}, + expected=basic_success, + msg="Null nameOnly should be accepted", + id="name_only_null", + ), + CommandTestCase( + command={"listDatabases": 1, "authorizedDatabases": None}, + expected=basic_success, + msg="Null authorizedDatabases should be accepted", + id="authorized_databases_null", + ), + CommandTestCase( + command={"listDatabases": 1, "comment": None}, + expected=basic_success, + msg="Null comment should be accepted", + id="comment_null", + ), + CommandTestCase( + command={"listDatabases": 1}, + expected=basic_success, + msg="Omitting all optional parameters should be accepted", + id="all_omitted", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "filter": None, + "nameOnly": None, + "authorizedDatabases": None, + "comment": None, + }, + expected=basic_success, + msg="All parameters set to null should be accepted", + id="param_all_null", + ), +] + +# Property [Command Field Value Acceptance]: the listDatabases command +# field accepts any BSON type as its value and the value is completely +# ignored. +COMMAND_FIELD_VALUE_TESTS: list[CommandTestCase] = [ + CommandTestCase( + command={"listDatabases": 0}, + expected=basic_success, + msg="int32 zero should be accepted as command field value", + id="cmd_val_int32_zero", + ), + CommandTestCase( + command={"listDatabases": -1}, + expected=basic_success, + msg="int32 negative should be accepted as command field value", + id="cmd_val_int32_neg", + ), + CommandTestCase( + command={"listDatabases": INT32_MAX}, + expected=basic_success, + msg="max 32-bit integer should be accepted as command field value", + id="cmd_val_int32_max", + ), + CommandTestCase( + command={"listDatabases": INT32_MIN}, + expected=basic_success, + msg="min 32-bit integer should be accepted as command field value", + id="cmd_val_int32_min", + ), + CommandTestCase( + command={"listDatabases": INT64_MAX}, + expected=basic_success, + msg="max 64-bit integer should be accepted as command field value", + id="cmd_val_int64_max", + ), + CommandTestCase( + command={"listDatabases": INT64_MIN}, + expected=basic_success, + msg="min 64-bit integer should be accepted as command field value", + id="cmd_val_int64_min", + ), + CommandTestCase( + command={"listDatabases": FLOAT_NAN}, + expected=basic_success, + msg="double NaN should be accepted as command field value", + id="cmd_val_double_nan", + ), + CommandTestCase( + command={"listDatabases": FLOAT_INFINITY}, + expected=basic_success, + msg="double Infinity should be accepted as command field value", + id="cmd_val_double_inf", + ), + CommandTestCase( + command={"listDatabases": FLOAT_NEGATIVE_INFINITY}, + expected=basic_success, + msg="double -Infinity should be accepted as command field value", + id="cmd_val_double_neg_inf", + ), + CommandTestCase( + command={"listDatabases": DOUBLE_NEGATIVE_ZERO}, + expected=basic_success, + msg="double -0.0 should be accepted as command field value", + id="cmd_val_double_neg_zero", + ), + CommandTestCase( + command={"listDatabases": DOUBLE_MAX}, + expected=basic_success, + msg="max double should be accepted as command field value", + id="cmd_val_double_max", + ), + CommandTestCase( + command={"listDatabases": DOUBLE_MIN_SUBNORMAL}, + expected=basic_success, + msg="min subnormal double should be accepted as command field value", + id="cmd_val_double_min_subnormal", + ), + CommandTestCase( + command={"listDatabases": DECIMAL128_NAN}, + expected=basic_success, + msg="Decimal128 NaN should be accepted as command field value", + id="cmd_val_decimal128_nan", + ), + CommandTestCase( + command={"listDatabases": DECIMAL128_INFINITY}, + expected=basic_success, + msg="Decimal128 Infinity should be accepted as command field value", + id="cmd_val_decimal128_inf", + ), + CommandTestCase( + command={"listDatabases": DECIMAL128_NEGATIVE_INFINITY}, + expected=basic_success, + msg="Decimal128 -Infinity should be accepted as command field value", + id="cmd_val_decimal128_neg_inf", + ), + CommandTestCase( + command={"listDatabases": DECIMAL128_NEGATIVE_ZERO}, + expected=basic_success, + msg="Decimal128 -0 should be accepted as command field value", + id="cmd_val_decimal128_neg_zero", + ), + CommandTestCase( + command={"listDatabases": DECIMAL128_MAX}, + expected=basic_success, + msg="Decimal128 max value should be accepted as command field value", + id="cmd_val_decimal128_max", + ), + CommandTestCase( + command={"listDatabases": Decimal128("1234567890123456789012345678901234")}, + expected=basic_success, + msg="Decimal128 max precision should be accepted as command field value", + id="cmd_val_decimal128_max_precision", + ), + CommandTestCase( + command={"listDatabases": True}, + expected=basic_success, + msg="bool true should be accepted as command field value", + id="cmd_val_bool_true", + ), + CommandTestCase( + command={"listDatabases": False}, + expected=basic_success, + msg="bool false should be accepted as command field value", + id="cmd_val_bool_false", + ), + CommandTestCase( + command={"listDatabases": ""}, + expected=basic_success, + msg="empty string should be accepted as command field value", + id="cmd_val_string_empty", + ), + CommandTestCase( + command={"listDatabases": "\u00e9\u00e0\u00fc"}, + expected=basic_success, + msg="unicode string should be accepted as command field value", + id="cmd_val_string_unicode", + ), + CommandTestCase( + command={"listDatabases": "x" * 10_000}, + expected=basic_success, + msg="10K character string should be accepted as command field value", + id="cmd_val_string_10k", + ), + CommandTestCase( + command={"listDatabases": []}, + expected=basic_success, + msg="empty array should be accepted as command field value", + id="cmd_val_array_empty", + ), + CommandTestCase( + command={"listDatabases": [[1, [2, [3]]]]}, + expected=basic_success, + msg="nested array should be accepted as command field value", + id="cmd_val_array_nested", + ), + CommandTestCase( + command={"listDatabases": {}}, + expected=basic_success, + msg="empty object should be accepted as command field value", + id="cmd_val_object_empty", + ), + CommandTestCase( + command={"listDatabases": {"$x": 1}}, + expected=basic_success, + msg="object with dollar-prefixed key should be accepted", + id="cmd_val_object_dollar_key", + ), + CommandTestCase( + command={"listDatabases": {"a": {"b": {"c": 1}}}}, + expected=basic_success, + msg="nested object should be accepted as command field value", + id="cmd_val_object_nested", + ), + CommandTestCase( + command={"listDatabases": ObjectId()}, + expected=basic_success, + msg="ObjectId should be accepted as command field value", + id="cmd_val_objectid", + ), + CommandTestCase( + command={"listDatabases": datetime.datetime(2024, 1, 1)}, + expected=basic_success, + msg="datetime should be accepted as command field value", + id="cmd_val_datetime", + ), + CommandTestCase( + command={"listDatabases": Timestamp(0, 0)}, + expected=basic_success, + msg="Timestamp should be accepted as command field value", + id="cmd_val_timestamp", + ), + CommandTestCase( + command={"listDatabases": Binary(b"\x01\x02")}, + expected=basic_success, + msg="Binary should be accepted as command field value", + id="cmd_val_binary", + ), + CommandTestCase( + command={"listDatabases": Regex("^abc")}, + expected=basic_success, + msg="Regex should be accepted as command field value", + id="cmd_val_regex", + ), + CommandTestCase( + command={"listDatabases": Code("function(){}")}, + expected=basic_success, + msg="Code should be accepted as command field value", + id="cmd_val_code", + ), + CommandTestCase( + command={"listDatabases": Code("function(){}", {"x": 1})}, + expected=basic_success, + msg="CodeWithScope should be accepted as command field value", + id="cmd_val_code_with_scope", + ), + CommandTestCase( + command={"listDatabases": MinKey()}, + expected=basic_success, + msg="MinKey should be accepted as command field value", + id="cmd_val_minkey", + ), + CommandTestCase( + command={"listDatabases": MaxKey()}, + expected=basic_success, + msg="MaxKey should be accepted as command field value", + id="cmd_val_maxkey", + ), +] + +# Property [Unrecognized Field Errors]: unrecognized fields in the +# command document produce an IDLUnknownField error. +UNRECOGNIZED_FIELD_ERROR_TESTS: list[CommandTestCase] = [ + CommandTestCase( + command={"listDatabases": 1, "unknownField": 1}, + error_code=UNRECOGNIZED_COMMAND_FIELD_ERROR, + msg="Unrecognized field should produce IDLUnknownField error", + id="unrecognized_field_simple", + ), + CommandTestCase( + command={"listDatabases": 1, "$badField": 1}, + error_code=UNRECOGNIZED_COMMAND_FIELD_ERROR, + msg="Dollar-prefixed unknown field should produce IDLUnknownField error", + id="unrecognized_field_dollar_prefix", + ), +] + +# Property [API Version Acceptance]: API version 1 with apiStrict and +# apiDeprecationErrors is accepted. +API_VERSION_ACCEPTED_TESTS: list[CommandTestCase] = [ + CommandTestCase( + command={ + "listDatabases": 1, + "apiVersion": "1", + "apiStrict": True, + "apiDeprecationErrors": True, + }, + expected=basic_success, + msg="API version 1 with apiStrict and apiDeprecationErrors should be accepted", + id="api_version_1_strict", + ), +] + +# Property [API Version Errors]: API version "2" produces an +# APIVersionError because only version "1" is accepted. +API_VERSION_ERROR_TESTS: list[CommandTestCase] = [ + CommandTestCase( + command={"listDatabases": 1, "apiVersion": "2"}, + error_code=API_VERSION_ERROR, + msg="API version 2 should produce APIVersionError", + id="api_version_2", + ), +] + +# Property [All Parameters Simultaneously]: all parameters (filter, +# nameOnly, authorizedDatabases, comment) can be set at the same time +# without conflict. +PARAM_INTERACTION_ALL_PARAMS_TESTS: list[CommandTestCase] = [ + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"name": "admin"}, + "nameOnly": True, + "authorizedDatabases": True, + "comment": "test", + }, + expected={"ok": Eq(1.0), "databases": Contains("name", "admin")}, + msg="All parameters set simultaneously should not conflict", + id="param_all_params_simultaneously", + ), +] + +COMMAND_ACCEPTANCE_TESTS: list[CommandTestCase] = ( + NULL_MISSING_TESTS + + COMMAND_FIELD_VALUE_TESTS + + UNRECOGNIZED_FIELD_ERROR_TESTS + + API_VERSION_ACCEPTED_TESTS + + API_VERSION_ERROR_TESTS + + PARAM_INTERACTION_ALL_PARAMS_TESTS +) + + +@pytest.mark.collection_mgmt +@pytest.mark.parametrize("test", pytest_params(COMMAND_ACCEPTANCE_TESTS)) +def test_listDatabases_command_acceptance(collection, test): + """Test listDatabases command field value acceptance.""" + ctx = CommandContext.from_collection(collection) + collection.database.create_collection(collection.name) + result = execute_admin_command(collection, test.build_command(ctx)) + assertResult( + result, + expected=test.build_expected(ctx), + error_code=test.error_code, + msg=test.msg, + raw_res=True, + ) diff --git a/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/test_listDatabases_comment.py b/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/test_listDatabases_comment.py new file mode 100644 index 00000000..7c50e542 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/test_listDatabases_comment.py @@ -0,0 +1,310 @@ +"""Tests for listDatabases comment parameter.""" + +from __future__ import annotations + +import datetime +import functools +from typing import Any + +import pytest +from bson import Binary, Code, MaxKey, MinKey, ObjectId, Regex, Timestamp + +from documentdb_tests.compatibility.tests.core.collections.commands.listDatabases.utils.listDatabases_common import ( # noqa: E501 + basic_success, +) +from documentdb_tests.compatibility.tests.core.collections.commands.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_INFINITY, + DECIMAL128_MAX, + DECIMAL128_NAN, + DECIMAL128_NEGATIVE_INFINITY, + DECIMAL128_NEGATIVE_ZERO, + DOUBLE_MAX, + DOUBLE_MIN_SUBNORMAL, + DOUBLE_NEGATIVE_ZERO, + FLOAT_INFINITY, + FLOAT_NAN, + FLOAT_NEGATIVE_INFINITY, + INT32_MAX, + INT32_MIN, + INT64_MAX, + INT64_MIN, +) + +# Property [comment BSON Type Acceptance]: the comment parameter +# accepts every BSON type without affecting the response. +COMMENT_BSON_TYPE_ACCEPTANCE_TESTS: list[CommandTestCase] = [ + CommandTestCase( + command={"listDatabases": 1, "comment": 0}, + expected=basic_success, + msg="int32 zero comment should be accepted", + id="comment_int32_zero", + ), + CommandTestCase( + command={"listDatabases": 1, "comment": INT32_MAX}, + expected=basic_success, + msg="max 32-bit integer comment should be accepted", + id="comment_int32_max", + ), + CommandTestCase( + command={"listDatabases": 1, "comment": INT32_MIN}, + expected=basic_success, + msg="min 32-bit integer comment should be accepted", + id="comment_int32_min", + ), + CommandTestCase( + command={"listDatabases": 1, "comment": INT64_MAX}, + expected=basic_success, + msg="max 64-bit integer comment should be accepted", + id="comment_int64_max", + ), + CommandTestCase( + command={"listDatabases": 1, "comment": INT64_MIN}, + expected=basic_success, + msg="min 64-bit integer comment should be accepted", + id="comment_int64_min", + ), + CommandTestCase( + command={"listDatabases": 1, "comment": FLOAT_NAN}, + expected=basic_success, + msg="double NaN comment should be accepted", + id="comment_double_nan", + ), + CommandTestCase( + command={"listDatabases": 1, "comment": FLOAT_INFINITY}, + expected=basic_success, + msg="double Infinity comment should be accepted", + id="comment_double_inf", + ), + CommandTestCase( + command={"listDatabases": 1, "comment": FLOAT_NEGATIVE_INFINITY}, + expected=basic_success, + msg="double -Infinity comment should be accepted", + id="comment_double_neg_inf", + ), + CommandTestCase( + command={"listDatabases": 1, "comment": DOUBLE_NEGATIVE_ZERO}, + expected=basic_success, + msg="double -0.0 comment should be accepted", + id="comment_double_neg_zero", + ), + CommandTestCase( + command={"listDatabases": 1, "comment": DOUBLE_MAX}, + expected=basic_success, + msg="max double comment should be accepted", + id="comment_double_max", + ), + CommandTestCase( + command={"listDatabases": 1, "comment": DOUBLE_MIN_SUBNORMAL}, + expected=basic_success, + msg="min subnormal double comment should be accepted", + id="comment_double_min_subnormal", + ), + CommandTestCase( + command={"listDatabases": 1, "comment": DECIMAL128_NAN}, + expected=basic_success, + msg="Decimal128 NaN comment should be accepted", + id="comment_decimal128_nan", + ), + CommandTestCase( + command={"listDatabases": 1, "comment": DECIMAL128_INFINITY}, + expected=basic_success, + msg="Decimal128 Infinity comment should be accepted", + id="comment_decimal128_inf", + ), + CommandTestCase( + command={"listDatabases": 1, "comment": DECIMAL128_NEGATIVE_INFINITY}, + expected=basic_success, + msg="Decimal128 -Infinity comment should be accepted", + id="comment_decimal128_neg_inf", + ), + CommandTestCase( + command={"listDatabases": 1, "comment": DECIMAL128_NEGATIVE_ZERO}, + expected=basic_success, + msg="Decimal128 -0 comment should be accepted", + id="comment_decimal128_neg_zero", + ), + CommandTestCase( + command={"listDatabases": 1, "comment": DECIMAL128_MAX}, + expected=basic_success, + msg="Decimal128 max comment should be accepted", + id="comment_decimal128_max", + ), + CommandTestCase( + command={"listDatabases": 1, "comment": ""}, + expected=basic_success, + msg="empty string comment should be accepted", + id="comment_string_empty", + ), + CommandTestCase( + command={"listDatabases": 1, "comment": "hello\x00world"}, + expected=basic_success, + msg="null-byte string comment should be accepted", + id="comment_string_null_byte", + ), + CommandTestCase( + command={"listDatabases": 1, "comment": "$expr"}, + expected=basic_success, + msg="dollar-prefixed string comment should be accepted", + id="comment_string_dollar_prefixed", + ), + CommandTestCase( + command={"listDatabases": 1, "comment": "x" * 15 * 1_024 * 1_024}, + expected=basic_success, + msg="15MB string comment should be accepted", + id="comment_string_15mb", + ), + CommandTestCase( + command={"listDatabases": 1, "comment": "\u4e16\u754c"}, + expected=basic_success, + msg="CJK string comment should be accepted", + id="comment_string_cjk", + ), + CommandTestCase( + command={"listDatabases": 1, "comment": "\U0001f600"}, + expected=basic_success, + msg="emoji string comment should be accepted", + id="comment_string_emoji", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "comment": "\U0001f468\u200d\U0001f469\u200d\U0001f467\u200d\U0001f466", + }, + expected=basic_success, + msg="ZWJ sequence string comment should be accepted", + id="comment_string_zwj", + ), + CommandTestCase( + command={"listDatabases": 1, "comment": "\ufeff"}, + expected=basic_success, + msg="BOM string comment should be accepted", + id="comment_string_bom", + ), + CommandTestCase( + command={"listDatabases": 1, "comment": "\x01\x02\x03"}, + expected=basic_success, + msg="control chars string comment should be accepted", + id="comment_string_control_chars", + ), + CommandTestCase( + command={"listDatabases": 1, "comment": []}, + expected=basic_success, + msg="empty array comment should be accepted", + id="comment_array_empty", + ), + CommandTestCase( + command={"listDatabases": 1, "comment": list(range(10_000))}, + expected=basic_success, + msg="large array comment should be accepted", + id="comment_array_large", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "comment": functools.reduce( + lambda inner, _: {"level": inner}, range(99), dict[str, Any]({"level": "leaf"}) + ), + }, + expected=basic_success, + msg="deeply nested (100 levels) object comment should be accepted", + id="comment_object_nested_100", + ), + CommandTestCase( + command={"listDatabases": 1, "comment": {}}, + expected=basic_success, + msg="empty object comment should be accepted", + id="comment_object_empty", + ), + CommandTestCase( + command={"listDatabases": 1, "comment": ObjectId()}, + expected=basic_success, + msg="ObjectId comment should be accepted", + id="comment_objectid", + ), + CommandTestCase( + command={"listDatabases": 1, "comment": datetime.datetime(2024, 1, 1)}, + expected=basic_success, + msg="datetime comment should be accepted", + id="comment_datetime", + ), + CommandTestCase( + command={"listDatabases": 1, "comment": Timestamp(0, 0)}, + expected=basic_success, + msg="Timestamp comment should be accepted", + id="comment_timestamp", + ), + CommandTestCase( + command={"listDatabases": 1, "comment": Binary(b"\x01\x02")}, + expected=basic_success, + msg="Binary comment should be accepted", + id="comment_binary", + ), + CommandTestCase( + command={"listDatabases": 1, "comment": Regex("^abc")}, + expected=basic_success, + msg="Regex comment should be accepted", + id="comment_regex", + ), + CommandTestCase( + command={"listDatabases": 1, "comment": Code("function(){}")}, + expected=basic_success, + msg="Code comment should be accepted", + id="comment_code", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "comment": Code("function(){}", {"x": 1}), + }, + expected=basic_success, + msg="CodeWithScope comment should be accepted", + id="comment_code_with_scope", + ), + CommandTestCase( + command={"listDatabases": 1, "comment": MinKey()}, + expected=basic_success, + msg="MinKey comment should be accepted", + id="comment_minkey", + ), + CommandTestCase( + command={"listDatabases": 1, "comment": MaxKey()}, + expected=basic_success, + msg="MaxKey comment should be accepted", + id="comment_maxkey", + ), + CommandTestCase( + command={"listDatabases": 1, "comment": True}, + expected=basic_success, + msg="bool true comment should be accepted", + id="comment_bool_true", + ), + CommandTestCase( + command={"listDatabases": 1, "comment": False}, + expected=basic_success, + msg="bool false comment should be accepted", + id="comment_bool_false", + ), +] + + +@pytest.mark.collection_mgmt +@pytest.mark.parametrize("test", pytest_params(COMMENT_BSON_TYPE_ACCEPTANCE_TESTS)) +def test_listDatabases_comment(collection, test): + """Test listDatabases comment parameter acceptance.""" + ctx = CommandContext.from_collection(collection) + collection.database.create_collection(collection.name) + result = execute_admin_command(collection, test.build_command(ctx)) + assertResult( + result, + expected=test.build_expected(ctx), + error_code=test.error_code, + msg=test.msg, + raw_res=True, + ) diff --git a/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/test_listDatabases_filter_expr.py b/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/test_listDatabases_filter_expr.py new file mode 100644 index 00000000..b0c99930 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/test_listDatabases_filter_expr.py @@ -0,0 +1,193 @@ +"""Tests for listDatabases filter $expr expression support.""" + +from __future__ import annotations + +import pytest + +from documentdb_tests.compatibility.tests.core.collections.commands.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.property_checks import Contains, Eq, Len + +# Property [Filter $expr Expression Support]: $expr in the filter +# supports aggregation expression operators with one representative +# per category: comparison, arithmetic, string, array, set, +# conditional, type, conversion, date, variable, and miscellaneous. +FILTER_EXPR_SUPPORT_TESTS: list[CommandTestCase] = [ + # Comparison. + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"$expr": {"$eq": ["$name", "admin"]}}, + }, + expected={"ok": Eq(1.0), "databases": Contains("name", "admin")}, + msg="$expr with $eq should match by name", + id="expr_eq", + ), + # Arithmetic. + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"$expr": {"$gt": [{"$add": ["$sizeOnDisk", 1]}, 0]}}, + }, + expected={"ok": Eq(1.0), "databases": Contains("name", "admin")}, + msg="$expr with $add should compute sum on sizeOnDisk", + id="expr_add", + ), + # String. + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"$expr": {"$eq": [{"$concat": ["$name", "_x"]}, "admin_x"]}}, + }, + expected={"ok": Eq(1.0), "databases": Contains("name", "admin")}, + msg="$expr with $concat should build and compare strings", + id="expr_concat", + ), + # Regex. + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"$expr": {"$regexMatch": {"input": "$name", "regex": "^ad"}}}, + }, + expected={"ok": Eq(1.0), "databases": Contains("name", "admin")}, + msg="$expr with $regexMatch should match patterns", + id="expr_regex_match", + ), + # Array. + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"$expr": {"$eq": [{"$size": {"$range": [0, 5]}}, 5]}}, + }, + expected={"ok": Eq(1.0), "databases": Contains("name", "admin")}, + msg="$expr with $size and $range should work on arrays", + id="expr_array", + ), + # Set. + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"$expr": {"$setEquals": [[1, 2], [2, 1]]}}, + }, + expected={"ok": Eq(1.0), "databases": Contains("name", "admin")}, + msg="$expr with $setEquals should compare sets", + id="expr_set_equals", + ), + # Conditional. + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"$expr": {"$cond": [{"$eq": ["$name", "admin"]}, True, False]}}, + }, + expected={"ok": Eq(1.0), "databases": Contains("name", "admin")}, + msg="$expr with $cond should evaluate conditional branches", + id="expr_cond", + ), + # Type. + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"$expr": {"$eq": [{"$type": "$name"}, "string"]}}, + }, + expected={"ok": Eq(1.0), "databases": Contains("name", "admin")}, + msg="$expr with $type should return the BSON type name", + id="expr_type", + ), + # Conversion. + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"$expr": {"$eq": [{"$toString": 1}, "1"]}}, + }, + expected={"ok": Eq(1.0), "databases": Contains("name", "admin")}, + msg="$expr with $toString should convert to string", + id="expr_to_string", + ), + # Date. + CommandTestCase( + command={ + "listDatabases": 1, + "filter": { + "$expr": {"$gt": [{"$year": {"$toDate": "2024-01-15"}}, 0]}, + }, + }, + expected={"ok": Eq(1.0), "databases": Contains("name", "admin")}, + msg="$expr with $year should extract year from date", + id="expr_year", + ), + # Variable. + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"$expr": {"$eq": ["$$CURRENT.name", "admin"]}}, + }, + expected={"ok": Eq(1.0), "databases": Contains("name", "admin")}, + msg="$$CURRENT should reference the current document in $expr", + id="expr_current", + ), + # $let. + CommandTestCase( + command={ + "listDatabases": 1, + "filter": { + "$expr": { + "$let": { + "vars": {"target": "admin"}, + "in": {"$eq": ["$name", "$$target"]}, + } + } + }, + }, + expected={"ok": Eq(1.0), "databases": Contains("name", "admin")}, + msg="$let with user-defined variables should work in $expr", + id="expr_let", + ), + # $literal. + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"$expr": {"$eq": [{"$literal": "$name"}, "$name"]}}, + }, + expected={"ok": Eq(1.0), "databases": Len(0)}, + msg="$literal string should not equal the resolved field value", + id="expr_literal_vs_field", + ), + # Miscellaneous. + CommandTestCase( + command={ + "listDatabases": 1, + "filter": { + "$expr": { + "$eq": [ + {"$type": {"$mergeObjects": [{"a": 1}, {"b": 2}]}}, + "object", + ], + }, + }, + }, + expected={"ok": Eq(1.0), "databases": Contains("name", "admin")}, + msg="$expr with $mergeObjects should merge documents", + id="expr_merge_objects", + ), +] + + +@pytest.mark.collection_mgmt +@pytest.mark.parametrize("test", pytest_params(FILTER_EXPR_SUPPORT_TESTS)) +def test_listDatabases_filter_expr(collection, test): + """Test listDatabases filter $expr expression support.""" + ctx = CommandContext.from_collection(collection) + collection.database.create_collection(collection.name) + result = execute_admin_command(collection, test.build_command(ctx)) + assertResult( + result, + expected=test.build_expected(ctx), + error_code=test.error_code, + msg=test.msg, + raw_res=True, + ) diff --git a/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/test_listDatabases_filter_predicates.py b/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/test_listDatabases_filter_predicates.py new file mode 100644 index 00000000..744f36e0 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/test_listDatabases_filter_predicates.py @@ -0,0 +1,373 @@ +"""Tests for listDatabases filter query predicates.""" + +from __future__ import annotations + +import functools +from typing import Any + +import pytest +from bson import Regex + +from documentdb_tests.compatibility.tests.core.collections.commands.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.property_checks import Contains, Eq, Len, NotContains + +# Property [Filter Query Predicate Support]: the filter parameter +# accepts standard query predicates that correctly select databases by +# matching against per-database fields (name, sizeOnDisk, empty), and +# returns empty results for unknown or response-level fields. +FILTER_QUERY_PREDICATE_TESTS: list[CommandTestCase] = [ + CommandTestCase( + command={"listDatabases": 1, "filter": {}}, + expected={"ok": Eq(1.0), "databases": Contains("name", "admin")}, + msg="Empty filter should return all databases", + id="filter_empty_doc", + ), + CommandTestCase( + command={"listDatabases": 1, "filter": {"name": "admin"}}, + expected={"ok": Eq(1.0), "databases": Contains("name", "admin")}, + msg="Implicit $eq on name should match", + id="filter_eq_name", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"name": {"$ne": "admin"}}, + }, + expected={"ok": Eq(1.0), "databases": NotContains("name", "admin")}, + msg="$ne should exclude the matched database", + id="filter_ne_name", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"name": {"$gt": "config"}}, + }, + expected={"ok": Eq(1.0), "databases": Contains("name", "local")}, + msg="$gt should return databases with name after config", + id="filter_gt_name", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"name": {"$gte": "config"}}, + }, + expected={"ok": Eq(1.0), "databases": NotContains("name", "admin")}, + msg="$gte should include the boundary value", + id="filter_gte_name", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"name": {"$lt": "config"}}, + }, + expected={"ok": Eq(1.0), "databases": Contains("name", "admin")}, + msg="$lt should return databases with name before config", + id="filter_lt_name", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"name": {"$lte": "config"}}, + }, + expected={"ok": Eq(1.0), "databases": Contains("name", "config")}, + msg="$lte should include the boundary value", + id="filter_lte_name", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"name": {"$in": ["admin", "local"]}}, + }, + expected={"ok": Eq(1.0), "databases": Contains("name", "local")}, + msg="$in should match databases in the list", + id="filter_in_name", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"name": {"$nin": ["admin"]}}, + }, + expected={"ok": Eq(1.0), "databases": NotContains("name", "admin")}, + msg="$nin should exclude databases in the list", + id="filter_nin_name", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"name": {"$exists": True}}, + }, + expected={"ok": Eq(1.0), "databases": Contains("name", "admin")}, + msg="$exists true on name should match all databases", + id="filter_exists_name", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"sizeOnDisk": {"$type": "number"}}, + }, + expected={"ok": Eq(1.0), "databases": Contains("name", "admin")}, + msg="$type number should match Int64 sizeOnDisk", + id="filter_type_number_sizeondisk", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"name": {"$type": -1}}, + }, + expected={"ok": Eq(1.0), "databases": Len(0)}, + msg="$type with negative int should silently return no results", + id="filter_type_negative", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"name": {"$regex": "^ad"}}, + }, + expected={"ok": Eq(1.0), "databases": Contains("name", "admin")}, + msg="$regex should match name field", + id="filter_regex_name", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"name": {"$not": {"$eq": "admin"}}}, + }, + expected={"ok": Eq(1.0), "databases": NotContains("name", "admin")}, + msg="$not should negate the inner predicate", + id="filter_not_eq_name", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"sizeOnDisk": {"$mod": [2, 0]}}, + }, + expected={"ok": Eq(1.0), "databases": Contains("name", "admin")}, + msg="$mod should work on sizeOnDisk", + id="filter_mod_sizeondisk", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"name": {"$all": ["admin"]}}, + }, + expected={"ok": Eq(1.0), "databases": Contains("name", "admin")}, + msg="$all on scalar name field should work", + id="filter_all_name", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"name": {"$elemMatch": {"$eq": "admin"}}}, + }, + expected={"ok": Eq(1.0), "databases": Len(0)}, + msg="$elemMatch on scalar name should return 0 results", + id="filter_elemmatch_scalar", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"name": {"$size": 1}}, + }, + expected={"ok": Eq(1.0), "databases": Len(0)}, + msg="$size on scalar name should return 0 results", + id="filter_size_scalar", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"sizeOnDisk": {"$bitsAllSet": 0}}, + }, + expected={"ok": Eq(1.0), "databases": Contains("name", "admin")}, + msg="$bitsAllSet with 0 should match all sizeOnDisk values", + id="filter_bitsallset_sizeondisk", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"sizeOnDisk": {"$bitsAnySet": 0}}, + }, + expected={"ok": Eq(1.0), "databases": Len(0)}, + msg="$bitsAnySet with 0 should match no sizeOnDisk values", + id="filter_bitsanyset_sizeondisk", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"sizeOnDisk": {"$bitsAllClear": 0}}, + }, + expected={"ok": Eq(1.0), "databases": Contains("name", "admin")}, + msg="$bitsAllClear with 0 should match all sizeOnDisk values", + id="filter_bitsallclear_sizeondisk", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"sizeOnDisk": {"$bitsAnyClear": 0}}, + }, + expected={"ok": Eq(1.0), "databases": Len(0)}, + msg="$bitsAnyClear with 0 should match no sizeOnDisk values", + id="filter_bitsanyclear_sizeondisk", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"$jsonSchema": {"properties": {"name": {"pattern": "^admin$"}}}}, + }, + expected={"ok": Eq(1.0), "databases": Contains("name", "admin")}, + msg="$jsonSchema should filter by name pattern", + id="filter_jsonschema", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"name": "admin", "$comment": "test"}, + }, + expected={"ok": Eq(1.0), "databases": Contains("name", "admin")}, + msg="$comment in filter should not affect results", + id="filter_comment", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"unknownField": "x"}, + }, + expected={"ok": Eq(1.0), "databases": Len(0)}, + msg="Unknown field in filter should return empty result", + id="filter_unknown_field", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"totalSize": 0}, + }, + expected={"ok": Eq(1.0), "databases": Len(0)}, + msg="Response-level field totalSize should return empty result", + id="filter_response_level_field", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"empty": False}, + }, + expected={"ok": Eq(1.0), "databases": Contains("name", "admin")}, + msg="Filtering on empty field should work", + id="filter_empty_field", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"name": {"$gte": "admin"}, "empty": False}, + }, + expected={"ok": Eq(1.0), "databases": Contains("name", "admin")}, + msg="Implicit $and with multiple fields should work", + id="filter_compound_implicit_and", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "filter": { + "$and": [ + {"name": {"$gte": "admin"}}, + {"empty": False}, + ] + }, + }, + expected={"ok": Eq(1.0), "databases": Contains("name", "admin")}, + msg="Explicit $and should work", + id="filter_compound_explicit_and", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"$or": [{"name": "admin"}, {"name": "local"}]}, + }, + expected={"ok": Eq(1.0), "databases": Contains("name", "local")}, + msg="$or should return databases matching either condition", + id="filter_compound_or", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"$nor": [{"name": "admin"}]}, + }, + expected={"ok": Eq(1.0), "databases": NotContains("name", "admin")}, + msg="$nor should exclude databases matching the condition", + id="filter_compound_nor", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "filter": functools.reduce( + lambda inner, _: {"$and": [inner]}, range(99), dict[str, Any]({"name": "admin"}) + ), + }, + expected={"ok": Eq(1.0), "databases": Contains("name", "admin")}, + msg="Nesting depth up to 99 levels should be accepted", + id="filter_nesting_99", + ), +] + +# Property [Filter Regex Support]: regex filtering works with $regex +# string, BSON Regex object, and case-insensitive $options. +FILTER_REGEX_TESTS: list[CommandTestCase] = [ + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"name": {"$regex": "^ad"}}, + }, + expected={"ok": Eq(1.0), "databases": Contains("name", "admin")}, + msg="$regex string should match the name field", + id="regex_string", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"name": Regex("^ad")}, + }, + expected={"ok": Eq(1.0), "databases": Contains("name", "admin")}, + msg="BSON Regex object should match the name field", + id="regex_bson_object", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"name": {"$regex": "^ADMIN", "$options": "i"}}, + }, + expected={"ok": Eq(1.0), "databases": Contains("name", "admin")}, + msg="$options 'i' should enable case-insensitive matching", + id="regex_options_i", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"sizeOnDisk": {"$regex": ".*"}}, + }, + expected={"ok": Eq(1.0), "databases": Len(0)}, + msg="$regex on non-string field should return 0 results", + id="regex_non_string_field", + ), +] + +FILTER_PREDICATE_TESTS: list[CommandTestCase] = FILTER_QUERY_PREDICATE_TESTS + FILTER_REGEX_TESTS + + +@pytest.mark.collection_mgmt +@pytest.mark.parametrize("test", pytest_params(FILTER_PREDICATE_TESTS)) +def test_listDatabases_filter_predicates(collection, test): + """Test listDatabases filter query predicate support.""" + ctx = CommandContext.from_collection(collection) + collection.database.create_collection(collection.name) + result = execute_admin_command(collection, test.build_command(ctx)) + assertResult( + result, + expected=test.build_expected(ctx), + error_code=test.error_code, + msg=test.msg, + raw_res=True, + ) diff --git a/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/test_listDatabases_max_time_ms.py b/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/test_listDatabases_max_time_ms.py new file mode 100644 index 00000000..ec7e99b6 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/test_listDatabases_max_time_ms.py @@ -0,0 +1,310 @@ +"""Tests for listDatabases maxTimeMS parameter.""" + +from __future__ import annotations + +import datetime + +import pytest +from bson import Binary, Code, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp + +from documentdb_tests.compatibility.tests.core.collections.commands.listDatabases.utils.listDatabases_common import ( # noqa: E501 + basic_success, +) +from documentdb_tests.compatibility.tests.core.collections.commands.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.error_codes import ( + BAD_VALUE_ERROR, + FAILED_TO_PARSE_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_constants import ( + DECIMAL128_INFINITY, + DECIMAL128_NAN, + DECIMAL128_NEGATIVE_INFINITY, + DECIMAL128_NEGATIVE_ZERO, + DECIMAL128_ONE_AND_HALF, + DOUBLE_NEGATIVE_ZERO, + FLOAT_INFINITY, + FLOAT_NAN, + FLOAT_NEGATIVE_INFINITY, + INT32_MAX, + INT64_ZERO, +) + +# Property [maxTimeMS Accepted Values]: maxTimeMS accepts int32, +# Int64, double, and Decimal128 whole numbers up to INT32_MAX, +# including negative-zero representations. +MAX_TIME_MS_ACCEPTED_TESTS: list[CommandTestCase] = [ + CommandTestCase( + command={"listDatabases": 1, "maxTimeMS": 1_000}, + expected=basic_success, + msg="maxTimeMS with int32 should be accepted", + id="max_time_ms_int32", + ), + CommandTestCase( + command={"listDatabases": 1, "maxTimeMS": INT64_ZERO}, + expected=basic_success, + msg="maxTimeMS with Int64 zero should be accepted", + id="max_time_ms_int64", + ), + CommandTestCase( + command={"listDatabases": 1, "maxTimeMS": 1_000.0}, + expected=basic_success, + msg="maxTimeMS with double should be accepted", + id="max_time_ms_double", + ), + CommandTestCase( + command={"listDatabases": 1, "maxTimeMS": Decimal128("1000")}, + expected=basic_success, + msg="maxTimeMS with Decimal128 should be accepted", + id="max_time_ms_decimal128", + ), + CommandTestCase( + command={"listDatabases": 1, "maxTimeMS": INT32_MAX}, + expected=basic_success, + msg="maxTimeMS with max 32-bit integer should be accepted", + id="max_time_ms_int32_max", + ), + CommandTestCase( + command={"listDatabases": 1, "maxTimeMS": DOUBLE_NEGATIVE_ZERO}, + expected=basic_success, + msg="maxTimeMS with -0.0 should be accepted as zero", + id="max_time_ms_neg_zero", + ), + CommandTestCase( + command={"listDatabases": 1, "maxTimeMS": DECIMAL128_NEGATIVE_ZERO}, + expected=basic_success, + msg="maxTimeMS with Decimal128 -0 should be accepted as zero", + id="max_time_ms_decimal128_neg_zero", + ), +] + +# Property [maxTimeMS Type Errors]: maxTimeMS rejects non-numeric +# BSON types with a TypeMismatch error. +MAX_TIME_MS_TYPE_ERROR_TESTS: list[CommandTestCase] = [ + CommandTestCase( + command={"listDatabases": 1, "maxTimeMS": True}, + error_code=TYPE_MISMATCH_ERROR, + msg="maxTimeMS with bool should produce TypeMismatch", + id="max_time_ms_bool_true", + ), + CommandTestCase( + command={"listDatabases": 1, "maxTimeMS": "abc"}, + error_code=TYPE_MISMATCH_ERROR, + msg="maxTimeMS with string should produce TypeMismatch", + id="max_time_ms_string", + ), + CommandTestCase( + command={"listDatabases": 1, "maxTimeMS": [1]}, + error_code=TYPE_MISMATCH_ERROR, + msg="maxTimeMS with array should produce TypeMismatch", + id="max_time_ms_array", + ), + CommandTestCase( + command={"listDatabases": 1, "maxTimeMS": {}}, + error_code=TYPE_MISMATCH_ERROR, + msg="maxTimeMS with object should produce TypeMismatch", + id="max_time_ms_object", + ), + CommandTestCase( + command={"listDatabases": 1, "maxTimeMS": ObjectId()}, + error_code=TYPE_MISMATCH_ERROR, + msg="maxTimeMS with ObjectId should produce TypeMismatch", + id="max_time_ms_objectid", + ), + CommandTestCase( + command={"listDatabases": 1, "maxTimeMS": datetime.datetime(2024, 1, 1)}, + error_code=TYPE_MISMATCH_ERROR, + msg="maxTimeMS with datetime should produce TypeMismatch", + id="max_time_ms_datetime", + ), + CommandTestCase( + command={"listDatabases": 1, "maxTimeMS": Timestamp(1, 1)}, + error_code=TYPE_MISMATCH_ERROR, + msg="maxTimeMS with Timestamp should produce TypeMismatch", + id="max_time_ms_timestamp", + ), + CommandTestCase( + command={"listDatabases": 1, "maxTimeMS": Binary(b"\x01")}, + error_code=TYPE_MISMATCH_ERROR, + msg="maxTimeMS with Binary should produce TypeMismatch", + id="max_time_ms_binary", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "maxTimeMS": Binary( + b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10", + subtype=4, + ), + }, + error_code=TYPE_MISMATCH_ERROR, + msg="maxTimeMS with Binary UUID should produce TypeMismatch", + id="max_time_ms_binary_uuid", + ), + CommandTestCase( + command={"listDatabases": 1, "maxTimeMS": Regex(".*")}, + error_code=TYPE_MISMATCH_ERROR, + msg="maxTimeMS with Regex should produce TypeMismatch", + id="max_time_ms_regex", + ), + CommandTestCase( + command={"listDatabases": 1, "maxTimeMS": Code("function(){}")}, + error_code=TYPE_MISMATCH_ERROR, + msg="maxTimeMS with Code should produce TypeMismatch", + id="max_time_ms_code", + ), + CommandTestCase( + command={"listDatabases": 1, "maxTimeMS": Code("function(){}", {"x": 1})}, + error_code=TYPE_MISMATCH_ERROR, + msg="maxTimeMS with CodeWithScope should produce TypeMismatch", + id="max_time_ms_code_with_scope", + ), + CommandTestCase( + command={"listDatabases": 1, "maxTimeMS": MinKey()}, + error_code=TYPE_MISMATCH_ERROR, + msg="maxTimeMS with MinKey should produce TypeMismatch", + id="max_time_ms_minkey", + ), + CommandTestCase( + command={"listDatabases": 1, "maxTimeMS": MaxKey()}, + error_code=TYPE_MISMATCH_ERROR, + msg="maxTimeMS with MaxKey should produce TypeMismatch", + id="max_time_ms_maxkey", + ), +] + +# Property [maxTimeMS Fractional and Non-Finite Errors]: maxTimeMS +# rejects fractional values, NaN, and Infinity with a parse error. +MAX_TIME_MS_FRACTIONAL_ERROR_TESTS: list[CommandTestCase] = [ + CommandTestCase( + command={"listDatabases": 1, "maxTimeMS": 1.5}, + error_code=FAILED_TO_PARSE_ERROR, + msg="maxTimeMS with fractional double should produce FailedToParse", + id="max_time_ms_fractional_double", + ), + CommandTestCase( + command={"listDatabases": 1, "maxTimeMS": DECIMAL128_ONE_AND_HALF}, + error_code=FAILED_TO_PARSE_ERROR, + msg="maxTimeMS with fractional Decimal128 should produce FailedToParse", + id="max_time_ms_fractional_decimal128", + ), + CommandTestCase( + command={"listDatabases": 1, "maxTimeMS": FLOAT_NAN}, + error_code=FAILED_TO_PARSE_ERROR, + msg="maxTimeMS with NaN should produce FailedToParse", + id="max_time_ms_nan", + ), + CommandTestCase( + command={"listDatabases": 1, "maxTimeMS": FLOAT_INFINITY}, + error_code=FAILED_TO_PARSE_ERROR, + msg="maxTimeMS with Infinity should produce FailedToParse", + id="max_time_ms_infinity", + ), + CommandTestCase( + command={"listDatabases": 1, "maxTimeMS": FLOAT_NEGATIVE_INFINITY}, + error_code=FAILED_TO_PARSE_ERROR, + msg="maxTimeMS with -Infinity should produce FailedToParse", + id="max_time_ms_neg_infinity", + ), + CommandTestCase( + command={"listDatabases": 1, "maxTimeMS": DECIMAL128_NAN}, + error_code=FAILED_TO_PARSE_ERROR, + msg="maxTimeMS with Decimal128 NaN should produce FailedToParse", + id="max_time_ms_decimal128_nan", + ), + CommandTestCase( + command={"listDatabases": 1, "maxTimeMS": DECIMAL128_INFINITY}, + error_code=FAILED_TO_PARSE_ERROR, + msg="maxTimeMS with Decimal128 Infinity should produce FailedToParse", + id="max_time_ms_decimal128_infinity", + ), + CommandTestCase( + command={"listDatabases": 1, "maxTimeMS": DECIMAL128_NEGATIVE_INFINITY}, + error_code=FAILED_TO_PARSE_ERROR, + msg="maxTimeMS with Decimal128 -Infinity should produce FailedToParse", + id="max_time_ms_decimal128_neg_infinity", + ), +] + +# Property [maxTimeMS Negative and Overflow Errors]: maxTimeMS +# rejects negative finite values and values exceeding INT32_MAX with a +# bad value error. +MAX_TIME_MS_NEGATIVE_ERROR_TESTS: list[CommandTestCase] = [ + CommandTestCase( + command={"listDatabases": 1, "maxTimeMS": -1}, + error_code=BAD_VALUE_ERROR, + msg="maxTimeMS with negative int32 should produce BadValue", + id="max_time_ms_neg_int32", + ), + CommandTestCase( + command={"listDatabases": 1, "maxTimeMS": Int64(-100)}, + error_code=BAD_VALUE_ERROR, + msg="maxTimeMS with negative Int64 should produce BadValue", + id="max_time_ms_neg_int64", + ), + CommandTestCase( + command={"listDatabases": 1, "maxTimeMS": -1.0}, + error_code=BAD_VALUE_ERROR, + msg="maxTimeMS with negative double should produce BadValue", + id="max_time_ms_neg_double", + ), + CommandTestCase( + command={"listDatabases": 1, "maxTimeMS": Decimal128("-1")}, + error_code=BAD_VALUE_ERROR, + msg="maxTimeMS with negative Decimal128 should produce BadValue", + id="max_time_ms_neg_decimal128", + ), + CommandTestCase( + command={"listDatabases": 1, "maxTimeMS": INT32_MAX + 1}, + error_code=BAD_VALUE_ERROR, + msg="maxTimeMS exceeding INT32_MAX as int should produce BadValue", + id="max_time_ms_exceeds_int32_max_int", + ), + CommandTestCase( + command={"listDatabases": 1, "maxTimeMS": Int64(INT32_MAX + 1)}, + error_code=BAD_VALUE_ERROR, + msg="maxTimeMS exceeding INT32_MAX as Int64 should produce BadValue", + id="max_time_ms_exceeds_int32_max_int64", + ), + CommandTestCase( + command={"listDatabases": 1, "maxTimeMS": float(INT32_MAX + 1)}, + error_code=BAD_VALUE_ERROR, + msg="maxTimeMS exceeding INT32_MAX as double should produce BadValue", + id="max_time_ms_exceeds_int32_max_double", + ), + CommandTestCase( + command={"listDatabases": 1, "maxTimeMS": Decimal128(str(INT32_MAX + 1))}, + error_code=BAD_VALUE_ERROR, + msg="maxTimeMS exceeding INT32_MAX as Decimal128 should produce BadValue", + id="max_time_ms_exceeds_int32_max_decimal128", + ), +] + +MAX_TIME_MS_TESTS: list[CommandTestCase] = ( + MAX_TIME_MS_ACCEPTED_TESTS + + MAX_TIME_MS_TYPE_ERROR_TESTS + + MAX_TIME_MS_FRACTIONAL_ERROR_TESTS + + MAX_TIME_MS_NEGATIVE_ERROR_TESTS +) + + +@pytest.mark.collection_mgmt +@pytest.mark.parametrize("test", pytest_params(MAX_TIME_MS_TESTS)) +def test_listDatabases_max_time_ms(collection, test): + """Test listDatabases maxTimeMS parameter behavior.""" + ctx = CommandContext.from_collection(collection) + collection.database.create_collection(collection.name) + result = execute_admin_command(collection, test.build_command(ctx)) + assertResult( + result, + expected=test.build_expected(ctx), + error_code=test.error_code, + msg=test.msg, + raw_res=True, + ) diff --git a/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/test_listDatabases_nameonly.py b/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/test_listDatabases_nameonly.py new file mode 100644 index 00000000..0d2d9a12 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/test_listDatabases_nameonly.py @@ -0,0 +1,411 @@ +"""Tests for listDatabases nameOnly parameter.""" + +from __future__ import annotations + +import datetime + +import pytest +from bson import Binary, Code, Decimal128, MaxKey, MinKey, ObjectId, Regex, Timestamp + +from documentdb_tests.compatibility.tests.core.collections.commands.listDatabases.utils.listDatabases_common import ( # noqa: E501 + full_structure_success, + name_only_success, +) +from documentdb_tests.compatibility.tests.core.collections.commands.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertResult +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 Contains, Eq, Len +from documentdb_tests.framework.test_constants import ( + DECIMAL128_INFINITY, + DECIMAL128_MAX, + DECIMAL128_MIN_POSITIVE, + DECIMAL128_NAN, + DECIMAL128_NEGATIVE_INFINITY, + DECIMAL128_NEGATIVE_ZERO, + DECIMAL128_ZERO, + DOUBLE_MIN_SUBNORMAL, + DOUBLE_NEGATIVE_ZERO, + DOUBLE_ZERO, + FLOAT_INFINITY, + FLOAT_NAN, + FLOAT_NEGATIVE_INFINITY, + INT32_MAX, + INT32_MIN, + INT64_MAX, + INT64_MIN, + INT64_ZERO, +) + +# Property [nameOnly Response Structure]: when nameOnly is true, the +# response contains only databases and ok, and each database entry +# contains only the name field. +name_only_success_TESTS: list[CommandTestCase] = [ + CommandTestCase( + command={"listDatabases": 1, "nameOnly": True}, + expected=name_only_success, + msg="nameOnly=true should produce only databases and ok at top level", + id="name_only_top_level_keys", + ), +] + +# Property [nameOnly Boolean Coercion - Falsy]: zero numeric values +# passed to nameOnly are coerced to false, producing the full response +# with size information. +NAME_ONLY_FALSY_TESTS: list[CommandTestCase] = [ + CommandTestCase( + command={"listDatabases": 1, "nameOnly": 0}, + expected=full_structure_success, + msg="int32 0 should be falsy for nameOnly", + id="name_only_int32_zero", + ), + CommandTestCase( + command={"listDatabases": 1, "nameOnly": INT64_ZERO}, + expected=full_structure_success, + msg="Int64 0 should be falsy for nameOnly", + id="name_only_int64_zero", + ), + CommandTestCase( + command={"listDatabases": 1, "nameOnly": DOUBLE_ZERO}, + expected=full_structure_success, + msg="double 0.0 should be falsy for nameOnly", + id="name_only_double_zero", + ), + CommandTestCase( + command={"listDatabases": 1, "nameOnly": DOUBLE_NEGATIVE_ZERO}, + expected=full_structure_success, + msg="double -0.0 should be falsy for nameOnly", + id="name_only_double_neg_zero", + ), + CommandTestCase( + command={"listDatabases": 1, "nameOnly": DECIMAL128_ZERO}, + expected=full_structure_success, + msg="Decimal128 '0' should be falsy for nameOnly", + id="name_only_decimal128_zero", + ), + CommandTestCase( + command={"listDatabases": 1, "nameOnly": DECIMAL128_NEGATIVE_ZERO}, + expected=full_structure_success, + msg="Decimal128 '-0' should be falsy for nameOnly", + id="name_only_decimal128_neg_zero", + ), + CommandTestCase( + command={"listDatabases": 1, "nameOnly": Decimal128("-0.0")}, + expected=full_structure_success, + msg="Decimal128 '-0.0' should be falsy for nameOnly", + id="name_only_decimal128_neg_zero_dot", + ), + CommandTestCase( + command={"listDatabases": 1, "nameOnly": Decimal128("-0E+10")}, + expected=full_structure_success, + msg="Decimal128 '-0E+10' should be falsy for nameOnly", + id="name_only_decimal128_neg_zero_exp", + ), + CommandTestCase( + command={"listDatabases": 1, "nameOnly": Decimal128("0E-10")}, + expected=full_structure_success, + msg="Decimal128 '0E-10' should be falsy for nameOnly", + id="name_only_decimal128_zero_neg_exp", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "nameOnly": Decimal128("0." + "0" * 32), + }, + expected=full_structure_success, + msg="Decimal128 '0.000...0' (32 trailing zeros) should be falsy", + id="name_only_decimal128_zero_trailing", + ), +] + +# Property [nameOnly Boolean Coercion - Truthy]: all non-zero numeric +# values passed to nameOnly are coerced to true, producing the +# name-only response without size information. +NAME_ONLY_TRUTHY_TESTS: list[CommandTestCase] = [ + CommandTestCase( + command={"listDatabases": 1, "nameOnly": 1}, + expected=name_only_success, + msg="int32 1 should be truthy for nameOnly", + id="name_only_int32_one", + ), + CommandTestCase( + command={"listDatabases": 1, "nameOnly": INT32_MAX}, + expected=name_only_success, + msg="max 32-bit integer should be truthy for nameOnly", + id="name_only_int32_max", + ), + CommandTestCase( + command={"listDatabases": 1, "nameOnly": INT32_MIN}, + expected=name_only_success, + msg="min 32-bit integer should be truthy for nameOnly", + id="name_only_int32_min", + ), + CommandTestCase( + command={"listDatabases": 1, "nameOnly": INT64_MAX}, + expected=name_only_success, + msg="max 64-bit integer should be truthy for nameOnly", + id="name_only_int64_max", + ), + CommandTestCase( + command={"listDatabases": 1, "nameOnly": INT64_MIN}, + expected=name_only_success, + msg="min 64-bit integer should be truthy for nameOnly", + id="name_only_int64_min", + ), + CommandTestCase( + command={"listDatabases": 1, "nameOnly": FLOAT_NAN}, + expected=name_only_success, + msg="double NaN should be truthy for nameOnly", + id="name_only_double_nan", + ), + CommandTestCase( + command={"listDatabases": 1, "nameOnly": FLOAT_INFINITY}, + expected=name_only_success, + msg="double Infinity should be truthy for nameOnly", + id="name_only_double_inf", + ), + CommandTestCase( + command={"listDatabases": 1, "nameOnly": FLOAT_NEGATIVE_INFINITY}, + expected=name_only_success, + msg="double -Infinity should be truthy for nameOnly", + id="name_only_double_neg_inf", + ), + CommandTestCase( + command={"listDatabases": 1, "nameOnly": DOUBLE_MIN_SUBNORMAL}, + expected=name_only_success, + msg="min subnormal double should be truthy for nameOnly", + id="name_only_double_subnormal", + ), + CommandTestCase( + command={"listDatabases": 1, "nameOnly": 0.5}, + expected=name_only_success, + msg="double 0.5 should be truthy for nameOnly", + id="name_only_double_half", + ), + CommandTestCase( + command={"listDatabases": 1, "nameOnly": 0.1}, + expected=name_only_success, + msg="double 0.1 should be truthy for nameOnly", + id="name_only_double_tenth", + ), + CommandTestCase( + command={"listDatabases": 1, "nameOnly": DECIMAL128_NAN}, + expected=name_only_success, + msg="Decimal128 NaN should be truthy for nameOnly", + id="name_only_decimal128_nan", + ), + CommandTestCase( + command={"listDatabases": 1, "nameOnly": DECIMAL128_INFINITY}, + expected=name_only_success, + msg="Decimal128 Infinity should be truthy for nameOnly", + id="name_only_decimal128_inf", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "nameOnly": DECIMAL128_NEGATIVE_INFINITY, + }, + expected=name_only_success, + msg="Decimal128 -Infinity should be truthy for nameOnly", + id="name_only_decimal128_neg_inf", + ), + CommandTestCase( + command={"listDatabases": 1, "nameOnly": DECIMAL128_MAX}, + expected=name_only_success, + msg="Decimal128 max value should be truthy for nameOnly", + id="name_only_decimal128_max", + ), + CommandTestCase( + command={"listDatabases": 1, "nameOnly": DECIMAL128_MIN_POSITIVE}, + expected=name_only_success, + msg="Decimal128 min positive should be truthy for nameOnly", + id="name_only_decimal128_min_positive", + ), +] + +# Property [nameOnly Type Strictness]: nameOnly rejects non-bool, +# non-numeric types with a TypeMismatch error. +NAME_ONLY_TYPE_ERROR_TESTS: list[CommandTestCase] = [ + CommandTestCase( + command={"listDatabases": 1, "nameOnly": "true"}, + error_code=TYPE_MISMATCH_ERROR, + msg="nameOnly with string should produce TypeMismatch", + id="name_only_type_string", + ), + CommandTestCase( + command={"listDatabases": 1, "nameOnly": []}, + error_code=TYPE_MISMATCH_ERROR, + msg="nameOnly with array should produce TypeMismatch", + id="name_only_type_array", + ), + CommandTestCase( + command={"listDatabases": 1, "nameOnly": {}}, + error_code=TYPE_MISMATCH_ERROR, + msg="nameOnly with object should produce TypeMismatch", + id="name_only_type_object", + ), + CommandTestCase( + command={"listDatabases": 1, "nameOnly": ObjectId()}, + error_code=TYPE_MISMATCH_ERROR, + msg="nameOnly with ObjectId should produce TypeMismatch", + id="name_only_type_objectid", + ), + CommandTestCase( + command={"listDatabases": 1, "nameOnly": datetime.datetime(2024, 1, 1)}, + error_code=TYPE_MISMATCH_ERROR, + msg="nameOnly with datetime should produce TypeMismatch", + id="name_only_type_datetime", + ), + CommandTestCase( + command={"listDatabases": 1, "nameOnly": Timestamp(0, 0)}, + error_code=TYPE_MISMATCH_ERROR, + msg="nameOnly with Timestamp should produce TypeMismatch", + id="name_only_type_timestamp", + ), + CommandTestCase( + command={"listDatabases": 1, "nameOnly": Binary(b"\x01")}, + error_code=TYPE_MISMATCH_ERROR, + msg="nameOnly with Binary should produce TypeMismatch", + id="name_only_type_binary", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "nameOnly": Binary( + b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10", subtype=4 + ), + }, + error_code=TYPE_MISMATCH_ERROR, + msg="nameOnly with Binary UUID subtype should produce TypeMismatch", + id="name_only_type_binary_uuid", + ), + CommandTestCase( + command={"listDatabases": 1, "nameOnly": Regex("^x")}, + error_code=TYPE_MISMATCH_ERROR, + msg="nameOnly with Regex should produce TypeMismatch", + id="name_only_type_regex", + ), + CommandTestCase( + command={"listDatabases": 1, "nameOnly": Code("function(){}")}, + error_code=TYPE_MISMATCH_ERROR, + msg="nameOnly with Code should produce TypeMismatch", + id="name_only_type_code", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "nameOnly": Code("function(){}", {"x": 1}), + }, + error_code=TYPE_MISMATCH_ERROR, + msg="nameOnly with CodeWithScope should produce TypeMismatch", + id="name_only_type_code_with_scope", + ), + CommandTestCase( + command={"listDatabases": 1, "nameOnly": MinKey()}, + error_code=TYPE_MISMATCH_ERROR, + msg="nameOnly with MinKey should produce TypeMismatch", + id="name_only_type_minkey", + ), + CommandTestCase( + command={"listDatabases": 1, "nameOnly": MaxKey()}, + error_code=TYPE_MISMATCH_ERROR, + msg="nameOnly with MaxKey should produce TypeMismatch", + id="name_only_type_maxkey", + ), +] + +# Property [nameOnly Filter Projection]: when nameOnly is true, the +# filter operates on the projected document containing only the name +# field, so filtering on sizeOnDisk or empty returns zero results, +# $expr references to those fields resolve to missing, and +# $exists:false on sizeOnDisk matches all databases. +PARAM_INTERACTION_NAMEONLY_FILTER_TESTS: list[CommandTestCase] = [ + CommandTestCase( + command={ + "listDatabases": 1, + "nameOnly": True, + "filter": {"sizeOnDisk": {"$gte": 0}}, + }, + expected={"ok": Eq(1.0), "databases": Len(0)}, + msg="nameOnly=true should hide sizeOnDisk from filter, returning 0 results", + id="param_nameonly_filter_sizeondisk", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "nameOnly": True, + "filter": {"empty": False}, + }, + expected={"ok": Eq(1.0), "databases": Len(0)}, + msg="nameOnly=true should hide empty from filter, returning 0 results", + id="param_nameonly_filter_empty", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "nameOnly": True, + "filter": {"$expr": "$sizeOnDisk"}, + }, + expected={"ok": Eq(1.0), "databases": Len(0)}, + msg="nameOnly=true should make $sizeOnDisk resolve to missing (falsy) in $expr", + id="param_nameonly_expr_sizeondisk", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "nameOnly": True, + "filter": {"$expr": {"$eq": [{"$ifNull": ["$empty", "was_missing"]}, "was_missing"]}}, + }, + expected={"ok": Eq(1.0), "databases": Contains("name", "admin")}, + msg="nameOnly=true should make $empty resolve to missing in $expr", + id="param_nameonly_expr_empty", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "nameOnly": True, + "filter": {"sizeOnDisk": {"$exists": False}}, + }, + expected={"ok": Eq(1.0), "databases": Contains("name", "admin")}, + msg="nameOnly=true with $exists:false on sizeOnDisk should match all databases", + id="param_nameonly_exists_false_sizeondisk", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "nameOnly": True, + "filter": {"name": "admin"}, + }, + expected={"ok": Eq(1.0), "databases": Contains("name", "admin")}, + msg="Name-based filter should work with nameOnly=true", + id="param_nameonly_filter_name", + ), +] + +NAMEONLY_TESTS: list[CommandTestCase] = ( + name_only_success_TESTS + + NAME_ONLY_FALSY_TESTS + + NAME_ONLY_TRUTHY_TESTS + + NAME_ONLY_TYPE_ERROR_TESTS + + PARAM_INTERACTION_NAMEONLY_FILTER_TESTS +) + + +@pytest.mark.collection_mgmt +@pytest.mark.parametrize("test", pytest_params(NAMEONLY_TESTS)) +def test_listDatabases_nameonly(collection, test): + """Test listDatabases nameOnly parameter behavior.""" + ctx = CommandContext.from_collection(collection) + collection.database.create_collection(collection.name) + result = execute_admin_command(collection, test.build_command(ctx)) + assertResult( + result, + expected=test.build_expected(ctx), + error_code=test.error_code, + msg=test.msg, + raw_res=True, + ) diff --git a/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/test_listDatabases_read_concern.py b/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/test_listDatabases_read_concern.py new file mode 100644 index 00000000..c87009e2 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/test_listDatabases_read_concern.py @@ -0,0 +1,247 @@ +"""Tests for listDatabases readConcern behavior.""" + +from __future__ import annotations + +import datetime + +import pytest +from bson import Binary, Code, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp + +from documentdb_tests.compatibility.tests.core.collections.commands.listDatabases.utils.listDatabases_common import ( # noqa: E501 + basic_success, +) +from documentdb_tests.compatibility.tests.core.collections.commands.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.error_codes import ( + BAD_VALUE_ERROR, + INVALID_OPTIONS_ERROR, + TYPE_MISMATCH_ERROR, + UNRECOGNIZED_COMMAND_FIELD_ERROR, +) +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Concern Accepted Values]: readConcern with level local, +# an empty document, or null is accepted; writeConcern with null is +# accepted. All other concern values are rejected. +CONCERN_ACCEPTED_TESTS: list[CommandTestCase] = [ + CommandTestCase( + command={"listDatabases": 1, "readConcern": {"level": "local"}}, + expected=basic_success, + msg="readConcern with level local should be accepted", + id="read_concern_local", + ), + CommandTestCase( + command={"listDatabases": 1, "readConcern": {}}, + expected=basic_success, + msg="readConcern with empty document should be accepted", + id="read_concern_empty", + ), + CommandTestCase( + command={"listDatabases": 1, "readConcern": None}, + expected=basic_success, + msg="readConcern with null should be accepted", + id="read_concern_null", + ), + CommandTestCase( + command={"listDatabases": 1, "writeConcern": None}, + expected=basic_success, + msg="writeConcern with null should be accepted", + id="write_concern_null", + ), +] + +# Property [readConcern Type Strictness]: readConcern with a +# non-document type produces a TypeMismatch error. +READ_CONCERN_TYPE_ERROR_TESTS: list[CommandTestCase] = [ + CommandTestCase( + command={"listDatabases": 1, "readConcern": 1}, + error_code=TYPE_MISMATCH_ERROR, + msg="readConcern with int32 should produce TypeMismatch", + id="read_concern_type_int32", + ), + CommandTestCase( + command={"listDatabases": 1, "readConcern": Int64(1)}, + error_code=TYPE_MISMATCH_ERROR, + msg="readConcern with Int64 should produce TypeMismatch", + id="read_concern_type_int64", + ), + CommandTestCase( + command={"listDatabases": 1, "readConcern": 3.14}, + error_code=TYPE_MISMATCH_ERROR, + msg="readConcern with double should produce TypeMismatch", + id="read_concern_type_double", + ), + CommandTestCase( + command={"listDatabases": 1, "readConcern": Decimal128("99")}, + error_code=TYPE_MISMATCH_ERROR, + msg="readConcern with Decimal128 should produce TypeMismatch", + id="read_concern_type_decimal128", + ), + CommandTestCase( + command={"listDatabases": 1, "readConcern": True}, + error_code=TYPE_MISMATCH_ERROR, + msg="readConcern with bool should produce TypeMismatch", + id="read_concern_type_bool", + ), + CommandTestCase( + command={"listDatabases": 1, "readConcern": "bad"}, + error_code=TYPE_MISMATCH_ERROR, + msg="readConcern with string should produce TypeMismatch", + id="read_concern_type_string", + ), + CommandTestCase( + command={"listDatabases": 1, "readConcern": [{}]}, + error_code=TYPE_MISMATCH_ERROR, + msg="readConcern with array should produce TypeMismatch", + id="read_concern_type_array", + ), + CommandTestCase( + command={"listDatabases": 1, "readConcern": ObjectId()}, + error_code=TYPE_MISMATCH_ERROR, + msg="readConcern with ObjectId should produce TypeMismatch", + id="read_concern_type_objectid", + ), + CommandTestCase( + command={"listDatabases": 1, "readConcern": datetime.datetime(2024, 1, 1)}, + error_code=TYPE_MISMATCH_ERROR, + msg="readConcern with datetime should produce TypeMismatch", + id="read_concern_type_datetime", + ), + CommandTestCase( + command={"listDatabases": 1, "readConcern": Timestamp(1, 1)}, + error_code=TYPE_MISMATCH_ERROR, + msg="readConcern with Timestamp should produce TypeMismatch", + id="read_concern_type_timestamp", + ), + CommandTestCase( + command={"listDatabases": 1, "readConcern": Binary(b"\x01")}, + error_code=TYPE_MISMATCH_ERROR, + msg="readConcern with Binary should produce TypeMismatch", + id="read_concern_type_binary", + ), + CommandTestCase( + command={"listDatabases": 1, "readConcern": Regex(".*")}, + error_code=TYPE_MISMATCH_ERROR, + msg="readConcern with Regex should produce TypeMismatch", + id="read_concern_type_regex", + ), + CommandTestCase( + command={"listDatabases": 1, "readConcern": Code("function(){}")}, + error_code=TYPE_MISMATCH_ERROR, + msg="readConcern with Code should produce TypeMismatch", + id="read_concern_type_code", + ), + CommandTestCase( + command={"listDatabases": 1, "readConcern": Code("function(){}", {"x": 1})}, + error_code=TYPE_MISMATCH_ERROR, + msg="readConcern with CodeWithScope should produce TypeMismatch", + id="read_concern_type_code_with_scope", + ), + CommandTestCase( + command={"listDatabases": 1, "readConcern": MinKey()}, + error_code=TYPE_MISMATCH_ERROR, + msg="readConcern with MinKey should produce TypeMismatch", + id="read_concern_type_minkey", + ), + CommandTestCase( + command={"listDatabases": 1, "readConcern": MaxKey()}, + error_code=TYPE_MISMATCH_ERROR, + msg="readConcern with MaxKey should produce TypeMismatch", + id="read_concern_type_maxkey", + ), +] + +# Property [readConcern Unsupported Levels]: unsupported read concern +# levels (majority, available, linearizable, snapshot) produce an +# InvalidOptions error. +READ_CONCERN_UNSUPPORTED_LEVEL_TESTS: list[CommandTestCase] = [ + CommandTestCase( + command={"listDatabases": 1, "readConcern": {"level": "majority"}}, + error_code=INVALID_OPTIONS_ERROR, + msg="readConcern level majority should produce InvalidOptions", + id="read_concern_majority", + ), + CommandTestCase( + command={"listDatabases": 1, "readConcern": {"level": "available"}}, + error_code=INVALID_OPTIONS_ERROR, + msg="readConcern level available should produce InvalidOptions", + id="read_concern_available", + ), + CommandTestCase( + command={"listDatabases": 1, "readConcern": {"level": "linearizable"}}, + error_code=INVALID_OPTIONS_ERROR, + msg="readConcern level linearizable should produce InvalidOptions", + id="read_concern_linearizable", + ), + CommandTestCase( + command={"listDatabases": 1, "readConcern": {"level": "snapshot"}}, + error_code=INVALID_OPTIONS_ERROR, + msg="readConcern level snapshot should produce InvalidOptions", + id="read_concern_snapshot", + ), +] + +# Property [readConcern Invalid Value]: an invalid read concern level +# value produces a BadValue error. +READ_CONCERN_INVALID_VALUE_TESTS: list[CommandTestCase] = [ + CommandTestCase( + command={"listDatabases": 1, "readConcern": {"level": "invalid_value"}}, + error_code=BAD_VALUE_ERROR, + msg="readConcern with invalid level value should produce BadValue", + id="read_concern_invalid_value", + ), +] + +# Property [readConcern Unknown Fields]: unknown fields inside the +# readConcern document produce an IDLUnknownField error. +READ_CONCERN_UNKNOWN_FIELD_TESTS: list[CommandTestCase] = [ + CommandTestCase( + command={"listDatabases": 1, "readConcern": {"unknownField": 1}}, + error_code=UNRECOGNIZED_COMMAND_FIELD_ERROR, + msg="Unknown field inside readConcern should produce IDLUnknownField", + id="read_concern_unknown_field", + ), +] + +# Property [readConcern afterClusterTime Null]: afterClusterTime set +# to null with level local produces a TypeMismatch error. +READ_CONCERN_AFTER_CLUSTER_TIME_NULL_TESTS: list[CommandTestCase] = [ + CommandTestCase( + command={ + "listDatabases": 1, + "readConcern": {"level": "local", "afterClusterTime": None}, + }, + error_code=TYPE_MISMATCH_ERROR, + msg="afterClusterTime null with level local should produce TypeMismatch", + id="read_concern_after_cluster_time_null", + ), +] + +READ_CONCERN_TESTS: list[CommandTestCase] = ( + CONCERN_ACCEPTED_TESTS + + READ_CONCERN_TYPE_ERROR_TESTS + + READ_CONCERN_UNSUPPORTED_LEVEL_TESTS + + READ_CONCERN_INVALID_VALUE_TESTS + + READ_CONCERN_UNKNOWN_FIELD_TESTS + + READ_CONCERN_AFTER_CLUSTER_TIME_NULL_TESTS +) + + +@pytest.mark.collection_mgmt +@pytest.mark.parametrize("test", pytest_params(READ_CONCERN_TESTS)) +def test_listDatabases_read_concern(collection, test): + """Test listDatabases readConcern behavior.""" + ctx = CommandContext.from_collection(collection) + collection.database.create_collection(collection.name) + result = execute_admin_command(collection, test.build_command(ctx)) + assertResult( + result, + expected=test.build_expected(ctx), + error_code=test.error_code, + msg=test.msg, + raw_res=True, + ) diff --git a/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/test_listDatabases_response_structure.py b/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/test_listDatabases_response_structure.py new file mode 100644 index 00000000..b9cf3aa9 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/test_listDatabases_response_structure.py @@ -0,0 +1,85 @@ +"""Tests for listDatabases response structure.""" + +from __future__ import annotations + +import pytest + +from documentdb_tests.compatibility.tests.core.collections.commands.listDatabases.utils.listDatabases_common import ( # noqa: E501 + full_structure_success, +) +from documentdb_tests.compatibility.tests.core.collections.commands.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertResult +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 +from documentdb_tests.framework.test_constants import INT64_ZERO + +# Property [Full Response Structure]: when nameOnly is false or +# omitted, the response contains databases (array), totalSize (Int64), +# totalSizeMb (Int64), and ok (double 1.0), and each database entry +# contains name (string), sizeOnDisk (Int64), and empty (bool). +FULL_RESPONSE_STRUCTURE_TESTS: list[CommandTestCase] = [ + CommandTestCase( + command={"listDatabases": 1}, + expected=full_structure_success, + msg="Full response should have databases, totalSize, totalSizeMb, ok", + id="full_response_top_level_keys", + ), + CommandTestCase( + command={"listDatabases": 1, "nameOnly": False}, + expected=full_structure_success, + msg="Explicit nameOnly=false should produce full response structure", + id="full_response_name_only_false", + ), +] + +# Property [Empty Result Structure]: when the filter matches no +# databases, the response contains an empty databases array with +# appropriate fields depending on nameOnly. +EMPTY_RESULT_TESTS: list[CommandTestCase] = [ + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"name": "nonexistent_db_xyz_99999"}, + }, + expected={ + "ok": Eq(1.0), + "databases": Eq([]), + "totalSize": Eq(INT64_ZERO), + "totalSizeMb": Eq(INT64_ZERO), + }, + msg="Empty result with nameOnly=false should have totalSize=0 and totalSizeMb=0", + id="empty_result_full", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "filter": {"name": "nonexistent_db_xyz_99999"}, + "nameOnly": True, + }, + expected={"ok": Eq(1.0), "databases": Eq([])}, + msg="Empty result with nameOnly=true should have only databases and ok", + id="empty_result_name_only", + ), +] + +RESPONSE_STRUCTURE_TESTS: list[CommandTestCase] = FULL_RESPONSE_STRUCTURE_TESTS + EMPTY_RESULT_TESTS + + +@pytest.mark.collection_mgmt +@pytest.mark.parametrize("test", pytest_params(RESPONSE_STRUCTURE_TESTS)) +def test_listDatabases_response_structure(collection, test): + """Test listDatabases response structure.""" + ctx = CommandContext.from_collection(collection) + collection.database.create_collection(collection.name) + result = execute_admin_command(collection, test.build_command(ctx)) + assertResult( + result, + expected=test.build_expected(ctx), + error_code=test.error_code, + msg=test.msg, + raw_res=True, + ) diff --git a/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/test_listDatabases_type_errors_filter.py b/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/test_listDatabases_type_errors_filter.py new file mode 100644 index 00000000..a6a38f24 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/test_listDatabases_type_errors_filter.py @@ -0,0 +1,144 @@ +"""Tests for listDatabases filter type strictness.""" + +from __future__ import annotations + +import datetime + +import pytest +from bson import Binary, Code, MaxKey, MinKey, ObjectId, Regex, Timestamp + +from documentdb_tests.compatibility.tests.core.collections.commands.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertResult +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.test_constants import DECIMAL128_ZERO, INT64_ZERO + +# Property [filter Type Strictness]: filter rejects all non-document, +# non-null BSON types with a TypeMismatch error. +FILTER_TYPE_ERROR_TESTS: list[CommandTestCase] = [ + CommandTestCase( + command={"listDatabases": 1, "filter": "bad"}, + error_code=TYPE_MISMATCH_ERROR, + msg="filter with string should produce TypeMismatch", + id="filter_type_string", + ), + CommandTestCase( + command={"listDatabases": 1, "filter": 1}, + error_code=TYPE_MISMATCH_ERROR, + msg="filter with int32 should produce TypeMismatch", + id="filter_type_int32", + ), + CommandTestCase( + command={"listDatabases": 1, "filter": INT64_ZERO}, + error_code=TYPE_MISMATCH_ERROR, + msg="filter with Int64 should produce TypeMismatch", + id="filter_type_int64", + ), + CommandTestCase( + command={"listDatabases": 1, "filter": 1.0}, + error_code=TYPE_MISMATCH_ERROR, + msg="filter with double should produce TypeMismatch", + id="filter_type_double", + ), + CommandTestCase( + command={"listDatabases": 1, "filter": DECIMAL128_ZERO}, + error_code=TYPE_MISMATCH_ERROR, + msg="filter with Decimal128 should produce TypeMismatch", + id="filter_type_decimal128", + ), + CommandTestCase( + command={"listDatabases": 1, "filter": True}, + error_code=TYPE_MISMATCH_ERROR, + msg="filter with bool should produce TypeMismatch", + id="filter_type_bool", + ), + CommandTestCase( + command={"listDatabases": 1, "filter": []}, + error_code=TYPE_MISMATCH_ERROR, + msg="filter with empty array should produce TypeMismatch", + id="filter_type_empty_array", + ), + CommandTestCase( + command={"listDatabases": 1, "filter": [1, 2]}, + error_code=TYPE_MISMATCH_ERROR, + msg="filter with non-empty array should produce TypeMismatch", + id="filter_type_array", + ), + CommandTestCase( + command={"listDatabases": 1, "filter": ObjectId()}, + error_code=TYPE_MISMATCH_ERROR, + msg="filter with ObjectId should produce TypeMismatch", + id="filter_type_objectid", + ), + CommandTestCase( + command={"listDatabases": 1, "filter": datetime.datetime(2024, 1, 1)}, + error_code=TYPE_MISMATCH_ERROR, + msg="filter with datetime should produce TypeMismatch", + id="filter_type_datetime", + ), + CommandTestCase( + command={"listDatabases": 1, "filter": Timestamp(0, 0)}, + error_code=TYPE_MISMATCH_ERROR, + msg="filter with Timestamp should produce TypeMismatch", + id="filter_type_timestamp", + ), + CommandTestCase( + command={"listDatabases": 1, "filter": Binary(b"\x01")}, + error_code=TYPE_MISMATCH_ERROR, + msg="filter with Binary should produce TypeMismatch", + id="filter_type_binary", + ), + CommandTestCase( + command={"listDatabases": 1, "filter": Regex("^x")}, + error_code=TYPE_MISMATCH_ERROR, + msg="filter with Regex should produce TypeMismatch", + id="filter_type_regex", + ), + CommandTestCase( + command={"listDatabases": 1, "filter": Code("function(){}")}, + error_code=TYPE_MISMATCH_ERROR, + msg="filter with Code should produce TypeMismatch", + id="filter_type_code", + ), + CommandTestCase( + command={ + "listDatabases": 1, + "filter": Code("function(){}", {"x": 1}), + }, + error_code=TYPE_MISMATCH_ERROR, + msg="filter with CodeWithScope should produce TypeMismatch", + id="filter_type_code_with_scope", + ), + CommandTestCase( + command={"listDatabases": 1, "filter": MinKey()}, + error_code=TYPE_MISMATCH_ERROR, + msg="filter with MinKey should produce TypeMismatch", + id="filter_type_minkey", + ), + CommandTestCase( + command={"listDatabases": 1, "filter": MaxKey()}, + error_code=TYPE_MISMATCH_ERROR, + msg="filter with MaxKey should produce TypeMismatch", + id="filter_type_maxkey", + ), +] + + +@pytest.mark.collection_mgmt +@pytest.mark.parametrize("test", pytest_params(FILTER_TYPE_ERROR_TESTS)) +def test_listDatabases_type_errors_filter(collection, test): + """Test listDatabases filter type strictness.""" + ctx = CommandContext.from_collection(collection) + collection.database.create_collection(collection.name) + result = execute_admin_command(collection, test.build_command(ctx)) + assertResult( + result, + expected=test.build_expected(ctx), + error_code=test.error_code, + msg=test.msg, + raw_res=True, + ) diff --git a/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/test_listDatabases_with_expr.py b/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/test_listDatabases_with_expr.py deleted file mode 100644 index 624b259b..00000000 --- a/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/test_listDatabases_with_expr.py +++ /dev/null @@ -1,25 +0,0 @@ -""" -Tests for $expr in listDatabases command contexts. -""" - -from documentdb_tests.framework.assertions import assertSuccess -from documentdb_tests.framework.executor import execute_admin_command - - -def test_expr_in_list_databases(collection): - """Test $expr in listDatabases filter.""" - collection.insert_one({"_id": 1}) - db_name = collection.database.name - result = execute_admin_command( - collection, - { - "listDatabases": 1, - "filter": {"$expr": {"$eq": ["$name", db_name]}}, - }, - ) - assertSuccess( - result, - True, - raw_res=True, - transform=lambda r: db_name in [d["name"] for d in r.get("databases", [])], - ) diff --git a/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/test_listDatabases_write_concern.py b/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/test_listDatabases_write_concern.py new file mode 100644 index 00000000..2cdb730c --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/test_listDatabases_write_concern.py @@ -0,0 +1,156 @@ +"""Tests for listDatabases writeConcern behavior.""" + +from __future__ import annotations + +import datetime + +import pytest +from bson import Binary, Code, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp + +from documentdb_tests.compatibility.tests.core.collections.commands.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.error_codes import INVALID_OPTIONS_ERROR, TYPE_MISMATCH_ERROR +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params + +# Property [writeConcern Type Strictness]: writeConcern with a +# non-document type produces a TypeMismatch error. +WRITE_CONCERN_TYPE_ERROR_TESTS: list[CommandTestCase] = [ + CommandTestCase( + command={"listDatabases": 1, "writeConcern": 1}, + error_code=TYPE_MISMATCH_ERROR, + msg="writeConcern with int32 should produce TypeMismatch", + id="write_concern_type_int32", + ), + CommandTestCase( + command={"listDatabases": 1, "writeConcern": Int64(1)}, + error_code=TYPE_MISMATCH_ERROR, + msg="writeConcern with Int64 should produce TypeMismatch", + id="write_concern_type_int64", + ), + CommandTestCase( + command={"listDatabases": 1, "writeConcern": 3.14}, + error_code=TYPE_MISMATCH_ERROR, + msg="writeConcern with double should produce TypeMismatch", + id="write_concern_type_double", + ), + CommandTestCase( + command={"listDatabases": 1, "writeConcern": Decimal128("99")}, + error_code=TYPE_MISMATCH_ERROR, + msg="writeConcern with Decimal128 should produce TypeMismatch", + id="write_concern_type_decimal128", + ), + CommandTestCase( + command={"listDatabases": 1, "writeConcern": True}, + error_code=TYPE_MISMATCH_ERROR, + msg="writeConcern with bool should produce TypeMismatch", + id="write_concern_type_bool", + ), + CommandTestCase( + command={"listDatabases": 1, "writeConcern": "bad"}, + error_code=TYPE_MISMATCH_ERROR, + msg="writeConcern with string should produce TypeMismatch", + id="write_concern_type_string", + ), + CommandTestCase( + command={"listDatabases": 1, "writeConcern": [{"w": 1}]}, + error_code=TYPE_MISMATCH_ERROR, + msg="writeConcern with array should produce TypeMismatch", + id="write_concern_type_array", + ), + CommandTestCase( + command={"listDatabases": 1, "writeConcern": ObjectId()}, + error_code=TYPE_MISMATCH_ERROR, + msg="writeConcern with ObjectId should produce TypeMismatch", + id="write_concern_type_objectid", + ), + CommandTestCase( + command={"listDatabases": 1, "writeConcern": datetime.datetime(2024, 1, 1)}, + error_code=TYPE_MISMATCH_ERROR, + msg="writeConcern with datetime should produce TypeMismatch", + id="write_concern_type_datetime", + ), + CommandTestCase( + command={"listDatabases": 1, "writeConcern": Timestamp(1, 1)}, + error_code=TYPE_MISMATCH_ERROR, + msg="writeConcern with Timestamp should produce TypeMismatch", + id="write_concern_type_timestamp", + ), + CommandTestCase( + command={"listDatabases": 1, "writeConcern": Binary(b"\x01")}, + error_code=TYPE_MISMATCH_ERROR, + msg="writeConcern with Binary should produce TypeMismatch", + id="write_concern_type_binary", + ), + CommandTestCase( + command={"listDatabases": 1, "writeConcern": Regex(".*")}, + error_code=TYPE_MISMATCH_ERROR, + msg="writeConcern with Regex should produce TypeMismatch", + id="write_concern_type_regex", + ), + CommandTestCase( + command={"listDatabases": 1, "writeConcern": Code("function(){}")}, + error_code=TYPE_MISMATCH_ERROR, + msg="writeConcern with Code should produce TypeMismatch", + id="write_concern_type_code", + ), + CommandTestCase( + command={"listDatabases": 1, "writeConcern": Code("function(){}", {"x": 1})}, + error_code=TYPE_MISMATCH_ERROR, + msg="writeConcern with CodeWithScope should produce TypeMismatch", + id="write_concern_type_code_with_scope", + ), + CommandTestCase( + command={"listDatabases": 1, "writeConcern": MinKey()}, + error_code=TYPE_MISMATCH_ERROR, + msg="writeConcern with MinKey should produce TypeMismatch", + id="write_concern_type_minkey", + ), + CommandTestCase( + command={"listDatabases": 1, "writeConcern": MaxKey()}, + error_code=TYPE_MISMATCH_ERROR, + msg="writeConcern with MaxKey should produce TypeMismatch", + id="write_concern_type_maxkey", + ), +] + +# Property [writeConcern Not Supported]: a valid writeConcern +# document produces an InvalidOptions error because listDatabases is a +# read command that does not support write concern. +WRITE_CONCERN_NOT_SUPPORTED_TESTS: list[CommandTestCase] = [ + CommandTestCase( + command={"listDatabases": 1, "writeConcern": {}}, + error_code=INVALID_OPTIONS_ERROR, + msg="Empty writeConcern document should produce InvalidOptions", + id="write_concern_empty_doc", + ), + CommandTestCase( + command={"listDatabases": 1, "writeConcern": {"w": 1}}, + error_code=INVALID_OPTIONS_ERROR, + msg="writeConcern with w:1 should produce InvalidOptions", + id="write_concern_w_1", + ), +] + +WRITE_CONCERN_TESTS: list[CommandTestCase] = ( + WRITE_CONCERN_TYPE_ERROR_TESTS + WRITE_CONCERN_NOT_SUPPORTED_TESTS +) + + +@pytest.mark.collection_mgmt +@pytest.mark.parametrize("test", pytest_params(WRITE_CONCERN_TESTS)) +def test_listDatabases_write_concern(collection, test): + """Test listDatabases writeConcern behavior.""" + ctx = CommandContext.from_collection(collection) + collection.database.create_collection(collection.name) + result = execute_admin_command(collection, test.build_command(ctx)) + assertResult( + result, + expected=test.build_expected(ctx), + error_code=test.error_code, + msg=test.msg, + raw_res=True, + ) diff --git a/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/utils/__init__.py b/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/utils/listDatabases_common.py b/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/utils/listDatabases_common.py new file mode 100644 index 00000000..cee6501f --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/collections/commands/listDatabases/utils/listDatabases_common.py @@ -0,0 +1,49 @@ +"""Shared expected-value patterns for listDatabases tests. + +Each function returns a dict of property checks suitable for the +``expected`` argument of :class:`CommandTestCase`. +""" + +from __future__ import annotations + +from documentdb_tests.compatibility.tests.core.collections.commands.utils.command_test_case import ( + CommandContext, +) +from documentdb_tests.framework.property_checks import Contains, Eq, Exists, IsType, NotExists + + +def basic_success(ctx: CommandContext) -> dict: + """Successful response containing the test database.""" + return { + "ok": Eq(1.0), + "totalSize": Exists(), + "databases": Contains("name", ctx.database), + } + + +def full_structure_success(_) -> dict: + """Full response with correct BSON types for all fields.""" + return { + "ok": Eq(1.0), + "totalSize": IsType("long"), + "totalSizeMb": IsType("long"), + "databases.0": { + "name": IsType("string"), + "sizeOnDisk": IsType("long"), + "empty": IsType("bool"), + }, + } + + +def name_only_success(_) -> dict: + """Name-only response with size fields absent.""" + return { + "ok": Eq(1.0), + "totalSize": NotExists(), + "totalSizeMb": NotExists(), + "databases.0": { + "name": IsType("string"), + "sizeOnDisk": NotExists(), + "empty": NotExists(), + }, + } diff --git a/documentdb_tests/framework/error_codes.py b/documentdb_tests/framework/error_codes.py index 0e80b474..76dc26c2 100644 --- a/documentdb_tests/framework/error_codes.py +++ b/documentdb_tests/framework/error_codes.py @@ -21,6 +21,7 @@ INVALID_INDEX_SPEC_OPTION_ERROR = 197 EXPR_IN_ARRAY_FILTERS_ERROR = 224 CONVERSION_FAILURE_ERROR = 241 +API_VERSION_ERROR = 322 EXPRESSION_NOT_OBJECT_ERROR = 10065 DUPLICATE_KEY_ERROR = 11000 SORT_COMPOUND_KEY_LIMIT_ERROR = 13103 @@ -224,6 +225,7 @@ ARRAY_TO_OBJECT_INVALID_PAIR_ERROR = 40397 ARRAY_TO_OBJECT_INVALID_ELEMENT_ERROR = 40398 MERGE_OBJECTS_INVALID_TYPE_ERROR = 40400 +DUPLICATE_FIELD_ERROR = 40413 MISSING_FIELD_ERROR = 40414 UNRECOGNIZED_COMMAND_FIELD_ERROR = 40415 INVALID_TIMEZONE_ERROR = 40485