From c07d9204395923b95c8f9450437e3cf7e399bd9f Mon Sep 17 00:00:00 2001 From: "Victor [C] Tsang" Date: Tue, 23 Jun 2026 16:17:49 +0000 Subject: [PATCH] Add admin command tests for killOp Signed-off-by: Victor [C] Tsang --- .../commands/killOp/__init__.py | 0 .../test_killOp_bson_type_validation.py | 114 +++++++++++++++++ .../killOp/test_killOp_core_behavior.py | 90 ++++++++++++++ .../commands/killOp/test_killOp_errors.py | 117 ++++++++++++++++++ documentdb_tests/framework/error_codes.py | 1 + documentdb_tests/framework/test_constants.py | 3 + 6 files changed, 325 insertions(+) create mode 100644 documentdb_tests/compatibility/tests/system/administration/commands/killOp/__init__.py create mode 100644 documentdb_tests/compatibility/tests/system/administration/commands/killOp/test_killOp_bson_type_validation.py create mode 100644 documentdb_tests/compatibility/tests/system/administration/commands/killOp/test_killOp_core_behavior.py create mode 100644 documentdb_tests/compatibility/tests/system/administration/commands/killOp/test_killOp_errors.py diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/killOp/__init__.py b/documentdb_tests/compatibility/tests/system/administration/commands/killOp/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/killOp/test_killOp_bson_type_validation.py b/documentdb_tests/compatibility/tests/system/administration/commands/killOp/test_killOp_bson_type_validation.py new file mode 100644 index 000000000..c9b8b5f5b --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/administration/commands/killOp/test_killOp_bson_type_validation.py @@ -0,0 +1,114 @@ +"""Tests for killOp BSON type validation.""" + +import pytest +from bson import Decimal128, Int64 + +from documentdb_tests.framework.assertions import assertFailureCode, assertProperties +from documentdb_tests.framework.bson_type_validator import ( + BsonType, + BsonTypeTestCase, + generate_bson_acceptance_test_cases, + generate_bson_rejection_test_cases, +) +from documentdb_tests.framework.error_codes import TYPE_MISMATCH_ERROR +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.property_checks import Eq, NotExists +from documentdb_tests.framework.test_constants import NON_RUNNING_OP_ID + +pytestmark = [pytest.mark.admin, pytest.mark.no_parallel] + + +KILLOP_VALUE_PARAM = [ + BsonTypeTestCase( + id="killOp_value", + msg="killOp should accept all BSON types for the command field value", + keyword="killOp", + valid_types=list(BsonType), + requires={"op": NON_RUNNING_OP_ID}, + ), +] + +COMMENT_PARAM = [ + BsonTypeTestCase( + id="comment", + msg="killOp should accept all BSON types for the comment field", + keyword="comment", + valid_types=list(BsonType), + requires={"op": NON_RUNNING_OP_ID}, + ), +] + +OP_VALUE_PARAM = [ + BsonTypeTestCase( + id="op", + msg="op field should accept numeric types only", + keyword="op", + valid_types=[ + BsonType.INT, + BsonType.DOUBLE, + BsonType.LONG, + BsonType.DECIMAL, + ], + valid_inputs={ + BsonType.DOUBLE: 999999.0, + BsonType.LONG: Int64(999999), + BsonType.DECIMAL: Decimal128("999999"), + }, + default_error_code=TYPE_MISMATCH_ERROR, + ), +] + +# killOp and comment accept all BSON types, so they have acceptance cases only (no rejections). +KILLOP_ACCEPTANCE = generate_bson_acceptance_test_cases(KILLOP_VALUE_PARAM) +COMMENT_ACCEPTANCE = generate_bson_acceptance_test_cases(COMMENT_PARAM) +OP_ACCEPTANCE = generate_bson_acceptance_test_cases(OP_VALUE_PARAM) +OP_REJECTIONS = generate_bson_rejection_test_cases(OP_VALUE_PARAM) + + +@pytest.mark.parametrize("bson_type,sample_value,spec", KILLOP_ACCEPTANCE) +def test_killOp_bson_type_accepted(collection, bson_type, sample_value, spec): + """Test killOp accepts valid BSON types for the killOp command field.""" + result = execute_admin_command(collection, {"killOp": sample_value, "op": NON_RUNNING_OP_ID}) + assertProperties( + result, + {"ok": Eq(1.0)}, + msg=f"{spec.msg} (bson_type={bson_type.value})", + raw_res=True, + ) + + +@pytest.mark.parametrize("bson_type,sample_value,spec", COMMENT_ACCEPTANCE) +def test_killOp_comment_bson_type_accepted(collection, bson_type, sample_value, spec): + """Test comment field accepts all BSON types and is consumed (not echoed back).""" + result = execute_admin_command( + collection, {"killOp": 1, "comment": sample_value, "op": NON_RUNNING_OP_ID} + ) + assertProperties( + result, + {"ok": Eq(1.0), "comment": NotExists()}, + msg=f"{spec.msg} (bson_type={bson_type.value})", + raw_res=True, + ) + + +@pytest.mark.parametrize("bson_type,sample_value,spec", OP_ACCEPTANCE) +def test_killOp_op_bson_type_accepted(collection, bson_type, sample_value, spec): + """Test op field accepts the numeric BSON types.""" + result = execute_admin_command(collection, {"killOp": 1, "op": sample_value}) + assertProperties( + result, + {"ok": Eq(1.0)}, + msg=f"{spec.msg} (bson_type={bson_type.value})", + raw_res=True, + ) + + +@pytest.mark.parametrize("bson_type,sample_value,spec", OP_REJECTIONS) +def test_killOp_op_bson_type_rejected(collection, bson_type, sample_value, spec): + """Test op field rejects non-numeric BSON types.""" + result = execute_admin_command(collection, {"killOp": 1, "op": sample_value}) + assertFailureCode( + result, + spec.expected_code(bson_type), + msg=f"killOp should reject {bson_type.value} for op field", + ) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/killOp/test_killOp_core_behavior.py b/documentdb_tests/compatibility/tests/system/administration/commands/killOp/test_killOp_core_behavior.py new file mode 100644 index 000000000..f04430aa7 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/administration/commands/killOp/test_killOp_core_behavior.py @@ -0,0 +1,90 @@ +"""Tests for killOp command core behavior. + +Validates killOp response structure, behavior with non-existent operations, +and edge cases for opId values. +""" + +import pytest + +from documentdb_tests.framework.assertions import assertSuccessPartial +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.test_constants import ( + INT32_MAX, + INT32_MIN, + INT32_ZERO, + NON_RUNNING_OP_ID, +) + +pytestmark = [pytest.mark.admin, pytest.mark.no_parallel] + + +def test_killOp_nonexistent_opid_returns_ok(collection): + """Test killOp with opId that does not correspond to any running operation returns ok:1.""" + result = execute_admin_command(collection, {"killOp": 1, "op": NON_RUNNING_OP_ID}) + assertSuccessPartial( + result, + {"ok": 1.0, "info": "attempting to kill op"}, + msg="Should return ok:1 for non-existent opId", + ) + + +def test_killOp_op_int32_min(collection): + """Test killOp with minimum int32 opId returns ok:1.""" + result = execute_admin_command(collection, {"killOp": 1, "op": INT32_MIN}) + assertSuccessPartial( + result, + {"ok": 1.0, "info": "attempting to kill op"}, + msg="Should return ok:1 for INT32_MIN opId", + ) + + +def test_killOp_op_zero(collection): + """Test killOp with zero opId returns ok:1.""" + result = execute_admin_command(collection, {"killOp": 1, "op": INT32_ZERO}) + assertSuccessPartial( + result, + {"ok": 1.0, "info": "attempting to kill op"}, + msg="Should return ok:1 for opId 0", + ) + + +def test_killOp_op_int32_max(collection): + """Test killOp with maximum int32 opId returns ok:1.""" + result = execute_admin_command(collection, {"killOp": 1, "op": INT32_MAX}) + assertSuccessPartial( + result, + {"ok": 1.0, "info": "attempting to kill op"}, + msg="Should return ok:1 for INT32_MAX opId", + ) + + +def test_killOp_op_int32_max_as_double(collection): + """Test killOp accepts INT32_MAX as a whole-number double.""" + result = execute_admin_command(collection, {"killOp": 1, "op": float(INT32_MAX)}) + assertSuccessPartial( + result, + {"ok": 1.0, "info": "attempting to kill op"}, + msg="Should return ok:1 for INT32_MAX as a double", + ) + + +def test_killOp_op_int32_min_as_double(collection): + """Test killOp accepts INT32_MIN as a whole-number double.""" + result = execute_admin_command(collection, {"killOp": 1, "op": float(INT32_MIN)}) + assertSuccessPartial( + result, + {"ok": 1.0, "info": "attempting to kill op"}, + msg="Should return ok:1 for INT32_MIN as a double", + ) + + +def test_killOp_extra_unrecognized_field(collection): + """Test killOp with unrecognized top-level field is accepted (not rejected).""" + result = execute_admin_command( + collection, {"killOp": 1, "op": NON_RUNNING_OP_ID, "unknownField": 1} + ) + assertSuccessPartial( + result, + {"ok": 1.0, "info": "attempting to kill op"}, + msg="Should ignore unrecognized fields", + ) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/killOp/test_killOp_errors.py b/documentdb_tests/compatibility/tests/system/administration/commands/killOp/test_killOp_errors.py new file mode 100644 index 000000000..43f3dca5b --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/administration/commands/killOp/test_killOp_errors.py @@ -0,0 +1,117 @@ +"""Tests for killOp command error cases. + +Consolidates all failure-asserting test cases for the killOp command: +non-admin database rejection, missing op field, and invalid op values +(non-integral, or a valid integer outside the int32 range). +""" + +import pytest +from bson import Decimal128, Int64 + +from documentdb_tests.framework.assertions import assertFailureCode +from documentdb_tests.framework.error_codes import ( + BAD_VALUE_ERROR, + KILLOP_OPID_NOT_INT32_ERROR, + NO_SUCH_KEY_ERROR, + UNAUTHORIZED_ERROR, +) +from documentdb_tests.framework.executor import execute_admin_command, execute_command +from documentdb_tests.framework.test_constants import ( + INT32_OVERFLOW, + INT32_UNDERFLOW, + INT64_MAX, + INT64_MIN, + NON_RUNNING_OP_ID, +) + +pytestmark = [pytest.mark.admin, pytest.mark.no_parallel] + + +def test_killOp_op_fractional_double_rejected(collection): + """Test killOp rejects a fractional double op value.""" + result = execute_admin_command(collection, {"killOp": 1, "op": 3.14}) + assertFailureCode(result, BAD_VALUE_ERROR, msg="killOp should reject a non-integral op value") + + +def test_killOp_op_fractional_decimal_rejected(collection): + """Test killOp rejects a fractional decimal op value.""" + result = execute_admin_command(collection, {"killOp": 1, "op": Decimal128("3.14")}) + assertFailureCode(result, BAD_VALUE_ERROR, msg="killOp should reject a non-integral op value") + + +def test_killOp_op_nearly_int_double_rejected(collection): + """Test killOp rejects a double that is very close to but not an integer.""" + result = execute_admin_command(collection, {"killOp": 1, "op": 0.9999999999999999}) + assertFailureCode(result, BAD_VALUE_ERROR, msg="killOp should reject a non-integral op value") + + +def test_killOp_op_double_above_int32_max_rejected(collection): + """Test killOp rejects a double above the int32 range.""" + result = execute_admin_command(collection, {"killOp": 1, "op": float(INT32_OVERFLOW)}) + assertFailureCode( + result, + KILLOP_OPID_NOT_INT32_ERROR, + msg="killOp should reject an op value outside the int32 range", + ) + + +def test_killOp_op_long_above_int32_max_rejected(collection): + """Test killOp rejects a long above the int32 range.""" + result = execute_admin_command(collection, {"killOp": 1, "op": Int64(INT32_OVERFLOW)}) + assertFailureCode( + result, + KILLOP_OPID_NOT_INT32_ERROR, + msg="killOp should reject an op value outside the int32 range", + ) + + +def test_killOp_op_long_below_int32_min_rejected(collection): + """Test killOp rejects a long below the int32 range.""" + result = execute_admin_command(collection, {"killOp": 1, "op": Int64(INT32_UNDERFLOW)}) + assertFailureCode( + result, + KILLOP_OPID_NOT_INT32_ERROR, + msg="killOp should reject an op value outside the int32 range", + ) + + +def test_killOp_op_int64_max_rejected(collection): + """Test killOp rejects INT64_MAX (outside the int32 range).""" + result = execute_admin_command(collection, {"killOp": 1, "op": INT64_MAX}) + assertFailureCode( + result, + KILLOP_OPID_NOT_INT32_ERROR, + msg="killOp should reject an op value outside the int32 range", + ) + + +def test_killOp_op_int64_min_rejected(collection): + """Test killOp rejects INT64_MIN (outside the int32 range).""" + result = execute_admin_command(collection, {"killOp": 1, "op": INT64_MIN}) + assertFailureCode( + result, + KILLOP_OPID_NOT_INT32_ERROR, + msg="killOp should reject an op value outside the int32 range", + ) + + +def test_killOp_op_decimal_above_int32_max_rejected(collection): + """Test killOp rejects a decimal above the int32 range.""" + result = execute_admin_command(collection, {"killOp": 1, "op": Decimal128(str(INT32_OVERFLOW))}) + assertFailureCode( + result, + KILLOP_OPID_NOT_INT32_ERROR, + msg="killOp should reject an op value outside the int32 range", + ) + + +def test_killOp_on_non_admin_database_fails(collection): + """Test killOp run against non-admin database fails with error.""" + result = execute_command(collection, {"killOp": 1, "op": NON_RUNNING_OP_ID}) + assertFailureCode(result, UNAUTHORIZED_ERROR, msg="Should fail on non-admin database") + + +def test_killOp_missing_op_field(collection): + """Test killOp without op field fails with error.""" + result = execute_admin_command(collection, {"killOp": 1}) + assertFailureCode(result, NO_SUCH_KEY_ERROR, msg="Should fail without op field") diff --git a/documentdb_tests/framework/error_codes.py b/documentdb_tests/framework/error_codes.py index 2375b9dcd..b19f63778 100644 --- a/documentdb_tests/framework/error_codes.py +++ b/documentdb_tests/framework/error_codes.py @@ -146,6 +146,7 @@ DATETOSTRING_YEAR_RANGE_ERROR = 18537 DATETOSTRING_MISSING_DATE_ERROR = 18628 DATETOSTRING_NON_OBJECT_ERROR = 18629 +KILLOP_OPID_NOT_INT32_ERROR = 26823 EMPTY_DB_NAME_ERROR = 28539 FILTER_NON_OBJECT_ARG_ERROR = 28646 FILTER_UNKNOWN_FIELD_ERROR = 28647 diff --git a/documentdb_tests/framework/test_constants.py b/documentdb_tests/framework/test_constants.py index 4583984d7..992e75ef3 100644 --- a/documentdb_tests/framework/test_constants.py +++ b/documentdb_tests/framework/test_constants.py @@ -87,6 +87,9 @@ REGEX_PATTERN_LIMIT_BYTES = 16 * 1024 CLUSTERED_RECORD_ID_LIMIT_BYTES = 8 * 1024 * 1024 +# An op id used by killOp tests +NON_RUNNING_OP_ID = 999999999 + # Int32 lists NUMERIC_INT32_NEGATIVE = [INT32_UNDERFLOW, INT32_MIN] NUMERIC_INT32_ZERO = [INT32_ZERO]