mirror of https://github.com/nucypher/nucypher.git
465 lines
15 KiB
Python
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
|