diff --git a/api/api.go b/api/api.go index 118957c..237fb6c 100644 --- a/api/api.go +++ b/api/api.go @@ -25,6 +25,7 @@ import ( "io" "net/http" "net/url" + "os" "strings" "time" @@ -130,6 +131,9 @@ type ClientOptions struct { // ShowHTTP is a flag that indicates whether or not HTTP requests and // responses should be logged to stdout ShowHTTP bool + + // CertFile is the path to the reverseproxy tls certificate file + CertFile string } // New returns a new API client. @@ -166,6 +170,17 @@ func New( if err != nil { return nil, errSysCerts } + if opts.CertFile != "" { + revProxyCert, err := os.ReadFile(opts.CertFile) + if err != nil { + c.doLog(log.WithError(err).Error, "Unable to read certificate file") + return nil, err + } + if ok := pool.AppendCertsFromPEM(revProxyCert); !ok { + c.doLog(log.Error, "Failed to append reverse proxy certificate to pool") + return nil, errors.New("failed to append reverse proxy certificate to pool") + } + } c.http.Transport = &http.Transport{ // #nosec G402 TLSClientConfig: &tls.Config{ diff --git a/api/api_logging_test.go b/api/api_logging_test.go new file mode 100644 index 0000000..3e8fb06 --- /dev/null +++ b/api/api_logging_test.go @@ -0,0 +1,172 @@ +/* + Copyright © 2025 Dell Inc. or its subsidiaries. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package api + +import ( + "bytes" + "context" + "io" + "net/http" + "testing" + + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" +) + +func TestIsBinOctetBody(t *testing.T) { + tests := []struct { + name string + header http.Header + expected bool + }{ + {"BinaryOctetStream", http.Header{HeaderKeyContentType: []string{headerValContentTypeBinaryOctetStream}}, true}, + {"NonBinaryOctetStream", http.Header{HeaderKeyContentType: []string{"text/plain"}}, false}, + {"EmptyHeader", http.Header{}, false}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := isBinOctetBody(test.header) + assert.Equal(t, test.expected, result) + }) + } +} + +func TestLogRequest(t *testing.T) { + req, err := http.NewRequest("GET", "http://example.com", nil) + assert.NoError(t, err) + + var buf bytes.Buffer + log.SetOutput(&buf) + log.SetLevel(log.DebugLevel) + + logRequest(context.Background(), req, func(lf func(args ...interface{}), msg string) { + lf(msg) + }) + + assert.Contains(t, buf.String(), "POWERMAX HTTP REQUEST") +} + +func TestLogResponse(t *testing.T) { + res := &http.Response{ + Status: "200 OK", + StatusCode: 200, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(bytes.NewBufferString(`{"key":"value"}`)), + } + + var buf bytes.Buffer + log.SetOutput(&buf) + log.SetLevel(log.DebugLevel) + + logResponse(context.Background(), res, func(lf func(args ...interface{}), msg string) { + lf(msg) + }) + + assert.Contains(t, buf.String(), "POWERMAX HTTP RESPONSE") +} + +func TestWriteIndentedN(t *testing.T) { + var buf bytes.Buffer + err := WriteIndentedN(&buf, []byte("line1\nline2"), 2) + assert.NoError(t, err) + assert.Equal(t, " line1\n line2", buf.String()) +} + +func TestWriteIndented(t *testing.T) { + var buf bytes.Buffer + err := WriteIndented(&buf, []byte("line1\nline2")) + assert.NoError(t, err) + assert.Equal(t, " line1\n line2", buf.String()) +} + +func TestDrainBody(t *testing.T) { + body := io.NopCloser(bytes.NewBufferString("test body")) + r1, r2, err := drainBody(body) + assert.NoError(t, err) + + buf1 := new(bytes.Buffer) + buf1.ReadFrom(r1) + assert.Equal(t, "test body", buf1.String()) + + buf2 := new(bytes.Buffer) + buf2.ReadFrom(r2) + assert.Equal(t, "test body", buf2.String()) +} + +func TestDumpRequest(t *testing.T) { + tests := []struct { + name string + method string + url string + body string + headers map[string]string + expectError bool + expected []string + }{ + { + name: "GET request without body", + method: "GET", + url: "http://example.com", + body: "", + headers: map[string]string{}, + expected: []string{"GET / HTTP/1.1", "Host: example.com"}, + }, + { + name: "POST request with body", + method: "POST", + url: "http://example.com", + body: "test body", + headers: map[string]string{"Content-Type": "application/json"}, + expected: []string{"POST / HTTP/1.1", "Host: example.com", "Content-Type: application/json", "test body"}, + }, + { + name: "Request with Authorization header", + method: "GET", + url: "http://example.com", + body: "", + headers: map[string]string{"Authorization": "Basic dXNlcjpwYXNz"}, + expected: []string{"GET / HTTP/1.1", "Host: example.com"}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + req, err := http.NewRequest(test.method, test.url, bytes.NewBufferString(test.body)) + assert.NoError(t, err) + + for key, value := range test.headers { + req.Header.Set(key, value) + } + + var buf bytes.Buffer + log.SetOutput(&buf) + log.SetLevel(log.DebugLevel) + + dump, err := dumpRequest(req, true) + if test.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + for _, expected := range test.expected { + assert.Contains(t, string(dump), expected) + } + if _, isAuth := test.headers["Authorization"]; isAuth { + assert.Contains(t, buf.String(), "username: user , password: *****") + } + } + }) + } +} diff --git a/api/api_test.go b/api/api_test.go index ed6ba6a..8bffd42 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -1,5 +1,5 @@ /* - Copyright © 2021 Dell Inc. or its subsidiaries. All Rights Reserved. + Copyright © 2021-2025 Dell Inc. or its subsidiaries. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,9 +15,19 @@ package api import ( + "bytes" + "context" + "errors" + "io" + "log" "net/http" "reflect" "testing" + "time" + + types "github.com/dell/gopowermax/v2/types/v100" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" ) type stubTypeWithMetaData struct{} @@ -62,3 +72,516 @@ func Test_addMetaData(t *testing.T) { }) } } + +func TestNew(t *testing.T) { + tests := []struct { + name string + host string + opts ClientOptions + debug bool + expectError bool + }{ + { + name: "Valid host without options", + host: "http://example.com", + opts: ClientOptions{}, + debug: false, + expectError: false, + }, + { + name: "Empty host", + host: "", + opts: ClientOptions{}, + debug: false, + expectError: true, + }, + { + name: "Valid host with timeout", + host: "http://example.com", + opts: ClientOptions{ + Timeout: 10 * time.Second, + }, + debug: false, + expectError: false, + }, + { + name: "Valid host with insecure option", + host: "http://example.com", + opts: ClientOptions{ + Insecure: true, + }, + debug: false, + expectError: false, + }, + { + name: "Valid host with dummy cert file", + host: "http://example.com", + opts: ClientOptions{ + CertFile: "../mock/cert.pem", + }, + debug: false, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c, err := New(tt.host, tt.opts, tt.debug) + if tt.expectError { + if err == nil { + t.Error("Expected error, but got nil") + } + } else { + if err != nil { + t.Errorf("Expected nil error, but got: %v", err) + } + if c == nil { + t.Error("Expected non-nil client, but got nil") + } + } + }) + } +} + +func (m *MockClient) GetHTTPClient() *http.Client { + return m.http +} + +func (m *MockClient) DoWithHeaders( + ctx context.Context, + method, path string, + headers map[string]string, + body, resp interface{}, +) error { + args := m.Called(ctx, method, path, headers, body, resp) + return args.Error(0) +} + +func TestGetHTTPClient(t *testing.T) { + mockClient := &MockClient{http: &http.Client{}} + client := &client{http: mockClient.GetHTTPClient()} + + assert.Equal(t, mockClient.GetHTTPClient(), client.GetHTTPClient()) +} + +func TestBeginsWithSlash(t *testing.T) { + tests := []struct { + input string + expected bool + }{ + {"/path", true}, + {"path/", false}, + {"/", true}, + } + + for _, test := range tests { + result := beginsWithSlash(test.input) + if result != test.expected { + t.Errorf("beginsWithSlash(%q) = %v; want %v", test.input, result, test.expected) + } + } +} + +func TestEndsWithSlash(t *testing.T) { + tests := []struct { + input string + expected bool + }{ + {"/path/", true}, + {"path/", true}, + {"/", true}, + {"path", false}, + } + + for _, test := range tests { + result := endsWithSlash(test.input) + if result != test.expected { + t.Errorf("endsWithSlash(%q) = %v; want %v", test.input, result, test.expected) + } + } +} + +type MockClient struct { + *client + http *http.Client + mock.Mock +} + +func (m *MockClient) DoAndGetResponseBody( + ctx context.Context, + method, uri string, + headers map[string]string, + body interface{}, +) (*http.Response, error) { + args := m.Called(ctx, method, uri, headers, body) + resp := args.Get(0) + if resp == nil { + return nil, args.Error(1) + } + return resp.(*http.Response), args.Error(1) +} + +func TestDoWithHeaders(t *testing.T) { + mockHTTPClient := new(MockHTTPClient) + httpClient := &http.Client{ + Transport: &MockTransport{mockHTTPClient: mockHTTPClient}, + } + mockClient := &client{ + http: httpClient, + host: "https://example.com", + token: "mockToken", + showHTTP: false, + } + + tests := []struct { + name string + method string + uri string + headers map[string]string + body interface{} + resp interface{} + mockResponse *http.Response + mockDoError error + expectedError string + }{ + { + name: "Successful GET request with response", + method: http.MethodGet, + uri: "/test", + headers: map[string]string{ + "Custom-Header": "value", + }, + body: nil, + resp: &map[string]interface{}{}, + mockResponse: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBufferString(`{"success": true}`)), + }, + mockDoError: nil, + expectedError: "", + }, + { + name: "Nil response", + method: http.MethodGet, + uri: "/test", + headers: nil, + body: nil, + resp: nil, + mockResponse: nil, + mockDoError: nil, + expectedError: "", + }, + { + name: "Failed to decode response body", + method: http.MethodGet, + uri: "/test", + headers: map[string]string{ + "Custom-Header": "value", + }, + body: nil, + resp: &map[string]interface{}{}, + mockResponse: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBufferString(`{"invalid_json":`)), + }, + mockDoError: nil, + expectedError: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockHTTPClient.On("Do", mock.Anything).Return(tt.mockResponse, tt.mockDoError) + + err := mockClient.DoWithHeaders( + context.TODO(), + tt.method, + tt.uri, + tt.headers, + tt.body, + tt.resp, + ) + + if tt.expectedError != "" { + if err == nil || err.Error() != tt.expectedError { + t.Errorf("Expected error: %v, but got: %v", tt.expectedError, err) + } + } else { + if err != nil { + t.Errorf("Did not expect an error, but got: %v", err) + } + } + + mockHTTPClient.AssertExpectations(t) + }) + } +} + +// MockHTTPClient is a mock http client +type MockHTTPClient struct { + mock.Mock + http.Client +} + +// Do sends an HTTP request to the API +func (m *MockHTTPClient) Do(req *http.Request) (*http.Response, error) { + args := m.Called(req) + resp := args.Get(0) + if resp == nil { + return nil, args.Error(1) + } + return resp.(*http.Response), args.Error(1) +} + +// MockTransport is a mock http transport +type MockTransport struct { + mockHTTPClient *MockHTTPClient + Response *http.Response +} + +// RoundTrip sends an HTTP request to the API +func (m *MockTransport) RoundTrip(req *http.Request) (*http.Response, error) { + return m.mockHTTPClient.Do(req) +} + +func TestDoAndGetResponseBody(t *testing.T) { + mockHTTPClient := new(MockHTTPClient) + httpClient := &http.Client{ + Transport: &MockTransport{mockHTTPClient: mockHTTPClient}, + } + c := &client{ + http: httpClient, + host: "https://example.com", + token: "mockToken", + showHTTP: false, + } + + tests := []struct { + name string + method string + uri string + headers map[string]string + body interface{} + mockResponse *http.Response + mockError error + expectedError string + }{ + { + name: "Successful GET request", + method: http.MethodGet, + uri: "/test", + headers: map[string]string{ + "Custom-Header": "value", + }, + body: nil, + mockResponse: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBufferString(`{"success": true}`)), + }, + mockError: nil, + expectedError: "", + }, + { + name: "Failed POST request with invalid JSON body", + method: http.MethodPost, + uri: "/test", + headers: map[string]string{ + "Content-Type": "application/json", + }, + body: make(chan int), // invalid JSON body + mockResponse: nil, + mockError: errors.New("unsupported type error"), + expectedError: "json: unsupported type: chan int", + }, + { + name: "Handle io.ReadCloser body", + method: http.MethodPost, + uri: "/test", + headers: map[string]string{ + "Content-Type": "application/octet-stream", + }, + body: io.NopCloser(bytes.NewBufferString(`binary content`)), + mockResponse: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBufferString(`{"success": true}`)), + }, + mockError: nil, + expectedError: "", + }, + { + name: "POST request with JSON body and Content-Type header set", + method: http.MethodPost, + uri: "/test", + headers: map[string]string{ + "Content-Type": "application/json", + }, + body: map[string]string{"key": "value"}, + mockResponse: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBufferString(`{"success": true}`)), + }, + mockError: nil, + expectedError: "", + }, + { + name: "POST request with JSON body without Content-Type header set", + method: http.MethodPost, + uri: "/test", + headers: map[string]string{}, + body: map[string]string{"key": "value"}, + mockResponse: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBufferString(`{"success": true}`)), + }, + mockError: nil, + expectedError: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockHTTPClient.On("Do", mock.Anything).Return(tt.mockResponse, tt.mockError) + + res, err := c.DoAndGetResponseBody( + context.Background(), + tt.method, + tt.uri, + tt.headers, + tt.body, + ) + + if tt.expectedError != "" { + if err == nil || err.Error() != tt.expectedError { + t.Errorf("Expected error: %v, but got: %v", tt.expectedError, err) + } + } else { + if err != nil { + t.Errorf("Did not expect an error, but got: %v", err) + } + if !reflect.DeepEqual(res, tt.mockResponse) { + t.Errorf("Expected response: %v, but got: %v", tt.mockResponse, res) + } + } + + mockHTTPClient.AssertExpectations(t) + }) + } +} + +func TestGetToken(t *testing.T) { + // Test case: token is not set + c := &client{} + if c.GetToken() != "" { + t.Errorf("GetToken() = %v, want %v", c.GetToken(), "") + } + + // Test case: token is set + c.token = "testToken" + if c.GetToken() != "testToken" { + t.Errorf("GetToken() = %v, want %v", c.GetToken(), "testToken") + } +} + +func TestSetToken(t *testing.T) { + c := &client{} + + c.SetToken("token1") + if c.token != "token1" { + t.Errorf("Expected token to be 'token1', got '%s'", c.token) + } + + c.SetToken("token2") + if c.token != "token2" { + t.Errorf("Expected token to be 'token2', got '%s'", c.token) + } +} + +func TestParseJSONError(t *testing.T) { + tests := []struct { + name string + responseBody string + statusCode int + expectedError *types.Error + }{ + { + name: "Valid JSON error response", + responseBody: `{"Message": "error occurred"}`, + statusCode: http.StatusBadRequest, + expectedError: &types.Error{ + HTTPStatusCode: http.StatusBadRequest, + Message: "error occurred", + }, + }, + { + name: "Invalid JSON error response", + responseBody: `invalid json`, + statusCode: http.StatusInternalServerError, + expectedError: &types.Error{ + HTTPStatusCode: http.StatusInternalServerError, + Message: http.StatusText(http.StatusInternalServerError), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &http.Response{ + StatusCode: tt.statusCode, + Body: io.NopCloser(bytes.NewBufferString(tt.responseBody)), + } + + c := &client{} + err := c.ParseJSONError(r) + + assert.Error(t, err) + if e, ok := err.(*types.Error); ok { + assert.Equal(t, tt.expectedError.HTTPStatusCode, e.HTTPStatusCode) + assert.Equal(t, tt.expectedError.Message, e.Message) + } + }) + } +} + +func TestDoLog(t *testing.T) { + type fields struct { + debug bool + } + type args struct { + l func(args ...interface{}) + msg string + } + tests := []struct { + name string + fields fields + args args + }{ + { + name: "debug is true", + fields: fields{ + debug: true, + }, + args: args{ + l: log.Println, + msg: "test message", + }, + }, + { + name: "debug is false", + fields: fields{ + debug: false, + }, + args: args{ + l: log.Println, + msg: "test message", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(_ *testing.T) { + c := &client{ + debug: tt.fields.debug, + } + c.doLog(tt.args.l, tt.args.msg) + }) + } +} diff --git a/authenticate.go b/authenticate.go index dc10f37..08685be 100644 --- a/authenticate.go +++ b/authenticate.go @@ -126,7 +126,8 @@ func NewClient() (client Pmax, err error) { os.Getenv("CSI_POWERMAX_ENDPOINT"), os.Getenv("CSI_APPLICATION_NAME"), os.Getenv("CSI_POWERMAX_INSECURE") == "true", - os.Getenv("CSI_POWERMAX_USECERTS") == "true") + os.Getenv("CSI_POWERMAX_USECERTS") == "true", + "") } // NewClientWithArgs allows the user to specify the endpoint, version, application name, insecure boolean, and useCerts boolean @@ -136,13 +137,15 @@ func NewClientWithArgs( applicationName string, insecure, useCerts bool, + certFile string, ) (client Pmax, err error) { logResponseTimes, _ = strconv.ParseBool(os.Getenv("X_CSI_POWERMAX_RESPONSE_TIMES")) contextTimeout := defaultPmaxTimeout if timeoutStr := os.Getenv("X_CSI_UNISPHERE_TIMEOUT"); timeoutStr != "" { - if timeout, err := time.ParseDuration(timeoutStr); err == nil { + if timeout, err := time.ParseDuration(timeoutStr); err != nil { doLog(log.WithError(err).Error, "Unable to parse Unisphere timout") + } else { contextTimeout = timeout } } @@ -168,6 +171,7 @@ func NewClientWithArgs( Insecure: insecure, UseCerts: useCerts, ShowHTTP: debug, + CertFile: certFile, } if applicationType != "" { diff --git a/go.mod b/go.mod index bda31f8..8de5814 100644 --- a/go.mod +++ b/go.mod @@ -7,16 +7,21 @@ require ( github.com/gorilla/mux v1.7.3 github.com/jinzhu/copier v0.2.4 github.com/sirupsen/logrus v1.4.2 + github.com/stretchr/testify v1.7.1 ) require ( github.com/cucumber/gherkin-go/v19 v19.0.3 // indirect github.com/cucumber/messages-go/v16 v16.0.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-memdb v1.3.4 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/objx v0.1.1 // indirect golang.org/x/sys v0.1.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 41ae017..a4c4f41 100644 --- a/go.sum +++ b/go.sum @@ -35,10 +35,12 @@ github.com/jinzhu/copier v0.2.4/go.mod h1:24xnZezI2Yqac9J61UC6/dG/k76ttpq0DdJI3Q github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -53,6 +55,7 @@ github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -67,6 +70,7 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/inttest/pmax_integration_test.go b/inttest/pmax_integration_test.go index 012c83c..0624efa 100644 --- a/inttest/pmax_integration_test.go +++ b/inttest/pmax_integration_test.go @@ -339,7 +339,7 @@ func cleanupRDFSetup(t *testing.T) { func getClient() error { var err error client, err = pmax.NewClientWithArgs(endpoint, "CSI Driver for Dell EMC PowerMax v1.0", - true, false) + true, false, "") if err != nil { return err } diff --git a/mock/cert.pem b/mock/cert.pem new file mode 100644 index 0000000..325ba9c --- /dev/null +++ b/mock/cert.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIDXTCCAkWgAwIBAgIJALfjQ0Vz0OwhMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV +BAYTAk5PMREwDwYDVQQIDAhTZXJ2ZXJzMQ8wDQYDVQQKDAZNeUNvbXBhbnkxIjAg +BgNVBAMMGU15Q29tcGFueSBDZXJ0aWZpY2F0ZTAgFw0xOTEyMDQxMzE3NTdaGA8y +MTkzMTIwNDEzMTc1N1owRTELMAkGA1UEBhMCTk8xETAPBgNVBAgMCE1vbWVudDEP +MA0GA1UECgwGTXlDb21wYW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAmh/n4A0MYW5NfIjJdpN6fDgVtUeNQZ+3D/mEHoH8gJ5u0lZB+6G/1hd5+SSlH +cC1QZUt/2ZLPUwXtxGgFvqQm9uqD7MfYw5I1I6iHhD6uWZw/oEt2Z60n8Esm7L4e +bBXYAbVX5L5A/xm9KeUPXG2N7E9OwbBkcq5Zb9XGpDPq2CwD45ktpF5/7W0F64N2F +XW+O9eRXDmyM3EZ3G/AY+evdsgSo0hE= +-----END CERTIFICATE----- diff --git a/mock/mock.go b/mock/mock.go index 4db9f6d..1e706aa 100644 --- a/mock/mock.go +++ b/mock/mock.go @@ -2304,6 +2304,7 @@ func newVolume(volumeID, volumeIdentifier string, size int, sgList []string) { VolumeIdentifier: volumeIdentifier, WWN: "600009700001979000465330303" + volumeID, EffectiveWWN: "600009700001979000465330303" + volumeID, + NGUID: "600009700001979000465330303" + volumeID, Encapsulated: false, NumberOfStorageGroups: 1, NumberOfFrontEndPaths: 0, diff --git a/unit_steps_test.go b/unit_steps_test.go index c01814c..77d1f12 100644 --- a/unit_steps_test.go +++ b/unit_steps_test.go @@ -515,7 +515,7 @@ func (c *unitContext) iCallAuthenticateWithEndpointCredentials(endpoint, credent URL = "" } fmt.Printf("apiVersion: %s\n", apiVersion) - client, err := NewClientWithArgs(URL, "", true, false) + client, err := NewClientWithArgs(URL, "", true, false, "") if err != nil { c.err = err return nil diff --git a/unittest/pmax.feature b/unittest/pmax.feature index 03fb460..4ee3f47 100644 --- a/unittest/pmax.feature +++ b/unittest/pmax.feature @@ -246,9 +246,9 @@ Feature: PMAX Client library | "IntgC" | 1 | "UpdateStorageGroupError" | "A job was not returned from UpdateStorageGroup" | "" | | "IntgD" | 1 | "httpStatus500" | "A job was not returned from UpdateStorageGroup" | "" | | "IntgE" | 1 | "GetJobError" | "induced error" | "" | - | "IntgF" | 1 | "JobFailedError" | "The UpdateStorageGroup job failed" | "" | - | "IntgG" | 1 | "GetVolumeError" | "Failed to find newly created volume with name: IntgG" | "" | - | "IntgH" | 1 | "VolumeNotCreatedError" | "Failed to find newly created volume with name: IntgH" | "" | + | "IntgF" | 1 | "JobFailedError" | "the UpdateStorageGroup job failed" | "" | + | "IntgG" | 1 | "GetVolumeError" | "failed to find newly created volume with name: IntgG" | "" | + | "IntgH" | 1 | "VolumeNotCreatedError" | "failed to find newly created volume with name: IntgH" | "" | | "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxy"| 1 | "none" | "Length of volumeName exceeds max limit" | "" | | "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk" | 1 | "none" | "none" | "" | | "IntgA" | 1 | "none" | "ignored as it is not managed" | "ignored" | @@ -299,9 +299,9 @@ Scenario Outline: Test cases for modifyMobility for volume | "IntgC" | 1 | "GB" | "UpdateStorageGroupError" | "A job was not returned from UpdateStorageGroup" | "" | | "IntgD" | 1 | "GB" | "httpStatus500" | "A job was not returned from UpdateStorageGroup" | "" | | "IntgE" | 1 | "GB" | "GetJobError" | "induced error" | "" | - | "IntgF" | 1 | "GB" | "JobFailedError" | "The UpdateStorageGroup job failed" | "" | - | "IntgG" | 1 | "GB" | "GetVolumeError" | "Failed to find newly created volume with name: IntgG" | "" | - | "IntgH" | 1 | "GB" | "VolumeNotCreatedError" | "Failed to find newly created volume with name: IntgH" | "" | + | "IntgF" | 1 | "GB" | "JobFailedError" | "the UpdateStorageGroup job failed" | "" | + | "IntgG" | 1 | "GB" | "GetVolumeError" | "failed to find newly created volume with name: IntgG" | "" | + | "IntgH" | 1 | "GB" | "VolumeNotCreatedError" | "failed to find newly created volume with name: IntgH" | "" | | "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxy"| 1 | "GB" | "none" | "Length of volumeName exceeds max limit" | "" | | "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk" | 1 | "CYL" | "none" | "none" | "" | | "IntgA" | 1 | "GB" | "none" | "ignored as it is not managed" | "ignored" | @@ -320,8 +320,8 @@ Scenario Outline: Test cases for Synchronous CreateVolumeInStorageGroup for v90 | volname | size | induced | errormsg | arrays | | "IntgA" | 1 | "none" | "none" | "" | | "IntgB" | 5 | "none" | "none" | "" | - | "IntgG" | 1 | "GetVolumeError" | "Failed to find newly created volume with name: IntgG" | "" | - | "IntgH" | 1 | "VolumeNotCreatedError" | "Failed to find newly created volume with name: IntgH" | "" | + | "IntgG" | 1 | "GetVolumeError" | "failed to find newly created volume with name: IntgG" | "" | + | "IntgH" | 1 | "VolumeNotCreatedError" | "failed to find newly created volume with name: IntgH" | "" | | "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxy"| 1 | "none" | "Length of volumeName exceeds max limit" | "" | | "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk" | 1 | "none" | "none" | "" | | "IntgA" | 1 | "none" | "ignored as it is not managed" | "ignored" | @@ -338,8 +338,8 @@ Scenario Outline: Test cases for Synchronous CreateVolumeInStorageGroup for v90 | volname | size |capUnit | induced | errormsg | arrays | | "IntgA" | 1 | "CYL" | "none" | "none" | "" | | "IntgB" | 5 | "CYL" | "none" | "none" | "" | - | "IntgG" | 1 | "CYL" | "GetVolumeError" | "Failed to find newly created volume with name: IntgG" | "" | - | "IntgH" | 1 | "CYL" | "VolumeNotCreatedError" | "Failed to find newly created volume with name: IntgH" | "" | + | "IntgG" | 1 | "CYL" | "GetVolumeError" | "failed to find newly created volume with name: IntgG" | "" | + | "IntgH" | 1 | "CYL" | "VolumeNotCreatedError" | "failed to find newly created volume with name: IntgH" | "" | | "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxy"| 1 | "CYL" | "none" | "Length of volumeName exceeds max limit" | "" | | "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk" | 1 | "CYL" | "none" | "none" | "" | | "IntgA" | 1 | "CYL" | "none" | "ignored as it is not managed" | "ignored" | @@ -356,8 +356,8 @@ Scenario Outline: Test cases for Synchronous CreateVolumeInStorageGroup with met | volname | size | induced | errormsg | arrays | | "IntgA" | 1 | "none" | "none" | "" | | "IntgB" | 5 | "none" | "none" | "" | - | "IntgG" | 1 | "GetVolumeError" | "Failed to find newly created volume with name: IntgG" | "" | - | "IntgH" | 1 | "VolumeNotCreatedError" | "Failed to find newly created volume with name: IntgH" | "" | + | "IntgG" | 1 | "GetVolumeError" | "failed to find newly created volume with name: IntgG" | "" | + | "IntgH" | 1 | "VolumeNotCreatedError" | "failed to find newly created volume with name: IntgH" | "" | | "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxy"| 1 | "none" | "Length of volumeName exceeds max limit" | "" | | "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk" | 1 | "none" | "none" | "" | | "IntgA" | 1 | "none" | "ignored as it is not managed" | "ignored" | @@ -377,9 +377,9 @@ Scenario Outline: Test cases for Synchronous CreateVolumeInStorageGroup with met | "IntgC" | 1 | "UpdateStorageGroupError" | "A job was not returned from UpdateStorageGroup" | "" | | "IntgD" | 1 | "httpStatus500" | "A job was not returned from UpdateStorageGroup" | "" | | "IntgE" | 1 | "GetJobError" | "induced error" | "" | - | "IntgF" | 1 | "JobFailedError" | "The UpdateStorageGroup job failed" | "" | - | "IntgG" | 1 | "GetVolumeError" | "Failed to find newly created volume with name: IntgG" | "" | - | "IntgH" | 1 | "VolumeNotCreatedError" | "Failed to find newly created volume with name: IntgH" | "" | + | "IntgF" | 1 | "JobFailedError" | "the UpdateStorageGroup job failed" | "" | + | "IntgG" | 1 | "GetVolumeError" | "failed to find newly created volume with name: IntgG" | "" | + | "IntgH" | 1 | "VolumeNotCreatedError" | "failed to find newly created volume with name: IntgH" | "" | | "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxy"| 1 | "none" | "Length of volumeName exceeds max limit" | "" | | "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk" | 1 | "none" | "none" | "" | | "IntgA" | 1 | "none" | "ignored as it is not managed" | "ignored" | @@ -396,8 +396,8 @@ Scenario Outline: Test cases for Synchronous CreateVolumeInStorageGroup with met | volname | size | induced | errormsg | arrays | | "IntgA" | 1 | "none" | "none" | "" | | "IntgB" | 5 | "none" | "none" | "" | - | "IntgG" | 1 | "GetVolumeError" | "Failed to find newly created volume with name: IntgG" | "" | - | "IntgH" | 1 | "VolumeNotCreatedError" | "Failed to find newly created volume with name: IntgH" | "" | + | "IntgG" | 1 | "GetVolumeError" | "failed to find newly created volume with name: IntgG" | "" | + | "IntgH" | 1 | "VolumeNotCreatedError" | "failed to find newly created volume with name: IntgH" | "" | | "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxy"| 1 | "none" | "Length of volumeName exceeds max limit" | "" | | "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk" | 1 | "none" | "none" | "" | | "IntgA" | 1 | "none" | "ignored as it is not managed" | "ignored" | @@ -414,8 +414,8 @@ Scenario Outline: Test cases for Synchronous CreateVolumeInStorageGroup with met | volname | size | induced | errormsg | arrays | | "IntgA" | 1 | "none" | "none" | "" | | "IntgB" | 5 | "none" | "none" | "" | - | "IntgG" | 1 | "GetVolumeError" | "Failed to find newly created volume with name: IntgG" | "" | - | "IntgH" | 1 | "VolumeNotCreatedError" | "Failed to find newly created volume with name: IntgH" | "" | + | "IntgG" | 1 | "GetVolumeError" | "failed to find newly created volume with name: IntgG" | "" | + | "IntgH" | 1 | "VolumeNotCreatedError" | "failed to find newly created volume with name: IntgH" | "" | | "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxy"| 1 | "none" | "Length of volumeName exceeds max limit" | "" | | "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk" | 1 | "none" | "none" | "" | | "IntgA" | 1 | "none" | "ignored as it is not managed" | "ignored" | @@ -944,7 +944,7 @@ Scenario Outline: Test GetHostList | 0 | "TestSG" |"none" | "At least one volume id has to be specified" | "" | | 5 | "TestSG" |"VolumeNotAddedError" | "A job was not returned from UpdateStorageGroup" | "" | | 3 | "TestSG" |"UpdateStorageGroupError" | "A job was not returned from UpdateStorageGroup" | "" | - | 1 | "TestSG" |"JobFailedError" | "The UpdateStorageGroup job failed" | "" | + | 1 | "TestSG" |"JobFailedError" | "UpdateStorageGroup job failed" | "" | | 1 | "TestSG" |"GetJobError" | "induced error" | "" | | 1 | "TestSG" |"none" | "ignored as it is not managed" | "ignored" | @@ -982,7 +982,7 @@ Scenario Outline: Test GetHostList | 0 | "TestSG" |"none" | "At least one volume id has to be specified" | "" | | 5 | "TestSG" |"VolumeNotAddedError" | "A job was not returned from UpdateStorageGroup" | "" | | 3 | "TestSG" |"UpdateStorageGroupError" | "A job was not returned from UpdateStorageGroup" | "" | - | 1 | "TestSG" |"JobFailedError" | "The UpdateStorageGroup job failed" | "" | + | 1 | "TestSG" |"JobFailedError" | "UpdateStorageGroup job failed" | "" | | 1 | "TestSG" |"GetJobError" | "induced error" | "" | | 1 | "TestSG" |"none" | "ignored as it is not managed" | "ignored" | diff --git a/unittest/srdf.feature b/unittest/srdf.feature index c38e02f..557a6c2 100644 --- a/unittest/srdf.feature +++ b/unittest/srdf.feature @@ -148,8 +148,8 @@ Feature: PMAX SRDF test | volname | size | induced | errormsg | arrays | | "IntgA" | 1 | "none" | "none" | "" | | "IntgB" | 5 | "none" | "none" | "" | - | "IntgG" | 1 | "GetVolumeError" | "Failed to find newly created volume with name: IntgG" | "" | - | "IntgH" | 1 | "VolumeNotCreatedError" | "Failed to find newly created volume with name: IntgH" | "" | + | "IntgG" | 1 | "GetVolumeError" | "failed to find newly created volume with name: IntgG" | "" | + | "IntgH" | 1 | "VolumeNotCreatedError" | "failed to find newly created volume with name: IntgH" | "" | | "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxy" | 1 | "none" | "Length of volumeName exceeds max limit" | "" | | "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk" | 1 | "none" | "none" | "" | | "IntgA" | 1 | "none" | "ignored as it is not managed" | "ignored" |