diff --git a/openfeature/providers/elixir-provider/lib/provider/provider.ex b/openfeature/providers/elixir-provider/lib/provider/provider.ex index 2eee0405177..98dc79abf00 100644 --- a/openfeature/providers/elixir-provider/lib/provider/provider.ex +++ b/openfeature/providers/elixir-provider/lib/provider/provider.ex @@ -1,6 +1,7 @@ defmodule ElixirProvider.Provider do @behaviour OpenFeature.Provider + require Logger alias ElixirProvider.CacheController alias ElixirProvider.ContextTransformer alias ElixirProvider.DataCollectorHook @@ -19,15 +20,17 @@ defmodule ElixirProvider.Provider do defstruct [ :options, :http_client, - :data_collector_hook, + :hooks, :ws, - :domain + :domain, + name: "ElixirProvider" ] @type t :: %__MODULE__{ + name: String.t(), options: GoFeatureFlagOptions.t(), http_client: HttpClient.t(), - data_collector_hook: DataCollectorHook.t() | nil, + hooks: DataCollectorHook.t() | nil, ws: GoFWebSocketClient.t(), domain: String.t() } @@ -35,14 +38,14 @@ defmodule ElixirProvider.Provider do @impl true def initialize(%__MODULE__{} = provider, domain, _context) do {:ok, http_client} = HttpClient.start_http_connection(provider.options) - {:ok, data_collector_hook} = DataCollectorHook.start(provider.options, http_client) + {:ok, hooks} = DataCollectorHook.start(provider.options, http_client) {:ok, ws} = GoFWebSocketClient.connect(provider.options.endpoint) updated_provider = %__MODULE__{ provider | domain: domain, http_client: http_client, - data_collector_hook: data_collector_hook, + hooks: hooks, ws: ws } @@ -56,7 +59,7 @@ defmodule ElixirProvider.Provider do if(GenServer.whereis(GoFWebSocketClient), do: GoFWebSocketClient.stop()) if(GenServer.whereis(DataCollectorHook), - do: DataCollectorHook.stop(provider.data_collector_hook) + do: DataCollectorHook.stop(provider.hooks) ) :ok @@ -104,6 +107,7 @@ 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) diff --git a/openfeature/providers/elixir-provider/lib/provider/web_socket.ex b/openfeature/providers/elixir-provider/lib/provider/web_socket.ex index 00fc74ea217..c5e297d855e 100644 --- a/openfeature/providers/elixir-provider/lib/provider/web_socket.ex +++ b/openfeature/providers/elixir-provider/lib/provider/web_socket.ex @@ -23,7 +23,7 @@ defmodule ElixirProvider.GoFWebSocketClient do closing?: boolean() } - @websocket_uri "/ws/v1/flag/change" + @websocket_uri "ws/v1/flag/change" def connect(url) do with {:ok, socket} <- GenServer.start_link(__MODULE__, [], name: __MODULE__), @@ -45,36 +45,42 @@ defmodule ElixirProvider.GoFWebSocketClient do def handle_call({:connect, url}, from, state) do uri = URI.parse(url) - http_scheme = + {http_scheme, ws_scheme} = case uri.scheme do - "ws" -> :http - "wss" -> :https + "ws" -> {:http, :ws} + "wss" -> {:https, :wss} + "http" -> {:http, :ws} + "https" -> {:https, :wss} + _ -> {:reply, {:error, :invalid_scheme}, state} end - ws_scheme = - case uri.scheme do - "ws" -> :ws - "wss" -> :wss - end - - # Construct the WebSocket path - path = uri.path <> @websocket_uri + # Ensure the path is not nil + path = (uri.path || "/") <> @websocket_uri - with {:ok, conn} <- Mint.HTTP.connect(http_scheme, uri.host, uri.port), + with {:ok, conn} <- + Mint.HTTP.connect(http_scheme, uri.host, uri.port || default_port(http_scheme)), {:ok, conn, ref} <- Mint.WebSocket.upgrade(ws_scheme, conn, path, []) do state = %{state | conn: conn, request_ref: ref, caller: from} + {:noreply, state} else {:error, reason} -> + Logger.info("Parsed URI path: #{inspect("hi")}") {:reply, {:error, reason}, state} {:error, conn, reason} -> + Logger.info("Parsed URI path: #{inspect(reason)}") {:reply, {:error, reason}, put_in(state.conn, conn)} end end + defp default_port(:http), do: 80 + defp default_port(:https), do: 443 + @impl GenServer def handle_info(message, state) do + Logger.info("Received message: #{inspect(message)}") + case Mint.WebSocket.stream(state.conn, message) do {:ok, conn, responses} -> state = put_in(state.conn, conn) |> handle_responses(responses) @@ -165,7 +171,7 @@ defmodule ElixirProvider.GoFWebSocketClient do defp do_close(state) do Mint.HTTP.close(state.conn) - Logger.info("Comfy websocket closed") + Logger.info("Websocket closed") {:stop, :normal, state} end diff --git a/openfeature/providers/elixir-provider/mix.exs b/openfeature/providers/elixir-provider/mix.exs index b44b7091c00..07c61da8be7 100644 --- a/openfeature/providers/elixir-provider/mix.exs +++ b/openfeature/providers/elixir-provider/mix.exs @@ -25,7 +25,9 @@ defmodule ElixirProvider.MixProject do {:jason, "~> 1.4"}, {:mint, "~> 1.6"}, {:mint_web_socket, "~> 1.0"}, - {:credo, "~> 1.7", only: [:dev, :test], runtime: false} + {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, + {:bypass, "~> 2.1", only: :test}, + {:plug, "~> 1.16", only: :test} ] end end diff --git a/openfeature/providers/elixir-provider/mix.lock b/openfeature/providers/elixir-provider/mix.lock index d145ceabf6b..9c006dbd850 100644 --- a/openfeature/providers/elixir-provider/mix.lock +++ b/openfeature/providers/elixir-provider/mix.lock @@ -1,13 +1,23 @@ %{ "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, + "bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"}, + "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, + "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, + "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, "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"}, "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"}, "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"}, "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"}, + "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, + "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "websocket_client": {:hex, :websocket_client, "1.5.0", "e825f23c51a867681a222148ed5200cc4a12e4fb5ff0b0b35963e916e2b5766b", [:rebar3], [], "hexpm", "2b9b201cc5c82b9d4e6966ad8e605832eab8f4ddb39f57ac62f34cb208b68de9"}, "websockex": {:hex, :websockex, "0.4.3", "92b7905769c79c6480c02daacaca2ddd49de936d912976a4d3c923723b647bf0", [:mix], [], "hexpm", "95f2e7072b85a3a4cc385602d42115b73ce0b74a9121d0d6dbbf557645ac53e4"}, } diff --git a/openfeature/providers/elixir-provider/test/elixir_provider_test.exs b/openfeature/providers/elixir-provider/test/elixir_provider_test.exs index 23131f98d15..df083306cf0 100644 --- a/openfeature/providers/elixir-provider/test/elixir_provider_test.exs +++ b/openfeature/providers/elixir-provider/test/elixir_provider_test.exs @@ -4,82 +4,145 @@ defmodule ElixirProviderTest do """ use ExUnit.Case doctest ElixirProvider + alias OpenFeature + alias OpenFeature.Client - ## TEST CONTEXT TRANSFORMER + @endpoint "http://localhost:1031" - test "should use the targetingKey as user key" do - got = - ElixirProvider.ContextTransformer.transform_context(%{ - targetingKey: "user-key" - }) + @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"] + } - want = - {:ok, - %ElixirProvider.GofEvaluationContext{ - key: "user-key", - custom: %{} - }} + setup do + provider = %ElixirProvider.Provider{ + options: %ElixirProvider.GoFeatureFlagOptions{ + endpoint: @endpoint, + data_flush_interval: 100, + disable_cache_invalidation: true + } + } - assert got == want + bypass = Bypass.open() + OpenFeature.set_provider(provider) + client = OpenFeature.get_client() + {:ok, bypass: bypass, client: client} 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 CONTEXT TRANSFORMER - 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" - }) + # test "should use the targetingKey as user key" do + # got = + # ElixirProvider.ContextTransformer.transform_context(%{ + # targetingKey: "user-key" + # }) - want = {:error, "targeting key not found"} + # want =/ + # {:ok, + # %ElixirProvider.GofEvaluationContext{ + # key: "user-key", + # custom: %{} + # }} - assert got == want - end + # 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 + # 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