33package api
44
55import (
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