- internal/gitea:僅標準庫的 HTTP 用戶端;ListAll 以 limit=50 逐頁抓滿 - 認證選序 --token → TEAI_TOKEN → tea 組態(--config/TEA_CONFIG/預設路徑); token 不入輸出、日誌與錯誤訊息 - 共用輸出 --output json|table(預設 json)、--timeout(預設 30s) - CLI 結束碼 0/2/3:用法錯誤 → 2、API 錯誤(*gitea.ErrAPI)→ 3 - 單元測試:認證選序、tea 組態解析、分頁、逾時/401/5xx 錯誤映射、輸出 實作 issue #5
182 lines
5.6 KiB
Go
182 lines
5.6 KiB
Go
// auth_test.go 驗證認證來源選序(--token → TEAI_TOKEN → tea 組態)
|
||
// 與 tea 組態解析。
|
||
package gitea
|
||
|
||
import (
|
||
"errors"
|
||
"testing"
|
||
)
|
||
|
||
// fakeEnv 依 map 回傳環境變數。
|
||
func fakeEnv(m map[string]string) EnvLookup {
|
||
return func(key string) (string, bool) {
|
||
v, ok := m[key]
|
||
return v, ok
|
||
}
|
||
}
|
||
|
||
// fakeReadFile 依 map 回傳檔案內容;不存在時回 os.ErrNotExist 類錯誤。
|
||
func fakeReadFile(m map[string]string) ReadFileFunc {
|
||
return func(path string) ([]byte, error) {
|
||
if data, ok := m[path]; ok {
|
||
return []byte(data), nil
|
||
}
|
||
return nil, errors.New("file does not exist")
|
||
}
|
||
}
|
||
|
||
const sampleTeaConfig = `logins:
|
||
- name: other.example.com
|
||
url: https://other.example.com
|
||
token: other-token
|
||
default: false
|
||
- name: gitea.alterminal.com
|
||
url: https://gitea.alterminal.com
|
||
token: site-token
|
||
default: true
|
||
preferences:
|
||
editor: false
|
||
`
|
||
|
||
func TestResolveTokenOrdering(t *testing.T) {
|
||
base := AuthOptions{
|
||
SiteURL: "https://gitea.alterminal.com",
|
||
Env: fakeEnv(nil),
|
||
ReadFile: fakeReadFile(map[string]string{
|
||
"/cfg/config.yml": sampleTeaConfig,
|
||
}),
|
||
ConfigPath: "/cfg/config.yml",
|
||
}
|
||
|
||
t.Run("flag wins over env and config", func(t *testing.T) {
|
||
opts := base
|
||
opts.FlagToken = "flag-token"
|
||
opts.Env = fakeEnv(map[string]string{"TEAI_TOKEN": "env-token"})
|
||
tok, src, err := ResolveToken(opts)
|
||
if err != nil || tok != "flag-token" || src != SourceFlag {
|
||
t.Fatalf("want flag-token/flag, got %q/%v (err %v)", tok, src, err)
|
||
}
|
||
})
|
||
|
||
t.Run("env wins over config", func(t *testing.T) {
|
||
opts := base
|
||
opts.Env = fakeEnv(map[string]string{"TEAI_TOKEN": "env-token"})
|
||
tok, src, err := ResolveToken(opts)
|
||
if err != nil || tok != "env-token" || src != SourceEnv {
|
||
t.Fatalf("want env-token/env, got %q/%v (err %v)", tok, src, err)
|
||
}
|
||
})
|
||
|
||
t.Run("tea config used when flag and env absent", func(t *testing.T) {
|
||
opts := base
|
||
tok, src, err := ResolveToken(opts)
|
||
if err != nil || tok != "site-token" || src != SourceTeaConfig {
|
||
t.Fatalf("want site-token/tea-config, got %q/%v (err %v)", tok, src, err)
|
||
}
|
||
})
|
||
|
||
t.Run("none when all sources absent", func(t *testing.T) {
|
||
opts := base
|
||
opts.ConfigPath = "/cfg/missing.yml"
|
||
tok, src, err := ResolveToken(opts)
|
||
if err != nil || tok != "" || src != SourceNone {
|
||
t.Fatalf("want \"\"/none, got %q/%v (err %v)", tok, src, err)
|
||
}
|
||
})
|
||
|
||
t.Run("blank env var is skipped", func(t *testing.T) {
|
||
opts := base
|
||
opts.Env = fakeEnv(map[string]string{"TEAI_TOKEN": " "})
|
||
tok, src, err := ResolveToken(opts)
|
||
if err != nil || tok != "site-token" || src != SourceTeaConfig {
|
||
t.Fatalf("want site-token/tea-config, got %q/%v (err %v)", tok, src, err)
|
||
}
|
||
})
|
||
|
||
t.Run("blank flag is skipped", func(t *testing.T) {
|
||
opts := base
|
||
opts.FlagToken = " "
|
||
opts.Env = fakeEnv(map[string]string{"TEAI_TOKEN": "env-token"})
|
||
tok, src, err := ResolveToken(opts)
|
||
if err != nil || tok != "env-token" || src != SourceEnv {
|
||
t.Fatalf("want env-token/env, got %q/%v (err %v)", tok, src, err)
|
||
}
|
||
})
|
||
|
||
t.Run("TEA_CONFIG points config path", func(t *testing.T) {
|
||
opts := base
|
||
opts.ConfigPath = "" // 不用 --config,改用 TEA_CONFIG
|
||
opts.Env = fakeEnv(map[string]string{
|
||
"TEA_CONFIG": "/cfg/config.yml",
|
||
})
|
||
tok, src, err := ResolveToken(opts)
|
||
if err != nil || tok != "site-token" || src != SourceTeaConfig {
|
||
t.Fatalf("want site-token/tea-config via TEA_CONFIG, got %q/%v (err %v)", tok, src, err)
|
||
}
|
||
})
|
||
|
||
t.Run("explicit --config beats TEA_CONFIG", func(t *testing.T) {
|
||
opts := base // ConfigPath = /cfg/config.yml(有 site-token)
|
||
opts.Env = fakeEnv(map[string]string{
|
||
"TEA_CONFIG": "/cfg/other.yml",
|
||
})
|
||
read := fakeReadFile(map[string]string{
|
||
"/cfg/config.yml": sampleTeaConfig,
|
||
"/cfg/other.yml": "logins:\n - name: x\n url: https://gitea.alterminal.com\n token: other\n default: true\n",
|
||
})
|
||
opts.ReadFile = read
|
||
tok, src, err := ResolveToken(opts)
|
||
if err != nil || tok != "site-token" || src != SourceTeaConfig {
|
||
t.Fatalf("want site-token from explicit config, got %q/%v (err %v)", tok, src, err)
|
||
}
|
||
})
|
||
}
|
||
|
||
func TestParseTeaConfigPicksMatchingLogin(t *testing.T) {
|
||
t.Run("host match preferred over default", func(t *testing.T) {
|
||
cfg := `logins:
|
||
- name: other
|
||
url: https://other.example.com
|
||
token: wrong-token
|
||
default: true
|
||
- name: target
|
||
url: https://gitea.alterminal.com
|
||
token: right-token
|
||
default: false
|
||
`
|
||
login, err := parseTeaConfig([]byte(cfg), "https://gitea.alterminal.com")
|
||
if err != nil {
|
||
t.Fatalf("parse error: %v", err)
|
||
}
|
||
if login.Token != "right-token" {
|
||
t.Fatalf("host-matched login should win, got token %q", login.Token)
|
||
}
|
||
})
|
||
|
||
t.Run("quoted token unquoted", func(t *testing.T) {
|
||
cfg := "logins:\n - name: t\n url: https://gitea.alterminal.com\n token: \"quoted-token\"\n default: true\n"
|
||
login, err := parseTeaConfig([]byte(cfg), "https://gitea.alterminal.com")
|
||
if err != nil {
|
||
t.Fatalf("parse error: %v", err)
|
||
}
|
||
if login.Token != "quoted-token" {
|
||
t.Fatalf("want quoted-token (unquoted), got %q", login.Token)
|
||
}
|
||
})
|
||
|
||
t.Run("malformed login entry errors", func(t *testing.T) {
|
||
cfg := "logins:\n - name: t\n url: https://gitea.alterminal.com\n oops\n"
|
||
if _, err := parseTeaConfig([]byte(cfg), ""); err == nil {
|
||
t.Fatal("malformed line should error")
|
||
}
|
||
})
|
||
|
||
t.Run("no logins yields empty", func(t *testing.T) {
|
||
cfg := "preferences:\n editor: false\n"
|
||
login, err := parseTeaConfig([]byte(cfg), "")
|
||
if err != nil || login.Token != "" {
|
||
t.Fatalf("want empty login, got %+v (err %v)", login, err)
|
||
}
|
||
})
|
||
}
|