test: add tests for recovery codes event reducers (#11773)

# Which Problems Are Solved

Add integration tests for recovery codes event reducers in the RT model.

# How the Problems Are Solved

Added integration tests for recovery codes event reducers

# Additional Changes

N/A

# Additional Context
- Closes https://github.com/zitadel/zitadel/issues/11707
- Todo: add tests for other reducers in the `users_relational`
projection.

---------

Co-authored-by: Livio Spring <[email protected]>
This commit is contained in:
Gayathri Vijayan
2026-03-09 10:40:02 +00:00
committed by GitHub
co-authored by Livio Spring
parent 282b2486d9
commit 3f258953ce
2 changed files with 225 additions and 0 deletions
@@ -424,6 +424,7 @@ func (p *userRelationalProjection) Reducers() []handler.AggregateReducer {
Event: user.HumanInviteCheckFailedType,
Reduce: p.reduceInviteCheckFailed,
},
// recovery code reducers
{
Event: user.HumanRecoveryCodesAddedType,
Reduce: p.reduceRecoveryCodesAdded,
@@ -0,0 +1,224 @@
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 := &userRelationalProjection{}
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
}