Skip to content

Commit

Permalink
Fixes SparkPost#12:
Browse files Browse the repository at this point in the history
* Switch to HTTPoison dependency
* Refactor tests to use HTTPoison
  • Loading branch information
DavidAntaramian committed Apr 15, 2016
1 parent 50d4a19 commit 784898f
Show file tree
Hide file tree
Showing 9 changed files with 96 additions and 98 deletions.
5 changes: 3 additions & 2 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
# and its dependencies with the aid of the Mix.Config module.
use Mix.Config

config :sparkpost, api_endpoint: "https://api.sparkpost.com/api/v1/"
config :sparkpost, api_key: "YOUR API KEY HERE"
config :sparkpost,
api_endpoint: "https://api.sparkpost.com/api/v1/",
api_key: "YOUR API KEY HERE"

# This configuration is loaded before any dependency and is restricted
# to this project. If another project depends on this project, this
Expand Down
76 changes: 44 additions & 32 deletions lib/endpoint.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,19 @@ defmodule SparkPost.Endpoint do
Make a request to the SparkPost API.
## Parameters
- method: HTTP request method as atom (:get, :post, ...)
- endpoint: SparkPost API endpoint as string ("transmissions", "templates", ...)
- options: keyword of optional elements including:
- :params: keyword of query parameters
- :body: request body (string)
- `method`: HTTP 1.1 request method as an atom:
- `:delete`
- `:get`
- `:head`
- `:options`
- `:patch`
- `:post`
- `:put`
- `endpoint`: SparkPost API endpoint as string ("transmissions", "templates", ...)
- `body`: A Map that will be encoded to JSON to be sent as the body of the request (defaults to empty)
- `headers`: A Map of headers of the form %{"Header-Name" => "Value"} to be sent with the request
- `options`: A Keyword list of optional elements including:
- `:params`: A Keyword list of query parameters
## Example
List transmissions for the "ElixirRox" campaign:
Expand All @@ -24,32 +32,20 @@ defmodule SparkPost.Endpoint do
"id" => "102258558346809186", "name" => "102258558346809186",
"state" => "Success"}, ...], status_code: 200}
"""
def request(method, endpoint, options) do
url = if Keyword.has_key?(options, :params) do
Application.get_env(:sparkpost, :api_endpoint, @default_endpoint) <> endpoint
<> "?" <> URI.encode_query(options[:params])
else
Application.get_env(:sparkpost, :api_endpoint, @default_endpoint) <> endpoint
end
def request(method, endpoint, body \\ %{}, headers \\ %{}, options \\ []) do
url = Application.get_env(:sparkpost, :api_endpoint, @default_endpoint) <> endpoint

reqopts = if method in [:get, :delete] do
[ headers: base_request_headers() ]
else
[
headers: ["Content-Type": "application/json"] ++ base_request_headers(),
body: encode_request_body(options[:body])
]
end
{:ok, request_body} = encode_request_body(body)

request_headers = if method in [:get, :delete] do
headers
else
Map.merge(headers, %{"Content-Type": "application/json"})
end
|> Map.merge(base_request_headers)

%{status_code: status_code, body: json} = HTTPotion.request(method, url, reqopts)

body = decode_response_body(json)

if Map.has_key?(body, :errors) do
%SparkPost.Endpoint.Error{ status_code: status_code, errors: body.errors }
else
%SparkPost.Endpoint.Response{ status_code: status_code, results: body.results }
end
HTTPoison.request(method, url, request_body, request_headers, options)
|> handle_response
end

def marshal_response(response, struct_type, subkey\\nil)
Expand All @@ -70,16 +66,32 @@ defmodule SparkPost.Endpoint do
response
end

defp handle_response({:ok, %HTTPoison.Response{status_code: code, body: body}}) when code >= 200 and code < 300 do
decoded_body = decode_response_body(body)
%SparkPost.Endpoint.Response{status_code: 200, results: decoded_body.results}
end

defp handle_response({:ok, %HTTPoison.Response{status_code: code, body: body}}) when code >= 400 do
decoded_body = decode_response_body(body)
if Map.has_key?(decoded_body, :errors) do
%SparkPost.Endpoint.Error{status_code: code, errors: decoded_body.errors}
else
%SparkPost.Endpoint.Error{status_code: code, errors: []}
end
end

defp base_request_headers() do
{:ok, version} = :application.get_key(:sparkpost, :vsn)
[
%{
"User-Agent": "elixir-sparkpost/" <> to_string(version),
"Authorization": Application.get_env(:sparkpost, :api_key)
]
}
end

# Do not try to remove nils from an empty map
defp encode_request_body(body) when is_map(body) and map_size(body) == 0, do: {:ok, ""}
defp encode_request_body(body) do
body |> Washup.filter |> Poison.encode!
body |> Washup.filter |> Poison.encode
end

defp decode_response_body(body) do
Expand Down
2 changes: 1 addition & 1 deletion lib/mockserver.ex
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,6 @@ defmodule SparkPost.MockServer do
end

def mk_http_resp(status_code, body) do
fn (_method, _url, _opts) -> %{status_code: status_code, body: body} end
fn (_method, _url, _body, _headers, _opts) -> {:ok, %HTTPoison.Response{status_code: status_code, body: body}} end
end
end
6 changes: 3 additions & 3 deletions lib/transmission.ex
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ defmodule SparkPost.Transmission do
recipients: Recipient.to_recipient_list(body.recipients),
content: Content.to_content(body.content)
}
response = Endpoint.request(:post, "transmissions", [body: body])
response = Endpoint.request(:post, "transmissions", body)
Endpoint.marshal_response(response, Transmission.Response)
end

Expand All @@ -150,7 +150,7 @@ defmodule SparkPost.Transmission do
substitution_data: ""}
"""
def get(transid) do
response = Endpoint.request(:get, "transmissions/" <> transid, [])
response = Endpoint.request(:get, "transmissions/" <> transid)
Endpoint.marshal_response(response, __MODULE__, :transmission)
end

