- 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
196 lines
5.2 KiB
Go
196 lines
5.2 KiB
Go
// 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
|
|
}
|