Skip to content

Instantly share code, notes, and snippets.

@pachun
Created July 11, 2025 16:10
Show Gist options
  • Save pachun/b2cff09e6f15ae72f9421f37a0e27446 to your computer and use it in GitHub Desktop.
Save pachun/b2cff09e6f15ae72f9421f37a0e27446 to your computer and use it in GitHub Desktop.
defmodule EmmaApi.ExpectDSL do
defmacro expect({op, _meta, _args} = assertion_ast) when op in [:==, :=~, :>, :<, :<=, :>=] do
quote do
assert unquote(assertion_ast)
end
end
defmacro expect(module) do
quote do: unquote(module)
end
defmacro to_have_received(module, function, args \\ []) do
call_ast = {{:., [], [module, function]}, [], args}
quote do
require Patch
Patch.assert_called(unquote(call_ast))
end
end
end
defmodule EmmaApi.AllowDSL do
defstruct [:module, :function, :arity]
def allow(module), do: %__MODULE__{module: module}
def to_receive(%__MODULE__{module: module} = dsl, function, arity) do
args = for _ <- 1..arity, do: Macro.var(:_, nil)
fun =
quote do
fn unquote_splicing(args) -> :stubbed end
end
Code.eval_quoted(
quote do
Patch.patch(unquote(module), unquote(function), unquote(fun))
end
)
%__MODULE__{dsl | function: function, arity: arity}
end
def and_respond(
%__MODULE__{module: module, function: function},
%Patch.Mock.Values.Sequence{} = sequence
) do
Patch.patch(module, function, sequence)
end
def and_respond(
%__MODULE__{module: module, function: function, arity: expected_arity},
response_fn
)
when is_function(response_fn) do
actual_arity =
case :erlang.fun_info(response_fn, :arity) do
{:arity, arity} -> arity
_ -> raise ArgumentError, "Could not determine function arity"
end
if actual_arity != expected_arity do
raise ArgumentError,
"Function given to `and_respond` has arity #{actual_arity}, but expected arity #{expected_arity}"
end
Patch.patch(module, function, response_fn)
end
end
defmodule EmmaApi.Aliases do
defmacro __using__(_opts) do
quote do
alias EmmaApi.{
GmailAccount,
Gmail.SendNewInboxEmailsPushNotifications,
Factory
}
alias EmmaApiWeb.GoogleJWTTokenValidator
end
end
end
defmodule EmmaApi.TestDSL do
defmacro __using__(_opts) do
quote do
use Patch
import EmmaApi.AllowDSL
import EmmaApi.ExpectDSL
use EmmaApi.Aliases
end
end
end
defmodule EmmaApiWeb.Gmail.WebhookControllerTest do
use EmmaApiWeb.ConnCase
use EmmaApi.TestDSL
test "sends new inbox email push notifications and returns an ok status", %{conn: conn} do
%{id: id, gmail_address: gmail_address} = Factory.insert(:gmail_account)
allow(SendNewInboxEmailsPushNotifications)
|> to_receive(:send, 2)
allow(GoogleJWTTokenValidator)
|> to_receive(:verify_and_validate, 1)
|> and_respond(
sequence([
fn "valid_token" -> {:ok, %{}} end,
fn "valid_token" -> {:error, "BAD"} end
])
)
response =
send_post(conn, "valid_token", %{
emailAddress: gmail_address,
historyId: "new_history_id"
})
expect(json_response(response, 200) == %{})
expect(EmmaApi.Gmail.SendNewInboxEmailsPushNotifications)
|> to_have_received(:send, [%GmailAccount{id: ^id}, "new_history_id"])
end
defp send_post(conn, jwt_token, body) do
conn
|> put_req_header("authorization", "Bearer " <> jwt_token)
|> post(
"/api/gmail_webhook",
%{
"message" => %{
"data" => Base.encode64(Jason.encode!(body))
}
}
)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment