rickub/clipublic Fork 0
main
Commits
Clone
git clone https://git.rickub.com/rickub/cli.git
git clone ssh://git@rickub.com/rickub/cli.git
issues_watch_test.go · 197 lines · 7.3 KBGo Blame HistoryRaw
Initial import of the rickub CLI as a standalone public project 1a1d430 Olivier Girardot 9h ago1package cmd
2
3import (
4 "encoding/json"
5 "errors"
6 "fmt"
7 "net/http"
8 "net/http/httptest"
9 "os"
10 "path/filepath"
11 "strings"
12 "testing"
13)
14
15// issueCmdServer stubs the pieces the `issue` and `milestone` commands touch.
16func issueCmdServer(t *testing.T) *httptest.Server {
17 t.Helper()
18 return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
19 switch {
20 case r.Method == "GET" && r.URL.Path == "/api/v1/repos/ricktester/portal/issues":
21 _ = json.NewEncoder(w).Encode(map[string]any{
22 "items": []map[string]any{
23 {"number": 1, "title": "build broke", "state": "open", "author": "rick", "labels": []map[string]any{{"name": "bug"}}, "milestone": map[string]any{"title": "v1.0"}},
24 },
25 "page": 1, "per_page": 30, "has_next": false,
26 })
27 case r.Method == "POST" && r.URL.Path == "/api/v1/repos/ricktester/portal/issues":
28 var body map[string]string
29 _ = json.NewDecoder(r.Body).Decode(&body)
30 _ = json.NewEncoder(w).Encode(map[string]any{"number": 9, "title": body["title"], "state": "open"})
31 case r.Method == "POST" && r.URL.Path == "/api/v1/repos/ricktester/portal/issues/1/state":
32 var body map[string]string
33 _ = json.NewDecoder(r.Body).Decode(&body)
34 _ = json.NewEncoder(w).Encode(map[string]any{"number": 1, "state": body["state"], "title": "build broke"})
35 default:
36 w.WriteHeader(http.StatusNotFound)
37 _, _ = w.Write([]byte(`{"error":{"code":"not_found","message":"no"}}`))
38 }
39 }))
40}
41
42func TestIssueListCreateClose(t *testing.T) {
43 srv := issueCmdServer(t)
44 defer srv.Close()
45
46 out, _, err := execute(t, "issue", "list", "-R", "ricktester/portal", "--host", srv.URL, "--token", "rickub_pat_t")
47 if err != nil {
48 t.Fatalf("issue list: %v (out=%s)", err, out)
49 }
50 for _, want := range []string{"1", "open", "bug", "v1.0", "build broke"} {
51 if !strings.Contains(out, want) {
52 t.Errorf("issue list output missing %q: %s", want, out)
53 }
54 }
55
56 out, _, err = execute(t, "issue", "create", "-R", "ricktester/portal", "--host", srv.URL, "--token", "rickub_pat_t", "-t", "a new one", "-b", "body text")
57 if err != nil {
58 t.Fatalf("issue create: %v (out=%s)", err, out)
59 }
60 if !strings.Contains(out, "Opened issue #9") {
61 t.Errorf("issue create output: %s", out)
62 }
63
64 out, _, err = execute(t, "issue", "close", "1", "-R", "ricktester/portal", "--host", srv.URL, "--token", "rickub_pat_t")
65 if err != nil {
66 t.Fatalf("issue close: %v (out=%s)", err, out)
67 }
68 if !strings.Contains(out, "now closed") {
69 t.Errorf("issue close output: %s", out)
70 }
71
72 // create without --title fails fast, client-side.
73 if _, _, err := execute(t, "issue", "create", "-R", "ricktester/portal", "--host", srv.URL, "--token", "rickub_pat_t"); err == nil {
74 t.Error("issue create without --title should fail")
75 }
76}
77
78// runWatchServer serves a run that is queued for the first two GETs and
79// success afterwards (or failure, per wantStatus).
80func runWatchServer(t *testing.T, wantStatus string) *httptest.Server {
81 t.Helper()
82 calls := 0
83 return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
84 if r.URL.Path != "/api/v1/repos/ricktester/portal/actions/runs/7" {
85 w.WriteHeader(http.StatusNotFound)
86 return
87 }
88 calls++
89 status := "queued"
90 if calls > 2 {
91 status = wantStatus
92 }
93 _ = json.NewEncoder(w).Encode(map[string]any{
94 "number": 7, "workflow": "CI", "status": status, "branch": "main", "head_sha": "abc12345",
95 "jobs": []map[string]any{{"name": "build", "status": status, "steps": []map[string]any{{"ordinal": 1, "name": "compile", "status": status}}}},
96 })
97 }))
98}
99
100func TestRunWatchSuccessAndFailure(t *testing.T) {
101 srv := runWatchServer(t, "success")
102 defer srv.Close()
103 out, _, err := execute(t, "run", "watch", "7", "-R", "ricktester/portal", "--host", srv.URL, "--token", "rickub_pat_t", "--interval", "5ms", "--timeout", "5s")
104 if err != nil {
105 t.Fatalf("watch success: %v (out=%s)", err, out)
106 }
107 if !strings.Contains(out, "finished: success") {
108 t.Errorf("watch success output: %s", out)
109 }
110
111 srv2 := runWatchServer(t, "failure")
112 defer srv2.Close()
113 out, _, err = execute(t, "run", "watch", "7", "-R", "ricktester/portal", "--host", srv2.URL, "--token", "rickub_pat_t", "--interval", "5ms", "--timeout", "5s")
114 var exitErr *runWatchExitError
115 if err == nil {
116 t.Fatal("watch failure should error")
117 }
118 if !errors.As(err, &exitErr) || exitErr.status != "failure" {
119 t.Fatalf("watch failure error: %v", err)
120 }
121 if !strings.Contains(out, "finished: failure") {
122 t.Errorf("watch failure output: %s", out)
123 }
124}
125
126func TestRunWatchTimeout(t *testing.T) {
127 srv := runWatchServer(t, "running") // never terminal
128 defer srv.Close()
129 _, _, err := execute(t, "run", "watch", "7", "-R", "ricktester/portal", "--host", srv.URL, "--token", "rickub_pat_t", "--interval", "5ms", "--timeout", "30ms")
130 if err == nil || !strings.Contains(fmt.Sprint(err), "timed out") {
131 t.Fatalf("watch timeout error: %v", err)
132 }
133}
134
135// TestAuthLoginDeviceFlow drives `rickub auth login` end-to-end against a stub
136// host: code issuance, a pending poll, approval, whoami verification, and the
137// config write (isolated via XDG_CONFIG_HOME).
138func TestAuthLoginDeviceFlow(t *testing.T) {
139 xdg := t.TempDir()
140 t.Setenv("XDG_CONFIG_HOME", xdg)
141 t.Setenv("RICKUB_HOST", "")
142 t.Setenv("RICKUB_TOKEN", "")
143
144 pollCalls := 0
145 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
146 switch {
147 case r.Method == "POST" && r.URL.Path == "/api/v1/device/code":
148 if h := r.Header.Get("Authorization"); h != "" {
149 t.Errorf("device/code sent an Authorization header: %q", h)
150 }
151 _ = json.NewEncoder(w).Encode(map[string]any{
152 "device_code": "dc-123", "user_code": "ABCD-EFGH",
153 "verification_url": "https://stub/login/device",
154 "verification_uri_complete": "https://stub/login/device?user_code=ABCD-EFGH",
155 "expires_in": 600, "interval": 1,
156 })
157 case r.Method == "POST" && r.URL.Path == "/api/v1/device/token":
158 pollCalls++
159 if pollCalls == 1 {
160 w.WriteHeader(http.StatusBadRequest)
161 _, _ = w.Write([]byte(`{"error":{"code":"authorization_pending","message":"keep polling"}}`))
162 return
163 }
164 _, _ = w.Write([]byte(`{"access_token":"rickub_pat_minted","token_type":"bearer","scope":"all"}`))
165 case r.Method == "GET" && r.URL.Path == "/api/v1/user":
166 if got := r.Header.Get("Authorization"); got != "Bearer rickub_pat_minted" {
167 t.Errorf("whoami auth = %q", got)
168 }
169 _ = json.NewEncoder(w).Encode(map[string]any{"handle": "ricktester"})
170 default:
171 w.WriteHeader(http.StatusNotFound)
172 _, _ = w.Write([]byte(`{"error":{"code":"not_found","message":"no"}}`))
173 }
174 }))
175 defer srv.Close()
176
177 out, _, err := execute(t, "auth", "login", "--host", srv.URL, "--no-browser")
178 if err != nil {
179 t.Fatalf("auth login: %v (out=%s)", err, out)
180 }
181 for _, want := range []string{"ABCD-EFGH", "https://stub/login/device", "Approved", "Logged in", "ricktester"} {
182 if !strings.Contains(out, want) {
183 t.Errorf("auth login output missing %q:\n%s", want, out)
184 }
185 }
186 // The minted token landed in the isolated config file.
187 b, err := os.ReadFile(filepath.Join(xdg, "rickub", "config.yaml"))
188 if err != nil {
189 t.Fatalf("read config: %v", err)
190 }
191 if !strings.Contains(string(b), "rickub_pat_minted") || !strings.Contains(string(b), srv.URL) {
192 t.Errorf("config file content: %s", string(b))
193 }
194 if pollCalls != 2 {
195 t.Errorf("poll calls = %d, want 2 (pending then minted)", pollCalls)
196 }
197}