defmodule GitGudWeb.RunnerControllerTest do
@moduledoc """
Round-trip tests against the runner Twirp endpoints in both JSON and
protobuf wire formats. Verifies the encoded shapes a real
forgejo-runner produces are decoded correctly and the responses we
produce decode back into the right message types.
"""
use GitGudWeb.ConnCase, async: false
import Ecto.Query, warn: false
alias Runner.V1.{
PingRequest,
PingResponse,
RegisterRequest,
RegisterResponse,
FetchTaskRequest,
FetchTaskResponse,
UpdateLogRequest,
UpdateLogResponse,
LogRow
}
@reg_token "test-reg-token"
setup do
original = Application.get_env(:git_gud, GitGud.Workflows.Runners, [])
Application.put_env(:git_gud, GitGud.Workflows.Runners, registration_token: @reg_token)
on_exit(fn -> Application.put_env(:git_gud, GitGud.Workflows.Runners, original) end)
:ok
end
describe "Ping" do
test "protobuf round-trip", %{conn: conn} do
body = PingRequest.encode(%PingRequest{data: "hello"}) |> IO.iodata_to_binary()
resp =
conn
|> put_req_header("content-type", "application/protobuf")
|> put_req_header("accept", "application/protobuf")
|> post("/api/actions/_apis/runner.v1.PingService/Ping", body)
assert resp.status == 200
assert PingResponse.decode(resp.resp_body).data == "hello"
end
test "json round-trip", %{conn: conn} do
resp =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/actions/_apis/runner.v1.PingService/Ping", %{"data" => "hello"})
assert resp.status == 200
assert Jason.decode!(resp.resp_body)["data"] == "hello"
end
end
describe "Register" do
test "happy path returns a runner + token (protobuf)", %{conn: conn} do
req = %RegisterRequest{name: "alpha", token: @reg_token, version: "v1", labels: ["x86_64"]}
body = RegisterRequest.encode(req) |> IO.iodata_to_binary()
resp =
conn
|> put_req_header("content-type", "application/protobuf")
|> post("/api/actions/_apis/runner.v1.RunnerService/Register", body)
assert resp.status == 200
decoded = RegisterResponse.decode(resp.resp_body)
assert decoded.runner.name == "alpha"
assert decoded.runner.token != ""
assert "x86_64" in decoded.runner.labels
end
test "bad registration token", %{conn: conn} do
req = %RegisterRequest{name: "x", token: "wrong"}
body = RegisterRequest.encode(req) |> IO.iodata_to_binary()
resp =
conn
|> put_req_header("content-type", "application/protobuf")
|> post("/api/actions/_apis/runner.v1.RunnerService/Register", body)
assert resp.status == 401
end
end
describe "Auth-gated methods" do
setup ctx do
{:ok, runner, token} =
GitGud.Workflows.Runners.register(%{
name: "test-runner",
version: "v1",
labels: %{"labels" => ["ubuntu-latest"]}
})
Map.merge(ctx, %{runner: runner, runner_token: token})
end
test "FetchTask returns empty task when queue is empty", %{
conn: conn,
runner_token: token
} do
body = FetchTaskRequest.encode(%FetchTaskRequest{tasks_version: 0}) |> IO.iodata_to_binary()
resp =
conn
|> put_req_header("content-type", "application/protobuf")
|> put_req_header("authorization", "Bearer " <> token)
|> post("/api/actions/_apis/runner.v1.RunnerService/FetchTask", body)
assert resp.status == 200
decoded = FetchTaskResponse.decode(resp.resp_body)
# No task queued in this test → task is nil
assert decoded.task == nil
end
test "FetchTask accepts x-runner-token header (real forgejo-runner shape)", %{
conn: conn,
runner_token: token
} do
body = FetchTaskRequest.encode(%FetchTaskRequest{tasks_version: 0}) |> IO.iodata_to_binary()
resp =
conn
|> put_req_header("content-type", "application/protobuf")
|> put_req_header("x-runner-token", token)
|> post("/api/actions/_apis/runner.v1.RunnerService/FetchTask", body)
assert resp.status == 200
end
test "FetchTask without token → 401", %{conn: conn} do
body = FetchTaskRequest.encode(%FetchTaskRequest{}) |> IO.iodata_to_binary()
resp =
conn
|> put_req_header("content-type", "application/protobuf")
|> post("/api/actions/_apis/runner.v1.RunnerService/FetchTask", body)
assert resp.status == 401
end
test "UpdateLog records a chunk and acks the count", %{
conn: conn,
runner_token: token
} do
# Set up a step to log against.
user = GitGud.AccountsFixtures.user_fixture()
{:ok, repo} = GitGud.Repositories.create_repository(user, %{"name" => "loggy"})
run =
%GitGud.Workflows.WorkflowRun{repository_id: repo.id}
|> GitGud.Workflows.WorkflowRun.create_changeset(%{
number: 1,
workflow_path: ".github/workflows/ci.yml",
trigger: "push",
head_sha: :crypto.strong_rand_bytes(20)
})
|> GitGud.Repo.insert!()
runner = %{conn: conn}[:runner] || GitGud.Repo.one!(GitGud.Workflows.Runner)
job =
%GitGud.Workflows.WorkflowJob{workflow_run_id: run.id}
|> GitGud.Workflows.WorkflowJob.create_changeset(%{
job_id: "build",
definition: %{}
})
|> GitGud.Repo.insert!()
# UpdateLog routes by the runner's current claim, not by an id
# in the request, so the test needs the job to look "running".
|> Ecto.Changeset.change(
status: "running",
runner_id: runner.id,
started_at: DateTime.utc_now(:second)
)
|> GitGud.Repo.update!()
step =
%GitGud.Workflows.WorkflowStep{workflow_job_id: job.id}
|> GitGud.Workflows.WorkflowStep.create_changeset(%{number: 0, name: "echo"})
|> GitGud.Repo.insert!()
req = %UpdateLogRequest{
task_id: 0,
index: 0,
rows: [
%LogRow{content: "line 1"},
%LogRow{content: "line 2"}
]
}
body = UpdateLogRequest.encode(req) |> IO.iodata_to_binary()
resp =
conn
|> put_req_header("content-type", "application/protobuf")
|> put_req_header("authorization", "Bearer " <> token)
|> post("/api/actions/_apis/runner.v1.RunnerService/UpdateLog", body)
assert resp.status == 200
assert UpdateLogResponse.decode(resp.resp_body).ack_index == 2
# Real log row landed in PG.
logs =
GitGud.Repo.all(
from l in "workflow_step_logs", where: l.workflow_step_id == ^step.id, select: l.body
)
assert Enum.any?(logs, &(&1 =~ "line 1"))
end
end
end
6.7 KiB · text
5af55d9