opnsense-portal/internal/web/session_test.go
Carsten Abele bb2bc223b9 feat(web): Sessions, Rate-Limiting, Security-Header, Templates und eingebettete Assets
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBHF4R9EAejDJUMdwr6C68
2026-08-14 09:20:45 +02:00

130 lines
3.6 KiB
Go

package web
import (
"testing"
"time"
"git.ravensburg.dev/cabele/opnsense-portal/internal/auth"
)
func testIdentity() *auth.Identity {
return &auth.Identity{Username: "mmueller", Groups: []string{"VPN-Users"}}
}
func TestCreateAndGetSession(t *testing.T) {
st := NewSessionStore(10*time.Minute, nil)
sess, err := st.Create(testIdentity())
if err != nil {
t.Fatalf("Create: %v", err)
}
if len(sess.Token) < 32 {
t.Errorf("Token %q ist zu kurz für ausreichende Entropie", sess.Token)
}
if sess.CSRF == "" || sess.CSRF == sess.Token {
t.Error("CSRF-Token muss existieren und vom Session-Token verschieden sein")
}
got, ok := st.Get(sess.Token)
if !ok || got.Identity.Username != "mmueller" {
t.Fatalf("Get = %+v, ok=%v", got, ok)
}
}
func TestTokensAreUnique(t *testing.T) {
st := NewSessionStore(time.Minute, nil)
seen := map[string]bool{}
for i := 0; i < 200; i++ {
s, err := st.Create(testIdentity())
if err != nil {
t.Fatal(err)
}
if seen[s.Token] {
t.Fatalf("doppelter Token %q", s.Token)
}
seen[s.Token] = true
}
}
func TestSessionExpires(t *testing.T) {
now := time.Date(2026, 8, 14, 12, 0, 0, 0, time.UTC)
st := NewSessionStore(10*time.Minute, func() time.Time { return now })
sess, _ := st.Create(testIdentity())
now = now.Add(9 * time.Minute)
if _, ok := st.Get(sess.Token); !ok {
t.Fatal("Session darf nach 9 von 10 Minuten nicht abgelaufen sein")
}
now = now.Add(2 * time.Minute)
if _, ok := st.Get(sess.Token); ok {
t.Fatal("Session muss nach Ablauf der TTL ungültig sein")
}
if st.Count() != 0 {
t.Errorf("abgelaufene Session muss entfernt werden, Count = %d", st.Count())
}
}
func TestTTLIsAbsoluteNotSliding(t *testing.T) {
now := time.Date(2026, 8, 14, 12, 0, 0, 0, time.UTC)
st := NewSessionStore(10*time.Minute, func() time.Time { return now })
sess, _ := st.Create(testIdentity())
// Regelmäßige Zugriffe dürfen die Sitzung nicht verlängern.
for i := 0; i < 5; i++ {
now = now.Add(2 * time.Minute)
st.Get(sess.Token)
}
if _, ok := st.Get(sess.Token); ok {
t.Fatal("die TTL ist absolut; Aktivität darf sie nicht verlängern")
}
}
func TestDestroyRemovesSession(t *testing.T) {
st := NewSessionStore(time.Minute, nil)
sess, _ := st.Create(testIdentity())
st.Destroy(sess.Token)
if _, ok := st.Get(sess.Token); ok {
t.Fatal("zerstörte Session darf nicht mehr auffindbar sein")
}
st.Destroy("existiert-nicht") // darf nicht paniken
}
func TestGetRejectsUnknownAndEmptyToken(t *testing.T) {
st := NewSessionStore(time.Minute, nil)
if _, ok := st.Get(""); ok {
t.Error("leerer Token darf nie gültig sein")
}
if _, ok := st.Get("beliebig"); ok {
t.Error("unbekannter Token darf nie gültig sein")
}
}
func TestValidCSRF(t *testing.T) {
st := NewSessionStore(time.Minute, nil)
sess, _ := st.Create(testIdentity())
if !sess.ValidCSRF(sess.CSRF) {
t.Error("eigenes CSRF-Token muss akzeptiert werden")
}
for _, bad := range []string{"", "falsch", sess.CSRF + "x", sess.CSRF[:len(sess.CSRF)-1]} {
if sess.ValidCSRF(bad) {
t.Errorf("CSRF-Token %q darf nicht akzeptiert werden", bad)
}
}
}
func TestGCRemovesOnlyExpired(t *testing.T) {
now := time.Date(2026, 8, 14, 12, 0, 0, 0, time.UTC)
st := NewSessionStore(10*time.Minute, func() time.Time { return now })
old, _ := st.Create(testIdentity())
now = now.Add(11 * time.Minute)
fresh, _ := st.Create(testIdentity())
st.GC()
if st.Count() != 1 {
t.Fatalf("Count = %d, want 1", st.Count())
}
if _, ok := st.Get(old.Token); ok {
t.Error("alte Session muss weg sein")
}
if _, ok := st.Get(fresh.Token); !ok {
t.Error("frische Session muss bleiben")
}
}