go-api/sessions_test.go
Snider e00ef00db8 feat: add WithSessions server-side session middleware
Co-Authored-By: Virgil <virgil@lethean.io>
2026-02-20 23:44:46 +00:00

198 lines
5.1 KiB
Go

// SPDX-License-Identifier: EUPL-1.2
package api_test
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
api "forge.lthn.ai/core/go-api"
)
// ── Helpers ─────────────────────────────────────────────────────────────
// sessionTestGroup provides /sess/set and /sess/get endpoints for session tests.
type sessionTestGroup struct{}
func (s *sessionTestGroup) Name() string { return "sess" }
func (s *sessionTestGroup) BasePath() string { return "/sess" }
func (s *sessionTestGroup) RegisterRoutes(rg *gin.RouterGroup) {
rg.POST("/set", func(c *gin.Context) {
session := sessions.Default(c)
session.Set("key", "value")
session.Save()
c.JSON(http.StatusOK, api.OK("saved"))
})
rg.GET("/get", func(c *gin.Context) {
session := sessions.Default(c)
val := session.Get("key")
c.JSON(http.StatusOK, api.OK(val))
})
}
// ── WithSessions ────────────────────────────────────────────────────────
func TestWithSessions_Good_SetsSessionCookie(t *testing.T) {
gin.SetMode(gin.TestMode)
e, _ := api.New(api.WithSessions("session", []byte("test-secret-key!")))
e.Register(&sessionTestGroup{})
h := e.Handler()
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodPost, "/sess/set", nil)
h.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", w.Code)
}
cookies := w.Result().Cookies()
found := false
for _, c := range cookies {
if c.Name == "session" {
found = true
break
}
}
if !found {
t.Fatal("expected Set-Cookie header with name 'session'")
}
}
func TestWithSessions_Good_SessionPersistsAcrossRequests(t *testing.T) {
gin.SetMode(gin.TestMode)
e, _ := api.New(api.WithSessions("session", []byte("test-secret-key!")))
e.Register(&sessionTestGroup{})
h := e.Handler()
// First request: set session value.
w1 := httptest.NewRecorder()
req1, _ := http.NewRequest(http.MethodPost, "/sess/set", nil)
h.ServeHTTP(w1, req1)
if w1.Code != http.StatusOK {
t.Fatalf("set: expected 200, got %d", w1.Code)
}
// Extract the session cookie from the response.
var sessionCookie *http.Cookie
for _, c := range w1.Result().Cookies() {
if c.Name == "session" {
sessionCookie = c
break
}
}
if sessionCookie == nil {
t.Fatal("set: expected session cookie in response")
}
// Second request: get session value, sending the cookie back.
w2 := httptest.NewRecorder()
req2, _ := http.NewRequest(http.MethodGet, "/sess/get", nil)
req2.AddCookie(sessionCookie)
h.ServeHTTP(w2, req2)
if w2.Code != http.StatusOK {
t.Fatalf("get: expected 200, got %d", w2.Code)
}
var resp api.Response[any]
if err := json.Unmarshal(w2.Body.Bytes(), &resp); err != nil {
t.Fatalf("unmarshal error: %v", err)
}
data, ok := resp.Data.(string)
if !ok || data != "value" {
t.Fatalf("expected Data=%q, got %v", "value", resp.Data)
}
}
func TestWithSessions_Good_EmptySessionReturnsNil(t *testing.T) {
gin.SetMode(gin.TestMode)
e, _ := api.New(api.WithSessions("session", []byte("test-secret-key!")))
e.Register(&sessionTestGroup{})
h := e.Handler()
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/sess/get", nil)
h.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", w.Code)
}
var resp api.Response[any]
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("unmarshal error: %v", err)
}
if resp.Data != nil {
t.Fatalf("expected nil Data for empty session, got %v", resp.Data)
}
}
func TestWithSessions_Good_CombinesWithOtherMiddleware(t *testing.T) {
gin.SetMode(gin.TestMode)
e, _ := api.New(
api.WithSessions("session", []byte("test-secret-key!")),
api.WithRequestID(),
)
e.Register(&sessionTestGroup{})
h := e.Handler()
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodPost, "/sess/set", nil)
h.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", w.Code)
}
// Session cookie should be present.
found := false
for _, c := range w.Result().Cookies() {
if c.Name == "session" {
found = true
break
}
}
if !found {
t.Fatal("expected session cookie")
}
// Request ID should also be present.
rid := w.Header().Get("X-Request-ID")
if rid == "" {
t.Fatal("expected X-Request-ID header from WithRequestID")
}
}
func TestWithSessions_Ugly_DoubleSessionsDoesNotPanic(t *testing.T) {
gin.SetMode(gin.TestMode)
// Applying WithSessions twice should not panic.
e, err := api.New(
api.WithSessions("session", []byte("secret-one-here!")),
api.WithSessions("session", []byte("secret-two-here!")),
)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
e.Register(&sessionTestGroup{})
h := e.Handler()
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodPost, "/sess/set", nil)
h.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", w.Code)
}
}