Files
teai/internal/gitea/auth_test.go
T
max 6951817313 internal/gitea:API 用戶端、認證與輸出基礎(#5)
- 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
2026-09-10 08:20:08 +08:00

182 lines
5.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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)
}
})
}