Ctrl K
ComunidadGuías…

coderd/coderdtest/users.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package coderdtest

import (
	"context"
	"database/sql"
	"fmt"
	"slices"
	"strings"
	"testing"
	"time"

	"github.com/google/uuid"
	"github.com/stretchr/testify/require"

	"github.com/coder/coder/v2/coderd/database"
	"github.com/coder/coder/v2/coderd/database/db2sdk"
	"github.com/coder/coder/v2/coderd/database/dbauthz"
	"github.com/coder/coder/v2/coderd/database/dbtime"
	"github.com/coder/coder/v2/coderd/rbac"
	"github.com/coder/coder/v2/coderd/userpassword"
	"github.com/coder/coder/v2/coderd/util/slice"
	"github.com/coder/coder/v2/codersdk"
	"github.com/coder/coder/v2/testutil"
)

// UsersPagination creates a set of users for testing pagination.  It can be
// used to test paginating both users and group members.
func UsersPagination(
	ctx context.Context,
	t *testing.T,
	client *codersdk.Client,
	setup func(users []codersdk.User),
	fetch func(req codersdk.UsersRequest) ([]codersdk.ReducedUser, int),
) {
	t.Helper()

	firstUser, err := client.User(ctx, codersdk.Me)
	require.NoError(t, err, "fetch me")

	count := 10
	users := make([]codersdk.User, count)
	orgID := firstUser.OrganizationIDs[0]
	users[0] = firstUser
	for i := range count - 1 {
		_, user := CreateAnotherUserMutators(t, client, orgID, nil, func(r *codersdk.CreateUserRequestWithOrgs) {
			if i < 5 {
				r.Name = fmt.Sprintf("before%d", i)
			} else {
				r.Name = fmt.Sprintf("after%d", i)
			}
		})
		users[i+1] = user
	}

	slices.SortFunc(users, func(a, b codersdk.User) int {
		return slice.Ascending(strings.ToLower(a.Username), strings.ToLower(b.Username))
	})

	if setup != nil {
		setup(users)
	}

	gotUsers, gotCount := fetch(codersdk.UsersRequest{})
	require.Len(t, gotUsers, count)
	require.Equal(t, gotCount, count)

	gotUsers, gotCount = fetch(codersdk.UsersRequest{
		Pagination: codersdk.Pagination{
			Limit: 1,
		},
	})
	require.Len(t, gotUsers, 1)
	require.Equal(t, gotCount, count)

	gotUsers, gotCount = fetch(codersdk.UsersRequest{
		Pagination: codersdk.Pagination{
			Offset: 1,
		},
	})
	require.Len(t, gotUsers, count-1)
	require.Equal(t, gotCount, count)

	gotUsers, gotCount = fetch(codersdk.UsersRequest{
		Pagination: codersdk.Pagination{
			Limit:  1,
			Offset: 1,
		},
	})
	require.Len(t, gotUsers, 1)
	require.Equal(t, gotCount, count)

	// If offset is higher than the count postgres returns an empty array
	// and not an ErrNoRows error.
	gotUsers, gotCount = fetch(codersdk.UsersRequest{
		Pagination: codersdk.Pagination{
			Offset: count + 1,
		},
	})
	require.Len(t, gotUsers, 0)
	require.Equal(t, gotCount, 0)

	// Check that AfterID works.
	gotUsers, gotCount = fetch(codersdk.UsersRequest{
		Pagination: codersdk.Pagination{
			AfterID: users[5].ID,
		},
	})
	require.NoError(t, err)
	require.Len(t, gotUsers, 4)
	require.Equal(t, gotCount, 4)

	// Check we can paginate a filtered response.
	gotUsers, gotCount = fetch(codersdk.UsersRequest{
		SearchQuery: "name:after",
		Pagination: codersdk.Pagination{
			Limit:  1,
			Offset: 1,
		},
	})
	require.NoError(t, err)
	require.Len(t, gotUsers, 1)
	require.Equal(t, gotCount, 4)
	require.Contains(t, gotUsers[0].Name, "after")
}

type UsersFilterOptions struct {
	CreateServiceAccounts bool
}

