defmodule FauxMQ do @moduledoc """ FauxMQ - a controllable dummy AMQP 0-9-1 broker for integration testing. FauxMQ runs a real TCP server that speaks the AMQP 0-9-1 protocol and allows you to control broker behaviour from Elixir tests. It is designed for testing ejabberd/XMPP and other systems that use AMQP for backend integration. Typical usage: * start a FauxMQ server on a random port * inject its `endpoint/1` into your system-under-test configuration * define expectations and stubs using `stub/3` and `expect/4` * run assertions against collected call history via `calls/1` This module exposes the main public API. All functions are dialyzer-friendly and documented with types. """ alias FauxMQ.{MockServer, Server, Types} @type server :: pid() @doc """ Starts a FauxMQ server process linked to the caller. Options: * `:host` - IP tuple (default: application `:default_host`) * `:port` - TCP port; `0` means use application `:default_port` (see `FauxMQ.Server` / README for binding behaviour) """ @spec start_link(keyword()) :: GenServer.on_start() def start_link(opts \\ []) do Server.start_link(opts) end @doc """ Returns a child spec for use in supervision trees. """ @spec child_spec(keyword()) :: Supervisor.child_spec() def child_spec(opts) do %{ id: Keyword.get(opts, :id, __MODULE__), start: {__MODULE__, :start_link, [opts]}, type: :worker, restart: :temporary, shutdown: 5_000 } end @doc """ Gracefully stops a FauxMQ server. """ @spec stop(server()) :: :ok def stop(server) do Server.stop(server) end @doc """ Returns the TCP port that the server is listening on. """ @spec port(server()) :: :inet.port_number() def port(server) do Server.port(server) end @doc """ Returns the host IP tuple that the server is bound to. """ @spec host(server()) :: :inet.ip_address() def host(server) do Server.host(server) end @doc """ Returns a convenient endpoint map `%{host: host, port: port}`. """ @spec endpoint(server()) :: %{host: :inet.ip_address(), port: :inet.port_number()} def endpoint(server) do %{host: host(server), port: port(server)} end @doc """ Registers a stub rule on the given server. The simple form `stub(server, match, action)` creates a rule that will be applied whenever an incoming AMQP method matches the given `match` pattern. The `action` controls how the server responds. See `FauxMQ.Types.match_spec/0` and `FauxMQ.Types.action_spec/0` for details. """ @spec stub(server(), Types.match_spec(), Types.action_spec()) :: :ok def stub(server, match, action) do MockServer.stub(server, match, action) end @doc """ Registers an expectation on the given server. Expectations are like stubs but are intended for asserting that certain calls happened a given number of times. The `times` argument controls how many times the rule may fire before it is removed. """ @spec expect(server(), Types.match_spec(), non_neg_integer(), Types.action_spec()) :: :ok def expect(server, match, times, action) when times >= 0 do MockServer.expect(server, match, times, action) end @doc """ Injects a delivery into the server for the specified logical queue/consumer. This is useful for tests that want to drive messages to consumers without going through `basic.publish` semantics. """ @spec push_delivery(server(), Types.push_delivery_spec()) :: :ok def push_delivery(server, delivery) do MockServer.push_delivery(server, delivery) end @doc """ Injects a raw AMQP frame-level action initiated by the server. """ @spec push_frame(server(), Types.push_frame_spec()) :: :ok def push_frame(server, frame_spec) do MockServer.push_frame(server, frame_spec) end @doc """ Returns the history of handled AMQP calls for the given server. """ @spec calls(server()) :: [Types.call_record()] def calls(server) do MockServer.calls(server) end @doc """ Resets mock rules and call history on the embedded `MockServer` only. In-memory AMQP queues/bindings/consumers on `FauxMQ.Server` are **not** cleared; use `reset_test_broker_state/1` between tests when published messages must not linger. """ @spec reset!(server()) :: :ok def reset!(server) do MockServer.reset!(server) end @doc """ Clears mock rules/history **and** in-memory queues, bindings, and consumers. TCP connections stay open. Prefer this between ExUnit cases so long `mix test` runs do not need a Docker/BEAM restart to match a clean broker. """ @spec reset_test_broker_state(server()) :: :ok def reset_test_broker_state(server) do Server.reset_test_broker_state(server) end end