diff --git a/lib/endpoint.ex b/lib/endpoint.ex index 8f63d2b..728c7ba 100644 --- a/lib/endpoint.ex +++ b/lib/endpoint.ex @@ -36,9 +36,9 @@ defmodule SparkPost.Endpoint do url = Application.get_env(:sparkpost, :api_endpoint, @default_endpoint) <> endpoint {:ok, request_body} = encode_request_body(body) - + request_headers = if method in [:get, :delete] do - headers + headers else Map.merge(headers, %{"Content-Type": "application/json"}) end @@ -73,9 +73,9 @@ defmodule SparkPost.Endpoint do defp handle_response({:ok, %HTTPoison.Response{status_code: code, body: body}}, decode_results) when code >= 200 and code < 300 do decoded_body = decode_response_body(body) if decode_results do - %SparkPost.Endpoint.Response{status_code: 200, results: decoded_body.results} + %SparkPost.Endpoint.Response{status_code: code, results: decoded_body.results} else - %SparkPost.Endpoint.Response{status_code: 200, results: decoded_body} + %SparkPost.Endpoint.Response{status_code: code, results: decoded_body} end end @@ -86,6 +86,10 @@ defmodule SparkPost.Endpoint do end end + defp handle_response({:error, %HTTPoison.Error{reason: reason}}, _decode_results) do + %SparkPost.Endpoint.Error{status_code: nil, errors: [reason]} + end + defp base_request_headers() do {:ok, version} = :application.get_key(:sparkpost, :vsn) %{ @@ -100,6 +104,7 @@ defmodule SparkPost.Endpoint do body |> Washup.filter |> Poison.encode end + defp decode_response_body(body) when byte_size(body) == 0, do: "" defp decode_response_body(body) do # TODO: [key: :atoms] is unsafe for open-ended structures such as # metadata and substitution_data diff --git a/lib/mockserver.ex b/lib/mockserver.ex index a10c2fa..6094ccd 100644 --- a/lib/mockserver.ex +++ b/lib/mockserver.ex @@ -36,4 +36,8 @@ defmodule SparkPost.MockServer do def mk_http_resp(status_code, body) do fn (_method, _url, _body, _headers, _opts) -> {:ok, %HTTPoison.Response{status_code: status_code, body: body}} end end + + def mk_error(reason) do + fn (_method, _url, _body, _headers, _opts) -> {:error, %HTTPoison.Error{reason: reason}} end + end end diff --git a/lib/suppression_list.ex b/lib/suppression_list.ex index d17bfa5..1e158ac 100644 --- a/lib/suppression_list.ex +++ b/lib/suppression_list.ex @@ -1,17 +1,68 @@ defmodule SparkPost.SuppressionList do @moduledoc """ The SparkPost Suppression List API for working with suppression lists. - Use `SparkPost.SuppressionList.search/1` to search through your account's suppression list. + Use `SparkPost.SuppressionList.delete/1` to delete a single entry from a list, + `SparkPost.SuppressionList.upsert_one/3` to insert or update a single list entry, + or `SparkPost.SuppressionList.search/1` to search through your account's suppression list. Check out the documentation for each function or use the [SparkPost API reference](https://developers.sparkpost.com/api/suppression_list.html) for details. + Returned by `SparkPost.SuppressionList.delete/1`: + - {:ok, ""} + + Returned by `SparkPost.SuppressionList.upsert_one/3`: + - {:ok, message} (A success message string) + Returned by `SparkPost.SuppressionList.search/1`. - %SparkPost.SuppressionList.SearchResult{} """ alias SparkPost.Endpoint + @doc """ + Insert or update a single entry in the suppression list. + Returns a single string with the success message if the entry + was updated or inserted. Returns a %SparkPost.Endpoint.Error{} with a 400 + if there was an issue with the request format. + + Parameters: + - recipient: the email to insert or update in the suppression list + - type: one of "transactional" or "non_transactional" + - description (optional): optional description of this entry in the suppression list + """ + def upsert_one(recipient, type, description \\ nil) do + body = if description == nil do + %{type: type} + else + %{type: type, description: description} + end + response = Endpoint.request(:put, "suppression-list/#{recipient}", body) + case response do + %SparkPost.Endpoint.Response{status_code: 200, results: results} -> + {:ok, Map.get(results, :message, "")} + _ -> {:error, response} + end + end + + @doc """ + Deletes a specific entry from the list. Returns an empty string if + the deletion was successful. Returns a %SparkPost.Endpoint.Error{} with a 404 + if the specified entry is not in the list. Returns a %SparkPost.Endpoint.Error{} + with a 403 if the entry could not be removed for any reason (such as Compliance). + + Parameters: + recipient: the entry to delete from the suppression list. + """ + def delete(recipient) do + response = Endpoint.request(:delete, "suppression-list/#{recipient}", %{}, %{}, [], false) + case response do + %SparkPost.Endpoint.Response{status_code: 204} -> + {:ok, ""} + _ -> {:error, response} + end + end + @doc """ Execute a search of the suppression list based on the provided parameters. diff --git a/test/data/suppressiondelete_fail.json b/test/data/suppressiondelete_fail.json new file mode 100644 index 0000000..b5a05be --- /dev/null +++ b/test/data/suppressiondelete_fail.json @@ -0,0 +1,7 @@ +{ + "errors": [ + { + "message": "Recipient could not be found" + } + ] +} diff --git a/test/data/suppressionlistupdate.json b/test/data/suppressionlistupdate.json new file mode 100644 index 0000000..b53ba33 --- /dev/null +++ b/test/data/suppressionlistupdate.json @@ -0,0 +1,5 @@ +{ + "results": { + "message": "Test response message" + } +} diff --git a/test/data/suppressionupdate_fail.json b/test/data/suppressionupdate_fail.json new file mode 100644 index 0000000..cd63df1 --- /dev/null +++ b/test/data/suppressionupdate_fail.json @@ -0,0 +1,7 @@ +{ + "errors": [ + { + "message": "Type must be one of: 'transactional', 'non_transactional'" + } + ] +} diff --git a/test/endpoint_test.exs b/test/endpoint_test.exs index 1c89a8b..2d44d60 100644 --- a/test/endpoint_test.exs +++ b/test/endpoint_test.exs @@ -102,4 +102,14 @@ defmodule SparkPost.EndpointTest do Endpoint.request(:get, "transmissions", %{}, %{}, []) end end + + test_with_mock "Endpoint request can handle httpoison timeouts", HTTPoison, + [request: fn (method, url, body, headers, opts) -> + fun = MockServer.mk_error(:timeout) + fun.(method, url, body, headers, opts) + end + ] do + assert %Endpoint.Error{errors: [:timeout], status_code: nil, results: nil} == + Endpoint.request(:post, "transmissions", %{}, %{}, []) + end end diff --git a/test/suppression_list_test.exs b/test/suppression_list_test.exs index 2a4b238..175631f 100644 --- a/test/suppression_list_test.exs +++ b/test/suppression_list_test.exs @@ -8,6 +8,50 @@ defmodule SparkPost.SuppressionListTest do import Mock + test_with_mock "SuppressionList.upsert_one succeeds with message", + HTTPoison, [request: fn (method, url, body, headers, opts) -> + assert method == :put + fun = MockServer.mk_http_resp(200, MockServer.get_json("suppressionlistupdate")) + fun.(method, url, body, headers, opts) + end] do + {:ok, resp} = SuppressionList.upsert_one("test@marketing.com", "non_transactional", "test description") + assert resp == "Test response message" + end + + test_with_mock "SuppressionList.upsert_one fails with invalid type", + HTTPoison, [request: fn (method, url, body, headers, opts) -> + assert method == :put + fun = MockServer.mk_http_resp(400, MockServer.get_json("suppressionupdate_fail")) + fun.(method, url, body, headers, opts) + end] do + {:error, resp} = SuppressionList.upsert_one("test@marketing.com", "bad_type") + assert %SparkPost.Endpoint.Error{} = resp + assert resp.status_code == 400 + assert resp.errors == [%{message: "Type must be one of: 'transactional', 'non_transactional'"}] + end + + test_with_mock "SuppressionList.delete succeeds with empty body", + HTTPoison, [request: fn (method, url, body, headers, opts) -> + assert method == :delete + fun = MockServer.mk_http_resp(204, "") + fun.(method, url, body, headers, opts) + end] do + {:ok, resp} = SuppressionList.delete("test@marketing.com") + assert resp == "" + end + + test_with_mock "SuppressionList.delete fails 404", + HTTPoison, [request: fn (method, url, body, headers, opts) -> + assert method == :delete + fun = MockServer.mk_http_resp(404, MockServer.get_json("suppressiondelete_fail")) + fun.(method, url, body, headers, opts) + end] do + {:error, resp} = SuppressionList.delete("test@marketing.com") + assert %SparkPost.Endpoint.Error{} = resp + assert resp.status_code == 404 + assert resp.errors == [%{message: "Recipient could not be found"}] + end + test_with_mock "SuppressionList.search succeeds with SuppressionList.SearchResult", HTTPoison, [request: fn (method, url, body, headers, opts) -> assert method == :get