Expand Down Expand Up @@ -179,7 +179,7 @@ defmodule SparkPost.Transmission do
return_path: :required, state: "Success", substitution_data: nil}]
"""
def list(filters\\[]) do
response = Endpoint.request(:get, "transmissions", [params: filters])
response = Endpoint.request(:get, "transmissions", %{}, %{}, [params: filters])
case response do
%Endpoint.Response{} ->
Enum.map(response.results, fn (trans) -> struct(__MODULE__, trans) end)
Expand Down
5 changes: 2 additions & 3 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,12 @@ defmodule SparkPost.Mixfile do
end

def application do
[applications: [:httpotion]]
[applications: [:httpoison]]
end

defp deps do
[
{:ibrowse, github: "cmullaparthi/ibrowse", tag: "v4.1.2"},
{:httpotion, "~> 2.1.0"},
{:httpoison, "~> 0.8.1"},
{:poison, "~> 1.5"},
{:mock, "~> 0.1.1", only: :test},
{:excoveralls, "~> 0.4", only: :test},
Expand Down
7 changes: 3 additions & 4 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
%{"bunt": {:hex, :bunt, "0.1.4"},
%{"bunt": {:hex, :bunt, "0.1.5"},
"certifi": {:hex, :certifi, "0.3.0"},
"credo": {:hex, :credo, "0.3.0-dev"},
"credo": {:hex, :credo, "0.3.6"},
"earmark": {:hex, :earmark, "0.2.1"},
"ex_doc": {:hex, :ex_doc, "0.11.3"},
"excoveralls": {:hex, :excoveralls, "0.4.5"},
"exjsx": {:hex, :exjsx, "3.2.0"},
"hackney": {:hex, :hackney, "1.4.8"},
"httpotion": {:hex, :httpotion, "2.1.0"},
"ibrowse": {:git, "https://github.com/cmullaparthi/ibrowse.git", "ea3305d21f37eced4fac290f64b068e56df7de80", [tag: "v4.1.2"]},
"httpoison": {:hex, :httpoison, "0.8.1"},
"idna": {:hex, :idna, "1.0.3"},
"jsx": {:hex, :jsx, "2.6.2"},
"meck": {:hex, :meck, "0.8.4"},
Expand Down
53 changes: 20 additions & 33 deletions test/endpoint_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -26,39 +26,26 @@ defmodule SparkPost.EndpointTest do
end
end

test "Endpoint.request forms correct URLs" do
base_url = Application.get_env(:sparkpost, :api_endpoint)
endpt = "transmissions"
params = [campaign_id: "campaign101"]
paramstr = URI.encode_query(params)
respfn = MockServer.mk_resp
with_mock HTTPotion, [request: fn(method, url, opts) ->
assert url == base_url <> endpt <> "?" <> paramstr
respfn.(method, url, opts)
end] do
Endpoint.request(:get, "transmissions", [params: params])
end
end

test "Endpoint.request succeeds with Endpoint.Response" do
with_mock HTTPotion, [request: MockServer.mk_resp] do
Endpoint.request(:get, "transmissions", [])
with_mock HTTPoison, [request: fn(_, _, _, _, _) ->
r = MockServer.mk_resp
r.(nil, nil, nil, nil, nil)
end] do
Endpoint.request(:get, "transmissions", %{}, %{}, [])
end
end

test "Endpoint.request populates Endpoint.Response" do
status_code = 200
results = Poison.decode!(MockServer.create_json, [keys: :atoms]).results
with_mock HTTPotion, [request: MockServer.mk_resp] do
resp = %Endpoint.Response{} = Endpoint.request(
:get, "transmissions", [])

with_mock HTTPoison, [request: MockServer.mk_resp] do
resp = %Endpoint.Response{} = Endpoint.request(:get, "transmissions", %{}, %{}, [])
assert %Endpoint.Response{status_code: ^status_code, results: ^results} = resp
end
end

test "Endpoint.request fails with Endpoint.Error" do
with_mock HTTPotion, [request: MockServer.mk_fail] do
with_mock HTTPoison, [request: MockServer.mk_fail] do
%Endpoint.Error{} = Endpoint.request(
:get, "transmissions", [])
end
Expand All @@ -67,7 +54,7 @@ defmodule SparkPost.EndpointTest do
test "Endpoint.request populates Endpoint.Error" do
status_code = 400
errors = Poison.decode!(MockServer.create_fail_json, [keys: :atoms]).errors
with_mock HTTPotion, [request: MockServer.mk_fail] do
with_mock HTTPoison, [request: MockServer.mk_fail] do
resp = %Endpoint.Error{} = Endpoint.request(
:get, "transmissions", [])

Expand All @@ -77,29 +64,29 @@ defmodule SparkPost.EndpointTest do

test "Endpoint.request includes the core HTTP headers" do
respfn = MockServer.mk_resp
with_mock HTTPotion, [request: fn (method, url, opts) ->
with_mock HTTPoison, [request: fn (method, url, body, headers, opts) ->
Enum.each(Headers.for_method(method), fn {header, tester} ->
header_atom = String.to_atom(header)
assert Keyword.has_key?(opts[:headers], header_atom), "#{header} header required for #{method} requests"
assert tester.(opts[:headers][header_atom]), "Malformed header: #{header}. See Headers module in #{__ENV__.file} for formatting rules."
assert Map.has_key?(headers, header_atom), "#{header} header required for #{method} requests"
assert tester.(headers[header_atom]), "Malformed header: #{header}. See Headers module in #{__ENV__.file} for formatting rules."
end)
respfn.(method, url, opts)
respfn.(method, url, body, headers, opts)
end
] do
Enum.each([:get, :post, :put, :delete], fn method ->
Endpoint.request(method, "transmissions", []) end)
Enum.each([:get, :post, :put, :delete], fn method ->
Endpoint.request(method, "transmissions", %{}, %{}, []) end)
end
end

test "Endpoint.request includes request bodies for appropriate methods" do
respfn = MockServer.mk_resp
with_mock HTTPotion, [request: fn (method, url, opts) ->
assert opts[:body] == "{}"
respfn.(method, url, opts)
with_mock HTTPoison, [request: fn (method, url, body, headers, opts) ->
assert body == ""
respfn.(method, url, body, headers, opts)
end
] do
Endpoint.request(:post, "transmissions", [body: %{}])
Endpoint.request(:put, "transmissions", [body: %{}])
Endpoint.request(:post, "transmissions", %{}, %{}, [])
Endpoint.request(:put, "transmissions", %{}, %{}, [])
end
end
end
10 changes: 5 additions & 5 deletions test/sparkpost_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ defmodule SparkPostTest do
import Mock

test "send succeeds with a Transmission.Response" do
with_mock HTTPotion, [request: MockServer.mk_resp] do
with_mock HTTPoison, [request: MockServer.mk_resp] do
resp = SparkPost.send(
to: "[email protected]",
from: "[email protected]",
Expand All @@ -19,7 +19,7 @@ defmodule SparkPostTest do
end

test "send fails with a Endpoint.Error" do
with_mock HTTPotion, [request: MockServer.mk_fail] do
with_mock HTTPoison, [request: MockServer.mk_fail] do
resp = SparkPost.send(
to: "[email protected]",
from: "[email protected]",
Expand All @@ -37,16 +37,16 @@ defmodule SparkPostTest do
subject = "Elixir and SparkPost..."
text = "Raw text email is boring"
html = "<marquee>Rich text email is terrifying</marquee>"
with_mock HTTPotion, [request: fn (method, url, opts) ->
inreq = Poison.decode!(opts[:body], [keys: :atoms])
with_mock HTTPoison, [request: fn (method, url, body, headers, opts) ->
inreq = Poison.decode!(body, [keys: :atoms])
assert Recipient.to_recipient_list(inreq.recipients) == Recipient.to_recipient_list(to)
assert Content.to_content(inreq.content) == %Content.Inline{
from: Address.to_address(from),
subject: subject,
text: text,
html: html
}
MockServer.mk_resp.(method, url, opts)
MockServer.mk_resp.(method, url, body, headers, opts)
end] do
SparkPost.send(
to: to,
Expand Down
Loading

0 comments on commit 784898f

Please sign in to comment.