Skip to content
Merged
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
28 changes: 3 additions & 25 deletions lib/faultex.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,8 @@ defmodule Faultex do
end
end

@spec inject(
Faultex.Injector.ErrorInjector.t()
| Faultex.Injector.SlowInjector.t()
| Faultex.Injector.RejectInjector.t()
| Faultex.Injector.RandomInjector.t()
| Faultex.Injector.ChainInjector.t()
) :: Faultex.Response.t()
def inject(%Faultex.Injector.ErrorInjector{} = injector) do
Faultex.Injector.ErrorInjector.inject(injector)
end

def inject(%Faultex.Injector.SlowInjector{} = injector) do
Faultex.Injector.SlowInjector.inject(injector)
end

def inject(%Faultex.Injector.RejectInjector{} = injector) do
Faultex.Injector.RejectInjector.inject(injector)
end

def inject(%Faultex.Injector.RandomInjector{} = injector) do
Faultex.Injector.RandomInjector.inject(injector)
end

def inject(%Faultex.Injector.ChainInjector{} = injector) do
Faultex.Injector.ChainInjector.inject(injector)
@spec inject(Faultex.Injector.t()) :: Faultex.Response.t()
def inject(injector) do
Faultex.Injector.inject(injector)
end
end
29 changes: 21 additions & 8 deletions lib/faultex/httpoison.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,27 @@ defmodule Faultex.HTTPoison do
{true, injector} ->
resp = Faultex.inject(injector)

{:ok,
%HTTPoison.Response{
body: resp.body,
headers: resp.headers,
request: request,
request_url: url,
status_code: resp.status
}}
case resp.action do
:reject ->
{:error, %HTTPoison.Error{reason: :closed}}

:passthrough ->
super(method, url, body, headers, options)

:response ->
{:ok,
%HTTPoison.Response{
body: resp.body,
headers: resp.headers,
request: request,
request_url: url,
status_code: resp.status
}}

:steal ->
_ = super(method, url, body, headers, options)
{:error, %HTTPoison.Error{reason: :closed}}
end

{false, _} ->
super(method, url, body, headers, options)
Expand Down
7 changes: 4 additions & 3 deletions lib/faultex/injector.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
defmodule Faultex.Injector do
defprotocol Faultex.Injector do
@moduledoc """
Behaviour for fault injectors
Protocol for fault injectors
"""

@callback inject(request :: term()) :: Faultex.Response.t()
@spec inject(t) :: Faultex.Response.t()
def inject(injector)
end
7 changes: 4 additions & 3 deletions lib/faultex/injector/chain_injector.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,14 @@ defmodule Faultex.Injector.ChainInjector do
:injectors
]

@behaviour Faultex.Injector

@impl Faultex.Injector
@spec inject(t()) :: Faultex.Response.t()
def inject(%__MODULE__{injectors: injectors}) do
Enum.reduce(injectors, %Faultex.Response{}, fn inj, _acc ->
Faultex.inject(inj)
end)
end
end

defimpl Faultex.Injector, for: Faultex.Injector.ChainInjector do
def inject(injector), do: Faultex.Injector.ChainInjector.inject(injector)
end
8 changes: 5 additions & 3 deletions lib/faultex/injector/error_injector.ex
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,6 @@ defmodule Faultex.Injector.ErrorInjector do
:resp_delay
]

@behaviour Faultex.Injector

@impl Faultex.Injector
@spec inject(t()) :: Faultex.Response.t()
def inject(injector) do
resp_delay =
Expand All @@ -49,9 +46,14 @@ defmodule Faultex.Injector.ErrorInjector do
end

%Faultex.Response{
action: :response,
status: injector.resp_status,
headers: injector.resp_headers,
body: injector.resp_body
}
end
end

defimpl Faultex.Injector, for: Faultex.Injector.ErrorInjector do
def inject(injector), do: Faultex.Injector.ErrorInjector.inject(injector)
end
7 changes: 4 additions & 3 deletions lib/faultex/injector/random_injector.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,12 @@ defmodule Faultex.Injector.RandomInjector do
:injectors
]

