Skip to content

Commit

Permalink
tests: improve http server tests (#102)
Browse files Browse the repository at this point in the history
  • Loading branch information
nrwiersma authored Nov 9, 2023
1 parent ab88691 commit 65da52a
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 32 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
go-version: [ "1.20", "1.21" ]
runs-on: ubuntu-latest
env:
GOLANGCI_LINT_VERSION: v1.54.2
GOLANGCI_LINT_VERSION: v1.55.2

steps:
- name: Install Go
Expand Down
1 change: 1 addition & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ linters:
- gochecknoinits
- goerr113
- gomnd
- inamedparam
- ireturn
- nlreturn
- varnamelen
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ A collection of Go packages
Install with:

```shell
go get github.com/hamba/pkg
go get github.com/hamba/pkg/v2
```
14 changes: 13 additions & 1 deletion http/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ func WithH2C() SrvOptFunc {
}
}

var testHookServerServe func(net.Listener)

// Server is a convenience wrapper around the standard
// library HTTP server.
type Server struct {
Expand All @@ -63,7 +65,10 @@ type Server struct {
// NewServer returns a server with the base context ctx.
func NewServer(ctx context.Context, addr string, h http.Handler, opts ...SrvOptFunc) *Server {
srv := &http.Server{
BaseContext: func(_ net.Listener) context.Context {
BaseContext: func(ln net.Listener) context.Context {
if testHookServerServe != nil {
testHookServerServe(ln)
}
return ctx
},
Addr: addr,
Expand All @@ -86,6 +91,13 @@ func NewServer(ctx context.Context, addr string, h http.Handler, opts ...SrvOptF
// Serve starts the server in a non-blocking way.
func (s *Server) Serve(errFn func(error)) {
go func() {
if s.srv.TLSConfig != nil {
if err := s.srv.ListenAndServeTLS("", ""); err != nil && !errors.Is(err, http.ErrServerClosed) {
errFn(err)
}
return
}

if err := s.srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
errFn(err)
}
Expand Down
206 changes: 177 additions & 29 deletions http/server_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package http_test
package http

import (
"context"
Expand All @@ -11,7 +11,6 @@ import (
"time"

"github.com/hamba/logger/v2"
httpx "github.com/hamba/pkg/v2/http"
"github.com/hamba/pkg/v2/http/healthz"
"github.com/hamba/statter/v2"
"github.com/stretchr/testify/assert"
Expand All @@ -20,49 +19,114 @@ import (
)

func TestServer(t *testing.T) {
lnCh := make(chan net.Listener, 1)
setTestHookServerServe(func(ln net.Listener) {
lnCh <- ln
})
t.Cleanup(func() { setTestHookServerServe(nil) })

var handlerCalled bool
h := http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
handlerCalled = true
})

srv := httpx.NewServer(context.Background(), ":16543", h)
srv := NewServer(context.Background(), "localhost:0", h)
srv.Serve(func(err error) {
require.NoError(t, err)
})
t.Cleanup(func() {
_ = srv.Close()
})

res, err := http.DefaultClient.Get("http://localhost:16543/")
var ln net.Listener
select {
case <-time.After(30 * time.Second):
require.Fail(t, "Timed out waiting for server listener")
case ln = <-lnCh:
}

url := "http://" + ln.Addr().String() + "/"
statusCode, _ := requireDoRequest(t, url)

err := srv.Shutdown(time.Second)
require.NoError(t, err)

assert.Equal(t, statusCode, http.StatusOK)
assert.True(t, handlerCalled)
}

func TestServer_WithTLSConfig(t *testing.T) {
lnCh := make(chan net.Listener, 1)
setTestHookServerServe(func(ln net.Listener) {
lnCh <- ln
})
t.Cleanup(func() { setTestHookServerServe(nil) })

var handlerCalled bool
h := http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
handlerCalled = true
})

cert, err := tls.X509KeyPair(localhostCert, localhostKey)
require.NoError(t, err)
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
}

srv := NewServer(context.Background(), "localhost:0", h, WithTLSConfig(tlsConfig))
srv.Serve(func(err error) {
require.NoError(t, err)
})
t.Cleanup(func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
_ = srv.Close()
})

var ln net.Listener
select {
case <-time.After(30 * time.Second):
require.Fail(t, "Timed out waiting for server listener")
case ln = <-lnCh:
}

url := "https://" + ln.Addr().String() + "/"
statusCode, _ := requireDoRequest(t, url)

err = srv.Shutdown(time.Second)
require.NoError(t, err)

assert.Equal(t, res.StatusCode, http.StatusOK)
assert.Equal(t, statusCode, http.StatusOK)
assert.True(t, handlerCalled)
}

func TestServer_WithH2C(t *testing.T) {
lnCh := make(chan net.Listener, 1)
setTestHookServerServe(func(ln net.Listener) {
lnCh <- ln
})
t.Cleanup(func() { setTestHookServerServe(nil) })

var handlerCalled bool
h := http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) {
assert.True(t, req.ProtoAtLeast(2, 0))

handlerCalled = true
})

srv := httpx.NewServer(context.Background(), ":16543", h, httpx.WithH2C())
srv := NewServer(context.Background(), "localhost:0", h, WithH2C())
srv.Serve(func(err error) {
require.NoError(t, err)
})
t.Cleanup(func() {
_ = srv.Close()
})

var ln net.Listener
select {
case <-time.After(30 * time.Second):
require.Fail(t, "Timed out waiting for server listener")
case ln = <-lnCh:
}

c := &http.Client{
Transport: &http2.Transport{
AllowHTTP: true,
Expand All @@ -71,7 +135,7 @@ func TestServer_WithH2C(t *testing.T) {
},
},
}
res, err := c.Get("http://localhost:16543/")
res, err := c.Get("http://" + ln.Addr().String() + "/")
require.NoError(t, err)
t.Cleanup(func() {
_, _ = io.Copy(io.Discard, res.Body)
Expand All @@ -89,12 +153,18 @@ func TestHealthServer(t *testing.T) {
stats := statter.New(statter.DiscardReporter, time.Minute)
log := logger.New(io.Discard, logger.LogfmtFormat(), logger.Error)

lnCh := make(chan net.Listener, 1)
setTestHookServerServe(func(ln net.Listener) {
lnCh <- ln
})
t.Cleanup(func() { setTestHookServerServe(nil) })

check := healthz.NamedCheck("test", func(*http.Request) error {
return nil
})

srv := httpx.NewHealthServer(context.Background(), httpx.HealthServerConfig{
Addr: ":16543",
srv := NewHealthServer(context.Background(), HealthServerConfig{
Addr: "localhost:0",
Handler: http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}),
Stats: stats,
Log: log,
Expand All @@ -108,33 +178,43 @@ func TestHealthServer(t *testing.T) {
_ = srv.Close()
})

statusCode, body := requireDoRequest(t, "http://localhost:16543/readyz?verbose=1")
var ln net.Listener
select {
case <-time.After(30 * time.Second):
require.Fail(t, "Timed out waiting for server listener")
case ln = <-lnCh:
}

url := "http://" + ln.Addr().String() + "/readyz?verbose=1"
statusCode, body := requireDoRequest(t, url)
assert.Equal(t, statusCode, http.StatusOK)
want := `+ test ok
+ shutdown ok
readyz check passed`
assert.Equal(t, want, body)
assert.Equal(t, "+ test ok\n+ shutdown ok\nreadyz check passed", body)

statusCode, body = requireDoRequest(t, "http://localhost:16543/livez?verbose=1")
url = "http://" + ln.Addr().String() + "/livez?verbose=1"
statusCode, body = requireDoRequest(t, url)
assert.Equal(t, statusCode, http.StatusOK)
want = `+ test ok
livez check passed`
assert.Equal(t, want, body)
assert.Equal(t, "+ test ok\nlivez check passed", body)
}

func TestHealthServer_ShutdownCausesReadyzCheckToFail(t *testing.T) {
stats := statter.New(statter.DiscardReporter, time.Minute)
log := logger.New(io.Discard, logger.LogfmtFormat(), logger.Error)

lnCh := make(chan net.Listener, 1)
setTestHookServerServe(func(ln net.Listener) {
lnCh <- ln
})
t.Cleanup(func() { setTestHookServerServe(nil) })

calledCh := make(chan struct{})
check := healthz.NamedCheck("test", func(*http.Request) error {
close(calledCh)
time.Sleep(time.Millisecond)
return nil
})

srv := httpx.NewHealthServer(context.Background(), httpx.HealthServerConfig{
Addr: ":16543",
srv := NewHealthServer(context.Background(), HealthServerConfig{
Addr: "localhost:0",
Handler: http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}),
Stats: stats,
Log: log,
Expand All @@ -148,6 +228,13 @@ func TestHealthServer_ShutdownCausesReadyzCheckToFail(t *testing.T) {
_ = srv.Close()
})

var ln net.Listener
select {
case <-time.After(30 * time.Second):
require.Fail(t, "Timed out waiting for server listener")
case ln = <-lnCh:
}

var wg sync.WaitGroup
wg.Add(1)
go func() {
Expand All @@ -159,24 +246,29 @@ func TestHealthServer_ShutdownCausesReadyzCheckToFail(t *testing.T) {
require.NoError(t, err)
}()

statusCode, body := requireDoRequest(t, "http://localhost:16543/readyz?verbose=1")
url := "http://" + ln.Addr().String() + "/readyz?verbose=1"
statusCode, body := requireDoRequest(t, url)
assert.Equal(t, statusCode, http.StatusInternalServerError)
want := `+ test ok
- shutdown failed
readyz check failed
`
assert.Equal(t, want, body)
assert.Equal(t, "+ test ok\n- shutdown failed\nreadyz check failed\n", body)

wg.Wait()
}

func requireDoRequest(t *testing.T, path string) (int, string) {
t.Helper()

client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
}

req, err := http.NewRequest(http.MethodGet, path, nil)
require.NoError(t, err)

resp, err := http.DefaultClient.Do(req)
resp, err := client.Do(req)
require.NoError(t, err)
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
Expand All @@ -188,3 +280,59 @@ func requireDoRequest(t *testing.T, path string) (int, string) {

return resp.StatusCode, string(b)
}

func setTestHookServerServe(fn func(net.Listener)) {
testHookServerServe = fn
}

var (
localhostCert = []byte(`-----BEGIN CERTIFICATE-----
MIIDOTCCAiGgAwIBAgIQSRJrEpBGFc7tNb1fb5pKFzANBgkqhkiG9w0BAQsFADAS
MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw
MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEA6Gba5tHV1dAKouAaXO3/ebDUU4rvwCUg/CNaJ2PT5xLD4N1Vcb8r
bFSW2HXKq+MPfVdwIKR/1DczEoAGf/JWQTW7EgzlXrCd3rlajEX2D73faWJekD0U
aUgz5vtrTXZ90BQL7WvRICd7FlEZ6FPOcPlumiyNmzUqtwGhO+9ad1W5BqJaRI6P
YfouNkwR6Na4TzSj5BrqUfP0FwDizKSJ0XXmh8g8G9mtwxOSN3Ru1QFc61Xyeluk
POGKBV/q6RBNklTNe0gI8usUMlYyoC7ytppNMW7X2vodAelSu25jgx2anj9fDVZu
h7AXF5+4nJS4AAt0n1lNY7nGSsdZas8PbQIDAQABo4GIMIGFMA4GA1UdDwEB/wQE
AwICpDATBgNVHSUEDDAKBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud
DgQWBBStsdjh3/JCXXYlQryOrL4Sh7BW5TAuBgNVHREEJzAlggtleGFtcGxlLmNv
bYcEfwAAAYcQAAAAAAAAAAAAAAAAAAAAATANBgkqhkiG9w0BAQsFAAOCAQEAxWGI
5NhpF3nwwy/4yB4i/CwwSpLrWUa70NyhvprUBC50PxiXav1TeDzwzLx/o5HyNwsv
cxv3HdkLW59i/0SlJSrNnWdfZ19oTcS+6PtLoVyISgtyN6DpkKpdG1cOkW3Cy2P2
+tK/tKHRP1Y/Ra0RiDpOAmqn0gCOFGz8+lqDIor/T7MTpibL3IxqWfPrvfVRHL3B
grw/ZQTTIVjjh4JBSW3WyWgNo/ikC1lrVxzl4iPUGptxT36Cr7Zk2Bsg0XqwbOvK
5d+NTDREkSnUbie4GeutujmX3Dsx88UiV6UY/4lHJa6I5leHUNOHahRbpbWeOfs/
WkBKOclmOV2xlTVuPw==
-----END CERTIFICATE-----`)

localhostKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDoZtrm0dXV0Aqi
4Bpc7f95sNRTiu/AJSD8I1onY9PnEsPg3VVxvytsVJbYdcqr4w99V3AgpH/UNzMS
gAZ/8lZBNbsSDOVesJ3euVqMRfYPvd9pYl6QPRRpSDPm+2tNdn3QFAvta9EgJ3sW
URnoU85w+W6aLI2bNSq3AaE771p3VbkGolpEjo9h+i42TBHo1rhPNKPkGupR8/QX
AOLMpInRdeaHyDwb2a3DE5I3dG7VAVzrVfJ6W6Q84YoFX+rpEE2SVM17SAjy6xQy
VjKgLvK2mk0xbtfa+h0B6VK7bmODHZqeP18NVm6HsBcXn7iclLgAC3SfWU1jucZK
x1lqzw9tAgMBAAECggEABWzxS1Y2wckblnXY57Z+sl6YdmLV+gxj2r8Qib7g4ZIk
lIlWR1OJNfw7kU4eryib4fc6nOh6O4AWZyYqAK6tqNQSS/eVG0LQTLTTEldHyVJL
dvBe+MsUQOj4nTndZW+QvFzbcm2D8lY5n2nBSxU5ypVoKZ1EqQzytFcLZpTN7d89
EPj0qDyrV4NZlWAwL1AygCwnlwhMQjXEalVF1ylXwU3QzyZ/6MgvF6d3SSUlh+sq
XefuyigXw484cQQgbzopv6niMOmGP3of+yV4JQqUSb3IDmmT68XjGd2Dkxl4iPki
6ZwXf3CCi+c+i/zVEcufgZ3SLf8D99kUGE7v7fZ6AQKBgQD1ZX3RAla9hIhxCf+O
3D+I1j2LMrdjAh0ZKKqwMR4JnHX3mjQI6LwqIctPWTU8wYFECSh9klEclSdCa64s
uI/GNpcqPXejd0cAAdqHEEeG5sHMDt0oFSurL4lyud0GtZvwlzLuwEweuDtvT9cJ
Wfvl86uyO36IW8JdvUprYDctrQKBgQDycZ697qutBieZlGkHpnYWUAeImVA878sJ
w44NuXHvMxBPz+lbJGAg8Cn8fcxNAPqHIraK+kx3po8cZGQywKHUWsxi23ozHoxo
+bGqeQb9U661TnfdDspIXia+xilZt3mm5BPzOUuRqlh4Y9SOBpSWRmEhyw76w4ZP
OPxjWYAgwQKBgA/FehSYxeJgRjSdo+MWnK66tjHgDJE8bYpUZsP0JC4R9DL5oiaA
brd2fI6Y+SbyeNBallObt8LSgzdtnEAbjIH8uDJqyOmknNePRvAvR6mP4xyuR+Bv
m+Lgp0DMWTw5J9CKpydZDItc49T/mJ5tPhdFVd+am0NAQnmr1MCZ6nHxAoGABS3Y
LkaC9FdFUUqSU8+Chkd/YbOkuyiENdkvl6t2e52jo5DVc1T7mLiIrRQi4SI8N9bN
/3oJWCT+uaSLX2ouCtNFunblzWHBrhxnZzTeqVq4SLc8aESAnbslKL4i8/+vYZlN
s8xtiNcSvL+lMsOBORSXzpj/4Ot8WwTkn1qyGgECgYBKNTypzAHeLE6yVadFp3nQ
Ckq9yzvP/ib05rvgbvrne00YeOxqJ9gtTrzgh7koqJyX1L4NwdkEza4ilDWpucn0
xiUZS4SoaJq6ZvcBYS62Yr1t8n09iG47YL8ibgtmH3L+svaotvpVxVK+d7BLevA/
ZboOWVe3icTy64BT3OQhmg==
-----END RSA PRIVATE KEY-----`)
)

0 comments on commit 65da52a

Please sign in to comment.