- 專案骨架:Elixir + Mix escript(單檔可執行 bear),HTTP 一律 Req - 設定/憑證檔讀寫:0600、原子寫入(rename)、O_CREAT|O_EXCL 防 symlink - PAT Bearer 呼叫 /userinfo;--json 輸出;規格化退出碼 - 補齊 main 規格承諾的 BEAR_TOKEN 環境變數(優先於憑證檔、不寫入) - login 無效 PAT 退出碼 3(依 docs/commands.md §3.1);token --refresh 用法錯誤 2(§3.3) - 35 個單元測試;mix precommit(compile --warnings-as-errors + format + test)全綠 - 沿用已關閉 PR #3 的實作(feat/pat-mode-mvp 分支),文件改依 main 現行版本更新 apps 管理指令待 #5 規格與 alterminal/bear#28 API 就緒後實作。
300 lines
8.0 KiB
Elixir
300 lines
8.0 KiB
Elixir
defmodule BearCli.CLITest do
|
|
use ExUnit.Case, async: false
|
|
|
|
alias BearCli.CLI
|
|
|
|
setup do
|
|
old_creds = System.get_env("BEAR_CREDENTIALS")
|
|
|
|
on_exit(fn ->
|
|
restore_env("BEAR_CREDENTIALS", old_creds)
|
|
end)
|
|
|
|
:ok
|
|
end
|
|
|
|
describe "parse/1" do
|
|
test "no args -> help" do
|
|
assert {:help, %{}} = CLI.parse([])
|
|
end
|
|
|
|
test "--version" do
|
|
assert {:version, %{}} = CLI.parse(["--version"])
|
|
end
|
|
|
|
test "-h alias -> help" do
|
|
assert {:help, %{}} = CLI.parse(["-h"])
|
|
end
|
|
|
|
test "login --token X" do
|
|
assert {:ok, :login, %{token: "X"}} = CLI.parse(["login", "--token", "X"])
|
|
end
|
|
|
|
test "global option before command" do
|
|
assert {:ok, :whoami, %{json: true}} = CLI.parse(["--json", "whoami"])
|
|
end
|
|
|
|
test "global option after command" do
|
|
assert {:ok, :status, %{json: true}} = CLI.parse(["status", "--json"])
|
|
end
|
|
|
|
test "token --refresh" do
|
|
assert {:ok, :token, %{refresh: true}} = CLI.parse(["token", "--refresh"])
|
|
end
|
|
|
|
test "issuer flag is captured" do
|
|
assert {:ok, :whoami, %{issuer: "https://x"}} =
|
|
CLI.parse(["whoami", "--issuer", "https://x"])
|
|
end
|
|
|
|
test "unknown command -> usage error" do
|
|
assert {:error, _, 2} = CLI.parse(["frobnicate"])
|
|
end
|
|
|
|
test "unknown option -> usage error" do
|
|
assert {:error, _, 2} = CLI.parse(["--bogus"])
|
|
end
|
|
end
|
|
|
|
describe "dispatch/2 (injected api)" do
|
|
test "login --token writes credentials and prints email" do
|
|
path = tmp_path()
|
|
on_exit(fn -> File.rm(path) end)
|
|
System.put_env("BEAR_CREDENTIALS", path)
|
|
|
|
{code, out} =
|
|
ExUnit.CaptureIO.with_io(fn ->
|
|
CLI.dispatch(CLI.parse(["login", "--token", "good"]), api: fake_api())
|
|
end)
|
|
|
|
assert code == 0
|
|
assert out =~ "已登入:alice@example.com"
|
|
assert {:ok, %{"access_token" => "good"}} = BearCli.Credentials.load(path)
|
|
end
|
|
|
|
test "login with invalid token exits 3" do
|
|
System.put_env("BEAR_CREDENTIALS", tmp_path())
|
|
|
|
{code, err} =
|
|
ExUnit.CaptureIO.with_io(
|
|
:stderr,
|
|
"",
|
|
fn -> CLI.dispatch(CLI.parse(["login", "--token", "bad"]), api: fake_api()) end
|
|
)
|
|
|
|
assert code == 3
|
|
assert err =~ "PAT 無效"
|
|
end
|
|
|
|
test "login without --token points to Device Flow being unavailable" do
|
|
System.put_env("BEAR_CREDENTIALS", tmp_path())
|
|
|
|
{code, err} =
|
|
ExUnit.CaptureIO.with_io(
|
|
:stderr,
|
|
"",
|
|
fn -> CLI.dispatch(CLI.parse(["login"]), api: fake_api()) end
|
|
)
|
|
|
|
assert code == 2
|
|
assert err =~ "Device Flow"
|
|
end
|
|
|
|
test "whoami when not logged in exits 3" do
|
|
System.put_env("BEAR_CREDENTIALS", "/nonexistent/credentials.json")
|
|
|
|
{code, err} =
|
|
ExUnit.CaptureIO.with_io(
|
|
:stderr,
|
|
"",
|
|
fn -> CLI.dispatch(CLI.parse(["whoami"]), api: fake_api()) end
|
|
)
|
|
|
|
assert code == 3
|
|
assert err =~ "未登入"
|
|
end
|
|
|
|
test "whoami prints claims" do
|
|
path = tmp_path()
|
|
on_exit(fn -> File.rm(path) end)
|
|
System.put_env("BEAR_CREDENTIALS", path)
|
|
BearCli.Credentials.save(%{"issuer" => "https://x", "access_token" => "good"}, path)
|
|
|
|
{code, out} =
|
|
ExUnit.CaptureIO.with_io(fn ->
|
|
CLI.dispatch(CLI.parse(["whoami"]), api: fake_api())
|
|
end)
|
|
|
|
assert code == 0
|
|
assert out =~ "email"
|
|
assert out =~ "alice@example.com"
|
|
end
|
|
|
|
test "token prints only the token" do
|
|
path = tmp_path()
|
|
on_exit(fn -> File.rm(path) end)
|
|
System.put_env("BEAR_CREDENTIALS", path)
|
|
BearCli.Credentials.save(%{"issuer" => "https://x", "access_token" => "good-token"}, path)
|
|
|
|
{code, out} =
|
|
ExUnit.CaptureIO.with_io(fn ->
|
|
CLI.dispatch(CLI.parse(["token"]), api: fake_api())
|
|
end)
|
|
|
|
assert code == 0
|
|
assert String.trim(out) == "good-token"
|
|
end
|
|
|
|
test "token --refresh is a usage error in PAT mode" do
|
|
path = tmp_path()
|
|
on_exit(fn -> File.rm(path) end)
|
|
System.put_env("BEAR_CREDENTIALS", path)
|
|
BearCli.Credentials.save(%{"issuer" => "https://x", "access_token" => "good-token"}, path)
|
|
|
|
{code, err} =
|
|
ExUnit.CaptureIO.with_io(
|
|
:stderr,
|
|
"",
|
|
fn -> CLI.dispatch(CLI.parse(["token", "--refresh"]), api: fake_api()) end
|
|
)
|
|
|
|
assert code == 2
|
|
assert err =~ "--refresh"
|
|
end
|
|
|
|
test "status when logged in exits 0 and reports PAT mode" do
|
|
path = tmp_path()
|
|
on_exit(fn -> File.rm(path) end)
|
|
System.put_env("BEAR_CREDENTIALS", path)
|
|
|
|
BearCli.Credentials.save(
|
|
%{"issuer" => "https://x", "email" => "a@b.c", "access_token" => "t"},
|
|
path
|
|
)
|
|
|
|
{code, out} =
|
|
ExUnit.CaptureIO.with_io(fn ->
|
|
CLI.dispatch(CLI.parse(["status"]), api: fake_api())
|
|
end)
|
|
|
|
assert code == 0
|
|
assert out =~ "已登入:a@b.c"
|
|
assert out =~ "PAT"
|
|
end
|
|
|
|
test "logout clears credentials" do
|
|
path = tmp_path()
|
|
on_exit(fn -> File.rm(path) end)
|
|
System.put_env("BEAR_CREDENTIALS", path)
|
|
BearCli.Credentials.save(%{"issuer" => "https://x", "access_token" => "t"}, path)
|
|
|
|
{code, _out} =
|
|
ExUnit.CaptureIO.with_io(fn ->
|
|
CLI.dispatch(CLI.parse(["logout"]), api: fake_api())
|
|
end)
|
|
|
|
assert code == 0
|
|
assert :error == BearCli.Credentials.load(path)
|
|
end
|
|
end
|
|
|
|
describe "BEAR_TOKEN environment variable" do
|
|
setup do
|
|
old_creds = System.get_env("BEAR_CREDENTIALS")
|
|
old_token = System.get_env("BEAR_TOKEN")
|
|
|
|
on_exit(fn ->
|
|
restore_env("BEAR_CREDENTIALS", old_creds)
|
|
restore_env("BEAR_TOKEN", old_token)
|
|
end)
|
|
|
|
:ok
|
|
end
|
|
|
|
test "whoami uses BEAR_TOKEN without credentials file" do
|
|
System.put_env("BEAR_CREDENTIALS", "/nonexistent/credentials.json")
|
|
System.put_env("BEAR_TOKEN", "good")
|
|
|
|
{code, out} =
|
|
ExUnit.CaptureIO.with_io(fn ->
|
|
CLI.dispatch(CLI.parse(["whoami"]), api: fake_api())
|
|
end)
|
|
|
|
assert code == 0
|
|
assert out =~ "alice@example.com"
|
|
end
|
|
|
|
test "whoami with invalid BEAR_TOKEN exits 3" do
|
|
System.put_env("BEAR_CREDENTIALS", "/nonexistent/credentials.json")
|
|
System.put_env("BEAR_TOKEN", "bad")
|
|
|
|
{code, err} =
|
|
ExUnit.CaptureIO.with_io(
|
|
:stderr,
|
|
"",
|
|
fn -> CLI.dispatch(CLI.parse(["whoami"]), api: fake_api()) end
|
|
)
|
|
|
|
assert code == 3
|
|
assert err =~ "token 無效"
|
|
end
|
|
|
|
test "token prints BEAR_TOKEN" do
|
|
System.put_env("BEAR_CREDENTIALS", "/nonexistent/credentials.json")
|
|
System.put_env("BEAR_TOKEN", "env-pat")
|
|
|
|
{code, out} =
|
|
ExUnit.CaptureIO.with_io(fn ->
|
|
CLI.dispatch(CLI.parse(["token"]), api: fake_api())
|
|
end)
|
|
|
|
assert code == 0
|
|
assert String.trim(out) == "env-pat"
|
|
end
|
|
|
|
test "status reports env token mode" do
|
|
System.put_env("BEAR_CREDENTIALS", "/nonexistent/credentials.json")
|
|
System.put_env("BEAR_TOKEN", "env-pat")
|
|
|
|
{code, out} =
|
|
ExUnit.CaptureIO.with_io(fn ->
|
|
CLI.dispatch(CLI.parse(["status"]), api: fake_api())
|
|
end)
|
|
|
|
assert code == 0
|
|
assert out =~ "BEAR_TOKEN"
|
|
end
|
|
|
|
test "blank BEAR_TOKEN is ignored" do
|
|
System.put_env("BEAR_CREDENTIALS", "/nonexistent/credentials.json")
|
|
System.put_env("BEAR_TOKEN", " ")
|
|
|
|
{code, _err} =
|
|
ExUnit.CaptureIO.with_io(
|
|
:stderr,
|
|
"",
|
|
fn -> CLI.dispatch(CLI.parse(["whoami"]), api: fake_api()) end
|
|
)
|
|
|
|
assert code == 3
|
|
end
|
|
end
|
|
|
|
defp fake_api do
|
|
fn _issuer, token ->
|
|
case token do
|
|
"good" -> {:ok, %{"sub" => "u1", "email" => "alice@example.com", "name" => "Alice"}}
|
|
"bad" -> {:error, :unauthorized, "invalid_token"}
|
|
_ -> {:error, :server_error, 500}
|
|
end
|
|
end
|
|
end
|
|
|
|
defp tmp_path do
|
|
Path.join(System.tmp_dir!(), "bear_cli_cli_#{System.unique_integer([:positive])}.json")
|
|
end
|
|
|
|
defp restore_env(key, nil), do: System.delete_env(key)
|
|
defp restore_env(key, value), do: System.put_env(key, value)
|
|
end
|