- LIKE wildcard injection: sanitize %, _ in search queries (devices, sites, gaiia) - Jason.decode! → Jason.decode with error handling for untrusted input - inspect() leak: replace with generic error messages, log details server-side - SSRF protection: URL validator blocks private IPs, localhost, non-HTTP schemes - SSRF validation added to HTTP monitoring executor and integration credentials - GraphQL complexity limits: always applied, not just in prod - GraphQL introspection: also check GET query params, not just body - Stripe webhook: explicit nil/empty checks for signature and body - Cookie security: secure flag for session (prod), http_only+secure for remember_me - Honeybadger API key: read from env var with fallback - String.to_integer → Integer.parse with fallback for URL params - String.to_atom → whitelist map for HTTP methods - Gaiia webhook: remove secret_len and expected signature from log - Admin API: add rate limiting pipeline - to_atom_keys: per-key fallback instead of all-or-nothing rescue
6.3 KiB
6.3 KiB
LiveView Test Template
Copy-pasteable template for testing a LiveView page following TowerOps conventions.
Replace __RESOURCE__ with PascalCase resource (e.g., Schedule).
Replace __resource__ with snake_case (e.g., schedule).
Replace __resources__ with plural snake_case (e.g., schedules).
Replace __CONTEXT__ with context module name (e.g., Schedules).
test/towerops_web/live/__resource___live_test.exs
defmodule ToweropsWeb.__RESOURCE__LiveTest do
use ToweropsWeb.ConnCase
import Phoenix.LiveViewTest
alias Towerops.__CONTEXT__
setup :register_and_log_in_user
setup %{conn: conn, user: user} do
{:ok, organization} =
Towerops.Organizations.create_organization(%{name: "Test Org", use_sites: true}, user.id)
conn = Plug.Conn.put_session(conn, :current_organization_id, organization.id)
%{conn: conn, organization: organization}
end
describe "Index" do
test "requires authentication" do
conn = build_conn()
{:error, redirect} = live(conn, ~p"/__resources__")
assert {:redirect, %{to: path}} = redirect
assert path == ~p"/users/log-in"
end
test "renders page title and header", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/__resources__")
assert html =~ "__RESOURCES_TITLE__"
end
test "shows empty state when no data exists", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/__resources__")
assert html =~ "No __resources__ found"
end
test "lists existing records", %{conn: conn, organization: org} do
{:ok, _} =
__CONTEXT__.create___resource__(%{
name: "Test Item",
organization_id: org.id
})
{:ok, _view, html} = live(conn, ~p"/__resources__")
assert html =~ "Test Item"
end
test "filters by status via URL params", %{conn: conn, organization: org} do
{:ok, _} =
__CONTEXT__.create___resource__(%{
name: "Active Item",
organization_id: org.id,
enabled: true
})
{:ok, _} =
__CONTEXT__.create___resource__(%{
name: "Archived Item",
organization_id: org.id,
enabled: false
})
{:ok, view, _html} = live(conn, ~p"/__resources__?filter=active")
html = render(view)
assert html =~ "Active Item"
refute html =~ "Archived Item"
end
test "navigates to new form", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/__resources__")
assert view
|> element("a", "New")
|> render_click()
|> follow_redirect(conn, ~p"/__resources__/new")
end
test "deletes a record", %{conn: conn, organization: org} do
{:ok, item} =
__CONTEXT__.create___resource__(%{
name: "To Delete",
organization_id: org.id
})
{:ok, view, html} = live(conn, ~p"/__resources__")
assert html =~ "To Delete"
view
|> element("button[phx-click='delete'][phx-value-id='#{item.id}']")
|> render_click()
html = render(view)
refute html =~ "To Delete"
end
end
describe "Form - New" do
test "renders new form", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/__resources__/new")
assert html =~ "New __RESOURCE_TITLE__"
assert html =~ "Save"
end
test "validates form on change", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/__resources__/new")
html =
view
|> form("#__resource__-form", __resource__: %{name: ""})
|> render_change()
assert html =~ "can't be blank"
end
test "creates record on valid submit", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/__resources__/new")
{:ok, _view, html} =
view
|> form("#__resource__-form", __resource__: %{name: "New Item"})
|> render_submit()
|> follow_redirect(conn)
assert html =~ "__RESOURCE_TITLE__ created"
assert html =~ "New Item"
end
end
describe "Form - Edit" do
test "renders edit form with existing data", %{conn: conn, organization: org} do
{:ok, item} =
__CONTEXT__.create___resource__(%{
name: "Existing Item",
organization_id: org.id
})
{:ok, _view, html} = live(conn, ~p"/__resources__/#{item.id}/edit")
assert html =~ "Edit __RESOURCE_TITLE__"
assert html =~ "Existing Item"
end
test "updates record on valid submit", %{conn: conn, organization: org} do
{:ok, item} =
__CONTEXT__.create___resource__(%{
name: "Original",
organization_id: org.id
})
{:ok, view, _html} = live(conn, ~p"/__resources__/#{item.id}/edit")
{:ok, _view, html} =
view
|> form("#__resource__-form", __resource__: %{name: "Updated"})
|> render_submit()
|> follow_redirect(conn)
assert html =~ "__RESOURCE_TITLE__ updated"
assert html =~ "Updated"
end
test "shows delete button in edit mode", %{conn: conn, organization: org} do
{:ok, item} =
__CONTEXT__.create___resource__(%{
name: "Delete Me",
organization_id: org.id
})
{:ok, _view, html} = live(conn, ~p"/__resources__/#{item.id}/edit")
assert html =~ "Delete"
end
test "deletes record from edit form", %{conn: conn, organization: org} do
{:ok, item} =
__CONTEXT__.create___resource__(%{
name: "Delete Me",
organization_id: org.id
})
{:ok, view, _html} = live(conn, ~p"/__resources__/#{item.id}/edit")
{:ok, _view, html} =
view
|> element("button[phx-click='delete']")
|> render_click()
|> follow_redirect(conn, ~p"/__resources__")
assert html =~ "__RESOURCE_TITLE__ deleted"
refute html =~ "Delete Me"
end
end
end
Notes
register_and_log_in_useris a shared setup helper defined inConnCase— it creates a user and logs them in.Plug.Conn.put_session(:current_organization_id)sets the org context for the session.- Use
live/2to mount a LiveView, returns{:ok, view, html}. - Use
render_click/1,render_change/1,render_submit/1for events. - Use
element/2,3to target specific DOM elements. - Use
follow_redirect/2afterpush_navigateactions. - Use
render/1to get the current HTML after an event.