Skip to content

Commit e38aa07

Browse files
committed
Add tests and refactor a bit to support that
1 parent 9c2a855 commit e38aa07

12 files changed

Lines changed: 890 additions & 251 deletions

cmd/client.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package cmd
2+
3+
import "io"
4+
5+
// restClient is the subset of api.RESTClient methods needed by the various commands.
6+
type restClient interface {
7+
Get(path string, resp interface{}) error
8+
Delete(path string, resp interface{}) error
9+
Do(method string, path string, body io.Reader, resp interface{}) error
10+
Patch(path string, body io.Reader, resp interface{}) error
11+
Post(path string, body io.Reader, resp interface{}) error
12+
Put(path string, body io.Reader, resp interface{}) error
13+
}

cmd/create.go

Lines changed: 65 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -41,78 +41,19 @@ func init() {
4141
$ gh runtime create --app my-app --env key1=value1 --env key2=value2 --secret key3=value3 --secret key4=value4
4242
# => Creates the app named 'my-app'
4343
`),
44-
Run: func(cmd *cobra.Command, args []string) {
45-
if createCmdFlags.app == "" {
46-
fmt.Println("Error: --app flag is required")
47-
return
48-
}
49-
50-
// Construct the request body
51-
requestBody := createReq{
52-
EnvironmentVariables: map[string]string{},
53-
Secrets: map[string]string{},
54-
}
55-
56-
for _, pair := range createCmdFlags.EnvironmentVariables {
57-
parts := strings.SplitN(pair, "=", 2)
58-
if len(parts) == 2 {
59-
key := parts[0]
60-
value := parts[1]
61-
requestBody.EnvironmentVariables[key] = value
62-
} else {
63-
fmt.Printf("Error: Invalid environment variable format (%s). Must be in the form 'key=value'\n", pair)
64-
return
65-
}
66-
}
67-
68-
for _, pair := range createCmdFlags.Secrets {
69-
parts := strings.SplitN(pair, "=", 2)
70-
if len(parts) == 2 {
71-
key := parts[0]
72-
value := parts[1]
73-
requestBody.Secrets[key] = value
74-
} else {
75-
fmt.Printf("Error: Invalid secret format (%s). Must be in the form 'key=value'\n", pair)
76-
return
77-
}
78-
}
79-
80-
body, err := json.Marshal(requestBody)
81-
if err != nil {
82-
fmt.Printf("Error marshalling request body: %v\n", err)
83-
return
84-
}
85-
86-
createUrl := fmt.Sprintf("runtime/%s/deployment", createCmdFlags.app)
87-
params := url.Values{}
88-
if createCmdFlags.RevisionName != "" {
89-
params.Add("revision_name", createCmdFlags.RevisionName)
90-
}
91-
if len(params) > 0 {
92-
createUrl += "?" + params.Encode()
93-
}
94-
44+
RunE: func(cmd *cobra.Command, args []string) error {
9545
client, err := api.DefaultRESTClient()
9646
if err != nil {
97-
fmt.Println(err)
98-
return
47+
return fmt.Errorf("failed creating REST client: %v", err)
9948
}
100-
response := createResp{}
101-
err = client.Put(createUrl, bytes.NewReader(body), &response)
49+
50+
appUrl, err := runCreate(client, createCmdFlags)
10251
if err != nil {
103-
fmt.Printf("Error creating app: %v\n", err)
104-
return
52+
return err
10553
}
10654

107-
fmt.Printf("App created: %s\n", response.AppUrl) // TODO pretty print details
108-
109-
if createCmdFlags.Init {
110-
err = writeRuntimeConfig(createCmdFlags.app, "")
111-
if err != nil {
112-
fmt.Printf("Error initializing config: %v\n", err)
113-
return
114-
}
115-
}
55+
fmt.Printf("App created: %s\n", appUrl)
56+
return nil
11657
},
11758
}
11859

@@ -123,3 +64,61 @@ func init() {
12364
createCmd.Flags().BoolVar(&createCmdFlags.Init, "init", false, "Initialize a runtime.config.json file in the current directory after creating the app")
12465
rootCmd.AddCommand(createCmd)
12566
}
67+
68+
func runCreate(client restClient, flags createCmdFlags) (string, error) {
69+
if flags.app == "" {
70+
return "", fmt.Errorf("--app flag is required")
71+
}
72+
73+
requestBody := createReq{
74+
EnvironmentVariables: map[string]string{},
75+
Secrets: map[string]string{},
76+
}
77+
78+
for _, pair := range flags.EnvironmentVariables {
79+
parts := strings.SplitN(pair, "=", 2)
80+
if len(parts) == 2 {
81+
requestBody.EnvironmentVariables[parts[0]] = parts[1]
82+
} else {
83+
return "", fmt.Errorf("invalid environment variable format (%s). Must be in the form 'key=value'", pair)
84+
}
85+
}
86+
87+
for _, pair := range flags.Secrets {
88+
parts := strings.SplitN(pair, "=", 2)
89+
if len(parts) == 2 {
90+
requestBody.Secrets[parts[0]] = parts[1]
91+
} else {
92+
return "", fmt.Errorf("invalid secret format (%s). Must be in the form 'key=value'", pair)
93+
}
94+
}
95+
96+
body, err := json.Marshal(requestBody)
97+
if err != nil {
98+
return "", fmt.Errorf("error marshalling request body: %v", err)
99+
}
100+
101+
createUrl := fmt.Sprintf("runtime/%s/deployment", flags.app)
102+
params := url.Values{}
103+
if flags.RevisionName != "" {
104+
params.Add("revision_name", flags.RevisionName)
105+
}
106+
if len(params) > 0 {
107+
createUrl += "?" + params.Encode()
108+
}
109+
110+
response := createResp{}
111+
err = client.Put(createUrl, bytes.NewReader(body), &response)
112+
if err != nil {
113+
return "", fmt.Errorf("error creating app: %v", err)
114+
}
115+
116+
if flags.Init {
117+
err = writeRuntimeConfig(flags.app, "")
118+
if err != nil {
119+
return response.AppUrl, fmt.Errorf("error initializing config: %v", err)
120+
}
121+
}
122+
123+
return response.AppUrl, nil
124+
}

cmd/create_test.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package cmd
2+
3+
import (
4+
"encoding/json"
5+
"io"
6+
"os"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func TestRunCreate_NoApp(t *testing.T) {
14+
client := &mockRESTClient{}
15+
_, err := runCreate(client, createCmdFlags{})
16+
require.ErrorContains(t, err, "--app flag is required")
17+
}
18+
19+
func TestRunCreate_InvalidEnvVarFormat(t *testing.T) {
20+
client := &mockRESTClient{}
21+
_, err := runCreate(client, createCmdFlags{app: "my-app", EnvironmentVariables: []string{"BADFORMAT"}})
22+
require.ErrorContains(t, err, "invalid environment variable format")
23+
}
24+
25+
func TestRunCreate_InvalidSecretFormat(t *testing.T) {
26+
client := &mockRESTClient{}
27+
_, err := runCreate(client, createCmdFlags{app: "my-app", Secrets: []string{"NOSEPARATOR"}})
28+
require.ErrorContains(t, err, "invalid secret format")
29+
}
30+
31+
func TestRunCreate_APIError(t *testing.T) {
32+
client := &mockRESTClient{
33+
putFunc: mockPutError("server error"),
34+
}
35+
_, err := runCreate(client, createCmdFlags{app: "my-app", EnvironmentVariables: []string{"K=V"}})
36+
require.ErrorContains(t, err, "error creating app")
37+
}
38+
39+
func TestRunCreate_Success(t *testing.T) {
40+
var capturedPath string
41+
var capturedBody []byte
42+
client := &mockRESTClient{
43+
putFunc: func(path string, body io.Reader, resp interface{}) error {
44+
capturedPath = path
45+
capturedBody, _ = io.ReadAll(body)
46+
return json.Unmarshal([]byte(`{"app_url":"https://new-app.example.com"}`), resp)
47+
},
48+
}
49+
50+
appUrl, err := runCreate(client, createCmdFlags{
51+
app: "my-app",
52+
EnvironmentVariables: []string{"KEY1=val1", "KEY2=val2"},
53+
Secrets: []string{"SECRET=sval"},
54+
})
55+
require.NoError(t, err)
56+
assert.Equal(t, "https://new-app.example.com", appUrl)
57+
assert.Equal(t, "runtime/my-app/deployment", capturedPath)
58+
59+
var req createReq
60+
json.Unmarshal(capturedBody, &req)
61+
assert.Equal(t, "val1", req.EnvironmentVariables["KEY1"])
62+
assert.Equal(t, "sval", req.Secrets["SECRET"])
63+
}
64+
65+
func TestRunCreate_WithRevisionName(t *testing.T) {
66+
var capturedPath string
67+
client := &mockRESTClient{
68+
putFunc: func(path string, body io.Reader, resp interface{}) error {
69+
capturedPath = path
70+
return json.Unmarshal([]byte(`{"app_url":"https://app.example.com"}`), resp)
71+
},
72+
}
73+
74+
_, err := runCreate(client, createCmdFlags{app: "my-app", RevisionName: "v2"})
75+
require.NoError(t, err)
76+
assert.Contains(t, capturedPath, "revision_name=v2")
77+
}
78+
79+
func TestRunCreate_WithInit(t *testing.T) {
80+
tmp := t.TempDir()
81+
origDir, _ := os.Getwd()
82+
os.Chdir(tmp)
83+
defer os.Chdir(origDir)
84+
85+
client := &mockRESTClient{
86+
putFunc: mockPutResponse(`{"app_url":"https://init-app.example.com"}`),
87+
}
88+
89+
appUrl, err := runCreate(client, createCmdFlags{app: "init-app", Init: true})
90+
require.NoError(t, err)
91+
assert.Equal(t, "https://init-app.example.com", appUrl)
92+
93+
data, err := os.ReadFile("runtime.config.json")
94+
require.NoError(t, err, "expected runtime.config.json to be created")
95+
assert.Contains(t, string(data), "init-app")
96+
}
97+
98+
func TestRunCreate_EnvVarWithEqualsInValue(t *testing.T) {
99+
var capturedBody []byte
100+
client := &mockRESTClient{
101+
putFunc: func(path string, body io.Reader, resp interface{}) error {
102+
capturedBody, _ = io.ReadAll(body)
103+
return json.Unmarshal([]byte(`{"app_url":"https://app.example.com"}`), resp)
104+
},
105+
}
106+
107+
_, err := runCreate(client, createCmdFlags{app: "my-app", EnvironmentVariables: []string{"KEY=val=with=equals"}})
108+
require.NoError(t, err)
109+
110+
var req createReq
111+
json.Unmarshal(capturedBody, &req)
112+
assert.Equal(t, "val=with=equals", req.EnvironmentVariables["KEY"])
113+
}

cmd/delete.go

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cmd
33
import (
44
"fmt"
55
"net/url"
6+
67
"github.com/MakeNowJust/heredoc"
78
"github.com/cli/go-gh/v2/pkg/api"
89
"github.com/spf13/cobra"
@@ -13,9 +14,6 @@ type deleteCmdFlags struct {
1314
revisionName string
1415
}
1516

16-
type deleteResp struct {
17-
}
18-
1917
func init() {
2018
deleteCmdFlags := deleteCmdFlags{}
2119
deleteCmd := &cobra.Command{
@@ -28,40 +26,47 @@ func init() {
2826
$ gh runtime delete --app my-app
2927
# => Deletes the app named 'my-app'
3028
`),
31-
Run: func(cmd *cobra.Command, args []string) {
32-
if deleteCmdFlags.app == "" {
33-
fmt.Println("Error: --app flag is required")
34-
return
35-
}
36-
37-
deleteUrl := fmt.Sprintf("runtime/%s/deployment", deleteCmdFlags.app)
38-
params := url.Values{}
39-
if deleteCmdFlags.revisionName != "" {
40-
params.Add("revision_name", deleteCmdFlags.revisionName)
41-
}
42-
if len(params) > 0 {
43-
deleteUrl += "?" + params.Encode()
44-
}
45-
29+
RunE: func(cmd *cobra.Command, args []string) error {
4630
client, err := api.DefaultRESTClient()
4731
if err != nil {
48-
fmt.Println(err)
49-
return
32+
return fmt.Errorf("failed creating REST client: %v", err)
5033
}
51-
var response string
52-
err = client.Delete(deleteUrl, &response)
34+
35+
response, err := runDelete(client, deleteCmdFlags)
5336
if err != nil {
54-
// print err and response
55-
fmt.Printf("Error deleting app: %v\n", err)
56-
fmt.Printf("Response: %v\n", response)
57-
return
37+
return err
5838
}
5939

6040
fmt.Printf("App deleted: %s\n", response)
41+
return nil
6142
},
6243
}
6344

6445
deleteCmd.Flags().StringVarP(&deleteCmdFlags.app, "app", "a", "", "The app to delete")
6546
deleteCmd.Flags().StringVarP(&deleteCmdFlags.revisionName, "revision-name", "r", "", "The revision name to use for the app")
6647
rootCmd.AddCommand(deleteCmd)
6748
}
49+
50+
func runDelete(client restClient, flags deleteCmdFlags) (string, error) {
51+
if flags.app == "" {
52+
return "", fmt.Errorf("--app flag is required")
53+
}
54+
55+
deleteUrl := fmt.Sprintf("runtime/%s/deployment", flags.app)
56+
params := url.Values{}
57+
if flags.revisionName != "" {
58+
params.Add("revision_name", flags.revisionName)
59+
}
60+
if len(params) > 0 {
61+
deleteUrl += "?" + params.Encode()
62+
}
63+
64+
var response string
65+
err := client.Delete(deleteUrl, &response)
66+
if err != nil {
67+
return response, fmt.Errorf("error deleting app: %v", err)
68+
}
69+
70+
// Actual response on success is empty body so return the ID
71+
return flags.app, nil
72+
}

0 commit comments

Comments
 (0)