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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy