a73x

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