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
repo_test.go · 203 lines · 6.3 KBGo Blame HistoryRaw
Initial import of the rickub CLI as a standalone public project 1a1d430 Olivier Girardot 9h ago1package cmd
2
3import (
4 "bytes"
5 "encoding/json"
6 "net/http"
7 "net/http/httptest"
8 "strings"
9 "testing"
10)
11
12// execute runs the root command with args against a fresh output buffer, after
13// resetting the shared flag globals so tests don't leak state into each other.
14func execute(t *testing.T, args ...string) (string, string, error) {
15 t.Helper()
16 // Reset globals touched by these tests.
17 flagJSON = false
18 flagHost = ""
19 flagToken = ""
20 repoListUser = ""
21 repoListOrg = ""
22 pageFlag = 0
23 perPageFlag = 0
24 issueTitle = ""
25 issueBody = ""
26 issueRepo = ""
27 issueState = "open"
28 milestoneRepo = ""
29 milestoneState = "open"
30 milestoneTitle = ""
31 milestoneDue = ""
32 milestoneDescription = ""
33 runRepo = ""
34 runWatchInterval = "2s"
35 runWatchTimeout = "30m"
36 runWatchLogs = false
37 authLoginScope = "all"
38 authLoginNoBrowser = false
39
40 var out, errBuf bytes.Buffer
41 rootCmd.SetOut(&out)
42 rootCmd.SetErr(&errBuf)
43 rootCmd.SetArgs(args)
44 err := rootCmd.Execute()
45 return out.String(), errBuf.String(), err
46}
47
48func repoListServer(t *testing.T) *httptest.Server {
49 return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
50 if r.URL.Path != "/api/v1/users/ricktester/repos" {
51 w.WriteHeader(http.StatusNotFound)
52 w.Write([]byte(`{"error":{"code":"not_found","message":"no"}}`))
53 return
54 }
55 json.NewEncoder(w).Encode(map[string]any{
56 "page": 1, "per_page": 30, "has_next": false,
57 "items": []map[string]any{
58 {"full_name": "ricktester/alpha", "visibility": "public", "description": "first"},
59 {"full_name": "ricktester/beta", "visibility": "private", "description": ""},
60 },
61 })
62 }))
63}
64
65func TestRepoListTable(t *testing.T) {
66 srv := repoListServer(t)
67 defer srv.Close()
68 t.Setenv("XDG_CONFIG_HOME", t.TempDir())
69 t.Setenv("RICKUB_HOST", srv.URL)
70 t.Setenv("RICKUB_TOKEN", "rickub_pat_test")
71
72 out, _, err := execute(t, "repo", "list", "--user", "ricktester")
73 if err != nil {
74 t.Fatalf("execute: %v", err)
75 }
76 if !strings.Contains(out, "NAME") || !strings.Contains(out, "VISIBILITY") {
77 t.Errorf("missing table header:\n%s", out)
78 }
79 if !strings.Contains(out, "ricktester/alpha") || !strings.Contains(out, "ricktester/beta") {
80 t.Errorf("missing rows:\n%s", out)
81 }
82 // Empty description rendered as a dash.
83 if !strings.Contains(out, "-") {
84 t.Errorf("expected dash for empty description:\n%s", out)
85 }
86}
87
88func TestRepoListJSONPassthrough(t *testing.T) {
89 srv := repoListServer(t)
90 defer srv.Close()
91 t.Setenv("XDG_CONFIG_HOME", t.TempDir())
92 t.Setenv("RICKUB_HOST", srv.URL)
93 t.Setenv("RICKUB_TOKEN", "rickub_pat_test")
94
95 out, _, err := execute(t, "repo", "list", "--user", "ricktester", "--json")
96 if err != nil {
97 t.Fatalf("execute: %v", err)
98 }
99 var decoded struct {
100 Items []struct {
101 FullName string `json:"full_name"`
102 } `json:"items"`
103 }
104 if err := json.Unmarshal([]byte(out), &decoded); err != nil {
105 t.Fatalf("output is not JSON: %v\n%s", err, out)
106 }
107 if len(decoded.Items) != 2 || decoded.Items[0].FullName != "ricktester/alpha" {
108 t.Errorf("unexpected JSON items: %+v", decoded.Items)
109 }
110}
111
112func TestRepoViewNotFoundError(t *testing.T) {
113 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
114 w.WriteHeader(http.StatusNotFound)
115 w.Write([]byte(`{"error":{"code":"not_found","message":"repository not found"}}`))
116 }))
117 defer srv.Close()
118 t.Setenv("XDG_CONFIG_HOME", t.TempDir())
119 t.Setenv("RICKUB_HOST", srv.URL)
120 t.Setenv("RICKUB_TOKEN", "rickub_pat_test")
121
122 _, _, err := execute(t, "repo", "view", "ghost/repo")
123 if err == nil {
124 t.Fatal("expected error for 404")
125 }
126 if !strings.Contains(err.Error(), "not_found") {
127 t.Errorf("error should surface code: %v", err)
128 }
129}
130
131func TestHumanTime(t *testing.T) {
132 cases := map[string]string{
133 "": "-", // empty -> dash
134 "2026-07-21T00:07:53.58474Z": "2026-07-21 00:07 UTC", // sub-second RFC3339
135 "2026-07-21T00:07:53Z": "2026-07-21 00:07 UTC", // whole-second RFC3339
136 "2026-07-21T02:07:53+02:00": "2026-07-21 00:07 UTC", // offset normalized to UTC
137 "not-a-timestamp": "not-a-timestamp", // unparseable -> unchanged
138 }
139 for in, want := range cases {
140 if got := humanTime(in); got != want {
141 t.Errorf("humanTime(%q) = %q, want %q", in, got, want)
142 }
143 }
144}
145
146// TestRepoViewHumanizesTimestamp verifies the default output humanizes created_at
147// while --json passes the raw timestamp through unchanged.
148func TestRepoViewHumanizesTimestamp(t *testing.T) {
149 const raw = "2026-07-21T00:07:53.58474Z"
150 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
151 w.Header().Set("Content-Type", "application/json")
152 _, _ = w.Write([]byte(`{"owner":"ricktester","name":"demo","full_name":"ricktester/demo","visibility":"public","default_branch":"main","created_at":"` + raw + `"}`))
153 }))
154 defer srv.Close()
155 t.Setenv("XDG_CONFIG_HOME", t.TempDir())
156 t.Setenv("RICKUB_HOST", srv.URL)
157 t.Setenv("RICKUB_TOKEN", "rickub_pat_test")
158
159 // Default (human) output: humanized, and NOT the raw nanosecond string.
160 out, _, err := execute(t, "repo", "view", "ricktester/demo")
161 if err != nil {
162 t.Fatalf("execute: %v", err)
163 }
164 if !strings.Contains(out, "2026-07-21 00:07 UTC") {
165 t.Errorf("human output not humanized:\n%s", out)
166 }
167 if strings.Contains(out, raw) {
168 t.Errorf("human output still contains the raw timestamp:\n%s", out)
169 }
170
171 // --json output: raw timestamp preserved verbatim.
172 jsonOut, _, err := execute(t, "repo", "view", "ricktester/demo", "--json")
173 if err != nil {
174 t.Fatalf("execute --json: %v", err)
175 }
176 if !strings.Contains(jsonOut, raw) {
177 t.Errorf("--json output should keep the raw timestamp:\n%s", jsonOut)
178 }
179}
180
181func TestParseOwnerRepo(t *testing.T) {
182 o, r, err := parseOwnerRepo("ricktester/demo.git")
183 if err != nil || o != "ricktester" || r != "demo" {
184 t.Errorf("got %q/%q err=%v", o, r, err)
185 }
186 if _, _, err := parseOwnerRepo("nope"); err == nil {
187 t.Error("expected error for missing slash")
188 }
189}
190
191func TestOwnerRepoFromRemote(t *testing.T) {
192 cases := map[string][2]string{
193 "http://localhost:8080/ricktester/demo.git": {"ricktester", "demo"},
194 "ssh://git@localhost:2222/ricktester/demo": {"ricktester", "demo"},
195 "git@rickub.com:acme/api.git": {"acme", "api"},
196 }
197 for url, want := range cases {
198 o, r, err := ownerRepoFromRemote(url)
199 if err != nil || o != want[0] || r != want[1] {
200 t.Errorf("%s => %q/%q err=%v, want %v", url, o, r, err, want)
201 }
202 }
203}