From 0239397b0f7563f9590ef675f6fe6f832cc819b3 Mon Sep 17 00:00:00 2001 From: Daniel Frankcom Date: Fri, 19 Jun 2026 11:00:38 -0700 Subject: [PATCH] Add getDefaultRWConcern command tests Signed-off-by: Daniel Frankcom --- ...est_getDefaultRWConcern_command_options.py | 229 +++++++++++++++ .../test_getDefaultRWConcern_read_concern.py | 264 ++++++++++++++++++ .../test_getDefaultRWConcern_response.py | 138 +++++++++ .../test_getDefaultRWConcern_write_concern.py | 106 +++++++ documentdb_tests/framework/error_codes.py | 1 + 5 files changed, 738 insertions(+) create mode 100644 documentdb_tests/compatibility/tests/system/administration/commands/getDefaultRWConcern/test_getDefaultRWConcern_command_options.py create mode 100644 documentdb_tests/compatibility/tests/system/administration/commands/getDefaultRWConcern/test_getDefaultRWConcern_read_concern.py create mode 100644 documentdb_tests/compatibility/tests/system/administration/commands/getDefaultRWConcern/test_getDefaultRWConcern_response.py create mode 100644 documentdb_tests/compatibility/tests/system/administration/commands/getDefaultRWConcern/test_getDefaultRWConcern_write_concern.py diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getDefaultRWConcern/test_getDefaultRWConcern_command_options.py b/documentdb_tests/compatibility/tests/system/administration/commands/getDefaultRWConcern/test_getDefaultRWConcern_command_options.py new file mode 100644 index 000000000..645edd9dd --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/administration/commands/getDefaultRWConcern/test_getDefaultRWConcern_command_options.py @@ -0,0 +1,229 @@ +"""Tests for getDefaultRWConcern command input acceptance and rejection behavior.""" + +from datetime import datetime, timezone + +import pytest +from bson import ( + Binary, + Code, + Int64, + MaxKey, + MinKey, + ObjectId, + Regex, + Timestamp, +) +from bson.binary import UUID_SUBTYPE + +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.error_codes import ( + TYPE_MISMATCH_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 Eq, NotExists +from documentdb_tests.framework.test_constants import ( + DECIMAL128_ONE_AND_HALF, +) + +# Property [Null Field Handling]: a null optional field is accepted and treated +# as absent, so it is not echoed back. +GETDEFAULTRWCONCERN_NULL_FIELD_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "null_in_memory", + command={"getDefaultRWConcern": 1, "inMemory": None}, + expected={"ok": Eq(1.0), "inMemory": NotExists()}, + msg="getDefaultRWConcern should treat a null inMemory as field-absent and not echo it", + ), + CommandTestCase( + "null_comment", + command={"getDefaultRWConcern": 1, "comment": None}, + expected={"ok": Eq(1.0), "comment": NotExists()}, + msg="getDefaultRWConcern should accept a null comment and not echo it", + ), + CommandTestCase( + "null_all", + command={"getDefaultRWConcern": None, "inMemory": None, "comment": None}, + expected={"ok": Eq(1.0), "inMemory": NotExists(), "comment": NotExists()}, + msg="getDefaultRWConcern should accept null command value, inMemory, and comment together", + ), +] + +# Property [Command Value Behavior]: the command value is ignored and never +# type-validated, so any BSON value is accepted. +GETDEFAULTRWCONCERN_COMMAND_VALUE_TESTS: list[CommandTestCase] = [ + CommandTestCase( + f"command_value_{tid}", + command={"getDefaultRWConcern": val}, + expected={"ok": Eq(1.0)}, + msg=f"getDefaultRWConcern should ignore a {tid} command value and succeed", + ) + for tid, val in [ + ("null", None), + ("int32", 42), + ("int64", Int64(2)), + ("double", 3.14), + ("decimal128", DECIMAL128_ONE_AND_HALF), + ("bool", True), + ("string", "hello"), + ("empty_string", ""), + ("objectid", ObjectId("507f1f77bcf86cd799439011")), + ("datetime", datetime(2024, 1, 1, tzinfo=timezone.utc)), + ("timestamp", Timestamp(1, 1)), + ("binary", Binary(b"\x01\x02\x03")), + ("regex", Regex(".*", "i")), + ("code", Code("function(){}")), + ("minkey", MinKey()), + ("maxkey", MaxKey()), + ("array_single", [1]), + ("object", {"a": 1}), + ] +] + +# Property [inMemory Behavior]: inMemory is echoed back only when set true; +# false or omitted is accepted but not echoed. +GETDEFAULTRWCONCERN_IN_MEMORY_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "in_memory_true", + command={"getDefaultRWConcern": 1, "inMemory": True}, + expected={"ok": Eq(1.0), "inMemory": Eq(True)}, + msg="getDefaultRWConcern should accept inMemory true and echo it back as true", + ), + CommandTestCase( + "in_memory_false", + command={"getDefaultRWConcern": 1, "inMemory": False}, + expected={"ok": Eq(1.0), "inMemory": NotExists()}, + msg="getDefaultRWConcern should accept inMemory false and not echo it", + ), + CommandTestCase( + "in_memory_omitted", + command={"getDefaultRWConcern": 1}, + expected={"ok": Eq(1.0), "inMemory": NotExists()}, + msg="getDefaultRWConcern should default omitted inMemory to false and not echo it", + ), +] + +# Property [comment Behavior]: comment accepts any BSON value and is never +# echoed back. +GETDEFAULTRWCONCERN_COMMENT_TESTS: list[CommandTestCase] = [ + CommandTestCase( + f"comment_{tid}", + command={"getDefaultRWConcern": 1, "comment": val}, + expected={"ok": Eq(1.0), "comment": NotExists()}, + msg=f"getDefaultRWConcern should accept a {tid} comment and not echo it", + ) + for tid, val in [ + ("string", "hello"), + ("int32", 42), + ("int64", Int64(2)), + ("double", 3.14), + ("decimal128", DECIMAL128_ONE_AND_HALF), + ("bool", True), + ("objectid", ObjectId("507f1f77bcf86cd799439011")), + ("datetime", datetime(2024, 1, 1, tzinfo=timezone.utc)), + ("timestamp", Timestamp(1, 1)), + ("binary", Binary(b"\x01\x02\x03")), + ("regex", Regex(".*", "i")), + ("code", Code("function(){}")), + ("minkey", MinKey()), + ("maxkey", MaxKey()), + ("array", [1, 2, 3]), + ("object", {"a": 1}), + ] +] + +# Property [Generic Command Options (smoke)]: the command accepts the generic +# command options shared across commands. +GETDEFAULTRWCONCERN_GENERIC_OPTIONS_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "generic_max_time_ms", + command={"getDefaultRWConcern": 1, "maxTimeMS": 100}, + expected={"ok": Eq(1.0)}, + msg="getDefaultRWConcern should accept a maxTimeMS option", + ), + CommandTestCase( + "generic_api_version", + command={"getDefaultRWConcern": 1, "apiVersion": "1"}, + expected={"ok": Eq(1.0)}, + msg="getDefaultRWConcern should accept an apiVersion option", + ), + CommandTestCase( + "generic_read_preference", + command={"getDefaultRWConcern": 1, "$readPreference": {"mode": "secondary"}}, + expected={"ok": Eq(1.0)}, + msg="getDefaultRWConcern should accept a $readPreference option", + ), + CommandTestCase( + "generic_lsid", + command={"getDefaultRWConcern": 1, "lsid": {"id": Binary(b"\x01" * 16, UUID_SUBTYPE)}}, + expected={"ok": Eq(1.0)}, + msg="getDefaultRWConcern should accept an lsid session option", + ), +] + +# Property [inMemory Type Errors]: inMemory is strictly typed as bool, so every +# non-bool, non-null value is rejected. +GETDEFAULTRWCONCERN_IN_MEMORY_TYPE_ERROR_TESTS: list[CommandTestCase] = [ + CommandTestCase( + f"in_memory_type_{tid}", + command={"getDefaultRWConcern": 1, "inMemory": val}, + error_code=TYPE_MISMATCH_ERROR, + msg=f"getDefaultRWConcern should reject a {tid} inMemory value as a non-bool", + ) + for tid, val in [ + ("int32", 42), + ("int64", Int64(2)), + ("double", 3.14), + ("decimal128", DECIMAL128_ONE_AND_HALF), + ("string", "x"), + ("objectid", ObjectId("507f1f77bcf86cd799439011")), + ("datetime", datetime(2024, 1, 1, tzinfo=timezone.utc)), + ("timestamp", Timestamp(1, 1)), + ("binary", Binary(b"\x01\x02\x03")), + ("regex", Regex(".*", "i")), + ("code", Code("function(){}")), + ("minkey", MinKey()), + ("maxkey", MaxKey()), + ("array", [1]), + ("object", {"a": 1}), + ] +] + +# Property [Syntax Validation Errors]: an unknown top-level command field is +# rejected. +GETDEFAULTRWCONCERN_SYNTAX_ERROR_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "unknown_field", + command={"getDefaultRWConcern": 1, "unknownField": 1}, + error_code=UNRECOGNIZED_COMMAND_FIELD_ERROR, + msg="getDefaultRWConcern should reject an unknown command field", + ), +] + +GETDEFAULTRWCONCERN_COMMAND_OPTION_TESTS: list[CommandTestCase] = ( + GETDEFAULTRWCONCERN_NULL_FIELD_TESTS + + GETDEFAULTRWCONCERN_COMMAND_VALUE_TESTS + + GETDEFAULTRWCONCERN_IN_MEMORY_TESTS + + GETDEFAULTRWCONCERN_COMMENT_TESTS + + GETDEFAULTRWCONCERN_GENERIC_OPTIONS_TESTS + + GETDEFAULTRWCONCERN_IN_MEMORY_TYPE_ERROR_TESTS + + GETDEFAULTRWCONCERN_SYNTAX_ERROR_TESTS +) + + +@pytest.mark.replica_set +@pytest.mark.parametrize("test", pytest_params(GETDEFAULTRWCONCERN_COMMAND_OPTION_TESTS)) +def test_getDefaultRWConcern_command_options(collection, test): + """Test getDefaultRWConcern command value and option acceptance and rejection behavior.""" + result = execute_admin_command(collection, test.command) + assertResult( + result, + expected=test.expected, + error_code=test.error_code, + msg=test.msg, + raw_res=True, + ) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getDefaultRWConcern/test_getDefaultRWConcern_read_concern.py b/documentdb_tests/compatibility/tests/system/administration/commands/getDefaultRWConcern/test_getDefaultRWConcern_read_concern.py new file mode 100644 index 000000000..f28e20ced --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/administration/commands/getDefaultRWConcern/test_getDefaultRWConcern_read_concern.py @@ -0,0 +1,264 @@ +"""Tests for getDefaultRWConcern command input acceptance and rejection behavior.""" + +from datetime import datetime, timezone + +import pytest +from bson import ( + Binary, + Code, + Int64, + MaxKey, + MinKey, + ObjectId, + Regex, + Timestamp, +) + +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + 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 +from documentdb_tests.framework.property_checks import Eq +from documentdb_tests.framework.test_constants import ( + DECIMAL128_ONE_AND_HALF, +) + +# Property [readConcern Acceptance]: the command accepts a readConcern that +# resolves to the supported local level. +GETDEFAULTRWCONCERN_READ_CONCERN_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "read_concern_local", + command={"getDefaultRWConcern": 1, "readConcern": {"level": "local"}}, + expected={"ok": Eq(1.0)}, + msg="getDefaultRWConcern should accept a readConcern with level local", + ), + CommandTestCase( + "read_concern_empty", + command={"getDefaultRWConcern": 1, "readConcern": {}}, + expected={"ok": Eq(1.0)}, + msg="getDefaultRWConcern should accept an empty readConcern object", + ), + CommandTestCase( + "read_concern_level_null", + command={"getDefaultRWConcern": 1, "readConcern": {"level": None}}, + expected={"ok": Eq(1.0)}, + msg="getDefaultRWConcern should treat a null readConcern level as the default and succeed", + ), + CommandTestCase( + "read_concern_after_cluster_time", + command={"getDefaultRWConcern": 1, "readConcern": {"afterClusterTime": Timestamp(1, 1)}}, + expected={"ok": Eq(1.0)}, + msg="getDefaultRWConcern should accept a readConcern with afterClusterTime", + ), +] + +# Property [readConcern Type Errors]: a non-object, non-null readConcern is +# rejected by the object type check. +GETDEFAULTRWCONCERN_READ_CONCERN_TYPE_ERROR_TESTS: list[CommandTestCase] = [ + CommandTestCase( + f"read_concern_type_{tid}", + command={"getDefaultRWConcern": 1, "readConcern": val}, + error_code=TYPE_MISMATCH_ERROR, + msg=f"getDefaultRWConcern should reject a {tid} readConcern as a non-object", + ) + for tid, val in [ + ("int32", 42), + ("int64", Int64(2)), + ("double", 3.14), + ("decimal128", DECIMAL128_ONE_AND_HALF), + ("bool", True), + ("string", "local"), + ("objectid", ObjectId("507f1f77bcf86cd799439011")), + ("datetime", datetime(2024, 1, 1, tzinfo=timezone.utc)), + ("timestamp", Timestamp(1, 1)), + ("binary", Binary(b"\x01\x02\x03")), + ("regex", Regex(".*", "i")), + ("code", Code("function(){}")), + ("minkey", MinKey()), + ("maxkey", MaxKey()), + ("array", [1]), + ] +] + +# Property [readConcern level Type Errors]: a non-string, non-null readConcern +# level is rejected by the type check. +GETDEFAULTRWCONCERN_READ_CONCERN_LEVEL_TYPE_ERROR_TESTS: list[CommandTestCase] = [ + CommandTestCase( + f"read_concern_level_type_{tid}", + command={"getDefaultRWConcern": 1, "readConcern": {"level": val}}, + error_code=TYPE_MISMATCH_ERROR, + msg=f"getDefaultRWConcern should reject a {tid} readConcern level as a non-string", + ) + for tid, val in [ + ("int32", 42), + ("int64", Int64(2)), + ("double", 3.14), + ("decimal128", DECIMAL128_ONE_AND_HALF), + ("bool", True), + ("objectid", ObjectId("507f1f77bcf86cd799439011")), + ("datetime", datetime(2024, 1, 1, tzinfo=timezone.utc)), + ("timestamp", Timestamp(1, 1)), + ("binary", Binary(b"\x01\x02\x03")), + ("regex", Regex(".*", "i")), + ("code", Code("function(){}")), + ("minkey", MinKey()), + ("maxkey", MaxKey()), + ("array", ["local"]), + ("object", {"a": 1}), + ] +] + +# Property [readConcern level Value Errors]: a string readConcern level that is +# not a recognized value is rejected. +GETDEFAULTRWCONCERN_READ_CONCERN_LEVEL_VALUE_ERROR_TESTS: list[CommandTestCase] = [ + CommandTestCase( + f"read_concern_level_value_{tid}", + command={"getDefaultRWConcern": 1, "readConcern": {"level": val}}, + error_code=BAD_VALUE_ERROR, + msg=f"getDefaultRWConcern should reject a {tid} readConcern level value", + ) + for tid, val in [ + ("empty", ""), + ("unknown", "bogus"), + ("wrong_case", "LOCAL"), + ] +] + +# Property [Unsupported readConcern Levels]: recognized read concern levels +# other than local are rejected as unsupported. +GETDEFAULTRWCONCERN_READ_CONCERN_ERROR_TESTS: list[CommandTestCase] = [ + CommandTestCase( + f"read_concern_{level}", + command={"getDefaultRWConcern": 1, "readConcern": {"level": level}}, + error_code=INVALID_OPTIONS_ERROR, + msg=f"getDefaultRWConcern should reject the {level} read concern level", + ) + for level in ["available", "majority", "linearizable", "snapshot"] +] + +# Property [readConcern atClusterTime]: atClusterTime is only valid with a +# snapshot level, so supplying it without one is rejected. +GETDEFAULTRWCONCERN_READ_CONCERN_AT_CLUSTER_TIME_ERROR_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "read_concern_at_cluster_time_without_snapshot", + command={"getDefaultRWConcern": 1, "readConcern": {"atClusterTime": Timestamp(1, 1)}}, + error_code=INVALID_OPTIONS_ERROR, + msg="getDefaultRWConcern should reject atClusterTime without a snapshot level", + ), +] + +# Property [readConcern afterClusterTime Type Errors]: afterClusterTime is +# strictly typed as a timestamp, so every non-timestamp value is rejected. +GETDEFAULTRWCONCERN_READ_CONCERN_AFTER_CLUSTER_TIME_TYPE_ERROR_TESTS: list[CommandTestCase] = [ + CommandTestCase( + f"read_concern_after_cluster_time_type_{tid}", + command={"getDefaultRWConcern": 1, "readConcern": {"afterClusterTime": val}}, + error_code=TYPE_MISMATCH_ERROR, + msg=f"getDefaultRWConcern should reject a {tid} afterClusterTime as a non-timestamp", + ) + for tid, val in [ + ("int32", 42), + ("int64", Int64(2)), + ("double", 3.14), + ("decimal128", DECIMAL128_ONE_AND_HALF), + ("bool", True), + ("string", "x"), + ("null", None), + ("objectid", ObjectId("507f1f77bcf86cd799439011")), + ("datetime", datetime(2024, 1, 1, tzinfo=timezone.utc)), + ("binary", Binary(b"\x01\x02\x03")), + ("regex", Regex(".*", "i")), + ("code", Code("function(){}")), + ("minkey", MinKey()), + ("maxkey", MaxKey()), + ("array", [1]), + ("object", {"a": 1}), + ] +] + +# Property [readConcern afterClusterTime Value Errors]: a zero-valued (null) +# timestamp is a valid timestamp type but not a usable cluster time, so +# afterClusterTime rejects it. +GETDEFAULTRWCONCERN_READ_CONCERN_AFTER_CLUSTER_TIME_VALUE_ERROR_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "read_concern_after_cluster_time_null_timestamp", + command={"getDefaultRWConcern": 1, "readConcern": {"afterClusterTime": Timestamp(0, 0)}}, + error_code=INVALID_OPTIONS_ERROR, + msg="getDefaultRWConcern should reject a null timestamp afterClusterTime", + ), +] + +# Property [readConcern atClusterTime Type Errors]: atClusterTime is strictly +# typed as a timestamp, so every non-timestamp value is rejected. +GETDEFAULTRWCONCERN_READ_CONCERN_AT_CLUSTER_TIME_TYPE_ERROR_TESTS: list[CommandTestCase] = [ + CommandTestCase( + f"read_concern_at_cluster_time_type_{tid}", + command={"getDefaultRWConcern": 1, "readConcern": {"atClusterTime": val}}, + error_code=TYPE_MISMATCH_ERROR, + msg=f"getDefaultRWConcern should reject a {tid} atClusterTime as a non-timestamp", + ) + for tid, val in [ + ("int32", 42), + ("int64", Int64(2)), + ("double", 3.14), + ("decimal128", DECIMAL128_ONE_AND_HALF), + ("bool", True), + ("string", "x"), + ("null", None), + ("objectid", ObjectId("507f1f77bcf86cd799439011")), + ("datetime", datetime(2024, 1, 1, tzinfo=timezone.utc)), + ("binary", Binary(b"\x01\x02\x03")), + ("regex", Regex(".*", "i")), + ("code", Code("function(){}")), + ("minkey", MinKey()), + ("maxkey", MaxKey()), + ("array", [1]), + ("object", {"a": 1}), + ] +] + +# Property [readConcern Sub-Field Validation]: an unknown sub-field inside the +# readConcern document is rejected. +GETDEFAULTRWCONCERN_READ_CONCERN_UNKNOWN_FIELD_ERROR_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "read_concern_unknown_subfield", + command={"getDefaultRWConcern": 1, "readConcern": {"level": "local", "unknownField": 1}}, + error_code=UNRECOGNIZED_COMMAND_FIELD_ERROR, + msg="getDefaultRWConcern should reject an unknown readConcern sub-field", + ), +] + +GETDEFAULTRWCONCERN_READ_CONCERN_ALL_TESTS: list[CommandTestCase] = ( + GETDEFAULTRWCONCERN_READ_CONCERN_TESTS + + GETDEFAULTRWCONCERN_READ_CONCERN_TYPE_ERROR_TESTS + + GETDEFAULTRWCONCERN_READ_CONCERN_LEVEL_TYPE_ERROR_TESTS + + GETDEFAULTRWCONCERN_READ_CONCERN_LEVEL_VALUE_ERROR_TESTS + + GETDEFAULTRWCONCERN_READ_CONCERN_ERROR_TESTS + + GETDEFAULTRWCONCERN_READ_CONCERN_AT_CLUSTER_TIME_ERROR_TESTS + + GETDEFAULTRWCONCERN_READ_CONCERN_AFTER_CLUSTER_TIME_TYPE_ERROR_TESTS + + GETDEFAULTRWCONCERN_READ_CONCERN_AFTER_CLUSTER_TIME_VALUE_ERROR_TESTS + + GETDEFAULTRWCONCERN_READ_CONCERN_AT_CLUSTER_TIME_TYPE_ERROR_TESTS + + GETDEFAULTRWCONCERN_READ_CONCERN_UNKNOWN_FIELD_ERROR_TESTS +) + + +@pytest.mark.replica_set +@pytest.mark.parametrize("test", pytest_params(GETDEFAULTRWCONCERN_READ_CONCERN_ALL_TESTS)) +def test_getDefaultRWConcern_read_concern(collection, test): + """Test getDefaultRWConcern readConcern acceptance and rejection behavior.""" + result = execute_admin_command(collection, test.command) + assertResult( + result, + expected=test.expected, + error_code=test.error_code, + msg=test.msg, + raw_res=True, + ) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getDefaultRWConcern/test_getDefaultRWConcern_response.py b/documentdb_tests/compatibility/tests/system/administration/commands/getDefaultRWConcern/test_getDefaultRWConcern_response.py new file mode 100644 index 000000000..4d30ef789 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/administration/commands/getDefaultRWConcern/test_getDefaultRWConcern_response.py @@ -0,0 +1,138 @@ +"""Tests for getDefaultRWConcern command input acceptance and rejection behavior.""" + +import pytest + +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.error_codes import ( + NOT_SUPPORTED_ON_STANDALONE_ERROR, +) +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.property_checks import Eq, IsType + +# Property [Output Schema and Return Shape]: a successful response always +# returns the full defaults schema, for both an on-disk and an in-memory read. +GETDEFAULTRWCONCERN_OUTPUT_SCHEMA_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "output_schema", + command={"getDefaultRWConcern": 1}, + expected={ + "ok": Eq(1.0), + "defaultReadConcern": IsType("object"), + "defaultWriteConcern": IsType("object"), + "defaultReadConcernSource": IsType("string"), + "defaultWriteConcernSource": IsType("string"), + "localUpdateWallClockTime": IsType("date"), + }, + msg="getDefaultRWConcern should always return the read/write concern " + "defaults, their sources, and localUpdateWallClockTime", + ), + CommandTestCase( + "output_schema_in_memory", + command={"getDefaultRWConcern": 1, "inMemory": True}, + expected={ + "ok": Eq(1.0), + "inMemory": Eq(True), + "defaultReadConcern": IsType("object"), + "defaultWriteConcern": IsType("object"), + "defaultReadConcernSource": IsType("string"), + "defaultWriteConcernSource": IsType("string"), + "localUpdateWallClockTime": IsType("date"), + }, + msg="getDefaultRWConcern should return the full defaults schema for an " + "in-memory read and echo inMemory true", + ), +] + + +@pytest.mark.replica_set +@pytest.mark.parametrize("test", pytest_params(GETDEFAULTRWCONCERN_OUTPUT_SCHEMA_TESTS)) +def test_getDefaultRWConcern_output_schema(collection, test): + """Test getDefaultRWConcern always returns the full defaults schema.""" + result = execute_admin_command(collection, test.command) + assertResult( + result, + expected=test.expected, + error_code=test.error_code, + msg=test.msg, + raw_res=True, + ) + + +# Property [Topology Errors]: on a standalone node the command is unsupported +# and is rejected. +def test_getDefaultRWConcern_topology_error(collection): + """Test getDefaultRWConcern is unsupported on standalone nodes.""" + result = execute_admin_command(collection, {"getDefaultRWConcern": 1}) + assertResult( + result, + error_code=NOT_SUPPORTED_ON_STANDALONE_ERROR, + msg="getDefaultRWConcern should be unsupported on a standalone node", + raw_res=True, + ) + + +# Property [Default Concern Source Semantics]: setting a default flips the +# corresponding source from implicit to global and surfaces the update +# timestamps. This mutates global state irreversibly (the default write concern +# cannot be unset), so it runs no_parallel. +@pytest.mark.replica_set +@pytest.mark.no_parallel +def test_getDefaultRWConcern_source_global_after_set(collection): + """Test getDefaultRWConcern reports global sources once defaults are set.""" + execute_admin_command( + collection, + { + "setDefaultRWConcern": 1, + "defaultReadConcern": {"level": "local"}, + "defaultWriteConcern": {"w": "majority", "wtimeout": 0}, + }, + ) + result = execute_admin_command(collection, {"getDefaultRWConcern": 1}) + assertResult( + result, + expected={ + "ok": Eq(1.0), + "defaultReadConcernSource": Eq("global"), + "defaultWriteConcernSource": Eq("global"), + "updateOpTime": IsType("timestamp"), + "updateWallClockTime": IsType("date"), + }, + msg="getDefaultRWConcern should report global sources and present update " + "timestamps once defaults are set", + raw_res=True, + ) + + +# Property [Default Concern Source Semantics]: unsetting the default read concern +# reverts its source to implicit, while the write concern source stays global +# because it cannot be unset. This mutates global state irreversibly, so it runs +# no_parallel. +@pytest.mark.replica_set +@pytest.mark.no_parallel +def test_getDefaultRWConcern_read_source_reverts_on_unset(collection): + """Test getDefaultRWConcern reverts the read source to implicit on unset.""" + execute_admin_command( + collection, + { + "setDefaultRWConcern": 1, + "defaultReadConcern": {"level": "local"}, + "defaultWriteConcern": {"w": "majority", "wtimeout": 0}, + }, + ) + execute_admin_command(collection, {"setDefaultRWConcern": 1, "defaultReadConcern": {}}) + result = execute_admin_command(collection, {"getDefaultRWConcern": 1}) + assertResult( + result, + expected={ + "ok": Eq(1.0), + "defaultReadConcernSource": Eq("implicit"), + "defaultWriteConcernSource": Eq("global"), + }, + msg="getDefaultRWConcern should revert the read concern source to implicit " + "after unset while the write concern source remains global", + raw_res=True, + ) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getDefaultRWConcern/test_getDefaultRWConcern_write_concern.py b/documentdb_tests/compatibility/tests/system/administration/commands/getDefaultRWConcern/test_getDefaultRWConcern_write_concern.py new file mode 100644 index 000000000..ee6d52a41 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/administration/commands/getDefaultRWConcern/test_getDefaultRWConcern_write_concern.py @@ -0,0 +1,106 @@ +"""Tests for getDefaultRWConcern command input acceptance and rejection behavior.""" + +from datetime import datetime, timezone + +import pytest +from bson import ( + Binary, + Code, + Int64, + MaxKey, + MinKey, + ObjectId, + Regex, + Timestamp, +) + +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + 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 +from documentdb_tests.framework.property_checks import Eq +from documentdb_tests.framework.test_constants import ( + DECIMAL128_ONE_AND_HALF, +) + +# Property [writeConcern Acceptance]: a null writeConcern is treated as absent +# and accepted. +GETDEFAULTRWCONCERN_WRITE_CONCERN_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "write_concern_null", + command={"getDefaultRWConcern": 1, "writeConcern": None}, + expected={"ok": Eq(1.0)}, + msg="getDefaultRWConcern should treat a null writeConcern as field-absent and succeed", + ), +] + +# Property [Unsupported writeConcern]: writeConcern is not supported, so any +# writeConcern object is rejected. +GETDEFAULTRWCONCERN_WRITE_CONCERN_ERROR_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "write_concern_non_empty", + command={"getDefaultRWConcern": 1, "writeConcern": {"w": "majority"}}, + error_code=INVALID_OPTIONS_ERROR, + msg="getDefaultRWConcern should reject a non-empty writeConcern object as unsupported", + ), + CommandTestCase( + "write_concern_empty", + command={"getDefaultRWConcern": 1, "writeConcern": {}}, + error_code=INVALID_OPTIONS_ERROR, + msg="getDefaultRWConcern should reject an empty writeConcern object as unsupported", + ), +] + +# Property [writeConcern Type Errors]: a non-object writeConcern is rejected by +# the object type check. +GETDEFAULTRWCONCERN_WRITE_CONCERN_TYPE_ERROR_TESTS: list[CommandTestCase] = [ + CommandTestCase( + f"write_concern_type_{tid}", + command={"getDefaultRWConcern": 1, "writeConcern": val}, + error_code=TYPE_MISMATCH_ERROR, + msg=f"getDefaultRWConcern should reject a {tid} writeConcern as a non-object", + ) + for tid, val in [ + ("int32", 42), + ("int64", Int64(2)), + ("double", 3.14), + ("decimal128", DECIMAL128_ONE_AND_HALF), + ("bool", True), + ("string", "majority"), + ("objectid", ObjectId("507f1f77bcf86cd799439011")), + ("datetime", datetime(2024, 1, 1, tzinfo=timezone.utc)), + ("timestamp", Timestamp(1, 1)), + ("binary", Binary(b"\x01\x02\x03")), + ("regex", Regex(".*", "i")), + ("code", Code("function(){}")), + ("minkey", MinKey()), + ("maxkey", MaxKey()), + ("array", [1]), + ] +] + +GETDEFAULTRWCONCERN_WRITE_CONCERN_ALL_TESTS: list[CommandTestCase] = ( + GETDEFAULTRWCONCERN_WRITE_CONCERN_TESTS + + GETDEFAULTRWCONCERN_WRITE_CONCERN_ERROR_TESTS + + GETDEFAULTRWCONCERN_WRITE_CONCERN_TYPE_ERROR_TESTS +) + + +@pytest.mark.replica_set +@pytest.mark.parametrize("test", pytest_params(GETDEFAULTRWCONCERN_WRITE_CONCERN_ALL_TESTS)) +def test_getDefaultRWConcern_write_concern(collection, test): + """Test getDefaultRWConcern writeConcern acceptance and rejection behavior.""" + result = execute_admin_command(collection, test.command) + assertResult( + result, + expected=test.expected, + error_code=test.error_code, + msg=test.msg, + raw_res=True, + ) diff --git a/documentdb_tests/framework/error_codes.py b/documentdb_tests/framework/error_codes.py index 2375b9dcd..a7b00e576 100644 --- a/documentdb_tests/framework/error_codes.py +++ b/documentdb_tests/framework/error_codes.py @@ -385,6 +385,7 @@ PROJECT_EMPTY_SUB_PROJECTION_ERROR = 51270 PROJECT_EMPTY_SPEC_ERROR = 51272 MERGE_LET_RESERVED_NEW_ERROR = 51273 +NOT_SUPPORTED_ON_STANDALONE_ERROR = 51300 REPLACE_MISSING_REPLACEMENT_ERROR = 51747 REPLACE_MISSING_FIND_ERROR = 51748 REPLACE_MISSING_INPUT_ERROR = 51749