From ff6b53b8b9e3ed63ae7dae4f3afa75a28bc1dc5a Mon Sep 17 00:00:00 2001 From: JoE11-y Date: Wed, 11 Dec 2024 06:17:23 +0100 Subject: [PATCH] add http mock to tests and update code --- .../elixir-provider/config/config.exs | 7 + .../providers/elixir-provider/config/dev.exs | 1 + .../providers/elixir-provider/config/prod.exs | 1 + .../providers/elixir-provider/config/test.exs | 6 + .../lib/provider/cache_controller.ex | 4 +- .../lib/provider/data_collector_hook.ex | 104 ++++---- .../lib/provider/evaluation_context.ex | 4 +- .../lib/provider/feature_event.ex | 4 +- .../lib/provider/http_client.ex | 46 +--- .../elixir-provider/lib/provider/provider.ex | 20 +- .../lib/provider/request_flag_evaluation.ex | 1 + .../lib/provider/response_flag_evalution.ex | 21 +- .../lib/provider/server_supervisor.ex | 2 - openfeature/providers/elixir-provider/mix.exs | 4 +- .../providers/elixir-provider/mix.lock | 4 + .../elixir_provider/elixir_provider_test.exs | 231 ++++++++++++++++++ .../test/elixir_provider_test.exs | 148 ----------- .../elixir-provider/test/test_helper.exs | 2 + 18 files changed, 359 insertions(+), 251 deletions(-) create mode 100644 openfeature/providers/elixir-provider/config/config.exs create mode 100644 openfeature/providers/elixir-provider/config/dev.exs create mode 100644 openfeature/providers/elixir-provider/config/prod.exs create mode 100644 openfeature/providers/elixir-provider/config/test.exs create mode 100644 openfeature/providers/elixir-provider/test/elixir_provider/elixir_provider_test.exs delete mode 100644 openfeature/providers/elixir-provider/test/elixir_provider_test.exs diff --git a/openfeature/providers/elixir-provider/config/config.exs b/openfeature/providers/elixir-provider/config/config.exs new file mode 100644 index 00000000000..17739f2bd81 --- /dev/null +++ b/openfeature/providers/elixir-provider/config/config.exs @@ -0,0 +1,7 @@ +import Config + +config :elixir_provider, + max_wait_time: 5000, + hackney_options: [timeout: :infinity, recv_timeout: :infinity] + +import_config "#{config_env()}.exs" diff --git a/openfeature/providers/elixir-provider/config/dev.exs b/openfeature/providers/elixir-provider/config/dev.exs new file mode 100644 index 00000000000..becde76932f --- /dev/null +++ b/openfeature/providers/elixir-provider/config/dev.exs @@ -0,0 +1 @@ +import Config diff --git a/openfeature/providers/elixir-provider/config/prod.exs b/openfeature/providers/elixir-provider/config/prod.exs new file mode 100644 index 00000000000..becde76932f --- /dev/null +++ b/openfeature/providers/elixir-provider/config/prod.exs @@ -0,0 +1 @@ +import Config diff --git a/openfeature/providers/elixir-provider/config/test.exs b/openfeature/providers/elixir-provider/config/test.exs new file mode 100644 index 00000000000..b6ab92fda62 --- /dev/null +++ b/openfeature/providers/elixir-provider/config/test.exs @@ -0,0 +1,6 @@ +import Config + +# Prevents timeouts in ExUnit +config :elixir_provider, + hackney_options: [timeout: 10_000, recv_timeout: 10_000], + tmp_dir_prefix: "wallaby_test" diff --git a/openfeature/providers/elixir-provider/lib/provider/cache_controller.ex b/openfeature/providers/elixir-provider/lib/provider/cache_controller.ex index 27889fb1f79..07b59f0ef8d 100644 --- a/openfeature/providers/elixir-provider/lib/provider/cache_controller.ex +++ b/openfeature/providers/elixir-provider/lib/provider/cache_controller.ex @@ -6,8 +6,8 @@ defmodule ElixirProvider.CacheController do use GenServer @flag_table :flag_cache - @spec start_link() :: GenServer.on_start() - def start_link do + @spec start_link(any()) :: GenServer.on_start() + def start_link(_args) do GenServer.start_link(__MODULE__, :ok, name: __MODULE__) end diff --git a/openfeature/providers/elixir-provider/lib/provider/data_collector_hook.ex b/openfeature/providers/elixir-provider/lib/provider/data_collector_hook.ex index 722e85b5849..28c5a543a0a 100644 --- a/openfeature/providers/elixir-provider/lib/provider/data_collector_hook.ex +++ b/openfeature/providers/elixir-provider/lib/provider/data_collector_hook.ex @@ -2,24 +2,25 @@ defmodule ElixirProvider.DataCollectorHook do @moduledoc """ Data collector hook """ - use GenServer require Logger - alias ElixirProvider.HttpClient - alias ElixirProvider.{FeatureEvent, RequestDataCollector} + alias OpenFeature.Hook + alias ElixirProvider.{FeatureEvent, HttpClient, RequestDataCollector} @default_targeting_key "undefined-targetingKey" defstruct [ + :base_hook, :http_client, :data_collector_endpoint, :disable_data_collection, - data_flush_interval: 60_000, - event_queue: [] + :data_flush_interval, + :event_queue ] @type t :: %__MODULE__{ + base_hook: Hook.t(), http_client: HttpClient.t(), data_collector_endpoint: String.t(), disable_data_collection: boolean(), @@ -27,8 +28,28 @@ defmodule ElixirProvider.DataCollectorHook do event_queue: list(FeatureEvent.t()) } + def start(options, http_client) do + state = %__MODULE__{ + base_hook: %Hook{ + before: &before_hook/2, + after: &after_hook/4, + error: &error_hook/3, + finally: &finally_hook/2 + }, + http_client: http_client, + data_collector_endpoint: options.endpoint <> "/v1/data/collector", + disable_data_collection: options.disable_data_collection || false, + data_flush_interval: options.data_flush_interval || 60_000, + event_queue: [] + } + + schedule_collect_data(state.data_flush_interval) + {:ok, state} + end + # Starts the GenServer and initializes with options - def start_link do + @spec start_link(any()) :: GenServer.on_start() + def start_link(_args) do GenServer.start_link(__MODULE__, [], name: __MODULE__) end @@ -50,27 +71,13 @@ defmodule ElixirProvider.DataCollectorHook do {:ok, %__MODULE__{}} end - # Initializes the state with the provided options - def start(options, http_client) do - state = %__MODULE__{ - http_client: http_client, - data_collector_endpoint: options.endpoint, - disable_data_collection: options.disable_data_collection || false, - data_flush_interval: options.data_flush_interval || 60_000, - event_queue: [] - } - - schedule_collect_data(state.data_flush_interval) - {:ok, state} - end - - # Schedule periodic data collection based on the interval - defp schedule_collect_data(interval) do - Process.send_after(self(), :collect_data, interval) + ### Hook Functions + defp before_hook(_hook_context, _hook_hints) do + # Define your `before` hook logic, if any + nil end - ### Hook Implementations - def after_hook(hook, hook_context, flag_evaluation_details, _hints) do + def after_hook(%__MODULE__{} = hook, hook_context, flag_evaluation_details, _hints) do if hook.disable_data_collection or flag_evaluation_details.reason != :CACHED do :ok else @@ -90,23 +97,36 @@ defmodule ElixirProvider.DataCollectorHook do end end - def error(hook, hook_context, _hints) do - if hook.disable_data_collection do - :ok - else - feature_event = %FeatureEvent{ - context_kind: - if(Map.get(hook_context.context, "anonymous"), do: "anonymousUser", else: "user"), - creation_date: DateTime.utc_now() |> DateTime.to_unix(:millisecond), - default: true, - key: hook_context.flag_key, - value: Map.get(hook_context.context, "default_value"), - variation: "SdkDefault", - user_key: Map.get(hook_context.context, "targeting_key") || @default_targeting_key - } + defp error_hook(hook_context, any, _hints) do + # Logger.info("Data sent successfully: #{inspect(hook_context)}") + Logger.info("Data sent successfully: #{inspect(any)}") + # Logger.info("Data sent successfully: #{inspect(hints)}") + # if hook.disable_data_collection do + # :ok + # else + feature_event = %FeatureEvent{ + context_kind: + if(Map.get(hook_context.context, "anonymous"), do: "anonymousUser", else: "user"), + creation_date: DateTime.utc_now() |> DateTime.to_unix(:millisecond), + default: true, + key: hook_context.flag_key, + value: Map.get(hook_context.context, "default_value"), + variation: "SdkDefault", + user_key: Map.get(hook_context.context, "targeting_key") || @default_targeting_key + } - GenServer.call(__MODULE__, {:add_event, feature_event}) - end + GenServer.call(__MODULE__, {:add_event, feature_event}) + # end + end + + defp finally_hook(_hook_context, _hook_hints) do + # Define your `finally` hook logic, if any + :ok + end + + # Schedule periodic data collection based on the interval + defp schedule_collect_data(interval) do + Process.send_after(self(), :collect_data, interval) end ### GenServer Callbacks @@ -132,6 +152,8 @@ defmodule ElixirProvider.DataCollectorHook do http_client: http_client, data_collector_endpoint: endpoint }) do + Logger.info("Data sent successfully: #{inspect(event_queue)}") + if Enum.empty?(event_queue) do :ok else diff --git a/openfeature/providers/elixir-provider/lib/provider/evaluation_context.ex b/openfeature/providers/elixir-provider/lib/provider/evaluation_context.ex index 5a7ae148b98..8a890c1caa2 100644 --- a/openfeature/providers/elixir-provider/lib/provider/evaluation_context.ex +++ b/openfeature/providers/elixir-provider/lib/provider/evaluation_context.ex @@ -3,8 +3,8 @@ defmodule ElixirProvider.GofEvaluationContext do GoFeatureFlagEvaluationContext is an object representing a user context for evaluation. """ alias Jason - - defstruct [key: "", custom: %{}] + @derive Jason.Encoder + defstruct key: "", custom: %{} @type t :: %__MODULE__{ key: String.t(), diff --git a/openfeature/providers/elixir-provider/lib/provider/feature_event.ex b/openfeature/providers/elixir-provider/lib/provider/feature_event.ex index d631a9183ac..b3f91dea5cd 100644 --- a/openfeature/providers/elixir-provider/lib/provider/feature_event.ex +++ b/openfeature/providers/elixir-provider/lib/provider/feature_event.ex @@ -3,7 +3,7 @@ defmodule ElixirProvider.FeatureEvent do Represents a feature event with details about the feature flag evaluation. """ @enforce_keys [:context_kind, :user_key, :creation_date, :key, :variation] - defstruct [kind: "feature", + defstruct kind: "feature", context_kind: "", user_key: "", creation_date: 0, @@ -11,7 +11,7 @@ defmodule ElixirProvider.FeatureEvent do variation: "", value: nil, default: false, - source: "PROVIDER_CACHE"] + source: "PROVIDER_CACHE" @type t :: %__MODULE__{ kind: String.t(), diff --git a/openfeature/providers/elixir-provider/lib/provider/http_client.ex b/openfeature/providers/elixir-provider/lib/provider/http_client.ex index 0786da26de0..32db352fcd5 100644 --- a/openfeature/providers/elixir-provider/lib/provider/http_client.ex +++ b/openfeature/providers/elixir-provider/lib/provider/http_client.ex @@ -1,10 +1,8 @@ defmodule ElixirProvider.HttpClient do @moduledoc """ - Handles HTTP requests to the GO Feature Flag API. + Implements HttpClientBehaviour using Mint for HTTP requests. """ - use GenServer - # Define a struct to store HTTP connection, endpoint, and other configuration details defstruct [:conn, :endpoint, :headers] @@ -14,56 +12,28 @@ defmodule ElixirProvider.HttpClient do headers: list() } - @spec start_link() :: GenServer.on_start() - def start_link do - GenServer.start_link(__MODULE__, :ok, name: __MODULE__) - end - - def stop do - GenServer.stop(__MODULE__) - end - - @impl true - def init([]) do - {:ok, %__MODULE__{}} - end - - @spec start_http_connection(any()) :: - {:error, - %{ - :__exception__ => true, - :__struct__ => Mint.HTTPError | Mint.TransportError, - :reason => any(), - optional(:module) => any() - }} - | {:ok, ElixirProvider.HttpClient.t()} def start_http_connection(options) do uri = URI.parse(options.endpoint) scheme = if uri.scheme == "https", do: :https, else: :http case Mint.HTTP.connect(scheme, uri.host, uri.port) do {:ok, conn} -> - # Create the struct with the connection, endpoint, and default headers - config = %__MODULE__{ - conn: conn, - endpoint: options.endpoint, - headers: [{"content-type", "application/json"}] - } - - {:ok, config} + {:ok, + %{ + conn: conn, + endpoint: options.endpoint, + headers: [{"content-type", "application/json"}] + }} {:error, reason} -> {:error, reason} end end - @spec post(t(), String.t(), map()) :: {:ok, map()} | {:error, any()} - def post(%__MODULE__{conn: conn, endpoint: endpoint, headers: headers}, path, data) do - # Full URL path + def post(%{conn: conn, endpoint: endpoint, headers: headers}, path, data) do url = URI.merge(endpoint, path) |> URI.to_string() body = Jason.encode!(data) - # Make the POST request using the existing connection with {:ok, conn, request_ref} <- Mint.HTTP.request(conn, "POST", url, headers, body), {:ok, response} <- read_response(conn, request_ref) do Jason.decode(response) diff --git a/openfeature/providers/elixir-provider/lib/provider/provider.ex b/openfeature/providers/elixir-provider/lib/provider/provider.ex index 98dc79abf00..c5a760cd425 100644 --- a/openfeature/providers/elixir-provider/lib/provider/provider.ex +++ b/openfeature/providers/elixir-provider/lib/provider/provider.ex @@ -11,6 +11,7 @@ defmodule ElixirProvider.Provider do alias ElixirProvider.HttpClient alias ElixirProvider.RequestFlagEvaluation alias ElixirProvider.ResponseFlagEvaluation + alias OpenFeature.Hook alias OpenFeature.ResolutionDetails @moduledoc """ @@ -30,7 +31,7 @@ defmodule ElixirProvider.Provider do name: String.t(), options: GoFeatureFlagOptions.t(), http_client: HttpClient.t(), - hooks: DataCollectorHook.t() | nil, + hooks: [Hook.t()] | nil, ws: GoFWebSocketClient.t(), domain: String.t() } @@ -45,7 +46,7 @@ defmodule ElixirProvider.Provider do provider | domain: domain, http_client: http_client, - hooks: hooks, + hooks: [hooks.base_hook], ws: ws } @@ -87,8 +88,11 @@ defmodule ElixirProvider.Provider do defp generic_resolve(provider, type, flag_key, default_value, context) do {:ok, goff_context} = ContextTransformer.transform_context(context) + goff_request = %RequestFlagEvaluation{user: goff_context, default_value: default_value} eval_context_hash = GofEvaluationContext.hash(goff_context) + http_client = provider.http_client + Logger.debug("Unexpected frame received: #{inspect("fires")}") response_body = case CacheController.get(flag_key, eval_context_hash) do @@ -97,9 +101,12 @@ defmodule ElixirProvider.Provider do :miss -> # Fetch from HTTP if cache miss - case HttpClient.post(provider.http_client, "/v1/feature/#{flag_key}/eval", goff_request) do - {:ok, response} -> handle_response(flag_key, eval_context_hash, response) - {:error, reason} -> {:error, {:unexpected_error, reason}} + case HttpClient.post(http_client, "/v1/feature/#{flag_key}/eval", goff_request) do + {:ok, response} -> + handle_response(flag_key, eval_context_hash, response) + + {:error, reason} -> + {:error, {:unexpected_error, reason}} end end @@ -107,7 +114,6 @@ defmodule ElixirProvider.Provider do end defp handle_response(flag_key, eval_context_hash, response) do - Logger.debug("Unexpected frame received: #{inspect("here")}") # Build the flag evaluation struct directly from the response map flag_eval = ResponseFlagEvaluation.decode(response) @@ -120,6 +126,8 @@ defmodule ElixirProvider.Provider do end defp handle_flag_resolution(response, type, flag_key, _default_value) do + Logger.debug("Unexpected frame received: #{inspect(response)}") + case response do {:ok, %ResponseFlagEvaluation{value: value, reason: reason}} -> case {type, value} do diff --git a/openfeature/providers/elixir-provider/lib/provider/request_flag_evaluation.ex b/openfeature/providers/elixir-provider/lib/provider/request_flag_evaluation.ex index ddf8a7fccd3..2c0a413493a 100644 --- a/openfeature/providers/elixir-provider/lib/provider/request_flag_evaluation.ex +++ b/openfeature/providers/elixir-provider/lib/provider/request_flag_evaluation.ex @@ -5,6 +5,7 @@ defmodule ElixirProvider.RequestFlagEvaluation do alias ElixirProvider.GofEvaluationContext @enforce_keys [:user] + @derive Jason.Encoder defstruct [:default_value, :user] @type t :: %__MODULE__{ diff --git a/openfeature/providers/elixir-provider/lib/provider/response_flag_evalution.ex b/openfeature/providers/elixir-provider/lib/provider/response_flag_evalution.ex index 4202b703782..354351eaaf8 100644 --- a/openfeature/providers/elixir-provider/lib/provider/response_flag_evalution.ex +++ b/openfeature/providers/elixir-provider/lib/provider/response_flag_evalution.ex @@ -5,15 +5,18 @@ defmodule ElixirProvider.ResponseFlagEvaluation do alias ElixirProvider.Types @enforce_keys [:value, :failed, :reason] - defstruct [:value, error_code: nil, - failed: false, - reason: "", - track_events: nil, - variation_type: nil, - version: nil, - metadata: nil, - cacheable: nil - ] + @derive Jason.Encoder + defstruct [ + :value, + error_code: nil, + failed: false, + reason: "", + track_events: nil, + variation_type: nil, + version: nil, + metadata: nil, + cacheable: nil + ] @type t :: %__MODULE__{ error_code: String.t() | nil, diff --git a/openfeature/providers/elixir-provider/lib/provider/server_supervisor.ex b/openfeature/providers/elixir-provider/lib/provider/server_supervisor.ex index 7d579ff653d..74beb87103e 100644 --- a/openfeature/providers/elixir-provider/lib/provider/server_supervisor.ex +++ b/openfeature/providers/elixir-provider/lib/provider/server_supervisor.ex @@ -11,8 +11,6 @@ defmodule ElixirProvider.ServerSupervisor do @impl true def init([_args]) do children = [ - ElixirProvider.HttpClient, - ElixirProvider.GoFWebSocketClient, ElixirProvider.CacheController, ElixirProvider.DataCollectorHook ] diff --git a/openfeature/providers/elixir-provider/mix.exs b/openfeature/providers/elixir-provider/mix.exs index 07c61da8be7..1daa1beb8a9 100644 --- a/openfeature/providers/elixir-provider/mix.exs +++ b/openfeature/providers/elixir-provider/mix.exs @@ -27,7 +27,9 @@ defmodule ElixirProvider.MixProject do {:mint_web_socket, "~> 1.0"}, {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, {:bypass, "~> 2.1", only: :test}, - {:plug, "~> 1.16", only: :test} + {:plug, "~> 1.16", only: :test}, + {:mox, "~> 1.2", only: :test}, + {:mimic, "~> 1.7", only: :test} ] end end diff --git a/openfeature/providers/elixir-provider/mix.lock b/openfeature/providers/elixir-provider/mix.lock index 9c006dbd850..a34fa9e7c46 100644 --- a/openfeature/providers/elixir-provider/mix.lock +++ b/openfeature/providers/elixir-provider/mix.lock @@ -7,11 +7,15 @@ "credo": {:hex, :credo, "1.7.10", "6e64fe59be8da5e30a1b96273b247b5cf1cc9e336b5fd66302a64b25749ad44d", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "71fbc9a6b8be21d993deca85bf151df023a3097b01e09a2809d460348561d8cd"}, "elixir_sdk": {:git, "https://github.com/open-feature/elixir-sdk.git", "8e08041085aedec5d661b9a9a942cdf9f2606422", []}, "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"}, + "ham": {:hex, :ham, "0.3.0", "7cd031b4a55fba219c11553e7b13ba73bd86eab4034518445eff1e038cb9a44d", [:mix], [], "hexpm", "7d6c6b73d7a6a83233876cc1b06a4d9b5de05562b228effda4532f9a49852bf6"}, "hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, + "mimic": {:hex, :mimic, "1.10.2", "0d7e67ba09b1e8fe21a61a91f4cb2b876151c2d7e1c9bf6fc325195dd33075dd", [:mix], [{:ham, "~> 0.2", [hex: :ham, repo: "hexpm", optional: false]}], "hexpm", "21a50eddbdee1e9bad93cb8738bd4e224913d0d25a06692d34fb19881dba7292"}, "mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"}, "mint_web_socket": {:hex, :mint_web_socket, "1.0.4", "0b539116dbb3d3f861cdf5e15e269a933cb501c113a14db7001a3157d96ffafd", [:mix], [{:mint, ">= 1.4.1 and < 2.0.0-0", [hex: :mint, repo: "hexpm", optional: false]}], "hexpm", "027d4c5529c45a4ba0ce27a01c0f35f284a5468519c045ca15f43decb360a991"}, + "mox": {:hex, :mox, "1.2.0", "a2cd96b4b80a3883e3100a221e8adc1b98e4c3a332a8fc434c39526babafd5b3", [:mix], [{:nimble_ownership, "~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}], "hexpm", "c7b92b3cc69ee24a7eeeaf944cd7be22013c52fcb580c1f33f50845ec821089a"}, + "nimble_ownership": {:hex, :nimble_ownership, "1.0.1", "f69fae0cdd451b1614364013544e66e4f5d25f36a2056a9698b793305c5aa3a6", [:mix], [], "hexpm", "3825e461025464f519f3f3e4a1f9b68c47dc151369611629ad08b636b73bb22d"}, "open_feature": {:git, "https://github.com/open-feature/elixir-sdk.git", "8e08041085aedec5d661b9a9a942cdf9f2606422", []}, "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, "plug_cowboy": {:hex, :plug_cowboy, "2.7.2", "fdadb973799ae691bf9ecad99125b16625b1c6039999da5fe544d99218e662e4", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "245d8a11ee2306094840c000e8816f0cbed69a23fc0ac2bcf8d7835ae019bb2f"}, diff --git a/openfeature/providers/elixir-provider/test/elixir_provider/elixir_provider_test.exs b/openfeature/providers/elixir-provider/test/elixir_provider/elixir_provider_test.exs new file mode 100644 index 00000000000..510d14245ef --- /dev/null +++ b/openfeature/providers/elixir-provider/test/elixir_provider/elixir_provider_test.exs @@ -0,0 +1,231 @@ +defmodule ElixirProviderTest do + @moduledoc """ + Test file + """ + use ExUnit.Case, async: true + require Logger + + doctest ElixirProvider + alias OpenFeature + alias OpenFeature.Client + + @endpoint "http://localhost:1031" + + @default_evaluation_ctx %{ + targetingKey: "d45e303a-38c2-11ed-a261-0242ac120002", + email: "john.doe@gofeatureflag.org", + firstname: "john", + lastname: "doe", + anonymous: false, + professional: true, + rate: 3.14, + age: 30, + company_info: %{name: "my_company", size: 120}, + labels: ["pro", "beta"] + } + + setup do + _ = start_supervised!(ElixirProvider.ServerSupervisor) + + provider = %ElixirProvider.Provider{ + options: %ElixirProvider.GoFeatureFlagOptions{ + endpoint: @endpoint, + data_flush_interval: 100, + disable_cache_invalidation: true + } + } + + OpenFeature.set_provider(provider) + client = OpenFeature.get_client() + {:ok, client: client} + end + + ## TEST CONTEXT TRANSFORMER + + test "should use the targetingKey as user key" do + got = + ElixirProvider.ContextTransformer.transform_context(%{ + targetingKey: "user-key" + }) + + want( + = / + {:ok, + %ElixirProvider.GofEvaluationContext{ + key: "user-key", + custom: %{} + }} + ) + + assert got == want + end + + test "should specify the anonymous field base on the attributes" do + got = + ElixirProvider.ContextTransformer.transform_context(%{ + targetingKey: "user-key", + anonymous: true + }) + + want = + {:ok, + %ElixirProvider.GofEvaluationContext{ + key: "user-key", + custom: %{ + anonymous: true + } + }} + + assert got == want + end + + test "should fail if no targeting field is provided" do + got = + ElixirProvider.ContextTransformer.transform_context(%{ + anonymous: true, + firstname: "John", + lastname: "Doe", + email: "john.doe@gofeatureflag.org" + }) + + want = {:error, "targeting key not found"} + + assert got == want + end + + test "should fill custom fields if extra fields are present" do + got = + ElixirProvider.ContextTransformer.transform_context(%{ + targetingKey: "user-key", + anonymous: true, + firstname: "John", + lastname: "Doe", + email: "john.doe@gofeatureflag.org" + }) + + want = + {:ok, + %ElixirProvider.GofEvaluationContext{ + key: "user-key", + custom: %{ + firstname: "John", + lastname: "Doe", + email: "john.doe@gofeatureflag.org", + anonymous: true + } + }} + + assert got == want + end + + ## PROVIDER TESTS + + test "should provide an error if flag does not exist", %{client: client} do + flag_key = "flag_not_found" + default = false + ctx = @default_evaluation_ctx + + ElixirProvider.HttpClientMock + |> expect(:post, fn _client, path, _data -> + if path == "/v1/feature/#{flag_key}/eval" do + {:error, {:http_error, 404, "Not Found"}} + else + {:error, {:unexpected_path, path}} + end + end) + + # Make the client call + response = Client.get_boolean_details(client, flag_key, default, context: ctx) + + # # Define the expected response structure + # expected_response = %{ + # error_code: :provider_not_ready, + # error_message: + # "impossible to call go-feature-flag relay proxy on #{@endpoint}#{path}: Error: Request failed with status code 404", + # key: flag_key, + # reason: :error, + # value: false, + # flag_metadata: %{} + # } + + # # Assert the response matches the expected structure + assert response == "?" + end + + # test "should provide an error if flag does not exist", %{client: client} do + # flag_key = "flag_not_found" + # default = false + # ctx = @default_evaluation_ctx + # path = "/v1/feature/#{flag_key}/eval" + + # # Mock the Mint.HTTP.request/5 function + # Mimic.expect(Mint.HTTP, :request, fn _conn, "POST", url, headers, body -> + # assert url == "#{@endpoint}#{path}" + # assert headers == [{"content-type", "application/json"}] + # assert body == Jason.encode!(%{context: ctx, default: default}) + # {:ok, :mocked_conn, :mocked_request_ref} + # end) + + # # Mock the Mint.HTTP.stream/2 function to simulate a 404 error response + # Mimic.expect(Mint.HTTP, :stream, fn _conn, _message -> + # {:ok, :mocked_conn, + # [ + # {:status, :mocked_request_ref, 404}, + # {:headers, :mocked_request_ref, []}, + # {:data, :mocked_request_ref, ~s<{"error":"flag_not_found"}>}, + # {:done, :mocked_request_ref} + # ]} + # end) + + # # Call the function being tested + # response = Client.get_boolean_details(client, flag_key, default, context: ctx) + + # # Define the expected response + # # expected_response = %{ + # # error_code: :provider_not_ready, + # # error_message: + # # "impossible to call go-feature-flag relay proxy on #{endpoint}#{path}: Error: Request failed with status code 404", + # # key: flag_key, + # # reason: :error, + # # value: false, + # # flag_metadata: %{} + # # } + + # # Assert the response matches the expected response + # # assert response == "?" + # end + + test "post/3 sends a POST request and processes the response" do + # Mock the Mint.HTTP.request/5 function + Mimic.expect(Mint.HTTP, :request, fn _conn, "POST", url, headers, body -> + assert url == "https://api.example.com/v1/test/path" + assert headers == [{"content-type", "application/json"}] + assert body == ~s<{"key":"value"}> + {:ok, :mocked_conn, :mocked_request_ref} + end) + + # Mock the Mint.HTTP.stream/2 function to simulate a 200 OK response + Mimic.expect(Mint.HTTP, :stream, fn _conn, _message -> + {:ok, :mocked_conn, + [ + {:status, :mocked_request_ref, 200}, + {:headers, :mocked_request_ref, []}, + {:data, :mocked_request_ref, ~s<{"message":"success"}>}, + {:done, :mocked_request_ref} + ]} + end) + + # Prepare the connection struct + client = %ElixirProvider.HttpClient{ + conn: :mocked_conn, + endpoint: "https://api.example.com", + headers: [{"content-type", "application/json"}] + } + + # Call the post/3 function + response = ElixirProvider.HttpClient.post(client, "/v1/test/path", %{"key" => "value"}) + + # Assert the decoded response + assert {:ok, %{"message" => "success"}} == response + end +end diff --git a/openfeature/providers/elixir-provider/test/elixir_provider_test.exs b/openfeature/providers/elixir-provider/test/elixir_provider_test.exs deleted file mode 100644 index df083306cf0..00000000000 --- a/openfeature/providers/elixir-provider/test/elixir_provider_test.exs +++ /dev/null @@ -1,148 +0,0 @@ -defmodule ElixirProviderTest do - @moduledoc """ - Test file - """ - use ExUnit.Case - doctest ElixirProvider - alias OpenFeature - alias OpenFeature.Client - - @endpoint "http://localhost:1031" - - @default_evaluation_ctx %{ - targeting_key: "d45e303a-38c2-11ed-a261-0242ac120002", - email: "john.doe@gofeatureflag.org", - firstname: "john", - lastname: "doe", - anonymous: false, - professional: true, - rate: 3.14, - age: 30, - company_info: %{name: "my_company", size: 120}, - labels: ["pro", "beta"] - } - - setup do - provider = %ElixirProvider.Provider{ - options: %ElixirProvider.GoFeatureFlagOptions{ - endpoint: @endpoint, - data_flush_interval: 100, - disable_cache_invalidation: true - } - } - - bypass = Bypass.open() - OpenFeature.set_provider(provider) - client = OpenFeature.get_client() - {:ok, bypass: bypass, client: client} - end - - ## TEST CONTEXT TRANSFORMER - - # test "should use the targetingKey as user key" do - # got = - # ElixirProvider.ContextTransformer.transform_context(%{ - # targetingKey: "user-key" - # }) - - # want =/ - # {:ok, - # %ElixirProvider.GofEvaluationContext{ - # key: "user-key", - # custom: %{} - # }} - - # assert got == want - # end - - # test "should specify the anonymous field base on the attributes" do - # got = - # ElixirProvider.ContextTransformer.transform_context(%{ - # targetingKey: "user-key", - # anonymous: true - # }) - - # want = - # {:ok, - # %ElixirProvider.GofEvaluationContext{ - # key: "user-key", - # custom: %{ - # anonymous: true - # } - # }} - - # assert got == want - # end - - # test "should fail if no targeting field is provided" do - # got = - # ElixirProvider.ContextTransformer.transform_context(%{ - # anonymous: true, - # firstname: "John", - # lastname: "Doe", - # email: "john.doe@gofeatureflag.org" - # }) - - # want = {:error, "targeting key not found"} - - # assert got == want - # end - - # test "should fill custom fields if extra fields are present" do - # got = - # ElixirProvider.ContextTransformer.transform_context(%{ - # targetingKey: "user-key", - # anonymous: true, - # firstname: "John", - # lastname: "Doe", - # email: "john.doe@gofeatureflag.org" - # }) - - # want = - # {:ok, - # %ElixirProvider.GofEvaluationContext{ - # key: "user-key", - # custom: %{ - # firstname: "John", - # lastname: "Doe", - # email: "john.doe@gofeatureflag.org", - # anonymous: true - # } - # }} - - # assert got == want - # end - - ### PROVIDER TESTS - - test "should provide an error if flag does not exist", %{bypass: bypass, client: client} do - flag_key = "flag_not_found" - default = false - ctx = @default_evaluation_ctx - - # Corrected path (only the path, not the full URL) - path = "/v1/feature/#{flag_key}/eval" - - # Set up Bypass to handle the POST request - Bypass.expect_once(bypass, "POST", path, fn conn -> - Plug.Conn.resp(conn, 404, ~s<{"errors": [{"code": 88, "message": "Rate limit exceeded"}]}>) - end) - - # Make the client call - response = Client.get_boolean_details(client, flag_key, default, context: ctx) - - # Define the expected response structure - expected_response = %{ - error_code: :provider_not_ready, - error_message: - "impossible to call go-feature-flag relay proxy on #{@endpoint}#{path}: Error: Request failed with status code 404", - key: flag_key, - reason: :error, - value: false, - flag_metadata: %{} - } - - # Assert the response matches the expected structure - assert response == expected_response - end -end diff --git a/openfeature/providers/elixir-provider/test/test_helper.exs b/openfeature/providers/elixir-provider/test/test_helper.exs index 869559e709e..9264bb7105a 100644 --- a/openfeature/providers/elixir-provider/test/test_helper.exs +++ b/openfeature/providers/elixir-provider/test/test_helper.exs @@ -1 +1,3 @@ +Mimic.copy(Mint.HTTP) + ExUnit.start()