diff --git a/openfeature/memory_provider/BUILD b/openfeature/memory_provider/BUILD new file mode 100644 index 0000000..12a5b6f --- /dev/null +++ b/openfeature/memory_provider/BUILD @@ -0,0 +1,35 @@ +load("@rules_cc//cc:defs.bzl", "cc_library") + +package( + default_visibility = ["//visibility:public"], +) + +cc_library( + name = "flag", + hdrs = ["flag.h"], + include_prefix = "openfeature", + deps = [ + "//openfeature:evaluation_context", + "//openfeature:flag_metadata", + "@abseil-cpp//absl/status:statusor", + ], +) + +cc_library( + name = "in_memory_provider", + srcs = ["in_memory_provider.cpp"], + hdrs = ["in_memory_provider.h"], + include_prefix = "openfeature", + deps = [ + "//openfeature:error_code", + "//openfeature:evaluation_context", + ":flag", + "//openfeature:metadata", + "//openfeature:provider", + "//openfeature:provider_status", + "//openfeature:reason", + "//openfeature:resolution_details", + "@abseil-cpp//absl/status", + "@abseil-cpp//absl/status:statusor", + ], +) \ No newline at end of file diff --git a/openfeature/memory_provider/flag.h b/openfeature/memory_provider/flag.h new file mode 100644 index 0000000..1a12ed4 --- /dev/null +++ b/openfeature/memory_provider/flag.h @@ -0,0 +1,58 @@ +#ifndef CPP_SDK_INCLUDE_OPENFEATURE_MEMORY_PROVIDER_FLAG_H_ +#define CPP_SDK_INCLUDE_OPENFEATURE_MEMORY_PROVIDER_FLAG_H_ + +#include +#include +#include +#include +#include + +#include "absl/status/statusor.h" +#include "openfeature/evaluation_context.h" +#include "openfeature/flag_metadata.h" + +namespace openfeature { + +// This class represents the flag structure used for the InMemoryProvider. +template +class Flag { + public: + using ContextEvaluator = + std::function(const Flag&, const EvaluationContext&)>; + + Flag(std::unordered_map variants, + std::optional default_variant, + ContextEvaluator context_evaluator, FlagMetadata flag_metadata, + bool disabled = false) + : variants_(std::move(variants)), + default_variant_(std::move(default_variant)), + context_evaluator_(std::move(context_evaluator)), + flag_metadata_(std::move(flag_metadata)), + disabled_(disabled) {} + + const std::unordered_map& GetVariants() const { + return variants_; + } + + const std::optional& GetDefaultVariant() const { + return default_variant_; + } + + const ContextEvaluator& GetContextEvaluator() const { + return context_evaluator_; + } + + const FlagMetadata& GetFlagMetadata() const { return flag_metadata_; } + + bool IsDisabled() const { return disabled_; } + + private: + std::unordered_map variants_; + std::optional default_variant_; + ContextEvaluator context_evaluator_; + FlagMetadata flag_metadata_; + bool disabled_; +}; +} // namespace openfeature + +#endif // CPP_SDK_INCLUDE_OPENFEATURE_MEMORY_PROVIDER_FLAG_H_ diff --git a/openfeature/memory_provider/in_memory_provider.cpp b/openfeature/memory_provider/in_memory_provider.cpp new file mode 100644 index 0000000..5c44089 --- /dev/null +++ b/openfeature/memory_provider/in_memory_provider.cpp @@ -0,0 +1,144 @@ +#include "openfeature/memory_provider/in_memory_provider.h" + +#include +#include +#include + +#include "absl/status/statusor.h" +#include "openfeature/error_code.h" +#include "openfeature/memory_provider/flag.h" +#include "openfeature/reason.h" + +namespace openfeature { + +static constexpr std::string_view kName = "InMemoryProvider"; + +InMemoryProvider::InMemoryProvider( + std::unordered_map flags) + : flags_(std::move(flags)), status_(ProviderStatus::kNotReady) {} + +Metadata InMemoryProvider::GetMetadata() const { + return Metadata{std::string(kName)}; +} + +absl::Status InMemoryProvider::Init(const EvaluationContext& ctx) { + { + std::unique_lock lock(mutex_); + status_ = ProviderStatus::kReady; + } + return absl::OkStatus(); +} + +absl::Status InMemoryProvider::Shutdown() { + { + std::unique_lock lock(mutex_); + status_ = ProviderStatus::kNotReady; + } + return absl::OkStatus(); +} + +void InMemoryProvider::UpdateFlags( + std::unordered_map new_flags) { + std::unique_lock lock(mutex_); + for (auto& [key, value] : new_flags) { + flags_[key] = value; + } +} + +void InMemoryProvider::UpdateFlag(std::string key, std::any new_flag) { + std::unique_lock lock(mutex_); + flags_.insert_or_assign(std::move(key), std::move(new_flag)); +} + +std::unique_ptr InMemoryProvider::GetBooleanEvaluation( + std::string_view key, bool default_value, const EvaluationContext& ctx) { + return Evaluate(key, default_value, ctx); +} + +template +std::unique_ptr> InMemoryProvider::Evaluate( + std::string_view key, T default_value, const EvaluationContext& ctx) { + std::shared_lock lock(mutex_); + + if (status_ != ProviderStatus::kReady) { + if (status_ == ProviderStatus::kNotReady) { + return std::make_unique>( + default_value, Reason::kError, std::nullopt, FlagMetadata{}, + ErrorCode::kProviderNotReady, "Provider is not ready"); + } + if (status_ == ProviderStatus::kFatal) { + return std::make_unique>( + default_value, Reason::kError, std::nullopt, FlagMetadata{}, + ErrorCode::kProviderFatal, "Provider is in fatal error state"); + } + return std::make_unique>( + default_value, Reason::kError, std::nullopt, FlagMetadata{}, + ErrorCode::kGeneral, "Unknown error"); + } + + std::string key_str{key}; + auto it = flags_.find(key_str); + if (it == flags_.end()) { + return std::make_unique>( + default_value, Reason::kError, std::nullopt, FlagMetadata{}, + ErrorCode::kFlagNotFound, "Flag " + key_str + " not found"); + } + + const Flag* flag = std::any_cast>(&it->second); + + if (!flag) { + return std::make_unique>( + default_value, Reason::kError, std::nullopt, FlagMetadata{}, + ErrorCode::kTypeMismatch, "Flag type mismatch"); + } + + if (flag->IsDisabled()) { + return std::make_unique>( + default_value, Reason::kDisabled, std::nullopt, + flag->GetFlagMetadata()); + } + + T value; + Reason reason = Reason::kStatic; + const std::optional& variant_key = flag->GetDefaultVariant(); + bool context_eval_success = false; + const auto& evaluator = flag->GetContextEvaluator(); + + if (evaluator != nullptr) { + absl::StatusOr result = evaluator(*flag, ctx); + + if (result.ok()) { + value = *result; + reason = Reason::kTargetingMatch; + context_eval_success = true; + } else { + reason = Reason::kDefault; + } + } + + // Fallback to default variant if context evaluation failed. + if (!context_eval_success) { + if (variant_key.has_value()) { + const std::unordered_map& variants = flag->GetVariants(); + auto variant_it = variants.find(*variant_key); + + if (variant_it != variants.end()) { + value = variant_it->second; + } else { + return std::make_unique>( + default_value, Reason::kError, variant_key, flag->GetFlagMetadata(), + ErrorCode::kParseError, + "Default variant " + *variant_key + " not found in variants map"); + } + } else { + return std::make_unique>( + default_value, Reason::kDefault, std::nullopt, + flag->GetFlagMetadata()); + } + } + + return std::make_unique>(value, reason, variant_key, + flag->GetFlagMetadata()); +} + +} // namespace openfeature diff --git a/openfeature/memory_provider/in_memory_provider.h b/openfeature/memory_provider/in_memory_provider.h new file mode 100644 index 0000000..966827b --- /dev/null +++ b/openfeature/memory_provider/in_memory_provider.h @@ -0,0 +1,61 @@ +#ifndef CPP_SDK_INCLUDE_OPENFEATURE_IN_MEMORY_PROVIDER_H_ +#define CPP_SDK_INCLUDE_OPENFEATURE_IN_MEMORY_PROVIDER_H_ + +#include +#include +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "openfeature/evaluation_context.h" +#include "openfeature/metadata.h" +#include "openfeature/provider.h" +#include "openfeature/provider_status.h" +#include "openfeature/resolution_details.h" + +namespace openfeature { + +// This class implements the FeatureProvider interface and is intended to be +// used for testing. It stores feature flags in memory and allows for +// evaluation based on the provided EvaluationContext. +class InMemoryProvider : public FeatureProvider { + public: + InMemoryProvider(std::unordered_map flags); + + ~InMemoryProvider() = default; + + Metadata GetMetadata() const override; + + absl::Status Init(const EvaluationContext& ctx) override; + absl::Status Shutdown() override; + + // Updates the provider flags configuration. All existing flags will be + // replaced with the new ones. If there are any new flags, they will be + // added to the configuration. + void UpdateFlags(std::unordered_map new_flags); + + // Updates a single flag in the provider configuration. If the flag already + // exists, it will be replaced with the new one. If it doesn't exist, it + // will be added to the configuration. + void UpdateFlag(std::string key, std::any new_flag); + + std::unique_ptr GetBooleanEvaluation( + std::string_view key, bool default_value, + const EvaluationContext& ctx) override; + + private: + template + std::unique_ptr> Evaluate(std::string_view key, + T default_value, + const EvaluationContext& ctx); + + std::unordered_map flags_; + ProviderStatus status_; + mutable std::shared_mutex mutex_; +}; + +} // namespace openfeature + +#endif // CPP_SDK_INCLUDE_OPENFEATURE_IN_MEMORY_PROVIDER_H_ diff --git a/test/memory_provider/BUILD b/test/memory_provider/BUILD new file mode 100644 index 0000000..9f89c28 --- /dev/null +++ b/test/memory_provider/BUILD @@ -0,0 +1,19 @@ +load("@rules_cc//cc:defs.bzl", "cc_library", "cc_test") + +cc_test( + name = "flag_test", + srcs = ["flag_test.cpp"], + deps = [ + "//openfeature/memory_provider:flag", + "@googletest//:gtest_main", + ], +) + +cc_test( + name = "in_memory_provider_test", + srcs = ["in_memory_provider_test.cpp"], + deps = [ + "//openfeature/memory_provider:in_memory_provider", + "@googletest//:gtest_main", + ], +) \ No newline at end of file diff --git a/test/memory_provider/flag_test.cpp b/test/memory_provider/flag_test.cpp new file mode 100644 index 0000000..6a72ec7 --- /dev/null +++ b/test/memory_provider/flag_test.cpp @@ -0,0 +1,150 @@ +#include "openfeature/memory_provider/flag.h" + +#include +#include + +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "openfeature/evaluation_context.h" +#include "openfeature/flag_metadata.h" + +namespace openfeature { + +TEST(FlagTest, InitializesAndReturnsPropertiesCorrectly) { + std::unordered_map variants = { + {"v1", std::string("value1")}, {"v2", std::string("value2")}}; + std::string default_variant = "v1"; + + Flag::ContextEvaluator evaluator = + [](const Flag&, + const EvaluationContext&) -> absl::StatusOr { + return "evaluated"; + }; + + FlagMetadata metadata; + metadata.data["bool_prop"] = true; + metadata.data["str_prop"] = std::string("meta_str"); + metadata.data["int_prop"] = int64_t{42}; + metadata.data["double_prop"] = 3.14; + + Flag flag(variants, default_variant, evaluator, metadata, true); + + const std::unordered_map& got_variants = + flag.GetVariants(); + EXPECT_EQ(got_variants.size(), 2); + EXPECT_EQ(got_variants.at("v1"), "value1"); + EXPECT_EQ(got_variants.at("v2"), "value2"); + + EXPECT_EQ(flag.GetDefaultVariant(), "v1"); + + const auto& got_evaluator = flag.GetContextEvaluator(); + ASSERT_TRUE(got_evaluator != nullptr); + absl::StatusOr eval_result = + got_evaluator(flag, EvaluationContext::Builder().build()); + ASSERT_TRUE(eval_result.ok()); + EXPECT_EQ(*eval_result, "evaluated"); + + const FlagMetadata& got_metadata = flag.GetFlagMetadata(); + EXPECT_EQ(got_metadata.data.size(), 4); + EXPECT_EQ(std::get(got_metadata.data.at("bool_prop")), true); + EXPECT_EQ(std::get(got_metadata.data.at("str_prop")), + "meta_str"); + EXPECT_EQ(std::get(got_metadata.data.at("int_prop")), 42); + EXPECT_DOUBLE_EQ(std::get(got_metadata.data.at("double_prop")), 3.14); + + EXPECT_TRUE(flag.IsDisabled()); +} + +TEST(FlagTest, DefaultDisabledStateIsFalse) { + Flag::ContextEvaluator evaluator = + [](const Flag&, const EvaluationContext&) -> absl::StatusOr { + return 0; + }; + + Flag flag({}, "default", evaluator, FlagMetadata{}); + EXPECT_FALSE(flag.IsDisabled()); +} + +TEST(FlagTest, EvaluatorUsesTargetingKey) { + std::unordered_map variants = {{"feature_on", true}, + {"feature_off", false}}; + + Flag::ContextEvaluator evaluator = + [](const Flag& f, + const EvaluationContext& ctx) -> absl::StatusOr { + std::optional tk = ctx.GetTargetingKey(); + if (tk && *tk == "beta_tester") { + return f.GetVariants().at("feature_on"); + } + return f.GetVariants().at("feature_off"); + }; + + Flag flag(variants, "feature_off", evaluator, FlagMetadata{}); + const auto& eval_func = flag.GetContextEvaluator(); + + EvaluationContext beta_ctx = + EvaluationContext::Builder().WithTargetingKey("beta_tester").build(); + absl::StatusOr beta_res = eval_func(flag, beta_ctx); + ASSERT_TRUE(beta_res.ok()); + EXPECT_TRUE(*beta_res); + + EvaluationContext normal_ctx = + EvaluationContext::Builder().WithTargetingKey("regular_user").build(); + absl::StatusOr normal_res = eval_func(flag, normal_ctx); + ASSERT_TRUE(normal_res.ok()); + EXPECT_FALSE(*normal_res); +} + +TEST(FlagTest, EvaluatorUsesContextAttributes) { + std::unordered_map variants = {{"premium", 100}, + {"standard", 10}}; + + Flag::ContextEvaluator evaluator = + [](const Flag& f, + const EvaluationContext& ctx) -> absl::StatusOr { + const std::any* user_tier = ctx.GetValue("tier"); + + if (user_tier && user_tier->type() == typeid(std::string)) { + if (std::any_cast(*user_tier) == "premium") { + return f.GetVariants().at("premium"); + } + } + return f.GetVariants().at("standard"); + }; + + Flag flag(variants, "standard", evaluator, FlagMetadata{}); + const auto& eval_func = flag.GetContextEvaluator(); + + EvaluationContext premium_ctx = + EvaluationContext::Builder().WithAttribute("tier", "premium").build(); + absl::StatusOr premium_res = eval_func(flag, premium_ctx); + ASSERT_TRUE(premium_res.ok()); + EXPECT_EQ(*premium_res, 100); + + EvaluationContext standard_ctx = + EvaluationContext::Builder().WithAttribute("tier", "standard").build(); + absl::StatusOr standard_res = eval_func(flag, standard_ctx); + ASSERT_TRUE(standard_res.ok()); + EXPECT_EQ(*standard_res, 10); +} + +TEST(FlagTest, EvaluatorReturnsErrorStatus) { + Flag::ContextEvaluator evaluator = + [](const Flag&, + const EvaluationContext&) -> absl::StatusOr { + return absl::InvalidArgumentError("Missing required attribute"); + }; + + Flag flag({}, "default", evaluator, FlagMetadata{}); + + EvaluationContext empty_ctx = EvaluationContext::Builder().build(); + absl::StatusOr result = flag.GetContextEvaluator()(flag, empty_ctx); + + EXPECT_FALSE(result.ok()); + EXPECT_EQ(result.status().code(), absl::StatusCode::kInvalidArgument); + EXPECT_EQ(result.status().message(), "Missing required attribute"); +} +} // namespace openfeature \ No newline at end of file diff --git a/test/memory_provider/in_memory_provider_test.cpp b/test/memory_provider/in_memory_provider_test.cpp new file mode 100644 index 0000000..e4240ab --- /dev/null +++ b/test/memory_provider/in_memory_provider_test.cpp @@ -0,0 +1,345 @@ +#include "openfeature/memory_provider/in_memory_provider.h" + +#include +#include + +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "openfeature/error_code.h" +#include "openfeature/evaluation_context.h" +#include "openfeature/memory_provider/flag.h" +#include "openfeature/reason.h" +#include "openfeature/resolution_details.h" + +namespace openfeature { + +using ::testing::Eq; +using ::testing::Optional; + +class InMemoryProviderTest : public ::testing::Test { + protected: + EvaluationContext empty_ctx_ = EvaluationContext::Builder().build(); + + // Helper method to create a basic boolean flag for testing. + Flag CreateBoolFlag(std::unordered_map variants, + std::optional default_variant_opt, + Flag::ContextEvaluator evaluator = nullptr, + bool disabled = false) { + return Flag(std::move(variants), std::move(default_variant_opt), + std::move(evaluator), FlagMetadata{}, disabled); + } +}; + +TEST_F(InMemoryProviderTest, GetMetadataReturnsCorrectName) { + InMemoryProvider provider({}); + EXPECT_EQ(provider.GetMetadata().name, "InMemoryProvider"); +} + +TEST_F(InMemoryProviderTest, EvaluationFailsWhenNotReady) { + InMemoryProvider provider({}); + + // Evaluating without calling Init(). + std::unique_ptr res = + provider.GetBooleanEvaluation("any_flag", false, empty_ctx_); + + ASSERT_NE(res, nullptr); + EXPECT_EQ(res->GetReason(), Reason::kError); + EXPECT_EQ(res->GetErrorCode(), ErrorCode::kProviderNotReady); + EXPECT_FALSE(res->GetValue()); +} + +TEST_F(InMemoryProviderTest, InitAndShutdownUpdateStateCorrectly) { + InMemoryProvider provider({}); + + EXPECT_TRUE(provider.Init(empty_ctx_).ok()); + std::unique_ptr res = + provider.GetBooleanEvaluation("any_flag", false, empty_ctx_); + + // After initialization, the state is Ready, but flag is missing. + EXPECT_EQ(res->GetErrorCode(), ErrorCode::kFlagNotFound); + + EXPECT_TRUE(provider.Shutdown().ok()); + res = provider.GetBooleanEvaluation("any_flag", false, empty_ctx_); + + EXPECT_EQ(res->GetErrorCode(), ErrorCode::kProviderNotReady); +} + +TEST_F(InMemoryProviderTest, FlagNotFound) { + InMemoryProvider provider({}); + EXPECT_TRUE(provider.Init(empty_ctx_).ok()); + + std::unique_ptr res = + provider.GetBooleanEvaluation("missing", true, empty_ctx_); + + EXPECT_TRUE(res->GetValue()); + EXPECT_EQ(res->GetReason(), Reason::kError); + EXPECT_EQ(res->GetErrorCode(), ErrorCode::kFlagNotFound); +} + +TEST_F(InMemoryProviderTest, FlagTypeMismatch) { + std::unordered_map variants = { + {"v1", std::string("test")}}; + Flag str_flag(variants, "v1", nullptr, FlagMetadata{}); + + InMemoryProvider provider({}); + provider.UpdateFlag("str_flag", str_flag); + EXPECT_TRUE(provider.Init(empty_ctx_).ok()); + + std::unique_ptr res = + provider.GetBooleanEvaluation("str_flag", false, empty_ctx_); + + EXPECT_FALSE(res->GetValue()); + EXPECT_EQ(res->GetReason(), Reason::kError); + EXPECT_EQ(res->GetErrorCode(), ErrorCode::kTypeMismatch); +} + +TEST_F(InMemoryProviderTest, DisabledFlagReturnsDisabledReason) { + InMemoryProvider provider({}); + provider.UpdateFlag("disabled_flag", + CreateBoolFlag({{"on", true}}, "on", nullptr, true)); + EXPECT_TRUE(provider.Init(empty_ctx_).ok()); + + std::unique_ptr res = + provider.GetBooleanEvaluation("disabled_flag", false, empty_ctx_); + + EXPECT_FALSE(res->GetValue()); // fallback to default param. + EXPECT_EQ(res->GetReason(), Reason::kDisabled); + EXPECT_FALSE(res->GetErrorCode().has_value()); +} + +TEST_F(InMemoryProviderTest, StaticEvaluationSuccess) { + InMemoryProvider provider({}); + provider.UpdateFlag("static_flag", + CreateBoolFlag({{"on", true}, {"off", false}}, "on")); + EXPECT_TRUE(provider.Init(empty_ctx_).ok()); + + std::unique_ptr res = + provider.GetBooleanEvaluation("static_flag", false, empty_ctx_); + + EXPECT_TRUE(res->GetValue()); + EXPECT_EQ(res->GetReason(), Reason::kStatic); + EXPECT_THAT(res->GetVariant(), Optional(std::string("on"))); +} + +TEST_F(InMemoryProviderTest, ContextEvaluatorSuccess) { + auto evaluator = [](const Flag&, + const EvaluationContext&) -> absl::StatusOr { + return false; + }; + + InMemoryProvider provider({}); + provider.UpdateFlag("dyn_flag", CreateBoolFlag({{"on", true}, {"off", false}}, + "on", evaluator)); + EXPECT_TRUE(provider.Init(empty_ctx_).ok()); + + std::unique_ptr res = + provider.GetBooleanEvaluation("dyn_flag", true, empty_ctx_); + + EXPECT_FALSE(res->GetValue()); + EXPECT_EQ(res->GetReason(), Reason::kTargetingMatch); +} + +TEST_F(InMemoryProviderTest, ContextEvaluatorFailureFallsBackToDefaultVariant) { + auto evaluator = [](const Flag&, + const EvaluationContext&) -> absl::StatusOr { + return absl::InvalidArgumentError("Evaluator failed"); + }; + + InMemoryProvider provider({}); + provider.UpdateFlag("dyn_flag", CreateBoolFlag({{"on", true}, {"off", false}}, + "off", evaluator)); + EXPECT_TRUE(provider.Init(empty_ctx_).ok()); + + std::unique_ptr res = + provider.GetBooleanEvaluation("dyn_flag", true, empty_ctx_); + + EXPECT_FALSE(res->GetValue()); + EXPECT_EQ(res->GetReason(), Reason::kDefault); + EXPECT_THAT(res->GetVariant(), Optional(std::string("off"))); +} + +TEST_F(InMemoryProviderTest, FallbackFailsIfVariantTypeMismatch) { + std::unordered_map variants = {{"v1", true}}; + Flag bad_flag(variants, "v2", nullptr, FlagMetadata{}); + + InMemoryProvider provider({}); + provider.UpdateFlag("missing_variant_flag", bad_flag); + EXPECT_TRUE(provider.Init(empty_ctx_).ok()); + + std::unique_ptr res = + provider.GetBooleanEvaluation("missing_variant_flag", true, empty_ctx_); + EXPECT_TRUE(res->GetValue()); + EXPECT_EQ(res->GetReason(), Reason::kError); + EXPECT_EQ(res->GetErrorCode(), ErrorCode::kParseError); +} + +TEST_F(InMemoryProviderTest, FallbackFailsIfVariantMissing) { + std::unordered_map variants = {{"v1", true}}; + Flag bad_flag(variants, "v2", nullptr, FlagMetadata{}); + + InMemoryProvider provider({}); + provider.UpdateFlag("missing_variant_flag", bad_flag); + EXPECT_TRUE(provider.Init(empty_ctx_).ok()); + + std::unique_ptr res = + provider.GetBooleanEvaluation("missing_variant_flag", true, empty_ctx_); + + EXPECT_TRUE(res->GetValue()); + EXPECT_EQ(res->GetReason(), Reason::kError); + EXPECT_EQ(res->GetErrorCode(), ErrorCode::kParseError); + EXPECT_THAT( + res->GetErrorMessage(), + Optional(std::string("Default variant v2 not found in variants map"))); +} + +TEST_F(InMemoryProviderTest, UpdateFlagReplacesAndAddsNew) { + InMemoryProvider provider({}); + EXPECT_TRUE(provider.Init(empty_ctx_).ok()); + + provider.UpdateFlag("flag1", CreateBoolFlag({{"on", true}}, "on")); + EXPECT_TRUE( + provider.GetBooleanEvaluation("flag1", false, empty_ctx_)->GetValue()); + + provider.UpdateFlag("flag1", CreateBoolFlag({{"off", false}}, "off")); + EXPECT_FALSE( + provider.GetBooleanEvaluation("flag1", true, empty_ctx_)->GetValue()); + + provider.UpdateFlag("new_flag", CreateBoolFlag({{"added", true}}, "added")); + EXPECT_TRUE( + provider.GetBooleanEvaluation("new_flag", false, empty_ctx_)->GetValue()); +} + +TEST_F(InMemoryProviderTest, UpdateFlagsAddsAndOverwritesExisting) { + std::unordered_map initial; + initial["flag1"] = CreateBoolFlag({{"on", true}}, "on"); + initial["common_flag"] = CreateBoolFlag({{"initial", false}}, "initial"); + + InMemoryProvider provider(std::move(initial)); + EXPECT_TRUE(provider.Init(empty_ctx_).ok()); + EXPECT_TRUE( + provider.GetBooleanEvaluation("flag1", false, empty_ctx_)->GetValue()); + EXPECT_FALSE(provider.GetBooleanEvaluation("common_flag", true, empty_ctx_) + ->GetValue()); + + std::unordered_map updated; + updated["flag2"] = CreateBoolFlag({{"off", false}}, "off"); + updated["common_flag"] = CreateBoolFlag({{"updated", true}}, "updated"); + + provider.UpdateFlags(std::move(updated)); + + // flag1 should still exist and retain its value as it was not in + // `updated_flags_map` + std::unique_ptr flag1_res = + provider.GetBooleanEvaluation("flag1", false, empty_ctx_); + EXPECT_TRUE(flag1_res->GetValue()); + EXPECT_EQ(flag1_res->GetReason(), Reason::kStatic); + EXPECT_THAT(flag1_res->GetVariant(), Optional(std::string("on"))); + + // flag2 should now exist and be evaluated to its new value + std::unique_ptr flag2_res = + provider.GetBooleanEvaluation("flag2", true, empty_ctx_); + EXPECT_FALSE(flag2_res->GetValue()); + EXPECT_EQ(flag2_res->GetReason(), Reason::kStatic); + EXPECT_THAT(flag2_res->GetVariant(), Optional(std::string("off"))); + + // common_flag should be updated to true + std::unique_ptr common_flag_res = + provider.GetBooleanEvaluation("common_flag", false, empty_ctx_); + EXPECT_TRUE(common_flag_res->GetValue()); + EXPECT_EQ(common_flag_res->GetReason(), Reason::kStatic); + EXPECT_THAT(common_flag_res->GetVariant(), Optional(std::string("updated"))); +} + +TEST_F(InMemoryProviderTest, NoDefaultVariantAndEvaluatorFailsOrMissing) { + InMemoryProvider provider1({}); + provider1.UpdateFlag("no_default_no_evaluator", + CreateBoolFlag({{"v1", true}}, std::nullopt, nullptr)); + EXPECT_TRUE(provider1.Init(empty_ctx_).ok()); + std::unique_ptr res1 = provider1.GetBooleanEvaluation( + "no_default_no_evaluator", true, empty_ctx_); + EXPECT_TRUE(res1->GetValue()); + EXPECT_EQ(res1->GetReason(), Reason::kDefault); + EXPECT_FALSE(res1->GetVariant().has_value()); + + auto failing_evaluator = + [](const Flag&, const EvaluationContext&) -> absl::StatusOr { + return absl::InvalidArgumentError("Evaluator explicitly failed"); + }; + InMemoryProvider provider2({}); + provider2.UpdateFlag( + "no_default_failing_evaluator", + CreateBoolFlag({{"v1", true}}, std::nullopt, failing_evaluator)); + EXPECT_TRUE(provider2.Init(empty_ctx_).ok()); + std::unique_ptr res2 = provider2.GetBooleanEvaluation( + "no_default_failing_evaluator", false, empty_ctx_); + EXPECT_FALSE(res2->GetValue()); + EXPECT_EQ(res2->GetReason(), Reason::kDefault); + EXPECT_FALSE(res2->GetVariant().has_value()); +} + +TEST_F(InMemoryProviderTest, ContextEvaluatorUsesContext) { + auto context_aware_evaluator = + [](const Flag&, + const EvaluationContext& ctx) -> absl::StatusOr { + const std::any* val_ptr = ctx.GetValue("user_is_admin"); + if (val_ptr != nullptr) { + try { + return std::any_cast(*val_ptr); + } catch (const std::bad_any_cast& e) { + return absl::InvalidArgumentError( + "Context attribute 'user_is_admin' is not of type bool"); + } + } + return false; + }; + + InMemoryProvider provider({}); + provider.UpdateFlag("admin_flag", + CreateBoolFlag({{"on", true}, {"off", false}}, "off", + context_aware_evaluator)); + EXPECT_TRUE(provider.Init(empty_ctx_).ok()); + + EvaluationContext admin_ctx = + EvaluationContext::Builder().WithAttribute("user_is_admin", true).build(); + std::unique_ptr res_admin = + provider.GetBooleanEvaluation("admin_flag", false, admin_ctx); + ASSERT_NE(res_admin, nullptr); + EXPECT_TRUE(res_admin->GetValue()); + EXPECT_EQ(res_admin->GetReason(), Reason::kTargetingMatch); + EXPECT_FALSE(res_admin->GetErrorCode().has_value()); + + EvaluationContext non_admin_ctx = EvaluationContext::Builder() + .WithAttribute("user_is_admin", false) + .build(); + std::unique_ptr res_non_admin = + provider.GetBooleanEvaluation("admin_flag", true, non_admin_ctx); + ASSERT_NE(res_non_admin, nullptr); + EXPECT_FALSE(res_non_admin->GetValue()); + EXPECT_EQ(res_non_admin->GetReason(), Reason::kTargetingMatch); + EXPECT_FALSE(res_non_admin->GetErrorCode().has_value()); + + std::unique_ptr res_no_attr = + provider.GetBooleanEvaluation("admin_flag", true, empty_ctx_); + ASSERT_THAT(res_no_attr, testing::NotNull()); + EXPECT_FALSE(res_no_attr->GetValue()); + EXPECT_EQ(res_no_attr->GetReason(), Reason::kTargetingMatch); + EXPECT_FALSE(res_no_attr->GetErrorCode().has_value()); + + EvaluationContext wrong_type_ctx = + EvaluationContext::Builder() + .WithAttribute("user_is_admin", std::string("true")) + .build(); + std::unique_ptr res_wrong_type = + provider.GetBooleanEvaluation("admin_flag", true, wrong_type_ctx); + ASSERT_NE(res_wrong_type, nullptr); + EXPECT_FALSE(res_wrong_type->GetValue()); + EXPECT_EQ(res_wrong_type->GetReason(), Reason::kDefault); + EXPECT_THAT(res_wrong_type->GetVariant(), Optional(std::string("off"))); + EXPECT_FALSE(res_wrong_type->GetErrorCode().has_value()); +} +} // namespace openfeature