Files
gitea/models/auth/user_session_test.go
T

255 lines
8.8 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package auth_test
import (
"testing"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/timeutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCreateUserSession(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
sess := &auth_model.UserSession{
ID: "test-session-create",
UserID: 1,
LoginIP: "192.168.1.1",
LastIP: "192.168.1.1",
UserAgent: "Mozilla/5.0 Test",
LoginMethod: "form",
}
require.NoError(t, auth_model.CreateUserSession(t.Context(), sess))
unittest.AssertExistsAndLoadBean(t, &auth_model.UserSession{ID: "test-session-create"})
}
func TestGetUserSessionByID(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
sess := &auth_model.UserSession{
ID: "test-session-get",
UserID: 2,
LoginIP: "10.0.0.1",
LastIP: "10.0.0.1",
UserAgent: "TestAgent",
LoginMethod: "oauth2",
}
require.NoError(t, auth_model.CreateUserSession(t.Context(), sess))
got, err := auth_model.GetUserSessionByID(t.Context(), "test-session-get")
require.NoError(t, err)
assert.Equal(t, int64(2), got.UserID)
assert.Equal(t, "10.0.0.1", got.LoginIP)
assert.Equal(t, "TestAgent", got.UserAgent)
_, err = auth_model.GetUserSessionByID(t.Context(), "nonexistent")
assert.True(t, auth_model.IsErrUserSessionNotExist(err))
}
func TestGetUserSessionsByUserID(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
for _, id := range []string{"sess-list-1", "sess-list-2", "sess-list-3"} {
require.NoError(t, auth_model.CreateUserSession(t.Context(), &auth_model.UserSession{
ID: id,
UserID: 5,
LoginIP: "127.0.0.1",
LastIP: "127.0.0.1",
}))
}
sessions, err := auth_model.GetUserSessionsByUserID(t.Context(), 5)
require.NoError(t, err)
assert.Len(t, sessions, 3)
sessions, err = auth_model.GetUserSessionsByUserID(t.Context(), 99999)
require.NoError(t, err)
assert.Empty(t, sessions)
}
func TestInvalidateUserSession(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
require.NoError(t, auth_model.CreateUserSession(t.Context(), &auth_model.UserSession{
ID: "sess-invalidate",
UserID: 1,
}))
require.NoError(t, auth_model.InvalidateUserSession(t.Context(), "sess-invalidate"))
got, err := auth_model.GetUserSessionByID(t.Context(), "sess-invalidate")
require.NoError(t, err)
assert.NotZero(t, got.LogoutUnix, "LogoutUnix should be set after invalidation")
}
func TestInvalidateAllUserSessions(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
for _, id := range []string{"sess-all-1", "sess-all-2", "sess-all-3"} {
require.NoError(t, auth_model.CreateUserSession(t.Context(), &auth_model.UserSession{
ID: id,
UserID: 3,
}))
}
// Invalidate all except sess-all-2
require.NoError(t, auth_model.InvalidateAllUserSessions(t.Context(), 3, "sess-all-2"))
kept, err := auth_model.GetUserSessionByID(t.Context(), "sess-all-2")
require.NoError(t, err)
assert.Zero(t, kept.LogoutUnix, "excluded session should not be invalidated")
for _, id := range []string{"sess-all-1", "sess-all-3"} {
got, err := auth_model.GetUserSessionByID(t.Context(), id)
require.NoError(t, err)
assert.NotZero(t, got.LogoutUnix, "session %s should be invalidated", id)
}
}
func TestUpdateSessionActivity(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
require.NoError(t, auth_model.CreateUserSession(t.Context(), &auth_model.UserSession{
ID: "sess-activity",
UserID: 1,
LastIP: "10.0.0.1",
}))
// Update with same IP — only LastAccessUnix should change
require.NoError(t, auth_model.UpdateSessionActivity(t.Context(), "sess-activity", "10.0.0.1"))
got, err := auth_model.GetUserSessionByID(t.Context(), "sess-activity")
require.NoError(t, err)
assert.Equal(t, "10.0.0.1", got.LastIP)
assert.Empty(t, got.PrevIP)
// Update with new IP — PrevIP should shift
require.NoError(t, auth_model.UpdateSessionActivity(t.Context(), "sess-activity", "172.16.0.1"))
got, err = auth_model.GetUserSessionByID(t.Context(), "sess-activity")
require.NoError(t, err)
assert.Equal(t, "172.16.0.1", got.LastIP)
assert.Equal(t, "10.0.0.1", got.PrevIP)
// Updating a nonexistent session should not error
require.NoError(t, auth_model.UpdateSessionActivity(t.Context(), "nonexistent", "10.0.0.1"))
// Updating an already-logged-out session should be a no-op
require.NoError(t, auth_model.InvalidateUserSession(t.Context(), "sess-activity"))
beforeUpdate, err := auth_model.GetUserSessionByID(t.Context(), "sess-activity")
require.NoError(t, err)
require.NoError(t, auth_model.UpdateSessionActivity(t.Context(), "sess-activity", "192.168.1.1"))
afterUpdate, err := auth_model.GetUserSessionByID(t.Context(), "sess-activity")
require.NoError(t, err)
assert.Equal(t, beforeUpdate.LastIP, afterUpdate.LastIP, "logged-out session IP should not change")
assert.Equal(t, beforeUpdate.LastAccessUnix, afterUpdate.LastAccessUnix, "logged-out session timestamp should not change")
}
func TestDeleteUserSessionsByUserID(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
for _, id := range []string{"sess-del-1", "sess-del-2"} {
require.NoError(t, auth_model.CreateUserSession(t.Context(), &auth_model.UserSession{
ID: id,
UserID: 4,
}))
}
require.NoError(t, auth_model.DeleteUserSessionsByUserID(t.Context(), 4))
sessions, err := auth_model.GetUserSessionsByUserID(t.Context(), 4)
require.NoError(t, err)
assert.Empty(t, sessions)
}
func TestCleanupExpiredUserSessions(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
now := timeutil.TimeStampNow()
// Active session — should survive
require.NoError(t, auth_model.CreateUserSession(t.Context(), &auth_model.UserSession{
ID: "sess-cleanup-active",
UserID: 1,
LastAccessUnix: now,
}))
// Old logged-out session — should be cleaned up.
// Use raw engine insert to bypass the "created" auto-fill.
_, err := db.GetEngine(t.Context()).Insert(&auth_model.UserSession{
ID: "sess-cleanup-old",
UserID: 1,
LogoutUnix: timeutil.TimeStamp(int64(now) - 86400*60),
LastAccessUnix: timeutil.TimeStamp(int64(now) - 86400*60),
CreatedUnix: timeutil.TimeStamp(int64(now) - 86400*60),
})
require.NoError(t, err)
retentionSeconds := int64(86400 * 30) // 30 days
maxLifetime := int64(86400) // 1 day
require.NoError(t, auth_model.CleanupExpiredUserSessions(t.Context(), retentionSeconds, maxLifetime))
// Active session should still exist
_, err = auth_model.GetUserSessionByID(t.Context(), "sess-cleanup-active")
require.NoError(t, err)
// Old session should be gone
_, err = auth_model.GetUserSessionByID(t.Context(), "sess-cleanup-old")
assert.True(t, auth_model.IsErrUserSessionNotExist(err))
}
func TestCleanupExpiredUserSessionsAbandoned(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
now := timeutil.TimeStampNow()
retentionSeconds := int64(86400 * 30) // 30 days
maxLifetime := int64(86400) // 1 day
cutoff := int64(now) - maxLifetime - retentionSeconds
// Abandoned session clearly older than cutoff — should be cleaned up.
_, err := db.GetEngine(t.Context()).Insert(&auth_model.UserSession{
ID: "sess-cleanup-abandoned-old",
UserID: 1,
LastAccessUnix: timeutil.TimeStamp(cutoff - 1),
CreatedUnix: timeutil.TimeStamp(cutoff - 1),
})
require.NoError(t, err)
// Abandoned session exactly at cutoff — should be preserved (strict < comparison).
_, err = db.GetEngine(t.Context()).Insert(&auth_model.UserSession{
ID: "sess-cleanup-abandoned-boundary",
UserID: 1,
LastAccessUnix: timeutil.TimeStamp(cutoff),
CreatedUnix: timeutil.TimeStamp(cutoff),
})
require.NoError(t, err)
// Abandoned session newer than cutoff — should be preserved.
_, err = db.GetEngine(t.Context()).Insert(&auth_model.UserSession{
ID: "sess-cleanup-abandoned-new",
UserID: 1,
LastAccessUnix: timeutil.TimeStamp(cutoff + 1),
CreatedUnix: timeutil.TimeStamp(cutoff + 1),
})
require.NoError(t, err)
require.NoError(t, auth_model.CleanupExpiredUserSessions(t.Context(), retentionSeconds, maxLifetime))
// Clearly old abandoned session should be gone.
_, err = auth_model.GetUserSessionByID(t.Context(), "sess-cleanup-abandoned-old")
assert.True(t, auth_model.IsErrUserSessionNotExist(err))
// Boundary and newer abandoned sessions should still exist.
_, err = auth_model.GetUserSessionByID(t.Context(), "sess-cleanup-abandoned-boundary")
require.NoError(t, err)
_, err = auth_model.GetUserSessionByID(t.Context(), "sess-cleanup-abandoned-new")
require.NoError(t, err)
}