2022-01-20 13:46:51 +00:00
|
|
|
package coderd_test
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2022-05-27 20:47:03 +00:00
|
|
|
"database/sql"
|
2022-04-22 20:27:55 +00:00
|
|
|
"fmt"
|
2022-01-25 01:09:39 +00:00
|
|
|
"net/http"
|
2022-04-25 15:27:08 +00:00
|
|
|
"sort"
|
2022-05-27 20:47:03 +00:00
|
|
|
"strings"
|
2022-01-20 13:46:51 +00:00
|
|
|
"testing"
|
2022-06-01 19:58:55 +00:00
|
|
|
"time"
|
2022-01-20 13:46:51 +00:00
|
|
|
|
2022-03-07 17:40:54 +00:00
|
|
|
"github.com/google/uuid"
|
2022-01-20 16:00:13 +00:00
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
|
2022-01-20 13:46:51 +00:00
|
|
|
"github.com/coder/coder/coderd/coderdtest"
|
2022-06-01 19:58:55 +00:00
|
|
|
"github.com/coder/coder/coderd/database"
|
2022-05-27 20:47:03 +00:00
|
|
|
"github.com/coder/coder/coderd/database/databasefake"
|
2022-03-25 21:07:45 +00:00
|
|
|
"github.com/coder/coder/coderd/httpmw"
|
2022-04-29 14:04:19 +00:00
|
|
|
"github.com/coder/coder/coderd/rbac"
|
2022-02-06 00:24:51 +00:00
|
|
|
"github.com/coder/coder/codersdk"
|
2022-01-20 13:46:51 +00:00
|
|
|
)
|
|
|
|
|
2022-03-07 17:40:54 +00:00
|
|
|
func TestFirstUser(t *testing.T) {
|
2022-02-10 14:33:27 +00:00
|
|
|
t.Parallel()
|
2022-03-07 17:40:54 +00:00
|
|
|
t.Run("BadRequest", func(t *testing.T) {
|
2022-02-10 14:33:27 +00:00
|
|
|
t.Parallel()
|
2022-02-21 20:36:29 +00:00
|
|
|
client := coderdtest.New(t, nil)
|
2022-03-22 19:17:50 +00:00
|
|
|
_, err := client.CreateFirstUser(context.Background(), codersdk.CreateFirstUserRequest{})
|
2022-03-07 17:40:54 +00:00
|
|
|
require.Error(t, err)
|
2022-02-10 14:33:27 +00:00
|
|
|
})
|
|
|
|
|
2022-03-07 17:40:54 +00:00
|
|
|
t.Run("AlreadyExists", func(t *testing.T) {
|
2022-02-10 14:33:27 +00:00
|
|
|
t.Parallel()
|
2022-02-21 20:36:29 +00:00
|
|
|
client := coderdtest.New(t, nil)
|
2022-03-07 17:40:54 +00:00
|
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
2022-03-22 19:17:50 +00:00
|
|
|
_, err := client.CreateFirstUser(context.Background(), codersdk.CreateFirstUserRequest{
|
2022-04-01 19:42:36 +00:00
|
|
|
Email: "some@email.com",
|
|
|
|
Username: "exampleuser",
|
|
|
|
Password: "password",
|
|
|
|
OrganizationName: "someorg",
|
2022-03-07 17:40:54 +00:00
|
|
|
})
|
|
|
|
var apiErr *codersdk.Error
|
|
|
|
require.ErrorAs(t, err, &apiErr)
|
|
|
|
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("Create", func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
2022-02-10 14:33:27 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-03-07 17:40:54 +00:00
|
|
|
func TestPostLogin(t *testing.T) {
|
2022-01-20 13:46:51 +00:00
|
|
|
t.Parallel()
|
2022-03-07 17:40:54 +00:00
|
|
|
t.Run("InvalidUser", func(t *testing.T) {
|
2022-01-20 13:46:51 +00:00
|
|
|
t.Parallel()
|
2022-02-21 20:36:29 +00:00
|
|
|
client := coderdtest.New(t, nil)
|
2022-03-22 19:17:50 +00:00
|
|
|
_, err := client.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{
|
2022-03-07 17:40:54 +00:00
|
|
|
Email: "my@email.org",
|
|
|
|
Password: "password",
|
|
|
|
})
|
|
|
|
var apiErr *codersdk.Error
|
|
|
|
require.ErrorAs(t, err, &apiErr)
|
|
|
|
require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
|
2022-01-20 13:46:51 +00:00
|
|
|
})
|
|
|
|
|
2022-03-07 17:40:54 +00:00
|
|
|
t.Run("BadPassword", func(t *testing.T) {
|
2022-01-20 13:46:51 +00:00
|
|
|
t.Parallel()
|
2022-02-21 20:36:29 +00:00
|
|
|
client := coderdtest.New(t, nil)
|
2022-03-22 19:17:50 +00:00
|
|
|
req := codersdk.CreateFirstUserRequest{
|
2022-04-01 19:42:36 +00:00
|
|
|
Email: "testuser@coder.com",
|
|
|
|
Username: "testuser",
|
|
|
|
Password: "testpass",
|
|
|
|
OrganizationName: "testorg",
|
2022-03-07 17:40:54 +00:00
|
|
|
}
|
|
|
|
_, err := client.CreateFirstUser(context.Background(), req)
|
|
|
|
require.NoError(t, err)
|
2022-03-22 19:17:50 +00:00
|
|
|
_, err = client.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{
|
2022-03-07 17:40:54 +00:00
|
|
|
Email: req.Email,
|
|
|
|
Password: "badpass",
|
2022-01-20 13:46:51 +00:00
|
|
|
})
|
2022-02-06 00:24:51 +00:00
|
|
|
var apiErr *codersdk.Error
|
|
|
|
require.ErrorAs(t, err, &apiErr)
|
2022-03-07 17:40:54 +00:00
|
|
|
require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
|
2022-01-20 13:46:51 +00:00
|
|
|
})
|
|
|
|
|
2022-05-31 13:06:42 +00:00
|
|
|
t.Run("Suspended", func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
|
|
first := coderdtest.CreateFirstUser(t, client)
|
|
|
|
|
|
|
|
member := coderdtest.CreateAnotherUser(t, client, first.OrganizationID)
|
|
|
|
memberUser, err := member.User(context.Background(), codersdk.Me)
|
|
|
|
require.NoError(t, err, "fetch member user")
|
|
|
|
|
|
|
|
_, err = client.UpdateUserStatus(context.Background(), memberUser.Username, codersdk.UserStatusSuspended)
|
|
|
|
require.NoError(t, err, "suspend member")
|
|
|
|
|
|
|
|
// Test an existing session
|
|
|
|
_, err = member.User(context.Background(), codersdk.Me)
|
|
|
|
var apiErr *codersdk.Error
|
|
|
|
require.ErrorAs(t, err, &apiErr)
|
|
|
|
require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
|
2022-06-03 21:48:09 +00:00
|
|
|
require.Contains(t, apiErr.Message, "Contact an admin")
|
2022-05-31 13:06:42 +00:00
|
|
|
|
|
|
|
// Test a new session
|
|
|
|
_, err = client.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{
|
|
|
|
Email: memberUser.Email,
|
|
|
|
Password: "testpass",
|
|
|
|
})
|
|
|
|
require.ErrorAs(t, err, &apiErr)
|
|
|
|
require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
|
|
|
|
require.Contains(t, apiErr.Message, "suspended")
|
|
|
|
})
|
|
|
|
|
2022-03-07 17:40:54 +00:00
|
|
|
t.Run("Success", func(t *testing.T) {
|
2022-01-24 17:07:42 +00:00
|
|
|
t.Parallel()
|
2022-02-21 20:36:29 +00:00
|
|
|
client := coderdtest.New(t, nil)
|
2022-03-22 19:17:50 +00:00
|
|
|
req := codersdk.CreateFirstUserRequest{
|
2022-04-01 19:42:36 +00:00
|
|
|
Email: "testuser@coder.com",
|
|
|
|
Username: "testuser",
|
|
|
|
Password: "testpass",
|
|
|
|
OrganizationName: "testorg",
|
2022-03-07 17:40:54 +00:00
|
|
|
}
|
|
|
|
_, err := client.CreateFirstUser(context.Background(), req)
|
|
|
|
require.NoError(t, err)
|
2022-03-22 19:17:50 +00:00
|
|
|
_, err = client.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{
|
2022-03-07 17:40:54 +00:00
|
|
|
Email: req.Email,
|
|
|
|
Password: req.Password,
|
|
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
})
|
2022-06-01 19:58:55 +00:00
|
|
|
|
|
|
|
t.Run("Lifetime&Expire", func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
var (
|
|
|
|
ctx = context.Background()
|
|
|
|
)
|
|
|
|
client, api := coderdtest.NewWithAPI(t, nil)
|
|
|
|
admin := coderdtest.CreateFirstUser(t, client)
|
|
|
|
|
|
|
|
split := strings.Split(client.SessionToken, "-")
|
|
|
|
loginKey, err := api.Database.GetAPIKeyByID(ctx, split[0])
|
|
|
|
require.NoError(t, err, "fetch login key")
|
|
|
|
require.Equal(t, int64(86400), loginKey.LifetimeSeconds, "default should be 86400")
|
|
|
|
|
|
|
|
// Generated tokens have a longer life
|
|
|
|
token, err := client.CreateAPIKey(ctx, admin.UserID.String())
|
|
|
|
require.NoError(t, err, "make new api key")
|
|
|
|
split = strings.Split(token.Key, "-")
|
|
|
|
apiKey, err := api.Database.GetAPIKeyByID(ctx, split[0])
|
|
|
|
require.NoError(t, err, "fetch api key")
|
|
|
|
|
|
|
|
require.True(t, apiKey.ExpiresAt.After(time.Now().Add(time.Hour*24*6)), "api key lasts more than 6 days")
|
|
|
|
require.True(t, apiKey.ExpiresAt.After(loginKey.ExpiresAt.Add(time.Hour)), "api key should be longer expires")
|
|
|
|
require.Greater(t, apiKey.LifetimeSeconds, loginKey.LifetimeSeconds, "api key should have longer lifetime")
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("APIKeyExtend", func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
var (
|
|
|
|
ctx = context.Background()
|
|
|
|
)
|
|
|
|
client, api := coderdtest.NewWithAPI(t, nil)
|
|
|
|
admin := coderdtest.CreateFirstUser(t, client)
|
|
|
|
|
|
|
|
token, err := client.CreateAPIKey(ctx, admin.UserID.String())
|
|
|
|
require.NoError(t, err, "make new api key")
|
|
|
|
client.SessionToken = token.Key
|
|
|
|
split := strings.Split(token.Key, "-")
|
|
|
|
|
|
|
|
apiKey, err := api.Database.GetAPIKeyByID(ctx, split[0])
|
|
|
|
require.NoError(t, err, "fetch api key")
|
|
|
|
|
|
|
|
err = api.Database.UpdateAPIKeyByID(ctx, database.UpdateAPIKeyByIDParams{
|
2022-06-22 17:32:21 +00:00
|
|
|
ID: apiKey.ID,
|
|
|
|
LastUsed: apiKey.LastUsed,
|
|
|
|
IPAddress: apiKey.IPAddress,
|
2022-06-01 19:58:55 +00:00
|
|
|
// This should cause a refresh
|
|
|
|
ExpiresAt: apiKey.ExpiresAt.Add(time.Hour * -2),
|
|
|
|
OAuthAccessToken: apiKey.OAuthAccessToken,
|
|
|
|
OAuthRefreshToken: apiKey.OAuthRefreshToken,
|
|
|
|
OAuthExpiry: apiKey.OAuthExpiry,
|
|
|
|
})
|
|
|
|
require.NoError(t, err, "update api key")
|
|
|
|
|
|
|
|
_, err = client.User(ctx, codersdk.Me)
|
|
|
|
require.NoError(t, err, "fetch user")
|
|
|
|
|
|
|
|
apiKey, err = api.Database.GetAPIKeyByID(ctx, split[0])
|
|
|
|
require.NoError(t, err, "fetch refreshed api key")
|
|
|
|
// 1 minute tolerance
|
|
|
|
require.True(t, apiKey.ExpiresAt.After(time.Now().Add(time.Hour*24*7).Add(time.Minute*-1)), "api key lasts 7 days")
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("LoginKeyExtend", func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
var (
|
|
|
|
ctx = context.Background()
|
|
|
|
)
|
|
|
|
client, api := coderdtest.NewWithAPI(t, nil)
|
|
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
|
|
split := strings.Split(client.SessionToken, "-")
|
|
|
|
|
|
|
|
apiKey, err := api.Database.GetAPIKeyByID(ctx, split[0])
|
|
|
|
require.NoError(t, err, "fetch login key")
|
|
|
|
|
|
|
|
err = api.Database.UpdateAPIKeyByID(ctx, database.UpdateAPIKeyByIDParams{
|
2022-06-22 17:32:21 +00:00
|
|
|
ID: apiKey.ID,
|
|
|
|
LastUsed: apiKey.LastUsed,
|
|
|
|
IPAddress: apiKey.IPAddress,
|
2022-06-01 19:58:55 +00:00
|
|
|
// This should cause a refresh
|
|
|
|
ExpiresAt: apiKey.ExpiresAt.Add(time.Hour * -2),
|
|
|
|
OAuthAccessToken: apiKey.OAuthAccessToken,
|
|
|
|
OAuthRefreshToken: apiKey.OAuthRefreshToken,
|
|
|
|
OAuthExpiry: apiKey.OAuthExpiry,
|
|
|
|
})
|
|
|
|
require.NoError(t, err, "update login key")
|
|
|
|
|
|
|
|
_, err = client.User(ctx, codersdk.Me)
|
|
|
|
require.NoError(t, err, "fetch user")
|
|
|
|
|
|
|
|
apiKey, err = api.Database.GetAPIKeyByID(ctx, split[0])
|
|
|
|
require.NoError(t, err, "fetch refreshed login key")
|
|
|
|
// 1 minute tolerance
|
|
|
|
require.True(t, apiKey.ExpiresAt.After(time.Now().Add(time.Hour*24).Add(time.Minute*-1)), "login key lasts 24 hrs")
|
|
|
|
})
|
2022-03-07 17:40:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestPostLogout(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
2022-05-27 20:47:03 +00:00
|
|
|
// Checks that the cookie is cleared and the API Key is deleted from the database.
|
|
|
|
t.Run("Logout", func(t *testing.T) {
|
2022-03-07 17:40:54 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2022-05-27 20:47:03 +00:00
|
|
|
ctx := context.Background()
|
|
|
|
client, api := coderdtest.NewWithAPI(t, nil)
|
|
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
|
|
keyID := strings.Split(client.SessionToken, "-")[0]
|
|
|
|
|
|
|
|
apiKey, err := api.Database.GetAPIKeyByID(ctx, keyID)
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, keyID, apiKey.ID, "API key should exist in the database")
|
|
|
|
|
2022-03-07 17:40:54 +00:00
|
|
|
fullURL, err := client.URL.Parse("/api/v2/users/logout")
|
|
|
|
require.NoError(t, err, "Server URL should parse successfully")
|
|
|
|
|
2022-05-27 20:47:03 +00:00
|
|
|
res, err := client.Request(ctx, http.MethodPost, fullURL.String(), nil)
|
|
|
|
require.NoError(t, err, "/logout request should succeed")
|
|
|
|
res.Body.Close()
|
|
|
|
require.Equal(t, http.StatusOK, res.StatusCode)
|
2022-03-07 17:40:54 +00:00
|
|
|
|
2022-05-27 20:47:03 +00:00
|
|
|
cookies := res.Cookies()
|
|
|
|
require.Len(t, cookies, 1, "Exactly one cookie should be returned")
|
|
|
|
|
|
|
|
require.Equal(t, httpmw.SessionTokenKey, cookies[0].Name, "Cookie should be the auth cookie")
|
|
|
|
require.Equal(t, -1, cookies[0].MaxAge, "Cookie should be set to delete")
|
2022-03-07 17:40:54 +00:00
|
|
|
|
2022-05-27 20:47:03 +00:00
|
|
|
apiKey, err = api.Database.GetAPIKeyByID(ctx, keyID)
|
|
|
|
require.ErrorIs(t, err, sql.ErrNoRows, "API key should not exist in the database")
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("LogoutWithoutKey", func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
client, api := coderdtest.NewWithAPI(t, nil)
|
|
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
|
|
keyID := strings.Split(client.SessionToken, "-")[0]
|
|
|
|
|
|
|
|
apiKey, err := api.Database.GetAPIKeyByID(ctx, keyID)
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, keyID, apiKey.ID, "API key should exist in the database")
|
|
|
|
|
|
|
|
// Setting a fake database without the API Key to be used by the API.
|
|
|
|
// The middleware that extracts the API key is already set to read
|
|
|
|
// from the original database.
|
|
|
|
dbWithoutKey := databasefake.New()
|
|
|
|
api.Database = dbWithoutKey
|
|
|
|
|
|
|
|
fullURL, err := client.URL.Parse("/api/v2/users/logout")
|
|
|
|
require.NoError(t, err, "Server URL should parse successfully")
|
|
|
|
|
|
|
|
res, err := client.Request(ctx, http.MethodPost, fullURL.String(), nil)
|
2022-03-07 17:40:54 +00:00
|
|
|
require.NoError(t, err, "/logout request should succeed")
|
2022-05-27 20:47:03 +00:00
|
|
|
res.Body.Close()
|
|
|
|
require.Equal(t, http.StatusInternalServerError, res.StatusCode)
|
2022-03-07 17:40:54 +00:00
|
|
|
|
2022-05-27 20:47:03 +00:00
|
|
|
cookies := res.Cookies()
|
2022-03-07 17:40:54 +00:00
|
|
|
require.Len(t, cookies, 1, "Exactly one cookie should be returned")
|
|
|
|
|
2022-05-27 20:47:03 +00:00
|
|
|
require.Equal(t, httpmw.SessionTokenKey, cookies[0].Name, "Cookie should be the auth cookie")
|
|
|
|
require.Equal(t, -1, cookies[0].MaxAge, "Cookie should be set to delete")
|
|
|
|
|
|
|
|
apiKey, err = api.Database.GetAPIKeyByID(ctx, keyID)
|
|
|
|
require.ErrorIs(t, err, sql.ErrNoRows, "API key should not exist in the database")
|
2022-01-23 05:58:10 +00:00
|
|
|
})
|
2022-02-06 00:24:51 +00:00
|
|
|
}
|
2022-01-23 05:58:10 +00:00
|
|
|
|
2022-02-06 00:24:51 +00:00
|
|
|
func TestPostUsers(t *testing.T) {
|
|
|
|
t.Parallel()
|
2022-03-07 17:40:54 +00:00
|
|
|
t.Run("NoAuth", func(t *testing.T) {
|
2022-01-20 13:46:51 +00:00
|
|
|
t.Parallel()
|
2022-02-21 20:36:29 +00:00
|
|
|
client := coderdtest.New(t, nil)
|
2022-03-22 19:17:50 +00:00
|
|
|
_, err := client.CreateUser(context.Background(), codersdk.CreateUserRequest{})
|
2022-01-20 13:46:51 +00:00
|
|
|
require.Error(t, err)
|
|
|
|
})
|
|
|
|
|
2022-02-06 00:24:51 +00:00
|
|
|
t.Run("Conflicting", func(t *testing.T) {
|
2022-01-20 13:46:51 +00:00
|
|
|
t.Parallel()
|
2022-02-21 20:36:29 +00:00
|
|
|
client := coderdtest.New(t, nil)
|
2022-03-07 17:40:54 +00:00
|
|
|
coderdtest.CreateFirstUser(t, client)
|
2022-04-01 19:42:36 +00:00
|
|
|
me, err := client.User(context.Background(), codersdk.Me)
|
2022-03-07 17:40:54 +00:00
|
|
|
require.NoError(t, err)
|
2022-03-22 19:17:50 +00:00
|
|
|
_, err = client.CreateUser(context.Background(), codersdk.CreateUserRequest{
|
2022-03-07 17:40:54 +00:00
|
|
|
Email: me.Email,
|
|
|
|
Username: me.Username,
|
|
|
|
Password: "password",
|
2022-04-01 19:42:36 +00:00
|
|
|
OrganizationID: uuid.New(),
|
2022-01-20 13:46:51 +00:00
|
|
|
})
|
2022-02-06 00:24:51 +00:00
|
|
|
var apiErr *codersdk.Error
|
|
|
|
require.ErrorAs(t, err, &apiErr)
|
|
|
|
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
|
2022-01-20 13:46:51 +00:00
|
|
|
})
|
2022-01-23 05:58:10 +00:00
|
|
|
|
2022-03-07 17:40:54 +00:00
|
|
|
t.Run("OrganizationNotFound", func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
|
|
coderdtest.CreateFirstUser(t, client)
|
2022-03-22 19:17:50 +00:00
|
|
|
_, err := client.CreateUser(context.Background(), codersdk.CreateUserRequest{
|
2022-04-01 19:42:36 +00:00
|
|
|
OrganizationID: uuid.New(),
|
2022-03-07 17:40:54 +00:00
|
|
|
Email: "another@user.org",
|
|
|
|
Username: "someone-else",
|
|
|
|
Password: "testing",
|
|
|
|
})
|
|
|
|
var apiErr *codersdk.Error
|
|
|
|
require.ErrorAs(t, err, &apiErr)
|
|
|
|
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("OrganizationNoAccess", func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
|
|
first := coderdtest.CreateFirstUser(t, client)
|
2022-05-17 18:43:19 +00:00
|
|
|
notInOrg := coderdtest.CreateAnotherUser(t, client, first.OrganizationID)
|
2022-05-18 23:15:19 +00:00
|
|
|
other := coderdtest.CreateAnotherUser(t, client, first.OrganizationID, rbac.RoleAdmin(), rbac.RoleMember())
|
2022-05-27 16:19:13 +00:00
|
|
|
org, err := other.CreateOrganization(context.Background(), codersdk.CreateOrganizationRequest{
|
2022-03-07 17:40:54 +00:00
|
|
|
Name: "another",
|
|
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
2022-05-17 18:43:19 +00:00
|
|
|
_, err = notInOrg.CreateUser(context.Background(), codersdk.CreateUserRequest{
|
2022-03-07 17:40:54 +00:00
|
|
|
Email: "some@domain.com",
|
|
|
|
Username: "anotheruser",
|
|
|
|
Password: "testing",
|
|
|
|
OrganizationID: org.ID,
|
|
|
|
})
|
|
|
|
var apiErr *codersdk.Error
|
|
|
|
require.ErrorAs(t, err, &apiErr)
|
2022-05-17 18:43:19 +00:00
|
|
|
require.Equal(t, http.StatusForbidden, apiErr.StatusCode())
|
2022-03-07 17:40:54 +00:00
|
|
|
})
|
|
|
|
|
2022-02-06 00:24:51 +00:00
|
|
|
t.Run("Create", func(t *testing.T) {
|
2022-01-23 05:58:10 +00:00
|
|
|
t.Parallel()
|
2022-02-21 20:36:29 +00:00
|
|
|
client := coderdtest.New(t, nil)
|
2022-03-07 17:40:54 +00:00
|
|
|
user := coderdtest.CreateFirstUser(t, client)
|
2022-03-22 19:17:50 +00:00
|
|
|
_, err := client.CreateUser(context.Background(), codersdk.CreateUserRequest{
|
2022-03-07 17:40:54 +00:00
|
|
|
OrganizationID: user.OrganizationID,
|
|
|
|
Email: "another@user.org",
|
|
|
|
Username: "someone-else",
|
|
|
|
Password: "testing",
|
2022-02-06 00:24:51 +00:00
|
|
|
})
|
2022-01-23 05:58:10 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
})
|
2022-02-06 00:24:51 +00:00
|
|
|
}
|
2022-01-25 19:52:58 +00:00
|
|
|
|
2022-04-12 14:05:21 +00:00
|
|
|
func TestUpdateUserProfile(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
t.Run("UserNotFound", func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
|
|
coderdtest.CreateFirstUser(t, client)
|
2022-05-16 20:29:27 +00:00
|
|
|
_, err := client.UpdateUserProfile(context.Background(), uuid.New().String(), codersdk.UpdateUserProfileRequest{
|
2022-04-12 14:05:21 +00:00
|
|
|
Username: "newusername",
|
|
|
|
})
|
|
|
|
var apiErr *codersdk.Error
|
|
|
|
require.ErrorAs(t, err, &apiErr)
|
|
|
|
// Right now, we are raising a BAD request error because we don't support a
|
|
|
|
// user accessing other users info
|
|
|
|
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("ConflictingUsername", func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
|
|
user := coderdtest.CreateFirstUser(t, client)
|
2022-04-23 22:58:57 +00:00
|
|
|
existentUser, err := client.CreateUser(context.Background(), codersdk.CreateUserRequest{
|
2022-04-12 14:05:21 +00:00
|
|
|
Email: "bruno@coder.com",
|
|
|
|
Username: "bruno",
|
|
|
|
Password: "password",
|
|
|
|
OrganizationID: user.OrganizationID,
|
|
|
|
})
|
2022-04-23 22:58:57 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
_, err = client.UpdateUserProfile(context.Background(), codersdk.Me, codersdk.UpdateUserProfileRequest{
|
2022-04-12 14:05:21 +00:00
|
|
|
Username: existentUser.Username,
|
|
|
|
})
|
|
|
|
var apiErr *codersdk.Error
|
|
|
|
require.ErrorAs(t, err, &apiErr)
|
|
|
|
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
|
|
|
|
})
|
|
|
|
|
2022-05-27 22:25:04 +00:00
|
|
|
t.Run("UpdateUsername", func(t *testing.T) {
|
2022-04-12 14:05:21 +00:00
|
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
|
|
coderdtest.CreateFirstUser(t, client)
|
2022-05-27 22:25:04 +00:00
|
|
|
_, _ = client.User(context.Background(), codersdk.Me)
|
2022-04-12 14:05:21 +00:00
|
|
|
userProfile, err := client.UpdateUserProfile(context.Background(), codersdk.Me, codersdk.UpdateUserProfileRequest{
|
|
|
|
Username: "newusername",
|
|
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, userProfile.Username, "newusername")
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-05-06 14:20:08 +00:00
|
|
|
func TestUpdateUserPassword(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
t.Run("MemberCantUpdateAdminPassword", func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
|
|
admin := coderdtest.CreateFirstUser(t, client)
|
|
|
|
member := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
|
2022-05-16 20:29:27 +00:00
|
|
|
err := member.UpdateUserPassword(context.Background(), admin.UserID.String(), codersdk.UpdateUserPasswordRequest{
|
2022-05-06 14:20:08 +00:00
|
|
|
Password: "newpassword",
|
|
|
|
})
|
|
|
|
require.Error(t, err, "member should not be able to update admin password")
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("AdminCanUpdateMemberPassword", func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
|
|
admin := coderdtest.CreateFirstUser(t, client)
|
|
|
|
member, err := client.CreateUser(context.Background(), codersdk.CreateUserRequest{
|
|
|
|
Email: "coder@coder.com",
|
|
|
|
Username: "coder",
|
|
|
|
Password: "password",
|
|
|
|
OrganizationID: admin.OrganizationID,
|
|
|
|
})
|
|
|
|
require.NoError(t, err, "create member")
|
2022-05-16 20:29:27 +00:00
|
|
|
err = client.UpdateUserPassword(context.Background(), member.ID.String(), codersdk.UpdateUserPasswordRequest{
|
2022-05-06 14:20:08 +00:00
|
|
|
Password: "newpassword",
|
|
|
|
})
|
|
|
|
require.NoError(t, err, "admin should be able to update member password")
|
|
|
|
// Check if the member can login using the new password
|
|
|
|
_, err = client.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{
|
|
|
|
Email: "coder@coder.com",
|
|
|
|
Password: "newpassword",
|
|
|
|
})
|
|
|
|
require.NoError(t, err, "member should login successfully with the new password")
|
|
|
|
})
|
2022-05-27 17:29:55 +00:00
|
|
|
t.Run("MemberCanUpdateOwnPassword", func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
|
|
admin := coderdtest.CreateFirstUser(t, client)
|
|
|
|
member := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
|
|
|
|
err := member.UpdateUserPassword(context.Background(), "me", codersdk.UpdateUserPasswordRequest{
|
|
|
|
OldPassword: "testpass",
|
|
|
|
Password: "newpassword",
|
|
|
|
})
|
|
|
|
require.NoError(t, err, "member should be able to update own password")
|
|
|
|
})
|
|
|
|
t.Run("MemberCantUpdateOwnPasswordWithoutOldPassword", func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
|
|
admin := coderdtest.CreateFirstUser(t, client)
|
|
|
|
member := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
|
|
|
|
err := member.UpdateUserPassword(context.Background(), "me", codersdk.UpdateUserPasswordRequest{
|
|
|
|
Password: "newpassword",
|
|
|
|
})
|
|
|
|
require.Error(t, err, "member should not be able to update own password without providing old password")
|
|
|
|
})
|
2022-06-10 01:43:54 +00:00
|
|
|
t.Run("AdminCanUpdateOwnPasswordWithoutOldPassword", func(t *testing.T) {
|
2022-05-27 17:29:55 +00:00
|
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
|
|
err := client.UpdateUserPassword(context.Background(), "me", codersdk.UpdateUserPasswordRequest{
|
|
|
|
Password: "newpassword",
|
|
|
|
})
|
2022-06-10 01:43:54 +00:00
|
|
|
require.NoError(t, err, "admin should be able to update own password without providing old password")
|
2022-05-27 17:29:55 +00:00
|
|
|
})
|
2022-05-06 14:20:08 +00:00
|
|
|
}
|
|
|
|
|
2022-04-29 14:04:19 +00:00
|
|
|
func TestGrantRoles(t *testing.T) {
|
|
|
|
t.Parallel()
|
2022-05-25 16:00:59 +00:00
|
|
|
|
|
|
|
requireStatusCode := func(t *testing.T, err error, statusCode int) {
|
|
|
|
t.Helper()
|
|
|
|
var e *codersdk.Error
|
|
|
|
require.ErrorAs(t, err, &e, "error is codersdk error")
|
|
|
|
require.Equal(t, statusCode, e.StatusCode(), "correct status code")
|
|
|
|
}
|
|
|
|
|
2022-04-29 14:04:19 +00:00
|
|
|
t.Run("UpdateIncorrectRoles", func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
2022-05-31 20:50:38 +00:00
|
|
|
var err error
|
|
|
|
|
2022-04-29 14:04:19 +00:00
|
|
|
admin := coderdtest.New(t, nil)
|
|
|
|
first := coderdtest.CreateFirstUser(t, admin)
|
|
|
|
member := coderdtest.CreateAnotherUser(t, admin, first.OrganizationID)
|
|
|
|
|
2022-05-25 16:00:59 +00:00
|
|
|
_, err = admin.UpdateUserRoles(ctx, codersdk.Me, codersdk.UpdateRoles{
|
2022-06-01 14:07:50 +00:00
|
|
|
Roles: []string{rbac.RoleOrgAdmin(first.OrganizationID)},
|
2022-04-29 14:04:19 +00:00
|
|
|
})
|
|
|
|
require.Error(t, err, "org role in site")
|
2022-05-25 16:00:59 +00:00
|
|
|
requireStatusCode(t, err, http.StatusBadRequest)
|
2022-04-29 14:04:19 +00:00
|
|
|
|
2022-05-16 20:29:27 +00:00
|
|
|
_, err = admin.UpdateUserRoles(ctx, uuid.New().String(), codersdk.UpdateRoles{
|
2022-06-01 14:07:50 +00:00
|
|
|
Roles: []string{rbac.RoleOrgAdmin(first.OrganizationID)},
|
2022-04-29 14:04:19 +00:00
|
|
|
})
|
|
|
|
require.Error(t, err, "user does not exist")
|
2022-05-25 16:00:59 +00:00
|
|
|
requireStatusCode(t, err, http.StatusBadRequest)
|
2022-04-29 14:04:19 +00:00
|
|
|
|
|
|
|
_, err = admin.UpdateOrganizationMemberRoles(ctx, first.OrganizationID, codersdk.Me, codersdk.UpdateRoles{
|
2022-06-01 14:07:50 +00:00
|
|
|
Roles: []string{rbac.RoleAdmin()},
|
2022-04-29 14:04:19 +00:00
|
|
|
})
|
|
|
|
require.Error(t, err, "site role in org")
|
2022-05-25 16:00:59 +00:00
|
|
|
requireStatusCode(t, err, http.StatusBadRequest)
|
2022-04-29 14:04:19 +00:00
|
|
|
|
|
|
|
_, err = admin.UpdateOrganizationMemberRoles(ctx, uuid.New(), codersdk.Me, codersdk.UpdateRoles{
|
2022-06-01 14:07:50 +00:00
|
|
|
Roles: []string{},
|
2022-04-29 14:04:19 +00:00
|
|
|
})
|
|
|
|
require.Error(t, err, "role in org without membership")
|
2022-05-25 16:00:59 +00:00
|
|
|
requireStatusCode(t, err, http.StatusNotFound)
|
2022-04-29 14:04:19 +00:00
|
|
|
|
2022-05-16 20:29:27 +00:00
|
|
|
_, err = member.UpdateUserRoles(ctx, first.UserID.String(), codersdk.UpdateRoles{
|
2022-06-01 14:07:50 +00:00
|
|
|
Roles: []string{},
|
2022-04-29 14:04:19 +00:00
|
|
|
})
|
|
|
|
require.Error(t, err, "member cannot change other's roles")
|
2022-05-25 16:00:59 +00:00
|
|
|
requireStatusCode(t, err, http.StatusForbidden)
|
|
|
|
|
2022-05-31 20:50:38 +00:00
|
|
|
_, err = member.UpdateUserRoles(ctx, first.UserID.String(), codersdk.UpdateRoles{
|
2022-06-01 14:07:50 +00:00
|
|
|
Roles: []string{},
|
2022-05-25 16:00:59 +00:00
|
|
|
})
|
|
|
|
require.Error(t, err, "member cannot change any roles")
|
|
|
|
requireStatusCode(t, err, http.StatusForbidden)
|
2022-04-29 14:04:19 +00:00
|
|
|
|
2022-05-16 20:29:27 +00:00
|
|
|
_, err = member.UpdateOrganizationMemberRoles(ctx, first.OrganizationID, first.UserID.String(), codersdk.UpdateRoles{
|
2022-06-01 14:07:50 +00:00
|
|
|
Roles: []string{},
|
2022-04-29 14:04:19 +00:00
|
|
|
})
|
|
|
|
require.Error(t, err, "member cannot change other's org roles")
|
2022-05-25 16:00:59 +00:00
|
|
|
requireStatusCode(t, err, http.StatusForbidden)
|
2022-05-31 20:50:38 +00:00
|
|
|
|
|
|
|
_, err = admin.UpdateUserRoles(ctx, first.UserID.String(), codersdk.UpdateRoles{
|
|
|
|
Roles: []string{},
|
|
|
|
})
|
|
|
|
require.Error(t, err, "admin cannot change self roles")
|
|
|
|
requireStatusCode(t, err, http.StatusBadRequest)
|
|
|
|
|
|
|
|
_, err = admin.UpdateOrganizationMemberRoles(ctx, first.OrganizationID, first.UserID.String(), codersdk.UpdateRoles{
|
|
|
|
Roles: []string{},
|
|
|
|
})
|
|
|
|
require.Error(t, err, "admin cannot change self org roles")
|
|
|
|
requireStatusCode(t, err, http.StatusBadRequest)
|
2022-04-29 14:04:19 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("FirstUserRoles", func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
|
|
first := coderdtest.CreateFirstUser(t, client)
|
|
|
|
|
|
|
|
roles, err := client.GetUserRoles(ctx, codersdk.Me)
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.ElementsMatch(t, roles.Roles, []string{
|
|
|
|
rbac.RoleAdmin(),
|
|
|
|
}, "should be a member and admin")
|
|
|
|
|
|
|
|
require.ElementsMatch(t, roles.OrganizationRoles[first.OrganizationID], []string{
|
|
|
|
rbac.RoleOrgAdmin(first.OrganizationID),
|
|
|
|
}, "should be a member and admin")
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("GrantAdmin", func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
|
|
admin := coderdtest.New(t, nil)
|
|
|
|
first := coderdtest.CreateFirstUser(t, admin)
|
|
|
|
|
|
|
|
member := coderdtest.CreateAnotherUser(t, admin, first.OrganizationID)
|
|
|
|
roles, err := member.GetUserRoles(ctx, codersdk.Me)
|
|
|
|
require.NoError(t, err)
|
2022-06-01 14:07:50 +00:00
|
|
|
require.ElementsMatch(t, roles.Roles, []string{}, "should be a member")
|
2022-04-29 14:04:19 +00:00
|
|
|
require.ElementsMatch(t,
|
|
|
|
roles.OrganizationRoles[first.OrganizationID],
|
2022-06-01 14:07:50 +00:00
|
|
|
[]string{},
|
2022-04-29 14:04:19 +00:00
|
|
|
)
|
|
|
|
|
2022-05-17 18:43:19 +00:00
|
|
|
memberUser, err := member.User(ctx, codersdk.Me)
|
|
|
|
require.NoError(t, err, "fetch member")
|
|
|
|
|
2022-04-29 14:04:19 +00:00
|
|
|
// Grant
|
2022-05-17 18:43:19 +00:00
|
|
|
_, err = admin.UpdateUserRoles(ctx, memberUser.ID.String(), codersdk.UpdateRoles{
|
2022-04-29 14:04:19 +00:00
|
|
|
Roles: []string{
|
|
|
|
// Promote to site admin
|
|
|
|
rbac.RoleAdmin(),
|
|
|
|
},
|
|
|
|
})
|
|
|
|
require.NoError(t, err, "grant member admin role")
|
|
|
|
|
|
|
|
// Promote to org admin
|
2022-05-31 20:50:38 +00:00
|
|
|
_, err = admin.UpdateOrganizationMemberRoles(ctx, first.OrganizationID, memberUser.ID.String(), codersdk.UpdateRoles{
|
2022-04-29 14:04:19 +00:00
|
|
|
Roles: []string{
|
|
|
|
// Promote to org admin
|
|
|
|
rbac.RoleOrgAdmin(first.OrganizationID),
|
|
|
|
},
|
|
|
|
})
|
|
|
|
require.NoError(t, err, "grant member org admin role")
|
|
|
|
|
|
|
|
roles, err = member.GetUserRoles(ctx, codersdk.Me)
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.ElementsMatch(t, roles.Roles, []string{
|
|
|
|
rbac.RoleAdmin(),
|
|
|
|
}, "should be a member and admin")
|
|
|
|
|
|
|
|
require.ElementsMatch(t, roles.OrganizationRoles[first.OrganizationID], []string{
|
|
|
|
rbac.RoleOrgAdmin(first.OrganizationID),
|
|
|
|
}, "should be a member and admin")
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-04-26 14:00:07 +00:00
|
|
|
func TestPutUserSuspend(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
t.Run("SuspendAnotherUser", func(t *testing.T) {
|
|
|
|
t.Skip()
|
|
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
|
|
me := coderdtest.CreateFirstUser(t, client)
|
|
|
|
client.User(context.Background(), codersdk.Me)
|
|
|
|
user, _ := client.CreateUser(context.Background(), codersdk.CreateUserRequest{
|
|
|
|
Email: "bruno@coder.com",
|
|
|
|
Username: "bruno",
|
|
|
|
Password: "password",
|
|
|
|
OrganizationID: me.OrganizationID,
|
|
|
|
})
|
2022-05-16 20:29:27 +00:00
|
|
|
user, err := client.UpdateUserStatus(context.Background(), user.Username, codersdk.UserStatusSuspended)
|
2022-04-26 14:00:07 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, user.Status, codersdk.UserStatusSuspended)
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("SuspendItSelf", func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
|
|
coderdtest.CreateFirstUser(t, client)
|
|
|
|
client.User(context.Background(), codersdk.Me)
|
2022-05-16 20:29:27 +00:00
|
|
|
_, err := client.UpdateUserStatus(context.Background(), codersdk.Me, codersdk.UserStatusSuspended)
|
2022-04-26 14:00:07 +00:00
|
|
|
|
2022-05-16 20:29:27 +00:00
|
|
|
require.ErrorContains(t, err, "suspend yourself", "cannot suspend yourself")
|
2022-04-26 14:00:07 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-04-29 16:44:22 +00:00
|
|
|
func TestGetUser(t *testing.T) {
|
2022-02-06 00:24:51 +00:00
|
|
|
t.Parallel()
|
2022-04-28 14:10:17 +00:00
|
|
|
|
2022-04-29 16:44:22 +00:00
|
|
|
t.Run("ByMe", func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
client := coderdtest.New(t, nil)
|
|
|
|
firstUser := coderdtest.CreateFirstUser(t, client)
|
|
|
|
|
|
|
|
user, err := client.User(context.Background(), codersdk.Me)
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, firstUser.UserID, user.ID)
|
|
|
|
require.Equal(t, firstUser.OrganizationID, user.OrganizationIDs[0])
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("ByID", func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
client := coderdtest.New(t, nil)
|
|
|
|
firstUser := coderdtest.CreateFirstUser(t, client)
|
|
|
|
|
2022-05-16 20:29:27 +00:00
|
|
|
user, err := client.User(context.Background(), firstUser.UserID.String())
|
2022-04-29 16:44:22 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, firstUser.UserID, user.ID)
|
|
|
|
require.Equal(t, firstUser.OrganizationID, user.OrganizationIDs[0])
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("ByUsername", func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
client := coderdtest.New(t, nil)
|
|
|
|
firstUser := coderdtest.CreateFirstUser(t, client)
|
2022-05-16 20:29:27 +00:00
|
|
|
exp, err := client.User(context.Background(), firstUser.UserID.String())
|
2022-04-29 16:44:22 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
2022-05-16 20:29:27 +00:00
|
|
|
user, err := client.User(context.Background(), exp.Username)
|
2022-04-29 16:44:22 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, exp, user)
|
|
|
|
})
|
2022-02-06 00:24:51 +00:00
|
|
|
}
|
|
|
|
|
2022-06-24 15:02:23 +00:00
|
|
|
// TestUsersFilter creates a set of users to run various filters against for testing.
|
|
|
|
func TestUsersFilter(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
|
|
|
|
first := coderdtest.CreateFirstUser(t, client)
|
|
|
|
firstUser, err := client.User(context.Background(), codersdk.Me)
|
|
|
|
require.NoError(t, err, "fetch me")
|
|
|
|
|
|
|
|
users := make([]codersdk.User, 0)
|
|
|
|
users = append(users, firstUser)
|
|
|
|
for i := 0; i < 15; i++ {
|
|
|
|
roles := []string{}
|
|
|
|
if i%2 == 0 {
|
|
|
|
roles = append(roles, rbac.RoleAdmin())
|
|
|
|
}
|
|
|
|
if i%3 == 0 {
|
|
|
|
roles = append(roles, "auditor")
|
|
|
|
}
|
|
|
|
userClient := coderdtest.CreateAnotherUser(t, client, first.OrganizationID, roles...)
|
|
|
|
user, err := userClient.User(context.Background(), codersdk.Me)
|
|
|
|
require.NoError(t, err, "fetch me")
|
|
|
|
|
|
|
|
if i%4 == 0 {
|
|
|
|
user, err = client.UpdateUserStatus(context.Background(), user.ID.String(), codersdk.UserStatusSuspended)
|
|
|
|
require.NoError(t, err, "suspend user")
|
|
|
|
}
|
|
|
|
|
|
|
|
users = append(users, user)
|
|
|
|
}
|
|
|
|
|
|
|
|
// --- 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, u 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: "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.Contains(u.Username, "a") || strings.Contains(u.Email, "a"))
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "Admins",
|
|
|
|
Filter: codersdk.UsersRequest{
|
|
|
|
Role: rbac.RoleAdmin(),
|
|
|
|
Status: codersdk.UserStatusSuspended + "," + codersdk.UserStatusActive,
|
|
|
|
},
|
|
|
|
FilterF: func(_ codersdk.UsersRequest, u codersdk.User) bool {
|
|
|
|
for _, r := range u.Roles {
|
|
|
|
if r.Name == rbac.RoleAdmin() {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "SearchQuery",
|
|
|
|
Filter: codersdk.UsersRequest{
|
|
|
|
SearchQuery: "i role:admin status:active",
|
|
|
|
},
|
|
|
|
FilterF: func(_ codersdk.UsersRequest, u codersdk.User) bool {
|
|
|
|
for _, r := range u.Roles {
|
|
|
|
if r.Name == rbac.RoleAdmin() {
|
|
|
|
return (strings.Contains(u.Username, "i") || strings.Contains(u.Email, "i")) &&
|
|
|
|
u.Status == codersdk.UserStatusActive
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, c := range testCases {
|
|
|
|
c := c
|
|
|
|
t.Run(c.Name, func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
matched, err := client.Users(context.Background(), c.Filter)
|
|
|
|
require.NoError(t, err, "fetch workspaces")
|
|
|
|
|
|
|
|
exp := make([]codersdk.User, 0)
|
|
|
|
for _, made := range users {
|
|
|
|
match := c.FilterF(c.Filter, made)
|
|
|
|
if match {
|
|
|
|
exp = append(exp, made)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
require.ElementsMatch(t, exp, matched, "expected workspaces returned")
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-18 17:19:47 +00:00
|
|
|
func TestGetUsers(t *testing.T) {
|
|
|
|
t.Parallel()
|
2022-04-29 13:29:53 +00:00
|
|
|
t.Run("AllUsers", func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
|
|
client.CreateUser(context.Background(), codersdk.CreateUserRequest{
|
|
|
|
Email: "alice@email.com",
|
|
|
|
Username: "alice",
|
|
|
|
Password: "password",
|
|
|
|
OrganizationID: user.OrganizationID,
|
|
|
|
})
|
|
|
|
// No params is all users
|
|
|
|
users, err := client.Users(context.Background(), codersdk.UsersRequest{})
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, users, 2)
|
|
|
|
require.Len(t, users[0].OrganizationIDs, 1)
|
|
|
|
})
|
|
|
|
t.Run("ActiveUsers", func(t *testing.T) {
|
|
|
|
t.Parallel()
|
2022-05-16 20:29:27 +00:00
|
|
|
active := make([]codersdk.User, 0)
|
2022-04-29 13:29:53 +00:00
|
|
|
client := coderdtest.New(t, nil)
|
|
|
|
first := coderdtest.CreateFirstUser(t, client)
|
2022-05-16 20:29:27 +00:00
|
|
|
|
|
|
|
firstUser, err := client.User(context.Background(), first.UserID.String())
|
|
|
|
require.NoError(t, err, "")
|
|
|
|
active = append(active, firstUser)
|
|
|
|
|
|
|
|
// Alice will be suspended
|
2022-04-29 13:29:53 +00:00
|
|
|
alice, err := client.CreateUser(context.Background(), codersdk.CreateUserRequest{
|
|
|
|
Email: "alice@email.com",
|
|
|
|
Username: "alice",
|
|
|
|
Password: "password",
|
|
|
|
OrganizationID: first.OrganizationID,
|
|
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
bruno, err := client.CreateUser(context.Background(), codersdk.CreateUserRequest{
|
|
|
|
Email: "bruno@email.com",
|
|
|
|
Username: "bruno",
|
|
|
|
Password: "password",
|
|
|
|
OrganizationID: first.OrganizationID,
|
|
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
active = append(active, bruno)
|
|
|
|
|
2022-05-16 20:29:27 +00:00
|
|
|
_, err = client.UpdateUserStatus(context.Background(), alice.Username, codersdk.UserStatusSuspended)
|
2022-04-29 13:29:53 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
users, err := client.Users(context.Background(), codersdk.UsersRequest{
|
2022-06-24 15:02:23 +00:00
|
|
|
Status: codersdk.UserStatusActive,
|
2022-04-29 13:29:53 +00:00
|
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.ElementsMatch(t, active, users)
|
2022-04-18 17:19:47 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-03-07 17:40:54 +00:00
|
|
|
func TestPostAPIKey(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
t.Run("InvalidUser", func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
|
|
|
|
|
|
client.SessionToken = ""
|
2022-04-01 19:42:36 +00:00
|
|
|
_, err := client.CreateAPIKey(context.Background(), codersdk.Me)
|
2022-02-06 00:24:51 +00:00
|
|
|
var apiErr *codersdk.Error
|
|
|
|
require.ErrorAs(t, err, &apiErr)
|
|
|
|
require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
|
2022-01-25 19:52:58 +00:00
|
|
|
})
|
|
|
|
|
2022-02-06 00:24:51 +00:00
|
|
|
t.Run("Success", func(t *testing.T) {
|
2022-01-25 19:52:58 +00:00
|
|
|
t.Parallel()
|
2022-02-21 20:36:29 +00:00
|
|
|
client := coderdtest.New(t, nil)
|
2022-03-07 17:40:54 +00:00
|
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
2022-04-01 19:42:36 +00:00
|
|
|
apiKey, err := client.CreateAPIKey(context.Background(), codersdk.Me)
|
2022-03-07 17:40:54 +00:00
|
|
|
require.NotNil(t, apiKey)
|
|
|
|
require.GreaterOrEqual(t, len(apiKey.Key), 2)
|
2022-02-06 00:24:51 +00:00
|
|
|
require.NoError(t, err)
|
2022-01-25 19:52:58 +00:00
|
|
|
})
|
2022-01-20 13:46:51 +00:00
|
|
|
}
|
2022-01-25 01:09:39 +00:00
|
|
|
|
2022-05-10 02:38:20 +00:00
|
|
|
func TestWorkspacesByUser(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
t.Run("Empty", func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
2022-05-18 15:09:07 +00:00
|
|
|
workspaces, err := client.Workspaces(context.Background(), codersdk.WorkspaceFilter{
|
|
|
|
Owner: codersdk.Me,
|
|
|
|
})
|
2022-05-10 02:38:20 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, workspaces, 0)
|
|
|
|
})
|
|
|
|
t.Run("Access", func(t *testing.T) {
|
|
|
|
t.Parallel()
|
2022-05-19 22:47:45 +00:00
|
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
|
2022-05-10 02:38:20 +00:00
|
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
|
|
newUser, err := client.CreateUser(context.Background(), codersdk.CreateUserRequest{
|
|
|
|
Email: "test@coder.com",
|
|
|
|
Username: "someone",
|
|
|
|
Password: "password",
|
|
|
|
OrganizationID: user.OrganizationID,
|
|
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
auth, err := client.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{
|
|
|
|
Email: newUser.Email,
|
|
|
|
Password: "password",
|
|
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
newUserClient := codersdk.New(client.URL)
|
|
|
|
newUserClient.SessionToken = auth.SessionToken
|
|
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
|
|
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
|
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
|
|
coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
|
|
|
|
2022-05-18 15:09:07 +00:00
|
|
|
workspaces, err := newUserClient.Workspaces(context.Background(), codersdk.WorkspaceFilter{Owner: codersdk.Me})
|
2022-05-10 02:38:20 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, workspaces, 0)
|
|
|
|
|
2022-05-18 15:09:07 +00:00
|
|
|
workspaces, err = client.Workspaces(context.Background(), codersdk.WorkspaceFilter{Owner: codersdk.Me})
|
2022-05-10 02:38:20 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, workspaces, 1)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-06-02 14:01:45 +00:00
|
|
|
// 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.
|
|
|
|
// This is mainly to confirm the db fake has the same behavior.
|
|
|
|
func TestSuspendedPagination(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
|
|
client := coderdtest.New(t, &coderdtest.Options{APIRateLimit: -1})
|
|
|
|
coderdtest.CreateFirstUser(t, client)
|
|
|
|
me, err := client.User(context.Background(), codersdk.Me)
|
|
|
|
require.NoError(t, err)
|
|
|
|
orgID := me.OrganizationIDs[0]
|
|
|
|
|
|
|
|
total := 10
|
|
|
|
users := make([]codersdk.User, 0, total)
|
|
|
|
// Create users
|
|
|
|
for i := 0; i < total; i++ {
|
|
|
|
email := fmt.Sprintf("%d@coder.com", i)
|
|
|
|
username := fmt.Sprintf("user%d", i)
|
|
|
|
user, err := client.CreateUser(context.Background(), codersdk.CreateUserRequest{
|
|
|
|
Email: email,
|
|
|
|
Username: username,
|
|
|
|
Password: "password",
|
|
|
|
OrganizationID: orgID,
|
|
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
users = append(users, user)
|
|
|
|
}
|
|
|
|
sortUsers(users)
|
|
|
|
deletedUser := users[2]
|
|
|
|
expected := users[3:8]
|
|
|
|
_, err = client.UpdateUserStatus(ctx, deletedUser.ID.String(), codersdk.UserStatusSuspended)
|
|
|
|
require.NoError(t, err, "suspend user")
|
|
|
|
|
|
|
|
page, err := client.Users(ctx, codersdk.UsersRequest{
|
|
|
|
Pagination: codersdk.Pagination{
|
|
|
|
Limit: len(expected),
|
|
|
|
AfterID: deletedUser.ID,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, expected, page, "expected page")
|
|
|
|
}
|
|
|
|
|
2022-04-22 20:27:55 +00:00
|
|
|
// TestPaginatedUsers creates a list of users, then tries to paginate through
|
|
|
|
// them using different page sizes.
|
|
|
|
func TestPaginatedUsers(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
|
|
client := coderdtest.New(t, &coderdtest.Options{APIRateLimit: -1})
|
|
|
|
coderdtest.CreateFirstUser(t, client)
|
|
|
|
me, err := client.User(context.Background(), codersdk.Me)
|
|
|
|
require.NoError(t, err)
|
2022-04-28 14:10:17 +00:00
|
|
|
orgID := me.OrganizationIDs[0]
|
2022-04-22 20:27:55 +00:00
|
|
|
|
|
|
|
allUsers := make([]codersdk.User, 0)
|
|
|
|
allUsers = append(allUsers, me)
|
|
|
|
specialUsers := make([]codersdk.User, 0)
|
|
|
|
|
|
|
|
// When 100 users exist
|
|
|
|
total := 100
|
|
|
|
// Create users
|
|
|
|
for i := 0; i < total; i++ {
|
|
|
|
email := fmt.Sprintf("%d@coder.com", i)
|
|
|
|
username := fmt.Sprintf("user%d", i)
|
|
|
|
if i%2 == 0 {
|
|
|
|
email = fmt.Sprintf("%d@gmail.com", i)
|
|
|
|
username = fmt.Sprintf("specialuser%d", i)
|
|
|
|
}
|
|
|
|
// One side effect of having to use the api vs the db calls directly, is you cannot
|
|
|
|
// mock time. Ideally I could pass in mocked times and space these users out.
|
|
|
|
//
|
|
|
|
// But this also serves as a good test. Postgres has microsecond precision on its timestamps.
|
|
|
|
// If 2 users share the same created_at, that could cause an issue if you are strictly paginating via
|
|
|
|
// timestamps. The pagination goes by timestamps and uuids.
|
|
|
|
newUser, err := client.CreateUser(context.Background(), codersdk.CreateUserRequest{
|
|
|
|
Email: email,
|
|
|
|
Username: username,
|
|
|
|
Password: "password",
|
2022-04-28 14:10:17 +00:00
|
|
|
OrganizationID: orgID,
|
2022-04-22 20:27:55 +00:00
|
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
allUsers = append(allUsers, newUser)
|
|
|
|
if i%2 == 0 {
|
|
|
|
specialUsers = append(specialUsers, newUser)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-25 15:27:08 +00:00
|
|
|
// Sorting the users will sort by (created_at, uuid). This is to handle
|
|
|
|
// the off case that created_at is identical for 2 users.
|
|
|
|
// This is a really rare case in production, but does happen in unit tests
|
|
|
|
// due to the fake database being in memory and exceptionally quick.
|
|
|
|
sortUsers(allUsers)
|
|
|
|
sortUsers(specialUsers)
|
|
|
|
|
2022-04-22 20:27:55 +00:00
|
|
|
assertPagination(ctx, t, client, 10, allUsers, nil)
|
|
|
|
assertPagination(ctx, t, client, 5, allUsers, nil)
|
|
|
|
assertPagination(ctx, t, client, 3, allUsers, nil)
|
|
|
|
assertPagination(ctx, t, client, 1, allUsers, nil)
|
|
|
|
|
|
|
|
// Try a search
|
|
|
|
gmailSearch := func(request codersdk.UsersRequest) codersdk.UsersRequest {
|
|
|
|
request.Search = "gmail"
|
|
|
|
return request
|
|
|
|
}
|
|
|
|
assertPagination(ctx, t, client, 3, specialUsers, gmailSearch)
|
|
|
|
assertPagination(ctx, t, client, 7, specialUsers, gmailSearch)
|
|
|
|
|
|
|
|
usernameSearch := func(request codersdk.UsersRequest) codersdk.UsersRequest {
|
|
|
|
request.Search = "specialuser"
|
|
|
|
return request
|
|
|
|
}
|
|
|
|
assertPagination(ctx, t, client, 3, specialUsers, usernameSearch)
|
|
|
|
assertPagination(ctx, t, client, 1, specialUsers, usernameSearch)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Assert pagination will page through the list of all users using the given
|
|
|
|
// limit for each page. The 'allUsers' is the expected full list to compare
|
|
|
|
// against.
|
|
|
|
func assertPagination(ctx context.Context, t *testing.T, client *codersdk.Client, limit int, allUsers []codersdk.User,
|
|
|
|
opt func(request codersdk.UsersRequest) codersdk.UsersRequest) {
|
|
|
|
var count int
|
|
|
|
if opt == nil {
|
|
|
|
opt = func(request codersdk.UsersRequest) codersdk.UsersRequest {
|
|
|
|
return request
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check the first page
|
|
|
|
page, err := client.Users(ctx, opt(codersdk.UsersRequest{
|
2022-05-10 07:44:09 +00:00
|
|
|
Pagination: codersdk.Pagination{
|
|
|
|
Limit: limit,
|
|
|
|
},
|
2022-04-22 20:27:55 +00:00
|
|
|
}))
|
|
|
|
require.NoError(t, err, "first page")
|
|
|
|
require.Equalf(t, page, allUsers[:limit], "first page, limit=%d", limit)
|
|
|
|
count += len(page)
|
|
|
|
|
|
|
|
for {
|
|
|
|
if len(page) == 0 {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
afterCursor := page[len(page)-1].ID
|
|
|
|
// Assert each page is the next expected page
|
|
|
|
// This is using a cursor, and only works if all users created_at
|
|
|
|
// is unique.
|
|
|
|
page, err = client.Users(ctx, opt(codersdk.UsersRequest{
|
2022-05-10 07:44:09 +00:00
|
|
|
Pagination: codersdk.Pagination{
|
|
|
|
Limit: limit,
|
|
|
|
AfterID: afterCursor,
|
|
|
|
},
|
2022-04-22 20:27:55 +00:00
|
|
|
}))
|
|
|
|
require.NoError(t, err, "next cursor page")
|
|
|
|
|
|
|
|
// Also check page by offset
|
|
|
|
offsetPage, err := client.Users(ctx, opt(codersdk.UsersRequest{
|
2022-05-10 07:44:09 +00:00
|
|
|
Pagination: codersdk.Pagination{
|
|
|
|
Limit: limit,
|
|
|
|
Offset: count,
|
|
|
|
},
|
2022-04-22 20:27:55 +00:00
|
|
|
}))
|
|
|
|
require.NoError(t, err, "next offset page")
|
|
|
|
|
|
|
|
var expected []codersdk.User
|
|
|
|
if count+limit > len(allUsers) {
|
|
|
|
expected = allUsers[count:]
|
|
|
|
} else {
|
|
|
|
expected = allUsers[count : count+limit]
|
|
|
|
}
|
|
|
|
require.Equalf(t, page, expected, "next users, after=%s, limit=%d", afterCursor, limit)
|
|
|
|
require.Equalf(t, offsetPage, expected, "offset users, offset=%d, limit=%d", count, limit)
|
|
|
|
|
|
|
|
// Also check the before
|
|
|
|
prevPage, err := client.Users(ctx, opt(codersdk.UsersRequest{
|
2022-05-10 07:44:09 +00:00
|
|
|
Pagination: codersdk.Pagination{
|
|
|
|
Offset: count - limit,
|
|
|
|
Limit: limit,
|
|
|
|
},
|
2022-04-22 20:27:55 +00:00
|
|
|
}))
|
|
|
|
require.NoError(t, err, "prev page")
|
|
|
|
require.Equal(t, allUsers[count-limit:count], prevPage, "prev users")
|
|
|
|
count += len(page)
|
|
|
|
}
|
|
|
|
}
|
2022-04-25 15:27:08 +00:00
|
|
|
|
|
|
|
// sortUsers sorts by (created_at, id)
|
|
|
|
func sortUsers(users []codersdk.User) {
|
|
|
|
sort.Slice(users, func(i, j int) bool {
|
|
|
|
if users[i].CreatedAt.Equal(users[j].CreatedAt) {
|
|
|
|
return users[i].ID.String() < users[j].ID.String()
|
|
|
|
}
|
|
|
|
return users[i].CreatedAt.Before(users[j].CreatedAt)
|
|
|
|
})
|
|
|
|
}
|