Files
zitadel/internal/command/instance_test.go
T
3ff4fbc7e4 fix(eventstore): add owners to unique constraints (#12740)
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
# Which Problems Are Solved

Bulk unique-constraint deletes (org, IDP with many links, user, project)
reconstruct every uniqueness key in Go and emit N `DELETE`s. That can
OOM on large tenants.

A one-shot owner backfill is also not enough: during a rolling deploy,
older binaries still insert `owners = '{}'`, so a later owner-only
delete would miss those rows.

Owner-only org/user/grant delete only matches rows whose `owners` bag
contains that tag. Several grant and member types stamped the aggregate
resource owner, which is not always the user’s home org or the granted
org. Username-scope rewrites also dropped `user:` tags, so owner-only
user delete could miss rewritten username rows.

Command must not switch to owner-only delete on the first 79 of a
version, because leftover `'{}'` rows from rolling-update overlap are
only backfilled on a later 79. A failed 79 must not turn owner-delete
on, and backfill joins must not stamp the wrong org (unverified domains,
org-scoped usernames colliding with instance-scoped ones).

# How the Problems Are Solved

Uniqueness stays the existing primary key `(instance_id, unique_type,
unique_field)`. `owners` is only a lifecycle bag of tags (`org:<id>`,
`user:<id>`, `idp:<id>`, `project:<id>`, `grant:<id>`).

- Add `owners TEXT[] NOT NULL DEFAULT '{}'` on
`eventstore.unique_constraints` (init SQL for greenfield, setup 78 for
existing clusters).
- Create `unique_constraints_owners_gin` with `CREATE INDEX
CONCURRENTLY` **outside a transaction** (same pattern as step 76).
Built-in `GIN (owners)` only; no `btree_gin`.
- Stamp `WithOwners` on **add** events. Field-level removes ignore
owners. `RemoveByOwner` stores the tag in `Owners` and both v1/v3 and v2
pushers run `DELETE … WHERE instance_id = $1 AND owners @>
ARRAY[$tag]::text[]`.
- Setup 79 backfills `owners` from projections via numbered
`cmd/setup/79/*.sql` files. Table names are interpolated from
`internal/query/projection` constants. Matches use `unique_field IN (x,
lower(x))` so the primary key can be used. Missing projection tables
(`42P01`) are skipped.
- Org domains are tagged only when `is_verified`. Instance-scoped
usernames join only users that are not org-scoped (organization settings
or domain policy `user_login_must_be_domain`). Every username row still
gets `org:` + `user:` of that user.
- 79 is a **repeatable** migration. `Check` re-runs when
`lastRun["version"]` changed or when setup-only `ForceFinalize` is set
and `lastRun["finalized"]` is still false. `Finalized` is written only
after Execute succeeds.
- Failed setup events keep the last successful `lastRun`. Command reads
`finalized` only from `system.migration.repeatable.done`.
- Org / user / project remove skip N-key event replay **when that
`finalized` bit is true**. Otherwise they keep today’s N-key lists
**and** still emit `RemoveByOwner`. The “ready” result is cached in
memory once true.

Setup writes `finalized` on the 79 repeatable.done payload:

- First 79 of a version: backfill only, `finalized=false` (unless
`ForceFinalize` is already set).
- Later 79: version change or
`BackfillUniqueConstraintOwners.ForceFinalize` /
`ZITADEL_BACKFILLUNIQUECONSTRAINTOWNERS_FORCEFINALIZE` after the rolling
update is done. That run backfills leftovers and writes
`finalized=true`.

Member and grant owner bags now include every org that should delete the
row:

- Instance, org, and project members also stamp
`org:<user_resource_owner>`.
- User grants stamp grant, user, project, and granted orgs.
- Project grant members stamp the project org, user org, and granted
org.
- Project grants stamp both orgs plus `grant:<id>`. Grant remove emits
`RemoveByOwner(grant)`.
- Username-scope rewrites keep `user:<id>` as well as `org:`.
- User/grant orgs are carried as unexported add-event fields (not
persisted JSON). `WithOwners` skips empty and duplicate tags.

# Additional Changes

- Table-driven owner-tag tests, including project grant (two `org:` tags
plus `grant:`), cross-org members, user grants, and username-scope
rewrite `user:` tags.
- Table-driven 79 `Check` + `finalizeAfterSuccess` tests for version
change, sequential version bumps, ForceFinalize restart, and
already-finalized skip.
- Command tests: a failed 79 event with `finalized=true` does not enable
owner-delete.
- Integration fixtures (verified vs unverified org domain;
instance-scoped `bob` vs org-scoped `bob`).
- Instance-only and global unique types stay untagged (`'{}'`). Instance
wipe still uses `UniqueConstraintInstanceRemove`.
- Pusher owner cases live in `eventstore_pusher_owners_test.go` so
`eventstore_pusher_test.go` stays under 1k lines.

# Additional Context

A later cleanup can delete the N-key replay helpers once a release that
ran 79 a second time (or an operator who set `ForceFinalize`) is in
production. That is not required for correctness.

<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a
href="https://cursor.com/agents/bc-44f9bc9b-9a93-4cd9-8dd1-663a7b9aad2a?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a
href="https://cursor.com/background-agent?bcId=bc-44f9bc9b-9a93-4cd9-8dd1-663a7b9aad2a&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img
alt="Open in Cursor" width="131" height="28"
src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div>

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Silvan <[email protected]>
2026-09-18 15:08:42 +02:00

1807 lines
59 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",
managementConsoleAppID: "console-id",
authAppID: "auth-id",
mgmtAppID: "mgmt-id",
adminAppID: "admin-id",
}
}
func projectAddedEvents(ctx context.Context, instanceID, orgID, id 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")...)
managementConsoleAppID := "console-id"
managementConsoleClientID := "clientID"
events = append(events, oidcAppEvents(ctx, orgID, id, managementConsoleAppID, "Management Console", managementConsoleClientID, externalSecure)...)
events = append(events,
instance.NewIAMManagementConsoleSetEvent(ctx,
&instance.NewAggregate(instanceID).Aggregate,
&managementConsoleClientID,
&managementConsoleAppID,
),
)
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,
"",
"",
"",
"",
nil),
}
}
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),
}
if machine {
machineID := "USER-MACHINE"
events = append(events, machineEvents(ctx, instanceID, orgID, machineID, "PAT")...)
}
if human {
userID := "USER"
events = append(events, humanEvents(ctx, instanceID, orgID, 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, externalSecure)...)
return events
}
func orgIDs() []string {
return slices.Concat([]string{"USER-MACHINE", "PAT", "USER", "USER-LOGIN-CLIENT", "LOGIN-CLIENT-PAT"}, projectClientIDs())
}
func instancePoliciesFilters(_ 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
IgnoreUnknownUsernames 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(),
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),
instance.NewSecretGeneratorAddedEvent(ctx, &instanceAgg.Aggregate, domain.SecretGeneratorTypeInviteCode, 6, 72*time.Hour, false, true, 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},
InviteCode: &crypto.GeneratorConfig{Length: 6, Expiry: 72 * time.Hour, IncludeUpperLetters: true, IncludeDigits: true},
}
}
func setupInstanceFilters(instanceID, orgID, projectID, appID, domain string, trustedDomains []string) []expect {
return slices.Concat(
setupInstanceElementsFilters(instanceID),
orgFilters(orgID, true, true, true),
generatedDomainFilters(instanceID, orgID, projectID, appID, domain),
generatedTrustedDomainsFilters(trustedDomains),
)
}
func setupInstanceEvents(
ctx context.Context,
instanceID, orgID, projectID, appID, instanceName, orgName string,
defaultLanguage language.Tag,
domain string,
externalSecure bool,
trustedDomains []string,
) []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),
generatedTrustedDomainEvents(ctx, instanceID, trustedDomains),
instanceCreatedMilestoneEvent(ctx, instanceID),
)
}
func setupInstanceConfig() *InstanceSetup {
conf := setupInstanceElementsConfig()
conf.Org = InstanceOrgSetup{
Name: "ZITADEL",
Machine: instanceSetupMachineConfig(),
Human: instanceSetupHumanConfig(),
LoginClient: instanceSetupLoginClientConfig(),
}
conf.CustomDomain = ""
conf.TrustedDomains = []string{"trusted-domain.com", "another-trusted-domain.com"}
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 generatedTrustedDomainEvents(ctx context.Context, instanceID string, trustedDomains []string) []eventstore.Command {
commands := make([]eventstore.Command, len(trustedDomains))
for i, trustedDomain := range trustedDomains {
commands[i] = instance.NewTrustedDomainAddedEvent(ctx, &instance.NewAggregate(instanceID).Aggregate, trustedDomain)
}
return commands
}
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,
"",
"",
"",
"",
nil),
),
expectFilter(
func() eventstore.Event {
event := instance.NewDomainAddedEvent(context.Background(),
&instance.NewAggregate(instanceID).Aggregate,
generatedDomain,
true,
)
event.Data, _ = json.Marshal(event)
return event
}(),
),
}
}
func generatedTrustedDomainsFilters(trustedDomains []string) []expect {
filters := make([]expect, len(trustedDomains))
for i := range trustedDomains {
filters[i] = expectFilter()
}
return filters
}
func humanFilters(orgID string) []expect {
return []expect{
expectFilter(),
expectFilter(
org.NewDomainPolicyAddedEvent(
context.Background(),
&org.NewAggregate(orgID).Aggregate,
true,
true,
true,
),
),
expectFilterOrganizationSettings("org1", false, false),
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("[email protected]"),
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,
),
),
expectFilterOrganizationSettings("org1", false, false),
}
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,
),
),
expectFilterOrganizationSettings("org1", false, false),
}
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).WithUserResourceOwner(orgID),
instance.NewMemberAddedEvent(ctx, &instanceAgg.Aggregate, userID, domain.RoleIAMOwner).WithUserResourceOwner(orgID),
}
}
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,
"[email protected]",
false,
)
event.AddPasswordData("$plain$x$password", false)
return event
}()
}
// machineEvents all events from setup to create the service account, 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).WithUserResourceOwner(orgID),
instance.NewMemberAddedEvent(ctx, &instanceAgg.Aggregate, userID, domain.RoleIAMOwner).WithUserResourceOwner(orgID),
)
}
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).WithUserResourceOwner(orgID),
}
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",
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,
authAlgorithm: &mockAuthCrypto{},
}
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("[email protected]"),
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",
managementConsoleAppID: "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,
authAlgorithm: &mockAuthCrypto{},
}
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", []string{"trusted-domain.com", "another-trusted-domain.com"}),
[]expect{
expectPush(
setupInstanceEvents(context.Background(),
"INSTANCE",
"ORG",
"PROJECT",
"console-id",
"ZITADEL",
"ZITADEL",
language.English,
"DOMAIN",
false,
[]string{"trusted-domain.com", "another-trusted-domain.com"},
)...,
),
},
)...,
),
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,
authAlgorithm: &mockAuthCrypto{},
}
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 func(t *testing.T) *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: expectEventstore(),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
name: " ",
},
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"),
name: "INSTANCE_CHANGED",
},
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"),
name: "INSTANCE_CHANGED",
},
res: res{
err: zerrors.IsNotFound,
},
},
{
name: "no changes, precondition error",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
instance.NewInstanceAddedEvent(
context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
"INSTANCE",
),
),
),
),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
name: "INSTANCE",
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "INSTANCE",
},
},
},
{
name: "instance change, ok",
fields: fields{
eventstore: expectEventstore(
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(t),
}
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
errorIfNotFound bool
}
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",
errorIfNotFound: true,
},
res: res{
err: zerrors.IsNotFound,
},
},
{
name: "instance not existing, no error",
fields: fields{
eventstore: expectEventstore(
expectFilter(),
),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
instanceID: "INSTANCE",
errorIfNotFound: false,
},
res: res{},
},
{
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",
errorIfNotFound: true,
},
res: res{
err: zerrors.IsNotFound,
},
},
{
name: "instance removed, no 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",
errorIfNotFound: false,
},
res: res{},
},
{
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, tt.args.errorIfNotFound)
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)
}
})
}
}