mirror of
https://github.com/hanzoai/team.git
synced 2026-09-30 01:57:35 +00:00
The server module named github.com/hanzoai/team/server/v8 — a path that resolves to the retired workspace's repository, not this one — while server/public still published github.com/mattermost/mattermost/server/public. Both are github.com/hanzoteam/server/... now, which is where this code actually lives. That second half is what made the binary unbootable. Upstream's public path stayed claimed by upstream, mattermost-plugin-ai imported it, and the build linked public/model twice; public/plugin's gob.Register then ran twice under one name and panicked at init, before main. Reverting the rename traded the brand for a working binary. Rebranding the plugin alongside it needs no such trade: the agents fork is github.com/hanzoteam/agents/v2 against our public module, so no module named mattermost/mattermost is in the graph at all. Measured: `go list -deps ./cmd/mattermost` names exactly one public/model, and it is ours.
333 lines
11 KiB
Go
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/hanzoteam/server/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)
|
|
}
|