nucypher/tests/unit/conditions/test_compound_condition.py

465 lines
15 KiB
Python

from unittest.mock import Mock
import pytest
from nucypher.policy.conditions.base import AccessControlCondition
from nucypher.policy.conditions.exceptions import InvalidCondition
from nucypher.policy.conditions.lingo import (
AndCompoundCondition,
CompoundAccessControlCondition,
ConditionType,
NotCompoundCondition,
OrCompoundCondition,
)
@pytest.fixture(scope="function")
def mock_conditions():
condition_1 = Mock(spec=AccessControlCondition)
condition_1.verify.return_value = (True, 1)
condition_1.to_dict.return_value = {
"value": 1
} # needed for "id" value calc for CompoundAccessControlCondition
condition_2 = Mock(spec=AccessControlCondition)
condition_2.verify.return_value = (True, 2)
condition_2.to_dict.return_value = {"value": 2}
condition_3 = Mock(spec=AccessControlCondition)
condition_3.verify.return_value = (True, 3)
condition_3.to_dict.return_value = {"value": 3}
condition_4 = Mock(spec=AccessControlCondition)
condition_4.verify.return_value = (True, 4)
condition_4.to_dict.return_value = {"value": 4}
return condition_1, condition_2, condition_3, condition_4
def test_invalid_compound_condition(time_condition, rpc_condition):
for operator in CompoundAccessControlCondition.OPERATORS:
if operator == CompoundAccessControlCondition.NOT_OPERATOR:
operands = [time_condition]
else:
operands = [time_condition, rpc_condition]
# invalid condition type
with pytest.raises(InvalidCondition, match=ConditionType.COMPOUND.value):
_ = CompoundAccessControlCondition(
condition_type=ConditionType.TIME.value,
operator=operator,
operands=operands,
)
# invalid operator - 1 operand
with pytest.raises(InvalidCondition):
_ = CompoundAccessControlCondition(operator="5True", operands=[time_condition])
# invalid operator - 2 operands
with pytest.raises(InvalidCondition):
_ = CompoundAccessControlCondition(
operator="5True", operands=[time_condition, rpc_condition]
)
# no operands
with pytest.raises(InvalidCondition):
_ = CompoundAccessControlCondition(operator=operator, operands=[])
# > 1 operand for not operator
with pytest.raises(InvalidCondition):
_ = CompoundAccessControlCondition(
operator=CompoundAccessControlCondition.NOT_OPERATOR,
operands=[time_condition, rpc_condition],
)
# < 2 operands for or operator
with pytest.raises(InvalidCondition):
_ = CompoundAccessControlCondition(
operator=CompoundAccessControlCondition.OR_OPERATOR,
operands=[time_condition],
)
# < 2 operands for and operator
with pytest.raises(InvalidCondition):
_ = CompoundAccessControlCondition(
operator=CompoundAccessControlCondition.AND_OPERATOR,
operands=[rpc_condition],
)
@pytest.mark.parametrize("operator", CompoundAccessControlCondition.OPERATORS)
def test_compound_condition_schema_validation(operator, time_condition, rpc_condition):
if operator == CompoundAccessControlCondition.NOT_OPERATOR:
operands = [time_condition]
else:
operands = [time_condition, rpc_condition]
compound_condition = CompoundAccessControlCondition(
operator=operator, operands=operands
)
compound_condition_dict = compound_condition.to_dict()
# no issues here
CompoundAccessControlCondition.validate(compound_condition_dict)
# no issues with optional name
compound_condition_dict["name"] = "my_contract_condition"
CompoundAccessControlCondition.validate(compound_condition_dict)
with pytest.raises(InvalidCondition):
# incorrect condition type
compound_condition_dict = compound_condition.to_dict()
compound_condition_dict["condition_type"] = ConditionType.RPC.value
CompoundAccessControlCondition.validate(compound_condition_dict)
with pytest.raises(InvalidCondition):
# invalid operator
compound_condition_dict = compound_condition.to_dict()
compound_condition_dict["operator"] = "5True"
CompoundAccessControlCondition.validate(compound_condition_dict)
with pytest.raises(InvalidCondition):
# no operator
compound_condition_dict = compound_condition.to_dict()
del compound_condition_dict["operator"]
CompoundAccessControlCondition.validate(compound_condition_dict)
with pytest.raises(InvalidCondition):
# no operands
compound_condition_dict = compound_condition.to_dict()
del compound_condition_dict["operands"]
CompoundAccessControlCondition.validate(compound_condition_dict)
def test_and_condition_and_short_circuit(mock_conditions):
condition_1, condition_2, condition_3, condition_4 = mock_conditions
and_condition = AndCompoundCondition(
operands=[
condition_1,
condition_2,
condition_3,
condition_4,
]
)
# ensure that all conditions evaluated when all return True
result, value = and_condition.verify()
assert result is True
assert len(value) == 4, "all conditions evaluated"
assert value == [1, 2, 3, 4]
# ensure that short circuit happens when 1st condition is false
condition_1.verify.return_value = (False, 1)
result, value = and_condition.verify()
assert result is False
assert len(value) == 1, "only one condition evaluated"
assert value == [1]
# short circuit occurs for 3rd entry
condition_1.verify.return_value = (True, 1)
condition_3.verify.return_value = (False, 3)
result, value = and_condition.verify()
assert result is False
assert len(value) == 3, "3-of-4 conditions evaluated"
assert value == [1, 2, 3]
def test_or_condition_and_short_circuit(mock_conditions):
condition_1, condition_2, condition_3, condition_4 = mock_conditions
or_condition = OrCompoundCondition(
operands=[
condition_1,
condition_2,
condition_3,
condition_4,
]
)
# ensure that only first condition evaluated when first is True
condition_1.verify.return_value = (True, 1) # short circuit here
result, value = or_condition.verify()
assert result is True
assert len(value) == 1, "only first condition needs to be evaluated"
assert value == [1]
# ensure first True condition is returned
condition_1.verify.return_value = (False, 1)
condition_2.verify.return_value = (False, 2)
condition_3.verify.return_value = (True, 3) # short circuit here
result, value = or_condition.verify()
assert result is True
assert len(value) == 3, "third condition causes short circuit"
assert value == [1, 2, 3]
# no short circuit occurs when all are False
condition_1.verify.return_value = (False, 1)
condition_2.verify.return_value = (False, 2)
condition_3.verify.return_value = (False, 3)
condition_4.verify.return_value = (False, 4)
result, value = or_condition.verify()
assert result is False
assert len(value) == 4, "all conditions evaluated"
assert value == [1, 2, 3, 4]
def test_compound_condition(mock_conditions):
condition_1, condition_2, condition_3, condition_4 = mock_conditions
compound_condition = AndCompoundCondition(
operands=[
OrCompoundCondition(
operands=[
condition_1,
condition_2,
condition_3,
]
),
condition_4,
]
)
# all conditions are True
result, value = compound_condition.verify()
assert result is True
assert len(value) == 2, "or_condition and condition_4"
assert value == [[1], 4]
# or condition is False
condition_1.verify.return_value = (False, 1)
condition_2.verify.return_value = (False, 2)
condition_3.verify.return_value = (False, 3)
result, value = compound_condition.verify()
assert result is False
assert len(value) == 1, "or_condition"
assert value == [
[1, 2, 3]
] # or-condition does not short circuit, but and-condition is short-circuited because or-condition is False
# or condition is True but condition 4 is False
condition_1.verify.return_value = (True, 1)
condition_4.verify.return_value = (False, 4)
result, value = compound_condition.verify()
assert result is False
assert len(value) == 2, "or_condition and condition_4"
assert value == [
[1],
4,
] # or-condition short-circuited because condition_1 was True
# condition_4 is now true
condition_4.verify.return_value = (True, 4)
result, value = compound_condition.verify()
assert result is True
assert len(value) == 2, "or_condition and condition_4"
assert value == [
[1],
4,
] # or-condition short-circuited because condition_1 was True
def test_nested_compound_condition(mock_conditions):
condition_1, condition_2, condition_3, condition_4 = mock_conditions
nested_compound_condition = AndCompoundCondition(
operands=[
OrCompoundCondition(
operands=[
condition_1,
AndCompoundCondition(
operands=[
condition_2,
condition_3,
]
),
]
),
condition_4,
]
)
# all conditions are True
result, value = nested_compound_condition.verify()
assert result is True
assert len(value) == 2, "or_condition and condition_4"
assert value == [[1], 4] # or short-circuited since condition_1 is True
# set condition_1 to False so nested and-condition must be evaluated
condition_1.verify.return_value = (False, 1)
result, value = nested_compound_condition.verify()
assert result is True
assert len(value) == 2, "or_condition and condition_4"
assert value == [
[1, [2, 3]],
4,
] # nested and-condition was evaluated and evaluated to True
# set condition_4 to False so that overall result flips to False
condition_4.verify.return_value = (False, 4)
result, value = nested_compound_condition.verify()
assert result is False
assert len(value) == 2, "or_condition and condition_4"
assert value == [[1, [2, 3]], 4]
def test_not_compound_condition(mock_conditions):
condition_1, condition_2, condition_3, condition_4 = mock_conditions
not_condition = NotCompoundCondition(operand=condition_1)
#
# simple `not`
#
condition_1.verify.return_value = (True, 1)
result, value = not_condition.verify()
assert result is False
assert value == 1
condition_1.verify.return_value = (False, 2)
result, value = not_condition.verify()
assert result is True
assert value == 2
#
# `not` of `or` condition
#
# only True
condition_1.verify.return_value = (True, 1)
condition_2.verify.return_value = (True, 2)
condition_3.verify.return_value = (True, 3)
or_condition = OrCompoundCondition(
operands=[
condition_1,
condition_2,
condition_3,
]
)
not_condition = NotCompoundCondition(operand=or_condition)
or_result, or_value = or_condition.verify()
result, value = not_condition.verify()
assert result is False
assert result is (not or_result)
assert value == or_value
# only False
condition_1.verify.return_value = (False, 1)
condition_2.verify.return_value = (False, 2)
condition_3.verify.return_value = (False, 3)
or_result, or_value = or_condition.verify()
result, value = not_condition.verify()
assert result is True
assert result is (not or_result)
assert value == or_value
# mixture of True/False
condition_1.verify.return_value = (False, 1)
condition_2.verify.return_value = (False, 2)
condition_3.verify.return_value = (True, 3)
or_result, or_value = or_condition.verify()
result, value = not_condition.verify()
assert result is False
assert result is (not or_result)
assert value == or_value
#
# `not` of `and` condition
#
# only True
condition_1.verify.return_value = (True, 1)
condition_2.verify.return_value = (True, 2)
condition_3.verify.return_value = (True, 3)
and_condition = AndCompoundCondition(
operands=[
condition_1,
condition_2,
condition_3,
]
)
not_condition = NotCompoundCondition(operand=and_condition)
and_result, and_value = and_condition.verify()
result, value = not_condition.verify()
assert result is False
assert result is (not and_result)
assert value == and_value
# only False
condition_1.verify.return_value = (False, 1)
condition_2.verify.return_value = (False, 2)
condition_3.verify.return_value = (False, 3)
and_result, and_value = and_condition.verify()
result, value = not_condition.verify()
assert result is True
assert result is (not and_result)
assert value == and_value
# mixture of True/False
condition_1.verify.return_value = (False, 1)
condition_2.verify.return_value = (True, 2)
condition_3.verify.return_value = (False, 3)
and_result, and_value = and_condition.verify()
result, value = not_condition.verify()
assert result is True
assert result is (not and_result)
assert value == and_value
#
# Complex nested `or` and `and` (reused nested compound condition in previous test)
#
nested_compound_condition = AndCompoundCondition(
operands=[
OrCompoundCondition(
operands=[
condition_1,
AndCompoundCondition(
operands=[
condition_2,
condition_3,
]
),
]
),
condition_4,
]
)
not_condition = NotCompoundCondition(operand=nested_compound_condition)
# reset all conditions to True
condition_1.verify.return_value = (True, 1)
condition_2.verify.return_value = (True, 2)
condition_3.verify.return_value = (True, 3)
condition_4.verify.return_value = (True, 4)
nested_result, nested_value = nested_compound_condition.verify()
result, value = not_condition.verify()
assert result is False
assert result is (not nested_result)
assert value == nested_value
# set condition_1 to False so nested and-condition must be evaluated
condition_1.verify.return_value = (False, 1)
nested_result, nested_value = nested_compound_condition.verify()
result, value = not_condition.verify()
assert result is False
assert result is (not nested_result)
assert value == nested_value
# set condition_4 to False so that overall result flips to False, so `not` is now True
condition_4.verify.return_value = (False, 4)
nested_result, nested_value = nested_compound_condition.verify()
result, value = not_condition.verify()
assert result is True
assert result is (not nested_result)
assert value == nested_value