PushX.Test (PushX v0.15.0)

Copy Markdown View Source

Test delivery mode: assert what your application pushed without touching APNS or FCM.

Set delivery: :test in your test config and every send — PushX.push/4, push_data/4, batches, PushX.APNS/PushX.FCM directly, and named instances — is validated exactly as in production (required :topic, target format, :mode, payload encoding and size limits) and then recorded and answered locally with {:ok, %PushX.Response{status: :sent}} instead of being sent. No credentials are needed; retries, the circuit breaker and rate limiter are not involved.

# config/test.exs
config :pushx, delivery: :test

Recorded pushes are scoped to the test process: a test only sees the pushes made by itself and by processes it started (Task, Task.Supervisor children including push_batch/4 workers, and anything else that carries $callers), so async: true tests do not interfere. Records are deleted automatically when the test process exits, so memory stays bounded and no clear/0 between tests is needed.

Asserting

use ExUnit.Case, async: true
import PushX.Test.Assertions

test "order shipped notifies the customer" do
  MyApp.Orders.ship(order)

  push = assert_pushed(%{provider: :apns, target: ^device_token})
  assert push.payload["aps"]["alert"]["title"] == "Order shipped"
  assert push.opts[:topic] == "com.example.app"

  refute_pushed(%{provider: :fcm})
end

assert_pushed/1 and refute_pushed/1 take a pattern (pins allowed) that is matched against each recorded PushX.Test.Push.t/0; pushes/0 returns the raw list when you'd rather filter yourself.

Scripting failures

Use stub/1 to make specific pushes fail — the idiomatic way to test token cleanup, since :on_invalid_token and PushX.Response.should_remove_token?/1 behave exactly as with a real provider response:

test "dead tokens are deleted" do
  PushX.Test.stub(fn
    %{target: "dead-token"} -> {:error, :unregistered}
    _push -> :ok
  end)

  MyApp.Notifier.broadcast("Hi")

  assert_receive {:token_deleted, "dead-token"}
end

The stub receives the PushX.Test.Push.t/0 and returns :ok (delivered), {:error, status} (a PushX.Response error with that status), or a full {:ok, %PushX.Response{}} / {:error, %PushX.Response{}}. Stubs are per test process too.

Named instances in tests

PushX.Instance.start/3 validates credentials before starting, so use the throwaway-key helpers rather than committing keys. In test delivery mode no Goth process is started for FCM instances (nothing contacts Google):

PushX.Instance.start(:tenant_apns, :apns,
  key_id: "TEST", team_id: "TEST", private_key: PushX.Test.apns_private_key(), mode: :sandbox)

PushX.Instance.start(:tenant_fcm, :fcm,
  project_id: "tenant", credentials: PushX.Test.fcm_credentials())

In test delivery mode the instances never contact the providers.

What is not simulated

Test mode answers after local validation, so it cannot tell you what Apple or Google would have said about a token or payload — only that PushX would have sent it. Use stub/1 to model provider responses you care about.

Summary

Functions

True when delivery: :test is configured.

A freshly generated P-256 EC private key (PEM), the kind APNS signs with. Generated once per VM and cached; tied to no Apple account. For starting APNS instances in tests.

Forgets the current test process's recorded pushes and stub.

A service-account credentials map with a freshly generated RSA key, valid for PushX.Instance.start/3's credential validation. Generated once per VM and cached; tied to no Google project.

The most recent recorded push for the current test process, or nil.

All pushes recorded for the current test process (and processes it spawned), in recording order — for concurrent batch workers that is completion order, not input order.

Scripts responses for the current test process. fun receives each PushX.Test.Push.t/0 (with result: nil) and returns

A Web Push subscription map that passes validation (an https endpoint, a real P-256 p256dh point and a 16-byte auth secret), for sending through PushX.push(:webpush, ...) in test delivery mode — unlike an APNS/FCM token string, a subscription is checked cryptographically before it is recorded, so a hand-written %{"keys" => %{"p256dh" => "abc"}} is rejected as :invalid_token (and would fire :on_invalid_token). Fresh keys every call.

Functions

active?()

@spec active?() :: boolean()

True when delivery: :test is configured.

apns_private_key()

@spec apns_private_key() :: String.t()

A freshly generated P-256 EC private key (PEM), the kind APNS signs with. Generated once per VM and cached; tied to no Apple account. For starting APNS instances in tests.

clear()

@spec clear() :: :ok

Forgets the current test process's recorded pushes and stub.

Not required between tests (records are per process), but handy inside a test that exercises several scenarios.

fcm_credentials()

@spec fcm_credentials() :: map()

A service-account credentials map with a freshly generated RSA key, valid for PushX.Instance.start/3's credential validation. Generated once per VM and cached; tied to no Google project.

last_push()

@spec last_push() :: PushX.Test.Push.t() | nil

The most recent recorded push for the current test process, or nil.

pushes()

@spec pushes() :: [PushX.Test.Push.t()]

All pushes recorded for the current test process (and processes it spawned), in recording order — for concurrent batch workers that is completion order, not input order.

stub(fun)

@spec stub(
  (PushX.Test.Push.t() ->
     :ok
     | {:error, atom()}
     | {:ok, PushX.Response.t()}
     | {:error, PushX.Response.t()})
  | nil
) :: :ok

Scripts responses for the current test process. fun receives each PushX.Test.Push.t/0 (with result: nil) and returns:

  • :ok — delivered (the default when no stub is set)
  • {:error, status} — a PushX.Response error with that status atom
  • {:ok, %PushX.Response{}} or {:error, %PushX.Response{}} — verbatim

Pass nil to remove the stub.

webpush_subscription(opts \\ [])

@spec webpush_subscription(keyword()) :: map()

A Web Push subscription map that passes validation (an https endpoint, a real P-256 p256dh point and a 16-byte auth secret), for sending through PushX.push(:webpush, ...) in test delivery mode — unlike an APNS/FCM token string, a subscription is checked cryptographically before it is recorded, so a hand-written %{"keys" => %{"p256dh" => "abc"}} is rejected as :invalid_token (and would fire :on_invalid_token). Fresh keys every call.

sub = PushX.Test.webpush_subscription(endpoint: "https://push.example/abc")
PushX.push(:webpush, sub, "Hi")
assert_pushed(%{provider: :webpush, target: ^sub})