Skip to content

Commit f192a6c

Browse files
Improved coverage for core/concurrent_cache, cli/api packages
1 parent 3935ddf commit f192a6c

File tree

11 files changed

+4750
-86
lines changed

11 files changed

+4750
-86
lines changed

cli/api/rest_test.go

Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,17 @@
33
package api
44

55
import (
6+
"bytes"
67
"io"
8+
"net/http"
9+
"net/http/httptest"
710
"os"
11+
"strings"
812
"testing"
913

14+
"github.com/stretchr/testify/assert"
15+
"github.com/stretchr/testify/require"
16+
1017
"github.com/netapp/trident/logging"
1118
)
1219

@@ -15,3 +22,308 @@ func TestMain(m *testing.M) {
1522
logging.InitLogOutput(io.Discard)
1623
os.Exit(m.Run())
1724
}
25+
26+
func TestInvokeRESTAPI_Success(t *testing.T) {
27+
tests := []struct {
28+
name string
29+
method string
30+
requestBody []byte
31+
serverResp string
32+
statusCode int
33+
}{
34+
{
35+
name: "GET request with nil body",
36+
method: "GET",
37+
requestBody: nil,
38+
serverResp: `{"result": "success"}`,
39+
statusCode: 200,
40+
},
41+
{
42+
name: "POST request with JSON body",
43+
method: "POST",
44+
requestBody: []byte(`{"name": "test"}`),
45+
serverResp: `{"id": "123"}`,
46+
statusCode: 201,
47+
},
48+
{
49+
name: "PUT request with empty body",
50+
method: "PUT",
51+
requestBody: []byte{},
52+
serverResp: `{"updated": true}`,
53+
statusCode: 200,
54+
},
55+
{
56+
name: "DELETE request",
57+
method: "DELETE",
58+
requestBody: nil,
59+
serverResp: "",
60+
statusCode: 204,
61+
},
62+
}
63+
64+
for _, tt := range tests {
65+
t.Run(tt.name, func(t *testing.T) {
66+
// Create test server
67+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
68+
// Verify request method
69+
assert.Equal(t, tt.method, r.Method)
70+
71+
// Verify Content-Type header
72+
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
73+
74+
// Verify request body if provided
75+
if tt.requestBody != nil {
76+
body, err := io.ReadAll(r.Body)
77+
require.NoError(t, err)
78+
assert.Equal(t, tt.requestBody, body)
79+
}
80+
81+
// Send response
82+
w.WriteHeader(tt.statusCode)
83+
w.Write([]byte(tt.serverResp))
84+
}))
85+
defer server.Close()
86+
87+
// Call function
88+
resp, body, err := InvokeRESTAPI(tt.method, server.URL, tt.requestBody)
89+
90+
// Assertions
91+
require.NoError(t, err)
92+
assert.NotNil(t, resp)
93+
assert.Equal(t, tt.statusCode, resp.StatusCode)
94+
assert.Equal(t, []byte(tt.serverResp), body)
95+
})
96+
}
97+
}
98+
99+
func TestInvokeRESTAPI_InvalidURL(t *testing.T) {
100+
// Test with invalid URL
101+
resp, body, err := InvokeRESTAPI("GET", "://invalid-url", nil)
102+
103+
assert.Error(t, err)
104+
assert.Nil(t, resp)
105+
assert.Nil(t, body)
106+
}
107+
108+
func TestInvokeRESTAPI_NetworkError(t *testing.T) {
109+
// Test with non-existent server
110+
resp, body, err := InvokeRESTAPI("GET", "http://localhost:99999/nonexistent", nil)
111+
112+
assert.Error(t, err)
113+
assert.Contains(t, err.Error(), "error communicating with Trident REST API")
114+
assert.Nil(t, resp)
115+
assert.Nil(t, body)
116+
}
117+
118+
func TestInvokeRESTAPI_ServerError(t *testing.T) {
119+
// Create server that returns error
120+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
121+
w.WriteHeader(http.StatusInternalServerError)
122+
w.Write([]byte(`{"error": "internal server error"}`))
123+
}))
124+
defer server.Close()
125+
126+
resp, body, err := InvokeRESTAPI("GET", server.URL, nil)
127+
128+
require.NoError(t, err)
129+
assert.NotNil(t, resp)
130+
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
131+
assert.Equal(t, []byte(`{"error": "internal server error"}`), body)
132+
}
133+
134+
func TestInvokeRESTAPI_ReadBodyError(t *testing.T) {
135+
// Create server that closes connection unexpectedly
136+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
137+
w.Header().Set("Content-Length", "100") // Claim more content than we'll send
138+
w.WriteHeader(200)
139+
w.Write([]byte("short"))
140+
// Close connection without sending full content
141+
if f, ok := w.(http.Flusher); ok {
142+
f.Flush()
143+
}
144+
if hj, ok := w.(http.Hijacker); ok {
145+
conn, _, _ := hj.Hijack()
146+
conn.Close()
147+
}
148+
}))
149+
defer server.Close()
150+
151+
// This may or may not trigger the read error depending on the HTTP implementation
152+
// But we still test the code path
153+
resp, body, err := InvokeRESTAPI("GET", server.URL, nil)
154+
155+
// The behavior may vary, but we should get either a response or an error
156+
if err != nil {
157+
assert.Contains(t, strings.ToLower(err.Error()), "error")
158+
} else {
159+
assert.NotNil(t, resp)
160+
}
161+
// Body might be partial or nil
162+
_ = body
163+
}
164+
165+
func TestLogHTTPRequest(t *testing.T) {
166+
tests := []struct {
167+
name string
168+
method string
169+
url string
170+
headers map[string]string
171+
requestBody []byte
172+
}{
173+
{
174+
name: "GET request with nil body",
175+
method: "GET",
176+
url: "http://example.com/api/v1/test",
177+
headers: map[string]string{"Authorization": "Bearer token"},
178+
requestBody: nil,
179+
},
180+
{
181+
name: "POST request with JSON body",
182+
method: "POST",
183+
url: "http://example.com/api/v1/create",
184+
headers: map[string]string{"Content-Type": "application/json"},
185+
requestBody: []byte(`{"name": "test", "value": 123}`),
186+
},
187+
{
188+
name: "PUT request with empty body",
189+
method: "PUT",
190+
url: "http://example.com/api/v1/update/123",
191+
headers: map[string]string{"User-Agent": "test-agent"},
192+
requestBody: []byte{},
193+
},
194+
}
195+
196+
for _, tt := range tests {
197+
t.Run(tt.name, func(t *testing.T) {
198+
// Create request
199+
var body io.Reader
200+
if tt.requestBody != nil {
201+
body = bytes.NewBuffer(tt.requestBody)
202+
}
203+
204+
req, err := http.NewRequest(tt.method, tt.url, body)
205+
require.NoError(t, err)
206+
207+
// Add headers
208+
for key, value := range tt.headers {
209+
req.Header.Set(key, value)
210+
}
211+
212+
// Test the function - should not panic
213+
assert.NotPanics(t, func() {
214+
LogHTTPRequest(req, tt.requestBody)
215+
})
216+
})
217+
}
218+
}
219+
220+
func TestLogHTTPResponse(t *testing.T) {
221+
tests := []struct {
222+
name string
223+
response *http.Response
224+
responseBody []byte
225+
}{
226+
{
227+
name: "successful response with body",
228+
response: &http.Response{
229+
Status: "200 OK",
230+
StatusCode: 200,
231+
Header: http.Header{"Content-Type": []string{"application/json"}},
232+
},
233+
responseBody: []byte(`{"result": "success"}`),
234+
},
235+
{
236+
name: "error response with body",
237+
response: &http.Response{
238+
Status: "404 Not Found",
239+
StatusCode: 404,
240+
Header: http.Header{"Content-Type": []string{"application/json"}},
241+
},
242+
responseBody: []byte(`{"error": "not found"}`),
243+
},
244+
{
245+
name: "nil response",
246+
response: nil,
247+
responseBody: []byte(`{"message": "test"}`),
248+
},
249+
{
250+
name: "response with nil body",
251+
response: &http.Response{
252+
Status: "204 No Content",
253+
StatusCode: 204,
254+
Header: http.Header{},
255+
},
256+
responseBody: nil,
257+
},
258+
{
259+
name: "response with empty body",
260+
response: &http.Response{
261+
Status: "200 OK",
262+
StatusCode: 200,
263+
Header: http.Header{"Content-Length": []string{"0"}},
264+
},
265+
responseBody: []byte{},
266+
},
267+
}
268+
269+
for _, tt := range tests {
270+
t.Run(tt.name, func(t *testing.T) {
271+
// Test the function - should not panic
272+
assert.NotPanics(t, func() {
273+
LogHTTPResponse(tt.response, tt.responseBody)
274+
})
275+
})
276+
}
277+
}
278+
279+
// Test various edge cases and error conditions
280+
func TestInvokeRESTAPI_EdgeCases(t *testing.T) {
281+
t.Run("empty method", func(t *testing.T) {
282+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
283+
w.WriteHeader(200)
284+
}))
285+
defer server.Close()
286+
287+
resp, body, err := InvokeRESTAPI("", server.URL, nil)
288+
// Empty method should still work (defaults to GET in http.NewRequest)
289+
require.NoError(t, err)
290+
assert.NotNil(t, resp)
291+
assert.Equal(t, 200, resp.StatusCode)
292+
_ = body
293+
})
294+
295+
t.Run("very large request body", func(t *testing.T) {
296+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
297+
body, _ := io.ReadAll(r.Body)
298+
w.WriteHeader(200)
299+
w.Write([]byte("received"))
300+
assert.True(t, len(body) > 1000)
301+
}))
302+
defer server.Close()
303+
304+
largeBody := bytes.Repeat([]byte("x"), 10000)
305+
resp, body, err := InvokeRESTAPI("POST", server.URL, largeBody)
306+
307+
require.NoError(t, err)
308+
assert.NotNil(t, resp)
309+
assert.Equal(t, 200, resp.StatusCode)
310+
assert.Equal(t, []byte("received"), body)
311+
})
312+
313+
t.Run("special characters in body", func(t *testing.T) {
314+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
315+
body, _ := io.ReadAll(r.Body)
316+
w.WriteHeader(200)
317+
w.Write(body) // Echo back the body
318+
}))
319+
defer server.Close()
320+
321+
specialBody := []byte(`{"text": "Hello 世界! 🌍 Special chars: \n\t\r\"'"}`)
322+
resp, body, err := InvokeRESTAPI("POST", server.URL, specialBody)
323+
324+
require.NoError(t, err)
325+
assert.NotNil(t, resp)
326+
assert.Equal(t, 200, resp.StatusCode)
327+
assert.Equal(t, specialBody, body)
328+
})
329+
}

0 commit comments

Comments
 (0)