diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 00000000..4a7f4bb0 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,4 @@ +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], + line_length: 80 +] diff --git a/.gitignore b/.gitignore index f3a4463c..8abc2786 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,7 @@ erl_crash.dump *.ez *.pem +*.p8 +scratchpad.txt +.DS_Store .iex.exs diff --git a/.travis.yml b/.travis.yml index ceb9af78..bd2b2a47 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,17 @@ language: elixir elixir: - - 1.4.5 - - 1.5.3 - - 1.6.0 +- 1.4.5 +- 1.5.3 +- 1.6.5 otp_release: - - 19.2 - - 20.0 +- 19.2 +- 20.0 before_install: -- openssl aes-256-cbc -K $encrypted_3033c569c247_key -iv - $encrypted_3033c569c247_iv -in cert_key.tar.enc -out - cert_key.tar -d -- tar xvf cert_key.tar +- openssl aes-256-cbc -K $encrypted_3033c569c247_key -iv $encrypted_3033c569c247_iv -in secrets.tar.enc -out secrets.tar -d +- tar xvf secrets.tar script: - - "MIX_ENV=test mix do deps.get, compile, coveralls.travis" +- MIX_ENV=test mix do deps.get, compile, coveralls.travis +env: + global: + - secure: kd6KF3eMgtaWIQbd/MG8m8L2cR3fqxmgRHWng4fqxRwPH45hP4IVLf2cQctGA9mm6nOV5wrNtys1fQzYPuXAKA/LAYJ/pc66ISdEjKjNpqxIGRCCJsB6lZft5XPgpAJTClwKGpFvEd7cLtz2ir4PBN0JjDyWtMTKHXarKzn7cOQNpRypwc5wWU1dmuqSsNNjr2hk3HUAwSNqB8W960Zqd39lqo0m+/tlq3L8LghcFlibn8XFNlzAmERpUpERkJZPbRplgx+ofigATGaC9Tzuc04WWLUthsPyBJkli2tYs/8Yd1tCjAYz90Tde0E1/Mzj6b4/LW4AGXrb4I5pKZrkkDw0gjqJ1Buu4U2d/TGZc+wU13BD5icpjq7zx8ZZ08N3XmQ6Y2ydd5p9MwCtUx5Dg2d9ybvdeCI1kj4HVcYAxqoBmf12od3fRZ+IwzbpUIvFn8R05wlvhA8nSLGloUceag3qUHJLD/tJOj9GRgR8ubw6hAQz7w7uWJhq3XSvHvdtYw2tfc9pGpjloA2gOhaStHrjJGFpzMFPITCEcQF1B7KraY/FPxMyvQL70l9RhQZ11F+56uwZ9oZFEQeyr5Tr6mPISyp1enz50HjwRrVy4pw5hOaTOCxA1dxrstWPhdKHOyYNtZDMFopn8yAIGzPE3z6UQ4bABqIlz8tpEEy13Eg= + - secure: f7LJkl/HyS/YghhADhVSNZjOsYkzuWXL9kqYAqjTxzB4g/2HPxkyjGjtlUHv82MvjvoIs7icDNjIveVf0ZP/is5t5T88HXfobYIW1H4hexXbN94oLJTaehdlkv0Fdi9Rfm7sBBH+tyd9HwS25R9MMSndRkxcnr78huFP8iWgRFFF4dDHyh8Dozzo+tHbrPPJtt8iJPz1Fs8U3nMQOdwHj8cA3fZXg65S7IzeavGAnkxtUDV22FgJd0PLD2i2RHAXAxjpmjLY3GIZDquSAO9DWJGElNRphg91nIT726vylm6IZ0JQ0H13hXbVfD4akYYb9ipL8CS/FzZexArtd1cg/nJKSz4G6AJyfByw0KqJwUgaPhvhlCeG8RDse4CH9dH7d8ODbUpxkcvh3Z31EUECY/2BC7Ma4ieLC4VAMBBgc21XxMPklt5z+wW8v2mblVudTSIRnXLL3QiiM2SHV0PLHyH7TPGiKU92J5HrhT3cKkrUMwcePLb6GUBKg6tSiF52DEiiGXWDWbj3rMEnwRbvAa5QkHNhVLwre1tsOt8nCX6cK3rY5fygR3W7lfzMQGMs/XzMdMaqFamJ0YRqUMh2msL0SdzltqFsXlS80JWS8U3zM1IlS448/qmhHzDjyV5mW/bd5iC7uDPQ/5rJfSaicfS1pNNp83Ufg7byNVaIbL4= diff --git a/CHANGELOG.md b/CHANGELOG.md index 5add718a..665be456 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,12 @@ # Changelog +## v1.2.0 +* Support for APNS JWT configuration +* Bump `kadabra` dependency to `v0.4.2` + ## v1.1.6 * Relax `gen_stage` dependency to `~> 0.12` -* Bump `kadabra` dependency to 0.3.7 +* Bump `kadabra` dependency to `v0.3.7` ## v1.1.5 * Fix: relax `httpoison` dependency to allow `0.x` or `1.0` diff --git a/README.md b/README.md index 9b254f34..0f188185 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@ Add pigeon and kadabra as `mix.exs` dependencies: ```elixir def deps do [ - {:pigeon, "~> 1.1.6"}, - {:kadabra, "~> 0.3.7"} + {:pigeon, "~> 1.2.0"}, + {:kadabra, "~> 0.4.2"} ] end ``` @@ -39,6 +39,22 @@ Add pigeon and kadabra as `mix.exs` dependencies: * Full-text string of the file contents * `{:my_app, "certs/cert.pem"}` (indicates path relative to the `priv` folder of the given application) + Alternatively, you can use token based authentication: + + ```elixir + config :pigeon, :apns, + apns_default: %{ + key: "AuthKey.p8", + key_identifier: "ABC1234567", + team_id: "DEF8901234", + mode: :dev + } + ``` + + * `:key` - Created and downloaded via your developer account. Like `:cert` this can be a file path, file contents string or tuple + * `:key_identifier` - The 10-character key identifier associated with `:key`, obtained from your developer account + * `:team_id` - Your 10-character Team ID, obtained from your developer account + 2. Create a notification packet. **Note: Your push topic is generally the app's bundle identifier.** ```elixir diff --git a/cert_key.tar.enc b/cert_key.tar.enc deleted file mode 100644 index 6653fac2..00000000 Binary files a/cert_key.tar.enc and /dev/null differ diff --git a/config/config.exs b/config/config.exs index a7be61a8..75e524c5 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,2 +1,2 @@ use Mix.Config -import_config "#{Mix.env}.exs" +import_config "#{Mix.env()}.exs" diff --git a/config/test.exs b/config/test.exs index eb53e5f4..6aebf897 100644 --- a/config/test.exs +++ b/config/test.exs @@ -8,11 +8,14 @@ config :pigeon, :test, apns_key: "key_unencrypted.pem", apns_topic: System.get_env("APNS_TOPIC") -config :pigeon, workers: [ - {Pigeon.TestConfig, :apns_dynamic}, - {Pigeon.TestConfig, :fcm_dynamic}, - {Pigeon.TestConfig, :adm_dynamic} -] +config :pigeon, + debug_log: true, + workers: [ + {Pigeon.TestConfig, :apns_dynamic}, + {Pigeon.TestConfig, :apns_jwt_dynamic}, + {Pigeon.TestConfig, :fcm_dynamic}, + {Pigeon.TestConfig, :adm_dynamic} + ] config :pigeon, :fcm, fcm_default: %{ @@ -24,6 +27,12 @@ config :pigeon, :apns, cert: "cert.pem", key: "key_unencrypted.pem", mode: :dev + }, + apns_jwt_static: %{ + key: "AuthKey.p8", + key_identifier: System.get_env("APNS_JWT_KEY_IDENTIFIER"), + team_id: System.get_env("APNS_JWT_TEAM_ID"), + mode: :dev } config :pigeon, :adm, diff --git a/docs/APNS Apple iOS.md b/docs/APNS Apple iOS.md index 79c3e166..4b078aa2 100644 --- a/docs/APNS Apple iOS.md +++ b/docs/APNS Apple iOS.md @@ -18,6 +18,22 @@ * Full-text string of the file contents (useful for environment variables) * `{:my_app, "certs/cert.pem"}` (indicates path relative to the `priv` folder of the given application) + Alternatively, you can use token based authentication: + + ```elixir + config :pigeon, :apns, + apns_default: %{ + key: "AuthKey.p8", + key_identifier: "ABC1234567", + team_id: "DEF8901234", + mode: :dev + } + ``` + + * `:key` - Created and downloaded via your developer account. Like `:cert` this can be a file path, file contents string or tuple + * `:key_identifier` - The 10-character key identifier associated with `:key`, obtained from your developer account + * `:team_id` - Your 10-character Team ID, obtained from your developer account + 2. Create a notification packet. **Note: Your push topic is generally the app's bundle identifier.** ```elixir diff --git a/docs/Getting Started.md b/docs/Getting Started.md index b548d087..a27dbd62 100644 --- a/docs/Getting Started.md +++ b/docs/Getting Started.md @@ -2,9 +2,9 @@ > HTTP2-compliant wrapper for sending iOS and Android push notifications. [![Build Status](https://travis-ci.org/codedge-llc/pigeon.svg?branch=master)](https://travis-ci.org/codedge-llc/pigeon) -[![Coverage Status](https://coveralls.io/repos/github/codedge-llc/pigeon/badge.svg?branch=v1.1.0)](https://coveralls.io/github/codedge-llc/pigeon) -[![Hex.pm](http://img.shields.io/hexpm/v/pigeon.svg)](https://hex.pm/packages/pigeon) [![Hex.pm](http://img.shields.io/hexpm/dt/pigeon.svg)](https://hex.pm/packages/pigeon) -[![Deps Status](https://beta.hexfaktor.org/badge/all/github/codedge-llc/pigeon.svg)](https://beta.hexfaktor.org/github/codedge-llc/pigeon) +[![Coverage Status](https://coveralls.io/repos/github/codedge-llc/pigeon/badge.svg)](https://coveralls.io/github/codedge-llc/pigeon) +[![Hex.pm](http://img.shields.io/hexpm/v/pigeon.svg)](https://hex.pm/packages/pigeon) +[![Hex.pm](http://img.shields.io/hexpm/dt/pigeon.svg)](https://hex.pm/packages/pigeon) ## Installation @@ -12,8 +12,8 @@ Add pigeon and kadabra as `mix.exs` dependencies: ```elixir def deps do [ - {:pigeon, "~> 1.1.6"}, - {:kadabra, "~> 0.3.7"} + {:pigeon, "~> 1.2.0"}, + {:kadabra, "~> 0.4.2"} ] end ``` diff --git a/lib/pigeon.ex b/lib/pigeon.ex index cb663f20..88b651ba 100644 --- a/lib/pigeon.ex +++ b/lib/pigeon.ex @@ -19,11 +19,19 @@ defmodule Pigeon do end defp workers do - adm_workers() - ++ apns_workers() - ++ fcm_workers() - ++ env_workers() - ++ task_supervisors() + [ + adm_workers(), + apns_workers(), + fcm_workers(), + env_workers(), + apns_token_agent(), + task_supervisors() + ] + |> List.flatten() + end + + defp apns_token_agent do + [worker(APNS.Token, [%{}], restart: :permanent, shutdown: 5_000)] end defp task_supervisors do @@ -32,9 +40,11 @@ defmodule Pigeon do defp env_workers do case Application.get_env(:pigeon, :workers) do - nil -> [] + nil -> + [] + workers -> - Enum.map(workers, fn({mod, fun}) -> + Enum.map(workers, fn {mod, fun} -> config = apply(mod, fun, []) worker(config) end) @@ -44,6 +54,7 @@ defmodule Pigeon do defp worker(%ADM.Config{} = config) do worker(ADM.Worker, [config], id: config.name, restart: :temporary) end + defp worker(config) do worker(Pigeon.Worker, [config], id: config.name, restart: :temporary) end @@ -53,7 +64,7 @@ defmodule Pigeon do end defp apns_workers do - workers_for(:apns, &APNS.Config.new/1, Pigeon.Worker) + workers_for(:apns, &APNS.ConfigParser.parse/1, Pigeon.Worker) end defp fcm_workers do @@ -62,9 +73,11 @@ defmodule Pigeon do defp workers_for(name, config_fn, mod) do case Application.get_env(:pigeon, name) do - nil -> [] + nil -> + [] + workers -> - Enum.map(workers, fn({worker_name, _config}) -> + Enum.map(workers, fn {worker_name, _config} -> config = config_fn.(worker_name) worker(mod, [config], id: config.name, restart: :temporary) end) @@ -73,7 +86,7 @@ defmodule Pigeon do @doc false def start_connection(state) do - opts = [restart: :temporary, id: :erlang.make_ref] + opts = [restart: :temporary, id: :erlang.make_ref()] spec = worker(Pigeon.Connection, [state], opts) Supervisor.start_child(:pigeon, spec) end diff --git a/lib/pigeon/adm.ex b/lib/pigeon/adm.ex index d65e1915..3a8342ed 100644 --- a/lib/pigeon/adm.ex +++ b/lib/pigeon/adm.ex @@ -26,7 +26,7 @@ defmodule Pigeon.ADM do n = Pigeon.ADM.Notification.new("token", %{"message" => "test"}) Pigeon.ADM.push(n, on_response: handler) """ - @type on_response :: ((Notification.t) -> no_return) + @type on_response :: (Notification.t() -> no_return) @typedoc ~S""" Options for sending push notifications. @@ -36,12 +36,13 @@ defmodule Pigeon.ADM do See `t:on_response/0` """ @type push_opts :: [ - to: atom | pid | nil, - on_response: on_response | nil - ] + to: atom | pid | nil, + on_response: on_response | nil + ] - @type connection_response :: {:ok, pid} - | {:error, {:already_started, pid}} + @type connection_response :: + {:ok, pid} + | {:error, {:already_started, pid}} @default_timeout 5_000 @@ -87,28 +88,30 @@ defmodule Pigeon.ADM do iex> notif.response :timeout """ - @spec push(Notification.t | [Notification.t], Keyword.t) :: no_return + @spec push(Notification.t() | [Notification.t()], Keyword.t()) :: no_return def push(notifications, opts \\ []) + def push(notifications, opts) when is_list(notifications) do - worker_name = opts[:to] || Config.default_name + worker_name = opts[:to] || Config.default_name() if Keyword.has_key?(opts, :on_response) do cast_push(worker_name, notifications, opts[:on_response]) else notifications - |> Enum.map(& Task.async(fn -> sync_push(worker_name, &1) end)) + |> Enum.map(&Task.async(fn -> sync_push(worker_name, &1) end)) |> Task.yield_many(@default_timeout + 500) |> Enum.map(fn {task, response} -> - case response do - nil -> Task.shutdown(task, :brutal_kill) - {:ok, resp} -> resp - _error -> nil - end - end) + case response do + nil -> Task.shutdown(task, :brutal_kill) + {:ok, resp} -> resp + _error -> nil + end + end) end end + def push(notification, opts) do - worker_name = opts[:to] || Config.default_name + worker_name = opts[:to] || Config.default_name() if Keyword.has_key?(opts, :on_response) do cast_push(worker_name, notification, opts[:on_response]) @@ -117,17 +120,19 @@ defmodule Pigeon.ADM do end end - defp cast_push(worker_name, notifications, on_response) when is_list(notifications) do + defp cast_push(worker_name, notifications, on_response) + when is_list(notifications) do for n <- notifications, do: cast_push(worker_name, n, on_response) end + defp cast_push(worker_name, notification, on_response) do GenServer.cast(worker_name, {:push, :adm, notification, on_response}) end defp sync_push(worker_name, notification) do pid = self() - ref = :erlang.make_ref - on_response = fn(x) -> send pid, {ref, x} end + ref = :erlang.make_ref() + on_response = fn x -> send(pid, {ref, x}) end GenServer.cast(worker_name, {:push, :adm, notification, on_response}) @@ -148,17 +153,19 @@ defmodule Pigeon.ADM do iex> Process.alive?(pid) true """ - @spec start_connection(atom | Config.t | Keyword.t) :: connection_response + @spec start_connection(atom | Config.t() | Keyword.t()) :: connection_response def start_connection(name) when is_atom(name) do config = Config.new(name) Supervisor.start_child(:pigeon, worker(Worker, [config], id: name)) end + def start_connection(%Config{} = config) do Worker.start_link(config) end + def start_connection(opts) when is_list(opts) do opts - |> Config.new + |> Config.new() |> start_connection() end diff --git a/lib/pigeon/adm/config.ex b/lib/pigeon/adm/config.ex index 0adf32a5..36586446 100644 --- a/lib/pigeon/adm/config.ex +++ b/lib/pigeon/adm/config.ex @@ -6,10 +6,10 @@ defmodule Pigeon.ADM.Config do defstruct name: nil, client_id: nil, client_secret: nil @type t :: %__MODULE__{ - client_id: String.t | nil, - client_secret: String.t | nil, - name: atom | nil - } + client_id: String.t() | nil, + client_secret: String.t() | nil, + name: atom | nil + } def default_name, do: :adm_default @@ -26,7 +26,7 @@ defmodule Pigeon.ADM.Config do %Pigeon.ADM.Config{name: :test, client_id: "amzn.client.id", client_secret: "1234secret"} """ - @spec new(Keyword.t | atom) :: t + @spec new(Keyword.t() | atom) :: t def new(opts) when is_list(opts) do %__MODULE__{ name: opts[:name], @@ -34,9 +34,10 @@ defmodule Pigeon.ADM.Config do client_secret: opts[:client_secret] } end + def new(name) when is_atom(name) do Application.get_env(:pigeon, :adm)[name] - |> Enum.to_list + |> Enum.to_list() |> Keyword.put(:name, name) |> new() end diff --git a/lib/pigeon/adm/notification.ex b/lib/pigeon/adm/notification.ex index 075805d7..e5219d49 100644 --- a/lib/pigeon/adm/notification.ex +++ b/lib/pigeon/adm/notification.ex @@ -29,14 +29,14 @@ defmodule Pigeon.ADM.Notification do } """ @type t :: %__MODULE__{ - consolidation_key: String.t, - expires_after: integer, - md5: binary, - payload: %{}, - registration_id: String.t, - response: response, - updated_registration_id: String.t - } + consolidation_key: String.t(), + expires_after: integer, + md5: binary, + payload: %{}, + registration_id: String.t(), + response: response, + updated_registration_id: String.t() + } @typedoc ~S""" ADM push response @@ -52,16 +52,17 @@ defmodule Pigeon.ADM.Notification do @typedoc ~S""" ADM error responses """ - @type error_response :: :access_token_expired - | :invalid_registration_id - | :invalid_data - | :invalid_consolidation_key - | :invalid_expiration - | :invalid_checksum - | :invalid_type - | :max_rate_exceeded - | :message_too_large - | :unregistered + @type error_response :: + :access_token_expired + | :invalid_registration_id + | :invalid_data + | :invalid_consolidation_key + | :invalid_expiration + | :invalid_checksum + | :invalid_type + | :max_rate_exceeded + | :message_too_large + | :unregistered @doc ~S""" Creates `ADM.Notification` struct with device registration ID and optional data payload. @@ -97,7 +98,7 @@ defmodule Pigeon.ADM.Notification do updated_registration_id: nil } """ - @spec new(String.t, %{required(String.t) => term}) :: t + @spec new(String.t(), %{required(String.t()) => term}) :: t def new(registration_id, data \\ %{}) do %Pigeon.ADM.Notification{registration_id: registration_id} |> put_data(data) @@ -130,6 +131,7 @@ defmodule Pigeon.ADM.Notification do payload = notification.payload |> Map.put(key, value) + %{notification | payload: payload} end @@ -139,20 +141,23 @@ defmodule Pigeon.ADM.Notification do |> Enum.map(fn {key, value} -> {"#{key}", "#{value}"} end) |> Enum.into(%{}) end + def ensure_strings(_else), do: %{} @doc false - def calculate_md5(%{payload: %{"data" => data}} = notification) when is_map(data) do + def calculate_md5(%{payload: %{"data" => data}} = notification) + when is_map(data) do concat = data - |> Map.keys - |> Enum.sort + |> Map.keys() + |> Enum.sort() |> Enum.map(fn key -> "#{key}:#{data[key]}" end) |> Enum.join(",") - md5 = :md5 |> :crypto.hash(concat) |> Base.encode64 + md5 = :md5 |> :crypto.hash(concat) |> Base.encode64() %{notification | md5: md5} end + def calculate_md5(notification), do: notification end diff --git a/lib/pigeon/adm/result_parser.ex b/lib/pigeon/adm/result_parser.ex index 08e6912c..48d1165b 100644 --- a/lib/pigeon/adm/result_parser.ex +++ b/lib/pigeon/adm/result_parser.ex @@ -25,7 +25,7 @@ defmodule Pigeon.ADM.ResultParser do end def parse(notification, %{"reason" => error}, on_response) do - error = error |> Macro.underscore |> String.to_existing_atom + error = error |> Macro.underscore() |> String.to_existing_atom() n = %{notification | response: error} on_response.(n) end diff --git a/lib/pigeon/adm/worker.ex b/lib/pigeon/adm/worker.ex index fb46687d..133d6fd3 100644 --- a/lib/pigeon/adm/worker.ex +++ b/lib/pigeon/adm/worker.ex @@ -24,13 +24,14 @@ defmodule Pigeon.ADM.Worker do def init({:ok, config}), do: initialize_worker(config) def initialize_worker(config) do - {:ok, %{ - config: config, - access_token: nil, - access_token_refreshed_datetime_erl: {{0, 0, 0}, {0, 0, 0}}, - access_token_expiration_seconds: 0, - access_token_type: nil - }} + {:ok, + %{ + config: config, + access_token: nil, + access_token_refreshed_datetime_erl: {{0, 0, 0}, {0, 0, 0}}, + access_token_expiration_seconds: 0, + access_token_type: nil + }} end def handle_cast(:stop, state) do @@ -42,6 +43,7 @@ defmodule Pigeon.ADM.Worker do {:ok, state} -> :ok = do_push(notification, state, on_response) {:noreply, state} + {:error, reason} -> notification = %{notification | response: reason} process_on_response(on_response, notification) @@ -59,14 +61,17 @@ defmodule Pigeon.ADM.Worker do cond do is_nil(access_token) -> refresh_access_token(state) + access_token_expired?(access_ref_dt_erl, access_ref_exp_secs) -> refresh_access_token(state) + true -> {:ok, state} end end defp access_token_expired?(_refreshed_datetime_erl, 0), do: true + defp access_token_expired?(refreshed_datetime_erl, expiration_seconds) do seconds_since(refreshed_datetime_erl) >= expiration_seconds end @@ -74,23 +79,28 @@ defmodule Pigeon.ADM.Worker do defp seconds_since(datetime_erl) do gregorian_seconds = datetime_erl - |> :calendar.datetime_to_gregorian_seconds + |> :calendar.datetime_to_gregorian_seconds() now_gregorian_seconds = - :os.timestamp - |> :calendar.now_to_universal_time - |> :calendar.datetime_to_gregorian_seconds + :os.timestamp() + |> :calendar.now_to_universal_time() + |> :calendar.datetime_to_gregorian_seconds() now_gregorian_seconds - gregorian_seconds end defp refresh_access_token(state) do - post = HTTPoison.post(@token_refresh_uri, - token_refresh_body(state), - token_refresh_headers()) + post = + HTTPoison.post( + @token_refresh_uri, + token_refresh_body(state), + token_refresh_headers() + ) + case post do {:ok, %{status_code: 200, body: response_body}} -> {:ok, response_json} = Poison.decode(response_body) + %{ "access_token" => access_token, "expires_in" => expiration_seconds, @@ -98,29 +108,34 @@ defmodule Pigeon.ADM.Worker do "token_type" => token_type } = response_json - now_datetime_erl = :os.timestamp |> :calendar.now_to_universal_time + now_datetime_erl = :os.timestamp() |> :calendar.now_to_universal_time() - {:ok, %{state | access_token: access_token, - access_token_refreshed_datetime_erl: now_datetime_erl, - access_token_expiration_seconds: expiration_seconds, - access_token_type: token_type}} + {:ok, + %{ + state + | access_token: access_token, + access_token_refreshed_datetime_erl: now_datetime_erl, + access_token_expiration_seconds: expiration_seconds, + access_token_type: token_type + }} {:ok, %{body: response_body}} -> {:ok, response_json} = Poison.decode(response_body) - Logger.error "Refresh token response: #{inspect response_json}" + Logger.error("Refresh token response: #{inspect(response_json)}") {:error, response_json["reason"]} end end - defp token_refresh_body(%{config: %{client_id: client_id, - client_secret: client_secret}}) do + defp token_refresh_body(%{ + config: %{client_id: client_id, client_secret: client_secret} + }) do %{ "grant_type" => "client_credentials", "scope" => "messaging:push", "client_id" => client_id, "client_secret" => client_secret } - |> URI.encode_query + |> URI.encode_query() end defp token_refresh_headers do @@ -133,11 +148,12 @@ defmodule Pigeon.ADM.Worker do response = case on_response do nil -> - fn({reg_id, payload}) -> + fn {reg_id, payload} -> HTTPoison.post(adm_uri(reg_id), payload, adm_headers(state)) end + _ -> - fn({reg_id, payload}) -> + fn {reg_id, payload} -> {:ok, %HTTPoison.Response{status_code: status, body: body}} = HTTPoison.post(adm_uri(reg_id), payload, adm_headers(state)) @@ -145,6 +161,7 @@ defmodule Pigeon.ADM.Worker do process_response(status, body, notification, on_response) end end + Task.Supervisor.start_child(Pigeon.Tasks, fn -> response.(request) end) :ok end @@ -154,11 +171,13 @@ defmodule Pigeon.ADM.Worker do end defp adm_headers(%{access_token: access_token, access_token_type: token_type}) do - [{"Authorization", "#{token_type} #{access_token}"}, - {"Content-Type", "application/json"}, - {"X-Amzn-Type-Version", "com.amazon.device.messaging.ADMMessage@1.0"}, - {"Accept", "application/json"}, - {"X-Amzn-Accept-Type", "com.amazon.device.messaging.ADMSendResult@1.0"}] + [ + {"Authorization", "#{token_type} #{access_token}"}, + {"Content-Type", "application/json"}, + {"X-Amzn-Type-Version", "com.amazon.device.messaging.ADMMessage@1.0"}, + {"Accept", "application/json"}, + {"X-Amzn-Accept-Type", "com.amazon.device.messaging.ADMSendResult@1.0"} + ] end defp encode_payload(notification) do @@ -166,26 +185,30 @@ defmodule Pigeon.ADM.Worker do |> put_consolidation_key(notification.consolidation_key) |> put_expires_after(notification.expires_after) |> put_md5(notification.md5) - |> Poison.encode! + |> Poison.encode!() end defp put_consolidation_key(payload, nil), do: payload + defp put_consolidation_key(payload, consolidation_key) do payload |> Map.put("consolidationKey", consolidation_key) end defp put_expires_after(payload, nil), do: payload + defp put_expires_after(payload, expires_after) do payload |> Map.put("expiresAfter", expires_after) end defp put_md5(payload, nil), do: payload + defp put_md5(payload, md5) do payload |> Map.put("md5", md5) end defp process_response(200, body, notification, on_response), do: handle_200_status(body, notification, on_response) + defp process_response(status, body, notification, on_response), do: handle_error_status_code(status, body, notification, on_response) @@ -198,6 +221,7 @@ defmodule Pigeon.ADM.Worker do case Poison.decode(body) do {:ok, %{"reason" => _reason} = result_json} -> parse_result(notification, result_json, on_response) + {:error, _} -> n = %{notification | response: generic_error_reason(status)} process_on_response(on_response, n) diff --git a/lib/pigeon/apns.ex b/lib/pigeon/apns.ex index fbd4b9e9..7b8a0c31 100644 --- a/lib/pigeon/apns.ex +++ b/lib/pigeon/apns.ex @@ -12,7 +12,7 @@ defmodule Pigeon.APNS do @typedoc ~S""" Can be either a single notification or a list. """ - @type notification :: Notification.t | [Notification.t, ...] + @type notification :: Notification.t() | [Notification.t(), ...] @typedoc ~S""" Async callback for push notification response. @@ -33,7 +33,7 @@ defmodule Pigeon.APNS do n = Pigeon.APNS.Notification.new("msg", "device token", "push topic") Pigeon.APNS.push(n, on_response: handler) """ - @type on_response :: ((Notification.t) -> no_return) + @type on_response :: (Notification.t() -> no_return) @typedoc ~S""" Options for sending push notifications. @@ -43,9 +43,9 @@ defmodule Pigeon.APNS do See `t:on_response/0` """ @type push_opts :: [ - to: atom | pid | nil, - on_response: on_response | nil - ] + to: atom | pid | nil, + on_response: on_response | nil + ] @default_timeout 5_000 @@ -73,25 +73,27 @@ defmodule Pigeon.APNS do response: :bad_device_token, id: nil, payload: %{"aps" => %{"alert" => "msg"}}, topic: "topic"}] """ - @spec push(notification, push_opts) :: {:ok, term} | {:error, term, term} + @spec push(notification, push_opts) :: notification | :ok def push(notification, opts \\ []) + def push(notification, opts) when is_list(notification) do if Keyword.has_key?(opts, :on_response) do push(notification, opts[:on_response], opts) :ok else notification - |> Enum.map(& Task.async(fn -> sync_push(&1, opts) end)) + |> Enum.map(&Task.async(fn -> sync_push(&1, opts) end)) |> Task.yield_many(@default_timeout + 500) |> Enum.map(fn {task, response} -> - case response do - nil -> Task.shutdown(task, :brutal_kill) - {:ok, resp} -> resp - _error -> nil - end - end) + case response do + nil -> Task.shutdown(task, :brutal_kill) + {:ok, resp} -> resp + _error -> nil + end + end) end end + def push(notification, opts) do if Keyword.has_key?(opts, :on_response) do push(notification, opts[:on_response], opts) @@ -104,8 +106,9 @@ defmodule Pigeon.APNS do defp push(notification, on_response, opts) when is_list(notification) do for n <- notification, do: push(n, on_response, opts) end + defp push(notification, on_response, opts) do - worker_name = opts[:to] || Config.default_name + worker_name = opts[:to] || Config.default_name() Worker.send_push(worker_name, notification, on_response: on_response) end @@ -119,17 +122,19 @@ defmodule Pigeon.APNS do iex> Process.alive?(pid) true """ - @spec start_connection(atom | Config.t | Keyword.t) :: {:ok, pid} + @spec start_connection(atom | Config.t() | Keyword.t()) :: {:ok, pid} def start_connection(name) when is_atom(name) do config = Config.new(name) Supervisor.start_child(:pigeon, worker(Pigeon.Worker, [config], id: name)) end - def start_connection(%Config{} = config) do + + def start_connection(%_{} = config) do Worker.start_link(config) end + def start_connection(opts) when is_list(opts) do opts - |> Config.new + |> Config.new() |> start_connection() end @@ -151,10 +156,10 @@ defmodule Pigeon.APNS do defp sync_push(notification, opts) do pid = self() - ref = :erlang.make_ref - on_response = fn(x) -> send pid, {ref, x} end + ref = :erlang.make_ref() + on_response = fn x -> send(pid, {ref, x}) end - worker_name = opts[:to] || Config.default_name + worker_name = opts[:to] || Config.default_name() Worker.send_push(worker_name, notification, on_response: on_response) receive do diff --git a/lib/pigeon/apns/config.ex b/lib/pigeon/apns/config.ex index a2c20170..d8d8162e 100644 --- a/lib/pigeon/apns/config.ex +++ b/lib/pigeon/apns/config.ex @@ -1,6 +1,6 @@ defmodule Pigeon.APNS.Config do - @moduledoc """ - Configuration for APNS Workers + @moduledoc ~S""" + Configuration for APNS Workers using certificates. """ defstruct name: nil, @@ -14,7 +14,7 @@ defmodule Pigeon.APNS.Config do ping_period: 600_000 @typedoc ~S""" - APNS configuration struct + Certificate APNS configuration struct This struct should not be set directly. Instead use `new/1` with `t:config_opts/0`. @@ -34,20 +34,21 @@ defmodule Pigeon.APNS.Config do } """ @type t :: %__MODULE__{ - name: atom | nil, - reconnect: boolean, - cert: binary | nil, - certfile: binary | nil, - key: binary | nil, - keyfile: binary | nil, - uri: binary | nil, - port: pos_integer, - ping_period: pos_integer - } + name: atom | nil, + reconnect: boolean, + cert: binary | nil, + certfile: binary | nil, + key: binary | nil, + keyfile: binary | nil, + uri: binary | nil, + port: pos_integer, + ping_period: pos_integer + } @typedoc ~S""" - Options for configuring APNS connections. + Options for configuring certificate APNS connections. + ## Configuration Options - `:name` - Registered worker name. - `:mode` - If set to `:dev` or `:prod`, will set the appropriate `:uri` - `:cert` - Push certificate. Can be one of three options: @@ -62,20 +63,25 @@ defmodule Pigeon.APNS.Config do `443` and `2197` - `:ping_period` - Interval between server pings. Necessary to keep long running APNS connections alive. Defaults to 10 minutes. + + ## Deprecated Options + - `:reconnect` - No longer used as of `v1.2.0`. """ @type config_opts :: [ - name: atom | nil, - mode: :dev | :prod | nil, - cert: binary | {atom, binary}, - key: binary | {atom, binary}, - reconnect: boolean, - ping_period: pos_integer, - port: pos_integer, - uri: binary - ] - - @apns_production_api_uri "api.push.apple.com" - @apns_development_api_uri "api.development.push.apple.com" + name: atom | nil, + mode: :dev | :prod | nil, + cert: binary | {atom, binary}, + key: binary | {atom, binary}, + reconnect: boolean, + ping_period: pos_integer, + port: pos_integer, + uri: binary, + jwt_key: binary | {atom, binary}, + jwt_key_identifier: binary | nil, + jwt_team_id: binary | nil + ] + + alias Pigeon.APNS.ConfigParser @doc false def default_name, do: :apns_default @@ -83,7 +89,7 @@ defmodule Pigeon.APNS.Config do @doc ~S""" Returns a new `APNS.Config` with given `opts` or name. - If given an atom, returns the config specified in your `mix.exs`. + If given an atom, returns the config specified in your `config.exs`. ## Examples @@ -92,7 +98,6 @@ defmodule Pigeon.APNS.Config do ...> mode: :prod, ...> cert: "test_cert.pem", ...> key: "test_key.pem", - ...> reconnect: false, ...> port: 2197, ...> ping_period: 300_000 ...> ) @@ -101,181 +106,91 @@ defmodule Pigeon.APNS.Config do iex> config = Pigeon.APNS.Config.new(:apns_default) iex> %{config | certfile: nil, keyfile: nil} # Hide for testing - %Pigeon.APNS.Config{uri: "api.development.push.apple.com", - name: :apns_default, ping_period: 600_000, port: 443, reconnect: false} + iex> match? %_{uri: "api.development.push.apple.com", + ...> name: :apns_default, ping_period: 600_000, port: 443}, config + true """ - @spec new(atom | config_opts) :: t def new(opts) when is_list(opts) do %__MODULE__{ name: opts[:name], reconnect: Keyword.get(opts, :reconnect, false), - cert: cert(opts[:cert]), - certfile: file_path(opts[:cert]), - key: key(opts[:key]), - keyfile: file_path(opts[:key]), - uri: Keyword.get(opts, :uri, uri_for_mode(opts[:mode])), + cert: ConfigParser.cert(opts[:cert]), + certfile: ConfigParser.file_path(opts[:cert]), + key: ConfigParser.key(opts[:key]), + keyfile: ConfigParser.file_path(opts[:key]), + uri: Keyword.get(opts, :uri, ConfigParser.uri_for_mode(opts[:mode])), port: Keyword.get(opts, :port, 443), ping_period: Keyword.get(opts, :ping_period, 600_000) } end - def new(name) when is_atom(name) do - Application.get_env(:pigeon, :apns)[name] - |> Enum.to_list - |> Keyword.put(:name, name) - |> new() - end - - defp uri_for_mode(:dev), do: @apns_development_api_uri - defp uri_for_mode(:prod), do: @apns_production_api_uri - defp uri_for_mode(_else), do: nil - @doc false - def file_path(nil), do: nil - def file_path(path) when is_binary(path) do - if :filelib.is_file(path), do: Path.expand(path), else: nil - end - def file_path({app_name, path}) when is_atom(app_name), - do: Path.expand(path, :code.priv_dir(app_name)) - - @doc false - def cert({_app_name, _path}), do: nil - def cert(nil), do: nil - def cert(bin) do - case :public_key.pem_decode(bin) do - [{:Certificate, cert, _}] -> cert - _ -> nil - end - end - - @doc false - def key({_app_name, _path}), do: nil - def key(nil), do: nil - def key(bin) do - case :public_key.pem_decode(bin) do - [{:RSAPrivateKey, key, _}] -> {:RSAPrivateKey, key} - _ -> nil - end - end + def new(name) when is_atom(name), do: ConfigParser.parse(name) end defimpl Pigeon.Configurable, for: Pigeon.APNS.Config do @moduledoc false - import Pigeon.Tasks, only: [process_on_response: 2] - - alias Pigeon.APNS.{Config, Error} + alias Pigeon.APNS.Shared @type sock :: {:sslsocket, any, pid | {any, any}} # Configurable Callbacks - @spec worker_name(any) :: atom | nil - def worker_name(%Config{name: name}), do: name + defdelegate worker_name(any), to: Shared - @spec max_demand(any) :: non_neg_integer - def max_demand(_config), do: 1_000 + defdelegate max_demand(any), to: Shared - @spec connect(any) :: {:ok, sock} | {:error, String.t} - def connect(%Config{uri: uri} = config) do + @spec connect(any) :: {:ok, sock} | {:error, String.t()} + def connect(%{uri: uri} = config) do uri = to_charlist(uri) + case connect_socket_options(config) do {:ok, options} -> Pigeon.Http2.Client.default().connect(uri, :https, options) - error -> error - end - end - def push_headers(_config, notification, _opts) do - json = Poison.encode!(notification.payload) - - [ - {":method", "POST"}, - {":path", "/3/device/#{notification.device_token}"}, - {"content-length", "#{byte_size(json)}"} - ] - |> put_apns_id(notification) - |> put_apns_topic(notification) - end - - def push_payload(_config, notification, _opts) do - Poison.encode!(notification.payload) - end - - defp put_apns_id(headers, notification) do - case notification.id do - nil -> headers - id -> headers ++ [{"apns-id", id}] + error -> + error end end - defp put_apns_topic(headers, notification) do - case notification.topic do - nil -> headers - topic -> headers ++ [{"apns-topic", topic}] - end - end + defdelegate push_headers(config, notification, opts), to: Shared - def handle_end_stream(_config, - %{headers: headers, body: body, status: status}, - notification, - on_response) do - case status do - 200 -> - n = %{notification | id: get_apns_id(headers), response: :success} - process_on_response(on_response, n) - _error -> - reason = Error.parse(body) - Error.log(reason, notification) - notification = %{notification | response: reason} - process_on_response(on_response, notification) - end - end + defdelegate push_payload(config, notification, opts), to: Shared - def get_apns_id(headers) do - case Enum.find(headers, fn({key, _val}) -> key == "apns-id" end) do - {"apns-id", id} -> id - nil -> nil - end - end - - @spec schedule_ping(any) :: no_return - def schedule_ping(%Config{ping_period: ping}) do - Process.send_after(self(), :ping, ping) - end + defdelegate handle_end_stream(config, stream, notification, on_response), + to: Shared - def close(_config) do - end + defdelegate schedule_ping(any), to: Shared - # Everything Else + defdelegate close(config), to: Shared - def connect_socket_options(%Config{cert: nil, certfile: nil}) do + def connect_socket_options(%{cert: nil, certfile: nil}) do {:error, :invalid_config} end - def connect_socket_options(%Config{key: nil, keyfile: nil}) do + + def connect_socket_options(%{key: nil, keyfile: nil}) do {:error, :invalid_config} end + def connect_socket_options(config) do - options = [ - cert_option(config), - key_option(config), - {:password, ''}, - {:packet, 0}, - {:reuseaddr, true}, - {:active, true}, - {:reconnect, config.reconnect}, - :binary - ] - |> add_port(config) + options = + [ + cert_option(config), + key_option(config), + {:password, ''}, + {:packet, 0}, + {:reuseaddr, true}, + {:active, true}, + :binary + ] + |> Shared.add_port(config) {:ok, options} end - def cert_option(%Config{cert: cert, certfile: nil}), do: {:cert, cert} - def cert_option(%Config{cert: nil, certfile: file}), do: {:certfile, file} - - def key_option(%Config{key: key, keyfile: nil}), do: {:key, key} - def key_option(%Config{key: nil, keyfile: file}), do: {:keyfile, file} + def cert_option(%{cert: cert, certfile: nil}), do: {:cert, cert} + def cert_option(%{cert: nil, certfile: file}), do: {:certfile, file} - defp add_port(opts, %Config{port: 443}), do: opts - defp add_port(opts, %Config{port: port}), do: [{:port, port} | opts] + def key_option(%{key: key, keyfile: nil}), do: {:key, key} + def key_option(%{key: nil, keyfile: file}), do: {:keyfile, file} end diff --git a/lib/pigeon/apns/config_parser.ex b/lib/pigeon/apns/config_parser.ex new file mode 100644 index 00000000..5d2e78b4 --- /dev/null +++ b/lib/pigeon/apns/config_parser.ex @@ -0,0 +1,79 @@ +defmodule Pigeon.APNS.ConfigParser do + @moduledoc false + + alias Pigeon.APNS.{Config, JWTConfig} + + @type config_opts :: [ + name: atom | nil, + mode: :dev | :prod | nil, + cert: binary | {atom, binary}, + key: binary | {atom, binary}, + reconnect: boolean, + ping_period: pos_integer, + port: pos_integer, + uri: binary, + key_identifier: binary | nil, + team_id: binary | nil + ] + + @type config :: Config.t() | JWTConfig.t() + + @apns_production_api_uri "api.push.apple.com" + @apns_development_api_uri "api.development.push.apple.com" + + @spec parse(atom | config_opts) :: config | {:error, :invalid_config} + def parse(opts) when is_list(opts) do + case config_type(Enum.into(opts, %{})) do + :error -> raise "invalid apns configuration #{inspect(opts)}" + type -> type.new(opts) + end + end + + def parse(name) when is_atom(name) do + Application.get_env(:pigeon, :apns)[name] + |> Enum.to_list() + |> Keyword.put(:name, name) + |> parse() + end + + defp config_type(%{cert: _cert, key_identifier: _key_id}), do: :error + defp config_type(%{cert: _cert}), do: Config + defp config_type(%{key_identifier: _jwt_key}), do: JWTConfig + defp config_type(_else), do: :error + + @doc false + def file_path(nil), do: nil + + def file_path(path) when is_binary(path) do + if :filelib.is_file(path), do: Path.expand(path), else: nil + end + + def file_path({app_name, path}) when is_atom(app_name), + do: Path.expand(path, :code.priv_dir(app_name)) + + @doc false + def cert({_app_name, _path}), do: nil + def cert(nil), do: nil + + def cert(bin) do + case :public_key.pem_decode(bin) do + [{:Certificate, cert, _}] -> cert + _ -> nil + end + end + + @doc false + def key({_app_name, _path}), do: nil + def key(nil), do: nil + + def key(bin) do + case :public_key.pem_decode(bin) do + [{:RSAPrivateKey, key, _}] -> {:RSAPrivateKey, key} + _ -> nil + end + end + + def uri_for_mode(:dev), do: @apns_development_api_uri + def uri_for_mode(:prod), do: @apns_production_api_uri + def uri_for_mode(_else), do: nil +end diff --git a/lib/pigeon/apns/error.ex b/lib/pigeon/apns/error.ex index cdfe81fc..d4ef881c 100644 --- a/lib/pigeon/apns/error.ex +++ b/lib/pigeon/apns/error.ex @@ -7,79 +7,86 @@ defmodule Pigeon.APNS.Error do alias Pigeon.APNS.Notification - @type error_response :: :bad_collapse_id - | :bad_device_token - | :bad_expiration_date - | :bad_message_id - | :bad_priority - | :bad_topic - | :device_token_not_for_topic - | :duplicate_headers - | :idle_timeout - | :missing_device_token - | :missing_topic - | :payload_empty - | :topic_disallowed - | :bad_certificate - | :bad_certificate_environment - | :expired_provider_token - | :forbidden - | :invalid_provider_token - | :missing_provider_token - | :bad_path - | :method_not_allowed - | :unregistered - | :payload_too_large - | :too_many_provider_token_updates - | :too_many_requests - | :internal_server_error - | :service_unavailable - | :shutdown + @type error_response :: + :bad_collapse_id + | :bad_device_token + | :bad_expiration_date + | :bad_message_id + | :bad_priority + | :bad_topic + | :device_token_not_for_topic + | :duplicate_headers + | :idle_timeout + | :missing_device_token + | :missing_topic + | :payload_empty + | :topic_disallowed + | :bad_certificate + | :bad_certificate_environment + | :expired_provider_token + | :forbidden + | :invalid_provider_token + | :missing_provider_token + | :bad_path + | :method_not_allowed + | :unregistered + | :payload_too_large + | :too_many_provider_token_updates + | :too_many_requests + | :internal_server_error + | :service_unavailable + | :shutdown @doc false @spec parse(binary) :: error_response def parse(data) do {:ok, response} = Poison.decode(data) - response["reason"] |> Macro.underscore |> String.to_existing_atom + response["reason"] |> Macro.underscore() |> String.to_existing_atom() end @doc ~S""" If enabled, logs a notification and its error response. """ - @spec log(error_response, Notification.t) :: :ok + @spec log(error_response, Notification.t()) :: :ok def log(reason, notification) do - if Pigeon.debug_log? do + if Pigeon.debug_log?() do Logger.error("#{reason}: #{msg(reason)}\n#{inspect(notification)}") end end @doc false - @spec msg(error_response) :: String.t + @spec msg(error_response) :: String.t() # 400 def msg(:bad_collapse_id) do "The collapse identifier exceeds the maximum allowed size" end + def msg(:bad_device_token) do """ The specified device token was bad. Verify that the request contains a valid token and that the token matches the environment. """ end + def msg(:bad_expiration_date), do: "The apns-expiration value is bad." def msg(:bad_message_id), do: "The apns-id value is bad." def msg(:bad_priority), do: "The apns-priority value is bad." def msg(:bad_topic), do: "The apns-topic was invalid." + def msg(:device_token_not_for_topic) do "The device token does not match the specified topic." end + def msg(:duplicate_headers), do: "One or more headers were repeated." def msg(:idle_timeout), do: "Idle time out." + def msg(:missing_device_token) do """ The device token is not specified in the request :path. Verify that the :path header contains the device token. """ end + def msg(:missing_topic) do """ The apns-topic header of the request was not specified and was required. @@ -87,26 +94,32 @@ defmodule Pigeon.APNS.Error do certificate that supports multiple topics. """ end + def msg(:payload_empty), do: "The message payload was empty." def msg(:topic_disallowed), do: "Pushing to this topic is not allowed." # 403 def msg(:bad_certificate), do: "The certificate was bad." + def msg(:bad_certificate_environment) do "The client certificate was for the wrong environment." end + def msg(:expired_provider_token) do "The provider token is stale and a new token should be generated." end + def msg(:forbidden) do "The specified action is not allowed." end + def msg(:invalid_provider_token) do """ The provider token is not valid or the token signature could not be verified." """ end + def msg(:missing_provider_token) do """ No provider certificate was used to connect to APNs and Authorization @@ -132,10 +145,12 @@ defmodule Pigeon.APNS.Error do 4096 bytes. """ end + # 429 def msg(:too_many_provider_token_updates) do "The provider token is being updated too often." end + def msg(:too_many_requests) do "Too many requests were made consecutively to the same device token." end diff --git a/lib/pigeon/apns/jwt_config.ex b/lib/pigeon/apns/jwt_config.ex new file mode 100644 index 00000000..a3fff164 --- /dev/null +++ b/lib/pigeon/apns/jwt_config.ex @@ -0,0 +1,239 @@ +defmodule Pigeon.APNS.JWTConfig do + @moduledoc """ + Configuration for APNS Workers using JWT. + """ + + defstruct name: nil, + reconnect: true, + uri: nil, + port: 443, + ping_period: 600_000, + key: nil, + keyfile: nil, + key_identifier: nil, + team_id: nil + + alias Pigeon.APNS.{Config, ConfigParser} + + @typedoc ~S""" + JWT APNS configuration struct + + This struct should not be set directly. Instead use `new/1` + with `t:config_opts/0`. + + ## Examples + + %Pigeon.APNS.JWTConfig{ + name: :apns_default, + reconnect: true, + uri: "api.push.apple.com", + port: 443, + ping_period: 600_000, + key: nil, + keyfile: "key.p8", + key_identifier: "ABC1234567", + team_id: "DEF1234567" + } + """ + @type t :: %__MODULE__{ + name: atom | nil, + reconnect: boolean, + uri: binary | nil, + port: pos_integer, + ping_period: pos_integer, + key: binary | nil, + keyfile: binary | nil, + key_identifier: binary | nil, + team_id: binary | nil + } + + @typedoc ~S""" + Options for configuring JWT APNS connections. + + ## Configuration Options + - `:name` - Registered worker name. + - `:mode` - If set to `:dev` or `:prod`, will set the appropriate `:uri` + - `:key` - JWT private key. Can be one of three options: + - Static file path + - Full-text string of the file contents (useful for environment variables) + - `{:my_app, "keys/private_key.p8"}` (indicates path relative to the `priv` + folder of the given application) + - `:key_identifier` - A 10-character key identifier (kid) key, obtained from + your developer account + - `:team_id` - Your 10-character Team ID, obtained from your developer account + - `:uri` - Push server uri. If set, overrides uri defined by `:mode`. + Useful for test environments. + - `:port` - Push server port. Can be any value, but APNS only accepts + `443` and `2197` + - `:ping_period` - Interval between server pings. Necessary to keep long + running APNS connections alive. Defaults to 10 minutes. + + ## Deprecated Options + - `:reconnect` - No longer used as of `v1.2.0`. + """ + @type config_opts :: [ + name: atom | nil, + mode: :dev | :prod | nil, + key: binary | {atom, binary}, + key_identifier: binary | nil, + team_id: binary | nil, + reconnect: boolean, + ping_period: pos_integer, + port: pos_integer, + uri: binary + ] + + @doc ~S""" + Returns a new `APNS.JWTConfig` with given `opts` or name. + + If given an atom, returns the config specified in your `config.exs`. + + ## Examples + + iex> Pigeon.APNS.JWTConfig.new( + ...> name: :test, + ...> mode: :prod, + ...> key: "key.p8", + ...> key_identifier: "ABC1234567", + ...> team_id: "DEF1234567", + ...> port: 2197, + ...> ping_period: 300_000 + ...> ) + %Pigeon.APNS.JWTConfig{uri: "api.push.apple.com", name: :test, + team_id: "DEF1234567", key_identifier: "ABC1234567", key: "key.p8", + ping_period: 300000, port: 2197, reconnect: false} + + iex> config = Pigeon.APNS.JWTConfig.new(:apns_jwt_static) + iex> %{config | key: nil, key_identifier: nil, team_id: nil} # Hide for testing + iex> match? %_{uri: "api.development.push.apple.com", name: :apns_jwt_static, + ...> ping_period: 600_000, port: 443, reconnect: false}, config + true + """ + def new(opts) when is_list(opts) do + %__MODULE__{ + name: opts[:name], + reconnect: Keyword.get(opts, :reconnect, false), + uri: Keyword.get(opts, :uri, ConfigParser.uri_for_mode(opts[:mode])), + port: Keyword.get(opts, :port, 443), + ping_period: Keyword.get(opts, :ping_period, 600_000), + key: opts[:key], + keyfile: ConfigParser.file_path(opts[:key]), + key_identifier: Keyword.get(opts, :key_identifier), + team_id: Keyword.get(opts, :team_id) + } + end + + def new(name) when is_atom(name), do: ConfigParser.parse(name) +end + +defimpl Pigeon.Configurable, for: Pigeon.APNS.JWTConfig do + @moduledoc false + + alias Pigeon.APNS.{Config, JWTConfig, Notification, Shared} + + @type sock :: {:sslsocket, any, pid | {any, any}} + + # Seconds + @token_max_age 3_590 + + # Configurable Callbacks + + defdelegate worker_name(any), to: Shared + + defdelegate max_demand(any), to: Shared + + @spec connect(any) :: {:ok, sock} | {:error, String.t()} + def connect(%{uri: uri} = config) do + uri = to_charlist(uri) + + case connect_socket_options(config) do + {:ok, options} -> + Pigeon.Http2.Client.default().connect(uri, :https, options) + + error -> + error + end + end + + @spec push_headers(JWTConfig.t(), Notification.t(), Keyword.t()) :: + Shared.headers() + def push_headers(config, notification, opts) do + config + |> Shared.push_headers(notification, opts) + |> put_bearer_token(config) + end + + defdelegate push_payload(config, notification, opts), to: Shared + + defdelegate handle_end_stream(config, stream, notification, on_response), + to: Shared + + defdelegate schedule_ping(any), to: Shared + + defdelegate close(config), to: Shared + + def connect_socket_options(%{key: nil}) do + {:error, :invalid_config} + end + + def connect_socket_options(%{key: _jwt_key} = config) do + options = + [ + {:packet, 0}, + {:reuseaddr, true}, + {:active, true}, + :binary + ] + |> Shared.add_port(config) + + {:ok, options} + end + + @spec put_bearer_token(Config.headers(), JWTConfig.t()) :: Config.headers() + defp put_bearer_token(headers, %{key: nil}), do: headers + + defp put_bearer_token(headers, config) do + token_storage_key = config.key_identifier <> ":" <> config.team_id + {timestamp, saved_token} = Pigeon.APNS.Token.get(token_storage_key) + now = :os.system_time(:seconds) + + token = + case now - timestamp do + age when age < @token_max_age -> saved_token + _ -> generate_apns_jwt(config, token_storage_key) + end + + [{"authorization", "bearer " <> token} | headers] + end + + @spec generate_apns_jwt(JWTConfig.t(), String.t()) :: String.t() + defp generate_apns_jwt(config, token_storage_key) do + import Joken + + key = get_token_key(config) + + now = :os.system_time(:seconds) + + token = + token() + |> with_claims(%{"iss" => config.team_id, "iat" => now}) + |> with_header_arg("alg", "ES256") + |> with_header_arg("typ", "JWT") + |> with_header_arg("kid", config.key_identifier) + |> sign(es256(key)) + |> get_compact + + :ok = Pigeon.APNS.Token.update(token_storage_key, {now, token}) + + token + end + + @spec get_token_key(JWTConfig.t()) :: JOSE.JWK.t() + defp get_token_key(%JWTConfig{keyfile: nil} = config) do + JOSE.JWK.from_pem(config.key) + end + + defp get_token_key(%JWTConfig{keyfile: file}) do + JOSE.JWK.from_pem_file(file) + end +end diff --git a/lib/pigeon/apns/notification.ex b/lib/pigeon/apns/notification.ex index b418863a..4f467411 100644 --- a/lib/pigeon/apns/notification.ex +++ b/lib/pigeon/apns/notification.ex @@ -27,13 +27,13 @@ defmodule Pigeon.APNS.Notification do } """ @type t :: %__MODULE__{ - device_token: String.t | nil, - expiration: String.t | nil, - id: String.t | nil, - payload: %{String.t => String.t}, - response: response, - topic: String.t | nil - } + device_token: String.t() | nil, + expiration: String.t() | nil, + id: String.t() | nil, + payload: %{String.t() => String.t()}, + response: response, + topic: String.t() | nil + } @typedoc ~S""" APNS push response @@ -44,7 +44,7 @@ defmodule Pigeon.APNS.Notification do server responded with error - `:timeout` - Internal error. Push did not reach APNS servers """ - @type response :: nil | :success | Error.error_response | :timeout + @type response :: nil | :success | Error.error_response() | :timeout @doc """ Returns an `APNS.Notification` struct with given message, device token, and @@ -63,7 +63,7 @@ defmodule Pigeon.APNS.Notification do topic: nil } """ - @spec new(String.t, String.t, String.t | nil) :: t + @spec new(String.t(), String.t(), String.t() | nil) :: t def new(msg, token, topic \\ nil) do %Notification{ device_token: token, @@ -89,7 +89,7 @@ defmodule Pigeon.APNS.Notification do topic: "topic" } """ - @spec new(String.t, String.t, String.t, String.t) :: t + @spec new(String.t(), String.t(), String.t(), String.t()) :: t def new(msg, token, topic, id) do %Notification{ device_token: token, @@ -115,8 +115,9 @@ defmodule Pigeon.APNS.Notification do topic: nil } """ - @spec put_alert(t, String.t) :: t - def put_alert(notification, alert), do: update_payload(notification, "alert", alert) + @spec put_alert(t, String.t()) :: t + def put_alert(notification, alert), + do: update_payload(notification, "alert", alert) @doc """ Updates `"badge"` key in push payload. @@ -135,7 +136,8 @@ defmodule Pigeon.APNS.Notification do } """ @spec put_badge(t, integer) :: t - def put_badge(notification, badge), do: update_payload(notification, "badge", badge) + def put_badge(notification, badge), + do: update_payload(notification, "badge", badge) @doc """ Updates `"sound"` key in push payload. @@ -154,8 +156,9 @@ defmodule Pigeon.APNS.Notification do topic: nil } """ - @spec put_sound(t, String.t) :: t - def put_sound(notification, sound), do: update_payload(notification, "sound", sound) + @spec put_sound(t, String.t()) :: t + def put_sound(notification, sound), + do: update_payload(notification, "sound", sound) @doc """ Sets `"content-available"` flag in push payload. @@ -175,7 +178,8 @@ defmodule Pigeon.APNS.Notification do } """ @spec put_content_available(t) :: t - def put_content_available(notification), do: update_payload(notification, "content-available", 1) + def put_content_available(notification), + do: update_payload(notification, "content-available", 1) @doc """ Updates `"category"` key in push payload. @@ -191,8 +195,9 @@ defmodule Pigeon.APNS.Notification do topic: nil } """ - @spec put_category(t, String.t) :: t - def put_category(notification, category), do: update_payload(notification, "category", category) + @spec put_category(t, String.t()) :: t + def put_category(notification, category), + do: update_payload(notification, "category", category) @doc """ Sets `"mutable-content"` flag in push payload. @@ -211,13 +216,15 @@ defmodule Pigeon.APNS.Notification do } """ @spec put_mutable_content(t) :: t - def put_mutable_content(notification), do: update_payload(notification, "mutable-content", 1) + def put_mutable_content(notification), + do: update_payload(notification, "mutable-content", 1) defp update_payload(notification, key, value) do new_aps = notification.payload |> Map.get("aps") |> Map.put(key, value) + new_payload = notification.payload |> Map.put("aps", new_aps) %{notification | payload: new_payload} end @@ -237,7 +244,7 @@ defmodule Pigeon.APNS.Notification do topic: nil } """ - @spec put_custom(t, %{String.t => String.t}) :: t + @spec put_custom(t, %{String.t() => String.t()}) :: t def put_custom(notification, data) do new_payload = Map.merge(notification.payload, data) %{notification | payload: new_payload} diff --git a/lib/pigeon/apns/shared.ex b/lib/pigeon/apns/shared.ex new file mode 100644 index 00000000..2de5e7ba --- /dev/null +++ b/lib/pigeon/apns/shared.ex @@ -0,0 +1,84 @@ +defmodule Pigeon.APNS.Shared do + @moduledoc false + + import Pigeon.Tasks, only: [process_on_response: 2] + + alias Pigeon.APNS.{Config, Error, JWTConfig, Notification} + + @type config :: Config.t() | JWTConfig.t() + + @type headers :: [{binary(), any()}] + + @spec worker_name(any) :: atom | nil + def worker_name(%{name: name}), do: name + + @spec max_demand(any) :: non_neg_integer + def max_demand(_config), do: 1_000 + + @spec push_headers(Config.t(), Notification.t(), Keyword.t()) :: headers() + def push_headers(_config, notification, _opts) do + json = Poison.encode!(notification.payload) + + [ + {":method", "POST"}, + {":path", "/3/device/#{notification.device_token}"}, + {"content-length", "#{byte_size(json)}"} + ] + |> put_apns_id(notification) + |> put_apns_topic(notification) + end + + @spec push_payload(config, Notification.t(), Keyword.t()) :: + iodata | no_return + def push_payload(_config, notification, _opts) do + Poison.encode!(notification.payload) + end + + def handle_end_stream(_config, stream, notification, on_response) do + %{headers: headers, body: body, status: status} = stream + + case status do + 200 -> + n = %{notification | id: get_apns_id(headers), response: :success} + process_on_response(on_response, n) + + _error -> + reason = Error.parse(body) + Error.log(reason, notification) + notification = %{notification | response: reason} + process_on_response(on_response, notification) + end + end + + @spec schedule_ping(any) :: no_return + def schedule_ping(%{ping_period: ping}) do + Process.send_after(self(), :ping, ping) + end + + def close(_config) do + end + + def put_apns_id(headers, notification) do + case notification.id do + nil -> headers + id -> headers ++ [{"apns-id", id}] + end + end + + def put_apns_topic(headers, notification) do + case notification.topic do + nil -> headers + topic -> headers ++ [{"apns-topic", topic}] + end + end + + def get_apns_id(headers) do + case Enum.find(headers, fn {key, _val} -> key == "apns-id" end) do + {"apns-id", id} -> id + nil -> nil + end + end + + def add_port(opts, %{port: 443}), do: opts + def add_port(opts, %{port: port}), do: [{:port, port} | opts] +end diff --git a/lib/pigeon/apns/token.ex b/lib/pigeon/apns/token.ex new file mode 100644 index 00000000..cb02f8a0 --- /dev/null +++ b/lib/pigeon/apns/token.ex @@ -0,0 +1,20 @@ +defmodule Pigeon.APNS.Token do + @moduledoc false + + @type t :: {non_neg_integer(), binary() | nil} + + @spec start_link((() -> any())) :: Agent.on_start() + def start_link(_) do + Agent.start_link(fn -> %{} end, name: __MODULE__) + end + + @spec get(String.t()) :: t + def get(name) do + Agent.get(__MODULE__, &Map.get(&1, name, {0, nil})) + end + + @spec update(String.t(), t) :: :ok + def update(name, token) do + Agent.update(__MODULE__, &Map.put(&1, name, token)) + end +end diff --git a/lib/pigeon/configurable.ex b/lib/pigeon/configurable.ex index b7c04303..ce2cb30d 100644 --- a/lib/pigeon/configurable.ex +++ b/lib/pigeon/configurable.ex @@ -15,7 +15,7 @@ defprotocol Pigeon.Configurable do @spec worker_name(any) :: atom | nil def worker_name(config) - @spec connect(any) :: {:ok, sock} | {:error, String.t} + @spec connect(any) :: {:ok, sock} | {:error, String.t()} def connect(config) def push_headers(config, notification, opts) diff --git a/lib/pigeon/connection.ex b/lib/pigeon/connection.ex index 14baa1f0..f0f04adb 100644 --- a/lib/pigeon/connection.ex +++ b/lib/pigeon/connection.ex @@ -9,7 +9,7 @@ defmodule Pigeon.Connection do requested: 0, socket: nil, stream_id: 1, - queue: NotificationQueue.new + queue: NotificationQueue.new() use GenStage require Logger @@ -22,10 +22,12 @@ defmodule Pigeon.Connection do def handle_subscribe(:producer, _opts, from, state) do demand = Configurable.max_demand(state.config) GenStage.ask(from, demand) + state = state |> inc_requested(demand) |> Map.put(:from, from) + {:manual, state} end @@ -35,15 +37,19 @@ defmodule Pigeon.Connection do def init({config, from}) do state = %Connection{config: config, from: from} + case connect_socket(config, 0) do {:ok, socket} -> Configurable.schedule_ping(config) {:consumer, %{state | socket: socket}, subscribe_to: [from]} - {:error, reason} -> {:stop, reason} + + {:error, reason} -> + {:stop, reason} end end def connect_socket(_config, 3), do: {:error, :timeout} + def connect_socket(config, tries) do case Configurable.connect(config) do {:ok, socket} -> {:ok, socket} @@ -88,21 +94,24 @@ defmodule Pigeon.Connection do def handle_events(events, _from, state) do state = - Enum.reduce(events, state, fn({:push, notif, opts}, state) -> + Enum.reduce(events, state, fn {:push, notif, opts}, state -> send_push(state, notif, opts) end) {:noreply, [], state} end - def process_end_stream(%Stream{id: stream_id} = stream, - %{queue: queue, config: config} = state) do + def process_end_stream(%Stream{id: stream_id} = stream, state) do + %{queue: queue, config: config} = state + case NotificationQueue.pop(queue, stream_id) do {nil, new_queue} -> # Do nothing if no queued item for stream {:noreply, [], %{state | queue: new_queue}} + {{notif, on_response}, new_queue} -> Configurable.handle_end_stream(config, stream, notif, on_response) + state = state |> inc_completed(1) @@ -111,6 +120,7 @@ defmodule Pigeon.Connection do total_requests = state.completed + state.requested max_demand = Configurable.max_demand(state.config) + state = if total_requests < @limit and state.requested < max_demand do to_ask = min(@limit - total_requests, max_demand - state.requested) @@ -123,6 +133,7 @@ defmodule Pigeon.Connection do if state.completed >= @limit do GenStage.cancel(state.from, :stream_id_exhausted) end + {:noreply, [], state} end end @@ -133,10 +144,13 @@ defmodule Pigeon.Connection do Client.default().send_request(state.socket, headers, payload) - new_q = NotificationQueue.add(queue, - state.stream_id, - notification, - opts[:on_response]) + new_q = + NotificationQueue.add( + queue, + state.stream_id, + notification, + opts[:on_response] + ) state |> inc_stream_id() diff --git a/lib/pigeon/exceptions.ex b/lib/pigeon/exceptions.ex new file mode 100644 index 00000000..9cb18172 --- /dev/null +++ b/lib/pigeon/exceptions.ex @@ -0,0 +1,10 @@ +defmodule Pigeon.ConfigError do + @moduledoc """ + This error represents configuration errors: for example, configuring + both `:cert` and `:jwt_key` for APNS. + """ + + defexception [:message] + + @type t :: %__MODULE__{message: binary} +end diff --git a/lib/pigeon/fcm.ex b/lib/pigeon/fcm.ex index 77add119..b46e854b 100644 --- a/lib/pigeon/fcm.ex +++ b/lib/pigeon/fcm.ex @@ -9,6 +9,11 @@ defmodule Pigeon.FCM do alias Pigeon.FCM.{Config, Notification} alias Pigeon.Worker + @typedoc ~S""" + Can be either a single notification or a list. + """ + @type notification :: Notification.t() | [Notification.t(), ...] + @typedoc ~S""" Async callback for push notification response. @@ -30,7 +35,7 @@ defmodule Pigeon.FCM do n = Pigeon.FCM.Notification.new("device token", %{}, %{"message" => "test"}) Pigeon.FCM.push(n, on_response: handler) """ - @type on_response :: ((Notification.t) -> no_return) + @type on_response :: (Notification.t() -> no_return) @typedoc ~S""" Options for sending push notifications. @@ -42,10 +47,10 @@ defmodule Pigeon.FCM do batches synchronously. """ @type push_opts :: [ - to: atom | pid | nil, - timeout: pos_integer | nil, - on_response: on_response | nil - ] + to: atom | pid | nil, + timeout: pos_integer | nil, + on_response: on_response | nil + ] @default_timeout 5_000 @default_worker :fcm_default @@ -82,21 +87,23 @@ defmodule Pigeon.FCM do [[invalid_registration: "regId", invalid_registration: "regId"], [invalid_registration: "regId", invalid_registration: "regId"]] """ - @spec push(Notification.t, Keyword.t) :: Notification.t - @spec push([Notification.t, ...], Keyword.t) :: [Notification.t, ...] + @spec push(notification, Keyword.t()) :: notification | :ok def push(notification, opts \\ []) + def push(notification, opts) when is_list(notification) do timeout = Keyword.get(opts, :timeout, @default_timeout) + if Keyword.has_key?(opts, :on_response) do for n <- notification, do: send_push(n, opts[:on_response], opts) :ok else notification - |> Enum.map(& Task.async(fn -> sync_push(&1, opts) end)) + |> Enum.map(&Task.async(fn -> sync_push(&1, opts) end)) |> Task.yield_many(timeout) |> Enum.map(&task_mapper(&1)) end end + def push(notification, opts) do if Keyword.has_key?(opts, :on_response) do send_push(notification, opts[:on_response], opts) @@ -113,11 +120,14 @@ defmodule Pigeon.FCM do end end - defp send_push(notifications, on_response, opts) when is_list(notifications) do + defp send_push(notifications, on_response, opts) + when is_list(notifications) do worker_name = opts[:to] || @default_worker + notifications - |> Enum.map(& cast_request(worker_name, &1, on_response, opts)) + |> Enum.map(&cast_request(worker_name, &1, on_response, opts)) end + defp send_push(notification, on_response, opts) do send_push([notification], on_response, opts) end @@ -129,10 +139,11 @@ defmodule Pigeon.FCM do defp sync_push(notification, opts) do timeout = Keyword.get(opts, :timeout, @default_timeout) - ref = :erlang.make_ref + ref = :erlang.make_ref() pid = self() - on_response = fn(x) -> send pid, {ref, x} end + on_response = fn x -> send(pid, {ref, x}) end send_push(notification, on_response, opts) + receive do {^ref, x} -> x after @@ -151,16 +162,19 @@ defmodule Pigeon.FCM do true """ def start_connection(opts \\ []) + def start_connection(name) when is_atom(name) do worker = worker(Pigeon.Worker, [Config.new(name)], id: name) Supervisor.start_child(:pigeon, worker) end + def start_connection(%Config{} = config) do Worker.start_link(config) end + def start_connection(opts) do opts - |> Config.new + |> Config.new() |> start_connection() end diff --git a/lib/pigeon/fcm/config.ex b/lib/pigeon/fcm/config.ex index 345e61e6..2449dcbf 100644 --- a/lib/pigeon/fcm/config.ex +++ b/lib/pigeon/fcm/config.ex @@ -7,11 +7,11 @@ defmodule Pigeon.FCM.Config do name: nil @type t :: %__MODULE__{ - key: binary, - name: term, - port: pos_integer, - uri: charlist, - } + key: binary, + name: term, + port: pos_integer, + uri: charlist + } @doc ~S""" Returns a new `FCM.Config` with given `opts`. @@ -35,9 +35,10 @@ defmodule Pigeon.FCM.Config do port: Keyword.get(opts, :port, 443) } end + def new(name) when is_atom(name) do Application.get_env(:pigeon, :fcm)[name] - |> Enum.to_list + |> Enum.to_list() |> Keyword.put(:name, name) |> new() end @@ -63,7 +64,7 @@ defimpl Pigeon.Configurable, for: Pigeon.FCM.Config do @spec max_demand(any) :: non_neg_integer def max_demand(_config), do: 100 - @spec connect(any) :: {:ok, sock} | {:error, String.t} + @spec connect(any) :: {:ok, sock} | {:error, String.t()} def connect(%Config{uri: uri} = config) do case connect_socket_options(config) do {:ok, options} -> @@ -72,15 +73,15 @@ defimpl Pigeon.Configurable, for: Pigeon.FCM.Config do end def connect_socket_options(config) do - opts = [ - {:active, :once}, - {:packet, :raw}, - {:reuseaddr, true}, - {:alpn_advertised_protocols, [<<"h2">>]}, - {:reconnect, false}, - :binary - ] - |> add_port(config) + opts = + [ + {:active, :once}, + {:packet, :raw}, + {:reuseaddr, true}, + {:alpn_advertised_protocols, [<<"h2">>]}, + :binary + ] + |> add_port(config) {:ok, opts} end @@ -102,14 +103,13 @@ defimpl Pigeon.Configurable, for: Pigeon.FCM.Config do Encodable.binary_payload(notification) end - def handle_end_stream(_config, - %{body: body, status: status, error: nil}, - notif, - on_response) do - do_handle_end_stream(status, body, notif, on_response) + def handle_end_stream(_config, %{error: nil} = stream, notif, on_response) do + do_handle_end_stream(stream.status, stream.body, notif, on_response) end + def handle_end_stream(_config, %{error: _error}, _notif, nil), do: :ok - def handle_end_stream(_config, %{error: _error}, {_regids, notif}, on_response) do + + def handle_end_stream(_config, _stream, {_regids, notif}, on_response) do notif = %{notif | status: :unavailable} process_on_response(on_response, notif) end @@ -119,21 +119,25 @@ defimpl Pigeon.Configurable, for: Pigeon.FCM.Config do notif = %{notif | status: :success} parse_result(notif.registration_id, result, on_response, notif) end + defp do_handle_end_stream(400, _body, notif, on_response) do log_error("400", "Malformed JSON") notif = %{notif | status: :malformed_json} process_on_response(on_response, notif) end + defp do_handle_end_stream(401, _body, notif, on_response) do log_error("401", "Unauthorized") notif = %{notif | status: :unauthorized} process_on_response(on_response, notif) end + defp do_handle_end_stream(500, _body, notif, on_response) do log_error("500", "Internal server error") notif = %{notif | status: :internal_server_error} process_on_response(on_response, notif) end + defp do_handle_end_stream(code, body, notif, on_response) do reason = parse_error(body) log_error(code, reason) @@ -156,14 +160,15 @@ defimpl Pigeon.Configurable, for: Pigeon.FCM.Config do def parse_error(data) do case Poison.decode(data) do {:ok, response} -> - response["reason"] |> Macro.underscore |> String.to_existing_atom + response["reason"] |> Macro.underscore() |> String.to_existing_atom() + error -> "Poison parse failed: #{inspect(error)}, body: #{inspect(data)}" - |> Logger.error + |> Logger.error() end end defp log_error(code, reason) do - if Pigeon.debug_log?, do: Logger.error("#{reason}: #{code}") + if Pigeon.debug_log?(), do: Logger.error("#{reason}: #{code}") end end diff --git a/lib/pigeon/fcm/notification.ex b/lib/pigeon/fcm/notification.ex index e6f69728..cd6f9c90 100644 --- a/lib/pigeon/fcm/notification.ex +++ b/lib/pigeon/fcm/notification.ex @@ -13,13 +13,13 @@ defmodule Pigeon.FCM.Notification do alias Pigeon.FCM.Notification @type t :: %__MODULE__{ - message_id: nil | String.t, - payload: %{}, - priority: :normal | :high, - registration_id: String.t | [String.t], - status: status | nil, - response: [] | [regid_response, ...] - } + message_id: nil | String.t(), + payload: %{}, + priority: :normal | :high, + registration_id: String.t() | [String.t()], + status: status | nil, + response: [] | [regid_response, ...] + } @typedoc ~S""" Status of FCM request @@ -33,12 +33,13 @@ defmodule Pigeon.FCM.Notification do to process the request - `:unavailable` - FCM server couldn't process the request in time """ - @type status :: :success - | :timeout - | :unauthorized - | :malformed_json - | :internal_server_error - | :unavailable + @type status :: + :success + | :timeout + | :unauthorized + | :malformed_json + | :internal_server_error + | :unavailable @typedoc ~S""" FCM push response for individual registration IDs @@ -49,22 +50,24 @@ defmodule Pigeon.FCM.Notification do - `{regid_error_response, "reg_id"}` - Push attempted but server responded with error """ - @type regid_response :: {:success, binary} - | {regid_error_response, binary} - | {:update, {binary, binary}} - - @type regid_error_response :: :device_essage_rate_exceeded - | :invalid_data_key - | :invalid_package_name - | :invalid_paramteres - | :invalid_registration - | :invalid_ttl - | :message_too_big - | :missing_registration - | :mismatch_sender_id - | :not_registered - | :topics_message_rate_exceeded - | :unavailable + @type regid_response :: + {:success, binary} + | {regid_error_response, binary} + | {:update, {binary, binary}} + + @type regid_error_response :: + :device_message_rate_exceeded + | :invalid_data_key + | :invalid_package_name + | :invalid_paramteres + | :invalid_registration + | :invalid_ttl + | :message_too_big + | :missing_registration + | :mismatch_sender_id + | :not_registered + | :topics_message_rate_exceeded + | :unavailable @chunk_size 1_000 @@ -108,21 +111,24 @@ defmodule Pigeon.FCM.Notification do 500 """ def new(registration_ids, notification \\ %{}, data \\ %{}) + def new(reg_id, notification, data) when is_binary(reg_id) do %Pigeon.FCM.Notification{registration_id: reg_id} |> put_notification(notification) |> put_data(data) end + def new(reg_ids, notification, data) when length(reg_ids) < 1001 do %Pigeon.FCM.Notification{registration_id: reg_ids} |> put_notification(notification) |> put_data(data) end + def new(reg_ids, notification, data) do reg_ids |> chunk(@chunk_size, @chunk_size, []) - |> Enum.map(& new(&1, notification, data)) - |> List.flatten + |> Enum.map(&new(&1, notification, data)) + |> List.flatten() end defp chunk(collection, chunk_size, step, padding) do @@ -158,7 +164,8 @@ defmodule Pigeon.FCM.Notification do registration_id: nil } """ - def put_notification(n, notification), do: update_payload(n, "notification", notification) + def put_notification(n, notification), + do: update_payload(n, "notification", notification) @doc """ Updates `"priority"` key. @@ -175,14 +182,17 @@ defmodule Pigeon.FCM.Notification do %Pigeon.FCM.Notification{priority: :normal} """ def put_priority(n, :normal), do: %{n | priority: :normal} - def put_priority(n, :high), do: %{n | priority: :high} - def put_priority(n, _), do: n + def put_priority(n, :high), do: %{n | priority: :high} + def put_priority(n, _), do: n + + defp update_payload(notification, _key, value) when value == %{}, + do: notification - defp update_payload(notification, _key, value) when value == %{}, do: notification defp update_payload(notification, key, value) do payload = notification.payload |> Map.put(key, value) + %{notification | payload: payload} end @@ -248,10 +258,10 @@ defmodule Pigeon.FCM.Notification do """ def remove?(%{response: response}) do response - |> Enum.filter(fn({k, _v}) -> + |> Enum.filter(fn {k, _v} -> k == :invalid_registration || k == :not_registered end) - |> Keyword.values + |> Keyword.values() end end @@ -261,17 +271,22 @@ defimpl Pigeon.Encodable, for: Pigeon.FCM.Notification do end @doc false - def encode_requests(%{registration_id: regid} = notification) when is_binary(regid) do + def encode_requests(%{registration_id: regid} = notification) + when is_binary(regid) do encode_requests(%{notification | registration_id: [regid]}) end - def encode_requests(%{registration_id: regid} = notification) when is_list(regid) do + + def encode_requests(%{registration_id: regid} = notification) + when is_list(regid) do regid |> recipient_attr() |> Map.merge(notification.payload) |> Map.put("priority", to_string(notification.priority)) - |> Poison.encode! + |> Poison.encode!() end defp recipient_attr([regid]), do: %{"to" => regid} - defp recipient_attr(regid) when is_list(regid), do: %{"registration_ids" => regid} + + defp recipient_attr(regid) when is_list(regid), + do: %{"registration_ids" => regid} end diff --git a/lib/pigeon/fcm/result_parser.ex b/lib/pigeon/fcm/result_parser.ex index 7005714f..3be23c25 100644 --- a/lib/pigeon/fcm/result_parser.ex +++ b/lib/pigeon/fcm/result_parser.ex @@ -3,8 +3,6 @@ defmodule Pigeon.FCM.ResultParser do import Pigeon.Tasks, only: [process_on_response: 2] - alias Pigeon.FCM.Notification - def parse([], [], on_response, notif) do process_on_response(on_response, notif) end @@ -13,36 +11,39 @@ defmodule Pigeon.FCM.ResultParser do parse([regid], results, on_response, notif) end - # Handle RegID updates - def parse([regid | reg_res], - [%{"message_id" => id, "registration_id" => new_regid} | rest], - on_response, - %Notification{response: resp} = notif) do + def parse([regid | reg_res], [result | rest_results], on_response, notif) do + updated_notif = + case result do + %{"message_id" => id, "registration_id" => new_regid} -> + notif + |> put_update(regid, new_regid) + |> Map.put(:message_id, id) + + %{"message_id" => id} -> + notif + |> put_success(regid) + |> Map.put(:message_id, id) + + %{"error" => error} -> + notif + |> put_error(regid, error) + end + + parse(reg_res, rest_results, on_response, updated_notif) + end + defp put_update(%{response: resp} = notif, regid, new_regid) do new_resp = [{:update, {regid, new_regid}} | resp] - notif = %{notif | message_id: id, response: new_resp} - parse(reg_res, rest, on_response, notif) + %{notif | response: new_resp} end - # Handle successful RegIDs, also parse `message_id` - def parse([regid | reg_res], - [%{"message_id" => id} | rest_results], - on_response, - %Notification{response: resp} = notif) do - + defp put_success(%{response: resp} = notif, regid) do new_resp = [{:success, regid} | resp] - n = %{notif | message_id: id, response: new_resp} - parse(reg_res, rest_results, on_response, n) + %{notif | response: new_resp} end - # Handle error RegIDs - def parse([regid | reg_res], - [%{"error" => error} | rest_results], - on_response, - %Notification{response: resp} = notif) do - - error = error |> Macro.underscore |> String.to_atom - n = %{notif | response: [{error, regid} | resp]} - parse(reg_res, rest_results, on_response, n) + defp put_error(%{response: resp} = notif, regid, error) do + error = error |> Macro.underscore() |> String.to_atom() + %{notif | response: [{error, regid} | resp]} end end diff --git a/lib/pigeon/http2/client.ex b/lib/pigeon/http2/client.ex index f395b59d..ef941f40 100644 --- a/lib/pigeon/http2/client.ex +++ b/lib/pigeon/http2/client.ex @@ -77,6 +77,8 @@ defmodule Pigeon.Http2.Client do config :pigeon, http2_client: Pigeon.YourCustomAdapter """ + @type uri :: charlist + @doc ~S""" Default http2 client to use. @@ -93,15 +95,14 @@ defmodule Pigeon.Http2.Client do @callback start() :: no_return - @callback connect(uri :: charlist, scheme :: :https, options :: Keyword.t) - :: {:ok, pid} - | {:error, any} + @callback connect(uri :: uri, scheme :: :https, options :: Keyword.t()) :: + {:ok, pid} | {:error, any} @callback send_ping(pid) :: :ok - @callback send_request(pid, headers :: Keyword.t, data :: String.t) :: :ok + @callback send_request(pid, headers :: Keyword.t(), data :: String.t()) :: :ok - @callback handle_end_stream(msg :: term, state :: term) - :: {:ok, %Pigeon.Http2.Stream{}} - | any + @callback handle_end_stream(msg :: term, state :: term) :: + {:ok, %Pigeon.Http2.Stream{}} + | any end diff --git a/lib/pigeon/http2/kadabra.ex b/lib/pigeon/http2/kadabra.ex index 6ffbd43e..1d3e867b 100644 --- a/lib/pigeon/http2/kadabra.ex +++ b/lib/pigeon/http2/kadabra.ex @@ -1,45 +1,50 @@ if Code.ensure_loaded?(Kadabra) do -defmodule Pigeon.Http2.Client.Kadabra do - @moduledoc false + defmodule Pigeon.Http2.Client.Kadabra do + @moduledoc false - @behaviour Pigeon.Http2.Client + @behaviour Pigeon.Http2.Client - def start do - Application.ensure_all_started(:kadabra) - end + def start do + Application.ensure_all_started(:kadabra) + end - def connect(uri, scheme, opts) do - Kadabra.open(uri, scheme, opts) - end + def connect(uri, scheme, opts) do + host = "#{scheme}://#{uri}" + Kadabra.open(host, ssl: opts) + end - def send_request(pid, headers, data) do - Kadabra.request(pid, headers, data) - end + def send_request(pid, headers, data) do + Kadabra.request(pid, headers: headers, body: data) + end - @doc ~S""" - send_ping/1 implementation + @doc ~S""" + send_ping/1 implementation - ## Examples + ## Examples - iex> {:ok, pid} = Kadabra.open('http2.golang.org', :https) - iex> Pigeon.Http2.Client.Kadabra.send_ping(pid) - :ok - """ - def send_ping(pid) do - Kadabra.ping(pid) - end + iex> {:ok, pid} = Kadabra.open("https://http2.golang.org") + iex> Pigeon.Http2.Client.Kadabra.send_ping(pid) + :ok + """ + def send_ping(pid) do + Kadabra.ping(pid) + end - def handle_end_stream({:end_stream, %{id: id, - status: status, - headers: headers, - body: body}}, _state) do - {:ok, %Pigeon.Http2.Stream{ + def handle_end_stream({:end_stream, stream}, _state) do + %{id: id, status: status, headers: headers, body: body} = stream + + pigeon_stream = %Pigeon.Http2.Stream{ id: id, status: status, headers: headers, body: body - }} + } + + {:ok, pigeon_stream} + end + + def handle_end_stream(msg, _state) do + msg + end end - def handle_end_stream(msg, _state), do: msg -end end diff --git a/lib/pigeon/http2/stream.ex b/lib/pigeon/http2/stream.ex index da082fe9..f4ee1f06 100644 --- a/lib/pigeon/http2/stream.ex +++ b/lib/pigeon/http2/stream.ex @@ -4,9 +4,9 @@ defmodule Pigeon.Http2.Stream do defstruct id: nil, status: nil, headers: nil, body: nil, error: nil @type t :: %__MODULE__{ - id: pos_integer, - headers: [{binary, binary}, ...], - body: binary, - error: term - } + id: pos_integer, + headers: [{binary, binary}, ...], + body: binary, + error: term + } end diff --git a/lib/pigeon/tasks.ex b/lib/pigeon/tasks.ex index eb3df219..ca19133f 100644 --- a/lib/pigeon/tasks.ex +++ b/lib/pigeon/tasks.ex @@ -2,6 +2,7 @@ defmodule Pigeon.Tasks do @moduledoc false def process_on_response(nil, _notif), do: :ok + def process_on_response(on_response, notif) do Task.Supervisor.start_child(Pigeon.Tasks, fn -> on_response.(notif) end) end diff --git a/lib/pigeon/worker.ex b/lib/pigeon/worker.ex index fae23bcd..f22089ef 100644 --- a/lib/pigeon/worker.ex +++ b/lib/pigeon/worker.ex @@ -4,7 +4,7 @@ defmodule Pigeon.Worker do defstruct config: nil, connections: 0, pending_demand: 0, - queue: :queue.new + queue: :queue.new() use GenStage @@ -12,10 +12,11 @@ defmodule Pigeon.Worker do require Logger - @type config :: APNS.Config.t | FCM.Config.t + @type config :: APNS.Config.t() | FCM.Config.t() - @type notification :: APNS.Notification.t - | FCM.Notification.t + @type notification :: + APNS.Notification.t() + | FCM.Notification.t() @spec start_link(config) :: {:ok, pid} def start_link(config) do @@ -30,7 +31,7 @@ defmodule Pigeon.Worker do GenStage.cast(pid, :stop) end - @spec send_push(atom | pid, notification, Keyword.t) :: :ok + @spec send_push(atom | pid, notification, Keyword.t()) :: :ok def send_push(name, notification, opts) do # Ensure connections are live before trying to push # Doesn't play nice if you try to do it all in one step @@ -50,8 +51,9 @@ defmodule Pigeon.Worker do def handle_call({:push, _notif, _opts} = msg, from, state) do GenStage.reply(from, :ok) + state - |> Map.update(:queue, :queue.new, &(:queue.in(msg, &1))) + |> Map.update(:queue, :queue.new(), &:queue.in(msg, &1)) |> dispatch_events([]) end @@ -89,11 +91,13 @@ defmodule Pigeon.Worker do Pigeon.start_connection({state.config, self()}) %{state | connections: state.connections + 1} end + defp ensure_connections(state), do: state defp dispatch_events(%{pending_demand: 0} = state, events) do {:noreply, Enum.reverse(events), state} end + defp dispatch_events(%{queue: queue} = state, events) do case :queue.out(queue) do {{:value, event}, queue} -> @@ -101,6 +105,7 @@ defmodule Pigeon.Worker do |> Map.put(:queue, queue) |> increment_demand(-1) |> dispatch_events([event | events]) + {:empty, _queue} -> {:noreply, Enum.reverse(events), state} end diff --git a/mix.exs b/mix.exs index b0831344..bbae4a1d 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule Pigeon.Mixfile do use Mix.Project - @version "1.1.6" + @version "1.2.0" def project do [ @@ -33,7 +33,6 @@ defmodule Pigeon.Mixfile do "docs/APNS Apple iOS.md", "docs/FCM Android.md", "docs/ADM Amazon Android.md", - "docs/Migrating to v1-1-0.md", "CHANGELOG.md" ] ], @@ -53,9 +52,10 @@ defmodule Pigeon.Mixfile do {:poison, "~> 2.0 or ~> 3.0"}, {:httpoison, "~> 0.7 or ~> 1.0"}, {:gen_stage, "~> 0.12"}, - {:kadabra, "~> 0.3.7", optional: true}, + {:joken, "~> 1.5.0"}, + {:kadabra, "~> 0.4.2", optional: true}, {:earmark, "~> 1.0", only: :dev}, - {:ex_doc, "~> 0.17.0", only: :dev}, + {:ex_doc, "~> 0.18", only: :dev}, {:excoveralls, "~> 0.5", only: :test}, {:dialyxir, "~> 0.5", only: [:dev], runtime: false}, {:credo, "~> 0.8", only: [:dev, :test], runtime: false} diff --git a/mix.lock b/mix.lock index e0676337..c9bffaaa 100644 --- a/mix.lock +++ b/mix.lock @@ -1,23 +1,27 @@ %{ + "base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm"}, "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, - "certifi": {:hex, :certifi, "2.0.0", "a0c0e475107135f76b8c1d5bc7efb33cd3815cb3cf3dea7aefdd174dabead064", [:rebar3], [], "hexpm"}, - "credo": {:hex, :credo, "0.8.10", "261862bb7363247762e1063713bb85df2bbd84af8d8610d1272cd9c1943bba63", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}], "hexpm"}, + "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.9.2", "841d316612f568beb22ba310d816353dddf31c2d94aa488ae5a27bb53760d0bf", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm"}, "dogma": {:hex, :dogma, "0.1.15", "5bceba9054b2b97a4adcb2ab4948ca9245e5258b883946e82d32f785340fd411", [:mix], [{:poison, ">= 2.0.0", [hex: :poison, optional: false]}]}, - "earmark": {:hex, :earmark, "1.2.4", "99b637c62a4d65a20a9fb674b8cffb8baa771c04605a80c911c4418c69b75439", [:mix], [], "hexpm"}, - "ex_doc": {:hex, :ex_doc, "0.17.1", "39f777415e769992e6732d9589dc5846ea587f01412241f4a774664c746affbb", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, - "excoveralls": {:hex, :excoveralls, "0.8.1", "0bbf67f22c7dbf7503981d21a5eef5db8bbc3cb86e70d3798e8c802c74fa5e27", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, + "earmark": {:hex, :earmark, "1.2.5", "4d21980d5d2862a2e13ec3c49ad9ad783ffc7ca5769cf6ff891a4553fbaae761", [:mix], [], "hexpm"}, + "ex_doc": {:hex, :ex_doc, "0.18.3", "f4b0e4a2ec6f333dccf761838a4b253d75e11f714b85ae271c9ae361367897b7", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, + "excoveralls": {:hex, :excoveralls, "0.8.2", "b941a08a1842d7aa629e0bbc969186a4cefdd035bad9fe15d43aaaaaeb8fae36", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"}, "gen_stage": {:hex, :gen_stage, "0.13.1", "edff5bca9cab22c5d03a834062515e6a1aeeb7665fb44eddae086252e39c4378", [:mix], [], "hexpm"}, - "hackney": {:hex, :hackney, "1.11.0", "4951ee019df102492dabba66a09e305f61919a8a183a7860236c0fde586134b6", [:rebar3], [{:certifi, "2.0.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.0", [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"}, + "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"}, "hpack": {:hex, :hpack_erl, "0.2.3", "17670f83ff984ae6cd74b1c456edde906d27ff013740ee4d9efaa4f1bf999633", [:rebar3], [], "hexpm"}, - "httpoison": {:hex, :httpoison, "1.0.0", "1f02f827148d945d40b24f0b0a89afe40bfe037171a6cf70f2486976d86921cd", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, - "idna": {:hex, :idna, "5.1.0", "d72b4effeb324ad5da3cab1767cb16b17939004e789d8c0ad5b70f3cea20c89a", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, + "httpoison": {:hex, :httpoison, "1.1.1", "96ed7ab79f78a31081bb523eefec205fd2900a02cda6dbc2300e7a1226219566", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, + "idna": {:hex, :idna, "5.1.1", "cbc3b2fa1645113267cc59c760bafa64b2ea0334635ef06dbac8801e42f7279c", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, + "joken": {:hex, :joken, "1.5.0", "42a0953e80bd933fc98a0874e156771f78bf0e92abe6c3a9c22feb6da28efb0b", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}, {:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, + "jose": {:hex, :jose, "1.8.4", "7946d1e5c03a76ac9ef42a6e6a20001d35987afd68c2107bcd8f01a84e75aa73", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"}, "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm"}, - "kadabra": {:hex, :kadabra, "0.3.7", "23032c7293922df3371d359e74de7b7abd6f6af64837551cc7e72c7bdfd1e123", [:mix], [{:hpack, "~> 0.2.3", [hex: :hpack_erl, repo: "hexpm", optional: false]}, {:scribe, "~> 0.4", [hex: :scribe, repo: "hexpm", optional: true]}], "hexpm"}, + "kadabra": {:hex, :kadabra, "0.4.2", "e046b8596e3fc79301d612e0b605c82be6372908ee78d8a73344f0e8773d816f", [:mix], [{:gen_stage, "~> 0.13.1", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:hpack, "~> 0.2.3", [hex: :hpack_erl, repo: "hexpm", optional: false]}], "hexpm"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, "pane": {:hex, :pane, "0.1.1", "4a9b46957a02991acbce012169ab7e8ecff74ad24886f94b142680062b10f167", [], [], "hexpm"}, + "parse_trans": {:hex, :parse_trans, "3.2.0", "2adfa4daf80c14dc36f522cf190eb5c4ee3e28008fc6394397c16f62a26258c2", [:rebar3], [], "hexpm"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, "scribe": {:hex, :scribe, "0.5.0", "1fe6dfffc4264a39633c1bd3fc9ce8377546974c40c7cd2a06194b2c2e91bba0", [], [{:pane, "~> 0.1", [hex: :pane, repo: "hexpm", optional: false]}], "hexpm"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"}, diff --git a/secrets.tar.enc b/secrets.tar.enc new file mode 100644 index 00000000..85273d2c Binary files /dev/null and b/secrets.tar.enc differ diff --git a/test/adm/notification_test.exs b/test/adm/notification_test.exs index b850f69c..5e37fe2f 100644 --- a/test/adm/notification_test.exs +++ b/test/adm/notification_test.exs @@ -3,7 +3,7 @@ defmodule Pigeon.ADMNotificationTest do doctest Pigeon.ADM.Notification def test_registration_id, do: "test1234" - def test_data, do: %{ message: "your message" } + def test_data, do: %{message: "your message"} test "new/1" do expected_result = %Pigeon.ADM.Notification{ @@ -11,29 +11,34 @@ defmodule Pigeon.ADMNotificationTest do payload: %{"data" => %{}}, updated_registration_id: nil, consolidation_key: nil, - expires_after: 604800, + expires_after: 604_800, md5: "1B2M2Y8AsgTpgAmY7PhCfg==" } - assert expected_result == Pigeon.ADM.Notification.new(test_registration_id()) + + assert expected_result == + Pigeon.ADM.Notification.new(test_registration_id()) end test "new/2" do expected_result = %Pigeon.ADM.Notification{ registration_id: test_registration_id(), - payload: %{"data" => %{ "message" => "your message" }}, + payload: %{"data" => %{"message" => "your message"}}, updated_registration_id: nil, consolidation_key: nil, - expires_after: 604800, + expires_after: 604_800, md5: "qzF+HgArKZjJrpfcTbiFxg==" } - assert expected_result == Pigeon.ADM.Notification.new(test_registration_id(), test_data()) + + assert expected_result == + Pigeon.ADM.Notification.new(test_registration_id(), test_data()) end describe "calculate_md5/1" do test "puts md5 hash if a valid data payload" do n = %Pigeon.ADM.Notification{ - payload: %{"data" => %{ message: "your message", hi: "bye" }} + payload: %{"data" => %{message: "your message", hi: "bye"}} } + result_n = Pigeon.ADM.Notification.calculate_md5(n) assert "w2qyl/pbK7HVl9zfzu7Nww==" == result_n.md5 end @@ -42,6 +47,7 @@ defmodule Pigeon.ADMNotificationTest do n = %Pigeon.ADM.Notification{ payload: :bad } + result_n = Pigeon.ADM.Notification.calculate_md5(n) refute result_n.md5 end @@ -54,6 +60,7 @@ defmodule Pigeon.ADMNotificationTest do "something" => 123, 456 => true } + n = Pigeon.ADM.Notification.new(test_registration_id(), data) expected_result = %{ @@ -62,6 +69,7 @@ defmodule Pigeon.ADMNotificationTest do "something" => "123", "456" => "true" } + assert expected_result == n.payload["data"] end end diff --git a/test/apns/jwt_config_test.exs b/test/apns/jwt_config_test.exs new file mode 100644 index 00000000..4999b2ad --- /dev/null +++ b/test/apns/jwt_config_test.exs @@ -0,0 +1,21 @@ +defmodule Pigeon.APNS.JWTConfigTest do + use ExUnit.Case + doctest Pigeon.APNS.JWTConfig, import: true + + def test_message(msg), + do: "#{DateTime.to_string(DateTime.utc_now())} - #{msg}" + + def test_topic, do: Application.get_env(:pigeon, :test)[:apns_topic] + def test_token, do: Application.get_env(:pigeon, :test)[:valid_apns_token] + + test "sends a push with valid config" do + n = + Pigeon.APNS.Notification.new( + test_message("push/1 with jwt"), + test_token(), + test_topic() + ) + + assert Pigeon.APNS.push(n, to: :apns_jwt_dynamic).response == :success + end +end diff --git a/test/apns/notification_test.exs b/test/apns/notification_test.exs index beeb8ab6..73cbaba2 100644 --- a/test/apns/notification_test.exs +++ b/test/apns/notification_test.exs @@ -12,8 +12,12 @@ defmodule Pigeon.APNS.NotificationTest do payload: %{"aps" => %{"alert" => test_msg()}}, expiration: nil } - assert Pigeon.APNS.Notification.new(test_msg(), test_device_token(), test_topic()) == - expected_result + + assert Pigeon.APNS.Notification.new( + test_msg(), + test_device_token(), + test_topic() + ) == expected_result end test "put_alert" do @@ -31,6 +35,7 @@ defmodule Pigeon.APNS.NotificationTest do test "put_badge" do badge = 5 + n = test_msg() |> Pigeon.APNS.Notification.new(test_device_token(), test_topic()) @@ -41,6 +46,7 @@ defmodule Pigeon.APNS.NotificationTest do test "put_sound" do sound = "default" + n = test_msg() |> Pigeon.APNS.Notification.new(test_device_token(), test_topic()) @@ -53,38 +59,49 @@ defmodule Pigeon.APNS.NotificationTest do n = test_msg() |> Pigeon.APNS.Notification.new(test_device_token(), test_topic()) - |> Pigeon.APNS.Notification.put_content_available + |> Pigeon.APNS.Notification.put_content_available() - assert n.payload == %{"aps" => %{"alert" => test_msg(), "content-available" => 1}} + assert n.payload == %{ + "aps" => %{"alert" => test_msg(), "content-available" => 1} + } end test "put_category" do category = "test-category" + n = test_msg() |> Pigeon.APNS.Notification.new(test_device_token(), test_topic()) |> Pigeon.APNS.Notification.put_category(category) - assert n.payload == %{"aps" => %{"alert" => test_msg(), "category" => category}} + assert n.payload == %{ + "aps" => %{"alert" => test_msg(), "category" => category} + } end test "put_mutable_content" do n = test_msg() |> Pigeon.APNS.Notification.new(test_device_token(), test_topic()) - |> Pigeon.APNS.Notification.put_mutable_content + |> Pigeon.APNS.Notification.put_mutable_content() - assert n.payload == %{"aps" => %{"alert" => test_msg(), "mutable-content" => 1}} + assert n.payload == %{ + "aps" => %{"alert" => test_msg(), "mutable-content" => 1} + } end test "put_custom" do custom = %{"custom-key" => %{"custom-value" => 500}} + n = test_msg() |> Pigeon.APNS.Notification.new(test_device_token(), test_topic()) |> Pigeon.APNS.Notification.put_custom(custom) assert n.payload == - %{"aps" => %{"alert" => test_msg()}, "custom-key" => %{"custom-value" => 500}} + %{ + "aps" => %{"alert" => test_msg()}, + "custom-key" => %{"custom-value" => 500} + } end end diff --git a/test/apns_test.exs b/test/apns_test.exs index ca110533..bb139691 100644 --- a/test/apns_test.exs +++ b/test/apns_test.exs @@ -3,10 +3,15 @@ defmodule Pigeon.APNSTest do doctest Pigeon.APNS doctest Pigeon.APNS.Notification - def test_message(msg), do: "#{DateTime.to_string(DateTime.utc_now())} - #{msg}" + def test_message(msg), + do: "#{DateTime.to_string(DateTime.utc_now())} - #{msg}" + def test_topic, do: Application.get_env(:pigeon, :test)[:apns_topic] def test_token, do: Application.get_env(:pigeon, :test)[:valid_apns_token] - def bad_token, do: "00fc13adff785122b4ad28809a3420982341241421348097878e577c991de8f0" + + def bad_token, + do: "00fc13adff785122b4ad28809a3420982341241421348097878e577c991de8f0" + def bad_id, do: "123e4567-e89b-12d3-a456-42665544000" describe "start_connection/1" do @@ -14,6 +19,10 @@ defmodule Pigeon.APNSTest do opts = [ cert: Application.get_env(:pigeon, :test)[:apns_cert], key: Application.get_env(:pigeon, :test)[:apns_key], + jwt_key: Application.get_env(:pigeon, :test)[:apns_jwt_key], + jwt_key_identifier: + Application.get_env(:pigeon, :test)[:apns_jwt_key_identifier], + jwt_team_id: Application.get_env(:pigeon, :test)[:apns_jwt_team_id], mode: :dev ] @@ -32,6 +41,10 @@ defmodule Pigeon.APNSTest do opts = [ cert: Application.get_env(:pigeon, :test)[:apns_cert], key: Application.get_env(:pigeon, :test)[:apns_key], + jwt_key: Application.get_env(:pigeon, :test)[:apns_jwt_key], + jwt_key_identifier: + Application.get_env(:pigeon, :test)[:apns_jwt_key_identifier], + jwt_team_id: Application.get_env(:pigeon, :test)[:apns_jwt_team_id], mode: :dev, ping_period: 30_000 ] @@ -44,20 +57,49 @@ defmodule Pigeon.APNSTest do describe "push/1" do test "returns notification with :success on successful push" do - n = Pigeon.APNS.Notification.new(test_message("push/1"), test_token(), test_topic()) + n = + Pigeon.APNS.Notification.new( + test_message("push/1"), + test_token(), + test_topic() + ) + assert Pigeon.APNS.push(n).response == :success end test "returns notification with response error on unsuccessful push" do - n = Pigeon.APNS.Notification.new(test_message("push/1"), "bad_token", test_topic()) + n = + Pigeon.APNS.Notification.new( + test_message("push/1"), + "bad_token", + test_topic() + ) + assert Pigeon.APNS.push(n).response == :bad_device_token end test "returns list for multiple notifications" do - n = Pigeon.APNS.Notification.new(test_message("push/1"), test_token(), test_topic()) - bad_n = Pigeon.APNS.Notification.new(test_message("push/1"), "asdf1234", test_topic()) + n = + Pigeon.APNS.Notification.new( + test_message("push/1"), + test_token(), + test_topic() + ) + + bad_n = + Pigeon.APNS.Notification.new( + test_message("push/1"), + "asdf1234", + test_topic() + ) + actual = Pigeon.APNS.push([n, n, bad_n]) - assert Enum.map(actual, & &1.response) == [:success, :success, :bad_device_token] + + assert Enum.map(actual, & &1.response) == [ + :success, + :success, + :bad_device_token + ] end end @@ -66,48 +108,60 @@ defmodule Pigeon.APNSTest do n = Pigeon.APNS.Notification.new( test_message("push/1, custom worker"), - test_token(), test_topic() + test_token(), + test_topic() ) - #Pigeon.APNS.stop_connection(:apns_default) + # Pigeon.APNS.stop_connection(:apns_default) opts = [ cert: Application.get_env(:pigeon, :test)[:apns_cert], key: Application.get_env(:pigeon, :test)[:apns_key], + jwt_key: Application.get_env(:pigeon, :test)[:apns_jwt_key], + jwt_key_identifier: + Application.get_env(:pigeon, :test)[:apns_jwt_key_identifier], + jwt_team_id: Application.get_env(:pigeon, :test)[:apns_jwt_team_id], mode: :dev ] + {:ok, worker_pid} = Pigeon.APNS.start_connection(opts) assert Pigeon.APNS.push(n, to: worker_pid).response == :success - #Pigeon.APNS.start_connection(:apns_default) + # Pigeon.APNS.start_connection(:apns_default) end test "pushes to worker's atom name" do n = Pigeon.APNS.Notification.new( test_message("push/1, custom worker"), - test_token(), test_topic() + test_token(), + test_topic() ) - #Pigeon.APNS.stop_connection(:default) + # Pigeon.APNS.stop_connection(:default) opts = [ cert: Application.get_env(:pigeon, :test)[:apns_cert], key: Application.get_env(:pigeon, :test)[:apns_key], + jwt_key: Application.get_env(:pigeon, :test)[:apns_jwt_key], + jwt_key_identifier: + Application.get_env(:pigeon, :test)[:apns_jwt_key_identifier], + jwt_team_id: Application.get_env(:pigeon, :test)[:apns_jwt_team_id], mode: :dev, name: :custom ] + {:ok, _worker_pid} = Pigeon.APNS.start_connection(opts) assert Pigeon.APNS.push(n, to: :custom).response == :success - #Pigeon.APNS.start_connection(:apns_default) + # Pigeon.APNS.start_connection(:apns_default) end end describe "push/2 with :on_response" do test "returns :success response on successful push" do pid = self() - on_response = fn(x) -> send pid, x end + on_response = fn x -> send(pid, x) end n = "push/2 :ok" @@ -121,7 +175,8 @@ defmodule Pigeon.APNSTest do test "returns :bad_message_id response if apns-id is invalid" do pid = self() - on_response = fn(x) -> send pid, x end + on_response = fn x -> send(pid, x) end + n = "push/2 :bad_message_id" |> test_message() @@ -129,12 +184,16 @@ defmodule Pigeon.APNSTest do assert Pigeon.APNS.push(n, on_response: on_response) == :ok - assert_receive(%Pigeon.APNS.Notification{response: :bad_message_id}, 5_000) + assert_receive( + %Pigeon.APNS.Notification{response: :bad_message_id}, + 5_000 + ) end test "returns :bad_device_token if token is invalid" do pid = self() - on_response = fn(x) -> send pid, x end + on_response = fn x -> send(pid, x) end + n = "push/2 :bad_device_token" |> test_message() @@ -142,12 +201,16 @@ defmodule Pigeon.APNSTest do assert Pigeon.APNS.push(n, on_response: on_response) == :ok - assert_receive(%Pigeon.APNS.Notification{response: :bad_device_token}, 5_000) + assert_receive( + %Pigeon.APNS.Notification{response: :bad_device_token}, + 5_000 + ) end test "returns :missing_topic reponse on missing topic for certs supporting mult topics" do pid = self() - on_response = fn(x) -> send pid, x end + on_response = fn x -> send(pid, x) end + n = "push/2 :missing_topic" |> test_message() @@ -162,7 +225,7 @@ defmodule Pigeon.APNSTest do describe "push/1 with :on_response to custom worker" do test "sends to pid if specified" do pid = self() - on_response = fn(x) -> send pid, x end + on_response = fn x -> send(pid, x) end n = "push/2 :ok, custom worker" @@ -170,14 +233,21 @@ defmodule Pigeon.APNSTest do |> Pigeon.APNS.Notification.new(test_token(), test_topic()) Pigeon.APNS.stop_connection(:default) + opts = [ cert: Application.get_env(:pigeon, :test)[:apns_cert], key: Application.get_env(:pigeon, :test)[:apns_key], + jwt_key: Application.get_env(:pigeon, :test)[:apns_jwt_key], + jwt_key_identifier: + Application.get_env(:pigeon, :test)[:apns_jwt_key_identifier], + jwt_team_id: Application.get_env(:pigeon, :test)[:apns_jwt_team_id], mode: :dev ] + {:ok, worker_pid} = Pigeon.APNS.start_connection(opts) - assert Pigeon.APNS.push(n, on_response: on_response, to: worker_pid) == :ok + assert Pigeon.APNS.push(n, on_response: on_response, to: worker_pid) == + :ok assert_receive(%Pigeon.APNS.Notification{response: :success}, 5_000) @@ -186,27 +256,32 @@ defmodule Pigeon.APNSTest do test "sends to worker's atom name if specified" do pid = self() - on_response = fn(x) -> send pid, x end + on_response = fn x -> send(pid, x) end n = "push/2 :ok, custom worker" |> test_message() |> Pigeon.APNS.Notification.new(test_token(), test_topic()) - #Pigeon.APNS.stop_connection(:default) + # Pigeon.APNS.stop_connection(:default) opts = [ cert: Application.get_env(:pigeon, :test)[:apns_cert], key: Application.get_env(:pigeon, :test)[:apns_key], + jwt_key: Application.get_env(:pigeon, :test)[:apns_jwt_key], + jwt_key_identifier: + Application.get_env(:pigeon, :test)[:apns_jwt_key_identifier], + jwt_team_id: Application.get_env(:pigeon, :test)[:apns_jwt_team_id], mode: :dev, name: :custom ] + {:ok, _worker_pid} = Pigeon.APNS.start_connection(opts) assert Pigeon.APNS.push(n, on_response: on_response, to: :custom) == :ok assert_receive(%Pigeon.APNS.Notification{response: :success}, 5_000) - #Pigeon.APNS.start_connection(:apns_default) + # Pigeon.APNS.start_connection(:apns_default) end end end diff --git a/test/fcm/result_parser_test.exs b/test/fcm/result_parser_test.exs index 14c92080..93151c70 100644 --- a/test/fcm/result_parser_test.exs +++ b/test/fcm/result_parser_test.exs @@ -10,28 +10,33 @@ defmodule Pigeon.FCM.ResultParserTest do test "parse_result with success" do expected = [success: "regid"] - ResultParser.parse(["regid"], - [%{"message_id" => "1:0408"}], - & assert_response(&1, expected), - %Notification{}) + ResultParser.parse( + ["regid"], + [%{"message_id" => "1:0408"}], + &assert_response(&1, expected), + %Notification{} + ) end test "parse_result with single non-list regid" do expected = [success: "regid"] - ResultParser.parse("regid", - [%{"message_id" => "1:0408"}], - & assert_response(&1, expected), - %Notification{}) + + ResultParser.parse( + "regid", + [%{"message_id" => "1:0408"}], + &assert_response(&1, expected), + %Notification{} + ) end test "parse_result with success and new registration_id" do pid = self() - resp = fn resp -> send pid, resp end + resp = fn resp -> send(pid, resp) end ResultParser.parse( ["regid"], [%{"message_id" => "1:2342", "registration_id" => "32"}], - & resp.(&1), + &resp.(&1), %Notification{} ) @@ -40,39 +45,51 @@ defmodule Pigeon.FCM.ResultParserTest do assert notif.response == [update: {"regid", "32"}] assert notif.message_id == "1:2342" after - 5_000 -> flunk "No response received." + 5_000 -> flunk("No response received.") end end test "parse_result with error Unavailable" do expected = [unavailable: "regid"] - ResultParser.parse(["regid"], - [%{"error" => "Unavailable"}], - & assert_response(&1, expected), - %Notification{}) + + ResultParser.parse( + ["regid"], + [%{"error" => "Unavailable"}], + &assert_response(&1, expected), + %Notification{} + ) end test "parse_result with error NotRegistered" do expected = [not_registered: "regid"] - ResultParser.parse(["regid"], - [%{"error" => "NotRegistered"}], - & assert_response(&1, expected), - %Notification{}) + + ResultParser.parse( + ["regid"], + [%{"error" => "NotRegistered"}], + &assert_response(&1, expected), + %Notification{} + ) end test "parse_result with error InvalidRegistration" do expected = [invalid_registration: "regid"] - ResultParser.parse(["regid"], - [%{"error" => "InvalidRegistration"}], - & assert_response(&1, expected), - %Notification{}) + + ResultParser.parse( + ["regid"], + [%{"error" => "InvalidRegistration"}], + &assert_response(&1, expected), + %Notification{} + ) end test "parse_result with custom error" do expected = [custom_error: "regid"] - ResultParser.parse(["regid"], - [%{"error" => "CustomError"}], - & assert_response(&1, expected), - %Notification{}) + + ResultParser.parse( + ["regid"], + [%{"error" => "CustomError"}], + &assert_response(&1, expected), + %Notification{} + ) end end diff --git a/test/fcm/worker_test.exs b/test/fcm/worker_test.exs index 46ff4375..7f6ec261 100644 --- a/test/fcm/worker_test.exs +++ b/test/fcm/worker_test.exs @@ -12,15 +12,17 @@ defmodule Pigeon.FCM.WorkerTest do pid |> :sys.get_state() |> Map.get(:consumers) - |> Map.values - |> List.first + |> Map.values() + |> List.first() + conn_pid end defp send_push(pid, count) do n = FCM.Notification.new(valid_fcm_reg_id(), %{}, %{"message" => "Test"}) + 1..count - |> Enum.each(fn(_x) -> + |> Enum.each(fn _x -> assert _notif = Pigeon.FCM.push(n, to: pid) end) end @@ -33,6 +35,7 @@ defmodule Pigeon.FCM.WorkerTest do opts = [ key: Application.get_env(:pigeon, :test)[:fcm_key] ] + {:ok, pid} = FCM.start_connection(opts) send_push(pid, 3) @@ -56,6 +59,7 @@ defmodule Pigeon.FCM.WorkerTest do opts = [ key: Application.get_env(:pigeon, :test)[:fcm_key] ] + {:ok, pid} = FCM.start_connection(opts) send_push(pid, 1) diff --git a/test/fcm_test.exs b/test/fcm_test.exs index a16022bc..7e2f8b0c 100644 --- a/test/fcm_test.exs +++ b/test/fcm_test.exs @@ -15,9 +15,11 @@ defmodule Pigeon.FCMTest do describe "start_connection/1" do test "starts conneciton with opts keyword list" do fcm_key = Application.get_env(:pigeon, :test)[:fcm_key] + opts = [ key: fcm_key ] + {:ok, pid} = Pigeon.FCM.start_connection(opts) assert is_pid(pid) @@ -36,6 +38,7 @@ defmodule Pigeon.FCMTest do opts = [ key: Application.get_env(:pigeon, :test)[:fcm_key] ] + {:ok, worker_pid} = Pigeon.FCM.start_connection(opts) expected = [success: valid_fcm_reg_id()] @@ -52,6 +55,7 @@ defmodule Pigeon.FCMTest do key: Application.get_env(:pigeon, :test)[:fcm_key], name: :custom ] + {:ok, _worker_pid} = Pigeon.FCM.start_connection(opts) expected = [success: valid_fcm_reg_id()] @@ -64,7 +68,7 @@ defmodule Pigeon.FCMTest do notification = valid_fcm_reg_id() |> Notification.new(%{}, @data) - |> Pigeon.FCM.push + |> Pigeon.FCM.push() expected = [success: valid_fcm_reg_id()] assert notification.response == expected @@ -76,14 +80,14 @@ defmodule Pigeon.FCMTest do |> Notification.new(%{}, @data) |> Pigeon.FCM.push(key: "explicit") - assert notif.status == :unauthorized + assert notif.status == :unauthorized end test "successfully sends a valid push with callback" do reg_id = valid_fcm_reg_id() n = Notification.new(reg_id, %{}, @data) pid = self() - FCM.push(n, on_response: fn(x) -> send pid, x end) + FCM.push(n, on_response: fn x -> send(pid, x) end) assert_receive(n = %Notification{response: regids}, 5000) assert n.status == :success @@ -94,7 +98,7 @@ defmodule Pigeon.FCMTest do reg_id = "bad_reg_id" n = Notification.new(reg_id, %{}, @data) pid = self() - Pigeon.FCM.push(n, on_response: fn(x) -> send pid, x end) + Pigeon.FCM.push(n, on_response: fn x -> send(pid, x) end) assert_receive(n = %Notification{}, 5000) assert n.status == :success diff --git a/test/notification_test.exs b/test/notification_test.exs index 395f77d0..43eeee54 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -8,6 +8,7 @@ defmodule Pigeon.NotificationTest do registration_id: @reg_id, payload: %{} } + assert Pigeon.FCM.Notification.new(@reg_id) == expected_result end @@ -18,6 +19,7 @@ defmodule Pigeon.NotificationTest do registration_id: reg_ids, payload: %{} } + assert Pigeon.FCM.Notification.new(reg_ids) == expected_result end @@ -27,10 +29,12 @@ defmodule Pigeon.NotificationTest do "title" => "Test Push", "icon" => "icon" } + expected_result = %Pigeon.FCM.Notification{ registration_id: @reg_id, payload: %{"notification" => n} } + assert Pigeon.FCM.Notification.new(@reg_id, n) == expected_result end @@ -38,10 +42,12 @@ defmodule Pigeon.NotificationTest do data = %{ "message" => "test" } + expected_result = %Pigeon.FCM.Notification{ registration_id: @reg_id, payload: %{"data" => data} } + assert Pigeon.FCM.Notification.new(@reg_id, %{}, data) == expected_result end @@ -51,13 +57,16 @@ defmodule Pigeon.NotificationTest do "title" => "Test Push", "icon" => "icon" } + data = %{ "message" => "test" } + expected_result = %Pigeon.FCM.Notification{ registration_id: @reg_id, payload: %{"notification" => n, "data" => data} } + assert Pigeon.FCM.Notification.new(@reg_id, n, data) == expected_result end end diff --git a/test/support/test_config.ex b/test/support/test_config.ex index 41803acf..5bf39bbc 100644 --- a/test/support/test_config.ex +++ b/test/support/test_config.ex @@ -1,4 +1,6 @@ defmodule Pigeon.TestConfig do + @moduledoc false + alias Pigeon.{APNS, FCM, ADM} def apns_dynamic do @@ -10,6 +12,16 @@ defmodule Pigeon.TestConfig do ) end + def apns_jwt_dynamic do + APNS.JWTConfig.new( + name: :apns_jwt_dynamic, + key: "AuthKey.p8", + key_identifier: System.get_env("APNS_JWT_KEY_IDENTIFIER"), + team_id: System.get_env("APNS_JWT_TEAM_ID"), + mode: :dev + ) + end + def fcm_dynamic do FCM.Config.new( name: :fcm_dynamic,