Files
zitadel/backend/v3/storage/database/events_testing/users_test.go
T
5df8a97f9d feat(repository): implement user repository (#10967)
## Summary

This PR introduces the relational user repository for backend v3,
including schema, repository APIs, reducers/projections, and tests for
both human and machine users.

## What changed

* Added user-focused relational migrations (users, verifications,
metadata, PATs, machine keys, passkeys, identity provider links) and
related FK wiring.
* Implemented/expanded repository domain contracts and storage
implementations for human and machine user flows.
* Added support for machine secret handling and aligned access token
type modeling.
* Extended user domain models (verifications, password/email/phone/TOTP
state, passkeys, metadata, invite handling, identity provider links).
* Updated projection/reduction coverage and integration tests for key
user lifecycle events.
* Regenerated and updated mocks/enums plus supporting database
change/column utilities.

## Notes for reviewers

* This PR is large because it establishes the foundational relational
user storage layer and related test scaffolding. Sorry for that.

## Closes

Closes #10218
Closes #10224
Closes #10225
Closes #10209
Closes #10802

---------

Co-authored-by: Iraq Jaber <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Marco A. <[email protected]>
Co-authored-by: Tim Möhlmann <[email protected]>
2026-02-23 16:03:47 +01:00

1108 lines
35 KiB
Go

//go:build integration
package events_test
import (
"bytes"
_ "embed"
"encoding/json"
"testing"
"time"
"github.com/brianvoe/gofakeit/v6"
"github.com/go-resty/resty/v2"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/headzoo/surf.v1"
"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/integration"
"github.com/zitadel/zitadel/pkg/grpc/management"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2"
"github.com/zitadel/zitadel/pkg/grpc/user"
v2_user "github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func TestServer_TestHumanUserReduces(t *testing.T) {
instanceID := Instance.ID()
orgID := Instance.DefaultOrg.Id
userRepo := repository.UserRepository()
t.Run("test human user added reduced", func(t *testing.T) {
humanUserRequest := &management.AddHumanUserRequest{
UserName: gofakeit.Username(),
Profile: &management.AddHumanUserRequest_Profile{
FirstName: "first",
LastName: "last",
NickName: "nick",
DisplayName: "display",
PreferredLanguage: "en",
Gender: user.Gender_GENDER_MALE,
},
Email: &management.AddHumanUserRequest_Email{
Email: gofakeit.Email(),
IsEmailVerified: true,
},
Phone: &management.AddHumanUserRequest_Phone{
Phone: "+1" + gofakeit.Phone(),
IsPhoneVerified: true,
},
InitialPassword: "Password1!",
}
before := time.Now()
resp, err := MgmtClient.AddHumanUser(IAMCTX, humanUserRequest)
require.NoError(t, err)
after := time.Now()
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*15)
assert.EventuallyWithT(t, func(t *assert.CollectT) {
user, err := userRepo.Get(
CTX,
pool,
database.WithCondition(database.And(
userRepo.InstanceIDCondition(instanceID),
userRepo.OrganizationIDCondition(orgID),
userRepo.IDCondition(resp.UserId),
)),
)
require.NoError(t, err)
// event user.added
// event user.human.added
// domain.User
assert.Equal(t, instanceID, user.InstanceID)
assert.Equal(t, orgID, user.OrganizationID)
assert.Equal(t, resp.UserId, user.ID)
assert.Equal(t, humanUserRequest.UserName, user.Username)
assert.Equal(t, domain.UserStateActive, user.State)
assert.WithinRange(t, user.UpdatedAt, before, after)
assert.WithinRange(t, user.CreatedAt, before, after)
// Email
assert.Equal(t, humanUserRequest.Email.Email, user.Human.Email.Address)
assert.NotZero(t, user.Human.Email.VerifiedAt)
assert.Nil(t, user.Human.Email.PendingVerification)
// Phone
if !assert.NotNil(t, humanUserRequest.Phone) {
assert.Equal(t, humanUserRequest.Phone.Phone, user.Human.Phone.Number)
assert.NotZero(t, user.Human.Phone.VerifiedAt)
assert.Nil(t, user.Human.Phone.PendingVerification)
}
// Human
assert.Equal(t, humanUserRequest.Profile.FirstName, user.Human.FirstName)
assert.Equal(t, humanUserRequest.Profile.LastName, user.Human.LastName)
assert.Equal(t, humanUserRequest.Profile.NickName, user.Human.Nickname)
assert.Equal(t, humanUserRequest.Profile.DisplayName, user.Human.DisplayName)
assert.Equal(t, humanUserRequest.Profile.PreferredLanguage, user.Human.PreferredLanguage.String())
assert.EqualValues(t, humanUserRequest.Profile.Gender, user.Human.Gender)
}, retryDuration, tick)
})
t.Run("test human user register reduced", func(t *testing.T) {
token := integration.SystemToken
client := resty.New()
bow := surf.NewBrowser()
err := bow.Open("http://localhost:8082" + "/ui/login/register/org")
require.NoError(t, err)
require.Equal(t, 200, bow.StatusCode())
csfr, err := bow.Forms()[1].Value("gorilla.csrf.Token")
require.NoError(t, err)
before := time.Now()
client.SetCookieJar(bow.CookieJar())
firstName := gofakeit.Name()
lastName := gofakeit.Name()
email := gofakeit.Email()
out, err := client.R().SetAuthToken(token).
SetFormData(map[string]string{
"gorilla.csrf.Token": csfr,
"orgname": gofakeit.Name(),
"firstname": firstName,
"lastname": lastName,
"email": email,
"register-password": "Password1!",
"register-password-confirmation": "Password1!",
}).
Post("http://localhost:8082" + "/ui/login/register/org")
require.NoError(t, err)
require.Equal(t, 200, out.StatusCode())
after := time.Now()
ctx := t.Context()
instanceRepo := repository.InstanceRepository()
instance, err := instanceRepo.Get(ctx, pool, database.WithCondition(instanceRepo.NameCondition(database.TextOperationEqual, "ZITADEL")))
assert.NoError(t, err)
instanceID := instance.ID
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*20)
assert.EventuallyWithT(t, func(t *assert.CollectT) {
user, err := userRepo.Get(
CTX,
pool,
database.WithCondition(database.And(
userRepo.InstanceIDCondition(instanceID),
userRepo.Human().FirstNameCondition(database.TextOperationEqual, firstName),
userRepo.Human().LastNameCondition(database.TextOperationEqual, lastName),
)),
)
require.NoError(t, err)
// domain.User
assert.Equal(t, instanceID, user.InstanceID)
assert.Equal(t, email, user.Username)
assert.Equal(t, domain.UserStateActive, user.State)
assert.WithinRange(t, user.CreatedAt, before, after)
// Email
assert.Equal(t, email, user.Human.Email.UnverifiedAddress)
assert.Zero(t, user.Human.Email.VerifiedAt)
assert.Nil(t, user.Human.Email.PendingVerification)
// Human
assert.Equal(t, firstName, user.Human.FirstName)
assert.Equal(t, lastName, user.Human.LastName)
}, retryDuration, tick)
})
t.Run("test human user locked reduced", func(t *testing.T) {
humanUserRequest := &management.AddHumanUserRequest{
UserName: gofakeit.Username(),
Profile: &management.AddHumanUserRequest_Profile{
FirstName: "first",
LastName: "last",
NickName: "nick",
DisplayName: "display",
PreferredLanguage: "en",
Gender: user.Gender_GENDER_MALE,
},
Email: &management.AddHumanUserRequest_Email{
Email: gofakeit.Email(),
IsEmailVerified: true,
},
Phone: &management.AddHumanUserRequest_Phone{
Phone: "+1" + gofakeit.Phone(),
IsPhoneVerified: true,
},
InitialPassword: "Password1!",
}
resp, err := MgmtClient.AddHumanUser(IAMCTX, humanUserRequest)
require.NoError(t, err)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*15)
assert.EventuallyWithT(t, func(t *assert.CollectT) {
user, err := userRepo.Get(
CTX,
pool,
database.WithCondition(database.And(
userRepo.InstanceIDCondition(instanceID),
userRepo.OrganizationIDCondition(orgID),
userRepo.IDCondition(resp.UserId),
)),
)
require.NoError(t, err)
assert.Equal(t, domain.UserStateActive, user.State)
}, retryDuration, tick)
before := time.Now()
_, err = MgmtClient.LockUser(IAMCTX, &management.LockUserRequest{
Id: resp.UserId,
})
require.NoError(t, err)
after := time.Now()
retryDuration, tick = integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*15)
assert.EventuallyWithT(t, func(t *assert.CollectT) {
user, err := userRepo.Get(
CTX,
pool,
database.WithCondition(database.And(
userRepo.InstanceIDCondition(instanceID),
userRepo.OrganizationIDCondition(orgID),
userRepo.IDCondition(resp.UserId),
)),
)
require.NoError(t, err)
// event user.locked
assert.Equal(t, domain.UserStateLocked, user.State)
assert.WithinRange(t, user.UpdatedAt, before, after)
}, retryDuration, tick)
})
t.Run("test human user unlock reduced", func(t *testing.T) {
humanUserRequest := &management.AddHumanUserRequest{
UserName: gofakeit.Username(),
Profile: &management.AddHumanUserRequest_Profile{
FirstName: "first",
LastName: "last",
NickName: "nick",
DisplayName: "display",
PreferredLanguage: "en",
Gender: user.Gender_GENDER_MALE,
},
Email: &management.AddHumanUserRequest_Email{
Email: gofakeit.Email(),
IsEmailVerified: true,
},
Phone: &management.AddHumanUserRequest_Phone{
Phone: "+1" + gofakeit.Phone(),
IsPhoneVerified: true,
},
InitialPassword: "Password1!",
}
resp, err := MgmtClient.AddHumanUser(IAMCTX, humanUserRequest)
require.NoError(t, err)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*15)
assert.EventuallyWithT(t, func(t *assert.CollectT) {
user, err := userRepo.Get(
CTX,
pool,
database.WithCondition(database.And(
userRepo.InstanceIDCondition(instanceID),
userRepo.OrganizationIDCondition(orgID),
userRepo.IDCondition(resp.UserId),
)),
)
require.NoError(t, err)
assert.Equal(t, domain.UserStateActive, user.State)
}, retryDuration, tick)
_, err = MgmtClient.LockUser(IAMCTX, &management.LockUserRequest{
Id: resp.UserId,
})
require.NoError(t, err)
retryDuration, tick = integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*15)
assert.EventuallyWithT(t, func(t *assert.CollectT) {
user, err := userRepo.Get(
CTX,
pool,
database.WithCondition(database.And(
userRepo.InstanceIDCondition(instanceID),
userRepo.OrganizationIDCondition(orgID),
userRepo.IDCondition(resp.UserId),
)),
)
require.NoError(t, err)
assert.Equal(t, domain.UserStateLocked, user.State)
}, retryDuration, tick)
before := time.Now()
_, err = MgmtClient.UnlockUser(IAMCTX, &management.UnlockUserRequest{
Id: resp.UserId,
})
require.NoError(t, err)
after := time.Now()
retryDuration, tick = integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*15)
assert.EventuallyWithT(t, func(t *assert.CollectT) {
user, err := userRepo.Get(
CTX,
pool,
database.WithCondition(database.And(
userRepo.InstanceIDCondition(instanceID),
userRepo.OrganizationIDCondition(orgID),
userRepo.IDCondition(resp.UserId),
)),
)
require.NoError(t, err)
// event user.unlocked
assert.Equal(t, domain.UserStateActive, user.State)
assert.WithinRange(t, user.UpdatedAt, before, after)
}, retryDuration, tick)
})
t.Run("test human user deactivate reduced", func(t *testing.T) {
humanUserRequest := &management.AddHumanUserRequest{
UserName: gofakeit.Username(),
Profile: &management.AddHumanUserRequest_Profile{
FirstName: "first",
LastName: "last",
NickName: "nick",
DisplayName: "display",
PreferredLanguage: "en",
Gender: user.Gender_GENDER_MALE,
},
Email: &management.AddHumanUserRequest_Email{
Email: gofakeit.Email(),
IsEmailVerified: true,
},
Phone: &management.AddHumanUserRequest_Phone{
Phone: "+1" + gofakeit.Phone(),
IsPhoneVerified: true,
},
InitialPassword: "Password1!",
}
resp, err := MgmtClient.AddHumanUser(IAMCTX, humanUserRequest)
require.NoError(t, err)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*15)
assert.EventuallyWithT(t, func(t *assert.CollectT) {
user, err := userRepo.Get(
CTX,
pool,
database.WithCondition(database.And(
userRepo.InstanceIDCondition(instanceID),
userRepo.OrganizationIDCondition(orgID),
userRepo.IDCondition(resp.UserId),
)),
)
require.NoError(t, err)
assert.Equal(t, domain.UserStateActive, user.State)
}, retryDuration, tick)
before := time.Now()
_, err = MgmtClient.DeactivateUser(IAMCTX, &management.DeactivateUserRequest{
Id: resp.UserId,
})
require.NoError(t, err)
after := time.Now()
retryDuration, tick = integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*15)
assert.EventuallyWithT(t, func(t *assert.CollectT) {
user, err := userRepo.Get(
CTX,
pool,
database.WithCondition(database.And(
userRepo.InstanceIDCondition(instanceID),
userRepo.OrganizationIDCondition(orgID),
userRepo.IDCondition(resp.UserId),
)),
)
require.NoError(t, err)
// event user.deactivated
assert.Equal(t, domain.UserStateInactive, user.State)
assert.WithinRange(t, user.UpdatedAt, before, after)
}, retryDuration, tick)
})
t.Run("test human user reactivate reduced", func(t *testing.T) {
humanUserRequest := &management.AddHumanUserRequest{
UserName: gofakeit.Username(),
Profile: &management.AddHumanUserRequest_Profile{
FirstName: "first",
LastName: "last",
NickName: "nick",
DisplayName: "display",
PreferredLanguage: "en",
Gender: user.Gender_GENDER_MALE,
},
Email: &management.AddHumanUserRequest_Email{
Email: gofakeit.Email(),
IsEmailVerified: true,
},
Phone: &management.AddHumanUserRequest_Phone{
Phone: "+1" + gofakeit.Phone(),
IsPhoneVerified: true,
},
InitialPassword: "Password1!",
}
resp, err := MgmtClient.AddHumanUser(IAMCTX, humanUserRequest)
require.NoError(t, err)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*15)
assert.EventuallyWithT(t, func(t *assert.CollectT) {
user, err := userRepo.Get(
CTX,
pool,
database.WithCondition(database.And(
userRepo.InstanceIDCondition(instanceID),
userRepo.OrganizationIDCondition(orgID),
userRepo.IDCondition(resp.UserId),
)),
)
require.NoError(t, err)
assert.Equal(t, domain.UserStateActive, user.State)
}, retryDuration, tick)
_, err = MgmtClient.DeactivateUser(IAMCTX, &management.DeactivateUserRequest{
Id: resp.UserId,
})
require.NoError(t, err)
retryDuration, tick = integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*15)
assert.EventuallyWithT(t, func(t *assert.CollectT) {
user, err := userRepo.Get(
CTX,
pool,
database.WithCondition(database.And(
userRepo.InstanceIDCondition(instanceID),
userRepo.OrganizationIDCondition(orgID),
userRepo.IDCondition(resp.UserId),
)),
)
require.NoError(t, err)
assert.Equal(t, domain.UserStateInactive, user.State)
}, retryDuration, tick)
before := time.Now()
_, err = MgmtClient.ReactivateUser(IAMCTX, &management.ReactivateUserRequest{
Id: resp.UserId,
})
require.NoError(t, err)
after := time.Now()
retryDuration, tick = integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*15)
assert.EventuallyWithT(t, func(t *assert.CollectT) {
user, err := userRepo.Get(
CTX,
pool,
database.WithCondition(database.And(
userRepo.InstanceIDCondition(instanceID),
userRepo.OrganizationIDCondition(orgID),
userRepo.IDCondition(resp.UserId),
)),
)
require.NoError(t, err)
// event user.reactivated
assert.Equal(t, domain.UserStateActive, user.State)
assert.WithinRange(t, user.UpdatedAt, before, after)
}, retryDuration, tick)
})
t.Run("test human user removed reduced", func(t *testing.T) {
humanUserRequest := &management.AddHumanUserRequest{
UserName: gofakeit.Username(),
Profile: &management.AddHumanUserRequest_Profile{
FirstName: "first",
LastName: "last",
NickName: "nick",
DisplayName: "display",
PreferredLanguage: "en",
Gender: user.Gender_GENDER_MALE,
},
Email: &management.AddHumanUserRequest_Email{
Email: gofakeit.Email(),
IsEmailVerified: true,
},
InitialPassword: "Password1!",
}
resp, err := MgmtClient.AddHumanUser(IAMCTX, humanUserRequest)
require.NoError(t, err)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*15)
assert.EventuallyWithT(t, func(t *assert.CollectT) {
user, err := userRepo.Get(
CTX,
pool,
database.WithCondition(database.And(
userRepo.InstanceIDCondition(instanceID),
userRepo.OrganizationIDCondition(orgID),
userRepo.IDCondition(resp.UserId),
)),
)
require.NoError(t, err)
assert.NotNil(t, user)
}, retryDuration, tick)
_, err = UserClient.DeleteUser(IAMCTX, &v2_user.DeleteUserRequest{
UserId: resp.UserId,
})
require.NoError(t, err)
retryDuration, tick = integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*15)
assert.EventuallyWithT(t, func(t *assert.CollectT) {
_, err := userRepo.Get(
CTX,
pool,
database.WithCondition(database.And(
userRepo.InstanceIDCondition(instanceID),
userRepo.OrganizationIDCondition(orgID),
userRepo.IDCondition(resp.UserId),
)),
)
// event user.removed
require.ErrorIs(t, &database.NoRowFoundError{}, err)
}, retryDuration, tick)
})
t.Run("test human user change name reduced", func(t *testing.T) {
humanUserRequest := &management.AddHumanUserRequest{
UserName: gofakeit.Username(),
Profile: &management.AddHumanUserRequest_Profile{
FirstName: "first",
LastName: "last",
NickName: "nick",
DisplayName: "display",
PreferredLanguage: "en",
Gender: user.Gender_GENDER_MALE,
},
Email: &management.AddHumanUserRequest_Email{
Email: gofakeit.Email(),
IsEmailVerified: true,
},
InitialPassword: "Password1!",
}
resp, err := MgmtClient.AddHumanUser(IAMCTX, humanUserRequest)
require.NoError(t, err)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*15)
assert.EventuallyWithT(t, func(t *assert.CollectT) {
user, err := userRepo.Get(
CTX,
pool,
database.WithCondition(database.And(
userRepo.InstanceIDCondition(instanceID),
userRepo.OrganizationIDCondition(orgID),
userRepo.IDCondition(resp.UserId),
)),
)
require.NoError(t, err)
assert.NotNil(t, user)
}, retryDuration, tick)
username := gofakeit.Username()
_, err = UserClient.UpdateHumanUser(IAMCTX, &v2_user.UpdateHumanUserRequest{
UserId: resp.UserId,
Username: &username,
})
require.NoError(t, err)
retryDuration, tick = integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*15)
assert.EventuallyWithT(t, func(t *assert.CollectT) {
user, err := userRepo.Get(
CTX,
pool,
database.WithCondition(database.And(
userRepo.InstanceIDCondition(instanceID),
userRepo.OrganizationIDCondition(orgID),
userRepo.IDCondition(resp.UserId),
)),
)
require.NoError(t, err)
// event user.username.changed
assert.Equal(t, username, user.Username)
}, retryDuration, tick)
})
t.Run("test human user profile change reduced", func(t *testing.T) {
humanUserRequest := &management.AddHumanUserRequest{
UserName: gofakeit.Username(),
Profile: &management.AddHumanUserRequest_Profile{
FirstName: "first",
LastName: "last",
NickName: "nick",
DisplayName: "display",
PreferredLanguage: "en",
Gender: user.Gender_GENDER_MALE,
},
Email: &management.AddHumanUserRequest_Email{
Email: gofakeit.Email(),
IsEmailVerified: true,
},
Phone: &management.AddHumanUserRequest_Phone{
Phone: "+1" + gofakeit.Phone(),
IsPhoneVerified: true,
},
InitialPassword: "Password1!",
}
resp, err := MgmtClient.AddHumanUser(IAMCTX, humanUserRequest)
require.NoError(t, err)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*15)
assert.EventuallyWithT(t, func(t *assert.CollectT) {
user, err := userRepo.Get(
CTX,
pool,
database.WithCondition(database.And(
userRepo.InstanceIDCondition(instanceID),
userRepo.OrganizationIDCondition(orgID),
userRepo.IDCondition(resp.UserId),
)),
)
require.NoError(t, err)
assert.NotNil(t, user)
}, retryDuration, tick)
profile := &v2_user.SetHumanProfile{
GivenName: gofakeit.FirstName(),
FamilyName: gofakeit.LastName(),
NickName: gu.Ptr(gofakeit.Color()),
DisplayName: gu.Ptr(gofakeit.Name()),
PreferredLanguage: gu.Ptr("fr"),
Gender: gu.Ptr(v2_user.Gender_GENDER_FEMALE),
}
_, err = UserClient.UpdateHumanUser(IAMCTX, &v2_user.UpdateHumanUserRequest{
UserId: resp.UserId,
Profile: profile,
})
require.NoError(t, err)
retryDuration, tick = integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*15)
assert.EventuallyWithT(t, func(t *assert.CollectT) {
user, err := userRepo.Get(
CTX,
pool,
database.WithCondition(database.And(
userRepo.InstanceIDCondition(instanceID),
userRepo.OrganizationIDCondition(orgID),
userRepo.IDCondition(resp.UserId),
)),
)
require.NoError(t, err)
// event user.human.profile.changed
assert.Equal(t, profile.GivenName, user.Human.FirstName)
assert.Equal(t, profile.FamilyName, user.Human.LastName)
assert.Equal(t, *profile.NickName, user.Human.Nickname)
assert.Equal(t, *profile.DisplayName, user.Human.DisplayName)
assert.Equal(t, *profile.PreferredLanguage, user.Human.PreferredLanguage.String())
assert.EqualValues(t, *profile.Gender, user.Human.Gender)
}, retryDuration, tick)
})
// I added a delete user command (Over REST because there is no gRPC client (as far as I can tell) for the default instance)
// the issue is if the user does not exist it will throw and error when trying to delete it
t.Run("test human user avatar added reduced", func(t *testing.T) {
ctx := t.Context()
token := integration.SystemToken
instanceRepo := repository.InstanceRepository()
instance, err := instanceRepo.Get(ctx, pool, database.WithCondition(instanceRepo.NameCondition(database.TextOperationEqual, "ZITADEL")))
instanceID := instance.ID
require.NoError(t, err)
orgRepo := repository.OrganizationRepository()
org_, err := orgRepo.Get(ctx, pool, database.WithCondition(database.And(orgRepo.InstanceIDCondition(instanceID), orgRepo.NameCondition(database.TextOperationEqual, "ZITADEL"))))
require.NoError(t, err)
orgID := org_.ID
// delete user tester
deleteUserReq := v2_user.DeleteUserRequest{
UserId: "tester",
}
deleteUserReqJSON, err := json.Marshal(&deleteUserReq)
require.NoError(t, err)
client := resty.New()
out, err := client.R().SetAuthToken(token).
SetHeader("x-zitadel-orgid", orgID).
SetBody(deleteUserReqJSON).
Delete("http://localhost:8082" + "/v2beta/users/human")
require.NoError(t, err)
// require.Equal(t, 200, out.StatusCode())
// require.NoError(t, err)
// create user tester
createUserReq := &v2_user.AddHumanUserRequest{
UserId: gu.Ptr("tester"), // YOLO
Organization: &object.Organization{
Org: &object.Organization_OrgId{
OrgId: orgID,
},
},
Profile: &v2_user.SetHumanProfile{
GivenName: "Donald",
FamilyName: "Duck",
NickName: gu.Ptr("Dukkie"),
DisplayName: gu.Ptr("Donald Duck"),
PreferredLanguage: gu.Ptr("en"),
Gender: v2_user.Gender_GENDER_DIVERSE.Enum(),
},
Email: &v2_user.SetHumanEmail{
Email: gofakeit.Email(),
},
Phone: &v2_user.SetHumanPhone{},
Metadata: []*v2_user.SetMetadataEntry{
{
Key: "somekey",
Value: []byte("somevalue"),
},
},
PasswordType: &v2_user.AddHumanUserRequest_Password{
Password: &v2_user.Password{
Password: "DifficultPW666!",
ChangeRequired: true,
},
},
}
createUserReqJSON, err := json.Marshal(createUserReq)
require.NoError(t, err)
client = resty.New()
out, err = client.R().SetAuthToken(token).
SetHeader("x-zitadel-orgid", orgID).
SetBody(createUserReqJSON).
Post("http://localhost:8082" + "/v2beta/users/human")
require.NoError(t, err)
// require.Equal(t, 201, out.StatusCode())
require.NoError(t, err)
// client = resty.New()
// POST avatar
before := time.Now()
out, err = client.R().SetAuthToken(token).
SetMultipartField("file", "filename", "image/png", bytes.NewReader(picture)).
SetHeader("x-zitadel-orgid", orgID).
Post("http://localhost:8082" + "/assets/v1" + "/users/me/avatar")
require.NoError(t, err)
require.Equal(t, 200, out.StatusCode())
after := time.Now()
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*15)
assert.EventuallyWithT(t, func(t *assert.CollectT) {
user, err := userRepo.Get(
CTX,
pool,
database.WithCondition(database.And(
userRepo.InstanceIDCondition(instanceID),
userRepo.OrganizationIDCondition(orgID),
userRepo.IDCondition("tester"),
)),
)
require.NoError(t, err)
// event user.human.avatar.added
avatarUrl := "users/tester/avatar?"
assert.Contains(t, user.Human.AvatarKey, avatarUrl)
assert.WithinRange(t, user.UpdatedAt, before, after)
}, retryDuration, tick)
})
// I added a delete user command (Over REST because there is no gRPC client (as far as I can tell) for the default instance)
// the issue is if the user does not exist it will throw and error when trying to delete it
t.Run("test human user avatar removed reduced", func(t *testing.T) {
ctx := t.Context()
token := integration.SystemToken
instanceRepo := repository.InstanceRepository()
instance, err := instanceRepo.Get(ctx, pool, database.WithCondition(instanceRepo.NameCondition(database.TextOperationEqual, "ZITADEL")))
instanceID := instance.ID
require.NoError(t, err)
orgRepo := repository.OrganizationRepository()
org_, err := orgRepo.Get(ctx, pool, database.WithCondition(database.And(orgRepo.InstanceIDCondition(instanceID), orgRepo.NameCondition(database.TextOperationEqual, "ZITADEL"))))
require.NoError(t, err)
orgID := org_.ID
// delete user tester
deleteUserReq := v2_user.DeleteUserRequest{
UserId: "tester",
}
deleteUserReqJSON, err := json.Marshal(&deleteUserReq)
require.NoError(t, err)
client := resty.New()
out, err := client.R().SetAuthToken(token).
SetHeader("x-zitadel-orgid", orgID).
SetBody(deleteUserReqJSON).
Delete("http://localhost:8082" + "/v2beta/users/human")
require.NoError(t, err)
// require.Equal(t, 200, out.StatusCode())
// require.NoError(t, err)
// create user tester
createUserReq := &v2_user.AddHumanUserRequest{
UserId: gu.Ptr("tester"), // YOLO
Organization: &object.Organization{
Org: &object.Organization_OrgId{
OrgId: orgID,
},
},
Profile: &v2_user.SetHumanProfile{
GivenName: "Donald",
FamilyName: "Duck",
NickName: gu.Ptr("Dukkie"),
DisplayName: gu.Ptr("Donald Duck"),
PreferredLanguage: gu.Ptr("en"),
Gender: v2_user.Gender_GENDER_DIVERSE.Enum(),
},
Email: &v2_user.SetHumanEmail{
Email: gofakeit.Email(),
},
Phone: &v2_user.SetHumanPhone{},
Metadata: []*v2_user.SetMetadataEntry{
{
Key: "somekey",
Value: []byte("somevalue"),
},
},
PasswordType: &v2_user.AddHumanUserRequest_Password{
Password: &v2_user.Password{
Password: "DifficultPW666!",
ChangeRequired: true,
},
},
}
createUserReqJSON, err := json.Marshal(createUserReq)
require.NoError(t, err)
client = resty.New()
out, err = client.R().SetAuthToken(token).
SetHeader("x-zitadel-orgid", orgID).
SetBody(createUserReqJSON).
Post("http://localhost:8082" + "/v2beta/users/human")
require.NoError(t, err)
// require.Equal(t, 201, out.StatusCode())
require.NoError(t, err)
// POST avatar
out, err = client.R().SetAuthToken(token).
SetMultipartField("file", "filename", "image/png", bytes.NewReader(picture)).
SetHeader("x-zitadel-orgid", orgID).
Post("http://localhost:8082" + "/assets/v1" + "/users/me/avatar")
require.NoError(t, err)
require.Equal(t, 200, out.StatusCode())
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*15)
assert.EventuallyWithT(t, func(t *assert.CollectT) {
user, err := userRepo.Get(
CTX,
pool,
database.WithCondition(database.And(
userRepo.InstanceIDCondition(instanceID),
userRepo.OrganizationIDCondition(orgID),
userRepo.IDCondition("tester"),
)),
)
require.NoError(t, err)
require.NotEmpty(t, user.Human.AvatarKey)
avatarUrl := "users/tester/avatar?"
assert.Equal(t, avatarUrl, string(user.Human.AvatarKey)[:len(avatarUrl)])
}, retryDuration, tick)
// delete avatar
before := time.Now()
out, err = client.R().SetAuthToken(token).
SetHeader("x-zitadel-orgid", orgID).
Delete("http://localhost:8082/auth/v1/users/me/avatar")
require.NoError(t, err)
require.Equal(t, 200, out.StatusCode())
after := time.Now()
retryDuration, tick = integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*15)
assert.EventuallyWithT(t, func(t *assert.CollectT) {
user, err := userRepo.Get(
CTX,
pool,
database.WithCondition(database.And(
userRepo.InstanceIDCondition(instanceID),
userRepo.OrganizationIDCondition(orgID),
userRepo.IDCondition("tester"),
)),
)
require.NoError(t, err)
assert.Empty(t, user.Human.AvatarKey)
assert.WithinRange(t, user.UpdatedAt, before, after)
}, retryDuration, tick)
})
t.Run("test human user password change reduced", func(t *testing.T) {
createUserReq := &v2_user.AddHumanUserRequest{
// UserId: gu.Ptr("tester"),
Organization: &object.Organization{
Org: &object.Organization_OrgId{
OrgId: orgID,
},
},
Profile: &v2_user.SetHumanProfile{
GivenName: "Donald",
FamilyName: "Duck",
NickName: gu.Ptr("Dukkie"),
DisplayName: gu.Ptr("Donald Duck"),
PreferredLanguage: gu.Ptr("en"),
Gender: v2_user.Gender_GENDER_DIVERSE.Enum(),
},
Email: &v2_user.SetHumanEmail{
Email: gofakeit.Email(),
},
Phone: &v2_user.SetHumanPhone{},
Metadata: []*v2_user.SetMetadataEntry{
{
Key: "somekey",
Value: []byte("somevalue"),
},
},
PasswordType: &v2_user.AddHumanUserRequest_Password{
Password: &v2_user.Password{
Password: "Password1!",
ChangeRequired: true,
},
},
}
res, err := UserClient.AddHumanUser(IAMCTX, createUserReq)
require.NoError(t, err)
userID := res.UserId
// make sure user exists
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*15)
assert.EventuallyWithT(t, func(t *assert.CollectT) {
user, err := userRepo.Get(
CTX,
pool,
database.WithCondition(database.And(
userRepo.InstanceIDCondition(instanceID),
userRepo.OrganizationIDCondition(orgID),
userRepo.IDCondition(userID),
)),
)
require.NoError(t, err)
assert.NotNil(t, user)
}, retryDuration, tick)
// change user password
before := time.Now()
_, err = UserClient.UpdateHumanUser(IAMCTX, &v2_user.UpdateHumanUserRequest{
UserId: userID,
Password: &v2_user.SetPassword{
PasswordType: &v2_user.SetPassword_Password{
Password: &v2_user.Password{
Password: "Password2!",
ChangeRequired: true,
},
},
Verification: &v2_user.SetPassword_CurrentPassword{
CurrentPassword: "Password1!",
},
},
})
require.NoError(t, err)
after := time.Now()
// check password updated
retryDuration, tick = integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*15)
assert.EventuallyWithT(t, func(t *assert.CollectT) {
humanUser, err := userRepo.Get(
CTX,
pool,
database.WithCondition(database.And(
userRepo.InstanceIDCondition(instanceID),
userRepo.OrganizationIDCondition(orgID),
userRepo.IDCondition(userID),
)),
)
require.NoError(t, err)
assert.Equal(t, true, humanUser.Human.Password.IsChangeRequired)
assert.WithinRange(t, humanUser.Human.Password.ChangedAt, before, after)
}, retryDuration, tick)
})
}
func TestServer_TestMachineUserReduces(t *testing.T) {
instanceID := Instance.ID()
orgID := Instance.DefaultOrg.Id
userRepo := repository.UserRepository()
t.Run("test machine user add reduced", func(t *testing.T) {
machineUserRequest := &management.AddMachineUserRequest{
UserId: gu.Ptr(gofakeit.Name()),
UserName: gofakeit.Name(),
Name: gofakeit.Name(),
Description: gofakeit.Blurb(),
AccessTokenType: user.AccessTokenType_ACCESS_TOKEN_TYPE_BEARER,
}
before := time.Now()
resp, err := MgmtClient.AddMachineUser(IAMCTX, machineUserRequest)
require.NoError(t, err)
after := time.Now()
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*15)
assert.EventuallyWithT(t, func(t *assert.CollectT) {
user, err := userRepo.Get(
CTX,
pool,
database.WithCondition(database.And(
userRepo.InstanceIDCondition(instanceID),
userRepo.OrganizationIDCondition(orgID),
userRepo.IDCondition(resp.UserId),
)),
)
require.NoError(t, err)
// event user.machine.added
// user
assert.Equal(t, instanceID, user.InstanceID)
assert.Equal(t, orgID, user.OrganizationID)
assert.Equal(t, *machineUserRequest.UserId, user.ID)
assert.Equal(t, machineUserRequest.UserName, user.Username)
assert.Equal(t, domain.UserStateActive, user.State)
assert.WithinRange(t, user.UpdatedAt, before, after)
assert.WithinRange(t, user.CreatedAt, before, after)
// machine
assert.Equal(t, machineUserRequest.Name, user.Machine.Name)
assert.Equal(t, machineUserRequest.Description, user.Machine.Description)
assert.Equal(t, domain.AccessTokenTypeBearer, user.Machine.AccessTokenType)
}, retryDuration, tick)
})
t.Run("test machine user change reduced", func(t *testing.T) {
userID := gofakeit.Name()
machineUserRequest := &management.AddMachineUserRequest{
UserId: &userID,
UserName: gofakeit.Name(),
Name: gofakeit.Name(),
Description: gofakeit.Blurb(),
AccessTokenType: user.AccessTokenType_ACCESS_TOKEN_TYPE_BEARER,
}
_, err := MgmtClient.AddMachineUser(IAMCTX, machineUserRequest)
require.NoError(t, err)
updateMachineUserReq := &management.UpdateMachineRequest{
UserId: userID,
Name: gofakeit.Name(),
Description: gofakeit.Blurb(),
AccessTokenType: user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT,
}
before := time.Now()
_, err = MgmtClient.UpdateMachine(IAMCTX, updateMachineUserReq)
require.NoError(t, err)
after := time.Now()
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Second*15)
assert.EventuallyWithT(t, func(t *assert.CollectT) {
user, err := userRepo.Get(
CTX,
pool,
database.WithCondition(database.And(
userRepo.InstanceIDCondition(instanceID),
userRepo.OrganizationIDCondition(orgID),
userRepo.IDCondition(userID),
)),
)
require.NoError(t, err)
// event user.machine.changed
// user
assert.WithinRange(t, user.UpdatedAt, before, after)
// machine
assert.Equal(t, updateMachineUserReq.Name, user.Machine.Name)
assert.Equal(t, updateMachineUserReq.Description, user.Machine.Description)
assert.Equal(t, domain.AccessTokenTypeJWT, user.Machine.AccessTokenType)
}, retryDuration, tick, "event not reduced in time for user %q", userID)
t.Log("hodor")
})
}