forked from quipucords/quipucords
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request quipucords#2132 from nicolearagao/task-feature-flag
Creates class FeatureFlag to handle when new features will be added
- Loading branch information
Showing
5 changed files
with
217 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -125,3 +125,6 @@ yarn.lock | |
|
||
# symbolic link to roles | ||
quipucords/roles/ | ||
|
||
# asdf | ||
.tool-versions |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
"""Filters feature flags from system env variables.""" | ||
|
||
import os | ||
|
||
DEFAULT_FEATURE_FLAGS_VALUES = { | ||
"OVERALL_STATUS": False, | ||
} | ||
|
||
VALID_VALUES_FOR_ENV_VARIABLES = ["0", "1"] | ||
|
||
|
||
class FeatureFlag: | ||
"""Handle environment variables and its status.""" | ||
|
||
def __init__(self): | ||
"""Attributes and values are generated dynamically. | ||
Attributes keys-values are received in initial_data. | ||
""" | ||
initial_data = self.get_feature_flags_from_env() | ||
for key, value in initial_data.items(): | ||
setattr(self, key, value) | ||
|
||
def is_feature_active(self, feature_name): | ||
"""Return attribute value.""" | ||
try: | ||
return getattr(self, feature_name) | ||
except AttributeError: | ||
raise ValueError(f"{feature_name=} is not a valid input.") | ||
|
||
@classmethod | ||
def get_feature_flags_from_env(cls): | ||
"""Filter feature flags from environment variables.""" | ||
feature_flags = DEFAULT_FEATURE_FLAGS_VALUES.copy() | ||
for key, value in os.environ.items(): | ||
if key.upper().startswith("QPC_FEATURE_"): | ||
feature_name = key.upper().replace("QPC_FEATURE_", "") | ||
feature_value = value.strip() | ||
if feature_value in VALID_VALUES_FOR_ENV_VARIABLES: | ||
feature_flags[feature_name] = bool(int(feature_value)) | ||
else: | ||
raise ValueError( | ||
f"'{feature_value}' from '{key}' can't be converted " | ||
"to int, verify your environment variables." | ||
) | ||
return feature_flags |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
"""Test the FeatureFlag class and helper function.""" | ||
|
||
import os | ||
from unittest import mock | ||
|
||
import pytest | ||
|
||
from .featureflag import FeatureFlag | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"env_name,env_value,feature_name,feature_value", | ||
( | ||
("QPC_FEATURE_TEST", "0", "TEST", False), | ||
("QPC_FEATURE_TEST", "1", "TEST", True), | ||
), | ||
) | ||
def test_get_feature_flags_from_env( | ||
env_name, env_value, feature_name, feature_value | ||
): | ||
"""Tests if function retrieves new variables set in .env.""" | ||
with mock.patch.dict(os.environ, {env_name: env_value}): | ||
dict_with_test_flags = FeatureFlag.get_feature_flags_from_env() | ||
assert feature_name in dict_with_test_flags | ||
assert dict_with_test_flags[feature_name] is feature_value | ||
|
||
|
||
def test_if_returns_default_value_if_another_env_set(): | ||
"""Tests if function also returns variables in default dict.""" | ||
with mock.patch.dict(os.environ, ({"QPC_FEATURE_TEST": "0"})): | ||
dict_with_test_flags = FeatureFlag.get_feature_flags_from_env() | ||
assert "OVERALL_STATUS" in dict_with_test_flags | ||
assert "TEST" in dict_with_test_flags | ||
assert dict_with_test_flags["OVERALL_STATUS"] is False | ||
assert dict_with_test_flags["TEST"] is False | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"env_name,env_value,feature_name,feature_value", | ||
( | ||
("QPC_FEATURE_OVERALL_STATUS", "1", "OVERALL_STATUS", True), | ||
("QPC_FEATURE_TEST", "0", "TEST", False), | ||
), | ||
) | ||
def test_if_value_for_env_default_list_gets_updated( | ||
env_name, env_value, feature_name, feature_value | ||
): | ||
"""Tests if function updates env variable in default.""" | ||
with mock.patch.dict(os.environ, ({env_name: env_value})): | ||
dict_with_test_flags = FeatureFlag.get_feature_flags_from_env() | ||
assert feature_name in dict_with_test_flags | ||
assert dict_with_test_flags[feature_name] is feature_value | ||
|
||
|
||
def test_when_value_cant_be_cast_to_int(): | ||
"""Tests if function only updates values if it can be cast to int.""" | ||
with mock.patch.dict( | ||
os.environ, ({"QPC_FEATURE_OVERALL_STATUS": "wrong"}) | ||
), pytest.raises(ValueError) as exc: | ||
FeatureFlag.get_feature_flags_from_env() | ||
assert ( | ||
str(exc.value) == "'wrong' from 'QPC_FEATURE_OVERALL_STATUS'" | ||
" can't be converted to int, verify your" | ||
" environment variables." | ||
) | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"env_name,env_value", | ||
( | ||
("QPC_FEATURE_TEST", "10"), | ||
("QPC_FEATURE_TEST1", "3"), | ||
("QPC_FEATURE_TEST2", "2000"), | ||
), | ||
) | ||
def test_when_int_is_not_valid_value_for_env( | ||
env_name, env_value, | ||
): | ||
"""Test when int is not a valid value for env.""" | ||
with mock.patch.dict(os.environ, ({env_name: env_value})), pytest.raises( | ||
ValueError, match="can't be converted to int" | ||
): | ||
FeatureFlag.get_feature_flags_from_env() | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"env_name,env_value,feature_name", | ||
( | ||
("TEST_QPC_FEATURE_", "1", "TEST"), | ||
("TEST_QPC_FEATURE_", "1", "TEST_"), | ||
("QPC_TEST1_FEATURE_", "0", "TEST1"), | ||
("QPC_TEST1_FEATURE_", "0", "_TEST1"), | ||
("QPC_TEST1_FEATURE_", "0", "TEST1_"), | ||
), | ||
) | ||
def test_function_only_adds_names_follow_standard( | ||
env_name, env_value, feature_name | ||
): | ||
"""Tests if function only adds variables that start with QPC_FEATURE_.""" | ||
with mock.patch.dict(os.environ, ({env_name: env_value})): | ||
dict_with_test_flags = FeatureFlag.get_feature_flags_from_env() | ||
assert feature_name not in dict_with_test_flags | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"env_name,env_value,feature_name,feature_value", | ||
( | ||
("qpc_feature_test", "1", "TEST", True), | ||
("qpc_feature_TEST1", "0", "TEST1", False), | ||
("QPC_feature_TEST2", "0", "TEST2", False), | ||
("qpc_FEATURE_test3", "1", "TEST3", True), | ||
), | ||
) | ||
def test_if_function_is_not_case_sensitive( | ||
env_name, env_value, feature_name, feature_value | ||
): | ||
"""Tests if function is not case-sensitive.""" | ||
with mock.patch.dict(os.environ, ({env_name: env_value})): | ||
dict_with_test_flags = FeatureFlag.get_feature_flags_from_env() | ||
assert feature_name in dict_with_test_flags | ||
assert dict_with_test_flags[feature_name] is feature_value | ||
|
||
|
||
@pytest.fixture | ||
def setup_feature_flag_instance_for_tests(): | ||
"""Set up instance of FeatureFlag class for tests.""" | ||
with mock.patch.dict(os.environ, {"QPC_FEATURE_TEST": "1"}): | ||
feature_flag_instance = FeatureFlag() | ||
return feature_flag_instance | ||
|
||
|
||
def test_if_instance_contains_all_attributes( | ||
setup_feature_flag_instance_for_tests, # pylint: disable=redefined-outer-name # noqa: E501 | ||
): | ||
"""Tests if the constructor loads all attributes correctly.""" | ||
assert hasattr(setup_feature_flag_instance_for_tests, "TEST") | ||
assert hasattr(setup_feature_flag_instance_for_tests, "OVERALL_STATUS") | ||
|
||
|
||
def test_if_instance_attributes_values_are_correct( | ||
setup_feature_flag_instance_for_tests, # pylint: disable=redefined-outer-name # noqa: E501 | ||
): | ||
"""Tests if the right values are attributed to attribute.""" | ||
assert setup_feature_flag_instance_for_tests.TEST is True | ||
assert setup_feature_flag_instance_for_tests.OVERALL_STATUS is False | ||
|
||
|
||
def test_is_feature_active(setup_feature_flag_instance_for_tests): # pylint: disable=redefined-outer-name # noqa: E501 | ||
"""Tests method is_feature_active.""" | ||
assert ( | ||
setup_feature_flag_instance_for_tests.is_feature_active("TEST") is True | ||
) | ||
assert ( | ||
setup_feature_flag_instance_for_tests.is_feature_active( | ||
"OVERALL_STATUS" | ||
) | ||
is False | ||
) | ||
with pytest.raises(ValueError): | ||
setup_feature_flag_instance_for_tests.is_feature_active( | ||
"FALSE_ATTRIBUTE" | ||
) |