@behaviour Faultex.Injector

@impl Faultex.Injector
@spec inject(t()) :: Faultex.Response.t()
def inject(%__MODULE__{injectors: injectors}) do
injectors |> Enum.random() |> Faultex.inject()
end
end

defimpl Faultex.Injector, for: Faultex.Injector.RandomInjector do
def inject(injector), do: Faultex.Injector.RandomInjector.inject(injector)
end
9 changes: 5 additions & 4 deletions lib/faultex/injector/reject_injector.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,12 @@ defmodule Faultex.Injector.RejectInjector do
:resp_delay
]

@behaviour Faultex.Injector

@impl Faultex.Injector
@spec inject(t()) :: Faultex.Response.t()
def inject(_injector) do
%Faultex.Response{headers: [], body: ""}
%Faultex.Response{action: :reject, headers: [], body: ""}
end
end

defimpl Faultex.Injector, for: Faultex.Injector.RejectInjector do
def inject(injector), do: Faultex.Injector.RejectInjector.inject(injector)
end
9 changes: 5 additions & 4 deletions lib/faultex/injector/slow_injector.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@ defmodule Faultex.Injector.SlowInjector do
:resp_delay
]

@behaviour Faultex.Injector

@impl Faultex.Injector
@spec inject(t()) :: Faultex.Response.t()
def inject(injector) do
resp_delay =
Expand All @@ -40,6 +37,10 @@ defmodule Faultex.Injector.SlowInjector do
Process.sleep(resp_delay)
end

%Faultex.Response{}
%Faultex.Response{action: :passthrough}
end
end

defimpl Faultex.Injector, for: Faultex.Injector.SlowInjector do
def inject(injector), do: Faultex.Injector.SlowInjector.inject(injector)
end
36 changes: 36 additions & 0 deletions lib/faultex/injector/steal_response_injector.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
defmodule Faultex.Injector.StealResponseInjector do
@moduledoc """
Simulates a stolen response where the server processes the request but the response never reaches the client.
"""

@type t :: %__MODULE__{
id: term(),
disable: boolean() | nil,
host: String.t() | nil,
method: String.t() | nil,
path: String.t() | nil,
headers: [{String.t(), String.t()}] | nil,
percentage: integer() | nil,
resp_delay: integer() | nil
}

defstruct [
:id,
:disable,
:host,
:method,
:path,
:headers,
:percentage,
:resp_delay
]

@spec inject(t()) :: Faultex.Response.t()
def inject(_injector) do
%Faultex.Response{action: :steal}
end
end

