mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-16 07:16:38 +00:00
Add LocalAuthDisabled option to embedded IdP configuration This adds the ability to disable local (email/password) authentication when using the embedded Dex identity provider. When disabled, users can only authenticate via external identity providers (Google, OIDC, etc.). This simplifies user login when there is only one external IdP configured. The login page will redirect directly to the IdP login page. Key changes: Added LocalAuthDisabled field to EmbeddedIdPConfig Added methods to check and toggle local auth: IsLocalAuthEnabled, HasNonLocalConnectors, DisableLocalAuth, EnableLocalAuth Validation prevents disabling local auth if no external connectors are configured Existing local users are preserved when disabled and can login again when re-enabled Operations are idempotent (disabling already disabled is a no-op)
660 lines
21 KiB
Go
660 lines
21 KiB
Go
package users
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
nbcontext "github.com/netbirdio/netbird/management/server/context"
|
|
"github.com/netbirdio/netbird/management/server/mock_server"
|
|
"github.com/netbirdio/netbird/management/server/types"
|
|
"github.com/netbirdio/netbird/shared/auth"
|
|
"github.com/netbirdio/netbird/shared/management/http/api"
|
|
"github.com/netbirdio/netbird/shared/management/status"
|
|
)
|
|
|
|
const (
|
|
testAccountID = "test-account-id"
|
|
testUserID = "test-user-id"
|
|
testInviteID = "test-invite-id"
|
|
testInviteToken = "nbi_testtoken123456789012345678"
|
|
testEmail = "invite@example.com"
|
|
testName = "Test User"
|
|
)
|
|
|
|
func setupInvitesTestHandler(am *mock_server.MockAccountManager) *invitesHandler {
|
|
return &invitesHandler{
|
|
accountManager: am,
|
|
}
|
|
}
|
|
|
|
func TestListInvites(t *testing.T) {
|
|
now := time.Now().UTC()
|
|
testInvites := []*types.UserInvite{
|
|
{
|
|
UserInfo: &types.UserInfo{
|
|
ID: "invite-1",
|
|
Email: "user1@example.com",
|
|
Name: "User One",
|
|
Role: "user",
|
|
AutoGroups: []string{"group-1"},
|
|
},
|
|
InviteExpiresAt: now.Add(24 * time.Hour),
|
|
InviteCreatedAt: now,
|
|
},
|
|
{
|
|
UserInfo: &types.UserInfo{
|
|
ID: "invite-2",
|
|
Email: "user2@example.com",
|
|
Name: "User Two",
|
|
Role: "admin",
|
|
AutoGroups: nil,
|
|
},
|
|
InviteExpiresAt: now.Add(-1 * time.Hour), // Expired
|
|
InviteCreatedAt: now.Add(-48 * time.Hour),
|
|
},
|
|
}
|
|
|
|
tt := []struct {
|
|
name string
|
|
expectedStatus int
|
|
mockFunc func(ctx context.Context, accountID, initiatorUserID string) ([]*types.UserInvite, error)
|
|
expectedCount int
|
|
}{
|
|
{
|
|
name: "successful list",
|
|
expectedStatus: http.StatusOK,
|
|
mockFunc: func(ctx context.Context, accountID, initiatorUserID string) ([]*types.UserInvite, error) {
|
|
return testInvites, nil
|
|
},
|
|
expectedCount: 2,
|
|
},
|
|
{
|
|
name: "empty list",
|
|
expectedStatus: http.StatusOK,
|
|
mockFunc: func(ctx context.Context, accountID, initiatorUserID string) ([]*types.UserInvite, error) {
|
|
return []*types.UserInvite{}, nil
|
|
},
|
|
expectedCount: 0,
|
|
},
|
|
{
|
|
name: "permission denied",
|
|
expectedStatus: http.StatusForbidden,
|
|
mockFunc: func(ctx context.Context, accountID, initiatorUserID string) ([]*types.UserInvite, error) {
|
|
return nil, status.NewPermissionDeniedError()
|
|
},
|
|
expectedCount: 0,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tt {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
am := &mock_server.MockAccountManager{
|
|
ListUserInvitesFunc: tc.mockFunc,
|
|
}
|
|
handler := setupInvitesTestHandler(am)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/users/invites", nil)
|
|
req = nbcontext.SetUserAuthInRequest(req, auth.UserAuth{
|
|
UserId: testUserID,
|
|
AccountId: testAccountID,
|
|
})
|
|
|
|
rr := httptest.NewRecorder()
|
|
handler.listInvites(rr, req)
|
|
|
|
assert.Equal(t, tc.expectedStatus, rr.Code)
|
|
|
|
if tc.expectedStatus == http.StatusOK {
|
|
var resp []api.UserInvite
|
|
err := json.NewDecoder(rr.Body).Decode(&resp)
|
|
require.NoError(t, err)
|
|
assert.Len(t, resp, tc.expectedCount)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCreateInvite(t *testing.T) {
|
|
now := time.Now().UTC()
|
|
expiresAt := now.Add(72 * time.Hour)
|
|
|
|
tt := []struct {
|
|
name string
|
|
requestBody string
|
|
expectedStatus int
|
|
mockFunc func(ctx context.Context, accountID, initiatorUserID string, invite *types.UserInfo, expiresIn int) (*types.UserInvite, error)
|
|
}{
|
|
{
|
|
name: "successful create",
|
|
requestBody: `{"email":"test@example.com","name":"Test User","role":"user","auto_groups":["group-1"]}`,
|
|
expectedStatus: http.StatusOK,
|
|
mockFunc: func(ctx context.Context, accountID, initiatorUserID string, invite *types.UserInfo, expiresIn int) (*types.UserInvite, error) {
|
|
return &types.UserInvite{
|
|
UserInfo: &types.UserInfo{
|
|
ID: testInviteID,
|
|
Email: invite.Email,
|
|
Name: invite.Name,
|
|
Role: invite.Role,
|
|
AutoGroups: invite.AutoGroups,
|
|
Status: string(types.UserStatusInvited),
|
|
},
|
|
InviteToken: testInviteToken,
|
|
InviteExpiresAt: expiresAt,
|
|
}, nil
|
|
},
|
|
},
|
|
{
|
|
name: "successful create with custom expiration",
|
|
requestBody: `{"email":"test@example.com","name":"Test User","role":"admin","auto_groups":[],"expires_in":3600}`,
|
|
expectedStatus: http.StatusOK,
|
|
mockFunc: func(ctx context.Context, accountID, initiatorUserID string, invite *types.UserInfo, expiresIn int) (*types.UserInvite, error) {
|
|
assert.Equal(t, 3600, expiresIn)
|
|
return &types.UserInvite{
|
|
UserInfo: &types.UserInfo{
|
|
ID: testInviteID,
|
|
Email: invite.Email,
|
|
Name: invite.Name,
|
|
Role: invite.Role,
|
|
AutoGroups: []string{},
|
|
Status: string(types.UserStatusInvited),
|
|
},
|
|
InviteToken: testInviteToken,
|
|
InviteExpiresAt: expiresAt,
|
|
}, nil
|
|
},
|
|
},
|
|
{
|
|
name: "user already exists",
|
|
requestBody: `{"email":"existing@example.com","name":"Existing User","role":"user","auto_groups":[]}`,
|
|
expectedStatus: http.StatusConflict,
|
|
mockFunc: func(ctx context.Context, accountID, initiatorUserID string, invite *types.UserInfo, expiresIn int) (*types.UserInvite, error) {
|
|
return nil, status.Errorf(status.UserAlreadyExists, "user with this email already exists")
|
|
},
|
|
},
|
|
{
|
|
name: "invite already exists",
|
|
requestBody: `{"email":"invited@example.com","name":"Invited User","role":"user","auto_groups":[]}`,
|
|
expectedStatus: http.StatusConflict,
|
|
mockFunc: func(ctx context.Context, accountID, initiatorUserID string, invite *types.UserInfo, expiresIn int) (*types.UserInvite, error) {
|
|
return nil, status.Errorf(status.AlreadyExists, "invite already exists for this email")
|
|
},
|
|
},
|
|
{
|
|
name: "permission denied",
|
|
requestBody: `{"email":"test@example.com","name":"Test User","role":"user","auto_groups":[]}`,
|
|
expectedStatus: http.StatusForbidden,
|
|
mockFunc: func(ctx context.Context, accountID, initiatorUserID string, invite *types.UserInfo, expiresIn int) (*types.UserInvite, error) {
|
|
return nil, status.NewPermissionDeniedError()
|
|
},
|
|
},
|
|
{
|
|
name: "embedded IDP not enabled",
|
|
requestBody: `{"email":"test@example.com","name":"Test User","role":"user","auto_groups":[]}`,
|
|
expectedStatus: http.StatusPreconditionFailed,
|
|
mockFunc: func(ctx context.Context, accountID, initiatorUserID string, invite *types.UserInfo, expiresIn int) (*types.UserInvite, error) {
|
|
return nil, status.Errorf(status.PreconditionFailed, "invite links are only available with embedded identity provider")
|
|
},
|
|
},
|
|
{
|
|
name: "local auth disabled",
|
|
requestBody: `{"email":"test@example.com","name":"Test User","role":"user","auto_groups":[]}`,
|
|
expectedStatus: http.StatusPreconditionFailed,
|
|
mockFunc: func(ctx context.Context, accountID, initiatorUserID string, invite *types.UserInfo, expiresIn int) (*types.UserInvite, error) {
|
|
return nil, status.Errorf(status.PreconditionFailed, "local user creation is disabled - use an external identity provider")
|
|
},
|
|
},
|
|
{
|
|
name: "invalid JSON",
|
|
requestBody: `{invalid json}`,
|
|
expectedStatus: http.StatusBadRequest,
|
|
mockFunc: nil,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tt {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
am := &mock_server.MockAccountManager{
|
|
CreateUserInviteFunc: tc.mockFunc,
|
|
}
|
|
handler := setupInvitesTestHandler(am)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/users/invites", bytes.NewBufferString(tc.requestBody))
|
|
req = nbcontext.SetUserAuthInRequest(req, auth.UserAuth{
|
|
UserId: testUserID,
|
|
AccountId: testAccountID,
|
|
})
|
|
|
|
rr := httptest.NewRecorder()
|
|
handler.createInvite(rr, req)
|
|
|
|
assert.Equal(t, tc.expectedStatus, rr.Code)
|
|
|
|
if tc.expectedStatus == http.StatusOK {
|
|
var resp api.UserInvite
|
|
err := json.NewDecoder(rr.Body).Decode(&resp)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, testInviteID, resp.Id)
|
|
assert.NotNil(t, resp.InviteToken)
|
|
assert.NotEmpty(t, *resp.InviteToken)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetInviteInfo(t *testing.T) {
|
|
now := time.Now().UTC()
|
|
|
|
tt := []struct {
|
|
name string
|
|
token string
|
|
expectedStatus int
|
|
mockFunc func(ctx context.Context, token string) (*types.UserInviteInfo, error)
|
|
}{
|
|
{
|
|
name: "successful get valid invite",
|
|
token: testInviteToken,
|
|
expectedStatus: http.StatusOK,
|
|
mockFunc: func(ctx context.Context, token string) (*types.UserInviteInfo, error) {
|
|
return &types.UserInviteInfo{
|
|
Email: testEmail,
|
|
Name: testName,
|
|
ExpiresAt: now.Add(24 * time.Hour),
|
|
Valid: true,
|
|
InvitedBy: "Admin User",
|
|
}, nil
|
|
},
|
|
},
|
|
{
|
|
name: "successful get expired invite",
|
|
token: testInviteToken,
|
|
expectedStatus: http.StatusOK,
|
|
mockFunc: func(ctx context.Context, token string) (*types.UserInviteInfo, error) {
|
|
return &types.UserInviteInfo{
|
|
Email: testEmail,
|
|
Name: testName,
|
|
ExpiresAt: now.Add(-24 * time.Hour),
|
|
Valid: false,
|
|
InvitedBy: "Admin User",
|
|
}, nil
|
|
},
|
|
},
|
|
{
|
|
name: "invite not found",
|
|
token: "nbi_invalidtoken1234567890123456",
|
|
expectedStatus: http.StatusNotFound,
|
|
mockFunc: func(ctx context.Context, token string) (*types.UserInviteInfo, error) {
|
|
return nil, status.Errorf(status.NotFound, "invite not found")
|
|
},
|
|
},
|
|
{
|
|
name: "invalid token format",
|
|
token: "invalid",
|
|
expectedStatus: http.StatusUnprocessableEntity,
|
|
mockFunc: func(ctx context.Context, token string) (*types.UserInviteInfo, error) {
|
|
return nil, status.Errorf(status.InvalidArgument, "invalid invite token")
|
|
},
|
|
},
|
|
{
|
|
name: "missing token",
|
|
token: "",
|
|
expectedStatus: http.StatusUnprocessableEntity,
|
|
mockFunc: nil,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tt {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
am := &mock_server.MockAccountManager{
|
|
GetUserInviteInfoFunc: tc.mockFunc,
|
|
}
|
|
handler := setupInvitesTestHandler(am)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/users/invites/"+tc.token, nil)
|
|
if tc.token != "" {
|
|
req = mux.SetURLVars(req, map[string]string{"token": tc.token})
|
|
}
|
|
|
|
rr := httptest.NewRecorder()
|
|
handler.getInviteInfo(rr, req)
|
|
|
|
assert.Equal(t, tc.expectedStatus, rr.Code)
|
|
|
|
if tc.expectedStatus == http.StatusOK {
|
|
var resp api.UserInviteInfo
|
|
err := json.NewDecoder(rr.Body).Decode(&resp)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, testEmail, resp.Email)
|
|
assert.Equal(t, testName, resp.Name)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAcceptInvite(t *testing.T) {
|
|
tt := []struct {
|
|
name string
|
|
token string
|
|
requestBody string
|
|
expectedStatus int
|
|
mockFunc func(ctx context.Context, token, password string) error
|
|
}{
|
|
{
|
|
name: "successful accept",
|
|
token: testInviteToken,
|
|
requestBody: `{"password":"SecurePass123!"}`,
|
|
expectedStatus: http.StatusOK,
|
|
mockFunc: func(ctx context.Context, token, password string) error {
|
|
return nil
|
|
},
|
|
},
|
|
{
|
|
name: "invite not found",
|
|
token: "nbi_invalidtoken1234567890123456",
|
|
requestBody: `{"password":"SecurePass123!"}`,
|
|
expectedStatus: http.StatusNotFound,
|
|
mockFunc: func(ctx context.Context, token, password string) error {
|
|
return status.Errorf(status.NotFound, "invite not found")
|
|
},
|
|
},
|
|
{
|
|
name: "invite expired",
|
|
token: testInviteToken,
|
|
requestBody: `{"password":"SecurePass123!"}`,
|
|
expectedStatus: http.StatusUnprocessableEntity,
|
|
mockFunc: func(ctx context.Context, token, password string) error {
|
|
return status.Errorf(status.InvalidArgument, "invite has expired")
|
|
},
|
|
},
|
|
{
|
|
name: "embedded IDP not enabled",
|
|
token: testInviteToken,
|
|
requestBody: `{"password":"SecurePass123!"}`,
|
|
expectedStatus: http.StatusPreconditionFailed,
|
|
mockFunc: func(ctx context.Context, token, password string) error {
|
|
return status.Errorf(status.PreconditionFailed, "invite links are only available with embedded identity provider")
|
|
},
|
|
},
|
|
{
|
|
name: "local auth disabled",
|
|
token: testInviteToken,
|
|
requestBody: `{"password":"SecurePass123!"}`,
|
|
expectedStatus: http.StatusPreconditionFailed,
|
|
mockFunc: func(ctx context.Context, token, password string) error {
|
|
return status.Errorf(status.PreconditionFailed, "local user creation is disabled - use an external identity provider")
|
|
},
|
|
},
|
|
{
|
|
name: "missing token",
|
|
token: "",
|
|
requestBody: `{"password":"SecurePass123!"}`,
|
|
expectedStatus: http.StatusUnprocessableEntity,
|
|
mockFunc: nil,
|
|
},
|
|
{
|
|
name: "invalid JSON",
|
|
token: testInviteToken,
|
|
requestBody: `{invalid}`,
|
|
expectedStatus: http.StatusBadRequest,
|
|
mockFunc: nil,
|
|
},
|
|
{
|
|
name: "password too short",
|
|
token: testInviteToken,
|
|
requestBody: `{"password":"Short1!"}`,
|
|
expectedStatus: http.StatusUnprocessableEntity,
|
|
mockFunc: func(ctx context.Context, token, password string) error {
|
|
return status.Errorf(status.InvalidArgument, "password must be at least 8 characters long")
|
|
},
|
|
},
|
|
{
|
|
name: "password missing digit",
|
|
token: testInviteToken,
|
|
requestBody: `{"password":"NoDigitPass!"}`,
|
|
expectedStatus: http.StatusUnprocessableEntity,
|
|
mockFunc: func(ctx context.Context, token, password string) error {
|
|
return status.Errorf(status.InvalidArgument, "password must contain at least one digit")
|
|
},
|
|
},
|
|
{
|
|
name: "password missing uppercase",
|
|
token: testInviteToken,
|
|
requestBody: `{"password":"nouppercase1!"}`,
|
|
expectedStatus: http.StatusUnprocessableEntity,
|
|
mockFunc: func(ctx context.Context, token, password string) error {
|
|
return status.Errorf(status.InvalidArgument, "password must contain at least one uppercase letter")
|
|
},
|
|
},
|
|
{
|
|
name: "password missing special character",
|
|
token: testInviteToken,
|
|
requestBody: `{"password":"NoSpecial123"}`,
|
|
expectedStatus: http.StatusUnprocessableEntity,
|
|
mockFunc: func(ctx context.Context, token, password string) error {
|
|
return status.Errorf(status.InvalidArgument, "password must contain at least one special character")
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tt {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
am := &mock_server.MockAccountManager{
|
|
AcceptUserInviteFunc: tc.mockFunc,
|
|
}
|
|
handler := setupInvitesTestHandler(am)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/users/invites/"+tc.token+"/accept", bytes.NewBufferString(tc.requestBody))
|
|
if tc.token != "" {
|
|
req = mux.SetURLVars(req, map[string]string{"token": tc.token})
|
|
}
|
|
|
|
rr := httptest.NewRecorder()
|
|
handler.acceptInvite(rr, req)
|
|
|
|
assert.Equal(t, tc.expectedStatus, rr.Code)
|
|
|
|
if tc.expectedStatus == http.StatusOK {
|
|
var resp api.UserInviteAcceptResponse
|
|
err := json.NewDecoder(rr.Body).Decode(&resp)
|
|
require.NoError(t, err)
|
|
assert.True(t, resp.Success)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRegenerateInvite(t *testing.T) {
|
|
now := time.Now().UTC()
|
|
expiresAt := now.Add(72 * time.Hour)
|
|
|
|
tt := []struct {
|
|
name string
|
|
inviteID string
|
|
requestBody string
|
|
expectedStatus int
|
|
mockFunc func(ctx context.Context, accountID, initiatorUserID, inviteID string, expiresIn int) (*types.UserInvite, error)
|
|
}{
|
|
{
|
|
name: "successful regenerate with empty body",
|
|
inviteID: testInviteID,
|
|
requestBody: "",
|
|
expectedStatus: http.StatusOK,
|
|
mockFunc: func(ctx context.Context, accountID, initiatorUserID, inviteID string, expiresIn int) (*types.UserInvite, error) {
|
|
assert.Equal(t, 0, expiresIn)
|
|
return &types.UserInvite{
|
|
UserInfo: &types.UserInfo{
|
|
ID: inviteID,
|
|
Email: testEmail,
|
|
},
|
|
InviteToken: "nbi_newtoken12345678901234567890",
|
|
InviteExpiresAt: expiresAt,
|
|
}, nil
|
|
},
|
|
},
|
|
{
|
|
name: "successful regenerate with custom expiration",
|
|
inviteID: testInviteID,
|
|
requestBody: `{"expires_in":7200}`,
|
|
expectedStatus: http.StatusOK,
|
|
mockFunc: func(ctx context.Context, accountID, initiatorUserID, inviteID string, expiresIn int) (*types.UserInvite, error) {
|
|
assert.Equal(t, 7200, expiresIn)
|
|
return &types.UserInvite{
|
|
UserInfo: &types.UserInfo{
|
|
ID: inviteID,
|
|
Email: testEmail,
|
|
},
|
|
InviteToken: "nbi_newtoken12345678901234567890",
|
|
InviteExpiresAt: expiresAt,
|
|
}, nil
|
|
},
|
|
},
|
|
{
|
|
name: "invite not found",
|
|
inviteID: "non-existent-invite",
|
|
requestBody: "",
|
|
expectedStatus: http.StatusNotFound,
|
|
mockFunc: func(ctx context.Context, accountID, initiatorUserID, inviteID string, expiresIn int) (*types.UserInvite, error) {
|
|
return nil, status.Errorf(status.NotFound, "invite not found")
|
|
},
|
|
},
|
|
{
|
|
name: "permission denied",
|
|
inviteID: testInviteID,
|
|
requestBody: "",
|
|
expectedStatus: http.StatusForbidden,
|
|
mockFunc: func(ctx context.Context, accountID, initiatorUserID, inviteID string, expiresIn int) (*types.UserInvite, error) {
|
|
return nil, status.NewPermissionDeniedError()
|
|
},
|
|
},
|
|
{
|
|
name: "missing invite ID",
|
|
inviteID: "",
|
|
requestBody: "",
|
|
expectedStatus: http.StatusUnprocessableEntity,
|
|
mockFunc: nil,
|
|
},
|
|
{
|
|
name: "invalid JSON should return error",
|
|
inviteID: testInviteID,
|
|
requestBody: `{invalid json}`,
|
|
expectedStatus: http.StatusBadRequest,
|
|
mockFunc: nil,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tt {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
am := &mock_server.MockAccountManager{
|
|
RegenerateUserInviteFunc: tc.mockFunc,
|
|
}
|
|
handler := setupInvitesTestHandler(am)
|
|
|
|
var body io.Reader
|
|
if tc.requestBody != "" {
|
|
body = bytes.NewBufferString(tc.requestBody)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/users/invites/"+tc.inviteID+"/regenerate", body)
|
|
req = nbcontext.SetUserAuthInRequest(req, auth.UserAuth{
|
|
UserId: testUserID,
|
|
AccountId: testAccountID,
|
|
})
|
|
if tc.inviteID != "" {
|
|
req = mux.SetURLVars(req, map[string]string{"inviteId": tc.inviteID})
|
|
}
|
|
|
|
rr := httptest.NewRecorder()
|
|
handler.regenerateInvite(rr, req)
|
|
|
|
assert.Equal(t, tc.expectedStatus, rr.Code)
|
|
|
|
if tc.expectedStatus == http.StatusOK {
|
|
var resp api.UserInviteRegenerateResponse
|
|
err := json.NewDecoder(rr.Body).Decode(&resp)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, resp.InviteToken)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDeleteInvite(t *testing.T) {
|
|
tt := []struct {
|
|
name string
|
|
inviteID string
|
|
expectedStatus int
|
|
mockFunc func(ctx context.Context, accountID, initiatorUserID, inviteID string) error
|
|
}{
|
|
{
|
|
name: "successful delete",
|
|
inviteID: testInviteID,
|
|
expectedStatus: http.StatusOK,
|
|
mockFunc: func(ctx context.Context, accountID, initiatorUserID, inviteID string) error {
|
|
return nil
|
|
},
|
|
},
|
|
{
|
|
name: "invite not found",
|
|
inviteID: "non-existent-invite",
|
|
expectedStatus: http.StatusNotFound,
|
|
mockFunc: func(ctx context.Context, accountID, initiatorUserID, inviteID string) error {
|
|
return status.Errorf(status.NotFound, "invite not found")
|
|
},
|
|
},
|
|
{
|
|
name: "permission denied",
|
|
inviteID: testInviteID,
|
|
expectedStatus: http.StatusForbidden,
|
|
mockFunc: func(ctx context.Context, accountID, initiatorUserID, inviteID string) error {
|
|
return status.NewPermissionDeniedError()
|
|
},
|
|
},
|
|
{
|
|
name: "embedded IDP not enabled",
|
|
inviteID: testInviteID,
|
|
expectedStatus: http.StatusPreconditionFailed,
|
|
mockFunc: func(ctx context.Context, accountID, initiatorUserID, inviteID string) error {
|
|
return status.Errorf(status.PreconditionFailed, "invite links are only available with embedded identity provider")
|
|
},
|
|
},
|
|
{
|
|
name: "missing invite ID",
|
|
inviteID: "",
|
|
expectedStatus: http.StatusUnprocessableEntity,
|
|
mockFunc: nil,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tt {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
am := &mock_server.MockAccountManager{
|
|
DeleteUserInviteFunc: tc.mockFunc,
|
|
}
|
|
handler := setupInvitesTestHandler(am)
|
|
|
|
req := httptest.NewRequest(http.MethodDelete, "/api/users/invites/"+tc.inviteID, nil)
|
|
req = nbcontext.SetUserAuthInRequest(req, auth.UserAuth{
|
|
UserId: testUserID,
|
|
AccountId: testAccountID,
|
|
})
|
|
if tc.inviteID != "" {
|
|
req = mux.SetURLVars(req, map[string]string{"inviteId": tc.inviteID})
|
|
}
|
|
|
|
rr := httptest.NewRecorder()
|
|
handler.deleteInvite(rr, req)
|
|
|
|
assert.Equal(t, tc.expectedStatus, rr.Code)
|
|
})
|
|
}
|
|
}
|