pkg/network/prober/prober_test.go

382 lines
10 KiB
Go

/*
Copyright 2019 The Knative Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package prober
import (
"context"
"errors"
"net"
"net/http"
"net/http/httptest"
"testing"
"time"
"k8s.io/apimachinery/pkg/util/wait"
"knative.dev/pkg/network"
)
const (
systemName = "test-server"
unexpectedProbeMessage = "unexpected probe header value: whatever"
probeInterval = 10 * time.Millisecond
probeTimeout = 200 * time.Millisecond
)
func probeServeFunc(w http.ResponseWriter, r *http.Request) {
s := r.Header.Get(network.ProbeHeaderName)
switch s {
case "":
// No header.
w.WriteHeader(http.StatusNotFound)
case systemName:
// Expected header value.
w.Write([]byte(systemName))
default:
// Unexpected header value.
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(unexpectedProbeMessage))
}
}
func TestDoServing(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(probeServeFunc))
defer ts.Close()
tests := []struct {
name string
headerValue string
want bool
expErr bool
}{{
name: "ok",
headerValue: systemName,
want: true,
expErr: false,
}, {
name: "wrong system",
headerValue: "bells-and-whistles",
want: false,
expErr: true,
}, {
name: "no header",
headerValue: "",
want: false,
expErr: true,
}}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := Do(context.Background(), network.NewProberTransport(), ts.URL, WithHeader(network.ProbeHeaderName, test.headerValue), ExpectsBody(systemName), ExpectsStatusCodes([]int{http.StatusOK}))
if want := test.want; got != want {
t.Errorf("Got = %v, want: %v", got, want)
}
if err != nil && !test.expErr {
t.Errorf("Do() = %v, no error expected", err)
}
if err == nil && test.expErr {
t.Errorf("Do() = nil, expected an error")
}
})
}
}
func TestBlackHole(t *testing.T) {
transport := &http.Transport{
Dial: (&net.Dialer{
Timeout: 10 * time.Millisecond,
}).Dial,
}
got, err := Do(context.Background(), transport, "http://gone.fishing.svc.custer.local:8080", ExpectsStatusCodes([]int{http.StatusOK}))
if want := false; got != want {
t.Errorf("Got = %v, want: %v", got, want)
}
if err == nil {
t.Error("Do did not return an error")
}
}
func TestBadURL(t *testing.T) {
_, err := Do(context.Background(), network.NewProberTransport(), ":foo", ExpectsStatusCodes([]int{http.StatusOK}))
if err == nil {
t.Error("Do did not return an error")
}
t.Log("For the curious the error was:", err)
}
func TestDoAsync(t *testing.T) {
// This replicates the TestDo.
ts := httptest.NewServer(http.HandlerFunc(probeServeFunc))
defer ts.Close()
wch := make(chan interface{})
defer close(wch)
tests := []struct {
name string
headerValue string
cb Done
}{{
name: "ok",
headerValue: systemName,
cb: func(arg interface{}, ret bool, err error) {
defer func() {
wch <- 42
}()
if got, want := arg.(string), "ok"; got != want {
t.Errorf("arg = %s, want: %s", got, want)
}
if !ret {
t.Error("result was false")
}
},
}, {
name: "wrong system",
headerValue: "bells-and-whistles",
cb: func(arg interface{}, ret bool, err error) {
defer func() {
wch <- 1984
}()
if ret {
t.Error("result was true")
}
},
}, {
name: "no header",
headerValue: "",
cb: func(arg interface{}, ret bool, err error) {
defer func() {
wch <- 2006
}()
if ret {
t.Error("result was true")
}
},
}}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
m := New(test.cb, network.NewProberTransport())
m.Offer(context.Background(), ts.URL, test.name, probeInterval, probeTimeout, WithHeader(network.ProbeHeaderName, test.headerValue), ExpectsBody(test.headerValue), ExpectsStatusCodes([]int{http.StatusOK}))
<-wch
})
}
}
type thirdTimesTheCharmProber struct {
calls int
}
func (t *thirdTimesTheCharmProber) ServeHTTP(w http.ResponseWriter, r *http.Request) {
t.calls++
if t.calls < 3 {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(unexpectedProbeMessage))
return
}
w.Write([]byte(systemName))
}
func TestDoAsyncRepeat(t *testing.T) {
c := &thirdTimesTheCharmProber{}
ts := httptest.NewServer(c)
defer ts.Close()
wch := make(chan interface{})
defer close(wch)
cb := func(arg interface{}, done bool, err error) {
if !done {
t.Error("done was false")
}
if err != nil {
t.Error("Unexpected error =", err)
}
wch <- arg
}
m := New(cb, network.NewProberTransport())
m.Offer(context.Background(), ts.URL, 42, probeInterval, probeTimeout, WithHeader(network.ProbeHeaderName, systemName), ExpectsBody(systemName), ExpectsStatusCodes([]int{http.StatusOK}))
<-wch
if got, want := c.calls, 3; got != want {
t.Errorf("Probe invocation count = %d, want: %d", got, want)
}
}
func TestDoAsyncTimeout(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))
defer ts.Close()
wch := make(chan interface{})
defer close(wch)
cb := func(arg interface{}, done bool, err error) {
if done {
t.Errorf("done was true")
}
if !errors.Is(err, wait.ErrWaitTimeout) {
t.Error("Unexpected error =", err)
}
wch <- arg
}
m := New(cb, network.NewProberTransport())
m.Offer(context.Background(), ts.URL, 2009, probeInterval, probeTimeout, ExpectsStatusCodes([]int{http.StatusOK}))
<-wch
}
func TestAsyncMultiple(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(probeServeFunc))
defer ts.Close()
wch := make(chan interface{})
defer close(wch)
cb := func(arg interface{}, done bool, err error) {
<-wch
wch <- 2006
}
m := New(cb, network.NewProberTransport())
if !m.Offer(context.Background(), ts.URL, 1984, probeInterval, probeTimeout, ExpectsStatusCodes([]int{http.StatusOK})) {
t.Error("First call to offer returned false")
}
if m.Offer(context.Background(), ts.URL, 1982, probeInterval, probeTimeout, ExpectsStatusCodes([]int{http.StatusOK})) {
t.Error("Second call to offer returned true")
}
if got, want := m.len(), 1; got != want {
t.Errorf("Number of queued items = %d, want: %d", got, want)
}
// Make sure we terminate the first probe.
wch <- 2009
<-wch
wait.PollImmediate(probeInterval, probeTimeout, func() (bool, error) {
return m.len() == 0, nil
})
if got, want := m.len(), 0; got != want {
t.Errorf("Number of queued items = %d, want: %d", got, want)
}
}
func TestWithPathOption(t *testing.T) {
const path = "/correct/probe/path/"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Logf("Got: %s, Want: %s\n", r.URL.Path, path)
if r.URL.Path != path {
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
tests := []struct {
name string
options []interface{}
}{{
name: "no path",
options: []interface{}{ExpectsStatusCodes([]int{http.StatusNotFound})},
}, {
name: "expected path",
options: []interface{}{WithPath(path), ExpectsStatusCodes([]int{http.StatusOK})},
}, {
name: "wrong path",
options: []interface{}{WithPath("/wrong/path"), ExpectsStatusCodes([]int{http.StatusNotFound})},
}}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if ok, _ := Do(context.Background(), network.AutoTransport, ts.URL, test.options...); !ok {
t.Error("Unexpected probe failure")
}
})
}
}
func TestWithHostOption(t *testing.T) {
host := "foobar.com"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Logf("Want: %s, Got: %s\n", host, r.Host)
if r.Host != host {
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
tests := []struct {
name string
options []interface{}
}{{
name: "no hosts",
options: []interface{}{ExpectsStatusCodes([]int{http.StatusNotFound})},
}, {
name: "expected host",
options: []interface{}{WithHost(host), ExpectsStatusCodes([]int{http.StatusOK})},
}, {
name: "wrong host",
options: []interface{}{WithHost("nope.com"), ExpectsStatusCodes([]int{http.StatusNotFound})},
}}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if ok, _ := Do(context.Background(), network.AutoTransport, ts.URL, test.options...); !ok {
t.Error("Unexpected probe result")
}
})
}
}
func TestExpectsHeaderOption(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Foo", "Bar")
}))
defer ts.Close()
tests := []struct {
name string
options []interface{}
success bool
expErr bool
}{{
name: "header is present",
options: []interface{}{ExpectsHeader("Foo", "Bar"), ExpectsStatusCodes([]int{http.StatusOK})},
success: true,
}, {
name: "header is absent",
options: []interface{}{ExpectsHeader("Baz", "Nope"), ExpectsStatusCodes([]int{http.StatusOK})},
success: false,
expErr: true,
}, {
name: "header value doesn't match",
options: []interface{}{ExpectsHeader("Foo", "Baz"), ExpectsStatusCodes([]int{http.StatusOK})},
success: false,
expErr: true,
}}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ok, err := Do(context.Background(), network.AutoTransport, ts.URL, test.options...)
if ok != test.success {
t.Errorf("unexpected probe result: want: %v, got: %v", test.success, ok)
}
if err != nil && !test.expErr {
t.Errorf("Do() = %v, no error expected", err)
}
if err == nil && test.expErr {
t.Errorf("Do() = nil, expected an error")
}
})
}
}
func (m *Manager) len() int {
m.mu.Lock()
defer m.mu.Unlock()
return m.keys.Len()
}