From fac51927e17f65a2ce234a7180ce6e875623dd4b Mon Sep 17 00:00:00 2001 From: Valerio Pizzichini Date: Tue, 27 Aug 2024 10:12:25 +0200 Subject: [PATCH] feat: adapt library to remote needs - Cleanup of not needed adapters like: - Legacy FCM - ADM (Amazon) - Fix deprecation warning - Catch `{:error, reason}` on `DispatcherWorker.handle_info\1` - Add a quick way to setup test environment variables --- .env.test.example | 10 + .github/workflows/ci.yml | 25 +- .gitignore | 2 + README.md | 6 + config/test.exs | 24 +- lib/pigeon/adapter.ex | 3 +- lib/pigeon/adm.ex | 378 -------------- lib/pigeon/adm/config.ex | 67 --- lib/pigeon/adm/notification.ex | 165 ------- lib/pigeon/adm/result_parser.ex | 47 -- lib/pigeon/apns/token.ex | 2 +- lib/pigeon/application.ex | 2 +- lib/pigeon/dispatcher_worker.ex | 3 + lib/pigeon/fcm/config.ex | 4 +- lib/pigeon/legacy_fcm.ex | 298 ----------- lib/pigeon/legacy_fcm/config.ex | 177 ------- lib/pigeon/legacy_fcm/notification.ex | 461 ------------------ lib/pigeon/legacy_fcm/result_parser.ex | 79 --- test/pigeon/adm/notification_test.exs | 75 --- test/pigeon/adm/result_parser_test.exs | 19 - test/pigeon/adm_test.exs | 71 --- test/pigeon/apns_test.exs | 15 +- test/pigeon/fcm_test.exs | 11 +- test/pigeon/legacy_fcm/notification_test.exs | 74 --- test/pigeon/legacy_fcm/result_parser_test.exs | 87 ---- test/pigeon/legacy_fcm_test.exs | 71 --- test/support/adm.ex | 4 - test/test_helper.exs | 2 - 28 files changed, 62 insertions(+), 2120 deletions(-) create mode 100644 .env.test.example delete mode 100644 lib/pigeon/adm.ex delete mode 100644 lib/pigeon/adm/config.ex delete mode 100644 lib/pigeon/adm/notification.ex delete mode 100644 lib/pigeon/adm/result_parser.ex delete mode 100644 lib/pigeon/legacy_fcm.ex delete mode 100644 lib/pigeon/legacy_fcm/config.ex delete mode 100644 lib/pigeon/legacy_fcm/notification.ex delete mode 100644 lib/pigeon/legacy_fcm/result_parser.ex delete mode 100644 test/pigeon/adm/notification_test.exs delete mode 100644 test/pigeon/adm/result_parser_test.exs delete mode 100644 test/pigeon/adm_test.exs delete mode 100644 test/pigeon/legacy_fcm/notification_test.exs delete mode 100644 test/pigeon/legacy_fcm/result_parser_test.exs delete mode 100644 test/pigeon/legacy_fcm_test.exs delete mode 100644 test/support/adm.ex diff --git a/.env.test.example b/.env.test.example new file mode 100644 index 00000000..94e79963 --- /dev/null +++ b/.env.test.example @@ -0,0 +1,10 @@ +export FCM_PROJECT_ID="your-firebase-project-id" +export FCM_SERVICE_ACCOUNT_JSON=$(cat /path/to/service/account/json) + +export APNS_KEY_UNENCRYPTED=$(cat /path/to/apns/key-unencrypted.p8) +export APNS_KEY_IDENTIFIER="your-apple-key-identifier" +export APNS_TEAM_ID="your-apple-team-id" +export APNS_TOPIC="com.your.project.topic" + +export VALID_APNS_TOKEN="a-valid-apple-device-token" +export VALID_GCM_REG_ID="a-valid-android-device-token" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 141563a6..56423793 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,8 +31,8 @@ jobs: - name: Set up Elixir uses: erlef/setup-beam@v1 with: - elixir-version: "1.14.3" - otp-version: "24.3" + elixir-version: "1.15.5" + otp-version: "26.0.2" - name: Restore dependencies cache uses: actions/cache@v3 with: @@ -53,8 +53,8 @@ jobs: - name: Set up Elixir uses: erlef/setup-beam@v1 with: - elixir-version: "1.14.3" - otp-version: "24.3" + elixir-version: "1.15.5" + otp-version: "26.0.2" - name: Restore dependencies cache uses: actions/cache@v3 with: @@ -65,17 +65,12 @@ jobs: run: mix deps.get - name: Run tests env: - ADM_OAUTH2_CLIENT_ID: ${{ secrets.ADM_OAUTH2_CLIENT_ID }} - ADM_OAUTH2_CLIENT_SECRET: ${{ secrets.ADM_OAUTH2_CLIENT_SECRET }} - APNS_AUTH_KEY_P8: ${{ secrets.APNS_AUTH_KEY_P8 }} - APNS_CERT: ${{ secrets.APNS_CERT }} - APNS_JWT_KEY_IDENTIFIER: ${{ secrets.APNS_JWT_KEY_IDENTIFIER }} - APNS_JWT_TEAM_ID: ${{ secrets.APNS_JWT_TEAM_ID }} + APNS_KEY_IDENTIFIER: ${{ secrets.APNS_KEY_IDENTIFIER }} + APNS_TEAM_ID: ${{ secrets.APNS_TEAM_ID }} APNS_KEY_UNENCRYPTED: ${{ secrets.APNS_KEY_UNENCRYPTED }} - APNS_TOPIC: ${{ secrets.APNS_TOPIC }} - FCM_PROJECT: ${{ secrets.FCM_PROJECT }} + APNS_TOPIC: ${{ vars.APNS_TOPIC }} + FCM_PROJECT_ID: ${{ secrets.FCM_PROJECT_ID }} FCM_SERVICE_ACCOUNT_JSON: ${{ secrets.FCM_SERVICE_ACCOUNT_JSON }} - GCM_KEY: ${{ secrets.GCM_KEY }} - VALID_APNS_TOKEN: ${{ secrets.VALID_APNS_TOKEN }} - VALID_GCM_REG_ID: ${{ secrets.VALID_GCM_REG_ID }} + VALID_APNS_TOKEN: ${{ vars.VALID_APNS_TOKEN }} + VALID_GCM_REG_ID: ${{ vars.VALID_GCM_REG_ID }} run: mix test diff --git a/.gitignore b/.gitignore index 814a69d9..caf39330 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,5 @@ scratchpad.txt .idea pigeon.iml .envrc + +.env.test diff --git a/README.md b/README.md index fa74c06a..0e315445 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,12 @@ Unit tests can be run with `mix test` or `mix coveralls.html`. Environment varia various credentials. See [config/test.exs](https://github.com/codedge-llc/pigeon/blob/master/config/test.exs) for the full list. +```sh +cp .env.test.example .env.test +# Update the environment values with your project ones +source .env.test +``` + ### Formatting This project uses Elixir's `mix format` and [Prettier](https://prettier.io) for formatting. diff --git a/config/test.exs b/config/test.exs index 9893ae60..f5e8a8b0 100644 --- a/config/test.exs +++ b/config/test.exs @@ -8,31 +8,23 @@ config :pigeon, :test, valid_apns_token: System.get_env("VALID_APNS_TOKEN"), valid_fcm_reg_id: System.get_env("VALID_GCM_REG_ID") -config :pigeon, PigeonTest.ADM, - adapter: Pigeon.ADM, - client_id: System.get_env("ADM_OAUTH2_CLIENT_ID"), - client_secret: System.get_env("ADM_OAUTH2_CLIENT_SECRET") - config :pigeon, PigeonTest.APNS, adapter: Pigeon.APNS, - cert: System.get_env("APNS_CERT"), key: System.get_env("APNS_KEY_UNENCRYPTED"), - mode: :dev + key_identifier: System.get_env("APNS_KEY_IDENTIFIER"), + team_id: System.get_env("APNS_TEAM_ID"), + mode: :prod config :pigeon, PigeonTest.APNS.JWT, adapter: Pigeon.APNS, - key: System.get_env("APNS_AUTH_KEY_P8"), - key_identifier: System.get_env("APNS_JWT_KEY_IDENTIFIER"), - team_id: System.get_env("APNS_JWT_TEAM_ID"), - mode: :dev - -config :pigeon, PigeonTest.LegacyFCM, - adapter: Pigeon.LegacyFCM, - key: System.get_env("GCM_KEY") + key: System.get_env("APNS_KEY_UNENCRYPTED"), + key_identifier: System.get_env("APNS_KEY_IDENTIFIER"), + team_id: System.get_env("APNS_TEAM_ID"), + mode: :prod config :pigeon, PigeonTest.FCM, adapter: Pigeon.FCM, - project_id: System.get_env("FCM_PROJECT"), + project_id: System.get_env("FCM_PROJECT_ID"), service_account_json: System.get_env("FCM_SERVICE_ACCOUNT_JSON") config :pigeon, PigeonTest.Sandbox, adapter: Pigeon.Sandbox diff --git a/lib/pigeon/adapter.ex b/lib/pigeon/adapter.ex index b54b574a..879ab64d 100644 --- a/lib/pigeon/adapter.ex +++ b/lib/pigeon/adapter.ex @@ -48,7 +48,8 @@ defmodule Pigeon.Adapter do @doc """ Invoked to handle all other messages. """ - @callback handle_info(term, term) :: {:noreply, term} + @callback handle_info(term, term) :: + {:noreply, term} | {:stop, reason :: term} @doc """ Invoked to handle push notifications. diff --git a/lib/pigeon/adm.ex b/lib/pigeon/adm.ex deleted file mode 100644 index 479811c7..00000000 --- a/lib/pigeon/adm.ex +++ /dev/null @@ -1,378 +0,0 @@ -defmodule Pigeon.ADM do - @moduledoc """ - `Pigeon.Adapter` for ADM (Amazon Android) push notifications. - - ## Getting Started - - 1. Create an ADM dispatcher. - - ``` - # lib/adm.ex - defmodule YourApp.ADM do - use Pigeon.Dispatcher, otp_app: :your_app - end - ``` - - 2. (Optional) Add configuration to your `config.exs`. - - ``` - # config.exs - - config :your_app, YourApp.ADM, - adapter: Pigeon.ADM, - client_id: "your_oauth2_client_id_here", - client_secret: "your_oauth2_client_secret_here" - ``` - - 3. Start your dispatcher on application boot. - - ``` - defmodule YourApp.Application do - @moduledoc false - - use Application - - @doc false - def start(_type, _args) do - children = [ - YourApp.ADM - ] - opts = [strategy: :one_for_one, name: YourApp.Supervisor] - Supervisor.start_link(children, opts) - end - end - ``` - - If you skipped step two, include your configuration. - - ``` - defmodule YourApp.Application do - @moduledoc false - - use Application - - @doc false - def start(_type, _args) do - children = [ - {YourApp.ADM, adm_opts()} - ] - opts = [strategy: :one_for_one, name: YourApp.Supervisor] - Supervisor.start_link(children, opts) - end - - defp adm_opts do - [ - adapter: Pigeon.ADM, - client_id: "client_id", - client_secret: "secret" - ] - end - end - ``` - - 4. Create a notification. - - ``` - msg = %{ "body" => "your message" } - n = Pigeon.ADM.Notification.new("your device registration ID", msg) - ``` - - 5. Send the notification. - - ``` - YourApp.ADM.push(n) - ``` - - ## Handling Push Responses - - 1. Pass an optional anonymous function as your second parameter. - - ``` - data = %{ message: "your message" } - n = Pigeon.ADM.Notification.new("device registration ID", data) - YourApp.ADM.push(n, on_response: fn(x) -> IO.inspect(x) end) - ``` - - 2. Responses return a notification with an updated `:response` key. - You could handle responses like so: - - ``` - on_response_handler = fn(x) -> - case x.response do - :success -> - # Push successful - :ok - :update -> - new_reg_id = x.updated_registration_id - # Update the registration ID in the database - :invalid_registration_id -> - # Remove the bad ID from the database - :unregistered -> - # Remove the bad ID from the database - error -> - # Handle other errors - end - end - - data = %{ message: "your message" } - n = Pigeon.ADM.Notification.new("your registration id", data) - Pigeon.ADM.push(n, on_response: on_response_handler) - ``` - - ## Error Responses - - *Taken from [Amazon Device Messaging docs](https://developer.amazon.com/public/apis/engage/device-messaging/tech-docs/06-sending-a-message)* - - | Reason | Description | - |----------------------------------|----------------------------------| - | `:invalid_registration_id` | Invalid Registration Token | - | `:invalid_data` | Bad format JSON data | - | `:invalid_consolidation_key` | Invalid Consolidation Key | - | `:invalid_expiration` | Invalid expiresAfter value | - | `:invalid_checksum` | Invalid md5 value | - | `:invalid_type` | Invalid Type header | - | `:unregistered` | App instance no longer available | - | `:access_token_expired` | Expired OAuth access token | - | `:message_too_large` | Data size exceeds 6 KB | - | `:max_rate_exceeded` | See Retry-After response header | - | `:unknown_error` | Unknown Error | - """ - - @behaviour Pigeon.Adapter - - import Pigeon.Tasks, only: [process_on_response: 1] - alias Pigeon.ADM.{Config, ResultParser} - require Logger - - @token_refresh_uri "https://api.amazon.com/auth/O2/token" - @token_refresh_early_seconds 5 - - @impl true - def init(opts) do - config = %Config{ - client_id: Keyword.get(opts, :client_id), - client_secret: Keyword.get(opts, :client_secret) - } - - Config.validate!(config) - - {: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 - - @impl true - def handle_push(notification, state) do - case refresh_access_token_if_needed(state) do - {:ok, state} -> - :ok = do_push(notification, state) - {:noreply, state} - - {:error, reason} -> - notification - |> Map.put(:response, reason) - |> process_on_response() - - {:noreply, state} - end - end - - @impl true - def handle_info({_from, {:ok, %HTTPoison.Response{status_code: 200}}}, state) do - {:noreply, state} - end - - def handle_info(_msg, state) do - {:noreply, state} - end - - defp refresh_access_token_if_needed(state) do - %{ - access_token: access_token, - access_token_refreshed_datetime_erl: access_ref_dt_erl, - access_token_expiration_seconds: access_ref_exp_secs - } = state - - 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 - @token_refresh_early_seconds - end - - defp seconds_since(datetime_erl) do - gregorian_seconds = - datetime_erl - |> :calendar.datetime_to_gregorian_seconds() - - now_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() - ) - - case post do - {:ok, %{status_code: 200, body: response_body}} -> - {:ok, response_json} = Pigeon.json_library().decode(response_body) - - %{ - "access_token" => access_token, - "expires_in" => expiration_seconds, - "scope" => _scope, - "token_type" => token_type - } = response_json - - 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, %{body: response_body}} -> - {:ok, response_json} = Pigeon.json_library().decode(response_body) - 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 - %{ - "grant_type" => "client_credentials", - "scope" => "messaging:push", - "client_id" => client_id, - "client_secret" => client_secret - } - |> URI.encode_query() - end - - defp token_refresh_headers do - [{"Content-Type", "application/x-www-form-urlencoded;charset=UTF-8"}] - end - - defp do_push(notification, state) do - request = {notification.registration_id, encode_payload(notification)} - - response = fn {reg_id, payload} -> - case HTTPoison.post(adm_uri(reg_id), payload, adm_headers(state)) do - {:ok, %HTTPoison.Response{status_code: status, body: body}} -> - notification = %{notification | registration_id: reg_id} - process_response(status, body, notification) - - {:error, %HTTPoison.Error{reason: :connect_timeout}} -> - notification - |> Map.put(:response, :timeout) - |> process_on_response() - end - end - - Task.Supervisor.start_child(Pigeon.Tasks, fn -> response.(request) end) - :ok - end - - defp adm_uri(reg_id) do - "https://api.amazon.com/messaging/registrations/#{reg_id}/messages" - 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"} - ] - end - - defp encode_payload(notification) do - notification.payload - |> put_consolidation_key(notification.consolidation_key) - |> put_expires_after(notification.expires_after) - |> put_md5(notification.md5) - |> Pigeon.json_library().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), - do: handle_200_status(body, notification) - - defp process_response(status, body, notification), - do: handle_error_status_code(status, body, notification) - - defp handle_200_status(body, notification) do - {:ok, json} = Pigeon.json_library().decode(body) - - notification - |> ResultParser.parse(json) - |> process_on_response() - end - - defp handle_error_status_code(status, body, notification) do - case Pigeon.json_library().decode(body) do - {:ok, %{"reason" => _reason} = result_json} -> - notification - |> ResultParser.parse(result_json) - |> process_on_response() - - {:error, _} -> - notification - |> Map.put(:response, generic_error_reason(status)) - |> process_on_response() - end - end - - defp generic_error_reason(400), do: :invalid_json - defp generic_error_reason(401), do: :authentication_error - defp generic_error_reason(500), do: :internal_server_error - defp generic_error_reason(_), do: :unknown_error -end diff --git a/lib/pigeon/adm/config.ex b/lib/pigeon/adm/config.ex deleted file mode 100644 index 8a89e507..00000000 --- a/lib/pigeon/adm/config.ex +++ /dev/null @@ -1,67 +0,0 @@ -defmodule Pigeon.ADM.Config do - @moduledoc false - - defstruct client_id: nil, client_secret: nil - - @type t :: %__MODULE__{ - client_id: String.t() | nil, - client_secret: String.t() | nil - } - - @doc ~S""" - Returns a new `ADM.Config` with given `opts`. - - ## Examples - - iex> Pigeon.ADM.Config.new( - ...> client_id: "amzn.client.id", - ...> client_secret: "1234secret" - ...> ) - %Pigeon.ADM.Config{ - client_id: "amzn.client.id", - client_secret: "1234secret" - } - """ - @spec new(Keyword.t() | atom) :: t - def new(opts) when is_list(opts) do - %__MODULE__{ - client_id: opts[:client_id], - client_secret: opts[:client_secret] - } - end - - @doc ~S""" - Returns whether a given config has valid credentials. - - ## Examples - - iex> [] |> new() |> valid?() - false - """ - def valid?(config) do - valid_item?(config.client_id) and valid_item?(config.client_secret) - end - - defp valid_item?(item), do: is_binary(item) and String.length(item) > 0 - - @spec validate!(any) :: :ok | no_return - def validate!(config) do - if valid?(config) do - :ok - else - raise Pigeon.ConfigError, - reason: "attempted to start without valid client id and secret", - config: redact(config) - end - end - - defp redact(config) do - [:client_id, :client_secret] - |> Enum.reduce(config, fn k, acc -> - case Map.get(acc, k) do - nil -> acc - _ -> Map.put(acc, k, "[FILTERED]") - end - end) - end -end diff --git a/lib/pigeon/adm/notification.ex b/lib/pigeon/adm/notification.ex deleted file mode 100644 index cf94989c..00000000 --- a/lib/pigeon/adm/notification.ex +++ /dev/null @@ -1,165 +0,0 @@ -defmodule Pigeon.ADM.Notification do - @moduledoc """ - Defines Amazon ADM notification struct and convenience constructor functions. - """ - - defstruct __meta__: %Pigeon.Metadata{}, - consolidation_key: nil, - expires_after: 604_800, - md5: nil, - payload: %{}, - registration_id: nil, - response: nil, - updated_registration_id: nil - - @typedoc ~S""" - ADM notification - - ## Examples - - %Pigeon.ADM.Notification{ - consolidation_key: nil, - expires_after: 604_800, - md5: "qzF+HgArKZjJrpfcTbiFxg==", - payload: %{ - "data" => %{"message" => "your message"} - }, - registration_id: "reg ID", - response: nil, # Set on push response - updated_registration_id: nil - } - """ - @type t :: %__MODULE__{ - __meta__: Pigeon.Metadata.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 - - - nil - Push has not been sent yet - - `:success` - Push was successfully sent - - `t:error_response/0` - Push attempted but server responded - with error - - `:timeout` - Internal error. Push did not reach ADM servers - """ - @type response :: nil | :success | error_response | :timeout - - @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 - | :unknown_error - - @doc ~S""" - Creates `ADM.Notification` struct with device registration ID and optional data payload. - - ## Examples - - iex> Pigeon.ADM.Notification.new("reg ID") - %Pigeon.ADM.Notification{ - consolidation_key: nil, - md5: "1B2M2Y8AsgTpgAmY7PhCfg==", - payload: %{"data" => %{}}, - registration_id: "reg ID", - updated_registration_id: nil - } - - iex> Pigeon.ADM.Notification.new("reg ID", %{"message" => "your message"}) - %Pigeon.ADM.Notification{ - consolidation_key: nil, - md5: "qzF+HgArKZjJrpfcTbiFxg==", - payload: %{ - "data" => %{"message" => "your message"} - }, - registration_id: "reg ID", - updated_registration_id: nil - } - - iex> Pigeon.ADM.Notification.new("reg ID", "not a map") - %Pigeon.ADM.Notification{ - consolidation_key: nil, - md5: "1B2M2Y8AsgTpgAmY7PhCfg==", - payload: %{"data" => %{}}, - registration_id: "reg ID", - updated_registration_id: nil - } - """ - @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) - end - - @doc """ - Updates `"data"` key on push payload and calculates `md5` hash. - - ## Examples - - iex> n = %Pigeon.ADM.Notification{} - iex> Pigeon.ADM.Notification.put_data(n, %{"message" => "your message"}) - %Pigeon.ADM.Notification{ - consolidation_key: nil, - md5: "qzF+HgArKZjJrpfcTbiFxg==", - payload: %{ - "data" => %{"message" => "your message"} - }, - registration_id: nil, - updated_registration_id: nil - } - """ - def put_data(n, data) do - n - |> update_payload("data", ensure_strings(data)) - |> calculate_md5 - end - - defp update_payload(notification, key, value) do - payload = - notification.payload - |> Map.put(key, value) - - %{notification | payload: payload} - end - - @doc false - def ensure_strings(%{} = data) do - data - |> 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 - concat = - data - |> Map.keys() - |> Enum.sort() - |> Enum.map_join(",", fn key -> "#{key}:#{data[key]}" end) - - 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 deleted file mode 100644 index 21cbdd3d..00000000 --- a/lib/pigeon/adm/result_parser.ex +++ /dev/null @@ -1,47 +0,0 @@ -defmodule Pigeon.ADM.ResultParser do - @moduledoc false - - @doc ~S""" - Parses response and updates notification. - - ## Examples - - iex> parse(%Pigeon.ADM.Notification{}, %{}) - %Pigeon.ADM.Notification{response: :success} - - iex> parse(%Pigeon.ADM.Notification{}, %{"registrationID" => "test"}) - %Pigeon.ADM.Notification{response: :update, - updated_registration_id: "test"} - - iex> parse(%Pigeon.ADM.Notification{}, %{"reason" => "InvalidRegistrationId"}) - %Pigeon.ADM.Notification{response: :invalid_registration_id} - """ - # Handle RegID updates - def parse(notification, %{"registrationID" => new_regid}) do - notification - |> Map.put(:response, :update) - |> Map.put(:updated_registration_id, new_regid) - end - - def parse(notification, %{"reason" => error}) do - notification - |> Map.put(:response, to_error_atom(error)) - end - - def parse(notification, %{}) do - notification - |> Map.put(:response, :success) - end - - defp to_error_atom("InvalidRegistrationId"), do: :invalid_registration_id - defp to_error_atom("InvalidData"), do: :invalid_data - defp to_error_atom("InvalidConsolidationKey"), do: :invalid_consolidation_key - defp to_error_atom("InvalidExpiration"), do: :invalid_expiration - defp to_error_atom("InvalidChecksum"), do: :invalid_checksum - defp to_error_atom("InvalidType"), do: :invalid_type - defp to_error_atom("Unregistered"), do: :unregistered - defp to_error_atom("AccessTokenExpired"), do: :access_token_expired - defp to_error_atom("MessageTooLarge"), do: :message_too_large - defp to_error_atom("MaxRateExceeded"), do: :max_rate_exceeded - defp to_error_atom(_), do: :unknown_error -end diff --git a/lib/pigeon/apns/token.ex b/lib/pigeon/apns/token.ex index 96280ced..ecf9fa6b 100644 --- a/lib/pigeon/apns/token.ex +++ b/lib/pigeon/apns/token.ex @@ -9,7 +9,7 @@ defmodule Pigeon.APNS.Token do @type t :: {non_neg_integer(), binary() | nil} - @spec start_link((() -> any())) :: Agent.on_start() + @spec start_link((-> any())) :: Agent.on_start() def start_link(_) do Agent.start_link(fn -> %{} end, name: __MODULE__) end diff --git a/lib/pigeon/application.ex b/lib/pigeon/application.ex index f03b0669..83a8e27e 100644 --- a/lib/pigeon/application.ex +++ b/lib/pigeon/application.ex @@ -7,7 +7,7 @@ defmodule Pigeon.Application do @doc false def start(_type, _args) do - Client.default().start + Client.default().start() children = [ Pigeon.Registry, diff --git a/lib/pigeon/dispatcher_worker.ex b/lib/pigeon/dispatcher_worker.ex index 935d0c87..9aa5c791 100644 --- a/lib/pigeon/dispatcher_worker.ex +++ b/lib/pigeon/dispatcher_worker.ex @@ -39,6 +39,9 @@ defmodule Pigeon.DispatcherWorker do {:noreply, new_state} -> {:noreply, %{adapter: adapter, state: new_state}} + {:stop, reason} -> + {:stop, reason, %{adapter: adapter, state: state}} + {:stop, reason, new_state} -> {:stop, reason, %{adapter: adapter, state: new_state}} end diff --git a/lib/pigeon/fcm/config.ex b/lib/pigeon/fcm/config.ex index 72b5717d..39c76160 100644 --- a/lib/pigeon/fcm/config.ex +++ b/lib/pigeon/fcm/config.ex @@ -4,7 +4,7 @@ defmodule Pigeon.FCM.Config do defstruct port: 443, project_id: nil, service_account_json: nil, - uri: 'fcm.googleapis.com' + uri: ~c"fcm.googleapis.com" @type t :: %__MODULE__{ port: pos_integer, @@ -44,7 +44,7 @@ defmodule Pigeon.FCM.Config do port: Keyword.get(opts, :port, 443), project_id: project_id, service_account_json: service_account_json, - uri: Keyword.get(opts, :uri, 'fcm.googleapis.com') + uri: Keyword.get(opts, :uri, ~c"fcm.googleapis.com") } end diff --git a/lib/pigeon/legacy_fcm.ex b/lib/pigeon/legacy_fcm.ex deleted file mode 100644 index 63672c3d..00000000 --- a/lib/pigeon/legacy_fcm.ex +++ /dev/null @@ -1,298 +0,0 @@ -defmodule Pigeon.LegacyFCM do - @moduledoc """ - `Pigeon.Adapter` for Legacy Firebase Cloud Messaging (FCM) push notifications. - - ## Getting Started - - 1. Create a `LegacyFCM` dispatcher. - - ``` - # lib/legacy_fcm.ex - defmodule YourApp.LegacyFCM do - use Pigeon.Dispatcher, otp_app: :your_app - end - ``` - - 2. (Optional) Add configuration to your `config.exs`. - - ``` - # config.exs - - config :your_app, YourApp.LegacyFCM, - adapter: Pigeon.LegacyFCM, - key: "your_fcm_key_here" - ``` - - 3. Start your dispatcher on application boot. - - ``` - defmodule YourApp.Application do - @moduledoc false - - use Application - - @doc false - def start(_type, _args) do - children = [ - YourApp.LegacyFCM - ] - opts = [strategy: :one_for_one, name: YourApp.Supervisor] - Supervisor.start_link(children, opts) - end - end - ``` - - If you skipped step two, include your configuration. - - ``` - defmodule YourApp.Application do - @moduledoc false - - use Application - - @doc false - def start(_type, _args) do - children = [ - {YourApp.ADM, legacy_fcm_opts()} - ] - opts = [strategy: :one_for_one, name: YourApp.Supervisor] - Supervisor.start_link(children, opts) - end - - defp legacy_fcm_opts do - [ - adapter: Pigeon.LegacyFCM, - key: "your_fcm_key_here" - ] - end - end - ``` - - 4. Create a notification. - - ``` - msg = %{"body" => "your message"} - n = Pigeon.LegacyFCM.Notification.new("your device registration ID", msg) - ``` - - 5. Send the notification. - - Pushes are synchronous and return the notification with - updated `:status` and `:response` keys. If `:status` is success, `:response` - will contain a keyword list of individual registration ID responses. - - ``` - YourApp.LegacyFCM.push(n) - ``` - - ## Sending to Multiple Registration IDs - - Pass in a list of registration IDs, as many as you want. - - ``` - msg = %{"body" => "your message"} - n = Pigeon.FCM.Notification.new(["first ID", "second ID"], msg) - ``` - - ## Notification Struct - - ``` - %Pigeon.LegacyFCM.Notification{ - collapse_key: nil | String.t(), - dry_run: boolean, - message_id: nil | String.t(), - payload: %{...}, - priority: :normal | :high, - registration_id: String.t() | [String.t(), ...], - response: [] | [{atom, String.t()}, ...], | atom, - restricted_package_name: nil | String.t(), - status: atom | nil, - time_to_live: non_neg_integer - } - ``` - - ## Notifications with Custom Data - - FCM accepts both `notification` and `data` keys in its JSON payload. Set them like so: - - ``` - notification = %{"body" => "your message"} - data = %{"key" => "value"} - Pigeon.LegacyFCM.Notification.new("registration ID", notification, data) - ``` - - or - - ``` - Pigeon.LegacyFCM.Notification.new("registration ID") - |> put_notification(%{"body" => "your message"}) - |> put_data(%{"key" => "value"}) - ``` - - ## Handling Push Responses - - 1. Pass an optional anonymous function as your second parameter. - - ``` - data = %{message: "your message"} - n = Pigeon.FCM.Notification.new(data, "device registration ID") - Pigeon.FCM.push(n, fn(x) -> IO.inspect(x) end) - {:ok, %Pigeon.FCM.Notification{...}} - ``` - - 2. Responses return the notification with an updated response. - - ``` - on_response = fn(n) -> - case n.status do - :success -> - bad_regids = FCM.Notification.remove?(n) - to_retry = FCM.Notification.retry?(n) - # Handle updated regids, remove bad ones, etc - :unauthorized -> - # Bad FCM key - error -> - # Some other error - end - end - - data = %{message: "your message"} - n = Pigeon.FCM.Notification.new("your device token", data) - Pigeon.FCM.push(n, on_response: on_response) - ``` - - ## Error Responses - - *Slightly modified from [FCM Server Reference](https://firebase.google.com/docs/cloud-messaging/http-server-ref#error-codes)* - - | Reason | Description | - |----------------------------------|------------------------------| - | `:missing_registration` | Missing Registration Token | - | `:invalid_registration` | Invalid Registration Token | - | `:not_registered` | Unregistered Device | - | `:invalid_package_name` | Invalid Package Name | - | `:authentication_error` | Authentication Error | - | `:mismatch_sender_id` | Mismatched Sender | - | `:invalid_json` | Invalid JSON | - | `:message_too_big` | Message Too Big | - | `:invalid_data_key` | Invalid Data Key | - | `:invalid_ttl` | Invalid Time to Live | - | `:unavailable` | Timeout | - | `:internal_server_error` | Internal Server Error | - | `:device_message_rate_exceeded` | Message Rate Exceeded | - | `:topics_message_rate_exceeded` | Topics Message Rate Exceeded | - | `:unknown_error` | Unknown Error | - """ - - defstruct queue: Pigeon.NotificationQueue.new(), - stream_id: 1, - socket: nil, - config: nil - - @behaviour Pigeon.Adapter - - alias Pigeon.{Configurable, NotificationQueue} - alias Pigeon.Http2.{Client, Stream} - - @impl true - def init(opts) do - config = Pigeon.LegacyFCM.Config.new(opts) - Configurable.validate!(config) - - state = %__MODULE__{config: config} - - case connect_socket(config) do - {:ok, socket} -> - Configurable.schedule_ping(config) - {:ok, %{state | socket: socket}} - - {:error, reason} -> - {:stop, reason} - end - end - - @impl true - def handle_push(notification, %{config: config, queue: queue} = state) do - headers = Configurable.push_headers(config, notification, []) - payload = Configurable.push_payload(config, notification, []) - - Client.default().send_request(state.socket, headers, payload) - - new_q = NotificationQueue.add(queue, state.stream_id, notification) - - state = - state - |> inc_stream_id() - |> Map.put(:queue, new_q) - - {:noreply, state} - end - - def handle_info(:ping, state) do - Client.default().send_ping(state.socket) - Configurable.schedule_ping(state.config) - - {:noreply, state} - end - - def handle_info({:closed, _}, %{config: config} = state) do - case connect_socket(config) do - {:ok, socket} -> - Configurable.schedule_ping(config) - - state = - state - |> reset_stream_id() - |> Map.put(:socket, socket) - - {:noreply, state} - - {:error, reason} -> - {:stop, reason} - end - end - - @impl true - def handle_info(msg, state) do - case Client.default().handle_end_stream(msg, state) do - {:ok, %Stream{} = stream} -> process_end_stream(stream, state) - _else -> {:noreply, state} - end - end - - defp connect_socket(config), do: connect_socket(config, 0) - - defp connect_socket(_config, 3), do: {:error, :timeout} - - defp connect_socket(config, tries) do - case Configurable.connect(config) do - {:ok, socket} -> {:ok, socket} - {:error, _reason} -> connect_socket(config, tries + 1) - end - end - - @doc false - 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, new_queue} -> - Configurable.handle_end_stream(config, stream, notif) - {:noreply, %{state | queue: new_queue}} - end - end - - @doc false - def inc_stream_id(%{stream_id: stream_id} = state) do - %{state | stream_id: stream_id + 2} - end - - @doc false - def reset_stream_id(state) do - %{state | stream_id: 1} - end -end diff --git a/lib/pigeon/legacy_fcm/config.ex b/lib/pigeon/legacy_fcm/config.ex deleted file mode 100644 index 89da6614..00000000 --- a/lib/pigeon/legacy_fcm/config.ex +++ /dev/null @@ -1,177 +0,0 @@ -defmodule Pigeon.LegacyFCM.Config do - @moduledoc false - - defstruct key: nil, - uri: 'fcm.googleapis.com', - port: 443 - - @type t :: %__MODULE__{ - key: binary, - port: pos_integer, - uri: charlist - } - - @doc ~S""" - Returns a new `LegacyFCM.Config` with given `opts`. - - ## Examples - - iex> Pigeon.LegacyFCM.Config.new( - ...> key: "fcm_key", - ...> uri: 'test.server.example.com', - ...> port: 5228 - ...> ) - %Pigeon.LegacyFCM.Config{ - key: "fcm_key", - port: 5228, - uri: 'test.server.example.com' - } - """ - def new(opts) when is_list(opts) do - %__MODULE__{ - key: Keyword.get(opts, :key), - uri: Keyword.get(opts, :uri, 'fcm.googleapis.com'), - port: Keyword.get(opts, :port, 443) - } - end -end - -defimpl Pigeon.Configurable, for: Pigeon.LegacyFCM.Config do - @moduledoc false - - require Logger - - import Pigeon.Tasks, only: [process_on_response: 1] - - alias Pigeon.Encodable - alias Pigeon.LegacyFCM.{Config, ResultParser} - - @type sock :: {:sslsocket, any, pid | {any, any}} - - # Configurable Callbacks - - @spec connect(any) :: {:ok, sock} | {:error, String.t()} - def connect(%Config{uri: uri} = config) do - case connect_socket_options(config) do - {:ok, options} -> - Pigeon.Http2.Client.default().connect(uri, :https, options) - end - end - - def connect_socket_options(config) do - opts = - [ - {:active, :once}, - {:packet, :raw}, - {:reuseaddr, true}, - {:alpn_advertised_protocols, [<<"h2">>]}, - :binary - ] - |> add_port(config) - - {:ok, opts} - end - - def add_port(opts, %Config{port: 443}), do: opts - def add_port(opts, %Config{port: port}), do: [{:port, port} | opts] - - def push_headers(%Config{key: key}, _notification, opts) do - [ - {":method", "POST"}, - {":path", "/fcm/send"}, - {"authorization", "key=#{opts[:key] || key}"}, - {"content-type", "application/json"}, - {"accept", "application/json"} - ] - end - - def push_payload(_config, notification, _opts) do - Encodable.binary_payload(notification) - end - - def handle_end_stream(_config, %{error: nil} = stream, notif) do - do_handle_end_stream(stream.status, stream.body, notif) - end - - def handle_end_stream(_config, _stream, {_regids, notif}) do - notif - |> Map.put(:status, :unavailable) - |> process_on_response() - end - - defp do_handle_end_stream(200, body, notif) do - result = Pigeon.json_library().decode!(body) - notif = %{notif | status: :success} - - notif.registration_id - |> parse_result(result, notif) - |> process_on_response() - end - - defp do_handle_end_stream(400, _body, notif) do - notif - |> Map.put(:status, :malformed_json) - |> process_on_response() - end - - defp do_handle_end_stream(401, _body, notif) do - notif - |> Map.put(:status, :unauthorized) - |> process_on_response() - end - - defp do_handle_end_stream(500, _body, notif) do - notif - |> Map.put(:status, :internal_server_error) - |> process_on_response() - end - - defp do_handle_end_stream(_code, body, notif) do - reason = parse_error(body) - - notif - |> Map.put(:response, reason) - |> process_on_response() - end - - def schedule_ping(_config), do: :ok - - def close(_config) do - end - - def validate!(%{key: key}) when is_binary(key) do - :ok - end - - def validate!(config) do - raise Pigeon.ConfigError, - reason: "attempted to start without valid key", - config: redact(config) - end - - defp redact(%{key: key} = config) when is_binary(key) do - Map.put(config, :key, "[FILTERED]") - end - - defp redact(config), do: config - - def parse_result(ids, %{"results" => results}, notification) do - ResultParser.parse(ids, results, notification) - end - - def parse_result(id, %{"message_id" => _} = result, notification) - when is_binary(id) do - parse_result([id], %{"results" => [result]}, notification) - end - - def parse_error(data) do - case Pigeon.json_library().decode(data) do - {:ok, response} -> - response["reason"] |> Macro.underscore() |> String.to_existing_atom() - - error -> - "JSON parse failed: #{inspect(error)}, body: #{inspect(data)}" - |> Logger.error() - end - end -end diff --git a/lib/pigeon/legacy_fcm/notification.ex b/lib/pigeon/legacy_fcm/notification.ex deleted file mode 100644 index 2df298c6..00000000 --- a/lib/pigeon/legacy_fcm/notification.ex +++ /dev/null @@ -1,461 +0,0 @@ -defmodule Pigeon.LegacyFCM.Notification do - @moduledoc """ - Defines `Pigeon.LegacyFCM.Notification` struct and convenience constructor functions. - """ - - defstruct __meta__: %Pigeon.Metadata{}, - collapse_key: nil, - condition: nil, - content_available: false, - dry_run: false, - message_id: nil, - mutable_content: false, - payload: %{}, - priority: :normal, - registration_id: nil, - response: [], - restricted_package_name: nil, - status: nil, - time_to_live: 2_419_200 - - alias Pigeon.LegacyFCM.Notification - - @type t :: %__MODULE__{ - __meta__: Pigeon.Metadata.t(), - collapse_key: nil | String.t(), - condition: nil | String.t(), - content_available: boolean, - dry_run: boolean, - message_id: nil | String.t(), - mutable_content: boolean, - payload: map, - priority: :normal | :high, - registration_id: String.t() | [String.t()], - response: [] | [regid_response, ...], - restricted_package_name: nil | String.t(), - status: status | nil, - time_to_live: non_neg_integer - } - - @typedoc ~S""" - Status of LegacyFCM request - - - `:success` - Notification was processed successfully - - `:timeout` - Worker did not respond within timeout. This is likely an - internal error - - `:unauthorized` - Bad LegacyFCM key - - `:malformed_json` - Push payload was invalid JSON - - `:internal_server_error` - LegacyFCM server encountered an error while trying - to process the request - - `:unavailable` - LegacyFCM server couldn't process the request in time - """ - @type status :: - :success - | :timeout - | :unauthorized - | :malformed_json - | :internal_server_error - | :unavailable - - @typedoc ~S""" - LegacyFCM push response for individual registration IDs - - - `{:success, "reg_id"}` - Push was successfully sent - - `{:update, {"reg_id", "new_reg_id"}}` - Push successful but user should - use new registration ID for future pushes - - `{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_message_rate_exceeded - | :internal_server_error - | :invalid_apns_credential - | :invalid_data_key - | :invalid_package_name - | :invalid_parameters - | :invalid_registration - | :invalid_ttl - | :message_too_big - | :missing_registration - | :mismatch_sender_id - | :not_registered - | :topics_message_rate_exceeded - | :unavailable - | :unknown_error - - @chunk_size 1_000 - - @doc """ - Creates `LegacyFCM.Notification` struct with device registration IDs and optional - notification and data payloads. - - ## Examples - - iex> Pigeon.LegacyFCM.Notification.new("reg ID") - %Pigeon.LegacyFCM.Notification{ - payload: %{}, - registration_id: "reg ID", - priority: :normal - } - - iex> Pigeon.LegacyFCM.Notification.new("reg ID", %{"body" => "test message"}) - %Pigeon.LegacyFCM.Notification{ - payload: %{"notification" => %{"body" => "test message"}}, - registration_id: "reg ID", - priority: :normal - } - - iex> Pigeon.LegacyFCM.Notification.new("reg ID", %{"body" => "test message"}, - ...> %{"key" => "value"}) - %Pigeon.LegacyFCM.Notification{ - payload: %{ - "data" => %{"key" => "value"}, - "notification" => %{"body" => "test message"} - }, - registration_id: "reg ID", - priority: :normal - } - - iex> regids = Enum.map(0..1_499, fn(_x) -> "reg ID" end) - iex> [n1 | [n2]] = Pigeon.LegacyFCM.Notification.new(regids, - ...> %{"body" => "test message"}, %{"key" => "value"}) - iex> Enum.count(n1.registration_id) - 1000 - iex> Enum.count(n2.registration_id) - 500 - """ - def new(registration_ids, notification \\ %{}, data \\ %{}) - - def new(reg_id, notification, data) when is_binary(reg_id) do - %Pigeon.LegacyFCM.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.LegacyFCM.Notification{registration_id: reg_ids} - |> put_notification(notification) - |> put_data(data) - end - - def new(reg_ids, notification, data) do - reg_ids - |> Enum.chunk_every(@chunk_size, @chunk_size, []) - |> Enum.map(&new(&1, notification, data)) - |> List.flatten() - end - - @doc """ - Updates `"data"` key in push payload. - - This parameter specifies the custom key-value pairs of the message's payload. - - For example, with data:{"score":"3x1"}: - - On iOS, if the message is sent via APNs, it represents the custom data fields. - If it is sent via FCM connection server, it would be represented as key value - dictionary in AppDelegate application:didReceiveRemoteNotification:. - - On Android, this would result in an intent extra named score with the string - value 3x1. - - The key should not be a reserved word ("from" or any word starting with - "google" or "gcm"). Do not use any of the words defined in this table - (such as collapse_key). - - Values in string types are recommended. You have to convert values in objects - or other non-string data types (e.g., integers or booleans) to string. - - ## Examples - - iex> put_data(%Pigeon.LegacyFCM.Notification{}, %{"key" => 1234}) - %Pigeon.LegacyFCM.Notification{ - payload: %{"data" => %{"key" => 1234}}, - registration_id: nil - } - """ - def put_data(n, data), do: update_payload(n, "data", data) - - @doc """ - Updates `"notification"` key in push payload. - - This parameter specifies the predefined, user-visible key-value pairs of the - notification payload. See Notification payload support for detail. For more - information about notification message and data message options, see - [Message types](https://firebase.google.com/docs/cloud-messaging/concept-options#notifications_and_data_messages). - If a notification payload is provided, or the content_available option is set - to true for a message to an iOS device, the message is sent through APNs, - otherwise it is sent through the FCM connection server. - - ## Examples - - iex> put_notification(%Pigeon.LegacyFCM.Notification{}, - ...> %{"body" => "message"}) - %Pigeon.LegacyFCM.Notification{ - payload: %{"notification" => %{"body" => "message"}}, - registration_id: nil - } - """ - def put_notification(n, notification), - do: update_payload(n, "notification", notification) - - @doc """ - Updates `"priority"` key. - - Sets the priority of the message. Valid values are "normal" and "high." On - iOS, these correspond to APNs priorities 5 and 10. - - By default, notification messages are sent with high priority, and data - messages are sent with normal priority. Normal priority optimizes the client - app's battery consumption and should be used unless immediate delivery is - required. For messages with normal priority, the app may receive the message - with unspecified delay. - - When a message is sent with high priority, it is sent immediately, and the app - can display a notification. - - ## Examples - - iex> put_priority(%Pigeon.LegacyFCM.Notification{}, :normal) - %Pigeon.LegacyFCM.Notification{priority: :normal} - - iex> put_priority(%Pigeon.LegacyFCM.Notification{}, :high) - %Pigeon.LegacyFCM.Notification{priority: :high} - - iex> put_priority(%Pigeon.LegacyFCM.Notification{priority: :normal}, :bad) - %Pigeon.LegacyFCM.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 - - @doc """ - Updates `"time_to_live"` key. - - This parameter specifies how long (in seconds) the message should be kept in - FCM storage if the device is offline. The maximum time to live supported is 4 - weeks, and the default value is 4 weeks. For more information, see - [Setting the lifespan of a message](https://firebase.google.com/docs/cloud-messaging/concept-options#ttl). - - ## Examples - - iex> put_time_to_live(%Pigeon.LegacyFCM.Notification{}, 60 * 60 * 24) - %Pigeon.LegacyFCM.Notification{time_to_live: 86_400} - """ - def put_time_to_live(n, ttl) when is_integer(ttl), - do: %{n | time_to_live: ttl} - - @doc """ - Sets `"dry_run"` key to true. - - This parameter, when set to true, allows developers to test a request without - actually sending a message. - - The default value is false. - - ## Examples - - iex> put_dry_run(%Pigeon.LegacyFCM.Notification{}) - %Pigeon.LegacyFCM.Notification{dry_run: true} - """ - def put_dry_run(n), do: %{n | dry_run: true} - - @doc """ - Updates `"collapse_key"` key. - - ## Examples - - iex> put_collapse_key(%Pigeon.LegacyFCM.Notification{}, "Updates available") - %Pigeon.LegacyFCM.Notification{collapse_key: "Updates available"} - """ - def put_collapse_key(n, key) when is_binary(key), do: %{n | collapse_key: key} - - @doc """ - Updates `"restricted_package_name"` key. - - This parameter specifies the package name of the application where the - registration tokens must match in order to receive the message. (Only affects android) - - ## Examples - - iex> put_restricted_package_name(%Pigeon.LegacyFCM.Notification{}, "com.example.app") - %Pigeon.LegacyFCM.Notification{restricted_package_name: "com.example.app"} - """ - def put_restricted_package_name(n, name) when is_binary(name), - do: %{n | restricted_package_name: name} - - @doc """ - Updates `"content_available"` key. - - On iOS, use this field to represent content-available in the APNs payload. - When a notification or message is sent and this is set to true, an inactive - client app is awoken, and the message is sent through APNs as a silent - notification and not through the FCM connection server. Note that silent - notifications in APNs are not guaranteed to be delivered, and can depend on - factors such as the user turning on Low Power Mode, force quitting the app, - etc. On Android, data messages wake the app by default. On Chrome, currently - not supported. - - ## Examples - - iex> put_content_available(%Pigeon.LegacyFCM.Notification{}, true) - %Pigeon.LegacyFCM.Notification{content_available: true} - """ - def put_content_available(n, enabled) when is_boolean(enabled), - do: %{n | content_available: enabled} - - @doc """ - Updates `"mutable_content"` key. - - Currently for iOS 10+ devices only. On iOS, use this field to represent - mutable-content in the APNs payload. When a notification is sent and this is - set to true, the content of the notification can be modified before it is - displayed, using a Notification Service app extension. This parameter will be - ignored for Android and web. - - ## Examples - - iex> put_mutable_content(%Pigeon.LegacyFCM.Notification{}, true) - %Pigeon.LegacyFCM.Notification{mutable_content: true} - """ - def put_mutable_content(n, enabled) when is_boolean(enabled), - do: %{n | mutable_content: enabled} - - @doc """ - Updates `"condition"` key. - - Supported condition: Topic, formatted as "'yourTopic' in topics". This value - is case-insensitive. - - Supported operators: &&, ||. Maximum two operators per topic message supported. - - ## Examples - - iex> put_condition(%Pigeon.LegacyFCM.Notification{}, "'test' in topics") - %Pigeon.LegacyFCM.Notification{condition: "'test' in topics"} - """ - def put_condition(n, condition) when is_binary(condition), - do: %{n | condition: condition} - - 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 - - @doc ~S""" - Returns a list of successful registration IDs - - ## Examples - - iex> n = %Pigeon.LegacyFCM.Notification{response: [ - ...> {:success, "regid1"}, {:invalid_registration, "regid2"}, - ...> {:success, "regid3"}, {:update, {"regid4", "new_regid4"}}, - ...> {:not_registered, "regid5"}, {:unavailable, "regid6"}]} - iex> success?(n) - ["regid1", "regid3"] - """ - def success?(%Notification{response: response}) do - Keyword.get_values(response, :success) - end - - @doc ~S""" - Returns a list of registration IDs and their corresponding new ID - - ## Examples - - iex> n = %Pigeon.LegacyFCM.Notification{response: [ - ...> {:success, "regid1"}, {:invalid_registration, "regid2"}, - ...> {:success, "regid3"}, {:update, {"regid4", "new_regid4"}}, - ...> {:not_registered, "regid5"}, {:unavailable, "regid6"}]} - iex> update?(n) - [{"regid4", "new_regid4"}] - """ - def update?(%Notification{response: response}) do - Keyword.get_values(response, :update) - end - - @doc ~S""" - Returns a list of registration IDs that should be retried - - ## Examples - - iex> n = %Pigeon.LegacyFCM.Notification{response: [ - ...> {:success, "regid1"}, {:invalid_registration, "regid2"}, - ...> {:success, "regid3"}, {:update, {"regid4", "new_regid4"}}, - ...> {:not_registered, "regid5"}, {:unavailable, "regid6"}]} - iex> retry?(n) - ["regid6"] - """ - def retry?(%{response: response}) do - Keyword.get_values(response, :unavailable) - end - - @doc ~S""" - Returns a list of registration IDs that should be removed - - ## Examples - - iex> n = %Pigeon.LegacyFCM.Notification{response: [ - ...> {:success, "regid1"}, {:invalid_registration, "regid2"}, - ...> {:success, "regid3"}, {:update, {"regid4", "new_regid4"}}, - ...> {:not_registered, "regid5"}, {:unavailable, "regid6"}]} - iex> remove?(n) - ["regid2", "regid5"] - """ - def remove?(%{response: response}) do - response - |> Enum.filter(fn {k, _v} -> - k == :invalid_registration || k == :not_registered - end) - |> Keyword.values() - end -end - -defimpl Pigeon.Encodable, for: Pigeon.LegacyFCM.Notification do - def binary_payload(notif) do - encode_requests(notif) - end - - @doc false - def encode_requests(%{registration_id: regid} = notif) - when is_binary(regid) do - encode_requests(%{notif | registration_id: [regid]}) - end - - def encode_requests(%{registration_id: regid} = notif) when is_list(regid) do - regid - |> recipient_attr() - |> Map.merge(notif.payload) - |> encode_attr("priority", to_string(notif.priority)) - |> encode_attr("time_to_live", notif.time_to_live) - |> encode_attr("collapse_key", notif.collapse_key) - |> encode_attr("restricted_package_name", notif.restricted_package_name) - |> encode_attr("dry_run", notif.dry_run) - |> encode_attr("content_available", notif.content_available) - |> encode_attr("mutable_content", notif.mutable_content) - |> encode_attr("condition", notif.condition) - |> Pigeon.json_library().encode!() - end - - defp encode_attr(map, _key, nil), do: map - - defp encode_attr(map, key, val) do - Map.put(map, key, val) - end - - defp recipient_attr([regid]), do: %{"to" => regid} - - defp recipient_attr(regid) when is_list(regid), - do: %{"registration_ids" => regid} -end diff --git a/lib/pigeon/legacy_fcm/result_parser.ex b/lib/pigeon/legacy_fcm/result_parser.ex deleted file mode 100644 index 73598d99..00000000 --- a/lib/pigeon/legacy_fcm/result_parser.ex +++ /dev/null @@ -1,79 +0,0 @@ -defmodule Pigeon.LegacyFCM.ResultParser do - @moduledoc false - - def parse([], [], notif) do - notif - end - - def parse(regid, results, notif) when is_binary(regid) do - parse([regid], results, notif) - end - - def parse([regid | reg_res], [result | rest_results], 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, updated_notif) - end - - defp put_update(%{response: resp} = notif, regid, new_regid) do - new_resp = [{:update, {regid, new_regid}} | resp] - %{notif | response: new_resp} - end - - defp put_success(%{response: resp} = notif, regid) do - new_resp = [{:success, regid} | resp] - %{notif | response: new_resp} - end - - defp put_error(%{response: resp} = notif, regid, error) do - error = parse_error(error) - %{notif | response: [{error, regid} | resp]} - end - - def parse_error("DeviceMessageRateExceeded"), - do: :device_message_rate_exceeded - - def parse_error("InternalServerError"), do: :internal_server_error - - def parse_error("InvalidApnsCredential"), do: :invalid_apns_credential - - def parse_error("InvalidDataKey"), do: :invalid_data_key - - def parse_error("InvalidPackageName"), do: :invalid_package_name - - def parse_error("InvalidParameters"), do: :invalid_parameters - - def parse_error("InvalidRegistration"), do: :invalid_registration - - def parse_error("InvalidTtl"), do: :invalid_ttl - - def parse_error("MessageTooBig"), do: :message_too_big - - def parse_error("MissingRegistration"), do: :missing_registration - - def parse_error("MismatchSenderId"), do: :mismatch_sender_id - - def parse_error("NotRegistered"), do: :not_registered - - def parse_error("TopicsMessageRateExceeded"), - do: :topics_message_rate_exceeded - - def parse_error("Unavailable"), do: :unavailable - - def parse_error(_), do: :unknown_error -end diff --git a/test/pigeon/adm/notification_test.exs b/test/pigeon/adm/notification_test.exs deleted file mode 100644 index b47caa77..00000000 --- a/test/pigeon/adm/notification_test.exs +++ /dev/null @@ -1,75 +0,0 @@ -defmodule Pigeon.ADM.NotificationTest do - use ExUnit.Case - doctest Pigeon.ADM.Notification - - def test_registration_id, do: "test1234" - def test_data, do: %{message: "your message"} - - test "new/1" do - expected_result = %Pigeon.ADM.Notification{ - registration_id: test_registration_id(), - payload: %{"data" => %{}}, - updated_registration_id: nil, - consolidation_key: nil, - expires_after: 604_800, - md5: "1B2M2Y8AsgTpgAmY7PhCfg==" - } - - 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"}}, - updated_registration_id: nil, - consolidation_key: nil, - expires_after: 604_800, - md5: "qzF+HgArKZjJrpfcTbiFxg==" - } - - 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"}} - } - - result_n = Pigeon.ADM.Notification.calculate_md5(n) - assert "w2qyl/pbK7HVl9zfzu7Nww==" == result_n.md5 - end - - test "does nothing if invalid data payload" do - n = %Pigeon.ADM.Notification{ - payload: :bad - } - - result_n = Pigeon.ADM.Notification.calculate_md5(n) - refute result_n.md5 - end - end - - test "ensure_strings" do - data = %{ - :message => "your message", - "string_key" => "string_value", - "something" => 123, - 456 => true - } - - n = Pigeon.ADM.Notification.new(test_registration_id(), data) - - expected_result = %{ - "message" => "your message", - "string_key" => "string_value", - "something" => "123", - "456" => "true" - } - - assert expected_result == n.payload["data"] - end -end diff --git a/test/pigeon/adm/result_parser_test.exs b/test/pigeon/adm/result_parser_test.exs deleted file mode 100644 index 8e3356f0..00000000 --- a/test/pigeon/adm/result_parser_test.exs +++ /dev/null @@ -1,19 +0,0 @@ -defmodule Pigeon.ADM.ResultParserTest do - use ExUnit.Case - doctest Pigeon.ADM.ResultParser, import: true - - test "parses known error reasons without crashing" do - n = Pigeon.ADM.Notification.new("test") - - Pigeon.ADM.ResultParser.parse(n, %{"reason" => "InvalidRegistrationId"}) - Pigeon.ADM.ResultParser.parse(n, %{"reason" => "InvalidData"}) - Pigeon.ADM.ResultParser.parse(n, %{"reason" => "InvalidConsolidationKey"}) - Pigeon.ADM.ResultParser.parse(n, %{"reason" => "InvalidExpiration"}) - Pigeon.ADM.ResultParser.parse(n, %{"reason" => "InvalidChecksum"}) - Pigeon.ADM.ResultParser.parse(n, %{"reason" => "InvalidType"}) - Pigeon.ADM.ResultParser.parse(n, %{"reason" => "Unregistered"}) - Pigeon.ADM.ResultParser.parse(n, %{"reason" => "AccessTokenExpired"}) - Pigeon.ADM.ResultParser.parse(n, %{"reason" => "MessageTooLarge"}) - Pigeon.ADM.ResultParser.parse(n, %{"reason" => "MaxRateExceeded"}) - end -end diff --git a/test/pigeon/adm_test.exs b/test/pigeon/adm_test.exs deleted file mode 100644 index 09a6884e..00000000 --- a/test/pigeon/adm_test.exs +++ /dev/null @@ -1,71 +0,0 @@ -defmodule Pigeon.ADMTest do - use ExUnit.Case - doctest Pigeon.ADM, import: true - doctest Pigeon.ADM.Config, import: true - - alias Pigeon.ADM.Notification - - @invalid_key_msg ~r/^attempted to start without valid client id and secret/ - - describe "init/1" do - test "initializes correctly if configured with client id and secret" do - opts = [client_id: "amzn1.iba-client.abc123", client_secret: "abc123"] - - expected = - {:ok, - %{ - config: %Pigeon.ADM.Config{ - client_id: "amzn1.iba-client.abc123", - client_secret: "abc123" - }, - access_token: nil, - access_token_refreshed_datetime_erl: {{0, 0, 0}, {0, 0, 0}}, - access_token_expiration_seconds: 0, - access_token_type: nil - }} - - assert Pigeon.ADM.init(opts) == expected - end - - test "raises if configured with invalid client id" do - assert_raise(Pigeon.ConfigError, @invalid_key_msg, fn -> - opts = [client_id: nil, client_secret: "abc123"] - Pigeon.ADM.init(opts) - end) - end - - test "raises if configured with invalid client secret" do - assert_raise(Pigeon.ConfigError, @invalid_key_msg, fn -> - opts = [ - client_id: "amzn1.iba-client.abc123", - client_secret: nil - ] - - Pigeon.ADM.init(opts) - end) - end - end - - describe "handle_push/3" do - test "returns an error on pushing with a bad registration_id" do - reg_id = "bad_reg_id" - n = Notification.new(reg_id, %{"message" => "example"}) - pid = self() - PigeonTest.ADM.push(n, on_response: fn x -> send(pid, x) end) - - assert_receive(n = %Notification{}, 5000) - assert n.response == :invalid_registration_id - assert n.registration_id == reg_id - assert n.payload == %{"data" => %{"message" => "example"}} - end - - test "handles nil on_response" do - n = Notification.new("bad_reg_id", %{"message" => "example"}) - PigeonTest.ADM.push(n, on_response: nil) - end - end - - test "handle_info/2 handles random messages" do - assert Pigeon.ADM.handle_info("random", %{}) == {:noreply, %{}} - end -end diff --git a/test/pigeon/apns_test.exs b/test/pigeon/apns_test.exs index 3655b7c1..c788f7aa 100644 --- a/test/pigeon/apns_test.exs +++ b/test/pigeon/apns_test.exs @@ -9,6 +9,7 @@ defmodule Pigeon.APNSTest do @invalid_key_msg ~r/^attempted to start without valid key/ @invalid_team_id_msg ~r/^attempted to start without valid team_id/ @invalid_key_id_msg ~r/^attempted to start without valid key_identifier/ + @default_timeout 10_000 def test_message(msg) do "#{DateTime.to_string(DateTime.utc_now())} - #{msg}" @@ -163,7 +164,10 @@ defmodule Pigeon.APNSTest do assert PigeonTest.APNS.push(n, on_response: on_response) == :ok - assert_receive(%Pigeon.APNS.Notification{response: :success}, 5_000) + assert_receive( + %Pigeon.APNS.Notification{response: :success}, + @default_timeout + ) end test "returns :bad_message_id response if apns-id is invalid" do @@ -179,7 +183,7 @@ defmodule Pigeon.APNSTest do assert_receive( %Pigeon.APNS.Notification{response: :bad_message_id}, - 5_000 + @default_timeout ) end @@ -196,7 +200,7 @@ defmodule Pigeon.APNSTest do assert_receive( %Pigeon.APNS.Notification{response: :bad_device_token}, - 5_000 + @default_timeout ) end @@ -211,7 +215,10 @@ defmodule Pigeon.APNSTest do assert PigeonTest.APNS.push(n, on_response: on_response) == :ok - assert_receive(%Pigeon.APNS.Notification{response: :missing_topic}, 5_000) + assert_receive( + %Pigeon.APNS.Notification{response: :missing_topic}, + @default_timeout + ) end end end diff --git a/test/pigeon/fcm_test.exs b/test/pigeon/fcm_test.exs index 98f7617f..ccf68378 100644 --- a/test/pigeon/fcm_test.exs +++ b/test/pigeon/fcm_test.exs @@ -10,6 +10,7 @@ defmodule Pigeon.FCMTest do @data %{"message" => "Test push"} @invalid_project_msg ~r/^attempted to start without valid :project_id/ @invalid_service_account_json_msg ~r/^attempted to start without valid :service_account_json/ + @default_timeout 10_000 defp valid_fcm_reg_id do Application.get_env(:pigeon, :test)[:valid_fcm_reg_id] @@ -36,7 +37,7 @@ defmodule Pigeon.FCMTest do notification = {:token, valid_fcm_reg_id()} |> Notification.new(%{}, @data) - |> PigeonTest.FCM.push() + |> PigeonTest.FCM.push(timeout: @default_timeout) assert notification.name end @@ -47,7 +48,7 @@ defmodule Pigeon.FCMTest do pid = self() PigeonTest.FCM.push(n, on_response: fn x -> send(pid, x) end) - assert_receive(n = %Notification{target: ^target}, 5000) + assert_receive(n = %Notification{target: ^target}, @default_timeout) assert n.name assert n.response == :success end @@ -64,7 +65,7 @@ defmodule Pigeon.FCMTest do Pigeon.push(dispatcher, n, on_response: fn x -> send(pid, x) end) - assert_receive(n = %Notification{target: ^target}, 5000) + assert_receive(n = %Notification{target: ^target}, @default_timeout) assert n.name assert n.response == :success end @@ -75,7 +76,7 @@ defmodule Pigeon.FCMTest do pid = self() PigeonTest.FCM.push(n, on_response: fn x -> send(pid, x) end) - assert_receive(n = %Notification{target: ^target}, 5000) + assert_receive(n = %Notification{target: ^target}, @default_timeout) assert n.error refute n.name assert n.response == :invalid_argument @@ -90,7 +91,7 @@ defmodule Pigeon.FCMTest do on_response: fn x -> send(pid, x) end ) - assert_receive(n = %Notification{target: ^target}, 5000) + assert_receive(n = %Notification{target: ^target}, @default_timeout) refute n.name assert n.response == :not_started end diff --git a/test/pigeon/legacy_fcm/notification_test.exs b/test/pigeon/legacy_fcm/notification_test.exs deleted file mode 100644 index 5c2fcb14..00000000 --- a/test/pigeon/legacy_fcm/notification_test.exs +++ /dev/null @@ -1,74 +0,0 @@ -defmodule Pigeon.LegacyFCM.NotificationTest do - use ExUnit.Case - - @reg_id "123456" - - test "LegacyFCM new with one registration_id" do - expected_result = %Pigeon.LegacyFCM.Notification{ - registration_id: @reg_id, - payload: %{} - } - - assert Pigeon.LegacyFCM.Notification.new(@reg_id) == expected_result - end - - test "LegacyFCM new with multiple registration_ids" do - reg_ids = ["aaaaaa", "bbbbbb", "cccccc"] - - expected_result = %Pigeon.LegacyFCM.Notification{ - registration_id: reg_ids, - payload: %{} - } - - assert Pigeon.LegacyFCM.Notification.new(reg_ids) == expected_result - end - - test "LegacyFCM new with notification map" do - n = %{ - "body" => "test body", - "title" => "Test Push", - "icon" => "icon" - } - - expected_result = %Pigeon.LegacyFCM.Notification{ - registration_id: @reg_id, - payload: %{"notification" => n} - } - - assert Pigeon.LegacyFCM.Notification.new(@reg_id, n) == expected_result - end - - test "LegacyFCM new with data map" do - data = %{ - "message" => "test" - } - - expected_result = %Pigeon.LegacyFCM.Notification{ - registration_id: @reg_id, - payload: %{"data" => data} - } - - assert Pigeon.LegacyFCM.Notification.new(@reg_id, %{}, data) == - expected_result - end - - test "LegacyFCM new with notification and data maps" do - n = %{ - "body" => "test body", - "title" => "Test Push", - "icon" => "icon" - } - - data = %{ - "message" => "test" - } - - expected_result = %Pigeon.LegacyFCM.Notification{ - registration_id: @reg_id, - payload: %{"notification" => n, "data" => data} - } - - assert Pigeon.LegacyFCM.Notification.new(@reg_id, n, data) == - expected_result - end -end diff --git a/test/pigeon/legacy_fcm/result_parser_test.exs b/test/pigeon/legacy_fcm/result_parser_test.exs deleted file mode 100644 index e3f3dbb6..00000000 --- a/test/pigeon/legacy_fcm/result_parser_test.exs +++ /dev/null @@ -1,87 +0,0 @@ -defmodule Pigeon.LegacyFCM.ResultParserTest do - use ExUnit.Case - - alias Pigeon.LegacyFCM.{Notification, ResultParser} - - def assert_response(notif, expected) do - assert notif.response == expected - end - - test "parse_result with success" do - notif = - ResultParser.parse( - ["regid"], - [%{"message_id" => "1:0408"}], - %Notification{} - ) - - assert notif.response == [success: "regid"] - end - - test "parse_result with single non-list regid" do - notif = - ResultParser.parse( - "regid", - [%{"message_id" => "1:0408"}], - %Notification{} - ) - - assert notif.response == [success: "regid"] - end - - test "parse_result with success and new registration_id" do - notif = - ResultParser.parse( - ["regid"], - [%{"message_id" => "1:2342", "registration_id" => "32"}], - %Notification{} - ) - - assert notif.response == [update: {"regid", "32"}] - assert notif.message_id == "1:2342" - end - - test "parse_result with error Unavailable" do - notif = - ResultParser.parse( - ["regid"], - [%{"error" => "Unavailable"}], - %Notification{} - ) - - assert notif.response == [unavailable: "regid"] - end - - test "parse_result with error NotRegistered" do - notif = - ResultParser.parse( - ["regid"], - [%{"error" => "NotRegistered"}], - %Notification{} - ) - - assert notif.response == [not_registered: "regid"] - end - - test "parse_result with error InvalidRegistration" do - notif = - ResultParser.parse( - ["regid"], - [%{"error" => "InvalidRegistration"}], - %Notification{} - ) - - assert notif.response == [invalid_registration: "regid"] - end - - test "parse_result with custom error" do - notif = - ResultParser.parse( - ["regid"], - [%{"error" => "CustomError"}], - %Notification{} - ) - - assert notif.response == [unknown_error: "regid"] - end -end diff --git a/test/pigeon/legacy_fcm_test.exs b/test/pigeon/legacy_fcm_test.exs deleted file mode 100644 index 446d4ca3..00000000 --- a/test/pigeon/legacy_fcm_test.exs +++ /dev/null @@ -1,71 +0,0 @@ -defmodule Pigeon.LegacyFCMTest do - use ExUnit.Case - doctest Pigeon.LegacyFCM, import: true - doctest Pigeon.LegacyFCM.Config, import: true - doctest Pigeon.LegacyFCM.Notification, import: true - - alias Pigeon.LegacyFCM.Notification - require Logger - - @data %{"message" => "Test push"} - @invalid_key_msg ~r/^attempted to start without valid key/ - - defp valid_fcm_reg_id do - Application.get_env(:pigeon, :test)[:valid_fcm_reg_id] - end - - describe "init/1" do - test "raises if configured with invalid key" do - assert_raise(Pigeon.ConfigError, @invalid_key_msg, fn -> - [key: nil] - |> Pigeon.LegacyFCM.init() - end) - end - end - - describe "handle_push/3" do - test "successfully sends a valid push" do - notification = - valid_fcm_reg_id() - |> Notification.new(%{}, @data) - |> PigeonTest.LegacyFCM.push() - - expected = [success: valid_fcm_reg_id()] - assert notification.response == expected - end - - test "successfully handles multiple registration_ids" do - notification = - [valid_fcm_reg_id(), "bad_reg_id"] - |> Notification.new(%{}, @data) - |> PigeonTest.LegacyFCM.push() - - assert notification.response[:success] == valid_fcm_reg_id() - assert notification.response[:invalid_registration] == "bad_reg_id" - end - - test "successfully sends a valid push with callback" do - reg_id = valid_fcm_reg_id() - n = Notification.new(reg_id, %{}, @data) - pid = self() - PigeonTest.LegacyFCM.push(n, on_response: fn x -> send(pid, x) end) - - assert_receive(n = %Notification{response: regids}, 5000) - assert n.status == :success - assert regids == [success: reg_id] - end - - test "returns an error on pushing with a bad registration_id" do - reg_id = "bad_reg_id" - n = Notification.new(reg_id, %{}, @data) - pid = self() - PigeonTest.LegacyFCM.push(n, on_response: fn x -> send(pid, x) end) - - assert_receive(n = %Notification{}, 5000) - assert n.status == :success - assert n.response == [invalid_registration: reg_id] - assert n.registration_id == reg_id - assert n.payload == %{"data" => @data} - end - end -end diff --git a/test/support/adm.ex b/test/support/adm.ex deleted file mode 100644 index 628ccaab..00000000 --- a/test/support/adm.ex +++ /dev/null @@ -1,4 +0,0 @@ -defmodule PigeonTest.ADM do - @moduledoc false - use Pigeon.Dispatcher, otp_app: :pigeon -end diff --git a/test/test_helper.exs b/test/test_helper.exs index 8bd694e6..2ff253e2 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,11 +1,9 @@ ExUnit.start(capture_log: true) workers = [ - PigeonTest.ADM, PigeonTest.APNS, PigeonTest.APNS.JWT, PigeonTest.FCM, - PigeonTest.LegacyFCM, PigeonTest.Sandbox ]