Skip to content

Commit

Permalink
add http mock to tests and update code
Browse files Browse the repository at this point in the history
  • Loading branch information
JoE11-y committed Dec 11, 2024
1 parent 50859b4 commit ff6b53b
Show file tree
Hide file tree
Showing 18 changed files with 359 additions and 251 deletions.
7 changes: 7 additions & 0 deletions openfeature/providers/elixir-provider/config/config.exs
Original file line number Diff line number Diff line change
@@ -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"
1 change: 1 addition & 0 deletions openfeature/providers/elixir-provider/config/dev.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import Config
1 change: 1 addition & 0 deletions openfeature/providers/elixir-provider/config/prod.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import Config
6 changes: 6 additions & 0 deletions openfeature/providers/elixir-provider/config/test.exs
Original file line number Diff line number Diff line change
@@ -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"
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,54 @@ 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(),
data_flush_interval: non_neg_integer(),
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

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ 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,
key: "",
variation: "",
value: nil,
default: false,
source: "PROVIDER_CACHE"]
source: "PROVIDER_CACHE"

@type t :: %__MODULE__{
kind: String.t(),
Expand Down
46 changes: 8 additions & 38 deletions openfeature/providers/elixir-provider/lib/provider/http_client.ex
Original file line number Diff line number Diff line change
@@ -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]

Expand All @@ -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)
Expand Down
20 changes: 14 additions & 6 deletions openfeature/providers/elixir-provider/lib/provider/provider.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ defmodule ElixirProvider.Provider do
alias ElixirProvider.HttpClient
alias ElixirProvider.RequestFlagEvaluation
alias ElixirProvider.ResponseFlagEvaluation
alias OpenFeature.Hook
alias OpenFeature.ResolutionDetails

@moduledoc """
Expand All @@ -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()
}
Expand All @@ -45,7 +46,7 @@ defmodule ElixirProvider.Provider do
provider
| domain: domain,
http_client: http_client,
hooks: hooks,
hooks: [hooks.base_hook],
ws: ws
}

Expand Down Expand Up @@ -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
Expand All @@ -97,17 +101,19 @@ 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

handle_flag_resolution(response_body, type, flag_key, default_value)
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)

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ defmodule ElixirProvider.RequestFlagEvaluation do
alias ElixirProvider.GofEvaluationContext

@enforce_keys [:user]
@derive Jason.Encoder
defstruct [:default_value, :user]

@type t :: %__MODULE__{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading

0 comments on commit ff6b53b

Please sign in to comment.