Amarula.Testing (amarula v0.1.0)
View SourceTest support for consumers of Amarula — drive your bot with synthetic inbound messages, with no WhatsApp connection.
A bot built on Amarula receives messages as {:whatsapp, :messages_upsert, %{from, id, messages: [%Amarula.Msg{}]}} delivered to its parent_pid, and
replies with Amarula.send_text/3 and friends. To exercise that — "when a
message like X arrives, does my bot reply with Y?" — you need a connection you
can feed messages into, and whose sends produce no real-world effect. That's what
this module gives you: an offline sandbox connection.
In sandbox mode the connection never reaches WhatsApp. Inbound messages are the
ones you deliver; outbound sends short-circuit to {:ok, msg_id} without
encrypting or putting a frame on any wire (see Amarula.new/1's offline: mode).
So your bot's full receive→reply path runs, unchanged, against nothing.
{:ok, conn} = Amarula.Testing.start_offline(profile: :test)
# The bot under test: replies "pong" to "ping".
defp handle({:whatsapp, :messages_upsert, %{messages: msgs}}, conn) do
for %Amarula.Msg{type: :text, content: "ping", channel: chan} <- msgs do
Amarula.send_text(conn, Amarula.Address.to_jid!(chan), "pong")
end
end
# Drive it: deliver an inbound message, let the bot react.
Amarula.Testing.deliver_text(conn, from: "15551234567@s.whatsapp.net", text: "ping")
assert_receive {:whatsapp, :messages_upsert, %{messages: [_]}} = event
handle(event, conn)
# The reply `Amarula.send_text(conn, ..., "pong")` returns `{:ok, id}` and
# does nothing else — no real message is sent.The delivered message is built by the real receive pipeline (decode → route →
classify → Amarula.Msg.from_proto/2), so the %Msg{} your bot sees is exactly
what production would produce — these helpers can't fabricate a Msg the wire
could never carry.
Notes
start_offline/1returns the connection pid (the same handleAmarula.connect/2returns). Pass it toAmarula.send_text/3etc.- Sends short-circuit to
{:ok, msg_id}.send_media/5is the exception — it uploads media, which needs a live socket, so it does not work in sandbox mode. - Events go to
:parent_pid(defaults to the calling process), soassert_receiveworks out of the box in the test that calledstart_offline/1. - Stop it with
Amarula.stop/1(or let the test process exit).
Summary
Types
The connection handle returned by start_offline/1.
Functions
Deliver an arbitrary message proto — the escape hatch for media, reactions,
locations, and anything else deliver_text/2 doesn't cover. Build the
%Amarula.Protocol.Proto.Message{} yourself; this wraps it in a <message>
stanza and runs it through the real receive pipeline.
Deliver a plain-text message to the connection, as if WhatsApp sent it.
Start an offline connection: :connected immediately, no websocket, no Noise
handshake, ready to receive injected messages.
Types
@type conn() :: pid()
The connection handle returned by start_offline/1.
Functions
Deliver an arbitrary message proto — the escape hatch for media, reactions,
locations, and anything else deliver_text/2 doesn't cover. Build the
%Amarula.Protocol.Proto.Message{} yourself; this wraps it in a <message>
stanza and runs it through the real receive pipeline.
proto = %Amarula.Protocol.Proto.Message{
imageMessage: %Amarula.Protocol.Proto.Message.ImageMessage{caption: "look"}
}
Amarula.Testing.deliver(conn, proto, from: "15551234567@s.whatsapp.net")Options:
:from— required; the sender's JID.:id— message id; random if absent.:participant— the writer's JID in a group (when:fromis the group).:recipient— the real recipient (used forfrom_meself/peer disambiguation).:from_me—trueto mark the message as sent by us (defaultfalse).:notify— the sender's display name (becomesMsg.pushname).
Deliver a plain-text message to the connection, as if WhatsApp sent it.
Options:
:from— required; the sender's JID (e.g."15551234567@s.whatsapp.net"), or a group JID for a group message (pair with:participant).:text— required; the message body.:id— the message id; a random one is generated if absent.:participant,:recipient,:from_me,:notify— stanza attributes for group / self-chat /from_mescenarios (seedeliver/2).
The bot's parent_pid receives {:whatsapp, :messages_upsert, %{messages: [%Amarula.Msg{type: :text, content: text}]}}.
Start an offline connection: :connected immediately, no websocket, no Noise
handshake, ready to receive injected messages.
Options:
:profile— required; the connection profile (e.g.:test). One connection per profile, so use a unique profile per test to run them async.:parent_pid— process that receives{:whatsapp, _, _}events. Defaults to the caller, soassert_receivein your test just works.:frame_sink— pid that receives{:frame_out, %Node{}}for every outbound frame (e.g. the delivery receipt Amarula sends for each inbound message, or anything your bot sends). Defaults to the caller. Drain or ignore these.:auth— credentials map. Defaults to freshly generated throwaway creds with a fakemeidentity, so you needn't pair. Override to control the logged-in identity (e.g. to test self-chat /from_mehandling).:storage— storage adapter spec. Defaults to aFileadapter under a throwaway temp dir; clean it up yourself if you need to.
Returns {:ok, pid} or {:error, reason} (e.g. {:already_running, pid} if a
connection for this profile is already up).