Page MenuHomePhorge

No OneTemporary

Size
47 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/tesla.ex b/lib/tesla.ex
index 35ebee7..43dcb45 100644
--- a/lib/tesla.ex
+++ b/lib/tesla.ex
@@ -1,233 +1,232 @@
defmodule Tesla.Error do
defexception message: "", reason: nil
end
defmodule Tesla.Env do
@type client :: Tesla.Client.t() | (t, stack -> t)
@type method :: :head | :get | :delete | :trace | :options | :post | :put | :patch
@type url :: binary
@type param :: binary | [{binary | atom, param}]
@type query :: [{binary | atom, param}]
@type headers :: %{binary => binary}
- #
+
@type body :: any
@type status :: integer
@type opts :: [any]
- @type __module__ :: atom
- @type __client__ :: function
@type stack :: [{atom, atom, any} | {atom, atom} | {:fn, (t -> t)} | {:fn, (t, stack -> t)}]
@type t :: %__MODULE__{
method: method,
query: query,
url: url,
headers: headers,
body: body,
status: status,
opts: opts,
- __module__: __module__,
- __client__: __client__
+ __module__: atom,
+ __client__: client
}
defstruct method: nil,
url: "",
query: [],
headers: [],
body: nil,
status: nil,
opts: [],
__module__: nil,
__client__: nil
end
defmodule Tesla.Client do
@type t :: %__MODULE__{
fun: (Tesla.Env.t(), Tesla.Env.stack() -> Tesla.Env.t()) | nil,
pre: Tesla.Env.stack(),
post: Tesla.Env.stack()
}
defstruct fun: nil,
pre: [],
post: []
end
defmodule Tesla.Middleware do
@callback call(env :: Tesla.Env.t(), next :: Tesla.Env.stack(), options :: any) :: Tesla.Env.t()
end
defmodule Tesla.Adapter do
@callback call(env :: Tesla.Env.t(), options :: any) :: Tesla.Env.t()
end
defmodule Tesla do
use Tesla.Builder
+ alias Tesla.Env
+
@default_adapter Tesla.Adapter.Httpc
@moduledoc """
A HTTP toolkit for building API clients using middlewares
Include Tesla module in your api client:
```ex
defmodule ExampleApi do
use Tesla
plug Tesla.Middleware.BaseUrl, "http://api.example.com"
plug Tesla.Middleware.JSON
end
"""
defmacro __using__(opts \\ []) do
quote do
use Tesla.Builder, unquote(opts)
end
end
@doc false
def execute(module, %{fun: fun, pre: pre, post: post} = client, options) do
- env = struct(Tesla.Env, options ++ [__module__: module, __client__: client])
+ env = struct(Env, options ++ [__module__: module, __client__: client])
stack = pre ++ wrapfun(fun) ++ module.__middleware__ ++ post ++ [effective_adapter(module)]
run(env, stack)
end
defp wrapfun(nil), do: []
defp wrapfun(fun), do: [{:fn, fun}]
@doc false
def effective_adapter(module) do
with nil <- adapter_per_module_from_config(module),
nil <- adapter_per_module(module),
nil <- adapter_from_config() do
adapter_default()
end
end
defp adapter_per_module_from_config(module) do
case Application.get_env(:tesla, module, [])[:adapter] do
nil -> nil
{adapter, opts} -> {adapter, :call, [opts]}
adapter -> {adapter, :call, [[]]}
end
end
defp adapter_per_module(module) do
module.__adapter__
end
defp adapter_from_config do
case Application.get_env(:tesla, :adapter) do
nil -> nil
{adapter, opts} -> {adapter, :call, [opts]}
adapter -> {adapter, :call, [[]]}
end
end
defp adapter_default do
{@default_adapter, :call, [[]]}
end
def run_default_adapter(env, opts \\ []) do
apply(@default_adapter, :call, [env, opts])
end
-
# empty stack case is useful for reusing/testing middlewares (just pass [] as next)
def run(env, []), do: env
# last item in stack is adapter - skip passing rest of stack
def run(env, [{:fn, f}]), do: apply(f, [env])
def run(env, [{m, f, a}]), do: apply(m, f, [env | a])
# for all other elements pass (env, next, opts)
def run(env, [{:fn, f} | rest]), do: apply(f, [env, rest])
def run(env, [{m, f, a} | rest]), do: apply(m, f, [env, rest | a])
# useful helper fuctions
def put_opt(env, key, value) do
Map.update!(env, :opts, &Keyword.put(&1, key, value))
end
- @spec get_header(Tesla.Env.t, binary) :: binary | nil
- def get_header(%Tesla.Env{headers: headers}, key) when is_list(headers) do
+ @spec get_header(Env.t(), binary) :: binary | nil
+ def get_header(%Env{headers: headers}, key) do
case List.keyfind(headers, key, 0) do
{_, value} -> value
_ -> nil
end
end
- @spec get_headers(Tesla.Env.t, binary) :: [binary]
- def get_headers(%Tesla.Env{headers: headers}, key) do
- for {k,v} <- headers, k == key, do: v
+ @spec get_headers(Env.t(), binary) :: [binary]
+ def get_headers(%Env{headers: headers}, key) do
+ for {k, v} <- headers, k == key, do: v
end
- @spec put_header(Tesla.Env.t, binary, binary) :: Tesla.Env.t
- def put_header(env, key, value) do
+ @spec put_header(Env.t(), binary, binary) :: Env.t()
+ def put_header(%Env{} = env, key, value) do
headers = List.keystore(env.headers, key, 0, {key, value})
%{env | headers: headers}
end
- @spec put_headers(Tesla.Env.t, [{binary, binary}]) :: Tesla.Env.t
- def put_headers(env, list) when is_list(list) do
+ @spec put_headers(Env.t(), [{binary, binary}]) :: Env.t()
+ def put_headers(%Env{} = env, list) when is_list(list) do
%{env | headers: env.headers ++ list}
end
- @spec delete_header(Tesla.Env.t, binary) :: Tesla.Env.t
- def delete_header(env, key) do
- headers = for {k,v} <- env.headers, k != key, do: {k,v}
+ @spec delete_header(Env.t(), binary) :: Env.t()
+ def delete_header(%Env{} = env, key) do
+ headers = for {k, v} <- env.headers, k != key, do: {k, v}
%{env | headers: headers}
end
@doc """
Dynamically build client from list of middlewares.
```ex
defmodule ExampleAPI do
use Tesla
def new(token) do
Tesla.build_client([
{Tesla.Middleware.Headers, %{"Authorization" => token}}
])
end
end
client = ExampleAPI.new(token: "abc")
client |> ExampleAPI.get("/me")
```
"""
defmacro build_client(pre, post \\ []) do
quote do
require Tesla.Builder
Tesla.Builder.client(unquote(pre), unquote(post))
end
end
def build_adapter(fun) do
%Tesla.Client{post: [{:fn, fn env, _next -> fun.(env) end}]}
end
def build_url(url, []), do: url
def build_url(url, query) do
join = if String.contains?(url, "?"), do: "&", else: "?"
url <> join <> encode_query(query)
end
defp encode_query(query) do
query
|> Enum.flat_map(&encode_pair/1)
|> URI.encode_query()
end
defp encode_pair({key, value}) when is_list(value) do
if Keyword.keyword?(value) do
Enum.flat_map(value, fn {k, v} -> encode_pair({"#{key}[#{k}]", v}) end)
else
Enum.map(value, fn e -> {"#{key}[]", e} end)
end
end
defp encode_pair({key, value}), do: [{key, value}]
end
diff --git a/lib/tesla/adapter/ibrowse.ex b/lib/tesla/adapter/ibrowse.ex
index 1101405..b98bd62 100644
--- a/lib/tesla/adapter/ibrowse.ex
+++ b/lib/tesla/adapter/ibrowse.ex
@@ -1,93 +1,98 @@
if Code.ensure_loaded?(:ibrowse) do
defmodule Tesla.Adapter.Ibrowse do
@moduledoc """
Adapter for [ibrowse](https://github.com/cmullaparthi/ibrowse)
Remember to add `{:ibrowse, "~> 4.2"}` to dependencies (and `:ibrowse` to applications in `mix.exs`)
Also, you need to recompile tesla after adding `:ibrowse` dependency:
```
mix deps.clean tesla
mix deps.compile tesla
```
### Example usage
```
# set globally in config/config.exs
config :tesla, :adapter, :ibrowse
# set per module
defmodule MyClient do
use Tesla
adapter :ibrowse
end
```
"""
@behaviour Tesla.Adapter
import Tesla.Adapter.Shared, only: [stream_to_fun: 1, next_chunk: 1]
alias Tesla.Multipart
def call(env, opts) do
with {:ok, status, headers, body} <- request(env, opts || []) do
- %{env | status: format_status(status), headers: format_headers(headers), body: format_body(body)}
+ %{
+ env
+ | status: format_status(status),
+ headers: format_headers(headers),
+ body: format_body(body)
+ }
else
{:error, reason} ->
raise %Tesla.Error{message: "adapter error: #{inspect(reason)}", reason: reason}
end
end
defp format_status(status) when is_list(status) do
status |> to_string() |> String.to_integer()
end
defp format_headers(headers) do
for {key, value} <- headers do
{String.downcase(to_string(key)), to_string(value)}
end
end
defp format_body(data) when is_list(data), do: IO.iodata_to_binary(data)
defp format_body(data) when is_binary(data), do: data
defp request(env, opts) do
body = env.body || []
handle(
request(
Tesla.build_url(env.url, env.query) |> to_charlist,
env.headers,
env.method,
body,
opts ++ env.opts
)
)
end
defp request(url, headers, method, %Multipart{} = mp, opts) do
headers = headers ++ Multipart.headers(mp)
body = stream_to_fun(Multipart.body(mp))
request(url, headers, method, body, opts)
end
defp request(url, headers, method, %Stream{} = body, opts) do
fun = stream_to_fun(body)
request(url, headers, method, fun, opts)
end
defp request(url, headers, method, body, opts) when is_function(body) do
body = {&next_chunk/1, body}
opts = Keyword.put(opts, :transfer_encoding, :chunked)
request(url, headers, method, body, opts)
end
defp request(url, headers, method, body, opts) do
:ibrowse.send_req(url, headers, method, body, opts)
end
defp handle({:error, {:conn_failed, error}}), do: error
defp handle(response), do: response
end
end
diff --git a/lib/tesla/builder.ex b/lib/tesla/builder.ex
index 9ed7c7b..074c66f 100644
--- a/lib/tesla/builder.ex
+++ b/lib/tesla/builder.ex
@@ -1,331 +1,334 @@
defmodule Tesla.Builder do
@http_verbs ~w(head get delete trace options post put patch)a
defmacro __using__(opts \\ []) do
opts = Macro.prewalk(opts, &Macro.expand(&1, __CALLER__))
docs = Keyword.get(opts, :docs, true)
quote do
Module.register_attribute(__MODULE__, :__middleware__, accumulate: true)
Module.register_attribute(__MODULE__, :__adapter__, [])
if unquote(docs) do
@type option ::
{:method, Tesla.Env.method()}
| {:url, Tesla.Env.url()}
| {:query, Tesla.Env.query()}
| {:headers, Tesla.Env.headers()}
| {:body, Tesla.Env.body()}
| {:opts, Tesla.Env.opts()}
@doc """
Perform a request using client function
Options:
- `:method` - the request method, one of [:head, :get, :delete, :trace, :options, :post, :put, :patch]
- `:url` - either full url e.g. "http://example.com/some/path" or just "/some/path" if using `Tesla.Middleware.BaseUrl`
- `:query` - a keyword list of query params, e.g. `[page: 1, per_page: 100]`
- `:headers` - a keyworld list of headers, e.g. `[{"content-type", "text/plain"}]`
- `:body` - depends on used middleware:
- by default it can be a binary
- if using e.g. JSON encoding middleware it can be a nested map
- if adapter supports it it can be a Stream with any of the above
- `:opts` - custom, per-request middleware or adapter options
Examples:
ExampleApi.request(method: :get, url: "/users/path")
You can also use shortcut methods like:
ExampleApi.get("/users/1")
or
myclient |> ExampleApi.post("/users", %{name: "Jon"})
"""
@spec request(Tesla.Env.client(), [option]) :: Tesla.Env.t()
else
@doc false
end
def request(%Tesla.Client{} = client, options) do
Tesla.execute(__MODULE__, client, options)
end
if unquote(docs) do
@doc """
Perform a request. See `request/2` for available options.
"""
@spec request([option]) :: Tesla.Env.t()
else
@doc false
end
def request(options) do
Tesla.execute(__MODULE__, %Tesla.Client{}, options)
end
unquote(generate_http_verbs(opts))
import Tesla.Builder, only: [plug: 1, plug: 2, adapter: 1, adapter: 2]
@before_compile Tesla.Builder
end
end
@doc """
Attach middleware to your API client
```ex
defmodule ExampleApi do
use Tesla
# plug middleware module with options
plug Tesla.Middleware.BaseUrl, "http://api.example.com"
plug Tesla.Middleware.JSON, engine: Poison
# plug middleware function
plug :handle_errors
# middleware function gets two parameters: Tesla.Env and the rest of middleware call stack
# and must return Tesla.Env
def handle_errors(env, next) do
env
|> modify_env_before_request
|> Tesla.run(next) # run the rest of stack
|> modify_env_after_request
end
end
"""
defmacro plug(middleware, opts \\ nil) do
- quote do: @__middleware__ {unquote(Macro.escape(middleware)), unquote(Macro.escape(opts))}
+ quote do: @__middleware__({unquote(Macro.escape(middleware)), unquote(Macro.escape(opts))})
end
@doc """
Choose adapter for your API client
```ex
defmodule ExampleApi do
use Tesla
# set adapter as module
adapter Tesla.Adapter.Hackney
# set adapter as function
adapter :local_adapter
# set adapter as anonymous function
adapter fn env ->
...
env
end
# adapter function gets Tesla.Env as parameter and must return Tesla.Env
def local_adapter(env) do
...
env
end
end
"""
defmacro adapter(adapter, opts \\ nil) do
- quote do: @__adapter__ {unquote(Macro.escape(adapter)), unquote(Macro.escape(opts))}
+ quote do: @__adapter__({unquote(Macro.escape(adapter)), unquote(Macro.escape(opts))})
end
defp generate_http_verbs(opts) do
only = Keyword.get(opts, :only, @http_verbs)
except = Keyword.get(opts, :except, [])
@http_verbs
- |> Enum.filter(&(&1 in only && not &1 in except))
+ |> Enum.filter(&(&1 in only && &1 not in except))
|> Enum.map(&generate_api(&1, Keyword.get(opts, :docs, true)))
end
defp generate_api(method, docs) when method in [:post, :put, :patch] do
quote do
if unquote(docs) do
@doc """
Perform a #{unquote(method |> to_string |> String.upcase())} request.
See `request/1` or `request/2` for options definition.
Example
myclient |> ExampleApi.#{unquote(method)}("/users", %{name: "Jon"}, query: [scope: "admin"])
"""
@spec unquote(method)(Tesla.Env.client(), Tesla.Env.url(), Tesla.Env.body(), [option]) ::
Tesla.Env.t()
else
@doc false
end
def unquote(method)(%Tesla.Client{} = client, url, body, options) when is_list(options) do
request(client, [method: unquote(method), url: url, body: body] ++ options)
end
# fallback to keep backward compatibility
def unquote(method)(fun, url, body, options) when is_function(fun) and is_list(options) do
unquote(method)(%Tesla.Client{fun: fun}, url, body, options)
end
if unquote(docs) do
@doc """
Perform a #{unquote(method |> to_string |> String.upcase())} request.
See `request/1` or `request/2` for options definition.
Example
myclient |> ExampleApi.#{unquote(method)}("/users", %{name: "Jon"})
ExampleApi.#{unquote(method)}("/users", %{name: "Jon"}, query: [scope: "admin"])
"""
@spec unquote(method)(Tesla.Env.client(), Tesla.Env.url(), Tesla.Env.body()) ::
Tesla.Env.t()
else
@doc false
end
def unquote(method)(%Tesla.Client{} = client, url, body) do
request(client, method: unquote(method), url: url, body: body)
end
# fallback to keep backward compatibility
def unquote(method)(fun, url, body) when is_function(fun) do
unquote(method)(%Tesla.Client{fun: fun}, url, body)
end
if unquote(docs) do
@spec unquote(method)(Tesla.Env.url(), Tesla.Env.body(), [option]) :: Tesla.Env.t()
else
@doc false
end
def unquote(method)(url, body, options) when is_list(options) do
request([method: unquote(method), url: url, body: body] ++ options)
end
if unquote(docs) do
@doc """
Perform a #{unquote(method |> to_string |> String.upcase())} request.
See `request/1` or `request/2` for options definition.
Example
ExampleApi.#{unquote(method)}("/users", %{name: "Jon"})
"""
@spec unquote(method)(Tesla.Env.url(), Tesla.Env.body()) :: Tesla.Env.t()
else
@doc false
end
def unquote(method)(url, body) do
request(method: unquote(method), url: url, body: body)
end
end
end
defp generate_api(method, docs) when method in [:head, :get, :delete, :trace, :options] do
quote do
if unquote(docs) do
@doc """
Perform a #{unquote(method |> to_string |> String.upcase())} request.
See `request/1` or `request/2` for options definition.
Example
myclient |> ExampleApi.#{unquote(method)}("/users", query: [page: 1])
"""
@spec unquote(method)(Tesla.Env.client(), Tesla.Env.url(), [option]) :: Tesla.Env.t()
else
@doc false
end
def unquote(method)(%Tesla.Client{} = client, url, options) when is_list(options) do
request(client, [method: unquote(method), url: url] ++ options)
end
# fallback to keep backward compatibility
def unquote(method)(fun, url, options) when is_function(fun) and is_list(options) do
unquote(method)(%Tesla.Client{fun: fun}, url, options)
end
if unquote(docs) do
@doc """
Perform a #{unquote(method |> to_string |> String.upcase())} request.
See `request/1` or `request/2` for options definition.
Example
myclient |> ExampleApi.#{unquote(method)}("/users")
ExampleApi.#{unquote(method)}("/users", query: [page: 1])
"""
@spec unquote(method)(Tesla.Env.client(), Tesla.Env.url()) :: Tesla.Env.t()
else
@doc false
end
def unquote(method)(%Tesla.Client{} = client, url) do
request(client, method: unquote(method), url: url)
end
# fallback to keep backward compatibility
def unquote(method)(fun, url) when is_function(fun) do
unquote(method)(%Tesla.Client{fun: fun}, url)
end
if unquote(docs) do
@spec unquote(method)(Tesla.Env.url(), [option]) :: Tesla.Env.t()
else
@doc false
end
def unquote(method)(url, options) when is_list(options) do
request([method: unquote(method), url: url] ++ options)
end
if unquote(docs) do
@doc """
Perform a #{unquote(method |> to_string |> String.upcase())} request.
See `request/1` or `request/2` for options definition.
Example
ExampleApi.#{unquote(method)}("/users")
"""
@spec unquote(method)(Tesla.Env.url()) :: Tesla.Env.t()
else
@doc false
end
def unquote(method)(url) do
request(method: unquote(method), url: url)
end
end
end
defmacro __before_compile__(env) do
adapter =
env.module
|> Module.get_attribute(:__adapter__)
|> prepare()
middleware =
env.module
|> Module.get_attribute(:__middleware__)
|> Enum.reverse()
|> prepare()
quote do
def __middleware__, do: unquote(middleware)
def __adapter__, do: unquote(adapter)
end
end
defmacro client(pre, post) do
quote do
%Tesla.Client{
pre: unquote(prepare(pre)),
post: unquote(prepare(post))
}
end
end
defp prepare(list) when is_list(list), do: Enum.map(list, &prepare/1)
defp prepare(nil), do: nil
defp prepare({{:fn, _, _} = fun, nil}), do: {:fn, fun}
- defp prepare({{:__aliases__, _, _} = name, opts}), do: quote do: {unquote(name), :call, [unquote(opts)]}
- defp prepare({name, nil}) when is_atom(name), do: quote do: {__MODULE__, unquote(name), []}
+
+ defp prepare({{:__aliases__, _, _} = name, opts}),
+ do: quote(do: {unquote(name), :call, [unquote(opts)]})
+
+ defp prepare({name, nil}) when is_atom(name), do: quote(do: {__MODULE__, unquote(name), []})
defp prepare(name), do: prepare({name, nil})
end
diff --git a/lib/tesla/middleware/digest_auth.ex b/lib/tesla/middleware/digest_auth.ex
index 1b0364c..109f3cc 100644
--- a/lib/tesla/middleware/digest_auth.ex
+++ b/lib/tesla/middleware/digest_auth.ex
@@ -1,124 +1,126 @@
defmodule Tesla.Middleware.DigestAuth do
@behaviour Tesla.Middleware
@moduledoc """
Digest access authentication middleware
[Wiki on the topic](https://en.wikipedia.org/wiki/Digest_access_authentication)
**NOTE**: Currently the implementation is incomplete and works only for MD5 algorithm
and auth qop.
### Example
```
defmodule MyClient do
use Tesla
def client(username, password, opts \\ %{}) do
Tesla.build_client [
{Tesla.Middleware.DigestAuth, Map.merge(%{username: username, password: password}, opts)}
]
end
end
```
### Options
- `:username` - username (defaults to `""`)
- `:password` - password (defaults to `""`)
- `:cnonce_fn` - custom function generating client nonce (defaults to `&Tesla.Middleware.DigestAuth.cnonce/0`)
- `:nc` - nonce counter (defaults to `"00000000"`)
"""
def call(env, next, opts) do
if env.opts && Keyword.get(env.opts, :digest_auth_handshake) do
Tesla.run(env, next)
else
opts = opts || %{}
env
|> Tesla.put_headers(authorization_header(env, opts))
|> Tesla.run(next)
end
end
defp authorization_header(env, opts) do
env
|> authorization_vars(opts)
|> calculated_authorization_values
|> create_header
end
defp authorization_vars(env, opts) do
unauthorized_response =
env.__module__.get(
env.__client__,
env.url,
opts: Keyword.put(env.opts || [], :digest_auth_handshake, true)
)
%{
username: opts[:username] || "",
password: opts[:password] || "",
path: URI.parse(env.url).path,
- auth: Tesla.get_header(unauthorized_response, "www-authenticate") |> parse_www_authenticate_header,
+ auth:
+ Tesla.get_header(unauthorized_response, "www-authenticate")
+ |> parse_www_authenticate_header,
method: env.method |> to_string |> String.upcase(),
client_nonce: (opts[:cnonce_fn] || &cnonce/0).(),
nc: opts[:nc] || "00000000"
}
end
defp calculated_authorization_values(%{auth: auth}) when auth == %{}, do: []
defp calculated_authorization_values(auth_vars) do
[
{"username", auth_vars.username},
{"realm", auth_vars.auth["realm"]},
{"uri", auth_vars[:path]},
{"nonce", auth_vars.auth["nonce"]},
{"nc", auth_vars.nc},
{"cnonce", auth_vars.client_nonce},
{"response", response(auth_vars)},
# hard-coded, will not work for MD5-sess
{"algorithm", "MD5"},
# hard-coded, will not work for auth-int or unspecified
{"qop", "auth"}
]
end
defp single_header_val({k, v}) when k in ~w(nc qop algorithm), do: "#{k}=#{v}"
defp single_header_val({k, v}), do: "#{k}=\"#{v}\""
defp create_header([]), do: []
defp create_header(calculated_authorization_values) do
vals =
calculated_authorization_values
|> Enum.reduce([], fn val, acc -> [single_header_val(val) | acc] end)
|> Enum.join(", ")
[{"authorization", "Digest #{vals}"}]
end
defp ha1(%{username: username, auth: %{"realm" => realm}, password: password}) do
md5("#{username}:#{realm}:#{password}")
end
defp ha2(%{method: method, path: path}) do
md5("#{method}:#{path}")
end
defp response(%{auth: %{"nonce" => nonce}, nc: nc, client_nonce: client_nonce} = auth_vars) do
md5("#{ha1(auth_vars)}:#{nonce}:#{nc}:#{client_nonce}:auth:#{ha2(auth_vars)}")
end
defp parse_www_authenticate_header(nil), do: %{}
defp parse_www_authenticate_header(header) do
Regex.scan(~r/(\w+?)="(.+?)"/, header)
|> Enum.reduce(%{}, fn [_, key, val], acc -> Map.merge(acc, %{key => val}) end)
end
defp md5(data), do: Base.encode16(:erlang.md5(data), case: :lower)
defp cnonce, do: :crypto.strong_rand_bytes(4) |> Base.encode16(case: :lower)
end
diff --git a/test/tesla/builder_test.exs b/test/tesla/builder_test.exs
index 2f6f15c..4478ee5 100644
--- a/test/tesla/builder_test.exs
+++ b/test/tesla/builder_test.exs
@@ -1,105 +1,105 @@
defmodule Tesla.BuilderTest do
use ExUnit.Case
alias Tesla.Builder
describe "Compilation" do
defmodule TestClientPlug do
use Tesla.Builder
@attr "value"
plug FirstMiddleware, @attr
plug SecondMiddleware, options: :are, fun: 1
plug ThirdMiddleware
plug :local_middleware
plug fn env, _next -> env end
end
defmodule TestClientModule do
use Tesla.Builder
adapter TheAdapter, hello: "world"
end
defmodule TestClientFunction do
use Tesla.Builder
adapter :local_adapter
end
defmodule TestClientAnon do
use Tesla.Builder
adapter fn env -> env end
end
-
test "generate __middleware__/0" do
assert [
- {FirstMiddleware, :call, ["value"]},
- {SecondMiddleware, :call, [[options: :are, fun: 1]]},
- {ThirdMiddleware, :call, [nil]},
- {TestClientPlug, :local_middleware, []},
- {:fn, fun}
- ] = TestClientPlug.__middleware__
+ {FirstMiddleware, :call, ["value"]},
+ {SecondMiddleware, :call, [[options: :are, fun: 1]]},
+ {ThirdMiddleware, :call, [nil]},
+ {TestClientPlug, :local_middleware, []},
+ {:fn, fun}
+ ] = TestClientPlug.__middleware__()
+
assert is_function(fun)
end
test "generate __adapter__/0 - adapter not set" do
- assert TestClientPlug.__adapter__ == nil
+ assert TestClientPlug.__adapter__() == nil
end
test "generate __adapter__/0 - adapter as module" do
- assert TestClientModule.__adapter__ == {TheAdapter, :call, [[hello: "world"]]}
+ assert TestClientModule.__adapter__() == {TheAdapter, :call, [[hello: "world"]]}
end
test "generate __adapter__/0 - adapter as module function" do
- assert TestClientFunction.__adapter__ == {TestClientFunction, :local_adapter, []}
+ assert TestClientFunction.__adapter__() == {TestClientFunction, :local_adapter, []}
end
test "generate __adapter__/0 - adapter as anonymous function" do
- assert {:fn, fun} = TestClientAnon.__adapter__
+ assert {:fn, fun} = TestClientAnon.__adapter__()
assert is_function(fun)
end
end
describe ":only/:except options" do
defmodule OnlyGetClient do
use Builder, only: [:get]
end
defmodule ExceptDeleteClient do
use Builder, except: ~w(delete)a
end
@http_verbs ~w(head get delete trace options post put patch)a
test "limit generated functions (only)" do
functions = OnlyGetClient.__info__(:functions) |> Keyword.keys() |> Enum.uniq()
assert :get in functions
refute Enum.any?(@http_verbs -- [:get], &(&1 in functions))
end
test "limit generated functions (except)" do
functions = ExceptDeleteClient.__info__(:functions) |> Keyword.keys() |> Enum.uniq()
refute :delete in functions
assert Enum.all?(@http_verbs -- [:delete], &(&1 in functions))
end
end
describe ":docs option" do
# Code.get_docs/2 requires .beam file of given module to exist in file system
# See test/support/docs.ex file for definitions of TeslaDocsTest.* modules
test "generate docs by default" do
docs = Code.get_docs(TeslaDocsTest.Default, :docs)
assert {_, _, _, _, doc} = Enum.find(docs, &match?({{:get, 1}, _, :def, _, _}, &1))
assert doc != false
end
test "do not generate docs for HTTP methods when docs: false" do
docs = Code.get_docs(TeslaDocsTest.NoDocs, :docs)
assert {_, _, _, _, false} = Enum.find(docs, &match?({{:get, 1}, _, :def, _, _}, &1))
assert {_, _, _, _, doc} = Enum.find(docs, &match?({{:custom, 1}, _, :def, _, _}, &1))
assert doc =~ ~r/something/
end
end
end
diff --git a/test/tesla/middleware/digest_auth_test.exs b/test/tesla/middleware/digest_auth_test.exs
index 96dd84f..15f2a0e 100644
--- a/test/tesla/middleware/digest_auth_test.exs
+++ b/test/tesla/middleware/digest_auth_test.exs
@@ -1,99 +1,103 @@
defmodule Tesla.Middleware.DigestAuthTest do
use ExUnit.Case, async: false
defmodule DigestClient do
use Tesla
adapter fn env ->
cond do
env.url == "/no-digest-auth" ->
env
Tesla.get_header(env, "authorization") ->
env
true ->
Tesla.put_headers(env, [
{"www-authenticate",
"""
Digest realm="testrealm@host.com",
qop="auth,auth-int",
nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
opaque="5ccc069c403ebaf9f0171e9517f40e41"
"""}
])
end
end
def client(username, password, opts \\ %{}) do
Tesla.build_client([
{
Tesla.Middleware.DigestAuth,
Map.merge(
%{
username: username,
password: password,
cnonce_fn: fn -> "0a4f113b" end,
nc: "00000001"
},
opts
)
}
])
end
end
defmodule DigestClientWithDefaults do
use Tesla
def client do
Tesla.build_client([
{Tesla.Middleware.DigestAuth, nil}
])
end
end
test "sends request with proper authorization header" do
request =
DigestClient.client("Mufasa", "Circle Of Life") |> DigestClient.get("/dir/index.html")
auth_header = Tesla.get_header(request, "authorization")
assert auth_header =~ ~r/^Digest /
assert auth_header =~ "username=\"Mufasa\""
assert auth_header =~ "realm=\"testrealm@host.com\""
assert auth_header =~ "algorithm=MD5"
assert auth_header =~ "qop=auth"
assert auth_header =~ "uri=\"/dir/index.html\""
assert auth_header =~ "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\""
assert auth_header =~ "nc=00000001"
assert auth_header =~ "cnonce=\"0a4f113b\""
assert auth_header =~ "response=\"6629fae49393a05397450978507c4ef1\""
end
test "has default values for username and cn" do
response = DigestClientWithDefaults.client() |> DigestClient.get("/")
auth_header = Tesla.get_header(response, "authorization")
assert auth_header =~ "username=\"\""
assert auth_header =~ "nc=00000000"
end
test "generates different cnonce with each request by default" do
request = fn -> DigestClientWithDefaults.client() |> DigestClient.get("/") end
- cnonce_1 = Regex.run(~r/cnonce="(.*?)"/, Tesla.get_header(request.(), "authorization")) |> Enum.at(1)
- cnonce_2 = Regex.run(~r/cnonce="(.*?)"/, Tesla.get_header(request.(), "authorization")) |> Enum.at(1)
+
+ cnonce_1 =
+ Regex.run(~r/cnonce="(.*?)"/, Tesla.get_header(request.(), "authorization")) |> Enum.at(1)
+
+ cnonce_2 =
+ Regex.run(~r/cnonce="(.*?)"/, Tesla.get_header(request.(), "authorization")) |> Enum.at(1)
assert cnonce_1 != cnonce_2
end
test "works when passing custom opts" do
request = DigestClientWithDefaults.client() |> DigestClient.get("/", opts: [hodor: "hodor"])
assert request.opts == [hodor: "hodor"]
end
test "ignores digest auth when server doesn't respond with www-authenticate header" do
response = DigestClientWithDefaults.client() |> DigestClient.get("/no-digest-auth")
refute Tesla.get_header(response, "authorization")
end
end
diff --git a/test/tesla/middleware/fuse_test.exs b/test/tesla/middleware/fuse_test.exs
index a1f2da8..54dc45d 100644
--- a/test/tesla/middleware/fuse_test.exs
+++ b/test/tesla/middleware/fuse_test.exs
@@ -1,47 +1,50 @@
defmodule Tesla.Middleware.FuseTest do
use ExUnit.Case, async: false
defmodule Client do
use Tesla
plug Tesla.Middleware.Fuse
plug :report
def report(env, next) do
send(self(), :request_made)
Tesla.run(env, next)
end
adapter fn env ->
case env.url do
- "/ok" -> env
- "/unavailable" -> raise %Tesla.Error{message: "adapter error: :econnrefused}", reason: :econnrefused}
+ "/ok" ->
+ env
+
+ "/unavailable" ->
+ raise %Tesla.Error{message: "adapter error: :econnrefused}", reason: :econnrefused}
end
end
end
setup do
Application.ensure_all_started(:fuse)
:fuse.reset(Client)
:ok
end
test "regular endpoint" do
assert %Tesla.Env{url: "/ok"} = Client.get("/ok")
end
test "unavailable endpoint" do
assert {:error, :unavailable} = Client.get("/unavailable")
assert_receive :request_made
assert {:error, :unavailable} = Client.get("/unavailable")
assert_receive :request_made
assert {:error, :unavailable} = Client.get("/unavailable")
assert_receive :request_made
assert {:error, :unavailable} = Client.get("/unavailable")
refute_receive :request_made
assert {:error, :unavailable} = Client.get("/unavailable")
refute_receive :request_made
end
end
diff --git a/test/tesla/middleware/retry_test.exs b/test/tesla/middleware/retry_test.exs
index 05d6d76..5f0693c 100644
--- a/test/tesla/middleware/retry_test.exs
+++ b/test/tesla/middleware/retry_test.exs
@@ -1,62 +1,65 @@
defmodule Tesla.Middleware.RetryTest do
use ExUnit.Case, async: false
defmodule LaggyAdapter do
def start_link, do: Agent.start_link(fn -> 0 end, name: __MODULE__)
def call(env, _opts) do
Agent.get_and_update(__MODULE__, fn retries ->
response =
case env.url do
"/ok" -> env
"/maybe" when retries < 5 -> {:error, :econnrefused}
"/maybe" -> env
"/nope" -> {:error, :econnrefused}
end
{response, retries + 1}
end)
|> case do
- {:error, :econnrefused} -> raise %Tesla.Error{message: "adapter error: :econnrefused}", reason: :econnrefused}
- env -> env
+ {:error, :econnrefused} ->
+ raise %Tesla.Error{message: "adapter error: :econnrefused}", reason: :econnrefused}
+
+ env ->
+ env
end
end
end
defmodule Client do
use Tesla
plug Tesla.Middleware.Retry, delay: 10, max_retries: 10
adapter LaggyAdapter
end
setup do
{:ok, _} = LaggyAdapter.start_link()
:ok
end
test "pass on successful request" do
assert %Tesla.Env{url: "/ok", method: :get} = Client.get("/ok")
end
test "finally pass on laggy request" do
assert %Tesla.Env{url: "/maybe", method: :get} = Client.get("/maybe")
end
test "raise if max_retries is exceeded" do
assert_raise Tesla.Error, fn -> Client.get("/nope") end
end
defmodule DefunctClient do
use Tesla
plug Tesla.Middleware.Retry
adapter fn _ -> raise "runtime-error" end
end
test "raise in case or unexpected error" do
assert_raise RuntimeError, fn -> DefunctClient.get("/blow") end
end
end
diff --git a/test/tesla/middleware/tuples_test.exs b/test/tesla/middleware/tuples_test.exs
index d226040..e711abd 100644
--- a/test/tesla/middleware/tuples_test.exs
+++ b/test/tesla/middleware/tuples_test.exs
@@ -1,47 +1,54 @@
defmodule Tesla.Middleware.TuplesTest do
use ExUnit.Case, async: false
defmodule(Custom1, do: defexception(message: "Custom 1"))
defmodule(Custom2, do: defexception(message: "Custom 2"))
defmodule Client do
use Tesla
plug Tesla.Middleware.Tuples, rescue_errors: [Custom1]
plug Tesla.Middleware.JSON
adapter fn env ->
case env.url do
- "/ok" -> env
- "/econnrefused" -> raise %Tesla.Error{message: "adapter error: :econnrefused}", reason: :econnrefused}
- "/custom-1" -> raise %Custom1{}
- "/custom-2" -> raise %Custom2{}
+ "/ok" ->
+ env
+
+ "/econnrefused" ->
+ raise %Tesla.Error{message: "adapter error: :econnrefused}", reason: :econnrefused}
+
+ "/custom-1" ->
+ raise %Custom1{}
+
+ "/custom-2" ->
+ raise %Custom2{}
end
end
end
defmodule DefaultClient do
use Tesla
plug Tesla.Middleware.Tuples
adapter fn _ -> raise %Custom1{} end
end
test "return {:ok, env} for successful transaction" do
assert {:ok, %Tesla.Env{}} = Client.get("/ok")
end
test "return {:error, reason} for unsuccessful transaction" do
assert {:error, %Tesla.Error{}} = Client.get("/econnrefused")
end
test "rescue listed custom exception" do
assert {:error, %Custom1{}} = Client.get("/custom-1")
end
test "do not rescue not-listed custom exception" do
assert catch_error(Client.get("/custom-2"))
assert catch_error(DefaultClient.get("/"))
end
end
diff --git a/test/tesla/mock_test.exs b/test/tesla/mock_test.exs
index f8b5533..7b08869 100644
--- a/test/tesla/mock_test.exs
+++ b/test/tesla/mock_test.exs
@@ -1,42 +1,45 @@
defmodule Tesla.MockTest do
use ExUnit.Case
defp setup_mock(_) do
Tesla.Mock.mock(fn
- %{method: :get, url: "http://example.com/list"} -> %Tesla.Env{status: 200, body: "hello"}
- %{method: :post, url: "http://example.com/create", body: ~s({"some":"data"})} -> {201, [{"content-type", "application/json"}], ~s"{\"id\":42}"}
+ %{method: :get, url: "http://example.com/list"} ->
+ %Tesla.Env{status: 200, body: "hello"}
+
+ %{method: :post, url: "http://example.com/create", body: ~s({"some":"data"})} ->
+ {201, [{"content-type", "application/json"}], ~s"{\"id\":42}"}
end)
:ok
end
describe "with mock" do
setup :setup_mock
test "mock get request" do
assert %Tesla.Env{} = env = MockClient.list()
assert env.status == 200
assert env.body == "hello"
end
test "raise on unmocked request" do
assert_raise Tesla.Mock.Error, fn ->
MockClient.search()
end
end
test "mock post request" do
assert %Tesla.Env{} = env = MockClient.create(%{"some" => "data"})
assert env.status == 201
assert env.body == %{"id" => 42}
end
end
describe "without mock" do
test "raise on unmocked request" do
assert_raise Tesla.Mock.Error, fn ->
MockClient.search()
end
end
end
end
diff --git a/test/tesla_test.exs b/test/tesla_test.exs
index 0f0dff1..3d6e40f 100644
--- a/test/tesla_test.exs
+++ b/test/tesla_test.exs
@@ -1,298 +1,298 @@
defmodule TeslaTest do
use ExUnit.Case
require Tesla
@url "http://localhost:#{Application.get_env(:httparrot, :http_port)}"
describe "Adapters" do
defmodule ModuleAdapter do
def call(env, opts \\ []) do
Map.put(env, :url, env.url <> "/module/" <> opts[:with])
end
end
defmodule EmptyClient do
use Tesla
end
defmodule ModuleAdapterClient do
use Tesla
adapter ModuleAdapter, with: "someopt"
end
defmodule LocalAdapterClient do
use Tesla
adapter :local_adapter
def local_adapter(env) do
Map.put(env, :url, env.url <> "/local")
end
end
defmodule FunAdapterClient do
use Tesla
adapter fn env ->
Map.put(env, :url, env.url <> "/anon")
end
end
setup do
# clean config
Application.delete_env(:tesla, EmptyClient)
Application.delete_env(:tesla, ModuleAdapterClient)
:ok
end
test "defauilt adapter" do
assert Tesla.effective_adapter(EmptyClient) == {Tesla.Adapter.Httpc, :call, [[]]}
end
test "execute module adapter" do
response = ModuleAdapterClient.request(url: "test")
assert response.url == "test/module/someopt"
end
test "execute local function adapter" do
response = LocalAdapterClient.request(url: "test")
assert response.url == "test/local"
end
test "execute anonymous function adapter" do
response = FunAdapterClient.request(url: "test")
assert response.url == "test/anon"
end
test "use adapter override from config" do
Application.put_env(:tesla, EmptyClient, adapter: Tesla.Mock)
assert Tesla.effective_adapter(EmptyClient) == {Tesla.Mock, :call, [[]]}
end
test "prefer config over module setting" do
Application.put_env(:tesla, ModuleAdapterClient, adapter: Tesla.Mock)
assert Tesla.effective_adapter(ModuleAdapterClient) == {Tesla.Mock, :call, [[]]}
end
end
describe "Middleware" do
defmodule AppendOne do
@behaviour Tesla.Middleware
def call(env, next, _opts) do
env
|> Map.put(:url, "#{env.url}/1")
|> Tesla.run(next)
end
end
defmodule AppendWith do
@behaviour Tesla.Middleware
def call(env, next, opts) do
env
|> Map.update!(:url, fn url -> url <> "/MB" <> opts[:with] end)
|> Tesla.run(next)
|> Map.update!(:url, fn url -> url <> "/MA" <> opts[:with] end)
end
end
defmodule AppendClient do
use Tesla
plug AppendOne
plug AppendWith, with: "1"
plug AppendWith, with: "2"
plug :local_middleware
adapter fn env -> env end
def local_middleware(env, next) do
env
|> Map.update!(:url, fn url -> url <> "/LB" end)
|> Tesla.run(next)
|> Map.update!(:url, fn url -> url <> "/LA" end)
end
end
test "execute middleware top down" do
response = AppendClient.get("one")
assert response.url == "one/1/MB1/MB2/LB/LA/MA2/MA1"
end
end
describe "Dynamic client" do
defmodule DynamicClient do
use Tesla
adapter fn env ->
if String.ends_with?(env.url, "/cached") do
%{env | body: "cached", status: 304}
else
Tesla.run_default_adapter(env)
end
end
def help(client \\ %Tesla.Client{}) do
get(client, "/help")
end
end
test "override adapter - Tesla.build_client" do
client =
Tesla.build_client([], [
fn env, _next ->
%{env | body: "new"}
end
])
assert %{body: "new"} = DynamicClient.help(client)
end
test "override adapter - Tesla.build_adapter" do
client =
Tesla.build_adapter(fn env ->
%{env | body: "new"}
end)
assert %{body: "new"} = DynamicClient.help(client)
end
test "statically override adapter" do
assert %{status: 200} = DynamicClient.get(@url <> "/ip")
assert %{status: 304} = DynamicClient.get(@url <> "/cached")
end
end
describe "request API" do
defmodule SimpleClient do
use Tesla
adapter fn env ->
env
end
end
test "basic request" do
response = SimpleClient.request(url: "/", method: :post, query: [page: 1], body: "data")
assert response.method == :post
assert response.url == "/"
assert response.query == [page: 1]
assert response.body == "data"
end
test "shortcut function" do
response = SimpleClient.get("/get")
assert response.method == :get
assert response.url == "/get"
end
test "shortcut function with body" do
response = SimpleClient.post("/post", "some-data")
assert response.method == :post
assert response.url == "/post"
assert response.body == "some-data"
end
test "request with client" do
client = fn env, next ->
env
|> Map.put(:url, "/prefix" <> env.url)
|> Tesla.run(next)
end
response = SimpleClient.get("/")
assert response.url == "/"
- refute response.__client__
+ assert response.__client__ == %Tesla.Client{}
response = client |> SimpleClient.get("/")
assert response.url == "/prefix/"
assert response.__client__ == %Tesla.Client{fun: client}
end
test "better errors when given nil opts" do
assert_raise FunctionClauseError, fn ->
Tesla.get("/", nil)
end
end
end
alias Tesla.Env
import Tesla
describe "get_header/2" do
test "non existing header" do
env = %Env{headers: [{"server", "Cowboy"}]}
assert get_header(env, "some-key") == nil
end
test "existing header" do
env = %Env{headers: [{"server", "Cowboy"}]}
assert get_header(env, "server") == "Cowboy"
end
test "first of multiple headers with the same name" do
env = %Env{headers: [{"cookie", "chocolate"}, {"cookie", "biscuits"}]}
assert get_header(env, "cookie") == "chocolate"
end
end
describe "get_headers/2" do
test "none matching" do
env = %Env{headers: [{"server", "Cowboy"}]}
assert get_headers(env, "cookie") == []
end
test "multiple matches matching" do
env = %Env{headers: [{"cookie", "chocolate"}, {"cookie", "biscuits"}]}
assert get_headers(env, "cookie") == ["chocolate", "biscuits"]
end
end
describe "put_header/3" do
test "add new header" do
env = %Env{}
env = put_header(env, "server", "Cowboy")
assert get_header(env, "server") == "Cowboy"
end
test "override existing header" do
env = %Env{headers: [{"server", "Cowboy"}]}
env = put_header(env, "server", "nginx")
assert get_header(env, "server") == "nginx"
end
end
describe "put_headers/2" do
test "add headers to env existing header" do
env = %Env{}
assert get_header(env, "server") == nil
env = Tesla.put_headers(env, [{"server", "Cowboy"}, {"content-length", "100"}])
assert get_header(env, "server") == "Cowboy"
assert get_header(env, "content-length") == "100"
env = Tesla.put_headers(env, [{"server", "nginx"}, {"content-type", "text/plain"}])
assert get_header(env, "server") == "Cowboy"
assert get_header(env, "content-length") == "100"
assert get_header(env, "content-type") == "text/plain"
end
test "add multiple headers with the same name" do
env = %Env{}
env = Tesla.put_headers(env, [{"cookie", "chocolate"}, {"cookie", "biscuits"}])
assert get_headers(env, "cookie") == ["chocolate", "biscuits"]
end
end
describe "delete_header/2" do
test "delete all headers with given name" do
env = %Env{headers: [{"cookie", "chocolate"}, {"server", "Cowboy"}, {"cookie", "biscuits"}]}
env = delete_header(env, "cookie")
assert get_header(env, "cookie") == nil
assert get_header(env, "server") == "Cowboy"
end
end
end

File Metadata

Mime Type
text/x-diff
Expires
Mon, Nov 25, 3:07 PM (1 d, 6 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
39938
Default Alt Text
(47 KB)

Event Timeline