Files
wonderwall/pkg/session/data_test.go
Trong Huu Nguyen f2def8d00d fix(session/data): next refresh time should account for inactivity timeouts
The default auto-refresh behaviour occurs 5 minutes before tokens
expire, at the earliest. Without inactivity however, tokens are still
refreshed at any point after this, as long as the session has not ended.

This however, means that refreshes don't occur often enough when inactivity
timeouts are enabled. In practice, the session is only refreshed if a
request is received within the 5 minute leeway window between a token's expiry
and the inactivity timeout.

This commit will apply auto-refreshes at the half-life of the inactivity
timeout instead, so that users' sessions and timeouts are properly
extended on activity.
2025-01-22 15:58:53 +01:00

386 lines
11 KiB
Go

package session_test
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/nais/wonderwall/pkg/session"
)
func TestData_HasAccessToken(t *testing.T) {
data := session.Data{}
assert.False(t, data.HasAccessToken())
data.AccessToken = "some-access-token"
assert.True(t, data.HasAccessToken())
}
func TestData_HasRefreshToken(t *testing.T) {
data := session.Data{}
assert.False(t, data.HasRefreshToken())
data.RefreshToken = "some-refresh-token"
assert.True(t, data.HasRefreshToken())
}
func TestNewMetadata(t *testing.T) {
tokenLifetime := 30 * time.Minute
sessionLifetime := time.Hour
metadata := session.NewMetadata(tokenLifetime, sessionLifetime)
maxDelta := time.Second
expected := time.Now()
actual := metadata.Session.CreatedAt
assert.WithinDuration(t, expected, actual, maxDelta)
expected = time.Now().Add(sessionLifetime)
actual = metadata.Session.EndsAt
assert.WithinDuration(t, expected, actual, maxDelta)
assert.True(t, metadata.Session.TimeoutAt.IsZero())
expected = time.Now()
actual = metadata.Tokens.RefreshedAt
assert.WithinDuration(t, expected, actual, maxDelta)
expected = time.Now().Add(tokenLifetime)
actual = metadata.Tokens.ExpireAt
assert.WithinDuration(t, expected, actual, maxDelta)
}
func TestMetadata_WithTimeout(t *testing.T) {
tokenLifetime := 30 * time.Minute
sessionLifetime := time.Hour
sessionInactivityTimeout := 15 * time.Minute
maxDelta := time.Second
metadata := session.NewMetadata(tokenLifetime, sessionLifetime)
assert.WithinDuration(t, time.Now().Add(tokenLifetime), metadata.Tokens.ExpireAt, maxDelta)
metadata.WithTimeout(sessionInactivityTimeout)
assert.False(t, metadata.Session.TimeoutAt.IsZero())
assert.WithinDuration(t, time.Now().Add(sessionInactivityTimeout), metadata.Tokens.ExpireAt, maxDelta)
assert.WithinDuration(t, metadata.Session.TimeoutAt, metadata.Tokens.ExpireAt, maxDelta)
expected := time.Now().Add(sessionInactivityTimeout)
actual := metadata.Session.TimeoutAt
assert.WithinDuration(t, expected, actual, maxDelta)
previousTimeoutAt := metadata.Session.TimeoutAt
time.Sleep(100 * time.Millisecond)
metadata.WithTimeout(sessionInactivityTimeout)
assert.True(t, metadata.Session.TimeoutAt.After(previousTimeoutAt))
}
func TestMetadata_IsExpired(t *testing.T) {
t.Run("expired", func(t *testing.T) {
metadata := session.Metadata{
Tokens: session.MetadataTokens{
ExpireAt: time.Now().Add(-time.Second),
},
}
assert.True(t, metadata.IsExpired())
})
t.Run("not expired", func(t *testing.T) {
metadata := session.Metadata{
Tokens: session.MetadataTokens{
ExpireAt: time.Now().Add(time.Second),
},
}
assert.False(t, metadata.IsExpired())
})
}
func TestMetadata_IsRefreshOnCooldown(t *testing.T) {
t.Run("delta to last refresh below minimum interval", func(t *testing.T) {
metadata := session.Metadata{
Tokens: session.MetadataTokens{
RefreshedAt: time.Now(),
ExpireAt: time.Now().Add(time.Minute),
},
}
assert.True(t, metadata.IsRefreshOnCooldown())
})
t.Run("delta to last refresh above minimum interval", func(t *testing.T) {
metadata := session.Metadata{
Tokens: session.MetadataTokens{
RefreshedAt: time.Now().Add(-2 * time.Minute),
ExpireAt: time.Now().Add(time.Minute),
},
}
assert.False(t, metadata.IsRefreshOnCooldown())
})
}
func TestMetadata_Refresh(t *testing.T) {
metadata := session.Metadata{
Tokens: session.MetadataTokens{
RefreshedAt: time.Now(),
ExpireAt: time.Now().Add(time.Minute),
},
}
prevRefreshedAt := metadata.Tokens.RefreshedAt
prevExpireAt := metadata.Tokens.ExpireAt
nextExpirySeconds := int64((2 * time.Minute).Seconds())
metadata.Refresh(nextExpirySeconds)
assert.True(t, metadata.Tokens.RefreshedAt.After(prevRefreshedAt))
assert.True(t, metadata.Tokens.ExpireAt.After(prevExpireAt))
}
func TestMetadata_RefreshCooldown(t *testing.T) {
t.Run("token lifetime less than interval", func(t *testing.T) {
tokenLifetime := time.Minute
metadata := session.Metadata{
Tokens: session.MetadataTokens{
RefreshedAt: time.Now(),
ExpireAt: time.Now().Add(tokenLifetime),
},
}
expected := time.Now().Add(tokenLifetime / 2)
assert.WithinDuration(t, expected, metadata.RefreshCooldown(), time.Second)
})
t.Run("token lifetime longer than interval", func(t *testing.T) {
metadata := session.Metadata{
Tokens: session.MetadataTokens{
RefreshedAt: time.Now(),
ExpireAt: time.Now().Add(time.Hour),
},
}
expected := metadata.Tokens.RefreshedAt.Add(session.RefreshMinInterval)
assert.WithinDuration(t, expected, metadata.RefreshCooldown(), time.Second)
})
}
func TestMetadata_ShouldRefresh(t *testing.T) {
t.Run("refresh is on cooldown", func(t *testing.T) {
metadata := session.Metadata{
Tokens: session.MetadataTokens{
RefreshedAt: time.Now(),
ExpireAt: time.Now().Add(time.Minute),
},
}
assert.False(t, metadata.ShouldRefresh())
})
t.Run("token is not within expiry range", func(t *testing.T) {
metadata := session.Metadata{
Tokens: session.MetadataTokens{
RefreshedAt: time.Now(),
ExpireAt: time.Now().Add(time.Hour),
},
}
assert.False(t, metadata.ShouldRefresh())
})
t.Run("token is about to expire", func(t *testing.T) {
metadata := session.Metadata{
Tokens: session.MetadataTokens{
RefreshedAt: time.Now().Add(-5 * time.Minute),
ExpireAt: time.Now().Add(time.Minute),
},
}
assert.True(t, metadata.ShouldRefresh())
})
t.Run("token has expired", func(t *testing.T) {
metadata := session.Metadata{
Tokens: session.MetadataTokens{
RefreshedAt: time.Now().Add(-5 * time.Minute),
ExpireAt: time.Now().Add(-5 * time.Minute),
},
}
assert.True(t, metadata.ShouldRefresh())
})
t.Run("refresh is on cooldown and token has expired", func(t *testing.T) {
metadata := session.Metadata{
Tokens: session.MetadataTokens{
RefreshedAt: time.Now(),
ExpireAt: time.Now().Add(-5 * time.Minute),
},
}
assert.True(t, metadata.ShouldRefresh())
})
t.Run("time between last refresh and timeout has passed half-life", func(t *testing.T) {
metadata := session.Metadata{
Session: session.MetadataSession{
TimeoutAt: time.Now().Add(10 * time.Minute),
},
Tokens: session.MetadataTokens{
RefreshedAt: time.Now().Add(-15 * time.Minute),
ExpireAt: time.Now().Add(time.Hour),
},
}
assert.True(t, metadata.ShouldRefresh())
})
t.Run("time between last refresh and timeout has not passed half-life", func(t *testing.T) {
metadata := session.Metadata{
Session: session.MetadataSession{
TimeoutAt: time.Now().Add(10 * time.Minute),
},
Tokens: session.MetadataTokens{
RefreshedAt: time.Now().Add(-5 * time.Minute),
ExpireAt: time.Now().Add(time.Hour),
},
}
assert.False(t, metadata.ShouldRefresh())
})
t.Run("token expiry occurs before inactivity, token is not within expiry range", func(t *testing.T) {
metadata := session.Metadata{
Session: session.MetadataSession{
TimeoutAt: time.Now().Add(2 * time.Hour),
},
Tokens: session.MetadataTokens{
RefreshedAt: time.Now().Add(-5 * time.Minute),
ExpireAt: time.Now().Add(time.Hour),
},
}
assert.False(t, metadata.ShouldRefresh())
})
t.Run("token expiry occurs before inactivity, token is about to expire", func(t *testing.T) {
metadata := session.Metadata{
Session: session.MetadataSession{
TimeoutAt: time.Now().Add(time.Hour),
},
Tokens: session.MetadataTokens{
RefreshedAt: time.Now().Add(-5 * time.Minute),
ExpireAt: time.Now().Add(time.Minute),
},
}
assert.True(t, metadata.ShouldRefresh())
})
}
func TestMetadata_TokenLifetime(t *testing.T) {
metadata := session.Metadata{
Tokens: session.MetadataTokens{
RefreshedAt: time.Now(),
ExpireAt: time.Now().Add(time.Minute),
},
}
assert.Equal(t, time.Minute, metadata.TokenLifetime().Truncate(time.Second))
}
func TestMetadata_Verbose(t *testing.T) {
tokenLifetime := 30 * time.Minute
sessionLifetime := time.Hour
metadata := session.NewMetadata(tokenLifetime, sessionLifetime)
verbose := metadata.Verbose()
maxDelta := time.Second
expected := time.Now().Add(sessionLifetime)
actual := time.Now().Add(durationSeconds(verbose.Session.EndsInSeconds))
assert.WithinDuration(t, expected, actual, maxDelta)
expected = time.Now().Add(tokenLifetime)
actual = time.Now().Add(durationSeconds(verbose.Tokens.ExpireInSeconds))
assert.WithinDuration(t, expected, actual, maxDelta)
expected = time.Now().Add(tokenLifetime).Add(-session.RefreshLeeway)
actual = time.Now().Add(durationSeconds(verbose.Tokens.NextAutoRefreshInSeconds))
assert.WithinDuration(t, expected, actual, maxDelta)
assert.True(t, verbose.Session.Active)
assert.True(t, verbose.Session.TimeoutAt.IsZero())
assert.Equal(t, int64(-1), verbose.Session.TimeoutInSeconds)
t.Run("refresh on cooldown", func(t *testing.T) {
assert.True(t, verbose.Tokens.RefreshCooldown)
expected = time.Now().Add(session.RefreshMinInterval)
actual = time.Now().Add(durationSeconds(verbose.Tokens.RefreshCooldownSeconds))
assert.WithinDuration(t, expected, actual, maxDelta)
})
t.Run("refresh not on cooldown", func(t *testing.T) {
metadata := session.NewMetadata(tokenLifetime, sessionLifetime)
metadata.Tokens.RefreshedAt = time.Now().Add(-5 * time.Minute)
verbose := metadata.Verbose()
assert.False(t, verbose.Tokens.RefreshCooldown)
assert.Equal(t, int64(0), verbose.Tokens.RefreshCooldownSeconds)
})
}
func TestMetadata_Verbose_WithTimeout(t *testing.T) {
tokenLifetime := 30 * time.Minute
sessionLifetime := time.Hour
timeout := 15 * time.Minute
metadata := session.NewMetadata(tokenLifetime, sessionLifetime)
metadata.WithTimeout(timeout)
maxDelta := time.Second
verbose := metadata.Verbose()
assert.True(t, verbose.Session.Active)
assert.False(t, verbose.Session.TimeoutAt.IsZero())
expected := time.Now().Add(timeout)
actual := verbose.Session.TimeoutAt
assert.WithinDuration(t, expected, actual, maxDelta)
expected = time.Now().Add(durationSeconds(verbose.Session.TimeoutInSeconds))
actual = verbose.Session.TimeoutAt
assert.WithinDuration(t, expected, actual, maxDelta)
}
func TestMetadata_IsTimedOut(t *testing.T) {
tokenLifetime := 30 * time.Minute
sessionLifetime := time.Hour
t.Run("timeout is zero", func(t *testing.T) {
metadata := session.NewMetadata(tokenLifetime, sessionLifetime)
assert.False(t, metadata.IsTimedOut())
})
t.Run("timeout is non-zero", func(t *testing.T) {
timeout := 15 * time.Minute
metadata := session.NewMetadata(tokenLifetime, sessionLifetime)
metadata.WithTimeout(timeout)
assert.False(t, metadata.IsTimedOut())
metadata.WithTimeout(-timeout)
assert.True(t, metadata.IsTimedOut())
})
}
func durationSeconds(seconds int64) time.Duration {
return time.Duration(seconds) * time.Second
}