defmodule TarakanWeb.InfestationLiveTest do use TarakanWeb.ConnCase, async: true import Phoenix.LiveViewTest alias Tarakan.FindingMemory test "index and show render multi-repo patterns", %{conn: conn} do submitter = github_account_fixture() other = github_account_fixture() repo_a = listed_github_repository_fixture(submitter) {:ok, repo_b} = Tarakan.Repositories.register_github_repository("acme/widget", other) repo_b = listed_repository_fixture(repo_b) title = "Live infestation #{System.unique_integer([:positive])}" findings = fn path -> Jason.encode!(%{ "tarakan_scan_format" => 1, "findings" => [ %{ "file" => path, "severity" => "critical", "title" => title, "description" => "Infestation liveview fixture." } ] }) end scan_fixture(repo_a, submitter, %{"findings_json" => findings.("a.ex")}) scan_fixture(repo_b, other, %{"findings_json" => findings.("b.ex")}) # repo_a is hit twice by the same pattern at different paths, so the # per-repo rollup and the per-finding ledger must disagree on row count. scan_fixture(repo_a, submitter, %{"findings_json" => findings.("a2.ex")}) pattern = FindingMemory.pattern_key(title) {:ok, index, html} = live(conn, ~p"/infestations") assert html =~ "Infestations" assert has_element?(index, "#infestations-constellation") assert has_element?(index, "#infestation-#{pattern}") {:ok, show, show_html} = live(conn, ~p"/infestations/#{pattern}") assert show_html =~ title assert has_element?(show, "#infestation-repo-count", "2") assert has_element?(show, "#infestation-graph") assert has_element?(show, "#infestation-instances") # The repo table is a rollup: one row per repository, carrying the counts # that distinguish it from the instance ledger below. graph_rows = show |> element("#infestation-graph tbody") |> render() assert graph_rows =~ "#{repo_a.owner}/#{repo_a.name}" assert graph_rows =~ "#{repo_b.owner}/#{repo_b.name}" row_a = show |> element("#infestation-graph-node-#{repo_a.id}") |> render() |> squish() assert row_a =~ ">2", "repo_a is hit twice and must report 2, not a blank cell" row_b = show |> element("#infestation-graph-node-#{repo_b.id}") |> render() |> squish() assert row_b =~ ">1", "repo_b is hit once and must report 1" # 2 repository rows, 3 instance rows: the two views are different grains. ledger = show |> element("#infestation-ledger") |> render() assert length(Regex.scan(~r/]+id="ledger_instances-/, ledger)) == 3 end describe "fix propagation" do setup %{conn: conn} do submitter = github_account_fixture() other = github_account_fixture() source = listed_github_repository_fixture(submitter) {:ok, target} = Tarakan.Repositories.register_github_repository("acme/widget", other) target = listed_repository_fixture(target) title = "Propagatable finding #{System.unique_integer([:positive])}" findings = Jason.encode!(%{ "tarakan_scan_format" => 1, "findings" => [ %{ "file" => "lib/handler.ex", "severity" => "critical", "title" => title, "description" => "Shared across both repositories." } ] }) source_scan = scan_fixture(source, submitter, %{"findings_json" => findings}) scan_fixture(target, other, %{"findings_json" => findings}) # Settle the source instance as fixed so a template can be captured. [occurrence] = source_scan.findings source_finding = Tarakan.Repo.get!(Tarakan.Scans.CanonicalFinding, occurrence.canonical_finding_id) |> Ecto.Changeset.change(%{ status: "fixed", fixed_at: DateTime.utc_now(), fixed_commit_sha: source_scan.commit_sha }) |> Tarakan.Repo.update!() %Tarakan.Scans.FindingCheck{} |> Tarakan.Scans.FindingCheck.changeset(%{ commit_sha: source_scan.commit_sha, verdict: "fixed", provenance: "human", notes: "Escaped the interpolated value and added a regression test." }) |> Ecto.Changeset.put_change(:canonical_finding_id, source_finding.id) |> Ecto.Changeset.put_change(:account_id, submitter.id) |> Tarakan.Repo.insert!() moderator = moderator_account_fixture() {:ok, template} = Tarakan.Fixes.capture_fix( Tarakan.Accounts.Scope.for_account(moderator), source_finding ) %{ conn: log_in_account(conn, moderator), pattern: FindingMemory.pattern_key(title), source: source, target: target, template: template } end test "shows the captured fix and carries it to the remaining repository", %{ conn: conn, pattern: pattern, source: source, target: target, template: template } do {:ok, view, html} = live(conn, ~p"/infestations/#{pattern}") assert html =~ "Fixed on" assert html =~ "#{source.owner}/#{source.name}" assert html =~ "Escaped the interpolated value" assert has_element?(view, "#infestation-propagate-fix", "Propagate to 1") html = view |> element("#infestation-propagate-fix") |> render_click() assert html =~ "Opened 1 fix job" assert html =~ "carried to 1 repo" # A real, public job now exists on the other repository, carrying the fix. # Scan submission auto-opens a verification job too, so reach the fix job # through the propagation rather than by repository alone. assert [propagation] = Tarakan.Fixes.list_propagations(template) assert propagation.repository_id == target.id task = propagation.review_task assert task.status == "open" assert task.visibility == "public" assert task.description =~ "Escaped the interpolated value" end test "anonymous visitors see the fix but cannot propagate it", %{pattern: pattern} do {:ok, view, html} = live(build_conn(), ~p"/infestations/#{pattern}") assert html =~ "Fixed on" refute has_element?(view, "#infestation-propagate-fix") end end # Collapse the whitespace HEEx leaves around interpolated cell values so # assertions can match on the value itself. defp squish(html) do html |> String.replace(~r/>\s+/, ">") |> String.replace(~r/\s+