360 lines
9.4 KiB
Python
360 lines
9.4 KiB
Python
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from openfeature.api import (
|
|
add_handler,
|
|
add_hooks,
|
|
clear_hooks,
|
|
clear_providers,
|
|
get_client,
|
|
get_evaluation_context,
|
|
get_hooks,
|
|
get_provider_metadata,
|
|
remove_handler,
|
|
set_evaluation_context,
|
|
set_provider,
|
|
shutdown,
|
|
)
|
|
from openfeature.evaluation_context import EvaluationContext
|
|
from openfeature.event import EventDetails, ProviderEvent, ProviderEventDetails
|
|
from openfeature.exception import ErrorCode, GeneralError, ProviderFatalError
|
|
from openfeature.hook import Hook
|
|
from openfeature.provider import FeatureProvider, Metadata, ProviderStatus
|
|
from openfeature.provider.no_op_provider import NoOpProvider
|
|
|
|
|
|
def test_should_not_raise_exception_with_noop_client():
|
|
# Given
|
|
# No provider has been set
|
|
# When
|
|
client = get_client()
|
|
|
|
# Then
|
|
assert isinstance(client.provider, NoOpProvider)
|
|
|
|
|
|
def test_should_return_open_feature_client_when_configured_correctly():
|
|
# Given
|
|
set_provider(NoOpProvider())
|
|
|
|
# When
|
|
client = get_client()
|
|
|
|
# Then
|
|
assert isinstance(client.provider, NoOpProvider)
|
|
|
|
|
|
def test_should_try_set_provider_and_fail_if_none_provided():
|
|
# Given
|
|
# When
|
|
with pytest.raises(GeneralError) as ge:
|
|
set_provider(provider=None)
|
|
|
|
# Then
|
|
assert ge.value.error_message == "No provider"
|
|
assert ge.value.error_code == ErrorCode.GENERAL
|
|
|
|
|
|
def test_should_invoke_provider_initialize_function_on_newly_registered_provider():
|
|
# Given
|
|
evaluation_context = EvaluationContext("targeting_key", {"attr1": "val1"})
|
|
provider = MagicMock(spec=FeatureProvider)
|
|
|
|
# When
|
|
set_evaluation_context(evaluation_context)
|
|
set_provider(provider)
|
|
|
|
# Then
|
|
provider.initialize.assert_called_with(evaluation_context)
|
|
|
|
|
|
def test_should_invoke_provider_shutdown_function_once_provider_is_no_longer_in_use():
|
|
# Given
|
|
provider_1 = MagicMock(spec=FeatureProvider)
|
|
provider_2 = MagicMock(spec=FeatureProvider)
|
|
|
|
# When
|
|
set_provider(provider_1)
|
|
set_provider(provider_2)
|
|
|
|
# Then
|
|
assert provider_1.shutdown.called
|
|
|
|
|
|
def test_should_retrieve_metadata_for_configured_provider():
|
|
# Given
|
|
set_provider(NoOpProvider())
|
|
|
|
# When
|
|
metadata = get_provider_metadata()
|
|
|
|
# Then
|
|
assert isinstance(metadata, Metadata)
|
|
assert metadata.name == "No-op Provider"
|
|
|
|
|
|
def test_should_raise_an_exception_if_no_evaluation_context_set():
|
|
# Given
|
|
with pytest.raises(GeneralError) as ge:
|
|
set_evaluation_context(evaluation_context=None)
|
|
# Then
|
|
assert ge.value
|
|
assert ge.value.error_message == "No api level evaluation context"
|
|
assert ge.value.error_code == ErrorCode.GENERAL
|
|
|
|
|
|
def test_should_successfully_set_evaluation_context_for_api():
|
|
# Given
|
|
evaluation_context = EvaluationContext("targeting_key", {"attr1": "val1"})
|
|
|
|
# When
|
|
set_evaluation_context(evaluation_context)
|
|
global_evaluation_context = get_evaluation_context()
|
|
|
|
# Then
|
|
assert global_evaluation_context
|
|
assert global_evaluation_context.targeting_key == evaluation_context.targeting_key
|
|
assert global_evaluation_context.attributes == evaluation_context.attributes
|
|
|
|
|
|
def test_should_add_hooks_to_api_hooks():
|
|
# Given
|
|
hook_1 = MagicMock(spec=Hook)
|
|
hook_2 = MagicMock(spec=Hook)
|
|
clear_hooks()
|
|
|
|
# When
|
|
add_hooks([hook_1])
|
|
add_hooks([hook_2])
|
|
|
|
# Then
|
|
assert get_hooks() == [hook_1, hook_2]
|
|
|
|
|
|
def test_should_call_provider_shutdown_on_api_shutdown():
|
|
# Given
|
|
provider = MagicMock(spec=FeatureProvider)
|
|
set_provider(provider)
|
|
|
|
# When
|
|
shutdown()
|
|
|
|
# Then
|
|
assert provider.shutdown.called
|
|
|
|
|
|
def test_should_provide_a_function_to_bind_provider_through_domain():
|
|
# Given
|
|
provider = MagicMock(spec=FeatureProvider)
|
|
test_client = get_client("test")
|
|
default_client = get_client()
|
|
|
|
# When
|
|
set_provider(provider, domain="test")
|
|
|
|
# Then
|
|
assert default_client.provider != provider
|
|
assert default_client.domain is None
|
|
|
|
assert test_client.provider == provider
|
|
assert test_client.domain == "test"
|
|
|
|
|
|
def test_should_not_initialize_provider_already_bound_to_another_domain():
|
|
# Given
|
|
provider = MagicMock(spec=FeatureProvider)
|
|
set_provider(provider, "foo")
|
|
|
|
# When
|
|
set_provider(provider, "bar")
|
|
|
|
# Then
|
|
provider.initialize.assert_called_once()
|
|
|
|
|
|
def test_should_shutdown_unbound_provider():
|
|
# Given
|
|
provider = MagicMock(spec=FeatureProvider)
|
|
set_provider(provider, "foo")
|
|
|
|
# When
|
|
other_provider = MagicMock(spec=FeatureProvider)
|
|
set_provider(other_provider, "foo")
|
|
|
|
provider.shutdown.assert_called_once()
|
|
|
|
|
|
def test_should_not_shutdown_provider_bound_to_another_domain():
|
|
# Given
|
|
provider = MagicMock(spec=FeatureProvider)
|
|
set_provider(provider, "foo")
|
|
set_provider(provider, "bar")
|
|
|
|
# When
|
|
other_provider = MagicMock(spec=FeatureProvider)
|
|
set_provider(other_provider, "foo")
|
|
|
|
provider.shutdown.assert_not_called()
|
|
|
|
|
|
def test_shutdown_should_shutdown_every_registered_provider_once():
|
|
# Given
|
|
provider_1 = MagicMock(spec=FeatureProvider)
|
|
provider_2 = MagicMock(spec=FeatureProvider)
|
|
set_provider(provider_1)
|
|
set_provider(provider_1, "foo")
|
|
set_provider(provider_2, "bar")
|
|
set_provider(provider_2, "baz")
|
|
|
|
# When
|
|
shutdown()
|
|
|
|
# Then
|
|
provider_1.shutdown.assert_called_once()
|
|
provider_2.shutdown.assert_called_once()
|
|
|
|
|
|
def test_clear_providers_shutdowns_every_provider_and_resets_default_provider():
|
|
# Given
|
|
provider_1 = MagicMock(spec=FeatureProvider)
|
|
provider_2 = MagicMock(spec=FeatureProvider)
|
|
set_provider(provider_1)
|
|
set_provider(provider_2, "foo")
|
|
set_provider(provider_2, "bar")
|
|
|
|
# When
|
|
clear_providers()
|
|
|
|
# Then
|
|
provider_1.shutdown.assert_called_once()
|
|
provider_2.shutdown.assert_called_once()
|
|
assert isinstance(get_client().provider, NoOpProvider)
|
|
|
|
|
|
def test_provider_events():
|
|
# Given
|
|
spy = MagicMock()
|
|
provider = NoOpProvider()
|
|
set_provider(provider)
|
|
|
|
add_handler(ProviderEvent.PROVIDER_READY, spy.provider_ready)
|
|
add_handler(
|
|
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, spy.provider_configuration_changed
|
|
)
|
|
add_handler(ProviderEvent.PROVIDER_ERROR, spy.provider_error)
|
|
add_handler(ProviderEvent.PROVIDER_STALE, spy.provider_stale)
|
|
|
|
provider_details = ProviderEventDetails(message="message")
|
|
details = EventDetails.from_provider_event_details(
|
|
provider.get_metadata().name, provider_details
|
|
)
|
|
|
|
# When
|
|
provider.emit_provider_configuration_changed(provider_details)
|
|
provider.emit_provider_error(provider_details)
|
|
provider.emit_provider_stale(provider_details)
|
|
|
|
# Then
|
|
# NOTE: provider_ready is called immediately after adding the handler
|
|
spy.provider_ready.assert_called_once()
|
|
spy.provider_configuration_changed.assert_called_once_with(details)
|
|
spy.provider_error.assert_called_once_with(details)
|
|
spy.provider_stale.assert_called_once_with(details)
|
|
|
|
|
|
def test_add_remove_event_handler():
|
|
# Given
|
|
provider = NoOpProvider()
|
|
set_provider(provider)
|
|
|
|
spy = MagicMock()
|
|
|
|
add_handler(
|
|
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, spy.provider_configuration_changed
|
|
)
|
|
remove_handler(
|
|
ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, spy.provider_configuration_changed
|
|
)
|
|
|
|
provider_details = ProviderEventDetails(message="message")
|
|
|
|
# When
|
|
provider.emit_provider_configuration_changed(provider_details)
|
|
|
|
# Then
|
|
spy.provider_configuration_changed.assert_not_called()
|
|
|
|
|
|
# Requirement 5.3.3
|
|
def test_handlers_attached_to_provider_already_in_associated_state_should_run_immediately():
|
|
# Given
|
|
provider = NoOpProvider()
|
|
set_provider(provider)
|
|
spy = MagicMock()
|
|
|
|
# When
|
|
add_handler(ProviderEvent.PROVIDER_READY, spy.provider_ready)
|
|
|
|
# Then
|
|
spy.provider_ready.assert_called_once()
|
|
|
|
|
|
def test_provider_ready_handlers_run_if_provider_initialize_function_terminates_normally():
|
|
# Given
|
|
provider = NoOpProvider()
|
|
|
|
spy = MagicMock()
|
|
add_handler(ProviderEvent.PROVIDER_READY, spy.provider_ready)
|
|
spy.reset_mock() # reset the mock to avoid counting the immediate call on subscribe
|
|
|
|
# When
|
|
set_provider(provider)
|
|
|
|
# Then
|
|
spy.provider_ready.assert_called_once()
|
|
|
|
|
|
def test_provider_error_handlers_run_if_provider_initialize_function_terminates_abnormally():
|
|
# Given
|
|
provider = MagicMock(spec=FeatureProvider)
|
|
provider.initialize.side_effect = ProviderFatalError()
|
|
|
|
spy = MagicMock()
|
|
add_handler(ProviderEvent.PROVIDER_ERROR, spy.provider_error)
|
|
|
|
# When
|
|
set_provider(provider)
|
|
|
|
# Then
|
|
spy.provider_error.assert_called_once()
|
|
|
|
|
|
def test_provider_status_is_updated_after_provider_emits_event():
|
|
# Given
|
|
provider = NoOpProvider()
|
|
set_provider(provider)
|
|
client = get_client()
|
|
|
|
# When
|
|
provider.emit_provider_error(ProviderEventDetails(error_code=ErrorCode.GENERAL))
|
|
# Then
|
|
assert client.get_provider_status() == ProviderStatus.ERROR
|
|
|
|
# When
|
|
provider.emit_provider_error(
|
|
ProviderEventDetails(error_code=ErrorCode.PROVIDER_FATAL)
|
|
)
|
|
# Then
|
|
assert client.get_provider_status() == ProviderStatus.FATAL
|
|
|
|
# When
|
|
provider.emit_provider_stale(ProviderEventDetails())
|
|
# Then
|
|
assert client.get_provider_status() == ProviderStatus.STALE
|
|
|
|
# When
|
|
provider.emit_provider_ready(ProviderEventDetails())
|
|
# Then
|
|
assert client.get_provider_status() == ProviderStatus.READY
|