internal/gitea:API 用戶端、認證與輸出基礎(#5)
- internal/gitea:僅標準庫的 HTTP 用戶端;ListAll 以 limit=50 逐頁抓滿 - 認證選序 --token → TEAI_TOKEN → tea 組態(--config/TEA_CONFIG/預設路徑); token 不入輸出、日誌與錯err誤訊息 - 共用輸出 --output json|table(預設 json)、--timeout(預設 30s) - CLI 結束碼 0/2/3:用法錯誤 → 2、API 錯誤(*gitea.ErrAPI)→ 3 - 單元測試:認證選序、tea 組態解析、分颁頁、逾時/401/5xx 錯誤映射、輸出 實作 issue #5
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
// client_test.go 用 httptest 伺服器驗證用戶端行為:分頁(limit=50)、
|
||||
// 認證表頭、逾時與錯誤映射(連線失敗、401、5xx → *ErrAPI)。
|
||||
package gitea
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// newTestClient 對 srv 建立用戶端。
|
||||
func newTestClient(srvURL, token string, timeout time.Duration) (*Client, error) {
|
||||
return New(srvURL, token, timeout)
|
||||
}
|
||||
|
||||
func TestNewRejectsInvalidBaseURL(t *testing.T) {
|
||||
for _, bad := range []string{"", "not a url", "http://"} {
|
||||
if _, err := New(bad, "", 0); err == nil {
|
||||
t.Errorf("New(%q) should fail", bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientSendsTokenHeader(t *testing.T) {
|
||||
var gotAuth atomic.Value
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotAuth.Store(r.Header.Get("Authorization"))
|
||||
fmt.Fprint(w, `{"login":"max"}`)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c, err := newTestClient(srv.URL, "secret-token", time.Second)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var out map[string]any
|
||||
if err := c.GetJSON(context.Background(), "/user", &out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := gotAuth.Load(); got != "token secret-token" {
|
||||
t.Fatalf("Authorization header = %v, want token secret-token", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrAPIMessageHasNoToken(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c, err := newTestClient(srv.URL, "secret-token", time.Second)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = c.GetJSON(context.Background(), "/user", nil)
|
||||
var apiErr *ErrAPI
|
||||
if !errors.As(err, &apiErr) {
|
||||
t.Fatalf("want *ErrAPI, got %v", err)
|
||||
}
|
||||
if apiErr.StatusCode != http.StatusUnauthorized {
|
||||
t.Fatalf("status = %d, want 401", apiErr.StatusCode)
|
||||
}
|
||||
if got := apiErr.Error(); got == "" || contains(got, "secret-token") {
|
||||
t.Fatalf("error message must not leak token, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListAllPaginatesWithLimitFifty(t *testing.T) {
|
||||
var calls int32
|
||||
var limits []int32
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
atomic.AddInt32(&calls, 1)
|
||||
limit := int32(0)
|
||||
fmt.Sscanf(r.URL.Query().Get("limit"), "%d", &limit)
|
||||
limits = append(limits, limit)
|
||||
page := 1
|
||||
fmt.Sscanf(r.URL.Query().Get("page"), "%d", &page)
|
||||
// 第 1、2 頁各滿 50 筆,第 3 頁 10 筆後結束。
|
||||
total := 110
|
||||
start := (page - 1) * 50
|
||||
end := start + 50
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if start >= total {
|
||||
fmt.Fprint(w, "[]")
|
||||
return
|
||||
}
|
||||
items := make([]int, 0, end-start)
|
||||
for i := start; i < end; i++ {
|
||||
items = append(items, i)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(items)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c, err := newTestClient(srv.URL, "", time.Second)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var got []int
|
||||
if err := c.ListAll(context.Background(), "/things", ListOptions{}, &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(got) != 110 {
|
||||
t.Fatalf("want 110 items, got %d", len(got))
|
||||
}
|
||||
for i, v := range got {
|
||||
if v != i {
|
||||
t.Fatalf("item %d = %d, want %d", i, v, i)
|
||||
}
|
||||
}
|
||||
if n := atomic.LoadInt32(&calls); n != 3 {
|
||||
t.Fatalf("want 3 page requests, got %d", n)
|
||||
}
|
||||
for i, l := range limits {
|
||||
if l != 50 {
|
||||
t.Fatalf("page %d limit = %d, want 50", i+1, l)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestListAllRejectsBadOut(t *testing.T) {
|
||||
c, err := newTestClient("https://gitea.alterminal.com", "", time.Second)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var notSlice int
|
||||
if err := c.ListAll(context.Background(), "/x", ListOptions{}, ¬Slice); err == nil {
|
||||
t.Fatal("non-slice out should error")
|
||||
}
|
||||
preloaded := []int{1}
|
||||
if err := c.ListAll(context.Background(), "/x", ListOptions{}, &preloaded); err == nil {
|
||||
t.Fatal("non-empty out should error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientTimeoutSurfacesAsErrAPI(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
fmt.Fprint(w, `{}`)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c, err := newTestClient(srv.URL, "", 50*time.Millisecond)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = c.GetJSON(context.Background(), "/user", nil)
|
||||
var apiErr *ErrAPI
|
||||
if !errors.As(err, &apiErr) {
|
||||
t.Fatalf("timeout should surface as *ErrAPI, got %v", err)
|
||||
}
|
||||
if apiErr.StatusCode != 0 {
|
||||
t.Fatalf(" StatusCode should be 0 for transport error, got %d", apiErr.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientServerFiveHundred(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c, err := newTestClient(srv.URL, "", time.Second)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = c.GetJSON(context.Background(), "/user", nil)
|
||||
var apiErr *ErrAPI
|
||||
if !errors.As(err, &apiErr) || apiErr.StatusCode != 500 {
|
||||
t.Fatalf("want *ErrAPI with 500, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// contains 是簡單子字串檢查(避免只為此引入 strings 到測試以外的地方)。
|
||||
func contains(s, sub string) bool {
|
||||
return len(s) >= len(sub) && (s == sub || len(sub) == 0 || indexOf(s, sub) >= 0)
|
||||
}
|
||||
|
||||
func indexOf(s, sub string) int {
|
||||
for i := 0; i+len(sub) <= len(s); i++ {
|
||||
if s[i:i+len(sub)] == sub {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
Reference in New Issue
Block a user