From 341b513fe3fe67154de5227ccd88e290f9d6dcff Mon Sep 17 00:00:00 2001 From: Nikola Begedin Date: Tue, 3 Jul 2018 15:56:20 +0200 Subject: [PATCH 01/22] Add Template.create, Template.update --- lib/template.ex | 99 +++++++++++++++++++++++++++++------ lib/template/options.ex | 16 ++++++ lib/template/response.ex | 12 +++++ test/data/createtemplate.json | 5 ++ test/data/updatetemplate.json | 5 ++ test/template_test.exs | 70 +++++++++++++++++++++++-- 6 files changed, 188 insertions(+), 19 deletions(-) create mode 100644 lib/template/options.ex create mode 100644 lib/template/response.ex create mode 100644 test/data/createtemplate.json create mode 100644 test/data/updatetemplate.json diff --git a/lib/template.ex b/lib/template.ex index 4c0b529..c060b07 100644 --- a/lib/template.ex +++ b/lib/template.ex @@ -6,17 +6,29 @@ defmodule SparkPost.Template do Check out the documentation for each function or use the [SparkPost API reference](https://developers.sparkpost.com/api/templates.html) for details. - Returned by `SparkPost.template.preview/2`. - - from - - email - - name - - subject - - reply_to - - text - - html - - headers + ## Struct Fields + + - id: Template identifier, auto-generated if not provided upon create. + - name: Editable template display name, auto-generated if not provided. At minimum, `:name` or `:id` is required, but not both + - content: Content that will be used to construct a message. Can be a `%SparkPost.Content.Inline` or a `%SparkPost.Content.Raw{}` + - published: Boolean indicating the published/draft state of the template. Defaults to false + - description: Detailed description of the template + - options: A `%SparkPost.Transmission.Options{}` struzct, but only `:open_tracking`, `:click_tracking` and `:transactional` are accepted when working with a template. + - shared_with_subaccounts: boolean indicating if the template is accessible to subaccounts. Defaults to false. + - has_draft: Read-only. Indicates if template has a draft version. + - has_published: Read-only. Indicates if template has a published version. """ + defstruct id: nil, + name: nil, + content: %SparkPost.Content.Inline{}, + published: false, + description: nil, + options: %SparkPost.Transmission.Options{}, + shared_with_subaccounts: false, + has_draft: nil, + has_published: nil + alias SparkPost.Endpoint @doc """ @@ -31,17 +43,74 @@ defmodule SparkPost.Template do - substitution_data: k,v map consisting of substituions. See the [SparkPost Substitutions Reference](https://developers.sparkpost.com/api/substitutions-reference.html) for more details. + + Response is a `%SparkPost.Content.Inline{}` consisting of + - from + - email + - name + - subject + - reply_to + - text + - html + - headers """ def preview(%SparkPost.Content.TemplateRef{} = template, substitution_data) do - qs = if is_nil(template.use_draft_template) do - "" - else - "?draft=#{template.use_draft_template}" - end + qs = + if is_nil(template.use_draft_template) do + "" + else + "?draft=#{template.use_draft_template}" + end + body = %{substitution_data: substitution_data} + :post |> Endpoint.request("templates/#{template.template_id}/preview#{qs}", body) |> Endpoint.marshal_response(SparkPost.Content.Inline) - |> SparkPost.Content.Inline.convert_from_field + |> SparkPost.Content.Inline.convert_from_field() + end + + @doc """ + Create a SparkPost Template + + ## Parameters + + - `%SparkPost.Template{}` + + ## Response + + - `%SparkPost.Template.Response{}` + """ + def create(%__MODULE__{} = template) do + :post + |> Endpoint.request("templates", template) + |> Endpoint.marshal_response(SparkPost.Template.Response) + end + + @doc """ + Update a SparkPost Template + + ## Parameters + + - `%SparkPost.Template{}` containing a valid `:id` as well as the updated content + - optional keyword list as a second argument, supporting the fields + - `:update_published` - defaults to false, specifies if the published version of the template should be directly updated, instead of storing the update as a draft + + ## Note on `:update_published` option, vs `:published` struct field + + Setting `published: true` on the struct itself performs the act of publishing a draft template. If the field is set to + `true`, the `:update_published` option is ingored completely. + """ + def update(%__MODULE{id: template_id, published: published} = template, options \\ [update_published: false]) do + qs = + if published != true && Keyword.get(options, :update_published, false) == true do + "?update_published=true" + else + "" + end + + :put + |> Endpoint.request("templates/#{template_id}#{qs}", template) + |> Endpoint.marshal_response(SparkPost.Template.Response) end end diff --git a/lib/template/options.ex b/lib/template/options.ex new file mode 100644 index 0000000..498280d --- /dev/null +++ b/lib/template/options.ex @@ -0,0 +1,16 @@ +defmodule SparkPost.Template.Options do + @moduledoc """ + Template options. + + Designed for use in `%SparkPost.Content.Template{options: ...}` + + ## Fields + - open_tracking: enable 'email open' tracking? + - click_tracking: enable 'link click' tracking? + - transactional: is this a transactional message? + """ + + defstruct open_tracking: true, + click_tracking: true, + transactional: nil +end diff --git a/lib/template/response.ex b/lib/template/response.ex new file mode 100644 index 0000000..9debc94 --- /dev/null +++ b/lib/template/response.ex @@ -0,0 +1,12 @@ +defmodule SparkPost.Template.Response do + @moduledoc """ + The response generated when SparkPost receives a Template request. + + Returned by `SparkPost.Template.create/1` + + ## Fields + - id: Unique id of the template, generated automatically or specified as part of the original request + """ + + defstruct id: nil +end diff --git a/test/data/createtemplate.json b/test/data/createtemplate.json new file mode 100644 index 0000000..9e91f98 --- /dev/null +++ b/test/data/createtemplate.json @@ -0,0 +1,5 @@ +{ + "results": { + "id": "TEMPLATE_ID" + } +} diff --git a/test/data/updatetemplate.json b/test/data/updatetemplate.json new file mode 100644 index 0000000..9e91f98 --- /dev/null +++ b/test/data/updatetemplate.json @@ -0,0 +1,5 @@ +{ + "results": { + "id": "TEMPLATE_ID" + } +} diff --git a/test/template_test.exs b/test/template_test.exs index e2477ea..0ec8854 100644 --- a/test/template_test.exs +++ b/test/template_test.exs @@ -8,6 +8,10 @@ defmodule SparkPost.TemplateTest do defmodule TestStruct do def basic_template do + %SparkPost.Template{id: "TEMPLATE_ID"} + end + + def basic_template_ref do %TemplateRef{template_id: "TEMPLATE_ID", use_draft_template: nil} end @@ -31,7 +35,7 @@ defmodule SparkPost.TemplateTest do fun = MockServer.mk_http_resp(200, MockServer.get_json("previewtemplate")) fun.(method, url, body, headers, opts) end] do - resp = Template.preview(TestStruct.basic_template(), TestStruct.substitution_data()) + resp = Template.preview(TestStruct.basic_template_ref(), TestStruct.substitution_data()) assert %Inline{} = resp end @@ -48,7 +52,7 @@ defmodule SparkPost.TemplateTest do test_with_mock "Template.preview fails with Endpoint.Error", HTTPoison, [request: MockServer.mk_fail] do - resp = Template.preview(TestStruct.basic_template(), TestStruct.substitution_data()) + resp = Template.preview(TestStruct.basic_template_ref(), TestStruct.substitution_data()) assert %Endpoint.Error{} = resp end @@ -58,7 +62,7 @@ defmodule SparkPost.TemplateTest do fun = MockServer.mk_http_resp(200, MockServer.get_json("previewtemplate")) fun.(method, url, body, headers, opts) end] do - resp = Template.preview(TestStruct.basic_template(), TestStruct.substitution_data()) + resp = Template.preview(TestStruct.basic_template_ref(), TestStruct.substitution_data()) assert %SparkPost.Address{ name: "Example Company Marketing", "email": "marketing@bounces.company.example" @@ -71,10 +75,68 @@ defmodule SparkPost.TemplateTest do fun = MockServer.mk_http_resp(200, MockServer.get_json("previewtemplate_simpleemail")) fun.(method, url, body, headers, opts) end] do - resp = Template.preview(TestStruct.basic_template(), TestStruct.substitution_data()) + resp = Template.preview(TestStruct.basic_template_ref(), TestStruct.substitution_data()) assert %SparkPost.Address{ name: nil, "email": "marketing@bounces.company.example" } == resp.from end + + test_with_mock "Template.create succeeds with Template.Response", HTTPoison, + [request: fn (method, url, body, headers, opts) -> + assert method == :post + assert url =~ "/templates" + fun = MockServer.mk_http_resp(200, MockServer.get_json("createtemplate")) + fun.(method, url, body, headers, opts) + end] do + assert Template.create(TestStruct.basic_template()) == + %SparkPost.Template.Response{id: "TEMPLATE_ID"} + end + + test_with_mock "Template.create fails with Endpoint.Error", HTTPoison, + [request: MockServer.mk_fail] do + resp = Template.create(TestStruct.basic_template()) + assert %Endpoint.Error{} = resp + end + + test_with_mock "Template.update succeeds with Template.Response", HTTPoison, + [request: fn (method, url, body, headers, opts) -> + assert method == :put + assert url =~ "/templates/TEMPLATE_ID" + fun = MockServer.mk_http_resp(200, MockServer.get_json("updatetemplate")) + fun.(method, url, body, headers, opts) + end] do + assert Template.update(TestStruct.basic_template()) == + %SparkPost.Template.Response{id: "TEMPLATE_ID"} + end + + test_with_mock "Template.update succeeds with update_published set", HTTPoison, + [request: fn (method, url, body, headers, opts) -> + assert method == :put + assert url =~ "/templates/TEMPLATE_ID?update_published=true" + fun = MockServer.mk_http_resp(200, MockServer.get_json("updatetemplate")) + fun.(method, url, body, headers, opts) + end] do + assert Template.update(TestStruct.basic_template(), update_published: true) == + %SparkPost.Template.Response{id: "TEMPLATE_ID"} + end + + test_with_mock "Template.update ignores update_published set if published field set", HTTPoison, + [request: fn (method, url, body, headers, opts) -> + assert method == :put + refute url =~ "/templates/TEMPLATE_ID?update_published=true" + assert url =~ "/templates/TEMPLATE_ID" + fun = MockServer.mk_http_resp(200, MockServer.get_json("updatetemplate")) + fun.(method, url, body, headers, opts) + end] do + template = %{TestStruct.basic_template() | published: true} + assert Template.update(template , update_published: true) == %SparkPost.Template.Response{id: "TEMPLATE_ID"} + end + + + test_with_mock "Template.update fails with Endpoint.Error", HTTPoison, + [request: MockServer.mk_fail] do + resp = Template.update(TestStruct.basic_template()) + assert %Endpoint.Error{} = resp + end end From f36c9962a17512b6b481afc6d159de15e1baf7e3 Mon Sep 17 00:00:00 2001 From: Nikola Begedin Date: Tue, 3 Jul 2018 16:26:55 +0200 Subject: [PATCH 02/22] Add Template.delete --- lib/endpoint.ex | 6 ++-- lib/template.ex | 19 ++++++++++++ test/data/templatedelete_fail_404.json | 9 ++++++ test/data/templatedelete_fail_409.json | 9 ++++++ test/template_test.exs | 42 ++++++++++++++++++++++++++ 5 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 test/data/templatedelete_fail_404.json create mode 100644 test/data/templatedelete_fail_409.json diff --git a/lib/endpoint.ex b/lib/endpoint.ex index 728c7ba..6790c43 100644 --- a/lib/endpoint.ex +++ b/lib/endpoint.ex @@ -15,7 +15,7 @@ defmodule SparkPost.Endpoint do - `:get` - `:head` - `:options` - - `:patch` + - `:patch` - `:post` - `:put` - `endpoint`: SparkPost API endpoint as string ("transmissions", "templates", ...) @@ -72,7 +72,7 @@ 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 + if decode_results && Map.has_key?(decoded_body, :results) do %SparkPost.Endpoint.Response{status_code: code, results: decoded_body.results} else %SparkPost.Endpoint.Response{status_code: code, results: decoded_body} @@ -98,7 +98,7 @@ defmodule SparkPost.Endpoint do } end - # Do not try to remove nils from an empty map + # 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 diff --git a/lib/template.ex b/lib/template.ex index c060b07..3b08141 100644 --- a/lib/template.ex +++ b/lib/template.ex @@ -113,4 +113,23 @@ defmodule SparkPost.Template do |> Endpoint.request("templates/#{template_id}#{qs}", template) |> Endpoint.marshal_response(SparkPost.Template.Response) end + + @doc """ + Delete a SparkPost Template + + ## Parameters + + - a valid template id + + ## Response + + - `{:ok, %SparkPost.Endpoint.Response{}}` if successful + - `{:error, %SparkPost.Endpoint.Error{}}` if failure + """ + def delete(template_id) do + case Endpoint.request(:delete, "templates/#{template_id}") do + %SparkPost.Endpoint.Response{status_code: 200} = response -> {:ok, response} + other -> {:error, other} + end + end end diff --git a/test/data/templatedelete_fail_404.json b/test/data/templatedelete_fail_404.json new file mode 100644 index 0000000..34cd884 --- /dev/null +++ b/test/data/templatedelete_fail_404.json @@ -0,0 +1,9 @@ +{ + "errors": [ + { + "message": "resource not found", + "code": "1600", + "description": "Template does not exist" + } + ] +} diff --git a/test/data/templatedelete_fail_409.json b/test/data/templatedelete_fail_409.json new file mode 100644 index 0000000..3a5e918 --- /dev/null +++ b/test/data/templatedelete_fail_409.json @@ -0,0 +1,9 @@ +{ + "errors": [ + { + "message": "resource conflict", + "code": "1602", + "description": "Template is in use by msg generation" + } + ] +} diff --git a/test/template_test.exs b/test/template_test.exs index 0ec8854..260b885 100644 --- a/test/template_test.exs +++ b/test/template_test.exs @@ -139,4 +139,46 @@ defmodule SparkPost.TemplateTest do resp = Template.update(TestStruct.basic_template()) assert %Endpoint.Error{} = resp end + + test_with_mock "Template.delete succeeds with empty body", + HTTPoison, [request: fn (method, url, body, headers, opts) -> + assert method == :delete + assert url =~ "/templates/TEMPLATE_ID" + fun = MockServer.mk_http_resp(200, "{}") + fun.(method, url, body, headers, opts) + end] do + assert Template.delete("TEMPLATE_ID") == {:ok, %SparkPost.Endpoint.Response{results: %{}, status_code: 200}} + end + + test_with_mock "Template.delete fails with 404", + HTTPoison, [request: fn (method, url, body, headers, opts) -> + assert method == :delete + assert url =~ "/templates/TEMPLATE_ID" + fun = MockServer.mk_http_resp(404, MockServer.get_json("templatedelete_fail_404")) + fun.(method, url, body, headers, opts) + end] do + assert {:error, %Endpoint.Error{} = resp} = Template.delete("TEMPLATE_ID") + assert resp.status_code == 404 + assert resp.errors == [%{ + code: "1600", + description: "Template does not exist", + message: "resource not found" + }] + end + + test_with_mock "Template.delete fails with 409", + HTTPoison, [request: fn (method, url, body, headers, opts) -> + assert method == :delete + assert url =~ "/templates/TEMPLATE_ID" + fun = MockServer.mk_http_resp(409, MockServer.get_json("templatedelete_fail_409")) + fun.(method, url, body, headers, opts) + end] do + assert {:error, %Endpoint.Error{} = resp} = Template.delete("TEMPLATE_ID") + assert resp.status_code == 409 + assert resp.errors == [%{ + code: "1602", + description: "Template is in use by msg generation", + message: "resource conflict" + }] + end end From fa4e06b7e2d550f1a59bf67fb6bab85d0cc52897 Mon Sep 17 00:00:00 2001 From: Nikola Begedin Date: Tue, 3 Jul 2018 16:34:14 +0200 Subject: [PATCH 03/22] Add test clause for new endpoint behavior --- test/endpoint_test.exs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/test/endpoint_test.exs b/test/endpoint_test.exs index 2d44d60..29ea6ac 100644 --- a/test/endpoint_test.exs +++ b/test/endpoint_test.exs @@ -27,7 +27,7 @@ defmodule SparkPost.EndpointTest do end test "Endpoint.request succeeds with Endpoint.Response" do - with_mock HTTPoison, [request: fn(_, _, _, _, _) -> + with_mock HTTPoison, [request: fn(_, _, _, _, _) -> r = MockServer.mk_resp r.(nil, nil, nil, nil, nil) end] do @@ -112,4 +112,14 @@ defmodule SparkPost.EndpointTest do assert %Endpoint.Error{errors: [:timeout], status_code: nil, results: nil} == Endpoint.request(:post, "transmissions", %{}, %{}, []) end + + test_with_mock "Endpoint request can handle blank map as response", HTTPoison, + [request: fn (method, url, body, headers, opts) -> + fun = MockServer.mk_http_resp(200, "{}") + fun.(method, url, body, headers, opts) + end] + do + assert %Endpoint.Response{status_code: 200, results: %{}} == + Endpoint.request(:post, "transmissions", %{}, %{}, []) + end end From 83cfe7ffb2b34c55de0abb44a1ac169ff3dcabb8 Mon Sep 17 00:00:00 2001 From: Nikola Begedin Date: Tue, 3 Jul 2018 16:55:00 +0200 Subject: [PATCH 04/22] Correct error in update function signature --- lib/template.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/template.ex b/lib/template.ex index 3b08141..f0835e4 100644 --- a/lib/template.ex +++ b/lib/template.ex @@ -101,7 +101,7 @@ defmodule SparkPost.Template do Setting `published: true` on the struct itself performs the act of publishing a draft template. If the field is set to `true`, the `:update_published` option is ingored completely. """ - def update(%__MODULE{id: template_id, published: published} = template, options \\ [update_published: false]) do + def update(%__MODULE__{id: template_id, published: published} = template, options \\ [update_published: false]) do qs = if published != true && Keyword.get(options, :update_published, false) == true do "?update_published=true" From dbe63bcd5e0a1641f5d62afa118bbcb2e1b18aae Mon Sep 17 00:00:00 2001 From: begedin Date: Mon, 12 Oct 2020 08:31:59 +0200 Subject: [PATCH 05/22] Add CI infrastructure, update deps - add formatter - add credo - add dialyxir - add github action workflow - format everything - fix all credo strict issues - fix dialyzer issues - update elixir 1.2 -> 1.10.4 - update httpoison 1.0 -> 1.7 - update poison 2.0 or 3.0 -> 4.0 - update mock 0.2 -> 0.3.5 - update excoveralls 0.5.7 -> 0.13 - update earmark 1.0.3 -> 1.4 - update ex_doc 0.14.3 -> 0.20 --- .formatter.exs | 7 + .github/workflows/build-test.yml | 86 +++++++++++ .gitignore | 2 + .tool-versions | 1 + examples/simplesend.exs | 2 +- lib/address.ex | 19 ++- lib/content.ex | 6 +- lib/content/inline.ex | 15 +- lib/endpoint.ex | 35 +++-- lib/mockserver.ex | 14 +- lib/recipient.ex | 22 +-- lib/suppression_list.ex | 31 ++-- lib/suppression_list/list_entry.ex | 10 +- lib/suppression_list/search_result.ex | 4 +- lib/template.ex | 15 +- lib/transmission.ex | 63 ++++---- lib/transmission/options.ex | 14 +- lib/transmission/response.ex | 4 +- lib/washup.ex | 19 ++- mix.exs | 54 ++++--- mix.lock | 45 +++--- test/endpoint_test.exs | 126 +++++++++------- test/filter_test.exs | 2 +- test/sparkpost_test.exs | 65 ++++---- test/suppression_list_test.exs | 155 ++++++++++--------- test/template_test.exs | 206 ++++++++++++++------------ test/transmission_test.exs | 175 +++++++++++++--------- 27 files changed, 723 insertions(+), 474 deletions(-) create mode 100644 .formatter.exs create mode 100644 .github/workflows/build-test.yml create mode 100644 .tool-versions diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..a677202 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,7 @@ +[ + inputs: [ + "{mix, .formatter, .credo}.exs", + "{config,lib,test}/**/*.{ex,exs}" + ], + line_length: 100 +] diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml new file mode 100644 index 0000000..2926074 --- /dev/null +++ b/.github/workflows/build-test.yml @@ -0,0 +1,86 @@ +name: Elixir CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build_and_test: + name: Build and test + runs-on: ubuntu-latest + strategy: + matrix: + elixir: ["1.10.4"] + otp: [23] + cache: [1] + services: + db: + image: postgres:11 + ports: ['5432:5432'] + env: + POSTGRES_PASSWORD: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Set up Elixir + uses: actions/setup-elixir@v1 + with: + elixir-version: ${{ matrix.elixir }} + otp-version: ${{ matrix.otp }} + + - name: Restore dependencies cache + id: mix-deps-cache + uses: actions/cache@v2 + with: + path: deps + key: ${{ runner.os }}-mix-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-${{ matrix.cache }}-mix + + - name: Restore build cache + uses: actions/cache@v1 + with: + path: _build + key: cache-${{ runner.os }}-dialyzer_build-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }} + restore-keys: cache-${{ runner.os }}-${{ matrix.cache }}-dialyzer_build- + + - name: Install Mix Dependencies + if: steps.mix-deps-cache.outputs.cache-hit != 'true' + run: | + mix local.rebar --force + mix local.hex --force + mix deps.get + + - name: Credo + run: mix credo --strict + + - name: Check formatting + run: mix format --check-formatted + + - name: Restore PLT Cache + uses: actions/cache@v1 + id: plt-cache + with: + path: priv/plts + key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-plts-${{ hashFiles('**/mix.lock') }} + restore-keys: cache-${{ runner.os }}-${{ matrix.cache }}-dialyzer_build- + + - name: Create PLTs + if: steps.plt-cache.outputs.cache-hit != 'true' + run: | + mkdir -p plts + mix dialyzer --plt + + - name: Run dialyzer + run: mix dialyzer + + - name: Run tests + run: mix test + diff --git a/.gitignore b/.gitignore index b322b03..a0f49de 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ erl_crash.dump *.ez *.swp +/plts/*.plt +/plts/*.plt.hash diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..0eed8ce --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +elixir 1.10.4 diff --git a/examples/simplesend.exs b/examples/simplesend.exs index 966321c..ba6c6e2 100644 --- a/examples/simplesend.exs +++ b/examples/simplesend.exs @@ -3,7 +3,7 @@ from = "Elixir SparkPost " SparkPost.send( to: to, from: from, - subject: "My first Elixir email", + subject: "My first Elixir email", text: "This is the boring version of the email body", html: "This is the tasty rich version of the email body." ) diff --git a/lib/address.ex b/lib/address.ex index 4af05ce..d61a9ad 100644 --- a/lib/address.ex +++ b/lib/address.ex @@ -17,24 +17,27 @@ defmodule SparkPost.Address do - `%{name: ..., email: ...}` """ def to_address(email) when is_binary(email) do - parse_address(email) + parse_address(email) end - def to_address(%{name: name, email: email})do + def to_address(%{name: name, email: email}) do %__MODULE__{name: name, email: email} end - def to_address(%{email: email})do + def to_address(%{email: email}) do %__MODULE__{email: email} end defp parse_address(addr) when is_binary(addr) do case Regex.run(~r/\s*(.+)\s+<(.+@.+)>\s*$/, addr) do - [_, name, email] -> %__MODULE__{ name: name, email: email } - nil -> case Regex.run(~r/\s*(.+@.+)\s*$/, addr) do - [_, email] -> %__MODULE__{ email: email } - nil -> raise __MODULE__.FormatError, message: "Invalid email address: #{addr}" - end + [_, name, email] -> + %__MODULE__{name: name, email: email} + + nil -> + case Regex.run(~r/\s*(.+@.+)\s*$/, addr) do + [_, email] -> %__MODULE__{email: email} + nil -> raise __MODULE__.FormatError, message: "Invalid email address: #{addr}" + end end end end diff --git a/lib/content.ex b/lib/content.ex index e5efc9c..b35a8b4 100644 --- a/lib/content.ex +++ b/lib/content.ex @@ -64,12 +64,10 @@ defmodule SparkPost.Content do end def to_content(%SparkPost.Content.Inline{} = content) do - %{ content | - from: SparkPost.Address.to_address(content.from)} + %{content | from: SparkPost.Address.to_address(content.from)} end def to_content(content) when is_map(content) do - %{ struct(SparkPost.Content.Inline, content) | - from: SparkPost.Address.to_address(content.from)} + %{struct(SparkPost.Content.Inline, content) | from: SparkPost.Address.to_address(content.from)} end end diff --git a/lib/content/inline.ex b/lib/content/inline.ex index 8e803f2..9598ed6 100644 --- a/lib/content/inline.ex +++ b/lib/content/inline.ex @@ -18,18 +18,19 @@ defmodule SparkPost.Content.Inline do """ defstruct from: :required, - reply_to: nil, - headers: nil, - subject: :required, - text: nil, - html: nil, - attachments: nil, - inline_images: nil + reply_to: nil, + headers: nil, + subject: :required, + text: nil, + html: nil, + attachments: nil, + inline_images: nil @doc """ Convert a raw "from" field into a %SparkPost.Address{} object. """ def convert_from_field(%SparkPost.Endpoint.Error{} = content), do: content + def convert_from_field(%__MODULE__{} = content) do %{content | from: SparkPost.Address.to_address(content.from)} end diff --git a/lib/endpoint.ex b/lib/endpoint.ex index 6790c43..538dd6c 100644 --- a/lib/endpoint.ex +++ b/lib/endpoint.ex @@ -32,27 +32,29 @@ defmodule SparkPost.Endpoint do "id" => "102258558346809186", "name" => "102258558346809186", "state" => "Success"}, ...], status_code: 200} """ - def request(method, endpoint, body \\ %{}, headers \\ %{}, options \\ [], decode_results \\ true) do + def request(method, endpoint, body \\ %{}, headers \\ %{}, options \\ [], decode_results \\ true) 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 + request_headers = + if method in [:get, :delete] do headers else Map.merge(headers, %{"Content-Type": "application/json"}) end |> Map.merge(base_request_headers()) - request_options = options - |> Keyword.put(:timeout, Application.get_env(:sparkpost, :http_timeout, 30000)) - |> Keyword.put(:recv_timeout, Application.get_env(:sparkpost, :http_recv_timeout, 8000)) + request_options = + options + |> Keyword.put(:timeout, Application.get_env(:sparkpost, :http_timeout, 30_000)) + |> Keyword.put(:recv_timeout, Application.get_env(:sparkpost, :http_recv_timeout, 8000)) HTTPoison.request(method, url, request_body, request_headers, request_options) |> handle_response(decode_results) end - def marshal_response(response, struct_type, subkey\\nil) + def marshal_response(response, struct_type, subkey \\ nil) @doc """ Extract a meaningful structure from a generic endpoint response: @@ -70,8 +72,10 @@ defmodule SparkPost.Endpoint do response end - defp handle_response({:ok, %HTTPoison.Response{status_code: code, body: body}}, decode_results) when code >= 200 and code < 300 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 && Map.has_key?(decoded_body, :results) do %SparkPost.Endpoint.Response{status_code: code, results: decoded_body.results} else @@ -79,8 +83,10 @@ defmodule SparkPost.Endpoint do end end - defp handle_response({:ok, %HTTPoison.Response{status_code: code, body: body}}, _decode_results) when code >= 400 do + defp handle_response({:ok, %HTTPoison.Response{status_code: code, body: body}}, _decode_results) + 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} end @@ -90,24 +96,25 @@ defmodule SparkPost.Endpoint do %SparkPost.Endpoint.Error{status_code: nil, errors: [reason]} end - defp base_request_headers() do + 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) + 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) 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 - body |> Poison.decode!([keys: :atoms]) + body |> Poison.decode!(keys: :atoms) end end diff --git a/lib/mockserver.ex b/lib/mockserver.ex index 6094ccd..dcbcb30 100644 --- a/lib/mockserver.ex +++ b/lib/mockserver.ex @@ -1,19 +1,19 @@ defmodule SparkPost.MockServer do @moduledoc false - def create_json(endpoint\\"transmission") do + def create_json(endpoint \\ "transmission") do File.read!("test/data/create#{endpoint}.json") end - def create_fail_json(endpoint\\"transmission") do + def create_fail_json(endpoint \\ "transmission") do File.read!("test/data/create#{endpoint}fail.json") end - def list_json(endpoint\\"transmission") do + def list_json(endpoint \\ "transmission") do File.read!("test/data/list#{endpoint}.json") end - def get_json(endpoint\\"transmission") do + def get_json(endpoint \\ "transmission") do File.read!("test/data/#{endpoint}.json") end @@ -34,10 +34,12 @@ defmodule SparkPost.MockServer do end def mk_http_resp(status_code, body) do - fn (_method, _url, _body, _headers, _opts) -> {:ok, %HTTPoison.Response{status_code: status_code, body: body}} end + 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 + fn _method, _url, _body, _headers, _opts -> {:error, %HTTPoison.Error{reason: reason}} end end end diff --git a/lib/recipient.ex b/lib/recipient.ex index c9f8f7f..b5b6b8b 100644 --- a/lib/recipient.ex +++ b/lib/recipient.ex @@ -12,12 +12,12 @@ defmodule SparkPost.Recipient do """ defstruct address: :required, - return_path: nil, - tags: nil, - metadata: nil, - substitution_data: nil + return_path: nil, + tags: nil, + metadata: nil, + substitution_data: nil - alias SparkPost.{Recipient, Address} + alias SparkPost.{Address, Recipient} @doc """ Convenience conversions to `[ %SparkPost.Recipient{} ]` from: @@ -29,12 +29,11 @@ defmodule SparkPost.Recipient do end def to_recipient_list(email_list) when is_list(email_list) do - Enum.map(email_list, fn (recip) -> to_recipient(recip) - end) + Enum.map(email_list, fn recip -> to_recipient(recip) end) end def to_recipient_list(email) when is_binary(email) do - [ to_recipient(email) ] + [to_recipient(email)] end @doc """ @@ -58,15 +57,16 @@ defmodule SparkPost.Recipient do def to_recipient(%{address: address} = struc) do struct(__MODULE__, %{ - struc | address: Address.to_address(address) + struc + | address: Address.to_address(address) }) end def to_recipient(%{name: _name, email: _email} = struc) do - %__MODULE__{ address: Address.to_address(struc) } + %__MODULE__{address: Address.to_address(struc)} end def to_recipient(%{email: _} = struc) do - %__MODULE__{ address: Address.to_address(struc) } + %__MODULE__{address: Address.to_address(struc)} end end diff --git a/lib/suppression_list.ex b/lib/suppression_list.ex index f341a6b..e0135d7 100644 --- a/lib/suppression_list.ex +++ b/lib/suppression_list.ex @@ -32,16 +32,21 @@ defmodule SparkPost.SuppressionList do - 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 + 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} + + _ -> + {:error, response} end end @@ -56,10 +61,13 @@ defmodule SparkPost.SuppressionList do """ def delete(recipient) do response = Endpoint.request(:delete, "suppression-list/#{recipient}", %{}, %{}, [], false) + case response do %SparkPost.Endpoint.Response{status_code: 204} -> {:ok, ""} - _ -> {:error, response} + + _ -> + {:error, response} end end @@ -81,15 +89,20 @@ defmodule SparkPost.SuppressionList do """ def search(params \\ []) do response = Endpoint.request(:get, "suppression-list", %{}, %{}, [params: params], false) + case response do %SparkPost.Endpoint.Response{results: body} -> - mapped_results = Enum.map(body.results, fn res -> struct(SparkPost.SuppressionList.ListEntry, res) end) + mapped_results = + Enum.map(body.results, fn res -> struct(SparkPost.SuppressionList.ListEntry, res) end) + %SparkPost.SuppressionList.SearchResult{ results: mapped_results, links: body.links, total_count: body.total_count } - _ -> response + + _ -> + response end end end diff --git a/lib/suppression_list/list_entry.ex b/lib/suppression_list/list_entry.ex index 4b63e67..61f6a13 100644 --- a/lib/suppression_list/list_entry.ex +++ b/lib/suppression_list/list_entry.ex @@ -12,9 +12,9 @@ defmodule SparkPost.SuppressionList.ListEntry do """ defstruct recipient: :required, - type: :required, - source: nil, - description: nil, - transactional: nil, - non_transactional: nil + type: :required, + source: nil, + description: nil, + transactional: nil, + non_transactional: nil end diff --git a/lib/suppression_list/search_result.ex b/lib/suppression_list/search_result.ex index 16816e9..f374e84 100644 --- a/lib/suppression_list/search_result.ex +++ b/lib/suppression_list/search_result.ex @@ -10,6 +10,6 @@ defmodule SparkPost.SuppressionList.SearchResult do """ defstruct results: :required, - links: :required, - total_count: :required + links: :required, + total_count: :required end diff --git a/lib/template.ex b/lib/template.ex index f0835e4..5e4162b 100644 --- a/lib/template.ex +++ b/lib/template.ex @@ -19,18 +19,18 @@ defmodule SparkPost.Template do - has_published: Read-only. Indicates if template has a published version. """ + alias SparkPost.{Content, Endpoint, Transmission} + defstruct id: nil, name: nil, - content: %SparkPost.Content.Inline{}, + content: %Content.Inline{}, published: false, description: nil, - options: %SparkPost.Transmission.Options{}, + options: %Transmission.Options{}, shared_with_subaccounts: false, has_draft: nil, has_published: nil - alias SparkPost.Endpoint - @doc """ Generate a preview of an existing template. @@ -67,7 +67,7 @@ defmodule SparkPost.Template do :post |> Endpoint.request("templates/#{template.template_id}/preview#{qs}", body) |> Endpoint.marshal_response(SparkPost.Content.Inline) - |> SparkPost.Content.Inline.convert_from_field() + |> Content.Inline.convert_from_field() end @doc """ @@ -101,7 +101,10 @@ defmodule SparkPost.Template do Setting `published: true` on the struct itself performs the act of publishing a draft template. If the field is set to `true`, the `:update_published` option is ingored completely. """ - def update(%__MODULE__{id: template_id, published: published} = template, options \\ [update_published: false]) do + def update( + %__MODULE__{id: template_id, published: published} = template, + options \\ [update_published: false] + ) do qs = if published != true && Keyword.get(options, :update_published, false) == true do "?update_published=true" diff --git a/lib/transmission.ex b/lib/transmission.ex index 1c94d16..0289014 100644 --- a/lib/transmission.ex +++ b/lib/transmission.ex @@ -42,24 +42,30 @@ defmodule SparkPost.Transmission do """ defstruct options: %SparkPost.Transmission.Options{}, - campaign_id: nil, - return_path: nil, - metadata: nil, - substitution_data: nil, - recipients: :required, - content: :required, - id: nil, # System generated fields from this point on - description: nil, - state: nil, - rcpt_list_chunk_size: nil, - rcp_list_total_chunks: nil, - num_rcpts: nil, - num_generated: nil, - num_failed_gen: nil, - generation_start_time: nil, - generation_end_time: nil - - alias SparkPost.{Transmission, Recipient, Endpoint, Content} + campaign_id: nil, + return_path: nil, + metadata: nil, + substitution_data: nil, + recipients: :required, + content: :required, + # System generated fields from this point on + id: nil, + description: nil, + state: nil, + rcpt_list_chunk_size: nil, + rcp_list_total_chunks: nil, + num_rcpts: nil, + num_generated: nil, + num_failed_gen: nil, + generation_start_time: nil, + generation_end_time: nil + + alias SparkPost.{ + Content, + Endpoint, + Recipient, + Transmission + } @doc """ Create a new transmission and send some email. @@ -81,7 +87,7 @@ defmodule SparkPost.Transmission do Transmission.send(%Transmission{ recipients: ["to@you.com"], content: %Content.Inline{ - from: "from@me.com", + from: "from@me.com", subject: subject, text: text, html: html @@ -119,10 +125,12 @@ defmodule SparkPost.Transmission do total_accepted_recipients: 1, total_rejected_recipients: 0} """ def send(%__MODULE__{} = body) do - body = %{body | - recipients: Recipient.to_recipient_list(body.recipients), - content: Content.to_content(body.content) + body = %{ + body + | recipients: Recipient.to_recipient_list(body.recipients), + content: Content.to_content(body.content) } + response = Endpoint.request(:post, "transmissions", body) Endpoint.marshal_response(response, Transmission.Response) end @@ -175,12 +183,15 @@ defmodule SparkPost.Transmission do rcp_list_total_chunks: nil, rcpt_list_chunk_size: nil, recipients: :required, return_path: :nil, state: "Success", substitution_data: nil}] """ - def list(filters\\[]) do - response = Endpoint.request(:get, "transmissions", %{}, %{}, [params: filters]) + def list(filters \\ []) do + response = Endpoint.request(:get, "transmissions", %{}, %{}, params: filters) + case response do %Endpoint.Response{} -> - Enum.map(response.results, fn (trans) -> struct(__MODULE__, trans) end) - _ -> response + Enum.map(response.results, fn trans -> struct(__MODULE__, trans) end) + + _ -> + response end end end diff --git a/lib/transmission/options.ex b/lib/transmission/options.ex index acf30dc..e05fc5c 100644 --- a/lib/transmission/options.ex +++ b/lib/transmission/options.ex @@ -16,11 +16,11 @@ defmodule SparkPost.Transmission.Options do """ defstruct start_time: nil, - open_tracking: true, - click_tracking: true, - transactional: nil, - sandbox: nil, - skip_suppression: nil, - ip_pool: nil, - inline_css: nil + open_tracking: true, + click_tracking: true, + transactional: nil, + sandbox: nil, + skip_suppression: nil, + ip_pool: nil, + inline_css: nil end diff --git a/lib/transmission/response.ex b/lib/transmission/response.ex index af3bd0e..9fdfae6 100644 --- a/lib/transmission/response.ex +++ b/lib/transmission/response.ex @@ -10,6 +10,6 @@ defmodule SparkPost.Transmission.Response do """ defstruct id: nil, - total_accepted_recipients: nil, - total_rejected_recipients: nil + total_accepted_recipients: nil, + total_rejected_recipients: nil end diff --git a/lib/washup.ex b/lib/washup.ex index 8606e5c..e7c8e68 100644 --- a/lib/washup.ex +++ b/lib/washup.ex @@ -16,7 +16,7 @@ defmodule Washup do def filter(it) do cond do is_map(it) and Map.has_key?(it, :__struct__) -> filter(Map.from_struct(it)) - is_map(it) -> for {k,v} <- it, not is_nil(v), into: %{}, do: {k, do_filter(v)} + is_map(it) -> for {k, v} <- it, not is_nil(v), into: %{}, do: {k, do_filter(v)} is_list(it) -> for x <- it, not is_nil(x), do: do_filter(x) true -> it end @@ -39,21 +39,24 @@ defmodule Washup do iex> Washup.verify(jenny) ** (Washup.RequiredError) pets->listidx->species required """ - def verify(it, path\\[]) do + def verify(it, path \\ []) do cond do is_map(it) and Map.has_key?(it, :__struct__) -> verify(Map.from_struct(it), path) - is_map(it) -> for {k,v} <- it, into: %{}, do: {k, verify(v, [k|path])} - is_list(it) -> for x <- it, do: verify(x, ["listidx"|path]) + is_map(it) -> for {k, v} <- it, into: %{}, do: {k, verify(v, [k | path])} + is_list(it) -> for x <- it, do: verify(x, ["listidx" | path]) true -> verify_val(it, path) end end defp verify_val(val, path) do case val do - :required -> raise Washup.RequiredError, - path: path, - message: Enum.join(Enum.reverse(path), "->") <> " required" - _ -> val + :required -> + raise Washup.RequiredError, + path: path, + message: Enum.join(Enum.reverse(path), "->") <> " required" + + _ -> + val end end end diff --git a/mix.exs b/mix.exs index f0412f9..b3723c5 100644 --- a/mix.exs +++ b/mix.exs @@ -2,21 +2,29 @@ defmodule SparkPost.Mixfile do use Mix.Project def project do - [app: :sparkpost, - version: "0.5.2", - elixir: "~> 1.2", - build_embedded: Mix.env == :prod, - start_permanent: Mix.env == :prod, - source_url: "https://github.com/SparkPost/elixir-sparkpost", - description: "The official Elixir package for the SparkPost API", - package: package(), - deps: deps(), - docs: [ - extras: ["README.md", "CONTRIBUTING.md", "CHANGELOG.md"] - ], - test_coverage: [tool: ExCoveralls], - preferred_cli_env: ["coveralls": :test, "coveralls.detail": :test, "coveralls.post": :test] - ] + [ + app: :sparkpost, + build_embedded: Mix.env() == :prod, + deps: deps(), + description: "The official Elixir package for the SparkPost API", + dialyzer: dialyzer(), + docs: [extras: ["README.md", "CONTRIBUTING.md", "CHANGELOG.md"]], + elixir: "~> 1.10", + package: package(), + preferred_cli_env: [coveralls: :test, "coveralls.detail": :test, "coveralls.post": :test], + source_url: "https://github.com/SparkPost/elixir-sparkpost", + start_permanent: Mix.env() == :prod, + test_coverage: [tool: ExCoveralls], + version: "0.5.2" + ] + end + + defp dialyzer do + [ + plt_add_apps: [:mix, :ex_unit, :poison], + plt_core_path: "plts", + plt_file: {:no_warn, "plts/dialyzer.plt"} + ] end def application do @@ -25,13 +33,14 @@ defmodule SparkPost.Mixfile do defp deps do [ - {:httpoison, "~> 1.0"}, - {:poison, "~> 2.0 or ~> 3.0"}, - {:mock, "~> 0.2.0", only: :test}, - {:excoveralls, "~> 0.5.7", only: :test}, - {:credo, "~> 0.5.1", only: [:dev, :test]}, - {:earmark, "~> 1.0.3", only: :dev}, - {:ex_doc, "~> 0.14.3", only: :dev} + {:dialyxir, "~> 1.0.0-rc.6", only: [:dev, :test], runtime: false}, + {:httpoison, "~> 1.7"}, + {:poison, "~> 4.0"}, + {:mock, "~> 0.3.5", only: :test}, + {:excoveralls, "~> 0.13", only: :test}, + {:credo, "~> 1.4", only: [:dev, :test], runtime: false}, + {:earmark, "~> 1.4", only: :dev}, + {:ex_doc, "~> 0.20", only: :dev, runtime: false} ] end @@ -47,4 +56,3 @@ defmodule SparkPost.Mixfile do ] end end - diff --git a/mix.lock b/mix.lock index 6c58af5..efc55a0 100644 --- a/mix.lock +++ b/mix.lock @@ -1,24 +1,31 @@ %{ - "bunt": {:hex, :bunt, "0.1.6", "5d95a6882f73f3b9969fdfd1953798046664e6f77ec4e486e6fafc7caad97c6f", [:mix], []}, - "certifi": {:hex, :certifi, "2.3.1", "d0f424232390bf47d82da8478022301c561cf6445b5b5fb6a84d49a9e76d2639", [:rebar3], [{:parse_trans, "3.2.0", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, - "credo": {:hex, :credo, "0.5.1", "2395862b94628cadf0f5c68975c1440393f425b955f1e70ce1aea267e00187a1", [:mix], [{:bunt, "~> 0.1.6", [hex: :bunt, optional: false]}]}, - "earmark": {:hex, :earmark, "1.0.3", "89bdbaf2aca8bbb5c97d8b3b55c5dd0cff517ecc78d417e87f1d0982e514557b", [:mix], []}, - "ex_doc": {:hex, :ex_doc, "0.14.3", "e61cec6cf9731d7d23d254266ab06ac1decbb7651c3d1568402ec535d387b6f7", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]}, - "excoveralls": {:hex, :excoveralls, "0.5.7", "5d26e4a7cdf08294217594a1b0643636accc2ad30e984d62f1d166f70629ff50", [:mix], [{:exjsx, "~> 3.0", [hex: :exjsx, optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, optional: false]}]}, - "exjsx": {:hex, :exjsx, "3.2.1", "1bc5bf1e4fd249104178f0885030bcd75a4526f4d2a1e976f4b428d347614f0f", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, optional: false]}]}, - "hackney": {:hex, :hackney, "1.12.1", "8bf2d0e11e722e533903fe126e14d6e7e94d9b7983ced595b75f532e04b7fdc7", [:rebar3], [{:certifi, "2.3.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, - "httpoison": {:hex, :httpoison, "1.1.1", "96ed7ab79f78a31081bb523eefec205fd2900a02cda6dbc2300e7a1226219566", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, + "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, + "certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"}, + "credo": {:hex, :credo, "1.4.1", "16392f1edd2cdb1de9fe4004f5ab0ae612c92e230433968eab00aafd976282fc", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "155f8a2989ad77504de5d8291fa0d41320fdcaa6a1030472e9967f285f8c7692"}, + "dialyxir": {:hex, :dialyxir, "1.0.0", "6a1fa629f7881a9f5aaf3a78f094b2a51a0357c843871b8bc98824e7342d00a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "aeb06588145fac14ca08d8061a142d52753dbc2cf7f0d00fc1013f53f8654654"}, + "earmark": {:hex, :earmark, "1.4.10", "bddce5e8ea37712a5bfb01541be8ba57d3b171d3fa4f80a0be9bcf1db417bcaf", [:mix], [{:earmark_parser, ">= 1.4.10", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "12dbfa80810478e521d3ffb941ad9fbfcbbd7debe94e1341b4c4a1b2411c1c27"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.10", "6603d7a603b9c18d3d20db69921527f82ef09990885ed7525003c7fe7dc86c56", [:mix], [], "hexpm", "8e2d5370b732385db2c9b22215c3f59c84ac7dda7ed7e544d7c459496ae519c0"}, + "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, + "ex_doc": {:hex, :ex_doc, "0.22.6", "0fb1e09a3e8b69af0ae94c8b4e4df36995d8c88d5ec7dbd35617929144b62c00", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "1e0aceda15faf71f1b0983165e6e7313be628a460e22a031e32913b98edbd638"}, + "excoveralls": {:hex, :excoveralls, "0.13.2", "5ca05099750c086f144fcf75842c363fc15d7d9c6faa7ad323d010294ced685e", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1e7ed75c158808a5a8f019d3ad63a5efe482994f2f8336c0a8c77d2f0ab152ce"}, + "exjsx": {:hex, :exjsx, "3.2.1", "1bc5bf1e4fd249104178f0885030bcd75a4526f4d2a1e976f4b428d347614f0f", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm", "b55727b206dab96feb025267e5c122ddb448f55b6648f9156b8d481215d80290"}, + "hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"}, + "httpoison": {:hex, :httpoison, "1.7.0", "abba7d086233c2d8574726227b6c2c4f6e53c4deae7fe5f6de531162ce9929a0", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "975cc87c845a103d3d1ea1ccfd68a2700c211a434d8428b10c323dc95dc5b980"}, "httpotion": {:hex, :httpotion, "2.1.0", "3fe84fbd13d4560c2514da656d022b1191a079178ee4992d245fc3c33c01ee18", [:mix], []}, "ibrowse": {:git, "https://github.com/cmullaparthi/ibrowse.git", "ea3305d21f37eced4fac290f64b068e56df7de80", [tag: "v4.1.2"]}, - "idna": {:hex, :idna, "5.1.1", "cbc3b2fa1645113267cc59c760bafa64b2ea0334635ef06dbac8801e42f7279c", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, - "jsx": {:hex, :jsx, "2.8.0", "749bec6d205c694ae1786d62cea6cc45a390437e24835fd16d12d74f07097727", [:mix, :rebar], []}, - "meck": {:hex, :meck, "0.8.4", "59ca1cd971372aa223138efcf9b29475bde299e1953046a0c727184790ab1520", [:make, :rebar], []}, - "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []}, - "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []}, - "mock": {:hex, :mock, "0.2.0", "5991877be6bb514b647dbd6f4869bc12bd7f2829df16e86c98d6108f966d34d7", [:mix], [{:meck, "~> 0.8.2", [hex: :meck, optional: false]}]}, - "parse_trans": {:hex, :parse_trans, "3.2.0", "2adfa4daf80c14dc36f522cf190eb5c4ee3e28008fc6394397c16f62a26258c2", [:rebar3], [], "hexpm"}, - "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], []}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], []}, + "idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"}, + "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, + "jsx": {:hex, :jsx, "2.8.0", "749bec6d205c694ae1786d62cea6cc45a390437e24835fd16d12d74f07097727", [:mix, :rebar], [], "hexpm", "a8ba15d5bac2c48b2be1224a0542ad794538d79e2cc16841a4e24ca75f0f8378"}, + "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.15.0", "98312c9f0d3730fde4049985a1105da5155bfe5c11e47bdc7406d88e01e4219b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "75ffa34ab1056b7e24844c90bfc62aaf6f3a37a15faa76b07bc5eba27e4a8b4a"}, + "meck": {:hex, :meck, "0.8.13", "ffedb39f99b0b99703b8601c6f17c7f76313ee12de6b646e671e3188401f7866", [:rebar3], [], "hexpm", "d34f013c156db51ad57cc556891b9720e6a1c1df5fe2e15af999c84d6cebeb1a"}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, + "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, + "mock": {:hex, :mock, "0.3.5", "feb81f52b8dcf0a0d65001d2fec459f6b6a8c22562d94a965862f6cc066b5431", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "6fae404799408300f863550392635d8f7e3da6b71abdd5c393faf41b131c8728"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, + "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"}, + "poison": {:hex, :poison, "4.0.1", "bcb755a16fac91cad79bfe9fc3585bb07b9331e50cfe3420a24bcc2d735709ae", [:mix], [], "hexpm", "ba8836feea4b394bb718a161fc59a288fe0109b5006d6bdf97b6badfcf6f0f25"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, "ssl_verify_hostname": {:hex, :ssl_verify_hostname, "1.0.5", "2e73e068cd6393526f9fa6d399353d7c9477d6886ba005f323b592d389fb47be", [:make], []}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"}, } diff --git a/test/endpoint_test.exs b/test/endpoint_test.exs index 29ea6ac..bb6a7fa 100644 --- a/test/endpoint_test.exs +++ b/test/endpoint_test.exs @@ -1,4 +1,5 @@ defmodule SparkPost.EndpointTest do + @moduledoc false use ExUnit.Case, async: false alias SparkPost.Endpoint @@ -7,96 +8,119 @@ defmodule SparkPost.EndpointTest do import Mock defmodule Headers do + @moduledoc false def for_method(method) do - cond do - method in [:post, :put] -> Map.merge(for_body_requests(), core()) - true -> core() + if method in [:post, :put] do + Map.merge(for_body_requests(), core()) + else + core() end end defp for_body_requests do - %{"Content-Type" => &(&1=="application/json")} + %{"Content-Type" => &(&1 == "application/json")} end def core do %{ "Authorization" => &(Application.get_env(:sparkpost, :api_key) == &1), - "User-Agent" => &(Regex.match?(~r/elixir-sparkpost\/\d+\.\d+\.\d+/, &1)) + "User-Agent" => &Regex.match?(~r/elixir-sparkpost\/\d+\.\d+\.\d+/, &1) } end end test "Endpoint.request succeeds with Endpoint.Response" do - with_mock HTTPoison, [request: fn(_, _, _, _, _) -> - r = MockServer.mk_resp - r.(nil, nil, nil, nil, nil) - end] do + 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 + test "Endpoint.request populates Endpoint.Response" do status_code = 200 - results = Poison.decode!(MockServer.create_json, [keys: :atoms]).results - with_mock HTTPoison, [request: MockServer.mk_resp] do + results = Poison.decode!(MockServer.create_json(), keys: :atoms).results + + 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 HTTPoison, [request: MockServer.mk_fail] do - %Endpoint.Error{} = Endpoint.request( - :get, "transmissions", []) + with_mock HTTPoison, request: MockServer.mk_fail() do + %Endpoint.Error{} = + Endpoint.request( + :get, + "transmissions", + [] + ) end end test "Endpoint.request populates Endpoint.Error" do status_code = 400 - errors = Poison.decode!(MockServer.create_fail_json, [keys: :atoms]).errors - with_mock HTTPoison, [request: MockServer.mk_fail] do - resp = %Endpoint.Error{} = Endpoint.request( - :get, "transmissions", []) + errors = Poison.decode!(MockServer.create_fail_json(), keys: :atoms).errors + + with_mock HTTPoison, request: MockServer.mk_fail() do + resp = + %Endpoint.Error{} = + Endpoint.request( + :get, + "transmissions", + [] + ) assert %Endpoint.Error{status_code: ^status_code, errors: ^errors} = resp end end test "Endpoint.request includes the core HTTP headers" do - respfn = MockServer.mk_resp - 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 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, body, headers, opts) - end - ] do + respfn = MockServer.mk_resp() + + 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 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, body, headers, opts) + end do Enum.each([:get, :post, :put, :delete], fn method -> - Endpoint.request(method, "transmissions", %{}, %{}, []) end) + Endpoint.request(method, "transmissions", %{}, %{}, []) + end) end end test "Endpoint.request includes request bodies for appropriate methods" do - respfn = MockServer.mk_resp - with_mock HTTPoison, [request: fn (method, url, body, headers, opts) -> - assert body == "" - respfn.(method, url, body, headers, opts) - end - ] do + respfn = MockServer.mk_resp() + + with_mock HTTPoison, + request: fn method, url, body, headers, opts -> + assert body == "" + respfn.(method, url, body, headers, opts) + end do Endpoint.request(:post, "transmissions", %{}, %{}, []) Endpoint.request(:put, "transmissions", %{}, %{}, []) end end test "Endpoint.request includes request timeout" do - respfn = MockServer.mk_resp - with_mock HTTPoison, [request: fn (method, url, body, headers, opts) -> - assert Keyword.has_key?(opts, :timeout) - respfn.(method, url, body, headers, opts) - end - ] do + respfn = MockServer.mk_resp() + + with_mock HTTPoison, + request: fn method, url, body, headers, opts -> + assert Keyword.has_key?(opts, :timeout) + respfn.(method, url, body, headers, opts) + end do Endpoint.request(:post, "transmissions", %{}, %{}, []) Endpoint.request(:put, "transmissions", %{}, %{}, []) Endpoint.request(:get, "transmissions", %{}, %{}, []) @@ -104,22 +128,20 @@ defmodule SparkPost.EndpointTest do end test_with_mock "Endpoint request can handle httpoison timeouts", HTTPoison, - [request: fn (method, url, body, headers, opts) -> + 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 do + assert %Endpoint.Error{errors: [:timeout], status_code: nil, results: nil} == + Endpoint.request(:post, "transmissions", %{}, %{}, []) end test_with_mock "Endpoint request can handle blank map as response", HTTPoison, - [request: fn (method, url, body, headers, opts) -> + request: fn method, url, body, headers, opts -> fun = MockServer.mk_http_resp(200, "{}") fun.(method, url, body, headers, opts) - end] - do - assert %Endpoint.Response{status_code: 200, results: %{}} == - Endpoint.request(:post, "transmissions", %{}, %{}, []) + end do + assert %Endpoint.Response{status_code: 200, results: %{}} == + Endpoint.request(:post, "transmissions", %{}, %{}, []) end end diff --git a/test/filter_test.exs b/test/filter_test.exs index 680328e..71bb39d 100644 --- a/test/filter_test.exs +++ b/test/filter_test.exs @@ -23,6 +23,6 @@ defmodule Washup.Test.Filter do test "map -> map" do assert Washup.filter(%{key1: %{a: 1, b: nil, c: 3}, key2: nil, key3: %{q: "q"}}) == - %{key1: %{a: 1, c: 3}, key3: %{q: "q"}} + %{key1: %{a: 1, c: 3}, key3: %{q: "q"}} end end diff --git a/test/sparkpost_test.exs b/test/sparkpost_test.exs index 91a5d00..854beb1 100644 --- a/test/sparkpost_test.exs +++ b/test/sparkpost_test.exs @@ -1,32 +1,37 @@ defmodule SparkPostTest do + @moduledoc false use ExUnit.Case - alias SparkPost.{Address, Content, Recipient, MockServer} + alias SparkPost.{Address, Content, MockServer, Recipient} import Mock test "send succeeds with a Transmission.Response" do - with_mock HTTPoison, [request: MockServer.mk_resp] do - resp = SparkPost.send( - to: "you@there.com", - from: "me@here.com", - subject: "Elixir and SparkPost...", - text: "Raw text email is boring", - html: "Rich text email is terrifying" - ) + with_mock HTTPoison, request: MockServer.mk_resp() do + resp = + SparkPost.send( + to: "you@there.com", + from: "me@here.com", + subject: "Elixir and SparkPost...", + text: "Raw text email is boring", + html: "Rich text email is terrifying" + ) + assert %SparkPost.Transmission.Response{} = resp end end test "send fails with a Endpoint.Error" do - with_mock HTTPoison, [request: MockServer.mk_fail] do - resp = SparkPost.send( - to: "you@there.com", - from: "me@here.com", - subject: "Elixir and SparkPost...", - text: nil, - html: nil - ) + with_mock HTTPoison, request: MockServer.mk_fail() do + resp = + SparkPost.send( + to: "you@there.com", + from: "me@here.com", + subject: "Elixir and SparkPost...", + text: nil, + html: nil + ) + assert %SparkPost.Endpoint.Error{} = resp end end @@ -37,17 +42,21 @@ defmodule SparkPostTest do subject = "Elixir and SparkPost..." text = "Raw text email is boring" html = "Rich text email is terrifying" - 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, body, headers, opts) - end] do + + 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, body, headers, opts) + end do SparkPost.send( to: to, from: from, diff --git a/test/suppression_list_test.exs b/test/suppression_list_test.exs index 175631f..9b848f0 100644 --- a/test/suppression_list_test.exs +++ b/test/suppression_list_test.exs @@ -4,26 +4,30 @@ defmodule SparkPost.SuppressionListTest do use ExUnit.Case, async: false alias SparkPost.{MockServer, SuppressionList} - alias SparkPost.SuppressionList.{SearchResult, ListEntry} + alias SparkPost.SuppressionList.{ListEntry, SearchResult} 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") + 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 + 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 @@ -31,87 +35,92 @@ defmodule SparkPost.SuppressionListTest do 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 == "" + 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"}] + 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 - fun = MockServer.mk_http_resp(200, MockServer.get_json("suppressionsearch")) - fun.(method, url, body, headers, opts) - end] do - resp = SuppressionList.search() - assert %SearchResult{} = resp + HTTPoison, + request: fn method, url, body, headers, opts -> + assert method == :get + fun = MockServer.mk_http_resp(200, MockServer.get_json("suppressionsearch")) + fun.(method, url, body, headers, opts) + end do + resp = SuppressionList.search() + assert %SearchResult{} = resp end test_with_mock "SuppressionList.search fails with Endpoint.Error", HTTPoison, - [request: MockServer.mk_fail] do - resp = SuppressionList.search() - assert %SparkPost.Endpoint.Error{} = resp + request: MockServer.mk_fail() do + resp = SuppressionList.search() + assert %SparkPost.Endpoint.Error{} = resp end test_with_mock "SuppressionList.search creates ListEntry structs", HTTPoison, - [request: fn (method, url, body, headers, opts) -> + request: fn method, url, body, headers, opts -> assert method == :get fun = MockServer.mk_http_resp(200, MockServer.get_json("suppressionsearch")) fun.(method, url, body, headers, opts) - end] do - resp = SuppressionList.search() - assert %SearchResult{ - results: [ - %ListEntry{ - recipient: "test@marketing.com", - type: "non_transactional", - source: nil, - description: nil, - non_transactional: nil - } - ], - links: [], - total_count: 1 - } == resp + end do + resp = SuppressionList.search() + + assert %SearchResult{ + results: [ + %ListEntry{ + recipient: "test@marketing.com", + type: "non_transactional", + source: nil, + description: nil, + non_transactional: nil + } + ], + links: [], + total_count: 1 + } == resp end test_with_mock "SuppressionList.search parses out cursor info", HTTPoison, - [request: fn (method, url, body, headers, opts) -> + request: fn method, url, body, headers, opts -> assert method == :get fun = MockServer.mk_http_resp(200, MockServer.get_json("suppressionsearch_links")) fun.(method, url, body, headers, opts) - end] do - resp = SuppressionList.search() - assert %SearchResult{ - results: [ - %ListEntry{ - recipient: "test@marketing.com", - type: "non_transactional", - source: nil, - description: nil, - non_transactional: nil - } - ], - links: [ - %{href: "/currentlink", rel: "first"}, - %{href: "/linkwithcursor", rel: "next"} - ], - total_count: 1 - } == resp + end do + resp = SuppressionList.search() + + assert %SearchResult{ + results: [ + %ListEntry{ + recipient: "test@marketing.com", + type: "non_transactional", + source: nil, + description: nil, + non_transactional: nil + } + ], + links: [ + %{href: "/currentlink", rel: "first"}, + %{href: "/linkwithcursor", rel: "next"} + ], + total_count: 1 + } == resp end end diff --git a/test/template_test.exs b/test/template_test.exs index 260b885..f84b848 100644 --- a/test/template_test.exs +++ b/test/template_test.exs @@ -1,12 +1,14 @@ defmodule SparkPost.TemplateTest do + @moduledoc false use ExUnit.Case, async: false - alias SparkPost.Content.{TemplateRef, Inline} alias SparkPost.{Endpoint, MockServer, Template} + alias SparkPost.Content.{Inline, TemplateRef} import Mock defmodule TestStruct do + @moduledoc false def basic_template do %SparkPost.Template{id: "TEMPLATE_ID"} end @@ -28,157 +30,179 @@ defmodule SparkPost.TemplateTest do end test_with_mock "Template.preview succeeds with Content.Inline", - HTTPoison, [request: fn (method, url, body, headers, opts) -> - assert method == :post - # draft not set - assert String.ends_with?(url, "preview") - fun = MockServer.mk_http_resp(200, MockServer.get_json("previewtemplate")) - fun.(method, url, body, headers, opts) - end] do - resp = Template.preview(TestStruct.basic_template_ref(), TestStruct.substitution_data()) - assert %Inline{} = resp + HTTPoison, + request: fn method, url, body, headers, opts -> + assert method == :post + # draft not set + assert String.ends_with?(url, "preview") + fun = MockServer.mk_http_resp(200, MockServer.get_json("previewtemplate")) + fun.(method, url, body, headers, opts) + end do + resp = Template.preview(TestStruct.basic_template_ref(), TestStruct.substitution_data()) + assert %Inline{} = resp end test_with_mock "Template.preview succeeds with Content.Inline and draft set", - HTTPoison, [request: fn (method, url, body, headers, opts) -> - assert method == :post - assert String.ends_with?(url, "preview?draft=true") - fun = MockServer.mk_http_resp(200, MockServer.get_json("previewtemplate")) - fun.(method, url, body, headers, opts) - end] do - resp = Template.preview(TestStruct.template_with_draft(), TestStruct.substitution_data()) - assert %Inline{} = resp + HTTPoison, + request: fn method, url, body, headers, opts -> + assert method == :post + assert String.ends_with?(url, "preview?draft=true") + fun = MockServer.mk_http_resp(200, MockServer.get_json("previewtemplate")) + fun.(method, url, body, headers, opts) + end do + resp = Template.preview(TestStruct.template_with_draft(), TestStruct.substitution_data()) + assert %Inline{} = resp end test_with_mock "Template.preview fails with Endpoint.Error", HTTPoison, - [request: MockServer.mk_fail] do - resp = Template.preview(TestStruct.basic_template_ref(), TestStruct.substitution_data()) - assert %Endpoint.Error{} = resp + request: MockServer.mk_fail() do + resp = Template.preview(TestStruct.basic_template_ref(), TestStruct.substitution_data()) + assert %Endpoint.Error{} = resp end test_with_mock "Template.preview unmarshals complex from field correctly", HTTPoison, - [request: fn (method, url, body, headers, opts) -> + request: fn method, url, body, headers, opts -> assert method == :post fun = MockServer.mk_http_resp(200, MockServer.get_json("previewtemplate")) fun.(method, url, body, headers, opts) - end] do - resp = Template.preview(TestStruct.basic_template_ref(), TestStruct.substitution_data()) - assert %SparkPost.Address{ - name: "Example Company Marketing", - "email": "marketing@bounces.company.example" - } == resp.from + end do + resp = Template.preview(TestStruct.basic_template_ref(), TestStruct.substitution_data()) + + assert %SparkPost.Address{ + name: "Example Company Marketing", + email: "marketing@bounces.company.example" + } == resp.from end test_with_mock "Template.preview unmarshals simple from field correctly", HTTPoison, - [request: fn (method, url, body, headers, opts) -> + request: fn method, url, body, headers, opts -> assert method == :post fun = MockServer.mk_http_resp(200, MockServer.get_json("previewtemplate_simpleemail")) fun.(method, url, body, headers, opts) - end] do - resp = Template.preview(TestStruct.basic_template_ref(), TestStruct.substitution_data()) - assert %SparkPost.Address{ - name: nil, - "email": "marketing@bounces.company.example" - } == resp.from + end do + resp = Template.preview(TestStruct.basic_template_ref(), TestStruct.substitution_data()) + + assert %SparkPost.Address{ + name: nil, + email: "marketing@bounces.company.example" + } == resp.from end test_with_mock "Template.create succeeds with Template.Response", HTTPoison, - [request: fn (method, url, body, headers, opts) -> + request: fn method, url, body, headers, opts -> assert method == :post assert url =~ "/templates" fun = MockServer.mk_http_resp(200, MockServer.get_json("createtemplate")) fun.(method, url, body, headers, opts) - end] do - assert Template.create(TestStruct.basic_template()) == - %SparkPost.Template.Response{id: "TEMPLATE_ID"} + end do + assert Template.create(TestStruct.basic_template()) == + %SparkPost.Template.Response{id: "TEMPLATE_ID"} end test_with_mock "Template.create fails with Endpoint.Error", HTTPoison, - [request: MockServer.mk_fail] do - resp = Template.create(TestStruct.basic_template()) - assert %Endpoint.Error{} = resp + request: MockServer.mk_fail() do + resp = Template.create(TestStruct.basic_template()) + assert %Endpoint.Error{} = resp end test_with_mock "Template.update succeeds with Template.Response", HTTPoison, - [request: fn (method, url, body, headers, opts) -> + request: fn method, url, body, headers, opts -> assert method == :put assert url =~ "/templates/TEMPLATE_ID" fun = MockServer.mk_http_resp(200, MockServer.get_json("updatetemplate")) fun.(method, url, body, headers, opts) - end] do - assert Template.update(TestStruct.basic_template()) == - %SparkPost.Template.Response{id: "TEMPLATE_ID"} + end do + assert Template.update(TestStruct.basic_template()) == + %SparkPost.Template.Response{id: "TEMPLATE_ID"} end test_with_mock "Template.update succeeds with update_published set", HTTPoison, - [request: fn (method, url, body, headers, opts) -> + request: fn method, url, body, headers, opts -> assert method == :put assert url =~ "/templates/TEMPLATE_ID?update_published=true" fun = MockServer.mk_http_resp(200, MockServer.get_json("updatetemplate")) fun.(method, url, body, headers, opts) - end] do - assert Template.update(TestStruct.basic_template(), update_published: true) == - %SparkPost.Template.Response{id: "TEMPLATE_ID"} + end do + assert Template.update(TestStruct.basic_template(), update_published: true) == + %SparkPost.Template.Response{id: "TEMPLATE_ID"} end test_with_mock "Template.update ignores update_published set if published field set", HTTPoison, - [request: fn (method, url, body, headers, opts) -> + request: fn method, url, body, headers, opts -> assert method == :put refute url =~ "/templates/TEMPLATE_ID?update_published=true" assert url =~ "/templates/TEMPLATE_ID" fun = MockServer.mk_http_resp(200, MockServer.get_json("updatetemplate")) fun.(method, url, body, headers, opts) - end] do - template = %{TestStruct.basic_template() | published: true} - assert Template.update(template , update_published: true) == %SparkPost.Template.Response{id: "TEMPLATE_ID"} - end + end do + template = %{TestStruct.basic_template() | published: true} + assert Template.update(template, update_published: true) == %SparkPost.Template.Response{ + id: "TEMPLATE_ID" + } + end test_with_mock "Template.update fails with Endpoint.Error", HTTPoison, - [request: MockServer.mk_fail] do - resp = Template.update(TestStruct.basic_template()) - assert %Endpoint.Error{} = resp + request: MockServer.mk_fail() do + resp = Template.update(TestStruct.basic_template()) + assert %Endpoint.Error{} = resp end test_with_mock "Template.delete succeeds with empty body", - HTTPoison, [request: fn (method, url, body, headers, opts) -> - assert method == :delete - assert url =~ "/templates/TEMPLATE_ID" - fun = MockServer.mk_http_resp(200, "{}") - fun.(method, url, body, headers, opts) - end] do - assert Template.delete("TEMPLATE_ID") == {:ok, %SparkPost.Endpoint.Response{results: %{}, status_code: 200}} + HTTPoison, + request: fn method, url, body, headers, opts -> + assert method == :delete + assert url =~ "/templates/TEMPLATE_ID" + fun = MockServer.mk_http_resp(200, "{}") + fun.(method, url, body, headers, opts) + end do + assert Template.delete("TEMPLATE_ID") == + {:ok, %SparkPost.Endpoint.Response{results: %{}, status_code: 200}} end test_with_mock "Template.delete fails with 404", - HTTPoison, [request: fn (method, url, body, headers, opts) -> - assert method == :delete - assert url =~ "/templates/TEMPLATE_ID" - fun = MockServer.mk_http_resp(404, MockServer.get_json("templatedelete_fail_404")) - fun.(method, url, body, headers, opts) - end] do - assert {:error, %Endpoint.Error{} = resp} = Template.delete("TEMPLATE_ID") - assert resp.status_code == 404 - assert resp.errors == [%{ - code: "1600", - description: "Template does not exist", - message: "resource not found" - }] + HTTPoison, + request: fn method, url, body, headers, opts -> + assert method == :delete + assert url =~ "/templates/TEMPLATE_ID" + + fun = + MockServer.mk_http_resp(404, MockServer.get_json("templatedelete_fail_404")) + + fun.(method, url, body, headers, opts) + end do + assert {:error, %Endpoint.Error{} = resp} = Template.delete("TEMPLATE_ID") + assert resp.status_code == 404 + + assert resp.errors == [ + %{ + code: "1600", + description: "Template does not exist", + message: "resource not found" + } + ] end test_with_mock "Template.delete fails with 409", - HTTPoison, [request: fn (method, url, body, headers, opts) -> - assert method == :delete - assert url =~ "/templates/TEMPLATE_ID" - fun = MockServer.mk_http_resp(409, MockServer.get_json("templatedelete_fail_409")) - fun.(method, url, body, headers, opts) - end] do - assert {:error, %Endpoint.Error{} = resp} = Template.delete("TEMPLATE_ID") - assert resp.status_code == 409 - assert resp.errors == [%{ - code: "1602", - description: "Template is in use by msg generation", - message: "resource conflict" - }] + HTTPoison, + request: fn method, url, body, headers, opts -> + assert method == :delete + assert url =~ "/templates/TEMPLATE_ID" + + fun = + MockServer.mk_http_resp(409, MockServer.get_json("templatedelete_fail_409")) + + fun.(method, url, body, headers, opts) + end do + assert {:error, %Endpoint.Error{} = resp} = Template.delete("TEMPLATE_ID") + assert resp.status_code == 409 + + assert resp.errors == [ + %{ + code: "1602", + description: "Template is in use by msg generation", + message: "resource conflict" + } + ] end end diff --git a/test/transmission_test.exs b/test/transmission_test.exs index 0e3864e..de3691e 100644 --- a/test/transmission_test.exs +++ b/test/transmission_test.exs @@ -1,11 +1,19 @@ defmodule SparkPost.TransmissionTest do + @moduledoc false use ExUnit.Case - alias SparkPost.{Transmission, Recipient, Address, Content, MockServer} + alias SparkPost.{ + Address, + Content, + MockServer, + Recipient, + Transmission + } import Mock defmodule TestStructs do + @moduledoc false def skeleton(options: options, recipients: recipients, content: content) do %Transmission{ options: options, @@ -22,21 +30,21 @@ defmodule SparkPost.TransmissionTest do end end - def full_addr_recipient(name\\"You There", email\\"you@there.com") do - %Recipient{ address: %Address{ name: name, email: email} } + def full_addr_recipient(name \\ "You There", email \\ "you@there.com") do + %Recipient{address: %Address{name: name, email: email}} end - def addr_spec_recipient(email\\"you@there.com") do - %Recipient{ address: %Address{ email: email } } + def addr_spec_recipient(email \\ "you@there.com") do + %Recipient{address: %Address{email: email}} end def inline_content do %Content.Inline{ - subject: "Subject line", - from: %Address{ email: "from@me.com" }, - text: "text content", - html: "html content" - } + subject: "Subject line", + from: %Address{email: "from@me.com"}, + text: "text content", + html: "html content" + } end def basic_transmission do @@ -49,25 +57,28 @@ defmodule SparkPost.TransmissionTest do end defmodule TestRequests do + @moduledoc false def test_send(req, test_fn) do - with_mock HTTPoison, [ - request: handle_send(test_fn) - ] do + with_mock HTTPoison, + request: handle_send(test_fn) do Transmission.send(req) end end defp handle_send(response_test_fn) do - fn (method, url, body, headers, opts) -> - req = Poison.decode!(body, [keys: :atoms]) - fullreq = struct(Transmission, %{ - req | - options: struct(Transmission.Options, req.options), - recipients: parse_recipients_field(req.recipients), - content: Content.to_content(req.content) - }) + fn method, url, body, headers, opts -> + req = Poison.decode!(body, keys: :atoms) + + fullreq = + struct(Transmission, %{ + req + | options: struct(Transmission.Options, req.options), + recipients: parse_recipients_field(req.recipients), + content: Content.to_content(req.content) + }) + response_test_fn.(fullreq) - MockServer.mk_resp.(method, url, body, headers, opts) + MockServer.mk_resp().(method, url, body, headers, opts) end end @@ -95,26 +106,27 @@ defmodule SparkPost.TransmissionTest do end test "Transmission.send succeeds with Transmission.Response" do - with_mock HTTPoison, [request: MockServer.mk_resp] do - resp = Transmission.send(TestStructs.basic_transmission) + with_mock HTTPoison, request: MockServer.mk_resp() do + resp = Transmission.send(TestStructs.basic_transmission()) assert %Transmission.Response{} = resp end end test "Transmission.send fails with Endpoint.Error" do - with_mock HTTPoison, [request: MockServer.mk_fail] do - req = TestStructs.basic_transmission + with_mock HTTPoison, request: MockServer.mk_fail() do + req = TestStructs.basic_transmission() resp = Transmission.send(req) assert %SparkPost.Endpoint.Error{} = resp end end test "Transmission.send emits a POST" do - with_mock HTTPoison, [request: fn (method, url, body, headers, opts) -> - assert method == :post - MockServer.mk_resp.(method, url, body, headers, opts) - end] do - Transmission.send(TestStructs.basic_transmission) + with_mock HTTPoison, + request: fn method, url, body, headers, opts -> + assert method == :post + MockServer.mk_resp().(method, url, body, headers, opts) + end do + Transmission.send(TestStructs.basic_transmission()) end end @@ -126,9 +138,10 @@ defmodule SparkPost.TransmissionTest do sandbox: false, skip_suppression: false } + TestRequests.test_send( - %{TestStructs.basic_transmission | options: transopts}, - &(assert &1.options == transopts) + %{TestStructs.basic_transmission() | options: transopts}, + &assert(&1.options == transopts) ) end @@ -137,33 +150,40 @@ defmodule SparkPost.TransmissionTest do TestStructs.full_addr_recipient("You There", "you@there.com"), TestStructs.full_addr_recipient("Them There", "them@there.com") ] + TestRequests.test_send( - %{TestStructs.basic_transmission | recipients: recipients}, - &(assert &1.recipients == recipients) + %{TestStructs.basic_transmission() | recipients: recipients}, + &assert(&1.recipients == recipients) ) end test "Transmission.send accepts a list of long-form recipient email addresses" do # RFC2822 3.4: Address Specification recipients = ["You There ", "You Too There "] - expected = Enum.map recipients, fn recip -> - TestStructs.full_addr_recipient(recip) - end + + expected = + Enum.map(recipients, fn recip -> + TestStructs.full_addr_recipient(recip) + end) + TestRequests.test_send( - %{TestStructs.basic_transmission | recipients: recipients}, - &(assert &1.recipients == expected) + %{TestStructs.basic_transmission() | recipients: recipients}, + &assert(&1.recipients == expected) ) end test "Transmission.send accepts a list of short-form recipient email addresses" do # RFC2822 3.4.1: Addr-spec specification recipients = ["you@there.com", "youtoo@theretoo.com"] - expected = Enum.map recipients, fn recip -> - TestStructs.addr_spec_recipient(recip) - end + + expected = + Enum.map(recipients, fn recip -> + TestStructs.addr_spec_recipient(recip) + end) + TestRequests.test_send( - %{TestStructs.basic_transmission | recipients: recipients}, - &(assert &1.recipients == expected) + %{TestStructs.basic_transmission() | recipients: recipients}, + &assert(&1.recipients == expected) ) end @@ -174,6 +194,7 @@ defmodule SparkPost.TransmissionTest do recip3 = %{name: "And You", email: "and@you.com"} recip4 = %{email: "me@too.com"} recipients = [recip0, recip1, recip2, recip3, recip4] + expected = [ TestStructs.full_addr_recipient(recip0), TestStructs.addr_spec_recipient(recip1), @@ -181,33 +202,40 @@ defmodule SparkPost.TransmissionTest do %Recipient{address: %Address{name: recip3.name, email: recip3.email}}, %Recipient{address: %Address{email: recip4.email}} ] + TestRequests.test_send( - %{TestStructs.basic_transmission | recipients: recipients}, - &(assert &1.recipients == expected) + %{TestStructs.basic_transmission() | recipients: recipients}, + &assert(&1.recipients == expected) ) end test "Transmission.send requires correctly-formatted email addresses" do - assert_raise Address.FormatError, fn -> TestRequests.test_send( - %{TestStructs.basic_transmission | recipients: "paula and paul"}, &(&1)) + assert_raise Address.FormatError, fn -> + TestRequests.test_send( + %{TestStructs.basic_transmission() | recipients: "paula and paul"}, + & &1 + ) end end test "Transmission.send marshals recipient lists correctly" do recip_lst = %Recipient.ListRef{list_id: "list101"} + TestRequests.test_send( - %{TestStructs.basic_transmission | recipients: recip_lst}, - &(assert &1.recipients == recip_lst) + %{TestStructs.basic_transmission() | recipients: recip_lst}, + &assert(&1.recipients == recip_lst) ) end test "Transmission.send marshals inline raw content correctly" do content = %Content.Raw{ - email_rfc822: "Content-Type: text/plain\r\nTo: \"{{address.name}}\" <{{address.email}}>\r\n\r\n We are testing Elixir and SparkPost together\r\n" + email_rfc822: + "Content-Type: text/plain\r\nTo: \"{{address.name}}\" <{{address.email}}>\r\n\r\n We are testing Elixir and SparkPost together\r\n" } + TestRequests.test_send( - %{TestStructs.basic_transmission | content: content}, - &(assert &1.content == content) + %{TestStructs.basic_transmission() | content: content}, + &assert(&1.content == content) ) end @@ -217,14 +245,16 @@ defmodule SparkPost.TransmissionTest do subject: "Testing SparkPost and Elixir", text: "We all live in a transient theoretical construct" } + expected = %Content.Inline{ from: %Address{email: "me@here.com"}, subject: "Testing SparkPost and Elixir", text: "We all live in a transient theoretical construct" } + TestRequests.test_send( - %{TestStructs.basic_transmission | content: content}, - &(assert &1.content == expected) + %{TestStructs.basic_transmission() | content: content}, + &assert(&1.content == expected) ) end @@ -233,33 +263,35 @@ defmodule SparkPost.TransmissionTest do template_id: "template101", use_draft_template: true } + TestRequests.test_send( - %{TestStructs.basic_transmission | content: content}, - &(assert &1.content == content) + %{TestStructs.basic_transmission() | content: content}, + &assert(&1.content == content) ) end test "Transmission.list succeeds with a list of Transmission" do - with_mock HTTPoison, [request: MockServer.mk_list] do - resp = Transmission.list + with_mock HTTPoison, request: MockServer.mk_list() do + resp = Transmission.list() assert is_list(resp) Enum.each(resp, fn r -> assert %Transmission{} = r end) end end test "Transmission.list fails with Endpoint.Error" do - with_mock HTTPoison, [request: MockServer.mk_fail] do - resp = Transmission.list + with_mock HTTPoison, request: MockServer.mk_fail() do + resp = Transmission.list() assert %SparkPost.Endpoint.Error{} = resp end end test "Transmission.list emits a GET" do - with_mock HTTPoison, [request: fn (method, url, body, headers, opts) -> - assert method == :get - MockServer.mk_list.(method, url, body, headers, opts) - end] do - Transmission.list + with_mock HTTPoison, + request: fn method, url, body, headers, opts -> + assert method == :get + MockServer.mk_list().(method, url, body, headers, opts) + end do + Transmission.list() end end @@ -267,10 +299,11 @@ defmodule SparkPost.TransmissionTest do end test "Transmission.get emits a GET" do - with_mock HTTPoison, [request: fn (method, url, body, headers, opts) -> - assert method == :get - MockServer.mk_get.(method, url, body, headers, opts) - end] do + with_mock HTTPoison, + request: fn method, url, body, headers, opts -> + assert method == :get + MockServer.mk_get().(method, url, body, headers, opts) + end do Transmission.get("TRANSMISSION_ID") end end From 6e4f080a809fd576063a6cc2eedefc84291ae228 Mon Sep 17 00:00:00 2001 From: begedin Date: Wed, 14 Oct 2020 08:08:15 +0200 Subject: [PATCH 06/22] Update docs and formatting in transmission module --- lib/endpoint.ex | 10 +-- lib/transmission.ex | 213 +++++++++++++++++++++++++++++--------------- 2 files changed, 145 insertions(+), 78 deletions(-) diff --git a/lib/endpoint.ex b/lib/endpoint.ex index 538dd6c..553d2ae 100644 --- a/lib/endpoint.ex +++ b/lib/endpoint.ex @@ -60,12 +60,12 @@ defmodule SparkPost.Endpoint do Extract a meaningful structure from a generic endpoint response: response.results[subkey] as struct_type """ + def marshal_response(%SparkPost.Endpoint.Response{} = response, struct_type, nil) do + struct(struct_type, response.results) + end + def marshal_response(%SparkPost.Endpoint.Response{} = response, struct_type, subkey) do - if subkey do - struct(struct_type, response.results[subkey]) - else - struct(struct_type, response.results) - end + struct(struct_type, response.results[subkey]) end def marshal_response(%SparkPost.Endpoint.Error{} = response, _struct_type, _subkey) do diff --git a/lib/transmission.ex b/lib/transmission.ex index 0289014..03167c1 100644 --- a/lib/transmission.ex +++ b/lib/transmission.ex @@ -1,13 +1,17 @@ defmodule SparkPost.Transmission do @moduledoc """ - The SparkPost Transmission API endpoint for sending email. Use `SparkPost.Transmission.send/1` to - send messages, `SparkPost.Transmission.list/1` to list previous sends and `SparkPost.Transmission.get/1` to - retrieve details on a given transmission. + The SparkPost Transmission API endpoint for sending email. + + Use `SparkPost.Transmission.send/1` to send messages, + `SparkPost.Transmission.list/1` to list previous sends and + `SparkPost.Transmission.get/1` to retrieve details on a given transmission. Check out the documentation for each function - or use the [SparkPost API reference](https://www.sparkPost.com/api#/reference/transmissions) for details. + or use the [SparkPost API reference](https://www.sparkPost.com/api#/reference/transmissions) + for details. ## Request Fields + Used in calls to `SparkPost.Transmission.send/1`. - campaign_id - return_path @@ -81,48 +85,66 @@ defmodule SparkPost.Transmission do - substitution_data: transmission-level substitution_data k/v pairs (keyword) ## Examples - Send a message to a single recipient with inline text and HTML content: - - alias SparkPost.{Content, Transmission} - Transmission.send(%Transmission{ - recipients: ["to@you.com"], - content: %Content.Inline{ - from: "from@me.com", - subject: subject, - text: text, - html: html - } - }) - #=> %Transmission.Response{id: "102258889940193104", - total_accepted_recipients: 1, total_rejected_recipients: 0} - - Send a message to 2 recipients using a stored message template: - alias SparkPost.Content, Transmission} - Transmission.send( - %Transmission{ - recipients: ["to@you.com", "to@youtoo.com"], - content: %Content.TemplateRef{ template_id: "test-template-1" } - } - ) - #=> %Transmission.Response{id: "102258889940193105", - total_accepted_recipients: 2, total_rejected_recipients: 0} - - Send a message with an attachment: - alias SparkPost.{Content, Transmission} - Transmission.send( - %Transmission{ - recipients: ["to@you.com"], - content: %Content.Inline{ - subject: "Now with attachments!", - text: "There is an attachment with this message", - attachments: [ - Content.to_attachment("cat.jpg", "image/jpeg", File.read!("cat.jpg")) - ] - } - } - ) - #=> %Transmission.Response{id: "102258889940193106", - total_accepted_recipients: 1, total_rejected_recipients: 0} + + ### Send a message to a single recipient with inline text and HTML content + + ``` + SparkPost.Transmission.send(%SparkPost.Transmission{ + recipients: ["to@you.com"], + content: %SparkPost.Content.Inline{ + from: "from@me.com", + subject: "A subject", + text: "Text body", + html: "HTML body" + } + }) + ``` + + #=> + + ``` + %SparkPost.Transmission.Response{ + id: "102258889940193104", + total_accepted_recipients: 1, + total_rejected_recipients: 0 + } + ``` + + ### Send a message to 2 recipients using a stored message template + + SparkPost.Transmission.send(%SparkPost.Transmission{ + recipients: ["to@you.com", "to@youtoo.com"], + content: %SparkPost.Content.TemplateRef{template_id: "test-template-1"} + }) + + #=> + + %SparkPost.Transmission.Response{ + id: "102258889940193105", + total_accepted_recipients: 2, + total_rejected_recipients: 0 + } + + ### Send a message with an attachment + + SparkPost.Transmission.send(%SparkPost.Transmission{ + recipients: ["to@you.com"], + content: %SparkPost.Content.Inline{ + subject: "Now with attachments!", + text: "There is an attachment with this message", + attachments: [ + SparkPost.Content.to_attachment("cat.jpg", "image/jpeg", File.read!("cat.jpg")) + ] + } + }) + + #=> + + %SparkPost.Transmission.Response{ + id: "102258889940193106", + total_accepted_recipients: 1, + total_rejected_recipients: 0 + } """ def send(%__MODULE__{} = body) do body = %{ @@ -141,19 +163,33 @@ defmodule SparkPost.Transmission do ## Parameters - transmission ID: identifier of the transmission to retrieve - ## Example - Transmission.get("102258889940193105") - #=> %Transmission{campaign_id: "", - content: %{template_id: "inline", template_version: 0, - use_draft_template: false}, description: "", - generation_end_time: "2016-01-14T12:52:05+00:00", - generation_start_time: "2016-01-14T12:52:05+00:00", id: "48215348926834924", - metadata: "", num_failed_gen: 0, num_generated: 2, num_rcpts: 2, - options: %{click_tracking: true, conversion_tracking: "", open_tracking: true}, - rcp_list_total_chunks: nil, rcpt_list_chunk_size: 100, recipients: :required, - return_path: nil, state: "Success", - substitution_data: ""} + ## Example: Fetch a transmission + + SparkPost.Transmission.get("102258889940193105") + + #=> + + %SparkPost.Transmission{ + campaign_id: "", + content: %{template_id: "inline", template_version: 0, use_draft_template: false}, + description: "", + generation_end_time: "2016-01-14T12:52:05+00:00", + generation_start_time: "2016-01-14T12:52:05+00:00", + id: "48215348926834924", + metadata: "", + num_failed_gen: 0, + num_generated: 2, + num_rcpts: 2, + options: %{click_tracking: true, conversion_tracking: "", open_tracking: true}, + rcp_list_total_chunks: nil, + rcpt_list_chunk_size: 100, + recipients: :required, + return_path: nil, + state: "Success", + substitution_data: "" + } """ + def get(transid) do response = Endpoint.request(:get, "transmissions/" <> transid) Endpoint.marshal_response(response, __MODULE__, :transmission) @@ -167,21 +203,52 @@ defmodule SparkPost.Transmission do - campaign_id - template_id - ## Example - List all multi-recipient transmissions: - Transmission.list() - #=> [%Transmission{campaign_id: "", content: %{template_id: "inline"}, - description: "", generation_end_time: nil, generation_start_time: nil, - id: "102258558346809186", metadata: nil, num_failed_gen: nil, - num_generated: nil, num_rcpts: nil, options: :required, - rcp_list_total_chunks: nil, rcpt_list_chunk_size: nil, recipients: :required, - return_path: :nil, state: "Success", substitution_data: nil}, - %Transmission{campaign_id: "", content: %{template_id: "inline"}, - description: "", generation_end_time: nil, generation_start_time: nil, - id: "48215348926834924", metadata: nil, num_failed_gen: nil, - num_generated: nil, num_rcpts: nil, options: :required, - rcp_list_total_chunks: nil, rcpt_list_chunk_size: nil, recipients: :required, - return_path: :nil, state: "Success", substitution_data: nil}] + ## Example: List all multi-recipient transmissions: + + SparkPost.Transmission.list() + + #=> + + [ + %SparkPost.Transmission{ + campaign_id: "", + content: %{template_id: "inline"}, + description: "", + generation_end_time: nil, + generation_start_time: nil, + id: "102258558346809186", + metadata: nil, + num_failed_gen: nil, + num_generated: nil, + num_rcpts: nil, + options: :required, + rcp_list_total_chunks: nil, + rcpt_list_chunk_size: nil, + recipients: :required, + return_path: nil, + state: "Success", + substitution_data: nil + }, + %SparkPost.Transmission{ + campaign_id: "", + content: %{template_id: "inline"}, + description: "", + generation_end_time: nil, + generation_start_time: nil, + id: "48215348926834924", + metadata: nil, + num_failed_gen: nil, + num_generated: nil, + num_rcpts: nil, + options: :required, + rcp_list_total_chunks: nil, + rcpt_list_chunk_size: nil, + recipients: :required, + return_path: nil, + state: "Success", + substitution_data: nil + } + ] """ def list(filters \\ []) do response = Endpoint.request(:get, "transmissions", %{}, %{}, params: filters) From 65aa30d45ea4e7cd2d5926d92d15e80b1a513984 Mon Sep 17 00:00:00 2001 From: begedin Date: Wed, 14 Oct 2020 08:15:09 +0200 Subject: [PATCH 07/22] Update changelog --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7664a4f..0d5aa7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ ## Change Log +## v0.6 (2020-10-14) [unreleased] + +- [INTERNAL] Add Github Actions Workflow for CI +- [INTERNAL] Add elixir formatter +- [INTERNAL] Add credo +- [INTERNAL] Add dialyzer +- Update mix elixir version 1.2 -> 1.10.4 +- Update htttpoison 1.0 -> 1.7 +- Update poison 2.0 or 3.0 -> 4.0 +- Update mock 0.2 -> 0.3.5 +- Update excoveralls 0.5.7 -> 0.13 +- Update earmark 1.0.3 -> 1.4 +- Update ex_doc 0.14.3 -> 0.20 +- [FEATURE] Add `SparkPost.Template.create/1` +- [FEATURE] Add `SparkPost.Template.update/2` +- [FEATURE] Add `SparkPost.Template.delete/1` + ## v0.5.2 (2018-05-14) - Bumped httpoison dep to ~1.0 (thanks @jasongoodwin) From 20a52ef9d3f2c00447e659520696a0ae69de2b4d Mon Sep 17 00:00:00 2001 From: begedin Date: Wed, 14 Oct 2020 08:16:21 +0200 Subject: [PATCH 08/22] Update mixfile version & information --- mix.exs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mix.exs b/mix.exs index b3723c5..067c355 100644 --- a/mix.exs +++ b/mix.exs @@ -1,4 +1,5 @@ defmodule SparkPost.Mixfile do + @moduledoc false use Mix.Project def project do @@ -15,7 +16,7 @@ defmodule SparkPost.Mixfile do source_url: "https://github.com/SparkPost/elixir-sparkpost", start_permanent: Mix.env() == :prod, test_coverage: [tool: ExCoveralls], - version: "0.5.2" + version: "0.6.0" ] end @@ -47,7 +48,7 @@ defmodule SparkPost.Mixfile do defp package do [ files: ["mix.exs", "lib", "README.md", "CONTRIBUTING.md", "LICENSE.md"], - maintainers: ["Ewan Dennis"], + maintainers: ["Ewan Dennis", "Nikola Begedin"], licenses: ["Apache 2.0"], links: %{ "Github" => "https://github.com/SparkPost/elixir-sparkpost", From 5fba93b09eaba4364be5b3d01e4fb9b3b1043116 Mon Sep 17 00:00:00 2001 From: begedin Date: Wed, 14 Oct 2020 08:22:33 +0200 Subject: [PATCH 09/22] Correct plt cache location --- .github/workflows/build-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 2926074..30b9c28 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -68,7 +68,7 @@ jobs: uses: actions/cache@v1 id: plt-cache with: - path: priv/plts + path: plts key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-plts-${{ hashFiles('**/mix.lock') }} restore-keys: cache-${{ runner.os }}-${{ matrix.cache }}-dialyzer_build- From ac5e94f6b29c0aedc7167238389584387b696be8 Mon Sep 17 00:00:00 2001 From: begedin Date: Mon, 14 Dec 2020 07:09:23 +0100 Subject: [PATCH 10/22] Support /events/messages endpoint --- CHANGELOG.md | 3 +- lib/endpoint/error.ex | 2 + lib/event.ex | 59 ++++++++++++++++++++++ lib/event/search_result.ex | 24 +++++++++ test/data/event.search_message_events.json | 57 +++++++++++++++++++++ test/event_test.exs | 29 +++++++++++ 6 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 lib/event.ex create mode 100644 lib/event/search_result.ex create mode 100644 test/data/event.search_message_events.json create mode 100644 test/event_test.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d5aa7b..c2b6c95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ ## Change Log -## v0.6 (2020-10-14) [unreleased] +## v0.6 (2020-12-14) [unreleased] +- [FEATURE] Add `SparkPost.Event.search_message_events/1` - [INTERNAL] Add Github Actions Workflow for CI - [INTERNAL] Add elixir formatter - [INTERNAL] Add credo diff --git a/lib/endpoint/error.ex b/lib/endpoint/error.ex index 097e821..abc6ed4 100644 --- a/lib/endpoint/error.ex +++ b/lib/endpoint/error.ex @@ -10,4 +10,6 @@ defmodule SparkPost.Endpoint.Error do """ defstruct status_code: nil, errors: nil, results: nil + + @type t :: %__MODULE__{} end diff --git a/lib/event.ex b/lib/event.ex new file mode 100644 index 0000000..0127abc --- /dev/null +++ b/lib/event.ex @@ -0,0 +1,59 @@ +defmodule SparkPost.Event do + @moduledoc """ + Defines actions for the events endpoint + + https://developers.sparkpost.com/api/events/ + """ + alias SparkPost.{Endpoint, Event} + + @type params :: %{ + optional(:ab_test_versions) => String.t(), + optional(:ab_tests) => String.t(), + optional(:bounce_classes) => String.t(), + optional(:campaigns) => String.t(), + optional(:cursor) => String.t(), + optional(:delimiter) => String.t(), + optional(:event_ids) => String.t(), + optional(:events) => String.t(), + optional(:from_addresses) => String.t(), + optional(:from) => String.t(), + optional(:ip_pools) => String.t(), + optional(:messages) => String.t(), + optional(:per_page) => integer, + optional(:reasons) => String.t(), + optional(:recipient_domains) => String.t(), + optional(:recipients) => String.t(), + optional(:sending_domains) => String.t(), + optional(:sending_ips) => String.t(), + optional(:subaccounts) => String.t(), + optional(:subjects) => String.t(), + optional(:templates) => String.t(), + optional(:to) => String.t(), + optional(:transmissions) => String.t() + } + + @doc """ + Search for message events + + https://developers.sparkpost.com/api/events/#events-get-search-for-message-events + + Does not sanitize parameters in any way. If a parameter is specified as needing + to be a comma-separated string, then it needs to be sent as such. + """ + @spec search_message_events(params) :: Event.SearchResult.t() | Endpoint.Error.t() + def search_message_events(%{} = params \\ %{}) do + response = Endpoint.request(:get, "events/message", %{}, %{}, [params: params], false) + + case response do + %SparkPost.Endpoint.Response{results: body} -> + %SparkPost.Event.SearchResult{ + results: body.results, + links: body.links, + total_count: body.total_count + } + + _ -> + response + end + end +end diff --git a/lib/event/search_result.ex b/lib/event/search_result.ex new file mode 100644 index 0000000..9fb7040 --- /dev/null +++ b/lib/event/search_result.ex @@ -0,0 +1,24 @@ +defmodule SparkPost.Event.SearchResult do + @moduledoc """ + Defines structure for an event search request response + + The same response structure applies when searching for message and ingest events. + + https://developers.sparkpost.com/api/events/#events-get-search-for-message-events + https://developers.sparkpost.com/api/events/#events-get-search-for-ingest-events + """ + + defstruct [ + :links, + :results, + :total_count + ] + + @type t :: %__MODULE__{ + results: list(map), + total_count: integer(), + links: %{ + optional(:next) => String.t() + } + } +end diff --git a/test/data/event.search_message_events.json b/test/data/event.search_message_events.json new file mode 100644 index 0000000..dcc0099 --- /dev/null +++ b/test/data/event.search_message_events.json @@ -0,0 +1,57 @@ +{ + "results": [ + { + "type": "click", + "campaign_id": "Example Campaign Name", + "customer_id": "1", + "delv_method": "esmtp", + "event_id": "92356927693813856", + "friendly_from": "sender@example.com", + "geo_ip": { + "country": "US", + "region": "MD", + "city": "Columbia", + "latitude": "39.1749", + "longitude": "-76.8375" + }, + "injection_time": "2016-04-18T14:25:07.000+00:00", + "ip_address": "127.0.0.1", + "ip_pool": "Example-Ip-Pool", + "message_id": "000443ee14578172be22", + "msg_from": "sender@example.com", + "msg_size": "1337", + "num_retries": "2", + "queue_time": "12", + "rcpt_meta": { + "customKey": "customValue", + "anotherKey": [ + "value1", + "value2", + "value3" + ] + }, + "rcpt_tags": [ + "male", + "US" + ], + "rcpt_to": "recipient@example.com", + "raw_rcpt_to": "recipient@example.com", + "rcpt_type": "cc", + "recipient_domain": "example.com", + "routing_domain": "example.com", + "sending_domain": "example.com", + "sending_ip": "127.0.0.1", + "subaccount_id": "101", + "subject": "Summer deals are here!", + "target_link_name": "Example Link Name", + "target_link_url": "http://example.com", + "template_id": "my-template", + "template_version": "1", + "timestamp": "2016-04-18T14:25:07.000+00:00", + "transmission_id": "65832150921904138", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36" + } + ], + "total_count": 1, + "links": {} +} \ No newline at end of file diff --git a/test/event_test.exs b/test/event_test.exs new file mode 100644 index 0000000..b9f6578 --- /dev/null +++ b/test/event_test.exs @@ -0,0 +1,29 @@ +defmodule SparkPost.EventTest do + @moduledoc false + + use ExUnit.Case, async: false + + alias SparkPost.{Event, MockServer} + + import Mock + + test_with_mock "Event.search_message_events/1 succeeds with Event.SearchResult", + HTTPoison, + request: fn method, url, body, headers, opts -> + assert method == :get + + fun = + MockServer.mk_http_resp( + 200, + MockServer.get_json("event.search_message_events") + ) + + fun.(method, url, body, headers, opts) + end do + assert %Event.SearchResult{ + results: [_] = results, + links: %{}, + total_count: 1 + } = Event.search_message_events() + end +end From 1038d0df665dc22d25dce570b70d53431a02f265 Mon Sep 17 00:00:00 2001 From: Nikola Begedin Date: Wed, 30 Nov 2022 08:23:11 +0100 Subject: [PATCH 11/22] Upgrade elixir --- .tool-versions | 3 ++- config/config.exs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.tool-versions b/.tool-versions index 0eed8ce..1c75547 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1,2 @@ -elixir 1.10.4 +elixir 1.14.1-otp-25 +erlang 25.1.1 \ No newline at end of file diff --git a/config/config.exs b/config/config.exs index f6060fc..a01ef32 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,6 +1,6 @@ # This file is responsible for configuring your application # and its dependencies with the aid of the Mix.Config module. -use Mix.Config +import Config config :sparkpost, api_endpoint: "https://api.sparkpost.com/api/v1/", From ed408190658dcde19e8858bfce5b09241a26e8e2 Mon Sep 17 00:00:00 2001 From: Nikola Begedin Date: Wed, 30 Nov 2022 08:24:42 +0100 Subject: [PATCH 12/22] Add `SparkPost.Template.list/1` --- CHANGELOG.md | 8 ++++++- lib/endpoint.ex | 5 +++++ lib/template.ex | 29 ++++++++++++++++++++++++- test/data/listtemplate.json | 25 ++++++++++++++++++++++ test/event_test.exs | 2 +- test/template_test.exs | 42 +++++++++++++++++++++++++++++++++++++ 6 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 test/data/listtemplate.json diff --git a/CHANGELOG.md b/CHANGELOG.md index c2b6c95..39b8097 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ ## Change Log +# 0.7 (2022-11-30) + +- [FEATURE] Add `SparkPost.Template.list/1` +- Upgrade elixir to 1.14.1 + +# Older releases ## v0.6 (2020-12-14) [unreleased] - [FEATURE] Add `SparkPost.Event.search_message_events/1` @@ -40,7 +46,7 @@ - Bumped default connection timeout to 30 secs ## v0.2.1 (2016/11/03) -- Added support for `start_time`, `ip_pool` and `inline_css` transmission options +- Added support for `start_time`, `ip_pool` and `inline_css` transmission options - Updated deps to latest versions ## v0.2.0 (2016/08/25) diff --git a/lib/endpoint.ex b/lib/endpoint.ex index 553d2ae..96d9a0e 100644 --- a/lib/endpoint.ex +++ b/lib/endpoint.ex @@ -60,6 +60,11 @@ defmodule SparkPost.Endpoint do Extract a meaningful structure from a generic endpoint response: response.results[subkey] as struct_type """ + def marshal_response(%SparkPost.Endpoint.Response{results: results}, struct_type, nil) + when is_list(results) do + Enum.map(results, &struct(struct_type, &1)) + end + def marshal_response(%SparkPost.Endpoint.Response{} = response, struct_type, nil) do struct(struct_type, response.results) end diff --git a/lib/template.ex b/lib/template.ex index 5e4162b..907d6b7 100644 --- a/lib/template.ex +++ b/lib/template.ex @@ -19,7 +19,9 @@ defmodule SparkPost.Template do - has_published: Read-only. Indicates if template has a published version. """ - alias SparkPost.{Content, Endpoint, Transmission} + alias SparkPost.Content + alias SparkPost.Endpoint + alias SparkPost.Transmission defstruct id: nil, name: nil, @@ -31,6 +33,31 @@ defmodule SparkPost.Template do has_draft: nil, has_published: nil + @type t :: %__MODULE__{} + + @doc """ + Lists email templates + + https://developers.sparkpost.com/api/templates/#templates-get-list-all-templates + + + """ + @spec list(%{ + optional(:draft) => boolean, + optional(:shared_with_subaccounts) => boolean + }) :: list(t) + def list(%{} = params \\ %{}) do + query = + params + |> Map.take([:draft, :shared_with_subaccounts]) + |> Enum.reject(fn {_k, v} -> is_nil(v) end) + |> Keyword.new() + + :get + |> Endpoint.request("templates", %{}, %{}, params: query) + |> Endpoint.marshal_response(__MODULE__) + end + @doc """ Generate a preview of an existing template. diff --git a/test/data/listtemplate.json b/test/data/listtemplate.json new file mode 100644 index 0000000..9f9750f --- /dev/null +++ b/test/data/listtemplate.json @@ -0,0 +1,25 @@ +{ + "results": [ + { + "id": "summer_sale", + "name": "Summer Sale!", + "published": false, + "description": "", + "has_draft": true, + "has_published": true, + "last_update_time": "2017-08-11T12:12:12+00:00", + "shared_with_subaccount": true + }, + { + "id": "daily", + "name": "daily", + "published": false, + "description": "Daily roundup email.", + "has_draft": true, + "has_published": true, + "last_use": "2018-05-08T14:49:03+00:00", + "last_update_time": "2018-02-10T14:15:16+00:00", + "shared_with_subaccount": true + } + ] +} diff --git a/test/event_test.exs b/test/event_test.exs index b9f6578..8af6f5c 100644 --- a/test/event_test.exs +++ b/test/event_test.exs @@ -21,7 +21,7 @@ defmodule SparkPost.EventTest do fun.(method, url, body, headers, opts) end do assert %Event.SearchResult{ - results: [_] = results, + results: [_], links: %{}, total_count: 1 } = Event.search_message_events() diff --git a/test/template_test.exs b/test/template_test.exs index f84b848..b81c726 100644 --- a/test/template_test.exs +++ b/test/template_test.exs @@ -29,6 +29,48 @@ defmodule SparkPost.TemplateTest do end end + describe "Template.list" do + test_with_mock "returns list of templates", HTTPoison, + request: fn method, url, body, headers, opts -> + assert method == :get + assert url =~ "/templates" + fun = MockServer.mk_http_resp(200, MockServer.get_json("listtemplate")) + fun.(method, url, body, headers, opts) + end do + assert [%SparkPost.Template{}, %SparkPost.Template{}] = Template.list() + end + + test_with_mock "passes :draft param", HTTPoison, + request: fn method, url, body, headers, opts -> + assert opts[:params] == [draft: true] + fun = MockServer.mk_http_resp(200, MockServer.get_json("listtemplate")) + fun.(method, url, body, headers, opts) + end do + assert Template.list(%{draft: true}) + end + + test_with_mock "passes :shared_with_subaccounts param", HTTPoison, + request: fn method, url, body, headers, opts -> + assert opts[:params] == [shared_with_subaccounts: false] + fun = MockServer.mk_http_resp(200, MockServer.get_json("listtemplate")) + fun.(method, url, body, headers, opts) + end do + assert Template.list(%{shared_with_subaccounts: false}) + end + + test_with_mock "returns error response", HTTPoison, + request: fn method, url, body, headers, opts -> + fun = MockServer.mk_error("Uknown") + fun.(method, url, body, headers, opts) + end do + assert Template.list() == %SparkPost.Endpoint.Error{ + status_code: nil, + errors: ["Uknown"], + results: nil + } + end + end + test_with_mock "Template.preview succeeds with Content.Inline", HTTPoison, request: fn method, url, body, headers, opts -> From e66943b7f3951e1a69624dfb00c97257ef74444a Mon Sep 17 00:00:00 2001 From: Nikola Begedin Date: Wed, 30 Nov 2022 08:25:28 +0100 Subject: [PATCH 13/22] Add vscode workspace settings, for easier contributing --- .vscode/settings.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..1457210 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "[elixir]": { + "editor.defaultFormatter": "JakeBecker.elixir-ls" + } +} \ No newline at end of file From 843f280f284046b891af5e2dcf34f8cf144e0600 Mon Sep 17 00:00:00 2001 From: Nikola Begedin Date: Wed, 30 Nov 2022 08:36:27 +0100 Subject: [PATCH 14/22] Update CI --- .github/workflows/build-test.yml | 105 +++++++++++++++---------------- 1 file changed, 52 insertions(+), 53 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 30b9c28..9deac85 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -2,9 +2,9 @@ name: Elixir CI on: push: - branches: [ master ] + branches: [master] pull_request: - branches: [ master ] + branches: [master] jobs: build_and_test: @@ -12,13 +12,13 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - elixir: ["1.10.4"] - otp: [23] + elixir: ["1.41.1"] + otp: [25] cache: [1] services: db: image: postgres:11 - ports: ['5432:5432'] + ports: ["5432:5432"] env: POSTGRES_PASSWORD: postgres options: >- @@ -27,60 +27,59 @@ jobs: --health-timeout 5s --health-retries 5 steps: - - name: Checkout - uses: actions/checkout@v2 + - name: Checkout + uses: actions/checkout@v2 - - name: Set up Elixir - uses: actions/setup-elixir@v1 - with: - elixir-version: ${{ matrix.elixir }} - otp-version: ${{ matrix.otp }} + - name: Set up Elixir + uses: actions/setup-elixir@v1 + with: + elixir-version: ${{ matrix.elixir }} + otp-version: ${{ matrix.otp }} - - name: Restore dependencies cache - id: mix-deps-cache - uses: actions/cache@v2 - with: - path: deps - key: ${{ runner.os }}-mix-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }} - restore-keys: ${{ runner.os }}-${{ matrix.cache }}-mix - - - name: Restore build cache - uses: actions/cache@v1 - with: - path: _build - key: cache-${{ runner.os }}-dialyzer_build-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }} - restore-keys: cache-${{ runner.os }}-${{ matrix.cache }}-dialyzer_build- + - name: Restore dependencies cache + id: mix-deps-cache + uses: actions/cache@v2 + with: + path: deps + key: ${{ runner.os }}-mix-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-${{ matrix.cache }}-mix - - name: Install Mix Dependencies - if: steps.mix-deps-cache.outputs.cache-hit != 'true' - run: | - mix local.rebar --force - mix local.hex --force - mix deps.get + - name: Restore build cache + uses: actions/cache@v1 + with: + path: _build + key: cache-${{ runner.os }}-dialyzer_build-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }} + restore-keys: cache-${{ runner.os }}-${{ matrix.cache }}-dialyzer_build- - - name: Credo - run: mix credo --strict + - name: Install Mix Dependencies + if: steps.mix-deps-cache.outputs.cache-hit != 'true' + run: | + mix local.rebar --force + mix local.hex --force + mix deps.get - - name: Check formatting - run: mix format --check-formatted - - - name: Restore PLT Cache - uses: actions/cache@v1 - id: plt-cache - with: - path: plts - key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-plts-${{ hashFiles('**/mix.lock') }} - restore-keys: cache-${{ runner.os }}-${{ matrix.cache }}-dialyzer_build- + - name: Credo + run: mix credo --strict - - name: Create PLTs - if: steps.plt-cache.outputs.cache-hit != 'true' - run: | - mkdir -p plts - mix dialyzer --plt + - name: Check formatting + run: mix format --check-formatted - - name: Run dialyzer - run: mix dialyzer + - name: Restore PLT Cache + uses: actions/cache@v1 + id: plt-cache + with: + path: plts + key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-plts-${{ hashFiles('**/mix.lock') }} + restore-keys: cache-${{ runner.os }}-${{ matrix.cache }}-dialyzer_build- - - name: Run tests - run: mix test + - name: Create PLTs + if: steps.plt-cache.outputs.cache-hit != 'true' + run: | + mkdir -p plts + mix dialyzer --plt + - name: Run dialyzer + run: mix dialyzer + + - name: Run tests + run: mix test From 0ee3806fcc2c12f25592a85b16a13457921c7c6b Mon Sep 17 00:00:00 2001 From: Nikola Begedin Date: Wed, 30 Nov 2022 08:36:46 +0100 Subject: [PATCH 15/22] Remove old CI --- .travis.yml | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 2a5b7d1..0000000 --- a/.travis.yml +++ /dev/null @@ -1,10 +0,0 @@ -language: elixir -elixir: -- 1.2.6 -- 1.3.4 -- 1.4.2 -after_success: -- MIX_ENV=test mix coveralls.travis -notifications: - slack: - secure: SPvOIJ1GcVbU4o4XIV7E0NajNrYYXlGYhk7MBOlsh9cSX5Y/eqVXyjVQu5WSuo+NrSlPeoN+lVd5D3hLjPyZMPP80WUjKMExBK1xHB3ZaXLqTprdtiJOn4cFEjEWQZSJ0je33A9p2L3MIQQtWN2I6u8VV6FYPMDJRrczBEoWliREfY2ETfOyAcvLbVP9UfzvGjI7/P9KWUHnlRAta5/uGqyIXQkF5NbVwD5j0qJgtW+yufXUuA4DZtvFfOBJUM68C8ZHl4lyTOfjECpwtBx7Rrkb/vq+Y7GT6ASi+hUNh1kfHJI8piUDvKjNhCXf/k5L/oMbfuVox6u9/L2grrcTrj+lHk3R041/URNI5lzNYZ4pAQFRjw+gkZ74kWXfmKAynF2htrtr+gO+fgzhWMuDKCdWyqUau11yezkkac45LQs62d73R6D2etHwPScBT1Au2CwAG7ZYdx/4ywEsykFmlutYIj5uJOgoqpdgYhOQVwzwd/p8IWEP3TIJv41xk4nECuRxacE8DKbHrNbDnTRKmV91qFGd6YmXLzZ6n2TGDgvZbykwzWo1MWdOO5vXtUw2CkQoWN+vm+16fVMciKxjsD801ub1FHuxu0ZNmo65WApK/HX2VpzzydrVMn6Oc+G3Ilqr2t4laywJPxs6ad3l19fmAnxnemrbEs6+hwlyXE8= From 6aecf2bcef59acbe46ee66aa2aa4fcee0b8d1c89 Mon Sep 17 00:00:00 2001 From: Nikola Begedin Date: Wed, 30 Nov 2022 08:37:12 +0100 Subject: [PATCH 16/22] Remove travis badge from readme --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8557a8d..ed02b9b 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,6 @@ # SparkPost Elixir Library -[![Travis CI](https://travis-ci.org/SparkPost/elixir-sparkpost.svg?branch=master)](https://travis-ci.org/SparkPost/elixir-sparkpost) [![Coverage Status](https://coveralls.io/repos/SparkPost/elixir-sparkpost/badge.svg?branch=master&service=github)](https://coveralls.io/github/SparkPost/elixir-sparkpost?branch=master) [![Slack Status](http://slack.sparkpost.com/badge.svg)](http://slack.sparkpost.com) - The official [Elixir](http://elixir-lang.org/) package for the [SparkPost API](https://www.sparkpost.com/api). Capabilities include: @@ -67,13 +65,13 @@ end ```elixir defmodule MyApp.Example do alias SparkPost.{Content, Recipient, Transmission} - + def send_message do Transmission.send(%Transmission{ recipients: [ "you@example.com" ], content: %Content.Inline{ subject: "Sending email from Elixir is awesome!", - from: "elixir@sparkpostbox.com", + from: "elixir@sparkpostbox.com", text: "Hi there!", html: "

Hi there!

" } From 77a66ed62bd6bbef281c2352bd19826d3003e8ea Mon Sep 17 00:00:00 2001 From: Nikola Begedin Date: Wed, 30 Nov 2022 08:38:05 +0100 Subject: [PATCH 17/22] FIX CI --- .github/workflows/build-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 9deac85..e7bb1b4 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - elixir: ["1.41.1"] + elixir: ["1.14.1"] otp: [25] cache: [1] services: From 2c927aee58cb4f70bad1d2285e4cd3279f7d7054 Mon Sep 17 00:00:00 2001 From: Nikola Begedin Date: Wed, 30 Nov 2022 08:41:03 +0100 Subject: [PATCH 18/22] Use correct otp --- .github/workflows/build-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index e7bb1b4..2e8f321 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: elixir: ["1.14.1"] - otp: [25] + otp: ["25.1.1"] cache: [1] services: db: From 39a5f459d26f26a5919340f2b5c633fe96df5f93 Mon Sep 17 00:00:00 2001 From: Nikola Begedin Date: Wed, 30 Nov 2022 08:42:00 +0100 Subject: [PATCH 19/22] Remove db service --- .github/workflows/build-test.yml | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 2e8f321..47fc2a3 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -15,17 +15,6 @@ jobs: elixir: ["1.14.1"] otp: ["25.1.1"] cache: [1] - services: - db: - image: postgres:11 - ports: ["5432:5432"] - env: - POSTGRES_PASSWORD: postgres - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 steps: - name: Checkout uses: actions/checkout@v2 From dd2582de1dd7afaf20be61a068ea15b3b1f47f01 Mon Sep 17 00:00:00 2001 From: Nikola Begedin Date: Wed, 30 Nov 2022 08:43:12 +0100 Subject: [PATCH 20/22] Use correct action to setup elixir --- .github/workflows/build-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 47fc2a3..e947931 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -20,7 +20,7 @@ jobs: uses: actions/checkout@v2 - name: Set up Elixir - uses: actions/setup-elixir@v1 + uses: erlef/setup-beam@v1 with: elixir-version: ${{ matrix.elixir }} otp-version: ${{ matrix.otp }} From 797bba676dc3922467e7563ed23f738845dfc4d3 Mon Sep 17 00:00:00 2001 From: Nikola Begedin Date: Wed, 30 Nov 2022 09:10:28 +0100 Subject: [PATCH 21/22] Upgrade deps --- CHANGELOG.md | 5 +++++ mix.exs | 12 ++++++------ mix.lock | 34 ++++++++++++++++++---------------- 3 files changed, 29 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39b8097..8c7cd64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ - [FEATURE] Add `SparkPost.Template.list/1` - Upgrade elixir to 1.14.1 +- Upgrade dialyxir to ~>1.2 +- Upgrade poison to ~>5.0 +- Upgrade excoveralls to ~>0.15 +- Upgrade credo to ~>1.6 +- Upgrade ex_doc to ~>0.29 # Older releases ## v0.6 (2020-12-14) [unreleased] diff --git a/mix.exs b/mix.exs index 067c355..65078e5 100644 --- a/mix.exs +++ b/mix.exs @@ -29,19 +29,19 @@ defmodule SparkPost.Mixfile do end def application do - [applications: [:httpoison]] + [] end defp deps do [ - {:dialyxir, "~> 1.0.0-rc.6", only: [:dev, :test], runtime: false}, + {:dialyxir, "~> 1.2", only: [:dev, :test], runtime: false}, {:httpoison, "~> 1.7"}, - {:poison, "~> 4.0"}, + {:poison, "~> 5.0"}, {:mock, "~> 0.3.5", only: :test}, - {:excoveralls, "~> 0.13", only: :test}, - {:credo, "~> 1.4", only: [:dev, :test], runtime: false}, + {:excoveralls, "~> 0.15", only: :test}, + {:credo, "~> 1.6", only: [:dev, :test], runtime: false}, {:earmark, "~> 1.4", only: :dev}, - {:ex_doc, "~> 0.20", only: :dev, runtime: false} + {:ex_doc, "~> 0.29", only: :dev, runtime: false} ] end diff --git a/mix.lock b/mix.lock index efc55a0..5c74df7 100644 --- a/mix.lock +++ b/mix.lock @@ -1,31 +1,33 @@ %{ - "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, - "certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"}, - "credo": {:hex, :credo, "1.4.1", "16392f1edd2cdb1de9fe4004f5ab0ae612c92e230433968eab00aafd976282fc", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "155f8a2989ad77504de5d8291fa0d41320fdcaa6a1030472e9967f285f8c7692"}, - "dialyxir": {:hex, :dialyxir, "1.0.0", "6a1fa629f7881a9f5aaf3a78f094b2a51a0357c843871b8bc98824e7342d00a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "aeb06588145fac14ca08d8061a142d52753dbc2cf7f0d00fc1013f53f8654654"}, + "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, + "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, + "credo": {:hex, :credo, "1.6.7", "323f5734350fd23a456f2688b9430e7d517afb313fbd38671b8a4449798a7854", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "41e110bfb007f7eda7f897c10bf019ceab9a0b269ce79f015d54b0dcf4fc7dd3"}, + "dialyxir": {:hex, :dialyxir, "1.2.0", "58344b3e87c2e7095304c81a9ae65cb68b613e28340690dfe1a5597fd08dec37", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "61072136427a851674cab81762be4dbeae7679f85b1272b6d25c3a839aff8463"}, "earmark": {:hex, :earmark, "1.4.10", "bddce5e8ea37712a5bfb01541be8ba57d3b171d3fa4f80a0be9bcf1db417bcaf", [:mix], [{:earmark_parser, ">= 1.4.10", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "12dbfa80810478e521d3ffb941ad9fbfcbbd7debe94e1341b4c4a1b2411c1c27"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.10", "6603d7a603b9c18d3d20db69921527f82ef09990885ed7525003c7fe7dc86c56", [:mix], [], "hexpm", "8e2d5370b732385db2c9b22215c3f59c84ac7dda7ed7e544d7c459496ae519c0"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.29", "149d50dcb3a93d9f3d6f3ecf18c918fb5a2d3c001b5d3305c926cddfbd33355b", [:mix], [], "hexpm", "4902af1b3eb139016aed210888748db8070b8125c2342ce3dcae4f38dcc63503"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, - "ex_doc": {:hex, :ex_doc, "0.22.6", "0fb1e09a3e8b69af0ae94c8b4e4df36995d8c88d5ec7dbd35617929144b62c00", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "1e0aceda15faf71f1b0983165e6e7313be628a460e22a031e32913b98edbd638"}, - "excoveralls": {:hex, :excoveralls, "0.13.2", "5ca05099750c086f144fcf75842c363fc15d7d9c6faa7ad323d010294ced685e", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1e7ed75c158808a5a8f019d3ad63a5efe482994f2f8336c0a8c77d2f0ab152ce"}, + "ex_doc": {:hex, :ex_doc, "0.29.1", "b1c652fa5f92ee9cf15c75271168027f92039b3877094290a75abcaac82a9f77", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "b7745fa6374a36daf484e2a2012274950e084815b936b1319aeebcf7809574f6"}, + "excoveralls": {:hex, :excoveralls, "0.15.1", "83c8cf7973dd9d1d853dce37a2fb98aaf29b564bf7d01866e409abf59dac2c0e", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "f8416bd90c0082d56a2178cf46c837595a06575f70a5624f164a1ffe37de07e7"}, "exjsx": {:hex, :exjsx, "3.2.1", "1bc5bf1e4fd249104178f0885030bcd75a4526f4d2a1e976f4b428d347614f0f", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm", "b55727b206dab96feb025267e5c122ddb448f55b6648f9156b8d481215d80290"}, - "hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"}, + "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, + "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, "httpoison": {:hex, :httpoison, "1.7.0", "abba7d086233c2d8574726227b6c2c4f6e53c4deae7fe5f6de531162ce9929a0", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "975cc87c845a103d3d1ea1ccfd68a2700c211a434d8428b10c323dc95dc5b980"}, "httpotion": {:hex, :httpotion, "2.1.0", "3fe84fbd13d4560c2514da656d022b1191a079178ee4992d245fc3c33c01ee18", [:mix], []}, "ibrowse": {:git, "https://github.com/cmullaparthi/ibrowse.git", "ea3305d21f37eced4fac290f64b068e56df7de80", [tag: "v4.1.2"]}, - "idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"}, - "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, + "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, + "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, "jsx": {:hex, :jsx, "2.8.0", "749bec6d205c694ae1786d62cea6cc45a390437e24835fd16d12d74f07097727", [:mix, :rebar], [], "hexpm", "a8ba15d5bac2c48b2be1224a0542ad794538d79e2cc16841a4e24ca75f0f8378"}, - "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.15.0", "98312c9f0d3730fde4049985a1105da5155bfe5c11e47bdc7406d88e01e4219b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "75ffa34ab1056b7e24844c90bfc62aaf6f3a37a15faa76b07bc5eba27e4a8b4a"}, + "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, "meck": {:hex, :meck, "0.8.13", "ffedb39f99b0b99703b8601c6f17c7f76313ee12de6b646e671e3188401f7866", [:rebar3], [], "hexpm", "d34f013c156db51ad57cc556891b9720e6a1c1df5fe2e15af999c84d6cebeb1a"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, "mock": {:hex, :mock, "0.3.5", "feb81f52b8dcf0a0d65001d2fec459f6b6a8c22562d94a965862f6cc066b5431", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "6fae404799408300f863550392635d8f7e3da6b71abdd5c393faf41b131c8728"}, - "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, - "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"}, - "poison": {:hex, :poison, "4.0.1", "bcb755a16fac91cad79bfe9fc3585bb07b9331e50cfe3420a24bcc2d735709ae", [:mix], [], "hexpm", "ba8836feea4b394bb718a161fc59a288fe0109b5006d6bdf97b6badfcf6f0f25"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, + "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, + "poison": {:hex, :poison, "5.0.0", "d2b54589ab4157bbb82ec2050757779bfed724463a544b6e20d79855a9e43b24", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "11dc6117c501b80c62a7594f941d043982a1bd05a1184280c0d9166eb4d8d3fc"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, "ssl_verify_hostname": {:hex, :ssl_verify_hostname, "1.0.5", "2e73e068cd6393526f9fa6d399353d7c9477d6886ba005f323b592d389fb47be", [:make], []}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, } From fada226db87c41360798c837b2a56d5c71a2d4a0 Mon Sep 17 00:00:00 2001 From: Nikola Begedin Date: Wed, 30 Nov 2022 09:52:41 +0100 Subject: [PATCH 22/22] Cleanup dialyzer issues --- lib/endpoint.ex | 29 +++++++++++++++++------------ test/endpoint_test.exs | 4 ++-- test/sparkpost_test.exs | 2 +- test/transmission_test.exs | 2 +- 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/lib/endpoint.ex b/lib/endpoint.ex index 96d9a0e..94cb089 100644 --- a/lib/endpoint.ex +++ b/lib/endpoint.ex @@ -81,20 +81,24 @@ defmodule SparkPost.Endpoint do when code >= 200 and code < 300 do decoded_body = decode_response_body(body) - if decode_results && Map.has_key?(decoded_body, :results) do - %SparkPost.Endpoint.Response{status_code: code, results: decoded_body.results} - else - %SparkPost.Endpoint.Response{status_code: code, results: decoded_body} - end + results = + case {decode_results, decoded_body} do + {true, %{results: results}} -> results + _ -> decoded_body + end + + %SparkPost.Endpoint.Response{status_code: code, results: results} end defp handle_response({:ok, %HTTPoison.Response{status_code: code, body: body}}, _decode_results) when code >= 400 do - decoded_body = decode_response_body(body) + errors = + case decode_response_body(body) do + %{errors: errors} -> errors + _ -> [] + end - if Map.has_key?(decoded_body, :errors) do - %SparkPost.Endpoint.Error{status_code: code, errors: decoded_body.errors} - end + %SparkPost.Endpoint.Error{status_code: code, errors: errors} end defp handle_response({:error, %HTTPoison.Error{reason: reason}}, _decode_results) do @@ -117,9 +121,10 @@ defmodule SparkPost.Endpoint do body |> Washup.filter() |> Poison.encode() end - defp decode_response_body(body) when byte_size(body) == 0, do: "" + @spec decode_response_body(String.t()) :: map + defp decode_response_body(body) when is_binary(body) and byte_size(body) == 0, do: %{} - defp decode_response_body(body) do - body |> Poison.decode!(keys: :atoms) + defp decode_response_body(body) when is_binary(body) do + Poison.decode!(body, %{keys: :atoms}) end end diff --git a/test/endpoint_test.exs b/test/endpoint_test.exs index bb6a7fa..7b79ef9 100644 --- a/test/endpoint_test.exs +++ b/test/endpoint_test.exs @@ -41,7 +41,7 @@ defmodule SparkPost.EndpointTest do test "Endpoint.request populates Endpoint.Response" do status_code = 200 - results = Poison.decode!(MockServer.create_json(), keys: :atoms).results + results = Poison.decode!(MockServer.create_json(), %{keys: :atoms}).results with_mock HTTPoison, request: MockServer.mk_resp() do resp = %Endpoint.Response{} = Endpoint.request(:get, "transmissions", %{}, %{}, []) @@ -62,7 +62,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 + errors = Poison.decode!(MockServer.create_fail_json(), %{keys: :atoms}).errors with_mock HTTPoison, request: MockServer.mk_fail() do resp = diff --git a/test/sparkpost_test.exs b/test/sparkpost_test.exs index 854beb1..19544a0 100644 --- a/test/sparkpost_test.exs +++ b/test/sparkpost_test.exs @@ -45,7 +45,7 @@ defmodule SparkPostTest do with_mock HTTPoison, request: fn method, url, body, headers, opts -> - inreq = Poison.decode!(body, keys: :atoms) + 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{ diff --git a/test/transmission_test.exs b/test/transmission_test.exs index de3691e..64cdfff 100644 --- a/test/transmission_test.exs +++ b/test/transmission_test.exs @@ -67,7 +67,7 @@ defmodule SparkPost.TransmissionTest do defp handle_send(response_test_fn) do fn method, url, body, headers, opts -> - req = Poison.decode!(body, keys: :atoms) + req = Poison.decode!(body, %{keys: :atoms}) fullreq = struct(Transmission, %{