Files
netbird/management/server/user_invite_test.go
2026-03-06 18:19:33 +01:00

1011 lines
30 KiB
Go

package server
import (
"context"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/netbirdio/netbird/management/internals/modules/permissions"
"github.com/netbirdio/netbird/management/server/activity"
"github.com/netbirdio/netbird/management/server/idp"
"github.com/netbirdio/netbird/management/server/store"
"github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/shared/management/status"
"github.com/netbirdio/netbird/util/crypt"
)
const (
testAccountID = "testAccountID"
testAdminUserID = "testAdminUserID"
testRegularUserID = "testRegularUserID"
)
// setupInviteTestManagerWithEmbeddedIdP creates a test manager with a real embedded IdP
// and store encryption enabled. This is required for tests that need to pass the IsEmbeddedIdp check.
func setupInviteTestManagerWithEmbeddedIdP(t *testing.T) (*DefaultAccountManager, func()) {
t.Helper()
ctx := context.Background()
tmpDir := t.TempDir()
dexDataDir := tmpDir + "/dex"
require.NoError(t, os.MkdirAll(dexDataDir, 0700))
// Create test store
s, cleanup, err := store.NewTestStoreFromSQL(ctx, "", tmpDir)
require.NoError(t, err, "Error when creating store")
// Enable encryption
key, err := crypt.GenerateKey()
require.NoError(t, err)
fieldEncrypt, err := crypt.NewFieldEncrypt(key)
require.NoError(t, err)
s.SetFieldEncrypt(fieldEncrypt)
// Create embedded IDP config
embeddedIdPConfig := &idp.EmbeddedIdPConfig{
Enabled: true,
Issuer: "http://localhost:5556/dex",
Storage: idp.EmbeddedStorageConfig{
Type: "sqlite3",
Config: idp.EmbeddedStorageTypeConfig{
File: dexDataDir + "/dex.db",
},
},
}
// Create embedded IDP manager
embeddedIdp, err := idp.NewEmbeddedIdPManager(ctx, embeddedIdPConfig, nil)
require.NoError(t, err)
account := newAccountWithId(ctx, testAccountID, testAdminUserID, "", "admin@test.com", "Admin User", false)
account.Users[testRegularUserID] = &types.User{
Id: testRegularUserID,
AccountID: testAccountID,
Role: types.UserRoleUser,
Email: "regular@test.com",
Name: "Regular User",
}
err = s.SaveAccount(ctx, account)
require.NoError(t, err, "Error when saving account")
permissionsManager := permissions.NewManager(s)
am := DefaultAccountManager{
Store: s,
eventStore: &activity.InMemoryEventStore{},
permissionsManager: permissionsManager,
idpManager: embeddedIdp,
}
cleanupFunc := func() {
_ = embeddedIdp.Stop(ctx)
cleanup()
}
return &am, cleanupFunc
}
func TestCreateUserInvite_Success(t *testing.T) {
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
defer cleanup()
invite := &types.UserInfo{
Email: "newuser@test.com",
Name: "New User",
Role: "user",
AutoGroups: []string{},
}
result, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
require.NoError(t, err)
require.NotNil(t, result)
assert.Equal(t, "newuser@test.com", result.UserInfo.Email)
assert.Equal(t, "New User", result.UserInfo.Name)
assert.Equal(t, "user", result.UserInfo.Role)
assert.Equal(t, string(types.UserStatusInvited), result.UserInfo.Status)
assert.NotEmpty(t, result.InviteToken)
assert.True(t, result.InviteExpiresAt.After(time.Now()))
// Verify invite is stored in DB
invites, err := am.Store.GetAccountUserInvites(context.Background(), store.LockingStrengthNone, testAccountID)
require.NoError(t, err)
assert.Len(t, invites, 1)
assert.Equal(t, "newuser@test.com", invites[0].Email)
}
func TestCreateUserInvite_DuplicateEmail(t *testing.T) {
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
defer cleanup()
invite := &types.UserInfo{
Email: "newuser@test.com",
Name: "New User",
Role: "user",
AutoGroups: []string{},
}
// Create first invite
_, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
require.NoError(t, err)
// Try to create duplicate invite
_, err = am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
require.Error(t, err)
sErr, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, status.AlreadyExists, sErr.Type())
}
func TestCreateUserInvite_ExistingUserEmail(t *testing.T) {
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
defer cleanup()
// Try to invite with an email that already exists as a user
invite := &types.UserInfo{
Email: "regular@test.com", // Already exists as a user
Name: "Duplicate User",
Role: "user",
AutoGroups: []string{},
}
_, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
require.Error(t, err)
sErr, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, status.UserAlreadyExists, sErr.Type())
}
func TestCreateUserInvite_PermissionDenied(t *testing.T) {
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
defer cleanup()
invite := &types.UserInfo{
Email: "newuser@test.com",
Name: "New User",
Role: "user",
AutoGroups: []string{},
}
// Regular user should not be able to create invites
_, err := am.CreateUserInvite(context.Background(), testAccountID, testRegularUserID, invite, 0)
require.Error(t, err)
}
func TestCreateUserInvite_InvalidEmail(t *testing.T) {
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
defer cleanup()
invite := &types.UserInfo{
Email: "",
Name: "New User",
Role: "user",
AutoGroups: []string{},
}
_, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
require.Error(t, err)
sErr, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, status.InvalidArgument, sErr.Type())
}
func TestCreateUserInvite_InvalidName(t *testing.T) {
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
defer cleanup()
invite := &types.UserInfo{
Email: "newuser@test.com",
Name: "",
Role: "user",
AutoGroups: []string{},
}
_, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
require.Error(t, err)
sErr, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, status.InvalidArgument, sErr.Type())
}
func TestCreateUserInvite_OwnerRole(t *testing.T) {
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
defer cleanup()
invite := &types.UserInfo{
Email: "newowner@test.com",
Name: "New Owner",
Role: "owner",
AutoGroups: []string{},
}
_, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
require.Error(t, err)
sErr, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, status.InvalidArgument, sErr.Type())
}
func TestCreateUserInvite_ExpirationTooShort(t *testing.T) {
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
defer cleanup()
invite := &types.UserInfo{
Email: "newuser@test.com",
Name: "New User",
Role: "user",
AutoGroups: []string{},
}
// Try to create with expiration less than 1 hour
_, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 1800) // 30 minutes
require.Error(t, err)
sErr, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, status.InvalidArgument, sErr.Type())
assert.Contains(t, err.Error(), "at least 1 hour")
}
func TestCreateUserInvite_CustomExpiration(t *testing.T) {
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
defer cleanup()
invite := &types.UserInfo{
Email: "newuser@test.com",
Name: "New User",
Role: "user",
AutoGroups: []string{},
}
expiresIn := 7200 // 2 hours
result, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, expiresIn)
require.NoError(t, err)
// Verify expiration is approximately 2 hours from now
expectedExpiration := time.Now().Add(time.Duration(expiresIn) * time.Second)
assert.WithinDuration(t, expectedExpiration, result.InviteExpiresAt, time.Minute)
}
func TestCreateUserInvite_WithAutoGroups(t *testing.T) {
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
defer cleanup()
invite := &types.UserInfo{
Email: "newuser@test.com",
Name: "New User",
Role: "user",
AutoGroups: []string{"group1", "group2"},
}
result, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
require.NoError(t, err)
assert.Equal(t, []string{"group1", "group2"}, result.UserInfo.AutoGroups)
// Verify invite in DB has auto groups
invites, err := am.Store.GetAccountUserInvites(context.Background(), store.LockingStrengthNone, testAccountID)
require.NoError(t, err)
require.Len(t, invites, 1)
assert.Equal(t, []string{"group1", "group2"}, invites[0].AutoGroups)
}
func TestGetUserInviteInfo_Success(t *testing.T) {
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
defer cleanup()
// Create an invite first
invite := &types.UserInfo{
Email: "newuser@test.com",
Name: "New User",
Role: "user",
AutoGroups: []string{},
}
result, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
require.NoError(t, err)
// Get the invite info using the token
info, err := am.GetUserInviteInfo(context.Background(), result.InviteToken)
require.NoError(t, err)
require.NotNil(t, info)
assert.Equal(t, "newuser@test.com", info.Email)
assert.Equal(t, "New User", info.Name)
assert.True(t, info.Valid)
assert.Equal(t, "Admin User", info.InvitedBy)
}
func TestGetUserInviteInfo_InvalidToken(t *testing.T) {
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
defer cleanup()
_, err := am.GetUserInviteInfo(context.Background(), "invalid_token")
require.Error(t, err)
sErr, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, status.InvalidArgument, sErr.Type())
}
func TestGetUserInviteInfo_TokenNotFound(t *testing.T) {
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
defer cleanup()
// Generate a valid token format that doesn't exist in DB
_, validToken, err := types.GenerateInviteToken()
require.NoError(t, err)
_, err = am.GetUserInviteInfo(context.Background(), validToken)
require.Error(t, err)
sErr, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, status.NotFound, sErr.Type())
}
func TestGetUserInviteInfo_ExpiredInvite(t *testing.T) {
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
defer cleanup()
// Create an invite with valid expiration
invite := &types.UserInfo{
Email: "newuser@test.com",
Name: "New User",
Role: "user",
AutoGroups: []string{},
}
result, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
require.NoError(t, err)
// Manually set the invite to expired by updating the store directly
inviteRecord, err := am.Store.GetUserInviteByID(context.Background(), store.LockingStrengthUpdate, testAccountID, result.UserInfo.ID)
require.NoError(t, err)
inviteRecord.ExpiresAt = time.Now().Add(-time.Hour) // Set to 1 hour ago
err = am.Store.SaveUserInvite(context.Background(), inviteRecord)
require.NoError(t, err)
// Get the invite info - should still return info but Valid should be false
info, err := am.GetUserInviteInfo(context.Background(), result.InviteToken)
require.NoError(t, err)
assert.False(t, info.Valid)
}
func TestListUserInvites_Success(t *testing.T) {
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
defer cleanup()
// Create multiple invites
for i, email := range []string{"user1@test.com", "user2@test.com", "user3@test.com"} {
invite := &types.UserInfo{
Email: email,
Name: "User " + string(rune('1'+i)),
Role: "user",
AutoGroups: []string{},
}
_, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
require.NoError(t, err)
}
// List invites
invites, err := am.ListUserInvites(context.Background(), testAccountID, testAdminUserID)
require.NoError(t, err)
assert.Len(t, invites, 3)
}
func TestListUserInvites_Empty(t *testing.T) {
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
defer cleanup()
invites, err := am.ListUserInvites(context.Background(), testAccountID, testAdminUserID)
require.NoError(t, err)
assert.Len(t, invites, 0)
}
func TestListUserInvites_PermissionDenied(t *testing.T) {
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
defer cleanup()
_, err := am.ListUserInvites(context.Background(), testAccountID, testRegularUserID)
require.Error(t, err)
}
func TestRegenerateUserInvite_Success(t *testing.T) {
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
defer cleanup()
// Create an invite first
invite := &types.UserInfo{
Email: "newuser@test.com",
Name: "New User",
Role: "user",
AutoGroups: []string{},
}
originalResult, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
require.NoError(t, err)
// Regenerate the invite
newResult, err := am.RegenerateUserInvite(context.Background(), testAccountID, testAdminUserID, originalResult.UserInfo.ID, 0)
require.NoError(t, err)
require.NotNil(t, newResult)
// Verify invite ID remains the same (stable ID for clients)
assert.Equal(t, originalResult.UserInfo.ID, newResult.UserInfo.ID)
// Verify new token is different
assert.NotEqual(t, originalResult.InviteToken, newResult.InviteToken)
assert.Equal(t, "newuser@test.com", newResult.UserInfo.Email)
assert.Equal(t, "New User", newResult.UserInfo.Name)
// Verify old token no longer works
_, err = am.GetUserInviteInfo(context.Background(), originalResult.InviteToken)
require.Error(t, err)
// Verify new token works
info, err := am.GetUserInviteInfo(context.Background(), newResult.InviteToken)
require.NoError(t, err)
assert.Equal(t, "newuser@test.com", info.Email)
}
func TestRegenerateUserInvite_NotFound(t *testing.T) {
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
defer cleanup()
_, err := am.RegenerateUserInvite(context.Background(), testAccountID, testAdminUserID, "nonexistent-id", 0)
require.Error(t, err)
sErr, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, status.NotFound, sErr.Type())
}
func TestRegenerateUserInvite_PermissionDenied(t *testing.T) {
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
defer cleanup()
// Create an invite first
invite := &types.UserInfo{
Email: "newuser@test.com",
Name: "New User",
Role: "user",
AutoGroups: []string{},
}
result, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
require.NoError(t, err)
// Regular user should not be able to regenerate
_, err = am.RegenerateUserInvite(context.Background(), testAccountID, testRegularUserID, result.UserInfo.ID, 0)
require.Error(t, err)
}
func TestDeleteUserInvite_Success(t *testing.T) {
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
defer cleanup()
// Create an invite first
invite := &types.UserInfo{
Email: "newuser@test.com",
Name: "New User",
Role: "user",
AutoGroups: []string{},
}
result, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
require.NoError(t, err)
// Delete the invite
err = am.DeleteUserInvite(context.Background(), testAccountID, testAdminUserID, result.UserInfo.ID)
require.NoError(t, err)
// Verify invite is deleted
invites, err := am.Store.GetAccountUserInvites(context.Background(), store.LockingStrengthNone, testAccountID)
require.NoError(t, err)
assert.Len(t, invites, 0)
// Verify token no longer works
_, err = am.GetUserInviteInfo(context.Background(), result.InviteToken)
require.Error(t, err)
}
func TestDeleteUserInvite_NotFound(t *testing.T) {
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
defer cleanup()
err := am.DeleteUserInvite(context.Background(), testAccountID, testAdminUserID, "nonexistent-id")
require.Error(t, err)
sErr, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, status.NotFound, sErr.Type())
}
func TestDeleteUserInvite_PermissionDenied(t *testing.T) {
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
defer cleanup()
// Create an invite first
invite := &types.UserInfo{
Email: "newuser@test.com",
Name: "New User",
Role: "user",
AutoGroups: []string{},
}
result, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
require.NoError(t, err)
// Regular user should not be able to delete
err = am.DeleteUserInvite(context.Background(), testAccountID, testRegularUserID, result.UserInfo.ID)
require.Error(t, err)
}
func TestDeleteUserInvite_WrongAccount(t *testing.T) {
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
defer cleanup()
// Create an invite
invite := &types.UserInfo{
Email: "newuser@test.com",
Name: "New User",
Role: "user",
AutoGroups: []string{},
}
result, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
require.NoError(t, err)
// Create another account
anotherAccountID := "anotherAccountID"
anotherAdminID := "anotherAdminID"
anotherAccount := newAccountWithId(context.Background(), anotherAccountID, anotherAdminID, "", "otheradmin@test.com", "Other Admin", false)
err = am.Store.SaveAccount(context.Background(), anotherAccount)
require.NoError(t, err)
// Try to delete from wrong account
err = am.DeleteUserInvite(context.Background(), anotherAccountID, anotherAdminID, result.UserInfo.ID)
require.Error(t, err)
sErr, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, status.NotFound, sErr.Type())
}
func TestAcceptUserInvite_Success(t *testing.T) {
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
defer cleanup()
// Create an invite
invite := &types.UserInfo{
Email: "newuser@test.com",
Name: "New User",
Role: "user",
AutoGroups: []string{},
}
result, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
require.NoError(t, err)
// Accept the invite with a valid password
err = am.AcceptUserInvite(context.Background(), result.InviteToken, "Password1!")
require.NoError(t, err)
// Verify user is created in DB
users, err := am.Store.GetAccountUsers(context.Background(), store.LockingStrengthNone, testAccountID)
require.NoError(t, err)
var foundUser *types.User
for _, u := range users {
if u.Email == "newuser@test.com" {
foundUser = u
break
}
}
require.NotNil(t, foundUser, "User should be created in DB")
assert.Equal(t, "New User", foundUser.Name)
assert.Equal(t, types.UserRoleUser, foundUser.Role)
// Verify invite is deleted
invites, err := am.Store.GetAccountUserInvites(context.Background(), store.LockingStrengthNone, testAccountID)
require.NoError(t, err)
assert.Len(t, invites, 0)
}
func TestAcceptUserInvite_InvalidToken(t *testing.T) {
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
defer cleanup()
err := am.AcceptUserInvite(context.Background(), "invalid_token", "Password1!")
require.Error(t, err)
sErr, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, status.InvalidArgument, sErr.Type())
}
func TestAcceptUserInvite_TokenNotFound(t *testing.T) {
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
defer cleanup()
// Generate a valid token format that doesn't exist in DB
_, validToken, err := types.GenerateInviteToken()
require.NoError(t, err)
err = am.AcceptUserInvite(context.Background(), validToken, "Password1!")
require.Error(t, err)
sErr, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, status.NotFound, sErr.Type())
}
func TestAcceptUserInvite_ExpiredToken(t *testing.T) {
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
defer cleanup()
// Create an invite with valid expiration
invite := &types.UserInfo{
Email: "newuser@test.com",
Name: "New User",
Role: "user",
AutoGroups: []string{},
}
result, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
require.NoError(t, err)
// Manually set the invite to expired by updating the store directly
inviteRecord, err := am.Store.GetUserInviteByID(context.Background(), store.LockingStrengthUpdate, testAccountID, result.UserInfo.ID)
require.NoError(t, err)
inviteRecord.ExpiresAt = time.Now().Add(-time.Hour) // Set to 1 hour ago
err = am.Store.SaveUserInvite(context.Background(), inviteRecord)
require.NoError(t, err)
err = am.AcceptUserInvite(context.Background(), result.InviteToken, "Password1!")
require.Error(t, err)
sErr, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, status.InvalidArgument, sErr.Type())
assert.Contains(t, err.Error(), "expired")
}
func TestAcceptUserInvite_EmptyPassword(t *testing.T) {
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
defer cleanup()
// Create an invite
invite := &types.UserInfo{
Email: "newuser@test.com",
Name: "New User",
Role: "user",
AutoGroups: []string{},
}
result, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
require.NoError(t, err)
err = am.AcceptUserInvite(context.Background(), result.InviteToken, "")
require.Error(t, err)
sErr, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, status.InvalidArgument, sErr.Type())
assert.Contains(t, err.Error(), "password is required")
}
func TestAcceptUserInvite_WeakPassword(t *testing.T) {
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
defer cleanup()
// Create an invite
invite := &types.UserInfo{
Email: "newuser@test.com",
Name: "New User",
Role: "user",
AutoGroups: []string{},
}
result, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
require.NoError(t, err)
testCases := []struct {
name string
password string
expectedMsg string
}{
{"too short", "Pass1!", "at least 8 characters"},
{"no digit", "Password!", "one digit"},
{"no uppercase", "password1!", "one uppercase"},
{"no special", "Password1", "one special character"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := am.AcceptUserInvite(context.Background(), result.InviteToken, tc.password)
require.Error(t, err)
assert.Contains(t, err.Error(), tc.expectedMsg)
})
}
}
func TestValidatePassword(t *testing.T) {
testCases := []struct {
name string
password string
expectError bool
errorMsg string
}{
{"valid password", "Password1!", false, ""},
{"valid complex password", "MyP@ssw0rd#2024", false, ""},
{"too short", "Pass1!", true, "at least 8 characters"},
{"no digit", "Password!", true, "one digit"},
{"no uppercase", "password1!", true, "one uppercase"},
{"no special", "Password1", true, "one special character"},
{"only lowercase", "password", true, "one digit"},
{"no uppercase no special", "password1", true, "one uppercase"},
{"all lowercase short", "pass", true, "at least 8 characters"},
{"empty", "", true, "at least 8 characters"},
{"spaces count as special", "Pass word1", false, ""},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := validatePassword(tc.password)
if tc.expectError {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.errorMsg)
} else {
require.NoError(t, err)
}
})
}
}
func TestInviteToken_GenerateAndValidate(t *testing.T) {
hashedToken, plainToken, err := types.GenerateInviteToken()
require.NoError(t, err)
require.NotEmpty(t, hashedToken)
require.NotEmpty(t, plainToken)
// Validate token format
assert.Len(t, plainToken, types.InviteTokenLength)
assert.True(t, len(plainToken) > len(types.InviteTokenPrefix))
assert.Equal(t, types.InviteTokenPrefix, plainToken[:len(types.InviteTokenPrefix)])
// Validate checksum
err = types.ValidateInviteToken(plainToken)
require.NoError(t, err)
// Verify hashing is consistent
hashedAgain := types.HashInviteToken(plainToken)
assert.Equal(t, hashedToken, hashedAgain)
}
func TestInviteToken_ValidateInvalid(t *testing.T) {
testCases := []struct {
name string
token string
}{
{"empty", ""},
{"too short", "nbi_abc"},
{"wrong prefix", "xyz_123456789012345678901234567890"},
{"invalid checksum", "nbi_123456789012345678901234567890abcdef"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := types.ValidateInviteToken(tc.token)
require.Error(t, err)
})
}
}
func TestUserInviteRecord_IsExpired(t *testing.T) {
// Not expired
invite := &types.UserInviteRecord{
ExpiresAt: time.Now().Add(time.Hour),
}
assert.False(t, invite.IsExpired())
// Expired
invite = &types.UserInviteRecord{
ExpiresAt: time.Now().Add(-time.Hour),
}
assert.True(t, invite.IsExpired())
}
func TestUserInviteRecord_Copy(t *testing.T) {
original := &types.UserInviteRecord{
ID: "invite-id",
AccountID: "account-id",
Email: "test@example.com",
Name: "Test User",
Role: "user",
AutoGroups: []string{"group1", "group2"},
HashedToken: "hashed-token",
ExpiresAt: time.Now().Add(time.Hour),
CreatedAt: time.Now(),
CreatedBy: "creator-id",
}
copied := original.Copy()
assert.Equal(t, original.ID, copied.ID)
assert.Equal(t, original.AccountID, copied.AccountID)
assert.Equal(t, original.Email, copied.Email)
assert.Equal(t, original.Name, copied.Name)
assert.Equal(t, original.Role, copied.Role)
assert.Equal(t, original.AutoGroups, copied.AutoGroups)
assert.Equal(t, original.HashedToken, copied.HashedToken)
assert.Equal(t, original.ExpiresAt, copied.ExpiresAt)
assert.Equal(t, original.CreatedAt, copied.CreatedAt)
assert.Equal(t, original.CreatedBy, copied.CreatedBy)
// Verify deep copy of AutoGroups
copied.AutoGroups[0] = "modified"
assert.NotEqual(t, original.AutoGroups[0], copied.AutoGroups[0])
}
func TestCreateUserInvite_NonEmbeddedIdP(t *testing.T) {
s, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir())
require.NoError(t, err)
defer cleanup()
account := newAccountWithId(context.Background(), testAccountID, testAdminUserID, "", "admin@test.com", "Admin User", false)
err = s.SaveAccount(context.Background(), account)
require.NoError(t, err)
permissionsManager := permissions.NewManager(s)
// Use nil IDP manager (non-embedded)
am := DefaultAccountManager{
Store: s,
eventStore: &activity.InMemoryEventStore{},
permissionsManager: permissionsManager,
idpManager: nil,
}
invite := &types.UserInfo{
Email: "newuser@test.com",
Name: "New User",
Role: "user",
AutoGroups: []string{},
}
_, err = am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
require.Error(t, err)
sErr, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, status.PreconditionFailed, sErr.Type())
assert.Contains(t, err.Error(), "embedded identity provider")
}
func TestAcceptUserInvite_WithAutoGroups(t *testing.T) {
am, cleanup := setupInviteTestManagerWithEmbeddedIdP(t)
defer cleanup()
// Create an invite with auto groups
invite := &types.UserInfo{
Email: "newuser@test.com",
Name: "New User",
Role: "admin",
AutoGroups: []string{"group1", "group2"},
}
result, err := am.CreateUserInvite(context.Background(), testAccountID, testAdminUserID, invite, 0)
require.NoError(t, err)
// Accept the invite
err = am.AcceptUserInvite(context.Background(), result.InviteToken, "Password1!")
require.NoError(t, err)
// Verify user has the auto groups and role
users, err := am.Store.GetAccountUsers(context.Background(), store.LockingStrengthNone, testAccountID)
require.NoError(t, err)
var foundUser *types.User
for _, u := range users {
if u.Email == "newuser@test.com" {
foundUser = u
break
}
}
require.NotNil(t, foundUser)
assert.Equal(t, types.UserRoleAdmin, foundUser.Role)
assert.Equal(t, []string{"group1", "group2"}, foundUser.AutoGroups)
}
func TestUserInvite_EncryptDecryptSensitiveData(t *testing.T) {
key, err := crypt.GenerateKey()
require.NoError(t, err)
fieldEncrypt, err := crypt.NewFieldEncrypt(key)
require.NoError(t, err)
t.Run("encrypt and decrypt", func(t *testing.T) {
invite := &types.UserInviteRecord{
ID: "test-invite",
AccountID: "test-account",
Email: "test@example.com",
Name: "Test User",
Role: "user",
}
// Encrypt
err := invite.EncryptSensitiveData(fieldEncrypt)
require.NoError(t, err)
// Verify encrypted values are different from original
assert.NotEqual(t, "test@example.com", invite.Email)
assert.NotEqual(t, "Test User", invite.Name)
// Decrypt
err = invite.DecryptSensitiveData(fieldEncrypt)
require.NoError(t, err)
// Verify decrypted values match original
assert.Equal(t, "test@example.com", invite.Email)
assert.Equal(t, "Test User", invite.Name)
})
t.Run("encrypt empty fields", func(t *testing.T) {
invite := &types.UserInviteRecord{
ID: "test-invite",
AccountID: "test-account",
Email: "",
Name: "",
Role: "user",
}
// Encrypt empty fields
err := invite.EncryptSensitiveData(fieldEncrypt)
require.NoError(t, err)
// Empty strings should remain empty
assert.Equal(t, "", invite.Email)
assert.Equal(t, "", invite.Name)
// Decrypt empty fields
err = invite.DecryptSensitiveData(fieldEncrypt)
require.NoError(t, err)
// Should still be empty
assert.Equal(t, "", invite.Email)
assert.Equal(t, "", invite.Name)
})
t.Run("nil encryptor", func(t *testing.T) {
invite := &types.UserInviteRecord{
ID: "test-invite",
AccountID: "test-account",
Email: "test@example.com",
Name: "Test User",
Role: "user",
}
// Encrypt with nil encryptor should be no-op
err := invite.EncryptSensitiveData(nil)
require.NoError(t, err)
assert.Equal(t, "test@example.com", invite.Email)
assert.Equal(t, "Test User", invite.Name)
// Decrypt with nil encryptor should be no-op
err = invite.DecryptSensitiveData(nil)
require.NoError(t, err)
assert.Equal(t, "test@example.com", invite.Email)
assert.Equal(t, "Test User", invite.Name)
})
}