mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-20 14:34:25 +00:00
# Which Problems Are Solved This PR introduces the possibility of using the relational DB when calling the ListSession gRPC endpoint. # How the Problems Are Solved - Implement the Querier interface for ListSession: - Create a `domain` model for the Session object to be able to map the gRPC session into it. - Match the permission filters on ES side to be sure the same sessions are returned - Implement converter methods to go from `gRPC` to `domain` models (both ways) - Fix the session sql relational migration: rework the trigger to update the `expiration` field to perform the update only when the input `lifetime` has a non-zero value. - Link the Querier implementation with the gRPC endpoint - Run the same integration tests against the relational DB, making sure the outcome is the same. ⚠️ Due to missing permission checks, some divergence is expected. Hence, specific code to circumvent the differences has been put in place # Additional Context - Closes #11039
1576 lines
43 KiB
Go
1576 lines
43 KiB
Go
package repository_test
|
|
|
|
import (
|
|
"net"
|
|
"net/http"
|
|
"slices"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/brianvoe/gofakeit/v6"
|
|
"github.com/muhlemmer/gu"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/text/language"
|
|
|
|
"github.com/zitadel/zitadel/backend/v3/domain"
|
|
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
|
"github.com/zitadel/zitadel/backend/v3/storage/database/repository"
|
|
"github.com/zitadel/zitadel/internal/crypto"
|
|
zdomain "github.com/zitadel/zitadel/internal/domain"
|
|
)
|
|
|
|
func TestSession_Create(t *testing.T) {
|
|
beforeCreate := time.Now()
|
|
tx, err := pool.Begin(t.Context(), nil)
|
|
require.NoError(t, err)
|
|
defer func() {
|
|
err := tx.Rollback(t.Context())
|
|
if err != nil {
|
|
t.Logf("error during rollback: %v", err)
|
|
}
|
|
}()
|
|
|
|
sessionRepo := repository.SessionRepository()
|
|
|
|
instanceRepo := repository.InstanceRepository()
|
|
instanceId := gofakeit.Name()
|
|
instance := domain.Instance{
|
|
ID: instanceId,
|
|
Name: gofakeit.Name(),
|
|
DefaultOrgID: "defaultOrgId",
|
|
IAMProjectID: "iamProject",
|
|
ConsoleClientID: "managementConsoleClient",
|
|
ConsoleAppID: "managementConsoleApp",
|
|
DefaultLanguage: "defaultLanguage",
|
|
}
|
|
err = instanceRepo.Create(t.Context(), tx, &instance)
|
|
require.NoError(t, err)
|
|
tests := []struct {
|
|
name string
|
|
session *domain.Session
|
|
err error
|
|
}{
|
|
{
|
|
name: "create valid session",
|
|
session: &domain.Session{
|
|
InstanceID: instanceId,
|
|
ID: gofakeit.Name(),
|
|
CreatedAt: beforeCreate,
|
|
UpdatedAt: beforeCreate,
|
|
Lifetime: time.Hour * 24,
|
|
CreatorID: gofakeit.Name(),
|
|
UserAgent: &domain.SessionUserAgent{
|
|
FingerprintID: gu.Ptr(gofakeit.Name()),
|
|
Description: gu.Ptr(gofakeit.Name()),
|
|
IP: net.IPv4(127, 0, 0, 1),
|
|
Header: http.Header{"User-Agent": []string{"user-agent"}},
|
|
},
|
|
},
|
|
err: nil,
|
|
},
|
|
{
|
|
name: "create valid session no dates",
|
|
session: &domain.Session{
|
|
InstanceID: instanceId,
|
|
ID: gofakeit.Name(),
|
|
Lifetime: time.Hour * 24,
|
|
CreatorID: gofakeit.Name(),
|
|
UserAgent: &domain.SessionUserAgent{
|
|
FingerprintID: gu.Ptr(gofakeit.Name()),
|
|
Description: gu.Ptr(gofakeit.Name()),
|
|
IP: net.IPv4(127, 0, 0, 1),
|
|
Header: http.Header{"User-Agent": []string{"user-agent"}},
|
|
},
|
|
},
|
|
err: nil,
|
|
},
|
|
{
|
|
name: "create session user agent fingerprint only",
|
|
session: &domain.Session{
|
|
InstanceID: instanceId,
|
|
ID: gofakeit.Name(),
|
|
Lifetime: time.Hour * 24,
|
|
CreatorID: gofakeit.Name(),
|
|
UserAgent: &domain.SessionUserAgent{
|
|
FingerprintID: gu.Ptr(gofakeit.Name()),
|
|
},
|
|
},
|
|
err: nil,
|
|
},
|
|
{
|
|
name: "create session empty fingerprint",
|
|
session: &domain.Session{
|
|
InstanceID: instanceId,
|
|
ID: gofakeit.Name(),
|
|
Lifetime: time.Hour * 24,
|
|
CreatorID: gofakeit.Name(),
|
|
UserAgent: &domain.SessionUserAgent{
|
|
FingerprintID: gu.Ptr(""),
|
|
},
|
|
},
|
|
err: new(database.CheckError),
|
|
},
|
|
{
|
|
name: "create session without user agent",
|
|
session: &domain.Session{
|
|
InstanceID: instanceId,
|
|
ID: gofakeit.Name(),
|
|
Lifetime: time.Hour * 24,
|
|
CreatorID: gofakeit.Name(),
|
|
},
|
|
err: nil,
|
|
},
|
|
{
|
|
name: "create session without lifetime",
|
|
session: &domain.Session{
|
|
InstanceID: instanceId,
|
|
ID: gofakeit.Name(),
|
|
CreatorID: gofakeit.Name(),
|
|
},
|
|
err: nil,
|
|
},
|
|
{
|
|
name: "create session without creatorID",
|
|
session: &domain.Session{
|
|
InstanceID: instanceId,
|
|
ID: gofakeit.Name(),
|
|
Lifetime: time.Hour * 24,
|
|
},
|
|
err: nil,
|
|
},
|
|
{
|
|
name: "create session without id",
|
|
session: &domain.Session{
|
|
InstanceID: instanceId,
|
|
Lifetime: time.Hour * 24,
|
|
CreatorID: gofakeit.Name(),
|
|
},
|
|
err: new(database.CheckError),
|
|
},
|
|
{
|
|
name: "create session without instanceID",
|
|
session: &domain.Session{
|
|
ID: gofakeit.Name(),
|
|
Lifetime: time.Hour * 24,
|
|
CreatorID: gofakeit.Name(),
|
|
},
|
|
err: new(database.ForeignKeyError),
|
|
},
|
|
{
|
|
name: "create session without existent instanceID",
|
|
session: &domain.Session{
|
|
InstanceID: gofakeit.Name(),
|
|
ID: gofakeit.Name(),
|
|
Lifetime: time.Hour * 24,
|
|
CreatorID: gofakeit.Name(),
|
|
},
|
|
err: new(database.ForeignKeyError),
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
savepoint, err := tx.Begin(t.Context())
|
|
require.NoError(t, err)
|
|
defer func() {
|
|
err = savepoint.Rollback(t.Context())
|
|
if err != nil {
|
|
t.Logf("error during rollback: %v", err)
|
|
}
|
|
}()
|
|
|
|
err = sessionRepo.Create(t.Context(), savepoint, tt.session)
|
|
assert.ErrorIs(t, err, tt.err)
|
|
if err != nil {
|
|
return
|
|
}
|
|
afterCreate := time.Now()
|
|
|
|
// check session values
|
|
createdSession, err := sessionRepo.Get(t.Context(), savepoint,
|
|
database.WithCondition(sessionRepo.PrimaryKeyCondition(tt.session.InstanceID, tt.session.ID)),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, tt.session.ID, createdSession.ID)
|
|
assert.Equal(t, tt.session.InstanceID, createdSession.InstanceID)
|
|
assert.Equal(t, tt.session.Lifetime, createdSession.Lifetime)
|
|
if tt.session.Lifetime != 0 {
|
|
assert.Equal(t, createdSession.UpdatedAt.Add(tt.session.Lifetime), createdSession.Expiration)
|
|
} else {
|
|
assert.Zero(t, createdSession.Expiration)
|
|
}
|
|
assert.Equal(t, tt.session.CreatorID, createdSession.CreatorID)
|
|
assert.Equal(t, tt.session.UserAgent, createdSession.UserAgent)
|
|
assert.WithinRange(t, createdSession.CreatedAt, beforeCreate.Add(-time.Second), afterCreate.Add(time.Second))
|
|
assert.WithinRange(t, createdSession.UpdatedAt, beforeCreate.Add(-time.Second), afterCreate.Add(time.Second))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSession_Update(t *testing.T) {
|
|
beforeUpdate := time.Now()
|
|
tx, err := pool.Begin(t.Context(), nil)
|
|
require.NoError(t, err)
|
|
defer func() {
|
|
err := tx.Rollback(t.Context())
|
|
if err != nil {
|
|
t.Logf("error during rollback: %v", err)
|
|
}
|
|
}()
|
|
|
|
instanceRepo := repository.InstanceRepository()
|
|
sessionRepo := repository.SessionRepository()
|
|
|
|
// create instance
|
|
instanceId := gofakeit.Name()
|
|
instance := domain.Instance{
|
|
ID: instanceId,
|
|
Name: gofakeit.Name(),
|
|
DefaultOrgID: "defaultOrgId",
|
|
IAMProjectID: "iamProject",
|
|
ConsoleClientID: "managementConsoleClient",
|
|
ConsoleAppID: "managementConsoleApp",
|
|
DefaultLanguage: "defaultLanguage",
|
|
}
|
|
err = instanceRepo.Create(t.Context(), tx, &instance)
|
|
require.NoError(t, err)
|
|
|
|
orgID := createOrganization(t, tx, instanceId)
|
|
userID := createHumanUser(t, tx, instanceId, orgID)
|
|
|
|
testNow := time.Now()
|
|
tests := []struct {
|
|
name string
|
|
testFunc func(t *testing.T) *domain.Session
|
|
update []database.Change
|
|
rowsAffected int64
|
|
err error
|
|
}{
|
|
{
|
|
name: "update no changes",
|
|
testFunc: func(t *testing.T) *domain.Session {
|
|
return &domain.Session{
|
|
InstanceID: instanceId,
|
|
ID: gofakeit.Name(),
|
|
Lifetime: time.Hour * 24,
|
|
CreatorID: gofakeit.Name(),
|
|
}
|
|
},
|
|
update: []database.Change{},
|
|
rowsAffected: 0,
|
|
err: database.ErrNoChanges,
|
|
},
|
|
{
|
|
name: "update non existent session",
|
|
testFunc: func(t *testing.T) *domain.Session {
|
|
return &domain.Session{
|
|
InstanceID: instanceId,
|
|
ID: gofakeit.Name(),
|
|
Lifetime: time.Hour * 24,
|
|
CreatorID: gofakeit.Name(),
|
|
}
|
|
},
|
|
update: []database.Change{
|
|
sessionRepo.SetLifetime(time.Hour * 48),
|
|
},
|
|
rowsAffected: 0,
|
|
},
|
|
{
|
|
name: "update session updated at",
|
|
testFunc: func(t *testing.T) *domain.Session {
|
|
session := &domain.Session{
|
|
InstanceID: instanceId,
|
|
ID: gofakeit.Name(),
|
|
Lifetime: time.Hour * 24,
|
|
CreatorID: gofakeit.Name(),
|
|
}
|
|
err := sessionRepo.Create(t.Context(), tx, session)
|
|
require.NoError(t, err)
|
|
|
|
// update with updated values
|
|
session.UpdatedAt = testNow
|
|
return session
|
|
},
|
|
update: []database.Change{
|
|
sessionRepo.SetUpdatedAt(testNow),
|
|
},
|
|
rowsAffected: 1,
|
|
},
|
|
{
|
|
name: "update session lifetime",
|
|
testFunc: func(t *testing.T) *domain.Session {
|
|
session := &domain.Session{
|
|
InstanceID: instanceId,
|
|
ID: gofakeit.Name(),
|
|
Lifetime: time.Hour * 24,
|
|
CreatorID: gofakeit.Name(),
|
|
}
|
|
err := sessionRepo.Create(t.Context(), tx, session)
|
|
require.NoError(t, err)
|
|
|
|
// update with updated values
|
|
session.Lifetime = time.Hour * 48
|
|
return session
|
|
},
|
|
update: []database.Change{
|
|
sessionRepo.SetLifetime(time.Hour * 48),
|
|
},
|
|
rowsAffected: 1,
|
|
},
|
|
{
|
|
name: "update session token",
|
|
testFunc: func(t *testing.T) *domain.Session {
|
|
session := &domain.Session{
|
|
InstanceID: instanceId,
|
|
ID: gofakeit.Name(),
|
|
Lifetime: time.Hour * 24,
|
|
CreatorID: gofakeit.Name(),
|
|
}
|
|
err := sessionRepo.Create(t.Context(), tx, session)
|
|
require.NoError(t, err)
|
|
|
|
// update with updated values
|
|
session.TokenID = "new-token"
|
|
return session
|
|
},
|
|
update: []database.Change{
|
|
sessionRepo.SetToken("new-token"),
|
|
},
|
|
rowsAffected: 1,
|
|
},
|
|
{
|
|
name: "update session passkey challenge",
|
|
testFunc: func(t *testing.T) *domain.Session {
|
|
session := &domain.Session{
|
|
InstanceID: instanceId,
|
|
ID: gofakeit.Name(),
|
|
Lifetime: time.Hour * 24,
|
|
CreatorID: gofakeit.Name(),
|
|
}
|
|
err := sessionRepo.Create(t.Context(), tx, session)
|
|
require.NoError(t, err)
|
|
|
|
// update with updated values
|
|
session.Challenges = []domain.SessionChallenge{
|
|
&domain.SessionChallengePasskey{
|
|
LastChallengedAt: testNow,
|
|
Challenge: "challenge",
|
|
AllowedCredentialIDs: [][]byte{[]byte("allowed-id-1"), []byte("allowed-id-2")},
|
|
UserVerification: zdomain.UserVerificationRequirementRequired,
|
|
RPID: "rp-id",
|
|
},
|
|
}
|
|
return session
|
|
},
|
|
update: []database.Change{
|
|
sessionRepo.SetChallenge(&domain.SessionChallengePasskey{
|
|
LastChallengedAt: testNow,
|
|
Challenge: "challenge",
|
|
AllowedCredentialIDs: [][]byte{[]byte("allowed-id-1"), []byte("allowed-id-2")},
|
|
UserVerification: zdomain.UserVerificationRequirementRequired,
|
|
RPID: "rp-id",
|
|
}),
|
|
},
|
|
rowsAffected: 1,
|
|
},
|
|
{
|
|
name: "update session otp sms challenge",
|
|
testFunc: func(t *testing.T) *domain.Session {
|
|
session := &domain.Session{
|
|
InstanceID: instanceId,
|
|
ID: gofakeit.Name(),
|
|
Lifetime: time.Hour * 24,
|
|
CreatorID: gofakeit.Name(),
|
|
}
|
|
err := sessionRepo.Create(t.Context(), tx, session)
|
|
require.NoError(t, err)
|
|
|
|
// update with updated values
|
|
session.Challenges = []domain.SessionChallenge{
|
|
&domain.SessionChallengeOTPSMS{
|
|
LastChallengedAt: testNow,
|
|
Code: &crypto.CryptoValue{
|
|
CryptoType: crypto.TypeEncryption,
|
|
Algorithm: "enc",
|
|
KeyID: "key-id",
|
|
Crypted: []byte("code"),
|
|
},
|
|
Expiry: time.Minute * 10,
|
|
CodeReturned: false,
|
|
GeneratorID: "",
|
|
TriggeredAtOrigin: "https://example.com",
|
|
},
|
|
}
|
|
return session
|
|
},
|
|
update: []database.Change{
|
|
sessionRepo.SetChallenge(&domain.SessionChallengeOTPSMS{
|
|
LastChallengedAt: testNow,
|
|
Code: &crypto.CryptoValue{
|
|
CryptoType: crypto.TypeEncryption,
|
|
Algorithm: "enc",
|
|
KeyID: "key-id",
|
|
Crypted: []byte("code"),
|
|
},
|
|
Expiry: time.Minute * 10,
|
|
CodeReturned: false,
|
|
GeneratorID: "",
|
|
TriggeredAtOrigin: "https://example.com",
|
|
}),
|
|
},
|
|
rowsAffected: 1,
|
|
},
|
|
{
|
|
name: "update session otp email challenge",
|
|
testFunc: func(t *testing.T) *domain.Session {
|
|
session := &domain.Session{
|
|
InstanceID: instanceId,
|
|
ID: gofakeit.Name(),
|
|
Lifetime: time.Hour * 24,
|
|
CreatorID: gofakeit.Name(),
|
|
}
|
|
err := sessionRepo.Create(t.Context(), tx, session)
|
|
require.NoError(t, err)
|
|
|
|
// update with updated values
|
|
session.Challenges = []domain.SessionChallenge{
|
|
&domain.SessionChallengeOTPEmail{
|
|
LastChallengedAt: testNow,
|
|
Code: &crypto.CryptoValue{
|
|
CryptoType: crypto.TypeEncryption,
|
|
Algorithm: "enc",
|
|
KeyID: "key-id",
|
|
Crypted: []byte("code"),
|
|
},
|
|
Expiry: time.Minute * 10,
|
|
CodeReturned: false,
|
|
TriggeredAtOrigin: "https://example.com",
|
|
},
|
|
}
|
|
return session
|
|
},
|
|
update: []database.Change{
|
|
sessionRepo.SetChallenge(&domain.SessionChallengeOTPEmail{
|
|
LastChallengedAt: testNow,
|
|
Code: &crypto.CryptoValue{
|
|
CryptoType: crypto.TypeEncryption,
|
|
Algorithm: "enc",
|
|
KeyID: "key-id",
|
|
Crypted: []byte("code"),
|
|
},
|
|
Expiry: time.Minute * 10,
|
|
CodeReturned: false,
|
|
TriggeredAtOrigin: "https://example.com",
|
|
}),
|
|
},
|
|
rowsAffected: 1,
|
|
},
|
|
{
|
|
name: "update session user factor",
|
|
testFunc: func(t *testing.T) *domain.Session {
|
|
session := &domain.Session{
|
|
InstanceID: instanceId,
|
|
ID: gofakeit.Name(),
|
|
Lifetime: time.Hour * 24,
|
|
CreatorID: gofakeit.Name(),
|
|
}
|
|
err := sessionRepo.Create(t.Context(), tx, session)
|
|
require.NoError(t, err)
|
|
|
|
// update with updated values
|
|
session.Factors = []domain.SessionFactor{
|
|
&domain.SessionFactorUser{
|
|
UserID: userID,
|
|
LastVerifiedAt: testNow,
|
|
},
|
|
}
|
|
return session
|
|
},
|
|
update: []database.Change{
|
|
sessionRepo.SetFactor(&domain.SessionFactorUser{
|
|
UserID: userID,
|
|
LastVerifiedAt: testNow,
|
|
}),
|
|
},
|
|
rowsAffected: 1,
|
|
},
|
|
{
|
|
name: "update session password factor",
|
|
testFunc: func(t *testing.T) *domain.Session {
|
|
session := &domain.Session{
|
|
InstanceID: instanceId,
|
|
ID: gofakeit.Name(),
|
|
Lifetime: time.Hour * 24,
|
|
CreatorID: gofakeit.Name(),
|
|
}
|
|
err := sessionRepo.Create(t.Context(), tx, session)
|
|
require.NoError(t, err)
|
|
|
|
// update with updated values
|
|
session.Factors = []domain.SessionFactor{
|
|
&domain.SessionFactorPassword{
|
|
LastVerifiedAt: testNow,
|
|
},
|
|
}
|
|
return session
|
|
},
|
|
update: []database.Change{
|
|
sessionRepo.SetFactor(&domain.SessionFactorPassword{
|
|
LastVerifiedAt: testNow,
|
|
}),
|
|
},
|
|
rowsAffected: 1,
|
|
},
|
|
{
|
|
name: "update session identity provider intent factor",
|
|
testFunc: func(t *testing.T) *domain.Session {
|
|
session := &domain.Session{
|
|
InstanceID: instanceId,
|
|
ID: gofakeit.Name(),
|
|
Lifetime: time.Hour * 24,
|
|
CreatorID: gofakeit.Name(),
|
|
}
|
|
err := sessionRepo.Create(t.Context(), tx, session)
|
|
require.NoError(t, err)
|
|
|
|
// update with updated values
|
|
session.Factors = []domain.SessionFactor{
|
|
&domain.SessionFactorIdentityProviderIntent{
|
|
LastVerifiedAt: testNow,
|
|
},
|
|
}
|
|
return session
|
|
},
|
|
update: []database.Change{
|
|
sessionRepo.SetFactor(&domain.SessionFactorIdentityProviderIntent{
|
|
LastVerifiedAt: testNow,
|
|
}),
|
|
},
|
|
rowsAffected: 1,
|
|
},
|
|
{
|
|
name: "update session passkey factor",
|
|
testFunc: func(t *testing.T) *domain.Session {
|
|
session := &domain.Session{
|
|
InstanceID: instanceId,
|
|
ID: gofakeit.Name(),
|
|
Lifetime: time.Hour * 24,
|
|
CreatorID: gofakeit.Name(),
|
|
}
|
|
err := sessionRepo.Create(t.Context(), tx, session)
|
|
require.NoError(t, err)
|
|
|
|
// update with updated values
|
|
session.Factors = []domain.SessionFactor{
|
|
&domain.SessionFactorPasskey{
|
|
LastVerifiedAt: testNow,
|
|
UserVerified: true,
|
|
},
|
|
}
|
|
return session
|
|
},
|
|
update: []database.Change{
|
|
sessionRepo.SetFactor(&domain.SessionFactorPasskey{
|
|
LastVerifiedAt: testNow,
|
|
UserVerified: true,
|
|
}),
|
|
},
|
|
rowsAffected: 1,
|
|
},
|
|
{
|
|
name: "update session totp factor",
|
|
testFunc: func(t *testing.T) *domain.Session {
|
|
session := &domain.Session{
|
|
InstanceID: instanceId,
|
|
ID: gofakeit.Name(),
|
|
Lifetime: time.Hour * 24,
|
|
CreatorID: gofakeit.Name(),
|
|
}
|
|
err := sessionRepo.Create(t.Context(), tx, session)
|
|
require.NoError(t, err)
|
|
|
|
// update with updated values
|
|
session.Factors = []domain.SessionFactor{
|
|
&domain.SessionFactorTOTP{
|
|
LastVerifiedAt: testNow,
|
|
},
|
|
}
|
|
return session
|
|
},
|
|
update: []database.Change{
|
|
sessionRepo.SetFactor(&domain.SessionFactorTOTP{
|
|
LastVerifiedAt: testNow,
|
|
}),
|
|
},
|
|
rowsAffected: 1,
|
|
},
|
|
{
|
|
name: "update session otp sms factor",
|
|
testFunc: func(t *testing.T) *domain.Session {
|
|
session := &domain.Session{
|
|
InstanceID: instanceId,
|
|
ID: gofakeit.Name(),
|
|
Lifetime: time.Hour * 24,
|
|
CreatorID: gofakeit.Name(),
|
|
}
|
|
err := sessionRepo.Create(t.Context(), tx, session)
|
|
require.NoError(t, err)
|
|
|
|
// update with updated values
|
|
session.Factors = []domain.SessionFactor{
|
|
&domain.SessionFactorOTPSMS{
|
|
LastVerifiedAt: testNow,
|
|
},
|
|
}
|
|
return session
|
|
},
|
|
update: []database.Change{
|
|
sessionRepo.SetFactor(&domain.SessionFactorOTPSMS{
|
|
LastVerifiedAt: testNow,
|
|
}),
|
|
},
|
|
rowsAffected: 1,
|
|
},
|
|
{
|
|
name: "update session otp email factor",
|
|
testFunc: func(t *testing.T) *domain.Session {
|
|
session := &domain.Session{
|
|
InstanceID: instanceId,
|
|
ID: gofakeit.Name(),
|
|
Lifetime: time.Hour * 24,
|
|
CreatorID: gofakeit.Name(),
|
|
}
|
|
err := sessionRepo.Create(t.Context(), tx, session)
|
|
require.NoError(t, err)
|
|
|
|
// update with updated values
|
|
session.Factors = []domain.SessionFactor{
|
|
&domain.SessionFactorOTPEmail{
|
|
LastVerifiedAt: testNow,
|
|
},
|
|
}
|
|
return session
|
|
},
|
|
update: []database.Change{
|
|
sessionRepo.SetFactor(&domain.SessionFactorOTPEmail{
|
|
LastVerifiedAt: testNow,
|
|
}),
|
|
},
|
|
rowsAffected: 1,
|
|
},
|
|
{
|
|
name: "update session recovery code factor",
|
|
testFunc: func(t *testing.T) *domain.Session {
|
|
session := &domain.Session{
|
|
InstanceID: instanceId,
|
|
ID: gofakeit.Name(),
|
|
Lifetime: time.Hour * 24,
|
|
CreatorID: gofakeit.Name(),
|
|
}
|
|
err := sessionRepo.Create(t.Context(), tx, session)
|
|
require.NoError(t, err)
|
|
|
|
// update with updated values
|
|
session.Factors = []domain.SessionFactor{
|
|
&domain.SessionFactorRecoveryCode{
|
|
LastVerifiedAt: testNow,
|
|
},
|
|
}
|
|
return session
|
|
},
|
|
update: []database.Change{
|
|
sessionRepo.SetFactor(&domain.SessionFactorRecoveryCode{
|
|
LastVerifiedAt: testNow,
|
|
}),
|
|
},
|
|
rowsAffected: 1,
|
|
},
|
|
{
|
|
name: "update session metadata",
|
|
testFunc: func(t *testing.T) *domain.Session {
|
|
session := &domain.Session{
|
|
InstanceID: instanceId,
|
|
ID: gofakeit.Name(),
|
|
Lifetime: time.Hour * 24,
|
|
CreatorID: gofakeit.Name(),
|
|
}
|
|
err := sessionRepo.Create(t.Context(), tx, session)
|
|
require.NoError(t, err)
|
|
|
|
// update with updated values
|
|
session.Metadata = []*domain.SessionMetadata{
|
|
{
|
|
Metadata: domain.Metadata{
|
|
InstanceID: instanceId,
|
|
Key: "key1",
|
|
Value: []byte("value1"),
|
|
},
|
|
SessionID: session.ID,
|
|
},
|
|
{
|
|
Metadata: domain.Metadata{
|
|
InstanceID: instanceId,
|
|
Key: "key2",
|
|
Value: []byte("value2"),
|
|
},
|
|
SessionID: session.ID,
|
|
},
|
|
}
|
|
return session
|
|
},
|
|
update: []database.Change{
|
|
sessionRepo.SetMetadata([]*domain.SessionMetadata{
|
|
{
|
|
Metadata: domain.Metadata{
|
|
Key: "key1",
|
|
Value: []byte("value1"),
|
|
},
|
|
},
|
|
{
|
|
Metadata: domain.Metadata{
|
|
Key: "key2",
|
|
Value: []byte("value2"),
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
rowsAffected: 1,
|
|
},
|
|
{
|
|
name: "update session multiple fields",
|
|
testFunc: func(t *testing.T) *domain.Session {
|
|
session := &domain.Session{
|
|
InstanceID: instanceId,
|
|
ID: gofakeit.Name(),
|
|
Lifetime: time.Hour * 24,
|
|
CreatorID: gofakeit.Name(),
|
|
}
|
|
err := sessionRepo.Create(t.Context(), tx, session)
|
|
require.NoError(t, err)
|
|
|
|
// update with updated values
|
|
session.UpdatedAt = testNow
|
|
session.Lifetime = time.Hour * 48
|
|
session.TokenID = "new-token"
|
|
session.Factors = []domain.SessionFactor{
|
|
&domain.SessionFactorUser{
|
|
UserID: userID,
|
|
LastVerifiedAt: testNow,
|
|
},
|
|
&domain.SessionFactorPassword{
|
|
LastVerifiedAt: testNow,
|
|
},
|
|
}
|
|
session.Challenges = []domain.SessionChallenge{
|
|
&domain.SessionChallengeOTPSMS{
|
|
LastChallengedAt: testNow,
|
|
Code: &crypto.CryptoValue{
|
|
CryptoType: crypto.TypeEncryption,
|
|
Algorithm: "enc",
|
|
KeyID: "key-id",
|
|
Crypted: []byte("code"),
|
|
},
|
|
Expiry: time.Minute * 10,
|
|
CodeReturned: false,
|
|
GeneratorID: "",
|
|
TriggeredAtOrigin: "https://example.com",
|
|
},
|
|
}
|
|
session.Metadata = []*domain.SessionMetadata{
|
|
{
|
|
Metadata: domain.Metadata{
|
|
InstanceID: instanceId,
|
|
Key: "key1",
|
|
Value: []byte("value1"),
|
|
},
|
|
SessionID: session.ID,
|
|
},
|
|
{
|
|
Metadata: domain.Metadata{
|
|
InstanceID: instanceId,
|
|
Key: "key2",
|
|
Value: []byte("value2"),
|
|
},
|
|
SessionID: session.ID,
|
|
},
|
|
}
|
|
return session
|
|
},
|
|
update: []database.Change{
|
|
sessionRepo.SetUpdatedAt(testNow),
|
|
sessionRepo.SetLifetime(time.Hour * 48),
|
|
sessionRepo.SetFactor(&domain.SessionFactorUser{
|
|
UserID: userID,
|
|
LastVerifiedAt: testNow,
|
|
}),
|
|
sessionRepo.SetFactor(&domain.SessionFactorPassword{
|
|
LastVerifiedAt: testNow,
|
|
}),
|
|
sessionRepo.SetChallenge(&domain.SessionChallengeOTPSMS{
|
|
LastChallengedAt: testNow,
|
|
Code: &crypto.CryptoValue{
|
|
CryptoType: crypto.TypeEncryption,
|
|
Algorithm: "enc",
|
|
KeyID: "key-id",
|
|
Crypted: []byte("code"),
|
|
},
|
|
Expiry: time.Minute * 10,
|
|
CodeReturned: false,
|
|
GeneratorID: "",
|
|
TriggeredAtOrigin: "https://example.com",
|
|
}),
|
|
sessionRepo.SetMetadata([]*domain.SessionMetadata{
|
|
{
|
|
Metadata: domain.Metadata{
|
|
Key: "key1",
|
|
Value: []byte("value1"),
|
|
},
|
|
},
|
|
{
|
|
Metadata: domain.Metadata{
|
|
Key: "key2",
|
|
Value: []byte("value2"),
|
|
},
|
|
},
|
|
}),
|
|
sessionRepo.SetToken("new-token"),
|
|
},
|
|
rowsAffected: 1,
|
|
},
|
|
{
|
|
name: "update session clear factor",
|
|
testFunc: func(t *testing.T) *domain.Session {
|
|
session := &domain.Session{
|
|
InstanceID: instanceId,
|
|
ID: gofakeit.Name(),
|
|
Lifetime: time.Hour * 24,
|
|
CreatorID: gofakeit.Name(),
|
|
}
|
|
err := sessionRepo.Create(t.Context(), tx, session)
|
|
require.NoError(t, err)
|
|
|
|
updatedRows, err := sessionRepo.Update(t.Context(), tx,
|
|
sessionRepo.PrimaryKeyCondition(session.InstanceID, session.ID),
|
|
sessionRepo.SetFactor(&domain.SessionFactorUser{
|
|
UserID: userID,
|
|
LastVerifiedAt: testNow,
|
|
}),
|
|
sessionRepo.SetFactor(&domain.SessionFactorPassword{
|
|
LastVerifiedAt: testNow,
|
|
}),
|
|
)
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(1), updatedRows)
|
|
|
|
// update with updated values
|
|
session.Factors = []domain.SessionFactor{
|
|
&domain.SessionFactorUser{
|
|
UserID: userID,
|
|
LastVerifiedAt: testNow,
|
|
},
|
|
}
|
|
return session
|
|
},
|
|
update: []database.Change{
|
|
sessionRepo.ClearFactor(domain.SessionFactorTypePassword),
|
|
},
|
|
rowsAffected: 1,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
createdSession := tt.testFunc(t)
|
|
|
|
// update org
|
|
rowsAffected, err := sessionRepo.Update(t.Context(), tx,
|
|
sessionRepo.PrimaryKeyCondition(createdSession.InstanceID, createdSession.ID),
|
|
tt.update...,
|
|
)
|
|
afterUpdate := time.Now()
|
|
require.ErrorIs(t, err, tt.err)
|
|
|
|
assert.Equal(t, tt.rowsAffected, rowsAffected)
|
|
|
|
if rowsAffected == 0 {
|
|
return
|
|
}
|
|
|
|
// check session values
|
|
session, err := sessionRepo.Get(t.Context(), tx,
|
|
database.WithCondition(
|
|
sessionRepo.PrimaryKeyCondition(createdSession.InstanceID, createdSession.ID),
|
|
),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, createdSession.ID, session.ID)
|
|
assert.Equal(t, createdSession.Lifetime, session.Lifetime)
|
|
assert.Equal(t, createdSession.TokenID, session.TokenID)
|
|
assert.WithinRange(t, session.Expiration, beforeUpdate.Add(createdSession.Lifetime), afterUpdate.Add(createdSession.Lifetime))
|
|
assert.Equal(t, createdSession.CreatorID, session.CreatorID)
|
|
assert.Equal(t, createdSession.UserAgent, session.UserAgent)
|
|
assert.WithinRange(t, session.UpdatedAt, beforeUpdate, afterUpdate)
|
|
assert.EqualExportedValues(t, createdSession.Challenges, session.Challenges)
|
|
slices.SortFunc(session.Factors, func(a, b domain.SessionFactor) int { // sort to make comparison possible
|
|
return int(a.SessionFactorType()) - int(b.SessionFactorType())
|
|
})
|
|
assert.EqualExportedValues(t, createdSession.Factors, session.Factors)
|
|
assert.EqualExportedValues(t, createdSession.Metadata, session.Metadata)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSession_Delete(t *testing.T) {
|
|
start := time.Now()
|
|
tx, err := pool.Begin(t.Context(), nil)
|
|
require.NoError(t, err)
|
|
defer func() {
|
|
err := tx.Rollback(t.Context())
|
|
if err != nil {
|
|
t.Logf("error during rollback: %v", err)
|
|
}
|
|
}()
|
|
|
|
instanceRepo := repository.InstanceRepository()
|
|
sessionRepo := repository.SessionRepository()
|
|
|
|
// create instance
|
|
instanceId := gofakeit.Name()
|
|
instance := domain.Instance{
|
|
ID: instanceId,
|
|
Name: gofakeit.Name(),
|
|
DefaultOrgID: "defaultOrgId",
|
|
IAMProjectID: "iamProject",
|
|
ConsoleClientID: "managementConsoleClient",
|
|
ConsoleAppID: "managementConsoleApp",
|
|
DefaultLanguage: "defaultLanguage",
|
|
}
|
|
err = instanceRepo.Create(t.Context(), tx, &instance)
|
|
require.NoError(t, err)
|
|
|
|
type fields struct {
|
|
testFunc func(t *testing.T) []*domain.Session
|
|
}
|
|
type args struct {
|
|
condition func([]*domain.Session) database.Condition
|
|
permissionCondition func([]*domain.Session) database.Condition
|
|
}
|
|
type res struct {
|
|
noOfDeletedRows int64
|
|
deletedAt time.Time
|
|
err error
|
|
}
|
|
|
|
type test struct {
|
|
name string
|
|
fields fields
|
|
args args
|
|
res res
|
|
}
|
|
tests := []test{
|
|
{
|
|
name: "delete existent session",
|
|
fields: fields{
|
|
testFunc: func(t *testing.T) []*domain.Session {
|
|
session := &domain.Session{
|
|
InstanceID: instanceId,
|
|
ID: gofakeit.Name(),
|
|
Lifetime: time.Hour * 24,
|
|
CreatorID: gofakeit.Name(),
|
|
}
|
|
err := sessionRepo.Create(t.Context(), tx, session)
|
|
require.NoError(t, err)
|
|
return []*domain.Session{session}
|
|
},
|
|
},
|
|
args: args{
|
|
condition: func(sessions []*domain.Session) database.Condition {
|
|
return sessionRepo.PrimaryKeyCondition(instanceId, sessions[0].ID)
|
|
},
|
|
},
|
|
res: res{
|
|
noOfDeletedRows: 1,
|
|
deletedAt: time.Now(),
|
|
},
|
|
},
|
|
{
|
|
name: "delete non existent session",
|
|
fields: fields{
|
|
testFunc: func(t *testing.T) []*domain.Session {
|
|
return []*domain.Session{}
|
|
},
|
|
},
|
|
args: args{
|
|
condition: func(sessions []*domain.Session) database.Condition {
|
|
return sessionRepo.PrimaryKeyCondition(instanceId, gofakeit.UUID())
|
|
},
|
|
},
|
|
res: res{
|
|
noOfDeletedRows: 0,
|
|
},
|
|
},
|
|
{
|
|
name: "delete previously existent session",
|
|
fields: fields{
|
|
testFunc: func(t *testing.T) []*domain.Session {
|
|
session := &domain.Session{
|
|
InstanceID: instanceId,
|
|
ID: gofakeit.Name(),
|
|
Lifetime: time.Hour * 24,
|
|
CreatorID: gofakeit.Name(),
|
|
}
|
|
err := sessionRepo.Create(t.Context(), tx, session)
|
|
require.NoError(t, err)
|
|
_, _, err = sessionRepo.Delete(t.Context(), tx, sessionRepo.PrimaryKeyCondition(session.InstanceID, session.ID), nil)
|
|
require.NoError(t, err)
|
|
return []*domain.Session{session}
|
|
},
|
|
},
|
|
args: args{
|
|
condition: func(sessions []*domain.Session) database.Condition {
|
|
return sessionRepo.PrimaryKeyCondition(instanceId, sessions[0].ID)
|
|
},
|
|
},
|
|
res: res{
|
|
noOfDeletedRows: 0,
|
|
deletedAt: time.Now(),
|
|
},
|
|
},
|
|
{
|
|
name: "delete multiple sessions",
|
|
fields: fields{
|
|
testFunc: func(t *testing.T) []*domain.Session {
|
|
session := &domain.Session{
|
|
InstanceID: instanceId,
|
|
ID: gofakeit.Name(),
|
|
Lifetime: time.Hour * 24,
|
|
CreatorID: gofakeit.Name(),
|
|
}
|
|
err := sessionRepo.Create(t.Context(), tx, session)
|
|
require.NoError(t, err)
|
|
session2 := &domain.Session{
|
|
InstanceID: instanceId,
|
|
ID: gofakeit.Name(),
|
|
Lifetime: time.Hour * 24,
|
|
CreatorID: gofakeit.Name(),
|
|
}
|
|
err = sessionRepo.Create(t.Context(), tx, session2)
|
|
require.NoError(t, err)
|
|
return []*domain.Session{session, session2}
|
|
},
|
|
},
|
|
args: args{
|
|
condition: func(sessions []*domain.Session) database.Condition {
|
|
return sessionRepo.InstanceIDCondition(instanceId)
|
|
},
|
|
},
|
|
res: res{
|
|
noOfDeletedRows: 2,
|
|
deletedAt: time.Time{},
|
|
},
|
|
},
|
|
{
|
|
name: "delete without permission",
|
|
fields: fields{
|
|
testFunc: func(t *testing.T) []*domain.Session {
|
|
session := &domain.Session{
|
|
InstanceID: instanceId,
|
|
ID: gofakeit.Name(),
|
|
TokenID: gofakeit.UUID(),
|
|
Lifetime: time.Hour * 24,
|
|
CreatorID: gofakeit.Name(),
|
|
}
|
|
err := sessionRepo.Create(t.Context(), tx, session)
|
|
require.NoError(t, err)
|
|
return []*domain.Session{session}
|
|
},
|
|
},
|
|
args: args{
|
|
condition: func(sessions []*domain.Session) database.Condition {
|
|
return sessionRepo.InstanceIDCondition(instanceId)
|
|
},
|
|
permissionCondition: func(sessions []*domain.Session) database.Condition {
|
|
return sessionRepo.TokenIDCondition("invalid")
|
|
},
|
|
},
|
|
res: res{
|
|
noOfDeletedRows: 0,
|
|
deletedAt: time.Time{},
|
|
err: database.NewPermissionError(nil),
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var sessions []*domain.Session
|
|
if tt.fields.testFunc != nil {
|
|
sessions = tt.fields.testFunc(t)
|
|
}
|
|
|
|
var condition, permissionCondition database.Condition
|
|
if tt.args.condition != nil {
|
|
condition = tt.args.condition(sessions)
|
|
}
|
|
if tt.args.permissionCondition != nil {
|
|
permissionCondition = tt.args.permissionCondition(sessions)
|
|
}
|
|
|
|
// let's create a snapshot before deletion to make sure we could rollback in case of error
|
|
snapshot, err := tx.Begin(t.Context())
|
|
require.NoError(t, err)
|
|
|
|
// delete session
|
|
deletedRows, deletedAt, err := sessionRepo.Delete(t.Context(), tx,
|
|
condition, permissionCondition,
|
|
)
|
|
require.ErrorIs(t, err, tt.res.err)
|
|
assert.Equal(t, tt.res.noOfDeletedRows, deletedRows)
|
|
if tt.res.deletedAt.IsZero() {
|
|
assert.Zero(t, deletedAt)
|
|
} else {
|
|
assert.WithinRange(t, deletedAt, start, time.Now())
|
|
}
|
|
|
|
if err != nil {
|
|
err = snapshot.Rollback(t.Context())
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// verify session deletion
|
|
deletedSessions, err := sessionRepo.List(t.Context(), tx,
|
|
database.WithCondition(condition),
|
|
)
|
|
require.NoError(t, err)
|
|
expectedSessions := 0
|
|
|
|
// in case of permission error, sessions are not deleted,
|
|
// so expectedSessions should be equal to the number of created sessions
|
|
if tt.res.err != nil {
|
|
expectedSessions = len(sessions)
|
|
}
|
|
assert.Len(t, deletedSessions, expectedSessions)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSession_Get(t *testing.T) {
|
|
tx, err := pool.Begin(t.Context(), nil)
|
|
require.NoError(t, err)
|
|
defer func() {
|
|
err := tx.Rollback(t.Context())
|
|
if err != nil {
|
|
t.Logf("error during rollback: %v", err)
|
|
}
|
|
}()
|
|
instanceRepo := repository.InstanceRepository()
|
|
sessionRepo := repository.SessionRepository().LoadUserData()
|
|
orgRepo := repository.OrganizationRepository()
|
|
orgDomainRepo := repository.OrganizationDomainRepository()
|
|
userRepo := repository.UserRepository()
|
|
settingsRepo := repository.DomainSettingsRepository()
|
|
|
|
// create instance
|
|
instanceId := gofakeit.Name()
|
|
instance := domain.Instance{
|
|
ID: instanceId,
|
|
Name: gofakeit.Name(),
|
|
DefaultOrgID: "defaultOrgId",
|
|
IAMProjectID: "iamProject",
|
|
ConsoleClientID: "managementConsoleClient",
|
|
ConsoleAppID: "managementConsoleApp",
|
|
DefaultLanguage: "defaultLanguage",
|
|
}
|
|
err = instanceRepo.Create(t.Context(), tx, &instance)
|
|
require.NoError(t, err)
|
|
|
|
// Create Organization
|
|
orgID := gofakeit.UUID()
|
|
org := domain.Organization{
|
|
ID: orgID,
|
|
Name: gofakeit.Name(),
|
|
InstanceID: instanceId,
|
|
State: domain.OrgStateActive,
|
|
}
|
|
err = orgRepo.Create(t.Context(), tx, &org)
|
|
require.NoError(t, err)
|
|
|
|
domainName := gofakeit.DomainName()
|
|
err = orgDomainRepo.Add(t.Context(), tx, &domain.AddOrganizationDomain{
|
|
InstanceID: instanceId,
|
|
OrgID: orgID,
|
|
Domain: domainName,
|
|
IsVerified: true,
|
|
IsPrimary: true,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = settingsRepo.Set(t.Context(), tx, &domain.DomainSettings{
|
|
Settings: domain.Settings{
|
|
InstanceID: instanceId,
|
|
OrganizationID: &orgID,
|
|
Type: domain.SettingTypeDomain,
|
|
State: domain.SettingStateActive,
|
|
},
|
|
DomainSettingsAttributes: domain.DomainSettingsAttributes{
|
|
LoginNameIncludesDomain: gu.Ptr(false),
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
preferredLoginName := gofakeit.Name()
|
|
userID := gofakeit.UUID()
|
|
displayName := gofakeit.Username()
|
|
user := domain.User{
|
|
InstanceID: instanceId,
|
|
OrganizationID: orgID,
|
|
ID: userID,
|
|
Username: preferredLoginName,
|
|
State: domain.UserStateActive,
|
|
Human: &domain.HumanUser{
|
|
FirstName: gofakeit.Name(),
|
|
LastName: gofakeit.Name(),
|
|
Nickname: gofakeit.Username(),
|
|
DisplayName: displayName,
|
|
PreferredLanguage: language.English,
|
|
Gender: domain.HumanGenderMale,
|
|
},
|
|
}
|
|
|
|
err = userRepo.Create(t.Context(), tx, &user)
|
|
require.NoError(t, err)
|
|
|
|
session := &domain.Session{
|
|
InstanceID: instanceId,
|
|
ID: gofakeit.Name(),
|
|
Lifetime: time.Hour * 24,
|
|
CreatorID: gofakeit.Name(),
|
|
}
|
|
err = sessionRepo.Create(t.Context(), tx, session)
|
|
require.NoError(t, err)
|
|
_, err = sessionRepo.Update(t.Context(), tx, sessionRepo.PrimaryKeyCondition(instanceId, session.ID), sessionRepo.SetFactor(&domain.SessionFactorUser{
|
|
UserID: userID,
|
|
LastVerifiedAt: time.Now(),
|
|
}))
|
|
require.NoError(t, err)
|
|
|
|
tests := []struct {
|
|
name string
|
|
condition database.Condition
|
|
err error
|
|
}{
|
|
{
|
|
name: "get existent session by primary key",
|
|
condition: sessionRepo.PrimaryKeyCondition(instanceId, session.ID),
|
|
err: nil,
|
|
},
|
|
{
|
|
name: "get without instanceID condition",
|
|
condition: sessionRepo.IDCondition(session.ID),
|
|
err: new(database.MissingConditionError),
|
|
},
|
|
{
|
|
name: "get non existent session",
|
|
condition: sessionRepo.PrimaryKeyCondition(instanceId, gofakeit.Name()),
|
|
err: new(database.NoRowFoundError),
|
|
},
|
|
// other conditions are tested in the List tests
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// get session
|
|
got, err := sessionRepo.Get(t.Context(), tx, database.WithCondition(tt.condition))
|
|
assert.ErrorIs(t, err, tt.err)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// check session values
|
|
assert.Equal(t, session.ID, got.ID)
|
|
assert.Equal(t, session.InstanceID, got.InstanceID)
|
|
assert.Equal(t, session.Lifetime, got.Lifetime)
|
|
assert.Equal(t, session.CreatorID, got.CreatorID)
|
|
require.NotNil(t, got.UserOrganizationID)
|
|
assert.Equal(t, orgID, *got.UserOrganizationID)
|
|
assert.Equal(t, preferredLoginName, got.UserPreferredLoginName)
|
|
require.NotNil(t, got.UserDisplayName)
|
|
assert.Equal(t, displayName, *got.UserDisplayName)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSession_List(t *testing.T) {
|
|
tx, err := pool.Begin(t.Context(), nil)
|
|
require.NoError(t, err)
|
|
defer func() {
|
|
err := tx.Rollback(t.Context())
|
|
if err != nil {
|
|
t.Logf("error during rollback: %v", err)
|
|
}
|
|
}()
|
|
|
|
instanceRepo := repository.InstanceRepository()
|
|
sessionRepo := repository.SessionRepository()
|
|
|
|
// create instance
|
|
instanceId := gofakeit.Name()
|
|
instance := domain.Instance{
|
|
ID: instanceId,
|
|
Name: gofakeit.Name(),
|
|
DefaultOrgID: "defaultOrgId",
|
|
IAMProjectID: "iamProject",
|
|
ConsoleClientID: "managementConsoleClient",
|
|
ConsoleAppID: "managementConsoleApp",
|
|
DefaultLanguage: "defaultLanguage",
|
|
}
|
|
err = instanceRepo.Create(t.Context(), tx, &instance)
|
|
require.NoError(t, err)
|
|
|
|
orgID := createOrganization(t, tx, instanceId)
|
|
userIDs := []string{createHumanUser(t, tx, instanceId, orgID), createHumanUser(t, tx, instanceId, orgID)}
|
|
|
|
// create sessions
|
|
sessions := make([]*domain.Session, 5)
|
|
for i := range 5 {
|
|
session := &domain.Session{
|
|
InstanceID: instanceId,
|
|
ID: strconv.Itoa(i),
|
|
Lifetime: time.Hour * 24,
|
|
CreatorID: gofakeit.Name(),
|
|
CreatedAt: time.Now(),
|
|
UserAgent: &domain.SessionUserAgent{
|
|
FingerprintID: gu.Ptr(gofakeit.UUID()),
|
|
Description: gu.Ptr(gofakeit.Name()),
|
|
IP: net.ParseIP(gofakeit.IPv4Address()),
|
|
Header: http.Header{"user-agent": []string{gofakeit.Name()}},
|
|
},
|
|
}
|
|
err = sessionRepo.Create(t.Context(), tx, session)
|
|
require.NoError(t, err)
|
|
session.Expiration = session.CreatedAt.Add(session.Lifetime)
|
|
|
|
changes := make([]database.Change, 0, 4)
|
|
updated := time.Now()
|
|
changes = append(changes, sessionRepo.SetUpdatedAt(updated))
|
|
session.UpdatedAt = updated
|
|
metadata := []*domain.SessionMetadata{
|
|
{
|
|
Metadata: domain.Metadata{
|
|
InstanceID: instanceId,
|
|
Key: "key" + strconv.Itoa(i),
|
|
Value: []byte{uint8(i)},
|
|
},
|
|
SessionID: session.ID,
|
|
},
|
|
}
|
|
session.Metadata = metadata
|
|
changes = append(changes, sessionRepo.SetMetadata(metadata))
|
|
userFactor := &domain.SessionFactorUser{
|
|
UserID: userIDs[i%2],
|
|
LastVerifiedAt: time.Now(),
|
|
}
|
|
session.Factors.AppendTo(userFactor)
|
|
changes = append(changes, sessionRepo.SetFactor(userFactor))
|
|
if i < 3 {
|
|
passwordFactor := &domain.SessionFactorPassword{
|
|
LastVerifiedAt: time.Now(),
|
|
}
|
|
session.Factors.AppendTo(passwordFactor)
|
|
changes = append(changes, sessionRepo.SetFactor(passwordFactor))
|
|
}
|
|
_, err = sessionRepo.Update(t.Context(), tx, sessionRepo.PrimaryKeyCondition(instanceId, session.ID),
|
|
changes...,
|
|
)
|
|
require.NoError(t, err)
|
|
session.UserID = userFactor.UserID
|
|
sessions[i] = session
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
options []database.QueryOption
|
|
expected []*domain.Session
|
|
err error
|
|
}{
|
|
{
|
|
name: "list sessions by instanceID",
|
|
options: []database.QueryOption{
|
|
database.WithCondition(
|
|
sessionRepo.InstanceIDCondition(instanceId),
|
|
),
|
|
database.WithOrderByAscending(sessionRepo.IDColumn()),
|
|
},
|
|
expected: sessions,
|
|
err: nil,
|
|
},
|
|
{
|
|
name: "list single session by id",
|
|
options: []database.QueryOption{
|
|
database.WithCondition(
|
|
database.And(
|
|
sessionRepo.IDCondition(sessions[0].ID),
|
|
sessionRepo.InstanceIDCondition(instanceId),
|
|
),
|
|
),
|
|
},
|
|
expected: []*domain.Session{sessions[0]},
|
|
err: nil,
|
|
},
|
|
{
|
|
name: "list sessions with non matching condition",
|
|
options: []database.QueryOption{
|
|
database.WithCondition(
|
|
database.And(
|
|
sessionRepo.IDCondition("non-existent-id"),
|
|
sessionRepo.InstanceIDCondition(instanceId),
|
|
),
|
|
),
|
|
},
|
|
expected: []*domain.Session{},
|
|
err: nil,
|
|
},
|
|
{
|
|
name: "list sessions without conditions",
|
|
options: nil,
|
|
expected: nil,
|
|
err: new(database.MissingConditionError),
|
|
},
|
|
{
|
|
name: "list sessions by user agent fingerprintID",
|
|
options: []database.QueryOption{
|
|
database.WithCondition(
|
|
database.And(
|
|
sessionRepo.UserAgentIDCondition(gu.Value(sessions[1].UserAgent.FingerprintID)),
|
|
sessionRepo.InstanceIDCondition(instanceId),
|
|
),
|
|
),
|
|
},
|
|
expected: []*domain.Session{sessions[1]},
|
|
err: nil,
|
|
},
|
|
{
|
|
name: "list sessions by userID",
|
|
options: []database.QueryOption{
|
|
database.WithCondition(
|
|
database.And(
|
|
sessionRepo.UserIDCondition(sessions[0].Factors.GetUserFactor().UserID),
|
|
sessionRepo.InstanceIDCondition(instanceId),
|
|
),
|
|
),
|
|
database.WithOrderByAscending(sessionRepo.IDColumn()),
|
|
database.WithLimit(2),
|
|
},
|
|
expected: []*domain.Session{sessions[0], sessions[2]},
|
|
err: nil,
|
|
},
|
|
{
|
|
name: "list sessions by creatorID",
|
|
options: []database.QueryOption{
|
|
database.WithCondition(
|
|
database.And(
|
|
sessionRepo.CreatorIDCondition(sessions[3].CreatorID),
|
|
sessionRepo.InstanceIDCondition(instanceId),
|
|
),
|
|
),
|
|
},
|
|
expected: []*domain.Session{sessions[3]},
|
|
},
|
|
{
|
|
name: "list sessions by expiration",
|
|
options: []database.QueryOption{
|
|
database.WithCondition(
|
|
database.And(
|
|
sessionRepo.ExpirationCondition(database.NumberOperationGreaterThanOrEqual, sessions[3].Expiration),
|
|
sessionRepo.InstanceIDCondition(instanceId),
|
|
),
|
|
),
|
|
database.WithOrderByAscending(sessionRepo.IDColumn()),
|
|
},
|
|
expected: []*domain.Session{sessions[3], sessions[4]},
|
|
},
|
|
{
|
|
name: "list sessions by created at",
|
|
options: []database.QueryOption{
|
|
database.WithCondition(
|
|
database.And(
|
|
sessionRepo.CreatedAtCondition(database.NumberOperationLessThan, sessions[3].CreatedAt),
|
|
sessionRepo.InstanceIDCondition(instanceId),
|
|
),
|
|
),
|
|
database.WithOrderByAscending(sessionRepo.IDColumn()),
|
|
},
|
|
expected: []*domain.Session{sessions[0], sessions[1], sessions[2]},
|
|
},
|
|
{
|
|
name: "list sessions by factor type",
|
|
options: []database.QueryOption{
|
|
database.WithCondition(
|
|
database.And(
|
|
sessionRepo.ExistsFactor(sessionRepo.FactorConditions().FactorTypeCondition(domain.SessionFactorTypePassword)),
|
|
sessionRepo.InstanceIDCondition(instanceId),
|
|
),
|
|
),
|
|
database.WithOrderByAscending(sessionRepo.IDColumn()),
|
|
},
|
|
expected: []*domain.Session{sessions[0], sessions[1], sessions[2]},
|
|
},
|
|
{
|
|
name: "list sessions by factor verification time",
|
|
options: []database.QueryOption{
|
|
database.WithCondition(
|
|
database.And(
|
|
sessionRepo.ExistsFactor(sessionRepo.FactorConditions().LastVerifiedBeforeCondition(sessions[2].Factors.GetUserFactor().LastVerifiedAt)),
|
|
sessionRepo.InstanceIDCondition(instanceId),
|
|
),
|
|
),
|
|
database.WithOrderByAscending(sessionRepo.IDColumn()),
|
|
},
|
|
expected: []*domain.Session{sessions[0], sessions[1]},
|
|
},
|
|
{
|
|
name: "list sessions by metadata key",
|
|
options: []database.QueryOption{
|
|
database.WithCondition(
|
|
database.And(
|
|
sessionRepo.ExistsMetadata(sessionRepo.MetadataConditions().KeyCondition(database.TextOperationEqual, "key3")),
|
|
sessionRepo.InstanceIDCondition(instanceId),
|
|
),
|
|
),
|
|
database.WithOrderByAscending(sessionRepo.IDColumn()),
|
|
},
|
|
expected: []*domain.Session{sessions[3]},
|
|
},
|
|
{
|
|
name: "list sessions by metadata value",
|
|
options: []database.QueryOption{
|
|
database.WithCondition(
|
|
database.And(
|
|
sessionRepo.ExistsMetadata(sessionRepo.MetadataConditions().ValueCondition(database.BytesOperationEqual, []byte{3})),
|
|
sessionRepo.InstanceIDCondition(instanceId),
|
|
),
|
|
),
|
|
database.WithOrderByAscending(sessionRepo.IDColumn()),
|
|
},
|
|
expected: []*domain.Session{sessions[3]},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// list sessions
|
|
got, err := sessionRepo.List(t.Context(), tx, tt.options...)
|
|
assert.ErrorIs(t, err, tt.err)
|
|
if err != nil {
|
|
return
|
|
}
|
|
assert.Len(t, got, len(tt.expected))
|
|
for _, session := range got {
|
|
slices.SortFunc(session.Factors, func(a, b domain.SessionFactor) int { // sort to make comparison possible
|
|
return int(a.SessionFactorType()) - int(b.SessionFactorType())
|
|
})
|
|
}
|
|
assert.EqualExportedValues(t, tt.expected, got)
|
|
})
|
|
}
|
|
}
|