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") } }