// UsersFilter creates a set of users to run various filters against for
// testing.  It can be used to test filtering both users and group members.
func UsersFilter(
	setupCtx context.Context,
	t *testing.T,
	client *codersdk.Client,
	db database.Store,
	options *UsersFilterOptions,
	setup func(users []codersdk.User),
	fetch func(ctx context.Context, req codersdk.UsersRequest) []codersdk.ReducedUser,
) {
	t.Helper()

	if options == nil {
		options = &UsersFilterOptions{}
	}

	firstUser, err := client.User(setupCtx, codersdk.Me)
	require.NoError(t, err, "fetch me")

	// Noon on Jan 18 is the "now" for this test for last_seen timestamps.
	// All these values are equal
	// 2023-01-18T12:00:00Z (UTC)
	// 2023-01-18T07:00:00-05:00 (America/New_York)
	// 2023-01-18T13:00:00+01:00 (Europe/Madrid)
	// 2023-01-16T00:00:00+12:00 (Asia/Anadyr)
	lastSeenNow := time.Date(2023, 1, 18, 12, 0, 0, 0, time.UTC)
	users := make([]codersdk.User, 0)
	users = append(users, firstUser)
	orgID := firstUser.OrganizationIDs[0]
	githubIDs := make(map[int]uuid.UUID)
	for i := range 15 {
		roles := []rbac.RoleIdentifier{}
		if i%2 == 0 {
			roles = append(roles, rbac.RoleTemplateAdmin(), rbac.RoleUserAdmin())
		}
		if i%3 == 0 {
			roles = append(roles, rbac.RoleAuditor())
		}
		userClient, userData := CreateAnotherUserMutators(t, client, orgID, roles, func(r *codersdk.CreateUserRequestWithOrgs) {
			switch {
			case i%7 == 0:
				r.UserLoginType = codersdk.LoginTypeGithub
				r.Password = ""
			case i%6 == 0:
				r.UserLoginType = codersdk.LoginTypeOIDC
				r.Password = ""
			default:
				r.UserLoginType = codersdk.LoginTypePassword
			}
		})

		// Set the last seen for each user to a unique day
		// nolint:gocritic // Setting up unit test data.
		_, err := db.UpdateUserLastSeenAt(dbauthz.AsSystemRestricted(setupCtx), database.UpdateUserLastSeenAtParams{
			ID:         userData.ID,
			LastSeenAt: lastSeenNow.Add(-1 * time.Hour * 24 * time.Duration(i)),
			UpdatedAt:  time.Now(),
		})
		require.NoError(t, err, "set a last seen")

		// Set a github user ID for github login types.
		if i%7 == 0 {
			// nolint:gocritic // Setting up unit test data.
			err = db.UpdateUserGithubComUserID(dbauthz.AsSystemRestricted(setupCtx), database.UpdateUserGithubComUserIDParams{
				ID: userData.ID,
				GithubComUserID: sql.NullInt64{
					Int64: int64(i),
					Valid: true,
				},
			})
			require.NoError(t, err)
			githubIDs[i] = userData.ID
		}

		user, err := userClient.User(setupCtx, codersdk.Me)
		require.NoError(t, err, "fetch me")

		if i%4 == 0 {
			user, err = client.UpdateUserStatus(setupCtx, user.ID.String(), codersdk.UserStatusSuspended)
			require.NoError(t, err, "suspend user")
		}

		if i%5 == 0 {
			user, err = client.UpdateUserProfile(setupCtx, user.ID.String(), codersdk.UpdateUserProfileRequest{
				Username: strings.ToUpper(user.Username),
			})
			require.NoError(t, err, "update username to uppercase")
		}

		users = append(users, user)
	}

	// Add some service accounts.
	if options.CreateServiceAccounts {
		for range 3 {
			_, user := CreateAnotherUserMutators(t, client, orgID, nil, func(r *codersdk.CreateUserRequestWithOrgs) {
				r.ServiceAccount = true
			})
			users = append(users, user)
		}
	}

	hashedPassword, err := userpassword.Hash("SomeStrongPassword!")
	require.NoError(t, err)

	// Add users with different creation dates for testing date filters
	for i := range 3 {
		// nolint:gocritic // Setting up unit test data.
		user1, err := db.InsertUser(dbauthz.AsSystemRestricted(setupCtx), database.InsertUserParams{
			ID:               uuid.New(),
			Email:            fmt.Sprintf("before%d@coder.com", i),
			Username:         fmt.Sprintf("before%d", i),
			Name:             fmt.Sprintf("Test User %d", i),
			HashedPassword:   []byte(hashedPassword),
			LoginType:        database.LoginTypeNone,
			Status:           string(codersdk.UserStatusActive),
			RBACRoles:        []string{codersdk.RoleMember},
			CreatedAt:        dbtime.Time(time.Date(2022, 12, 15+i, 12, 0, 0, 0, time.UTC)),
			UpdatedAt:        dbtime.Time(time.Date(2022, 12, 15+i, 12, 0, 0, 0, time.UTC)),
			IsServiceAccount: false,
		})
		require.NoError(t, err)
		// nolint:gocritic // Setting up unit test data.
		_, err = db.InsertOrganizationMember(dbauthz.AsSystemRestricted(setupCtx), database.InsertOrganizationMemberParams{
			OrganizationID: orgID,
			UserID:         user1.ID,
			CreatedAt:      dbtime.Now(),
			UpdatedAt:      dbtime.Now(),
			Roles:          []string{},
		})
		require.NoError(t, err)

		// The expected timestamps must be parsed from strings to compare equal during `ElementsMatch`
		sdkUser1 := db2sdk.User(user1, []uuid.UUID{orgID})
		sdkUser1.CreatedAt, err = time.Parse(time.RFC3339, sdkUser1.CreatedAt.Format(time.RFC3339))
		require.NoError(t, err)
		sdkUser1.UpdatedAt, err = time.Parse(time.RFC3339, sdkUser1.UpdatedAt.Format(time.RFC3339))
		require.NoError(t, err)
		sdkUser1.LastSeenAt, err = time.Parse(time.RFC3339, sdkUser1.LastSeenAt.Format(time.RFC3339))
		require.NoError(t, err)
		users = append(users, sdkUser1)

		// nolint:gocritic // Setting up unit test data.
		user2, err := db.InsertUser(dbauthz.AsSystemRestricted(setupCtx), database.InsertUserParams{
			ID:               uuid.New(),
			Email:            fmt.Sprintf("during%d@coder.com", i),
			Username:         fmt.Sprintf("during%d", i),
			Name:             "",
			HashedPassword:   []byte(hashedPassword),
			LoginType:        database.LoginTypeNone,
			Status:           string(codersdk.UserStatusActive),
			RBACRoles:        []string{codersdk.RoleOwner},
			CreatedAt:        dbtime.Time(time.Date(2023, 1, 15+i, 12, 0, 0, 0, time.UTC)),
			UpdatedAt:        dbtime.Time(time.Date(2023, 1, 15+i, 12, 0, 0, 0, time.UTC)),
			IsServiceAccount: false,
		})
		require.NoError(t, err)
		// nolint:gocritic // Setting up unit test data.
		_, err = db.InsertOrganizationMember(dbauthz.AsSystemRestricted(setupCtx), database.InsertOrganizationMemberParams{
			OrganizationID: orgID,
			UserID:         user2.ID,
			CreatedAt:      dbtime.Now(),
			UpdatedAt:      dbtime.Now(),
			Roles:          []string{},
		})
		require.NoError(t, err)

		sdkUser2 := db2sdk.User(user2, []uuid.UUID{orgID})
		sdkUser2.CreatedAt, err = time.Parse(time.RFC3339, sdkUser2.CreatedAt.Format(time.RFC3339))
		require.NoError(t, err)
		sdkUser2.UpdatedAt, err = time.Parse(time.RFC3339, sdkUser2.UpdatedAt.Format(time.RFC3339))
		require.NoError(t, err)
		sdkUser2.LastSeenAt, err = time.Parse(time.RFC3339, sdkUser2.LastSeenAt.Format(time.RFC3339))
		require.NoError(t, err)
		users = append(users, sdkUser2)

		// nolint:gocritic // Setting up unit test data.
		user3, err := db.InsertUser(dbauthz.AsSystemRestricted(setupCtx), database.InsertUserParams{
			ID:               uuid.New(),
			Email:            fmt.Sprintf("after%d@coder.com", i),
			Username:         fmt.Sprintf("after%d", i),
			Name:             "",
			HashedPassword:   []byte(hashedPassword),
			LoginType:        database.LoginTypeNone,
			Status:           string(codersdk.UserStatusActive),
			RBACRoles:        []string{codersdk.RoleOwner},
			CreatedAt:        dbtime.Time(time.Date(2023, 2, 15+i, 12, 0, 0, 0, time.UTC)),
			UpdatedAt:        dbtime.Time(time.Date(2023, 2, 15+i, 12, 0, 0, 0, time.UTC)),
			IsServiceAccount: false,
		})
		require.NoError(t, err)
		// nolint:gocritic // Setting up unit test data.
		_, err = db.InsertOrganizationMember(dbauthz.AsSystemRestricted(setupCtx), database.InsertOrganizationMemberParams{
			OrganizationID: orgID,
			UserID:         user3.ID,
			CreatedAt:      dbtime.Now(),
			UpdatedAt:      dbtime.Now(),
			Roles:          []string{},
		})
		require.NoError(t, err)

		sdkUser3 := db2sdk.User(user3, []uuid.UUID{orgID})
		sdkUser3.CreatedAt, err = time.Parse(time.RFC3339, sdkUser3.CreatedAt.Format(time.RFC3339))
		require.NoError(t, err)
		sdkUser3.UpdatedAt, err = time.Parse(time.RFC3339, sdkUser3.UpdatedAt.Format(time.RFC3339))
		require.NoError(t, err)
		sdkUser3.LastSeenAt, err = time.Parse(time.RFC3339, sdkUser3.LastSeenAt.Format(time.RFC3339))
		require.NoError(t, err)
		users = append(users, sdkUser3)
	}

	if setup != nil {
		setup(users)
	}

	// --- Setup done ---
	testCases := []struct {
		Name   string
		Filter codersdk.UsersRequest
		// If FilterF is true, we include it in the expected results
		FilterF func(f codersdk.UsersRequest, user codersdk.User) bool
	}{
		{
			Name: "All",
			Filter: codersdk.UsersRequest{
				Status: codersdk.UserStatusSuspended + "," + codersdk.UserStatusActive,
			},
			FilterF: func(_ codersdk.UsersRequest, _ codersdk.User) bool {
				return true
			},
		},
		{
			Name: "Active",
			Filter: codersdk.UsersRequest{
				Status: codersdk.UserStatusActive,
			},
			FilterF: func(_ codersdk.UsersRequest, u codersdk.User) bool {
				return u.Status == codersdk.UserStatusActive
			},
		},
		{
			Name: "GithubComUserID",
			Filter: codersdk.UsersRequest{
				SearchQuery: "github_com_user_id:7",
			},
			FilterF: func(_ codersdk.UsersRequest, u codersdk.User) bool {
				return u.ID == githubIDs[7]
			},
		},
		{
			Name: "ActiveUppercase",
			Filter: codersdk.UsersRequest{
				Status: "ACTIVE",
			},
			FilterF: func(_ codersdk.UsersRequest, u codersdk.User) bool {
				return u.Status == codersdk.UserStatusActive
			},
		},
		{
			Name: "Suspended",
			Filter: codersdk.UsersRequest{
				Status: codersdk.UserStatusSuspended,
			},
			FilterF: func(_ codersdk.UsersRequest, u codersdk.User) bool {
				return u.Status == codersdk.UserStatusSuspended
			},
		},
		{
			Name: "NameContains",
			Filter: codersdk.UsersRequest{
				Search: "a",
			},
			FilterF: func(_ codersdk.UsersRequest, u codersdk.User) bool {
				return (strings.ContainsAny(u.Username, "aA") || strings.ContainsAny(u.Email, "aA") || strings.ContainsAny(u.Name, "aA"))
			},
		},
		{
			Name: "DisplayNameSearch",
			Filter: codersdk.UsersRequest{
				Search: "user",
			},
			FilterF: func(_ codersdk.UsersRequest, u codersdk.User) bool {
				const term = "user"
				return strings.Contains(strings.ToLower(u.Username), term) ||
					strings.Contains(strings.ToLower(u.Email), term) ||
					strings.Contains(strings.ToLower(u.Name), term)
			},
		},
		{
			Name: "NameAndSearch",
			Filter: codersdk.UsersRequest{
				SearchQuery: "name:Test search:before1",
			},
			FilterF: func(_ codersdk.UsersRequest, u codersdk.User) bool {
				return u.Username == "before1"
			},
		},
		{
			Name: "ExactUsername",
			Filter: codersdk.UsersRequest{
				SearchQuery: "username:" + strings.ToUpper(users[0].Username),
			},
			FilterF: func(_ codersdk.UsersRequest, u codersdk.User) bool {
				return strings.EqualFold(u.Username, users[0].Username)
			},
		},
		{
			Name: "ExactEmail",
			Filter: codersdk.UsersRequest{
				SearchQuery: "email:" + strings.ToUpper(users[0].Email),
			},
			FilterF: func(_ codersdk.UsersRequest, u codersdk.User) bool {
				return strings.EqualFold(u.Email, users[0].Email)
			},
		},
		{
			Name: "NameNoMatch",
			Filter: codersdk.UsersRequest{
				Search: "nonexistent",
			},
			FilterF: func(_ codersdk.UsersRequest, _ codersdk.User) bool {
				return false
			},
		},
		{
			Name: "Admins",
			Filter: codersdk.UsersRequest{
				Role:   codersdk.RoleOwner,
				Status: codersdk.UserStatusSuspended + "," + codersdk.UserStatusActive,
			},
			FilterF: func(_ codersdk.UsersRequest, u codersdk.User) bool {
				for _, r := range u.Roles {
					if r.Name == codersdk.RoleOwner {
						return true
					}
				}
				return false
			},
		},
		{
			Name: "AdminsUppercase",
			Filter: codersdk.UsersRequest{
				Role:   "OWNER",
				Status: codersdk.UserStatusSuspended + "," + codersdk.UserStatusActive,
			},
			FilterF: func(_ codersdk.UsersRequest, u codersdk.User) bool {
				for _, r := range u.Roles {
					if r.Name == codersdk.RoleOwner {
						return true
					}
				}
				return false
			},
		},
		{
			Name: "Members",
			Filter: codersdk.UsersRequest{
				Role:   codersdk.RoleMember,
				Status: codersdk.UserStatusSuspended + "," + codersdk.UserStatusActive,
			},
			FilterF: func(_ codersdk.UsersRequest, _ codersdk.User) bool {
				return true
			},
		},
		{
			Name: "SearchQuery",
			Filter: codersdk.UsersRequest{
				SearchQuery: "i role:owner status:active",
			},
			FilterF: func(_ codersdk.UsersRequest, u codersdk.User) bool {
				for _, r := range u.Roles {
					if r.Name == codersdk.RoleOwner {
						return (strings.ContainsAny(u.Username, "iI") || strings.ContainsAny(u.Email, "iI") || strings.ContainsAny(u.Name, "iI")) &&
							u.Status == codersdk.UserStatusActive
					}
				}
				return false
			},
		},
		{
			Name: "SearchQueryInsensitive",
			Filter: codersdk.UsersRequest{
				SearchQuery: "i Role:Owner STATUS:Active",
			},
			FilterF: func(_ codersdk.UsersRequest, u codersdk.User) bool {
				for _, r := range u.Roles {
					if r.Name == codersdk.RoleOwner {
						return (strings.ContainsAny(u.Username, "iI") || strings.ContainsAny(u.Email, "iI") || strings.ContainsAny(u.Name, "iI")) &&
							u.Status == codersdk.UserStatusActive
					}
				}
				return false
			},
		},
		{
			Name: "LastSeenBeforeNow",
			Filter: codersdk.UsersRequest{
				SearchQuery: `last_seen_before:"2023-01-16T00:00:00+12:00"`,
			},
			FilterF: func(_ codersdk.UsersRequest, u codersdk.User) bool {
				return u.LastSeenAt.Before(lastSeenNow)
			},
		},
		{
			Name: "LastSeenLastWeek",
			Filter: codersdk.UsersRequest{
				SearchQuery: `last_seen_before:"2023-01-14T23:59:59Z" last_seen_after:"2023-01-08T00:00:00Z"`,
			},
			FilterF: func(_ codersdk.UsersRequest, u codersdk.User) bool {
				start := time.Date(2023, 1, 8, 0, 0, 0, 0, time.UTC)
				end := time.Date(2023, 1, 14, 23, 59, 59, 0, time.UTC)
				return u.LastSeenAt.Before(end) && u.LastSeenAt.After(start)
			},
		},
		{
			Name: "CreatedAtBefore",
			Filter: codersdk.UsersRequest{
				SearchQuery: `created_before:"2023-01-31T23:59:59Z"`,
			},
			FilterF: func(_ codersdk.UsersRequest, u codersdk.User) bool {
				end := time.Date(2023, 1, 31, 23, 59, 59, 0, time.UTC)
				return u.CreatedAt.Before(end)
			},
		},
		{
			Name: "CreatedAtAfter",
			Filter: codersdk.UsersRequest{
				SearchQuery: `created_after:"2023-01-01T00:00:00Z"`,
			},
			FilterF: func(_ codersdk.UsersRequest, u codersdk.User) bool {
				start := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)
				return u.CreatedAt.After(start)
			},
		},
		{
			Name: "CreatedAtRange",
			Filter: codersdk.UsersRequest{
				SearchQuery: `created_after:"2023-01-01T00:00:00Z" created_before:"2023-01-31T23:59:59Z"`,
			},
			FilterF: func(_ codersdk.UsersRequest, u codersdk.User) bool {
				start := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)
				end := time.Date(2023, 1, 31, 23, 59, 59, 0, time.UTC)
				return u.CreatedAt.After(start) && u.CreatedAt.Before(end)
			},
		},
		{
			Name: "LoginTypeNone",
			Filter: codersdk.UsersRequest{
				LoginType: []codersdk.LoginType{codersdk.LoginTypeNone},
			},
			FilterF: func(_ codersdk.UsersRequest, u codersdk.User) bool {
				return u.LoginType == codersdk.LoginTypeNone
			},
		},
		{
			Name: "LoginTypeOIDC",
			Filter: codersdk.UsersRequest{
				LoginType: []codersdk.LoginType{codersdk.LoginTypeOIDC},
			},
			FilterF: func(_ codersdk.UsersRequest, u codersdk.User) bool {
				return u.LoginType == codersdk.LoginTypeOIDC
			},
		},
		{
			Name: "LoginTypeMultiple",
			Filter: codersdk.UsersRequest{
				LoginType: []codersdk.LoginType{codersdk.LoginTypeNone, codersdk.LoginTypeGithub},
			},
			FilterF: func(_ codersdk.UsersRequest, u codersdk.User) bool {
				return u.LoginType == codersdk.LoginTypeNone || u.LoginType == codersdk.LoginTypeGithub
			},
		},
		{
			Name: "DormantUserWithLoginTypeNone",
			Filter: codersdk.UsersRequest{
				Status:    codersdk.UserStatusSuspended,
				LoginType: []codersdk.LoginType{codersdk.LoginTypeNone},
			},
			FilterF: func(_ codersdk.UsersRequest, u codersdk.User) bool {
				return u.Status == codersdk.UserStatusSuspended && u.LoginType == codersdk.LoginTypeNone
			},
		},
		{
			Name: "IsServiceAccount",
			Filter: codersdk.UsersRequest{
				Search: "service_account:true",
			},
			FilterF: func(_ codersdk.UsersRequest, u codersdk.User) bool {
				return u.IsServiceAccount
			},
		},
		{
			Name: "IsNotServiceAccount",
			Filter: codersdk.UsersRequest{
				Search: "service_account:false",
			},
			FilterF: func(_ codersdk.UsersRequest, u codersdk.User) bool {
				return !u.IsServiceAccount
			},
		},
	}

	for _, c := range testCases {
		t.Run(c.Name, func(t *testing.T) {
			t.Parallel()

			testCtx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
			defer cancel()

			got := fetch(testCtx, c.Filter)
			exp := make([]codersdk.ReducedUser, 0)
			for _, made := range users {
				match := c.FilterF(c.Filter, made)
				if match {
					exp = append(exp, made.ReducedUser)
				}
			}

			require.ElementsMatch(t, exp, got, "expected users returned")
		})
	}
}