7.1 KiB · text 5af55d9
defmodule GitGud.Federation.OutboundTest do
@moduledoc """
Covers the outbound paths: `Federation.Outbound.offer_pull_request/4`
for federated PRs, and `Reports.forward_to_source/2` for cross-instance
abuse report forwarding. Both produce a local `fed_activities` row and
enqueue `DeliverOutbound` to a remote inbox.
"""
use GitGud.DataCase, async: false
use Oban.Testing, repo: GitGud.Repo
alias GitGud.Federation.Actor
alias GitGud.Federation.Outbound
alias GitGud.Federation.Workers.DeliverOutbound
alias GitGud.Repo
alias GitGud.Reports
import GitGud.ForgeFixtures
setup do
{user, source_repo} = repository_fixture(%{name: "local-src"})
remote_actor = insert_remote_repo_actor("https://remote.example/r/them/proj")
{:ok, user: user, source_repo: source_repo, remote_actor: remote_actor}
end
test "offer_pull_request builds + queues delivery to the remote repo's inbox", %{
user: user,
source_repo: source_repo,
remote_actor: remote_actor
} do
head_hex = String.duplicate("a", 40)
assert {:ok, activity} =
Outbound.offer_pull_request(user, source_repo, %{
target_repo_url: remote_actor.actor_url,
source_branch: "feature",
target_branch: "main",
head_hex: head_hex,
title: "Please consider",
body: "Body text"
})
assert activity.type == "Offer"
assert activity.is_local
assert activity.target_actor_id == remote_actor.id
assert get_in(activity.raw, ["object", "head"]) == head_hex
assert get_in(activity.raw, ["object", "sourceBranch"]) == "feature"
assert get_in(activity.raw, ["object", "targetBranch"]) == "main"
assert get_in(activity.raw, ["object", "type"]) == "Ticket"
assert get_in(activity.raw, ["object", "isMerge"]) == true
# A DeliverOutbound job was queued at the remote actor's inbox.
[job] = all_enqueued(worker: DeliverOutbound)
assert job.args["activity_id"] == activity.id
assert job.args["target_inbox_url"] == remote_actor.inbox_url
end
test "offer_pull_request rejects a target that isn't a Repository actor", %{
user: user,
source_repo: source_repo
} do
person_actor = insert_remote_person_actor("https://remote.example/@bob")
assert {:error, :target_not_a_repository} =
Outbound.offer_pull_request(user, source_repo, %{
target_repo_url: person_actor.actor_url,
source_branch: "f",
target_branch: "main",
head_hex: String.duplicate("b", 40),
title: "x",
body: nil
})
end
test "Reports.forward_to_source queues a Flag for a federated PR", %{
user: user,
source_repo: target_repo,
remote_actor: remote_actor
} do
# Set up a federated PR row directly so we don't need to do a git fetch.
pr =
%GitGud.PullRequests.PullRequest{
repository_id: target_repo.id,
source_actor_id: remote_actor.id,
activity_url: "https://remote.example/activities/offer/forwardme",
source_clone_url: "https://remote.example/r/them/proj.git",
source_ref: "feature",
target_ref: "main",
head_sha: :crypto.strong_rand_bytes(20),
base_sha: :crypto.strong_rand_bytes(20),
title: "remote pr",
number: 1,
state: "open"
}
|> Repo.insert!()
{:ok, report} =
Reports.open(user, {"pull_request", pr.id}, "abuse", "spam")
assert {:ok, activity} = Reports.forward_to_source(report, user)
assert activity.type == "Flag"
assert get_in(activity.raw, ["object"]) == [pr.activity_url]
assert activity.target_actor_id == remote_actor.id
[job] = all_enqueued(worker: DeliverOutbound)
assert job.args["activity_id"] == activity.id
end
test "Reports.forward_to_source refuses non-federated targets", %{
user: user,
source_repo: target_repo
} do
pr =
pull_request_fixture(
target_repo |> seed_branches(user),
user,
"feature",
"main"
)
{:ok, report} = Reports.open(user, {"pull_request", pr.id}, "abuse")
assert {:error, :not_federated} = Reports.forward_to_source(report, user)
end
# ── helpers ──────────────────────────────────────────────────────────
defp insert_remote_repo_actor(url) do
Repo.insert!(%Actor{
kind: "remote",
actor_type: "Repository",
actor_url: url,
inbox_url: url <> "/inbox",
shared_inbox_url: nil,
preferred_username: "them/proj",
name: "them/proj",
public_key_pem: "stub",
public_key_id: url <> "#main-key",
remote_doc: %{},
last_fetched_at: DateTime.utc_now(:second)
})
end
defp insert_remote_person_actor(url) do
Repo.insert!(%Actor{
kind: "remote",
actor_type: "Person",
actor_url: url,
inbox_url: url <> "/inbox",
preferred_username: "bob",
name: "bob",
public_key_pem: "stub",
public_key_id: url <> "#main-key",
remote_doc: %{},
last_fetched_at: DateTime.utc_now(:second)
})
end
# Quick branch setup so a local PR fixture can be made for the
# "non-federated rejection" test.
defp seed_branches(repo, user) do
path = repo.disk_path
main_sha = commit_file(path, "README.md", "main\n", "initial", nil)
:ok = GitGud.Git.update_ref(path, "refs/heads/main", main_sha, nil)
feat_sha = commit_file(path, "feature.txt", "f\n", "feature", main_sha)
:ok = GitGud.Git.update_ref(path, "refs/heads/feature", feat_sha, nil)
_ = user
repo
end
defp commit_file(path, name, contents, message, parent) do
blob_tmp = Path.join(System.tmp_dir!(), "outb-blob-#{System.unique_integer([:positive])}")
File.write!(blob_tmp, contents)
{hex, 0} = System.cmd("git", ["-C", path, "hash-object", "-w", blob_tmp])
File.rm(blob_tmp)
{:ok, blob_sha} = GitGud.Git.from_hex(String.trim(hex))
index = Path.join(System.tmp_dir!(), "outb-index-#{System.unique_integer([:positive])}")
env = [{"GIT_INDEX_FILE", index}]
try do
if parent do
{:ok, parent_commit} = GitGud.Git.commit(path, parent)
{_, 0} =
System.cmd(
"git",
["-C", path, "read-tree", GitGud.Git.to_hex(parent_commit.tree_sha)],
env: env
)
end
{_, 0} =
System.cmd(
"git",
[
"-C",
path,
"update-index",
"--add",
"--cacheinfo",
"100644," <> GitGud.Git.to_hex(blob_sha) <> "," <> name
],
env: env
)
{tree_hex, 0} = System.cmd("git", ["-C", path, "write-tree"], env: env)
{:ok, tree_sha} = GitGud.Git.from_hex(String.trim(tree_hex))
parents = if parent, do: [parent], else: []
{:ok, commit_sha} =
GitGud.Git.commit_tree(path, tree_sha, parents, message,
author: {"tester", "tester@example", DateTime.utc_now(:second)}
)
commit_sha
after
_ = File.rm(index)
end
end
end