2017-04-18 09:22:06 +03:00
package middlewares
import (
"net/http"
"net/http/httptest"
2018-08-29 12:58:03 +03:00
"strings"
2017-04-18 09:22:06 +03:00
"testing"
2018-06-19 14:56:04 +03:00
"github.com/containous/traefik/testhelpers"
2018-08-29 12:58:03 +03:00
"github.com/gorilla/websocket"
2018-08-06 21:00:03 +03:00
"github.com/stretchr/testify/assert"
2018-08-29 12:58:03 +03:00
"github.com/stretchr/testify/require"
2018-06-19 14:56:04 +03:00
"github.com/vulcand/oxy/forward"
"github.com/vulcand/oxy/roundrobin"
2017-04-18 09:22:06 +03:00
)
func TestRetry ( t * testing . T ) {
testCases := [ ] struct {
2018-08-29 12:58:03 +03:00
desc string
maxRequestAttempts int
wantRetryAttempts int
wantResponseStatus int
amountFaultyEndpoints int
2017-04-18 09:22:06 +03:00
} {
{
2018-06-19 14:56:04 +03:00
desc : "no retry on success" ,
maxRequestAttempts : 1 ,
wantRetryAttempts : 0 ,
wantResponseStatus : http . StatusOK ,
amountFaultyEndpoints : 0 ,
} ,
{
desc : "no retry when max request attempts is one" ,
maxRequestAttempts : 1 ,
wantRetryAttempts : 0 ,
wantResponseStatus : http . StatusInternalServerError ,
amountFaultyEndpoints : 1 ,
} ,
{
desc : "one retry when one server is faulty" ,
maxRequestAttempts : 2 ,
wantRetryAttempts : 1 ,
wantResponseStatus : http . StatusOK ,
amountFaultyEndpoints : 1 ,
} ,
{
desc : "two retries when two servers are faulty" ,
maxRequestAttempts : 3 ,
wantRetryAttempts : 2 ,
wantResponseStatus : http . StatusOK ,
amountFaultyEndpoints : 2 ,
} ,
{
desc : "max attempts exhausted delivers the 5xx response" ,
maxRequestAttempts : 3 ,
wantRetryAttempts : 2 ,
wantResponseStatus : http . StatusInternalServerError ,
amountFaultyEndpoints : 3 ,
2017-04-18 09:22:06 +03:00
} ,
}
2018-06-19 14:56:04 +03:00
backendServer := httptest . NewServer ( http . HandlerFunc ( func ( rw http . ResponseWriter , req * http . Request ) {
rw . WriteHeader ( http . StatusOK )
rw . Write ( [ ] byte ( "OK" ) )
} ) )
forwarder , err := forward . New ( )
if err != nil {
t . Fatalf ( "Error creating forwarder: %s" , err )
}
2018-08-29 12:58:03 +03:00
for _ , test := range testCases {
test := test
2017-04-18 09:22:06 +03:00
2018-08-29 12:58:03 +03:00
t . Run ( test . desc , func ( t * testing . T ) {
2017-04-18 09:22:06 +03:00
t . Parallel ( )
2018-06-19 14:56:04 +03:00
loadBalancer , err := roundrobin . New ( forwarder )
if err != nil {
t . Fatalf ( "Error creating load balancer: %s" , err )
}
basePort := 33444
2018-08-29 12:58:03 +03:00
for i := 0 ; i < test . amountFaultyEndpoints ; i ++ {
2018-06-19 14:56:04 +03:00
// 192.0.2.0 is a non-routable IP for testing purposes.
// See: https://stackoverflow.com/questions/528538/non-routable-ip-address/18436928#18436928
// We only use the port specification here because the URL is used as identifier
// in the load balancer and using the exact same URL would not add a new server.
2018-08-06 21:00:03 +03:00
err = loadBalancer . UpsertServer ( testhelpers . MustParseURL ( "http://192.0.2.0:" + string ( basePort + i ) ) )
assert . NoError ( t , err )
2018-06-19 14:56:04 +03:00
}
// add the functioning server to the end of the load balancer list
2018-08-06 21:00:03 +03:00
err = loadBalancer . UpsertServer ( testhelpers . MustParseURL ( backendServer . URL ) )
assert . NoError ( t , err )
2018-06-19 14:56:04 +03:00
retryListener := & countingRetryListener { }
2018-08-29 12:58:03 +03:00
retry := NewRetry ( test . maxRequestAttempts , loadBalancer , retryListener )
2017-04-18 09:22:06 +03:00
recorder := httptest . NewRecorder ( )
2018-06-19 14:56:04 +03:00
req := httptest . NewRequest ( http . MethodGet , "http://localhost:3000/ok" , nil )
retry . ServeHTTP ( recorder , req )
2017-04-18 09:22:06 +03:00
2018-08-29 12:58:03 +03:00
assert . Equal ( t , test . wantResponseStatus , recorder . Code )
assert . Equal ( t , test . wantRetryAttempts , retryListener . timesCalled )
} )
}
}
func TestRetryWebsocket ( t * testing . T ) {
testCases := [ ] struct {
desc string
maxRequestAttempts int
expectedRetryAttempts int
expectedResponseStatus int
expectedError bool
amountFaultyEndpoints int
} {
{
desc : "Switching ok after 2 retries" ,
maxRequestAttempts : 3 ,
expectedRetryAttempts : 2 ,
amountFaultyEndpoints : 2 ,
expectedResponseStatus : http . StatusSwitchingProtocols ,
} ,
{
desc : "Switching failed" ,
maxRequestAttempts : 2 ,
expectedRetryAttempts : 1 ,
amountFaultyEndpoints : 2 ,
expectedResponseStatus : http . StatusBadGateway ,
expectedError : true ,
} ,
}
forwarder , err := forward . New ( )
if err != nil {
t . Fatalf ( "Error creating forwarder: %s" , err )
}
backendServer := httptest . NewServer ( http . HandlerFunc ( func ( rw http . ResponseWriter , req * http . Request ) {
upgrader := websocket . Upgrader { }
upgrader . Upgrade ( rw , req , nil )
} ) )
for _ , test := range testCases {
test := test
t . Run ( test . desc , func ( t * testing . T ) {
t . Parallel ( )
loadBalancer , err := roundrobin . New ( forwarder )
if err != nil {
t . Fatalf ( "Error creating load balancer: %s" , err )
2017-04-18 09:22:06 +03:00
}
2018-08-29 12:58:03 +03:00
basePort := 33444
for i := 0 ; i < test . amountFaultyEndpoints ; i ++ {
// 192.0.2.0 is a non-routable IP for testing purposes.
// See: https://stackoverflow.com/questions/528538/non-routable-ip-address/18436928#18436928
// We only use the port specification here because the URL is used as identifier
// in the load balancer and using the exact same URL would not add a new server.
loadBalancer . UpsertServer ( testhelpers . MustParseURL ( "http://192.0.2.0:" + string ( basePort + i ) ) )
2017-04-18 09:22:06 +03:00
}
2018-08-29 12:58:03 +03:00
// add the functioning server to the end of the load balancer list
loadBalancer . UpsertServer ( testhelpers . MustParseURL ( backendServer . URL ) )
retryListener := & countingRetryListener { }
retry := NewRetry ( test . maxRequestAttempts , loadBalancer , retryListener )
retryServer := httptest . NewServer ( retry )
url := strings . Replace ( retryServer . URL , "http" , "ws" , 1 )
_ , response , err := websocket . DefaultDialer . Dial ( url , nil )
if ! test . expectedError {
require . NoError ( t , err )
2017-04-18 09:22:06 +03:00
}
2018-08-29 12:58:03 +03:00
assert . Equal ( t , test . expectedResponseStatus , response . StatusCode )
assert . Equal ( t , test . expectedRetryAttempts , retryListener . timesCalled )
2017-04-18 09:22:06 +03:00
} )
}
}
2018-06-19 14:56:04 +03:00
func TestRetryEmptyServerList ( t * testing . T ) {
forwarder , err := forward . New ( )
if err != nil {
t . Fatalf ( "Error creating forwarder: %s" , err )
2017-05-03 11:20:33 +03:00
}
2018-06-19 14:56:04 +03:00
loadBalancer , err := roundrobin . New ( forwarder )
if err != nil {
t . Fatalf ( "Error creating load balancer: %s" , err )
2017-05-03 11:20:33 +03:00
}
2018-06-19 14:56:04 +03:00
// The EmptyBackendHandler middleware ensures that there is a 503
// response status set when there is no backend server in the pool.
next := NewEmptyBackendHandler ( loadBalancer )
retryListener := & countingRetryListener { }
retry := NewRetry ( 3 , next , retryListener )
recorder := httptest . NewRecorder ( )
req := httptest . NewRequest ( http . MethodGet , "http://localhost:3000/ok" , nil )
retry . ServeHTTP ( recorder , req )
const wantResponseStatus = http . StatusServiceUnavailable
if wantResponseStatus != recorder . Code {
t . Errorf ( "got status code %d, want %d" , recorder . Code , wantResponseStatus )
}
const wantRetryAttempts = 0
if wantRetryAttempts != retryListener . timesCalled {
t . Errorf ( "retry listener called %d time(s), want %d time(s)" , retryListener . timesCalled , wantRetryAttempts )
2017-05-03 11:20:33 +03:00
}
}
2017-08-28 13:50:02 +03:00
func TestRetryListeners ( t * testing . T ) {
req := httptest . NewRequest ( http . MethodGet , "/" , nil )
retryListeners := RetryListeners { & countingRetryListener { } , & countingRetryListener { } }
retryListeners . Retried ( req , 1 )
retryListeners . Retried ( req , 1 )
for _ , retryListener := range retryListeners {
listener := retryListener . ( * countingRetryListener )
if listener . timesCalled != 2 {
2018-06-19 14:56:04 +03:00
t . Errorf ( "retry listener was called %d time(s), want %d time(s)" , listener . timesCalled , 2 )
2017-08-28 13:50:02 +03:00
}
}
}
2017-04-18 09:22:06 +03:00
// countingRetryListener is a RetryListener implementation to count the times the Retried fn is called.
type countingRetryListener struct {
timesCalled int
}
2017-08-28 13:50:02 +03:00
func ( l * countingRetryListener ) Retried ( req * http . Request , attempt int ) {
2017-04-18 09:22:06 +03:00
l . timesCalled ++
}
2018-01-02 18:02:03 +03:00
func TestRetryWithFlush ( t * testing . T ) {
next := http . HandlerFunc ( func ( rw http . ResponseWriter , req * http . Request ) {
rw . WriteHeader ( 200 )
rw . Write ( [ ] byte ( "FULL " ) )
rw . ( http . Flusher ) . Flush ( )
rw . Write ( [ ] byte ( "DATA" ) )
} )
retry := NewRetry ( 1 , next , & countingRetryListener { } )
responseRecorder := httptest . NewRecorder ( )
retry . ServeHTTP ( responseRecorder , & http . Request { } )
if responseRecorder . Body . String ( ) != "FULL DATA" {
t . Errorf ( "Wrong body %q want %q" , responseRecorder . Body . String ( ) , "FULL DATA" )
}
}