feat: add "dormant" user state (#8644)

This commit is contained in:
Marcin Tojek
2023-08-02 16:31:25 +02:00
committed by GitHub
parent d2c7c8e1d8
commit d6e9870209
40 changed files with 587 additions and 239 deletions
+2
View File
@@ -10227,10 +10227,12 @@ const docTemplate = `{
"type": "string",
"enum": [
"active",
"dormant",
"suspended"
],
"x-enum-varnames": [
"UserStatusActive",
"UserStatusDormant",
"UserStatusSuspended"
]
},
+6 -2
View File
@@ -9256,8 +9256,12 @@
},
"codersdk.UserStatus": {
"type": "string",
"enum": ["active", "suspended"],
"x-enum-varnames": ["UserStatusActive", "UserStatusSuspended"]
"enum": ["active", "dormant", "suspended"],
"x-enum-varnames": [
"UserStatusActive",
"UserStatusDormant",
"UserStatusSuspended"
]
},
"codersdk.ValidationError": {
"type": "object",
+8
View File
@@ -587,6 +587,14 @@ func createAnotherUserRetry(t *testing.T, client *codersdk.Client, organizationI
sessionToken = token.Key
}
if user.Status == codersdk.UserStatusDormant {
// Use admin client so that user's LastSeenAt is not updated.
// In general we need to refresh the user status, which should
// transition from "dormant" to "active".
user, err = client.User(context.Background(), user.Username)
require.NoError(t, err)
}
other := codersdk.New(client.URL)
other.SetSessionToken(sessionToken)
t.Cleanup(func() {
+7
View File
@@ -2099,6 +2099,13 @@ func (q *querier) UpdateGroupByID(ctx context.Context, arg database.UpdateGroupB
return updateWithReturn(q.log, q.auth, fetch, q.db.UpdateGroupByID)(ctx, arg)
}
func (q *querier) UpdateInactiveUsersToDormant(ctx context.Context, lastSeenAfter database.UpdateInactiveUsersToDormantParams) ([]database.UpdateInactiveUsersToDormantRow, error) {
if err := q.authorizeContext(ctx, rbac.ActionCreate, rbac.ResourceSystem); err != nil {
return nil, err
}
return q.db.UpdateInactiveUsersToDormant(ctx, lastSeenAfter)
}
func (q *querier) UpdateMemberRoles(ctx context.Context, arg database.UpdateMemberRolesParams) (database.OrganizationMember, error) {
// Authorized fetch will check that the actor has read access to the org member since the org member is returned.
member, err := q.GetOrganizationMemberByUserID(ctx, database.GetOrganizationMemberByUserIDParams{
+24 -1
View File
@@ -3862,7 +3862,7 @@ func (q *FakeQuerier) InsertUser(_ context.Context, arg database.InsertUserParam
CreatedAt: arg.CreatedAt,
UpdatedAt: arg.UpdatedAt,
Username: arg.Username,
Status: database.UserStatusActive,
Status: database.UserStatusDormant,
RBACRoles: arg.RBACRoles,
LoginType: arg.LoginType,
}
@@ -4337,6 +4337,29 @@ func (q *FakeQuerier) UpdateGroupByID(_ context.Context, arg database.UpdateGrou
return database.Group{}, sql.ErrNoRows
}
func (q *FakeQuerier) UpdateInactiveUsersToDormant(_ context.Context, params database.UpdateInactiveUsersToDormantParams) ([]database.UpdateInactiveUsersToDormantRow, error) {
q.mutex.Lock()
defer q.mutex.Unlock()
var updated []database.UpdateInactiveUsersToDormantRow
for index, user := range q.users {
if user.Status == database.UserStatusActive && user.LastSeenAt.Before(params.LastSeenAfter) {
q.users[index].Status = database.UserStatusDormant
q.users[index].UpdatedAt = params.UpdatedAt
updated = append(updated, database.UpdateInactiveUsersToDormantRow{
ID: user.ID,
Email: user.Email,
LastSeenAt: user.LastSeenAt,
})
}
}
if len(updated) == 0 {
return nil, sql.ErrNoRows
}
return updated, nil
}
func (q *FakeQuerier) UpdateMemberRoles(_ context.Context, arg database.UpdateMemberRolesParams) (database.OrganizationMember, error) {
if err := validateDatabaseType(arg); err != nil {
return database.OrganizationMember{}, err
+7
View File
@@ -224,6 +224,13 @@ func User(t testing.TB, db database.Store, orig database.User) database.User {
})
require.NoError(t, err, "insert user")
user, err = db.UpdateUserStatus(genCtx, database.UpdateUserStatusParams{
ID: user.ID,
Status: database.UserStatusActive,
UpdatedAt: database.Now(),
})
require.NoError(t, err, "insert user")
if !orig.LastSeenAt.IsZero() {
user, err = db.UpdateUserLastSeenAt(genCtx, database.UpdateUserLastSeenAtParams{
ID: user.ID,
+7
View File
@@ -1313,6 +1313,13 @@ func (m metricsStore) UpdateGroupByID(ctx context.Context, arg database.UpdateGr
return group, err
}
func (m metricsStore) UpdateInactiveUsersToDormant(ctx context.Context, lastSeenAfter database.UpdateInactiveUsersToDormantParams) ([]database.UpdateInactiveUsersToDormantRow, error) {
start := time.Now()
r0, r1 := m.s.UpdateInactiveUsersToDormant(ctx, lastSeenAfter)
m.queryLatencies.WithLabelValues("UpdateInactiveUsersToDormant").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m metricsStore) UpdateMemberRoles(ctx context.Context, arg database.UpdateMemberRolesParams) (database.OrganizationMember, error) {
start := time.Now()
member, err := m.s.UpdateMemberRoles(ctx, arg)
+15
View File
@@ -2775,6 +2775,21 @@ func (mr *MockStoreMockRecorder) UpdateGroupByID(arg0, arg1 interface{}) *gomock
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateGroupByID", reflect.TypeOf((*MockStore)(nil).UpdateGroupByID), arg0, arg1)
}
// UpdateInactiveUsersToDormant mocks base method.
func (m *MockStore) UpdateInactiveUsersToDormant(arg0 context.Context, arg1 database.UpdateInactiveUsersToDormantParams) ([]database.UpdateInactiveUsersToDormantRow, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateInactiveUsersToDormant", arg0, arg1)
ret0, _ := ret[0].([]database.UpdateInactiveUsersToDormantRow)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateInactiveUsersToDormant indicates an expected call of UpdateInactiveUsersToDormant.
func (mr *MockStoreMockRecorder) UpdateInactiveUsersToDormant(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateInactiveUsersToDormant", reflect.TypeOf((*MockStore)(nil).UpdateInactiveUsersToDormant), arg0, arg1)
}
// UpdateMemberRoles mocks base method.
func (m *MockStore) UpdateMemberRoles(arg0 context.Context, arg1 database.UpdateMemberRolesParams) (database.OrganizationMember, error) {
m.ctrl.T.Helper()
+5 -2
View File
@@ -113,9 +113,12 @@ CREATE TYPE startup_script_behavior AS ENUM (
CREATE TYPE user_status AS ENUM (
'active',
'suspended'
'suspended',
'dormant'
);
COMMENT ON TYPE user_status IS 'Defines the user status: active, dormant, or suspended.';
CREATE TYPE workspace_agent_lifecycle_state AS ENUM (
'created',
'starting',
@@ -561,7 +564,7 @@ CREATE TABLE users (
hashed_password bytea NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
status user_status DEFAULT 'active'::user_status NOT NULL,
status user_status DEFAULT 'dormant'::user_status NOT NULL,
rbac_roles text[] DEFAULT '{}'::text[] NOT NULL,
login_type login_type DEFAULT 'password'::login_type NOT NULL,
avatar_url text,
@@ -0,0 +1,3 @@
-- It's not possible to drop enum values from enum types, so the UP has "IF NOT EXISTS"
UPDATE users SET status = 'active'::user_status WHERE status = 'dormant'::user_status;
@@ -0,0 +1,2 @@
ALTER TYPE user_status ADD VALUE IF NOT EXISTS 'dormant';
COMMENT ON TYPE user_status IS 'Defines the user status: active, dormant, or suspended.';
@@ -0,0 +1 @@
ALTER TABLE users ALTER COLUMN status SET DEFAULT 'active'::user_status;
@@ -0,0 +1 @@
ALTER TABLE users ALTER COLUMN status SET DEFAULT 'dormant'::user_status;
+5 -1
View File
@@ -1032,11 +1032,13 @@ func AllStartupScriptBehaviorValues() []StartupScriptBehavior {
}
}
// Defines the user status: active, dormant, or suspended.
type UserStatus string
const (
UserStatusActive UserStatus = "active"
UserStatusSuspended UserStatus = "suspended"
UserStatusDormant UserStatus = "dormant"
)
func (e *UserStatus) Scan(src interface{}) error {
@@ -1077,7 +1079,8 @@ func (ns NullUserStatus) Value() (driver.Value, error) {
func (e UserStatus) Valid() bool {
switch e {
case UserStatusActive,
UserStatusSuspended:
UserStatusSuspended,
UserStatusDormant:
return true
}
return false
@@ -1087,6 +1090,7 @@ func AllUserStatusValues() []UserStatus {
return []UserStatus{
UserStatusActive,
UserStatusSuspended,
UserStatusDormant,
}
}
+1
View File
@@ -236,6 +236,7 @@ type sqlcQuerier interface {
UpdateGitAuthLink(ctx context.Context, arg UpdateGitAuthLinkParams) (GitAuthLink, error)
UpdateGitSSHKey(ctx context.Context, arg UpdateGitSSHKeyParams) (GitSSHKey, error)
UpdateGroupByID(ctx context.Context, arg UpdateGroupByIDParams) (Group, error)
UpdateInactiveUsersToDormant(ctx context.Context, arg UpdateInactiveUsersToDormantParams) ([]UpdateInactiveUsersToDormantRow, error)
UpdateMemberRoles(ctx context.Context, arg UpdateMemberRolesParams) (OrganizationMember, error)
UpdateProvisionerJobByID(ctx context.Context, arg UpdateProvisionerJobByIDParams) error
UpdateProvisionerJobWithCancelByID(ctx context.Context, arg UpdateProvisionerJobWithCancelByIDParams) error
+46
View File
@@ -5708,6 +5708,52 @@ func (q *sqlQuerier) InsertUser(ctx context.Context, arg InsertUserParams) (User
return i, err
}
const updateInactiveUsersToDormant = `-- name: UpdateInactiveUsersToDormant :many
UPDATE
users
SET
status = 'dormant'::user_status,
updated_at = $1
WHERE
last_seen_at < $2 :: timestamp
AND status = 'active'::user_status
RETURNING id, email, last_seen_at
`
type UpdateInactiveUsersToDormantParams struct {
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
LastSeenAfter time.Time `db:"last_seen_after" json:"last_seen_after"`
}
type UpdateInactiveUsersToDormantRow struct {
ID uuid.UUID `db:"id" json:"id"`
Email string `db:"email" json:"email"`
LastSeenAt time.Time `db:"last_seen_at" json:"last_seen_at"`
}
func (q *sqlQuerier) UpdateInactiveUsersToDormant(ctx context.Context, arg UpdateInactiveUsersToDormantParams) ([]UpdateInactiveUsersToDormantRow, error) {
rows, err := q.db.QueryContext(ctx, updateInactiveUsersToDormant, arg.UpdatedAt, arg.LastSeenAfter)
if err != nil {
return nil, err
}
defer rows.Close()
var items []UpdateInactiveUsersToDormantRow
for rows.Next() {
var i UpdateInactiveUsersToDormantRow
if err := rows.Scan(&i.ID, &i.Email, &i.LastSeenAt); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateUserDeletedByID = `-- name: UpdateUserDeletedByID :exec
UPDATE
users
+12
View File
@@ -250,3 +250,15 @@ SET
WHERE
id = $1
RETURNING *;
-- name: UpdateInactiveUsersToDormant :many
UPDATE
users
SET
status = 'dormant'::user_status,
updated_at = @updated_at
WHERE
last_seen_at < @last_seen_after :: timestamp
AND status = 'active'::user_status
RETURNING id, email, last_seen_at;
+70
View File
@@ -0,0 +1,70 @@
package dormancy
import (
"context"
"database/sql"
"time"
"golang.org/x/xerrors"
"cdr.dev/slog"
"github.com/coder/coder/coderd/database"
)
const (
// Time interval between consecutive job runs
jobInterval = 15 * time.Minute
// User accounts inactive for `accountDormancyPeriod` will be marked as dormant
accountDormancyPeriod = 90 * 24 * time.Hour
)
// CheckInactiveUsers function updates status of inactive users from active to dormant
// using default parameters.
func CheckInactiveUsers(ctx context.Context, logger slog.Logger, db database.Store) func() {
return CheckInactiveUsersWithOptions(ctx, logger, db, jobInterval, accountDormancyPeriod)
}
// CheckInactiveUsersWithOptions function updates status of inactive users from active to dormant
// using provided parameters.
func CheckInactiveUsersWithOptions(ctx context.Context, logger slog.Logger, db database.Store, checkInterval, dormancyPeriod time.Duration) func() {
logger = logger.Named("dormancy")
ctx, cancelFunc := context.WithCancel(ctx)
done := make(chan struct{})
ticker := time.NewTicker(checkInterval)
go func() {
defer close(done)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
}
startTime := time.Now()
lastSeenAfter := database.Now().Add(-dormancyPeriod)
logger.Debug(ctx, "check inactive user accounts", slog.F("dormancy_period", dormancyPeriod), slog.F("last_seen_after", lastSeenAfter))
updatedUsers, err := db.UpdateInactiveUsersToDormant(ctx, database.UpdateInactiveUsersToDormantParams{
LastSeenAfter: lastSeenAfter,
UpdatedAt: database.Now(),
})
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
logger.Error(ctx, "can't mark inactive users as dormant", slog.Error(err))
continue
}
for _, u := range updatedUsers {
logger.Info(ctx, "account has been marked as dormant", slog.F("email", u.Email), slog.F("last_seen_at", u.LastSeenAt))
}
logger.Debug(ctx, "checking user accounts is done", slog.F("num_dormant_accounts", len(updatedUsers)), slog.F("execution_time", time.Since(startTime)))
}
}()
return func() {
cancelFunc()
<-done
}
}
+110
View File
@@ -0,0 +1,110 @@
package dormancy_test
import (
"context"
"testing"
"time"
"github.com/google/uuid"
"github.com/moby/moby/pkg/namesgenerator"
"github.com/stretchr/testify/require"
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/database/dbfake"
"github.com/coder/coder/coderd/dormancy"
"github.com/coder/coder/testutil"
)
func TestCheckInactiveUsers(t *testing.T) {
t.Parallel()
// Predefine job settings
interval := time.Millisecond
dormancyPeriod := 90 * 24 * time.Hour
// Add some dormant accounts
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
db := dbfake.New()
ctx, cancelFunc := context.WithCancel(context.Background())
t.Cleanup(cancelFunc)
inactiveUser1 := setupUser(ctx, t, db, "dormant-user-1@coder.com", database.UserStatusActive, time.Now().Add(-dormancyPeriod).Add(-time.Minute))
inactiveUser2 := setupUser(ctx, t, db, "dormant-user-2@coder.com", database.UserStatusActive, time.Now().Add(-dormancyPeriod).Add(-time.Hour))
inactiveUser3 := setupUser(ctx, t, db, "dormant-user-3@coder.com", database.UserStatusActive, time.Now().Add(-dormancyPeriod).Add(-6*time.Hour))
activeUser1 := setupUser(ctx, t, db, "active-user-1@coder.com", database.UserStatusActive, time.Now().Add(-dormancyPeriod).Add(time.Minute))
activeUser2 := setupUser(ctx, t, db, "active-user-2@coder.com", database.UserStatusActive, time.Now().Add(-dormancyPeriod).Add(time.Hour))
activeUser3 := setupUser(ctx, t, db, "active-user-3@coder.com", database.UserStatusActive, time.Now().Add(-dormancyPeriod).Add(6*time.Hour))
suspendedUser1 := setupUser(ctx, t, db, "suspended-user-1@coder.com", database.UserStatusSuspended, time.Now().Add(-dormancyPeriod).Add(-time.Minute))
suspendedUser2 := setupUser(ctx, t, db, "suspended-user-2@coder.com", database.UserStatusSuspended, time.Now().Add(-dormancyPeriod).Add(-time.Hour))
suspendedUser3 := setupUser(ctx, t, db, "suspended-user-3@coder.com", database.UserStatusSuspended, time.Now().Add(-dormancyPeriod).Add(-6*time.Hour))
// Run the periodic job
closeFunc := dormancy.CheckInactiveUsersWithOptions(ctx, logger, db, interval, dormancyPeriod)
t.Cleanup(closeFunc)
var rows []database.GetUsersRow
var err error
require.Eventually(t, func() bool {
rows, err = db.GetUsers(ctx, database.GetUsersParams{})
if err != nil {
return false
}
var dormant, suspended int
for _, row := range rows {
if row.Status == database.UserStatusDormant {
dormant++
} else if row.Status == database.UserStatusSuspended {
suspended++
}
}
// 6 users in total, 3 dormant, 3 suspended
return len(rows) == 9 && dormant == 3 && suspended == 3
}, testutil.WaitShort, testutil.IntervalMedium)
allUsers := ignoreUpdatedAt(database.ConvertUserRows(rows))
// Verify user status
expectedUsers := []database.User{
asDormant(inactiveUser1),
asDormant(inactiveUser2),
asDormant(inactiveUser3),
activeUser1,
activeUser2,
activeUser3,
suspendedUser1,
suspendedUser2,
suspendedUser3,
}
require.ElementsMatch(t, allUsers, expectedUsers)
}
func setupUser(ctx context.Context, t *testing.T, db database.Store, email string, status database.UserStatus, lastSeenAt time.Time) database.User {
t.Helper()
user, err := db.InsertUser(ctx, database.InsertUserParams{ID: uuid.New(), LoginType: database.LoginTypePassword, Username: namesgenerator.GetRandomName(8), Email: email})
require.NoError(t, err)
// At the beginning of the test all users are marked as active
user, err = db.UpdateUserStatus(ctx, database.UpdateUserStatusParams{ID: user.ID, Status: status})
require.NoError(t, err)
user, err = db.UpdateUserLastSeenAt(ctx, database.UpdateUserLastSeenAtParams{ID: user.ID, LastSeenAt: lastSeenAt})
require.NoError(t, err)
return user
}
func asDormant(user database.User) database.User {
user.Status = database.UserStatusDormant
return user
}
func ignoreUpdatedAt(rows []database.User) []database.User {
for i := range rows {
rows[i].UpdatedAt = time.Time{}
}
return rows
}
+17
View File
@@ -393,6 +393,23 @@ func ExtractAPIKey(rw http.ResponseWriter, r *http.Request, cfg ExtractAPIKeyCon
})
}
if roles.Status == database.UserStatusDormant {
// If coder confirms that the dormant user is valid, it can switch their account to active.
// nolint:gocritic
u, err := cfg.DB.UpdateUserStatus(dbauthz.AsSystemRestricted(ctx), database.UpdateUserStatusParams{
ID: key.UserID,
Status: database.UserStatusActive,
UpdatedAt: database.Now(),
})
if err != nil {
return write(http.StatusInternalServerError, codersdk.Response{
Message: internalErrorMessage,
Detail: fmt.Sprintf("can't activate a dormant user: %s", err.Error()),
})
}
roles.Status = u.Status
}
if roles.Status != database.UserStatusActive {
return write(http.StatusUnauthorized, codersdk.Response{
Message: fmt.Sprintf("User is not active (status = %q). Contact an admin to reactivate your account.", roles.Status),
+7
View File
@@ -156,6 +156,13 @@ func addUser(t *testing.T, db database.Store, roles ...string) (database.User, s
})
require.NoError(t, err)
user, err = db.UpdateUserStatus(context.Background(), database.UpdateUserStatusParams{
ID: user.ID,
Status: database.UserStatusActive,
UpdatedAt: database.Now(),
})
require.NoError(t, err)
_, err = db.InsertAPIKey(context.Background(), database.InsertAPIKeyParams{
ID: id,
UserID: user.ID,
+7
View File
@@ -48,6 +48,13 @@ func TestWorkspaceParam(t *testing.T) {
})
require.NoError(t, err)
user, err = db.UpdateUserStatus(context.Background(), database.UpdateUserStatusParams{
ID: user.ID,
Status: database.UserStatusActive,
UpdatedAt: database.Now(),
})
require.NoError(t, err)
_, err = db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
ID: id,
UserID: user.ID,
+31 -1
View File
@@ -320,6 +320,22 @@ func (api *API) loginRequest(ctx context.Context, rw http.ResponseWriter, req co
return user, database.GetAuthorizationUserRolesRow{}, false
}
if user.Status == database.UserStatusDormant {
//nolint:gocritic // System needs to update status of the user account (dormant -> active).
user, err = api.Database.UpdateUserStatus(dbauthz.AsSystemRestricted(ctx), database.UpdateUserStatusParams{
ID: user.ID,
Status: database.UserStatusActive,
UpdatedAt: database.Now(),
})
if err != nil {
logger.Error(ctx, "unable to update user status to active", slog.Error(err))
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error occurred. Try again later, or contact an admin for assistance.",
})
return user, database.GetAuthorizationUserRolesRow{}, false
}
}
//nolint:gocritic // System needs to fetch user roles in order to login user.
roles, err := api.Database.GetAuthorizationUserRoles(dbauthz.AsSystemRestricted(ctx), user.ID)
if err != nil {
@@ -333,7 +349,7 @@ func (api *API) loginRequest(ctx context.Context, rw http.ResponseWriter, req co
// If the user logged into a suspended account, reject the login request.
if roles.Status != database.UserStatusActive {
httpapi.Write(ctx, rw, http.StatusUnauthorized, codersdk.Response{
Message: "Your account is suspended. Contact an admin to reactivate your account.",
Message: fmt.Sprintf("Your account is %s. Contact an admin to reactivate your account.", roles.Status),
})
return user, database.GetAuthorizationUserRolesRow{}, false
}
@@ -1281,6 +1297,20 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C
}
}
// Activate dormant user on sigin
if user.Status == database.UserStatusDormant {
//nolint:gocritic // System needs to update status of the user account (dormant -> active).
user, err = tx.UpdateUserStatus(dbauthz.AsSystemRestricted(ctx), database.UpdateUserStatusParams{
ID: user.ID,
Status: database.UserStatusActive,
UpdatedAt: database.Now(),
})
if err != nil {
logger.Error(ctx, "unable to update user status to active", slog.Error(err))
return xerrors.Errorf("update user status: %w", err)
}
}
if link.UserID == uuid.Nil {
//nolint:gocritic
link, err = tx.InsertUserLink(dbauthz.AsSystemRestricted(ctx), database.InsertUserLinkParams{
+76 -5
View File
@@ -1048,6 +1048,35 @@ func TestPutUserSuspend(t *testing.T) {
})
}
func TestActivateDormantUser(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
// Create users
me := coderdtest.CreateFirstUser(t, client)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
anotherUser, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "coder@coder.com",
Username: "coder",
Password: "SomeStrongPassword!",
OrganizationID: me.OrganizationID,
})
require.NoError(t, err)
// Ensure that new user has dormant account
require.Equal(t, codersdk.UserStatusDormant, anotherUser.Status)
// Activate user account
_, err = client.UpdateUserStatus(ctx, anotherUser.Username, codersdk.UserStatusActive)
require.NoError(t, err)
// Verify if the account is active now
anotherUser, err = client.User(ctx, anotherUser.Username)
require.NoError(t, err)
require.Equal(t, codersdk.UserStatusActive, anotherUser.Status)
}
func TestGetUser(t *testing.T) {
t.Parallel()
@@ -1368,17 +1397,21 @@ func TestGetUsers(t *testing.T) {
})
require.NoError(t, err)
bruno, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "bruno@email.com",
Username: "bruno",
_, err = client.UpdateUserStatus(ctx, alice.Username, codersdk.UserStatusSuspended)
require.NoError(t, err)
// Tom will be active
tom, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "tom@email.com",
Username: "tom",
Password: "MySecurePassword!",
OrganizationID: first.OrganizationID,
})
require.NoError(t, err)
active = append(active, bruno)
_, err = client.UpdateUserStatus(ctx, alice.Username, codersdk.UserStatusSuspended)
tom, err = client.UpdateUserStatus(ctx, tom.Username, codersdk.UserStatusActive)
require.NoError(t, err)
active = append(active, tom)
res, err := client.Users(ctx, codersdk.UsersRequest{
Status: codersdk.UserStatusActive,
@@ -1510,6 +1543,44 @@ func TestWorkspacesByUser(t *testing.T) {
})
}
func TestDormantUser(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
// Create a new user
newUser, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "test@coder.com",
Username: "someone",
Password: "MySecurePassword!",
OrganizationID: user.OrganizationID,
})
require.NoError(t, err)
// User should be dormant as they haven't logged in yet
users, err := client.Users(ctx, codersdk.UsersRequest{Search: newUser.Username})
require.NoError(t, err)
require.Len(t, users.Users, 1)
require.Equal(t, codersdk.UserStatusDormant, users.Users[0].Status)
// User logs in now
_, err = client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
Email: newUser.Email,
Password: "MySecurePassword!",
})
require.NoError(t, err)
// User status should be active now
users, err = client.Users(ctx, codersdk.UsersRequest{Search: newUser.Username})
require.NoError(t, err)
require.Len(t, users.Users, 1)
require.Equal(t, codersdk.UserStatusActive, users.Users[0].Status)
}
// TestSuspendedPagination is when the after_id is a suspended record.
// The database query should still return the correct page, as the after_id
// is in a subquery that finds the record regardless of its status.