towerops/docs/templates/liveview-test.md
Graham McIntie 1590f78bdc fix: input validation, SSRF, API hardening, and cookie security
- 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
2026-03-14 14:48:59 -05:00

226 lines
6.3 KiB
Markdown

# 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`
```elixir
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_user`** is a shared setup helper defined in `ConnCase` — it creates a user and logs them in.
- **`Plug.Conn.put_session(:current_organization_id)`** sets the org context for the session.
- Use **`live/2`** to mount a LiveView, returns `{:ok, view, html}`.
- Use **`render_click/1`**, **`render_change/1`**, **`render_submit/1`** for events.
- Use **`element/2,3`** to target specific DOM elements.
- Use **`follow_redirect/2`** after `push_navigate` actions.
- Use **`render/1`** to get the current HTML after an event.