Commit 86a7379

mo khan <mo@mokhan.ca>
2025-05-03 22:17:20
feat: add test package with http helpers
1 parent b691eda
Changed files (3)
pkg/serde/media.go
@@ -19,6 +19,10 @@ const (
 	Default MediaType = JSON
 )
 
+func (self MediaType) String() string {
+	return string(self)
+}
+
 func MediaTypeFor(value string) MediaType {
 	mediaTypes := sortedByQualityValues(strings.Split(value, ","))
 
pkg/test/http.go
@@ -0,0 +1,68 @@
+package test
+
+import (
+	"bytes"
+	"context"
+	"io"
+	"net/http"
+	"net/http/httptest"
+
+	xcontext "github.com/xlgmokha/x/pkg/context"
+	"github.com/xlgmokha/x/pkg/serde"
+	"github.com/xlgmokha/x/pkg/x"
+)
+
+type RequestOption x.Option[*http.Request]
+
+func Request(method, target string, options ...RequestOption) *http.Request {
+	request := httptest.NewRequest(method, target, nil)
+	for _, option := range options {
+		request = option(request)
+	}
+	return request
+}
+
+func RequestResponse(method, target string, options ...RequestOption) (*http.Request, *httptest.ResponseRecorder) {
+	return Request(method, target, options...), httptest.NewRecorder()
+}
+
+func WithAcceptHeader(value serde.MediaType) RequestOption {
+	return WithRequestHeader("Accept", string(value))
+}
+
+func WithRequestHeader(key, value string) RequestOption {
+	return func(r *http.Request) *http.Request {
+		r.Header.Set(key, value)
+		return r
+	}
+}
+
+func WithContentType[T any](item T, mediaType serde.MediaType) RequestOption {
+	body := bytes.NewBuffer(nil)
+	x.Check(serde.To[T](body, item, mediaType))
+	return WithRequestBody(io.NopCloser(body))
+}
+
+func WithRequestBody(body io.ReadCloser) RequestOption {
+	return func(r *http.Request) *http.Request {
+		r.Body = body
+		return r
+	}
+}
+
+func WithContext(ctx context.Context) RequestOption {
+	return func(r *http.Request) *http.Request {
+		return r.WithContext(ctx)
+	}
+}
+
+func WithContextKeyValue[T any](ctx context.Context, key xcontext.Key[T], item T) RequestOption {
+	return WithContext(key.With(ctx, item))
+}
+
+func WithCookie(cookie *http.Cookie) RequestOption {
+	return func(r *http.Request) *http.Request {
+		r.AddCookie(cookie)
+		return r
+	}
+}
pkg/test/http_test.go
@@ -0,0 +1,169 @@
+package test
+
+import (
+	"fmt"
+	"io"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"github.com/xlgmokha/x/pkg/context"
+	"github.com/xlgmokha/x/pkg/serde"
+)
+
+var exampleHeader RequestOption = RequestOption(func(r *http.Request) *http.Request {
+	r.Header.Add("X-Example", "example")
+	return r
+})
+
+var withHost RequestOption = RequestOption(func(r *http.Request) *http.Request {
+	r.Host = "example.com"
+	return r
+})
+
+func TestRequest(t *testing.T) {
+	t.Run("without options", func(t *testing.T) {
+		r := Request("GET", "/example")
+
+		require.NotNil(t, r)
+		assert.Equal(t, "GET", r.Method)
+		assert.Equal(t, "/example", r.URL.Path)
+		assert.Zero(t, r.Body)
+	})
+
+	t.Run("with an option", func(t *testing.T) {
+		r := Request("GET", "/example", exampleHeader)
+
+		require.NotNil(t, r)
+		assert.Equal(t, "GET", r.Method)
+		assert.Equal(t, "/example", r.URL.Path)
+		assert.Zero(t, r.Body)
+		assert.Equal(t, "example", r.Header.Get("X-Example"))
+	})
+
+	t.Run("with options", func(t *testing.T) {
+		r := Request("GET", "/example", exampleHeader, withHost)
+
+		require.NotNil(t, r)
+		assert.Equal(t, "GET", r.Method)
+		assert.Equal(t, "/example", r.URL.Path)
+		assert.Zero(t, r.Body)
+		assert.Equal(t, "example", r.Header.Get("X-Example"))
+		assert.Equal(t, "example.com", r.Host)
+	})
+}
+
+func TestRequestResponse(t *testing.T) {
+	t.Run("without options", func(t *testing.T) {
+		r, w := RequestResponse("GET", "/health")
+
+		require.NotNil(t, r)
+		assert.Equal(t, "GET", r.Method)
+		assert.Equal(t, "/health", r.URL.Path)
+
+		require.NotNil(t, w)
+	})
+
+	t.Run("with options", func(t *testing.T) {
+		r, w := RequestResponse("GET", "/example", exampleHeader, withHost)
+
+		require.NotNil(t, r)
+		assert.Equal(t, "GET", r.Method)
+		assert.Equal(t, "/example", r.URL.Path)
+		assert.Zero(t, r.Body)
+		assert.Equal(t, "example", r.Header.Get("X-Example"))
+		assert.Equal(t, "example.com", r.Host)
+
+		require.NotNil(t, w)
+	})
+}
+
+func TestWithAcceptHeader(t *testing.T) {
+	t.Run("applies the Accept header", func(t *testing.T) {
+		r := Request("GET", "/example", WithAcceptHeader(serde.JSON))
+
+		require.NotNil(t, r)
+		assert.Equal(t, serde.JSON.String(), r.Header.Get("Accept"))
+	})
+}
+
+func TestWithRequestHeader(t *testing.T) {
+	t.Run("applies a header to the request", func(t *testing.T) {
+		r := Request("GET", "/example", WithRequestHeader("Via", "gtwy"))
+
+		require.NotNil(t, r)
+		assert.Equal(t, "gtwy", r.Header.Get("Via"))
+	})
+}
+
+func TestWithContentType(t *testing.T) {
+	type example struct {
+		ID   int    `json: "id" jsonapi:"primary,examples"`
+		Name string `json: "name" jsonapi:"attr,name"`
+	}
+	item := &example{ID: 1, Name: "example"}
+
+	tt := []serde.MediaType{serde.JSON, serde.YAML}
+	for _, mediaType := range tt {
+		t.Run(fmt.Sprintf("generates a %v request body", mediaType), func(t *testing.T) {
+			r := Request("GET", "/example", WithContentType(item, mediaType))
+			require.NotNil(t, r)
+
+			result, err := serde.From[example](r.Body, mediaType)
+			require.NoError(t, err)
+			assert.Equal(t, 1, result.ID)
+			assert.Equal(t, "example", result.Name)
+		})
+	}
+}
+
+func TestWithRequestBody(t *testing.T) {
+	t.Run("applies the io to the request body", func(t *testing.T) {
+		body := io.NopCloser(strings.NewReader("example"))
+		r := Request("GET", "/example", WithRequestBody(body))
+
+		require.NotNil(t, r)
+
+		b, err := io.ReadAll(r.Body)
+		require.NoError(t, err)
+		assert.Equal(t, "example", string(b))
+	})
+}
+
+func TestWithContext(t *testing.T) {
+	t.Run("returns a request with a new context", func(t *testing.T) {
+		key := context.Key[string]("x")
+
+		ctx := key.With(t.Context(), "example")
+		r := Request("GET", "/example", WithContext(ctx))
+
+		require.NotNil(t, r)
+		assert.Equal(t, "example", key.From(r.Context()))
+	})
+}
+
+func TestWithContextKeyValue(t *testing.T) {
+	t.Run("returns a request with a new context", func(t *testing.T) {
+		key := context.Key[string]("x")
+
+		r := Request("GET", "/example", WithContextKeyValue(t.Context(), key, "example"))
+
+		require.NotNil(t, r)
+		assert.Equal(t, "example", key.From(r.Context()))
+	})
+}
+
+func TestWithCookie(t *testing.T) {
+	t.Run("adds a cookie to the request", func(t *testing.T) {
+		r := Request("GET", "/example", WithCookie(&http.Cookie{Name: "example", Value: "value"}))
+
+		require.NotNil(t, r)
+		result, err := r.Cookie("example")
+		require.NoError(t, err)
+
+		assert.Equal(t, "example", result.Name)
+		assert.Equal(t, "value", result.Value)
+	})
+}