cli_test、dailyops_commands_test、login_test、exitcode_test 原以 int(ExitUsage)/int(ExitAPI)/int(ExitInternal) 常數互比,常數若再 與 README 脫鉤測試仍會綠。改為直接斷言 0/2/3(內部錯誤 1)。 常數本體已隨 #5(6951817)改為明確數值,未知命令實測回 2。
293 lines
11 KiB
Go
293 lines
11 KiB
Go
// dailyops_commands_test.go — #8 的驗收測試:以 httptest 假伺服器驗證
|
||
// issues/pulls/labels/milestones/releases/repos/api 的讀取行為,與
|
||
// 寫入命令的 --yes 閘門(未帶 --yes 不得發出任何請求)。
|
||
package cli
|
||
|
||
import (
|
||
"fmt"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// opsTestServer 建立假 Gitea API 伺服器與對應的執行輔助。
|
||
func opsTestServer(t *testing.T, handler http.HandlerFunc) func(args ...string) (string, string, int) {
|
||
t.Helper()
|
||
srv := httptest.NewServer(handler)
|
||
t.Cleanup(srv.Close)
|
||
return func(args ...string) (string, string, int) {
|
||
full := append([]string{"--url", srv.URL}, args...)
|
||
return runCLI(full...) // 套件級輔助(見下)
|
||
}
|
||
}
|
||
|
||
// runCLI 是 cli_test.go `run` 的別名,避免與各測試內的區域變數 run 混淆。
|
||
func runCLI(args ...string) (string, string, int) {
|
||
return run(args...)
|
||
}
|
||
|
||
// TestIssuesListReadsOpenIssuesOnly 驗證 issues list 帶 state=open 與
|
||
// type=issues(排除 PR),輸出 JSON 陣列。
|
||
func TestIssuesListReadsOpenIssuesOnly(t *testing.T) {
|
||
var gotQuery string
|
||
run := opsTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||
gotQuery = r.URL.RawQuery
|
||
fmt.Fprint(w, `[{"number":8,"title":"日常操作","state":"open","user":{"login":"ceo"},"assignees":[{"login":"alex"}],"updated_at":"2026-09-10T08:13:00Z"}]`)
|
||
})
|
||
stdout, _, code := run("issues", "list", "--repo", "alterminal/teai")
|
||
if code != 0 {
|
||
t.Fatalf("issues list 應回 0,得到 %d", code)
|
||
}
|
||
if !strings.Contains(gotQuery, "state=open") || !strings.Contains(gotQuery, "type=issues") {
|
||
t.Errorf("查詢參數應含 state=open 與 type=issues,得到 %q", gotQuery)
|
||
}
|
||
if !strings.Contains(stdout, `"number":8`) || !strings.Contains(stdout, `"author":"ceo"`) {
|
||
t.Errorf("輸出應含 number 與 author 欄位,得到:%s", stdout)
|
||
}
|
||
}
|
||
|
||
// TestIssuesListRequiresRepo 驗證 --repo 必填。
|
||
func TestIssuesListRequiresRepo(t *testing.T) {
|
||
run := opsTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||
t.Errorf("不應發出任何請求")
|
||
})
|
||
_, stderr, code := run("issues", "list")
|
||
// 對照 README「輸出與結束碼」:用法錯誤=2(#10)。
|
||
if code != 2 {
|
||
t.Errorf("缺 --repo 應回 2(README 用法錯誤),得到 %d", code)
|
||
}
|
||
if !strings.Contains(stderr, "--repo") {
|
||
t.Errorf("錯誤訊息應提及 --repo,得到:%s", stderr)
|
||
}
|
||
}
|
||
|
||
// TestIssuesViewWithComments 驗證 view --comments 連留言輸出。
|
||
func TestIssuesViewWithComments(t *testing.T) {
|
||
var paths []string
|
||
run := opsTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||
paths = append(paths, r.URL.Path)
|
||
switch {
|
||
case strings.HasSuffix(r.URL.Path, "/issues/8"):
|
||
fmt.Fprint(w, `{"number":8,"title":"日常操作","state":"open","user":{"login":"ceo"},"body":"內文","html_url":"https://x/8","labels":[],"updated_at":"t"}`)
|
||
case strings.HasSuffix(r.URL.Path, "/comments"):
|
||
fmt.Fprint(w, `[{"body":"開始處理","created_at":"t1","user":{"login":"alex"}}]`)
|
||
default:
|
||
fmt.Fprint(w, `[]`)
|
||
}
|
||
})
|
||
stdout, _, code := run("issues", "view", "--repo", "alterminal/teai", "--comments", "8")
|
||
if code != 0 {
|
||
t.Fatalf("view --comments 應回 0,得到 %d", code)
|
||
}
|
||
if len(paths) != 2 || !strings.HasSuffix(paths[1], "/issues/8/comments") {
|
||
t.Errorf("應依序呼叫 issue 與 comments 端點,得到 %v", paths)
|
||
}
|
||
if !strings.Contains(stdout, `"body":"開始處理"`) {
|
||
t.Errorf("輸出應含留言內容,得到:%s", stdout)
|
||
}
|
||
}
|
||
|
||
// TestWriteCommandsRequireYes 驗證所有寫入命令未帶 --yes 時:
|
||
// 回 ExitUsage 且不發出任何 HTTP 請求(issue 驗收條件)。
|
||
func TestWriteCommandsRequireYes(t *testing.T) {
|
||
cases := []struct {
|
||
name string
|
||
args []string
|
||
}{
|
||
{"issues create", []string{"issues", "create", "--repo", "a/b", "--title", "t"}},
|
||
{"issues close", []string{"issues", "close", "--repo", "a/b", "1"}},
|
||
{"issues comment", []string{"issues", "comment", "--repo", "a/b", "--body", "x", "1"}},
|
||
{"pulls create", []string{"pulls", "create", "--repo", "a/b", "--head", "h", "--title", "t"}},
|
||
{"pulls merge", []string{"pulls", "merge", "--repo", "a/b", "1"}},
|
||
{"pulls comment", []string{"pulls", "comment", "--repo", "a/b", "--body", "x", "1"}},
|
||
{"labels create", []string{"labels", "create", "--repo", "a/b", "--name", "n"}},
|
||
{"milestones create", []string{"milestones", "create", "--repo", "a/b", "--title", "t"}},
|
||
{"api POST", []string{"api", "--method", "POST", "/repos/a/b/issues"}},
|
||
}
|
||
for _, tc := range cases {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
run := opsTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||
t.Errorf("%s:未帶 --yes 不得發出請求(%s %s)", tc.name, r.Method, r.URL.Path)
|
||
})
|
||
_, stderr, code := run(tc.args...)
|
||
// 對照 README「輸出與結束碼」:用法錯誤=2(#10)。
|
||
if code != 2 {
|
||
t.Errorf("%s 未帶 --yes 應回 2(README 用法錯誤),得到 %d", tc.name, code)
|
||
}
|
||
if !strings.Contains(stderr, "--yes") {
|
||
t.Errorf("%s 錯誤訊息應提及 --yes,得到:%s", tc.name, stderr)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestIssuesCreateWithYes 驗證帶 --yes 時 POST 正確 payload。
|
||
func TestIssuesCreateWithYes(t *testing.T) {
|
||
var gotMethod, gotPath, gotBody string
|
||
run := opsTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||
gotMethod, gotPath = r.Method, r.URL.Path
|
||
buf := make([]byte, 512)
|
||
n, _ := r.Body.Read(buf)
|
||
gotBody = string(buf[:n])
|
||
fmt.Fprint(w, `{"number":15,"title":"新 issue","html_url":"https://x/15"}`)
|
||
})
|
||
stdout, _, code := run("issues", "create", "--repo", "alterminal/teai", "--title", "新 issue", "--body", "說明", "--assignees", "alex,ceo", "--yes")
|
||
if code != 0 {
|
||
t.Fatalf("create --yes 應回 0,得到 %d", code)
|
||
}
|
||
if gotMethod != "POST" || !strings.HasSuffix(gotPath, "/repos/alterminal/teai/issues") {
|
||
t.Errorf("應 POST /repos/alterminal/teai/issues,得到 %s %s", gotMethod, gotPath)
|
||
}
|
||
if !strings.Contains(gotBody, `"assignees":["alex","ceo"]`) {
|
||
t.Errorf("body 應含 assignees,得到:%s", gotBody)
|
||
}
|
||
if !strings.Contains(stdout, `"number":15`) {
|
||
t.Errorf("輸出應含新 issue 編號,得到:%s", stdout)
|
||
}
|
||
}
|
||
|
||
// TestPullsListAndSubcommands 驗證 pulls list 與 pulls(無子命令)可並存。
|
||
func TestPullsListAndSubcommands(t *testing.T) {
|
||
var gotPath string
|
||
run := opsTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||
gotPath = r.URL.Path
|
||
fmt.Fprint(w, `[{"number":13,"title":"PR","state":"open","user":{"login":"alex"},"head":{"ref":"feat"},"base":{"ref":"main"},"updated_at":"t"}]`)
|
||
})
|
||
stdout, _, code := run("pulls", "list", "--repo", "alterminal/teai")
|
||
if code != 0 {
|
||
t.Fatalf("pulls list 應回 0,得到 %d", code)
|
||
}
|
||
if !strings.HasSuffix(gotPath, "/repos/alterminal/teai/pulls") {
|
||
t.Errorf("應呼叫 /repos/alterminal/teai/pulls,得到 %s", gotPath)
|
||
}
|
||
if !strings.Contains(stdout, `"head":"feat"`) || !strings.Contains(stdout, `"base":"main"`) {
|
||
t.Errorf("輸出應含 head/base 欄位,得到:%s", stdout)
|
||
}
|
||
}
|
||
|
||
// TestLabelsMilestonesReleasesList 驗證三個清單命令打對端點並輸出。
|
||
func TestLabelsMilestonesReleasesList(t *testing.T) {
|
||
cases := []struct {
|
||
args []string
|
||
path string
|
||
resp string
|
||
wantOut string
|
||
}{
|
||
{
|
||
args: []string{"labels", "list", "--repo", "a/b"},
|
||
path: "/api/v1/repos/a/b/labels",
|
||
resp: `[{"name":"enhancement","color":"#84b6eb","description":""}]`,
|
||
wantOut: `"name":"enhancement"`,
|
||
},
|
||
{
|
||
args: []string{"milestones", "list", "--repo", "a/b"},
|
||
path: "/api/v1/repos/a/b/milestones",
|
||
resp: `[{"title":"v1","state":"open","description":"","due_on":""}]`,
|
||
wantOut: `"title":"v1"`,
|
||
},
|
||
{
|
||
args: []string{"releases", "list", "--repo", "a/b"},
|
||
path: "/api/v1/repos/a/b/releases",
|
||
resp: `[{"tag_name":"v1.0.0","name":"首個發佈","draft":false,"prerelease":false,"published_at":"t"}]`,
|
||
wantOut: `"tag_name":"v1.0.0"`,
|
||
},
|
||
{
|
||
args: []string{"repos", "list", "--org", "alterminal"},
|
||
path: "/api/v1/orgs/alterminal/repos",
|
||
resp: `[{"full_name":"alterminal/teai","private":false,"empty":false,"updated_at":"t","html_url":"u"}]`,
|
||
wantOut: `"full_name":"alterminal/teai"`,
|
||
},
|
||
}
|
||
for _, tc := range cases {
|
||
t.Run(strings.Join(tc.args, " "), func(t *testing.T) {
|
||
var gotPath string
|
||
run := opsTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||
gotPath = r.URL.Path
|
||
fmt.Fprint(w, tc.resp)
|
||
})
|
||
stdout, _, code := run(tc.args...)
|
||
if code != 0 {
|
||
t.Fatalf("%v 應回 0,得到 %d", tc.args, code)
|
||
}
|
||
if gotPath != tc.path {
|
||
t.Errorf("應呼叫 %s,得到 %s", tc.path, gotPath)
|
||
}
|
||
if !strings.Contains(stdout, tc.wantOut) {
|
||
t.Errorf("輸出應含 %s,得到:%s", tc.wantOut, stdout)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestAPIDirectPassthrough 驗證 teai api 原樣輸出、GET 免 --yes、
|
||
// POST 未帶 --yes 被擋。
|
||
func TestAPIDirectPassthrough(t *testing.T) {
|
||
var gotMethod string
|
||
run := opsTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||
gotMethod = r.Method
|
||
fmt.Fprint(w, `{"version":"1.27.0"}`)
|
||
})
|
||
stdout, _, code := run("api", "/version")
|
||
if code != 0 || gotMethod != "GET" {
|
||
t.Fatalf("GET /version 應回 0,得到 %d (%s)", code, gotMethod)
|
||
}
|
||
if strings.TrimSpace(stdout) != `{"version":"1.27.0"}` {
|
||
t.Errorf("api 應原樣輸出,得到:%s", stdout)
|
||
}
|
||
|
||
_, stderr, code := run("api", "--method", "POST", "/version")
|
||
// 對照 README「輸出與結束碼」:用法錯誤=2(#10)。
|
||
if code != 2 || !strings.Contains(stderr, "--yes") {
|
||
t.Errorf("POST 未帶 --yes 應回 2 並提及 --yes,得到 %d:%s", code, stderr)
|
||
}
|
||
}
|
||
|
||
// TestAPIDirectQueryInPath 驗證 path 內含 ?query 時查詢字串正確傳遞。
|
||
func TestAPIDirectQueryInPath(t *testing.T) {
|
||
var gotQuery, gotPath string
|
||
run := opsTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||
gotPath, gotQuery = r.URL.Path, r.URL.RawQuery
|
||
fmt.Fprint(w, `[]`)
|
||
})
|
||
_, _, code := run("api", "/repos/a/b/issues?state=closed&limit=10")
|
||
if code != 0 {
|
||
t.Fatalf("api 帶查詢字串應回 0,得到 %d", code)
|
||
}
|
||
if gotPath != "/api/v1/repos/a/b/issues" || gotQuery != "state=closed&limit=10" {
|
||
t.Errorf("應剝離查詢字串,得到 path=%s query=%s", gotPath, gotQuery)
|
||
}
|
||
}
|
||
|
||
// TestIssuesCommentPostsBody 驗證 comment --yes 送出 body。
|
||
func TestIssuesCommentPostsBody(t *testing.T) {
|
||
var gotPath, gotBody string
|
||
run := opsTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||
buf := make([]byte, 512)
|
||
n, _ := r.Body.Read(buf)
|
||
gotPath, gotBody = r.URL.Path, string(buf[:n])
|
||
fmt.Fprint(w, `{"body":"留言","created_at":"t","user":{"login":"alex"}}`)
|
||
})
|
||
_, _, code := run("issues", "comment", "--repo", "alterminal/teai", "--body", "留言", "--yes", "8")
|
||
if code != 0 {
|
||
t.Fatalf("comment --yes 應回 0,得到 %d", code)
|
||
}
|
||
if !strings.HasSuffix(gotPath, "/issues/8/comments") || !strings.Contains(gotBody, `"body":"留言"`) {
|
||
t.Errorf("應 POST comments 端點帶 body,得到 %s %s", gotPath, gotBody)
|
||
}
|
||
}
|
||
|
||
// TestExtractGlobalsAllowsCommandFlags 驗證命令旗標與全域選項可交錯。
|
||
func TestExtractGlobalsAllowsCommandFlags(t *testing.T) {
|
||
run := opsTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||
fmt.Fprint(w, `{"login":"alex"}`)
|
||
})
|
||
stdout, _, code := run("whoami", "--output", "table")
|
||
if code != 0 {
|
||
t.Fatalf("whoami --output table 應回 0,得到 %d", code)
|
||
}
|
||
if !strings.Contains(stdout, "alex") {
|
||
t.Errorf("table 輸出應含 alex,得到:%s", stdout)
|
||
}
|
||
}
|