1
package main
1
package main
2
2
3
import "bufio"
3
import "bufio"
4
import "fmt"
4
import "fmt"
5
import "os"
5
import "os"
6
import "strings"
6
import "strings"
7
7
8
func runAuth(args []string) error {
8
func runAuth(args []string) error {
9
if len(args) == 0 {
9
if len(args) == 0 {
10
return fmt.Errorf("usage: okg auth login")
10
return fmt.Errorf("usage: okg auth login")
11
}
11
}
12
switch args[0] {
12
switch args[0] {
13
case "login":
13
case "login":
14
return runAuthLogin(args[1:])
14
return runAuthLogin(args[1:])
15
default:
15
default:
16
return fmt.Errorf(
16
return fmt.Errorf(
17
"unknown auth command: %s", args[0])
17
"unknown auth command: %s", args[0])
18
}
18
}
19
}
19
}
20
20
21
func runAuthLogin(args []string) error {
21
func runAuthLogin(args []string) error {
22
host := ""
22
host := ""
23
user := ""
23
user := ""
24
for i := 0; i < len(args); i++ {
24
for i := 0; i < len(args); i++ {
25
switch args[i] {
25
switch args[i] {
26
case "--host":
26
case "--host":
27
i++
27
i++
28
if i >= len(args) {
28
if i >= len(args) {
29
return fmt.Errorf(
29
return fmt.Errorf(
30
"--host requires a value")
30
"--host requires a value")
31
}
31
}
32
host = args[i]
32
host = args[i]
33
case "--user":
33
case "--user":
34
i++
34
i++
35
if i >= len(args) {
35
if i >= len(args) {
36
return fmt.Errorf(
36
return fmt.Errorf(
37
"--user requires a value")
37
"--user requires a value")
38
}
38
}
39
user = args[i]
39
user = args[i]
40
default:
40
default:
41
return fmt.Errorf(
41
return fmt.Errorf(
42
"unknown flag: %s", args[i])
42
"unknown flag: %s", args[i])
43
}
43
}
44
}
44
}
45
45
46
reader := bufio.NewReader(os.Stdin)
46
reader := bufio.NewReader(os.Stdin)
47
47
48
if host == "" {
48
if host == "" {
49
fmt.Print(
49
fmt.Print(
50
"Host (default http://localhost:42069): ")
51
line, _ := reader.ReadString('\n')
50
line, _ := reader.ReadString('\n')
52
host = strings.TrimSpace(line)
51
host = strings.TrimSpace(line)
53
if host == "" {
52
if host == "" {
54
host = lost
53
host = lost
55
}
54
}
56
}
55
}
57
56
58
fmt.Print("API key: ")
57
fmt.Print("API key: ")
59
api_key, _ := reader.ReadString('\n')
58
api_key, _ := reader.ReadString('\n')
60
api_key = strings.TrimSpace(api_key)
59
api_key = strings.TrimSpace(api_key)
61
if api_key == "" {
60
if api_key == "" {
62
return fmt.Errorf("API key is required")
61
return fmt.Errorf("API key is required")
63
}
62
}
64
63
65
cfg := &Config{Host: host, ApiKey: api_key}
64
cfg := &Config{Host: host, ApiKey: api_key}
66
if err := saveConfig(cfg); err != nil {
65
if err := saveConfig(cfg); err != nil {
67
return fmt.Errorf("saving config: %v", err)
66
return fmt.Errorf("saving config: %v", err)
68
}
67
}
69
fmt.Printf("Saved config to %s\n", configPath())
68
fmt.Printf("Saved config to %s\n", configPath())
70
69
71
// If --user given, call profile edit to map
70
// If --user given, call profile edit to map
72
// the API key to this username in mock who.
71
// the API key to this username in mock who.
73
if user != "" {
72
if user != "" {
74
cl := newKleeClient(cfg)
73
cl := newKleeClient(cfg)
75
payload := map[string]string{
74
payload := map[string]string{
76
"username": user,
75
"username": user,
77
"name": user,
76
"name": user,
78
}
77
}
79
err := cl.PostJSON(
78
err := cl.PostJSON(
80
"/login/profile/edit", payload, nil)
79
"/login/profile/edit", payload, nil)
81
if err != nil {
80
if err != nil {
82
return fmt.Errorf(
81
return fmt.Errorf(
83
"setting username: %v", err)
82
"setting username: %v", err)
84
}
83
}
85
fmt.Printf("Authenticated as %s\n", user)
84
fmt.Printf("Authenticated as %s\n", user)
86
}
85
}
87
86
88
return nil
87
return nil
89
}
88
}
1
package main
1
package main
2
2
3
import "encoding/json"
3
import "encoding/json"
4
import "net/http"
4
import "net/http"
5
import "net/http/httptest"
5
import "net/http/httptest"
6
import "os"
6
import "os"
7
import "strings"
7
import "strings"
8
import "testing"
8
import "testing"
9
import "time"
9
import "time"
10
10
11
import "oscarkilo.com/okg/klee"
11
import "oscarkilo.com/okg/klee"
12
12
13
// TestMain prevents the entire test binary from accidentally reaching
14
// the production klee host (which is the default in loadConfig). Set
15
// it once here; individual tests that need a specific URL (e.g. a
16
// mock httptest server) still set OKG_HOST themselves.
17
func TestMain(m *testing.M) {
18
os.Setenv("OKG_HOST", "http://localhost:42069")
19
os.Exit(m.Run())
20
}
21
13
func TestRepoRegex(t *testing.T) {
22
func TestRepoRegex(t *testing.T) {
14
check := func(url, want string) {
23
check := func(url, want string) {
15
t.Helper()
24
t.Helper()
16
m := repoRegex.FindStringSubmatch(url)
25
m := repoRegex.FindStringSubmatch(url)
17
if want == "" {
26
if want == "" {
18
if m != nil {
27
if m != nil {
19
t.Errorf(
28
t.Errorf(
20
"%q: want no match, got %q", url, m[1])
29
"%q: want no match, got %q", url, m[1])
21
}
30
}
22
return
31
return
23
}
32
}
24
if m == nil {
33
if m == nil {
25
t.Errorf(
34
t.Errorf(
26
"%q: want %q, got no match", url, want)
35
"%q: want %q, got no match", url, want)
27
return
36
return
28
}
37
}
29
if m[1] != want {
38
if m[1] != want {
30
t.Errorf(
39
t.Errorf(
31
"%q: want %q, got %q", url, want, m[1])
40
"%q: want %q, got %q", url, want, m[1])
32
}
41
}
33
}
42
}
34
check(
43
check(
35
"https://code.oscarkilo.com/widget.git",
44
"https://code.oscarkilo.com/widget.git",
36
"widget")
45
"widget")
37
check(
46
check(
38
"https://code.oscarkilo.com/klee.git",
47
"https://code.oscarkilo.com/klee.git",
39
"klee")
48
"klee")
40
check(
49
check(
41
"https://code.oscarkilo.com/my-repo.git",
50
"https://code.oscarkilo.com/my-repo.git",
42
"my-repo")
51
"my-repo")
43
check(
52
check(
44
"https://code.oscarkilo.com/a123.git",
53
"https://code.oscarkilo.com/a123.git",
45
"a123")
54
"a123")
46
check(
55
check(
47
"https://github.com/foo/bar.git",
56
"https://github.com/foo/bar.git",
48
"")
57
"")
49
check("not-a-url", "")
58
check("not-a-url", "")
50
}
59
}
51
60
52
func TestResolveRepo(t *testing.T) {
61
func TestResolveRepo(t *testing.T) {
53
// Flag takes priority.
62
// Flag takes priority.
54
repo, err := resolveRepo("from-flag")
63
repo, err := resolveRepo("from-flag")
55
if err != nil {
64
if err != nil {
56
t.Fatal(err)
65
t.Fatal(err)
57
}
66
}
58
if repo != "from-flag" {
67
if repo != "from-flag" {
59
t.Errorf("want from-flag, got %q", repo)
68
t.Errorf("want from-flag, got %q", repo)
60
}
69
}
61
70
62
// Env var takes priority over detection.
71
// Env var takes priority over detection.
63
os.Setenv("OKG_REPO", "from-env")
72
os.Setenv("OKG_REPO", "from-env")
64
defer os.Unsetenv("OKG_REPO")
73
defer os.Unsetenv("OKG_REPO")
65
repo, err = resolveRepo("")
74
repo, err = resolveRepo("")
66
if err != nil {
75
if err != nil {
67
t.Fatal(err)
76
t.Fatal(err)
68
}
77
}
69
if repo != "from-env" {
78
if repo != "from-env" {
70
t.Errorf("want from-env, got %q", repo)
79
t.Errorf("want from-env, got %q", repo)
71
}
80
}
72
}
81
}
73
82
74
func TestParsePRFlags(t *testing.T) {
83
func TestParsePRFlags(t *testing.T) {
75
f, rest, err := parsePRFlags([]string{
84
f, rest, err := parsePRFlags([]string{
76
"--repo", "widget", "--json", "42",
85
"--repo", "widget", "--json", "42",
77
})
86
})
78
if err != nil {
87
if err != nil {
79
t.Fatal(err)
88
t.Fatal(err)
80
}
89
}
81
if f.repo != "widget" {
90
if f.repo != "widget" {
82
t.Errorf(
91
t.Errorf(
83
"repo: want widget, got %q", f.repo)
92
"repo: want widget, got %q", f.repo)
84
}
93
}
85
if !f.asJSON {
94
if !f.asJSON {
86
t.Error("asJSON: want true")
95
t.Error("asJSON: want true")
87
}
96
}
88
if len(rest) != 1 || rest[0] != "42" {
97
if len(rest) != 1 || rest[0] != "42" {
89
t.Errorf("rest: want [42], got %v", rest)
98
t.Errorf("rest: want [42], got %v", rest)
90
}
99
}
91
}
100
}
92
101
93
func TestParsePRFlagsEmpty(t *testing.T) {
102
func TestParsePRFlagsEmpty(t *testing.T) {
94
f, rest, err := parsePRFlags(nil)
103
f, rest, err := parsePRFlags(nil)
95
if err != nil {
104
if err != nil {
96
t.Fatal(err)
105
t.Fatal(err)
97
}
106
}
98
if f.repo != "" {
107
if f.repo != "" {
99
t.Errorf(
108
t.Errorf(
100
"repo: want empty, got %q", f.repo)
109
"repo: want empty, got %q", f.repo)
101
}
110
}
102
if f.asJSON {
111
if f.asJSON {
103
t.Error("asJSON: want false")
112
t.Error("asJSON: want false")
104
}
113
}
105
if len(rest) != 0 {
114
if len(rest) != 0 {
106
t.Errorf("rest: want empty, got %v", rest)
115
t.Errorf("rest: want empty, got %v", rest)
107
}
116
}
108
}
117
}
109
118
110
func TestParsePRFlagsMissingValue(t *testing.T) {
119
func TestParsePRFlagsMissingValue(t *testing.T) {
111
_, _, err := parsePRFlags([]string{"--repo"})
120
_, _, err := parsePRFlags([]string{"--repo"})
112
if err == nil {
121
if err == nil {
113
t.Error("want error for --repo without value")
122
t.Error("want error for --repo without value")
114
}
123
}
115
}
124
}
116
125
117
func TestAge(t *testing.T) {
126
func TestAge(t *testing.T) {
118
check := func(d time.Duration, want string) {
127
check := func(d time.Duration, want string) {
119
t.Helper()
128
t.Helper()
120
got := age(time.Now().Add(-d))
129
got := age(time.Now().Add(-d))
121
if got != want {
130
if got != want {
122
t.Errorf(
131
t.Errorf(
123
"age(-%v): want %q, got %q",
132
"age(-%v): want %q, got %q",
124
d, want, got)
133
d, want, got)
125
}
134
}
126
}
135
}
127
check(30*time.Second, "just now")
136
check(30*time.Second, "just now")
128
check(5*time.Minute, "5m")
137
check(5*time.Minute, "5m")
129
check(3*time.Hour, "3h")
138
check(3*time.Hour, "3h")
130
check(48*time.Hour, "2d")
139
check(48*time.Hour, "2d")
131
}
140
}
132
141
133
func TestConfigEnvOverrides(t *testing.T) {
142
func TestConfigEnvOverrides(t *testing.T) {
134
os.Setenv("OKG_HOST", "http://test:1234")
143
os.Setenv("OKG_HOST", "http://test:1234")
135
os.Setenv("KLEX_API_KEY", "env-key")
144
os.Setenv("KLEX_API_KEY", "env-key")
136
defer os.Unsetenv("OKG_HOST")
145
defer os.Unsetenv("OKG_HOST")
137
defer os.Unsetenv("KLEX_API_KEY")
146
defer os.Unsetenv("KLEX_API_KEY")
138
147
139
cfg, err := loadConfig()
148
cfg, err := loadConfig()
140
if err != nil {
149
if err != nil {
141
t.Fatal(err)
150
t.Fatal(err)
142
}
151
}
143
if cfg.Host != "http://test:1234" {
152
if cfg.Host != "http://test:1234" {
144
t.Errorf(
153
t.Errorf(
145
"Host: want http://test:1234, got %q",
154
"Host: want http://test:1234, got %q",
146
cfg.Host)
155
cfg.Host)
147
}
156
}
148
if cfg.ApiKey != "env-key" {
157
if cfg.ApiKey != "env-key" {
149
t.Errorf(
158
t.Errorf(
150
"ApiKey: want env-key, got %q",
159
"ApiKey: want env-key, got %q",
151
cfg.ApiKey)
160
cfg.ApiKey)
152
}
161
}
153
}
162
}
154
163
155
func TestConfigDefaultHost(t *testing.T) {
164
func TestConfigDefaultHost(t *testing.T) {
156
snste
165
snste
157
o.nsenv
166
o.nsenv
167
t.Setenv("HOME", t.TempDir())
168
t.Setenv("OKG_HOST", "")
169
t.Setenv("KLEX_API_KEY", "")
158
cfg, err := loadConfig()
170
cfg, err := loadConfig()
159
if err != nil {
171
if err != nil {
160
t.Fatal(err)
172
t.Fatal(err)
161
}
173
}
162
if cfg.Host != "http://oca" {
174
if cfg.Host != "http://oca" {
163
t.Errorf(
175
t.Errorf(
164
"Host: want http://oca, got %q",
176
"Host: want http://oca, got %q",
165
cfg.Host)
177
cfg.Host)
166
}
178
}
167
}
179
}
168
180
169
func TestRunRepoCreateArgs(t *testing.T) {
181
func TestRunRepoCreateArgs(t *testing.T) {
170
mock := newMockKleeRepo(t, 204, "")
182
mock := newMockKleeRepo(t, 204, "")
171
defer mock.Close()
183
defer mock.Close()
172
184
173
os.Setenv("OKG_HOST", mock.URL)
185
os.Setenv("OKG_HOST", mock.URL)
174
os.Setenv("KLEX_API_KEY", "test-key")
186
os.Setenv("KLEX_API_KEY", "test-key")
175
defer os.Unsetenv("OKG_HOST")
187
defer os.Unsetenv("OKG_HOST")
176
defer os.Unsetenv("KLEX_API_KEY")
188
defer os.Unsetenv("KLEX_API_KEY")
177
189
178
err := runRepoCreate([]string{
190
err := runRepoCreate([]string{
179
"my-repo", "--reader", "igor.agents",
191
"my-repo", "--reader", "igor.agents",
180
})
192
})
181
if err != nil {
193
if err != nil {
182
t.Fatal(err)
194
t.Fatal(err)
183
}
195
}
184
if mock.req.RepoName != "my-repo" {
196
if mock.req.RepoName != "my-repo" {
185
t.Errorf(
197
t.Errorf(
186
"repo_name: want my-repo, got %q",
198
"repo_name: want my-repo, got %q",
187
mock.req.RepoName)
199
mock.req.RepoName)
188
}
200
}
189
if mock.req.ReaderUsername != "igor.agents" {
201
if mock.req.ReaderUsername != "igor.agents" {
190
t.Errorf(
202
t.Errorf(
191
"reader: want igor.agents, got %q",
203
"reader: want igor.agents, got %q",
192
mock.req.ReaderUsername)
204
mock.req.ReaderUsername)
193
}
205
}
194
}
206
}
195
207
196
func TestRunRepoCreateNoReader(t *testing.T) {
208
func TestRunRepoCreateNoReader(t *testing.T) {
197
mock := newMockKleeRepo(t, 204, "")
209
mock := newMockKleeRepo(t, 204, "")
198
defer mock.Close()
210
defer mock.Close()
199
211
200
os.Setenv("OKG_HOST", mock.URL)
212
os.Setenv("OKG_HOST", mock.URL)
201
os.Setenv("KLEX_API_KEY", "test-key")
213
os.Setenv("KLEX_API_KEY", "test-key")
202
defer os.Unsetenv("OKG_HOST")
214
defer os.Unsetenv("OKG_HOST")
203
defer os.Unsetenv("KLEX_API_KEY")
215
defer os.Unsetenv("KLEX_API_KEY")
204
216
205
err := runRepoCreate([]string{"my-repo"})
217
err := runRepoCreate([]string{"my-repo"})
206
if err != nil {
218
if err != nil {
207
t.Fatal(err)
219
t.Fatal(err)
208
}
220
}
209
if mock.req.ReaderUsername != "" {
221
if mock.req.ReaderUsername != "" {
210
t.Errorf(
222
t.Errorf(
211
"reader: want empty, got %q",
223
"reader: want empty, got %q",
212
mock.req.ReaderUsername)
224
mock.req.ReaderUsername)
213
}
225
}
214
}
226
}
215
227
216
func TestRunRepoCreateMissingName(t *testing.T) {
228
func TestRunRepoCreateMissingName(t *testing.T) {
217
err := runRepoCreate(nil)
229
err := runRepoCreate(nil)
218
if err == nil {
230
if err == nil {
219
t.Fatal("want error for missing name")
231
t.Fatal("want error for missing name")
220
}
232
}
221
}
233
}
222
234
223
func TestRunRepoCreateReaderMissingValue(
235
func TestRunRepoCreateReaderMissingValue(
224
t *testing.T,
236
t *testing.T,
225
) {
237
) {
226
err := runRepoCreate([]string{
238
err := runRepoCreate([]string{
227
"my-repo", "--reader",
239
"my-repo", "--reader",
228
})
240
})
229
if err == nil {
241
if err == nil {
230
t.Fatal(
242
t.Fatal(
231
"want error for --reader without value")
243
"want error for --reader without value")
232
}
244
}
233
}
245
}
234
246
235
func TestRunRepoCreateUnknownFlag(t *testing.T) {
247
func TestRunRepoCreateUnknownFlag(t *testing.T) {
236
err := runRepoCreate([]string{
248
err := runRepoCreate([]string{
237
"my-repo", "--bogus",
249
"my-repo", "--bogus",
238
})
250
})
239
if err == nil {
251
if err == nil {
240
t.Fatal("want error for unknown flag")
252
t.Fatal("want error for unknown flag")
241
}
253
}
242
}
254
}
243
255
244
func TestRunRepoCreateDuplicateName(
256
func TestRunRepoCreateDuplicateName(
245
t *testing.T,
257
t *testing.T,
246
) {
258
) {
247
err := runRepoCreate([]string{
259
err := runRepoCreate([]string{
248
"my-repo", "extra",
260
"my-repo", "extra",
249
})
261
})
250
if err == nil {
262
if err == nil {
251
t.Fatal(
263
t.Fatal(
252
"want error for extra positional arg")
264
"want error for extra positional arg")
253
}
265
}
254
}
266
}
255
267
256
func TestRunRepoCreateServerError(t *testing.T) {
268
func TestRunRepoCreateServerError(t *testing.T) {
257
mock := newMockKleeRepo(t, 403,
269
mock := newMockKleeRepo(t, 403,
258
"you are not an owner of "+
270
"you are not an owner of "+
259
"klee://code.oscarkilo.com/.new-repo")
271
"klee://code.oscarkilo.com/.new-repo")
260
defer mock.Close()
272
defer mock.Close()
261
273
262
os.Setenv("OKG_HOST", mock.URL)
274
os.Setenv("OKG_HOST", mock.URL)
263
os.Setenv("KLEX_API_KEY", "test-key")
275
os.Setenv("KLEX_API_KEY", "test-key")
264
defer os.Unsetenv("OKG_HOST")
276
defer os.Unsetenv("OKG_HOST")
265
defer os.Unsetenv("KLEX_API_KEY")
277
defer os.Unsetenv("KLEX_API_KEY")
266
278
267
err := runRepoCreate([]string{"my-repo"})
279
err := runRepoCreate([]string{"my-repo"})
268
if err == nil {
280
if err == nil {
269
t.Fatal("want error on 403")
281
t.Fatal("want error on 403")
270
}
282
}
271
if !strings.Contains(err.Error(), "403") {
283
if !strings.Contains(err.Error(), "403") {
272
t.Errorf("want 403 in error, got %q", err)
284
t.Errorf("want 403 in error, got %q", err)
273
}
285
}
274
}
286
}
275
287
276
// mockKleeRepo captures the /.add-repo request.
288
// mockKleeRepo captures the /.add-repo request.
277
type mockKleeRepo struct {
289
type mockKleeRepo struct {
278
*httptest.Server
290
*httptest.Server
279
req klee.CreateRepoRequest
291
req klee.CreateRepoRequest
280
}
292
}
281
293
282
func newMockKleeRepo(
294
func newMockKleeRepo(
283
t *testing.T, status int, body string,
295
t *testing.T, status int, body string,
284
) *mockKleeRepo {
296
) *mockKleeRepo {
285
t.Helper()
297
t.Helper()
286
mk := &mockKleeRepo{}
298
mk := &mockKleeRepo{}
287
mk.Server = httptest.NewServer(
299
mk.Server = httptest.NewServer(
288
http.HandlerFunc(func(
300
http.HandlerFunc(func(
289
w http.ResponseWriter, r *http.Request,
301
w http.ResponseWriter, r *http.Request,
290
) {
302
) {
291
json.NewDecoder(r.Body).Decode(&mk.req)
303
json.NewDecoder(r.Body).Decode(&mk.req)
292
w.WriteHeader(status)
304
w.WriteHeader(status)
293
if body != "" {
305
if body != "" {
294
w.Write([]byte(body))
306
w.Write([]byte(body))
295
}
307
}
296
}))
308
}))
297
return mk
309
return mk
298
}
310
}