snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-15 21:19:12 +05:00
commit c60cc7ca75
8092 changed files with 394160 additions and 0 deletions

973
session/test/persistence.go Normal file
View File

@@ -0,0 +1,973 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package test
import (
"context"
"sync/atomic"
"testing"
"time"
"github.com/go-faker/faker/v4"
"github.com/gofrs/uuid"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/sync/errgroup"
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/persistence"
"github.com/ory/kratos/pkg/testhelpers"
"github.com/ory/kratos/session"
"github.com/ory/kratos/x"
"github.com/ory/x/contextx"
"github.com/ory/x/dbal"
"github.com/ory/x/pagination/keysetpagination"
"github.com/ory/x/randx"
"github.com/ory/x/sqlcon"
)
func TestPersister(ctx context.Context, conf *config.Config, p interface {
persistence.Persister
},
) func(t *testing.T) {
return func(t *testing.T) {
_, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p)
t.Run("case=not found", func(t *testing.T) {
_, err := p.GetSession(ctx, x.NewUUID(), session.ExpandNothing)
require.Error(t, err)
})
t.Run("case=create session", func(t *testing.T) {
var expected session.Session
require.NoError(t, faker.FakeData(&expected))
expected.Active = true
expected.AMR = session.AuthenticationMethods{
{Method: identity.CredentialsTypePassword, CompletedAt: time.Now().UTC().Round(time.Second)},
{Method: identity.CredentialsTypeOIDC, CompletedAt: time.Now().UTC().Round(time.Second)},
}
require.NoError(t, p.CreateIdentity(ctx, expected.Identity))
var expectedSessionDevice session.Device
require.NoError(t, faker.FakeData(&expectedSessionDevice))
expected.Devices = []session.Device{
expectedSessionDevice,
}
assert.Equal(t, uuid.Nil, expected.ID)
require.NoError(t, p.UpsertSession(ctx, &expected))
assert.NotEqual(t, uuid.Nil, expected.ID)
check := func(actual *session.Session, err error) {
require.NoError(t, err)
assert.Equal(t, expected.Identity.ID, actual.Identity.ID)
assert.NotEmpty(t, actual.Identity.SchemaURL)
assert.NotEmpty(t, actual.Identity.SchemaID)
assert.Equal(t, expected.ID, actual.ID)
assert.Equal(t, expected.Active, actual.Active)
assert.Equal(t, expected.Token, actual.Token)
assert.EqualValues(t, expected.ExpiresAt.Unix(), actual.ExpiresAt.Unix())
assert.Equal(t, expected.AuthenticatedAt.Unix(), actual.AuthenticatedAt.Unix())
assert.Equal(t, expected.IssuedAt.Unix(), actual.IssuedAt.Unix())
assert.Equal(t, expected.AuthenticatorAssuranceLevel, actual.AuthenticatorAssuranceLevel)
assert.Equal(t, expected.AMR, actual.AMR)
}
checkDevices := func(actual []session.Device, err error) {
require.NoError(t, err)
assert.Equal(t, len(expected.Devices), len(actual))
for i, d := range actual {
assert.Equal(t, expected.Devices[i].SessionID, d.SessionID)
assert.Equal(t, expected.Devices[i].NID, d.NID)
assert.Equal(t, *expected.Devices[i].IPAddress, *d.IPAddress)
assert.Equal(t, expected.Devices[i].UserAgent, d.UserAgent)
assert.Equal(t, *expected.Devices[i].Location, *d.Location)
assert.Equal(t, *expected.Devices[i].IdentityID, *d.IdentityID)
}
}
t.Run("method=get by id", func(t *testing.T) {
sess, err := p.GetSession(ctx, expected.ID, session.ExpandEverything)
check(sess, err)
checkDevices(sess.Devices, err)
t.Run("on another network", func(t *testing.T) {
_, p := testhelpers.NewNetwork(t, ctx, p)
_, err := p.GetSession(ctx, expected.ID, session.ExpandEverything)
assert.ErrorIs(t, err, sqlcon.ErrNoRows())
})
})
t.Run("method=get by token", func(t *testing.T) {
sess, err := p.GetSessionByToken(ctx, expected.Token, session.ExpandEverything, identity.ExpandDefault)
check(sess, err)
checkDevices(sess.Devices, err)
t.Run("on another network", func(t *testing.T) {
_, p := testhelpers.NewNetwork(t, ctx, p)
_, err := p.GetSessionByToken(ctx, expected.Token, session.ExpandNothing, identity.ExpandDefault)
assert.ErrorIs(t, err, sqlcon.ErrNoRows())
})
})
t.Run("case=update session", func(t *testing.T) {
expected.AuthenticatorAssuranceLevel = identity.AuthenticatorAssuranceLevel1
require.NoError(t, p.UpsertSession(ctx, &expected))
actual, err := p.GetSessionByToken(ctx, expected.Token, session.ExpandDefault, identity.ExpandDefault)
check(actual, err)
assert.Equal(t, identity.AuthenticatorAssuranceLevel1, actual.AuthenticatorAssuranceLevel)
})
t.Run("case=remove amr and update", func(t *testing.T) {
expected.AMR = nil
require.NoError(t, p.UpsertSession(ctx, &expected))
actual, err := p.GetSessionByToken(ctx, expected.Token, session.ExpandDefault, identity.ExpandDefault)
check(actual, err)
assert.Empty(t, actual.AMR)
})
})
t.Run("case=list sessions", func(t *testing.T) {
var identity1 identity.Identity
require.NoError(t, faker.FakeData(&identity1))
// Second identity to test listing by identity isolation
var identity2 identity.Identity
var identity2Session session.Session
require.NoError(t, faker.FakeData(&identity2))
require.NoError(t, faker.FakeData(&identity2Session))
// Create seed identities
_, l := testhelpers.NewNetwork(t, ctx, p)
require.NoError(t, l.CreateIdentity(ctx, &identity1))
require.NoError(t, l.CreateIdentity(ctx, &identity2))
seedSessionIDs := make([]uuid.UUID, 5)
seedSessionsList := make([]session.Session, 5)
now := time.Now().UTC()
for j := range seedSessionsList {
require.NoError(t, faker.FakeData(&seedSessionsList[j]))
seedSessionsList[j].Identity = &identity1
seedSessionsList[j].Active = j%2 == 0
if seedSessionsList[j].Active {
seedSessionsList[j].ExpiresAt = time.Now().UTC().Add(time.Hour)
} else {
seedSessionsList[j].ExpiresAt = time.Now().UTC().Add(-time.Hour)
}
var device session.Device
require.NoError(t, faker.FakeData(&device))
seedSessionsList[j].Devices = []session.Device{
device,
}
require.NoError(t, l.UpsertSession(ctx, &seedSessionsList[j]))
require.NoError(t, p.GetConnection(ctx).
RawQuery(
"UPDATE sessions SET created_at = ?, updated_at = ? WHERE id = ?",
now.Add(time.Duration(j)*time.Minute).Round(time.Second),
now.Add(time.Duration(j)*time.Minute).Round(time.Second),
seedSessionsList[j].ID).
Exec())
seedSessionIDs[j] = seedSessionsList[j].ID
}
identity2Session.Identity = &identity2
identity2Session.Active = true
identity2Session.ExpiresAt = time.Now().UTC().Add(time.Hour)
require.NoError(t, l.UpsertSession(ctx, &identity2Session))
for _, tc := range []struct {
desc string
except uuid.UUID
expectedSessionIds []uuid.UUID
active *bool
}{
{
desc: "all",
expectedSessionIds: seedSessionIDs,
},
{
desc: "except one",
except: seedSessionsList[0].ID,
expectedSessionIds: []uuid.UUID{
seedSessionIDs[1],
seedSessionIDs[2],
seedSessionIDs[3],
seedSessionIDs[4],
},
},
{
desc: "active only",
active: new(true),
expectedSessionIds: []uuid.UUID{
seedSessionIDs[0],
seedSessionIDs[2],
seedSessionIDs[4],
},
},
{
desc: "active only and except",
active: new(true),
except: seedSessionsList[0].ID,
expectedSessionIds: []uuid.UUID{
seedSessionIDs[2],
seedSessionIDs[4],
},
},
{
desc: "inactive only",
active: new(false),
expectedSessionIds: []uuid.UUID{
seedSessionIDs[1],
seedSessionIDs[3],
},
},
{
desc: "inactive only and except",
active: new(false),
except: seedSessionsList[3].ID,
expectedSessionIds: []uuid.UUID{
seedSessionIDs[1],
},
},
} {
t.Run("case=by Identity "+tc.desc, func(t *testing.T) {
actual, total, err := l.ListSessionsByIdentity(ctx, identity1.ID, tc.active, 1, 10, tc.except, session.ExpandEverything)
require.NoError(t, err)
actualSessionIds := make([]uuid.UUID, 0)
for _, s := range actual {
actualSessionIds = append(actualSessionIds, s.ID)
}
assert.Equal(t, int64(len(tc.expectedSessionIds)), total)
assert.ElementsMatch(t, tc.expectedSessionIds, actualSessionIds)
})
}
t.Run("case=by Identity on other network", func(t *testing.T) {
_, other := testhelpers.NewNetwork(t, ctx, p)
actual, total, err := other.ListSessionsByIdentity(ctx, identity1.ID, nil, 1, 10, uuid.Nil, session.ExpandNothing)
require.NoError(t, err)
require.Equal(t, int64(0), total)
assert.Len(t, actual, 0)
})
for _, tc := range []struct {
desc string
except uuid.UUID
expected []session.Session
active *bool
}{
{
desc: "all",
expected: append(seedSessionsList, identity2Session),
},
{
desc: "active only",
active: new(true),
expected: []session.Session{
seedSessionsList[0],
seedSessionsList[2],
seedSessionsList[4],
identity2Session,
},
},
{
desc: "inactive only",
active: new(false),
expected: []session.Session{
seedSessionsList[1],
seedSessionsList[3],
},
},
} {
t.Run("case=all "+tc.desc, func(t *testing.T) {
paginatorOpts := make([]keysetpagination.Option, 0)
actual, nextPage, err := l.ListSessions(ctx, tc.active, paginatorOpts, session.ExpandEverything)
require.NoError(t, err, "%+v", err)
require.Equal(t, len(tc.expected), len(actual))
assert.Equal(t, true, nextPage.IsLast())
mapPageToken := nextPage.Token().Parse("")
assert.Equal(t, uuid.Nil.String(), mapPageToken["id"])
assert.Equal(t, 250, nextPage.Size())
for _, es := range tc.expected {
found := false
for _, as := range actual {
if as.ID == es.ID {
found = true
assert.Equal(t, len(es.Devices), len(as.Devices))
assert.Equal(t, es.Identity.ID.String(), as.Identity.ID.String())
}
}
assert.True(t, found)
}
})
}
t.Run("case=all sessions pagination only one page", func(t *testing.T) {
paginatorOpts := make([]keysetpagination.Option, 0)
actual, page, err := l.ListSessions(ctx, nil, paginatorOpts, session.ExpandEverything)
require.NoError(t, err)
require.Equal(t, 6, len(actual))
assert.Equal(t, true, page.IsLast())
mapPageToken := page.Token().Parse("")
assert.Equal(t, uuid.Nil.String(), mapPageToken["id"])
assert.Equal(t, 250, page.Size())
})
t.Run("case=all sessions pagination multiple pages", func(t *testing.T) {
paginatorOpts := make([]keysetpagination.Option, 0)
paginatorOpts = append(paginatorOpts, keysetpagination.WithSize(3))
firstPageItems, page1, err := l.ListSessions(ctx, nil, paginatorOpts, session.ExpandEverything)
require.NoError(t, err)
assert.Len(t, firstPageItems, 3)
assert.Equal(t, false, page1.IsLast())
mapPageToken := page1.Token().Parse("")
assert.Equal(t, firstPageItems[len(firstPageItems)-1].ID.String(), mapPageToken["id"])
assert.Equal(t, 3, page1.Size())
// Validate secondPageItems page
secondPageItems, page2, err := l.ListSessions(ctx, nil, page1.ToOptions(), session.ExpandEverything)
require.NoError(t, err)
assert.Len(t, secondPageItems, 3)
acutalIDs := make([]uuid.UUID, 0)
for _, s := range append(firstPageItems, secondPageItems...) {
acutalIDs = append(acutalIDs, s.ID)
}
expect := append(seedSessionIDs, identity2Session.ID)
require.Len(t, acutalIDs, len(expect))
assert.ElementsMatch(t, expect, acutalIDs)
assert.True(t, page2.IsLast())
assert.Equal(t, 3, page2.Size())
})
})
t.Run("case=delete session", func(t *testing.T) {
var expected session.Session
require.NoError(t, faker.FakeData(&expected))
require.NoError(t, p.CreateIdentity(ctx, expected.Identity))
require.NoError(t, p.UpsertSession(ctx, &expected))
t.Run("on another network", func(t *testing.T) {
_, other := testhelpers.NewNetwork(t, ctx, p)
err := other.DeleteSession(ctx, expected.ID)
assert.ErrorIs(t, err, sqlcon.ErrNoRows())
_, err = p.GetSession(ctx, expected.ID, session.ExpandNothing)
assert.NoError(t, err)
})
require.NoError(t, p.DeleteSession(ctx, expected.ID))
_, err := p.GetSession(ctx, expected.ID, session.ExpandNothing)
assert.ErrorIs(t, err, sqlcon.ErrNoRows())
})
t.Run("case=delete session by token", func(t *testing.T) {
var expected session.Session
require.NoError(t, faker.FakeData(&expected))
require.NoError(t, p.CreateIdentity(ctx, expected.Identity))
require.NoError(t, p.UpsertSession(ctx, &expected))
t.Run("on another network", func(t *testing.T) {
_, other := testhelpers.NewNetwork(t, ctx, p)
err := other.DeleteSessionByToken(ctx, expected.Token)
assert.ErrorIs(t, err, sqlcon.ErrNoRows())
_, err = p.GetSessionByToken(ctx, expected.Token, session.ExpandNothing, identity.ExpandDefault)
assert.NoError(t, err)
})
require.NoError(t, p.DeleteSessionByToken(ctx, expected.Token))
_, err := p.GetSession(ctx, expected.ID, session.ExpandNothing)
require.Error(t, err)
})
t.Run("case=revoke session by token", func(t *testing.T) {
var expected session.Session
require.NoError(t, faker.FakeData(&expected))
expected.Active = true
require.NoError(t, p.CreateIdentity(ctx, expected.Identity))
require.NoError(t, p.UpsertSession(ctx, &expected))
actual, err := p.GetSession(ctx, expected.ID, session.ExpandNothing)
require.NoError(t, err)
assert.True(t, actual.Active)
t.Run("on another network", func(t *testing.T) {
_, other := testhelpers.NewNetwork(t, ctx, p)
revoked, err := other.RevokeSessionByToken(ctx, expected.Token)
assert.ErrorIs(t, err, sqlcon.ErrNoRows())
assert.Equal(t, uuid.Nil, revoked.ID, "session id must be nil when not found")
assert.Equal(t, uuid.Nil, revoked.IdentityID, "identity id must be nil when not found")
actual, err = p.GetSession(ctx, expected.ID, session.ExpandNothing)
require.NoError(t, err)
assert.True(t, actual.Active)
})
revoked, err := p.RevokeSessionByToken(ctx, expected.Token)
require.NoError(t, err)
assert.Equal(t, expected.ID, revoked.ID, "must return the session id")
assert.Equal(t, expected.IdentityID, revoked.IdentityID, "must return the identity id")
actual, err = p.GetSession(ctx, expected.ID, session.ExpandNothing)
require.NoError(t, err)
assert.False(t, actual.Active)
// Re-revoking an already-inactive session is silently
// idempotent — the CTE-based implementation returns the matched
// row, not the flipped one, so the IDs come back regardless.
revoked, err = p.RevokeSessionByToken(ctx, expected.Token)
require.NoError(t, err,
"re-revoking an already-inactive session must succeed silently on %s",
p.GetConnection(ctx).Dialect.Name())
assert.Equal(t, expected.ID, revoked.ID, "matched id must be returned on re-revoke")
assert.Equal(t, expected.IdentityID, revoked.IdentityID, "matched identity id must be returned on re-revoke")
actual, err = p.GetSession(ctx, expected.ID, session.ExpandNothing)
require.NoError(t, err, "session must still exist after re-revoke")
assert.False(t, actual.Active, "session must remain inactive after re-revoke")
t.Run("returns ErrNoRows for an unknown token", func(t *testing.T) {
revoked, err := p.RevokeSessionByToken(ctx, "this-token-does-not-exist-"+x.NewUUID().String())
assert.ErrorIs(t, err, sqlcon.ErrNoRows())
assert.Equal(t, uuid.Nil, revoked.ID)
assert.Equal(t, uuid.Nil, revoked.IdentityID)
})
})
t.Run("case=revoke session by id", func(t *testing.T) {
var expected session.Session
require.NoError(t, faker.FakeData(&expected))
expected.Active = true
require.NoError(t, p.CreateIdentity(ctx, expected.Identity))
require.NoError(t, p.UpsertSession(ctx, &expected))
actual, err := p.GetSession(ctx, expected.ID, session.ExpandNothing)
require.NoError(t, err)
assert.True(t, actual.Active)
t.Run("on another network", func(t *testing.T) {
_, other := testhelpers.NewNetwork(t, ctx, p)
err := other.RevokeSessionById(ctx, expected.ID)
assert.ErrorIs(t, err, sqlcon.ErrNoRows())
actual, err = p.GetSession(ctx, expected.ID, session.ExpandNothing)
require.NoError(t, err)
assert.True(t, actual.Active)
})
require.NoError(t, p.RevokeSessionById(ctx, expected.ID))
actual, err = p.GetSession(ctx, expected.ID, session.ExpandNothing)
require.NoError(t, err)
assert.False(t, actual.Active)
// Re-revoking an already-inactive session is silently
// idempotent — the admin disableSession handler relies on it.
require.NoError(t, p.RevokeSessionById(ctx, expected.ID),
"re-revoking an already-inactive session must succeed silently on %s",
p.GetConnection(ctx).Dialect.Name())
actual, err = p.GetSession(ctx, expected.ID, session.ExpandNothing)
require.NoError(t, err, "session must still exist after re-revoke")
assert.False(t, actual.Active, "session must remain inactive after re-revoke")
t.Run("returns ErrNoRows for an unknown id", func(t *testing.T) {
err := p.RevokeSessionById(ctx, x.NewUUID())
assert.ErrorIs(t, err, sqlcon.ErrNoRows())
})
})
t.Run("method=revoke other sessions for identity", func(t *testing.T) {
// here we set up 2 identities with each having 2 sessions
sessions := make([]session.Session, 4)
for i := range sessions {
require.NoError(t, faker.FakeData(&sessions[i]))
}
require.NoError(t, p.CreateIdentity(ctx, sessions[0].Identity))
require.NoError(t, p.CreateIdentity(ctx, sessions[2].Identity))
sessions[1].IdentityID, sessions[1].Identity = sessions[0].IdentityID, sessions[0].Identity
sessions[3].IdentityID, sessions[3].Identity = sessions[2].IdentityID, sessions[2].Identity
for i := range sessions {
sessions[i].Active = true
require.NoError(t, p.UpsertSession(ctx, &sessions[i]))
}
t.Run("on another network", func(t *testing.T) {
_, other := testhelpers.NewNetwork(t, ctx, p)
n, err := other.RevokeSessionsIdentityExcept(ctx, sessions[0].IdentityID, sessions[0].ID)
require.NoError(t, err)
assert.Equal(t, 0, n)
for _, s := range sessions {
actual, err := p.GetSession(ctx, s.ID, session.ExpandNothing)
require.NoError(t, err)
assert.True(t, actual.Active)
}
})
n, err := p.RevokeSessionsIdentityExcept(ctx, sessions[0].IdentityID, sessions[0].ID)
require.NoError(t, err)
assert.Equal(t, 1, n)
actual, total, err := p.ListSessionsByIdentity(ctx, sessions[0].IdentityID, nil, 1, 10, uuid.Nil, session.ExpandNothing)
require.NoError(t, err)
require.Len(t, actual, 2)
require.Equal(t, int64(2), total)
if actual[0].ID == sessions[0].ID {
assert.True(t, actual[0].Active)
assert.False(t, actual[1].Active)
} else {
assert.Equal(t, actual[0].ID, sessions[1].ID)
assert.True(t, actual[1].Active)
assert.False(t, actual[0].Active)
}
otherIdentitiesSessions, total, err := p.ListSessionsByIdentity(ctx, sessions[2].IdentityID, nil, 1, 10, uuid.Nil, session.ExpandNothing)
require.NoError(t, err)
require.Len(t, actual, 2)
require.Equal(t, int64(2), total)
for _, s := range otherIdentitiesSessions {
assert.True(t, s.Active)
}
t.Run("returns matched count, including already-inactive sessions", func(t *testing.T) {
// One identity, three sessions: the keeper, one already-
// inactive other, one active other. Returned count must be 2
// (both "other" sessions matched the predicate) — not 1
// (only the active "other" was actually flipped). The CTE on
// CRDB/Postgres must preserve matched-count, not flipped-count.
var keeper session.Session
require.NoError(t, faker.FakeData(&keeper))
keeper.Active = true
require.NoError(t, p.CreateIdentity(ctx, keeper.Identity))
require.NoError(t, p.UpsertSession(ctx, &keeper))
makeSibling := func() session.Session {
var s session.Session
require.NoError(t, faker.FakeData(&s))
s.Identity = keeper.Identity
s.IdentityID = keeper.IdentityID
s.Active = true
require.NoError(t, p.UpsertSession(ctx, &s))
return s
}
alreadyInactive := makeSibling()
require.NoError(t, p.RevokeSessionById(ctx, alreadyInactive.ID))
_ = makeSibling() // stillActive — will be flipped by the call below
n, err := p.RevokeSessionsIdentityExcept(ctx, keeper.IdentityID, keeper.ID)
require.NoError(t, err)
assert.Equal(t, 2, n,
"matched count must include already-inactive sessions on %s",
p.GetConnection(ctx).Dialect.Name())
})
})
t.Run("method=revoke specific session for identity", func(t *testing.T) {
sessions := make([]session.Session, 2)
for i := range sessions {
require.NoError(t, faker.FakeData(&sessions[i]))
}
require.NoError(t, p.CreateIdentity(ctx, sessions[0].Identity))
sessions[1].IdentityID, sessions[1].Identity = sessions[0].IdentityID, sessions[0].Identity
for i := range sessions {
sessions[i].Active = true
require.NoError(t, p.UpsertSession(ctx, &sessions[i]))
}
t.Run("on another network", func(t *testing.T) {
_, other := testhelpers.NewNetwork(t, ctx, p)
require.NoError(t, other.RevokeSession(ctx, sessions[0].IdentityID, sessions[0].ID))
for _, s := range sessions {
actual, err := p.GetSession(ctx, s.ID, session.ExpandNothing)
require.NoError(t, err)
assert.True(t, actual.Active)
}
})
require.NoError(t, p.RevokeSession(ctx, sessions[0].IdentityID, sessions[0].ID))
actual, total, err := p.ListSessionsByIdentity(ctx, sessions[0].IdentityID, nil, 1, 10, uuid.Nil, session.ExpandNothing)
require.NoError(t, err)
require.Len(t, actual, 2)
require.Equal(t, int64(2), total)
if actual[0].ID == sessions[0].ID {
assert.False(t, actual[0].Active)
assert.True(t, actual[1].Active)
} else {
assert.Equal(t, actual[0].ID, sessions[1].ID)
assert.False(t, actual[1].Active)
assert.True(t, actual[0].Active)
}
// Re-revoking an already-inactive session must succeed silently —
// the `AND active = true` predicate added on the CRDB/Postgres path
// must not change the function's nil-on-no-rows contract.
require.NoError(t, p.RevokeSession(ctx, sessions[0].IdentityID, sessions[0].ID),
"re-revoking an already-inactive session must succeed silently")
// Wrong identity_id for an existing session id is a silent no-op,
// not an error.
require.NoError(t, p.RevokeSession(ctx, x.NewUUID(), sessions[1].ID))
actual1, err := p.GetSession(ctx, sessions[1].ID, session.ExpandNothing)
require.NoError(t, err)
assert.True(t, actual1.Active, "session must stay active when identity_id mismatches")
})
t.Run("case=delete session for", func(t *testing.T) {
var expected1 session.Session
var expected2 session.Session
require.NoError(t, faker.FakeData(&expected1))
require.NoError(t, p.CreateIdentity(ctx, expected1.Identity))
require.NoError(t, p.UpsertSession(ctx, &expected1))
require.NoError(t, faker.FakeData(&expected2))
expected2.Identity = expected1.Identity
expected2.IdentityID = expected1.IdentityID
require.NoError(t, p.UpsertSession(ctx, &expected2))
t.Run("on another network", func(t *testing.T) {
_, other := testhelpers.NewNetwork(t, ctx, p)
err := other.DeleteSessionsByIdentity(ctx, expected2.IdentityID)
assert.ErrorIs(t, err, sqlcon.ErrNoRows())
_, err = p.GetSession(ctx, expected1.ID, session.ExpandNothing)
require.NoError(t, err)
})
require.NoError(t, p.DeleteSessionsByIdentity(ctx, expected2.IdentityID))
_, err := p.GetSession(ctx, expected1.ID, session.ExpandNothing)
require.Error(t, err)
_, err = p.GetSession(ctx, expected2.ID, session.ExpandNothing)
require.Error(t, err)
})
t.Run("network isolation", func(t *testing.T) {
nid1, p := testhelpers.NewNetwork(t, ctx, p)
nid2, _ := testhelpers.NewNetwork(t, ctx, p)
iid1, iid2 := x.NewUUID(), x.NewUUID()
require.NoError(t, p.GetConnection(ctx).RawQuery("INSERT INTO identities (id, nid, schema_id, traits, created_at, updated_at) VALUES (?, ?, 'default', '{}', ?, ?)", iid1, nid1, time.Now(), time.Now()).Exec())
require.NoError(t, p.GetConnection(ctx).RawQuery("INSERT INTO identities (id, nid, schema_id, traits, created_at, updated_at) VALUES (?, ?, 'default', '{}', ?, ?)", iid2, nid2, time.Now(), time.Now()).Exec())
t1, t2 := randx.MustString(32, randx.AlphaNum), randx.MustString(32, randx.AlphaNum)
sid1, sid2 := x.NewUUID(), x.NewUUID()
require.NoError(t, p.GetConnection(ctx).RawQuery("INSERT INTO sessions (id, nid, identity_id, token, expires_at,authenticated_at, created_at, updated_at, logout_token, authentication_methods) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", sid1, nid1, iid1, t1, time.Now().Add(time.Hour), time.Now(), time.Now(), time.Now(), randx.MustString(32, randx.AlphaNum), "[]").Exec())
require.NoError(t, p.GetConnection(ctx).RawQuery("INSERT INTO sessions (id, nid, identity_id, token, expires_at,authenticated_at, created_at, updated_at, logout_token, authentication_methods) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", sid2, nid2, iid2, t2, time.Now().Add(time.Hour), time.Now(), time.Now(), time.Now(), randx.MustString(32, randx.AlphaNum), "[]").Exec())
_, err := p.GetSession(ctx, sid1, session.ExpandEverything)
require.NoError(t, err)
_, err = p.GetSession(ctx, sid2, session.ExpandNothing)
require.ErrorIs(t, err, sqlcon.ErrNoRows())
_, err = p.GetSessionByToken(ctx, t1, session.ExpandNothing, identity.ExpandDefault)
require.NoError(t, err)
_, err = p.GetSessionByToken(ctx, t2, session.ExpandNothing, identity.ExpandDefault)
require.ErrorIs(t, err, sqlcon.ErrNoRows())
})
t.Run("extend session lifespan but min time is not yet reached", func(t *testing.T) {
ctx := contextx.WithConfigValues(ctx, map[string]any{config.ViperKeySessionRefreshMinTimeLeft: 2 * time.Hour})
var expected session.Session
require.NoError(t, faker.FakeData(&expected))
expected.ExpiresAt = time.Now().Add(time.Hour * 10).Round(time.Second).UTC()
require.NoError(t, p.CreateIdentity(ctx, expected.Identity))
require.NoError(t, p.UpsertSession(ctx, &expected))
require.NoError(t, p.ExtendSession(ctx, expected.ID))
actual, err := p.GetSession(ctx, expected.ID, session.ExpandNothing)
require.NoError(t, err)
assert.Equal(t, expected.ExpiresAt, actual.ExpiresAt)
})
t.Run("extend session lifespan", func(t *testing.T) {
ctx := contextx.WithConfigValues(ctx, map[string]any{config.ViperKeySessionRefreshMinTimeLeft: 2 * time.Hour})
var expected session.Session
require.NoError(t, faker.FakeData(&expected))
expected.ExpiresAt = time.Now().Add(time.Hour).UTC()
require.NoError(t, p.CreateIdentity(ctx, expected.Identity))
require.NoError(t, p.UpsertSession(ctx, &expected))
expectedExpiry := expected.Refresh(ctx, conf).ExpiresAt
require.NoError(t, p.ExtendSession(ctx, expected.ID))
actual, err := p.GetSession(ctx, expected.ID, session.ExpandNothing)
require.NoError(t, err)
assert.GreaterOrEqual(t, 10*time.Second, expectedExpiry.Sub(actual.ExpiresAt).Abs())
})
t.Run("extend session lifespan on CockroachDB", func(t *testing.T) {
if p.GetConnection(ctx).Dialect.Name() != dbal.DriverCockroachDB {
t.Skip("Skipping test because driver is not CockroachDB")
}
ctx := contextx.WithConfigValue(ctx, config.ViperKeySessionRefreshMinTimeLeft, 2*time.Hour)
var expected session.Session
require.NoError(t, faker.FakeData(&expected))
expected.ExpiresAt = time.Now().Add(time.Hour).UTC()
require.NoError(t, p.CreateIdentity(ctx, expected.Identity))
require.NoError(t, p.UpsertSession(ctx, &expected))
expectedExpiry := expected.Refresh(ctx, conf).ExpiresAt
foundExpectedCockroachError := atomic.Bool{}
g := errgroup.Group{}
for range 10 {
g.Go(func() error {
err := p.ExtendSession(ctx, expected.ID)
if errors.Is(err, sqlcon.ErrNoRows()) {
foundExpectedCockroachError.Store(true)
return nil
}
return err
})
}
require.NoError(t, g.Wait())
actual, err := p.GetSession(ctx, expected.ID, session.ExpandNothing)
require.NoError(t, err)
assert.LessOrEqual(t, expectedExpiry.Sub(actual.ExpiresAt).Abs(), 10*time.Second)
assert.True(t, foundExpectedCockroachError.Load(), "We expect to find a not found error caused by ... FOR UPDATE SKIP LOCKED")
})
// Helpers for the manage-sessions persister batch tests below. Each
// helper creates an active session belonging to a fresh identity and
// returns both, so callers can mutate state independently across
// subtests without sharing fixtures.
newActiveSession := func(t *testing.T) *session.Session {
t.Helper()
var s session.Session
require.NoError(t, faker.FakeData(&s))
s.Active = true
require.NoError(t, p.CreateIdentity(ctx, s.Identity))
require.NoError(t, p.UpsertSession(ctx, &s))
return &s
}
isActive := func(t *testing.T, sID uuid.UUID) bool {
t.Helper()
s, err := p.GetSession(ctx, sID, session.ExpandNothing)
require.NoError(t, err)
return s.Active
}
exists := func(t *testing.T, sID uuid.UUID) bool {
t.Helper()
_, err := p.GetSession(ctx, sID, session.ExpandNothing)
if errors.Is(err, sqlcon.ErrNoRows()) {
return false
}
require.NoError(t, err)
return true
}
t.Run("case=RevokeSessionsByIdentities updates only matching active rows", func(t *testing.T) {
s1 := newActiveSession(t)
s2 := newActiveSession(t)
other := newActiveSession(t)
n, err := p.RevokeSessionsByIdentities(ctx, []uuid.UUID{s1.IdentityID, s2.IdentityID})
require.NoError(t, err)
assert.Equal(t, 2, n)
assert.False(t, isActive(t, s1.ID))
assert.False(t, isActive(t, s2.ID))
assert.True(t, isActive(t, other.ID), "untargeted session must stay active")
n, err = p.RevokeSessionsByIdentities(ctx, []uuid.UUID{s1.IdentityID})
require.NoError(t, err)
assert.Equal(t, 0, n, "already-inactive sessions must not be re-touched")
})
t.Run("case=RevokeSessionsByIdentities is network-isolated", func(t *testing.T) {
s := newActiveSession(t)
_, other := testhelpers.NewNetwork(t, ctx, p)
n, err := other.RevokeSessionsByIdentities(ctx, []uuid.UUID{s.IdentityID})
require.NoError(t, err)
assert.Equal(t, 0, n)
assert.True(t, isActive(t, s.ID))
})
t.Run("case=RevokeSessionsByIdentities with empty slice is no-op", func(t *testing.T) {
n, err := p.RevokeSessionsByIdentities(ctx, nil)
require.NoError(t, err)
assert.Equal(t, 0, n)
})
t.Run("case=RevokeSessionsByIDs updates only matching active rows", func(t *testing.T) {
s1 := newActiveSession(t)
s2 := newActiveSession(t)
untouched := newActiveSession(t)
n, err := p.RevokeSessionsByIDs(ctx, []uuid.UUID{s1.ID, s2.ID})
require.NoError(t, err)
assert.Equal(t, 2, n)
assert.False(t, isActive(t, s1.ID))
assert.False(t, isActive(t, s2.ID))
assert.True(t, isActive(t, untouched.ID))
n, err = p.RevokeSessionsByIDs(ctx, []uuid.UUID{s1.ID})
require.NoError(t, err)
assert.Equal(t, 0, n, "already-inactive sessions must not be re-touched")
})
t.Run("case=RevokeSessionsByIDs is network-isolated", func(t *testing.T) {
s := newActiveSession(t)
_, other := testhelpers.NewNetwork(t, ctx, p)
n, err := other.RevokeSessionsByIDs(ctx, []uuid.UUID{s.ID})
require.NoError(t, err)
assert.Equal(t, 0, n)
assert.True(t, isActive(t, s.ID))
})
t.Run("case=RevokeSessionsByIDs with empty slice is no-op", func(t *testing.T) {
n, err := p.RevokeSessionsByIDs(ctx, nil)
require.NoError(t, err)
assert.Equal(t, 0, n)
})
t.Run("case=RevokeAllSessions deactivates only currently-active rows in this network", func(t *testing.T) {
_, scoped := testhelpers.NewNetwork(t, ctx, p)
active1 := newActiveSession(t)
active2 := newActiveSession(t)
// Create one session that's already inactive, in the original network.
alreadyInactive := newActiveSession(t)
require.NoError(t, p.RevokeSessionById(ctx, alreadyInactive.ID))
// Create a session in the other network — must remain untouched.
var foreign session.Session
require.NoError(t, faker.FakeData(&foreign))
foreign.Active = true
require.NoError(t, scoped.CreateIdentity(ctx, foreign.Identity))
require.NoError(t, scoped.UpsertSession(ctx, &foreign))
n, err := p.RevokeAllSessions(ctx, 100)
require.NoError(t, err)
assert.GreaterOrEqual(t, n, 2, "must have flipped active1 and active2 at least")
assert.False(t, isActive(t, active1.ID))
assert.False(t, isActive(t, active2.ID))
// The foreign session must still be active when read via its own network.
fs, err := scoped.GetSession(ctx, foreign.ID, session.ExpandNothing)
require.NoError(t, err)
assert.True(t, fs.Active, "foreign-network session must not be touched")
})
t.Run("case=RevokeAllSessions caps the work at limit", func(t *testing.T) {
// Isolated network so previous tests can't influence the count.
_, scoped := testhelpers.NewNetwork(t, ctx, p)
ids := make([]uuid.UUID, 3)
for i := range ids {
var s session.Session
require.NoError(t, faker.FakeData(&s))
s.Active = true
require.NoError(t, scoped.CreateIdentity(ctx, s.Identity))
require.NoError(t, scoped.UpsertSession(ctx, &s))
ids[i] = s.ID
}
n, err := scoped.RevokeAllSessions(ctx, 2)
require.NoError(t, err)
assert.Equal(t, 2, n, "must flip exactly limit rows")
activeCount := 0
for _, id := range ids {
s, err := scoped.GetSession(ctx, id, session.ExpandNothing)
require.NoError(t, err)
if s.Active {
activeCount++
}
}
assert.Equal(t, 1, activeCount, "limit must leave one session active")
})
t.Run("case=DeleteSessionsByIdentities removes matching rows", func(t *testing.T) {
s1 := newActiveSession(t)
s2 := newActiveSession(t)
survivor := newActiveSession(t)
n, err := p.DeleteSessionsByIdentities(ctx, []uuid.UUID{s1.IdentityID, s2.IdentityID})
require.NoError(t, err)
assert.Equal(t, 2, n)
assert.False(t, exists(t, s1.ID))
assert.False(t, exists(t, s2.ID))
assert.True(t, exists(t, survivor.ID))
})
t.Run("case=DeleteSessionsByIdentities is network-isolated", func(t *testing.T) {
s := newActiveSession(t)
_, other := testhelpers.NewNetwork(t, ctx, p)
n, err := other.DeleteSessionsByIdentities(ctx, []uuid.UUID{s.IdentityID})
require.NoError(t, err)
assert.Equal(t, 0, n)
assert.True(t, exists(t, s.ID))
})
t.Run("case=DeleteSessionsByIDs removes matching rows", func(t *testing.T) {
s1 := newActiveSession(t)
s2 := newActiveSession(t)
survivor := newActiveSession(t)
n, err := p.DeleteSessionsByIDs(ctx, []uuid.UUID{s1.ID, s2.ID})
require.NoError(t, err)
assert.Equal(t, 2, n)
assert.False(t, exists(t, s1.ID))
assert.False(t, exists(t, s2.ID))
assert.True(t, exists(t, survivor.ID))
})
t.Run("case=DeleteSessionsByIDs is network-isolated", func(t *testing.T) {
s := newActiveSession(t)
_, other := testhelpers.NewNetwork(t, ctx, p)
n, err := other.DeleteSessionsByIDs(ctx, []uuid.UUID{s.ID})
require.NoError(t, err)
assert.Equal(t, 0, n)
assert.True(t, exists(t, s.ID))
})
t.Run("case=DeleteAllSessions removes only this network's rows", func(t *testing.T) {
_, scoped := testhelpers.NewNetwork(t, ctx, p)
s1 := newActiveSession(t)
s2 := newActiveSession(t)
var foreign session.Session
require.NoError(t, faker.FakeData(&foreign))
foreign.Active = true
require.NoError(t, scoped.CreateIdentity(ctx, foreign.Identity))
require.NoError(t, scoped.UpsertSession(ctx, &foreign))
n, err := p.DeleteAllSessions(ctx, 100)
require.NoError(t, err)
assert.GreaterOrEqual(t, n, 2)
assert.False(t, exists(t, s1.ID))
assert.False(t, exists(t, s2.ID))
fs, err := scoped.GetSession(ctx, foreign.ID, session.ExpandNothing)
require.NoError(t, err)
assert.NotNil(t, fs, "foreign-network session must survive")
})
}
}