Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tests: improve http server tests #102

Merged
merged 3 commits into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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-----`)
)
Loading