zitadel/internal/command/instance_test.go
Elio Bischof a02a534cd2
feat: initial admin PAT has IAM_LOGIN_CLIENT (#10143)
# Which Problems Are Solved

We provide a seamless way to initialize Zitadel and the login together.

# How the Problems Are Solved

Additionally to the `IAM_OWNER` role, a set up admin user also gets the
`IAM_LOGIN_CLIENT` role if it is a machine user with a PAT.

# Additional Changes

- Simplifies the load balancing example, as the intermediate
configuration step is not needed anymore.

# Additional Context

- Depends on #10116 
- Contributes to https://github.com/zitadel/zitadel-charts/issues/332
- Contributes to https://github.com/zitadel/zitadel/issues/10016

---------

Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com>
2025-07-02 09:14:36 +00:00

1727 lines
56 KiB
Go

package command
import (
"context"
"encoding/json"
"slices"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/cache/connector/noop"
"github.com/zitadel/zitadel/internal/command/preparation"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/id"
id_mock "github.com/zitadel/zitadel/internal/id/mock"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/milestone"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/repository/project"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/zerrors"
)
func instanceSetupZitadelIDs() ZitadelConfig {
return ZitadelConfig{
instanceID: "INSTANCE",
orgID: "ORG",
projectID: "PROJECT",
consoleAppID: "console-id",
authAppID: "auth-id",
mgmtAppID: "mgmt-id",
adminAppID: "admin-id",
}
}
func projectAddedEvents(ctx context.Context, instanceID, orgID, id, owner string, externalSecure bool) []eventstore.Command {
events := []eventstore.Command{
project.NewProjectAddedEvent(ctx,
&project.NewAggregate(id, orgID).Aggregate,
"ZITADEL",
false,
false,
false,
domain.PrivateLabelingSettingUnspecified,
),
instance.NewIAMProjectSetEvent(ctx,
&instance.NewAggregate(instanceID).Aggregate,
id,
),
}
events = append(events, apiAppEvents(ctx, orgID, id, "mgmt-id", "Management-API")...)
events = append(events, apiAppEvents(ctx, orgID, id, "admin-id", "Admin-API")...)
events = append(events, apiAppEvents(ctx, orgID, id, "auth-id", "Auth-API")...)
consoleAppID := "console-id"
consoleClientID := "clientID"
events = append(events, oidcAppEvents(ctx, orgID, id, consoleAppID, "Console", consoleClientID, externalSecure)...)
events = append(events,
instance.NewIAMConsoleSetEvent(ctx,
&instance.NewAggregate(instanceID).Aggregate,
&consoleClientID,
&consoleAppID,
),
)
return events
}
func projectClientIDs() []string {
return []string{"clientID", "clientID", "clientID", "clientID"}
}
func apiAppEvents(ctx context.Context, orgID, projectID, id, name string) []eventstore.Command {
return []eventstore.Command{
project.NewApplicationAddedEvent(
ctx,
&project.NewAggregate(projectID, orgID).Aggregate,
id,
name,
),
project.NewAPIConfigAddedEvent(ctx,
&project.NewAggregate(projectID, orgID).Aggregate,
id,
"clientID",
"",
domain.APIAuthMethodTypePrivateKeyJWT,
),
}
}
func oidcAppEvents(ctx context.Context, orgID, projectID, id, name, clientID string, externalSecure bool) []eventstore.Command {
return []eventstore.Command{
project.NewApplicationAddedEvent(
ctx,
&project.NewAggregate(projectID, orgID).Aggregate,
id,
name,
),
project.NewOIDCConfigAddedEvent(ctx,
&project.NewAggregate(projectID, orgID).Aggregate,
domain.OIDCVersionV1,
id,
clientID,
"",
[]string{},
[]domain.OIDCResponseType{domain.OIDCResponseTypeCode},
[]domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
domain.OIDCApplicationTypeUserAgent,
domain.OIDCAuthMethodTypeNone,
[]string{},
!externalSecure,
domain.OIDCTokenTypeBearer,
false,
false,
false,
0,
nil,
false,
"",
domain.LoginVersionUnspecified,
"",
),
}
}
func orgFilters(orgID string, machine, human, loginClient bool) []expect {
filters := []expect{
expectFilter(),
expectFilter(
org.NewOrgAddedEvent(context.Background(), &org.NewAggregate(orgID).Aggregate, ""),
),
}
if machine {
filters = append(filters, machineFilters(orgID, true)...)
filters = append(filters, adminMemberFilters(orgID, "USER-MACHINE")...)
}
if human {
filters = append(filters, humanFilters(orgID)...)
filters = append(filters, adminMemberFilters(orgID, "USER")...)
}
if loginClient {
filters = append(filters, loginClientFilters(orgID, true)...)
filters = append(filters, instanceMemberFilters(orgID, "USER-LOGIN-CLIENT")...)
}
return append(filters,
projectFilters()...,
)
}
func orgEvents(ctx context.Context, instanceID, orgID, name, projectID, defaultDomain string, externalSecure bool, machine, human, loginClient bool) []eventstore.Command {
instanceAgg := instance.NewAggregate(instanceID)
orgAgg := org.NewAggregate(orgID)
domain := strings.ToLower(name + "." + defaultDomain)
events := []eventstore.Command{
org.NewOrgAddedEvent(ctx, &orgAgg.Aggregate, name),
org.NewDomainAddedEvent(ctx, &orgAgg.Aggregate, domain),
org.NewDomainVerifiedEvent(ctx, &orgAgg.Aggregate, domain),
org.NewDomainPrimarySetEvent(ctx, &orgAgg.Aggregate, domain),
instance.NewDefaultOrgSetEventEvent(ctx, &instanceAgg.Aggregate, orgID),
}
owner := ""
if machine {
machineID := "USER-MACHINE"
events = append(events, machineEvents(ctx, instanceID, orgID, machineID, "PAT")...)
owner = machineID
}
if human {
userID := "USER"
events = append(events, humanEvents(ctx, instanceID, orgID, userID)...)
owner = userID
}
if loginClient {
userID := "USER-LOGIN-CLIENT"
events = append(events, loginClientEvents(ctx, instanceID, orgID, userID, "LOGIN-CLIENT-PAT")...)
}
events = append(events, projectAddedEvents(ctx, instanceID, orgID, projectID, owner, externalSecure)...)
return events
}
func orgIDs() []string {
return slices.Concat([]string{"USER-MACHINE", "PAT", "USER", "USER-LOGIN-CLIENT", "LOGIN-CLIENT-PAT"}, projectClientIDs())
}
func instancePoliciesFilters(instanceID string) []expect {
return []expect{
expectFilter(),
expectFilter(),
expectFilter(),
expectFilter(),
expectFilter(),
expectFilter(),
expectFilter(),
expectFilter(),
expectFilter(),
expectFilter(),
expectFilter(),
}
}
func instancePoliciesEvents(ctx context.Context, instanceID string) []eventstore.Command {
instanceAgg := instance.NewAggregate(instanceID)
return []eventstore.Command{
instance.NewPasswordComplexityPolicyAddedEvent(ctx, &instanceAgg.Aggregate, 8, true, true, true, true),
instance.NewPasswordAgePolicyAddedEvent(ctx, &instanceAgg.Aggregate, 0, 0),
instance.NewDomainPolicyAddedEvent(ctx, &instanceAgg.Aggregate, false, false, false),
instance.NewLoginPolicyAddedEvent(ctx, &instanceAgg.Aggregate, true, true, true, false, false, false, false, true, false, false, domain.PasswordlessTypeAllowed, "", 240*time.Hour, 240*time.Hour, 720*time.Hour, 18*time.Hour, 12*time.Hour),
instance.NewLoginPolicySecondFactorAddedEvent(ctx, &instanceAgg.Aggregate, domain.SecondFactorTypeTOTP),
instance.NewLoginPolicySecondFactorAddedEvent(ctx, &instanceAgg.Aggregate, domain.SecondFactorTypeU2F),
instance.NewLoginPolicyMultiFactorAddedEvent(ctx, &instanceAgg.Aggregate, domain.MultiFactorTypeU2FWithPIN),
instance.NewPrivacyPolicyAddedEvent(ctx, &instanceAgg.Aggregate, "", "", "", "", "", "", ""),
instance.NewNotificationPolicyAddedEvent(ctx, &instanceAgg.Aggregate, true),
instance.NewLockoutPolicyAddedEvent(ctx, &instanceAgg.Aggregate, 0, 0, true),
instance.NewLabelPolicyAddedEvent(ctx, &instanceAgg.Aggregate, "#5469d4", "#fafafa", "#cd3d56", "#000000", "#2073c4", "#111827", "#ff3b5b", "#ffffff", false, false, false, domain.LabelPolicyThemeAuto),
instance.NewLabelPolicyActivatedEvent(ctx, &instanceAgg.Aggregate),
}
}
func instanceSetupPoliciesConfig() *InstanceSetup {
return &InstanceSetup{
PasswordComplexityPolicy: struct {
MinLength uint64
HasLowercase bool
HasUppercase bool
HasNumber bool
HasSymbol bool
}{8, true, true, true, true},
PasswordAgePolicy: struct {
ExpireWarnDays uint64
MaxAgeDays uint64
}{0, 0},
DomainPolicy: struct {
UserLoginMustBeDomain bool
ValidateOrgDomains bool
SMTPSenderAddressMatchesInstanceDomain bool
}{false, false, false},
LoginPolicy: struct {
AllowUsernamePassword bool
AllowRegister bool
AllowExternalIDP bool
ForceMFA bool
ForceMFALocalOnly bool
HidePasswordReset bool
IgnoreUnknownUsername bool
AllowDomainDiscovery bool
DisableLoginWithEmail bool
DisableLoginWithPhone bool
PasswordlessType domain.PasswordlessType
DefaultRedirectURI string
PasswordCheckLifetime time.Duration
ExternalLoginCheckLifetime time.Duration
MfaInitSkipLifetime time.Duration
SecondFactorCheckLifetime time.Duration
MultiFactorCheckLifetime time.Duration
}{true, true, true, false, false, false, false, true, false, false, domain.PasswordlessTypeAllowed, "", 240 * time.Hour, 240 * time.Hour, 720 * time.Hour, 18 * time.Hour, 12 * time.Hour},
NotificationPolicy: struct {
PasswordChange bool
}{true},
PrivacyPolicy: struct {
TOSLink string
PrivacyLink string
HelpLink string
SupportEmail domain.EmailAddress
DocsLink string
CustomLink string
CustomLinkText string
}{"", "", "", "", "", "", ""},
LabelPolicy: struct {
PrimaryColor string
BackgroundColor string
WarnColor string
FontColor string
PrimaryColorDark string
BackgroundColorDark string
WarnColorDark string
FontColorDark string
HideLoginNameSuffix bool
ErrorMsgPopup bool
DisableWatermark bool
ThemeMode domain.LabelPolicyThemeMode
}{"#5469d4", "#fafafa", "#cd3d56", "#000000", "#2073c4", "#111827", "#ff3b5b", "#ffffff", false, false, false, domain.LabelPolicyThemeAuto},
LockoutPolicy: struct {
MaxPasswordAttempts uint64
MaxOTPAttempts uint64
ShouldShowLockoutFailure bool
}{0, 0, true},
}
}
func setupInstanceElementsFilters(instanceID string) []expect {
return slices.Concat(
instanceElementsFilters(),
instancePoliciesFilters(instanceID),
// email template
[]expect{expectFilter()},
)
}
func setupInstanceElementsConfig() *InstanceSetup {
conf := instanceSetupPoliciesConfig()
conf.InstanceName = "ZITADEL"
conf.DefaultLanguage = language.English
conf.zitadel = instanceSetupZitadelIDs()
conf.SecretGenerators = instanceElementsConfig()
conf.EmailTemplate = []byte("something")
return conf
}
func setupInstanceElementsEvents(ctx context.Context, instanceID, instanceName string, defaultLanguage language.Tag) []eventstore.Command {
instanceAgg := instance.NewAggregate(instanceID)
return slices.Concat(
instanceElementsEvents(ctx, instanceID, instanceName, defaultLanguage),
instancePoliciesEvents(ctx, instanceID),
[]eventstore.Command{instance.NewMailTemplateAddedEvent(ctx, &instanceAgg.Aggregate, []byte("something"))},
)
}
func instanceElementsFilters() []expect {
return []expect{
expectFilter(),
expectFilter(),
expectFilter(),
expectFilter(),
expectFilter(),
expectFilter(),
expectFilter(),
expectFilter(),
expectFilter(),
}
}
func instanceElementsEvents(ctx context.Context, instanceID, instanceName string, defaultLanguage language.Tag) []eventstore.Command {
instanceAgg := instance.NewAggregate(instanceID)
return []eventstore.Command{
instance.NewInstanceAddedEvent(ctx, &instanceAgg.Aggregate, instanceName),
instance.NewDefaultLanguageSetEvent(ctx, &instanceAgg.Aggregate, defaultLanguage),
instance.NewSecretGeneratorAddedEvent(ctx, &instanceAgg.Aggregate, domain.SecretGeneratorTypeAppSecret, 64, 0, true, true, true, false),
instance.NewSecretGeneratorAddedEvent(ctx, &instanceAgg.Aggregate, domain.SecretGeneratorTypeInitCode, 6, 72*time.Hour, false, true, true, false),
instance.NewSecretGeneratorAddedEvent(ctx, &instanceAgg.Aggregate, domain.SecretGeneratorTypeVerifyEmailCode, 6, time.Hour, false, true, true, false),
instance.NewSecretGeneratorAddedEvent(ctx, &instanceAgg.Aggregate, domain.SecretGeneratorTypeVerifyPhoneCode, 6, time.Hour, false, true, true, false),
instance.NewSecretGeneratorAddedEvent(ctx, &instanceAgg.Aggregate, domain.SecretGeneratorTypePasswordResetCode, 6, time.Hour, false, true, true, false),
instance.NewSecretGeneratorAddedEvent(ctx, &instanceAgg.Aggregate, domain.SecretGeneratorTypePasswordlessInitCode, 12, time.Hour, true, true, true, false),
instance.NewSecretGeneratorAddedEvent(ctx, &instanceAgg.Aggregate, domain.SecretGeneratorTypeVerifyDomain, 32, 0, true, true, true, false),
instance.NewSecretGeneratorAddedEvent(ctx, &instanceAgg.Aggregate, domain.SecretGeneratorTypeOTPSMS, 8, 5*time.Minute, false, false, true, false),
instance.NewSecretGeneratorAddedEvent(ctx, &instanceAgg.Aggregate, domain.SecretGeneratorTypeOTPEmail, 8, 5*time.Minute, false, false, true, false),
}
}
func instanceElementsConfig() *SecretGenerators {
return &SecretGenerators{
ClientSecret: &crypto.GeneratorConfig{Length: 64, IncludeLowerLetters: true, IncludeUpperLetters: true, IncludeDigits: true},
InitializeUserCode: &crypto.GeneratorConfig{Length: 6, Expiry: 72 * time.Hour, IncludeUpperLetters: true, IncludeDigits: true},
EmailVerificationCode: &crypto.GeneratorConfig{Length: 6, Expiry: time.Hour, IncludeUpperLetters: true, IncludeDigits: true},
PhoneVerificationCode: &crypto.GeneratorConfig{Length: 6, Expiry: time.Hour, IncludeUpperLetters: true, IncludeDigits: true},
PasswordVerificationCode: &crypto.GeneratorConfig{Length: 6, Expiry: time.Hour, IncludeUpperLetters: true, IncludeDigits: true},
PasswordlessInitCode: &crypto.GeneratorConfig{Length: 12, Expiry: time.Hour, IncludeLowerLetters: true, IncludeUpperLetters: true, IncludeDigits: true},
DomainVerification: &crypto.GeneratorConfig{Length: 32, IncludeLowerLetters: true, IncludeUpperLetters: true, IncludeDigits: true},
OTPSMS: &crypto.GeneratorConfig{Length: 8, Expiry: 5 * time.Minute, IncludeDigits: true},
OTPEmail: &crypto.GeneratorConfig{Length: 8, Expiry: 5 * time.Minute, IncludeDigits: true},
}
}
func setupInstanceFilters(instanceID, orgID, projectID, appID, domain string) []expect {
return slices.Concat(
setupInstanceElementsFilters(instanceID),
orgFilters(orgID, true, true, true),
generatedDomainFilters(instanceID, orgID, projectID, appID, domain),
)
}
func setupInstanceEvents(ctx context.Context, instanceID, orgID, projectID, appID, instanceName, orgName string, defaultLanguage language.Tag, domain string, externalSecure bool) []eventstore.Command {
return slices.Concat(
setupInstanceElementsEvents(ctx, instanceID, instanceName, defaultLanguage),
orgEvents(ctx, instanceID, orgID, orgName, projectID, domain, externalSecure, true, true, true),
generatedDomainEvents(ctx, instanceID, orgID, projectID, appID, domain),
instanceCreatedMilestoneEvent(ctx, instanceID),
)
}
func setupInstanceConfig() *InstanceSetup {
conf := setupInstanceElementsConfig()
conf.Org = InstanceOrgSetup{
Name: "ZITADEL",
Machine: instanceSetupMachineConfig(),
Human: instanceSetupHumanConfig(),
LoginClient: instanceSetupLoginClientConfig(),
}
conf.CustomDomain = ""
return conf
}
func generatedDomainEvents(ctx context.Context, instanceID, orgID, projectID, appID, defaultDomain string) []eventstore.Command {
instanceAgg := instance.NewAggregate(instanceID)
changed, _ := project.NewOIDCConfigChangedEvent(ctx, &project.NewAggregate(projectID, orgID).Aggregate, appID,
[]project.OIDCConfigChanges{
project.ChangeRedirectURIs([]string{"http://" + defaultDomain + "/ui/console/auth/callback"}),
project.ChangePostLogoutRedirectURIs([]string{"http://" + defaultDomain + "/ui/console/signedout"}),
},
)
return []eventstore.Command{
instance.NewDomainAddedEvent(ctx, &instanceAgg.Aggregate, defaultDomain, true),
changed,
instance.NewDomainPrimarySetEvent(ctx, &instanceAgg.Aggregate, defaultDomain),
}
}
func instanceCreatedMilestoneEvent(ctx context.Context, instanceID string) []eventstore.Command {
return []eventstore.Command{
milestone.NewReachedEvent(ctx, milestone.NewInstanceAggregate(instanceID), milestone.InstanceCreated),
}
}
func generatedDomainFilters(instanceID, orgID, projectID, appID, generatedDomain string) []expect {
return []expect{
expectFilter(),
expectFilter(
project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate(projectID, orgID).Aggregate,
appID,
"console",
),
project.NewOIDCConfigAddedEvent(context.Background(),
&project.NewAggregate(projectID, orgID).Aggregate,
domain.OIDCVersionV1,
appID,
"clientID",
"",
[]string{},
[]domain.OIDCResponseType{domain.OIDCResponseTypeCode},
[]domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
domain.OIDCApplicationTypeUserAgent,
domain.OIDCAuthMethodTypeNone,
[]string{},
true,
domain.OIDCTokenTypeBearer,
false,
false,
false,
0,
nil,
false,
"",
domain.LoginVersionUnspecified,
"",
),
),
expectFilter(
func() eventstore.Event {
event := instance.NewDomainAddedEvent(context.Background(),
&instance.NewAggregate(instanceID).Aggregate,
generatedDomain,
true,
)
event.Data, _ = json.Marshal(event)
return event
}(),
),
}
}
func humanFilters(orgID string) []expect {
return []expect{
expectFilter(),
expectFilter(
org.NewDomainPolicyAddedEvent(
context.Background(),
&org.NewAggregate(orgID).Aggregate,
true,
true,
true,
),
),
expectFilter(
org.NewPasswordComplexityPolicyAddedEvent(
context.Background(),
&org.NewAggregate(orgID).Aggregate,
2,
false,
false,
false,
false,
),
),
}
}
func instanceSetupHumanConfig() *AddHuman {
return &AddHuman{
Username: "zitadel-admin",
FirstName: "ZITADEL",
LastName: "Admin",
Email: Email{
Address: domain.EmailAddress("admin@zitadel.test"),
Verified: true,
},
PreferredLanguage: language.English,
Password: "password",
PasswordChangeRequired: false,
}
}
func machineFilters(orgID string, pat bool) []expect {
filters := []expect{
expectFilter(),
expectFilter(
org.NewDomainPolicyAddedEvent(
context.Background(),
&org.NewAggregate(orgID).Aggregate,
true,
true,
true,
),
),
}
if pat {
filters = append(filters,
expectFilter(),
expectFilter(),
)
}
return filters
}
func instanceSetupMachineConfig() *AddMachine {
return &AddMachine{
Machine: &Machine{
Username: "zitadel-admin-machine",
Name: "ZITADEL-machine",
Description: "Admin",
AccessTokenType: domain.OIDCTokenTypeBearer,
},
Pat: &AddPat{
ExpirationDate: time.Time{},
Scopes: nil,
},
/* not predictable with the key value in the events
MachineKey: &AddMachineKey{
Type: domain.AuthNKeyTypeJSON,
ExpirationDate: time.Time{},
},
*/
}
}
func loginClientFilters(orgID string, pat bool) []expect {
filters := []expect{
expectFilter(),
expectFilter(
org.NewDomainPolicyAddedEvent(
context.Background(),
&org.NewAggregate(orgID).Aggregate,
true,
true,
true,
),
),
}
if pat {
filters = append(filters,
expectFilter(),
expectFilter(),
)
}
return filters
}
func instanceSetupLoginClientConfig() *AddLoginClient {
return &AddLoginClient{
Machine: &Machine{
Username: "zitadel-login-client",
Name: "ZITADEL-login-client",
Description: "Login Client",
AccessTokenType: domain.OIDCTokenTypeBearer,
},
Pat: &AddPat{
ExpirationDate: time.Time{},
Scopes: nil,
},
}
}
func projectFilters() []expect {
return []expect{
expectFilter(),
expectFilter(),
expectFilter(),
expectFilter(),
}
}
func adminMemberFilters(orgID, userID string) []expect {
filters := append(
orgMemberFilters(orgID, userID),
instanceMemberFilters(orgID, userID)...,
)
return filters
}
func orgMemberFilters(orgID, userID string) []expect {
return []expect{
expectFilter(
addHumanEvent(context.Background(), orgID, userID),
),
expectFilter(),
}
}
func instanceMemberFilters(orgID, userID string) []expect {
return []expect{
expectFilter(
addHumanEvent(context.Background(), orgID, userID),
),
expectFilter(),
}
}
func humanEvents(ctx context.Context, instanceID, orgID, userID string) []eventstore.Command {
agg := user.NewAggregate(userID, orgID)
instanceAgg := instance.NewAggregate(instanceID)
orgAgg := org.NewAggregate(orgID)
return []eventstore.Command{
addHumanEvent(ctx, orgID, userID),
user.NewHumanEmailVerifiedEvent(ctx, &agg.Aggregate),
org.NewMemberAddedEvent(ctx, &orgAgg.Aggregate, userID, domain.RoleOrgOwner),
instance.NewMemberAddedEvent(ctx, &instanceAgg.Aggregate, userID, domain.RoleIAMOwner),
}
}
func addHumanEvent(ctx context.Context, orgID, userID string) *user.HumanAddedEvent {
agg := user.NewAggregate(userID, orgID)
return func() *user.HumanAddedEvent {
event := user.NewHumanAddedEvent(
ctx,
&agg.Aggregate,
"zitadel-admin",
"ZITADEL",
"Admin",
"",
"ZITADEL Admin",
language.English,
0,
"admin@zitadel.test",
false,
)
event.AddPasswordData("$plain$x$password", false)
return event
}()
}
// machineEvents all events from setup to create the machine user, machinekey can't be tested here, as the public key is not provided and as such the value in the event can't be expected
func machineEvents(ctx context.Context, instanceID, orgID, userID, patID string) []eventstore.Command {
agg := user.NewAggregate(userID, orgID)
instanceAgg := instance.NewAggregate(instanceID)
orgAgg := org.NewAggregate(orgID)
events := []eventstore.Command{addMachineEvent(ctx, orgID, userID)}
if patID != "" {
events = append(events,
user.NewPersonalAccessTokenAddedEvent(
ctx,
&agg.Aggregate,
patID,
time.Date(9999, time.December, 31, 23, 59, 59, 0, time.UTC),
nil,
),
)
}
return append(events,
org.NewMemberAddedEvent(ctx, &orgAgg.Aggregate, userID, domain.RoleOrgOwner),
instance.NewMemberAddedEvent(ctx, &instanceAgg.Aggregate, userID, domain.RoleIAMOwner),
)
}
func addMachineEvent(ctx context.Context, orgID, userID string) *user.MachineAddedEvent {
agg := user.NewAggregate(userID, orgID)
return user.NewMachineAddedEvent(ctx,
&agg.Aggregate,
"zitadel-admin-machine",
"ZITADEL-machine",
"Admin",
false,
domain.OIDCTokenTypeBearer,
)
}
// loginClientEvents all events from setup to create the login client user
func loginClientEvents(ctx context.Context, instanceID, orgID, userID, patID string) []eventstore.Command {
agg := user.NewAggregate(userID, orgID)
instanceAgg := instance.NewAggregate(instanceID)
events := []eventstore.Command{
addLoginClientEvent(ctx, orgID, userID),
instance.NewMemberAddedEvent(ctx, &instanceAgg.Aggregate, userID, domain.RoleIAMLoginClient),
}
if patID != "" {
events = append(events,
user.NewPersonalAccessTokenAddedEvent(
ctx,
&agg.Aggregate,
patID,
time.Date(9999, time.December, 31, 23, 59, 59, 0, time.UTC),
nil,
),
)
}
return events
}
func addLoginClientEvent(ctx context.Context, orgID, userID string) *user.MachineAddedEvent {
agg := user.NewAggregate(userID, orgID)
return user.NewMachineAddedEvent(ctx,
&agg.Aggregate,
"zitadel-login-client",
"ZITADEL-login-client",
"Login Client",
false,
domain.OIDCTokenTypeBearer,
)
}
func testSetup(ctx context.Context, c *Commands, validations []preparation.Validation) error {
//nolint:staticcheck
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validations...)
if err != nil {
return err
}
_, err = c.eventstore.Push(ctx, cmds...)
return err
}
func TestCommandSide_setupMinimalInterfaces(t *testing.T) {
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
idGenerator id.Generator
}
type args struct {
ctx context.Context
instanceAgg *instance.Aggregate
orgAgg *org.Aggregate
owner string
ids ZitadelConfig
}
type res struct {
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "create, ok",
fields: fields{
eventstore: expectEventstore(
slices.Concat(
projectFilters(),
[]expect{
expectPush(
projectAddedEvents(context.Background(),
"INSTANCE",
"ORG",
"PROJECT",
"owner",
false,
)...,
),
},
)...,
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, projectClientIDs()...),
},
args: args{
ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch),
instanceAgg: instance.NewAggregate("INSTANCE"),
orgAgg: org.NewAggregate("ORG"),
owner: "owner",
ids: instanceSetupZitadelIDs(),
},
res: res{
err: nil,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore(t),
idGenerator: tt.fields.idGenerator,
}
validations := make([]preparation.Validation, 0)
setupMinimalInterfaces(r, &validations, tt.args.instanceAgg, tt.args.orgAgg, tt.args.owner, tt.args.ids)
err := testSetup(tt.args.ctx, r, validations)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
})
}
}
func validZitadelRoles() []authz.RoleMapping {
return []authz.RoleMapping{
{Role: domain.RoleOrgOwner, Permissions: []string{""}},
{Role: domain.RoleIAMOwner, Permissions: []string{""}},
{Role: domain.RoleIAMLoginClient, Permissions: []string{""}},
}
}
func TestCommandSide_setupAdmins(t *testing.T) {
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
idGenerator id.Generator
userPasswordHasher *crypto.Hasher
roles []authz.RoleMapping
keyAlgorithm crypto.EncryptionAlgorithm
}
type args struct {
ctx context.Context
instanceAgg *instance.Aggregate
orgAgg *org.Aggregate
machine *AddMachine
human *AddHuman
loginClient *AddLoginClient
}
type res struct {
owner string
pat bool
machineKey bool
loginClientPat bool
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "human, ok",
fields: fields{
eventstore: expectEventstore(
slices.Concat(
humanFilters("ORG"),
adminMemberFilters("ORG", "USER"),
[]expect{
expectPush(
humanEvents(context.Background(),
"INSTANCE",
"ORG",
"USER",
)...,
),
},
)...,
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "USER"),
userPasswordHasher: mockPasswordHasher("x"),
roles: validZitadelRoles(),
},
args: args{
ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch),
instanceAgg: instance.NewAggregate("INSTANCE"),
orgAgg: org.NewAggregate("ORG"),
human: instanceSetupHumanConfig(),
},
res: res{
owner: "USER",
pat: false,
machineKey: false,
err: nil,
},
},
{
name: "machine, ok",
fields: fields{
eventstore: expectEventstore(
slices.Concat(
machineFilters("ORG", true),
adminMemberFilters("ORG", "USER-MACHINE"),
[]expect{
expectPush(
machineEvents(context.Background(),
"INSTANCE",
"ORG",
"USER-MACHINE",
"PAT",
)...,
),
},
)...,
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "USER-MACHINE", "PAT"),
roles: validZitadelRoles(),
keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch),
instanceAgg: instance.NewAggregate("INSTANCE"),
orgAgg: org.NewAggregate("ORG"),
machine: instanceSetupMachineConfig(),
},
res: res{
owner: "USER-MACHINE",
pat: true,
machineKey: false,
err: nil,
},
},
{
name: "human and machine, ok",
fields: fields{
eventstore: expectEventstore(
slices.Concat(
machineFilters("ORG", true),
adminMemberFilters("ORG", "USER-MACHINE"),
humanFilters("ORG"),
adminMemberFilters("ORG", "USER"),
[]expect{
expectPush(
slices.Concat(
machineEvents(context.Background(),
"INSTANCE",
"ORG",
"USER-MACHINE",
"PAT",
),
humanEvents(context.Background(),
"INSTANCE",
"ORG",
"USER",
),
)...,
),
},
)...,
),
userPasswordHasher: mockPasswordHasher("x"),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "USER-MACHINE", "PAT", "USER"),
roles: validZitadelRoles(),
keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch),
instanceAgg: instance.NewAggregate("INSTANCE"),
orgAgg: org.NewAggregate("ORG"),
machine: instanceSetupMachineConfig(),
human: instanceSetupHumanConfig(),
},
res: res{
owner: "USER",
pat: true,
machineKey: false,
err: nil,
},
},
{
name: "human, machine and login client, ok",
fields: fields{
eventstore: expectEventstore(
slices.Concat(
machineFilters("ORG", true),
adminMemberFilters("ORG", "USER-MACHINE"),
humanFilters("ORG"),
adminMemberFilters("ORG", "USER"),
loginClientFilters("ORG", true),
instanceMemberFilters("ORG", "USER-LOGIN-CLIENT"),
[]expect{
expectPush(
slices.Concat(
machineEvents(context.Background(),
"INSTANCE",
"ORG",
"USER-MACHINE",
"PAT",
),
humanEvents(context.Background(),
"INSTANCE",
"ORG",
"USER",
),
loginClientEvents(context.Background(),
"INSTANCE",
"ORG",
"USER-LOGIN-CLIENT",
"LOGIN-CLIENT-PAT",
),
)...,
),
},
)...,
),
userPasswordHasher: mockPasswordHasher("x"),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "USER-MACHINE", "PAT", "USER", "USER-LOGIN-CLIENT", "LOGIN-CLIENT-PAT"),
roles: validZitadelRoles(),
keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch),
instanceAgg: instance.NewAggregate("INSTANCE"),
orgAgg: org.NewAggregate("ORG"),
machine: instanceSetupMachineConfig(),
human: instanceSetupHumanConfig(),
loginClient: instanceSetupLoginClientConfig(),
},
res: res{
owner: "USER",
pat: true,
machineKey: false,
loginClientPat: true,
err: nil,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore(t),
idGenerator: tt.fields.idGenerator,
zitadelRoles: tt.fields.roles,
userPasswordHasher: tt.fields.userPasswordHasher,
keyAlgorithm: tt.fields.keyAlgorithm,
}
validations := make([]preparation.Validation, 0)
owner, pat, mk, loginClientPat, err := setupAdmins(r, &validations, tt.args.instanceAgg, tt.args.orgAgg, tt.args.machine, tt.args.human, tt.args.loginClient)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
err = testSetup(tt.args.ctx, r, validations)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, owner, tt.res.owner)
if tt.res.pat {
assert.NotNil(t, pat)
}
if tt.res.machineKey {
assert.NotNil(t, mk)
}
if tt.res.loginClientPat {
assert.NotNil(t, loginClientPat)
}
}
})
}
}
func TestCommandSide_setupDefaultOrg(t *testing.T) {
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
idGenerator id.Generator
userPasswordHasher *crypto.Hasher
roles []authz.RoleMapping
keyAlgorithm crypto.EncryptionAlgorithm
}
type args struct {
ctx context.Context
instanceAgg *instance.Aggregate
orgName string
machine *AddMachine
human *AddHuman
loginClient *AddLoginClient
ids ZitadelConfig
}
type res struct {
pat bool
machineKey bool
loginClientPat bool
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "human, machine and login client, ok",
fields: fields{
eventstore: expectEventstore(
slices.Concat(
orgFilters(
"ORG",
true,
true,
true,
),
[]expect{
expectPush(
slices.Concat(
orgEvents(context.Background(),
"INSTANCE",
"ORG",
"ZITADEL",
"PROJECT",
"DOMAIN",
false,
true,
true,
true,
),
)...,
),
},
)...,
),
userPasswordHasher: mockPasswordHasher("x"),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, orgIDs()...),
roles: validZitadelRoles(),
keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch),
instanceAgg: instance.NewAggregate("INSTANCE"),
orgName: "ZITADEL",
machine: &AddMachine{
Machine: &Machine{
Username: "zitadel-admin-machine",
Name: "ZITADEL-machine",
Description: "Admin",
AccessTokenType: domain.OIDCTokenTypeBearer,
},
Pat: &AddPat{
ExpirationDate: time.Time{},
Scopes: nil,
},
/* not predictable with the key value in the events
MachineKey: &AddMachineKey{
Type: domain.AuthNKeyTypeJSON,
ExpirationDate: time.Time{},
},
*/
},
human: &AddHuman{
Username: "zitadel-admin",
FirstName: "ZITADEL",
LastName: "Admin",
Email: Email{
Address: domain.EmailAddress("admin@zitadel.test"),
Verified: true,
},
PreferredLanguage: language.English,
Password: "password",
PasswordChangeRequired: false,
},
loginClient: &AddLoginClient{
Machine: &Machine{
Username: "zitadel-login-client",
Name: "ZITADEL-login-client",
Description: "Login Client",
AccessTokenType: domain.OIDCTokenTypeBearer,
},
Pat: &AddPat{
ExpirationDate: time.Time{},
Scopes: nil,
},
},
ids: ZitadelConfig{
instanceID: "INSTANCE",
orgID: "ORG",
projectID: "PROJECT",
consoleAppID: "console-id",
authAppID: "auth-id",
mgmtAppID: "mgmt-id",
adminAppID: "admin-id",
},
},
res: res{
pat: true,
machineKey: false,
loginClientPat: true,
err: nil,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore(t),
idGenerator: tt.fields.idGenerator,
zitadelRoles: tt.fields.roles,
userPasswordHasher: tt.fields.userPasswordHasher,
keyAlgorithm: tt.fields.keyAlgorithm,
}
validations := make([]preparation.Validation, 0)
pat, mk, loginClientPat, err := setupDefaultOrg(tt.args.ctx, r, &validations, tt.args.instanceAgg, tt.args.orgName, tt.args.machine, tt.args.human, tt.args.loginClient, tt.args.ids)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
err = testSetup(context.Background(), r, validations)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
if tt.res.pat {
assert.NotNil(t, pat)
}
if tt.res.machineKey {
assert.NotNil(t, mk)
}
if tt.res.loginClientPat {
assert.NotNil(t, loginClientPat)
}
}
})
}
}
func TestCommandSide_setupInstanceElements(t *testing.T) {
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
instanceAgg *instance.Aggregate
setup *InstanceSetup
}
type res struct {
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "ok",
fields: fields{
eventstore: expectEventstore(
slices.Concat(
setupInstanceElementsFilters("INSTANCE"),
[]expect{
expectPush(
setupInstanceElementsEvents(context.Background(),
"INSTANCE",
"ZITADEL",
language.English,
)...,
),
},
)...,
),
},
args: args{
ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch),
instanceAgg: instance.NewAggregate("INSTANCE"),
setup: setupInstanceElementsConfig(),
},
res: res{
err: nil,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore(t),
}
validations := setupInstanceElements(tt.args.instanceAgg, tt.args.setup)
err := testSetup(context.Background(), r, validations)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
})
}
}
func TestCommandSide_setUpInstance(t *testing.T) {
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
idGenerator id.Generator
userPasswordHasher *crypto.Hasher
roles []authz.RoleMapping
keyAlgorithm crypto.EncryptionAlgorithm
generateDomain func(string, string) (string, error)
}
type args struct {
ctx context.Context
setup *InstanceSetup
}
type res struct {
pat bool
machineKey bool
loginClientPat bool
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "ok",
fields: fields{
eventstore: expectEventstore(
slices.Concat(
setupInstanceFilters("INSTANCE", "ORG", "PROJECT", "console-id", "DOMAIN"),
[]expect{
expectPush(
setupInstanceEvents(context.Background(),
"INSTANCE",
"ORG",
"PROJECT",
"console-id",
"ZITADEL",
"ZITADEL",
language.English,
"DOMAIN",
false,
)...,
),
},
)...,
),
userPasswordHasher: mockPasswordHasher("x"),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, orgIDs()...),
roles: validZitadelRoles(),
keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
generateDomain: func(string, string) (string, error) {
return "DOMAIN", nil
},
},
args: args{
ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch),
setup: setupInstanceConfig(),
},
res: res{
err: nil,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore(t),
idGenerator: tt.fields.idGenerator,
zitadelRoles: tt.fields.roles,
userPasswordHasher: tt.fields.userPasswordHasher,
keyAlgorithm: tt.fields.keyAlgorithm,
GenerateDomain: tt.fields.generateDomain,
}
validations, pat, mk, loginClientPat, err := setUpInstance(tt.args.ctx, r, tt.args.setup)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
err = testSetup(tt.args.ctx, r, validations)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
if tt.res.pat {
assert.NotNil(t, pat)
}
if tt.res.machineKey {
assert.NotNil(t, mk)
}
if tt.res.loginClientPat {
assert.NotNil(t, loginClientPat)
}
}
})
}
}
func TestCommandSide_UpdateInstance(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
}
type args struct {
ctx context.Context
name string
}
type res struct {
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "empty name, invalid error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
name: " ",
},
res: res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
name: "instance not existing, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
name: "INSTANCE_CHANGED",
},
res: res{
err: zerrors.IsNotFound,
},
},
{
name: "instance removed, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
instance.NewInstanceAddedEvent(
context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
"INSTANCE",
),
),
eventFromEventPusher(
instance.NewInstanceRemovedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
"INSTANCE",
nil,
),
),
),
),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
name: "INSTANCE_CHANGED",
},
res: res{
err: zerrors.IsNotFound,
},
},
{
name: "no changes, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
instance.NewInstanceAddedEvent(
context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
"INSTANCE",
),
),
),
),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
name: "INSTANCE",
},
res: res{
err: zerrors.IsPreconditionFailed,
},
},
{
name: "instance change, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusherWithInstanceID(
"INSTANCE",
instance.NewInstanceAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
"INSTANCE",
),
),
),
expectPush(
instance.NewInstanceChangedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
"INSTANCE_CHANGED",
),
),
),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
name: "INSTANCE_CHANGED",
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "INSTANCE",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
}
got, err := r.UpdateInstance(tt.args.ctx, tt.args.name)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assertObjectDetails(t, tt.res.want, got)
}
})
}
}
func TestCommandSide_RemoveInstance(t *testing.T) {
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
instanceID string
}
type res struct {
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "instance empty, invalid argument error",
fields: fields{
eventstore: func(t *testing.T) *eventstore.Eventstore { return &eventstore.Eventstore{} },
},
args: args{
ctx: authz.WithInstanceID(context.Background(), " "),
instanceID: " ",
},
res: res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
name: "instance too long, invalid argument error",
fields: fields{
eventstore: func(t *testing.T) *eventstore.Eventstore { return &eventstore.Eventstore{} },
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "averylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonginstance"),
instanceID: "averylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonginstance",
},
res: res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
name: "instance not existing, not found error",
fields: fields{
eventstore: expectEventstore(
expectFilter(),
),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
instanceID: "INSTANCE",
},
res: res{
err: zerrors.IsNotFound,
},
},
{
name: "instance removed, not found error",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
instance.NewInstanceAddedEvent(
context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
"INSTANCE",
),
),
eventFromEventPusher(
instance.NewInstanceRemovedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
"INSTANCE",
nil,
),
),
),
),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
instanceID: "INSTANCE",
},
res: res{
err: zerrors.IsNotFound,
},
},
{
name: "instance remove, ok",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusherWithInstanceID(
"INSTANCE",
instance.NewInstanceAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
"INSTANCE",
),
),
eventFromEventPusherWithInstanceID(
"INSTANCE",
instance.NewDomainAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
"instance.domain",
true,
),
),
eventFromEventPusherWithInstanceID(
"INSTANCE",
instance.NewDomainAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
"custom.domain",
false,
),
),
),
expectPush(
instance.NewInstanceRemovedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
"INSTANCE",
[]string{
"instance.domain",
"custom.domain",
},
),
milestone.NewReachedEvent(context.Background(),
milestone.NewInstanceAggregate("INSTANCE"),
milestone.InstanceDeleted,
),
),
),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
instanceID: "INSTANCE",
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "INSTANCE",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore(t),
caches: &Caches{
milestones: noop.NewCache[milestoneIndex, string, *MilestonesReached](),
},
}
got, err := r.RemoveInstance(tt.args.ctx, tt.args.instanceID)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assertObjectDetails(t, tt.res.want, got)
}
})
}
}