test/openagents_web/controllers/og_image_controller_test.exs

58e6347eeb72 · 14 KB

defmodule OpenAgentsWeb.OgImageControllerTest do
  @moduledoc """
  The card endpoint's contract: signed paths, public-only resolution,
  immutable caching, advisory versions, and a fallback that never errors.
  """

  use OpenAgentsWeb.ConnCase, async: false
  import Phoenix.LiveViewTest

  alias OpenAgents.Forge.Repos
  alias OpenAgents.Forum
  alias OpenAgents.Issues
  alias OpenAgents.PullRequests.PullRequest
  alias OpenAgents.Repo
  alias OpenAgents.Repositories
  alias OpenAgentsWeb.DocsCatalog

  @marker_png <<0x89, 0x50, 0x4E, 0x47, "FAKE-CARD-RENDER">>

  setup do
    base = Path.join(System.tmp_dir!(), "og-controller-#{System.unique_integer([:positive])}")
    File.mkdir_p!(base)

    previous = %{
      data: Application.get_env(:openagents, :forge_data_dir),
      wal: Application.get_env(:openagents, :forge_wal_dir),
      visibility: Application.get_env(:openagents, :forge_public_visibility),
      paths: Application.get_env(:openagents, :forge_public_paths),
      renderer: Application.get_env(:openagents, :og_rasterizer_mfa)
    }

    Application.put_env(:openagents, :forge_data_dir, Path.join(base, "data"))
    Application.put_env(:openagents, :forge_wal_dir, Path.join(base, "wal"))
    Application.put_env(:openagents, :forge_public_visibility, %{"openagents.com" => :l3})
    Application.delete_env(:openagents, :forge_public_paths)

    # Hermetic rendering: the endpoint contract does not depend on librsvg.
    Application.put_env(:openagents, :og_rasterizer_mfa, {__MODULE__, :fake_render, []})

    on_exit(fn ->
      if previous.data,
        do: Application.put_env(:openagents, :forge_data_dir, previous.data),
        else: Application.delete_env(:openagents, :forge_data_dir)

      if previous.wal,
        do: Application.put_env(:openagents, :forge_wal_dir, previous.wal),
        else: Application.delete_env(:openagents, :forge_wal_dir)

      if previous.visibility,
        do: Application.put_env(:openagents, :forge_public_visibility, previous.visibility),
        else: Application.delete_env(:openagents, :forge_public_visibility)

      if previous.paths,
        do: Application.put_env(:openagents, :forge_public_paths, previous.paths),
        else: Application.delete_env(:openagents, :forge_public_paths)

      if previous.renderer,
        do: Application.put_env(:openagents, :og_rasterizer_mfa, previous.renderer),
        else: Application.delete_env(:openagents, :og_rasterizer_mfa)

      File.rm_rf(base)
    end)

    shas = seed_repo("openagents.com")
    repository = Repositories.get_by_path!("OpenAgentsInc", "openagents.com")

    {:ok, repository: repository, shas: shas}
  end

  def fake_render(_svg), do: {:ok, @marker_png}

  defp signed_url(card) do
    path = OpenAgentsWeb.OG.request_path(card)
    path <> "?sig=" <> OpenAgentsWeb.OG.signature(path)
  end

  test "the static route serves the committed brand card", %{conn: conn} do
    conn = get(conn, "/og/static/card.png")

    assert response(conn, 200) == File.read!(committed_asset_path())
    assert resp_content_type(conn) == "image/png"
    assert cache_control(conn) == "public, max-age=21600, immutable"
  end

  test "a repository card renders with the endpoint's headers and cache policy", %{
    conn: conn,
    repository: repository
  } do
    conn = get(conn, signed_url(OpenAgentsWeb.OG.repo_card_for(repository)))

    assert response(conn, 200) == @marker_png
    assert resp_content_type(conn) == "image/png"
    assert cache_control(conn) == "public, max-age=21600, immutable"
    assert get_resp_header(conn, "x-content-type-options") == ["nosniff"]
  end

  test "issue cards render from the public issue path", %{conn: conn} do
    {:ok, issue} = Issues.create_issue(repository(), %{"title" => "Carded issue"})
    card = OpenAgentsWeb.OG.issue("OpenAgentsInc", "openagents.com", issue)

    assert response(get(conn, signed_url(card)), 200) == @marker_png
  end

  test "pull request cards render from the public pull request path", %{conn: conn} do
    {_issue, pull_request} = seed_pull_request("Carded pull request")

    card =
      OpenAgentsWeb.OG.pull_request("OpenAgentsInc", "openagents.com", pull_request)

    assert response(get(conn, signed_url(card)), 200) == @marker_png
  end

  test "docs cards render from the compile-time catalog and unknown slugs refuse", %{conn: conn} do
    {:ok, page} = DocsCatalog.render("stacked-pull-requests")

    assert response(get(conn, signed_url(OpenAgentsWeb.OG.docs(page))), 200) == @marker_png

    unknown = %OpenAgentsWeb.OG{
      kind: :docs,
      heading: "Nope",
      page_path: "/docs/never-existed",
      path_suffix: ["never-existed"]
    }

    assert response(get(conn, signed_url(unknown)), 404) == ""
  end

  test "forum board and topic cards render from public forum paths", %{conn: conn} do
    forum = seed_forum("general", "General")
    topic = seed_topic(forum)

    board_card = OpenAgentsWeb.OG.forum_board(forum)
    assert response(get(conn, signed_url(board_card)), 200) == @marker_png

    topic_card = OpenAgentsWeb.OG.forum_topic(forum, topic)
    assert response(get(conn, signed_url(topic_card)), 200) == @marker_png
  end

  test "private boards and archived topics refuse like a bad signature", %{conn: conn} do
    forum = seed_forum("staff", "Staff", visibility: "private")
    topic = seed_topic(forum)

    assert response(get(conn, signed_url(OpenAgentsWeb.OG.forum_board(forum))), 404) == ""
    assert response(get(conn, signed_url(OpenAgentsWeb.OG.forum_topic(forum, topic))), 404) == ""

    public = seed_forum("open", "Open")
    archived = seed_topic(public)

    archived
    |> Ecto.Changeset.change(archived_at: DateTime.utc_now())
    |> Repo.update!()

    assert response(get(conn, signed_url(OpenAgentsWeb.OG.forum_topic(public, archived))), 404) ==
             ""
  end

  test "blob cards pass through the same disclosure gate as the file page", %{
    conn: conn,
    repository: _repository
  } do
    card =
      OpenAgentsWeb.OG.blob("OpenAgentsInc", "openagents.com", "README.md", %{
        ref: "main",
        size: 42,
        lines: 2,
        truncated: false
      })

    assert response(get(conn, signed_url(card)), 200) == @marker_png
  end

  test "commit cards render for seeded commits", %{conn: conn, shas: shas} do
    card =
      OpenAgentsWeb.OG.commit(
        "OpenAgentsInc",
        "openagents.com",
        %{sha: shas.first, subject: "First commit", author: "Test Author", committed_at: nil},
        nil
      )

    assert response(get(conn, signed_url(card)), 200) == @marker_png
  end

  test "an invalid signature is refused like every other refusal", %{
    conn: conn,
    repository: repository
  } do
    path = OpenAgentsWeb.OG.request_path(OpenAgentsWeb.OG.repo_card_for(repository))
    unsigned = path
    forged = path <> "?sig=" <> String.duplicate("A", 22)

    refused_unsigned = get(conn, unsigned)
    refused_forged = get(conn, forged)

    assert response(refused_unsigned, 404) == ""
    assert response(refused_forged, 404) == ""
    assert get_resp_header(refused_unsigned, "cache-control") == ["public, max-age=60"]
  end

  test "private and unknown repositories are indistinguishable from signature refusals", %{
    conn: conn
  } do
    {:ok, _private} =
      Repositories.create_repository(%{
        owner: "SecondOrg",
        name: "secret-plans",
        visibility: "private"
      })

    private_card = repo_card_for_path(["SecondOrg", "secret-plans"])
    unknown_card = repo_card_for_path(["NobodyOrg", "never-existed"])

    real_path =
      OpenAgentsWeb.OG.request_path(
        OpenAgentsWeb.OG.repo_card_for(
          Repositories.get_by_path!("OpenAgentsInc", "openagents.com")
        )
      )

    bad_signature = real_path <> "?sig=bogus"

    refusals = [
      get(conn, signed_url(private_card)),
      get(conn, signed_url(unknown_card)),
      get(conn, bad_signature)
    ]

    assert Enum.all?(refusals, &(response(&1, 404) == ""))

    bodies = Enum.map(refusals, &response(&1, 404))
    assert length(Enum.uniq(bodies)) == 1
  end

  # The version segment exists so emitted URLs are content-addressed; it is
  # not an authorization input. A stale share with a wrong version still
  # heals to current data as long as its signature covers the path.
  test "versions are advisory: wrong version, valid signature, current card", %{
    conn: conn,
    repository: repository
  } do
    real_path = OpenAgentsWeb.OG.request_path(OpenAgentsWeb.OG.repo_card_for(repository))

    stale_path =
      Regex.replace(~r|^/og/v/[0-9a-f]+|, real_path, "/og/v/deadbeefcafe")

    stale_signed = stale_path <> "?sig=" <> OpenAgentsWeb.OG.signature(stale_path)

    conn = get(conn, stale_signed)
    assert response(conn, 200) == @marker_png
  end

  test "rasterizer failures degrade to the committed fallback card", %{
    conn: conn,
    repository: repository
  } do
    # Environment-independent: a failing renderer must produce the fallback
    # bytes whether or not the host has librsvg installed.
    Application.put_env(:openagents, :og_rasterizer_mfa, {__MODULE__, :failing_render, []})

    conn = get(conn, signed_url(OpenAgentsWeb.OG.repo_card_for(repository)))

    assert response(conn, 200) == File.read!(committed_asset_path())
    assert resp_content_type(conn) == "image/png"

    Application.put_env(:openagents, :og_rasterizer_mfa, {__MODULE__, :fake_render, []})
  end

  def failing_render(_svg), do: {:error, :rasterizer_failed}

  ## Meta-tag presence on the pages that emit them ----------------------------

  test "pages without a card emit honest site-level tags", %{conn: conn} do
    {:ok, _view, html} = live(conn, ~p"/")

    assert html =~ ~s(property="og:title" content="OpenAgents")
    assert html =~ ~s(name="twitter:card" content="summary_large_image")
    assert html =~ "og:image"
  end

  test "the repository page emits its own card URL", %{conn: conn} do
    {:ok, _view, html} = live(conn, ~p"/OpenAgentsInc/openagents.com")

    assert html =~
             ~r|property="og:image" content="[^"]*/og/v/[0-9a-f]{12}/repos/OpenAgentsInc/openagents\.com\.png\?sig=|

    assert html =~ ~s(OpenAgentsInc/openagents.com)
  end

  test "an issue page emits an issue-specific card URL", %{conn: conn} do
    {:ok, issue} = Issues.create_issue(repository(), %{"title" => "Shared on social"})

    {:ok, _view, html} = live(conn, ~p"/OpenAgentsInc/openagents.com/issues/#{issue.number}")

    assert html =~
             ~r|/og/v/[0-9a-f]{12}/repos/OpenAgentsInc/openagents\.com/issues/#{issue.number}\.png\?sig=|
  end

  test "a pull request page emits a pull-specific card URL", %{conn: conn} do
    {issue, _pull_request} = seed_pull_request("Shared pull request")

    {:ok, _view, html} = live(conn, ~p"/OpenAgentsInc/openagents.com/pulls/#{issue.number}")

    assert html =~
             ~r|/og/v/[0-9a-f]{12}/repos/OpenAgentsInc/openagents\.com/pulls/#{issue.number}\.png\?sig=|
  end

  test "a documentation page emits a docs-specific card URL", %{conn: conn} do
    {:ok, _view, html} = live(conn, ~p"/docs/stacked-pull-requests")

    assert html =~
             ~r|property="og:image" content="[^"]*/og/v/[0-9a-f]{12}/docs/stacked-pull-requests\.png\?sig=|
  end

  ## Helpers ------------------------------------------------------------------

  defp seed_pull_request(title) do
    repository = repository()
    {:ok, issue} = Issues.create_issue(repository, %{"title" => title})

    pull_request =
      %PullRequest{}
      |> PullRequest.changeset(%{
        repository_id: repository.id,
        issue_id: issue.id,
        head_repository_id: repository.id,
        head_ref: "feature",
        head_sha: String.duplicate("a", 40),
        base_ref: "main",
        base_sha: String.duplicate("b", 40)
      })
      |> Repo.insert!()

    {issue, Repo.preload(pull_request, [:issue, :head_repository])}
  end

  defp seed_forum(slug, title, attrs \\ []) do
    {:ok, forum} =
      %Forum.Forum{}
      |> Forum.Forum.changeset(
        Enum.into(attrs, %{slug: slug, title: title, description: "Everything else"})
      )
      |> Repo.insert()

    forum
  end

  defp seed_topic(forum) do
    {:ok, topic} =
      Forum.create_topic(forum, %{
        title: "Hello world",
        slug: "hello-world",
        body_text: "First post body",
        idempotency_key: Ecto.UUID.generate(),
        actor_ref: "agent:user_ed8297d8-1279-4b43-a1e7-f7867da19e20",
        actor_display_name: "Orrery",
        actor_slug: "orrery"
      })

    topic
  end

  defp committed_asset_path do
    Application.app_dir(:openagents, "priv/static/images/og-card-default.png")
  end

  defp repo_card_for_path([owner, name]) do
    suffix_card = %OpenAgentsWeb.OG{
      kind: :repo,
      kicker: "#{owner} /",
      heading: name,
      description: nil,
      title: "#{owner}/#{name}",
      page_path: "/#{owner}/#{name}",
      path_suffix: [owner, name]
    }

    suffix_card
  end

  defp cache_control(conn), do: conn |> get_resp_header("cache-control") |> List.first()

  defp resp_content_type(conn) do
    case get_resp_header(conn, "content-type") do
      [value | _] -> value
      [] -> nil
    end
  end

  ## Git fixture ---------------------------------------------------------------

  defp seed_repo(repo_name) do
    path = Repos.ensure_repo!(repo_name)

    readme = write_blob(path, "# OpenAgents\n\nCard fixture.\n")
    sample = write_blob(path, "defmodule Sample do\nend\n")

    lib_tree = mktree(path, "100644 blob #{sample}\tog_sample.ex\n")

    tree =
      mktree(
        path,
        "100644 blob #{readme}\tREADME.md\n" <>
          "040000 tree #{lib_tree}\tlib\n"
      )

    first = commit_tree(path, tree, [], "First commit\n")
    {_, 0} = Repos.git(path, ["update-ref", "refs/heads/main", first])

    %{first: first}
  end

  defp write_blob(path, content) do
    {sha, 0} = git_in(path, ["hash-object", "-w", "--stdin"], content)
    String.trim(sha)
  end

  defp mktree(path, listing) do
    {sha, 0} = git_in(path, ["mktree"], listing)
    String.trim(sha)
  end

  defp commit_tree(path, tree, parent_args, message) do
    {sha, 0} =
      git_in(path, ["commit-tree", tree] ++ parent_args, message,
        env: [
          {"GIT_AUTHOR_NAME", "Test Author"},
          {"GIT_AUTHOR_EMAIL", "author@example.test"},
          {"GIT_COMMITTER_NAME", "Test Author"},
          {"GIT_COMMITTER_EMAIL", "author@example.test"}
        ]
      )

    String.trim(sha)
  end

  defp git_in(path, args, stdin, opts \\ []) do
    input = Path.join(System.tmp_dir!(), "og-stdin-#{System.unique_integer([:positive])}")
    File.write!(input, stdin)

    try do
      System.cmd(
        "sh",
        ["-c", ~s(exec git "$@" < "$IN"), "git"] ++ args,
        cd: path,
        env: [{"IN", input}] ++ Keyword.get(opts, :env, []),
        stderr_to_stdout: true
      )
    after
      File.rm(input)
    end
  end

  defp repository do
    OpenAgents.Repositories.get_by_path!("OpenAgentsInc", "openagents.com")
  end
end