mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
## Problem description During tests we found concurrency issues in the relational table projections. Tests got flaky if a table (e.g. organization domains) had too many foreign keys to tables which were projected by another handler. ## Resolution The projections of the relational tables got merged into one handler to enforce sequential reduction of events. ## Additional info - closes https://github.com/zitadel/zitadel/issues/11703 - closes https://github.com/zitadel/zitadel/issues/10810 --------- Co-authored-by: Marco Ardizzone <[email protected]>
225 lines
6.9 KiB
Go
225 lines
6.9 KiB
Go
package projection
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/brianvoe/gofakeit/v6"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/zitadel/zitadel/backend/v3/domain"
|
|
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
|
"github.com/zitadel/zitadel/backend/v3/storage/database/dialect/sql"
|
|
"github.com/zitadel/zitadel/backend/v3/storage/database/repository"
|
|
"github.com/zitadel/zitadel/internal/repository/user"
|
|
)
|
|
|
|
func TestUserRelationalProjection_Reducers(t *testing.T) {
|
|
handler := &relationalTablesProjection{}
|
|
rawTx, tx := getTransactions(t)
|
|
t.Cleanup(func() {
|
|
require.NoError(t, rawTx.Rollback())
|
|
})
|
|
|
|
ctx := t.Context()
|
|
|
|
// create instance
|
|
instanceRepo := repository.InstanceRepository()
|
|
instanceID := gofakeit.UUID()
|
|
orgID := gofakeit.UUID()
|
|
err := instanceRepo.Create(ctx, tx, &domain.Instance{
|
|
ID: instanceID,
|
|
Name: "test-instance",
|
|
DefaultOrgID: orgID,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// create org
|
|
orgRepo := repository.OrganizationRepository()
|
|
err = orgRepo.Create(ctx, tx, &domain.Organization{
|
|
InstanceID: instanceID,
|
|
ID: orgID,
|
|
Name: "test-org",
|
|
State: domain.OrgStateActive,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
userRepo := repository.UserRepository()
|
|
|
|
// TODO: add tests for other reducers in user_relational.go
|
|
|
|
// recovery code reducers
|
|
t.Run("reduce user.human.mfa.recoverycode.added event", func(t *testing.T) {
|
|
// create user
|
|
existingUserID := gofakeit.UUID()
|
|
existingUserAgg := createUser(t, ctx, tx, userRepo, instanceID, orgID, existingUserID)
|
|
|
|
// set `user.human.mfa.recoverycode.added` event
|
|
recoveryCodes := []string{"code1", "code2", "code3"}
|
|
recoveryCodesAddedEvent := user.NewHumanRecoveryCodesAddedEvent(
|
|
ctx,
|
|
&existingUserAgg.Aggregate,
|
|
recoveryCodes,
|
|
nil)
|
|
|
|
// reduce the event
|
|
eventReduced := callReduce(t, ctx, rawTx, handler, recoveryCodesAddedEvent)
|
|
require.True(t, eventReduced)
|
|
|
|
// assert that the recovery codes are stored
|
|
gotUser, err := userRepo.Get(ctx, tx, database.WithCondition(
|
|
database.And(
|
|
userRepo.IDCondition(existingUserID),
|
|
userRepo.InstanceIDCondition(instanceID),
|
|
),
|
|
))
|
|
require.NoError(t, err)
|
|
assert.ElementsMatch(t, recoveryCodes, gotUser.Human.RecoveryCodes.Codes)
|
|
assert.Zero(t, gotUser.Human.RecoveryCodes.LastSuccessfullyCheckedAt)
|
|
assert.Zero(t, gotUser.Human.RecoveryCodes.FailedAttempts)
|
|
})
|
|
|
|
t.Run("reduce user.human.mfa.recoverycode.removed event", func(t *testing.T) {
|
|
// create user
|
|
existingUserID := gofakeit.UUID()
|
|
existingUserAgg := createUser(t, ctx, tx, userRepo, instanceID, orgID, existingUserID)
|
|
|
|
// add recovery codes to the user
|
|
_, err = userRepo.Update(ctx, tx,
|
|
userRepo.PrimaryKeyCondition(instanceID, existingUserID),
|
|
userRepo.Human().AddRecoveryCodes([]string{"code1", "code2", "code3"}))
|
|
require.NoError(t, err)
|
|
|
|
// set `user.human.mfa.recoverycode.removed` event
|
|
recoveryCodesRemovedEvent := user.NewHumanRecoveryCodeRemovedEvent(
|
|
ctx,
|
|
&existingUserAgg.Aggregate,
|
|
nil)
|
|
|
|
// reduce the event
|
|
eventReduced := callReduce(t, ctx, rawTx, handler, recoveryCodesRemovedEvent)
|
|
require.True(t, eventReduced)
|
|
|
|
// assert that the recovery codes are removed
|
|
gotUser, err := userRepo.Get(ctx, tx, database.WithCondition(
|
|
database.And(
|
|
userRepo.IDCondition(existingUserID),
|
|
userRepo.InstanceIDCondition(instanceID),
|
|
),
|
|
))
|
|
require.NoError(t, err)
|
|
assert.Empty(t, gotUser.Human.RecoveryCodes.Codes)
|
|
assert.Zero(t, gotUser.Human.RecoveryCodes.LastSuccessfullyCheckedAt)
|
|
assert.Zero(t, gotUser.Human.RecoveryCodes.FailedAttempts)
|
|
})
|
|
|
|
t.Run("reduce user.human.mfa.recoverycode.check.succeeded event", func(t *testing.T) {
|
|
// create user
|
|
existingUserID := gofakeit.UUID()
|
|
existingUserAgg := createUser(t, ctx, tx, userRepo, instanceID, orgID, existingUserID)
|
|
|
|
// add recovery codes to the user
|
|
_, err = userRepo.Update(ctx, tx,
|
|
userRepo.PrimaryKeyCondition(instanceID, existingUserID),
|
|
userRepo.Human().AddRecoveryCodes([]string{"code1", "code2", "code3"}))
|
|
require.NoError(t, err)
|
|
|
|
// set `user.human.mfa.recoverycode.check.succeeded` event
|
|
recoveryCodeCheckSucceededEvent := user.NewHumanRecoveryCodeCheckSucceededEvent(
|
|
ctx,
|
|
&existingUserAgg.Aggregate,
|
|
"code1",
|
|
nil)
|
|
|
|
// reduce the event
|
|
eventReduced := callReduce(t, ctx, rawTx, handler, recoveryCodeCheckSucceededEvent)
|
|
require.True(t, eventReduced)
|
|
|
|
// assert that the recovery code is removed and the last_successfully_checked_at timestamp is set
|
|
gotUser, err := userRepo.Get(ctx, tx, database.WithCondition(
|
|
database.And(
|
|
userRepo.IDCondition(existingUserID),
|
|
userRepo.InstanceIDCondition(instanceID),
|
|
),
|
|
))
|
|
require.NoError(t, err)
|
|
assert.ElementsMatch(t, []string{"code2", "code3"}, gotUser.Human.RecoveryCodes.Codes)
|
|
assert.NotZero(t, gotUser.Human.RecoveryCodes.LastSuccessfullyCheckedAt)
|
|
assert.Zero(t, gotUser.Human.RecoveryCodes.FailedAttempts)
|
|
})
|
|
|
|
t.Run("reduce user.human.mfa.recoverycode.check.failed event", func(t *testing.T) {
|
|
// create user
|
|
existingUserID := gofakeit.UUID()
|
|
existingUserAgg := createUser(t, ctx, tx, userRepo, instanceID, orgID, existingUserID)
|
|
|
|
// add recovery codes to the user
|
|
_, err = userRepo.Update(ctx, tx,
|
|
userRepo.PrimaryKeyCondition(instanceID, existingUserID),
|
|
userRepo.Human().AddRecoveryCodes([]string{"code1", "code2", "code3"}))
|
|
require.NoError(t, err)
|
|
|
|
// set `user.human.mfa.recoverycode.check.failed` event
|
|
recoveryCodeCheckFailedEvent := user.NewHumanRecoveryCodeCheckFailedEvent(
|
|
ctx,
|
|
&existingUserAgg.Aggregate,
|
|
nil,
|
|
)
|
|
|
|
// reduce the event
|
|
eventReduced := callReduce(t, ctx, rawTx, handler, recoveryCodeCheckFailedEvent)
|
|
require.True(t, eventReduced)
|
|
|
|
// assert that the recovery code failed_attempts is incremented
|
|
gotUser, err := userRepo.Get(ctx, tx, database.WithCondition(
|
|
database.And(
|
|
userRepo.IDCondition(existingUserID),
|
|
userRepo.InstanceIDCondition(instanceID),
|
|
),
|
|
))
|
|
require.NoError(t, err)
|
|
assert.ElementsMatch(t, []string{"code1", "code2", "code3"}, gotUser.Human.RecoveryCodes.Codes)
|
|
assert.Zero(t, gotUser.Human.RecoveryCodes.LastSuccessfullyCheckedAt)
|
|
assert.Equal(t, uint8(1), gotUser.Human.RecoveryCodes.FailedAttempts)
|
|
})
|
|
}
|
|
|
|
func createUser(t *testing.T,
|
|
ctx context.Context,
|
|
tx *sql.Transaction,
|
|
userRepo domain.UserRepository,
|
|
instanceID,
|
|
orgID,
|
|
userID string,
|
|
) *user.Aggregate {
|
|
userAgg := user.NewAggregate(userID, orgID)
|
|
userAgg.InstanceID = instanceID
|
|
|
|
err := userRepo.Create(ctx, tx, &domain.User{
|
|
InstanceID: instanceID,
|
|
OrganizationID: orgID,
|
|
ID: userID,
|
|
Username: userID + gofakeit.Username(),
|
|
State: domain.UserStateActive,
|
|
Human: &domain.HumanUser{
|
|
FirstName: gofakeit.FirstName(),
|
|
LastName: gofakeit.LastName(),
|
|
Email: domain.HumanEmail{
|
|
Address: gofakeit.Email(),
|
|
VerifiedAt: time.Now(),
|
|
},
|
|
},
|
|
Metadata: []*domain.Metadata{
|
|
{
|
|
Key: "key1",
|
|
Value: []byte("value1"),
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
return userAgg
|
|
}
|