Files
music_library/test/error_tracker/error_notifier_test.exs
T
2026-05-22 07:42:54 +01:00

260 lines
7.1 KiB
Elixir

defmodule ErrorTracker.ErrorNotifierTest do
use ExUnit.Case, async: true
import Swoosh.TestAssertions
alias ErrorTracker.{Error, ErrorNotifier, Occurrence}
alias Swoosh.Adapters.Sandbox, as: SwooshSandbox
@config [
from_email: "test@example.com",
to_email: "admin@example.com",
mailer: MusicLibrary.Mailer,
base_url: "https://example.com"
]
defp occurrence(attrs \\ %{}) do
struct!(
%Occurrence{
error_id: 42,
reason: "** (RuntimeError) something went wrong",
context: %{"request.path" => "/test", "live_view.view" => "TestLive"},
stacktrace: %{
lines: [
%{
module: "MyApp.Controller",
function: "index/2",
file: "lib/my_app/controller.ex",
line: 10
},
%{
module: "Phoenix.Router",
function: "call/2",
file: "lib/phoenix/router.ex",
line: 50
}
]
}
},
attrs
)
end
setup do
:ok = SwooshSandbox.checkout()
previous = Application.get_env(:music_library, ErrorNotifier)
case Process.whereis(ErrorNotifier) do
nil -> :ok
pid -> GenServer.stop(pid)
end
on_exit(fn ->
SwooshSandbox.checkin()
case Process.whereis(ErrorNotifier) do
nil -> :ok
pid -> GenServer.stop(pid)
end
Application.put_env(:music_library, ErrorNotifier, previous || [])
end)
:ok
end
describe "init/1" do
test "returns :ignore when unconfigured" do
Application.put_env(:music_library, ErrorNotifier, [])
assert :ignore = ErrorNotifier.init(:unused)
end
end
describe "Email.send/2" do
test "builds and delivers an email with expected content" do
Application.put_env(:music_library, ErrorNotifier, @config)
assert {:ok, :sent} = ErrorNotifier.Email.send(occurrence(), "New Error! (something)")
assert_email_sent(fn email ->
assert email.subject =~ "[MusicLibrary] Error:"
assert email.subject =~ "controller.ex"
assert [{"", "admin@example.com"}] = email.to
assert {"MusicLibrary", "test@example.com"} = email.from
body = email.html_body
assert body =~ "New Error!"
assert body =~ "something went wrong"
assert body =~ "MyApp.Controller.index/2"
assert body =~ "https://example.com/dev/errors/42"
assert body =~ "/test"
assert body =~ "TestLive"
true
end)
end
test "handles occurrence without stacktrace lines" do
Application.put_env(:music_library, ErrorNotifier, @config)
occ = occurrence(%{stacktrace: %{lines: []}})
assert {:ok, :sent} = ErrorNotifier.Email.send(occ, "Error")
assert_email_sent(fn email ->
assert email.subject =~ "unknown"
assert not (email.html_body =~ "Stack Trace:")
end)
end
end
describe "throttling" do
test "throttles repeated errors within the window" do
Application.put_env(
:music_library,
ErrorNotifier,
Keyword.put(@config, :throttle_seconds, 60)
)
{:ok, pid} = ErrorNotifier.start_link([])
SwooshSandbox.allow(self(), pid)
:telemetry.execute([:error_tracker, :error, :new], %{}, %{
error: %ErrorTracker.Error{id: 1, muted: false},
occurrence: occurrence(%{error_id: 1})
})
:sys.get_state(ErrorNotifier)
assert_email_sent()
# Second notification for same error: throttled
:telemetry.execute([:error_tracker, :occurrence, :new], %{}, %{
error: %Error{id: 1},
occurrence: occurrence(%{error_id: 1}),
muted: false
})
:sys.get_state(ErrorNotifier)
refute_email_sent()
GenServer.stop(pid)
end
end
describe "skipping" do
test "skips notification when error is muted" do
Application.put_env(:music_library, ErrorNotifier, @config)
{:ok, pid} = ErrorNotifier.start_link([])
SwooshSandbox.allow(self(), pid)
:telemetry.execute([:error_tracker, :error, :new], %{}, %{
error: %Error{id: 1, muted: false},
occurrence: occurrence(%{error_id: 1})
})
:sys.get_state(ErrorNotifier)
assert_email_sent()
:telemetry.execute([:error_tracker, :occurrence, :new], %{}, %{
error: %Error{id: 2},
occurrence: occurrence(%{error_id: 2}),
muted: true
})
:sys.get_state(ErrorNotifier)
refute_email_sent()
GenServer.stop(pid)
end
end
describe "telemetry integration" do
test "sends email on :error_tracker :error :new event" do
Application.put_env(:music_library, ErrorNotifier, @config)
{:ok, pid} = ErrorNotifier.start_link([])
SwooshSandbox.allow(self(), pid)
:telemetry.execute([:error_tracker, :error, :new], %{}, %{
error: %Error{id: 99, muted: false},
occurrence: occurrence(%{error_id: 99})
})
:sys.get_state(ErrorNotifier)
assert_email_sent(subject: ~r/MusicLibrary/)
GenServer.stop(pid)
end
test "sends regression email on :error_tracker :error :unresolved event" do
Application.put_env(:music_library, ErrorNotifier, @config)
{:ok, pid} = ErrorNotifier.start_link([])
SwooshSandbox.allow(self(), pid)
:telemetry.execute([:error_tracker, :error, :unresolved], %{}, %{
error: %Error{id: 1, muted: false},
occurrence: occurrence(%{error_id: 1})
})
:sys.get_state(ErrorNotifier)
assert_email_sent(subject: ~r/MusicLibrary/)
GenServer.stop(pid)
end
test "skips when unresolved has nil occurrence (manual UI unresolve)" do
Application.put_env(:music_library, ErrorNotifier, @config)
{:ok, pid} = ErrorNotifier.start_link([])
SwooshSandbox.allow(self(), pid)
:telemetry.execute([:error_tracker, :error, :unresolved], %{}, %{
error: %Error{id: 1, muted: false},
occurrence: nil
})
:sys.get_state(ErrorNotifier)
refute_email_sent()
GenServer.stop(pid)
end
test "skips regression email when error is muted" do
Application.put_env(:music_library, ErrorNotifier, @config)
{:ok, pid} = ErrorNotifier.start_link([])
SwooshSandbox.allow(self(), pid)
:telemetry.execute([:error_tracker, :error, :unresolved], %{}, %{
error: %Error{id: 1, muted: true},
occurrence: occurrence(%{error_id: 1})
})
:sys.get_state(ErrorNotifier)
refute_email_sent()
GenServer.stop(pid)
end
test "skips notification for :new event when error struct is muted" do
Application.put_env(:music_library, ErrorNotifier, @config)
{:ok, pid} = ErrorNotifier.start_link([])
SwooshSandbox.allow(self(), pid)
# Defensive: new errors are never muted in practice (PR #174),
# but the handler reads metadata.error.muted so we verify it is respected.
:telemetry.execute([:error_tracker, :error, :new], %{}, %{
error: %Error{id: 3, muted: true},
occurrence: occurrence(%{error_id: 3})
})
:sys.get_state(ErrorNotifier)
refute_email_sent()
GenServer.stop(pid)
end
end
end