Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: add hammercast #113

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ repos:
- id: mypy
args: [--strict]
additional_dependencies:
[pydantic, pytest, pytest_mock, types-requests, flagsmith-flag-engine, responses, sseclient-py]
[pydantic, pytest, pytest_mock, types-requests, flagsmith-flag-engine, responses, sseclient-py, pyhamcrest]
- repo: https://github.com/PyCQA/isort
rev: 5.13.2
hooks:
Expand Down
554 changes: 314 additions & 240 deletions poetry.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pytest-mock = "^3.6.1"
pre-commit = "^2.17.0"
responses = "^0.24.1"
types-requests = "^2.32"
pyhamcrest = "^2.0.5"

[tool.mypy]
plugins = ["pydantic.mypy"]
Expand Down
13 changes: 9 additions & 4 deletions tests/test_analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
from datetime import datetime, timedelta
from unittest import mock

from hamcrest import assert_that, equal_to, is_

from flagsmith.analytics import ANALYTICS_TIMER, AnalyticsProcessor


Expand All @@ -10,18 +12,18 @@ def test_analytics_processor_track_feature_updates_analytics_data(
) -> None:
# When
analytics_processor.track_feature("my_feature")
assert analytics_processor.analytics_data["my_feature"] == 1
assert_that(analytics_processor.analytics_data["my_feature"], is_(1))

analytics_processor.track_feature("my_feature")
assert analytics_processor.analytics_data["my_feature"] == 2
assert_that(analytics_processor.analytics_data["my_feature"], is_(2))


def test_analytics_processor_flush_clears_analytics_data(
analytics_processor: AnalyticsProcessor,
) -> None:
analytics_processor.track_feature("my_feature")
analytics_processor.flush()
assert analytics_processor.analytics_data == {}
assert_that(analytics_processor.analytics_data, equal_to({}))


def test_analytics_processor_flush_post_request_data_match_ananlytics_data(
Expand All @@ -36,7 +38,10 @@ def test_analytics_processor_flush_post_request_data_match_ananlytics_data(
# Then
session.post.assert_called()
post_call = session.mock_calls[0]
assert {"my_feature_1": 1, "my_feature_2": 1} == json.loads(post_call[2]["data"])
assert_that(
json.loads(post_call[2]["data"]),
equal_to({"my_feature_1": 1, "my_feature_2": 1}),
)


def test_analytics_processor_flush_early_exit_if_analytics_data_is_empty(
Expand Down
151 changes: 94 additions & 57 deletions tests/test_flagsmith.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,21 @@
import responses
from flag_engine.environments.models import EnvironmentModel
from flag_engine.features.models import FeatureModel, FeatureStateModel
from hamcrest import (
assert_that,
empty,
equal_to,
has_entry,
has_length,
is_,
is_not,
none,
not_none,
same_instance,
)
from pytest_mock import MockerFixture
from responses import matchers
from typing_extensions import Type

from flagsmith import Flagsmith
from flagsmith.exceptions import (
Expand Down Expand Up @@ -39,18 +52,20 @@ def test_flagsmith_starts_polling_manager_on_init_if_enabled(

@responses.activate()
def test_update_environment_sets_environment(
flagsmith: Flagsmith, environment_json: str, environment_model: EnvironmentModel
flagsmith: Flagsmith,
environment_json: str,
environment_model: Type[EnvironmentModel],
) -> None:
# Given
responses.add(method="GET", url=flagsmith.environment_url, body=environment_json)
assert flagsmith._environment is None
assert_that(flagsmith._environment, none())

# When
flagsmith.update_environment()

# Then
assert flagsmith._environment is not None
assert flagsmith._environment == environment_model
assert_that(flagsmith._environment, not_none())
assert_that(flagsmith._environment, is_(environment_model))


@responses.activate()
Expand All @@ -64,8 +79,11 @@ def test_get_environment_flags_calls_api_when_no_local_environment(
all_flags = flagsmith.get_environment_flags().all_flags()

# Then
assert len(responses.calls) == 1
assert responses.calls[0].request.headers["X-Environment-Key"] == api_key
assert_that(responses.calls, has_length(1))
assert_that(
responses.calls[0].request.headers,
has_entry("X-Environment-Key", api_key),
)

# Taken from hard coded values in tests/data/flags.json
assert all_flags[0].enabled is True
Expand All @@ -85,11 +103,16 @@ def test_get_environment_flags_uses_local_environment_when_available(
all_flags = flagsmith.get_environment_flags().all_flags()

# Then
assert len(responses.calls) == 0
assert len(all_flags) == 1
assert all_flags[0].feature_name == environment_model.feature_states[0].feature.name
assert all_flags[0].enabled == environment_model.feature_states[0].enabled
assert all_flags[0].value == environment_model.feature_states[0].get_value()
assert_that(responses.calls, empty())
assert_that(len(all_flags), equal_to(1))
assert_that(
all_flags[0].feature_name,
equal_to(environment_model.feature_states[0].feature.name),
)
assert_that(all_flags[0].enabled, is_(environment_model.feature_states[0].enabled))
assert_that(
all_flags[0].value, equal_to(environment_model.feature_states[0].get_value())
)


@responses.activate()
Expand All @@ -108,7 +131,7 @@ def test_get_identity_flags_calls_api_when_no_local_environment_no_traits(
if isinstance(body, bytes):
# Decode 'body' from bytes to string if it is in bytes format.
body = body.decode()
assert body == json.dumps({"identifier": identifier, "traits": []})
assert_that(body, equal_to(json.dumps({"identifier": identifier, "traits": []})))

# Taken from hard coded values in tests/data/identities.json
assert identity_flags[0].enabled is True
Expand All @@ -133,11 +156,19 @@ def test_get_identity_flags_calls_api_when_no_local_environment_with_traits(
if isinstance(body, bytes):
# Decode 'body' from bytes to string if it is in bytes format.
body = body.decode()
assert body == json.dumps(
{
"identifier": identifier,
"traits": [{"trait_key": k, "trait_value": v} for k, v in traits.items()],
}

assert_that(
body,
equal_to(
json.dumps(
{
"identifier": identifier,
"traits": [
{"trait_key": k, "trait_value": v} for k, v in traits.items()
],
}
)
),
)

# Taken from hard coded values in tests/data/identities.json
Expand Down Expand Up @@ -169,8 +200,8 @@ def test_get_identity_flags_uses_local_environment_when_available(

# Then
mock_engine.get_identity_feature_states.assert_called_once()
assert identity_flags[0].enabled is feature_state.enabled
assert identity_flags[0].value == feature_state.get_value()
assert_that(identity_flags[0].enabled, same_instance(feature_state.enabled))
assert_that(identity_flags[0].value, equal_to(feature_state.get_value()))


@responses.activate()
Expand Down Expand Up @@ -304,9 +335,9 @@ def default_flag_handler(feature_name: str) -> DefaultFlag:
# Then
# the data from the default flag is used
flag = flags.get_flag(feature_name)
assert flag.is_default
assert flag.enabled == default_flag.enabled
assert flag.value == default_flag.value
assert_that(flag.is_default, is_(True))
assert_that(flag.enabled, equal_to(default_flag.enabled))
assert_that(flag.value, equal_to(default_flag.value))


@responses.activate()
Expand Down Expand Up @@ -335,9 +366,11 @@ def default_flag_handler(feature_name: str) -> DefaultFlag:
# Then
# the data from the API response is used, not the default flag
flag = flags.get_flag(feature_name)
assert not flag.is_default
assert flag.value != default_flag.value
assert flag.value == "some-value" # hard coded value in tests/data/flags.json
assert_that(flag.is_default, is_not(True))
assert_that(flag.value, is_not(equal_to(default_flag.value)))
assert_that(
flag.value, equal_to("some-value")
) # hard coded value in tests/data/flags.json


@responses.activate()
Expand Down Expand Up @@ -370,9 +403,9 @@ def default_flag_handler(feature_name: str) -> DefaultFlag:
# Then
# the data from the default flag is used
flag = flags.get_flag(feature_name)
assert flag.is_default
assert flag.enabled == default_flag.enabled
assert flag.value == default_flag.value
assert_that(flag.is_default, is_(True))
assert_that(flag.enabled, equal_to(default_flag.enabled))
assert_that(flag.value, equal_to(default_flag.value))


@responses.activate()
Expand Down Expand Up @@ -401,9 +434,11 @@ def default_flag_handler(feature_name: str) -> DefaultFlag:
# Then
# the data from the API response is used, not the default flag
flag = flags.get_flag(feature_name)
assert not flag.is_default
assert flag.value != default_flag.value
assert flag.value == "some-value" # hard coded value in tests/data/identities.json
assert_that(flag.is_default, is_not(True))
assert_that(flag.value, is_not(equal_to(default_flag.value)))
assert_that(
flag.value, equal_to("some-value")
) # hard coded value in tests/data/identities.json


def test_default_flags_are_used_if_api_error_and_default_flag_handler_given(
Expand All @@ -429,7 +464,7 @@ def default_flag_handler(feature_name: str) -> DefaultFlag:
flags = flagsmith.get_environment_flags()

# Then
assert flags.get_flag("some-feature") == default_flag
assert_that(flags.get_flag("some-feature"), is_(equal_to(default_flag)))


def test_get_identity_segments_no_traits(
Expand All @@ -456,8 +491,10 @@ def test_get_identity_segments_with_valid_trait(
segments = local_eval_flagsmith.get_identity_segments(identifier, traits)

# Then
assert len(segments) == 1
assert segments[0].name == "Test segment" # obtained from data/environment.json
assert_that(len(segments), equal_to(1))
assert_that(
segments[0].name, is_("Test segment")
) # obtained from data/environment.json


def test_local_evaluation_requires_server_key() -> None:
Expand Down Expand Up @@ -488,10 +525,10 @@ def get_environment(self) -> EnvironmentModel:
# Then
# we can request the flags from the client successfully
environment_flags: Flags = flagsmith.get_environment_flags()
assert environment_flags.is_feature_enabled("some_feature") is True
assert_that(environment_flags.is_feature_enabled("some_feature"), is_(True))

identity_flags: Flags = flagsmith.get_identity_flags("identity")
assert identity_flags.is_feature_enabled("some_feature") is True
assert_that(identity_flags.is_feature_enabled("some_feature"), is_(True))


@responses.activate()
Expand Down Expand Up @@ -519,11 +556,11 @@ def test_flagsmith_uses_offline_handler_if_set_and_no_api_response(
# Then
mock_offline_handler.get_environment.assert_called_once_with()

assert environment_flags.is_feature_enabled("some_feature") is True
assert environment_flags.get_feature_value("some_feature") == "some-value"
assert_that(environment_flags.is_feature_enabled("some_feature"), is_(True))
assert_that(environment_flags.get_feature_value("some_feature"), is_("some-value"))

assert identity_flags.is_feature_enabled("some_feature") is True
assert identity_flags.get_feature_value("some_feature") == "some-value"
assert_that(identity_flags.is_feature_enabled("some_feature"), is_(True))
assert_that(identity_flags.get_feature_value("some_feature"), is_("some-value"))


@responses.activate()
Expand Down Expand Up @@ -555,15 +592,15 @@ def test_offline_mode__local_evaluation__correct_fallback(
# Then
mock_offline_handler.get_environment.assert_called_once_with()

assert environment_flags.is_feature_enabled("some_feature") is True
assert environment_flags.get_feature_value("some_feature") == "some-value"
assert_that(environment_flags.is_feature_enabled("some_feature"), is_(True))
assert_that(environment_flags.get_feature_value("some_feature"), is_("some-value"))

assert identity_flags.is_feature_enabled("some_feature") is True
assert identity_flags.get_feature_value("some_feature") == "some-value"
assert_that(identity_flags.is_feature_enabled("some_feature"), is_(True))
assert_that(identity_flags.get_feature_value("some_feature"), is_("some-value"))

[error_log_record] = caplog.records
assert error_log_record.levelname == "ERROR"
assert error_log_record.message == "Error updating environment"
assert_that(error_log_record.levelname, is_("ERROR"))
assert_that(error_log_record.message, is_("Error updating environment"))


def test_cannot_use_offline_mode_without_offline_handler() -> None:
Expand All @@ -572,9 +609,9 @@ def test_cannot_use_offline_mode_without_offline_handler() -> None:
Flagsmith(offline_mode=True, offline_handler=None)

# Then
assert (
e.exconly()
== "ValueError: offline_handler must be provided to use offline mode."
assert_that(
e.exconly(),
is_("ValueError: offline_handler must be provided to use offline mode."),
)


Expand All @@ -589,9 +626,9 @@ def test_cannot_use_default_handler_and_offline_handler(mocker: MockerFixture) -
)

# Then
assert (
e.exconly()
== "ValueError: Cannot use both default_flag_handler and offline_handler."
assert_that(
e.exconly(),
is_("ValueError: Cannot use both default_flag_handler and offline_handler."),
)


Expand All @@ -601,7 +638,7 @@ def test_cannot_create_flagsmith_client_in_remote_evaluation_without_api_key() -
Flagsmith()

# Then
assert e.exconly() == "ValueError: environment_key is required."
assert_that(e.exconly(), is_("ValueError: environment_key is required."))


def test_stream_not_used_by_default(
Expand All @@ -614,7 +651,7 @@ def test_stream_not_used_by_default(
)

# Then
assert hasattr(flagsmith, "event_stream_thread") is False
assert_that(hasattr(flagsmith, "event_stream_thread"), is_(False))


def test_stream_used_when_enable_realtime_updates_is_true(
Expand All @@ -628,7 +665,7 @@ def test_stream_used_when_enable_realtime_updates_is_true(
)

# Then
assert hasattr(flagsmith, "event_stream_thread") is True
assert_that(hasattr(flagsmith, "event_stream_thread"), is_(True))


def test_error_raised_when_realtime_updates_is_true_and_local_evaluation_false(
Expand Down Expand Up @@ -665,8 +702,8 @@ def test_flagsmith_client_get_identity_flags__local_evaluation__returns_expected
flag = flagsmith.get_identity_flags(identifier).get_flag("some_feature")

# Then
assert flag.enabled is False
assert flag.value == "some-overridden-value"
assert_that(flag.enabled, is_(False))
assert_that(flag.value, equal_to("some-overridden-value"))


def test_custom_feature_error_raised_when_invalid_feature(
Expand Down
5 changes: 3 additions & 2 deletions tests/test_offline_handlers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from unittest.mock import mock_open, patch

from flag_engine.environments.models import EnvironmentModel
from hamcrest import assert_that, equal_to

from flagsmith.offline_handlers import LocalFileHandler

Expand All @@ -16,7 +17,7 @@ def test_local_file_handler(environment_json: str) -> None:

# Then
assert isinstance(environment_model, EnvironmentModel)
assert (
environment_model.api_key == "B62qaMZNwfiqT76p38ggrQ"
assert_that(
environment_model.api_key, equal_to("B62qaMZNwfiqT76p38ggrQ")
) # hard coded from json file
mock_file.assert_called_once_with(environment_document_file_path)
Loading
Loading