test/openagents_web/authenticated_route_gate_test.exs

58e6347eeb72 · 3 KB

defmodule OpenAgentsWeb.AuthenticatedRouteGateTest do
  @moduledoc """
  The executable enumeration behind IDENTITY-002 and UI-001.

  IDENTITY-002 says every typed, memory, data, voice, and telemetry route fails
  before mutation without an active user, and UI-001 says the public default
  route is an authentication boundary. Both quantify over routes; both were
  proven by `test/openagents_web/auth_gate_test.exs`, which asks eleven
  hand-written paths whether they redirect. A route added without the
  `:authenticated` pipeline is not one of those eleven, so it serves an
  anonymous visitor and every test stays green — the shape ADMIN-001 carried
  before `443c74b`.

  This file derives the population instead. `OpenAgentsWeb.RouteAuthority`
  classifies every entry in the router, and `route_authority_test.exs` already
  fails a route it cannot classify. Each route it classifies
  `:authenticated_browser` is dispatched here without a session, and must
  refuse: a redirect to the public root, or `401` for the session-authenticated
  API routes that answer a client rather than a browser.

  The GitHub callback is in this class and refuses an anonymous request that
  does not carry a valid one-time OAuth attempt. The sign-in action is the
  bounded public action that creates that attempt, so it is classified
  separately.

  The websocket at `/live` is classified in the same class and is not
  dispatchable here; `OpenAgentsWeb.UserAuth.on_mount/4` gates it and
  `chat_live_test.exs` proves that path.
  """

  use OpenAgentsWeb.ConnCase, async: true

  import Phoenix.ConnTest

  alias OpenAgentsWeb.RouteAuthority

  @endpoint OpenAgentsWeb.Endpoint

  test "every authenticated browser route refuses an anonymous request" do
    routes =
      RouteAuthority.inventory()
      |> Enum.filter(&(&1.class == :authenticated_browser and &1.transport == :http))

    assert routes != [], "the route authority classified no authenticated browser route"

    for route <- routes do
      conn = dispatch_anonymously(route)
      location = conn |> Plug.Conn.get_resp_header("location") |> List.first()

      refused? =
        (conn.status == 302 and is_binary(location) and String.starts_with?(location, "/")) or
          conn.status == 401

      assert refused?, """
      `#{route.verb} #{route.path}` is classified `:authenticated_browser` but an
      anonymous request got #{conn.status}#{if location, do: " -> " <> location, else: ""}.

      IDENTITY-002 confines every ordinary server path to the active user's own
      data, and a route that answers without a session has no active user to
      confine it to. Move the route behind the `:authenticated` pipeline, or —
      if it is genuinely public — reclassify it in
      `OpenAgentsWeb.RouteAuthority` and say so in the invariant that covers it.
      """

      refute conn.status == 200,
             "`#{route.verb} #{route.path}` served an anonymous request."
    end
  end

  defp dispatch_anonymously(route) do
    path =
      route.path
      |> String.replace(":owner", "nobody")
      |> String.replace(":repo", "nonexistent")
      |> String.replace(":computer_id", "00000000-0000-4000-8000-000000000001")
      |> String.replace(":id", "00000000-0000-4000-8000-000000000001")

    dispatch(build_conn(), @endpoint, String.to_atom(route.verb), path)
  end
end