defmodule GitGudWeb.RepoAuditTest do
@moduledoc """
A repository's life leaves audit entries — created (by hand, by fork,
over the API), its settings and visibility, its interaction policy,
deleted — and its own audit page shows them to the people who may
see them.
"""
use GitGudWeb.ConnCase, async: false
import Ecto.Query
import Phoenix.LiveViewTest
import GitGud.AccountsFixtures
alias GitGud.Accounts
alias GitGud.Audit.Entry
alias GitGud.Organizations
alias GitGud.Repositories
@moduletag :git_required
defp entries, do: GitGud.Repo.all(from e in Entry, order_by: e.id)
defp entries(repo), do: Enum.filter(entries(), &(&1.repository_id == repo.id))
defp org_fixture(admin) do
handle = "ra#{System.unique_integer([:positive])}"
{:ok, org} = Organizations.create_organization(admin, %{"handle" => handle, "name" => handle})
org
end
defp repo_for(user, name \\ "proj", visibility \\ "public") do
{:ok, repo} =
Repositories.create_repository(user, %{"name" => name, "visibility" => visibility})
repo
end
defp rpath(repo, suffix \\ "") do
repo = GitGud.Repo.preload(repo, [:owner, :organization])
"/r/#{GitGud.Repositories.Storage.repo_handle(repo)}/#{repo.name}" <> suffix
end
describe "creating" do
test "a personal repository", %{conn: conn} do
user = user_fixture()
{:ok, lv, _} = live(log_in_user(conn, user), ~p"/repositories/new")
lv
|> form("#new-repo-form", %{"repository" => %{"name" => "fresh", "visibility" => "private"}})
|> render_submit()
assert [entry] = entries()
assert entry.action == "repo.created"
assert entry.actor_id == user.id
assert entry.repository_label == "#{user.handle}/fresh"
assert entry.metadata == %{"visibility" => "private"}
assert entry.organization_id == nil
end
test "an org's repository belongs to the org's log", %{conn: conn} do
admin = user_fixture()
org = org_fixture(admin)
{:ok, lv, _} = live(log_in_user(conn, admin), ~p"/repositories/new/#{org.handle}")
lv
|> form("#new-repo-form", %{"repository" => %{"name" => "web", "visibility" => "private"}})
|> render_submit(%{"namespace" => "org:#{org.handle}"})
assert [%Entry{action: "repo.created", organization_id: oid}] = entries()
assert oid == org.id
end
test "a failed create records nothing", %{conn: conn} do
user = user_fixture()
_ = repo_for(user, "taken")
{:ok, lv, _} = live(log_in_user(conn, user), ~p"/repositories/new")
lv
|> form("#new-repo-form", %{"repository" => %{"name" => "taken"}})
|> render_submit()
assert entries() == []
end
test "a fork, on both repositories", %{conn: conn} do
owner = user_fixture()
source = repo_for(owner, "upstreamy")
forker = user_fixture()
{:ok, lv, _} = live(log_in_user(conn, forker), rpath(source, "/fork"))
render_submit(lv, "save", %{"namespace" => "user", "name" => "mine"})
fork = Repositories.get_repository_by_path!(forker.handle, "mine")
assert [created] = entries(fork)
assert created.metadata == %{"forked_from" => "#{owner.handle}/upstreamy"}
assert [forked] = entries(source)
assert forked.action == "repo.forked"
assert forked.metadata == %{"destination" => "#{forker.handle}/mine"}
end
test "over the API, with the address it came from" do
admin = user_fixture()
org = org_fixture(admin)
{:ok, _t, token} = Accounts.create_personal_access_token(admin, "t", ["repo"])
conn =
build_conn()
|> put_req_header("authorization", "token " <> token)
|> put_req_header("user-agent", "gg-test/1")
|> Map.put(:remote_ip, {198, 51, 100, 4})
conn = post(conn, "/api/v1/orgs/#{org.handle}/repos", %{"name" => "api-made"})
assert conn.status == 201
assert [entry] = entries()
assert {entry.action, entry.via} == {"repo.created", "api"}
assert {entry.ip, entry.user_agent} == {"198.51.100.4", "gg-test/1"}
end
end
describe "settings" do
setup %{conn: conn} do
user = user_fixture()
repo = repo_for(user)
{:ok, lv, _} = live(log_in_user(conn, user), rpath(repo, "/settings"))
{:ok, user: user, repo: repo, lv: lv}
end
test "one save, an entry per kind of change", %{repo: repo, lv: lv} do
save = fn attrs -> render_submit(lv, "save", %{"repository" => attrs}) end
save.(%{"description" => "now with words", "visibility" => "private"})
save.(%{"description" => "now with words", "visibility" => "private"})
assert [settings, visibility] = entries(repo)
assert settings.action == "repo.settings_updated"
assert settings.metadata == %{"description" => "changed"}
assert visibility.action == "repo.visibility_changed"
assert visibility.metadata == %{"from" => "public", "to" => "private"}
end
test "a changed setting keeps old and new", %{repo: repo, lv: lv} do
render_submit(lv, "save", %{"repository" => %{"required_approvals" => "2"}})
assert [entry] = entries(repo)
assert entry.metadata == %{"required_approvals" => "0 → 2"}
end
test "the interaction policy", %{conn: conn, user: user, repo: repo} do
{:ok, lv, _} = live(log_in_user(conn, user), rpath(repo, "/settings/interaction"))
before = repo.interaction_policy
other = Enum.find(~w(public followers approved), &(&1 != before))
render_submit(lv, "save", %{"repository" => %{"interaction_policy" => other}})
render_submit(lv, "save", %{"repository" => %{"interaction_policy" => other}})
assert [entry] = entries(repo)
assert entry.action == "repo.interaction_policy_changed"
assert entry.metadata == %{"from" => before, "to" => other}
end
test "deleting keeps the name after the repository is gone", %{user: user, repo: repo, lv: lv} do
render_change(lv, "delete_confirm", %{"value" => repo.name})
render_click(lv, "delete", %{})
assert [entry] = entries()
assert entry.action == "repo.deleted"
assert entry.repository_id == nil
assert entry.repository_label == "#{user.handle}/#{repo.name}"
end
end
test "deleting over the API" do
admin = user_fixture()
org = org_fixture(admin)
{:ok, repo} = Repositories.create_repository_for_org(org, admin, %{"name" => "doomed"})
{:ok, _t, token} = Accounts.create_personal_access_token(admin, "t", ["repo"])
conn =
build_conn()
|> put_req_header("authorization", "token " <> token)
|> delete("/api/v1/orgs/#{org.handle}/repos/doomed")
assert conn.status == 204
assert [entry] = entries()
assert {entry.action, entry.organization_id, entry.via} == {"repo.deleted", org.id, "api"}
refute GitGud.Repo.get(Repositories.Repository, repo.id)
end
describe "the repository's audit page" do
test "its owner sees its entries, not others'", %{conn: conn} do
user = user_fixture()
repo = repo_for(user)
other = repo_for(user, "other")
conn = log_in_user(conn, user)
{:ok, lv, _} = live(conn, rpath(repo, "/settings"))
render_submit(lv, "save", %{"repository" => %{"description" => "x"}})
{:ok, lv, _} = live(conn, rpath(other, "/settings"))
render_submit(lv, "save", %{"repository" => %{"description" => "y"}})
[mine] = entries(repo)
[theirs] = entries(other)
{:ok, view, _} = live(conn, rpath(repo, "/settings/audit"))
assert has_element?(view, "#audit-entry-#{mine.id}")
refute has_element?(view, "#audit-entry-#{theirs.id}")
assert has_element?(view, "#audit-category-settings")
end
test "an org repository's: org admins and instance admins, not team admins", %{conn: conn} do
admin = user_fixture()
org = org_fixture(admin)
# Public: an instance admin isn't a reader of a private repository,
# and RepoAuth turns them away before the page is reached.
{:ok, repo} =
Repositories.create_repository_for_org(org, admin, %{
"name" => "svc",
"visibility" => "public"
})
{:ok, team} = Organizations.create_team(org, %{"name" => "ops"})
team_admin = user_fixture()
{:ok, _} = Organizations.add_member(org, team_admin)
{:ok, _} = Organizations.add_team_member(team, team_admin, "maintainer")
{:ok, _} = Organizations.grant_team_repo(team, repo, "admin")
staff = user_fixture() |> Accounts.User.admin_changeset(true) |> GitGud.Repo.update!()
for user <- [admin, staff] do
assert {:ok, _, _} = live(log_in_user(conn, user), rpath(repo, "/settings/audit"))
end
for user <- [team_admin, user_fixture()] do
assert {:error, {:redirect, %{to: to}}} =
live(log_in_user(conn, user), rpath(repo, "/settings/audit"))
assert to == rpath(repo)
end
end
test "filters by who, not by repository", %{conn: conn} do
user = user_fixture()
repo = repo_for(user)
conn = log_in_user(conn, user)
{:ok, lv, _} = live(conn, rpath(repo, "/settings"))
render_submit(lv, "save", %{"repository" => %{"description" => "x"}})
[mine] = entries(repo)
base = rpath(repo, "/settings/audit")
{:ok, view, _} = live(conn, base <> "/all/actor/#{user.handle}")
assert has_element?(view, "#audit-entry-#{mine.id}")
refute has_element?(view, "#audit-filter select")
{:ok, view, _} = live(conn, base <> "/all/actor/someone-else")
refute has_element?(view, "#audit-entry-#{mine.id}")
assert {:error, {:live_redirect, %{to: ^base}}} = live(conn, base <> "/all/repo/x")
end
test "it takes sudo mode", %{conn: conn} do
user = user_fixture()
repo = repo_for(user)
stale = DateTime.add(DateTime.utc_now(:second), -30, :minute)
conn = log_in_user(conn, user, token_authenticated_at: stale)
assert {:error, {:redirect, %{to: to}}} = live(conn, rpath(repo, "/settings/audit"))
assert to =~ "/users/log-in?"
end
test "the settings header links to it", %{conn: conn} do
user = user_fixture()
repo = repo_for(user)
{:ok, view, _} = live(log_in_user(conn, user), rpath(repo, "/settings"))
assert has_element?(view, ~s(a[href="#{rpath(repo, "/settings/audit")}"]))
end
end
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
10.2 KiB · text
History
b868907