Skip to content
This repository was archived by the owner on Aug 30, 2024. It is now read-only.

Commit 56a76c4

Browse files
authored
Merge pull request #86 from cdr/urfave
Migrate to cobra
2 parents bae77f0 + be6bc28 commit 56a76c4

28 files changed

+1050
-873
lines changed

ci/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# ci
2+
3+
## integration tests
4+
5+
### `tcli`
6+
7+
Package `tcli` provides a framework for writing end-to-end CLI tests.
8+
Each test group can have its own container for executing commands in a consistent
9+
and isolated filesystem.
10+
11+
### prerequisites
12+
13+
Assign the following environment variables to run the integration tests
14+
against an existing Enterprise deployment instance.
15+
16+
```bash
17+
export CODER_URL=...
18+
export CODER_EMAIL=...
19+
export CODER_PASSWORD=...
20+
```
21+
22+
Then, simply run the test command from the project root
23+
24+
```sh
25+
go test -v ./ci/integration
26+
```

ci/integration/integration_test.go

Lines changed: 9 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,11 @@ package integration
22

33
import (
44
"context"
5-
"encoding/json"
65
"math/rand"
76
"testing"
87
"time"
98

109
"cdr.dev/coder-cli/ci/tcli"
11-
"cdr.dev/coder-cli/internal/entclient"
12-
"cdr.dev/slog"
1310
"cdr.dev/slog/sloggers/slogtest/assert"
1411
)
1512

@@ -34,17 +31,15 @@ func TestCoderCLI(t *testing.T) {
3431
tcli.StderrEmpty(),
3532
)
3633

