Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add integration tests #42

Merged
merged 2 commits into from
Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions commands/env/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,8 @@ func handleGetAllEnvironments(c client.Client) error {
}

var data [][]string
for _, d := range r.Data {
data = append(data, display.FormattedEnvironment(&d.Environment)...)
for i := range r.Data {
data = append(data, display.FormattedEnvironment(&r.Data[i])...)
}
columns := []string{"App", "UUID", "Ready", "Repo", "PR#", "URL"}
display.RenderTable(os.Stdout, columns, data)
Expand Down Expand Up @@ -180,7 +180,7 @@ func handleGetEnvironmentByID(c client.Client, id string) error {
return err
}

data := display.FormattedEnvironment(&r.Data.Environment)
data := display.FormattedEnvironment(&r.Data)
columns := []string{"App", "UUID", "Ready", "Repo", "PR#", "URL"}
display.RenderTable(os.Stdout, columns, data)
return nil
Expand Down
10 changes: 3 additions & 7 deletions pkg/types/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,12 @@ func UnmarshalOrgs(body []byte) (*OrgsResponse, error) {
}

type Response struct {
Data struct {
Environment
} `json:"data"`
Data Environment `json:"data"`
}

type RespManyEnvs struct {
Data []struct {
Environment
} `json:"data"`
Links Links `json:"links"`
Data []Environment `json:"data"`
Links Links `json:"links"`
}

type UUIDResponse struct {
Expand Down
65 changes: 35 additions & 30 deletions pkg/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,42 +8,47 @@ type Service struct {
}

type Environment struct {
Attributes struct {
Name string `json:"name"`
URL string `json:"url"`
Ready bool `json:"ready"`

Projects []struct {
PullRequestNumber int `json:"pull_request_number"`
RepoName string `json:"repo_name"`
} `json:"projects"`
ID string `json:"id"`
Attributes EnvironmentAttributes `json:"attributes"`
}

Services []Service `json:"services"`
} `json:"attributes"`
type Project struct {
PullRequestNumber int `json:"pull_request_number"`
RepoName string `json:"repo_name"`
}

ID string `json:"id"`
type EnvironmentAttributes struct {
Name string `json:"name"`
URL string `json:"url"`
Ready bool `json:"ready"`
Projects []Project `json:"projects"`
Services []Service `json:"services"`
}

type Volume struct {
Attributes struct {
ComposePath string `json:"compose_path"`
RemoteComposeURL string `json:"remote_compose_url"`
Name string `json:"volume_name"`
ServiceName string `json:"service_name"`
VolumePath string `json:"volume_path"`
} `json:"attributes"`
ID string `json:"id"`
Type string `json:"type"`
Attributes VolumeAttributes `json:"attributes"`
ID string `json:"id"`
Type string `json:"type"`
}

type VolumeAttributes struct {
ComposePath string `json:"compose_path"`
RemoteComposeURL string `json:"remote_compose_url"`
Name string `json:"volume_name"`
ServiceName string `json:"service_name"`
VolumePath string `json:"volume_path"`
}

type Snapshot struct {
Attributes struct {
CreatedAt string `json:"created_at"`
FromSnapshotNumber int `json:"from_snapshot_number"`
SequenceNumber int `json:"sequence_number"`
Status string `json:"status"`
TotalSize int `json:"total_size"`
} `json:"attributes"`
ID string `json:"id"`
Type string `json:"type"`
Attributes SnapshotAttributes `json:"attributes"`
ID string `json:"id"`
Type string `json:"type"`
}

type SnapshotAttributes struct {
CreatedAt string `json:"created_at"`
FromSnapshotNumber int `json:"from_snapshot_number"`
SequenceNumber int `json:"sequence_number"`
Status string `json:"status"`
TotalSize int `json:"total_size"`
}
223 changes: 223 additions & 0 deletions tests/cli_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
package tests

import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"os/exec"
"testing"
"time"

"github.com/google/go-cmp/cmp"

"github.com/shipyard/shipyard-cli/pkg/types"
"github.com/shipyard/shipyard-cli/tests/server"
)

func TestMain(m *testing.M) {
cmd := exec.Command("go", "build", "-o", "shipyard", "..")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this have to build? That'd leave artifacts lying around. It seems like it'd make more sense to do these sorts of tests on the functions themselves.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, but that'd be not quite end-to-end. Also, TestMain deletes the binary after every run, so there shouldn't be any artifacts lying around.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

++ to what @dietb said here, I'm not sure if we necessarily need to test compilation (that + artifact gen should be done as part of CI anyways, right?), but IMO its not really an issue as it gets cleaned up.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd need to keep track of the binary (of a single architecture) in Git, which might be annoying. When there are changes to the code, someone will need to recompile it before running the integration tests.
It's not about testing compilation but running tests against the current code.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're not getting any benefit from running the compiled binary vs the functions directly from what I can tell. Meanwhile, the actual functions themselves don't have the test cases attached that were written for this testing.

For the sake of simplicity and avoiding artifact tracking and depending on people to manually, I'd suggest not doing it this way.

Copy link
Contributor Author

@maxsokolovsky maxsokolovsky Mar 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main reason for making these tests is ensuring the high-level configuration of the CLI works properly.
That the config file's values are honored, that env vars properly override those, that the most important flags (such as org) are properly seen by our configuration (viper). This used to be a source of bugs. So the tests themselves are made to catch those specific bugs.

I think we could test the functions themselves when we refactor the general setup, such that a lookup of anything is just an arbitrary function that can be mocked. But currently, all lookups are viper.GetString, which is not easy to test.

What do you think?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to expand on what I said earlier -- def shouldn't track the binaries in git, I was referring to tracking them in GitHub itself when creating a release that points to a tag. That being said def out of scope of this, so ignore what I said earlier about it.

var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
fmt.Printf("Setup failure: %s", stderr.String())
os.Exit(1)
}
srv := &http.Server{
Addr: ":8000",
ReadHeaderTimeout: time.Second,
Handler: server.NewHandler(),
}
go func() {
if err := srv.ListenAndServe(); err != nil {
log.Fatalf("Could not start server: %v\n", err)
}
}()

code := m.Run()
if err := os.Remove("shipyard"); err != nil {
fmt.Printf("Cleanup failure: %v", err)
}
os.Exit(code)
}

