diff --git a/ui/litellm-dashboard/src/components/UIAccessControlForm.test.tsx b/ui/litellm-dashboard/src/components/UIAccessControlForm.test.tsx new file mode 100644 index 00000000000..b1cf384e441 --- /dev/null +++ b/ui/litellm-dashboard/src/components/UIAccessControlForm.test.tsx @@ -0,0 +1,226 @@ +import React from 'react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import userEvent from '@testing-library/user-event'; +import { renderWithProviders, screen, waitFor } from "../../tests/test-utils"; +import UIAccessControlForm from './UIAccessControlForm'; +import * as networking from './networking'; + +// Mock the networking module +vi.mock('./networking', () => ({ + getSSOSettings: vi.fn(), + updateSSOSettings: vi.fn(), +})); + +// Mock NotificationManager +vi.mock('./molecules/notifications_manager', () => ({ + default: { + fromBackend: vi.fn(), + }, +})); + +describe('UIAccessControlForm', () => { + const mockAccessToken = 'test-access-token'; + const mockOnSuccess = vi.fn(); + + beforeEach(() => { + vi.resetAllMocks(); + + // Default mock for getSSOSettings + vi.mocked(networking.getSSOSettings).mockResolvedValue({ + values: { + ui_access_mode: { + type: 'restricted_sso_group', + restricted_sso_group: 'admin-group', + sso_group_jwt_field: 'groups', + } + } + }); + + // Default mock for updateSSOSettings + vi.mocked(networking.updateSSOSettings).mockResolvedValue({}); + }); + + it('renders the form with all expected fields', async () => { + renderWithProviders( + + ); + + // Wait for the form to load + await waitFor(() => { + expect(screen.getByText('UI Access Mode')).toBeInTheDocument(); + }); + + expect(screen.getByText('Configure who can access the UI interface and how group information is extracted from JWT tokens.')).toBeInTheDocument(); + expect(screen.getByText('SSO Group JWT Field')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Update UI Access Control' })).toBeInTheDocument(); + }); + + it('sends ui_access_mode="none" when ui_access_mode_type is "all_authenticated_users"', async () => { + const user = userEvent.setup(); + + renderWithProviders( + + ); + + // Wait for form to load + await waitFor(() => { + expect(screen.getByText('UI Access Mode')).toBeInTheDocument(); + }); + + // Select "All Authenticated Users" from the dropdown + const accessModeSelect = screen.getByRole('combobox'); + await user.click(accessModeSelect); + + const allUsersOption = screen.getByText('All Authenticated Users'); + await user.click(allUsersOption); + + // Fill in the JWT field (optional but good for completeness) + const jwtField = screen.getByPlaceholderText('groups'); + await user.clear(jwtField); + await user.type(jwtField, 'user_groups'); + + // Submit the form + const submitButton = screen.getByRole('button', { name: 'Update UI Access Control' }); + await user.click(submitButton); + + // Verify that updateSSOSettings was called with the correct payload + await waitFor(() => { + expect(networking.updateSSOSettings).toHaveBeenCalledWith( + mockAccessToken, + { + ui_access_mode: "none" + } + ); + }); + + expect(mockOnSuccess).toHaveBeenCalled(); + }); + + it('sends object structure when ui_access_mode_type is "restricted_sso_group"', async () => { + const user = userEvent.setup(); + + renderWithProviders( + + ); + + // Wait for form to load + await waitFor(() => { + expect(screen.getByText('UI Access Mode')).toBeInTheDocument(); + }); + + // Select "Restricted SSO Group" from the dropdown + const accessModeSelect = screen.getByRole('combobox'); + await user.click(accessModeSelect); + + const restrictedOption = screen.getByText('Restricted SSO Group'); + await user.click(restrictedOption); + + // Fill in the restricted SSO group field (should appear after selection) + await waitFor(() => { + expect(screen.getByText('Restricted SSO Group')).toBeInTheDocument(); + }); + + const restrictedGroupInput = screen.getByPlaceholderText('ui-access-group'); + await user.clear(restrictedGroupInput); + await user.type(restrictedGroupInput, 'admin-users'); + + // Fill in the JWT field + const jwtField = screen.getByPlaceholderText('groups'); + await user.clear(jwtField); + await user.type(jwtField, 'team_groups'); + + // Submit the form + const submitButton = screen.getByRole('button', { name: 'Update UI Access Control' }); + await user.click(submitButton); + + // Verify that updateSSOSettings was called with the correct object structure + await waitFor(() => { + expect(networking.updateSSOSettings).toHaveBeenCalledWith( + mockAccessToken, + { + ui_access_mode: { + type: 'restricted_sso_group', + restricted_sso_group: 'admin-users', + sso_group_jwt_field: 'team_groups', + } + } + ); + }); + + expect(mockOnSuccess).toHaveBeenCalled(); + }); + + it('shows restricted SSO group field only when restricted_sso_group is selected', async () => { + const user = userEvent.setup(); + + renderWithProviders( + + ); + + // Wait for form to load + await waitFor(() => { + expect(screen.getByText('UI Access Mode')).toBeInTheDocument(); + }); + + // Initially, with "restricted_sso_group" selected, the field should be visible + expect(screen.getByText('Restricted SSO Group')).toBeInTheDocument(); + + // Switch to "All Authenticated Users" + const accessModeSelect = screen.getByRole('combobox'); + await user.click(accessModeSelect); + + const allUsersOption = screen.getByText('All Authenticated Users'); + await user.click(allUsersOption); + + // The restricted SSO group field should disappear + await waitFor(() => { + expect(screen.queryByText('Restricted SSO Group')).not.toBeInTheDocument(); + }); + }); + + it('handles API errors gracefully', async () => { + const user = userEvent.setup(); + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + // Mock updateSSOSettings to reject + vi.mocked(networking.updateSSOSettings).mockRejectedValue(new Error('API Error')); + + renderWithProviders( + + ); + + // Wait for form to load + await waitFor(() => { + expect(screen.getByText('UI Access Mode')).toBeInTheDocument(); + }); + + // Submit the form + const submitButton = screen.getByRole('button', { name: 'Update UI Access Control' }); + await user.click(submitButton); + + // Verify error handling + await waitFor(() => { + expect(consoleSpy).toHaveBeenCalledWith('Failed to save UI access settings:', expect.any(Error)); + }); + + // onSuccess should not be called on error + expect(mockOnSuccess).not.toHaveBeenCalled(); + + consoleSpy.mockRestore(); + }); +});