Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F112947
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
37 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/tesla.ex b/lib/tesla.ex
index f5c06db..86e681c 100644
--- a/lib/tesla.ex
+++ b/lib/tesla.ex
@@ -1,536 +1,549 @@
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__
}
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.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.perform_request(__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.perform_request(__MODULE__, 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
opts = Macro.escape(opts)
quote do: @__middleware__({unquote(middleware), unquote(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({:fn, _, _} = adapter) do
adapter = Macro.escape(adapter)
quote do: @__adapter__(unquote(adapter))
end
defmacro adapter(adapter, opts \\ nil) do
quote do: @__adapter__({unquote(adapter), unquote(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.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 = Module.get_attribute(env.module, :__adapter__)
middleware = Module.get_attribute(env.module, :__middleware__) |> Enum.reverse()
quote do
def __middleware__, do: unquote(middleware)
def __adapter__, do: Tesla.adapter(__MODULE__, unquote(adapter))
end
end
end
defmodule Tesla do
use Tesla.Builder
@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
def perform_request(module, client \\ nil, options) do
%{fun: fun, pre: pre, post: post} = client || %Tesla.Client{}
stack =
pre ++
prepare(module, List.wrap(fun) ++ module.__middleware__ ++ default_middleware()) ++
post ++ prepare(module, [module.__adapter__])
env = struct(Tesla.Env, options ++ [__module__: module, __client__: client])
run(env, stack)
end
@spec prepare(atom, [any]) :: Tesla.Env.stack()
def prepare(module, stack) do
Enum.map(stack, fn
{name, opts} -> prepare_module(module, name, opts)
name when is_atom(name) -> prepare_module(module, name, nil)
fun when is_function(fun) -> {:fn, fun}
end)
end
defp prepare_module(module, name, opts) do
case Atom.to_charlist(name) do
~c"Elixir." ++ _ -> {name, :call, [opts]}
_ -> {module, name}
end
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}]), do: apply(m, 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} | rest]), do: apply(m, 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
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
+ end
+
+ @spec put_header(Tesla.Env.t, binary, binary) :: Tesla.Env.t
+ def put_header(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
- headers =
- Enum.reduce(list, env.headers, fn {k, v}, h ->
- k = String.downcase(k)
- List.keystore(h, k, 0, {k, v})
- end)
+ %{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}
%{env | headers: headers}
end
def adapter(module, custom) do
module_adapter_from_config(module) || custom || default_adapter()
end
defp module_adapter_from_config(module) do
Application.get_env(:tesla, module, [])[:adapter]
end
def default_adapter do
Application.get_env(:tesla, :adapter, Tesla.Adapter.Httpc)
end
def run_default_adapter(env, opts \\ []) do
apply(default_adapter(), :call, [env, opts])
end
def default_middleware do
[{Tesla.Middleware.Normalize, nil}]
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
%Tesla.Client{
pre: Tesla.prepare(__MODULE__, unquote(pre)),
post: Tesla.prepare(__MODULE__, 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/middleware/basic_auth.ex b/lib/tesla/middleware/basic_auth.ex
index 5a33942..22a316b 100644
--- a/lib/tesla/middleware/basic_auth.ex
+++ b/lib/tesla/middleware/basic_auth.ex
@@ -1,60 +1,60 @@
defmodule Tesla.Middleware.BasicAuth do
@behaviour Tesla.Middleware
@moduledoc """
Basic authentication middleware
[Wiki on the topic](https://en.wikipedia.org/wiki/Basic_access_authentication)
### Example
```
defmodule MyClient do
use Tesla
# static configuration
plug Tesla.Middleware.BasicAuth, username: "user", password: "pass"
# dynamic user & pass
def new(username, password, opts \\\\ %{}) do
Tesla.build_client [
{Tesla.Middleware.BasicAuth, Map.merge(%{username: username, password: password}, opts)}
]
end
end
```
### Options
- `:username` - username (defaults to `""`)
- `:password` - password (defaults to `""`)
"""
def call(env, next, opts) do
opts = opts || %{}
env
|> Tesla.put_headers(authorization_header(opts))
|> Tesla.run(next)
end
defp authorization_header(opts) do
opts
|> authorization_vars()
|> encode()
|> create_header()
end
defp authorization_vars(opts) do
%{
username: opts[:username] || "",
password: opts[:password] || ""
}
end
defp create_header(auth) do
- [{"Authorization", "Basic #{auth}"}]
+ [{"authorization", "Basic #{auth}"}]
end
defp encode(%{username: username, password: password}) do
Base.encode64("#{username}:#{password}")
end
end
diff --git a/lib/tesla/middleware/digest_auth.ex b/lib/tesla/middleware/digest_auth.ex
index 0fc7500..1b0364c 100644
--- a/lib/tesla/middleware/digest_auth.ex
+++ b/lib/tesla/middleware/digest_auth.ex
@@ -1,124 +1,124 @@
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,
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}"}]
+ [{"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/middleware/decode_rels_test.exs b/test/tesla/middleware/decode_rels_test.exs
index 3f27cba..7e9c17d 100644
--- a/test/tesla/middleware/decode_rels_test.exs
+++ b/test/tesla/middleware/decode_rels_test.exs
@@ -1,37 +1,37 @@
defmodule Tesla.Middleware.DecodeRelsTest do
use ExUnit.Case
defmodule Client do
use Tesla
plug Tesla.Middleware.DecodeRels
adapter fn env ->
case env.url do
"/rels" ->
Tesla.put_headers(env, [
- {"Link", ~s(<https://api.github.com/resource?page=2>; rel="next",
+ {"link", ~s(<https://api.github.com/resource?page=2>; rel="next",
<https://api.github.com/resource?page=5>; rel="last")}
])
_ ->
env
end
end
end
test "deocde rels" do
env = Client.get("/rels")
assert env.opts[:rels] == %{
"next" => "https://api.github.com/resource?page=2",
"last" => "https://api.github.com/resource?page=5"
}
end
test "skip if no Link header" do
env = Client.get("/")
assert env.opts[:rels] == nil
end
end
diff --git a/test/tesla/middleware/digest_auth_test.exs b/test/tesla/middleware/digest_auth_test.exs
index c731176..96dd84f 100644
--- a/test/tesla/middleware/digest_auth_test.exs
+++ b/test/tesla/middleware/digest_auth_test.exs
@@ -1,99 +1,99 @@
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",
+ {"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)
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/header_test.exs b/test/tesla/middleware/header_test.exs
index cb3940c..1d1ee95 100644
--- a/test/tesla/middleware/header_test.exs
+++ b/test/tesla/middleware/header_test.exs
@@ -1,15 +1,15 @@
defmodule Tesla.Middleware.HeadersTest do
use ExUnit.Case
alias Tesla.Env
@middleware Tesla.Middleware.Headers
test "merge headers" do
env =
@middleware.call(%Env{headers: [{"authorization", "secret"}]}, [], [
- {"Content-Type", "text/plain"}
+ {"content-type", "text/plain"}
])
assert env.headers == [{"authorization", "secret"}, {"content-type", "text/plain"}]
end
end
diff --git a/test/tesla_test.exs b/test/tesla_test.exs
index f3a9c29..45fdb21 100644
--- a/test/tesla_test.exs
+++ b/test/tesla_test.exs
@@ -1,313 +1,361 @@
defmodule TeslaTest do
use ExUnit.Case
require Tesla
@url "http://localhost:#{Application.get_env(:httparrot, :http_port)}"
describe "use Tesla options" do
defmodule OnlyGetClient do
use Tesla, only: [:get]
end
defmodule ExceptDeleteClient do
use Tesla.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" 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
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 EmptyClient.__adapter__() == Tesla.default_adapter()
end
test "adapter as module" do
assert ModuleAdapterClient.__adapter__() == {ModuleAdapter, [with: "someopt"]}
end
test "adapter as local function" do
assert LocalAdapterClient.__adapter__() == {:local_adapter, nil}
end
test "adapter as anonymous function" do
assert is_function(FunAdapterClient.__adapter__())
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 EmptyClient.__adapter__() == Tesla.Mock
end
test "prefer config over module setting" do
Application.put_env(:tesla, ModuleAdapterClient, adapter: Tesla.Mock)
assert ModuleAdapterClient.__adapter__() == Tesla.Mock
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 "middleware list" do
assert AppendClient.__middleware__() == [
{AppendOne, nil},
{AppendWith, [with: "1"]},
{AppendWith, [with: "2"]},
{:local_middleware, nil}
]
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__
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
- assert get_header(%Env{}, "some-key") == nil
+ env = %Env{headers: [{"server", "Cowboy"}]}
+ assert get_header(env, "some-key") == nil
end
- test "fetch existing header" do
- assert get_header(%Env{headers: [{"server", "Cowboy"}]}, "server") == "Cowboy"
+ 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") == "nginx"
+ 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
Sun, Nov 24, 3:08 AM (17 h, 30 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
39311
Default Alt Text
(37 KB)
Attached To
Mode
R28 tesla
Attached
Detach File
Event Timeline
Log In to Comment