Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
d11bed0
init websockets
mccutchen Nov 4, 2023
ba2d6e8
checkpoint
mccutchen Nov 5, 2023
2e613f5
working
mccutchen Nov 6, 2023
c03f3df
simpler
mccutchen Nov 6, 2023
9ac984e
more
mccutchen Nov 6, 2023
a971dfd
progress
mccutchen Nov 6, 2023
c67bd81
fixes
mccutchen Nov 7, 2023
41c6aaa
more
mccutchen Nov 7, 2023
bc3959a
properly close?
mccutchen Nov 7, 2023
666d8fb
close validation
mccutchen Nov 7, 2023
9e548c4
better close
mccutchen Nov 7, 2023
b3deb5a
rsv bits
mccutchen Nov 7, 2023
511d5bd
all passing!
mccutchen Nov 7, 2023
0da07da
blah
mccutchen Nov 7, 2023
32a1eae
wip integration tests
mccutchen Nov 8, 2023
9e4eaab
passing integration tests
mccutchen Nov 8, 2023
ad166d2
api tweaks to ensure handshake is completed
mccutchen Nov 10, 2023
b8ac49f
fewer methods
mccutchen Nov 12, 2023
f38f675
checkpoint: working, tests passing, etc
mccutchen Nov 8, 2023
54a524e
make autobahn tests optional
mccutchen Nov 14, 2023
5f34c68
better handling of RSV1-3
mccutchen Nov 14, 2023
6582b7a
more tests
mccutchen Nov 14, 2023
fd29784
basically all tests passing
mccutchen Nov 14, 2023
9668c2f
tweak API
mccutchen Nov 14, 2023
dbfb7ae
query params
mccutchen Nov 14, 2023
0624f39
simplfiy
mccutchen Nov 14, 2023
7b02423
more detailed reports
mccutchen Nov 15, 2023
3c8346a
shuffle
mccutchen Nov 15, 2023
17bdd8a
blah
mccutchen Nov 15, 2023
75bd586
optimize frame writing by skipping separate encode step
mccutchen Nov 15, 2023
d399da7
more optimization
mccutchen Nov 15, 2023
ec15692
tweaks
mccutchen Nov 15, 2023
7d3cc7c
simplify serveLoop
mccutchen Nov 15, 2023
55dad10
order
mccutchen Nov 15, 2023
7d191ac
minor tweak, one fewer error check to test
mccutchen Nov 15, 2023
94b83df
tweak
mccutchen Nov 15, 2023
f16afd7
lint
mccutchen Nov 15, 2023
3df1709
lint
mccutchen Nov 25, 2023
f17b0b7
fix integration tests under colima
mccutchen Nov 25, 2023
d6dd3ae
fixes
mccutchen Nov 25, 2023
9eec164
try to get this working on GitHub Actions
mccutchen Nov 25, 2023
79366e3
move
mccutchen Nov 25, 2023
cfc746a
fix: websocket conns do not require `Connection: upgrade` header (#161)
mccutchen Dec 13, 2023
018f556
fix: ensure websocket conns respect max duration (#156)
mccutchen Nov 30, 2023
3bde3a4
chore: fix tests after extraction from go-httpbin
mccutchen Nov 26, 2024
dd54bf8
chore: updates from template repo
mccutchen Nov 26, 2024
51a6f14
appease linter
mccutchen Nov 26, 2024
cf97be7
more appease linter
mccutchen Nov 26, 2024
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
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,19 @@ test:
# based on codecov.io's documentation:
# https://github.com/codecov/example-go/blob/b85638743b972bd0bd2af63421fe513c6f968930/README.md
testci:
go test $(TEST_ARGS) $(COVERAGE_ARGS) ./...
AUTOBAHN_TESTS=1 go test $(TEST_ARGS) $(COVERAGE_ARGS) ./...
.PHONY: testci

testcover: testci
go tool cover -html=$(COVERAGE_PATH)
.PHONY: testcover

# Run the autobahn fuzzingclient test suite
testautobahn:
AUTOBAHN_TESTS=1 AUTOBAHN_OPEN_REPORT=1 go test -v -run ^TestWebSocketServer$$ $(TEST_ARGS) ./...
.PHONY: autobahntests


lint:
test -z "$$(gofmt -d -s -e .)" || (echo "Error: gofmt failed"; gofmt -d -s -e . ; exit 1)
go vet ./...
Expand Down
27 changes: 8 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,11 @@
# go-pkg-template
# github.com/mccutchen/websocket

[![Documentation](https://pkg.go.dev/badge/github.com/mccutchen/go-pkg-template)](https://pkg.go.dev/github.com/mccutchen/go-pkg-template)
[![Build status](https://github.com/mccutchen/go-pkg-template/actions/workflows/test.yaml/badge.svg)](https://github.com/mccutchen/go-pkg-template/actions/workflows/test.yaml)
[![Code coverage](https://codecov.io/gh/mccutchen/go-pkg-template/branch/main/graph/badge.svg)](https://codecov.io/gh/mccutchen/go-pkg-template)
[![Go report card](http://goreportcard.com/badge/github.com/mccutchen/go-pkg-template)](https://goreportcard.com/report/github.com/mccutchen/go-pkg-template)
[![Documentation](https://pkg.go.dev/badge/github.com/mccutchen/websocket)](https://pkg.go.dev/github.com/mccutchen/websocket)
[![Build status](https://github.com/mccutchen/websocket/actions/workflows/test.yaml/badge.svg)](https://github.com/mccutchen/websocket/actions/workflows/test.yaml)
[![Code coverage](https://codecov.io/gh/mccutchen/websocket/branch/main/graph/badge.svg)](https://codecov.io/gh/mccutchen/websocket)
[![Go report card](http://goreportcard.com/badge/github.com/mccutchen/websocket)](https://goreportcard.com/report/github.com/mccutchen/websocket)

Template repo for Go packages/libraries

TODO:
- Replace go-pkg-template with new repo name
- Add high level description
- Add usage examples
- Set up branch protection rules
- Require linear history
- Require pull requests
- Require lint/test CI checks to pass
- Set up security scanning
- Maybe:
- Update go version in go.mod
- Update action versions in .github/workflows/*.yaml
A minimal Golang implementation of the websocket protocol, extracted from [mccutchen/go-httpbin][].

## Usage

Expand All @@ -29,3 +16,5 @@ TK
```bash
make test
```

[mccutchen/go-httpbin]: https://github.com/mccutchen/go-httpbin
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/mccutchen/go-pkg-template
module github.com/mccutchen/websocket

go 1.23.0
go 1.21.0
142 changes: 142 additions & 0 deletions internal/testing/assert/assert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Package assert implements common assertions used in go-httbin's unit tests.
package assert

import (
"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.T, 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)
}
}

// DeepEqual asserts that two values are deeply equal.
func DeepEqual[T any](t *testing.T, 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)
}
}

// NilError asserts that an error is nil.
func NilError(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatalf("expected nil error, got %s (%T)", err, err)
}
}

// Error asserts that an error is not nil.
func Error(t *testing.T, got, expected error) {
t.Helper()
if got != expected {
if got != nil && expected != nil {
if got.Error() == expected.Error() {
return
}
}
t.Fatalf("expected error %v, got %v", expected, got)
}
}

// StatusCode asserts that a response has a specific status code.
func StatusCode(t *testing.T, 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)
}
}
}

func isSafeContentType(ct string) bool {
return strings.HasPrefix(ct, "application/json") || strings.HasPrefix(ct, "text/plain") || strings.HasPrefix(ct, "application/octet-stream")
}

// Header asserts that a header key has a specific value in a response.
func Header(t *testing.T, 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)
}
}

// ContentType asserts that a response has a specific Content-Type header
// value.
func ContentType(t *testing.T, 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.T, s string, needle string, description string) {
t.Helper()
if !strings.Contains(s, needle) {
t.Fatalf("expected string %q in %s %q", needle, description, s)
}
}

// BodyContains asserts that a response body contains a specific substring.
func BodyContains(t *testing.T, 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.T, 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.T, 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.T, 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)
}
}

type number interface {
~int64 | ~float64
}

// RoughlyEqual asserts that a numeric value is within a certain tolerance.
func RoughlyEqual[T number](t *testing.T, 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)
}
}
48 changes: 48 additions & 0 deletions internal/testing/must/must.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Package must implements helper functions for testing to eliminate some error
// checking boilerplate.
package must

import (
"encoding/json"
"io"
"net/http"
"testing"
"time"
)

// DoReq makes an HTTP request and fails the test if there is an error.
func DoReq(t *testing.T, client *http.Client, req *http.Request) *http.Response {
t.Helper()
start := time.Now()
resp, err := client.Do(req)
if err != nil {
t.Fatalf("error making HTTP request: %s %s: %s", req.Method, req.URL, err)
}
t.Logf("HTTP request: %s %s => %s (%s)", req.Method, req.URL, resp.Status, time.Since(start))
return resp
}

// ReadAll reads all bytes from an io.Reader and fails the test if there is an
// error.
func ReadAll(t *testing.T, r io.Reader) string {
t.Helper()
body, err := io.ReadAll(r)
if err != nil {
t.Fatalf("error reading: %s", err)
}
if rc, ok := r.(io.ReadCloser); ok {
rc.Close()
}
return string(body)
}

// Unmarshal unmarshals JSON from an io.Reader into a value and fails the test
// if there is an error.
func Unmarshal[T any](t *testing.T, r io.Reader) T {
t.Helper()
var v T
if err := json.NewDecoder(r).Decode(&v); err != nil {
t.Fatal(err)
}
return v
}
Loading
Loading