@@ -11,8 +11,10 @@ import (
1111 "net/http/httptest"
1212 "os"
1313 "path/filepath"
14+ "strings"
1415 "syscall"
1516 "testing"
17+ "time"
1618
1719 "github.com/stretchr/testify/require"
1820)
@@ -80,9 +82,14 @@ func TestRedactURLQuery(t *testing.T) {
8082// attempt writes part of the body and then the connection drops mid-copy. The retry
8183// must start the file from scratch rather than append to the partial bytes.
8284func TestDownloadFileRetriesTruncatedBody (t * testing.T ) {
85+ fastRetries (t )
86+
8387 body := []byte ("cloudquery-plugin-binary-payload" )
8488
85- var attempts int
89+ var (
90+ attempts int
91+ hijackErr error
92+ )
8693 server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , _ * http.Request ) {
8794 attempts ++
8895 if attempts == 1 {
@@ -92,7 +99,10 @@ func TestDownloadFileRetriesTruncatedBody(t *testing.T) {
9299 w .(http.Flusher ).Flush ()
93100 // Close the connection mid-body so the client sees a truncated response.
94101 conn , _ , err := w .(http.Hijacker ).Hijack ()
95- require .NoError (t , err )
102+ if err != nil {
103+ hijackErr = err
104+ return
105+ }
96106 conn .Close ()
97107 return
98108 }
@@ -102,6 +112,7 @@ func TestDownloadFileRetriesTruncatedBody(t *testing.T) {
102112
103113 localPath := filepath .Join (t .TempDir (), "plugin.zip" )
104114 checksum , err := downloadFile (context .Background (), localPath , server .URL , DownloaderOptions {NoProgress : true })
115+ require .NoError (t , hijackErr )
105116 require .NoError (t , err )
106117 require .Equal (t , 2 , attempts )
107118
@@ -141,6 +152,8 @@ func TestDownloadFileDoesNotRetryNotFound(t *testing.T) {
141152}
142153
143154func TestDownloadFileRetriesServerError (t * testing.T ) {
155+ fastRetries (t )
156+
144157 body := []byte ("payload" )
145158
146159 var attempts int
@@ -162,6 +175,8 @@ func TestDownloadFileRetriesServerError(t *testing.T) {
162175}
163176
164177func TestDownloadFileGivesUpAfterRetryAttempts (t * testing.T ) {
178+ fastRetries (t )
179+
165180 var attempts int
166181 server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , _ * http.Request ) {
167182 attempts ++
@@ -182,3 +197,97 @@ func sha256Hex(b []byte) string {
182197 s .Write (b )
183198 return fmt .Sprintf ("%x" , s .Sum (nil ))
184199}
200+
201+ // fastRetries removes the real backoff so the retry tests do not sleep through it.
202+ func fastRetries (t * testing.T ) {
203+ t .Helper ()
204+
205+ delay , maxDelay := downloadRetryDelay , downloadRetryMaxDelay
206+ downloadRetryDelay , downloadRetryMaxDelay = time .Millisecond , time .Millisecond
207+ t .Cleanup (func () {
208+ downloadRetryDelay , downloadRetryMaxDelay = delay , maxDelay
209+ })
210+ }
211+
212+ // TestDownloadFileRedactsSignedTokenFromTransportErrors covers the leak that
213+ // url.Error reopens: it prints its URL verbatim, so wrapping one puts the signed
214+ // token back into the message once per attempt.
215+ func TestDownloadFileRedactsSignedTokenFromTransportErrors (t * testing.T ) {
216+ fastRetries (t )
217+
218+ const token = "SUPERSECRETTOKEN"
219+
220+ server := httptest .NewUnstartedServer (http .HandlerFunc (func (_ http.ResponseWriter , _ * http.Request ) {}))
221+ server .Config .ConnState = func (c net.Conn , state http.ConnState ) {
222+ if state == http .StateActive {
223+ c .Close ()
224+ }
225+ }
226+ server .Start ()
227+ t .Cleanup (server .Close )
228+
229+ localPath := filepath .Join (t .TempDir (), "plugin.zip" )
230+ _ , err := downloadFile (context .Background (), localPath , server .URL + "/asset?verify=" + token , DownloaderOptions {NoProgress : true })
231+ require .Error (t , err )
232+ require .NotContains (t , err .Error (), token )
233+ require .NotContains (t , err .Error (), "verify=" )
234+ }
235+
236+ func TestDownloadFileRetriesConnectionRefused (t * testing.T ) {
237+ fastRetries (t )
238+
239+ listener , err := net .Listen ("tcp" , "127.0.0.1:0" )
240+ require .NoError (t , err )
241+ addr := listener .Addr ().String ()
242+ require .NoError (t , listener .Close ())
243+
244+ localPath := filepath .Join (t .TempDir (), "plugin.zip" )
245+ _ , err = downloadFile (context .Background (), localPath , "http://" + addr + "/asset" , DownloaderOptions {NoProgress : true })
246+ require .Error (t , err )
247+ require .Equal (t , int (downloadRetryAttempts ), strings .Count (err .Error (), "connection refused" ))
248+ }
249+
250+ func TestIsRetryableDownloadErrorWindowsSocketMessages (t * testing.T ) {
251+ cases := []struct {
252+ name string
253+ msg string
254+ }{
255+ {name : "WSAECONNRESET" , msg : "wsarecv: An existing connection was forcibly closed by the remote host." },
256+ {name : "WSAECONNREFUSED" , msg : "connectex: No connection could be made because the target machine actively refused it." },
257+ {name : "WSAETIMEDOUT" , msg : "connectex: A connection attempt failed because the connected party did not properly respond after a period of time." },
258+ }
259+
260+ for _ , tc := range cases {
261+ t .Run (tc .name , func (t * testing.T ) {
262+ require .True (t , isRetryableDownloadError (fmt .Errorf ("failed to get url: %w" , errors .New (tc .msg ))))
263+ })
264+ }
265+ }
266+
267+ func TestIsRetryableDownloadErrorDNS (t * testing.T ) {
268+ require .True (t , isRetryableDownloadError (fmt .Errorf ("dial: %w" , & net.DNSError {Err : "no such host" , Name : "assets.cloudquery.io" , IsNotFound : true })))
269+ }
270+
271+ // TestTransportErrorFromRealRequestIsRetryable pins the gap that left
272+ // getURLLocation aborting on the first attempt: a transport failure surfaces as a
273+ // wrapped *url.Error, which its old identity comparison could never match.
274+ func TestTransportErrorFromRealRequestIsRetryable (t * testing.T ) {
275+ server := httptest .NewUnstartedServer (http .HandlerFunc (func (_ http.ResponseWriter , _ * http.Request ) {}))
276+ server .Config .ConnState = func (c net.Conn , state http.ConnState ) {
277+ if state == http .StateActive {
278+ c .Close ()
279+ }
280+ }
281+ server .Start ()
282+ t .Cleanup (server .Close )
283+
284+ resp , err := http .Get (server .URL + "/asset?verify=SUPERSECRETTOKEN" )
285+ if resp != nil {
286+ resp .Body .Close ()
287+ }
288+ require .Error (t , err )
289+
290+ wrapped := fmt .Errorf ("failed to get url %s: %w" , redactURLQuery (server .URL ), redactURLError (err ))
291+ require .True (t , isRetryableDownloadError (wrapped ))
292+ require .NotContains (t , wrapped .Error (), "SUPERSECRETTOKEN" )
293+ }
0 commit comments