nucypher/tests/unit/conditions/test_json_api_condition.py

378 lines
12 KiB
Python

import json
import pytest
import requests
from nucypher.policy.conditions.exceptions import (
InvalidCondition,
JsonRequestException,
)
from nucypher.policy.conditions.json.api import JsonApiCondition
from nucypher.policy.conditions.lingo import ConditionLingo, ReturnValueTest
def test_json_api_condition_initialization():
condition = JsonApiCondition(
endpoint="https://api.example.com/data",
query="$.store.book[0].price",
return_value_test=ReturnValueTest("==", 0),
)
assert condition.endpoint == "https://api.example.com/data"
assert condition.query == "$.store.book[0].price"
assert condition.return_value_test.eval(0)
assert condition.timeout == JsonApiCondition.EXECUTION_CALL_TYPE.TIMEOUT
def test_json_api_condition_invalid_type():
with pytest.raises(
InvalidCondition, match="'condition_type' field - Must be equal to json-api"
):
_ = JsonApiCondition(
condition_type="INVALID_TYPE",
endpoint="https://api.example.com/data",
query="$.store.book[0].price",
return_value_test=ReturnValueTest("==", 0),
)
def test_json_api_https_enforcement():
with pytest.raises(InvalidCondition, match="Not a valid URL"):
_ = JsonApiCondition(
endpoint="http://api.example.com/data",
query="$.store.book[0].price",
return_value_test=ReturnValueTest("==", 0),
)
def test_json_api_invalid_authorization_token():
with pytest.raises(InvalidCondition, match="Invalid value for authorization token"):
_ = JsonApiCondition(
endpoint="https://api.example.com/data",
authorization_token="1234", # doesn't make sense hardcoding the token
query="$.store.book[0].price",
return_value_test=ReturnValueTest("==", 0),
)
def test_json_api_condition_with_primitive_response(mocker):
mock_response = mocker.Mock(status_code=200)
mock_response.json.return_value = 1
mocker.patch("requests.get", return_value=mock_response)
condition = JsonApiCondition(
endpoint="https://api.example.com/data",
return_value_test=ReturnValueTest("==", 1),
)
success, result = condition.verify()
assert success is True
def test_json_api_condition_fetch(mocker):
mock_response = mocker.Mock(status_code=200)
mock_response.json.return_value = {"store": {"book": [{"title": "Test Title"}]}}
mocker.patch("requests.get", return_value=mock_response)
condition = JsonApiCondition(
endpoint="https://api.example.com/data",
query="$.store.book[0].title",
return_value_test=ReturnValueTest("==", "'Test Title'"),
)
data = condition.execution_call._fetch(condition.endpoint)
assert data == {"store": {"book": [{"title": "Test Title"}]}}
def test_json_api_condition_fetch_failure(mocker):
mocker.patch(
"requests.get", side_effect=requests.exceptions.RequestException("Error")
)
condition = JsonApiCondition(
endpoint="https://api.example.com/data",
query="$.store.book[0].price",
return_value_test=ReturnValueTest("==", 1),
)
with pytest.raises(JsonRequestException, match="Failed to fetch from endpoint"):
condition.execution_call._fetch(condition.endpoint)
def test_json_api_condition_verify(mocker):
mock_response = mocker.Mock(status_code=200)
mock_response.json.return_value = {"store": {"book": [{"price": 1}]}}
mocked_method = mocker.patch("requests.get", return_value=mock_response)
parameters = {
"arg1": "val1",
"arg2": "val2",
}
condition = JsonApiCondition(
endpoint="https://api.example.com/data",
parameters=parameters,
query="$.store.book[0].price",
return_value_test=ReturnValueTest("==", 1),
)
result, value = condition.verify()
assert result is True
assert value == 1
# check that appropriate kwarg used for respective http method
assert mocked_method.call_count == 1
assert mocked_method.call_args.kwargs["params"] == parameters
@pytest.mark.parametrize(
"json_return, value_test",
(
("Title", "'Title'"), # no whitespace
("Test Title", "'Test Title'"), # whitespace
('"Already double quoted"', "'Already double quoted'"), # already quoted
("'Already single quoted'", "'Already single quoted'"), # already quoted
("O'Sullivan", '"O\'Sullivan"'), # single quote present
('Hello, "World!"', "'Hello, \"World!\"'"), # double quotes present
),
)
def test_json_api_condition_verify_strings(json_return, value_test, mocker):
mock_response = mocker.Mock(status_code=200)
json_string = json.loads(json.dumps(json_return))
assert json_string == json_return, "valid json string used for test"
mock_response.json.return_value = {"store": {"book": [{"text": f"{json_return}"}]}}
mocker.patch("requests.get", return_value=mock_response)
condition = JsonApiCondition(
endpoint="https://api.example.com/data",
query="$.store.book[0].text",
return_value_test=ReturnValueTest("==", f"{value_test}"),
)
result, value = condition.verify()
assert result is True, f"{json_return} vs {value_test}"
assert value == json_return
def test_json_api_condition_verify_invalid_json(mocker):
mock_response = mocker.Mock(status_code=200)
mock_response.json.side_effect = requests.exceptions.RequestException("Error")
mocker.patch("requests.get", return_value=mock_response)
condition = JsonApiCondition(
endpoint="https://api.example.com/data",
query="$.store.book[0].price",
return_value_test=ReturnValueTest("==", 2),
)
with pytest.raises(JsonRequestException, match="Failed to extract JSON response"):
condition.verify()
def test_json_api_non_200_status(mocker):
# Mock the requests.get method to return a response with non-JSON content
mock_response = mocker.Mock()
mock_response.status_code = 400
mocker.patch("requests.get", return_value=mock_response)
condition = JsonApiCondition(
endpoint="https://api.example.com/data",
query="$.store.book[0].price",
return_value_test=ReturnValueTest("==", 18),
)
with pytest.raises(JsonRequestException, match="Failed to fetch from endpoint"):
condition.verify()
def test_json_api_non_json_response(mocker):
# Mock the requests.get method to return a response with non-JSON content
mock_response = mocker.Mock()
mock_response.status_code = 200
mock_response.json.side_effect = ValueError("No JSON object could be decoded")
mock_response.text = "This is not JSON"
mocker.patch("requests.get", return_value=mock_response)
condition = JsonApiCondition(
endpoint="https://api.example.com/data",
query="$.store.book[0].price",
return_value_test=ReturnValueTest("==", 18),
)
with pytest.raises(JsonRequestException, match="Failed to extract JSON response"):
condition.verify()
def test_basic_json_api_condition_evaluation_with_parameters(mocker):
mocked_get = mocker.patch(
"requests.get",
return_value=mocker.Mock(
status_code=200, json=lambda: {"ethereum": {"usd": 0.0}}
),
)
condition = JsonApiCondition(
endpoint="https://api.coingecko.com/api/v3/simple/price",
parameters={
"ids": "ethereum",
"vs_currencies": "usd",
},
query="ethereum.usd",
return_value_test=ReturnValueTest("==", 0.0),
)
assert condition.verify() == (True, 0.0)
assert mocked_get.call_count == 1
def test_json_api_condition_evaluation_with_auth_token(mocker):
mocked_get = mocker.patch(
"requests.get",
return_value=mocker.Mock(
status_code=200, json=lambda: {"ethereum": {"usd": 0.0}}
),
)
condition = JsonApiCondition(
endpoint="https://api.coingecko.com/api/v3/simple/price",
parameters={
"ids": "ethereum",
"vs_currencies": "usd",
},
authorization_token=":authToken",
query="ethereum.usd",
return_value_test=ReturnValueTest("==", 0.0),
)
assert condition.authorization_token == ":authToken"
auth_token = "1234567890"
context = {":authToken": f"{auth_token}"}
assert condition.verify(**context) == (True, 0.0)
assert mocked_get.call_count == 1
assert (
mocked_get.call_args.kwargs["headers"]["Authorization"]
== f"Bearer {auth_token}"
)
def test_json_api_condition_evaluation_with_user_address_context_variable(
mocker, valid_eip4361_auth_message
):
mocked_get = mocker.patch(
"requests.get",
return_value=mocker.Mock(status_code=200, json=lambda: 0.0),
)
condition = JsonApiCondition(
endpoint="https://randomapi.com/api/v3/:userAddress",
return_value_test=ReturnValueTest("==", 0.0),
)
auth_message = valid_eip4361_auth_message
context = {":userAddress": auth_message}
assert condition.verify(**context) == (True, 0.0)
assert mocked_get.call_count == 1
call_args = mocked_get.call_args
assert (
call_args.args[0] == f"https://randomapi.com/api/v3/{auth_message['address']}"
)
def test_json_api_condition_evaluation_with_various_context_variables(mocker):
mocked_get = mocker.patch(
"requests.get",
return_value=mocker.Mock(
status_code=200, json=lambda: {"ethereum": {"cad": 0.0}}
),
)
condition = JsonApiCondition(
endpoint="https://api.coingecko.com/api/:version/simple/:endpointPath",
parameters={
"ids": "ethereum",
"vs_currencies": ":vsCurrency",
},
authorization_token=":authToken",
query="ethereum.:vsCurrency",
return_value_test=ReturnValueTest("==", ":expectedPrice"),
)
assert condition.authorization_token == ":authToken"
auth_token = "1234567890"
context = {
":endpointPath": "price",
":version": "v3",
":vsCurrency": "cad",
":authToken": f"{auth_token}",
":expectedPrice": 0.0,
}
assert condition.verify(**context) == (True, 0.0)
assert mocked_get.call_count == 1
call_args = mocked_get.call_args
assert call_args.args == (
f"https://api.coingecko.com/api/{context[':version']}/simple/{context[':endpointPath']}",
)
assert call_args.kwargs["headers"]["Authorization"] == f"Bearer {auth_token}"
assert call_args.kwargs["params"] == {
"ids": "ethereum",
"vs_currencies": context[":vsCurrency"],
}
def test_json_api_condition_from_lingo_expression():
lingo_dict = {
"conditionType": "json-api",
"endpoint": "https://api.example.com/data",
"parameters": {
"ids": "ethereum",
"vs_currencies": "usd",
},
"query": "$.store.book[0].price",
"returnValueTest": {
"comparator": "==",
"value": 1.0,
},
}
cls = ConditionLingo.resolve_condition_class(lingo_dict, version=1)
assert cls == JsonApiCondition
lingo_json = json.dumps(lingo_dict)
condition = JsonApiCondition.from_json(lingo_json)
assert isinstance(condition, JsonApiCondition)
assert condition.to_dict() == lingo_dict
def test_json_api_condition_from_lingo_expression_with_authorization():
lingo_dict = {
"conditionType": "json-api",
"endpoint": "https://api.example.com/data",
"parameters": {
"ids": "ethereum",
"vs_currencies": "usd",
},
"authorizationToken": ":authorizationToken",
"query": "$.store.book[0].price",
"returnValueTest": {
"comparator": "==",
"value": 1.0,
},
}
cls = ConditionLingo.resolve_condition_class(lingo_dict, version=1)
assert cls == JsonApiCondition
lingo_json = json.dumps(lingo_dict)
condition = JsonApiCondition.from_json(lingo_json)
assert isinstance(condition, JsonApiCondition)
assert condition.to_dict() == lingo_dict
def test_ambiguous_json_path_multiple_results(mocker):
mock_response = mocker.Mock(status_code=200)
mock_response.json.return_value = {"store": {"book": [{"price": 1}, {"price": 2}]}}
mocker.patch("requests.get", return_value=mock_response)
condition = JsonApiCondition(
endpoint="https://api.example.com/data",
query="$.store.book[*].price",
return_value_test=ReturnValueTest("==", 1),
)
with pytest.raises(JsonRequestException, match="Ambiguous JSONPath query"):
condition.verify()