zitadel/internal/command/instance_test.go

1530 lines
42 KiB
Go
Raw Normal View History

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,
),
project.NewProjectMemberAddedEvent(ctx,
&project.NewAggregate(id, orgID).Aggregate,
owner,
domain.RoleProjectOwner,
),
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,
feat(OIDC): add back channel logout (#8837) # Which Problems Are Solved Currently ZITADEL supports RP-initiated logout for clients. Back-channel logout ensures that user sessions are terminated across all connected applications, even if the user closes their browser or loses connectivity providing a more secure alternative for certain use cases. # How the Problems Are Solved If the feature is activated and the client used for the authentication has a back_channel_logout_uri configured, a `session_logout.back_channel` will be registered. Once a user terminates their session, a (notification) handler will send a SET (form POST) to the registered uri containing a logout_token (with the user's ID and session ID). - A new feature "back_channel_logout" is added on system and instance level - A `back_channel_logout_uri` can be managed on OIDC applications - Added a `session_logout` aggregate to register and inform about sent `back_channel` notifications - Added a `SecurityEventToken` channel and `Form`message type in the notification handlers - Added `TriggeredAtOrigin` fields to `HumanSignedOut` and `TerminateSession` events for notification handling - Exported various functions and types in the `oidc` package to be able to reuse for token signing in the back_channel notifier. - To prevent that current existing session termination events will be handled, a setup step is added to set the `current_states` for the `projections.notifications_back_channel_logout` to the current position - [x] requires https://github.com/zitadel/oidc/pull/671 # Additional Changes - Updated all OTEL dependencies to v1.29.0, since OIDC already updated some of them to that version. - Single Session Termination feature is correctly checked (fixed feature mapping) # Additional Context - closes https://github.com/zitadel/zitadel/issues/8467 - TODO: - Documentation - UI to be done: https://github.com/zitadel/zitadel/issues/8469 --------- Co-authored-by: Hidde Wieringa <hidde@hiddewieringa.nl>
2024-10-31 14:57:17 +00:00
"",
),
}
}
func orgFilters(orgID string, machine, human 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")...)
}
return append(filters,
projectFilters()...,
)
}
func orgEvents(ctx context.Context, instanceID, orgID, name, projectID, defaultDomain string, externalSecure bool, machine, human 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
}
events = append(events, projectAddedEvents(ctx, instanceID, orgID, projectID, owner, externalSecure)...)
return events
}
func orgIDs() []string {
return slices.Concat([]string{"USER-MACHINE", "PAT", "USER"}, 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),
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),
generatedDomainEvents(ctx, instanceID, orgID, projectID, appID, domain),
instanceCreatedMilestoneEvent(ctx, instanceID),
)
}
func setupInstanceConfig() *InstanceSetup {
conf := setupInstanceElementsConfig()
conf.Org = InstanceOrgSetup{
Name: "ZITADEL",
Machine: instanceSetupMachineConfig(),
Human: instanceSetupHumanConfig(),
}
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,
feat(OIDC): add back channel logout (#8837) # Which Problems Are Solved Currently ZITADEL supports RP-initiated logout for clients. Back-channel logout ensures that user sessions are terminated across all connected applications, even if the user closes their browser or loses connectivity providing a more secure alternative for certain use cases. # How the Problems Are Solved If the feature is activated and the client used for the authentication has a back_channel_logout_uri configured, a `session_logout.back_channel` will be registered. Once a user terminates their session, a (notification) handler will send a SET (form POST) to the registered uri containing a logout_token (with the user's ID and session ID). - A new feature "back_channel_logout" is added on system and instance level - A `back_channel_logout_uri` can be managed on OIDC applications - Added a `session_logout` aggregate to register and inform about sent `back_channel` notifications - Added a `SecurityEventToken` channel and `Form`message type in the notification handlers - Added `TriggeredAtOrigin` fields to `HumanSignedOut` and `TerminateSession` events for notification handling - Exported various functions and types in the `oidc` package to be able to reuse for token signing in the back_channel notifier. - To prevent that current existing session termination events will be handled, a setup step is added to set the `current_states` for the `projections.notifications_back_channel_logout` to the current position - [x] requires https://github.com/zitadel/oidc/pull/671 # Additional Changes - Updated all OTEL dependencies to v1.29.0, since OIDC already updated some of them to that version. - Single Session Termination feature is correctly checked (fixed feature mapping) # Additional Context - closes https://github.com/zitadel/zitadel/issues/8467 - TODO: - Documentation - UI to be done: https://github.com/zitadel/zitadel/issues/8469 --------- Co-authored-by: Hidde Wieringa <hidde@hiddewieringa.nl>
2024-10-31 14:57:17 +00:00
"",
),
),
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 projectFilters() []expect {
return []expect{
expectFilter(),
expectFilter(),
expectFilter(),
expectFilter(),
}
}
func adminMemberFilters(orgID, userID string) []expect {
return []expect{
expectFilter(
addHumanEvent(context.Background(), orgID, userID),
),
expectFilter(),
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,
)
}
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"),
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 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
}
type res struct {
owner string
pat bool
machineKey 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: []authz.RoleMapping{
{Role: domain.RoleOrgOwner, Permissions: []string{""}},
{Role: domain.RoleIAMOwner, Permissions: []string{""}},
},
},
args: args{
ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"),
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: []authz.RoleMapping{
{Role: domain.RoleOrgOwner, Permissions: []string{""}},
{Role: domain.RoleIAMOwner, Permissions: []string{""}},
},
keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"),
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: []authz.RoleMapping{
{Role: domain.RoleOrgOwner, Permissions: []string{""}},
{Role: domain.RoleIAMOwner, Permissions: []string{""}},
},
keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"),
instanceAgg: instance.NewAggregate("INSTANCE"),
orgAgg: org.NewAggregate("ORG"),
machine: instanceSetupMachineConfig(),
human: instanceSetupHumanConfig(),
},
res: res{
owner: "USER",
pat: true,
machineKey: false,
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, err := setupAdmins(r, &validations, tt.args.instanceAgg, tt.args.orgAgg, tt.args.machine, tt.args.human)
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)
}
}
})
}
}
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
ids ZitadelConfig
}
type res struct {
pat bool
machineKey bool
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "human and machine, ok",
fields: fields{
eventstore: expectEventstore(
slices.Concat(
orgFilters(
"ORG",
true,
true,
),
[]expect{
expectPush(
slices.Concat(
orgEvents(context.Background(),
"INSTANCE",
"ORG",
"ZITADEL",
"PROJECT",
"DOMAIN",
false,
true,
true,
),
)...,
),
},
)...,
),
userPasswordHasher: mockPasswordHasher("x"),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, orgIDs()...),
roles: []authz.RoleMapping{
{Role: domain.RoleOrgOwner, Permissions: []string{""}},
{Role: domain.RoleIAMOwner, Permissions: []string{""}},
},
keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"),
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,
},
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,
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, err := setupDefaultOrg(tt.args.ctx, r, &validations, tt.args.instanceAgg, tt.args.orgName, tt.args.machine, tt.args.human, 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)
}
}
})
}
}
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"),
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
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: []authz.RoleMapping{
{Role: domain.RoleOrgOwner, Permissions: []string{""}},
{Role: domain.RoleIAMOwner, Permissions: []string{""}},
},
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"),
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, 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)
}
}
})
}
}
2022-10-26 13:06:48 +00:00
func TestCommandSide_UpdateInstance(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
}
type args struct {
2022-10-26 13:06:48 +00:00
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{
2022-10-26 13:06:48 +00:00
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{
2022-10-26 13:06:48 +00:00
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
name: "INSTANCE_CHANGED",
},
res: res{
err: zerrors.IsNotFound,
},
},
{
2022-10-26 13:06:48 +00:00
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",
2022-10-26 13:06:48 +00:00
nil,
),
),
),
),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
name: "INSTANCE_CHANGED",
},
res: res{
err: zerrors.IsNotFound,
},
2022-10-26 13:06:48 +00:00
},
{
name: "no changes, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
instance.NewInstanceAddedEvent(
context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
"INSTANCE",
),
),
),
),
},
args: args{
2022-10-26 13:06:48 +00:00
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)
}
})
}
}
2022-10-26 13:06:48 +00:00
func TestCommandSide_RemoveInstance(t *testing.T) {
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
2022-10-26 13:06:48 +00:00
}
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 not existing, not found error",
fields: fields{
eventstore: expectEventstore(
2022-10-26 13:06:48 +00:00
expectFilter(),
),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
instanceID: "INSTANCE",
},
res: res{
err: zerrors.IsNotFound,
2022-10-26 13:06:48 +00:00
},
},
{
name: "instance removed, not found error",
fields: fields{
eventstore: expectEventstore(
2022-10-26 13:06:48 +00:00
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,
2022-10-26 13:06:48 +00:00
},
},
{
name: "instance remove, ok",
fields: fields{
eventstore: expectEventstore(
2022-10-26 13:06:48 +00:00
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,
),
2022-10-26 13:06:48 +00:00
),
),
},
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](),
},
2022-10-26 13:06:48 +00:00
}
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)
2022-10-26 13:06:48 +00:00
}
})
}
}