Initial commit on Forgejo
Fresh repository history for elektrine/tarakan hosted at https://git.elektrine.com/elektrine/tarakan.
This commit is contained in:
commit
af6077b9c3
455 changed files with 78366 additions and 0 deletions
148
test/tarakan_web/controllers/api/client_auth_controller_test.exs
Normal file
148
test/tarakan_web/controllers/api/client_auth_controller_test.exs
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
defmodule TarakanWeb.API.ClientAuthControllerTest do
|
||||
use TarakanWeb.ConnCase, async: true
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Tarakan.Accounts.ApiCredentials
|
||||
|
||||
test "browser approval exchanges a device code for one scoped credential", %{conn: conn} do
|
||||
start_response =
|
||||
conn
|
||||
|> post(~p"/api/client-auth/start", %{client_name: "Tarakan CLI on laptop"})
|
||||
|> json_response(201)
|
||||
|
||||
assert start_response["device_code"] =~ "trkd_"
|
||||
assert start_response["user_code"] =~ ~r/^[A-Z2-7]{4}-[A-Z2-7]{4}$/
|
||||
|
||||
assert start_response["verification_uri_complete"] =~
|
||||
"/client/authorize/#{start_response["user_code"]}"
|
||||
|
||||
pending =
|
||||
build_conn()
|
||||
|> post(~p"/api/client-auth/exchange", %{device_code: start_response["device_code"]})
|
||||
|> json_response(400)
|
||||
|
||||
assert pending == %{"error" => "authorization_pending"}
|
||||
|
||||
account = account_fixture()
|
||||
|
||||
{:ok, view, _html} =
|
||||
build_conn()
|
||||
|> log_in_account(account)
|
||||
|> live(~p"/client/authorize/#{start_response["user_code"]}")
|
||||
|
||||
assert has_element?(view, "#client-authorization-code")
|
||||
assert has_element?(view, "#client-authorization-approve-button")
|
||||
|
||||
view
|
||||
|> element("#client-authorization-approve-button")
|
||||
|> render_click()
|
||||
|
||||
assert has_element?(view, "#client-authorization-approved")
|
||||
|
||||
exchange_response =
|
||||
build_conn()
|
||||
|> post(~p"/api/client-auth/exchange", %{device_code: start_response["device_code"]})
|
||||
|> json_response(200)
|
||||
|
||||
assert exchange_response["token"] =~ "trkn_"
|
||||
assert exchange_response["token_type"] == "Bearer"
|
||||
assert "tasks:read" in exchange_response["scopes"]
|
||||
assert "tasks:claim" in exchange_response["scopes"]
|
||||
assert "contributions:write" in exchange_response["scopes"]
|
||||
assert "reviews:submit" in exchange_response["scopes"]
|
||||
refute "reviews:verify" in exchange_response["scopes"]
|
||||
refute "reviews:read" in exchange_response["scopes"]
|
||||
|
||||
assert {:ok, ^account, credential} = ApiCredentials.authenticate(exchange_response["token"])
|
||||
assert credential.name == "Tarakan CLI on laptop"
|
||||
|
||||
# Device-minted credentials are short-lived (agent blast-radius control).
|
||||
expires_in_days = DateTime.diff(credential.expires_at, DateTime.utc_now(), :day)
|
||||
assert expires_in_days in 6..7
|
||||
|
||||
consumed =
|
||||
build_conn()
|
||||
|> post(~p"/api/client-auth/exchange", %{device_code: start_response["device_code"]})
|
||||
|> json_response(400)
|
||||
|
||||
assert consumed == %{"error" => "invalid_device_code"}
|
||||
|
||||
revoke_conn =
|
||||
build_conn()
|
||||
|> put_req_header("authorization", "Bearer #{exchange_response["token"]}")
|
||||
|> delete(~p"/api/client-auth/session")
|
||||
|
||||
assert response(revoke_conn, 204) == ""
|
||||
assert :error = ApiCredentials.authenticate(exchange_response["token"])
|
||||
end
|
||||
|
||||
test "the browser can deny a login without issuing a credential", %{conn: conn} do
|
||||
start_response =
|
||||
conn
|
||||
|> post(~p"/api/client-auth/start", %{})
|
||||
|> json_response(201)
|
||||
|
||||
{:ok, view, _html} =
|
||||
build_conn()
|
||||
|> log_in_account(account_fixture())
|
||||
|> live(~p"/client/authorize/#{start_response["user_code"]}")
|
||||
|
||||
view
|
||||
|> element("#client-authorization-deny-button")
|
||||
|> render_click()
|
||||
|
||||
assert has_element?(view, "#client-authorization-denied")
|
||||
|
||||
denied =
|
||||
build_conn()
|
||||
|> post(~p"/api/client-auth/exchange", %{device_code: start_response["device_code"]})
|
||||
|> json_response(403)
|
||||
|
||||
assert denied == %{"error" => "access_denied"}
|
||||
end
|
||||
|
||||
test "authorization page requires a signed-in web account", %{conn: conn} do
|
||||
start_response =
|
||||
conn
|
||||
|> post(~p"/api/client-auth/start", %{})
|
||||
|> json_response(201)
|
||||
|
||||
assert {:error, {:redirect, %{to: path}}} =
|
||||
live(build_conn(), ~p"/client/authorize/#{start_response["user_code"]}")
|
||||
|
||||
assert path == ~p"/accounts/log-in"
|
||||
end
|
||||
|
||||
test "a signed-in session can approve without recent reauth", %{conn: conn} do
|
||||
start_response =
|
||||
conn
|
||||
|> post(~p"/api/client-auth/start", %{})
|
||||
|> json_response(201)
|
||||
|
||||
return_to = ~p"/client/authorize/#{start_response["user_code"]}"
|
||||
stale_at = DateTime.add(DateTime.utc_now(:second), -9 * 60, :minute)
|
||||
|
||||
{:ok, view, _html} =
|
||||
build_conn()
|
||||
|> log_in_account(account_fixture(), token_authenticated_at: stale_at)
|
||||
|> live(return_to)
|
||||
|
||||
assert has_element?(view, "#client-authorization-approve-button")
|
||||
|
||||
view
|
||||
|> element("#client-authorization-approve-button")
|
||||
|> render_click()
|
||||
|
||||
assert has_element?(view, "#client-authorization-approved")
|
||||
end
|
||||
|
||||
test "invalid device codes fail without revealing authorization state", %{conn: conn} do
|
||||
response =
|
||||
conn
|
||||
|> post(~p"/api/client-auth/exchange", %{device_code: "not-a-device-code"})
|
||||
|> json_response(400)
|
||||
|
||||
assert response == %{"error" => "invalid_device_code"}
|
||||
end
|
||||
end
|
||||
142
test/tarakan_web/controllers/api/finding_controller_test.exs
Normal file
142
test/tarakan_web/controllers/api/finding_controller_test.exs
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
defmodule TarakanWeb.API.FindingControllerTest do
|
||||
use TarakanWeb.ConnCase
|
||||
|
||||
setup do
|
||||
submitter = github_account_fixture()
|
||||
repository = listed_github_repository_fixture(submitter)
|
||||
%{repository: repository, submitter: submitter}
|
||||
end
|
||||
|
||||
test "returns a serialized public finding", %{
|
||||
conn: conn,
|
||||
repository: repository,
|
||||
submitter: submitter
|
||||
} do
|
||||
scan =
|
||||
repository
|
||||
|> scan_fixture(submitter, %{"findings_json" => findings_json_with_disclosure_fixture()})
|
||||
|> publish_scan("public")
|
||||
|
||||
[finding] = scan.findings
|
||||
|
||||
conn = get(conn, ~p"/api/findings/#{finding.public_id}")
|
||||
|
||||
assert %{"finding" => data} = json_response(conn, 200)
|
||||
assert data["public_id"] == finding.public_id
|
||||
assert data["title"] == finding.title
|
||||
assert data["severity"] == "high"
|
||||
assert data["status"] == "verified"
|
||||
assert data["description"] =~ "String-built SQL"
|
||||
assert data["reproduction_steps"] =~ "Run the payload"
|
||||
assert data["affected_versions"] == ">= 1.0, < 2.0"
|
||||
assert data["cwe_id"] == "CWE-89"
|
||||
assert data["cve_id"] == "CVE-2026-12345"
|
||||
assert data["file_path"] == "lib/example/module_1.ex"
|
||||
assert data["line_start"] == 10
|
||||
assert data["line_end"] == 15
|
||||
assert data["first_seen_commit_sha"] == scan.commit_sha
|
||||
assert data["last_seen_commit_sha"] == scan.commit_sha
|
||||
assert data["verified_at"]
|
||||
assert is_map(data["counters"])
|
||||
assert data["repository"] == %{"host" => "github.com", "owner" => "openai", "name" => "codex"}
|
||||
assert data["record_url"] == TarakanWeb.Endpoint.url() <> "/findings/#{finding.public_id}"
|
||||
assert data["inserted_at"]
|
||||
end
|
||||
|
||||
test "404s for a restricted finding", %{
|
||||
conn: conn,
|
||||
repository: repository,
|
||||
submitter: submitter
|
||||
} do
|
||||
scan = scan_fixture(repository, submitter, %{"findings_json" => findings_json_fixture(1)})
|
||||
|
||||
{:ok, _restricted} =
|
||||
Tarakan.Scans.update_visibility(
|
||||
Tarakan.Accounts.Scope.for_account(moderator_account_fixture()),
|
||||
scan,
|
||||
"restricted",
|
||||
%{
|
||||
"moderation_reason" => "takedown_review",
|
||||
"moderation_notes" => "Restricted in this test to keep it off the public API."
|
||||
}
|
||||
)
|
||||
|
||||
[finding] = scan.findings
|
||||
|
||||
conn = get(conn, ~p"/api/findings/#{finding.public_id}")
|
||||
assert json_response(conn, 404)["error"] =~ "not found"
|
||||
end
|
||||
|
||||
test "404s for a summary-only finding", %{
|
||||
conn: conn,
|
||||
repository: repository,
|
||||
submitter: submitter
|
||||
} do
|
||||
scan =
|
||||
repository
|
||||
|> scan_fixture(submitter, %{"findings_json" => findings_json_fixture(1)})
|
||||
|> publish_scan("public_summary")
|
||||
|
||||
[finding] = scan.findings
|
||||
|
||||
conn = get(conn, ~p"/api/findings/#{finding.public_id}")
|
||||
assert json_response(conn, 404)["error"] =~ "not found"
|
||||
end
|
||||
|
||||
test "404s for an unknown public id", %{conn: conn} do
|
||||
conn = get(conn, ~p"/api/findings/#{Ecto.UUID.generate()}")
|
||||
assert json_response(conn, 404)["error"] =~ "not found"
|
||||
end
|
||||
|
||||
defp findings_json_with_disclosure_fixture do
|
||||
Jason.encode!(%{
|
||||
"tarakan_scan_format" => 1,
|
||||
"findings" => [
|
||||
%{
|
||||
"file" => "lib/example/module_1.ex",
|
||||
"line_start" => 10,
|
||||
"line_end" => 15,
|
||||
"severity" => "high",
|
||||
"title" => "Unsanitized input reaches interpolated query (1)",
|
||||
"description" => "String-built SQL executed with request parameters.",
|
||||
"reproduction" => "1. Check out the pinned commit\n2. Run the payload",
|
||||
"affected_versions" => ">= 1.0, < 2.0",
|
||||
"cwe" => "CWE-89",
|
||||
"cve" => "CVE-2026-12345"
|
||||
}
|
||||
]
|
||||
})
|
||||
end
|
||||
|
||||
defp publish_scan(scan, visibility) do
|
||||
scan = confirmation_fixture(scan, reviewer_account_fixture())
|
||||
scan = confirmation_fixture(scan, reviewer_account_fixture())
|
||||
|
||||
scope = Tarakan.Accounts.Scope.for_account(moderator_account_fixture())
|
||||
|
||||
if visibility == "public" do
|
||||
scan.repository
|
||||
|> Tarakan.Repositories.Repository.participation_changeset(%{
|
||||
participation_mode: "maintainer_verified"
|
||||
})
|
||||
|> Tarakan.Repo.update!()
|
||||
end
|
||||
|
||||
{:ok, scan} =
|
||||
Tarakan.Scans.accept_scan(scope, scan, %{
|
||||
"moderation_reason" => "evidence_reviewed",
|
||||
"moderation_notes" =>
|
||||
"Two independent reviewers supplied reproducible evidence for the pinned commit."
|
||||
})
|
||||
|
||||
{:ok, scan} =
|
||||
Tarakan.Scans.update_visibility(scope, scan, visibility, %{
|
||||
"moderation_reason" => "disclosure_reviewed",
|
||||
"moderation_notes" =>
|
||||
"Disclosure was separately reviewed for scope, secrets, and personal data.",
|
||||
"sensitive_data_reviewed" => "true"
|
||||
})
|
||||
|
||||
scan
|
||||
end
|
||||
end
|
||||
153
test/tarakan_web/controllers/api/finding_signals_api_test.exs
Normal file
153
test/tarakan_web/controllers/api/finding_signals_api_test.exs
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
defmodule TarakanWeb.API.FindingSignalsApiTest do
|
||||
@moduledoc """
|
||||
The three worker submission endpoints that had no HTTP surface until now:
|
||||
calibrated severity, code embeddings, and cluster detectors.
|
||||
"""
|
||||
use TarakanWeb.ConnCase, async: true
|
||||
|
||||
import Ecto.Query, only: [from: 2]
|
||||
|
||||
alias Tarakan.Accounts.ApiCredentials
|
||||
alias Tarakan.Repo
|
||||
alias Tarakan.Scans.CanonicalFinding
|
||||
|
||||
setup %{conn: conn} do
|
||||
submitter = github_account_fixture()
|
||||
repository = listed_github_repository_fixture(submitter)
|
||||
|
||||
findings =
|
||||
Jason.encode!(%{
|
||||
"tarakan_scan_format" => 1,
|
||||
"findings" => [
|
||||
%{
|
||||
"file" => "lib/vulnerable.ex",
|
||||
"severity" => "critical",
|
||||
"title" => "Command injection #{System.unique_integer([:positive])}",
|
||||
"description" => "User input reaches System.cmd without escaping."
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
scan_fixture(repository, submitter, %{"findings_json" => findings})
|
||||
finding = Repo.one!(from f in CanonicalFinding, where: f.repository_id == ^repository.id)
|
||||
|
||||
reviewer = reviewer_account_fixture()
|
||||
|
||||
{:ok, token, _cred} =
|
||||
ApiCredentials.create(reviewer, %{
|
||||
name: "signals",
|
||||
scopes: ["reviews:read", "reviews:verify"]
|
||||
})
|
||||
|
||||
%{
|
||||
conn: put_req_header(conn, "authorization", "Bearer #{token}"),
|
||||
anon: build_conn(),
|
||||
repository: repository,
|
||||
finding: finding
|
||||
}
|
||||
end
|
||||
|
||||
defp signal_path(repository, finding, suffix) do
|
||||
"/api/github.com/#{repository.owner}/#{repository.name}/findings/#{finding.public_id}/#{suffix}"
|
||||
end
|
||||
|
||||
describe "POST .../severity" do
|
||||
test "stores a rescore beside the submitter's claim", ctx do
|
||||
conn =
|
||||
post(ctx.conn, signal_path(ctx.repository, ctx.finding, "severity"), %{
|
||||
"severity" => "low",
|
||||
"rubric" => "rubric-v1"
|
||||
})
|
||||
|
||||
assert %{"calibrated_severity" => "low"} = json_response(conn, 200)
|
||||
assert Repo.get!(CanonicalFinding, ctx.finding.id).severity == "critical"
|
||||
end
|
||||
|
||||
test "rejects a severity outside the scale", ctx do
|
||||
conn =
|
||||
post(ctx.conn, signal_path(ctx.repository, ctx.finding, "severity"), %{
|
||||
"severity" => "catastrophic",
|
||||
"rubric" => "rubric-v1"
|
||||
})
|
||||
|
||||
assert %{"errors" => %{"calibrated_severity" => _}} = json_response(conn, 422)
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST .../embedding" do
|
||||
test "stores the vector and assigns a cluster", ctx do
|
||||
conn =
|
||||
post(ctx.conn, signal_path(ctx.repository, ctx.finding, "embedding"), %{
|
||||
"embedding" => [0.1, 0.9, 0.2],
|
||||
"embedding_model" => "test-embed-v1"
|
||||
})
|
||||
|
||||
assert %{"code_pattern_key" => key} = json_response(conn, 200)
|
||||
assert key =~ ~r/^code:/
|
||||
end
|
||||
|
||||
test "rejects an all-zero vector, which would match everything", ctx do
|
||||
conn =
|
||||
post(ctx.conn, signal_path(ctx.repository, ctx.finding, "embedding"), %{
|
||||
"embedding" => [0.0, 0.0, 0.0],
|
||||
"embedding_model" => "test-embed-v1"
|
||||
})
|
||||
|
||||
assert %{"errors" => %{"embedding" => _}} = json_response(conn, 422)
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST /api/patterns/:key/rule" do
|
||||
@rule """
|
||||
rules:
|
||||
- id: tarakan-cmd-injection
|
||||
message: user input reaches System.cmd
|
||||
severity: ERROR
|
||||
languages: [elixir]
|
||||
patterns:
|
||||
- pattern: System.cmd($C, $A)
|
||||
"""
|
||||
|
||||
test "accepts a validated detector and marks it servable", ctx do
|
||||
conn =
|
||||
post(ctx.conn, "/api/patterns/code:abc/rule", %{
|
||||
"rule_yaml" => @rule,
|
||||
"language" => "elixir",
|
||||
"checked_count" => 3,
|
||||
"matched_count" => 2,
|
||||
"matched_finding_ids" => [Ecto.UUID.generate(), Ecto.UUID.generate()]
|
||||
})
|
||||
|
||||
assert %{"servable" => true, "matched_count" => 2} = json_response(conn, 201)
|
||||
end
|
||||
|
||||
test "a detector that matched nothing is stored but not servable", ctx do
|
||||
conn =
|
||||
post(ctx.conn, "/api/patterns/code:abc/rule", %{
|
||||
"rule_yaml" => @rule,
|
||||
"checked_count" => 3,
|
||||
"matched_count" => 0,
|
||||
"matched_finding_ids" => []
|
||||
})
|
||||
|
||||
assert %{"servable" => false, "validated_at" => nil} = json_response(conn, 201)
|
||||
end
|
||||
|
||||
test "rejects a match count with no receipts", ctx do
|
||||
conn =
|
||||
post(ctx.conn, "/api/patterns/code:abc/rule", %{
|
||||
"rule_yaml" => @rule,
|
||||
"checked_count" => 3,
|
||||
"matched_count" => 2,
|
||||
"matched_finding_ids" => []
|
||||
})
|
||||
|
||||
assert %{"errors" => %{"matched_finding_ids" => _}} = json_response(conn, 422)
|
||||
end
|
||||
|
||||
test "anonymous submissions are refused", ctx do
|
||||
conn = post(ctx.anon, "/api/patterns/code:abc/rule", %{"rule_yaml" => @rule})
|
||||
assert json_response(conn, 401)
|
||||
end
|
||||
end
|
||||
end
|
||||
137
test/tarakan_web/controllers/api/infestation_controller_test.exs
Normal file
137
test/tarakan_web/controllers/api/infestation_controller_test.exs
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
defmodule TarakanWeb.API.InfestationControllerTest do
|
||||
use TarakanWeb.ConnCase, async: true
|
||||
|
||||
import Ecto.Query, only: [from: 2]
|
||||
|
||||
alias Tarakan.FindingMemory
|
||||
|
||||
setup 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 = "API infestation sample #{System.unique_integer([:positive])}"
|
||||
|
||||
findings = fn path ->
|
||||
Jason.encode!(%{
|
||||
"tarakan_scan_format" => 1,
|
||||
"findings" => [
|
||||
%{
|
||||
"file" => path,
|
||||
"line_start" => 1,
|
||||
"line_end" => 2,
|
||||
"severity" => "high",
|
||||
"title" => title,
|
||||
"description" => "Shared issue class for the patterns API test."
|
||||
}
|
||||
]
|
||||
})
|
||||
end
|
||||
|
||||
scan_fixture(repo_a, submitter, %{"findings_json" => findings.("lib/a.ex")})
|
||||
scan_fixture(repo_b, other, %{"findings_json" => findings.("src/b.rs")})
|
||||
|
||||
%{pattern_key: FindingMemory.pattern_key(title), title: title}
|
||||
end
|
||||
|
||||
test "lists patterns", %{conn: conn, pattern_key: pattern_key} do
|
||||
conn = get(conn, ~p"/api/infestations")
|
||||
|
||||
assert %{"patterns" => patterns} = json_response(conn, 200)
|
||||
assert pattern = Enum.find(patterns, &(&1["pattern_key"] == pattern_key))
|
||||
assert pattern["repo_count"] >= 2
|
||||
assert pattern["instance_count"] >= 2
|
||||
assert pattern["record_url"] =~ "/infestations/#{pattern_key}"
|
||||
end
|
||||
|
||||
test "whitelists query params", %{conn: conn, pattern_key: pattern_key} do
|
||||
conn = get(conn, ~p"/api/infestations?days=13&min_repos=0&limit=5000")
|
||||
assert %{"patterns" => _} = json_response(conn, 200)
|
||||
|
||||
conn = get(conn, ~p"/api/infestations?days=7&min_repos=2&limit=10")
|
||||
assert %{"patterns" => patterns} = json_response(conn, 200)
|
||||
assert Enum.any?(patterns, &(&1["pattern_key"] == pattern_key))
|
||||
end
|
||||
|
||||
test "shows a pattern with its first page of instances", %{
|
||||
conn: conn,
|
||||
pattern_key: pattern_key,
|
||||
title: title
|
||||
} do
|
||||
conn = get(conn, ~p"/api/infestations/#{pattern_key}")
|
||||
|
||||
assert %{"pattern" => pattern, "instances" => instances} = json_response(conn, 200)
|
||||
assert pattern["pattern_key"] == pattern_key
|
||||
assert pattern["title"] =~ title
|
||||
|
||||
assert length(instances) >= 2
|
||||
assert Enum.any?(instances, &(&1["repository"]["owner"] == "openai"))
|
||||
assert Enum.any?(instances, &(&1["repository"]["owner"] == "acme"))
|
||||
end
|
||||
|
||||
test "404s for an unknown pattern", %{conn: conn} do
|
||||
conn = get(conn, ~p"/api/infestations/no-such-pattern")
|
||||
assert json_response(conn, 404)["error"] =~ "not found"
|
||||
end
|
||||
|
||||
describe "vaccine packs" do
|
||||
alias Tarakan.Accounts.Scope
|
||||
alias Tarakan.FindingSignals
|
||||
alias Tarakan.Repo
|
||||
alias Tarakan.Scans.CanonicalFinding
|
||||
|
||||
# The endpoint must refuse to invent a detector. It used to always return
|
||||
# one, and the one it returned matched nothing.
|
||||
test "404s while nobody has written a validated detector", %{
|
||||
conn: conn,
|
||||
pattern_key: pattern_key
|
||||
} do
|
||||
conn = get(conn, ~p"/api/infestations/#{pattern_key}/vaccine.yaml")
|
||||
|
||||
assert %{"error" => error} = json_response(conn, 404)
|
||||
assert error =~ "no validated detector"
|
||||
end
|
||||
|
||||
test "serves the validated rule once one exists", %{conn: conn, pattern_key: pattern_key} do
|
||||
# Put this infestation's findings into one code cluster.
|
||||
cluster = "code:vaccinetest"
|
||||
|
||||
Repo.all(from f in CanonicalFinding, where: f.pattern_key == ^pattern_key)
|
||||
|> Enum.each(fn finding ->
|
||||
finding
|
||||
|> CanonicalFinding.embedding_changeset(%{code_pattern_key: cluster})
|
||||
|> Repo.update!()
|
||||
end)
|
||||
|
||||
scope = Scope.for_account(reviewer_account_fixture())
|
||||
|
||||
{:ok, _rule} =
|
||||
FindingSignals.put_rule(scope, cluster, %{
|
||||
engine: "semgrep",
|
||||
language: "elixir",
|
||||
rule_yaml: """
|
||||
rules:
|
||||
- id: tarakan-real-detector
|
||||
message: Shared issue class
|
||||
severity: ERROR
|
||||
languages: [elixir]
|
||||
patterns:
|
||||
- pattern: System.cmd($C, $A)
|
||||
""",
|
||||
checked_count: 2,
|
||||
matched_count: 2,
|
||||
matched_finding_ids: [Ecto.UUID.generate(), Ecto.UUID.generate()]
|
||||
})
|
||||
|
||||
conn = get(conn, ~p"/api/infestations/#{pattern_key}/vaccine.yaml")
|
||||
|
||||
assert body = response(conn, 200)
|
||||
assert get_resp_header(conn, "content-type") |> hd() =~ "application/x-yaml"
|
||||
assert body =~ "tarakan-real-detector"
|
||||
assert body =~ "matched 2 of 2 known instances"
|
||||
refute body =~ "tarakan-vaccine-placeholder"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
defmodule TarakanWeb.API.LeaderboardControllerTest do
|
||||
use TarakanWeb.ConnCase
|
||||
|
||||
setup do
|
||||
submitter = github_account_fixture()
|
||||
repository = listed_github_repository_fixture(submitter)
|
||||
|
||||
repository
|
||||
|> scan_fixture(submitter, %{"findings_json" => findings_json_fixture(1)})
|
||||
|> publish_scan("public")
|
||||
|
||||
%{submitter: submitter}
|
||||
end
|
||||
|
||||
test "returns ranked rows", %{conn: conn, submitter: submitter} do
|
||||
conn = get(conn, ~p"/api/leaderboard")
|
||||
|
||||
assert %{"leaderboard" => rows} = json_response(conn, 200)
|
||||
assert row = Enum.find(rows, &(&1["handle"] == submitter.handle))
|
||||
assert row["rank"] == 1
|
||||
assert row["reputation"]["total"] > 0
|
||||
assert is_map(row["stats"])
|
||||
assert row["slashed_stakes"] == 0
|
||||
assert row["profile_url"] == TarakanWeb.Endpoint.url() <> "/" <> submitter.handle
|
||||
end
|
||||
|
||||
test "whitelists sort, severity, and window params", %{conn: conn} do
|
||||
conn = get(conn, ~p"/api/leaderboard?sort=reviews&severity=high&window=7")
|
||||
assert %{"leaderboard" => _} = json_response(conn, 200)
|
||||
|
||||
conn = get(conn, ~p"/api/leaderboard?sort=bogus&severity=bogus&window=bogus")
|
||||
assert %{"leaderboard" => _} = json_response(conn, 200)
|
||||
end
|
||||
|
||||
defp publish_scan(scan, visibility) do
|
||||
scan = confirmation_fixture(scan, reviewer_account_fixture())
|
||||
scan = confirmation_fixture(scan, reviewer_account_fixture())
|
||||
|
||||
scope = Tarakan.Accounts.Scope.for_account(moderator_account_fixture())
|
||||
|
||||
if visibility == "public" do
|
||||
scan.repository
|
||||
|> Tarakan.Repositories.Repository.participation_changeset(%{
|
||||
participation_mode: "maintainer_verified"
|
||||
})
|
||||
|> Tarakan.Repo.update!()
|
||||
end
|
||||
|
||||
{:ok, scan} =
|
||||
Tarakan.Scans.accept_scan(scope, scan, %{
|
||||
"moderation_reason" => "evidence_reviewed",
|
||||
"moderation_notes" =>
|
||||
"Two independent reviewers supplied reproducible evidence for the pinned commit."
|
||||
})
|
||||
|
||||
{:ok, scan} =
|
||||
Tarakan.Scans.update_visibility(scope, scan, visibility, %{
|
||||
"moderation_reason" => "disclosure_reviewed",
|
||||
"moderation_notes" =>
|
||||
"Disclosure was separately reviewed for scope, secrets, and personal data.",
|
||||
"sensitive_data_reviewed" => "true"
|
||||
})
|
||||
|
||||
scan
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
defmodule TarakanWeb.API.RepositoryControllerTest do
|
||||
use TarakanWeb.ConnCase, async: true
|
||||
|
||||
alias Tarakan.Accounts.ApiCredentials
|
||||
alias Tarakan.Repositories
|
||||
|
||||
setup %{conn: conn} do
|
||||
account = github_account_fixture()
|
||||
token = api_token(account)
|
||||
%{conn: conn, account: account, token: token}
|
||||
end
|
||||
|
||||
defp authed(conn, token), do: put_req_header(conn, "authorization", "Bearer #{token}")
|
||||
|
||||
defp api_token(account) do
|
||||
{:ok, token, _credential} =
|
||||
ApiCredentials.create(account, %{
|
||||
name: "registry importer",
|
||||
scopes: ["findings:submit"]
|
||||
})
|
||||
|
||||
token
|
||||
end
|
||||
|
||||
test "rejects registration without a token", %{conn: conn} do
|
||||
conn = post(conn, ~p"/api/repositories", %{"url" => "openai/codex"})
|
||||
assert json_response(conn, 401)["error"] =~ "API token"
|
||||
end
|
||||
|
||||
test "registers a public repository by owner/name", %{conn: conn, token: token} do
|
||||
conn = conn |> authed(token) |> post(~p"/api/repositories", %{"url" => "openai/codex"})
|
||||
|
||||
assert %{"repository" => repo} = json_response(conn, 200)
|
||||
assert repo["owner"] == "openai"
|
||||
assert repo["name"] == "codex"
|
||||
assert repo["host"] == "github.com"
|
||||
assert repo["record_url"] =~ "/github.com/openai/codex"
|
||||
end
|
||||
|
||||
test "is idempotent on re-register", %{conn: conn, token: token, account: account} do
|
||||
{:ok, first} = Repositories.register_github_repository("openai/codex", account)
|
||||
|
||||
conn = conn |> authed(token) |> post(~p"/api/repositories", %{"url" => "openai/codex"})
|
||||
assert %{"repository" => repo} = json_response(conn, 200)
|
||||
assert repo["owner"] == first.owner
|
||||
assert repo["name"] == first.name
|
||||
end
|
||||
|
||||
test "requires a url", %{conn: conn, token: token} do
|
||||
conn = conn |> authed(token) |> post(~p"/api/repositories", %{})
|
||||
assert json_response(conn, 422)["errors"]["url"] == ["is required"]
|
||||
end
|
||||
|
||||
test "lists reviewable repositories", %{conn: conn, token: token, account: account} do
|
||||
{:ok, _repo} = Repositories.register_github_repository("openai/codex", account)
|
||||
|
||||
conn = conn |> authed(token) |> get(~p"/api/repositories?status=unscanned")
|
||||
assert %{"repositories" => repos} = json_response(conn, 200)
|
||||
assert Enum.any?(repos, &(&1["owner"] == "openai" and &1["name"] == "codex"))
|
||||
end
|
||||
|
||||
test "admin credentials are not registration rate limited", %{conn: conn} do
|
||||
admin =
|
||||
github_account_fixture()
|
||||
|> then(fn account ->
|
||||
account
|
||||
|> Tarakan.Accounts.Account.authorization_changeset(%{
|
||||
state: "active",
|
||||
platform_role: "admin",
|
||||
trust_tier: "reviewer"
|
||||
})
|
||||
|> Tarakan.Repo.update!()
|
||||
end)
|
||||
|
||||
token = api_token(admin)
|
||||
|
||||
# Well above the normal mutation (20) and repository_fetch (10) windows.
|
||||
for _ <- 1..35 do
|
||||
conn =
|
||||
conn
|
||||
|> recycle()
|
||||
|> authed(token)
|
||||
|> post(~p"/api/repositories", %{"url" => "openai/codex"})
|
||||
|
||||
assert json_response(conn, 200)["repository"]["name"] == "codex"
|
||||
end
|
||||
end
|
||||
end
|
||||
548
test/tarakan_web/controllers/api/scan_controller_test.exs
Normal file
548
test/tarakan_web/controllers/api/scan_controller_test.exs
Normal file
|
|
@ -0,0 +1,548 @@
|
|||
defmodule TarakanWeb.API.ScanControllerTest do
|
||||
use TarakanWeb.ConnCase, async: true
|
||||
|
||||
alias Tarakan.Accounts.ApiCredentials
|
||||
alias Tarakan.Accounts.Scope
|
||||
alias Tarakan.Repositories
|
||||
alias Tarakan.Repositories.Repository
|
||||
alias Tarakan.Repo
|
||||
alias Tarakan.Scans
|
||||
|
||||
setup %{conn: conn} do
|
||||
account = github_account_fixture()
|
||||
repository = github_repository_fixture(account)
|
||||
token = api_token(account)
|
||||
|
||||
%{conn: conn, account: account, repository: repository, token: token}
|
||||
end
|
||||
|
||||
defp authed(conn, token), do: put_req_header(conn, "authorization", "Bearer #{token}")
|
||||
|
||||
defp api_token(account) do
|
||||
{:ok, token, _credential} =
|
||||
ApiCredentials.create(account, %{
|
||||
name: "Scan submitter",
|
||||
scopes: ["findings:submit"]
|
||||
})
|
||||
|
||||
token
|
||||
end
|
||||
|
||||
defp scan_body(overrides \\ %{}) do
|
||||
Map.merge(
|
||||
%{
|
||||
"commit_sha" => random_commit_sha(),
|
||||
"model" => "claude-sonnet-5",
|
||||
"prompt_version" => "tarakan-baseline/v1",
|
||||
"run_id" => "api-run-#{System.unique_integer([:positive, :monotonic])}",
|
||||
"document" => %{"tarakan_scan_format" => 1, "findings" => []}
|
||||
},
|
||||
overrides
|
||||
)
|
||||
end
|
||||
|
||||
test "rejects requests without a token", %{conn: conn} do
|
||||
conn = post(conn, ~p"/api/github.com/openai/codex/reports", scan_body())
|
||||
|
||||
assert json_response(conn, 401)["error"] =~ "API token"
|
||||
end
|
||||
|
||||
test "rejects requests with an invalid or revoked token", %{conn: conn, account: account} do
|
||||
conn1 =
|
||||
conn |> authed("garbage") |> post(~p"/api/github.com/openai/codex/reports", scan_body())
|
||||
|
||||
assert json_response(conn1, 401)
|
||||
|
||||
old_token = api_token(account)
|
||||
[credential | _rest] = ApiCredentials.list(account)
|
||||
{:ok, _credential} = ApiCredentials.revoke(account, credential.id)
|
||||
|
||||
conn2 =
|
||||
conn |> authed(old_token) |> post(~p"/api/github.com/openai/codex/reports", scan_body())
|
||||
|
||||
assert json_response(conn2, 401)
|
||||
end
|
||||
|
||||
test "404s for a repository not in the registry", %{conn: conn, token: token} do
|
||||
conn = conn |> authed(token) |> post(~p"/api/github.com/unknown/repo/reports", scan_body())
|
||||
|
||||
assert json_response(conn, 404)["error"] =~ "not registered"
|
||||
end
|
||||
|
||||
test "does not disclose a quarantined repository to an unrelated credential", %{
|
||||
conn: conn,
|
||||
repository: repository
|
||||
} do
|
||||
_contained =
|
||||
repository
|
||||
|> Repository.listing_changeset(%{listing_status: "quarantined"})
|
||||
|> Repo.update!()
|
||||
|
||||
outsider_token = api_token(account_fixture())
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> authed(outsider_token)
|
||||
|> post(~p"/api/github.com/openai/codex/reports", scan_body())
|
||||
|
||||
assert json_response(conn, 404)["error"] =~ "not registered"
|
||||
end
|
||||
|
||||
test "records a clean scan", %{conn: conn, token: token} do
|
||||
conn = conn |> authed(token) |> post(~p"/api/github.com/openai/codex/reports", scan_body())
|
||||
|
||||
response = json_response(conn, 201)
|
||||
assert response["findings_count"] == 0
|
||||
assert response["verified"] == false
|
||||
assert response["review_status"] == "quarantined"
|
||||
assert response["visibility"] == "public"
|
||||
assert response["provenance_attestation"] == "self_reported"
|
||||
assert response["repository"] == "openai/codex"
|
||||
assert response["record_url"] =~ "/github.com/openai/codex"
|
||||
|
||||
repository = Repositories.get_github_repository("openai", "codex")
|
||||
assert repository.status == "reviewed"
|
||||
assert repository.scan_count == 1
|
||||
end
|
||||
|
||||
test "records a scan with findings", %{conn: conn, token: token} do
|
||||
document = Jason.decode!(findings_json_fixture(2))
|
||||
body = scan_body(%{"document" => document, "notes" => "run #7 of the nightly sweep"})
|
||||
|
||||
conn = conn |> authed(token) |> post(~p"/api/github.com/openai/codex/reports", body)
|
||||
|
||||
response = json_response(conn, 201)
|
||||
assert response["findings_count"] == 2
|
||||
assert response["kind"] == "report"
|
||||
assert response["disclosed"] == true
|
||||
assert is_list(response["findings"])
|
||||
assert length(response["findings"]) == 2
|
||||
assert hd(response["findings"])["url"] =~ "/findings/"
|
||||
|
||||
repository = Repositories.get_github_repository("openai", "codex")
|
||||
assert repository.status == "findings"
|
||||
assert repository.open_findings_count == 2
|
||||
end
|
||||
|
||||
test "mass report path works without a job claim", %{conn: conn, token: token} do
|
||||
document = Jason.decode!(findings_json_fixture(1))
|
||||
body = scan_body(%{"document" => document, "provenance" => "agent"})
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> authed(token)
|
||||
|> post(~p"/api/github.com/openai/codex/reports", body)
|
||||
|
||||
response = json_response(conn, 201)
|
||||
assert response["kind"] == "report"
|
||||
assert response["visibility"] == "public"
|
||||
assert response["findings_count"] == 1
|
||||
assert hd(response["findings"])["public_id"]
|
||||
end
|
||||
|
||||
test "cannot self-accept or self-publish through submission attributes", %{
|
||||
conn: conn,
|
||||
token: token
|
||||
} do
|
||||
body =
|
||||
scan_body(%{
|
||||
"review_status" => "accepted",
|
||||
"visibility" => "public",
|
||||
"verified_at" => DateTime.utc_now()
|
||||
})
|
||||
|
||||
conn = conn |> authed(token) |> post(~p"/api/github.com/openai/codex/reports", body)
|
||||
|
||||
response = json_response(conn, 201)
|
||||
assert response["review_status"] == "quarantined"
|
||||
assert response["visibility"] == "public"
|
||||
assert response["verified"] == false
|
||||
end
|
||||
|
||||
test "records a human-authored review without model metadata", %{conn: conn, token: token} do
|
||||
body = %{
|
||||
"commit_sha" => random_commit_sha(),
|
||||
"provenance" => "human",
|
||||
"review_kind" => "business_logic",
|
||||
"notes" => "Manually traced organization ownership transfer.",
|
||||
"document" => Jason.decode!(findings_json_fixture(1))
|
||||
}
|
||||
|
||||
conn = conn |> authed(token) |> post(~p"/api/github.com/openai/codex/reports", body)
|
||||
|
||||
response = json_response(conn, 201)
|
||||
assert response["provenance"] == "human"
|
||||
assert response["review_kind"] == "business_logic"
|
||||
assert response["model"] == nil
|
||||
assert response["prompt_version"] == nil
|
||||
assert response["findings_count"] == 1
|
||||
end
|
||||
|
||||
test "requires the scan document", %{conn: conn, token: token} do
|
||||
body = Map.delete(scan_body(), "document")
|
||||
conn1 = conn |> authed(token) |> post(~p"/api/github.com/openai/codex/reports", body)
|
||||
assert %{"document" => [message]} = json_response(conn1, 422)["errors"]
|
||||
assert message =~ "required"
|
||||
|
||||
conn2 =
|
||||
conn
|
||||
|> authed(token)
|
||||
|> post(~p"/api/github.com/openai/codex/reports", scan_body(%{"document" => "[]"}))
|
||||
|
||||
assert json_response(conn2, 422)["errors"]["document"]
|
||||
end
|
||||
|
||||
test "rejects an invalid document with the parser's message", %{conn: conn, token: token} do
|
||||
body = scan_body(%{"document" => %{"tarakan_scan_format" => 2, "findings" => []}})
|
||||
conn = conn |> authed(token) |> post(~p"/api/github.com/openai/codex/reports", body)
|
||||
|
||||
assert %{"findings_json" => [message]} = json_response(conn, 422)["errors"]
|
||||
assert message == "tarakan_scan_format must be 1"
|
||||
end
|
||||
|
||||
test "rejects envelope validation errors", %{conn: conn, token: token} do
|
||||
body = scan_body(%{"commit_sha" => "abc123", "model" => nil})
|
||||
conn = conn |> authed(token) |> post(~p"/api/github.com/openai/codex/reports", body)
|
||||
|
||||
errors = json_response(conn, 422)["errors"]
|
||||
assert errors["commit_sha"] == ["must be a full 40-character commit SHA"]
|
||||
assert errors["model"] == ["can't be blank"]
|
||||
end
|
||||
|
||||
test "rejects a commit GitHub does not know", %{conn: conn, token: token} do
|
||||
body = scan_body(%{"commit_sha" => "dead" <> String.duplicate("0", 36)})
|
||||
conn = conn |> authed(token) |> post(~p"/api/github.com/openai/codex/reports", body)
|
||||
|
||||
assert json_response(conn, 422)["errors"]["commit_sha"] == [
|
||||
"commit not found in this repository on GitHub"
|
||||
]
|
||||
end
|
||||
|
||||
test "blocks retrying the same run id", %{conn: conn, token: token} do
|
||||
body = scan_body()
|
||||
|
||||
conn1 = conn |> authed(token) |> post(~p"/api/github.com/openai/codex/reports", body)
|
||||
assert json_response(conn1, 201)
|
||||
|
||||
conn2 = conn |> authed(token) |> post(~p"/api/github.com/openai/codex/reports", body)
|
||||
|
||||
assert json_response(conn2, 422)["errors"]["run_id"] == [
|
||||
"this agent run was already submitted"
|
||||
]
|
||||
end
|
||||
|
||||
describe "GET /reports and verdict" do
|
||||
setup %{repository: repository, account: submitter} do
|
||||
scan =
|
||||
scan_fixture(repository, submitter, %{
|
||||
"findings_json" => findings_json_fixture(1)
|
||||
})
|
||||
|
||||
reviewer = reviewer_tier_account_fixture()
|
||||
|
||||
%{scan: scan, reviewer: reviewer, reviewer_token: reviews_token(reviewer)}
|
||||
end
|
||||
|
||||
test "reviewer-tier reviews:read token sees restricted findings", %{
|
||||
conn: conn,
|
||||
reviewer_token: token,
|
||||
scan: scan
|
||||
} do
|
||||
scan = restrict_scan(scan)
|
||||
|
||||
body =
|
||||
conn
|
||||
|> authed(token)
|
||||
|> get(~p"/api/github.com/openai/codex/reports")
|
||||
|> json_response(200)
|
||||
|
||||
entry = Enum.find(body["reports"], &(&1["id"] == scan.id))
|
||||
assert entry["details_visible"]
|
||||
assert length(entry["findings"]) == 1
|
||||
assert hd(entry["findings"])["severity"]
|
||||
end
|
||||
|
||||
test "returns compact canonical memory for reconciliation", %{
|
||||
conn: conn,
|
||||
token: token,
|
||||
scan: scan
|
||||
} do
|
||||
body =
|
||||
conn
|
||||
|> authed(token)
|
||||
|> get(~p"/api/github.com/openai/codex/memory?commit_sha=#{scan.commit_sha}")
|
||||
|> json_response(200)
|
||||
|
||||
assert body["target_commit_sha"] == scan.commit_sha
|
||||
assert [finding] = body["findings"]
|
||||
assert finding["same_commit"]
|
||||
assert finding["status"] == "open"
|
||||
assert finding["detections_count"] == 1
|
||||
assert finding["public_id"]
|
||||
|
||||
# Nothing disputed yet, but the corpus ships on every response so clients
|
||||
# can rely on the key existing.
|
||||
assert body["suppressions"]["repository"] == []
|
||||
assert body["suppressions"]["patterns"] == []
|
||||
assert body["suppressions"]["note"] =~ "non-bugs"
|
||||
end
|
||||
|
||||
test "memory carries disputed findings as suppressions", %{
|
||||
conn: conn,
|
||||
token: token,
|
||||
scan: scan
|
||||
} do
|
||||
[occurrence] = scan.findings
|
||||
|
||||
Tarakan.Repo.get!(Tarakan.Scans.CanonicalFinding, occurrence.canonical_finding_id)
|
||||
|> Ecto.Changeset.change(status: "disputed", disputes_count: 2)
|
||||
|> Tarakan.Repo.update!()
|
||||
|
||||
body =
|
||||
conn
|
||||
|> authed(token)
|
||||
|> get(~p"/api/github.com/openai/codex/memory?commit_sha=#{scan.commit_sha}")
|
||||
|> json_response(200)
|
||||
|
||||
assert [suppression] = body["suppressions"]["repository"]
|
||||
assert suppression["scope"] == "repository"
|
||||
assert suppression["fingerprint"]
|
||||
assert suppression["file_path"]
|
||||
end
|
||||
|
||||
test "records a check on one canonical finding", %{
|
||||
conn: conn,
|
||||
scan: scan
|
||||
} do
|
||||
token = reviews_token(moderator_account_fixture())
|
||||
|
||||
memory =
|
||||
conn
|
||||
|> authed(token)
|
||||
|> get(~p"/api/github.com/openai/codex/memory?commit_sha=#{scan.commit_sha}")
|
||||
|> json_response(200)
|
||||
|
||||
[finding] = memory["findings"]
|
||||
|
||||
body =
|
||||
conn
|
||||
|> recycle()
|
||||
|> authed(token)
|
||||
|> post(~p"/api/github.com/openai/codex/findings/#{finding["public_id"]}/check", %{
|
||||
"commit_sha" => scan.commit_sha,
|
||||
"verdict" => "confirmed",
|
||||
"provenance" => "human",
|
||||
"notes" => "Independently reproduced this individual finding at the pinned commit."
|
||||
})
|
||||
|> json_response(201)
|
||||
|
||||
assert body["status"] == "open"
|
||||
assert body["confirmations_count"] == 1
|
||||
end
|
||||
|
||||
test "a plain findings:submit token cannot see restricted findings", %{
|
||||
conn: conn,
|
||||
token: token,
|
||||
scan: scan
|
||||
} do
|
||||
scan = restrict_scan(scan)
|
||||
|
||||
body =
|
||||
conn
|
||||
|> authed(token)
|
||||
|> get(~p"/api/github.com/openai/codex/reports")
|
||||
|> json_response(200)
|
||||
|
||||
# After a moderator takedown, the submitter's own restricted scan is not
|
||||
# exposed to a non-reviewer token.
|
||||
refute Enum.any?(body["reports"], &(&1["id"] == scan.id and &1["findings"] != []))
|
||||
end
|
||||
|
||||
test "records a verdict with a proof-of-concept", %{
|
||||
conn: conn,
|
||||
reviewer_token: token,
|
||||
scan: scan
|
||||
} do
|
||||
body =
|
||||
conn
|
||||
|> authed(token)
|
||||
|> post(~p"/api/github.com/openai/codex/reports/#{scan.id}/check", %{
|
||||
"verdict" => "confirmed",
|
||||
"provenance" => "agent",
|
||||
"notes" => "Reproduced the reported issue against the pinned commit source.",
|
||||
"evidence" => "import test; test('repro', t => t.throws(() => vulnerable()))"
|
||||
})
|
||||
|> json_response(201)
|
||||
|
||||
assert length(body["confirmations"]) == 1
|
||||
assert hd(body["confirmations"])["verdict"] == "confirmed"
|
||||
end
|
||||
|
||||
test "the submitter cannot verify their own review", %{
|
||||
conn: conn,
|
||||
account: submitter,
|
||||
scan: scan
|
||||
} do
|
||||
token = reviews_token(reviewer_tier(submitter))
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> authed(token)
|
||||
|> post(~p"/api/github.com/openai/codex/reports/#{scan.id}/check", %{
|
||||
"verdict" => "confirmed",
|
||||
"notes" => "Trying to verify my own submission, which must be refused."
|
||||
})
|
||||
|
||||
assert json_response(conn, 409)["error"] =~ "submitter"
|
||||
end
|
||||
|
||||
test "a read-only reviewer credential cannot record a verdict", %{conn: conn, scan: scan} do
|
||||
# Reviewer-tier account (can see the scan) but the credential lacks
|
||||
# reviews:verify, so recording a verdict is forbidden.
|
||||
{:ok, read_only, _cred} =
|
||||
ApiCredentials.create(reviewer_tier_account_fixture(), %{
|
||||
name: "Read-only reviewer",
|
||||
scopes: ["reviews:read"]
|
||||
})
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> authed(read_only)
|
||||
|> post(~p"/api/github.com/openai/codex/reports/#{scan.id}/check", %{
|
||||
"verdict" => "confirmed",
|
||||
"notes" => "Has read access but no verify scope, so this must be denied."
|
||||
})
|
||||
|
||||
assert json_response(conn, 403)["error"] =~ "not authorized"
|
||||
end
|
||||
|
||||
test "a token that cannot see the review gets 404, not a leak", %{
|
||||
conn: conn,
|
||||
token: token,
|
||||
scan: scan
|
||||
} do
|
||||
scan = restrict_scan(scan)
|
||||
|
||||
# A plain findings:submit token lacks read scope, so the restricted scan
|
||||
# is invisible - the endpoint must 404 rather than reveal it exists.
|
||||
conn =
|
||||
conn
|
||||
|> authed(token)
|
||||
|> post(~p"/api/github.com/openai/codex/reports/#{scan.id}/check", %{
|
||||
"verdict" => "confirmed",
|
||||
"notes" => "This token cannot see the scan, so it must not verify it."
|
||||
})
|
||||
|
||||
assert json_response(conn, 404)["error"] =~ "not found"
|
||||
end
|
||||
|
||||
test "an unrelated credential cannot distinguish a quarantined repository", %{
|
||||
conn: conn,
|
||||
repository: repository,
|
||||
scan: scan
|
||||
} do
|
||||
_contained =
|
||||
repository
|
||||
|> Repository.listing_changeset(%{listing_status: "quarantined"})
|
||||
|> Repo.update!()
|
||||
|
||||
outsider_token = reviews_token(account_fixture())
|
||||
|
||||
conn1 =
|
||||
conn
|
||||
|> authed(outsider_token)
|
||||
|> get(~p"/api/github.com/openai/codex/reports")
|
||||
|
||||
assert json_response(conn1, 404)["error"] =~ "not registered"
|
||||
|
||||
conn2 =
|
||||
conn
|
||||
|> recycle()
|
||||
|> authed(outsider_token)
|
||||
|> post(~p"/api/github.com/openai/codex/reports/#{scan.id}/check", %{
|
||||
"verdict" => "confirmed",
|
||||
"notes" => "Quarantined repositories must look exactly like unregistered ones."
|
||||
})
|
||||
|
||||
assert json_response(conn2, 404)["error"] =~ "not registered"
|
||||
end
|
||||
|
||||
test "the submitter keeps access to their quarantined repository", %{
|
||||
conn: conn,
|
||||
repository: repository,
|
||||
account: submitter,
|
||||
scan: scan
|
||||
} do
|
||||
_contained =
|
||||
repository
|
||||
|> Repository.listing_changeset(%{listing_status: "quarantined"})
|
||||
|> Repo.update!()
|
||||
|
||||
{:ok, read_token, _credential} =
|
||||
ApiCredentials.create(submitter, %{name: "Reader", scopes: ["reviews:read"]})
|
||||
|
||||
body =
|
||||
conn
|
||||
|> authed(read_token)
|
||||
|> get(~p"/api/github.com/openai/codex/reports")
|
||||
|> json_response(200)
|
||||
|
||||
assert Enum.any?(body["reports"], &(&1["id"] == scan.id))
|
||||
end
|
||||
|
||||
test "an unrelated credential cannot list a pending repository's reviews", %{
|
||||
conn: conn,
|
||||
repository: repository
|
||||
} do
|
||||
_pending =
|
||||
repository
|
||||
|> Repository.listing_changeset(%{listing_status: "pending"})
|
||||
|> Repo.update!()
|
||||
|
||||
outsider_token = reviews_token(account_fixture())
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> authed(outsider_token)
|
||||
|> get(~p"/api/github.com/openai/codex/reports")
|
||||
|
||||
assert json_response(conn, 404)["error"] =~ "not registered"
|
||||
end
|
||||
end
|
||||
|
||||
defp restrict_scan(scan) do
|
||||
moderator_scope = Scope.for_account(moderator_account_fixture())
|
||||
|
||||
{:ok, scan} =
|
||||
Scans.update_visibility(moderator_scope, scan, "restricted", %{
|
||||
"moderation_reason" => "evidence_reviewed",
|
||||
"moderation_notes" => "Moderator takedown recorded for this visibility boundary test."
|
||||
})
|
||||
|
||||
scan
|
||||
end
|
||||
|
||||
defp reviewer_tier_account_fixture do
|
||||
account_fixture() |> reviewer_tier()
|
||||
end
|
||||
|
||||
defp reviewer_tier(account) do
|
||||
account
|
||||
|> Tarakan.Accounts.Account.authorization_changeset(%{
|
||||
state: "active",
|
||||
platform_role: "member",
|
||||
trust_tier: "reviewer"
|
||||
})
|
||||
|> Repo.update!()
|
||||
end
|
||||
|
||||
defp reviews_token(account) do
|
||||
{:ok, token, _credential} =
|
||||
ApiCredentials.create(account, %{
|
||||
name: "Verifier",
|
||||
scopes: ["reviews:read", "reviews:verify"]
|
||||
})
|
||||
|
||||
token
|
||||
end
|
||||
end
|
||||
601
test/tarakan_web/controllers/api/work_controller_test.exs
Normal file
601
test/tarakan_web/controllers/api/work_controller_test.exs
Normal file
|
|
@ -0,0 +1,601 @@
|
|||
defmodule TarakanWeb.API.WorkControllerTest do
|
||||
use TarakanWeb.ConnCase, async: true
|
||||
|
||||
alias Tarakan.Accounts
|
||||
alias Tarakan.Accounts.ApiCredentials
|
||||
alias Tarakan.Repositories.Repository
|
||||
alias Tarakan.Repo
|
||||
alias Tarakan.Work
|
||||
|
||||
setup %{conn: conn} do
|
||||
creator = github_account_fixture()
|
||||
repository = listed_github_repository_fixture(creator)
|
||||
worker = account_fixture()
|
||||
worker_token = client_token(worker)
|
||||
creator_token = client_token(creator)
|
||||
|
||||
%{
|
||||
conn: conn,
|
||||
creator: creator,
|
||||
creator_token: creator_token,
|
||||
repository: repository,
|
||||
worker: worker,
|
||||
worker_token: worker_token
|
||||
}
|
||||
end
|
||||
|
||||
defp authed(conn, token), do: put_req_header(conn, "authorization", "Bearer #{token}")
|
||||
|
||||
defp client_token(account) do
|
||||
{:ok, token, _credential} =
|
||||
ApiCredentials.create(account, %{
|
||||
name: "work controller test",
|
||||
scopes: ["tasks:read", "tasks:claim", "contributions:write"]
|
||||
})
|
||||
|
||||
token
|
||||
end
|
||||
|
||||
test "all work queue endpoints require an API token", %{conn: conn} do
|
||||
conn = get(conn, ~p"/api/github.com/openai/codex/jobs")
|
||||
|
||||
assert json_response(conn, 401)["error"] =~ "API token"
|
||||
end
|
||||
|
||||
test "lists the global open jobs queue", %{
|
||||
conn: conn,
|
||||
creator: creator,
|
||||
repository: repository,
|
||||
worker_token: token
|
||||
} do
|
||||
task = review_task_fixture(repository, creator)
|
||||
|
||||
conn = conn |> authed(token) |> get(~p"/api/jobs")
|
||||
|
||||
assert %{"jobs" => jobs} = json_response(conn, 200)
|
||||
assert Enum.any?(jobs, &(&1["id"] == task.id))
|
||||
match = Enum.find(jobs, &(&1["id"] == task.id))
|
||||
assert match["status"] == "open"
|
||||
assert match["repository"]["owner"] == repository.owner
|
||||
assert match["repository"]["name"] == repository.name
|
||||
assert match["repository"]["primary_language"] == repository.primary_language
|
||||
assert match["repository"]["stars_count"] == repository.stars_count
|
||||
end
|
||||
|
||||
test "global jobs queue filters by language and min stars", %{
|
||||
conn: conn,
|
||||
creator: creator,
|
||||
repository: repository,
|
||||
worker_token: token
|
||||
} do
|
||||
task = review_task_fixture(repository, creator)
|
||||
|
||||
# Fixture repo is Rust with 42000 stars (GitHub stub).
|
||||
conn =
|
||||
conn
|
||||
|> authed(token)
|
||||
|> get(~p"/api/jobs", %{"language" => "Rust", "min_stars" => "1000"})
|
||||
|
||||
assert %{"jobs" => jobs} = json_response(conn, 200)
|
||||
assert Enum.any?(jobs, &(&1["id"] == task.id))
|
||||
|
||||
conn =
|
||||
build_conn()
|
||||
|> authed(token)
|
||||
|> get(~p"/api/jobs", %{"language" => "Elixir"})
|
||||
|
||||
assert %{"jobs" => jobs} = json_response(conn, 200)
|
||||
refute Enum.any?(jobs, &(&1["id"] == task.id))
|
||||
|
||||
conn =
|
||||
build_conn()
|
||||
|> authed(token)
|
||||
|> get(~p"/api/jobs", %{"min_stars" => "999999"})
|
||||
|
||||
assert %{"jobs" => jobs} = json_response(conn, 200)
|
||||
refute Enum.any?(jobs, &(&1["id"] == task.id))
|
||||
end
|
||||
|
||||
# A worker declares what it can run so it never claims a kind it does not
|
||||
# implement and then holds the lease until it expires.
|
||||
test "global jobs queue filters by the kinds a worker can run", %{
|
||||
conn: conn,
|
||||
creator: creator,
|
||||
repository: repository,
|
||||
worker_token: token
|
||||
} do
|
||||
task = review_task_fixture(repository, creator)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> authed(token)
|
||||
|> get(~p"/api/jobs", %{"kinds" => "#{task.kind},refute_finding"})
|
||||
|
||||
assert %{"jobs" => jobs} = json_response(conn, 200)
|
||||
assert Enum.any?(jobs, &(&1["id"] == task.id))
|
||||
|
||||
conn =
|
||||
build_conn()
|
||||
|> authed(token)
|
||||
|> get(~p"/api/jobs", %{"kinds" => "reproduce_finding"})
|
||||
|
||||
assert %{"jobs" => jobs} = json_response(conn, 200)
|
||||
refute Enum.any?(jobs, &(&1["id"] == task.id))
|
||||
end
|
||||
|
||||
# An old client and a new server must still be able to talk, so a name the
|
||||
# server does not know is dropped rather than erroring or filtering to empty.
|
||||
test "unknown kind names are ignored rather than rejected", %{
|
||||
conn: conn,
|
||||
creator: creator,
|
||||
repository: repository,
|
||||
worker_token: token
|
||||
} do
|
||||
task = review_task_fixture(repository, creator)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> authed(token)
|
||||
|> get(~p"/api/jobs", %{"kinds" => "not_a_real_kind"})
|
||||
|
||||
assert %{"jobs" => jobs} = json_response(conn, 200)
|
||||
assert Enum.any?(jobs, &(&1["id"] == task.id))
|
||||
end
|
||||
|
||||
test "global jobs queue includes the caller's active claims", %{
|
||||
conn: conn,
|
||||
creator: creator,
|
||||
repository: repository,
|
||||
worker: worker,
|
||||
worker_token: token
|
||||
} do
|
||||
task =
|
||||
review_task_fixture(repository, creator, %{"kind" => "code_review", "capability" => "agent"})
|
||||
|
||||
{:ok, claimed} = Work.claim_task(task, worker)
|
||||
|
||||
assert claimed.status == "claimed"
|
||||
|
||||
conn = conn |> authed(token) |> get(~p"/api/jobs")
|
||||
assert %{"jobs" => jobs} = json_response(conn, 200)
|
||||
match = Enum.find(jobs, &(&1["id"] == claimed.id))
|
||||
assert match
|
||||
assert match["status"] == "claimed"
|
||||
assert match["lease"]["active"] == true
|
||||
end
|
||||
|
||||
test "lists a repository's tasks with client-ready relationships", %{
|
||||
conn: conn,
|
||||
creator: creator,
|
||||
repository: repository,
|
||||
worker_token: token
|
||||
} do
|
||||
task = review_task_fixture(repository, creator)
|
||||
|
||||
conn = conn |> authed(token) |> get(~p"/api/github.com/openai/codex/jobs")
|
||||
|
||||
assert %{"jobs" => [response]} = json_response(conn, 200)
|
||||
assert response["id"] == task.id
|
||||
assert response["kind"] == "threat_model"
|
||||
assert response["capability"] == "human"
|
||||
assert response["status"] == "open"
|
||||
assert response["visibility"] == "public"
|
||||
assert response["commit_sha"] == task.commit_sha
|
||||
assert response["commit_committed_at"] == "2026-07-01T12:00:00.000000Z"
|
||||
|
||||
assert response["repository"]["canonical_url"] == "https://github.com/openai/codex"
|
||||
assert response["repository"]["host"] == "github.com"
|
||||
assert response["repository"]["id"] == repository.id
|
||||
assert response["repository"]["name"] == "codex"
|
||||
assert response["repository"]["owner"] == "openai"
|
||||
assert response["repository"]["participation_mode"] == repository.participation_mode
|
||||
assert response["repository"]["record_url"] =~ "/github.com/openai/codex"
|
||||
|
||||
assert response["creator"]["id"] == creator.id
|
||||
assert response["creator"]["handle"] == creator.handle
|
||||
assert response["creator"] |> Map.keys() |> Enum.sort() == ["handle", "id"]
|
||||
assert response["claimant"] == nil
|
||||
assert response["lease"] == nil
|
||||
assert response["contribution"] == nil
|
||||
assert response["job_url"] =~ "/jobs/#{task.id}"
|
||||
end
|
||||
|
||||
test "returns an empty list and 404s an unregistered repository", %{
|
||||
conn: conn,
|
||||
worker_token: token
|
||||
} do
|
||||
conn1 = conn |> authed(token) |> get(~p"/api/github.com/openai/codex/jobs")
|
||||
assert json_response(conn1, 200) == %{"jobs" => []}
|
||||
|
||||
conn2 =
|
||||
conn
|
||||
|> recycle()
|
||||
|> authed(token)
|
||||
|> get(~p"/api/github.com/unknown/repository/jobs")
|
||||
|
||||
assert json_response(conn2, 404)["error"] =~ "not registered"
|
||||
end
|
||||
|
||||
test "does not disclose a quarantined repository to an unrelated credential", %{
|
||||
conn: conn,
|
||||
repository: repository,
|
||||
worker_token: token
|
||||
} do
|
||||
_contained =
|
||||
repository
|
||||
|> Repository.listing_changeset(%{listing_status: "quarantined"})
|
||||
|> Repo.update!()
|
||||
|
||||
conn = conn |> authed(token) |> get(~p"/api/github.com/openai/codex/jobs")
|
||||
|
||||
assert json_response(conn, 404)["error"] =~ "not registered"
|
||||
end
|
||||
|
||||
test "shows a task and returns 404 for missing or malformed ids", %{
|
||||
conn: conn,
|
||||
creator: creator,
|
||||
repository: repository,
|
||||
worker_token: token
|
||||
} do
|
||||
task = review_task_fixture(repository, creator)
|
||||
|
||||
conn1 = conn |> authed(token) |> get(~p"/api/jobs/#{task.id}")
|
||||
assert json_response(conn1, 200)["id"] == task.id
|
||||
|
||||
conn2 = conn |> recycle() |> authed(token) |> get("/api/jobs/not-an-id")
|
||||
assert json_response(conn2, 404)["error"] == "job not found"
|
||||
|
||||
conn3 = conn |> recycle() |> authed(token) |> get("/api/jobs/999999999")
|
||||
assert json_response(conn3, 404)["error"] == "job not found"
|
||||
end
|
||||
|
||||
test "claims a task and exposes the active lease", %{
|
||||
conn: conn,
|
||||
creator: creator,
|
||||
repository: repository,
|
||||
worker: worker,
|
||||
worker_token: token
|
||||
} do
|
||||
task = review_task_fixture(repository, creator)
|
||||
|
||||
conn = conn |> authed(token) |> post(~p"/api/jobs/#{task.id}/claim")
|
||||
|
||||
response = json_response(conn, 200)
|
||||
assert response["status"] == "claimed"
|
||||
assert response["claimant"]["id"] == worker.id
|
||||
assert response["lease"]["active"] == true
|
||||
assert response["lease"]["claimed_at"]
|
||||
assert response["lease"]["expires_at"]
|
||||
|
||||
original_expiry = response["lease"]["expires_at"]
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> recycle()
|
||||
|> authed(token)
|
||||
|> post(~p"/api/jobs/#{task.id}/claim")
|
||||
|
||||
repeated = json_response(conn, 200)
|
||||
assert repeated["claimant"]["id"] == worker.id
|
||||
assert repeated["lease"]["expires_at"] == original_expiry
|
||||
end
|
||||
|
||||
test "creators may claim their own jobs; active claims conflict for others", %{
|
||||
conn: conn,
|
||||
creator: creator,
|
||||
creator_token: creator_token,
|
||||
repository: repository,
|
||||
worker_token: worker_token
|
||||
} do
|
||||
task = review_task_fixture(repository, creator)
|
||||
|
||||
# Solo/hosted workflow: job creators may claim and perform their own Jobs.
|
||||
conn1 = conn |> authed(creator_token) |> post(~p"/api/jobs/#{task.id}/claim")
|
||||
assert json_response(conn1, 200)["claimant"]["id"] == creator.id
|
||||
|
||||
conn_release =
|
||||
conn |> recycle() |> authed(creator_token) |> delete(~p"/api/jobs/#{task.id}/claim")
|
||||
|
||||
assert json_response(conn_release, 200)["status"] == "open"
|
||||
|
||||
conn2 = conn |> recycle() |> authed(worker_token) |> post(~p"/api/jobs/#{task.id}/claim")
|
||||
assert json_response(conn2, 200)
|
||||
|
||||
outsider = account_fixture()
|
||||
outsider_token = client_token(outsider)
|
||||
|
||||
conn3 = conn |> recycle() |> authed(outsider_token) |> post(~p"/api/jobs/#{task.id}/claim")
|
||||
assert json_response(conn3, 409)["error"] =~ "active claim"
|
||||
end
|
||||
|
||||
test "only the claimant can release a task", %{
|
||||
conn: conn,
|
||||
creator: creator,
|
||||
repository: repository,
|
||||
worker: worker,
|
||||
worker_token: worker_token
|
||||
} do
|
||||
task = review_task_fixture(repository, creator)
|
||||
{:ok, task} = Work.claim_task(task, worker)
|
||||
outsider_token = client_token(account_fixture())
|
||||
|
||||
conn1 = conn |> authed(outsider_token) |> delete(~p"/api/jobs/#{task.id}/claim")
|
||||
assert json_response(conn1, 403)["error"] =~ "current claimant"
|
||||
|
||||
conn2 =
|
||||
conn
|
||||
|> recycle()
|
||||
|> authed(worker_token)
|
||||
|> delete(~p"/api/jobs/#{task.id}/claim")
|
||||
|
||||
response = json_response(conn2, 200)
|
||||
assert response["status"] == "open"
|
||||
assert response["claimant"] == nil
|
||||
assert response["lease"] == nil
|
||||
end
|
||||
|
||||
test "claimant can renew an active lease", %{
|
||||
conn: conn,
|
||||
creator: creator,
|
||||
repository: repository,
|
||||
worker_token: token
|
||||
} do
|
||||
task = review_task_fixture(repository, creator)
|
||||
|
||||
conn = conn |> authed(token) |> post(~p"/api/jobs/#{task.id}/claim")
|
||||
first = json_response(conn, 200)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> recycle()
|
||||
|> authed(token)
|
||||
|> post(~p"/api/jobs/#{task.id}/claim/renew")
|
||||
|
||||
renewed = json_response(conn, 200)
|
||||
assert renewed["lease"]["active"]
|
||||
assert renewed["lease"]["expires_at"] >= first["lease"]["expires_at"]
|
||||
end
|
||||
|
||||
test "completion path submits a claimed task for independent review", %{
|
||||
conn: conn,
|
||||
creator: creator,
|
||||
repository: repository,
|
||||
worker: worker,
|
||||
worker_token: token
|
||||
} do
|
||||
task = review_task_fixture(repository, creator)
|
||||
{:ok, task} = Work.claim_task(task, worker)
|
||||
|
||||
body = %{
|
||||
"provenance" => "hybrid",
|
||||
"summary" => "The authorization boundary is enforced.",
|
||||
"evidence" => "Reviewed both entry points and ran the focused regression test."
|
||||
}
|
||||
|
||||
conn = conn |> authed(token) |> post(~p"/api/jobs/#{task.id}/complete", body)
|
||||
|
||||
response = json_response(conn, 200)
|
||||
assert response["status"] == "submitted"
|
||||
assert response["visibility"] == "public"
|
||||
assert response["submitted_at"]
|
||||
assert response["completed_at"] == nil
|
||||
assert response["lease"]["active"] == false
|
||||
|
||||
assert response["contribution"] == %{
|
||||
"contributor" => %{
|
||||
"handle" => worker.handle,
|
||||
"id" => worker.id
|
||||
},
|
||||
"evidence" => body["evidence"],
|
||||
"id" => response["contribution"]["id"],
|
||||
"version" => 1,
|
||||
"provenance" => "hybrid",
|
||||
"submitted_at" => response["contribution"]["submitted_at"],
|
||||
"summary" => body["summary"]
|
||||
}
|
||||
|
||||
conn2 =
|
||||
conn
|
||||
|> recycle()
|
||||
|> authed(token)
|
||||
|> delete(~p"/api/jobs/#{task.id}/claim")
|
||||
|
||||
assert json_response(conn2, 403)["error"] =~ "current claimant"
|
||||
end
|
||||
|
||||
test "completion requires the active claimant and valid evidence metadata", %{
|
||||
conn: conn,
|
||||
creator: creator,
|
||||
repository: repository,
|
||||
worker: worker,
|
||||
worker_token: token
|
||||
} do
|
||||
task = review_task_fixture(repository, creator)
|
||||
|
||||
conn1 =
|
||||
conn
|
||||
|> authed(token)
|
||||
|> post(~p"/api/jobs/#{task.id}/complete", %{
|
||||
"provenance" => "human",
|
||||
"summary" => "Completed",
|
||||
"evidence" => "Attempted submission without first holding the task's active claim."
|
||||
})
|
||||
|
||||
assert json_response(conn1, 403)["error"] =~ "current claimant"
|
||||
|
||||
{:ok, _task} = Work.claim_task(task, worker)
|
||||
|
||||
conn2 =
|
||||
conn
|
||||
|> recycle()
|
||||
|> authed(token)
|
||||
|> post(~p"/api/jobs/#{task.id}/complete", %{
|
||||
"provenance" => "unverifiable",
|
||||
"summary" => ""
|
||||
})
|
||||
|
||||
errors = json_response(conn2, 422)["errors"]
|
||||
assert errors["provenance"] == ["is invalid"]
|
||||
assert errors["summary"] == ["can't be blank"]
|
||||
end
|
||||
|
||||
test "completion provenance must satisfy the requested capability", %{
|
||||
conn: conn,
|
||||
creator: creator,
|
||||
repository: repository,
|
||||
worker: worker,
|
||||
worker_token: token
|
||||
} do
|
||||
task = review_task_fixture(repository, creator, %{"capability" => "hybrid"})
|
||||
{:ok, _task} = Work.claim_task(task, worker)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> authed(token)
|
||||
|> post(~p"/api/jobs/#{task.id}/complete", %{
|
||||
"provenance" => "human",
|
||||
"summary" => "Manual review completed.",
|
||||
"evidence" => "Manually traced the requested paths and recorded reproducible notes."
|
||||
})
|
||||
|
||||
assert json_response(conn, 422)["errors"]["provenance"] == [
|
||||
"does not satisfy this task's required capability"
|
||||
]
|
||||
end
|
||||
|
||||
test "proposals are publicly listed and publishing is not exposed to client credentials", %{
|
||||
conn: conn,
|
||||
creator: creator,
|
||||
repository: repository,
|
||||
creator_token: creator_token,
|
||||
worker_token: worker_token
|
||||
} do
|
||||
proposal = proposed_review_task_fixture(repository, creator)
|
||||
|
||||
conn1 =
|
||||
conn
|
||||
|> authed(worker_token)
|
||||
|> get(~p"/api/github.com/openai/codex/jobs")
|
||||
|
||||
assert [listed] = json_response(conn1, 200)["jobs"]
|
||||
assert listed["id"] == proposal.id
|
||||
assert listed["status"] == "proposed"
|
||||
assert listed["visibility"] == "public"
|
||||
|
||||
conn2 =
|
||||
conn
|
||||
|> recycle()
|
||||
|> authed(creator_token)
|
||||
|> get(~p"/api/jobs/#{proposal.id}")
|
||||
|
||||
assert json_response(conn2, 200)["status"] == "proposed"
|
||||
|
||||
moderator = moderator_account_fixture()
|
||||
moderator_token = Accounts.create_account_api_token(moderator)
|
||||
|
||||
conn3 =
|
||||
conn
|
||||
|> recycle()
|
||||
|> authed(moderator_token)
|
||||
|> post("/api/jobs/#{proposal.id}/publish", %{
|
||||
"reason" => "The task scope is safe, bounded, and useful to contributors."
|
||||
})
|
||||
|
||||
assert response(conn3, 404) == "Not Found"
|
||||
|
||||
assert {:ok, open} =
|
||||
Work.publish_task(proposal, moderator, %{
|
||||
"reason" => "The task scope is safe, bounded, and useful to contributors."
|
||||
})
|
||||
|
||||
assert open.status == "open"
|
||||
end
|
||||
|
||||
test "acceptance is not exposed to client credentials", %{
|
||||
conn: conn,
|
||||
creator: creator,
|
||||
repository: repository,
|
||||
worker: worker,
|
||||
worker_token: worker_token
|
||||
} do
|
||||
task = review_task_fixture(repository, creator)
|
||||
{:ok, task} = Work.claim_task(task, worker)
|
||||
|
||||
{:ok, submitted} =
|
||||
Work.submit_task(task, worker, %{
|
||||
"provenance" => "human",
|
||||
"summary" => "The requested boundary was independently traced.",
|
||||
"evidence" => "Ran the negative authorization suite against both repository entry points."
|
||||
})
|
||||
|
||||
conn1 =
|
||||
conn
|
||||
|> authed(worker_token)
|
||||
|> post("/api/jobs/#{submitted.id}/accept", %{
|
||||
"reason" => "I should not be allowed to approve my own contribution.",
|
||||
"evidence" => "This evidence comes from the original contributor and is not independent."
|
||||
})
|
||||
|
||||
assert response(conn1, 404) == "Not Found"
|
||||
|
||||
reviewer = moderator_account_fixture()
|
||||
reviewer_token = Accounts.create_account_api_token(reviewer)
|
||||
assert Work.get_visible_task(submitted.id, Tarakan.Accounts.Scope.for_account(reviewer))
|
||||
|
||||
conn2 =
|
||||
conn
|
||||
|> recycle()
|
||||
|> authed(reviewer_token)
|
||||
|> post("/api/jobs/#{submitted.id}/accept", %{
|
||||
"reason" => "The result was independently reproduced at the pinned commit.",
|
||||
"evidence" => "Checked out the pinned SHA and ran the documented negative-path tests."
|
||||
})
|
||||
|
||||
assert response(conn2, 404) == "Not Found"
|
||||
|
||||
assert {:ok, accepted} =
|
||||
Work.accept_task(submitted, reviewer, %{
|
||||
"reason" => "The result was independently reproduced at the pinned commit.",
|
||||
"evidence" =>
|
||||
"Checked out the pinned SHA and ran the documented negative-path tests."
|
||||
})
|
||||
|
||||
assert accepted.status == "accepted"
|
||||
assert accepted.visibility == "public"
|
||||
|
||||
outsider = account_fixture()
|
||||
outsider_token = Accounts.create_account_api_token(outsider)
|
||||
|
||||
conn3 =
|
||||
conn
|
||||
|> recycle()
|
||||
|> authed(outsider_token)
|
||||
|> get(~p"/api/jobs/#{accepted.id}")
|
||||
|
||||
response = json_response(conn3, 200)
|
||||
assert response["status"] == "accepted"
|
||||
assert response["visibility"] == "public"
|
||||
assert response["contribution"]["summary"] =~ "boundary"
|
||||
assert response["contribution"]["evidence"] =~ "negative authorization suite"
|
||||
|
||||
assert {:ok, redacted} =
|
||||
Work.disclose_task(accepted, reviewer, "public_summary", %{
|
||||
"reason" =>
|
||||
"The redacted result is safe and useful without publishing raw evidence."
|
||||
})
|
||||
|
||||
conn4 =
|
||||
conn
|
||||
|> recycle()
|
||||
|> authed(outsider_token)
|
||||
|> get(~p"/api/jobs/#{redacted.id}")
|
||||
|
||||
response = json_response(conn4, 200)
|
||||
assert response["visibility"] == "public_summary"
|
||||
assert response["contribution"]["summary"] =~ "boundary"
|
||||
assert response["contribution"]["evidence"] == nil
|
||||
assert response["decisions"] == []
|
||||
assert response["disclosed_at"]
|
||||
assert response["sensitive_data_reviewed"] == false
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue