Initial commit on Forgejo
All checks were successful
CI and deploy / Test (push) Successful in 4m52s
CI and deploy / Deploy production (push) Successful in 23s

Fresh repository history for elektrine/tarakan hosted at
https://git.elektrine.com/elektrine/tarakan.
This commit is contained in:
Maxfield Luke 2026-07-29 04:43:40 -04:00
commit af6077b9c3
455 changed files with 78366 additions and 0 deletions

83
test/support/conn_case.ex Normal file
View file

@ -0,0 +1,83 @@
defmodule TarakanWeb.ConnCase do
@moduledoc """
This module defines the test case to be used by
tests that require setting up a connection.
Such tests rely on `Phoenix.ConnTest` and also
import other functionality to make it easier
to build common data structures and query the data layer.
Finally, if the test case interacts with the database,
we enable the SQL sandbox, so changes done to the database
are reverted at the end of every test. If you are using
PostgreSQL, you can even run database tests asynchronously
by setting `use TarakanWeb.ConnCase, async: true`, although
this option is not recommended for other databases.
"""
use ExUnit.CaseTemplate
using do
quote do
# The default endpoint for testing
@endpoint TarakanWeb.Endpoint
use TarakanWeb, :verified_routes
# Import conveniences for testing with connections
import Plug.Conn
import Phoenix.ConnTest
import Tarakan.AccountsFixtures
import Tarakan.ScansFixtures
import Tarakan.WorkFixtures
import Tarakan.MarketFixtures
import TarakanWeb.ConnCase
end
end
setup tags do
Tarakan.DataCase.setup_sandbox(tags)
{:ok, conn: Phoenix.ConnTest.build_conn()}
end
@doc """
Setup helper that registers and logs in accounts.
setup :register_and_log_in_account
It stores an updated connection and a registered account in the
test context.
"""
def register_and_log_in_account(%{conn: conn} = context) do
account = Tarakan.AccountsFixtures.account_fixture()
scope = Tarakan.Accounts.Scope.for_account(account)
opts =
context
|> Map.take([:token_authenticated_at])
|> Enum.into([])
%{conn: log_in_account(conn, account, opts), account: account, scope: scope}
end
@doc """
Logs the given `account` into the `conn`.
It returns an updated `conn`.
"""
def log_in_account(conn, account, opts \\ []) do
token = Tarakan.Accounts.generate_account_session_token(account)
maybe_set_token_authenticated_at(token, opts[:token_authenticated_at])
conn
|> Phoenix.ConnTest.init_test_session(%{})
|> Plug.Conn.put_session(:account_token, token)
end
defp maybe_set_token_authenticated_at(_token, nil), do: nil
defp maybe_set_token_authenticated_at(token, authenticated_at) do
Tarakan.AccountsFixtures.override_token_authenticated_at(token, authenticated_at)
end
end

62
test/support/data_case.ex Normal file
View file

@ -0,0 +1,62 @@
defmodule Tarakan.DataCase do
@moduledoc """
This module defines the setup for tests requiring
access to the application's data layer.
You may define functions here to be used as helpers in
your tests.
Finally, if the test case interacts with the database,
we enable the SQL sandbox, so changes done to the database
are reverted at the end of every test. If you are using
PostgreSQL, you can even run database tests asynchronously
by setting `use Tarakan.DataCase, async: true`, although
this option is not recommended for other databases.
"""
use ExUnit.CaseTemplate
using do
quote do
alias Tarakan.Repo
import Ecto
import Ecto.Changeset
import Ecto.Query
import Tarakan.AccountsFixtures
import Tarakan.ScansFixtures
import Tarakan.WorkFixtures
import Tarakan.MarketFixtures
import Tarakan.DataCase
end
end
setup tags do
Tarakan.DataCase.setup_sandbox(tags)
:ok
end
@doc """
Sets up the sandbox based on the test tags.
"""
def setup_sandbox(tags) do
pid = Ecto.Adapters.SQL.Sandbox.start_owner!(Tarakan.Repo, shared: not tags[:async])
on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
end
@doc """
A helper that transforms changeset errors into a map of messages.
assert {:error, changeset} = Accounts.create_user(%{password: "short"})
assert "password is too short" in errors_on(changeset).password
assert %{password: ["password is too short"]} = errors_on(changeset)
"""
def errors_on(changeset) do
Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
Regex.replace(~r"%{(\w+)}", message, fn _, key ->
opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
end)
end)
end
end

View file

