Commit 3a0c247

mo khan <mo@mokhan.ca>
2025-05-03 22:51:59
feat: add a cookie package
1 parent bb0a77d
pkg/cookie/expire.go
@@ -0,0 +1,11 @@
+package cookie
+
+import (
+	"net/http"
+
+	"github.com/xlgmokha/x/pkg/x"
+)
+
+func Expire(w http.ResponseWriter, name string, options ...x.Option[*http.Cookie]) {
+	http.SetCookie(w, Reset(name, options...))
+}
pkg/cookie/expire_test.go
@@ -0,0 +1,22 @@
+package cookie
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestExpire(t *testing.T) {
+	w := httptest.NewRecorder()
+
+	Expire(w, "example", WithDomain("example.com"))
+
+	cookie, err := http.ParseSetCookie(w.Header().Get("Set-Cookie"))
+	require.NoError(t, err)
+	assert.Equal(t, "example", cookie.Name)
+	assert.Equal(t, "", cookie.Value)
+	assert.Equal(t, "example.com", cookie.Domain)
+}
pkg/cookie/new.go
@@ -0,0 +1,17 @@
+package cookie
+
+import (
+	"net/http"
+
+	"github.com/xlgmokha/x/pkg/x"
+)
+
+func New(name string, options ...x.Option[*http.Cookie]) *http.Cookie {
+	options = x.Prepend[x.Option[*http.Cookie]](
+		options,
+		With(func(c *http.Cookie) {
+			c.Name = name
+		}),
+	)
+	return x.New[*http.Cookie](options...)
+}
pkg/cookie/new_test.go
@@ -0,0 +1,16 @@
+package cookie
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestNew(t *testing.T) {
+	cookie := New("name", WithValue("value"))
+
+	require.NotNil(t, cookie)
+	assert.Equal(t, "name", cookie.Name)
+	assert.Equal(t, "value", cookie.Value)
+}
pkg/cookie/option.go
@@ -0,0 +1,63 @@
+package cookie
+
+import (
+	"net/http"
+	"time"
+
+	"github.com/xlgmokha/x/pkg/x"
+)
+
+func With(with func(*http.Cookie)) x.Option[*http.Cookie] {
+	return func(c *http.Cookie) *http.Cookie {
+		with(c)
+		return c
+	}
+}
+
+func WithValue(value string) x.Option[*http.Cookie] {
+	return With(func(c *http.Cookie) {
+		c.Value = value
+	})
+}
+
+func WithPath(value string) x.Option[*http.Cookie] {
+	return With(func(c *http.Cookie) {
+		c.Path = value
+	})
+}
+
+func WithHttpOnly(value bool) x.Option[*http.Cookie] {
+	return With(func(c *http.Cookie) {
+		c.HttpOnly = value
+	})
+}
+
+func WithSecure(value bool) x.Option[*http.Cookie] {
+	return With(func(c *http.Cookie) {
+		c.Secure = value
+	})
+}
+
+func WithDomain(value string) x.Option[*http.Cookie] {
+	return With(func(c *http.Cookie) {
+		c.Domain = value
+	})
+}
+
+func WithSameSite(value http.SameSite) x.Option[*http.Cookie] {
+	return With(func(c *http.Cookie) {
+		c.SameSite = value
+	})
+}
+
+func WithExpiration(expires time.Time) x.Option[*http.Cookie] {
+	return With(func(c *http.Cookie) {
+		c.Expires = expires
+		if expires.Before(time.Now()) {
+			c.MaxAge = -1
+		} else {
+			duration := time.Until(expires).Round(time.Second)
+			c.MaxAge = int(duration.Seconds())
+		}
+	})
+}
pkg/cookie/option_test.go
@@ -0,0 +1,53 @@
+package cookie
+
+import (
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestOption(t *testing.T) {
+	t.Run("WithPath", func(t *testing.T) {
+		assert.Equal(t, "/blah", New("name", WithPath("/blah")).Path)
+	})
+
+	t.Run("WithHttpOnly", func(t *testing.T) {
+		assert.False(t, New("x", WithHttpOnly(false)).HttpOnly)
+		assert.True(t, New("x", WithHttpOnly(true)).HttpOnly)
+	})
+
+	t.Run("WithSecure", func(t *testing.T) {
+		assert.False(t, New("x", WithSecure(false)).Secure)
+		assert.True(t, New("x", WithSecure(true)).Secure)
+	})
+
+	t.Run("WithDomain", func(t *testing.T) {
+		assert.Equal(t, "example.com", New("x", WithDomain("example.com")).Domain)
+	})
+
+	t.Run("WithSameSite", func(t *testing.T) {
+		assert.Equal(t, http.SameSiteLaxMode, New("x", WithSameSite(http.SameSiteLaxMode)).SameSite)
+		assert.Equal(t, http.SameSiteStrictMode, New("x", WithSameSite(http.SameSiteStrictMode)).SameSite)
+		assert.Equal(t, http.SameSiteNoneMode, New("x", WithSameSite(http.SameSiteNoneMode)).SameSite)
+	})
+
+	t.Run("WithExpiration", func(t *testing.T) {
+		now := time.Now()
+
+		t.Run("with future time", func(t *testing.T) {
+			expires := now.Add(1 * time.Second)
+			cookie := New("x", WithExpiration(expires))
+			assert.Equal(t, expires, cookie.Expires)
+			assert.Equal(t, 1, cookie.MaxAge)
+		})
+
+		t.Run("with past time", func(t *testing.T) {
+			expires := now.Add(-1 * time.Second)
+			cookie := New("x", WithExpiration(expires))
+			assert.Equal(t, expires, cookie.Expires)
+			assert.Equal(t, -1, cookie.MaxAge)
+		})
+	})
+}
pkg/cookie/reset.go
@@ -0,0 +1,18 @@
+package cookie
+
+import (
+	"net/http"
+	"time"
+
+	"github.com/xlgmokha/x/pkg/x"
+)
+
+func Reset(name string, options ...x.Option[*http.Cookie]) *http.Cookie {
+	options = append(
+		options,
+		WithValue(""),
+		WithExpiration(time.Unix(0, 0)),
+	)
+
+	return New(name, options...)
+}
pkg/cookie/reset_test.go
@@ -0,0 +1,28 @@
+package cookie
+
+import (
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestReset(t *testing.T) {
+	result := Reset(
+		"example",
+		WithSecure(true),
+		WithHttpOnly(true),
+		WithSameSite(http.SameSiteDefaultMode),
+		WithDomain("example.com"),
+	)
+
+	assert.Equal(t, -1, result.MaxAge)
+	assert.Equal(t, time.Unix(0, 0), result.Expires)
+	assert.Empty(t, result.Value)
+	assert.Equal(t, time.Unix(0, 0), result.Expires)
+	assert.True(t, result.HttpOnly)
+	assert.True(t, result.Secure)
+	assert.Equal(t, http.SameSiteDefaultMode, result.SameSite)
+	assert.Equal(t, "example.com", result.Domain)
+}