defmodule GitGud.Workflows.UserRunnersTest do
@moduledoc """
A user's own runners (`runners.owner_id`): registered with a token
from /users/settings/runners, serving the repositories that user owns
personally — never an organization's, though an org repository's
`owner_id` is its creator's.
"""
use GitGud.DataCase, async: false
alias GitGud.Accounts.User
alias GitGud.Organizations
alias GitGud.Repositories
alias GitGud.Workflows
alias GitGud.Workflows.Runner
alias GitGud.Workflows.Runners
alias GitGud.Workflows.WorkflowJob
alias GitGud.Workflows.WorkflowRun
alias GitGud.Workflows.Yaml
import GitGud.AccountsFixtures
import GitGud.ForgeFixtures
setup do
{user, repo} = repository_fixture()
{:ok, user: user, repo: repo}
end
# A queued job in `repo`, through `rerun/1`'s creation path (as
# MatrixDispatchTest does): a seed row and a re-run, no git tree.
defp queued_job(repo) do
{:ok, doc} =
Yaml.parse("""
name: ci
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: true
""")
seed =
%WorkflowRun{repository_id: repo.id}
|> WorkflowRun.create_changeset(%{
"number" => System.unique_integer([:positive]),
"workflow_path" => ".forgejo/workflows/ci.yml",
"workflow_name" => doc.name,
"trigger" => "push",
"head_ref" => "refs/heads/main",
"head_sha" => :crypto.strong_rand_bytes(20),
"workflow_doc" => doc.raw
})
|> Repo.insert!()
{:ok, run} = Workflows.rerun(seed)
[job] = Repo.all(from j in WorkflowJob, where: j.workflow_run_id == ^run.id)
job
end
defp runner(scope \\ :global) do
{:ok, runner, _uuid, _token} =
Runners.create_offline(%{name: "r-#{System.unique_integer([:positive])}"}, scope)
runner
end
defp org_with_repo(admin) do
handle = "o#{System.unique_integer([:positive])}"
{:ok, org} = Organizations.create_organization(admin, %{"handle" => handle, "name" => handle})
{:ok, repo} =
Repositories.create_repository_for_org(org, admin, %{
"name" => unique_repo_name(),
"visibility" => "private"
})
{org, repo}
end
describe "registration" do
test "a user's token registers one runner of theirs", %{user: user} do
{:ok, token} = Runners.mint_registration_token(user)
assert Runners.has_token?(Repo.reload!(user))
assert {:ok, runner, bearer} = Runners.register_with_token(token, %{name: "laptop"})
assert is_binary(bearer)
assert runner.owner_id == user.id
assert is_nil(runner.repository_id) and is_nil(runner.organization_id)
assert Runner.scope_label(runner) == "user"
assert :error = Runners.register_with_token(token, %{name: "again"})
refute Runners.has_token?(Repo.reload!(user))
end
test "consuming resolves to the user", %{user: user} do
{:ok, token} = Runners.mint_registration_token(user)
assert {:ok, %User{id: id}} = Runners.consume_registration_token(token)
assert id == user.id
assert :error = Runners.consume_registration_token(token)
end
test "a user's token expires", %{user: user} do
{:ok, token} = Runners.mint_registration_token(user)
user
|> Repo.reload!()
|> Ecto.Changeset.change(
runner_reg_token_expires_at: DateTime.add(DateTime.utc_now(:second), -1)
)
|> Repo.update!()
assert :error = Runners.register_with_token(token, %{name: "late"})
end
test "regenerating invalidates the prior token", %{user: user} do
{:ok, t1} = Runners.mint_registration_token(user)
{:ok, t2} = Runners.mint_registration_token(user)
assert :error = Runners.consume_registration_token(t1)
assert {:ok, %User{}} = Runners.consume_registration_token(t2)
end
test "registration is audited in the user's own log", %{user: user} do
{:ok, token} = Runners.mint_registration_token(user)
{:ok, runner, _} =
Runners.register_with_token(token, %{name: "laptop"}, audit: {127, 0, 0, 1})
assert [entry] = Repo.all(GitGud.Audit.Entry)
assert {entry.action, entry.target_id} == {"runner.registered", runner.id}
assert entry.subject_user_id == user.id
assert is_nil(entry.repository_id) and is_nil(entry.organization_id)
end
test "offline: create_offline/2 with a user", %{user: user} do
assert %Runner{owner_id: uid} = runner(user)
assert uid == user.id
end
test "a runner can't be in two scopes", %{user: user, repo: repo} do
assert {:error, cs} =
%Runner{token_hash: :crypto.strong_rand_bytes(32), uuid: Ecto.UUID.generate()}
|> Runner.changeset(%{name: "both", repository_id: repo.id, owner_id: user.id})
|> Repo.insert()
assert {"runner may be scoped to one of a user, a repository or an organization", _} =
cs.errors[:base]
end
end
describe "list_for_scope/1" do
test "a user sees their own and global runners, not someone else's", %{user: user} do
own = runner(user)
global = runner()
_other = runner(user_fixture())
{_, repo} = repository_fixture()
_repo_scoped = runner(repo)
assert Runners.list_for_scope(user) |> Enum.map(& &1.id) |> Enum.sort() ==
Enum.sort([own.id, global.id])
end
test "a personal repository inherits its owner's runners", %{user: user, repo: repo} do
own = runner(user)
global = runner()
repo_scoped = runner(repo)
_other = runner(user_fixture())
assert Runners.list_for_scope(repo) |> Enum.map(& &1.id) |> Enum.sort() ==
Enum.sort([own.id, global.id, repo_scoped.id])
end
test "an org repository doesn't, though its creator is its owner_id", %{user: user} do
{org, org_repo} = org_with_repo(user)
_own = runner(user)
org_runner = runner(org)
global = runner()
assert org_repo.owner_id == user.id
assert Runners.list_for_scope(org_repo) |> Enum.map(& &1.id) |> Enum.sort() ==
Enum.sort([org_runner.id, global.id])
end
test ":global leaves a user's runner out", %{user: user} do
_own = runner(user)
global = runner()
assert [%Runner{id: id}] = Runners.list_for_scope(:global)
assert id == global.id
end
end
describe "claiming" do
test "a user's runner takes a job in their personal repository", %{user: user, repo: repo} do
job = queued_job(repo)
assert %WorkflowJob{id: id} = Workflows.claim_next_job(runner(user))
assert id == job.id
end
test "not one in another user's repository", %{user: user} do
{_, theirs} = repository_fixture()
_job = queued_job(theirs)
assert Workflows.claim_next_job(runner(user)) == nil
end
test "not one in an org repository the user created and belongs to", %{user: user} do
{_org, org_repo} = org_with_repo(user)
job = queued_job(org_repo)
assert Workflows.claim_next_job(runner(user)) == nil
# Nor when asked for that job by id.
assert Workflows.claim_job_by_handle(runner(user), to_string(job.id)) == nil
end
test "not an unapproved fork run's", %{user: user, repo: repo} do
job = queued_job(repo)
Repo.update_all(from(r in WorkflowRun, where: r.id == ^job.workflow_run_id),
set: [fork: true, approved_at: nil]
)
assert Workflows.claim_next_job(runner(user)) == nil
end
test "a global runner still takes anything", %{user: user, repo: repo} do
{_org, org_repo} = org_with_repo(user)
{_, theirs} = repository_fixture()
jobs = Enum.map([repo, org_repo, theirs], &queued_job/1)
global = runner()
claimed = for _ <- jobs, do: Workflows.claim_next_job(global).id
assert Enum.sort(claimed) == Enum.sort(Enum.map(jobs, & &1.id))
end
test "repo and org runners keep to their own", %{user: user, repo: repo} do
{org, org_repo} = org_with_repo(user)
repo_job = queued_job(repo)
org_job = queued_job(org_repo)
assert Workflows.claim_next_job(runner(org)).id == org_job.id
assert Workflows.claim_next_job(runner(repo)).id == repo_job.id
assert Workflows.claim_next_job(runner(org)) == nil
assert Workflows.claim_next_job(runner(repo)) == nil
end
end
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
8.2 KiB · text
History
b868907