@ -0,0 +1,114 @@
defmodule Tarakan.AccountsFixtures do
@moduledoc false
alias Tarakan.Accounts
def github_account_fixture(overrides \\ %{}) do
unique_id = System.unique_integer([:positive])
profile =
Map.merge(
%{
provider_uid: unique_id,
provider_login: "contributor-#{unique_id}",
name: "Tarakan Contributor",
avatar_url: "https://avatars.githubusercontent.com/u/#{unique_id}",
profile_url: "https://github.com/contributor-#{unique_id}"
},
overrides
)
{:ok, account} = Accounts.upsert_external_identity(:github, profile)
account
end
import Ecto.Query
alias Tarakan.Accounts
alias Tarakan.Accounts.Scope
def unique_account_email, do: "account#{System.unique_integer()}@example.com"
def unique_account_handle, do: "account-#{System.unique_integer([:positive])}"
def valid_account_password, do: "correct horse battery staple"
def valid_account_attributes(attrs \\ %{}) do
Enum.into(attrs, %{
email: unique_account_email(),
handle: unique_account_handle()
})
end
# Registration no longer stamps confirmed_at: email confirmation happens on
# the first magic-link login (see account_fixture/1 below).
def unconfirmed_account_fixture(attrs \\ %{}) do
{:ok, account} =
attrs
|> valid_account_attributes()
|> Accounts.register_account()
account
end
def account_fixture(attrs \\ %{}) do
account = unconfirmed_account_fixture(attrs)
token =
extract_account_token(fn url ->
Accounts.deliver_login_instructions(account, url)
end)
{:ok, {account, _expired_tokens}} =
Accounts.login_account_by_magic_link(token)
account
end
def account_scope_fixture do
account = account_fixture()
account_scope_fixture(account)
end
def account_scope_fixture(account) do
Scope.for_account(account)
end
def set_password(account) do
{:ok, {account, _expired_tokens}} =
Accounts.update_account_password(account, %{password: valid_account_password()})
account
end
def extract_account_token(fun) do
{:ok, captured_email} = fun.(&"[TOKEN]#{&1}[TOKEN]")
[_, token | _] = String.split(captured_email.text_body, "[TOKEN]")
token
end
def override_token_authenticated_at(token, authenticated_at) when is_binary(token) do
hashed = Accounts.AccountToken.hash_token(token)
Tarakan.Repo.update_all(
from(t in Accounts.AccountToken,
where: t.token == ^hashed
),
set: [authenticated_at: authenticated_at]
)
end
def generate_account_magic_link_token(account) do
{encoded_token, account_token} = Accounts.AccountToken.build_email_token(account, "login")
Tarakan.Repo.insert!(account_token)
{encoded_token, account_token.token}
end
def offset_account_token(token, amount_to_add, unit) do
dt = DateTime.add(DateTime.utc_now(:second), amount_to_add, unit)
hashed = Accounts.AccountToken.hash_token(token)
Tarakan.Repo.update_all(
from(ut in Accounts.AccountToken, where: ut.token == ^hashed),
set: [inserted_at: dt, authenticated_at: dt]
)
end
end

View file

