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

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_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.