defimpl Faultex.Injector, for: Faultex.Injector.StealResponseInjector do
def inject(injector), do: Faultex.Injector.StealResponseInjector.inject(injector)
end
18 changes: 12 additions & 6 deletions lib/faultex/matcher.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,7 @@ defmodule Faultex.Matcher do
"""

@type header :: {String.t(), String.t()}
@type injector ::
Faultex.Injector.ErrorInjector.t()
| Faultex.Injector.SlowInjector.t()
| Faultex.Injector.RejectInjector.t()
| Faultex.Injector.RandomInjector.t()
| Faultex.Injector.ChainInjector.t()
@type injector :: Faultex.Injector.t()
@type match_result :: {boolean(), injector() | nil}

@type t :: %__MODULE__{
Expand Down Expand Up @@ -150,6 +145,17 @@ defmodule Faultex.Matcher do
}
end

def do_build_matcher(injector) when is_struct(injector, Faultex.Injector.StealResponseInjector) do
validate_injector!(injector)

{
fill_matcher_params(injector),
%Faultex.Injector.StealResponseInjector{
resp_delay: Map.get(injector, :resp_delay, 0)
}
}
end

def do_build_matcher(injector) when is_struct(injector, Faultex.Injector.RandomInjector) do
validate_injector!(injector)
validate_injectors!(injector.injectors)
Expand Down
37 changes: 24 additions & 13 deletions lib/faultex/plug.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,40 +25,51 @@ defmodule Faultex.Plug do
matcher = opts[:matcher]

case match(matcher, conn) do
{true, %Faultex.Injector.SlowInjector{} = slow_injector} ->
_ = Faultex.inject(slow_injector)
conn

{true, injector} ->
send_resp_and_halt(conn, injector)
resp = Faultex.inject(injector)

case resp.action do
:passthrough ->
conn

:reject ->
conn |> Plug.Conn.halt()

:response ->
send_resp_and_halt(conn, resp)

:steal ->
Plug.Conn.register_before_send(conn, fn conn ->
Process.exit(self(), :kill)
conn
end)
end

{false, _} ->
conn
end
end

@spec send_resp_and_halt(Plug.Conn.t(), Faultex.Matcher.injector()) :: Plug.Conn.t()
def send_resp_and_halt(conn, injector) do
resp = Faultex.inject(injector)

@spec send_resp_and_halt(Plug.Conn.t(), Faultex.Response.t()) :: Plug.Conn.t()
defp send_resp_and_halt(conn, resp) do
conn
|> put_resp_headers(resp.headers)
|> Plug.Conn.send_resp(resp.status, resp.body)
|> Plug.Conn.halt()
end

@spec put_resp_headers(Plug.Conn.t(), [{String.t(), String.t()}] | nil) :: Plug.Conn.t()
def put_resp_headers(conn, nil), do: conn
def put_resp_headers(conn, []), do: conn
defp put_resp_headers(conn, nil), do: conn
defp put_resp_headers(conn, []), do: conn

def put_resp_headers(conn, headers) do
defp put_resp_headers(conn, headers) do
Enum.reduce(headers, conn, fn {k, v}, c ->
Plug.Conn.put_resp_header(c, k, v)
end)
end

@spec match(module(), Plug.Conn.t()) :: Faultex.Matcher.match_result()
def match(matcher, %Plug.Conn{} = conn) do
defp match(matcher, %Plug.Conn{} = conn) do
%{
host: host,
method: method,
Expand Down
5 changes: 4 additions & 1 deletion lib/faultex/response.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ defmodule Faultex.Response do
Response struct returned by injectors
"""

@type action :: :response | :passthrough | :reject | :steal

@type t :: %__MODULE__{
action: action(),
status: integer() | nil,
headers: [{String.t(), String.t()}] | nil,
body: String.t() | nil
}

defstruct [:status, :headers, :body]
defstruct [:action, :status, :headers, :body]
end
39 changes: 39 additions & 0 deletions test/faultex/httpoison_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,45 @@ defmodule Faultex.HTTPoisonTest do
]
end

defmodule MyApp.RejectHTTPoison do
use Faultex.HTTPoison,
injectors: [
%Faultex.Injector.RejectInjector{
host: "reject.example.com",
path: "/api",
method: "GET",
percentage: 100
}
]
end

defmodule MyApp.StealHTTPoison do
use Faultex.HTTPoison,
injectors: [
%Faultex.Injector.StealResponseInjector{
host: "github.com",
path: "/steal",
method: "GET",
percentage: 100
}
]
end

test "StealResponseInjector sends request then returns error with reason :closed" do
{:error, %HTTPoison.Error{reason: :closed}} =
MyApp.StealHTTPoison.get("https://github.com/steal")
end

test "StealResponseInjector passes through when not matched" do
{:ok, res} = MyApp.StealHTTPoison.get("https://github.com/")
assert res.status_code == 200
end

test "RejectInjector returns error with reason :closed" do
{:error, %HTTPoison.Error{reason: :closed}} =
MyApp.RejectHTTPoison.get("https://reject.example.com/api")
end

test "Request to remote server" do
{:ok, res} = MyApp.HTTPoison.get("https://github.com/", [])
assert res.status_code == 200
Expand Down
Loading
Loading