func TestGetAllEnvironments(t *testing.T) {
t.Parallel()
tests := []struct {
name string
args []string
ids []string
output string
}{
{
name: "default org",
args: []string{"get", "envs", "--json"},
ids: []string{"default-1", "default-2"},
},
{
name: "non default org",
args: []string{"get", "envs", "--org", "pugs", "--json"},
ids: []string{"pug-1", "pug-2"},
},
{
name: "non existent org",
args: []string{"get", "envs", "--org", "cats"},
output: "Command error: user org not found\n",
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
c := newCmd(test.args)
if err := c.cmd.Run(); err != nil {
if diff := cmp.Diff(c.stdErr.String(), test.output); diff != "" {
t.Error(diff)
}
return
}
var resp types.RespManyEnvs
if err := json.Unmarshal(c.stdOut.Bytes(), &resp); err != nil {
t.Fatal(err)
}
var ids []string
for i := range resp.Data {
ids = append(ids, resp.Data[i].ID)
}
want := test.ids
if !cmp.Equal(ids, want) {
t.Error(cmp.Diff(ids, want))
}
})
}
}

func TestGetEnvironmentByID(t *testing.T) {
t.Parallel()
tests := []struct {
name string
args []string
id string
output string
}{
{
name: "default org",
args: []string{"get", "env", "default-1", "--json"},
id: "default-1",
},
{
name: "non default org",
args: []string{"get", "env", "pug-1", "--org", "pugs", "--json"},
id: "pug-1",
},
{
name: "non existent env",
args: []string{"get", "env", "sharpei-1", "--org", "pugs", "--json"},
output: "Command error: environment not found\n",
},
{
name: "non existent org",
args: []string{"get", "env", "cat-1", "--org", "cats"},
output: "Command error: user org not found\n",
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
c := newCmd(test.args)
if err := c.cmd.Run(); err != nil {
if diff := cmp.Diff(c.stdErr.String(), test.output); diff != "" {
t.Error(diff)
}
return
}
var resp types.Response
if err := json.Unmarshal(c.stdOut.Bytes(), &resp); err != nil {
t.Fatal(err)
}
want := test.id
got := resp.Data.ID
if !cmp.Equal(got, want) {
t.Error(cmp.Diff(got, want))
}
})
}
}

func TestRebuildEnvironment(t *testing.T) {
t.Parallel()
tests := []struct {
name string
args []string
output string
}{
{
name: "default org",
args: []string{"rebuild", "env", "default-1"},
output: "Environment queued for a rebuild.\n",
},
{
name: "non default org",
args: []string{"rebuild", "env", "pug-1", "--org", "pugs"},
output: "Environment queued for a rebuild.\n",
},
{
name: "non existent env",
args: []string{"rebuild", "env", "sharpei-1", "--org", "pugs"},
output: "Command error: environment not found\n",
},
{
name: "non existent org",
args: []string{"rebuild", "env", "pug-1", "--org", "cats"},
output: "Command error: user org not found\n",
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
c := newCmd(test.args)
err := c.cmd.Run()
if err != nil {
if diff := cmp.Diff(c.stdErr.String(), test.output); diff != "" {
t.Error(diff)
}
return
}
if diff := cmp.Diff(c.stdOut.String(), test.output); diff != "" {
t.Error(diff)
}
})
}
}

// nolint:gosec // Bad arguments can't be passed in.
func newCmd(args []string) *cmdWrapper {
c := cmdWrapper{
args: args,
}
c.cmd = exec.Command("./shipyard", commandLine(c.args)...)
c.cmd.Env = []string{"SHIPYARD_BUILD_URL=http://localhost:8000"}
stderr, stdout := new(bytes.Buffer), new(bytes.Buffer)
c.cmd.Stderr = stderr
c.cmd.Stdout = stdout
c.stdErr = stderr
c.stdOut = stdout
return &c
}

func commandLine(in []string) []string {
args := []string{"--config", "config.yaml"}
args = append(args, in...)
return args
}

type cmdWrapper struct {
cmd *exec.Cmd
args []string
stdErr *bytes.Buffer
stdOut *bytes.Buffer
}
2 changes: 2 additions & 0 deletions tests/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
api_token: test
org: default
Loading
Loading