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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,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",
)
Original file line number Diff line number Diff line change
@@ -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",
)
Original file line number Diff line number Diff line change
@@ -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")
1 change: 1 addition & 0 deletions documentdb_tests/framework/error_codes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions documentdb_tests/framework/test_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Loading