From 514c2ecfc3fcf2484cf66b07700e93737c98b6df Mon Sep 17 00:00:00 2001 From: Max Sokolovsky Date: Wed, 28 Feb 2024 16:41:58 -0500 Subject: [PATCH 1/2] Add integration tests --- commands/env/get.go | 6 +- pkg/types/parse.go | 10 +- pkg/types/types.go | 25 +++-- tests/cli_test.go | 223 ++++++++++++++++++++++++++++++++++++++ tests/config.yaml | 2 + tests/server/handlers.go | 65 +++++++++++ tests/server/responses.go | 69 ++++++++++++ tests/server/server.go | 15 +++ 8 files changed, 393 insertions(+), 22 deletions(-) create mode 100644 tests/cli_test.go create mode 100644 tests/config.yaml create mode 100644 tests/server/handlers.go create mode 100644 tests/server/responses.go create mode 100644 tests/server/server.go diff --git a/commands/env/get.go b/commands/env/get.go index c6793d2..b339d0d 100644 --- a/commands/env/get.go +++ b/commands/env/get.go @@ -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) @@ -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 diff --git a/pkg/types/parse.go b/pkg/types/parse.go index 1c6e3be..1de26ee 100644 --- a/pkg/types/parse.go +++ b/pkg/types/parse.go @@ -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 { diff --git a/pkg/types/types.go b/pkg/types/types.go index e373dc9..7f444d1 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -8,20 +8,21 @@ 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 { diff --git a/tests/cli_test.go b/tests/cli_test.go new file mode 100644 index 0000000..5013e47 --- /dev/null +++ b/tests/cli_test.go @@ -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", "..") + 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 +} diff --git a/tests/config.yaml b/tests/config.yaml new file mode 100644 index 0000000..1fd0d1d --- /dev/null +++ b/tests/config.yaml @@ -0,0 +1,2 @@ +api_token: test +org: default diff --git a/tests/server/handlers.go b/tests/server/handlers.go new file mode 100644 index 0000000..2bbc6bd --- /dev/null +++ b/tests/server/handlers.go @@ -0,0 +1,65 @@ +package server + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/shipyard/shipyard-cli/pkg/types" +) + +func (handler) getAllEnvironments(w http.ResponseWriter, r *http.Request) { + org := r.URL.Query().Get("org") + envs, ok := store[org] + if !ok { + orgNotFound(w) + return + } + resp := types.RespManyEnvs{Data: envs} + if err := json.NewEncoder(w).Encode(resp); err != nil { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(err.Error())) + } +} + +func (handler) getEnvironmentByID(w http.ResponseWriter, r *http.Request) { + env := findEnvByID(w, r) + if env != nil { + resp := types.Response{Data: *env} + if err := json.NewEncoder(w).Encode(resp); err != nil { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(err.Error())) + } + } +} + +func (handler) rebuildEnvironment(w http.ResponseWriter, r *http.Request) { + _ = findEnvByID(w, r) +} + +func findEnvByID(w http.ResponseWriter, r *http.Request) *types.Environment { + org := r.URL.Query().Get("org") + envs, ok := store[org] + if !ok { + orgNotFound(w) + return nil + } + id := r.PathValue("id") + for i := range envs { + if envs[i].ID == id { + return &envs[i] + } + } + envNotFound(w) + return nil +} + +func orgNotFound(w http.ResponseWriter) { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "user org not found") +} + +func envNotFound(w http.ResponseWriter) { + w.WriteHeader(http.StatusNotFound) + fmt.Fprintf(w, "environment not found") +} diff --git a/tests/server/responses.go b/tests/server/responses.go new file mode 100644 index 0000000..aaefcbe --- /dev/null +++ b/tests/server/responses.go @@ -0,0 +1,69 @@ +package server + +import "github.com/shipyard/shipyard-cli/pkg/types" + +//nolint:gochecknoglobals // OK for testing. +var store = map[string][]types.Environment{ + "default": { + { + Attributes: types.EnvironmentAttributes{ + URL: "https://dev.example.com", + Ready: true, + Projects: []types.Project{ + {PullRequestNumber: 123, RepoName: "Repo1"}, + {PullRequestNumber: 456, RepoName: "Repo2"}, + }, + Services: []types.Service{ + { + Name: "postgres", + }, + { + Name: "web", + }, + }, + }, + ID: "default-1", + }, + { + Attributes: types.EnvironmentAttributes{ + URL: "https://dev.example.com", + Ready: true, + Projects: []types.Project{ + {PullRequestNumber: 123, RepoName: "Repo1"}, + {PullRequestNumber: 456, RepoName: "Repo2"}, + }, + }, + ID: "default-2", + }, + }, + "pugs": { + { + Attributes: types.EnvironmentAttributes{ + URL: "https://prod.example.com", + Ready: true, + Projects: []types.Project{ + {PullRequestNumber: 900, RepoName: "pugs"}, + }, + Services: []types.Service{ + { + Name: "mysql", + }, + { + Name: "nginx", + }, + }, + }, + ID: "pug-1", + }, + { + Attributes: types.EnvironmentAttributes{ + URL: "https://prod.example.com", + Ready: true, + Projects: []types.Project{ + {PullRequestNumber: 901, RepoName: "pugs"}, + }, + }, + ID: "pug-2", + }, + }, +} diff --git a/tests/server/server.go b/tests/server/server.go new file mode 100644 index 0000000..4cb13d3 --- /dev/null +++ b/tests/server/server.go @@ -0,0 +1,15 @@ +package server + +import "net/http" + +func NewHandler() http.Handler { + var h handler + mux := http.NewServeMux() + mux.HandleFunc("GET /environment", h.getAllEnvironments) + mux.HandleFunc("GET /environment/{id}", h.getEnvironmentByID) + mux.HandleFunc("POST /environment/{id}/rebuild", h.rebuildEnvironment) + return mux +} + +type handler struct { +} From 91efe810129087814f39688417a9461e2e550fa0 Mon Sep 17 00:00:00 2001 From: Max Sokolovsky Date: Wed, 6 Mar 2024 10:13:49 -0500 Subject: [PATCH 2/2] Unpack the type definitions for volume-related types --- pkg/types/types.go | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/pkg/types/types.go b/pkg/types/types.go index 7f444d1..99e9de4 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -26,25 +26,29 @@ type EnvironmentAttributes struct { } 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"` }