prop/test/microwaveprop/beacon_monitors_test.exs
Graham McIntire ee7809e17b
hardening: Rust worker circuit-breaker + delete wall-clock sleep from beacon test
**Rust workers exit after 30 consecutive claim_next failures.**
Both `prop-grid-rs` and `hrrr-point-worker` ran a `while` loop that
slept 10s and retried forever on `claim_next` errors. If Postgres
went unreachable, pods would stay alive but useless — no circuit
breaker, no liveness probe, just an infinite stream of error logs.
Now we increment a consecutive-failure counter and exit with an
error once it hits 30 (~5 minutes of DB unavailability). k8s
restart policy then cycles the pod, which both triggers an
operator alert and gives us a fresh sqlx pool on the way back up.
A successful claim (Some or None) resets the counter so one
flaky query doesn't poison it.

**BeaconMonitorsTest: drop the Process.sleep(1100).** The test
relied on wall-clock gaps to disambiguate `inserted_at` values on
the `:utc_datetime` (second-precision) timestamps — one full
second of sleep per run, and on shared CI it could still flake
when both inserts landed in the same second. Replaced with an
explicit `Repo.update_all` that back-dates the first row by one
second. 1.1s faster, deterministic.
2026-04-21 18:02:46 -05:00

113 lines
3.9 KiB
Elixir

defmodule Microwaveprop.BeaconMonitorsTest do
use Microwaveprop.DataCase
import Microwaveprop.AccountsFixtures
alias Microwaveprop.BeaconMonitors
alias Microwaveprop.BeaconMonitors.BeaconMonitor
describe "create_monitor/2" do
test "creates a monitor with a generated token" do
user = user_fixture()
assert {:ok, %BeaconMonitor{} = monitor} =
BeaconMonitors.create_monitor(user, %{"name" => "Shack Pi"})
assert monitor.user_id == user.id
assert monitor.name == "Shack Pi"
assert is_binary(monitor.token)
# strong_rand_bytes(32) |> url_encode64(padding: false) is 43 chars
assert String.length(monitor.token) == 43
end
test "generates a unique token per monitor" do
user = user_fixture()
{:ok, m1} = BeaconMonitors.create_monitor(user, %{"name" => "One"})
{:ok, m2} = BeaconMonitors.create_monitor(user, %{"name" => "Two"})
refute m1.token == m2.token
end
test "requires a name" do
user = user_fixture()
assert {:error, changeset} = BeaconMonitors.create_monitor(user, %{"name" => ""})
assert %{name: ["can't be blank"]} = errors_on(changeset)
end
end
describe "list_monitors_for_user/1" do
test "returns monitors belonging to the user, newest first" do
user = user_fixture()
other = user_fixture()
# beacon_monitors.inserted_at is :utc_datetime (second precision).
# Back-date the first row one second via a direct update so the
# ordering assertion is deterministic without burning ~1s on a
# Process.sleep — this is a slow-test-suite footgun that also
# makes the test flaky on a shared CI runner where the two
# inserts can land in the same wall-clock second.
{:ok, first} = BeaconMonitors.create_monitor(user, %{"name" => "First"})
backdated = DateTime.add(first.inserted_at, -1, :second)
{1, _} =
Microwaveprop.Repo.update_all(
from(m in BeaconMonitor, where: m.id == ^first.id),
set: [inserted_at: backdated]
)
{:ok, second} = BeaconMonitors.create_monitor(user, %{"name" => "Second"})
{:ok, _other} = BeaconMonitors.create_monitor(other, %{"name" => "Nope"})
assert [second_result, first_result] = BeaconMonitors.list_monitors_for_user(user)
assert second_result.id == second.id
assert first_result.id == first.id
end
test "returns empty list when user has no monitors" do
user = user_fixture()
assert BeaconMonitors.list_monitors_for_user(user) == []
end
end
describe "delete_monitor/2" do
test "deletes a monitor owned by the user" do
user = user_fixture()
{:ok, monitor} = BeaconMonitors.create_monitor(user, %{"name" => "Bye"})
assert {:ok, %BeaconMonitor{}} = BeaconMonitors.delete_monitor(user, monitor.id)
assert BeaconMonitors.list_monitors_for_user(user) == []
end
test "does not delete a monitor owned by another user" do
user = user_fixture()
other = user_fixture()
{:ok, monitor} = BeaconMonitors.create_monitor(other, %{"name" => "Theirs"})
assert {:error, :not_found} = BeaconMonitors.delete_monitor(user, monitor.id)
assert [_] = BeaconMonitors.list_monitors_for_user(other)
end
test "returns :not_found for nonexistent id" do
user = user_fixture()
assert {:error, :not_found} =
BeaconMonitors.delete_monitor(user, "11111111-1111-1111-1111-111111111111")
end
end
describe "get_monitor_by_token/1" do
test "returns the monitor matching the token" do
user = user_fixture()
{:ok, monitor} = BeaconMonitors.create_monitor(user, %{"name" => "Find Me"})
found = BeaconMonitors.get_monitor_by_token(monitor.token)
assert found.id == monitor.id
end
test "returns nil for unknown token" do
refute BeaconMonitors.get_monitor_by_token("not-a-real-token")
end
end
end