internal/server/store/sessions_test.go
Ref: Size: 3.4 KiB History
package store
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSessionLifecycle(t *testing.T) {
s := newStore(t)
id, err := s.CreateSession(testTenant, time.Hour)
require.NoError(t, err)
assert.Len(t, id, 64, "session id is 64-hex (256-bit)")
tenant, ok, err := s.SessionTenant(id)
require.NoError(t, err)
require.True(t, ok)
assert.Equal(t, testTenant, tenant)
// Unknown id.
_, ok, err = s.SessionTenant("nope")
require.NoError(t, err)
assert.False(t, ok)
// Delete revokes.
require.NoError(t, s.DeleteSession(id))
_, ok, err = s.SessionTenant(id)
require.NoError(t, err)
assert.False(t, ok)
}
func TestSessionExpiry(t *testing.T) {
s := newStore(t)
// A negative ttl mints an already-expired session; reads must reject it.
id, err := s.CreateSession(testTenant, -time.Minute)
require.NoError(t, err)
_, ok, err := s.SessionTenant(id)
require.NoError(t, err)
assert.False(t, ok, "expired session must not resolve")
}
func TestReapSessions(t *testing.T) {
s := newStore(t)
live, err := s.CreateSession(testTenant, time.Hour)
require.NoError(t, err)
_, err = s.CreateSession(testTenant, -time.Minute)
require.NoError(t, err)
_, err = s.CreateSession(testTenant, -time.Hour)
require.NoError(t, err)
n, err := s.ReapSessions()
require.NoError(t, err)
assert.Equal(t, int64(2), n, "both expired rows reaped")
// The live one survives.
_, ok, err := s.SessionTenant(live)
require.NoError(t, err)
assert.True(t, ok)
// Reaping again removes nothing.
n, err = s.ReapSessions()
require.NoError(t, err)
assert.Equal(t, int64(0), n)
}
// TestSessionIDNotStoredInCleartext is the property, not the plumbing: a copied
// database must not hand anyone a usable console session. The lifecycle tests
// above would pass just as well with the id stored verbatim.
func TestSessionIDNotStoredInCleartext(t *testing.T) {
s := newStore(t)
id, err := s.CreateSession(testTenant, time.Hour)
require.NoError(t, err)
var stored string
require.NoError(t, s.db.QueryRow(`SELECT id FROM sessions`).Scan(&stored))
assert.NotEqual(t, id, stored, "the cookie value must not be what is on disk")
assert.Equal(t, hashToken(id), stored,
"the sessions row must hold hashToken(id) and nothing else: the schema comment says SHA-256 of the "+
"session value, and a lookup that stored any other transform would still authenticate while "+
"leaving the column undescribed by the DDL beside it")
// And the stored value must not itself work as a cookie — otherwise the row
// is still a bearer credential, just a differently-spelled one.
_, ok, err := s.SessionTenant(stored)
require.NoError(t, err)
assert.False(t, ok, "presenting the stored hash must not authenticate")
}
// TestSessionRowsPredatingHashingAreInert pins what happens to sessions written
// before ids were hashed: they authenticate nobody, rather than still working.
func TestSessionRowsPredatingHashingAreInert(t *testing.T) {
s := newStore(t)
legacy := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
_, err := s.db.Exec(`INSERT INTO sessions(id, tenant, created_at, expires_at) VALUES (?,?,?,?)`,
legacy, testTenant, time.Now().UTC().Format(time.RFC3339),
time.Now().UTC().Add(time.Hour).Format(time.RFC3339))
require.NoError(t, err)
_, ok, err := s.SessionTenant(legacy)
require.NoError(t, err)
assert.False(t, ok, "a pre-hashing row must not authenticate")
}