37-
c.Run(ctx, "coder version").Assert(t,
34+
c.Run(ctx, "coder --version").Assert(t,
3835
tcli.StderrEmpty(),
3936
tcli.Success(),
4037
tcli.StdoutMatches("linux"),
4138
)
4239

43-
c.Run(ctx, "coder help").Assert(t,
40+
c.Run(ctx, "coder --help").Assert(t,
4441
tcli.Success(),
45-
tcli.StderrMatches("Commands:"),
46-
tcli.StderrMatches("Usage: coder"),
47-
tcli.StdoutEmpty(),
42+
tcli.StdoutMatches("Available Commands"),
4843
)
4944

5045
headlessLogin(ctx, t, c)
@@ -53,8 +48,12 @@ func TestCoderCLI(t *testing.T) {
5348
tcli.Success(),
5449
)
5550

51+
c.Run(ctx, "coder envs ls").Assert(t,
52+
tcli.Success(),
53+
)
54+
5655
c.Run(ctx, "coder urls").Assert(t,
57-
tcli.Error(),
56+
tcli.Success(),
5857
)
5958

6059
c.Run(ctx, "coder sync").Assert(t,
@@ -65,36 +64,15 @@ func TestCoderCLI(t *testing.T) {
6564
tcli.Error(),
6665
)
6766

68-
var user entclient.User
69-
c.Run(ctx, `coder users ls -o json | jq -c '.[] | select( .username == "charlie")'`).Assert(t,
70-
tcli.Success(),
71-
stdoutUnmarshalsJSON(&user),
72-
)
73-
assert.Equal(t, "user email is as expected", "charlie@coder.com", user.Email)
74-
assert.Equal(t, "username is as expected", "Charlie", user.Name)
75-
76-
c.Run(ctx, "coder users ls -o human | grep charlie").Assert(t,
77-
tcli.Success(),
78-
tcli.StdoutMatches("charlie"),
79-
)
80-
8167
c.Run(ctx, "coder logout").Assert(t,
8268
tcli.Success(),
8369
)
8470

85-
c.Run(ctx, "coder envs").Assert(t,
71+
c.Run(ctx, "coder envs ls").Assert(t,
8672
tcli.Error(),
8773
)
8874
}
8975

90-
func stdoutUnmarshalsJSON(target interface{}) tcli.Assertion {
91-
return func(t *testing.T, r *tcli.CommandResult) {
92-
slog.Helper()
93-
err := json.Unmarshal(r.Stdout, target)
94-
assert.Success(t, "json unmarshals", err)
95-
}
96-
}
97-
9876
var seededRand = rand.New(rand.NewSource(time.Now().UnixNano()))
9977

10078
func randString(length int) string {

ci/integration/secrets_test.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ func TestSecrets(t *testing.T) {
3636

3737
c.Run(ctx, "coder secrets create").Assert(t,
3838
tcli.Error(),
39-
tcli.StdoutEmpty(),
4039
)
4140

4241
// this tests the "Value:" prompt fallback
@@ -85,9 +84,6 @@ func TestSecrets(t *testing.T) {
8584
c.Run(ctx, fmt.Sprintf("echo %s > ~/secret.json", value)).Assert(t,
8685
tcli.Success(),
8786
)
88-
c.Run(ctx, fmt.Sprintf("coder secrets create %s --from-literal %s --from-file ~/secret.json", name, value)).Assert(t,
89-
tcli.Error(),
90-
)
9187
c.Run(ctx, fmt.Sprintf("coder secrets create %s --from-file ~/secret.json", name)).Assert(t,
9288
tcli.Success(),
9389
)

ci/integration/setup_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"golang.org/x/xerrors"
1414
)
1515

16+
// binpath is populated during package initialization with a path to the coder binary
1617
var binpath string
1718

1819
// initialize integration tests by building the coder-cli binary
@@ -39,7 +40,7 @@ func build(path string) error {
3940

4041
out, err := cmd.CombinedOutput()
4142
if err != nil {
42-
return xerrors.Errorf("failed to build coder-cli (%v): %w", string(out), err)
43+
return xerrors.Errorf("build coder-cli (%v): %w", string(out), err)
4344
}
4445
return nil
4546
}

ci/integration/users_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package integration
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
8+
"cdr.dev/coder-cli/ci/tcli"
9+
"cdr.dev/coder-cli/internal/entclient"
10+
"cdr.dev/slog/sloggers/slogtest/assert"
11+
)
12+
13+
func TestUsers(t *testing.T) {
14+
t.Parallel()
15+
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5)
16+
defer cancel()
17+
18+
c, err := tcli.NewContainerRunner(ctx, &tcli.ContainerConfig{
19+
Image: "codercom/enterprise-dev",
20+
Name: "users-cli-tests",
21+
BindMounts: map[string]string{
22+
binpath: "/bin/coder",
23+
},
24+
})
25+
assert.Success(t, "new run container", err)
26+
defer c.Close()
27+
28+
c.Run(ctx, "which coder").Assert(t,
29+
tcli.Success(),
30+
tcli.StdoutMatches("/usr/sbin/coder"),
31+
tcli.StderrEmpty(),
32+
)
33+
34+
headlessLogin(ctx, t, c)
35+
36+
var user entclient.User
37+
c.Run(ctx, `coder users ls --output json | jq -c '.[] | select( .username == "charlie")'`).Assert(t,
38+
tcli.Success(),
39+
tcli.StdoutJSONUnmarshal(&user),
40+
)
41+
assert.Equal(t, "user email is as expected", "charlie@coder.com", user.Email)
42+
assert.Equal(t, "username is as expected", "Charlie", user.Name)
43+
44+
c.Run(ctx, "coder users ls --output human | grep charlie").Assert(t,
45+
tcli.Success(),
46+
tcli.StdoutMatches("charlie"),
47+
)
48+
49+
c.Run(ctx, "coder logout").Assert(t,
50+
tcli.Success(),
51+
)
52+
53+
c.Run(ctx, "coder users ls").Assert(t,
54+
tcli.Error(),
55+
)
56+
}

ci/tcli/tcli.go

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package tcli
33
import (
44
"bytes"
55
"context"
6+
"encoding/json"
67
"fmt"
78
"io"
89
"os/exec"
@@ -76,7 +77,7 @@ func NewContainerRunner(ctx context.Context, config *ContainerConfig) (*Containe
7677
out, err := cmd.CombinedOutput()
7778
if err != nil {
7879
return nil, xerrors.Errorf(
79-
"failed to start testing container %q, (%s): %w",
80+
"start testing container %q, (%s): %w",
8081
config.Name, string(out), err)
8182
}
8283

@@ -97,7 +98,7 @@ func (r *ContainerRunner) Close() error {
9798
out, err := cmd.CombinedOutput()
9899
if err != nil {
99100
return xerrors.Errorf(
100-
"failed to stop testing container %q, (%s): %w",
101+
"stop testing container %q, (%s): %w",
101102
r.name, string(out), err)
102103
}
103104
return nil
@@ -290,7 +291,7 @@ func matches(t *testing.T, name, pattern string, target []byte) {
290291

291292
ok, err := regexp.Match(pattern, target)
292293
if err != nil {
293-
slogtest.Fatal(t, "failed to attempt regexp match", append(fields, slog.Error(err))...)
294+
slogtest.Fatal(t, "attempt regexp match", append(fields, slog.Error(err))...)
294295
}
295296
if !ok {
296297
slogtest.Fatal(t, "expected to find pattern, no match found", fields...)
@@ -329,3 +330,21 @@ func DurationGreaterThan(dur time.Duration) Assertion {
329330
}
330331
}
331332
}
333+
334+
// StdoutJSONUnmarshal attempts to unmarshal stdout into the given target
335+
func StdoutJSONUnmarshal(target interface{}) Assertion {
336+
return func(t *testing.T, r *CommandResult) {
337+
slog.Helper()
338+
err := json.Unmarshal(r.Stdout, target)
339+
assert.Success(t, "stdout json unmarshals", err)
340+
}
341+
}
342+
343+
// StderrJSONUnmarshal attempts to unmarshal stderr into the given target
344+
func StderrJSONUnmarshal(target interface{}) Assertion {
345+
return func(t *testing.T, r *CommandResult) {
346+
slog.Helper()
347+
err := json.Unmarshal(r.Stdout, target)
348+
assert.Success(t, "stderr json unmarshals", err)
349+
}
350+
}

cmd/coder/auth.go

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,38 @@ import (
55

66
"cdr.dev/coder-cli/internal/config"
77
"cdr.dev/coder-cli/internal/entclient"
8+
"golang.org/x/xerrors"
9+
10+
"go.coder.com/flog"
811
)
912

13+
// requireAuth exits the process with a nonzero exit code if the user is not authenticated to make requests
1014
func requireAuth() *entclient.Client {
15+
client, err := newClient()
16+
if err != nil {
17+
flog.Fatal("%v", err)
18+
}
19+
return client
20+
}
21+
22+
func newClient() (*entclient.Client, error) {
1123
sessionToken, err := config.Session.Read()
12-
requireSuccess(err, "read session: %v (did you run coder login?)", err)
24+
if err != nil {
25+
return nil, xerrors.Errorf("read session: %v (did you run coder login?)", err)
26+
}
1327

1428
rawURL, err := config.URL.Read()
15-
requireSuccess(err, "read url: %v (did you run coder login?)", err)
29+
if err != nil {
30+
return nil, xerrors.Errorf("read url: %v (did you run coder login?)", err)
31+
}
1632

1733
u, err := url.Parse(rawURL)
18-
requireSuccess(err, "url misformatted: %v (try runing coder login)", err)
34+
if err != nil {
35+
return nil, xerrors.Errorf("url misformatted: %v (try runing coder login)", err)
36+
}
1937

2038
return &entclient.Client{
2139
BaseURL: u,
2240
Token: sessionToken,
23-
}
41+
}, nil
2442
}

cmd/coder/ceapi.go

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package main
22

33
import (
4+
"golang.org/x/xerrors"
5+
46
"go.coder.com/flog"
57

68
"cdr.dev/coder-cli/internal/entclient"
@@ -25,43 +27,50 @@ outer:
2527
}
2628

2729
// getEnvs returns all environments for the user.
28-
func getEnvs(client *entclient.Client) []entclient.Environment {
30+
func getEnvs(client *entclient.Client) ([]entclient.Environment, error) {
2931
me, err := client.Me()
30-
requireSuccess(err, "get self: %+v", err)
32+
if err != nil {
33+
return nil, xerrors.Errorf("get self: %+v", err)
34+
}
3135

3236
orgs, err := client.Orgs()
33-
requireSuccess(err, "get orgs: %+v", err)
37+
if err != nil {
38+
return nil, xerrors.Errorf("get orgs: %+v", err)
39+
}
3440

3541
orgs = userOrgs(me, orgs)
3642

3743
var allEnvs []entclient.Environment
3844

3945
for _, org := range orgs {
4046
envs, err := client.Envs(me, org)
41-
requireSuccess(err, "get envs for %v: %+v", org.Name, err)
47+
if err != nil {
48+
return nil, xerrors.Errorf("get envs for %v: %+v", org.Name, err)
49+
}
4250

4351
for _, env := range envs {
4452
allEnvs = append(allEnvs, env)
4553
}
4654
}
47-
48-
return allEnvs
55+
return allEnvs, nil
4956
}
5057

5158
// findEnv returns a single environment by name (if it exists.)
52-
func findEnv(client *entclient.Client, name string) entclient.Environment {
53-
envs := getEnvs(client)
59+
func findEnv(client *entclient.Client, name string) (*entclient.Environment, error) {
60+
envs, err := getEnvs(client)
61+
if err != nil {
62+
return nil, xerrors.Errorf("get environments: %w", err)
63+
}
5464

5565
var found []string
5666

5767
for _, env := range envs {
5868
found = append(found, env.Name)
5969
if env.Name == name {
60-
return env
70+
return &env, nil
6171
}
6272
}
63-
64-
flog.Info("found %q", found)
65-
flog.Fatal("environment %q not found", name)
66-
panic("unreachable")
73+
flog.Error("found %q", found)
74+
flog.Error("%q not found", name)
75+
return nil, xerrors.New("environment not found")
6776
}

0 commit comments

Comments
 (0)