Files
upstream-mattermost/server/channels/app/platform/session_test.go
upstream-mirror 240db4e2a7
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 00:29:55 +05:00

333 lines
11 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
)
const (
dayInMillis = 86400000
grace = 5 * 1000
thirtyDays = dayInMillis * 30
)
func TestCache(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
session := &model.Session{
Id: model.NewId(),
Token: model.NewId(),
UserId: model.NewId(),
}
session2 := &model.Session{
Id: model.NewId(),
Token: model.NewId(),
UserId: model.NewId(),
}
err := th.Service.sessionCache.SetWithExpiry(session.Token, session, 5*time.Minute)
require.NoError(t, err)
err = th.Service.sessionCache.SetWithExpiry(session2.Token, session2, 5*time.Minute)
require.NoError(t, err)
var keys []string
err = th.Service.sessionCache.Scan(func(in []string) error {
keys = append(keys, in...)
return nil
})
require.NoError(t, err)
require.NotEmpty(t, keys)
th.Service.ClearUserSessionCache(session.UserId)
var rkeys []string
err = th.Service.sessionCache.Scan(func(in []string) error {
rkeys = append(rkeys, in...)
return nil
})
require.NoError(t, err)
require.Lenf(t, rkeys, len(keys)-1, "should have one less: %d - %d != 1", len(keys), len(rkeys))
require.NotEmpty(t, rkeys)
clear(rkeys)
rkeys = []string{}
err = th.Service.ClearAllUsersSessionCache()
require.NoError(t, err)
err = th.Service.sessionCache.Scan(func(in []string) error {
rkeys = append(rkeys, in...)
return nil
})
require.NoError(t, err)
require.Len(t, rkeys, 0)
}
func TestSetSessionExpireInHours(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
now := model.GetMillis()
createAt := now - (dayInMillis * 20)
tests := []struct {
name string
extend bool
create bool
days int
want int64
}{
{name: "zero days, extend", extend: true, create: true, days: 0, want: now},
{name: "zero days, extend", extend: true, create: false, days: 0, want: now},
{name: "zero days, no extend", extend: false, create: true, days: 0, want: createAt},
{name: "zero days, no extend", extend: false, create: false, days: 0, want: now},
{name: "thirty days, extend", extend: true, create: true, days: 30, want: now + thirtyDays},
{name: "thirty days, extend", extend: true, create: false, days: 30, want: now + thirtyDays},
{name: "thirty days, no extend", extend: false, create: true, days: 30, want: createAt + thirtyDays},
{name: "thirty days, no extend", extend: false, create: false, days: 30, want: now + thirtyDays},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
th.Service.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.ExtendSessionLengthWithActivity = tt.extend
})
var create int64
if tt.create {
create = createAt
}
session := &model.Session{
CreateAt: create,
ExpiresAt: model.GetMillis() + dayInMillis,
}
th.Service.SetSessionExpireInHours(session, tt.days*24)
// must be within 5 seconds of expected time.
require.GreaterOrEqual(t, session.ExpiresAt, tt.want-grace)
require.LessOrEqual(t, session.ExpiresAt, tt.want+grace)
})
}
}
func TestOAuthRevokeAccessToken(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
err := th.Service.RevokeAccessToken(th.Context, model.NewRandomString(16))
require.Error(t, err, "Should have failed due to an incorrect token")
session := &model.Session{}
session.CreateAt = model.GetMillis()
session.UserId = model.NewId()
session.Token = model.NewId()
session.Roles = model.SystemUserRoleId
th.Service.SetSessionExpireInHours(session, 24)
session, _ = th.Service.CreateSession(th.Context, session)
err = th.Service.RevokeAccessToken(th.Context, session.Token)
require.Error(t, err, "Should have failed does not have an access token")
accessData := &model.AccessData{}
accessData.Token = session.Token
accessData.UserId = session.UserId
accessData.RedirectUri = "http://example.com"
accessData.ClientId = model.NewId()
accessData.ExpiresAt = session.ExpiresAt
_, nErr := th.Service.Store.OAuth().SaveAccessData(accessData)
require.NoError(t, nErr)
err = th.Service.RevokeAccessToken(th.Context, accessData.Token)
require.NoError(t, err)
}
func TestUpdateSessionsIsGuest(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
t.Run("Test session is demoted", func(t *testing.T) {
user := th.CreateUserOrGuest(t, false)
session := &model.Session{}
session.CreateAt = model.GetMillis()
session.UserId = user.Id
session.Token = model.NewId()
session.Roles = "fake_role"
th.Service.SetSessionExpireInHours(session, 24)
session, _ = th.Service.CreateSession(th.Context, session)
demotedUser, err := th.Service.Store.User().DemoteUserToGuest(th.Context, user.Id)
require.NoError(t, err)
require.Equal(t, model.SystemGuestRoleId, demotedUser.Roles)
err = th.Service.UpdateSessionsIsGuest(th.Context, demotedUser, true)
require.NoError(t, err)
session, err = th.Service.GetSession(th.Context, session.Id)
require.NoError(t, err)
require.Equal(t, model.SystemGuestRoleId, session.Roles)
require.Equal(t, "true", session.Props[model.SessionPropIsGuest])
})
t.Run("Test session is promoted", func(t *testing.T) {
user := th.CreateUserOrGuest(t, true)
session := &model.Session{}
session.CreateAt = model.GetMillis()
session.UserId = user.Id
session.Token = model.NewId()
session.Roles = "fake_role"
th.Service.SetSessionExpireInHours(session, 24)
session, _ = th.Service.CreateSession(th.Context, session)
err := th.Service.Store.User().PromoteGuestToUser(th.Context, user.Id)
require.NoError(t, err)
promotedUser, err := th.Service.Store.User().Get(th.Context, user.Id)
require.NoError(t, err)
err = th.Service.UpdateSessionsIsGuest(th.Context, promotedUser, false)
require.NoError(t, err)
session, err = th.Service.GetSession(th.Context, session.Id)
require.NoError(t, err)
require.Equal(t, model.SystemUserRoleId, session.Roles)
require.Equal(t, "false", session.Props[model.SessionPropIsGuest])
})
}
func TestRevokeSessionsForDeviceTokens(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
// Both revoke variants are structurally identical (one matches DeviceId,
// the other VoIPDeviceId), and the invariants they must hold are the
// same. Drive them through a single table.
type variant struct {
name string
setToken func(s *model.Session, token string)
revoke func(userID, token, currentSessionID string) error
emptyTokenErr error
}
variants := []variant{
{
name: "RevokeOtherSessionsForDeviceId",
setToken: func(s *model.Session, token string) { s.DeviceId = token },
revoke: func(userID, token, currentSessionID string) error {
return th.Service.RevokeOtherSessionsForDeviceId(th.Context, userID, token, currentSessionID)
},
emptyTokenErr: ErrEmptyDeviceId,
},
{
name: "RevokeOtherSessionsForVoIPDeviceId",
setToken: func(s *model.Session, token string) { s.VoIPDeviceId = token },
revoke: func(userID, token, currentSessionID string) error {
return th.Service.RevokeOtherSessionsForVoIPDeviceId(th.Context, userID, token, currentSessionID)
},
emptyTokenErr: ErrEmptyVoIPDeviceId,
},
}
createSession := func(t *testing.T, v variant, userID, token string) *model.Session {
t.Helper()
s := &model.Session{UserId: userID, Roles: model.SystemUserRoleId}
v.setToken(s, token)
saved, err := th.Service.CreateSession(th.Context, s)
require.NoError(t, err)
return saved
}
sessionExists := func(t *testing.T, id string) bool {
t.Helper()
_, err := th.Service.GetSessionByID(th.Context, id)
return err == nil
}
for _, v := range variants {
t.Run(v.name, func(t *testing.T) {
t.Run("revokes the matching session", func(t *testing.T) {
token := "apple_rn:revokes-matching-" + v.name
target := createSession(t, v, th.BasicUser.Id, token)
require.NoError(t, v.revoke(th.BasicUser.Id, token, ""))
assert.False(t, sessionExists(t, target.Id), "matching session must be revoked")
})
t.Run("does not revoke the session passed as currentSessionId", func(t *testing.T) {
// Self-skip: a session re-attaching its own token must not
// revoke itself.
token := "apple_rn:self-skip-" + v.name
self := createSession(t, v, th.BasicUser.Id, token)
require.NoError(t, v.revoke(th.BasicUser.Id, token, self.Id))
assert.True(t, sessionExists(t, self.Id), "currentSessionId must be exempt from revocation")
})
t.Run("does not revoke sessions belonging to other users", func(t *testing.T) {
// The function scopes by userID — a session with a matching
// token on a different user must stay alive.
token := "apple_rn:cross-user-" + v.name
otherUserSession := createSession(t, v, th.BasicUser2.Id, token)
require.NoError(t, v.revoke(th.BasicUser.Id, token, ""))
assert.True(t, sessionExists(t, otherUserSession.Id),
"session on a different user must not be revoked")
})
t.Run("empty token returns an error and revokes nothing", func(t *testing.T) {
webSession := createSession(t, v, th.BasicUser.Id, "")
err := v.revoke(th.BasicUser.Id, "", "")
require.ErrorIs(t, err, v.emptyTokenErr)
assert.True(t, sessionExists(t, webSession.Id),
"empty-token revoke must not match sessions with no token in this slot")
})
t.Run("leaves siblings of the same user with different tokens alone", func(t *testing.T) {
targetToken := "apple_rn:target-" + v.name
siblingToken := "apple_rn:sibling-" + v.name
target := createSession(t, v, th.BasicUser.Id, targetToken)
sibling := createSession(t, v, th.BasicUser.Id, siblingToken)
require.NoError(t, v.revoke(th.BasicUser.Id, targetToken, ""))
assert.False(t, sessionExists(t, target.Id))
assert.True(t, sessionExists(t, sibling.Id),
"session with a different token must stay alive")
})
})
}
}
func TestRevokeAllSessionsReturnsRevokedSessions(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
userId := model.NewId()
s1 := &model.Session{UserId: userId, ExpiresAt: model.GetMillis() + dayInMillis, DeviceId: model.NewId()}
s1, _ = th.Service.CreateSession(th.Context, s1)
s2 := &model.Session{UserId: userId, ExpiresAt: model.GetMillis() + dayInMillis}
s2, _ = th.Service.CreateSession(th.Context, s2)
sessions, err := th.Service.RevokeAllSessions(th.Context, userId)
require.NoError(t, err)
require.Len(t, sessions, 2)
require.ElementsMatch(t, []string{s1.Id, s2.Id}, []string{sessions[0].Id, sessions[1].Id})
remaining, err := th.Service.Store.Session().GetSessions(th.Context, userId)
require.NoError(t, err)
require.Empty(t, remaining)
}