PropertyDamage.Adapter.Injector behaviour (PropertyDamage v0.2.0)
View SourceBehaviour for adapters that receive and inject external events.
Adapter.Injector modules handle asynchronous event sources like webhooks, callbacks, message queues, or any external system that pushes events into the test. They transform incoming payloads into domain events and push them to the shared EventQueue for processing.
Direction of Event Flow
Unlike Adapter (which executes commands → produces events), Adapter.Injector receives external events → transforms → pushes to EventQueue:
External System → Adapter.Injector.to_event/1 → EventQueue → ExecutorLifecycle
Adapter.Injector modules follow a similar lifecycle to Adapters:
setup/1- Start listening (webhooks, subscriptions, etc.)- External events arrive →
to_event/1→EventQueue.push/3 teardown/1- Stop listening, cleanup
The @emits Attribute
Declare which events this adapter can emit for validation:
@emits [PaymentConfirmed, PaymentDeclined]The framework uses this for:
- Validating that Model.injectable_events/0 covers all possible injected events
- Detecting orphan events that no assertion projection handles
Example
defmodule MyTest.PaymentWebhookAdapter do
use PropertyDamage.Adapter.Injector
@emits [PaymentConfirmed, PaymentDeclined]
@impl true
def setup(config) do
{:ok, server} = MockWebhookServer.start_link(
port: config[:port],
handler: &handle_webhook(&1, config.event_queue)
)
{:ok, %{server: server}}
end
@impl true
def teardown(%{server: server}) do
MockWebhookServer.stop(server)
:ok
end
@impl true
def to_event(%{"type" => "payment.success", "order_id" => id}) do
{:ok, %PaymentConfirmed{order_id: id}}
end
def to_event(%{"type" => "payment.failed", "order_id" => id, "reason" => r}) do
{:ok, %PaymentDeclined{order_id: id, reason: r}}
end
def to_event(_unknown), do: :skip
defp handle_webhook(payload, queue) do
case to_event(payload) do
{:ok, event} -> EventQueue.push(queue, __MODULE__, event)
:skip -> :ok
end
end
endOptional Responses
Some systems expect responses to webhooks. Implement respond/2 to generate:
@impl true
def respond(%PaymentConfirmed{}, _context) do
{:ok, %{status: 200, body: ~s({"ack": true})}}
end
Summary
Callbacks
Optional: Generate a response for the external system.
Called once per run to start listening for external events.
Called once per run to stop listening and cleanup.
Transform an external payload into a domain event.
Callbacks
Optional: Generate a response for the external system.
Some webhook systems expect acknowledgment responses. Implement this callback to generate appropriate responses based on the event.
Returns
{:ok, response}- Response to send back:none- No response needed
Called once per run to start listening for external events.
The config will include:
:event_queue- PID of the EventQueue for pushing events- Any adapter-specific config from the test setup
Returns
{:ok, context}- Setup succeeded{:error, reason}- Setup failed
@callback teardown(context :: map()) :: :ok
Called once per run to stop listening and cleanup.
Returns
Always returns :ok. Handle errors internally.
Transform an external payload into a domain event.
Called when the adapter receives data from the external source. The payload format depends on the source (JSON, protobuf, raw data, etc.).
Returns
{:ok, event}- Successfully transformed to domain event:skip- Ignore this payload (not relevant to test){:error, reason}- Transformation failed