@ -0,0 +1,131 @@
defmodule Tarakan.ScansFixtures do
@moduledoc """
Test helpers for creating entities via the `Tarakan.Scans` context.
"""
import Tarakan.AccountsFixtures
alias Tarakan.Repositories
alias Tarakan.Scans
alias Tarakan.Accounts.Account
@doc """
Registers the one repository the GitHub stub can verify.
"""
def github_repository_fixture(account \\ nil) do
account = account || github_account_fixture()
{:ok, repository} = Repositories.register_github_repository("openai/codex", account)
repository
end
@doc "Registers the GitHub fixture and makes it visible for public-record tests."
def listed_github_repository_fixture(account \\ nil) do
account
|> github_repository_fixture()
|> listed_repository_fixture()
end
@doc """
Inserts a listed Tarakan-hosted repository record.
Goes through the schema rather than `Tarakan.HostedRepositories.create/2` so
no bare repository is written to disk.
"""
def listed_hosted_repository_fixture(account \\ nil) do
account = account || github_account_fixture()
%Tarakan.Repositories.Repository{}
|> Tarakan.Repositories.Repository.hosted_changeset(
%{"name" => "hosted-#{System.unique_integer([:positive])}", "description" => "Hosted."},
account.handle
)
|> Ecto.Changeset.put_change(:submitted_by_id, account.id)
|> Tarakan.Repo.insert!()
|> listed_repository_fixture()
end
def listed_repository_fixture(repository) do
repository
|> Tarakan.Repositories.Repository.listing_changeset(%{listing_status: "listed"})
|> Tarakan.Repo.update!()
end
def valid_scan_attributes(overrides \\ %{}) do
Enum.into(overrides, %{
"commit_sha" => random_commit_sha(),
"model" => "claude-sonnet-5",
"prompt_version" => "tarakan-baseline/v1",
"run_id" => "fixture-#{System.unique_integer([:positive, :monotonic])}",
"findings_json" => nil
})
end
def scan_fixture(repository, account, overrides \\ %{}) do
{:ok, scan} = Scans.submit_scan(repository, account, valid_scan_attributes(overrides))
scan
end
def confirmation_fixture(scan, account, verdict \\ "confirmed") do
account = reviewer_account(account)
{:ok, scan} =
Scans.record_confirmation(scan, account, %{
"verdict" => verdict,
"notes" =>
"Independently traced and reproduced the reported behavior in a local checkout."
})
scan
end
def reviewer_account_fixture do
account_fixture() |> reviewer_account()
end
def moderator_account_fixture do
account_fixture() |> moderator_account()
end
def moderator_account(account) do
account
|> Account.authorization_changeset(%{
state: "active",
platform_role: "moderator",
trust_tier: "reviewer"
})
|> Tarakan.Repo.update!()
end
def reviewer_account(%Account{platform_role: "moderator", trust_tier: "reviewer"} = account),
do: account
def reviewer_account(account) do
account
|> Account.authorization_changeset(%{
state: "active",
platform_role: "moderator",
trust_tier: "reviewer"
})
|> Tarakan.Repo.update!()
end
def findings_json_fixture(count \\ 1) do
findings =
for index <- 1..count do
%{
"file" => "lib/example/module_#{index}.ex",
"line_start" => index * 10,
"line_end" => index * 10 + 5,
"severity" => "high",
"title" => "Unsanitized input reaches interpolated query (#{index})",
"description" => "String-built SQL executed with request parameters."
}
end
Jason.encode!(%{"tarakan_scan_format" => 1, "findings" => findings})
end
def random_commit_sha do
16 |> :crypto.strong_rand_bytes() |> Base.encode16(case: :lower) |> Kernel.<>("00000000")
end
end

View file

@ -0,0 +1,45 @@
defmodule Tarakan.WorkFixtures do
@moduledoc """
Test helpers for the public review work queue.
"""
alias Tarakan.Work
def proposed_review_task_fixture(repository, creator, overrides \\ %{}) do
{:ok, task} =
Work.create_task(repository, creator, valid_review_task_attributes(overrides))
task
end
def valid_review_task_attributes(overrides \\ %{}) do
Enum.into(overrides, %{
"commit_sha" => Tarakan.ScansFixtures.random_commit_sha(),
"kind" => "threat_model",
"capability" => "human",
"title" => "Map the authorization boundary",
"description" => "Trace organization membership checks and document trust assumptions."
})
end
def review_task_fixture(repository, creator, overrides \\ %{}) do
task = proposed_review_task_fixture(repository, creator, overrides)
publisher = Tarakan.AccountsFixtures.account_fixture()
publisher =
publisher
|> Tarakan.Accounts.Account.authorization_changeset(%{
state: "active",
platform_role: "moderator",
trust_tier: "reviewer"
})
|> Tarakan.Repo.update!()
{:ok, task} =
Work.publish_task(task, publisher, %{
"reason" => "The task has a bounded, safe, and useful review scope."
})
task
end
end

View file

@ -0,0 +1,22 @@
defmodule Tarakan.GitHubBulkStub do
@moduledoc """
Bulk-lookup stub. Tests script responses per node id with
`put_response/2`; unscripted ids resolve to `nil` (node gone).
"""
@behaviour Tarakan.GitHubBulkClient
def put_response(node_id, response) do
Process.put({__MODULE__, node_id}, response)
end
@impl true
def fetch_repositories_by_node_ids(node_ids) when is_list(node_ids) do
case Process.get({__MODULE__, :batch_error}) do
nil -> {:ok, Enum.map(node_ids, &Process.get({__MODULE__, &1}))}
reason -> {:error, reason}
end
end
def fail_batches_with(reason), do: Process.put({__MODULE__, :batch_error}, reason)
end

View file

