Skip to content

Commit 6aaf6e6

Browse files
CLOUDP-311065: improve --debug for auto-generated commands (#3840)
1 parent ab3d302 commit 6aaf6e6

File tree

10 files changed

+570
-67
lines changed

10 files changed

+570
-67
lines changed

internal/api/executor.go

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ import (
1818
"context"
1919
"errors"
2020
"net/http"
21+
"net/http/httputil"
2122

2223
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/config"
24+
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/log"
2325
storeTransport "github.com/mongodb/mongodb-atlas-cli/atlascli/internal/transport"
2426
)
2527

@@ -34,12 +36,13 @@ var (
3436

3537
type Executor struct {
3638
commandConverter CommandConverter
37-
httpClient *http.Client
39+
httpClient HTTPClient
3840
formatter ResponseFormatter
41+
logger Logger
3942
}
4043

4144
// We're expecting a http client that's authenticated.
42-
func NewExecutor(commandConverter CommandConverter, httpClient *http.Client, formatter ResponseFormatter) (*Executor, error) {
45+
func NewExecutor(commandConverter CommandConverter, httpClient HTTPClient, formatter ResponseFormatter, logger Logger) (*Executor, error) {
4346
if commandConverter == nil {
4447
return nil, errors.Join(ErrMissingDependency, errors.New("commandConverter is nil"))
4548
}
@@ -52,10 +55,15 @@ func NewExecutor(commandConverter CommandConverter, httpClient *http.Client, for
5255
return nil, errors.Join(ErrMissingDependency, errors.New("formatter is nil"))
5356
}
5457

58+
if logger == nil {
59+
return nil, errors.Join(ErrMissingDependency, errors.New("logger is nil"))
60+
}
61+
5562
return &Executor{
5663
commandConverter: commandConverter,
5764
httpClient: httpClient,
5865
formatter: formatter,
66+
logger: logger,
5967
}, nil
6068
}
6169

@@ -77,6 +85,7 @@ func NewDefaultExecutor(formatter ResponseFormatter) (*Executor, error) {
7785
commandConverter,
7886
client,
7987
formatter,
88+
log.Default(),
8089
)
8190
}
8291

@@ -104,13 +113,16 @@ func (e *Executor) ExecuteCommand(ctx context.Context, commandRequest CommandReq
104113

105114
// Set the context, so we can cancel the request
106115
httpRequest = httpRequest.WithContext(ctx)
116+
e.logRequest(httpRequest)
107117

108118
// Execute the request
109119
httpResponse, err := e.httpClient.Do(httpRequest)
110120
if err != nil {
111121
return nil, errors.Join(ErrFailedToConvertToHTTPRequest, err)
112122
}
113123

124+
e.logResponse(httpResponse)
125+
114126
//nolint: mnd // httpResponse.StatusCode >= StatusOK && httpResponse.StatusCode < StatusMultipleChoices makes this code harder to read
115127
isSuccess := httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300
116128
httpCode := httpResponse.StatusCode
@@ -138,3 +150,33 @@ func (e *Executor) SetContentType(commandRequest *CommandRequest) error {
138150

139151
return nil
140152
}
153+
154+
// Log the request if the logger is set to debug
155+
// Copied behavior and format used in the SDK: https://github.com/mongodb/atlas-sdk-go/blob/b3fee40e236a8ff2a1f1c160b6984a242136dbe6/admin/client.go#L322
156+
func (e *Executor) logRequest(httpRequest *http.Request) {
157+
if !e.logger.IsDebugLevel() {
158+
return
159+
}
160+
161+
dump, err := httputil.DumpRequestOut(httpRequest, true)
162+
if err != nil {
163+
return
164+
}
165+
166+
_, _ = e.logger.Debugf("\n%s\n", string(dump))
167+
}
168+
169+
// Log the response if the logger is set to debug
170+
// Copied behavior and format used in the SDK: https://github.com/mongodb/atlas-sdk-go/blob/b3fee40e236a8ff2a1f1c160b6984a242136dbe6/admin/client.go#L335
171+
func (e *Executor) logResponse(httpResponse *http.Response) {
172+
if !e.logger.IsDebugLevel() {
173+
return
174+
}
175+
176+
dump, err := httputil.DumpResponse(httpResponse, true)
177+
if err != nil {
178+
return
179+
}
180+
181+
_, _ = e.logger.Debugf("\n%s\n", string(dump))
182+
}

internal/api/executor_test.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// Copyright 2025 MongoDB Inc
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
//go:build unit
16+
17+
package api
18+
19+
import (
20+
"context"
21+
"io"
22+
"net/http"
23+
"strings"
24+
"testing"
25+
26+
"github.com/golang/mock/gomock"
27+
"github.com/mongodb/mongodb-atlas-cli/atlascli/tools/shared/api"
28+
"github.com/stretchr/testify/require"
29+
)
30+
31+
func TestExecutorHappyPathNoLogging(t *testing.T) {
32+
// Setup
33+
ctrl := gomock.NewController(t)
34+
35+
configProvider := NewMockConfigProvider(ctrl)
36+
configProvider.EXPECT().GetBaseURL().Return("https://cloud.mongodb.com", nil).AnyTimes()
37+
38+
commandConverter, err := NewDefaultCommandConverter(configProvider)
39+
require.NoError(t, err)
40+
require.NotNil(t, commandConverter)
41+
42+
httpClient := NewMockHTTPClient(ctrl)
43+
httpClient.EXPECT().Do(gomock.Any()).Return(&http.Response{
44+
StatusCode: http.StatusOK,
45+
Body: io.NopCloser(strings.NewReader(`{"success": true}`)),
46+
}, nil).AnyTimes()
47+
48+
logger := NewMockLogger(ctrl)
49+
logger.EXPECT().IsDebugLevel().Return(false).AnyTimes()
50+
51+
executor, err := NewExecutor(commandConverter, httpClient, NewFormatter(), logger)
52+
require.NoError(t, err)
53+
require.NotNil(t, executor)
54+
55+
// Act
56+
commandRequest := CommandRequest{
57+
Command: api.Command{
58+
OperationID: "testOperation",
59+
RequestParameters: api.RequestParameters{
60+
URL: "/test/url",
61+
},
62+
Versions: []api.Version{{
63+
Version: "1991-05-17",
64+
RequestContentType: "json",
65+
ResponseContentTypes: []string{"json"},
66+
}},
67+
},
68+
ContentType: "json",
69+
Format: "json",
70+
Parameters: nil,
71+
Version: "1991-05-17",
72+
}
73+
response, err := executor.ExecuteCommand(context.Background(), commandRequest)
74+
75+
// Assert
76+
require.NoError(t, err)
77+
require.NotNil(t, response)
78+
}
79+
80+
func TestExecutorHappyPathDebugLogging(t *testing.T) {
81+
// Setup
82+
ctrl := gomock.NewController(t)
83+
84+
configProvider := NewMockConfigProvider(ctrl)
85+
configProvider.EXPECT().GetBaseURL().Return("https://cloud.mongodb.com", nil).AnyTimes()
86+
87+
commandConverter, err := NewDefaultCommandConverter(configProvider)
88+
require.NoError(t, err)
89+
require.NotNil(t, commandConverter)
90+
91+
httpClient := NewMockHTTPClient(ctrl)
92+
httpClient.EXPECT().Do(gomock.Any()).Return(&http.Response{
93+
StatusCode: http.StatusOK,
94+
Body: io.NopCloser(strings.NewReader(`{"success": true}`)),
95+
}, nil).AnyTimes()
96+
97+
logger := NewMockLogger(ctrl)
98+
logger.EXPECT().IsDebugLevel().Return(true).AnyTimes()
99+
100+
gomock.InOrder(
101+
logger.EXPECT().Debugf(gomock.Any(), gomock.Any()).Do(func(_ string, args ...any) {
102+
require.Len(t, args, 1)
103+
require.IsType(t, "", args[0])
104+
require.Equal(t, "GET /test/url HTTP/1.1\r\nHost: cloud.mongodb.com\r\nUser-Agent: Go-http-client/1.1\r\nAccept: application/vnd.atlas.1991-05-17+json\r\nContent-Type: application/vnd.atlas.1991-05-17+json\r\nAccept-Encoding: gzip\r\n\r\n", args[0].(string))
105+
}).Times(1),
106+
logger.EXPECT().Debugf(gomock.Any(), gomock.Any()).Do(func(_ string, args ...any) {
107+
require.Len(t, args, 1)
108+
require.IsType(t, "", args[0])
109+
require.Equal(t, "HTTP/0.0 200 OK\r\n\r\n{\"success\": true}", args[0].(string))
110+
}).Times(1),
111+
)
112+
113+
executor, err := NewExecutor(commandConverter, httpClient, NewFormatter(), logger)
114+
require.NoError(t, err)
115+
require.NotNil(t, executor)
116+
117+
// Act
118+
commandRequest := CommandRequest{
119+
Command: api.Command{
120+
OperationID: "testOperation",
121+
RequestParameters: api.RequestParameters{
122+
URL: "/test/url",
123+
},
124+
Versions: []api.Version{{
125+
Version: "1991-05-17",
126+
RequestContentType: "json",
127+
ResponseContentTypes: []string{"json"},
128+
}},
129+
},
130+
ContentType: "json",
131+
Format: "json",
132+
Parameters: nil,
133+
Version: "1991-05-17",
134+
}
135+
response, err := executor.ExecuteCommand(context.Background(), commandRequest)
136+
137+
// Assert
138+
require.NoError(t, err)
139+
require.NotNil(t, response)
140+
}

internal/api/formatter_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
//go:build unit
16+
1517
package api
1618

1719
import (

internal/api/httprequest_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
//go:build unit
16+
1517
package api
1618

1719
import (

internal/api/interface.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,16 @@ import (
2222
"github.com/mongodb/mongodb-atlas-cli/atlascli/tools/shared/api"
2323
)
2424

25+
//go:generate mockgen -destination=./mocks.go -package=api github.com/mongodb/mongodb-atlas-cli/atlascli/internal/api CommandExecutor,HTTPClient,AccessTokenProvider,ConfigProvider,CommandConverter,Logger,ResponseFormatter
26+
2527
type CommandExecutor interface {
2628
ExecuteCommand(ctx context.Context, commandRequest CommandRequest) (*CommandResponse, error)
2729
}
2830

31+
type HTTPClient interface {
32+
Do(req *http.Request) (*http.Response, error)
33+
}
34+
2935
type AccessTokenProvider interface {
3036
GetAccessToken() (string, error)
3137
}
@@ -38,6 +44,11 @@ type CommandConverter interface {
3844
ConvertToHTTPRequest(request CommandRequest) (*http.Request, error)
3945
}
4046

47+
type Logger interface {
48+
Debugf(format string, a ...any) (int, error)
49+
IsDebugLevel() bool
50+
}
51+
4152
type CommandRequest struct {
4253
Command api.Command
4354
Content io.Reader

0 commit comments

Comments
 (0)