@@ -6,65 +6,206 @@ import (
66 "io"
77 "net/http"
88 "testing"
9+ "time"
910
1011 "github.com/stretchr/testify/assert"
1112)
1213
13- type MockRoundTripper struct {
14+ // errReadCloser simulates a failure while reading the body.
15+ type errReadCloser struct {}
16+
17+ func (e * errReadCloser ) Read (p []byte ) (int , error ) { return 0 , errors .New ("read error" ) }
18+ func (e * errReadCloser ) Close () error { return nil }
19+
20+ // RTResponse holds one RoundTrip outcome.
21+ type RTResponse struct {
1422 Response * http.Response
1523 Err error
1624}
1725
18- func (mrt * MockRoundTripper ) RoundTrip (req * http.Request ) (* http.Response , error ) {
19- return mrt .Response , mrt .Err
26+ // SeqRoundTripper returns a sequence of outcomes across calls.
27+ // If calls exceed length, it repeats the last element.
28+ type SeqRoundTripper struct {
29+ Seq []RTResponse
30+ Calls int
2031}
2132
22- type errReadCloser struct {}
33+ func (s * SeqRoundTripper ) RoundTrip (req * http.Request ) (* http.Response , error ) {
34+ i := s .Calls
35+ if i >= len (s .Seq ) {
36+ i = len (s .Seq ) - 1
37+ }
38+ s .Calls ++
39+ entry := s .Seq [i ]
40+ return entry .Response , entry .Err
41+ }
2342
24- func (e * errReadCloser ) Read (p []byte ) (int , error ) { return 0 , errors .New ("read error" ) }
25- func (e * errReadCloser ) Close () error { return nil }
43+ // makeResp builds a tiny HTTP response with given status and body.
44+ func makeResp (code int , body string ) * http.Response {
45+ return & http.Response {
46+ StatusCode : code ,
47+ Body : io .NopCloser (bytes .NewBufferString (body )),
48+ Header : make (http.Header ),
49+ }
50+ }
2651
2752func TestPostToGateway (t * testing.T ) {
2853 h , _ , _ := newMockHandler (t )
2954
30- t .Run ("success" , func (t * testing.T ) {
31- mockResponseBody := `{"jsonrpc":"2.0","id":"abc","result":{"ok":true}}`
32- mockResponse := & http.Response {
33- StatusCode : 200 ,
34- Body : io .NopCloser (bytes .NewBufferString (mockResponseBody )),
35- Header : make (http.Header ),
36- }
55+ t .Run ("success (single attempt)" , func (t * testing.T ) {
56+ body := `{"jsonrpc":"2.0","id":"abc","result":{"ok":true}}`
3757
38- // Wire mock transport into Handler's HTTP client
39- mockedHttpClient := & http.Client {Transport : & MockRoundTripper {Response : mockResponse }}
40- h .Gw = & HTTPClient {URL : "https://unit-test.gw" , Client : mockedHttpClient }
58+ rt := & SeqRoundTripper {
59+ Seq : []RTResponse {
60+ {Response : makeResp (200 , body )},
61+ },
62+ }
63+ mockedHTTP := & http.Client {Transport : rt }
64+ h .Gw = & HTTPClient {
65+ URL : "https://unit-test.gw" ,
66+ Client : mockedHTTP ,
67+ RetryAttempts : 3 ,
68+ RetryDelay : 0 , // fast tests
69+ }
4170
4271 respBytes , status , err := h .Gw .Post ([]byte (`{"x":1}` ))
4372 assert .NoError (t , err )
4473 assert .Equal (t , 200 , status )
45- assert .Equal (t , mockResponseBody , string (respBytes ))
74+ assert .Equal (t , body , string (respBytes ))
75+ assert .Equal (t , 1 , rt .Calls )
4676 })
4777
48- t .Run ("http error" , func (t * testing.T ) {
49- mockedHttpClient := & http.Client {Transport : & MockRoundTripper {Response : nil , Err : errors .New ("network down" )}}
50- h .Gw = & HTTPClient {URL : "https://unit-test.gw" , Client : mockedHttpClient }
78+ t .Run ("http transport error -> retries then fail" , func (t * testing.T ) {
79+ rt := & SeqRoundTripper {
80+ Seq : []RTResponse {
81+ {Err : errors .New ("network down" )},
82+ {Err : errors .New ("network still down" )},
83+ },
84+ }
85+ mockedHTTP := & http.Client {Transport : rt }
86+ h .Gw = & HTTPClient {
87+ URL : "https://unit-test.gw" ,
88+ Client : mockedHTTP ,
89+ RetryAttempts : 2 ,
90+ RetryDelay : 0 ,
91+ }
5192
5293 _ , _ , err := h .Gw .Post ([]byte (`{}` ))
5394 assert .Error (t , err )
54- assert .Contains (t , err .Error (), "network down" )
95+ assert .Contains (t , err .Error (), "network" )
96+ assert .Equal (t , 2 , rt .Calls )
5597 })
5698
57- t .Run ("read error" , func (t * testing.T ) {
58- mockResponse := & http.Response {
59- StatusCode : 200 ,
60- Body : & errReadCloser {},
61- Header : make (http.Header ),
99+ t .Run ("read error -> retries then fail" , func (t * testing.T ) {
100+ rt := & SeqRoundTripper {
101+ Seq : []RTResponse {
102+ {Response : & http.Response {StatusCode : 200 , Body : & errReadCloser {}, Header : make (http.Header )}},
103+ {Response : & http.Response {StatusCode : 200 , Body : & errReadCloser {}, Header : make (http.Header )}},
104+ },
105+ }
106+ mockedHTTP := & http.Client {Transport : rt }
107+ h .Gw = & HTTPClient {
108+ URL : "https://unit-test.gw" ,
109+ Client : mockedHTTP ,
110+ RetryAttempts : 2 ,
111+ RetryDelay : 0 ,
62112 }
63- mockedHttpClient := & http.Client {Transport : & MockRoundTripper {Response : mockResponse }}
64- h .Gw = & HTTPClient {URL : "https://unit-test.gw" , Client : mockedHttpClient }
65113
66114 _ , _ , err := h .Gw .Post ([]byte (`{}` ))
67115 assert .Error (t , err )
68116 assert .Contains (t , err .Error (), "read response body: read error" )
117+ assert .Equal (t , 2 , rt .Calls )
118+ })
119+
120+ t .Run ("non-200 then 200 -> success after retry" , func (t * testing.T ) {
121+ successBody := `{"ok":true}`
122+ rt := & SeqRoundTripper {
123+ Seq : []RTResponse {
124+ {Response : makeResp (503 , "temporary outage" )},
125+ {Response : makeResp (200 , successBody )},
126+ },
127+ }
128+ mockedHTTP := & http.Client {Transport : rt }
129+ h .Gw = & HTTPClient {
130+ URL : "https://unit-test.gw" ,
131+ Client : mockedHTTP ,
132+ RetryAttempts : 3 ,
133+ RetryDelay : 0 ,
134+ }
135+
136+ respBytes , status , err := h .Gw .Post ([]byte (`{}` ))
137+ assert .NoError (t , err )
138+ assert .Equal (t , 200 , status )
139+ assert .Equal (t , successBody , string (respBytes ))
140+ assert .Equal (t , 2 , rt .Calls )
141+ })
142+
143+ t .Run ("always non-200 -> fail after attempts" , func (t * testing.T ) {
144+ rt := & SeqRoundTripper {
145+ Seq : []RTResponse {
146+ {Response : makeResp (500 , "err1" )},
147+ {Response : makeResp (429 , "err2" )},
148+ {Response : makeResp (400 , "err3" )}, // any non-200 should still retry/fail
149+ },
150+ }
151+ mockedHTTP := & http.Client {Transport : rt }
152+ h .Gw = & HTTPClient {
153+ URL : "https://unit-test.gw" ,
154+ Client : mockedHTTP ,
155+ RetryAttempts : 3 ,
156+ RetryDelay : 0 ,
157+ }
158+
159+ _ , status , err := h .Gw .Post ([]byte (`{}` ))
160+ assert .Error (t , err )
161+ // status is from the last attempt
162+ assert .Equal (t , 400 , status )
163+ assert .Contains (t , err .Error (), "gateway POST failed" )
164+ assert .Equal (t , 3 , rt .Calls )
165+ })
166+
167+ t .Run ("error then success -> ok" , func (t * testing.T ) {
168+ body := `{"ok":true}`
169+ rt := & SeqRoundTripper {
170+ Seq : []RTResponse {
171+ {Err : errors .New ("dial tcp: i/o timeout" )},
172+ {Response : makeResp (200 , body )},
173+ },
174+ }
175+ mockedHTTP := & http.Client {Transport : rt }
176+ h .Gw = & HTTPClient {
177+ URL : "https://unit-test.gw" ,
178+ Client : mockedHTTP ,
179+ RetryAttempts : 5 ,
180+ RetryDelay : 0 ,
181+ }
182+
183+ respBytes , status , err := h .Gw .Post ([]byte (`{}` ))
184+ assert .NoError (t , err )
185+ assert .Equal (t , 200 , status )
186+ assert .Equal (t , body , string (respBytes ))
187+ assert .Equal (t , 2 , rt .Calls )
188+ })
189+
190+ // Optional: prove delays are honored if set
191+ t .Run ("honors small delay" , func (t * testing.T ) {
192+ rt := & SeqRoundTripper {
193+ Seq : []RTResponse {
194+ {Response : makeResp (503 , "nope" )},
195+ {Response : makeResp (200 , `{"ok":true}` )},
196+ },
197+ }
198+ mockedHTTP := & http.Client {Transport : rt }
199+ h .Gw = & HTTPClient {
200+ URL : "https://unit-test.gw" ,
201+ Client : mockedHTTP ,
202+ RetryAttempts : 2 ,
203+ RetryDelay : 5 * time .Millisecond ,
204+ }
205+
206+ _ , status , err := h .Gw .Post ([]byte (`{}` ))
207+ assert .NoError (t , err )
208+ assert .Equal (t , 200 , status )
209+ assert .Equal (t , 2 , rt .Calls )
69210 })
70211}
0 commit comments