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
83
test/support/conn_case.ex
Normal file
83
test/support/conn_case.ex
Normal 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
62
test/support/data_case.ex
Normal 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
|
||||
114
test/support/fixtures/accounts_fixtures.ex
Normal file
114
test/support/fixtures/accounts_fixtures.ex
Normal 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
|
||||
131
test/support/fixtures/scans_fixtures.ex
Normal file
131
test/support/fixtures/scans_fixtures.ex
Normal 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
|
||||
45
test/support/fixtures/work_fixtures.ex
Normal file
45
test/support/fixtures/work_fixtures.ex
Normal 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
|
||||
22
test/support/github_bulk_stubs.ex
Normal file
22
test/support/github_bulk_stubs.ex
Normal 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
|
||||
449
test/support/github_stubs.ex
Normal file
449
test/support/github_stubs.ex
Normal 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
|
||||
26
test/support/gitlab_stubs.ex
Normal file
26
test/support/gitlab_stubs.ex
Normal 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
|
||||
71
test/support/market_fixtures.ex
Normal file
71
test/support/market_fixtures.ex
Normal 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
|
||||
139
test/support/repository_code_instrumented_github_client.ex
Normal file
139
test/support/repository_code_instrumented_github_client.ex
Normal 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
|
||||
83
test/support/stripe_stub.ex
Normal file
83
test/support/stripe_stub.ex
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue