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
125
test/tarakan_web/live/account_live/confirmation_test.exs
Normal file
125
test/tarakan_web/live/account_live/confirmation_test.exs
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
defmodule TarakanWeb.AccountLive.ConfirmationTest do
|
||||
use TarakanWeb.ConnCase, async: true
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
import Tarakan.AccountsFixtures
|
||||
|
||||
alias Tarakan.Accounts
|
||||
|
||||
setup do
|
||||
%{unconfirmed_account: unconfirmed_account_fixture(), confirmed_account: account_fixture()}
|
||||
end
|
||||
|
||||
describe "Magic-link login" do
|
||||
test "renders login page for a legacy unconfirmed account", %{
|
||||
conn: conn,
|
||||
unconfirmed_account: account
|
||||
} do
|
||||
token =
|
||||
extract_account_token(fn url ->
|
||||
Accounts.deliver_login_instructions(account, url)
|
||||
end)
|
||||
|
||||
{:ok, _lv, html} = live(conn, ~p"/accounts/log-in/#{token}")
|
||||
refute html =~ "Confirm"
|
||||
assert html =~ "Log in"
|
||||
end
|
||||
|
||||
test "renders login page for confirmed account", %{conn: conn, confirmed_account: account} do
|
||||
token =
|
||||
extract_account_token(fn url ->
|
||||
Accounts.deliver_login_instructions(account, url)
|
||||
end)
|
||||
|
||||
{:ok, _lv, html} = live(conn, ~p"/accounts/log-in/#{token}")
|
||||
assert html =~ "Stay logged in on this device"
|
||||
end
|
||||
|
||||
test "renders login page for already logged in account", %{
|
||||
conn: conn,
|
||||
confirmed_account: account
|
||||
} do
|
||||
conn = log_in_account(conn, account)
|
||||
|
||||
token =
|
||||
extract_account_token(fn url ->
|
||||
Accounts.deliver_login_instructions(account, url)
|
||||
end)
|
||||
|
||||
{:ok, _lv, html} = live(conn, ~p"/accounts/log-in/#{token}")
|
||||
assert html =~ "Log in"
|
||||
end
|
||||
|
||||
test "logs in a legacy unconfirmed account with the given token once", %{
|
||||
conn: conn,
|
||||
unconfirmed_account: account
|
||||
} do
|
||||
token =
|
||||
extract_account_token(fn url ->
|
||||
Accounts.deliver_login_instructions(account, url)
|
||||
end)
|
||||
|
||||
{:ok, lv, _html} = live(conn, ~p"/accounts/log-in/#{token}")
|
||||
|
||||
form = form(lv, "#login_form", %{"account" => %{"token" => token}})
|
||||
render_submit(form)
|
||||
|
||||
conn = follow_trigger_action(form, conn)
|
||||
|
||||
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Welcome back!"
|
||||
|
||||
assert Accounts.get_account!(account.id).confirmed_at
|
||||
# we are logged in now
|
||||
assert get_session(conn, :account_token)
|
||||
assert redirected_to(conn) == ~p"/"
|
||||
|
||||
# log out, new conn
|
||||
conn = build_conn()
|
||||
|
||||
{:ok, _lv, html} =
|
||||
live(conn, ~p"/accounts/log-in/#{token}")
|
||||
|> follow_redirect(conn, ~p"/accounts/log-in")
|
||||
|
||||
assert html =~ "Magic link is invalid or it has expired"
|
||||
end
|
||||
|
||||
test "logs confirmed account in without changing confirmed_at", %{
|
||||
conn: conn,
|
||||
confirmed_account: account
|
||||
} do
|
||||
token =
|
||||
extract_account_token(fn url ->
|
||||
Accounts.deliver_login_instructions(account, url)
|
||||
end)
|
||||
|
||||
{:ok, lv, _html} = live(conn, ~p"/accounts/log-in/#{token}")
|
||||
|
||||
form = form(lv, "#login_form", %{"account" => %{"token" => token}})
|
||||
render_submit(form)
|
||||
|
||||
conn = follow_trigger_action(form, conn)
|
||||
|
||||
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~
|
||||
"Welcome back!"
|
||||
|
||||
assert Accounts.get_account!(account.id).confirmed_at == account.confirmed_at
|
||||
|
||||
# log out, new conn
|
||||
conn = build_conn()
|
||||
|
||||
{:ok, _lv, html} =
|
||||
live(conn, ~p"/accounts/log-in/#{token}")
|
||||
|> follow_redirect(conn, ~p"/accounts/log-in")
|
||||
|
||||
assert html =~ "Magic link is invalid or it has expired"
|
||||
end
|
||||
|
||||
test "raises error for invalid token", %{conn: conn} do
|
||||
{:ok, _lv, html} =
|
||||
live(conn, ~p"/accounts/log-in/invalid-token")
|
||||
|> follow_redirect(conn, ~p"/accounts/log-in")
|
||||
|
||||
assert html =~ "Magic link is invalid or it has expired"
|
||||
end
|
||||
end
|
||||
end
|
||||
134
test/tarakan_web/live/account_live/login_test.exs
Normal file
134
test/tarakan_web/live/account_live/login_test.exs
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
defmodule TarakanWeb.AccountLive.LoginTest do
|
||||
use TarakanWeb.ConnCase, async: true
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
import Tarakan.AccountsFixtures
|
||||
|
||||
describe "login page" do
|
||||
test "renders login page", %{conn: conn} do
|
||||
{:ok, _lv, html} = live(conn, ~p"/accounts/log-in")
|
||||
|
||||
assert html =~ "Log in"
|
||||
assert html =~ "Continue with GitHub"
|
||||
assert html =~ "Continue with GitLab"
|
||||
assert html =~ "Email me a login link"
|
||||
end
|
||||
|
||||
test "preserves a protected route stored in the session", %{conn: conn} do
|
||||
return_to = "/client/authorize/ABCD-EFGH"
|
||||
|
||||
{:ok, view, _html} =
|
||||
conn
|
||||
|> init_test_session(%{account_return_to: return_to})
|
||||
|> live(~p"/accounts/log-in")
|
||||
|
||||
assert has_element?(
|
||||
view,
|
||||
"#login_form_password input[name='account[return_to]'][value='#{return_to}']"
|
||||
)
|
||||
|
||||
assert has_element?(
|
||||
view,
|
||||
"#github-login-button[href='/auth/github?return_to=%2Fclient%2Fauthorize%2FABCD-EFGH']"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe "account login - magic link" do
|
||||
test "sends magic link email when account exists", %{conn: conn} do
|
||||
account = account_fixture()
|
||||
|
||||
{:ok, lv, _html} = live(conn, ~p"/accounts/log-in")
|
||||
|
||||
{:ok, _lv, html} =
|
||||
form(lv, "#login_form_magic", account: %{email: account.email})
|
||||
|> render_submit()
|
||||
|> follow_redirect(conn, ~p"/accounts/log-in")
|
||||
|
||||
assert html =~ "If your email is in our system"
|
||||
|
||||
assert Tarakan.Repo.get_by!(Tarakan.Accounts.AccountToken, account_id: account.id).context ==
|
||||
"login"
|
||||
end
|
||||
|
||||
test "does not disclose if account is registered", %{conn: conn} do
|
||||
{:ok, lv, _html} = live(conn, ~p"/accounts/log-in")
|
||||
|
||||
{:ok, _lv, html} =
|
||||
form(lv, "#login_form_magic", account: %{email: "idonotexist@example.com"})
|
||||
|> render_submit()
|
||||
|> follow_redirect(conn, ~p"/accounts/log-in")
|
||||
|
||||
assert html =~ "If your email is in our system"
|
||||
end
|
||||
end
|
||||
|
||||
describe "account login - password" do
|
||||
test "redirects if account logs in with valid credentials", %{conn: conn} do
|
||||
account = account_fixture() |> set_password()
|
||||
|
||||
{:ok, lv, _html} = live(conn, ~p"/accounts/log-in")
|
||||
|
||||
form =
|
||||
form(lv, "#login_form_password",
|
||||
account: %{
|
||||
identifier: account.handle,
|
||||
password: valid_account_password(),
|
||||
remember_me: true
|
||||
}
|
||||
)
|
||||
|
||||
conn = submit_form(form, conn)
|
||||
|
||||
assert redirected_to(conn) == ~p"/"
|
||||
end
|
||||
|
||||
test "redirects to login page with a flash error if credentials are invalid", %{
|
||||
conn: conn
|
||||
} do
|
||||
{:ok, lv, _html} = live(conn, ~p"/accounts/log-in")
|
||||
|
||||
form =
|
||||
form(lv, "#login_form_password",
|
||||
account: %{identifier: "test@email.com", password: "123456"}
|
||||
)
|
||||
|
||||
render_submit(form)
|
||||
|
||||
conn = follow_trigger_action(form, conn)
|
||||
|
||||
assert Phoenix.Flash.get(conn.assigns.flash, :error) ==
|
||||
"Invalid handle, email, or password"
|
||||
|
||||
assert redirected_to(conn) == ~p"/accounts/log-in"
|
||||
end
|
||||
end
|
||||
|
||||
describe "login navigation" do
|
||||
test "github is the primary path for new users", %{conn: conn} do
|
||||
{:ok, _lv, html} = live(conn, ~p"/accounts/log-in")
|
||||
|
||||
assert html =~ "One click. No password required."
|
||||
assert html =~ ~s(id="github-login-button")
|
||||
assert html =~ "Continue with GitHub"
|
||||
end
|
||||
end
|
||||
|
||||
describe "re-authentication (sudo mode)" do
|
||||
setup %{conn: conn} do
|
||||
account = account_fixture()
|
||||
%{account: account, conn: log_in_account(conn, account)}
|
||||
end
|
||||
|
||||
test "shows login page with email filled in", %{conn: conn, account: account} do
|
||||
{:ok, _lv, html} = live(conn, ~p"/accounts/log-in")
|
||||
|
||||
assert html =~ "Confirm it"
|
||||
assert html =~ "connect another host"
|
||||
assert html =~ "Email me a login link"
|
||||
|
||||
assert html =~
|
||||
~s(<input type="email" name="account[email]" id="login_form_magic_email" value="#{account.email}")
|
||||
end
|
||||
end
|
||||
end
|
||||
109
test/tarakan_web/live/account_live/registration_test.exs
Normal file
109
test/tarakan_web/live/account_live/registration_test.exs
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
defmodule TarakanWeb.AccountLive.RegistrationTest do
|
||||
use TarakanWeb.ConnCase, async: true
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
import Tarakan.AccountsFixtures
|
||||
|
||||
describe "Registration page" do
|
||||
test "renders registration page", %{conn: conn} do
|
||||
{:ok, _lv, html} = live(conn, ~p"/accounts/register")
|
||||
|
||||
assert html =~ "Join Tarakan"
|
||||
assert html =~ "Log in"
|
||||
end
|
||||
|
||||
test "redirects if already logged in", %{conn: conn} do
|
||||
result =
|
||||
conn
|
||||
|> log_in_account(account_fixture())
|
||||
|> live(~p"/accounts/register")
|
||||
|> follow_redirect(conn, ~p"/")
|
||||
|
||||
assert {:ok, _conn} = result
|
||||
end
|
||||
|
||||
test "renders errors for invalid data", %{conn: conn} do
|
||||
{:ok, lv, _html} = live(conn, ~p"/accounts/register")
|
||||
|
||||
result =
|
||||
lv
|
||||
|> element("#registration_form")
|
||||
|> render_change(account: %{"handle" => "valid-handle", "email" => "with spaces"})
|
||||
|
||||
assert result =~ "Join Tarakan"
|
||||
assert result =~ "must have the @ sign and no spaces"
|
||||
end
|
||||
end
|
||||
|
||||
describe "register account" do
|
||||
test "creates an unconfirmed account and logs in immediately", %{conn: conn} do
|
||||
{:ok, lv, _html} = live(conn, ~p"/accounts/register")
|
||||
|
||||
email = unique_account_email()
|
||||
form = form(lv, "#registration_form", account: valid_account_attributes(email: email))
|
||||
|
||||
render_submit(form)
|
||||
|
||||
login_form = form(lv, "#registration_login_form")
|
||||
conn = follow_trigger_action(login_form, conn)
|
||||
|
||||
assert get_session(conn, :account_token)
|
||||
assert redirected_to(conn) == ~p"/"
|
||||
|
||||
# The instant session does not confirm the email: only the emailed login
|
||||
# link proves mailbox control.
|
||||
assert %{confirmed_at: nil} = Tarakan.Accounts.get_account_by_email(email)
|
||||
end
|
||||
|
||||
test "does not reveal that an email is already registered", %{conn: conn} do
|
||||
{:ok, lv, _html} = live(conn, ~p"/accounts/register")
|
||||
|
||||
account = account_fixture(%{email: "test@email.com"})
|
||||
|
||||
{:ok, _lv, html} =
|
||||
lv
|
||||
|> form("#registration_form",
|
||||
account: %{"handle" => unique_account_handle(), "email" => account.email}
|
||||
)
|
||||
|> render_submit()
|
||||
|> follow_redirect(conn, ~p"/accounts/log-in")
|
||||
|
||||
assert html =~ "If an account matches those details, a sign-in link will arrive shortly."
|
||||
refute html =~ "has already been taken"
|
||||
refute html =~ account.email
|
||||
end
|
||||
|
||||
test "does not reveal that a handle is already registered", %{conn: conn} do
|
||||
{:ok, lv, _html} = live(conn, ~p"/accounts/register")
|
||||
|
||||
account = account_fixture()
|
||||
email = unique_account_email()
|
||||
|
||||
{:ok, _lv, html} =
|
||||
lv
|
||||
|> form("#registration_form",
|
||||
account: %{"handle" => account.handle, "email" => email}
|
||||
)
|
||||
|> render_submit()
|
||||
|> follow_redirect(conn, ~p"/accounts/log-in")
|
||||
|
||||
assert html =~ "If an account matches those details, a sign-in link will arrive shortly."
|
||||
refute html =~ "has already been taken"
|
||||
refute Tarakan.Accounts.get_account_by_email(email)
|
||||
end
|
||||
end
|
||||
|
||||
describe "registration navigation" do
|
||||
test "redirects to login page when the Log in button is clicked", %{conn: conn} do
|
||||
{:ok, lv, _html} = live(conn, ~p"/accounts/register")
|
||||
|
||||
{:ok, _login_live, login_html} =
|
||||
lv
|
||||
|> element("main a", "Log in")
|
||||
|> render_click()
|
||||
|> follow_redirect(conn, ~p"/accounts/log-in")
|
||||
|
||||
assert login_html =~ "Log in"
|
||||
end
|
||||
end
|
||||
end
|
||||
434
test/tarakan_web/live/account_live/settings_test.exs
Normal file
434
test/tarakan_web/live/account_live/settings_test.exs
Normal file
|
|
@ -0,0 +1,434 @@
|
|||
defmodule TarakanWeb.AccountLive.SettingsTest do
|
||||
use TarakanWeb.ConnCase, async: true
|
||||
|
||||
alias Tarakan.Accounts
|
||||
import Phoenix.LiveViewTest
|
||||
import Tarakan.AccountsFixtures
|
||||
|
||||
describe "Settings page" do
|
||||
test "opens on the account panel", %{conn: conn} do
|
||||
{:ok, view, html} =
|
||||
conn
|
||||
|> log_in_account(account_fixture())
|
||||
|> live(~p"/accounts/settings")
|
||||
|
||||
assert html =~ "Save email"
|
||||
assert html =~ "Save password"
|
||||
assert html =~ "Connected code hosts"
|
||||
assert html =~ "GitHub"
|
||||
assert html =~ "GitLab"
|
||||
|
||||
assert has_element?(view, "#settings-tab-account[aria-current=page]")
|
||||
refute has_element?(view, "#api-reference")
|
||||
refute has_element?(view, "#ssh-key-form")
|
||||
end
|
||||
|
||||
test "moves between panels from the rail", %{conn: conn} do
|
||||
{:ok, view, _html} =
|
||||
conn
|
||||
|> log_in_account(account_fixture())
|
||||
|> live(~p"/accounts/settings")
|
||||
|
||||
html =
|
||||
view
|
||||
|> element("#settings-tab-credentials")
|
||||
|> render_click()
|
||||
|
||||
assert html =~ "Client credentials"
|
||||
assert_patch(view, ~p"/accounts/settings/credentials")
|
||||
assert has_element?(view, "#settings-tab-credentials[aria-current=page]")
|
||||
refute has_element?(view, "#email_form")
|
||||
|
||||
view |> element("#settings-tab-ssh") |> render_click()
|
||||
assert_patch(view, ~p"/accounts/settings/ssh-keys")
|
||||
assert has_element?(view, "#ssh-key-form")
|
||||
|
||||
view |> element("#settings-tab-agents") |> render_click()
|
||||
assert_patch(view, ~p"/accounts/settings/agents")
|
||||
assert has_element?(view, "#settings-model-scoreboard", "None yet")
|
||||
end
|
||||
|
||||
test "an unknown panel slug falls back to the account panel", %{conn: conn} do
|
||||
{:ok, view, _html} =
|
||||
conn
|
||||
|> log_in_account(account_fixture())
|
||||
|> live(~p"/accounts/settings/nonsense")
|
||||
|
||||
assert has_element?(view, "#email_form")
|
||||
assert has_element?(view, "#settings-tab-account[aria-current=page]")
|
||||
end
|
||||
|
||||
test "renders the API reference on the credentials panel", %{conn: conn} do
|
||||
{:ok, view, html} =
|
||||
conn
|
||||
|> log_in_account(account_fixture())
|
||||
|> live(~p"/accounts/settings/credentials")
|
||||
|
||||
assert has_element?(view, "#api-reference", "API reference")
|
||||
assert has_element?(view, "#api-reference-base-url", "/api")
|
||||
assert html =~ "/client-auth/exchange"
|
||||
assert html =~ "/repositories"
|
||||
assert html =~ "/jobs/:id/claim/renew"
|
||||
assert html =~ "/:host/:owner/:name/reports"
|
||||
end
|
||||
|
||||
test "marks an attached code-host identity as connected", %{conn: conn} do
|
||||
{:ok, view, _html} =
|
||||
conn
|
||||
|> log_in_account(github_account_fixture())
|
||||
|> live(~p"/accounts/settings")
|
||||
|
||||
assert has_element?(view, "#settings-github-identity", "Connected")
|
||||
assert has_element?(view, "#settings-gitlab-identity", "Connect")
|
||||
end
|
||||
|
||||
test "redirects if account is not logged in", %{conn: conn} do
|
||||
assert {:error, redirect} = live(conn, ~p"/accounts/settings")
|
||||
|
||||
assert {:redirect, %{to: path, flash: flash}} = redirect
|
||||
assert path == ~p"/accounts/log-in"
|
||||
assert %{"error" => "You must log in to access this page."} = flash
|
||||
end
|
||||
|
||||
test "generates and independently revokes a client credential", %{conn: conn} do
|
||||
account = account_fixture()
|
||||
|
||||
{:ok, view, html} =
|
||||
conn
|
||||
|> log_in_account(account)
|
||||
|> live(~p"/accounts/settings/credentials")
|
||||
|
||||
assert html =~ "Client credentials"
|
||||
refute has_element?(view, "#api-token-value")
|
||||
|
||||
view
|
||||
|> form("#api-credential-form", %{
|
||||
"credential" => %{
|
||||
"name" => "Task reader",
|
||||
"scopes" => ["tasks:read"]
|
||||
}
|
||||
})
|
||||
|> render_submit()
|
||||
|
||||
token =
|
||||
view
|
||||
|> element("#api-token-value")
|
||||
|> render()
|
||||
|> String.replace(~r/<[^>]+>/, "")
|
||||
|> String.trim()
|
||||
|
||||
assert {:ok, fetched} = Accounts.fetch_account_by_api_token(token)
|
||||
assert fetched.id == account.id
|
||||
|
||||
[credential] = Tarakan.Accounts.ApiCredentials.list(account)
|
||||
assert credential.name == "Task reader"
|
||||
assert credential.scopes == ["tasks:read"]
|
||||
assert has_element?(view, "#api-credential-#{credential.id}")
|
||||
|
||||
view
|
||||
|> element("#revoke-api-credential-#{credential.id}")
|
||||
|> render_click()
|
||||
|
||||
assert :error = Accounts.fetch_account_by_api_token(token)
|
||||
refute has_element?(view, "#revoke-api-credential-#{credential.id}")
|
||||
end
|
||||
|
||||
test "can bind a least-privilege credential to one repository", %{conn: conn} do
|
||||
account = github_account_fixture()
|
||||
repository = github_repository_fixture(account)
|
||||
|
||||
{:ok, view, _html} =
|
||||
conn
|
||||
|> log_in_account(account)
|
||||
|> live(~p"/accounts/settings/credentials")
|
||||
|
||||
view
|
||||
|> form("#api-credential-form", %{
|
||||
"credential" => %{
|
||||
"name" => "Codex reader",
|
||||
"repository" => "openai/codex",
|
||||
"scopes" => ["tasks:read"]
|
||||
}
|
||||
})
|
||||
|> render_submit()
|
||||
|
||||
token =
|
||||
view
|
||||
|> element("#api-token-value")
|
||||
|> render()
|
||||
|> String.replace(~r/<[^>]+>/, "")
|
||||
|> String.trim()
|
||||
|
||||
assert {:ok, ^account, credential} =
|
||||
Tarakan.Accounts.ApiCredentials.authenticate(token)
|
||||
|
||||
assert credential.repository_id == repository.id
|
||||
assert credential.scopes == ["tasks:read"]
|
||||
end
|
||||
|
||||
test "redirects if account is not in sudo mode", %{conn: conn} do
|
||||
stale_at = DateTime.add(DateTime.utc_now(:second), -9 * 60, :minute)
|
||||
|
||||
{:ok, conn} =
|
||||
conn
|
||||
|> log_in_account(account_fixture(), token_authenticated_at: stale_at)
|
||||
|> live(~p"/accounts/settings")
|
||||
|> follow_redirect(conn, ~p"/accounts/log-in?return_to=%2Faccounts%2Fsettings")
|
||||
|
||||
assert conn.resp_body =~ "sign-in older than 2 hours for sensitive settings"
|
||||
end
|
||||
|
||||
test "prompts unconfirmed accounts to confirm email instead of credential forms", %{
|
||||
conn: conn
|
||||
} do
|
||||
conn = log_in_account(conn, unconfirmed_account_fixture())
|
||||
|
||||
{:ok, view, html} = live(conn, ~p"/accounts/settings")
|
||||
|
||||
assert has_element?(view, "#email-confirmation-required")
|
||||
assert has_element?(view, "#settings-tab-account", "unconfirmed")
|
||||
assert html =~ "Confirm your email to unlock credentials"
|
||||
refute has_element?(view, "#password_form")
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/accounts/settings/credentials")
|
||||
refute has_element?(view, "#api-credential-form")
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/accounts/settings/ssh-keys")
|
||||
refute has_element?(view, "#ssh-key-form")
|
||||
end
|
||||
end
|
||||
|
||||
describe "SSH keys" do
|
||||
@describetag :tmp_dir
|
||||
|
||||
defp generate_public_key(tmp_dir) do
|
||||
path = Path.join(tmp_dir, "key-#{System.unique_integer([:positive])}")
|
||||
{_output, 0} = System.cmd("ssh-keygen", ["-t", "ed25519", "-f", path, "-N", "", "-q"])
|
||||
File.read!(path <> ".pub")
|
||||
end
|
||||
|
||||
test "adds and removes a key", %{conn: conn, tmp_dir: tmp_dir} do
|
||||
account = account_fixture()
|
||||
|
||||
{:ok, view, html} =
|
||||
conn
|
||||
|> log_in_account(account)
|
||||
|> live(~p"/accounts/settings/ssh-keys")
|
||||
|
||||
assert html =~ "SSH keys"
|
||||
|
||||
view
|
||||
|> form("#ssh-key-form",
|
||||
ssh_key: %{name: "laptop", public_key: generate_public_key(tmp_dir)}
|
||||
)
|
||||
|> render_submit()
|
||||
|
||||
assert has_element?(view, "#ssh-key-list", "laptop")
|
||||
assert has_element?(view, "#ssh-key-list", "SHA256:")
|
||||
|
||||
[key] = Tarakan.Accounts.SshKeys.list_for_account(account)
|
||||
|
||||
view
|
||||
|> element("#delete-ssh-key-#{key.id}")
|
||||
|> render_click()
|
||||
|
||||
refute has_element?(view, "#ssh-key-list")
|
||||
assert Tarakan.Accounts.SshKeys.list_for_account(account) == []
|
||||
end
|
||||
|
||||
test "rejects an invalid key inline", %{conn: conn} do
|
||||
{:ok, view, _html} =
|
||||
conn
|
||||
|> log_in_account(account_fixture())
|
||||
|> live(~p"/accounts/settings/ssh-keys")
|
||||
|
||||
html =
|
||||
view
|
||||
|> form("#ssh-key-form", ssh_key: %{name: "junk", public_key: "not a key"})
|
||||
|> render_submit()
|
||||
|
||||
assert html =~ "is not a supported OpenSSH public key"
|
||||
end
|
||||
end
|
||||
|
||||
describe "update email form" do
|
||||
setup %{conn: conn} do
|
||||
account = account_fixture()
|
||||
%{conn: log_in_account(conn, account), account: account}
|
||||
end
|
||||
|
||||
test "updates the account email", %{conn: conn, account: account} do
|
||||
new_email = unique_account_email()
|
||||
|
||||
{:ok, lv, _html} = live(conn, ~p"/accounts/settings")
|
||||
|
||||
result =
|
||||
lv
|
||||
|> form("#email_form", %{
|
||||
"account" => %{"email" => new_email}
|
||||
})
|
||||
|> render_submit()
|
||||
|
||||
assert result =~ "A link to confirm your email"
|
||||
assert Accounts.get_account_by_email(account.email)
|
||||
end
|
||||
|
||||
test "renders errors with invalid data (phx-change)", %{conn: conn} do
|
||||
{:ok, lv, _html} = live(conn, ~p"/accounts/settings")
|
||||
|
||||
result =
|
||||
lv
|
||||
|> element("#email_form")
|
||||
|> render_change(%{
|
||||
"action" => "update_email",
|
||||
"account" => %{"email" => "with spaces"}
|
||||
})
|
||||
|
||||
assert result =~ "Save email"
|
||||
assert result =~ "must have the @ sign and no spaces"
|
||||
end
|
||||
|
||||
test "renders errors with invalid data (phx-submit)", %{conn: conn, account: account} do
|
||||
{:ok, lv, _html} = live(conn, ~p"/accounts/settings")
|
||||
|
||||
result =
|
||||
lv
|
||||
|> form("#email_form", %{
|
||||
"account" => %{"email" => account.email}
|
||||
})
|
||||
|> render_submit()
|
||||
|
||||
assert result =~ "Save email"
|
||||
assert result =~ "did not change"
|
||||
end
|
||||
end
|
||||
|
||||
describe "update password form" do
|
||||
setup %{conn: conn} do
|
||||
account = account_fixture()
|
||||
%{conn: log_in_account(conn, account), account: account}
|
||||
end
|
||||
|
||||
test "updates the account password", %{conn: conn, account: account} do
|
||||
new_password = valid_account_password()
|
||||
|
||||
{:ok, lv, _html} = live(conn, ~p"/accounts/settings")
|
||||
|
||||
form =
|
||||
form(lv, "#password_form", %{
|
||||
"account" => %{
|
||||
"password" => new_password,
|
||||
"password_confirmation" => new_password
|
||||
}
|
||||
})
|
||||
|
||||
render_submit(form)
|
||||
|
||||
new_password_conn = follow_trigger_action(form, conn)
|
||||
|
||||
assert redirected_to(new_password_conn) == ~p"/accounts/settings"
|
||||
|
||||
assert get_session(new_password_conn, :account_token) != get_session(conn, :account_token)
|
||||
|
||||
assert Phoenix.Flash.get(new_password_conn.assigns.flash, :info) =~
|
||||
"Password updated successfully"
|
||||
|
||||
assert Accounts.get_account_by_email_and_password(account.email, new_password)
|
||||
end
|
||||
|
||||
test "renders errors with invalid data (phx-change)", %{conn: conn} do
|
||||
{:ok, lv, _html} = live(conn, ~p"/accounts/settings")
|
||||
|
||||
result =
|
||||
lv
|
||||
|> element("#password_form")
|
||||
|> render_change(%{
|
||||
"account" => %{
|
||||
"password" => "too short",
|
||||
"password_confirmation" => "does not match"
|
||||
}
|
||||
})
|
||||
|
||||
assert result =~ "Save password"
|
||||
assert result =~ "should be at least 15 character(s)"
|
||||
assert result =~ "does not match password"
|
||||
end
|
||||
|
||||
test "renders errors with invalid data (phx-submit)", %{conn: conn} do
|
||||
{:ok, lv, _html} = live(conn, ~p"/accounts/settings")
|
||||
|
||||
result =
|
||||
lv
|
||||
|> form("#password_form", %{
|
||||
"account" => %{
|
||||
"password" => "too short",
|
||||
"password_confirmation" => "does not match"
|
||||
}
|
||||
})
|
||||
|> render_submit()
|
||||
|
||||
assert result =~ "Save password"
|
||||
assert result =~ "should be at least 15 character(s)"
|
||||
assert result =~ "does not match password"
|
||||
end
|
||||
end
|
||||
|
||||
describe "confirm email" do
|
||||
setup %{conn: conn} do
|
||||
account = account_fixture()
|
||||
email = unique_account_email()
|
||||
|
||||
token =
|
||||
extract_account_token(fn url ->
|
||||
Accounts.deliver_account_update_email_instructions(
|
||||
%{account | email: email},
|
||||
account.email,
|
||||
url
|
||||
)
|
||||
end)
|
||||
|
||||
%{conn: log_in_account(conn, account), token: token, email: email, account: account}
|
||||
end
|
||||
|
||||
test "updates the account email once", %{
|
||||
conn: conn,
|
||||
account: account,
|
||||
token: token,
|
||||
email: email
|
||||
} do
|
||||
{:error, redirect} = live(conn, ~p"/accounts/settings/confirm-email/#{token}")
|
||||
|
||||
assert {:live_redirect, %{to: path, flash: flash}} = redirect
|
||||
assert path == ~p"/accounts/settings"
|
||||
assert %{"info" => message} = flash
|
||||
assert message == "Email changed successfully."
|
||||
refute Accounts.get_account_by_email(account.email)
|
||||
assert Accounts.get_account_by_email(email)
|
||||
|
||||
# use confirm token again
|
||||
{:error, redirect} = live(conn, ~p"/accounts/settings/confirm-email/#{token}")
|
||||
assert {:live_redirect, %{to: path, flash: flash}} = redirect
|
||||
assert path == ~p"/accounts/settings"
|
||||
assert %{"error" => message} = flash
|
||||
assert message == "Email change link is invalid or it has expired."
|
||||
end
|
||||
|
||||
test "does not update email with invalid token", %{conn: conn, account: account} do
|
||||
{:error, redirect} = live(conn, ~p"/accounts/settings/confirm-email/oops")
|
||||
assert {:live_redirect, %{to: path, flash: flash}} = redirect
|
||||
assert path == ~p"/accounts/settings"
|
||||
assert %{"error" => message} = flash
|
||||
assert message == "Email change link is invalid or it has expired."
|
||||
assert Accounts.get_account_by_email(account.email)
|
||||
end
|
||||
|
||||
test "redirects if account is not logged in", %{token: token} do
|
||||
conn = build_conn()
|
||||
{:error, redirect} = live(conn, ~p"/accounts/settings/confirm-email/#{token}")
|
||||
assert {:redirect, %{to: path, flash: flash}} = redirect
|
||||
assert path == ~p"/accounts/log-in"
|
||||
assert %{"error" => message} = flash
|
||||
assert message == "You must log in to access this page."
|
||||
end
|
||||
end
|
||||
end
|
||||
134
test/tarakan_web/live/admin_live_test.exs
Normal file
134
test/tarakan_web/live/admin_live_test.exs
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
defmodule TarakanWeb.AdminLiveTest do
|
||||
use TarakanWeb.ConnCase, async: true
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Tarakan.Accounts
|
||||
alias Tarakan.Accounts.Account
|
||||
alias Tarakan.Repo
|
||||
|
||||
setup %{conn: conn} do
|
||||
admin =
|
||||
account_fixture()
|
||||
|> Account.authorization_changeset(%{
|
||||
state: "active",
|
||||
platform_role: "admin",
|
||||
trust_tier: "reviewer"
|
||||
})
|
||||
|> Repo.update!()
|
||||
|
||||
%{admin: admin, conn: log_in_account(conn, admin)}
|
||||
end
|
||||
|
||||
test "admin sees account summary, search, and management links", %{conn: conn} do
|
||||
target = account_fixture(%{handle: "managed-account"})
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/admin")
|
||||
|
||||
assert has_element?(view, "#admin-dashboard")
|
||||
assert has_element?(view, "#admin-summary-admins")
|
||||
assert has_element?(view, "#admin-account-filter")
|
||||
assert has_element?(view, "#admin-account-#{target.id}-manage")
|
||||
|
||||
view
|
||||
|> form("#admin-account-filter", filters: %{query: "managed-account"})
|
||||
|> render_change()
|
||||
|
||||
assert has_element?(view, "#admin-account-#{target.id}-manage")
|
||||
end
|
||||
|
||||
test "admin updates account standing, role, and trust tier", %{conn: conn} do
|
||||
target = account_fixture()
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/accounts/#{target.id}")
|
||||
|
||||
assert has_element?(view, "#admin-account")
|
||||
assert has_element?(view, "#admin-authorization-form")
|
||||
|
||||
view
|
||||
|> form("#admin-authorization-form",
|
||||
authorization: %{
|
||||
state: "active",
|
||||
platform_role: "moderator",
|
||||
trust_tier: "reviewer"
|
||||
}
|
||||
)
|
||||
|> render_submit()
|
||||
|
||||
updated = Accounts.get_account!(target.id)
|
||||
assert updated.state == "active"
|
||||
assert updated.platform_role == "moderator"
|
||||
assert updated.trust_tier == "reviewer"
|
||||
assert has_element?(view, "#admin-authorization-form")
|
||||
end
|
||||
|
||||
test "ordinary members cannot open platform administration", %{conn: _admin_conn} do
|
||||
member =
|
||||
account_fixture() |> Account.authorization_changeset(%{state: "active"}) |> Repo.update!()
|
||||
|
||||
conn = log_in_account(build_conn(), member)
|
||||
|
||||
assert {:error, {:redirect, %{to: path, flash: flash}}} = live(conn, ~p"/admin")
|
||||
assert path == ~p"/"
|
||||
assert flash["error"] == "Administrator access is required."
|
||||
end
|
||||
|
||||
test "last administrator cannot remove their own authority", %{conn: conn, admin: admin} do
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/accounts/#{admin.id}")
|
||||
|
||||
view
|
||||
|> form("#admin-authorization-form",
|
||||
authorization: %{
|
||||
state: "active",
|
||||
platform_role: "member",
|
||||
trust_tier: "reviewer"
|
||||
}
|
||||
)
|
||||
|> render_submit()
|
||||
|
||||
assert Accounts.get_account!(admin.id).platform_role == "admin"
|
||||
assert has_element?(view, "#admin-authorization-form")
|
||||
end
|
||||
|
||||
test "filtering requires a still-valid sudo session", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/admin")
|
||||
|
||||
expire_sudo_mode(view)
|
||||
|
||||
assert {:error, {:live_redirect, %{to: "/accounts/log-in?return_to=%2Fadmin"}}} =
|
||||
view
|
||||
|> form("#admin-account-filter", filters: %{query: "anything"})
|
||||
|> render_change()
|
||||
end
|
||||
|
||||
test "saving requires a still-valid sudo session", %{conn: conn} do
|
||||
target = account_fixture()
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/accounts/#{target.id}")
|
||||
|
||||
expire_sudo_mode(view)
|
||||
|
||||
assert {:error,
|
||||
{:live_redirect, %{to: "/accounts/log-in?return_to=%2Fadmin%2Faccounts%2F" <> _}}} =
|
||||
view
|
||||
|> form("#admin-authorization-form",
|
||||
authorization: %{
|
||||
state: "active",
|
||||
platform_role: "moderator",
|
||||
trust_tier: "reviewer"
|
||||
}
|
||||
)
|
||||
|> render_submit()
|
||||
|
||||
assert Accounts.get_account!(target.id).platform_role == target.platform_role
|
||||
end
|
||||
|
||||
# Simulates the two-hour sudo window expiring while the LiveView stays open.
|
||||
defp expire_sudo_mode(view) do
|
||||
stale = DateTime.add(DateTime.utc_now(), -3, :hour)
|
||||
|
||||
:sys.replace_state(view.pid, fn state ->
|
||||
account = %{state.socket.assigns.current_scope.account | authenticated_at: stale}
|
||||
put_in(state.socket.assigns.current_scope.account, account)
|
||||
end)
|
||||
end
|
||||
end
|
||||
34
test/tarakan_web/live/agents_live_test.exs
Normal file
34
test/tarakan_web/live/agents_live_test.exs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
defmodule TarakanWeb.AgentsLiveTest do
|
||||
use TarakanWeb.ConnCase, async: true
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
test "install, publish Reports, pick up Jobs", %{conn: conn} do
|
||||
{:ok, view, html} = live(conn, ~p"/agents")
|
||||
|
||||
assert has_element?(view, "#agents-commands")
|
||||
assert has_element?(view, "#agents-dump-commands")
|
||||
assert has_element?(view, "#agents-pickup-commands")
|
||||
assert has_element?(view, "#agents-report-api")
|
||||
|
||||
assert html =~ "/install.sh | bash"
|
||||
assert html =~ "tarakan login"
|
||||
assert html =~ "tarakan worker --agent kimi"
|
||||
assert html =~ "tarakan --agent kimi --pickup"
|
||||
assert html =~ "POST "
|
||||
assert html =~ "/reports"
|
||||
assert html =~ "Report"
|
||||
assert html =~ "Check"
|
||||
assert html =~ "Job"
|
||||
refute html =~ "tarakan login --url"
|
||||
end
|
||||
|
||||
test "install.sh covers worker and pickup", %{conn: conn} do
|
||||
conn = get(conn, "/install.sh")
|
||||
assert conn.status == 200
|
||||
body = response(conn, 200)
|
||||
assert body =~ "tarakan login"
|
||||
assert body =~ "tarakan worker --agent kimi"
|
||||
assert body =~ "tarakan --agent kimi --pickup"
|
||||
end
|
||||
end
|
||||
79
test/tarakan_web/live/alert_live_test.exs
Normal file
79
test/tarakan_web/live/alert_live_test.exs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
defmodule TarakanWeb.AlertLiveTest do
|
||||
use TarakanWeb.ConnCase, async: true
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Tarakan.Billing
|
||||
alias Tarakan.Repo
|
||||
|
||||
test "redirects anonymous visitors to login", %{conn: conn} do
|
||||
assert {:error, {:redirect, %{to: "/accounts/log-in"}}} = live(conn, ~p"/alerts")
|
||||
end
|
||||
|
||||
test "creates, edits, toggles, and deletes a watchlist", %{conn: conn} do
|
||||
account = account_fixture()
|
||||
{:ok, view, _html} = conn |> log_in_account(account) |> live(~p"/alerts")
|
||||
|
||||
assert has_element?(view, "#watchlists")
|
||||
assert has_element?(view, "#watchlist-form")
|
||||
|
||||
# Create
|
||||
view
|
||||
|> form("#watchlist-form", watchlist: %{name: "Deps", entries: "openai/codex\nacme/widget"})
|
||||
|> render_submit()
|
||||
|
||||
[watchlist] = Billing.list_watchlists(account)
|
||||
assert watchlist.name == "Deps"
|
||||
assert watchlist.entries == ["openai/codex", "acme/widget"]
|
||||
assert has_element?(view, "#watchlist-#{watchlist.id}")
|
||||
assert has_element?(view, "#watchlist-#{watchlist.id}", "openai/codex")
|
||||
|
||||
# Toggle notify off
|
||||
view |> element("#watchlist-notify-#{watchlist.id}") |> render_click()
|
||||
refute Repo.get!(Billing.Watchlist, watchlist.id).notify
|
||||
|
||||
# Edit
|
||||
view |> element("#watchlist-edit-#{watchlist.id}") |> render_click()
|
||||
|
||||
view
|
||||
|> form("#watchlist-form", watchlist: %{name: "Deps v2", entries: "openai/codex"})
|
||||
|> render_submit()
|
||||
|
||||
updated = Repo.get!(Billing.Watchlist, watchlist.id)
|
||||
assert updated.name == "Deps v2"
|
||||
assert updated.entries == ["openai/codex"]
|
||||
assert has_element?(view, "#watchlist-#{watchlist.id}", "Deps v2")
|
||||
|
||||
# Delete
|
||||
view |> element("#watchlist-delete-#{watchlist.id}") |> render_click()
|
||||
refute has_element?(view, "#watchlist-#{watchlist.id}")
|
||||
assert Billing.list_watchlists(account) == []
|
||||
end
|
||||
|
||||
test "invalid entries show an error and do not persist", %{conn: conn} do
|
||||
account = account_fixture()
|
||||
{:ok, view, _html} = conn |> log_in_account(account) |> live(~p"/alerts")
|
||||
|
||||
html =
|
||||
view
|
||||
|> form("#watchlist-form", watchlist: %{name: "Bad", entries: "not a ref"})
|
||||
|> render_submit()
|
||||
|
||||
assert html =~ "owner/name"
|
||||
assert Billing.list_watchlists(account) == []
|
||||
end
|
||||
|
||||
test "watchlists are owner-scoped", %{conn: conn} do
|
||||
owner = account_fixture()
|
||||
|
||||
{:ok, _} =
|
||||
Billing.create_watchlist(Tarakan.Accounts.Scope.for_account(owner), %{
|
||||
"name" => "Private",
|
||||
"entries" => ["openai/codex"]
|
||||
})
|
||||
|
||||
{:ok, view, _html} = conn |> log_in_account(account_fixture()) |> live(~p"/alerts")
|
||||
|
||||
refute has_element?(view, "#watchlists", "Private")
|
||||
end
|
||||
end
|
||||
88
test/tarakan_web/live/billing_live_test.exs
Normal file
88
test/tarakan_web/live/billing_live_test.exs
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
defmodule TarakanWeb.BillingLiveTest do
|
||||
use TarakanWeb.ConnCase, async: true
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Tarakan.Billing.Subscription
|
||||
alias Tarakan.Repo
|
||||
|
||||
defp subscription_fixture(account, overrides) do
|
||||
attrs =
|
||||
Map.merge(
|
||||
%{
|
||||
account_id: account.id,
|
||||
plan: "enterprise",
|
||||
status: "active",
|
||||
stripe_customer_id: "cus_lv",
|
||||
stripe_subscription_id: "sub_lv_#{account.id}",
|
||||
current_period_end: DateTime.utc_now()
|
||||
},
|
||||
overrides
|
||||
)
|
||||
|
||||
%Subscription{}
|
||||
|> Subscription.changeset(attrs)
|
||||
|> Repo.insert!()
|
||||
end
|
||||
|
||||
test "redirects anonymous visitors to login", %{conn: conn} do
|
||||
assert {:error, {:redirect, %{to: "/accounts/log-in"}}} = live(conn, ~p"/accounts/billing")
|
||||
end
|
||||
|
||||
test "shows the unsubscribed state with the managed-disclosure button", %{conn: conn} do
|
||||
{:ok, view, _html} = conn |> log_in_account(account_fixture()) |> live(~p"/accounts/billing")
|
||||
|
||||
assert has_element?(view, "#billing-current")
|
||||
assert has_element?(view, "#billing-plan", "None")
|
||||
assert has_element?(view, "#billing-subscribe-enterprise")
|
||||
refute has_element?(view, "#billing-manage-button")
|
||||
end
|
||||
|
||||
test "shows the active plan, status, period end, and manage button", %{conn: conn} do
|
||||
account = account_fixture()
|
||||
subscription_fixture(account, %{})
|
||||
|
||||
{:ok, view, _html} = conn |> log_in_account(account) |> live(~p"/accounts/billing")
|
||||
|
||||
assert has_element?(view, "#billing-plan", "Managed disclosure")
|
||||
assert has_element?(view, "#billing-status", "Active")
|
||||
assert has_element?(view, "#billing-manage-button")
|
||||
refute has_element?(view, "#billing-subscribe-enterprise")
|
||||
end
|
||||
|
||||
test "renders past_due and canceled states", %{conn: conn} do
|
||||
past_due = account_fixture()
|
||||
subscription_fixture(past_due, %{status: "past_due"})
|
||||
|
||||
{:ok, view, _html} = conn |> log_in_account(past_due) |> live(~p"/accounts/billing")
|
||||
assert has_element?(view, "#billing-status", "Past due")
|
||||
|
||||
canceled = account_fixture()
|
||||
subscription_fixture(canceled, %{status: "canceled"})
|
||||
|
||||
{:ok, view, _html} = conn |> log_in_account(canceled) |> live(~p"/accounts/billing")
|
||||
assert has_element?(view, "#billing-status", "Canceled")
|
||||
assert has_element?(view, "#billing-subscribe-enterprise")
|
||||
end
|
||||
|
||||
test "subscribe redirects to Stripe checkout", %{conn: conn} do
|
||||
{:ok, view, _html} = conn |> log_in_account(account_fixture()) |> live(~p"/accounts/billing")
|
||||
|
||||
assert {:error, {:redirect, %{to: checkout_url}}} =
|
||||
view |> element("#billing-subscribe-enterprise") |> render_click()
|
||||
|
||||
assert checkout_url =~ "https://checkout.stripe.com/"
|
||||
end
|
||||
|
||||
test "manage redirects to the Stripe billing portal", %{conn: conn} do
|
||||
account = account_fixture()
|
||||
subscription_fixture(account, %{})
|
||||
|
||||
{:ok, view, _html} = conn |> log_in_account(account) |> live(~p"/accounts/billing")
|
||||
|
||||
assert {:error, {:redirect, %{to: portal_url}}} =
|
||||
view |> element("#billing-manage-button") |> render_click()
|
||||
|
||||
assert portal_url =~ "https://billing.stripe.com/"
|
||||
end
|
||||
end
|
||||
155
test/tarakan_web/live/bounty_live_test.exs
Normal file
155
test/tarakan_web/live/bounty_live_test.exs
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
defmodule TarakanWeb.BountyLiveTest do
|
||||
use TarakanWeb.ConnCase, async: true
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Tarakan.Accounts.Scope
|
||||
alias Tarakan.Market
|
||||
alias Tarakan.Market.Bounty
|
||||
alias Tarakan.Repo
|
||||
|
||||
defp open_bounty do
|
||||
repository = listed_github_repository_fixture()
|
||||
sponsor = active_account_fixture() |> fund_credits(1_000)
|
||||
|
||||
bounty = credits_bounty_fixture(Scope.for_account(sponsor), repository)
|
||||
{bounty, sponsor, repository}
|
||||
end
|
||||
|
||||
describe "index" do
|
||||
test "renders open bounties for anonymous visitors", %{conn: conn} do
|
||||
{bounty, sponsor, _repository} = open_bounty()
|
||||
|
||||
{:ok, view, html} = live(conn, ~p"/bounties")
|
||||
|
||||
assert html =~ "Contracts"
|
||||
assert has_element?(view, "#bounties")
|
||||
assert has_element?(view, "#bounty-#{bounty.public_id}")
|
||||
assert html =~ bounty.title
|
||||
assert html =~ "500 credits"
|
||||
assert html =~ "@#{sponsor.handle}"
|
||||
assert has_element?(view, "#bounty-filters")
|
||||
end
|
||||
|
||||
test "shows the empty state when nothing is open", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/bounties")
|
||||
|
||||
assert has_element?(view, "#bounties-empty")
|
||||
end
|
||||
end
|
||||
|
||||
describe "new" do
|
||||
test "redirects anonymous visitors to login", %{conn: conn} do
|
||||
assert {:error, {:redirect, %{to: "/accounts/log-in"}}} = live(conn, ~p"/bounties/new")
|
||||
end
|
||||
|
||||
test "creates a credits bounty and lands on the poster", %{conn: conn} do
|
||||
repository = listed_github_repository_fixture()
|
||||
sponsor = active_account_fixture() |> fund_credits(1_000)
|
||||
conn = log_in_account(conn, sponsor)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/bounties/new")
|
||||
|
||||
assert has_element?(view, "#bounty-form")
|
||||
assert has_element?(view, "#bounty-credit-balance")
|
||||
|
||||
# The target is picked from search, not typed.
|
||||
view
|
||||
|> form("#bounty-form", %{"bounty" => %{"repo_query" => repository.name}})
|
||||
|> render_change()
|
||||
|
||||
assert view
|
||||
|> element("#bounty-repo-results button", "#{repository.owner}/#{repository.name}")
|
||||
|> render_click()
|
||||
|
||||
assert has_element?(
|
||||
view,
|
||||
"#bounty-selected-repository",
|
||||
"#{repository.host}/#{repository.owner}/#{repository.name}"
|
||||
)
|
||||
|
||||
{:ok, _show_view, html} =
|
||||
view
|
||||
|> form("#bounty-form", %{
|
||||
"bounty" => %{
|
||||
"target_type" => "repository",
|
||||
"title" => "Harden the webhook verifier",
|
||||
"description" => "Audit the signature verification path and ship a fix with tests.",
|
||||
"funding" => "credits",
|
||||
"amount" => "300"
|
||||
}
|
||||
})
|
||||
|> render_submit()
|
||||
|> follow_redirect(conn)
|
||||
|
||||
assert html =~ "Harden the webhook verifier"
|
||||
assert html =~ "300 credits"
|
||||
|
||||
bounty = Repo.one!(Bounty)
|
||||
assert bounty.status == "open"
|
||||
assert bounty.credit_amount == 300
|
||||
assert Tarakan.Credits.balance(sponsor) == 700
|
||||
end
|
||||
end
|
||||
|
||||
describe "show" do
|
||||
test "anonymous visitors see the poster and a login prompt, not a claim button", %{
|
||||
conn: conn
|
||||
} do
|
||||
{bounty, _sponsor, _repository} = open_bounty()
|
||||
|
||||
{:ok, view, html} = live(conn, ~p"/bounties/#{bounty.public_id}")
|
||||
|
||||
assert html =~ bounty.title
|
||||
assert has_element?(view, "#bounty-status")
|
||||
refute has_element?(view, "#bounty-claim-button")
|
||||
assert has_element?(view, "#bounty-login-to-claim")
|
||||
end
|
||||
|
||||
test "the sponsor sees cancel controls but no claim button", %{conn: conn} do
|
||||
{bounty, sponsor, _repository} = open_bounty()
|
||||
conn = log_in_account(conn, sponsor)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/bounties/#{bounty.public_id}")
|
||||
|
||||
refute has_element?(view, "#bounty-claim-button")
|
||||
assert has_element?(view, "#bounty-cancel-button")
|
||||
end
|
||||
|
||||
test "an eligible hunter sees and uses the claim button", %{conn: conn} do
|
||||
{bounty, _sponsor, _repository} = open_bounty()
|
||||
hunter = active_account_fixture()
|
||||
conn = log_in_account(conn, hunter)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/bounties/#{bounty.public_id}")
|
||||
|
||||
assert has_element?(view, "#bounty-claim-button")
|
||||
|
||||
html = view |> element("#bounty-claim-button") |> render_click()
|
||||
|
||||
assert html =~ "claimed"
|
||||
assert has_element?(view, "#bounty-claims")
|
||||
|
||||
bounty = Repo.get!(Bounty, bounty.id)
|
||||
assert bounty.status == "claimed"
|
||||
|
||||
claim = Repo.one!(Tarakan.Market.BountyClaim)
|
||||
assert claim.account_id == hunter.id
|
||||
assert claim.review_task_id
|
||||
end
|
||||
end
|
||||
|
||||
describe "wanted banners" do
|
||||
test "the repository security page shows the banner for its open bounty", %{conn: conn} do
|
||||
{bounty, _sponsor, repository} = open_bounty()
|
||||
|
||||
{:ok, view, _html} =
|
||||
live(conn, TarakanWeb.RepositoryPaths.repository_security_path(repository))
|
||||
|
||||
assert has_element?(view, "#wanted-banner")
|
||||
|
||||
bounty = Market.get_bounty!(bounty.public_id)
|
||||
assert bounty.status == "open"
|
||||
end
|
||||
end
|
||||
end
|
||||
79
test/tarakan_web/live/client_authorization_live_test.exs
Normal file
79
test/tarakan_web/live/client_authorization_live_test.exs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
defmodule TarakanWeb.ClientAuthorizationLiveTest do
|
||||
use TarakanWeb.ConnCase, async: true
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Tarakan.Accounts.ClientAuthorizations
|
||||
|
||||
setup :register_and_log_in_account
|
||||
|
||||
test "approve/deny with no loaded authorization flashes instead of crashing", %{conn: conn} do
|
||||
# An unknown/expired user code leaves @authorization nil and hides the
|
||||
# buttons - but a queued or crafted event must not reach the context with
|
||||
# nil and raise a FunctionClauseError.
|
||||
{:ok, view, html} = live(conn, ~p"/client/authorize/UNKNOWNXY")
|
||||
assert html =~ "invalid or expired"
|
||||
|
||||
assert render_click(view, "approve") =~ "expired or was already used"
|
||||
assert render_click(view, "deny") =~ "expired or was already used"
|
||||
end
|
||||
|
||||
describe "consent screen" do
|
||||
test "attributes the client-supplied name instead of asserting it", %{conn: conn} do
|
||||
{:ok, _device_code, user_code, _authorization} =
|
||||
ClientAuthorizations.start(%{"client_name" => "Totally Official Thing"})
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/client/authorize/#{user_code}")
|
||||
|
||||
# The name is presented as the request's own claim, next to a warning,
|
||||
# rather than as a statement this page vouches for.
|
||||
assert html =~ "Calls itself"
|
||||
assert html =~ "Totally Official Thing"
|
||||
assert html =~ "Nobody at Tarakan will"
|
||||
end
|
||||
|
||||
test "a name carrying control or bidi characters is cleaned before display", %{conn: conn} do
|
||||
hostile = "Tarakan" <> "\u202e" <> "lortnoC" <> "\u200b" <> " Client"
|
||||
|
||||
{:ok, _device_code, user_code, authorization} =
|
||||
ClientAuthorizations.start(%{"client_name" => hostile})
|
||||
|
||||
refute authorization.client_name =~ "\u202e"
|
||||
refute authorization.client_name =~ "\u200b"
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/client/authorize/#{user_code}")
|
||||
refute html =~ "\u202e"
|
||||
end
|
||||
|
||||
test "a name of only unusable characters falls back to the default", %{conn: _conn} do
|
||||
{:ok, _device_code, _user_code, authorization} =
|
||||
ClientAuthorizations.start(%{"client_name" => "\u200b\u202e"})
|
||||
|
||||
assert authorization.client_name == "Tarakan Client"
|
||||
end
|
||||
|
||||
test "a name with markup-shaped characters is refused outright" do
|
||||
assert {:error, changeset} =
|
||||
ClientAuthorizations.start(%{"client_name" => "Tarakan <script> Client"})
|
||||
|
||||
assert Keyword.has_key?(changeset.errors, :client_name)
|
||||
end
|
||||
end
|
||||
|
||||
describe "user code lookups" do
|
||||
test "wrong guesses are budgeted per account", %{conn: conn} do
|
||||
# Well past the per-account allowance; the page must stay uninformative
|
||||
# rather than answering "exists / does not exist" indefinitely.
|
||||
for index <- 1..35 do
|
||||
{:ok, _view, html} = live(conn, ~p"/client/authorize/#{"AAAA000" <> to_string(index)}")
|
||||
assert html =~ "invalid or expired"
|
||||
end
|
||||
|
||||
# A real code now reads the same as a wrong one: throttled and missing
|
||||
# are deliberately indistinguishable.
|
||||
{:ok, _device_code, user_code, _authorization} = ClientAuthorizations.start()
|
||||
{:ok, _view, html} = live(conn, ~p"/client/authorize/#{user_code}")
|
||||
assert html =~ "invalid or expired"
|
||||
end
|
||||
end
|
||||
end
|
||||
80
test/tarakan_web/live/community_live_test.exs
Normal file
80
test/tarakan_web/live/community_live_test.exs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
defmodule TarakanWeb.CommunityLiveTest do
|
||||
use TarakanWeb.ConnCase
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
test "the homepage shows the shoutbox and gates posting", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/")
|
||||
|
||||
assert has_element?(view, "#registry-shoutbox")
|
||||
refute has_element?(view, "#registry-presence")
|
||||
assert has_element?(view, "#shoutbox-empty")
|
||||
refute has_element?(view, "#shoutbox-form")
|
||||
assert has_element?(view, ~s(#registry-shoutbox a[href="/accounts/log-in"]))
|
||||
end
|
||||
|
||||
test "a signed-in contributor posts a live escaped shout", %{conn: conn} do
|
||||
account = account_fixture()
|
||||
{:ok, view, _html} = live(log_in_account(conn, account), ~p"/")
|
||||
{:ok, observer, _html} = live(Phoenix.ConnTest.build_conn(), ~p"/")
|
||||
|
||||
assert has_element?(view, "#shoutbox-form")
|
||||
assert has_element?(view, ~s(#shoutbox-form input[type="text"]))
|
||||
|
||||
html =
|
||||
view
|
||||
|> form("#shoutbox-form", shout: %{body: "Reviewing <script>alert(1)</script> auth."})
|
||||
|> render_submit()
|
||||
|
||||
assert has_element?(
|
||||
view,
|
||||
"#shoutbox-messages article",
|
||||
"Reviewing <script>alert(1)</script> auth."
|
||||
)
|
||||
|
||||
assert has_element?(view, "#shoutbox-body-1[phx-mounted]")
|
||||
|
||||
assert has_element?(
|
||||
observer,
|
||||
"#shoutbox-messages article",
|
||||
"Reviewing <script>alert(1)</script> auth."
|
||||
)
|
||||
|
||||
assert html =~ "<script>"
|
||||
refute html =~ "<script>alert(1)</script>"
|
||||
end
|
||||
|
||||
test "moderators can remove a shout from the homepage", %{conn: conn} do
|
||||
author = account_fixture()
|
||||
|
||||
{:ok, shout} =
|
||||
Tarakan.Community.create_shout(Tarakan.Accounts.Scope.for_account(author), %{
|
||||
"body" => "Off-topic shout"
|
||||
})
|
||||
|
||||
moderator = moderator_account_fixture()
|
||||
{:ok, view, _html} = live(log_in_account(conn, moderator), ~p"/")
|
||||
|
||||
view
|
||||
|> element("#shout-#{shout.id} button", "Remove")
|
||||
|> render_click()
|
||||
|
||||
assert has_element?(view, "#shout-#{shout.id}", "Removed by moderation")
|
||||
refute has_element?(view, "#shout-#{shout.id} button", "Remove")
|
||||
end
|
||||
|
||||
test "the compact composer rejects blank messages without an inline error", %{conn: conn} do
|
||||
{:ok, view, _html} = live(log_in_account(conn, account_fixture()), ~p"/")
|
||||
|
||||
assert has_element?(view, "#shoutbox-form input[required]")
|
||||
|
||||
html =
|
||||
view
|
||||
|> form("#shoutbox-form", shout: %{body: " "})
|
||||
|> render_submit()
|
||||
|
||||
assert html =~ "Write a message before sending."
|
||||
refute has_element?(view, "#shoutbox-body-0-error")
|
||||
refute html =~ "be blank"
|
||||
end
|
||||
end
|
||||
160
test/tarakan_web/live/explore_live_test.exs
Normal file
160
test/tarakan_web/live/explore_live_test.exs
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
defmodule TarakanWeb.ExploreLiveTest do
|
||||
use TarakanWeb.ConnCase
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Tarakan.Activity
|
||||
|
||||
test "renders the wire: registrations and unverified public reviews", %{conn: conn} do
|
||||
submitter = github_account_fixture()
|
||||
repository = listed_github_repository_fixture(submitter)
|
||||
scan = scan_fixture(repository, submitter, %{"findings_json" => findings_json_fixture(1)})
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/explore")
|
||||
|
||||
assert has_element?(view, "#activity-wire")
|
||||
assert has_element?(view, "#wire-reg-#{repository.id}", "openai/codex")
|
||||
assert has_element?(view, "#wire-scan-#{scan.id}", "@#{submitter.handle}")
|
||||
assert has_element?(view, "#wire-scan-#{scan.id}", "1 finding")
|
||||
end
|
||||
|
||||
test "filters the wire by kind", %{conn: conn} do
|
||||
submitter = github_account_fixture()
|
||||
repository = listed_github_repository_fixture(submitter)
|
||||
scan = scan_fixture(repository, submitter)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/explore")
|
||||
|
||||
view |> element("#explore-filter button", "Registrations") |> render_click()
|
||||
|
||||
assert has_element?(view, "#wire-reg-#{repository.id}")
|
||||
refute has_element?(view, "#wire-scan-#{scan.id}")
|
||||
|
||||
view |> element("#explore-filter button", "Reports") |> render_click()
|
||||
|
||||
assert has_element?(view, "#wire-scan-#{scan.id}")
|
||||
refute has_element?(view, "#wire-reg-#{repository.id}")
|
||||
end
|
||||
|
||||
test "new activity lands on the wire without a reload", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/explore")
|
||||
|
||||
assert has_element?(view, "#activity-wire-empty")
|
||||
|
||||
submitter = github_account_fixture()
|
||||
repository = listed_github_repository_fixture(submitter)
|
||||
Activity.broadcast_registration(repository)
|
||||
|
||||
assert has_element?(view, "#wire-reg-#{repository.id}", "openai/codex")
|
||||
end
|
||||
|
||||
test "verdicts on the wire show who ruled and how", %{conn: conn} do
|
||||
submitter = github_account_fixture()
|
||||
repository = listed_github_repository_fixture(submitter)
|
||||
scan = scan_fixture(repository, submitter, %{"findings_json" => findings_json_fixture(1)})
|
||||
reviewer = reviewer_account_fixture()
|
||||
confirmation_fixture(scan, reviewer)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/explore")
|
||||
|
||||
assert render(view) =~ "@#{reviewer.handle}"
|
||||
assert render(view) =~ "confirmed a finding on"
|
||||
end
|
||||
|
||||
test "handles link to contributor profiles", %{conn: conn} do
|
||||
submitter = github_account_fixture()
|
||||
repository = listed_github_repository_fixture(submitter)
|
||||
scan = scan_fixture(repository, submitter)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/explore")
|
||||
|
||||
assert view
|
||||
|> element("#wire-scan-#{scan.id} a[href='/#{submitter.handle}']")
|
||||
|> render() =~ "@#{submitter.handle}"
|
||||
end
|
||||
|
||||
test "comments join the wire live and on reload", %{conn: conn} do
|
||||
submitter = github_account_fixture()
|
||||
repository = listed_github_repository_fixture(submitter)
|
||||
scan = scan_fixture(repository, submitter, %{"findings_json" => findings_json_fixture(1)})
|
||||
[finding] = scan.findings
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/explore")
|
||||
|
||||
commenter = account_fixture()
|
||||
scope = Tarakan.Accounts.Scope.for_account(commenter)
|
||||
|
||||
{:ok, comment} =
|
||||
Tarakan.Discussion.create_comment(scope, %{finding | scan: scan}, %{
|
||||
"body" => "Reproduced on a clean checkout."
|
||||
})
|
||||
|
||||
assert has_element?(view, "#wire-comment-#{comment.id}", "@#{commenter.handle}")
|
||||
assert has_element?(view, "#wire-comment-#{comment.id}", "commented on")
|
||||
|
||||
{:ok, reloaded, _html} = live(conn, ~p"/explore")
|
||||
assert has_element?(reloaded, "#wire-comment-#{comment.id}")
|
||||
|
||||
reloaded |> element("#explore-filter button", "Comments") |> render_click()
|
||||
assert has_element?(reloaded, "#wire-comment-#{comment.id}")
|
||||
refute has_element?(reloaded, "#wire-scan-#{scan.id}")
|
||||
end
|
||||
|
||||
test "search narrows the wire by handle or repository", %{conn: conn} do
|
||||
submitter = github_account_fixture()
|
||||
repository = listed_github_repository_fixture(submitter)
|
||||
scan = scan_fixture(repository, submitter)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/explore")
|
||||
|
||||
view
|
||||
|> form("#explore-search-form", %{"q" => submitter.handle})
|
||||
|> render_change()
|
||||
|
||||
assert has_element?(view, "#wire-scan-#{scan.id}")
|
||||
refute has_element?(view, "#wire-reg-#{repository.id}")
|
||||
|
||||
view
|
||||
|> form("#explore-search-form", %{"q" => "no-such-thing"})
|
||||
|> render_change()
|
||||
|
||||
assert has_element?(view, "#activity-wire-empty")
|
||||
end
|
||||
|
||||
test "shows watcher presence", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/explore")
|
||||
|
||||
assert has_element?(view, "#explore-watchers")
|
||||
assert view |> element("#explore-watchers") |> render() =~ "watching"
|
||||
end
|
||||
|
||||
test "hot findings rail ranks upvoted findings and refreshes on votes", %{conn: conn} do
|
||||
submitter = github_account_fixture()
|
||||
repository = listed_github_repository_fixture(submitter)
|
||||
scan = scan_fixture(repository, submitter, %{"findings_json" => findings_json_fixture(1)})
|
||||
[finding] = scan.findings
|
||||
canonical = finding.canonical_finding
|
||||
|
||||
# Hot rail excludes pure single-run unconfirmed noise; second detection qualifies.
|
||||
_second =
|
||||
scan_fixture(repository, account_fixture(), %{
|
||||
"findings_json" => findings_json_fixture(1),
|
||||
"commit_sha" => scan.commit_sha
|
||||
})
|
||||
|
||||
canonical = Tarakan.Repo.get!(Tarakan.Scans.CanonicalFinding, canonical.id)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/explore")
|
||||
|
||||
assert has_element?(view, "#hot-findings", "No findings upvoted this week.")
|
||||
|
||||
voter = Tarakan.Accounts.Scope.for_account(account_fixture())
|
||||
{:ok, _summary} = Tarakan.Reputation.cast_vote(voter, "canonical_finding", canonical.id, 1)
|
||||
|
||||
assert has_element?(view, "#hot-finding-#{canonical.id}", "+1")
|
||||
|
||||
assert view
|
||||
|> element("#hot-finding-#{canonical.id}")
|
||||
|> render() =~ ~s(href="/findings/)
|
||||
end
|
||||
end
|
||||
112
test/tarakan_web/live/finding_discussion_test.exs
Normal file
112
test/tarakan_web/live/finding_discussion_test.exs
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
defmodule TarakanWeb.FindingDiscussionTest do
|
||||
use TarakanWeb.ConnCase
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Tarakan.Accounts.Scope
|
||||
alias Tarakan.Discussion
|
||||
|
||||
setup do
|
||||
submitter = github_account_fixture()
|
||||
repository = listed_github_repository_fixture(submitter)
|
||||
scan = scan_fixture(repository, submitter, %{"findings_json" => findings_json_fixture(1)})
|
||||
[finding] = scan.findings
|
||||
%{finding: %{finding | scan: scan}, repository: repository}
|
||||
end
|
||||
|
||||
test "anonymous visitors see the discussion but are prompted to sign in", %{
|
||||
conn: conn,
|
||||
finding: finding
|
||||
} do
|
||||
{:ok, view, _html} = live(conn, ~p"/findings/#{finding.public_id}")
|
||||
|
||||
assert has_element?(view, "#finding-discussion")
|
||||
assert has_element?(view, "#finding-comment-login")
|
||||
assert has_element?(view, "#finding-no-comments")
|
||||
refute has_element?(view, "#finding-comment-form")
|
||||
end
|
||||
|
||||
test "a signed-in account posts a comment", %{conn: conn, finding: finding} do
|
||||
conn = log_in_account(conn, account_fixture())
|
||||
{:ok, view, _html} = live(conn, ~p"/findings/#{finding.public_id}")
|
||||
|
||||
view
|
||||
|> form("#finding-comment-form", %{"body" => "Reproduced against the pinned commit."})
|
||||
|> render_submit()
|
||||
|
||||
assert has_element?(view, "#finding-comment-count", "1 comment")
|
||||
assert render(view) =~ "Reproduced against the pinned commit."
|
||||
refute has_element?(view, "#finding-no-comments")
|
||||
end
|
||||
|
||||
test "a reply nests under its parent", %{conn: conn, finding: finding} do
|
||||
author = account_fixture()
|
||||
|
||||
{:ok, parent} =
|
||||
Discussion.create_comment(Scope.for_account(author), finding, %{"body" => "Parent comment."})
|
||||
|
||||
conn = log_in_account(conn, account_fixture())
|
||||
{:ok, view, _html} = live(conn, ~p"/findings/#{finding.public_id}")
|
||||
|
||||
view |> element("#comment-#{parent.id} button", "Reply") |> render_click()
|
||||
|
||||
view
|
||||
|> form("#reply-form-#{parent.id}", %{"body" => "A threaded reply."})
|
||||
|> render_submit()
|
||||
|
||||
assert has_element?(view, "#comment-#{parent.id} #comment-#{last_comment_id()}")
|
||||
assert has_element?(view, "#finding-comment-count", "2 comments")
|
||||
end
|
||||
|
||||
test "a moderator can remove a comment and it shows as a placeholder", %{
|
||||
conn: conn,
|
||||
finding: finding
|
||||
} do
|
||||
author = account_fixture()
|
||||
|
||||
{:ok, comment} =
|
||||
Discussion.create_comment(Scope.for_account(author), finding, %{"body" => "Spammy comment."})
|
||||
|
||||
conn = log_in_account(conn, moderator_account_fixture())
|
||||
{:ok, view, _html} = live(conn, ~p"/findings/#{finding.public_id}")
|
||||
|
||||
assert has_element?(view, "#comment-#{comment.id} button", "Remove")
|
||||
|
||||
view
|
||||
|> element("#comment-#{comment.id} button", "Remove")
|
||||
|> render_click()
|
||||
|
||||
assert has_element?(view, "#comment-#{comment.id}", "Removed by moderation")
|
||||
end
|
||||
|
||||
test "restricted findings expose no discussion to anonymous visitors", %{
|
||||
conn: conn,
|
||||
repository: repository
|
||||
} do
|
||||
submitter = github_account_fixture(%{handle: "restricted-author"})
|
||||
scan = scan_fixture(repository, submitter, %{"findings_json" => findings_json_fixture(1)})
|
||||
[finding] = scan.findings
|
||||
|
||||
{:ok, _restricted} =
|
||||
Tarakan.Scans.update_visibility(
|
||||
Scope.for_account(moderator_account_fixture()),
|
||||
scan,
|
||||
"restricted",
|
||||
%{
|
||||
"moderation_reason" => "takedown_review",
|
||||
"moderation_notes" =>
|
||||
"Restricted in this test to confirm discussion follows finding disclosure."
|
||||
}
|
||||
)
|
||||
|
||||
assert_error_sent 404, fn -> get(conn, ~p"/findings/#{finding.public_id}") end
|
||||
end
|
||||
|
||||
defp last_comment_id do
|
||||
import Ecto.Query
|
||||
|
||||
Tarakan.Repo.one(
|
||||
from c in Tarakan.Discussion.Comment, order_by: [desc: c.id], limit: 1, select: c.id
|
||||
)
|
||||
end
|
||||
end
|
||||
420
test/tarakan_web/live/finding_live_test.exs
Normal file
420
test/tarakan_web/live/finding_live_test.exs
Normal file
|
|
@ -0,0 +1,420 @@
|
|||
defmodule TarakanWeb.FindingLiveTest do
|
||||
use TarakanWeb.ConnCase
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
setup do
|
||||
submitter = github_account_fixture()
|
||||
repository = listed_github_repository_fixture(submitter)
|
||||
%{repository: repository, submitter: submitter}
|
||||
end
|
||||
|
||||
test "renders a publicly disclosed finding", %{
|
||||
conn: conn,
|
||||
repository: repository,
|
||||
submitter: submitter
|
||||
} do
|
||||
scan =
|
||||
repository
|
||||
|> scan_fixture(submitter, %{"findings_json" => findings_json_fixture(1)})
|
||||
|> publish_scan("public")
|
||||
|
||||
[finding] = scan.findings
|
||||
|
||||
{:ok, view, html} = live(conn, ~p"/findings/#{finding.public_id}")
|
||||
|
||||
assert has_element?(view, "#finding-title", finding.title)
|
||||
assert has_element?(view, "#finding-severity", "high")
|
||||
assert has_element?(view, "#finding-description", "String-built SQL")
|
||||
assert has_element?(view, "#finding-canonical-memory", "detected in 1 run")
|
||||
assert has_element?(view, "#finding-source-link[href='/findings/#{finding.public_id}/code']")
|
||||
assert has_element?(view, "#finding-verified-badge")
|
||||
assert has_element?(view, "#finding-disclosure-badge")
|
||||
assert has_element?(view, "#finding-copy-link")
|
||||
assert has_element?(view, "#finding-cite", "/findings/#{finding.public_id}")
|
||||
assert has_element?(view, "#finding-verdict-counts", "2 confirmed · 0 disputed")
|
||||
assert html =~ scan.commit_sha
|
||||
assert html =~ "Public"
|
||||
assert has_element?(view, "#finding-record-link[href='/github.com/openai/codex/security']")
|
||||
end
|
||||
|
||||
test "shows the raw model report exactly as submitted", %{
|
||||
conn: conn,
|
||||
repository: repository,
|
||||
submitter: submitter
|
||||
} do
|
||||
scan = scan_fixture(repository, submitter, %{"findings_json" => findings_json_fixture(1)})
|
||||
[finding] = scan.findings
|
||||
|
||||
assert scan.raw_document == findings_json_fixture(1)
|
||||
|
||||
{:ok, view, html} = live(conn, ~p"/findings/#{finding.public_id}")
|
||||
|
||||
assert has_element?(view, "#finding-raw-report")
|
||||
assert html =~ "tarakan_scan_format"
|
||||
end
|
||||
|
||||
test "a qualified contributor records a verdict from the finding page", %{
|
||||
conn: conn,
|
||||
repository: repository,
|
||||
submitter: submitter
|
||||
} do
|
||||
scan = scan_fixture(repository, submitter, %{"findings_json" => findings_json_fixture(1)})
|
||||
[finding] = scan.findings
|
||||
|
||||
reviewer =
|
||||
account_fixture()
|
||||
|> Tarakan.Accounts.Account.authorization_changeset(%{
|
||||
state: "active",
|
||||
platform_role: "member",
|
||||
trust_tier: "reviewer"
|
||||
})
|
||||
|> Tarakan.Repo.update!()
|
||||
|
||||
conn = log_in_account(conn, reviewer)
|
||||
{:ok, view, _html} = live(conn, ~p"/findings/#{finding.public_id}")
|
||||
|
||||
assert has_element?(view, "#finding-verdict-counts", "0 confirmed · 0 disputed")
|
||||
|
||||
view
|
||||
|> form("#finding-verdict-form", %{
|
||||
"notes" => "Reproduced against the pinned commit with the documented payload."
|
||||
})
|
||||
|> render_submit(%{"verdict" => "confirmed"})
|
||||
|
||||
assert has_element?(view, "#finding-verdict-counts", "1 confirmed · 0 disputed")
|
||||
assert has_element?(view, "#finding-checks", "@#{reviewer.handle}")
|
||||
|
||||
assert has_element?(
|
||||
view,
|
||||
"#finding-checks",
|
||||
"Reproduced against the pinned commit with the documented payload."
|
||||
)
|
||||
|
||||
assert has_element?(view, "#finding-checks", "counts toward quorum")
|
||||
refute has_element?(view, "#finding-verdict-form")
|
||||
|
||||
{:ok, public_view, _html} =
|
||||
live(Phoenix.ConnTest.build_conn(), ~p"/findings/#{finding.public_id}")
|
||||
|
||||
assert has_element?(public_view, "#finding-checks", "@#{reviewer.handle}")
|
||||
assert has_element?(public_view, "#finding-checks", "documented payload")
|
||||
end
|
||||
|
||||
test "the submitter cannot verify their own finding", %{
|
||||
conn: conn,
|
||||
repository: repository,
|
||||
submitter: submitter
|
||||
} do
|
||||
scan = scan_fixture(repository, submitter, %{"findings_json" => findings_json_fixture(1)})
|
||||
[finding] = scan.findings
|
||||
|
||||
conn = log_in_account(conn, submitter)
|
||||
{:ok, view, _html} = live(conn, ~p"/findings/#{finding.public_id}")
|
||||
|
||||
refute has_element?(view, "#finding-verdict-form")
|
||||
end
|
||||
|
||||
test "the dead render carries the meta description and canonical URL", %{
|
||||
conn: conn,
|
||||
repository: repository,
|
||||
submitter: submitter
|
||||
} do
|
||||
scan =
|
||||
repository
|
||||
|> scan_fixture(submitter, %{"findings_json" => findings_json_fixture(1)})
|
||||
|> publish_scan("public")
|
||||
|
||||
[finding] = scan.findings
|
||||
|
||||
html = conn |> get(~p"/findings/#{finding.public_id}") |> html_response(200)
|
||||
|
||||
assert html =~ ~s(rel="canonical")
|
||||
assert html =~ "/findings/#{finding.public_id}"
|
||||
assert html =~ "High in openai/codex"
|
||||
assert get_resp_header(get(conn, ~p"/findings/#{finding.public_id}"), "x-robots-tag") == []
|
||||
end
|
||||
|
||||
test "fresh submissions resolve for anonymous visitors immediately", %{
|
||||
conn: conn,
|
||||
repository: repository,
|
||||
submitter: submitter
|
||||
} do
|
||||
scan = scan_fixture(repository, submitter, %{"findings_json" => findings_json_fixture(1)})
|
||||
[finding] = scan.findings
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/findings/#{finding.public_id}")
|
||||
|
||||
assert has_element?(view, "#finding-title", finding.title)
|
||||
refute has_element?(view, "#finding-verified-badge")
|
||||
end
|
||||
|
||||
test "restricted findings do not resolve for anonymous visitors", %{
|
||||
conn: conn,
|
||||
repository: repository,
|
||||
submitter: submitter
|
||||
} do
|
||||
scan = scan_fixture(repository, submitter, %{"findings_json" => findings_json_fixture(1)})
|
||||
[finding] = scan.findings
|
||||
|
||||
{:ok, _restricted} =
|
||||
Tarakan.Scans.update_visibility(
|
||||
Tarakan.Accounts.Scope.for_account(moderator_account_fixture()),
|
||||
scan,
|
||||
"restricted",
|
||||
%{
|
||||
"moderation_reason" => "takedown_review",
|
||||
"moderation_notes" =>
|
||||
"Deliberately restricted in this test to exercise the takedown boundary."
|
||||
}
|
||||
)
|
||||
|
||||
assert_error_sent 404, fn -> get(conn, ~p"/findings/#{finding.public_id}") end
|
||||
end
|
||||
|
||||
test "public-summary findings do not resolve for anonymous visitors", %{
|
||||
conn: conn,
|
||||
repository: repository,
|
||||
submitter: submitter
|
||||
} do
|
||||
scan = scan_fixture(repository, submitter, %{"findings_json" => findings_json_fixture(1)})
|
||||
[finding] = scan.findings
|
||||
publish_scan(scan, "public_summary")
|
||||
|
||||
assert_error_sent 404, fn -> get(conn, ~p"/findings/#{finding.public_id}") end
|
||||
end
|
||||
|
||||
test "the submitter can still open their own restricted finding", %{
|
||||
conn: conn,
|
||||
repository: repository,
|
||||
submitter: submitter
|
||||
} do
|
||||
scan = scan_fixture(repository, submitter, %{"findings_json" => findings_json_fixture(1)})
|
||||
[finding] = scan.findings
|
||||
|
||||
{:ok, _restricted} =
|
||||
Tarakan.Scans.update_visibility(
|
||||
Tarakan.Accounts.Scope.for_account(moderator_account_fixture()),
|
||||
scan,
|
||||
"restricted",
|
||||
%{
|
||||
"moderation_reason" => "takedown_review",
|
||||
"moderation_notes" =>
|
||||
"Deliberately restricted in this test to exercise submitter access."
|
||||
}
|
||||
)
|
||||
|
||||
conn = log_in_account(conn, submitter)
|
||||
{:ok, view, _html} = live(conn, ~p"/findings/#{finding.public_id}")
|
||||
|
||||
assert has_element?(view, "#finding-title", finding.title)
|
||||
end
|
||||
|
||||
test "the lifecycle timeline lists first-seen and check events", %{
|
||||
conn: conn,
|
||||
repository: repository,
|
||||
submitter: submitter
|
||||
} do
|
||||
scan = scan_fixture(repository, submitter, %{"findings_json" => findings_json_fixture(1)})
|
||||
[finding] = scan.findings
|
||||
|
||||
reviewer = reviewer_account_fixture()
|
||||
conn = log_in_account(conn, reviewer)
|
||||
{:ok, view, _html} = live(conn, ~p"/findings/#{finding.public_id}")
|
||||
|
||||
assert has_element?(view, "#finding-timeline-first-seen", "First seen")
|
||||
refute has_element?(view, "li[id^='finding-timeline-check-']")
|
||||
refute has_element?(view, "#finding-timeline-verified")
|
||||
refute has_element?(view, "#finding-timeline-vendor-notified")
|
||||
|
||||
view
|
||||
|> form("#finding-verdict-form", %{
|
||||
"notes" => "Reproduced against the pinned commit with the documented payload."
|
||||
})
|
||||
|> render_submit(%{"verdict" => "confirmed"})
|
||||
|
||||
assert has_element?(view, "#finding-timeline li[id^='finding-timeline-check-']")
|
||||
assert has_element?(view, "#finding-timeline", "@#{reviewer.handle}")
|
||||
assert has_element?(view, "#finding-timeline", "confirmed")
|
||||
end
|
||||
|
||||
test "the lifecycle timeline shows verified and vendor events when set", %{
|
||||
conn: conn,
|
||||
repository: repository,
|
||||
submitter: submitter
|
||||
} do
|
||||
scan =
|
||||
repository
|
||||
|> scan_fixture(submitter, %{"findings_json" => findings_json_fixture(1)})
|
||||
|> publish_scan("public")
|
||||
|
||||
[finding] = scan.findings
|
||||
|
||||
conn = log_in_account(conn, reviewer_account_fixture())
|
||||
{:ok, view, _html} = live(conn, ~p"/findings/#{finding.public_id}")
|
||||
|
||||
assert has_element?(view, "#finding-timeline-first-seen")
|
||||
assert has_element?(view, "#finding-timeline li[id^='finding-timeline-check-']")
|
||||
assert has_element?(view, "#finding-timeline-verified", "Verified")
|
||||
refute has_element?(view, "#finding-timeline-vendor-notified")
|
||||
|
||||
view
|
||||
|> form("#finding-vendor-form", %{
|
||||
"vendor_notification" => %{"notified_on" => "2026-01-15"}
|
||||
})
|
||||
|> render_submit()
|
||||
|
||||
assert has_element?(view, "#finding-timeline-vendor-notified", "Vendor notified")
|
||||
|
||||
canonical =
|
||||
Tarakan.Repo.get_by!(Tarakan.Scans.CanonicalFinding, repository_id: repository.id)
|
||||
|
||||
assert canonical.vendor_notified_at == ~U[2026-01-15 00:00:00.000000Z]
|
||||
|
||||
view
|
||||
|> form("#finding-vendor-form", %{"vendor_notification" => %{"notified_on" => ""}})
|
||||
|> render_submit()
|
||||
|
||||
refute has_element?(view, "#finding-timeline-vendor-notified")
|
||||
|
||||
assert Tarakan.Repo.get!(Tarakan.Scans.CanonicalFinding, canonical.id).vendor_notified_at ==
|
||||
nil
|
||||
end
|
||||
|
||||
test "CWE and CVE badges link out and disclosure metadata renders", %{
|
||||
conn: conn,
|
||||
repository: repository,
|
||||
submitter: submitter
|
||||
} do
|
||||
scan =
|
||||
scan_fixture(repository, submitter, %{
|
||||
"findings_json" => findings_json_with_disclosure_fixture()
|
||||
})
|
||||
|
||||
[finding] = scan.findings
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/findings/#{finding.public_id}")
|
||||
|
||||
assert has_element?(
|
||||
view,
|
||||
"#finding-cwe-badge[href='https://cwe.mitre.org/data/definitions/89.html']",
|
||||
"CWE-89"
|
||||
)
|
||||
|
||||
assert has_element?(
|
||||
view,
|
||||
"#finding-cve-badge[href='https://nvd.nist.gov/vuln/detail/CVE-2026-12345']",
|
||||
"CVE-2026-12345"
|
||||
)
|
||||
|
||||
assert has_element?(view, "#finding-reproduction", "Run the payload")
|
||||
assert has_element?(view, "#finding-affected-versions", ">= 1.0, < 2.0")
|
||||
end
|
||||
|
||||
test "CWE and CVE badges and disclosure metadata are absent when unset", %{
|
||||
conn: conn,
|
||||
repository: repository,
|
||||
submitter: submitter
|
||||
} do
|
||||
scan = scan_fixture(repository, submitter, %{"findings_json" => findings_json_fixture(1)})
|
||||
[finding] = scan.findings
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/findings/#{finding.public_id}")
|
||||
|
||||
refute has_element?(view, "#finding-cwe-badge")
|
||||
refute has_element?(view, "#finding-cve-badge")
|
||||
refute has_element?(view, "#finding-reproduction")
|
||||
refute has_element?(view, "#finding-affected-versions")
|
||||
end
|
||||
|
||||
test "the vendor notification form is hidden from ordinary members", %{
|
||||
conn: conn,
|
||||
repository: repository,
|
||||
submitter: submitter
|
||||
} do
|
||||
scan = scan_fixture(repository, submitter, %{"findings_json" => findings_json_fixture(1)})
|
||||
[finding] = scan.findings
|
||||
|
||||
{:ok, anonymous_view, _html} = live(conn, ~p"/findings/#{finding.public_id}")
|
||||
refute has_element?(anonymous_view, "#finding-vendor-form")
|
||||
|
||||
conn = log_in_account(conn, submitter)
|
||||
{:ok, view, _html} = live(conn, ~p"/findings/#{finding.public_id}")
|
||||
refute has_element?(view, "#finding-vendor-form")
|
||||
end
|
||||
|
||||
test "an unauthorized vendor notification event is rejected", %{
|
||||
conn: conn,
|
||||
repository: repository,
|
||||
submitter: submitter
|
||||
} do
|
||||
scan = scan_fixture(repository, submitter, %{"findings_json" => findings_json_fixture(1)})
|
||||
[finding] = scan.findings
|
||||
|
||||
conn = log_in_account(conn, submitter)
|
||||
{:ok, view, _html} = live(conn, ~p"/findings/#{finding.public_id}")
|
||||
|
||||
render_hook(view, "record_vendor_notification", %{
|
||||
"vendor_notification" => %{"notified_on" => "2026-01-15"}
|
||||
})
|
||||
|
||||
assert has_element?(view, "#flash-group", "not authorized")
|
||||
|
||||
canonical =
|
||||
Tarakan.Repo.get_by!(Tarakan.Scans.CanonicalFinding, repository_id: repository.id)
|
||||
|
||||
assert canonical.vendor_notified_at == nil
|
||||
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
|
||||
110
test/tarakan_web/live/finding_vote_test.exs
Normal file
110
test/tarakan_web/live/finding_vote_test.exs
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
defmodule TarakanWeb.FindingVoteTest do
|
||||
use TarakanWeb.ConnCase
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Tarakan.Accounts.Scope
|
||||
alias Tarakan.Discussion
|
||||
|
||||
setup do
|
||||
submitter = github_account_fixture()
|
||||
repository = listed_github_repository_fixture(submitter)
|
||||
scan = scan_fixture(repository, submitter, %{"findings_json" => findings_json_fixture(1)})
|
||||
[finding] = scan.findings
|
||||
%{finding: %{finding | scan: scan}, submitter: submitter}
|
||||
end
|
||||
|
||||
test "a signed-in visitor upvotes a finding and the score updates", %{
|
||||
conn: conn,
|
||||
finding: finding
|
||||
} do
|
||||
conn = log_in_account(conn, account_fixture())
|
||||
{:ok, view, _html} = live(conn, ~p"/findings/#{finding.public_id}")
|
||||
canonical_id = finding.canonical_finding.id
|
||||
|
||||
view
|
||||
|> element(~s(#vote-canonical_finding-#{canonical_id} button[phx-value-vote="1"]))
|
||||
|> render_click()
|
||||
|
||||
assert has_element?(view, "#vote-canonical_finding-#{canonical_id}", "1")
|
||||
end
|
||||
|
||||
test "the submitter cannot vote on their own finding", %{
|
||||
conn: conn,
|
||||
finding: finding,
|
||||
submitter: submitter
|
||||
} do
|
||||
conn = log_in_account(conn, submitter)
|
||||
{:ok, view, _html} = live(conn, ~p"/findings/#{finding.public_id}")
|
||||
canonical_id = finding.canonical_finding.id
|
||||
|
||||
html =
|
||||
view
|
||||
|> element(~s(#vote-canonical_finding-#{canonical_id} button[phx-value-vote="1"]))
|
||||
|> render_click()
|
||||
|
||||
assert html =~ "cannot vote on your own"
|
||||
assert has_element?(view, "#vote-canonical_finding-#{canonical_id}", "0")
|
||||
end
|
||||
|
||||
test "anonymous vote clicks are gated to login", %{conn: conn, finding: finding} do
|
||||
{:ok, view, _html} = live(conn, ~p"/findings/#{finding.public_id}")
|
||||
# Anonymous controls are disabled, so the buttons carry the disabled attr.
|
||||
assert has_element?(
|
||||
view,
|
||||
~s(#vote-canonical_finding-#{finding.canonical_finding.id} button[disabled])
|
||||
)
|
||||
end
|
||||
|
||||
test "a comment can be voted on", %{conn: conn, finding: finding} do
|
||||
{:ok, comment} =
|
||||
Discussion.create_comment(Scope.for_account(account_fixture()), finding, %{
|
||||
"body" => "This looks exploitable."
|
||||
})
|
||||
|
||||
conn = log_in_account(conn, account_fixture())
|
||||
{:ok, view, _html} = live(conn, ~p"/findings/#{finding.public_id}")
|
||||
|
||||
view
|
||||
|> element(~s(#vote-comment-#{comment.id} button[phx-value-vote="1"]))
|
||||
|> render_click()
|
||||
|
||||
assert has_element?(view, "#vote-comment-#{comment.id}", "1")
|
||||
end
|
||||
|
||||
test "malformed vote parameters are rejected without crashing", %{
|
||||
conn: conn,
|
||||
finding: finding
|
||||
} do
|
||||
conn = log_in_account(conn, account_fixture())
|
||||
{:ok, view, _html} = live(conn, ~p"/findings/#{finding.public_id}")
|
||||
|
||||
html =
|
||||
render_click(view, "vote", %{
|
||||
"type" => "canonical_finding",
|
||||
"id" => "1 OR 1=1",
|
||||
"vote" => "1"
|
||||
})
|
||||
|
||||
assert html =~ "could not be recorded"
|
||||
|
||||
html =
|
||||
render_click(view, "vote", %{
|
||||
"type" => "canonical_finding",
|
||||
"id" => "#{finding.canonical_finding.id}",
|
||||
"vote" => "1; DROP"
|
||||
})
|
||||
|
||||
assert html =~ "could not be recorded"
|
||||
assert has_element?(view, "#vote-canonical_finding-#{finding.canonical_finding.id}", "0")
|
||||
end
|
||||
|
||||
test "only the canonical finding on the security page carries a vote control", %{
|
||||
finding: finding
|
||||
} do
|
||||
conn = Phoenix.ConnTest.build_conn()
|
||||
{:ok, view, _html} = live(conn, ~p"/github.com/openai/codex/security")
|
||||
assert has_element?(view, "#vote-canonical_finding-#{finding.canonical_finding.id}")
|
||||
refute has_element?(view, "#vote-finding-#{finding.id}")
|
||||
end
|
||||
end
|
||||
180
test/tarakan_web/live/infestation_live_test.exs
Normal file
180
test/tarakan_web/live/infestation_live_test.exs
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
defmodule TarakanWeb.InfestationLiveTest do
|
||||
use TarakanWeb.ConnCase, async: true
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Tarakan.FindingMemory
|
||||
|
||||
test "index and show render multi-repo patterns", %{conn: conn} 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 = "Live infestation #{System.unique_integer([:positive])}"
|
||||
|
||||
findings = fn path ->
|
||||
Jason.encode!(%{
|
||||
"tarakan_scan_format" => 1,
|
||||
"findings" => [
|
||||
%{
|
||||
"file" => path,
|
||||
"severity" => "critical",
|
||||
"title" => title,
|
||||
"description" => "Infestation liveview fixture."
|
||||
}
|
||||
]
|
||||
})
|
||||
end
|
||||
|
||||
scan_fixture(repo_a, submitter, %{"findings_json" => findings.("a.ex")})
|
||||
scan_fixture(repo_b, other, %{"findings_json" => findings.("b.ex")})
|
||||
# repo_a is hit twice by the same pattern at different paths, so the
|
||||
# per-repo rollup and the per-finding ledger must disagree on row count.
|
||||
scan_fixture(repo_a, submitter, %{"findings_json" => findings.("a2.ex")})
|
||||
|
||||
pattern = FindingMemory.pattern_key(title)
|
||||
|
||||
{:ok, index, html} = live(conn, ~p"/infestations")
|
||||
assert html =~ "Infestations"
|
||||
assert has_element?(index, "#infestations-constellation")
|
||||
assert has_element?(index, "#infestation-#{pattern}")
|
||||
|
||||
{:ok, show, show_html} = live(conn, ~p"/infestations/#{pattern}")
|
||||
assert show_html =~ title
|
||||
assert has_element?(show, "#infestation-repo-count", "2")
|
||||
assert has_element?(show, "#infestation-graph")
|
||||
assert has_element?(show, "#infestation-instances")
|
||||
|
||||
# The repo table is a rollup: one row per repository, carrying the counts
|
||||
# that distinguish it from the instance ledger below.
|
||||
graph_rows = show |> element("#infestation-graph tbody") |> render()
|
||||
assert graph_rows =~ "#{repo_a.owner}/#{repo_a.name}"
|
||||
assert graph_rows =~ "#{repo_b.owner}/#{repo_b.name}"
|
||||
|
||||
row_a = show |> element("#infestation-graph-node-#{repo_a.id}") |> render() |> squish()
|
||||
assert row_a =~ ">2</td>", "repo_a is hit twice and must report 2, not a blank cell"
|
||||
|
||||
row_b = show |> element("#infestation-graph-node-#{repo_b.id}") |> render() |> squish()
|
||||
assert row_b =~ ">1</td>", "repo_b is hit once and must report 1"
|
||||
|
||||
# 2 repository rows, 3 instance rows: the two views are different grains.
|
||||
ledger = show |> element("#infestation-ledger") |> render()
|
||||
assert length(Regex.scan(~r/<li[^>]+id="ledger_instances-/, ledger)) == 3
|
||||
end
|
||||
|
||||
describe "fix propagation" do
|
||||
setup %{conn: conn} do
|
||||
submitter = github_account_fixture()
|
||||
other = github_account_fixture()
|
||||
|
||||
source = listed_github_repository_fixture(submitter)
|
||||
{:ok, target} = Tarakan.Repositories.register_github_repository("acme/widget", other)
|
||||
target = listed_repository_fixture(target)
|
||||
|
||||
title = "Propagatable finding #{System.unique_integer([:positive])}"
|
||||
|
||||
findings =
|
||||
Jason.encode!(%{
|
||||
"tarakan_scan_format" => 1,
|
||||
"findings" => [
|
||||
%{
|
||||
"file" => "lib/handler.ex",
|
||||
"severity" => "critical",
|
||||
"title" => title,
|
||||
"description" => "Shared across both repositories."
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
source_scan = scan_fixture(source, submitter, %{"findings_json" => findings})
|
||||
scan_fixture(target, other, %{"findings_json" => findings})
|
||||
|
||||
# Settle the source instance as fixed so a template can be captured.
|
||||
[occurrence] = source_scan.findings
|
||||
|
||||
source_finding =
|
||||
Tarakan.Repo.get!(Tarakan.Scans.CanonicalFinding, occurrence.canonical_finding_id)
|
||||
|> Ecto.Changeset.change(%{
|
||||
status: "fixed",
|
||||
fixed_at: DateTime.utc_now(),
|
||||
fixed_commit_sha: source_scan.commit_sha
|
||||
})
|
||||
|> Tarakan.Repo.update!()
|
||||
|
||||
%Tarakan.Scans.FindingCheck{}
|
||||
|> Tarakan.Scans.FindingCheck.changeset(%{
|
||||
commit_sha: source_scan.commit_sha,
|
||||
verdict: "fixed",
|
||||
provenance: "human",
|
||||
notes: "Escaped the interpolated value and added a regression test."
|
||||
})
|
||||
|> Ecto.Changeset.put_change(:canonical_finding_id, source_finding.id)
|
||||
|> Ecto.Changeset.put_change(:account_id, submitter.id)
|
||||
|> Tarakan.Repo.insert!()
|
||||
|
||||
moderator = moderator_account_fixture()
|
||||
|
||||
{:ok, template} =
|
||||
Tarakan.Fixes.capture_fix(
|
||||
Tarakan.Accounts.Scope.for_account(moderator),
|
||||
source_finding
|
||||
)
|
||||
|
||||
%{
|
||||
conn: log_in_account(conn, moderator),
|
||||
pattern: FindingMemory.pattern_key(title),
|
||||
source: source,
|
||||
target: target,
|
||||
template: template
|
||||
}
|
||||
end
|
||||
|
||||
test "shows the captured fix and carries it to the remaining repository", %{
|
||||
conn: conn,
|
||||
pattern: pattern,
|
||||
source: source,
|
||||
target: target,
|
||||
template: template
|
||||
} do
|
||||
{:ok, view, html} = live(conn, ~p"/infestations/#{pattern}")
|
||||
|
||||
assert html =~ "Fixed on"
|
||||
assert html =~ "#{source.owner}/#{source.name}"
|
||||
assert html =~ "Escaped the interpolated value"
|
||||
assert has_element?(view, "#infestation-propagate-fix", "Propagate to 1")
|
||||
|
||||
html = view |> element("#infestation-propagate-fix") |> render_click()
|
||||
|
||||
assert html =~ "Opened 1 fix job"
|
||||
assert html =~ "carried to 1 repo"
|
||||
|
||||
# A real, public job now exists on the other repository, carrying the fix.
|
||||
# Scan submission auto-opens a verification job too, so reach the fix job
|
||||
# through the propagation rather than by repository alone.
|
||||
assert [propagation] = Tarakan.Fixes.list_propagations(template)
|
||||
assert propagation.repository_id == target.id
|
||||
|
||||
task = propagation.review_task
|
||||
assert task.status == "open"
|
||||
assert task.visibility == "public"
|
||||
assert task.description =~ "Escaped the interpolated value"
|
||||
end
|
||||
|
||||
test "anonymous visitors see the fix but cannot propagate it", %{pattern: pattern} do
|
||||
{:ok, view, html} = live(build_conn(), ~p"/infestations/#{pattern}")
|
||||
|
||||
assert html =~ "Fixed on"
|
||||
refute has_element?(view, "#infestation-propagate-fix")
|
||||
end
|
||||
end
|
||||
|
||||
# Collapse the whitespace HEEx leaves around interpolated cell values so
|
||||
# assertions can match on the value itself.
|
||||
defp squish(html) do
|
||||
html
|
||||
|> String.replace(~r/>\s+/, ">")
|
||||
|> String.replace(~r/\s+</, "<")
|
||||
end
|
||||
end
|
||||
59
test/tarakan_web/live/jobs_live_test.exs
Normal file
59
test/tarakan_web/live/jobs_live_test.exs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
defmodule TarakanWeb.JobsLiveTest do
|
||||
use TarakanWeb.ConnCase, async: true
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
import Tarakan.WorkFixtures
|
||||
|
||||
test "renders the public jobs queue for anonymous visitors", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/jobs")
|
||||
|
||||
assert html =~ "Jobs"
|
||||
assert html =~ "tarakan login"
|
||||
assert html =~ "tarakan --agent kimi --pickup"
|
||||
assert html =~ "tarakan worker --agent kimi"
|
||||
end
|
||||
|
||||
test "lists open public jobs", %{conn: conn} do
|
||||
submitter = account_fixture()
|
||||
repository = listed_github_repository_fixture(submitter)
|
||||
|
||||
task =
|
||||
review_task_fixture(repository, submitter, %{
|
||||
"title" => "Review auth middleware",
|
||||
"visibility" => "public"
|
||||
})
|
||||
|
||||
{:ok, view, html} = live(conn, ~p"/jobs")
|
||||
|
||||
assert has_element?(view, "#job-#{task.id}")
|
||||
assert html =~ "Review auth middleware"
|
||||
end
|
||||
|
||||
test "check jobs surface before ordinary open jobs", %{conn: conn} do
|
||||
submitter = account_fixture()
|
||||
repository = listed_github_repository_fixture(submitter)
|
||||
|
||||
ordinary =
|
||||
review_task_fixture(repository, submitter, %{
|
||||
"title" => "Ordinary code review job",
|
||||
"kind" => "code_review",
|
||||
"visibility" => "public"
|
||||
})
|
||||
|
||||
# Publishing a Report with findings auto-opens a verify_findings Job.
|
||||
_scan =
|
||||
scan_fixture(repository, submitter, %{"findings_json" => findings_json_fixture(1)})
|
||||
|
||||
check =
|
||||
Tarakan.Work.list_open_public_tasks(20)
|
||||
|> Enum.find(&(&1.kind == "verify_findings" and &1.repository_id == repository.id))
|
||||
|
||||
assert check
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/jobs")
|
||||
|
||||
check_pos = :binary.match(html, "job-#{check.id}") |> elem(0)
|
||||
ordinary_pos = :binary.match(html, "job-#{ordinary.id}") |> elem(0)
|
||||
assert check_pos < ordinary_pos
|
||||
end
|
||||
end
|
||||
111
test/tarakan_web/live/leaderboard_live_test.exs
Normal file
111
test/tarakan_web/live/leaderboard_live_test.exs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
defmodule TarakanWeb.LeaderboardLiveTest do
|
||||
use TarakanWeb.ConnCase
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
defp verified_contributor do
|
||||
submitter = github_account_fixture()
|
||||
repository = listed_github_repository_fixture(submitter)
|
||||
scan = scan_fixture(repository, submitter, %{"findings_json" => findings_json_fixture(1)})
|
||||
confirmation_fixture(scan, reviewer_account_fixture())
|
||||
confirmation_fixture(scan, reviewer_account_fixture())
|
||||
{submitter, repository}
|
||||
end
|
||||
|
||||
test "renders ranked contributors linking to their profiles", %{conn: conn} do
|
||||
{submitter, _repository} = verified_contributor()
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/leaderboard")
|
||||
|
||||
assert has_element?(view, "#leaderboard-rank-1")
|
||||
assert has_element?(view, ~s(#leaderboard a[href="/#{submitter.handle}"]))
|
||||
# The distinct verified canonical finding (+30) puts the submitter on top;
|
||||
# the report occurrence itself is not a second reputation event.
|
||||
assert has_element?(view, "#leaderboard-rank-1", submitter.handle)
|
||||
assert has_element?(view, "#leaderboard-rank-1", "30")
|
||||
end
|
||||
|
||||
test "the leaderboard can be re-sorted", %{conn: conn} do
|
||||
verified_contributor()
|
||||
{:ok, view, _html} = live(conn, ~p"/leaderboard")
|
||||
|
||||
html = view |> element("#leaderboard-sort button[phx-value-by='findings']") |> render_click()
|
||||
assert_patch(view, ~p"/leaderboard?sort=findings")
|
||||
assert html =~ ~s(aria-pressed="true")
|
||||
end
|
||||
|
||||
test "filter controls render and patching filters the entries", %{conn: conn} do
|
||||
{submitter, _repository} = verified_contributor()
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/leaderboard")
|
||||
|
||||
assert has_element?(view, "#leaderboard-severity")
|
||||
assert has_element?(view, "#leaderboard-window")
|
||||
assert has_element?(view, "#leaderboard-rank-1")
|
||||
|
||||
# The fixture's findings are "high"; filtering to critical empties the board.
|
||||
view |> element("#leaderboard-severity-form") |> render_change(%{"severity" => "critical"})
|
||||
assert_patch(view, ~p"/leaderboard?severity=critical")
|
||||
assert has_element?(view, "#leaderboard-empty")
|
||||
refute has_element?(view, "#leaderboard-rank-1")
|
||||
|
||||
# Clearing the severity brings the contributor back; the window tabs patch too.
|
||||
view |> element("#leaderboard-severity-form") |> render_change(%{"severity" => ""})
|
||||
assert has_element?(view, "#leaderboard-rank-1", submitter.handle)
|
||||
|
||||
view |> element("#leaderboard-window button[phx-value-window='7']") |> render_click()
|
||||
assert_patch(view, ~p"/leaderboard?window=7")
|
||||
assert has_element?(view, "#leaderboard-rank-1", submitter.handle)
|
||||
end
|
||||
|
||||
test "rows surface slashed stakes", %{conn: conn} do
|
||||
{submitter, repository} = verified_contributor()
|
||||
|
||||
# A second report (a distinct finding) is refuted by a dispute quorum.
|
||||
refuted =
|
||||
scan_fixture(repository, submitter, %{
|
||||
"findings_json" =>
|
||||
Jason.encode!(%{
|
||||
"tarakan_scan_format" => 1,
|
||||
"findings" => [
|
||||
%{
|
||||
"file" => "lib/example/other.ex",
|
||||
"severity" => "high",
|
||||
"title" => "Refuted finding #{System.unique_integer([:positive])}",
|
||||
"description" => "Slashed stake fixture."
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
confirmation_fixture(refuted, reviewer_account_fixture(), "disputed")
|
||||
confirmation_fixture(refuted, reviewer_account_fixture(), "disputed")
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/leaderboard")
|
||||
assert has_element?(view, "#leaderboard-rank-1", "1 slashed")
|
||||
end
|
||||
|
||||
test "an empty record shows the empty state", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/leaderboard")
|
||||
assert has_element?(view, "#leaderboard-empty")
|
||||
end
|
||||
|
||||
test "rows show earned badge chips after the handle", %{conn: conn} do
|
||||
{submitter, _repository} = verified_contributor()
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/leaderboard")
|
||||
|
||||
assert has_element?(view, "#leaderboard-rank-1-badge-first_blood", "First blood")
|
||||
refute has_element?(view, "#leaderboard-rank-1-badge-century")
|
||||
|
||||
# The reviewer accounts checked the report but earned no badges, so their
|
||||
# rows (if ranked) carry no chips.
|
||||
html = render(view)
|
||||
assert html =~ submitter.handle
|
||||
end
|
||||
|
||||
test "the header links to the leaderboard", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/")
|
||||
assert has_element?(view, ~s(a[href="/leaderboard"]))
|
||||
end
|
||||
end
|
||||
72
test/tarakan_web/live/model_analytics_live_test.exs
Normal file
72
test/tarakan_web/live/model_analytics_live_test.exs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
defmodule TarakanWeb.ModelAnalyticsLiveTest do
|
||||
use TarakanWeb.ConnCase, async: true
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Tarakan.Repo
|
||||
alias Tarakan.Scans.CanonicalFinding
|
||||
|
||||
defp confirm_findings(scan) do
|
||||
for occurrence <- scan.findings do
|
||||
CanonicalFinding
|
||||
|> Repo.get!(occurrence.canonical_finding_id)
|
||||
|> Ecto.Changeset.change(status: "verified")
|
||||
|> Repo.update!()
|
||||
end
|
||||
end
|
||||
|
||||
test "says so plainly when nothing has been reported", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/models")
|
||||
|
||||
assert html =~ "No model has reported enough findings yet"
|
||||
end
|
||||
|
||||
# The scoreboard hides models below five reported findings, so both models
|
||||
# need a real body of work before they show up at all.
|
||||
defp unrelated_findings_json(count) do
|
||||
findings =
|
||||
for index <- 1..count do
|
||||
%{
|
||||
"file" => "lib/unrelated/module_#{index}.ex",
|
||||
"line_start" => index * 7,
|
||||
"line_end" => index * 7 + 2,
|
||||
"severity" => "low",
|
||||
"title" => "Unrelated observation (#{index})",
|
||||
"description" => "Something else entirely."
|
||||
}
|
||||
end
|
||||
|
||||
Jason.encode!(%{"tarakan_scan_format" => 1, "findings" => findings})
|
||||
end
|
||||
|
||||
test "scores models and lists the selected model's blind spots", %{conn: conn} do
|
||||
account = github_account_fixture()
|
||||
repository = github_repository_fixture(account)
|
||||
|
||||
found =
|
||||
scan_fixture(repository, account, %{
|
||||
"model" => "sharp-model",
|
||||
"findings_json" => findings_json_fixture(5)
|
||||
})
|
||||
|
||||
confirm_findings(found)
|
||||
|
||||
scan_fixture(repository, account, %{
|
||||
"model" => "blunt-model",
|
||||
"findings_json" => unrelated_findings_json(5)
|
||||
})
|
||||
|
||||
{:ok, view, html} = live(conn, ~p"/models")
|
||||
|
||||
assert html =~ "sharp-model"
|
||||
assert html =~ "blunt-model"
|
||||
|
||||
html =
|
||||
view
|
||||
|> element("#model-row-blunt-model")
|
||||
|> render_click()
|
||||
|
||||
assert html =~ "Blind spots"
|
||||
assert html =~ "Unsanitized input reaches interpolated query"
|
||||
end
|
||||
end
|
||||
207
test/tarakan_web/live/moderation_live_test.exs
Normal file
207
test/tarakan_web/live/moderation_live_test.exs
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
defmodule TarakanWeb.ModerationLiveTest do
|
||||
use TarakanWeb.ConnCase
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Tarakan.Accounts
|
||||
alias Tarakan.Accounts.Scope
|
||||
alias Tarakan.Moderation
|
||||
|
||||
test "reporting requires an authenticated account", %{conn: conn} do
|
||||
assert {:error, {:redirect, %{to: "/accounts/log-in"}}} =
|
||||
live(conn, ~p"/moderation/report")
|
||||
end
|
||||
|
||||
test "a participant submits a restricted report and can inspect its case", %{conn: conn} do
|
||||
reporter = account_fixture()
|
||||
subject = account_fixture()
|
||||
conn = log_in_account(conn, reporter)
|
||||
|
||||
{:ok, view, _html} =
|
||||
live(conn, ~p"/moderation/report?subject_type=account&subject_id=#{subject.id}")
|
||||
|
||||
assert has_element?(view, "#moderation-report-form")
|
||||
assert has_element?(view, "#header-report-content")
|
||||
refute has_element?(view, "#header-moderation-queue")
|
||||
|
||||
view
|
||||
|> form("#moderation-report-form",
|
||||
report: %{
|
||||
subject_type: "account",
|
||||
subject_id: subject.id,
|
||||
reason: "harassment",
|
||||
description: "This account is posting targeted harassment in public review evidence."
|
||||
}
|
||||
)
|
||||
|> render_submit()
|
||||
|
||||
{path, flash} = assert_redirect(view)
|
||||
assert path =~ ~r{^/moderation/cases/\d+$}
|
||||
assert flash["info"]
|
||||
|
||||
{:ok, case_view, _html} = live(conn, path)
|
||||
|
||||
assert has_element?(case_view, "#moderation-case")
|
||||
assert has_element?(case_view, "#moderation-case-status", "Open")
|
||||
refute has_element?(case_view, "#moderation-case-actions")
|
||||
refute has_element?(case_view, "#moderation-case-assignment")
|
||||
end
|
||||
|
||||
test "an ordinary account cannot open the moderator queue", %{conn: conn} do
|
||||
conn = log_in_account(conn, account_fixture())
|
||||
|
||||
assert {:error, {:redirect, %{to: "/"}}} = live(conn, ~p"/moderation/queue")
|
||||
end
|
||||
|
||||
test "a moderator assigns and resolves a queued report", %{conn: conn} do
|
||||
case_record = report_account!(account_fixture(), account_fixture())
|
||||
moderator = moderator_account_fixture()
|
||||
conn = log_in_account(conn, moderator)
|
||||
|
||||
{:ok, queue_view, _html} = live(conn, ~p"/moderation/queue")
|
||||
|
||||
assert has_element?(queue_view, "#header-moderation-queue")
|
||||
assert has_element?(queue_view, "#moderation-case-count", "1 open")
|
||||
assert has_element?(queue_view, "#assign-moderation-case-#{case_record.id}")
|
||||
|
||||
queue_view
|
||||
|> element("#assign-moderation-case-#{case_record.id}")
|
||||
|> render_click()
|
||||
|
||||
assert has_element?(queue_view, "#cases-#{case_record.id}", "In review")
|
||||
|
||||
{:ok, case_view, _html} = live(conn, ~p"/moderation/cases/#{case_record.id}")
|
||||
assert has_element?(case_view, "#moderation-case-decision-form")
|
||||
|
||||
case_view
|
||||
|> form("#moderation-case-decision-form",
|
||||
resolution: %{
|
||||
reason: "The evidence was independently reviewed and confirms a policy violation."
|
||||
}
|
||||
)
|
||||
|> render_submit(%{"disposition" => "resolved"})
|
||||
|
||||
assert has_element?(case_view, "#moderation-case-status", "Resolved")
|
||||
assert has_element?(case_view, "#moderation-case-resolution")
|
||||
refute has_element?(case_view, "#moderation-case-decision-form")
|
||||
end
|
||||
|
||||
test "the subject can appeal without receiving participant identity data", %{conn: conn} do
|
||||
reporter = account_fixture()
|
||||
subject = account_fixture()
|
||||
resolver = moderator_account_fixture()
|
||||
|
||||
case_record =
|
||||
reporter
|
||||
|> report_account!(subject)
|
||||
|> resolve_case!(resolver, "resolved")
|
||||
|
||||
conn = log_in_account(conn, subject)
|
||||
{:ok, view, _html} = live(conn, ~p"/moderation/cases/#{case_record.id}")
|
||||
|
||||
assert has_element?(view, "#moderation-appeal-form")
|
||||
refute has_element?(view, "#moderation-case-actions")
|
||||
|
||||
view
|
||||
|> form("#moderation-appeal-form",
|
||||
appeal: %{
|
||||
reason: "The decision cites evidence from a different account and should be reconsidered."
|
||||
}
|
||||
)
|
||||
|> render_submit()
|
||||
|
||||
assert has_element?(view, "#moderation-case-appeals")
|
||||
assert has_element?(view, "#moderation-case-appeals", "Open")
|
||||
refute has_element?(view, "#moderation-appeal-form")
|
||||
refute has_element?(view, "#moderation-case-actions")
|
||||
end
|
||||
|
||||
test "an independent moderator decides an appeal", %{conn: conn} do
|
||||
reporter = account_fixture()
|
||||
subject = account_fixture()
|
||||
resolver = moderator_account_fixture()
|
||||
appeal_moderator = moderator_account_fixture()
|
||||
|
||||
case_record =
|
||||
reporter
|
||||
|> report_account!(subject)
|
||||
|> resolve_case!(resolver, "resolved")
|
||||
|
||||
{:ok, appeal} =
|
||||
Moderation.appeal(Accounts.scope_for_account(subject), case_record, %{
|
||||
"reason" =>
|
||||
"The original decision relied on unrelated evidence and needs independent review."
|
||||
})
|
||||
|
||||
conn = log_in_account(conn, appeal_moderator)
|
||||
{:ok, view, _html} = live(conn, ~p"/moderation/cases/#{case_record.id}")
|
||||
|
||||
assert has_element?(view, "#moderation-appeal-decision-form-#{appeal.id}")
|
||||
|
||||
view
|
||||
|> form("#moderation-appeal-decision-form-#{appeal.id}",
|
||||
appeal_decision: %{
|
||||
appeal_id: appeal.id,
|
||||
reason: "Independent review confirms the original evidence was unrelated."
|
||||
}
|
||||
)
|
||||
|> render_submit(%{"decision" => "upheld"})
|
||||
|
||||
assert has_element?(view, "#moderation-case-status", "Overturned")
|
||||
assert has_element?(view, "#moderation-appeal-#{appeal.id}", "Upheld")
|
||||
refute has_element?(view, "#moderation-appeal-decision-form-#{appeal.id}")
|
||||
end
|
||||
|
||||
test "an expired sudo session redirects resolution to re-authentication", %{conn: conn} do
|
||||
case_record = report_account!(account_fixture(), account_fixture())
|
||||
moderator = moderator_account_fixture()
|
||||
|
||||
{:ok, assigned} = Moderation.assign(Scope.for_account(moderator), case_record)
|
||||
|
||||
# Outside the two-hour sudo window.
|
||||
stale_at = DateTime.add(DateTime.utc_now(), -3, :hour)
|
||||
conn = log_in_account(conn, moderator, token_authenticated_at: stale_at)
|
||||
{:ok, case_view, _html} = live(conn, ~p"/moderation/cases/#{assigned.id}")
|
||||
|
||||
assert has_element?(case_view, "#moderation-case-decision-form")
|
||||
|
||||
assert {:error,
|
||||
{:live_redirect, %{to: "/accounts/log-in?return_to=%2Fmoderation%2Fcases%2F" <> _}}} =
|
||||
case_view
|
||||
|> form("#moderation-case-decision-form",
|
||||
resolution: %{
|
||||
reason: "The evidence was independently reviewed and confirms a violation."
|
||||
}
|
||||
)
|
||||
|> render_submit(%{"disposition" => "resolved"})
|
||||
|
||||
{:ok, unresolved} = Moderation.get_case(Scope.for_account(moderator), assigned.id)
|
||||
assert unresolved.status == "in_review"
|
||||
end
|
||||
|
||||
defp report_account!(reporter, subject) do
|
||||
{:ok, case_record} =
|
||||
Moderation.report(Scope.for_account(reporter), %{
|
||||
"subject_type" => "account",
|
||||
"subject_id" => subject.id,
|
||||
"reason" => "fabricated_evidence",
|
||||
"description" => "The submitted evidence appears fabricated and needs independent review."
|
||||
})
|
||||
|
||||
case_record
|
||||
end
|
||||
|
||||
defp resolve_case!(case_record, moderator, disposition) do
|
||||
{:ok, assigned} = Moderation.assign(Scope.for_account(moderator), case_record)
|
||||
|
||||
{:ok, decided} =
|
||||
Moderation.resolve(
|
||||
Scope.for_account(moderator),
|
||||
assigned,
|
||||
disposition,
|
||||
"The evidence was independently reviewed and supports this moderation decision."
|
||||
)
|
||||
|
||||
decided
|
||||
end
|
||||
end
|
||||
98
test/tarakan_web/live/page_live_test.exs
Normal file
98
test/tarakan_web/live/page_live_test.exs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
defmodule TarakanWeb.PageLiveTest do
|
||||
use TarakanWeb.ConnCase
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
test "the disclosure policy page renders its sections", %{conn: conn} do
|
||||
html = conn |> get(~p"/policies/disclosure") |> html_response(200)
|
||||
|
||||
assert html =~ "Disclosure policy"
|
||||
assert html =~ "Public by default"
|
||||
assert html =~ "Visibility levels"
|
||||
assert html =~ "public_summary"
|
||||
assert html =~ "restricted"
|
||||
assert html =~ "Verification quorum"
|
||||
assert html =~ "Vendor notification"
|
||||
assert html =~ "security.txt"
|
||||
end
|
||||
|
||||
test "the content policy page renders its sections", %{conn: conn} do
|
||||
html = conn |> get(~p"/policies/content") |> html_response(200)
|
||||
|
||||
assert html =~ "Content policy"
|
||||
assert html =~ "Allowed content"
|
||||
assert html =~ "Prohibited content"
|
||||
assert html =~ "Secrets and credentials"
|
||||
assert html =~ "Takedowns and appeals"
|
||||
end
|
||||
|
||||
test "the pricing page renders the free record, bounties, and managed disclosure",
|
||||
%{conn: conn} do
|
||||
html = conn |> get(~p"/pricing") |> html_response(200)
|
||||
|
||||
assert html =~ ~s(id="pricing-free")
|
||||
assert html =~ ~s(id="pricing-contracts")
|
||||
assert html =~ ~s(id="pricing-managed")
|
||||
assert html =~ "Tarakan keeps 10%"
|
||||
assert html =~ "mailto:"
|
||||
|
||||
# No paid tiers, and the user-facing word is "contract", never "bounty".
|
||||
refute html =~ ~s(id="pricing-team")
|
||||
refute html =~ "Vaccine packs"
|
||||
refute html =~ "bounty"
|
||||
refute html =~ "Bounties"
|
||||
end
|
||||
|
||||
test "the pricing checkout POST redirects anonymous visitors to login", %{conn: conn} do
|
||||
conn = post(conn, ~p"/billing/checkout", %{"plan" => "team"})
|
||||
|
||||
assert redirected_to(conn) =~ "/accounts/log-in"
|
||||
end
|
||||
|
||||
test "the managed disclosure page renders its sections", %{conn: conn} do
|
||||
html = conn |> get(~p"/services/disclosure") |> html_response(200)
|
||||
|
||||
assert html =~ "Managed disclosure"
|
||||
assert html =~ "What it is"
|
||||
assert html =~ "Handling, not suppression"
|
||||
assert html =~ "Intake"
|
||||
assert html =~ ~s(id="managed-disclosure-contact")
|
||||
assert html =~ "mailto:"
|
||||
end
|
||||
|
||||
test "the pricing checkout POST redirects to Stripe for members", %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|> log_in_account(account_fixture())
|
||||
|> post(~p"/billing/checkout", %{"plan" => "enterprise"})
|
||||
|
||||
assert redirected_to(conn) =~ "https://checkout.stripe.com/"
|
||||
end
|
||||
|
||||
# These were plain controller pages, the only routes in the shell without a
|
||||
# LiveSocket. That made navigating to them a full reload and left them out of
|
||||
# anything driven by live navigation.
|
||||
test "the static pages are LiveViews", %{conn: conn} do
|
||||
for path <- ["/pricing", "/policies/disclosure", "/policies/content", "/services/disclosure"] do
|
||||
assert {:ok, _view, _html} = live(conn, path)
|
||||
end
|
||||
end
|
||||
|
||||
test "pricing marks its own nav entry active", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/pricing")
|
||||
|
||||
assert html =~ ~r/href="\/pricing"[^>]*aria-current="page"/
|
||||
end
|
||||
|
||||
test "nav links live-navigate between a LiveView and a policy page", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/leaderboard")
|
||||
|
||||
assert {:ok, _pricing, html} =
|
||||
view
|
||||
|> element(~s(nav[aria-label="Primary"] a[href="/pricing"]))
|
||||
|> render_click()
|
||||
|> follow_redirect(conn, ~p"/pricing")
|
||||
|
||||
assert html =~ "Pricing"
|
||||
end
|
||||
end
|
||||
101
test/tarakan_web/live/profile_live_test.exs
Normal file
101
test/tarakan_web/live/profile_live_test.exs
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
defmodule TarakanWeb.ProfileLiveTest do
|
||||
use TarakanWeb.ConnCase
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
test "renders a contributor profile with public contribution counts", %{conn: conn} do
|
||||
submitter = github_account_fixture()
|
||||
repository = listed_github_repository_fixture(submitter)
|
||||
scan = scan_fixture(repository, submitter, %{"findings_json" => findings_json_fixture(2)})
|
||||
confirmation_fixture(scan, reviewer_account_fixture())
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/#{submitter.handle}")
|
||||
|
||||
assert has_element?(view, "#profile-handle", submitter.handle)
|
||||
assert has_element?(view, "#profile-review-count", "1")
|
||||
assert has_element?(view, "#profile-finding-count", "2")
|
||||
assert has_element?(view, "#profile-repo-count", "1")
|
||||
assert has_element?(view, "#profile-reviews", "reviewed")
|
||||
assert has_element?(view, "#profile-repositories", repository.name)
|
||||
assert has_element?(view, "#profile-findings")
|
||||
end
|
||||
|
||||
test "a moderator profile shows the role badge", %{conn: conn} do
|
||||
moderator = moderator_account_fixture()
|
||||
{:ok, view, _html} = live(conn, ~p"/#{moderator.handle}")
|
||||
assert has_element?(view, "#profile-role", "Moderator")
|
||||
end
|
||||
|
||||
test "an unknown handle returns not found", %{conn: conn} do
|
||||
assert_error_sent 404, fn -> get(conn, ~p"/#{"ghost-nobody"}") end
|
||||
end
|
||||
|
||||
test "a submitter handle on the security page links to the profile", %{conn: conn} do
|
||||
submitter = github_account_fixture()
|
||||
repository = listed_github_repository_fixture(submitter)
|
||||
scan_fixture(repository, submitter, %{"findings_json" => findings_json_fixture(1)})
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/github.com/openai/codex/security")
|
||||
assert has_element?(view, ~s(a[href="/#{submitter.handle}"]))
|
||||
end
|
||||
|
||||
test "finding rows link to the finding page", %{conn: conn} do
|
||||
submitter = github_account_fixture()
|
||||
repository = listed_github_repository_fixture(submitter)
|
||||
scan = scan_fixture(repository, submitter, %{"findings_json" => findings_json_fixture(1)})
|
||||
finding = hd(scan.findings || Tarakan.Repo.preload(scan, :findings).findings)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/#{submitter.handle}")
|
||||
|
||||
assert has_element?(
|
||||
view,
|
||||
~s(#profile-finding-#{finding.public_id} a[href="/findings/#{finding.public_id}"])
|
||||
)
|
||||
end
|
||||
|
||||
test "renders the credits tile and the recent ledger", %{conn: conn} do
|
||||
account = account_fixture()
|
||||
|
||||
{:ok, _entry} =
|
||||
Tarakan.Credits.credit(account, :mint_finding_verified, {"canonical_finding", 1})
|
||||
|
||||
{:ok, _entry} = Tarakan.Credits.debit(account, :spend_priority_boost, nil, 20)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/#{account.handle}")
|
||||
|
||||
assert has_element?(view, "#profile-credit-balance", "30")
|
||||
assert has_element?(view, "#profile-credits", "mint finding verified")
|
||||
assert has_element?(view, "#profile-credits", "+50")
|
||||
assert has_element?(view, "#profile-credits", "-20")
|
||||
assert has_element?(view, "#profile-credits", "balance 30")
|
||||
end
|
||||
|
||||
test "renders an empty credits ledger", %{conn: conn} do
|
||||
account = account_fixture()
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/#{account.handle}")
|
||||
|
||||
assert has_element?(view, "#profile-credit-balance", "0")
|
||||
assert has_element?(view, "#profile-no-credits")
|
||||
end
|
||||
|
||||
test "the badge strip renders earned badges only", %{conn: conn} do
|
||||
submitter = github_account_fixture()
|
||||
repository = listed_github_repository_fixture(submitter)
|
||||
scan_fixture(repository, submitter, %{"findings_json" => findings_json_fixture(1)})
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/#{submitter.handle}")
|
||||
|
||||
assert has_element?(view, "#profile-badges")
|
||||
assert has_element?(view, "#profile-badge-first_blood", "First blood")
|
||||
refute has_element?(view, "#profile-badge-century")
|
||||
refute has_element?(view, "#profile-badge-sharpshooter")
|
||||
end
|
||||
|
||||
test "a profile without earned badges renders no badge strip", %{conn: conn} do
|
||||
account = account_fixture()
|
||||
{:ok, view, _html} = live(conn, ~p"/#{account.handle}")
|
||||
|
||||
refute has_element?(view, "#profile-badges")
|
||||
end
|
||||
end
|
||||
411
test/tarakan_web/live/repository_code_live_test.exs
Normal file
411
test/tarakan_web/live/repository_code_live_test.exs
Normal file
|
|
@ -0,0 +1,411 @@
|
|||
defmodule TarakanWeb.RepositoryCodeLiveTest do
|
||||
use TarakanWeb.ConnCase, async: false
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Tarakan.Accounts.Scope
|
||||
alias Tarakan.RepositoryCode.Cache
|
||||
alias Tarakan.Scans
|
||||
|
||||
@default_commit_sha String.duplicate("7", 40)
|
||||
# develop branch tip in the GitHub stub (must remain a real branch tip).
|
||||
@develop_commit_sha String.duplicate("8", 40)
|
||||
# A fetchable commit that is not the tip of any published branch (and not a
|
||||
# special-cased stub SHA such as all-c truncated trees).
|
||||
@historical_commit_sha String.duplicate("b", 40)
|
||||
|
||||
setup do
|
||||
Cache.clear()
|
||||
repository = listed_github_repository_fixture()
|
||||
%{repository: repository}
|
||||
end
|
||||
|
||||
test "repository roots open the code browser by default", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/github.com/openai/codex")
|
||||
render_async(view, 1_000)
|
||||
|
||||
assert has_element?(view, "#repository-navigation")
|
||||
assert has_element?(view, "#repository-code-tab[aria-current='page']", "Code")
|
||||
|
||||
assert has_element?(
|
||||
view,
|
||||
"#repository-overview-tab[href='/github.com/openai/codex/security']"
|
||||
)
|
||||
|
||||
assert has_element?(view, "#code-tree")
|
||||
end
|
||||
|
||||
test "the legacy code entry route still opens the default tree", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/github.com/openai/codex/code")
|
||||
render_async(view, 1_000)
|
||||
|
||||
assert has_element?(view, "#code-tree")
|
||||
assert has_element?(view, "#code-commit-sha", String.slice(@default_commit_sha, 0, 7))
|
||||
end
|
||||
|
||||
test "browses directories at the freshly resolved default commit", %{conn: conn} do
|
||||
{:ok, view, _html} =
|
||||
live(conn, ~p"/github.com/openai/codex/code/#{@default_commit_sha}")
|
||||
|
||||
render_async(view, 1_000)
|
||||
|
||||
assert has_element?(view, "#code-tree")
|
||||
assert has_element?(view, "#code-tree-entries [id^='code-entry-']", "lib")
|
||||
|
||||
assert has_element?(
|
||||
view,
|
||||
"#code-tree-entries a[href='/github.com/openai/codex/code/#{@default_commit_sha}/README.md']"
|
||||
)
|
||||
end
|
||||
|
||||
test "round-trips encoded spaces, percent signs, fragments, and Unicode filenames", %{
|
||||
conn: conn
|
||||
} do
|
||||
root = "/github.com/openai/codex/code/#{@default_commit_sha}"
|
||||
{:ok, tree_view, _html} = live(conn, root)
|
||||
render_async(tree_view, 1_000)
|
||||
|
||||
assert has_element?(
|
||||
tree_view,
|
||||
"a[href='#{root}/space%20%23%20100%25.txt']",
|
||||
"space # 100%.txt"
|
||||
)
|
||||
|
||||
assert has_element?(tree_view, "a[href='#{root}/%CE%BB.ex']", "λ.ex")
|
||||
|
||||
{:ok, encoded_view, _html} = live(conn, root <> "/space%20%23%20100%25.txt")
|
||||
render_async(encoded_view, 1_000)
|
||||
assert has_element?(encoded_view, "#code-file", "space # 100%.txt")
|
||||
assert has_element?(encoded_view, "#L1 code", "# Codex")
|
||||
|
||||
{:ok, unicode_view, _html} = live(conn, root <> "/%CE%BB.ex")
|
||||
render_async(unicode_view, 1_000)
|
||||
assert has_element?(unicode_view, "#code-file", "λ.ex")
|
||||
assert has_element?(unicode_view, "#L1 code", "defmodule Codex")
|
||||
end
|
||||
|
||||
test "folder and file rows show findings for the pinned commit", %{
|
||||
conn: conn,
|
||||
repository: repository
|
||||
} do
|
||||
scan = finding_scan_fixture(repository, account_fixture(), @default_commit_sha)
|
||||
_scan = publish_full_scan(repository, scan)
|
||||
|
||||
{:ok, root_view, _html} = live(conn, ~p"/github.com/openai/codex")
|
||||
render_async(root_view, 1_000)
|
||||
|
||||
assert has_element?(root_view, "#code-entry-bGli-findings", "1 finding")
|
||||
assert has_element?(root_view, "#code-reference", "1 finding")
|
||||
|
||||
{:ok, lib_view, _html} =
|
||||
live(conn, ~p"/github.com/openai/codex/code/#{@default_commit_sha}/lib")
|
||||
|
||||
render_async(lib_view, 1_000)
|
||||
|
||||
assert has_element?(lib_view, "#code-entry-bGliL2NvZGV4LmV4-findings", "1 finding")
|
||||
end
|
||||
|
||||
test "public summaries do not leak file paths through code-row badges", %{
|
||||
conn: conn,
|
||||
repository: repository
|
||||
} do
|
||||
scan = finding_scan_fixture(repository, account_fixture(), @default_commit_sha)
|
||||
scan = confirmation_fixture(scan, reviewer_account_fixture())
|
||||
scan = confirmation_fixture(scan, reviewer_account_fixture())
|
||||
moderator_scope = Scope.for_account(moderator_account_fixture())
|
||||
|
||||
{:ok, accepted} = Scans.accept_scan(moderator_scope, scan, moderation_attributes())
|
||||
|
||||
{:ok, _summary} =
|
||||
Scans.update_visibility(
|
||||
moderator_scope,
|
||||
accepted,
|
||||
"public_summary",
|
||||
moderation_attributes()
|
||||
)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/github.com/openai/codex")
|
||||
render_async(view, 1_000)
|
||||
html = render(view)
|
||||
|
||||
refute has_element?(view, "[id$='-findings']")
|
||||
refute html =~ "lib/codex.ex"
|
||||
end
|
||||
|
||||
test "renders escaped plain-text lines with stable anchors and selection", %{conn: conn} do
|
||||
{:ok, view, _html} =
|
||||
live(
|
||||
conn,
|
||||
~p"/github.com/openai/codex/code/#{@default_commit_sha}/README.md?lines=1"
|
||||
)
|
||||
|
||||
render_async(view, 1_000)
|
||||
|
||||
assert has_element?(view, "#code-file")
|
||||
assert has_element?(view, "#L1.bg-panel")
|
||||
assert has_element?(view, "#L1 code", "# Codex")
|
||||
|
||||
assert has_element?(
|
||||
view,
|
||||
"#L1 a[href='/github.com/openai/codex/code/#{@default_commit_sha}/README.md?lines=1#L1']"
|
||||
)
|
||||
end
|
||||
|
||||
test "rejects an out-of-range single-line selection", %{conn: conn} do
|
||||
{:ok, view, _html} =
|
||||
live(
|
||||
conn,
|
||||
~p"/github.com/openai/codex/code/#{@default_commit_sha}/README.md?lines=1000001"
|
||||
)
|
||||
|
||||
render_async(view, 1_000)
|
||||
|
||||
assert has_element?(view, "#invalid-line-selection")
|
||||
refute has_element?(view, "#code-lines .bg-panel")
|
||||
end
|
||||
|
||||
test "renders hostile source as escaped text rather than executable markup", %{conn: conn} do
|
||||
{:ok, view, _html} =
|
||||
live(
|
||||
conn,
|
||||
~p"/github.com/openai/codex/code/#{@default_commit_sha}/unsafe.html"
|
||||
)
|
||||
|
||||
render_async(view, 1_000)
|
||||
html = render(view)
|
||||
|
||||
assert has_element?(view, "#L1 code", "</code><script id=\"source-xss\">alert(1)</script>")
|
||||
refute has_element?(view, "script#source-xss")
|
||||
assert html =~ "</code><script id="source-xss">"
|
||||
end
|
||||
|
||||
test "rejects guessed historical SHAs on the generic browser", %{conn: conn} do
|
||||
paths = [
|
||||
~p"/github.com/openai/codex/code/#{@historical_commit_sha}",
|
||||
~p"/github.com/openai/codex/code/#{@historical_commit_sha}/README.md"
|
||||
]
|
||||
|
||||
for path <- paths do
|
||||
{:ok, view, _html} = live(conn, path)
|
||||
render_async(view, 1_000)
|
||||
|
||||
assert has_element?(view, "#code-browser-error")
|
||||
assert has_element?(view, "#code-browser-error", "Source not found")
|
||||
refute has_element?(view, "#code-tree")
|
||||
refute has_element?(view, "#code-file")
|
||||
end
|
||||
end
|
||||
|
||||
test "branch switch opens another published branch tip", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/github.com/openai/codex/code/#{@default_commit_sha}")
|
||||
render_async(view, 1_000)
|
||||
|
||||
assert has_element?(view, "#code-branch-select")
|
||||
assert has_element?(view, "#code-tree")
|
||||
|
||||
view
|
||||
|> form("#code-branch-form", %{branch: "develop"})
|
||||
|> render_change()
|
||||
|
||||
path = ~p"/github.com/openai/codex/code/#{@develop_commit_sha}"
|
||||
assert_redirect(view, path)
|
||||
|
||||
{:ok, switched, _html} = live(conn, path)
|
||||
render_async(switched, 1_000)
|
||||
|
||||
assert has_element?(switched, "#code-tree")
|
||||
assert has_element?(switched, "#code-commit-sha", String.slice(@develop_commit_sha, 0, 7))
|
||||
assert has_element?(switched, ~s(#code-branch-select option[value="develop"][selected]))
|
||||
end
|
||||
|
||||
test "deep link to a non-default branch tip works", %{conn: conn} do
|
||||
{:ok, view, _html} =
|
||||
live(conn, ~p"/github.com/openai/codex/code/#{@develop_commit_sha}/README.md")
|
||||
|
||||
render_async(view, 1_000)
|
||||
|
||||
assert has_element?(view, "#code-file")
|
||||
assert has_element?(view, "#code-commit-sha", String.slice(@develop_commit_sha, 0, 7))
|
||||
assert has_element?(view, ~s(#code-branch-select option[value="develop"][selected]))
|
||||
end
|
||||
|
||||
test "rejects encoded traversal and backslash paths at the route boundary", %{conn: conn} do
|
||||
unsafe_paths = [
|
||||
"/github.com/openai/codex/code/#{@default_commit_sha}/lib/%2E%2E/README.md",
|
||||
"/github.com/openai/codex/code/#{@default_commit_sha}/lib/%5C..%5Csecret"
|
||||
]
|
||||
|
||||
for path <- unsafe_paths do
|
||||
{:ok, view, _html} = live(conn, path)
|
||||
|
||||
assert has_element?(view, "#code-browser-error", "Invalid path")
|
||||
refute has_element?(view, "#code-file")
|
||||
end
|
||||
end
|
||||
|
||||
test "an authorized opaque finding route opens historical source without leaking path in links",
|
||||
%{
|
||||
conn: conn,
|
||||
repository: repository
|
||||
} do
|
||||
submitter = account_fixture()
|
||||
scan = finding_scan_fixture(repository, submitter, @historical_commit_sha)
|
||||
finding = hd(scan.findings)
|
||||
|
||||
{:ok, view, _html} =
|
||||
live(log_in_account(conn, submitter), ~p"/findings/#{finding.public_id}/code")
|
||||
|
||||
render_async(view, 1_000)
|
||||
|
||||
assert has_element?(view, "#finding-code-context")
|
||||
assert has_element?(view, "#code-file")
|
||||
assert has_element?(view, "#L2.bg-panel")
|
||||
refute has_element?(view, "#source-breadcrumbs a")
|
||||
assert has_element?(view, "#L2 a[href='/findings/#{finding.public_id}/code#L2']")
|
||||
end
|
||||
|
||||
test "the opaque route does not reveal a moderator-restricted finding to anonymous visitors",
|
||||
%{
|
||||
conn: conn,
|
||||
repository: repository
|
||||
} do
|
||||
scan = finding_scan_fixture(repository, account_fixture(), @historical_commit_sha)
|
||||
finding = hd(scan.findings)
|
||||
|
||||
moderator_scope = Scope.for_account(moderator_account_fixture())
|
||||
|
||||
{:ok, _restricted} =
|
||||
Scans.update_visibility(moderator_scope, scan, "restricted", moderation_attributes())
|
||||
|
||||
assert_raise Ecto.NoResultsError, fn ->
|
||||
live(conn, ~p"/findings/#{finding.public_id}/code")
|
||||
end
|
||||
end
|
||||
|
||||
test "marks a reported line range that does not exist at the pinned commit", %{
|
||||
conn: conn,
|
||||
repository: repository
|
||||
} do
|
||||
submitter = account_fixture()
|
||||
scan = finding_scan_fixture(repository, submitter, @historical_commit_sha, 99)
|
||||
finding = hd(scan.findings)
|
||||
|
||||
{:ok, view, _html} =
|
||||
live(log_in_account(conn, submitter), ~p"/findings/#{finding.public_id}/code")
|
||||
|
||||
render_async(view, 1_000)
|
||||
|
||||
assert has_element?(view, "#finding-line-outside-file")
|
||||
refute has_element?(view, "#code-lines .bg-panel")
|
||||
end
|
||||
|
||||
test "finding source routes reject enumerable integer identifiers", %{conn: conn} do
|
||||
assert_raise Ecto.NoResultsError, fn ->
|
||||
live(conn, "/findings/1/code")
|
||||
end
|
||||
end
|
||||
|
||||
test "a connected finding view is evicted when a moderator restricts the scan", %{
|
||||
conn: conn,
|
||||
repository: repository
|
||||
} do
|
||||
scan = finding_scan_fixture(repository, account_fixture(), @historical_commit_sha)
|
||||
scan = publish_full_scan(repository, scan)
|
||||
finding = hd(scan.findings)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/findings/#{finding.public_id}/code")
|
||||
render_async(view, 1_000)
|
||||
assert has_element?(view, "#code-file")
|
||||
|
||||
moderator_scope = Scope.for_account(moderator_account_fixture())
|
||||
|
||||
assert {:ok, _restricted} =
|
||||
Scans.update_visibility(moderator_scope, scan, "restricted", moderation_attributes())
|
||||
|
||||
assert_redirect(view, ~p"/github.com/openai/codex")
|
||||
end
|
||||
|
||||
test "the default code page is uncached but remains indexable", %{conn: conn} do
|
||||
conn = get(conn, ~p"/github.com/openai/codex")
|
||||
|
||||
assert html_response(conn, 200)
|
||||
assert get_resp_header(conn, "cache-control") == ["no-store"]
|
||||
assert get_resp_header(conn, "x-robots-tag") == []
|
||||
end
|
||||
|
||||
test "commit-pinned code routes prohibit caches and search indexing", %{conn: conn} do
|
||||
conn = get(conn, ~p"/github.com/openai/codex/code/#{@default_commit_sha}")
|
||||
|
||||
assert html_response(conn, 200)
|
||||
assert get_resp_header(conn, "cache-control") == ["no-store"]
|
||||
assert get_resp_header(conn, "x-robots-tag") == ["noindex, nofollow"]
|
||||
end
|
||||
|
||||
test "opaque finding routes prohibit caches and search indexing", %{
|
||||
conn: conn,
|
||||
repository: repository
|
||||
} do
|
||||
scan = finding_scan_fixture(repository, account_fixture(), @historical_commit_sha)
|
||||
finding = repository |> publish_full_scan(scan) |> Map.fetch!(:findings) |> hd()
|
||||
|
||||
conn = get(conn, ~p"/findings/#{finding.public_id}/code")
|
||||
|
||||
assert html_response(conn, 200)
|
||||
assert get_resp_header(conn, "cache-control") == ["no-store"]
|
||||
assert get_resp_header(conn, "x-robots-tag") == ["noindex, nofollow"]
|
||||
end
|
||||
|
||||
defp finding_scan_fixture(repository, submitter, commit_sha, line_start \\ 2) do
|
||||
document =
|
||||
Jason.encode!(%{
|
||||
"tarakan_scan_format" => 1,
|
||||
"findings" => [
|
||||
%{
|
||||
"file" => "lib/codex.ex",
|
||||
"line_start" => line_start,
|
||||
"line_end" => line_start,
|
||||
"severity" => "high",
|
||||
"title" => "Authorization is not checked",
|
||||
"description" => "A request reaches a privileged operation without a policy check."
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
scan_fixture(repository, submitter, %{
|
||||
"commit_sha" => commit_sha,
|
||||
"findings_json" => document
|
||||
})
|
||||
end
|
||||
|
||||
defp publish_full_scan(repository, scan) do
|
||||
repository
|
||||
|> Tarakan.Repositories.Repository.participation_changeset(%{
|
||||
participation_mode: "maintainer_verified"
|
||||
})
|
||||
|> Tarakan.Repo.update!()
|
||||
|
||||
scan = confirmation_fixture(scan, reviewer_account_fixture())
|
||||
scan = confirmation_fixture(scan, reviewer_account_fixture())
|
||||
moderator_scope = Scope.for_account(moderator_account_fixture())
|
||||
|
||||
{:ok, accepted} = Scans.accept_scan(moderator_scope, scan, moderation_attributes())
|
||||
|
||||
{:ok, disclosed} =
|
||||
Scans.update_visibility(
|
||||
moderator_scope,
|
||||
accepted,
|
||||
"public",
|
||||
moderation_attributes(%{"sensitive_data_reviewed" => "true"})
|
||||
)
|
||||
|
||||
disclosed
|
||||
end
|
||||
|
||||
defp moderation_attributes(overrides \\ %{}) do
|
||||
Enum.into(overrides, %{
|
||||
"moderation_reason" => "evidence_reviewed",
|
||||
"moderation_notes" =>
|
||||
"Independent evidence and disclosure boundaries were reviewed for this exact commit."
|
||||
})
|
||||
end
|
||||
end
|
||||
273
test/tarakan_web/live/repository_commits_live_test.exs
Normal file
273
test/tarakan_web/live/repository_commits_live_test.exs
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
defmodule TarakanWeb.RepositoryCommitsLiveTest do
|
||||
use TarakanWeb.ConnCase, async: false
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Tarakan.Accounts
|
||||
alias Tarakan.HostedRepositories
|
||||
alias Tarakan.HostedRepositories.Storage
|
||||
|
||||
@moduletag :tmp_dir
|
||||
|
||||
setup %{tmp_dir: tmp_dir} do
|
||||
on_exit(fn ->
|
||||
File.rm_rf(Application.fetch_env!(:tarakan, Tarakan.HostedRepositories)[:root])
|
||||
end)
|
||||
|
||||
Tarakan.RepositoryCode.Cache.clear()
|
||||
|
||||
account = account_fixture()
|
||||
scope = Accounts.scope_for_account(account)
|
||||
{:ok, repository} = HostedRepositories.create(scope, %{"name" => "history"})
|
||||
|
||||
%{repository: repository, tmp_dir: tmp_dir}
|
||||
end
|
||||
|
||||
defp push_commit(repository, tmp_dir, dir_name, message) do
|
||||
work = Path.join(tmp_dir, dir_name)
|
||||
|
||||
{_output, 0} =
|
||||
System.cmd("git", ["clone", "--quiet", Storage.dir(repository), work],
|
||||
stderr_to_stdout: true
|
||||
)
|
||||
|
||||
File.write!(Path.join(work, "README.md"), "# #{message}\n")
|
||||
|
||||
env = [
|
||||
{"GIT_AUTHOR_NAME", "t"},
|
||||
{"GIT_AUTHOR_EMAIL", "t@example.com"},
|
||||
{"GIT_COMMITTER_NAME", "t"},
|
||||
{"GIT_COMMITTER_EMAIL", "t@example.com"}
|
||||
]
|
||||
|
||||
{_output, 0} = System.cmd("git", ["-C", work, "add", "."], env: env)
|
||||
{_output, 0} = System.cmd("git", ["-C", work, "commit", "--quiet", "-m", message], env: env)
|
||||
|
||||
{_output, 0} =
|
||||
System.cmd("git", ["-C", work, "push", "--quiet", "origin", "HEAD"],
|
||||
env: env,
|
||||
stderr_to_stdout: true
|
||||
)
|
||||
|
||||
{sha, 0} = System.cmd("git", ["-C", work, "rev-parse", "HEAD"])
|
||||
String.trim(sha)
|
||||
end
|
||||
|
||||
defp push_merge_commit(repository, tmp_dir, dir_name, branch, message) do
|
||||
work = Path.join(tmp_dir, dir_name)
|
||||
|
||||
{_output, 0} =
|
||||
System.cmd("git", ["clone", "--quiet", Storage.dir(repository), work],
|
||||
stderr_to_stdout: true
|
||||
)
|
||||
|
||||
env = [
|
||||
{"GIT_AUTHOR_NAME", "t"},
|
||||
{"GIT_AUTHOR_EMAIL", "t@example.com"},
|
||||
{"GIT_COMMITTER_NAME", "t"},
|
||||
{"GIT_COMMITTER_EMAIL", "t@example.com"}
|
||||
]
|
||||
|
||||
{main, 0} = System.cmd("git", ["-C", work, "rev-parse", "--abbrev-ref", "HEAD"])
|
||||
main = String.trim(main)
|
||||
|
||||
{_output, 0} = System.cmd("git", ["-C", work, "checkout", "--quiet", "-b", branch], env: env)
|
||||
File.write!(Path.join(work, "side.txt"), "side\n")
|
||||
{_output, 0} = System.cmd("git", ["-C", work, "add", "."], env: env)
|
||||
|
||||
{_output, 0} =
|
||||
System.cmd("git", ["-C", work, "commit", "--quiet", "-m", "side change"], env: env)
|
||||
|
||||
{_output, 0} = System.cmd("git", ["-C", work, "checkout", "--quiet", main], env: env)
|
||||
File.write!(Path.join(work, "main.txt"), "main\n")
|
||||
{_output, 0} = System.cmd("git", ["-C", work, "add", "."], env: env)
|
||||
|
||||
{_output, 0} =
|
||||
System.cmd("git", ["-C", work, "commit", "--quiet", "-m", "main change"], env: env)
|
||||
|
||||
{_output, 0} =
|
||||
System.cmd("git", ["-C", work, "merge", "--quiet", "--no-ff", "-m", message, branch],
|
||||
env: env,
|
||||
stderr_to_stdout: true
|
||||
)
|
||||
|
||||
{_output, 0} =
|
||||
System.cmd("git", ["-C", work, "push", "--quiet", "origin", "HEAD"],
|
||||
env: env,
|
||||
stderr_to_stdout: true
|
||||
)
|
||||
|
||||
{sha, 0} = System.cmd("git", ["-C", work, "rev-parse", "HEAD"])
|
||||
String.trim(sha)
|
||||
end
|
||||
|
||||
defp push_branch_commit(repository, tmp_dir, dir_name, branch, message) do
|
||||
work = Path.join(tmp_dir, dir_name)
|
||||
|
||||
{_output, 0} =
|
||||
System.cmd("git", ["clone", "--quiet", Storage.dir(repository), work],
|
||||
stderr_to_stdout: true
|
||||
)
|
||||
|
||||
File.write!(Path.join(work, "FEATURE.md"), "# #{message}\n")
|
||||
|
||||
env = [
|
||||
{"GIT_AUTHOR_NAME", "t"},
|
||||
{"GIT_AUTHOR_EMAIL", "t@example.com"},
|
||||
{"GIT_COMMITTER_NAME", "t"},
|
||||
{"GIT_COMMITTER_EMAIL", "t@example.com"}
|
||||
]
|
||||
|
||||
{_output, 0} = System.cmd("git", ["-C", work, "checkout", "--quiet", "-b", branch], env: env)
|
||||
{_output, 0} = System.cmd("git", ["-C", work, "add", "."], env: env)
|
||||
{_output, 0} = System.cmd("git", ["-C", work, "commit", "--quiet", "-m", message], env: env)
|
||||
|
||||
{_output, 0} =
|
||||
System.cmd("git", ["-C", work, "push", "--quiet", "origin", branch],
|
||||
env: env,
|
||||
stderr_to_stdout: true
|
||||
)
|
||||
|
||||
{sha, 0} = System.cmd("git", ["-C", work, "rev-parse", "HEAD"])
|
||||
String.trim(sha)
|
||||
end
|
||||
|
||||
test "lists the default branch history and links each commit to its detail page", %{
|
||||
conn: conn,
|
||||
repository: repository,
|
||||
tmp_dir: tmp_dir
|
||||
} do
|
||||
first_sha = push_commit(repository, tmp_dir, "work-first", "initial")
|
||||
tip = push_commit(repository, tmp_dir, "work-second", "second")
|
||||
|
||||
{:ok, view, _html} = live(conn, "/#{repository.owner}/#{repository.name}/commits")
|
||||
render_async(view, 1_000)
|
||||
|
||||
assert has_element?(view, "#repository-commits-tab[aria-current='page']")
|
||||
|
||||
# The active tab is still a link: from a commit detail page it points at the
|
||||
# commit list, which is the obvious way back and used to be unclickable.
|
||||
assert has_element?(view, "a#repository-commits-tab[aria-current='page'][href]")
|
||||
assert has_element?(view, "#commits-list a", "second")
|
||||
assert has_element?(view, "#commits-list a", "initial")
|
||||
|
||||
assert has_element?(
|
||||
view,
|
||||
"#commits-list a[href='/#{repository.owner}/#{repository.name}/commits/#{tip}']"
|
||||
)
|
||||
|
||||
assert has_element?(
|
||||
view,
|
||||
"#commits-list a[href='/#{repository.owner}/#{repository.name}/commits/#{first_sha}']"
|
||||
)
|
||||
|
||||
refute has_element?(view, "#commits-load-more")
|
||||
end
|
||||
|
||||
test "the commit detail page shows the full message, patch, and code link", %{
|
||||
conn: conn,
|
||||
repository: repository,
|
||||
tmp_dir: tmp_dir
|
||||
} do
|
||||
_first_sha = push_commit(repository, tmp_dir, "work-first", "initial")
|
||||
tip = push_commit(repository, tmp_dir, "work-second", "second")
|
||||
|
||||
{:ok, view, _html} =
|
||||
live(conn, "/#{repository.owner}/#{repository.name}/commits/#{tip}")
|
||||
|
||||
render_async(view, 1_000)
|
||||
|
||||
assert has_element?(view, "#commit-subject", "second")
|
||||
assert has_element?(view, "#commit-detail-sha", String.slice(tip, 0, 7))
|
||||
assert has_element?(view, "#commit-patch", "+# second")
|
||||
|
||||
assert has_element?(
|
||||
view,
|
||||
"#commit-browse-code[href='/#{repository.owner}/#{repository.name}/code/#{tip}']"
|
||||
)
|
||||
|
||||
# The code link opens the browser pinned at this exact commit.
|
||||
{:ok, code_view, _html} = live(conn, "/#{repository.owner}/#{repository.name}/code/#{tip}")
|
||||
render_async(code_view, 1_000)
|
||||
assert has_element?(code_view, "#code-tree")
|
||||
end
|
||||
|
||||
test "merge commits render with an empty patch state", %{
|
||||
conn: conn,
|
||||
repository: repository,
|
||||
tmp_dir: tmp_dir
|
||||
} do
|
||||
_main_sha = push_commit(repository, tmp_dir, "work-first", "initial")
|
||||
merge_sha = push_merge_commit(repository, tmp_dir, "work-merge", "side", "merge side")
|
||||
|
||||
{:ok, view, _html} =
|
||||
live(conn, "/#{repository.owner}/#{repository.name}/commits/#{merge_sha}")
|
||||
|
||||
render_async(view, 1_000)
|
||||
|
||||
assert has_element?(view, "#commit-subject", "merge side")
|
||||
assert has_element?(view, "#commit-patch-empty", "No textual changes")
|
||||
refute has_element?(view, "#commits-error")
|
||||
end
|
||||
|
||||
test "the commit detail page rejects unreachable commits", %{
|
||||
conn: conn,
|
||||
repository: repository,
|
||||
tmp_dir: tmp_dir
|
||||
} do
|
||||
_sha = push_commit(repository, tmp_dir, "work-first", "initial")
|
||||
unknown = String.duplicate("d", 40)
|
||||
|
||||
{:ok, view, _html} =
|
||||
live(conn, "/#{repository.owner}/#{repository.name}/commits/#{unknown}")
|
||||
|
||||
render_async(view, 1_000)
|
||||
|
||||
assert has_element?(view, "#commits-error", "Commit not found")
|
||||
refute has_element?(view, "#commit-detail")
|
||||
end
|
||||
|
||||
test "the branch picker switches the listed history", %{
|
||||
conn: conn,
|
||||
repository: repository,
|
||||
tmp_dir: tmp_dir
|
||||
} do
|
||||
_main_sha = push_commit(repository, tmp_dir, "work-first", "initial")
|
||||
|
||||
feature_sha =
|
||||
push_branch_commit(repository, tmp_dir, "work-feature", "feature", "feature work")
|
||||
|
||||
{:ok, view, _html} = live(conn, "/#{repository.owner}/#{repository.name}/commits")
|
||||
render_async(view, 1_000)
|
||||
|
||||
assert has_element?(view, "#commits-branch-select")
|
||||
refute has_element?(view, "#commits-list a", "feature work")
|
||||
|
||||
view
|
||||
|> form("#commits-branch-form", %{branch: "feature"})
|
||||
|> render_change()
|
||||
|
||||
render_async(view, 1_000)
|
||||
|
||||
assert has_element?(view, "#commits-list a", "feature work")
|
||||
|
||||
assert has_element?(
|
||||
view,
|
||||
"#commits-list a[href='/#{repository.owner}/#{repository.name}/commits/#{feature_sha}']"
|
||||
)
|
||||
end
|
||||
|
||||
test "an empty repository shows an empty state", %{conn: conn, repository: repository} do
|
||||
{:ok, view, _html} = live(conn, "/#{repository.owner}/#{repository.name}/commits")
|
||||
render_async(view, 1_000)
|
||||
|
||||
assert has_element?(view, "#commits-error", "No commits yet")
|
||||
refute has_element?(view, "#commits-list")
|
||||
end
|
||||
|
||||
test "unknown repositories 404", %{conn: conn} do
|
||||
assert_raise Ecto.NoResultsError, fn ->
|
||||
live(conn, "/nobody/here/commits")
|
||||
end
|
||||
end
|
||||
end
|
||||
1008
test/tarakan_web/live/repository_live_test.exs
Normal file
1008
test/tarakan_web/live/repository_live_test.exs
Normal file
File diff suppressed because it is too large
Load diff
111
test/tarakan_web/live/repository_new_live_test.exs
Normal file
111
test/tarakan_web/live/repository_new_live_test.exs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
defmodule TarakanWeb.RepositoryNewLiveTest do
|
||||
use TarakanWeb.ConnCase, async: false
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Tarakan.HostedRepositories
|
||||
alias Tarakan.Repositories
|
||||
|
||||
setup :register_and_log_in_account
|
||||
|
||||
setup do
|
||||
on_exit(fn ->
|
||||
File.rm_rf(Application.fetch_env!(:tarakan, Tarakan.HostedRepositories)[:root])
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "requires authentication" do
|
||||
conn = Phoenix.ConnTest.build_conn()
|
||||
assert {:error, {:redirect, %{to: path}}} = live(conn, ~p"/repositories/new")
|
||||
assert path =~ "/accounts/log-in"
|
||||
end
|
||||
|
||||
test "creates a hosted repository and navigates to it", %{conn: conn, account: account} do
|
||||
{:ok, view, _html} = live(conn, ~p"/repositories/new")
|
||||
|
||||
assert has_element?(view, "#new-repository-form")
|
||||
|
||||
view
|
||||
|> form("#new-repository-form", repository: %{name: "from-the-ui"})
|
||||
|> render_submit()
|
||||
|
||||
assert_redirect(view, "/#{account.handle}/from-the-ui")
|
||||
assert HostedRepositories.resolve(account.handle, "from-the-ui")
|
||||
|
||||
# the bare GitHub-style path must dispatch through the hosted scope
|
||||
{:ok, repo_view, _html} = live(conn, "/#{account.handle}/from-the-ui")
|
||||
assert has_element?(repo_view, "#repository-name")
|
||||
end
|
||||
|
||||
test "shows validation errors inline", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/repositories/new")
|
||||
|
||||
html =
|
||||
view
|
||||
|> form("#new-repository-form", repository: %{name: "bad name"})
|
||||
|> render_submit()
|
||||
|
||||
assert html =~ "may only contain"
|
||||
end
|
||||
|
||||
describe "remote registration" do
|
||||
setup %{conn: conn} do
|
||||
%{conn: log_in_account(conn, github_account_fixture())}
|
||||
end
|
||||
|
||||
test "registers a repository and opens its public record", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/repositories/new")
|
||||
|
||||
view
|
||||
|> form("#register-remote-form", repository: %{url: "github.com/OpenAI/Codex"})
|
||||
|> render_submit()
|
||||
|
||||
assert_redirect(view, ~p"/github.com/openai/codex")
|
||||
assert Repositories.get_github_repository("openai", "codex")
|
||||
|
||||
{:ok, record_view, _html} = live(conn, ~p"/github.com/openai/codex/security")
|
||||
|
||||
assert has_element?(record_view, "#repository-name")
|
||||
assert has_element?(record_view, "#repository-source-link")
|
||||
assert has_element?(record_view, "#github-metadata")
|
||||
assert has_element?(record_view, "#repository-submitter")
|
||||
assert has_element?(record_view, "#scans-empty")
|
||||
end
|
||||
|
||||
test "shows a useful error for an invalid repository", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/repositories/new")
|
||||
|
||||
view
|
||||
|> form("#register-remote-form",
|
||||
repository: %{url: "https://example.com/not/github"}
|
||||
)
|
||||
|> render_submit()
|
||||
|
||||
assert has_element?(view, "#repository_url-error")
|
||||
end
|
||||
|
||||
test "rejects a valid-looking repository that GitHub cannot find", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/repositories/new")
|
||||
|
||||
view
|
||||
|> form("#register-remote-form", repository: %{url: "missing/repository"})
|
||||
|> render_submit()
|
||||
|
||||
assert has_element?(view, "#repository_url-error")
|
||||
refute Repositories.get_github_repository("missing", "repository")
|
||||
end
|
||||
|
||||
test "shows the same safe error for a private repository", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/repositories/new")
|
||||
|
||||
view
|
||||
|> form("#register-remote-form", repository: %{url: "private/repository"})
|
||||
|> render_submit()
|
||||
|
||||
assert has_element?(view, "#repository_url-error")
|
||||
refute Repositories.get_github_repository("private", "repository")
|
||||
end
|
||||
end
|
||||
end
|
||||
367
test/tarakan_web/live/review_task_live_test.exs
Normal file
367
test/tarakan_web/live/review_task_live_test.exs
Normal file
|
|
@ -0,0 +1,367 @@
|
|||
defmodule TarakanWeb.ReviewTaskLiveTest do
|
||||
use TarakanWeb.ConnCase
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
setup %{conn: conn} do
|
||||
creator = github_account_fixture()
|
||||
repository = listed_github_repository_fixture(creator)
|
||||
task = review_task_fixture(repository, creator)
|
||||
|
||||
%{conn: conn, creator: creator, repository: repository, task: task}
|
||||
end
|
||||
|
||||
test "the public can inspect a task but cannot claim anonymously", %{conn: conn, task: task} do
|
||||
{:ok, view, _html} = live(conn, ~p"/jobs/#{task.id}")
|
||||
|
||||
assert has_element?(view, "#review-task-title")
|
||||
assert has_element?(view, "#review-task-status", "Open")
|
||||
refute has_element?(view, "#claim-review-task-button")
|
||||
|
||||
html = render_hook(view, "claim", %{})
|
||||
assert html =~ "not authorized"
|
||||
|
||||
html = render_hook(view, "review", %{"action" => "invented", "decision" => %{}})
|
||||
assert html =~ "review action is invalid"
|
||||
|
||||
assert {:error, {:live_redirect, %{to: "/accounts/log-in?return_to=%2Fjobs%2F" <> _}}} =
|
||||
render_hook(view, "publish", %{"decision" => %{}})
|
||||
end
|
||||
|
||||
test "the task creator may claim and perform their own job", %{
|
||||
conn: conn,
|
||||
creator: creator,
|
||||
task: task
|
||||
} do
|
||||
conn = log_in_account(conn, creator)
|
||||
{:ok, view, _html} = live(conn, ~p"/jobs/#{task.id}")
|
||||
|
||||
assert has_element?(view, "#claim-review-task-button")
|
||||
refute has_element?(view, "#review-task-own-notice")
|
||||
|
||||
view |> element("#claim-review-task-button") |> render_click()
|
||||
assert has_element?(view, "#review-task-status", "Claimed")
|
||||
assert has_element?(view, "#review-task-completion-form")
|
||||
refute has_element?(view, "#review-task-agent-path")
|
||||
end
|
||||
|
||||
test "agent-required jobs show the CLI path instead of free-text completion", %{
|
||||
conn: conn,
|
||||
creator: creator,
|
||||
repository: repository
|
||||
} do
|
||||
task =
|
||||
review_task_fixture(repository, creator, %{
|
||||
"kind" => "code_review",
|
||||
"capability" => "agent",
|
||||
"title" => "Agent code review of auth boundaries"
|
||||
})
|
||||
|
||||
worker = account_fixture()
|
||||
conn = log_in_account(conn, worker)
|
||||
{:ok, view, _html} = live(conn, ~p"/jobs/#{task.id}")
|
||||
|
||||
assert has_element?(view, "#review-task-title")
|
||||
assert has_element?(view, "#review-task", "Agent required")
|
||||
|
||||
view |> element("#claim-review-task-button") |> render_click()
|
||||
|
||||
assert has_element?(view, "#review-task-status", "Claimed")
|
||||
assert has_element?(view, "#review-task-agent-path")
|
||||
refute has_element?(view, "#review-task-completion-form")
|
||||
assert render(view) =~ "tarakan report"
|
||||
assert render(view) =~ "--job #{task.id}"
|
||||
end
|
||||
|
||||
test "agent check jobs show only the check command and their target findings", %{
|
||||
conn: conn,
|
||||
creator: creator,
|
||||
repository: repository
|
||||
} do
|
||||
report =
|
||||
scan_fixture(repository, account_fixture(), %{"findings_json" => findings_json_fixture()})
|
||||
|
||||
task =
|
||||
review_task_fixture(repository, creator, %{
|
||||
"kind" => "verify_findings",
|
||||
"capability" => "agent",
|
||||
"target_review_id" => report.id,
|
||||
"title" => "Independently check the report"
|
||||
})
|
||||
|
||||
worker = reviewer_account_fixture()
|
||||
|
||||
{:ok, view, _html} =
|
||||
conn
|
||||
|> log_in_account(worker)
|
||||
|> live(~p"/jobs/#{task.id}")
|
||||
|
||||
assert has_element?(view, "#review-task-target-report", "Target report ##{report.id}")
|
||||
assert has_element?(view, "#review-task-target-report", "Unsanitized input")
|
||||
|
||||
view |> element("#claim-review-task-button") |> render_click()
|
||||
|
||||
assert has_element?(view, "#review-task-agent-path")
|
||||
assert render(view) =~ "tarakan worker --agent grok --once --jobs-only"
|
||||
refute render(view) =~ "tarakan report"
|
||||
end
|
||||
|
||||
test "a hybrid claimant can verify a report from the web", %{
|
||||
conn: conn,
|
||||
creator: creator,
|
||||
repository: repository
|
||||
} do
|
||||
report =
|
||||
scan_fixture(repository, account_fixture(), %{"findings_json" => findings_json_fixture()})
|
||||
|
||||
task =
|
||||
review_task_fixture(repository, creator, %{
|
||||
"kind" => "verify_findings",
|
||||
"capability" => "hybrid",
|
||||
"target_review_id" => report.id,
|
||||
"title" => "Reproduce the reported data flow"
|
||||
})
|
||||
|
||||
worker = reviewer_account_fixture()
|
||||
|
||||
{:ok, view, _html} =
|
||||
conn
|
||||
|> log_in_account(worker)
|
||||
|> live(~p"/jobs/#{task.id}")
|
||||
|
||||
view |> element("#claim-review-task-button") |> render_click()
|
||||
|
||||
assert has_element?(view, "#review-task-verification-form")
|
||||
refute has_element?(view, "#review-task-completion-form")
|
||||
|
||||
view
|
||||
|> form("#review-task-verification-form",
|
||||
verification: %{
|
||||
provenance: "hybrid",
|
||||
verdict: "confirmed",
|
||||
notes: "Reproduced every reported path against the exact pinned commit.",
|
||||
evidence: "Ran the negative-path request and observed the authorization bypass."
|
||||
}
|
||||
)
|
||||
|> render_submit()
|
||||
|
||||
assert has_element?(view, "#review-task-status", "Submitted")
|
||||
assert has_element?(view, "#review-task-target-confirmations", "authorization bypass")
|
||||
assert Tarakan.Work.get_task!(task.id).linked_review_id == report.id
|
||||
end
|
||||
|
||||
test "agent fix jobs show the autonomous patch workflow", %{
|
||||
conn: conn,
|
||||
creator: creator,
|
||||
repository: repository
|
||||
} do
|
||||
task =
|
||||
review_task_fixture(repository, creator, %{
|
||||
"kind" => "write_fix",
|
||||
"capability" => "agent",
|
||||
"title" => "Patch the authorization bypass"
|
||||
})
|
||||
|
||||
worker = account_fixture()
|
||||
|
||||
{:ok, view, _html} =
|
||||
conn
|
||||
|> log_in_account(worker)
|
||||
|> live(~p"/jobs/#{task.id}")
|
||||
|
||||
view |> element("#claim-review-task-button") |> render_click()
|
||||
|
||||
assert has_element?(view, "#review-task-agent-path", "reviewable unified diff")
|
||||
assert render(view) =~ "tarakan worker --agent grok --once --jobs-only"
|
||||
refute render(view) =~ "tarakan report"
|
||||
end
|
||||
|
||||
test "a proposed fix is rendered as a patch artifact", %{
|
||||
conn: conn,
|
||||
creator: creator,
|
||||
repository: repository
|
||||
} do
|
||||
task =
|
||||
review_task_fixture(repository, creator, %{
|
||||
"kind" => "write_fix",
|
||||
"capability" => "human",
|
||||
"title" => "Patch the authorization bypass"
|
||||
})
|
||||
|
||||
worker = account_fixture()
|
||||
|
||||
{:ok, view, _html} =
|
||||
conn
|
||||
|> log_in_account(worker)
|
||||
|> live(~p"/jobs/#{task.id}")
|
||||
|
||||
view |> element("#claim-review-task-button") |> render_click()
|
||||
|
||||
assert has_element?(
|
||||
view,
|
||||
"#review-task-completion-form",
|
||||
"Proposed unified diff and test plan"
|
||||
)
|
||||
|
||||
view
|
||||
|> form("#review-task-completion-form",
|
||||
contribution: %{
|
||||
provenance: "human",
|
||||
summary: "Require ownership before the state transition.",
|
||||
evidence:
|
||||
"diff --git a/lib/action.ex b/lib/action.ex\n--- a/lib/action.ex\n+++ b/lib/action.ex\n@@ -1 +1,2 @@\n run()\n+authorize()\n\nTest: mix test"
|
||||
}
|
||||
)
|
||||
|> render_submit()
|
||||
|
||||
assert has_element?(view, "#review-task-fix-badge", "Patch proposal")
|
||||
assert has_element?(view, "#review-task-contribution", "Proposed fix")
|
||||
assert has_element?(view, "#review-task-contribution", "diff --git")
|
||||
refute has_element?(view, "#review-task-historical-badge")
|
||||
end
|
||||
|
||||
test "another contributor claims and submits the task for review", %{conn: conn, task: task} do
|
||||
worker = account_fixture()
|
||||
conn = log_in_account(conn, worker)
|
||||
{:ok, view, _html} = live(conn, ~p"/jobs/#{task.id}")
|
||||
|
||||
view |> element("#claim-review-task-button") |> render_click()
|
||||
|
||||
assert has_element?(view, "#review-task-status", "Claimed")
|
||||
assert has_element?(view, "#review-task-completion-form")
|
||||
refute has_element?(view, "#review-task-agent-path")
|
||||
|
||||
view
|
||||
|> form("#review-task-completion-form",
|
||||
contribution: %{
|
||||
provenance: "human",
|
||||
summary: "The boundary is enforced consistently.",
|
||||
evidence: "Reproduced the negative path with the repository test suite."
|
||||
}
|
||||
)
|
||||
|> render_submit()
|
||||
|
||||
assert has_element?(view, "#review-task-status", "Submitted")
|
||||
assert has_element?(view, "#review-task-review-pending")
|
||||
assert has_element?(view, "#review-task-contribution", "Human")
|
||||
assert has_element?(view, "#review-task-contribution", "boundary is enforced")
|
||||
refute has_element?(view, "#review-task-completion-form")
|
||||
end
|
||||
|
||||
test "a qualified independent reviewer accepts submitted evidence", %{
|
||||
conn: conn,
|
||||
task: task
|
||||
} do
|
||||
worker = account_fixture()
|
||||
{:ok, task} = Tarakan.Work.claim_task(task, worker)
|
||||
|
||||
{:ok, task} =
|
||||
Tarakan.Work.submit_task(task, worker, %{
|
||||
"provenance" => "human",
|
||||
"summary" => "The boundary is enforced consistently.",
|
||||
"evidence" => "Reproduced the negative path with the repository test suite."
|
||||
})
|
||||
|
||||
reviewer = moderator_account_fixture()
|
||||
conn = log_in_account(conn, reviewer)
|
||||
{:ok, view, _html} = live(conn, ~p"/jobs/#{task.id}")
|
||||
|
||||
assert has_element?(view, "#review-task-decision-form")
|
||||
|
||||
view
|
||||
|> form("#review-task-decision-form",
|
||||
decision: %{
|
||||
reason: "The submitted behavior was reproduced independently.",
|
||||
evidence: "Checked out the pinned SHA and ran the documented negative-path tests."
|
||||
}
|
||||
)
|
||||
|> render_submit(%{"action" => "accept"})
|
||||
|
||||
assert has_element?(view, "#review-task-status", "Accepted")
|
||||
assert has_element?(view, "#review-task-visibility", "Full evidence public")
|
||||
assert has_element?(view, "#review-task-contribution")
|
||||
assert has_element?(view, "#review-task-disclosure-form")
|
||||
refute has_element?(view, "#review-task-decision-form")
|
||||
assert Tarakan.Work.get_visible_task(task.id)
|
||||
|
||||
{:ok, accepted_view, accepted_html} =
|
||||
live(Phoenix.ConnTest.build_conn(), ~p"/jobs/#{task.id}")
|
||||
|
||||
assert has_element?(accepted_view, "#review-task-status", "Accepted")
|
||||
assert accepted_html =~ "Reproduced the negative path"
|
||||
|
||||
html =
|
||||
view
|
||||
|> form("#review-task-disclosure-form", disclosure: %{reason: "short"})
|
||||
|> render_submit(%{"visibility" => "public_summary"})
|
||||
|
||||
assert html =~ "should be at least 10 character"
|
||||
assert Tarakan.Work.get_task!(task.id).visibility == "public"
|
||||
|
||||
view
|
||||
|> form("#review-task-disclosure-form",
|
||||
disclosure: %{
|
||||
reason: "The redacted result is safe and useful to publish without raw evidence."
|
||||
}
|
||||
)
|
||||
|> render_submit(%{"visibility" => "public_summary"})
|
||||
|
||||
assert has_element?(view, "#review-task-visibility", "Public summary")
|
||||
|
||||
{:ok, public_view, public_html} =
|
||||
live(Phoenix.ConnTest.build_conn(), ~p"/jobs/#{task.id}")
|
||||
|
||||
assert has_element?(public_view, "#review-task-status", "Accepted")
|
||||
assert public_html =~ "boundary is enforced"
|
||||
refute public_html =~ "Reproduced the negative path"
|
||||
refute public_html =~ "documented negative-path tests"
|
||||
end
|
||||
|
||||
test "disclosure requires recent authentication", %{conn: conn, task: task} do
|
||||
worker = account_fixture()
|
||||
{:ok, task} = Tarakan.Work.claim_task(task, worker)
|
||||
|
||||
{:ok, task} =
|
||||
Tarakan.Work.submit_task(task, worker, %{
|
||||
"provenance" => "human",
|
||||
"summary" => "The boundary is enforced consistently.",
|
||||
"evidence" => "Reproduced the negative path with the repository test suite."
|
||||
})
|
||||
|
||||
reviewer = moderator_account_fixture()
|
||||
|
||||
{:ok, task} =
|
||||
Tarakan.Work.accept_task(task, reviewer, %{
|
||||
"reason" => "The submitted behavior was reproduced independently.",
|
||||
"evidence" => "Checked out the pinned SHA and ran the documented negative-path tests."
|
||||
})
|
||||
|
||||
# Outside the two-hour sudo window.
|
||||
stale_at = DateTime.add(DateTime.utc_now(), -3, :hour)
|
||||
conn = log_in_account(conn, reviewer, token_authenticated_at: stale_at)
|
||||
{:ok, view, _html} = live(conn, ~p"/jobs/#{task.id}")
|
||||
|
||||
assert {:error, {:live_redirect, %{to: "/accounts/log-in?return_to=%2Fjobs%2F" <> _}}} =
|
||||
view
|
||||
|> form("#review-task-disclosure-form",
|
||||
disclosure: %{
|
||||
reason: "The redacted result is safe and useful to publish without raw evidence."
|
||||
}
|
||||
)
|
||||
|> render_submit(%{"visibility" => "public_summary"})
|
||||
|
||||
assert Tarakan.Work.get_task!(task.id).visibility == "public"
|
||||
end
|
||||
|
||||
test "a claimant can release work back to the queue", %{conn: conn, task: task} do
|
||||
worker = account_fixture()
|
||||
conn = log_in_account(conn, worker)
|
||||
{:ok, view, _html} = live(conn, ~p"/jobs/#{task.id}")
|
||||
|
||||
view |> element("#claim-review-task-button") |> render_click()
|
||||
view |> element("#release-review-task-button") |> render_click()
|
||||
|
||||
assert has_element?(view, "#review-task-status", "Open")
|
||||
assert has_element?(view, "#claim-review-task-button")
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue