Skip to content
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 Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
COVERAGE_PATH ?= coverage.out
COVERAGE_ARGS ?= -covermode=atomic -coverprofile=$(COVERAGE_PATH)
TEST_ARGS ?= -race -count=1 -timeout=5s
CI_TEST_ARGS ?= -timeout=60s
CI_TEST_ARGS ?= -timeout=120s
AUTOBAHN_ARGS ?= -race -count=1 -timeout=120s
BENCH_COUNT ?= 10
BENCH_ARGS ?= -bench=. -benchmem -count=$(BENCH_COUNT) -run=^$$
Expand Down
213 changes: 86 additions & 127 deletions internal/testing/assert/assert.go
Original file line number Diff line number Diff line change
@@ -1,165 +1,124 @@
// Package assert implements common assertions used in go-httbin's unit tests.
// Package assert implements a set of basic test helpers.
//
// Lightly adapted from this blog post: https://antonz.org/do-not-testify/
package assert

import (
"bytes"
"errors"
"fmt"
"net/http"
"reflect"
"strings"
"testing"
"time"

"github.com/mccutchen/websocket/internal/testing/must"
)

// Equal asserts that two values are equal.
func Equal[T comparable](t testing.TB, got, want T, msg string, arg ...any) {
t.Helper()
if got != want {
if msg == "" {
msg = "expected values to match"
}
msg = fmt.Sprintf(msg, arg...)
t.Fatalf("%s:\nwant: %v\n got: %v", msg, want, got)
// Equal asserts that got is equal to want.
func Equal[T any](tb testing.TB, got T, want T, customMsg ...any) {
tb.Helper()
if areEqual(got, want) {
return
}
msg := formatMsg("expected values to be equal", customMsg)
tb.Errorf("%s:\ngot: %#v\nwant: %#v", msg, got, want)
}

// DeepEqual asserts that two values are deeply equal.
func DeepEqual[T any](t testing.TB, got, want T, msg string, arg ...any) {
t.Helper()
if !reflect.DeepEqual(got, want) {
if msg == "" {
msg = "expected values to match"
}
msg = fmt.Sprintf(msg, arg...)
t.Fatalf("%s:\nwant: %#v\n got: %#v", msg, want, got)
// True asserts that got is true.
func True(tb testing.TB, got bool, customMsg ...any) {
tb.Helper()
if !got {
tb.Error(formatMsg("expected value to be true", customMsg))
}
}

// NilError asserts that an error is nil.
func NilError(t testing.TB, err error) {
t.Helper()
if err != nil {
t.Fatalf("expected nil error, got %q (%T)", err, err)
// Error asserts that got matches want, which may be an error, an error type,
// an error string, or nil. If want is a string, it is considered a match if
// it is ia substring of got's string value.
func Error(tb testing.TB, got error, want any) {
tb.Helper()

if want != nil && got == nil {
tb.Errorf("errors do not match:\ngot: <nil>\nwant: %v", want)
return
}
}

// Error asserts that an error matches an expected error or any one of a list
// of expected errors.
func Error(t testing.TB, got, expected error, alternates ...error) {
t.Helper()
matched := false
wantAny := append([]error{expected}, alternates...)
for _, want := range wantAny {
if errorsMatch(t, got, want) {
matched = true
break
switch w := want.(type) {
case nil:
NilError(tb, got)
case error:
if !errors.Is(got, w) {
tb.Errorf("errors do not match:\ngot: %T(%v)\nwant: %T(%v)", got, got, w, w)
}
}
if !matched {
if len(wantAny) == 1 {
t.Fatalf("expected error %q, got %q (%T vs %T)", expected, got, expected, got)
} else {
t.Fatalf("expected one of %v, got %q (%T)", wantAny, got, got)
case string:
if !strings.Contains(got.Error(), w) {
tb.Errorf("error string does not match:\ngot: %q\nwant: %q", got.Error(), w)
}
case reflect.Type:
target := reflect.New(w).Interface()
if !errors.As(got, target) {
tb.Errorf("error type does not match:\ngot: %T\nwant: %s", got, w)
}
}
}

func errorsMatch(t testing.TB, got, expected error) bool {
t.Helper()
switch {
case got == expected:
return true
case errors.Is(got, expected):
return true
case got != nil && expected != nil:
return got.Error() == expected.Error()
default:
return false
tb.Errorf("unsupported want type: %T", want)
}
}

// StatusCode asserts that a response has a specific status code.
func StatusCode(t testing.TB, resp *http.Response, code int) {
t.Helper()
if resp.StatusCode != code {
t.Fatalf("expected status code %d, got %d", code, resp.StatusCode)
}
if resp.StatusCode >= 400 {
// Ensure our error responses are never served as HTML, so that we do
// not need to worry about XSS or other attacks in error responses.
if ct := resp.Header.Get("Content-Type"); !isSafeContentType(ct) {
t.Errorf("HTTP %s error served with dangerous content type: %s", resp.Status, ct)
}
// NilError asserts that got is nil.
func NilError(tb testing.TB, got error) {
tb.Helper()
if got != nil {
tb.Fatalf("expected nil error, got %q (%T)", got, got)
}
}

func isSafeContentType(ct string) bool {
return strings.HasPrefix(ct, "application/json") || strings.HasPrefix(ct, "text/plain") || strings.HasPrefix(ct, "application/octet-stream")
type equaler[T any] interface {
Equal(T) bool
}

// Header asserts that a header key has a specific value in a response.
func Header(t testing.TB, resp *http.Response, key, want string) {
t.Helper()
got := resp.Header.Get(key)
if want != got {
t.Fatalf("expected header %s=%#v, got %#v", key, want, got)
func areEqual[T any](a, b T) bool {
if isNil(a) && isNil(b) {
return true
}
}

// ContentType asserts that a response has a specific Content-Type header
// value.
func ContentType(t testing.TB, resp *http.Response, contentType string) {
t.Helper()
Header(t, resp, "Content-Type", contentType)
}

// Contains asserts that needle is found in the given string.
func Contains(t testing.TB, s string, needle string, description string) {
t.Helper()
if !strings.Contains(s, needle) {
t.Fatalf("expected string %q in %s %q", needle, description, s)
// special case types with an Equal method
if eq, ok := any(a).(equaler[T]); ok {
return eq.Equal(b)
}
}

// BodyContains asserts that a response body contains a specific substring.
func BodyContains(t testing.TB, resp *http.Response, needle string) {
t.Helper()
body := must.ReadAll(t, resp.Body)
Contains(t, body, needle, "body")
}

// BodyEquals asserts that a response body is equal to a specific string.
func BodyEquals(t testing.TB, resp *http.Response, want string) {
t.Helper()
got := must.ReadAll(t, resp.Body)
Equal(t, got, want, "incorrect response body")
}

// BodySize asserts that a response body is a specific size.
func BodySize(t testing.TB, resp *http.Response, want int) {
t.Helper()
got := must.ReadAll(t, resp.Body)
Equal(t, len(got), want, "incorrect response body size")
}

// DurationRange asserts that a duration is within a specific range.
func DurationRange(t testing.TB, got, minVal, maxVal time.Duration) {
t.Helper()
if got < minVal || got > maxVal {
t.Fatalf("expected duration between %s and %s, got %s", minVal, maxVal, got)
// special case byte slices
if aBytes, ok := any(a).([]byte); ok {
bBytes := any(b).([]byte)
return bytes.Equal(aBytes, bBytes)
}
return reflect.DeepEqual(a, b)
}

type number interface {
~int64 | ~float64
func isNil(v any) bool {
if v == nil {
return true
}
// A non-nil interface can still hold a nil value, so we check the
// underlying value.
rv := reflect.ValueOf(v)
switch rv.Kind() {
case reflect.Chan,
reflect.Func,
reflect.Interface,
reflect.Map,
reflect.Pointer,
reflect.Slice,
reflect.UnsafePointer:
return rv.IsNil()
default:
return false
}
}

// RoughlyEqual asserts that a numeric value is within a certain tolerance.
func RoughlyEqual[T number](t testing.TB, got, want T, epsilon T) {
t.Helper()
if got < want-epsilon || got > want+epsilon {
t.Fatalf("expected value between %v and %v, got %v", want-epsilon, want+epsilon, got)
func formatMsg(defaultMsg string, customMsg []any) string {
msg := defaultMsg
if len(customMsg) > 0 {
tmpl, ok := customMsg[0].(string)
if !ok {
tmpl = fmt.Sprintf("%v", customMsg[0])
}
msg = fmt.Sprintf(tmpl, customMsg[1:]...)
}
return msg
}
50 changes: 0 additions & 50 deletions internal/testing/must/must.go

This file was deleted.

15 changes: 7 additions & 8 deletions proto_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package websocket_test

import (
"bytes"
"errors"
"fmt"
"testing"

Expand All @@ -25,7 +24,7 @@ func TestFrameRoundTrip(t *testing.T) {
assert.NilError(t, err)

// ensure client and server frame match
assert.DeepEqual(t, serverFrame, clientFrame, "server and client frame mismatch")
assert.Equal(t, serverFrame, clientFrame, "server and client frame mismatch")
}

func TestMaxFrameSize(t *testing.T) {
Expand Down Expand Up @@ -148,31 +147,31 @@ func TestExampleFramesFromRFC(t *testing.T) {
t.Parallel()
buf := bytes.NewReader(tc.rawBytes)
got := mustReadFrame(t, buf, len(tc.rawBytes))
assert.DeepEqual(t, got, tc.wantFrame, "frames do not match")
assert.Equal(t, got, tc.wantFrame, "frames do not match")
})
}
}

func TestIncompleteFrames(t *testing.T) {
testCases := map[string]struct {
rawBytes []byte
wantErr error
wantErr string
}{
"2-byte extended payload can't be read": {
rawBytes: []byte{0x82, 0x7E},
wantErr: errors.New("error reading 2-byte extended payload length: EOF"),
wantErr: "error reading 2-byte extended payload length: EOF",
},
"8-byte extended payload can't be read": {
rawBytes: []byte{0x82, 0x7F},
wantErr: errors.New("error reading 8-byte extended payload length: EOF"),
wantErr: "error reading 8-byte extended payload length: EOF",
},
"mask can't be read": {
rawBytes: []byte{0x81, 0x85},
wantErr: errors.New("error reading mask key: EOF"),
wantErr: "error reading mask key: EOF",
},
"payload can't be read": {
rawBytes: []byte{0x81, 0x05},
wantErr: errors.New("error reading 5 byte payload: EOF"),
wantErr: "error reading 5 byte payload: EOF",
},
}

Expand Down
12 changes: 6 additions & 6 deletions websocket_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@ func TestDefaults(t *testing.T) {
assert.Equal(t, ws.mode, ServerMode, "incorrect mode value")
assert.Equal(t, ws.hooks.OnCloseHandshakeStart != nil, true, "OnCloseHandshakeStart hook is nil")
assert.Equal(t, ws.hooks.OnCloseHandshakeDone != nil, true, "OnCloseHandshakeDone hook is nil")
assert.Equal(t, ws.hooks.OnReadError != nil, true, "OnReadError hook is nil")
assert.Equal(t, ws.hooks.OnReadFrame != nil, true, "OnReadFrame hook is nil")
assert.Equal(t, ws.hooks.OnReadMessage != nil, true, "OnReadMessage hook is nil")
assert.Equal(t, ws.hooks.OnWriteError != nil, true, "OnWriteError hook is nil")
assert.Equal(t, ws.hooks.OnWriteFrame != nil, true, "OnWriteFrame hook is nil")
assert.Equal(t, ws.hooks.OnWriteMessage != nil, true, "OnWriteMessage hook is nil")
assert.True(t, ws.hooks.OnReadError != nil, "OnReadError hook is nil")
assert.True(t, ws.hooks.OnReadFrame != nil, "OnReadFrame hook is nil")
assert.True(t, ws.hooks.OnReadMessage != nil, "OnReadMessage hook is nil")
assert.True(t, ws.hooks.OnWriteError != nil, "OnWriteError hook is nil")
assert.True(t, ws.hooks.OnWriteFrame != nil, "OnWriteFrame hook is nil")
assert.True(t, ws.hooks.OnWriteMessage != nil, "OnWriteMessage hook is nil")

t.Run("CloseTimeout defaults to ReadTimeout if set", func(t *testing.T) {
var (
Expand Down
Loading
Loading