Files
zitadel/internal/query/user_test.go
T
318acd4bc3 perf(query): speed up ListUsers login name equality filters (#12460)
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Summary

`ListUsers` with `LoginNameQuery` + equals/equals-ignore-case was very
slow on large orgs (e.g. ~330k users) because the generated SQL filtered
the `projections.login_names3` **view** on the computed
`login_name_lower` expression and correlated that subquery on
`users14.id`. Postgres nested-looped every user and never used
`login_names3_users_search (instance_id, user_name_lower)`.

This change rewrites the **query planner** for that hot path: when an
equals/equals-ignore-case login-name filter is present (and not under
`OR`/`NOT`), the user list query **INNER JOINs** an indexed matches
subquery instead of filtering via the view expression. The matches SQL
mirrors `user_by_login_name.sql` (`user_name_lower` / domain paths +
`preferred` / `is_primary`).

Non-equals methods and OR combinations keep the previous view-based
filter so semantics stay unchanged.

Also adds a k6 use case that mirrors login v2 discovery
(`loginNameQuery` EQUALS_IGNORE_CASE + `organizationIdQuery`, `limit:
2`):

```bash
cd benchmark
make users_by_login_name USER_AMOUNT=100000 VUS=10 DURATION=60s
```

## Approach

1. `NewLoginNameSearchQuery` for equals / equals-ignore-case returns a
marker `loginNameEqualsFilter` (other methods unchanged).
2. `prepareUsersQuery` extracts that marker when safe, then:
- builds the usual `sq.SelectBuilder` **without** the login-name view
predicate
- adds `JoinClause` to `user_login_name_matches(.sql)` /
`_case_sensitive.sql` as `login_name_matches`
- keeps metadata JOIN/`DISTINCT` only when metadata filters are present
(same as before)
3. Embedded SQL files under `internal/query/` for the matches subquery.

Local smoke against ~330k synthetic users: baseline ~1392ms → rewritten
path ~0.4ms for a single equals-ignore-case lookup.

## Test plan

- [x] `go test ./internal/query/ -run
'TestLoginName|TestUsers|TestUserByLoginName'` (after generate-stubs)
- [x] Existing `user_test` expected SQL updated (no always-on metadata
join; login-name equals uses JOIN)
- [ ] Run k6 before/after on a large `USER_AMOUNT` (e.g. 50k–100k+) and
compare `list_users_duration` p50/p95/p99
- [x] Manual login v2 username discovery against a large org
- [x] Confirm OR / NOT / CONTAINS login-name queries still return
expected results

<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a
href="https://cursor.com/agents/bc-252883bd-48d9-492e-b619-5ccfa93cf9c3"><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-252883bd-48d9-492e-b619-5ccfa93cf9c3"><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 <cursoragent@cursor.com>
Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
2026-07-28 11:56:07 +00:00

1239 lines
32 KiB
Go

package query
import (
"context"
"database/sql"
"database/sql/driver"
"errors"
"fmt"
"regexp"
"testing"
sq "github.com/Masterminds/squirrel"
"github.com/stretchr/testify/assert"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/zerrors"
)
func TestUser_userCheckPermission(t *testing.T) {
type args struct {
ctxData string
resourceowner string
user string
}
type perm struct {
resourceowner string
user string
}
tests := []struct {
name string
wantErr bool
args args
permissions []perm
}{
{
name: "permission, self",
args: args{
resourceowner: "org",
user: "user",
ctxData: "user",
},
permissions: []perm{},
},
{
name: "permission, user",
args: args{
resourceowner: "org1",
user: "user1",
ctxData: "user2",
},
permissions: []perm{{"org1", "user1"}},
wantErr: false,
},
{
name: "permission, org",
args: args{
resourceowner: "org1",
user: "user1",
ctxData: "user2",
},
permissions: []perm{{"org1", "user3"}},
},
{
name: "permission, none",
args: args{
resourceowner: "org1",
user: "user1",
ctxData: "user2",
},
permissions: []perm{},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
checkPermission := func(ctx context.Context, permission, orgID, resourceID string) (err error) {
for _, perm := range tt.permissions {
if resourceID == perm.user {
return nil
}
if orgID == perm.resourceowner {
return nil
}
}
return errors.New("failed")
}
granted := userCheckPermission(authz.SetCtxData(context.Background(), authz.CtxData{UserID: tt.args.ctxData}), tt.args.resourceowner, tt.args.user, checkPermission)
if tt.wantErr {
assert.Error(t, granted)
} else {
assert.NoError(t, granted)
}
})
}
}
var (
profileQuery = `SELECT projections.users14.id,` +
` projections.users14.creation_date,` +
` projections.users14.change_date,` +
` projections.users14.resource_owner,` +
` projections.users14.sequence,` +
` projections.users14_humans.user_id,` +
` projections.users14_humans.first_name,` +
` projections.users14_humans.last_name,` +
` projections.users14_humans.nick_name,` +
` projections.users14_humans.display_name,` +
` projections.users14_humans.preferred_language,` +
` projections.users14_humans.gender,` +
` projections.users14_humans.avatar_key` +
` FROM projections.users14` +
` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id`
profileCols = []string{
"id",
"creation_date",
"change_date",
"resource_owner",
"sequence",
"user_id",
"first_name",
"last_name",
"nick_name",
"display_name",
"preferred_language",
"gender",
"avatar_key",
}
emailQuery = `SELECT projections.users14.id,` +
` projections.users14.creation_date,` +
` projections.users14.change_date,` +
` projections.users14.resource_owner,` +
` projections.users14.sequence,` +
` projections.users14_humans.user_id,` +
` projections.users14_humans.email,` +
` projections.users14_humans.is_email_verified` +
` FROM projections.users14` +
` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id`
emailCols = []string{
"id",
"creation_date",
"change_date",
"resource_owner",
"sequence",
"user_id",
"email",
"is_email_verified",
}
phoneQuery = `SELECT projections.users14.id,` +
` projections.users14.creation_date,` +
` projections.users14.change_date,` +
` projections.users14.resource_owner,` +
` projections.users14.sequence,` +
` projections.users14_humans.user_id,` +
` projections.users14_humans.phone,` +
` projections.users14_humans.is_phone_verified` +
` FROM projections.users14` +
` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id`
phoneCols = []string{
"id",
"creation_date",
"change_date",
"resource_owner",
"sequence",
"user_id",
"phone",
"is_phone_verified",
}
userUniqueQuery = `SELECT projections.users14.id,` +
` projections.users14.state,` +
` projections.users14.username,` +
` projections.users14_humans.user_id,` +
` projections.users14_humans.email,` +
` projections.users14_humans.is_email_verified` +
` FROM projections.users14` +
` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id`
userUniqueCols = []string{
"id",
"state",
"username",
"user_id",
"email",
"is_email_verified",
}
notifyUserQuery = `SELECT projections.users14.id,` +
` projections.users14.creation_date,` +
` projections.users14.change_date,` +
` projections.users14.resource_owner,` +
` projections.users14.sequence,` +
` projections.users14.state,` +
` projections.users14.type,` +
` projections.users14.username,` +
` login_names.login_names,` +
` login_names.preferred_login_name,` +
` projections.users14_humans.user_id,` +
` projections.users14_humans.first_name,` +
` projections.users14_humans.last_name,` +
` projections.users14_humans.nick_name,` +
` projections.users14_humans.display_name,` +
` projections.users14_humans.preferred_language,` +
` projections.users14_humans.gender,` +
` projections.users14_humans.avatar_key,` +
` projections.users14_notifications.user_id,` +
` projections.users14_notifications.last_email,` +
` projections.users14_notifications.verified_email,` +
` projections.users14_notifications.last_phone,` +
` projections.users14_notifications.verified_phone,` +
` projections.users14_notifications.password_set,` +
` COUNT(*) OVER ()` +
` FROM projections.users14` +
` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` +
` LEFT JOIN projections.users14_notifications ON projections.users14.id = projections.users14_notifications.user_id AND projections.users14.instance_id = projections.users14_notifications.instance_id` +
` LEFT JOIN LATERAL (SELECT ARRAY_AGG(ln.login_name ORDER BY ln.login_name) AS login_names, MAX(CASE WHEN ln.is_primary THEN ln.login_name ELSE NULL END) AS preferred_login_name FROM projections.login_names3 AS ln WHERE ln.user_id = projections.users14.id AND ln.instance_id = projections.users14.instance_id) AS login_names ON TRUE`
notifyUserCols = []string{
"id",
"creation_date",
"change_date",
"resource_owner",
"sequence",
"state",
"type",
"username",
"login_names",
"preferred_login_name",
// User (human)
"user_id",
"first_name",
"last_name",
"nick_name",
"display_name",
"preferred_language",
"gender",
"avatar_key",
// service account
"user_id",
"last_email",
"verified_email",
"last_phone",
"verified_phone",
"password_set",
"count",
}
usersQuery = `SELECT *, COUNT(*) OVER () FROM (` +
`SELECT projections.users14.id,` +
` projections.users14.creation_date,` +
` projections.users14.change_date,` +
` projections.users14.resource_owner,` +
` projections.users14.sequence,` +
` projections.users14.state,` +
` projections.users14.type,` +
` projections.users14.username,` +
` login_names.login_names,` +
` login_names.preferred_login_name,` +
` projections.users14_humans.user_id,` +
` projections.users14_humans.first_name,` +
` projections.users14_humans.last_name,` +
` projections.users14_humans.nick_name,` +
` projections.users14_humans.display_name,` +
` projections.users14_humans.preferred_language,` +
` projections.users14_humans.gender,` +
` projections.users14_humans.avatar_key,` +
` projections.users14_humans.email,` +
` projections.users14_humans.is_email_verified,` +
` projections.users14_humans.phone,` +
` projections.users14_humans.is_phone_verified,` +
` projections.users14_humans.password_change_required,` +
` projections.users14_humans.password_changed,` +
` projections.users14_humans.mfa_init_skipped,` +
` projections.users14_machines.user_id,` +
` projections.users14_machines.name,` +
` projections.users14_machines.description,` +
` projections.users14_machines.secret,` +
` projections.users14_machines.access_token_type,` +
` projections.users14.id` +
` FROM projections.users14` +
` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` +
` LEFT JOIN projections.users14_machines ON projections.users14.id = projections.users14_machines.user_id AND projections.users14.instance_id = projections.users14_machines.instance_id` +
` LEFT JOIN LATERAL (SELECT ARRAY_AGG(ln.login_name ORDER BY ln.login_name) AS login_names, MAX(CASE WHEN ln.is_primary THEN ln.login_name ELSE NULL END) AS preferred_login_name FROM projections.login_names3 AS ln WHERE ln.user_id = projections.users14.id AND ln.instance_id = projections.users14.instance_id) AS login_names ON TRUE` +
` WHERE projections.users14.instance_id = $1 ORDER BY projections.users14.id DESC` +
`) AS results`
usersCols = []string{
"id",
"creation_date",
"change_date",
"resource_owner",
"sequence",
"state",
"type",
"username",
"login_names",
"preferred_login_name",
// User (human)
"user_id",
"first_name",
"last_name",
"nick_name",
"display_name",
"preferred_language",
"gender",
"avatar_key",
"email",
"is_email_verified",
"phone",
"is_phone_verified",
"password_change_required",
"password_changed",
"mfa_init_skipped",
// service account
"user_id",
"name",
"description",
"secret",
"access_token_type",
"id",
"count",
}
countUsersQuery = "SELECT COUNT(*) OVER () FROM projections.users14"
countUsersCols = []string{"count"}
)
func Test_UserPrepares(t *testing.T) {
type want struct {
sqlExpectations sqlExpectation
err checkErr
}
tests := []struct {
name string
prepare interface{}
want want
object interface{}
}{
{
name: "prepareProfileQuery no result",
prepare: prepareProfileQuery,
want: want{
sqlExpectations: mockQueryScanErr(
regexp.QuoteMeta(profileQuery),
nil,
nil,
),
err: func(err error) (error, bool) {
if !zerrors.IsNotFound(err) {
return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false
}
return nil, true
},
},
object: (*Profile)(nil),
},
{
name: "prepareProfileQuery human found",
prepare: prepareProfileQuery,
want: want{
sqlExpectations: mockQuery(
regexp.QuoteMeta(profileQuery),
profileCols,
[]driver.Value{
"id",
testNow,
testNow,
"resource_owner",
uint64(20211108),
"id",
"first_name",
"last_name",
"nick_name",
"display_name",
"de",
domain.GenderUnspecified,
"avatar_key",
},
),
},
object: &Profile{
ID: "id",
CreationDate: testNow,
ChangeDate: testNow,
ResourceOwner: "resource_owner",
Sequence: 20211108,
FirstName: "first_name",
LastName: "last_name",
NickName: "nick_name",
DisplayName: "display_name",
AvatarKey: "avatar_key",
PreferredLanguage: language.German,
Gender: domain.GenderUnspecified,
},
},
{
name: "prepareProfileQuery not human found (error)",
prepare: prepareProfileQuery,
want: want{
sqlExpectations: mockQueryScanErr(
regexp.QuoteMeta(profileQuery),
profileCols,
[]driver.Value{
"id",
testNow,
testNow,
"resource_owner",
uint64(20211108),
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
},
),
err: func(err error) (error, bool) {
if !zerrors.IsPreconditionFailed(err) {
return fmt.Errorf("err should be zitadel.PredconditionError got: %w", err), false
}
return nil, true
},
},
object: (*Profile)(nil),
},
{
name: "prepareProfileQuery sql err",
prepare: prepareProfileQuery,
want: want{
sqlExpectations: mockQueryErr(
regexp.QuoteMeta(profileQuery),
sql.ErrConnDone,
),
err: func(err error) (error, bool) {
if !errors.Is(err, sql.ErrConnDone) {
return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false
}
return nil, true
},
},
object: (*Profile)(nil),
},
{
name: "prepareEmailQuery no result",
prepare: prepareEmailQuery,
want: want{
sqlExpectations: mockQueryScanErr(
regexp.QuoteMeta(emailQuery),
nil,
nil,
),
err: func(err error) (error, bool) {
if !zerrors.IsNotFound(err) {
return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false
}
return nil, true
},
},
object: (*Email)(nil),
},
{
name: "prepareEmailQuery human found",
prepare: prepareEmailQuery,
want: want{
sqlExpectations: mockQuery(
regexp.QuoteMeta(emailQuery),
emailCols,
[]driver.Value{
"id",
testNow,
testNow,
"resource_owner",
uint64(20211108),
//domain.UserStateActive,
"id",
"email",
true,
},
),
},
object: &Email{
ID: "id",
CreationDate: testNow,
ChangeDate: testNow,
ResourceOwner: "resource_owner",
Sequence: 20211108,
//State: domain.UserStateActive,
Email: "email",
IsVerified: true,
},
},
{
name: "prepareEmailQuery not human found (error)",
prepare: prepareEmailQuery,
want: want{
sqlExpectations: mockQueryScanErr(
regexp.QuoteMeta(emailQuery),
emailCols,
[]driver.Value{
"id",
testNow,
testNow,
"resource_owner",
uint64(20211108),
//domain.UserStateActive,
nil,
nil,
nil,
},
),
err: func(err error) (error, bool) {
if !zerrors.IsPreconditionFailed(err) {
return fmt.Errorf("err should be zitadel.PredconditionError got: %w", err), false
}
return nil, true
},
},
object: (*Email)(nil),
},
{
name: "prepareEmailQuery sql err",
prepare: prepareEmailQuery,
want: want{
sqlExpectations: mockQueryErr(
regexp.QuoteMeta(emailQuery),
sql.ErrConnDone,
),
err: func(err error) (error, bool) {
if !errors.Is(err, sql.ErrConnDone) {
return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false
}
return nil, true
},
},
object: (*Email)(nil),
},
{
name: "preparePhoneQuery no result",
prepare: preparePhoneQuery,
want: want{
sqlExpectations: mockQueryScanErr(
regexp.QuoteMeta(phoneQuery),
nil,
nil,
),
err: func(err error) (error, bool) {
if !zerrors.IsNotFound(err) {
return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false
}
return nil, true
},
},
object: (*Phone)(nil),
},
{
name: "preparePhoneQuery human found",
prepare: preparePhoneQuery,
want: want{
sqlExpectations: mockQuery(
regexp.QuoteMeta(phoneQuery),
phoneCols,
[]driver.Value{
"id",
testNow,
testNow,
"resource_owner",
uint64(20211108),
//domain.UserStateActive,
"id",
"phone",
true,
},
),
},
object: &Phone{
ID: "id",
CreationDate: testNow,
ChangeDate: testNow,
ResourceOwner: "resource_owner",
Sequence: 20211108,
//State: domain.UserStateActive,
Phone: "phone",
IsVerified: true,
},
},
{
name: "preparePhoneQuery not human found (error)",
prepare: preparePhoneQuery,
want: want{
sqlExpectations: mockQueryScanErr(
regexp.QuoteMeta(phoneQuery),
phoneCols,
[]driver.Value{
"id",
testNow,
testNow,
"resource_owner",
uint64(20211108),
//domain.UserStateActive,
nil,
nil,
nil,
},
),
err: func(err error) (error, bool) {
if !zerrors.IsPreconditionFailed(err) {
return fmt.Errorf("err should be zitadel.PredconditionError got: %w", err), false
}
return nil, true
},
},
object: (*Phone)(nil),
},
{
name: "preparePhoneQuery sql err",
prepare: preparePhoneQuery,
want: want{
sqlExpectations: mockQueryErr(
regexp.QuoteMeta(phoneQuery),
sql.ErrConnDone,
),
err: func(err error) (error, bool) {
if !errors.Is(err, sql.ErrConnDone) {
return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false
}
return nil, true
},
},
object: (*Phone)(nil),
},
{
name: "prepareUserUniqueQuery no result",
prepare: prepareUserUniqueQuery,
want: want{
sqlExpectations: mockQuery(
regexp.QuoteMeta(userUniqueQuery),
nil,
nil,
),
err: func(err error) (error, bool) {
if !zerrors.IsNotFound(err) {
return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false
}
return nil, true
},
},
object: true,
},
{
name: "prepareUserUniqueQuery found",
prepare: prepareUserUniqueQuery,
want: want{
sqlExpectations: mockQuery(
regexp.QuoteMeta(userUniqueQuery),
userUniqueCols,
[]driver.Value{
"id",
domain.UserStateActive,
"username",
"id",
"email",
true,
},
),
},
object: false,
},
{
name: "prepareUserUniqueQuery sql err",
prepare: prepareUserUniqueQuery,
want: want{
sqlExpectations: mockQueryErr(
regexp.QuoteMeta(userUniqueQuery),
sql.ErrConnDone,
),
err: func(err error) (error, bool) {
if !errors.Is(err, sql.ErrConnDone) {
return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false
}
return nil, true
},
},
object: false,
},
{
name: "prepareNotifyUserQuery no result",
prepare: prepareNotifyUserQuery,
want: want{
sqlExpectations: mockQueryScanErr(
regexp.QuoteMeta(notifyUserQuery),
nil,
nil,
),
err: func(err error) (error, bool) {
if !zerrors.IsNotFound(err) {
return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false
}
return nil, true
},
},
object: (*NotifyUser)(nil),
},
{
name: "prepareNotifyUserQuery notify found",
prepare: prepareNotifyUserQuery,
want: want{
sqlExpectations: mockQuery(
regexp.QuoteMeta(notifyUserQuery),
notifyUserCols,
[]driver.Value{
"id",
testNow,
testNow,
"resource_owner",
uint64(20211108),
domain.UserStateActive,
domain.UserTypeHuman,
"username",
database.TextArray[string]{"login_name1", "login_name2"},
"login_name1",
// User (human)
"id",
"first_name",
"last_name",
"nick_name",
"display_name",
"de",
domain.GenderUnspecified,
"avatar_key",
//notify
"id",
"lastEmail",
"verifiedEmail",
"lastPhone",
"verifiedPhone",
true,
1,
},
),
},
object: &NotifyUser{
ID: "id",
CreationDate: testNow,
ChangeDate: testNow,
ResourceOwner: "resource_owner",
Sequence: 20211108,
State: domain.UserStateActive,
Type: domain.UserTypeHuman,
Username: "username",
LoginNames: database.TextArray[string]{"login_name1", "login_name2"},
PreferredLoginName: "login_name1",
FirstName: "first_name",
LastName: "last_name",
NickName: "nick_name",
DisplayName: "display_name",
AvatarKey: "avatar_key",
PreferredLanguage: language.German,
Gender: domain.GenderUnspecified,
LastEmail: "lastEmail",
VerifiedEmail: "verifiedEmail",
LastPhone: "lastPhone",
VerifiedPhone: "verifiedPhone",
PasswordSet: true,
},
},
{
name: "prepareNotifyUserQuery not notify found (error)",
prepare: prepareNotifyUserQuery,
want: want{
sqlExpectations: mockQueryScanErr(
regexp.QuoteMeta(notifyUserQuery),
notifyUserCols,
[]driver.Value{
"id",
testNow,
testNow,
"resource_owner",
uint64(20211108),
domain.UserStateActive,
domain.UserTypeHuman,
"username",
database.TextArray[string]{"login_name1", "login_name2"},
"login_name1",
// User (human)
"id",
"first_name",
"last_name",
"nick_name",
"display_name",
"de",
domain.GenderUnspecified,
"avatar_key",
nil,
nil,
nil,
nil,
nil,
nil,
1,
},
),
err: func(err error) (error, bool) {
if !zerrors.IsPreconditionFailed(err) {
return fmt.Errorf("err should be zitadel.PredconditionError got: %w", err), false
}
return nil, true
},
},
object: (*NotifyUser)(nil),
},
{
name: "prepareNotifyUserQuery sql err",
prepare: prepareNotifyUserQuery,
want: want{
sqlExpectations: mockQueryErr(
regexp.QuoteMeta(notifyUserQuery),
sql.ErrConnDone,
),
err: func(err error) (error, bool) {
if !errors.Is(err, sql.ErrConnDone) {
return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false
}
return nil, true
},
},
object: (*NotifyUser)(nil),
},
{
name: "prepareUsersQuery no result",
prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Users, error)) {
q := &UserSearchQueries{}
return q.prepareUsersQuery(t.Context(), false)
},
want: want{
sqlExpectations: mockQuery(
regexp.QuoteMeta(usersQuery),
nil,
nil,
),
err: func(err error) (error, bool) {
if !zerrors.IsNotFound(err) {
return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false
}
return nil, true
},
},
object: &Users{Users: []*User{}},
},
{
name: "prepareUsersQuery one result",
prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Users, error)) {
q := &UserSearchQueries{}
return q.prepareUsersQuery(t.Context(), false)
},
want: want{
sqlExpectations: mockQueries(
regexp.QuoteMeta(usersQuery),
usersCols,
[][]driver.Value{
{
"id",
testNow,
testNow,
"resource_owner",
uint64(20211108),
domain.UserStateActive,
domain.UserTypeHuman,
"username",
database.TextArray[string]{"login_name1", "login_name2"},
"login_name1",
// User (human)
"id",
"first_name",
"last_name",
"nick_name",
"display_name",
"de",
domain.GenderUnspecified,
"avatar_key",
"email",
true,
"phone",
true,
true,
testNow,
testNow,
// service account
nil,
nil,
nil,
nil,
nil,
"id", // orderBy col
},
},
),
},
object: &Users{
SearchResponse: SearchResponse{
Count: 1,
},
Users: []*User{
{
ID: "id",
CreationDate: testNow,
ChangeDate: testNow,
ResourceOwner: "resource_owner",
Sequence: 20211108,
State: domain.UserStateActive,
Type: domain.UserTypeHuman,
Username: "username",
LoginNames: database.TextArray[string]{"login_name1", "login_name2"},
PreferredLoginName: "login_name1",
Human: &Human{
FirstName: "first_name",
LastName: "last_name",
NickName: "nick_name",
DisplayName: "display_name",
AvatarKey: "avatar_key",
PreferredLanguage: language.German,
Gender: domain.GenderUnspecified,
Email: "email",
IsEmailVerified: true,
Phone: "phone",
IsPhoneVerified: true,
PasswordChangeRequired: true,
PasswordChanged: testNow,
MFAInitSkipped: testNow,
},
},
},
},
},
{
name: "prepareUsersQuery one result, no sorting",
prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Users, error)) {
q := &UserSearchQueries{}
return q.prepareUsersQuery(t.Context(), false)
},
want: want{
sqlExpectations: mockQueries(
regexp.QuoteMeta(usersQuery),
usersCols,
[][]driver.Value{
{
"id",
testNow,
testNow,
"resource_owner",
uint64(20211108),
domain.UserStateActive,
domain.UserTypeHuman,
"username",
database.TextArray[string]{"login_name1", "login_name2"},
"login_name1",
// User (human)
"id",
"first_name",
"last_name",
"nick_name",
"display_name",
"de",
domain.GenderUnspecified,
"avatar_key",
"email",
true,
"phone",
true,
true,
testNow,
testNow,
// service account
nil,
nil,
nil,
nil,
nil,
"id", // orderBy col
},
},
),
},
object: &Users{
SearchResponse: SearchResponse{
Count: 1,
},
Users: []*User{
{
ID: "id",
CreationDate: testNow,
ChangeDate: testNow,
ResourceOwner: "resource_owner",
Sequence: 20211108,
State: domain.UserStateActive,
Type: domain.UserTypeHuman,
Username: "username",
LoginNames: database.TextArray[string]{"login_name1", "login_name2"},
PreferredLoginName: "login_name1",
Human: &Human{
FirstName: "first_name",
LastName: "last_name",
NickName: "nick_name",
DisplayName: "display_name",
AvatarKey: "avatar_key",
PreferredLanguage: language.German,
Gender: domain.GenderUnspecified,
Email: "email",
IsEmailVerified: true,
Phone: "phone",
IsPhoneVerified: true,
PasswordChangeRequired: true,
PasswordChanged: testNow,
MFAInitSkipped: testNow,
},
},
},
},
},
{
name: "prepareUsersQuery multiple results, offset and limit",
prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Users, error)) {
q := &UserSearchQueries{
SearchRequest: SearchRequest{
Offset: 1,
Limit: 2,
},
}
return q.prepareUsersQuery(t.Context(), false)
},
want: want{
sqlExpectations: mockQueries(
regexp.QuoteMeta(usersQuery+` LIMIT 2 OFFSET 1`),
usersCols,
[][]driver.Value{
{
"id",
testNow,
testNow,
"resource_owner",
uint64(20211108),
domain.UserStateActive,
domain.UserTypeHuman,
"username",
database.TextArray[string]{"login_name1", "login_name2"},
"login_name1",
// User (human)
"id",
"first_name",
"last_name",
"nick_name",
"display_name",
"de",
domain.GenderUnspecified,
"avatar_key",
"email",
true,
"phone",
true,
true,
testNow,
testNow,
// service account
nil,
nil,
nil,
nil,
nil,
"id", // orderBy col
},
{
"id",
testNow,
testNow,
"resource_owner",
uint64(20211108),
domain.UserStateActive,
domain.UserTypeMachine,
"username",
database.TextArray[string]{"login_name1", "login_name2"},
"login_name1",
// User (human)
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
// service account
"id",
"name",
"description",
"secret",
domain.OIDCTokenTypeBearer,
"id", // orderBy col
},
},
),
},
object: &Users{
SearchResponse: SearchResponse{
Count: 2,
},
Users: []*User{
{
ID: "id",
CreationDate: testNow,
ChangeDate: testNow,
ResourceOwner: "resource_owner",
Sequence: 20211108,
State: domain.UserStateActive,
Type: domain.UserTypeHuman,
Username: "username",
LoginNames: database.TextArray[string]{"login_name1", "login_name2"},
PreferredLoginName: "login_name1",
Human: &Human{
FirstName: "first_name",
LastName: "last_name",
NickName: "nick_name",
DisplayName: "display_name",
AvatarKey: "avatar_key",
PreferredLanguage: language.German,
Gender: domain.GenderUnspecified,
Email: "email",
IsEmailVerified: true,
Phone: "phone",
IsPhoneVerified: true,
PasswordChangeRequired: true,
PasswordChanged: testNow,
MFAInitSkipped: testNow,
},
},
{
ID: "id",
CreationDate: testNow,
ChangeDate: testNow,
ResourceOwner: "resource_owner",
Sequence: 20211108,
State: domain.UserStateActive,
Type: domain.UserTypeMachine,
Username: "username",
LoginNames: database.TextArray[string]{"login_name1", "login_name2"},
PreferredLoginName: "login_name1",
Machine: &Machine{
Name: "name",
Description: "description",
EncodedSecret: "secret",
AccessTokenType: domain.OIDCTokenTypeBearer,
},
},
},
},
},
{
name: "prepareUsersQuery sql err",
prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Users, error)) {
q := &UserSearchQueries{}
return q.prepareUsersQuery(t.Context(), false)
},
want: want{
sqlExpectations: mockQueryErr(
regexp.QuoteMeta(usersQuery),
sql.ErrConnDone,
),
err: func(err error) (error, bool) {
if !errors.Is(err, sql.ErrConnDone) {
return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false
}
return nil, true
},
},
object: (*Users)(nil),
},
{
name: "prepareCountUsersQuery no result",
prepare: prepareCountUsersQuery,
want: want{
sqlExpectations: mockQuery(
regexp.QuoteMeta(countUsersQuery),
nil,
nil,
),
},
object: uint64(0),
},
{
name: "prepareCountUsersQuery one result",
prepare: prepareCountUsersQuery,
want: want{
sqlExpectations: mockQueries(
regexp.QuoteMeta(countUsersQuery),
countUsersCols,
[][]driver.Value{{uint64(1)}},
),
},
object: uint64(1),
},
{
name: "prepareCountUsersQuery multiple results",
prepare: prepareCountUsersQuery,
want: want{
sqlExpectations: mockQueries(
regexp.QuoteMeta(countUsersQuery),
countUsersCols,
[][]driver.Value{{uint64(2)}},
),
},
object: uint64(2),
},
{
name: "prepareCountUsersQuery sql err",
prepare: prepareCountUsersQuery,
want: want{
sqlExpectations: mockQueryErr(
regexp.QuoteMeta(countUsersQuery),
sql.ErrConnDone,
),
err: func(err error) (error, bool) {
if !errors.Is(err, sql.ErrConnDone) {
return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false
}
return nil, true
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err)
})
}
}