feat: 實作 bear CLI PAT 模式 MVP(login/whoami/token/logout/status)
- 新增 Elixir + Mix escript 專案(Req 0.5+、Jason) - login --token <PAT>:以 PAT 驗證 /userinfo 後寫入憑證檔(0600) - whoami:GET /userinfo 顯示身分 claims(支援 --json) - token:印出 access token 供 pipe(--refresh 在 PAT 模式忽略) - logout:清除本機憑證;status:純本機判定 - 憑證檔採原子寫入(rename)、0600、O_CREAT|O_EXCL 防 symlink - 全域選項:--issuer/--config/--json/-v/-h/--version - 29 個單元測試;mix precommit(compile --warnings-as-errors + format + test) - 更新 README.md 與 docs/commands.md 反映 PAT 模式 MVP Device Flow 登入待伺服器端 P1(bear 倉庫)完成後再接。issue #2
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
defmodule BearCli.CredentialsTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias BearCli.Credentials
|
||||
|
||||
test "save/load round-trips JSON" do
|
||||
path = tmp_path()
|
||||
on_exit(fn -> File.rm(path) end)
|
||||
|
||||
assert :ok ==
|
||||
Credentials.save(
|
||||
%{"issuer" => "x", "access_token" => "tok", "email" => "a@b.c"},
|
||||
path
|
||||
)
|
||||
|
||||
assert {:ok, %{"access_token" => "tok", "email" => "a@b.c"}} = Credentials.load(path)
|
||||
end
|
||||
|
||||
test "save creates the file with 0600 permissions" do
|
||||
path = tmp_path()
|
||||
on_exit(fn -> File.rm(path) end)
|
||||
:ok = Credentials.save(%{"access_token" => "tok"}, path)
|
||||
|
||||
mode = File.stat!(path).mode
|
||||
assert Bitwise.band(mode, 0o777) == 0o600
|
||||
end
|
||||
|
||||
test "save creates the parent directory" do
|
||||
base = Path.join(System.tmp_dir!(), "bear_cli_nested_#{System.unique_integer([:positive])}")
|
||||
path = Path.join([base, "sub", "credentials.json"])
|
||||
on_exit(fn -> File.rm_rf(base) end)
|
||||
|
||||
:ok = Credentials.save(%{"a" => "b"}, path)
|
||||
assert File.regular?(path)
|
||||
end
|
||||
|
||||
test "load returns :error for a missing file" do
|
||||
assert :error == Credentials.load("/nonexistent/credentials.json")
|
||||
end
|
||||
|
||||
test "load returns :error for corrupt JSON" do
|
||||
path = tmp_path()
|
||||
on_exit(fn -> File.rm(path) end)
|
||||
File.write!(path, "{not json")
|
||||
|
||||
assert :error == Credentials.load(path)
|
||||
end
|
||||
|
||||
test "delete removes the file and is idempotent" do
|
||||
path = tmp_path()
|
||||
on_exit(fn -> File.rm(path) end)
|
||||
:ok = Credentials.save(%{"a" => "b"}, path)
|
||||
|
||||
assert :ok == Credentials.delete(path)
|
||||
assert :error == Credentials.load(path)
|
||||
assert :ok == Credentials.delete(path)
|
||||
end
|
||||
|
||||
defp tmp_path do
|
||||
Path.join(System.tmp_dir!(), "bear_cli_creds_#{System.unique_integer([:positive])}.json")
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user