Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F113886
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
47 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Nov 25, 3:07 PM (1 d, 8 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
39938
Default Alt Text
(47 KB)
Attached To
Mode
R28 tesla
Attached
Detach File
Event Timeline
Log In to Comment