@ -0,0 +1,449 @@
defmodule Tarakan.GitHubStub do
@moduledoc false
@behaviour Tarakan.GitHubClient
@root_tree_sha String.duplicate("1", 40)
@lib_tree_sha String.duplicate("2", 40)
@readme_blob_sha String.duplicate("3", 40)
@source_blob_sha String.duplicate("4", 40)
@large_blob_sha String.duplicate("5", 40)
@binary_blob_sha String.duplicate("6", 40)
@default_commit_sha String.duplicate("7", 40)
@symlink_blob_sha String.duplicate("9", 40)
@submodule_sha String.duplicate("a", 40)
@many_lines_blob_sha String.duplicate("d", 40)
@truncated_tree_sha String.duplicate("e", 40)
@unsafe_html_blob_sha String.duplicate("f", 40)
@readme "# Codex\n"
@source "defmodule Codex do\n def hello, do: :world\nend\n"
@unsafe_html "</code><script id=\"source-xss\">alert(1)</script>\n"
@codex_etag ~s(W/"codex-metadata")
def codex_etag, do: @codex_etag
@impl true
def fetch_repository(owner, name, opts \\ [])
def fetch_repository("openai", "codex", opts) do
if Keyword.get(opts, :etag) == @codex_etag do
:not_modified
else
{:ok,
%{
github_id: 95_959_834,
node_id: "R_kgDOcodex000",
host: "github.com",
owner: "openai",
name: "codex",
canonical_url: "https://github.com/openai/codex",
default_branch: "main",
description: "Lightweight coding agent that runs in your terminal",
primary_language: "Rust",
stars_count: 42_000,
forks_count: 4_200,
archived: false,
private: false,
visibility: "public",
last_synced_at: ~U[2026-07-10 04:00:00.000000Z],
etag: @codex_etag
}}
end
end
def fetch_repository("missing", "repository", _opts), do: {:error, :not_found}
# Renamed on the host: the old path 301s, the immutable id resolves to acme/widget.
def fetch_repository("legacy", "widget", _opts), do: {:error, :moved}
def fetch_repository("acme", "widget", _opts), do: {:ok, renamed_widget_metadata()}
def fetch_repository("acme", "gadget", _opts) do
{:ok,
%{
github_id: 77_778,
node_id: "R_kgDOgadget00",
host: "github.com",
owner: "acme",
name: "gadget",
canonical_url: "https://github.com/acme/gadget",
default_branch: "main",
description: "Gadget toolkit",
primary_language: "Go",
stars_count: 21,
forks_count: 2,
archived: false,
private: false,
visibility: "public",
last_synced_at: ~U[2026-07-10 04:00:00.000000Z]
}}
end
def fetch_repository("private", "repository", _opts) do
{:ok,
%{
github_id: 12_345,
host: "github.com",
owner: "private",
name: "repository",
canonical_url: "https://github.com/private/repository",
default_branch: "main",
private: true,
visibility: "private"
}}
end
def fetch_repository("rate", "limited", _opts), do: {:error, :rate_limited}
def fetch_repository("flip", "repository", _opts) do
count = Process.get(:github_flip_identity_count, 0)
Process.put(:github_flip_identity_count, count + 1)
if count == 0 do
{:ok,
%{
github_id: 88_888,
host: "github.com",
owner: "flip",
name: "repository",
canonical_url: "https://github.com/flip/repository",
default_branch: "main",
private: false,
visibility: "public"
}}
else
{:ok,
%{
github_id: 88_888,
owner: "flip",
name: "repository",
default_branch: "main",
private: true,
visibility: "private"
}}
end
end
def fetch_repository("lateflip", "repository", _opts) do
count = Process.get(:github_late_flip_identity_count, 0)
Process.put(:github_late_flip_identity_count, count + 1)
if count < 3 do
{:ok,
%{
github_id: 99_999,
host: "github.com",
owner: "lateflip",
name: "repository",
canonical_url: "https://github.com/lateflip/repository",
default_branch: "main",
private: false,
visibility: "public"
}}
else
{:ok,
%{
github_id: 99_999,
owner: "lateflip",
name: "repository",
default_branch: "main",
private: true,
visibility: "private"
}}
end
end
def fetch_repository(_owner, _name, _opts), do: {:error, :unavailable}
@impl true
def fetch_repository_by_id(95_959_834), do: fetch_repository("openai", "codex", [])
def fetch_repository_by_id(12_345), do: fetch_repository("private", "repository", [])
def fetch_repository_by_id(88_888), do: fetch_repository("flip", "repository", [])
def fetch_repository_by_id(99_999), do: fetch_repository("lateflip", "repository", [])
def fetch_repository_by_id(77_777), do: {:ok, renamed_widget_metadata()}
def fetch_repository_by_id(_github_id), do: {:error, :not_found}
defp renamed_widget_metadata do
%{
github_id: 77_777,
node_id: "R_kgDOwidget00",
host: "github.com",
owner: "acme",
name: "widget",
canonical_url: "https://github.com/acme/widget",
default_branch: "main",
description: "Widget toolkit",
primary_language: "Elixir",
stars_count: 12,
forks_count: 3,
archived: false,
private: false,
visibility: "public",
last_synced_at: ~U[2026-07-10 04:00:00.000000Z]
}
end
@impl true
def fetch_commit("openai", "codex", "dead" <> _rest), do: {:error, :not_found}
def fetch_commit("openai", "codex", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") do
{:ok,
%{
sha: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
tree_sha: @root_tree_sha,
committed_at: ~U[2026-07-01 12:00:00Z]
}}
end
def fetch_commit("openai", "codex", "cccccccccccccccccccccccccccccccccccccccc") do
{:ok,
%{
sha: "cccccccccccccccccccccccccccccccccccccccc",
tree_sha: @truncated_tree_sha,
committed_at: ~U[2026-07-01 12:00:00Z]
}}
end
# Second precision, mirroring what the GitHub API actually returns.
def fetch_commit("openai", "codex", sha),
do:
{:ok,
%{
sha: String.downcase(sha),
tree_sha: @root_tree_sha,
committed_at: ~U[2026-07-01 12:00:00Z]
}}
def fetch_commit("acme", "widget", sha), do: fetch_commit("openai", "codex", sha)
def fetch_commit("acme", "gadget", sha), do: fetch_commit("openai", "codex", sha)
def fetch_commit("rate", "limited", _sha), do: {:error, :rate_limited}
def fetch_commit("flip", "repository", sha), do: fetch_commit("openai", "codex", sha)
def fetch_commit("lateflip", "repository", sha), do: fetch_commit("openai", "codex", sha)
def fetch_commit(_owner, _name, _sha), do: {:error, :unavailable}
@impl true
def fetch_branch_head("openai", "codex", "main") do
{:ok,
%{
sha: @default_commit_sha,
tree_sha: @root_tree_sha,
committed_at: ~U[2026-07-01 12:00:00Z]
}}
end
def fetch_branch_head("openai", "codex", "develop") do
{:ok,
%{
sha: String.duplicate("8", 40),
tree_sha: @root_tree_sha,
committed_at: ~U[2026-07-02 12:00:00Z]
}}
end
def fetch_branch_head("acme", "widget", branch),
do: fetch_branch_head("openai", "codex", branch)
def fetch_branch_head("acme", "gadget", branch),
do: fetch_branch_head("openai", "codex", branch)
def fetch_branch_head("rate", "limited", _branch), do: {:error, :rate_limited}
def fetch_branch_head(_owner, _name, _branch), do: {:error, :not_found}
@impl true
def list_branches("openai", "codex"), do: {:ok, ["main", "develop", "feature/auth"]}
def list_branches("acme", "widget"), do: list_branches("openai", "codex")
def list_branches("acme", "gadget"), do: list_branches("openai", "codex")
def list_branches("rate", "limited"), do: {:error, :rate_limited}
def list_branches(_owner, _name), do: {:error, :not_found}
@impl true
def fetch_tree("openai", "codex", @root_tree_sha, recursive) do
direct_entries = [
%{path: "lib", mode: "040000", type: "tree", sha: @lib_tree_sha, size: nil},
%{
path: "README.md",
mode: "100644",
type: "blob",
sha: @readme_blob_sha,
size: byte_size(@readme)
},
%{
path: "space # 100%.txt",
mode: "100644",
type: "blob",
sha: @readme_blob_sha,
size: byte_size(@readme)
},
%{
path: "λ.ex",
mode: "100644",
type: "blob",
sha: @source_blob_sha,
size: byte_size(@source)
},
%{
path: "large.txt",
mode: "100644",
type: "blob",
sha: @large_blob_sha,
size: 512 * 1_024 + 1
},
%{
path: "binary.dat",
mode: "100644",
type: "blob",
sha: @binary_blob_sha,
size: 4
},
%{
path: "many-lines.txt",
mode: "100644",
type: "blob",
sha: @many_lines_blob_sha,
size: 10_000
},
%{
path: "unsafe.html",
mode: "100644",
type: "blob",
sha: @unsafe_html_blob_sha,
size: byte_size(@unsafe_html)
},
%{
path: "source-link",
mode: "120000",
type: "symlink",
sha: @symlink_blob_sha,
size: 12
},
%{
path: "dependency",
mode: "160000",
type: "submodule",
sha: @submodule_sha,
size: nil
}
]
entries =
if recursive do
direct_entries ++
[
%{
path: "lib/codex.ex",
mode: "100644",
type: "blob",
sha: @source_blob_sha,
size: byte_size(@source)
}
]
else
direct_entries
end
{:ok, %{sha: @root_tree_sha, truncated: false, entries: entries}}
end
def fetch_tree("openai", "codex", @lib_tree_sha, _recursive) do
{:ok,
%{
sha: @lib_tree_sha,
truncated: false,
entries: [
%{
path: "codex.ex",
mode: "100644",
type: "blob",
sha: @source_blob_sha,
size: byte_size(@source)
}
]
}}
end
def fetch_tree("openai", "codex", @truncated_tree_sha, _recursive) do
{:ok, %{sha: @truncated_tree_sha, truncated: true, entries: []}}
end
def fetch_tree("acme", "widget", tree_sha, recursive),
do: fetch_tree("openai", "codex", tree_sha, recursive)
def fetch_tree("acme", "gadget", tree_sha, recursive),
do: fetch_tree("openai", "codex", tree_sha, recursive)
def fetch_tree("rate", "limited", _tree_sha, _recursive), do: {:error, :rate_limited}
def fetch_tree("flip", "repository", tree_sha, recursive),
do: fetch_tree("openai", "codex", tree_sha, recursive)
def fetch_tree("lateflip", "repository", tree_sha, recursive),
do: fetch_tree("openai", "codex", tree_sha, recursive)
def fetch_tree(_owner, _name, _tree_sha, _recursive), do: {:error, :not_found}
@impl true
def fetch_text_blob("openai", "codex", @readme_blob_sha) do
{:ok, %{sha: @readme_blob_sha, size: byte_size(@readme), content: @readme}}
end
def fetch_text_blob("openai", "codex", @source_blob_sha) do
{:ok, %{sha: @source_blob_sha, size: byte_size(@source), content: @source}}
end
def fetch_text_blob("openai", "codex", @binary_blob_sha) do
{:ok, %{sha: @binary_blob_sha, size: 4, content: <<0, 1, 2, 3>>}}
end
def fetch_text_blob("openai", "codex", @many_lines_blob_sha) do
content = String.duplicate("\n", 10_000)
{:ok, %{sha: @many_lines_blob_sha, size: byte_size(content), content: content}}
end
def fetch_text_blob("openai", "codex", @unsafe_html_blob_sha) do
{:ok,
%{
sha: @unsafe_html_blob_sha,
size: byte_size(@unsafe_html),
content: @unsafe_html
}}
end
def fetch_text_blob("openai", "codex", @large_blob_sha), do: {:error, :unavailable}
def fetch_text_blob("acme", "widget", blob_sha),
do: fetch_text_blob("openai", "codex", blob_sha)
def fetch_text_blob("acme", "gadget", blob_sha),
do: fetch_text_blob("openai", "codex", blob_sha)
def fetch_text_blob("rate", "limited", _blob_sha), do: {:error, :rate_limited}
def fetch_text_blob(_owner, _name, _blob_sha), do: {:error, :not_found}
end
defmodule Tarakan.GitHub.OAuthStub do
@moduledoc false
@behaviour Tarakan.GitHub.OAuthClient
@impl true
def exchange_code("valid-code", verifier, redirect_uri)
when is_binary(verifier) and is_binary(redirect_uri),
do: {:ok, "temporary-user-token"}
def exchange_code(_code, _verifier, _redirect_uri), do: {:error, :authorization_failed}
@impl true
def fetch_user("temporary-user-token") do
{:ok,
%{
provider_uid: 58_399_067,
provider_login: "TarakanTester",
name: "Tarakan Tester",
avatar_url: "https://avatars.githubusercontent.com/u/58399067",
profile_url: "https://github.com/TarakanTester"
}}
end
def fetch_user(_token), do: {:error, :authorization_failed}
end

View file

@ -0,0 +1,26 @@
defmodule Tarakan.GitLab.OAuthStub do
@moduledoc false
@behaviour Tarakan.GitLab.OAuthClient
@impl true
def exchange_code("valid-gitlab-code", verifier, redirect_uri)
when is_binary(verifier) and is_binary(redirect_uri),
do: {:ok, "temporary-gitlab-user-token"}
def exchange_code(_code, _verifier, _redirect_uri), do: {:error, :authorization_failed}
@impl true
def fetch_user("temporary-gitlab-user-token") do
{:ok,
%{
provider_uid: 24_680,
provider_login: "GitLabSignal",
name: "GitLab Signal",
avatar_url: "https://gitlab.com/uploads/-/system/user/avatar/24680/avatar.png",
profile_url: "https://gitlab.com/GitLabSignal"
}}
end
def fetch_user(_token), do: {:error, :authorization_failed}
end

View file

@ -0,0 +1,71 @@
defmodule Tarakan.MarketFixtures do
@moduledoc """
Test helpers for the bounty marketplace.
"""
alias Tarakan.Accounts.Account
alias Tarakan.Accounts.Scope
alias Tarakan.Credits
alias Tarakan.Market
alias Tarakan.Repo
@doc "An account fixture promoted to active standing (bounties require it)."
def active_account_fixture(attrs \\ %{}) do
account = Tarakan.AccountsFixtures.account_fixture(attrs)
account
|> Account.authorization_changeset(%{
state: "active",
platform_role: account.platform_role,
trust_tier: account.trust_tier
})
|> Repo.update!()
end
@doc "Tops up an account's credit balance and returns the fresh account row."
def fund_credits(%Account{} = account, amount) do
{:ok, _entry} = Credits.credit(account, :receive_bounty, nil, amount)
Repo.get!(Account, account.id)
end
def valid_bounty_attributes(repository, overrides \\ %{}) do
Enum.into(overrides, %{
"target_type" => "repository",
"target_ref" => "#{repository.owner}/#{repository.name}",
"title" => "Fix the session auth bypass",
"description" =>
"Reproduce the authorization bypass in the session handler and ship a reviewed fix.",
"funding" => "credits",
"credit_amount" => 500
})
end
@doc "Creates a credits-funded bounty on the given listed repository."
def credits_bounty_fixture(%Scope{} = scope, repository, overrides \\ %{}) do
{:ok, bounty} =
Market.create_bounty(scope, valid_bounty_attributes(repository, overrides))
bounty
end
@doc "Creates a fiat bounty on the given listed repository (Stripe stubbed)."
def fiat_bounty_fixture(%Scope{} = scope, repository, overrides \\ %{}) do
attrs =
repository
|> valid_bounty_attributes(overrides)
|> Map.merge(%{"funding" => "fiat", "amount_cents" => 25_000})
|> Map.delete("credit_amount")
{:ok, bounty, checkout_url} = Market.create_bounty(scope, attrs)
{bounty, checkout_url}
end
@doc "Legacy-prose submission attrs accepted by Work.submit_task/3 in tests."
def submission_attributes do
%{
"provenance" => "human",
"summary" => "Traced the bypass and patched the guard clause.",
"evidence" => "Reproduced locally against the pinned commit; patch and test attached."
}
end
end

View file

@ -0,0 +1,139 @@
defmodule Tarakan.RepositoryCode.InstrumentedGitHubClient do
@moduledoc false
@behaviour Tarakan.GitHubClient
alias Tarakan.GitHubStub
def child_spec(opts) do
%{id: __MODULE__, start: {__MODULE__, :start_link, [opts]}}
end
def start_link(_opts) do
Agent.start_link(
fn ->
%{
calls: %{},
notify: nil,
block_once: MapSet.new(),
visibility: :public
}
end,
name: __MODULE__
)
end
def configure(opts) do
Agent.update(__MODULE__, fn state ->
%{
state
| notify: Keyword.get(opts, :notify, state.notify),
block_once: MapSet.new(Keyword.get(opts, :block_once, [])),
visibility: Keyword.get(opts, :visibility, state.visibility)
}
end)
end
def count(kind), do: Agent.get(__MODULE__, &Map.get(&1.calls, kind, 0))
@impl true
def fetch_repository(owner, name, opts \\ []) do
record_call(:repository)
case Agent.get(__MODULE__, & &1.visibility) do
:public ->
GitHubStub.fetch_repository(owner, name, opts)
:private ->
# A visibility change always rotates the upstream ETag, so a
# conditional request can never mask it with a 304.
with {:ok, metadata} <- GitHubStub.fetch_repository(owner, name) do
{:ok, %{metadata | private: true, visibility: "private"}}
end
:not_found ->
{:error, :not_found}
{:github_id, github_id} ->
with {:ok, metadata} <- GitHubStub.fetch_repository(owner, name) do
{:ok, %{metadata | github_id: github_id}}
end
end
end
@impl true
def fetch_repository_by_id(github_id) do
record_call(:repository_by_id)
case Agent.get(__MODULE__, & &1.visibility) do
:public ->
GitHubStub.fetch_repository_by_id(github_id)
:private ->
with {:ok, metadata} <- GitHubStub.fetch_repository_by_id(github_id) do
{:ok, %{metadata | private: true, visibility: "private"}}
end
:not_found ->
{:error, :not_found}
{:github_id, _forced_id} ->
GitHubStub.fetch_repository_by_id(github_id)
end
end
@impl true
def fetch_commit(owner, name, sha) do
record_call(:commit)
GitHubStub.fetch_commit(owner, name, sha)
end
@impl true
def fetch_branch_head(owner, name, branch) do
record_call(:head)
GitHubStub.fetch_branch_head(owner, name, branch)
end
@impl true
def list_branches(owner, name) do
record_call(:branches)
GitHubStub.list_branches(owner, name)
end
@impl true
def fetch_tree(owner, name, tree_sha, recursive) do
record_call(:tree)
GitHubStub.fetch_tree(owner, name, tree_sha, recursive)
end
@impl true
def fetch_text_blob(owner, name, blob_sha) do
record_call(:blob)
GitHubStub.fetch_text_blob(owner, name, blob_sha)
end
defp record_call(kind) do
{notify, block?} =
Agent.get_and_update(__MODULE__, fn state ->
block? = MapSet.member?(state.block_once, kind)
next_state = %{
state
| calls: Map.update(state.calls, kind, 1, &(&1 + 1)),
block_once: MapSet.delete(state.block_once, kind)
}
{{state.notify, block?}, next_state}
end)
if block? and is_pid(notify) do
send(notify, {:upstream_blocked, kind, self()})
receive do
{:release_upstream, ^kind} -> :ok
after
5_000 -> :ok
end
end
end
end

View file

@ -0,0 +1,83 @@
defmodule Tarakan.Billing.StripeStub do
@moduledoc """
Test double for `Tarakan.Billing.Stripe`.
Serves deterministic default responses for the endpoints the market flow
uses. Tests can override behaviour per process:
Tarakan.Billing.StripeStub.respond_with({:error, :card_declined})
Tarakan.Billing.StripeStub.respond_with(fn :post, "/v1/refunds", _body -> ... end)
Tarakan.Billing.StripeStub.reset()
"""
@behaviour Tarakan.Billing.Stripe
@impl true
def request(method, path, body_or_params \\ nil) do
case Process.get(:stripe_stub_response) do
nil -> default_response(method, path, body_or_params)
fun when is_function(fun, 3) -> fun.(method, path, body_or_params)
response -> response
end
end
@doc "Overrides the stubbed response for the current test process."
def respond_with(response_or_fun), do: Process.put(:stripe_stub_response, response_or_fun)
@doc "Restores the default responses for the current test process."
def reset, do: Process.delete(:stripe_stub_response)
defp default_response(:post, "/v1/checkout/sessions", _body) do
id = "cs_test_#{System.unique_integer([:positive])}"
{:ok,
%{
"id" => id,
"url" => "https://checkout.stripe.com/c/pay/#{id}",
"payment_intent" => "pi_#{id}",
"status" => "open"
}}
end
defp default_response(:get, "/v1/checkout/sessions/" <> id, _params) do
{:ok, %{"id" => id, "payment_intent" => "pi_#{id}", "status" => "complete"}}
end
defp default_response(:post, "/v1/refunds", body) do
payment_intent = body[:payment_intent] || body["payment_intent"]
{:ok,
%{
"id" => "re_test_#{System.unique_integer([:positive])}",
"payment_intent" => payment_intent,
"status" => "succeeded"
}}
end
defp default_response(:post, "/v1/billing_portal/sessions", body) do
customer =
case body do
map when is_map(map) ->
map[:customer] || map["customer"]
list when is_list(list) ->
Enum.find_value(list, fn
{:customer, value} -> value
{"customer", value} -> value
_other -> nil
end)
_other ->
nil
end
{:ok,
%{
"id" => "bps_test_#{System.unique_integer([:positive])}",
"customer" => customer,
"url" => "https://billing.stripe.com/p/session/test_#{System.unique_integer([:positive])}"
}}
end
defp default_response(_method, _path, _body), do: {:error, :unexpected_request}
end