| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327 |
- /**
- * Tests for OIDCProviderSettings — focused on the auto_link / require_email_verified
- * toggle interaction (SEC-1/SEC-6 UI enforcement).
- */
- import { describe, it, expect, beforeEach } from 'vitest';
- import { screen, waitFor, fireEvent } from '@testing-library/react';
- import userEvent from '@testing-library/user-event';
- import { render } from '../utils';
- import { OIDCProviderSettings } from '../../components/OIDCProviderSettings';
- import { http, HttpResponse } from 'msw';
- import { server } from '../mocks/server';
- const mockProviders = [
- {
- id: 1,
- name: 'TestIdP',
- issuer_url: 'https://idp.example.com',
- client_id: 'test-client',
- scopes: 'openid email profile',
- is_enabled: true,
- auto_create_users: false,
- auto_link_existing_accounts: false,
- email_claim: 'email',
- require_email_verified: true,
- icon_url: null,
- has_icon: false,
- default_group_id: null,
- created_at: '2026-01-01T00:00:00Z',
- updated_at: '2026-01-01T00:00:00Z',
- },
- ];
- beforeEach(() => {
- server.use(
- http.get('/api/v1/auth/oidc/providers/all', () => HttpResponse.json(mockProviders))
- );
- });
- describe('OIDCProviderSettings', () => {
- describe('ProviderForm — require_email_verified description logic', () => {
- it('shows standard description when require_email_verified is on and auto_link is off', async () => {
- server.use(http.get('/api/v1/auth/oidc/providers/all', () => HttpResponse.json([])));
- render(<OIDCProviderSettings />);
- await waitFor(() => {
- expect(screen.getAllByRole('button', { name: /Add Provider/i })[0]).toBeInTheDocument();
- });
- await userEvent.click(screen.getAllByRole('button', { name: /Add Provider/i })[0]);
- await waitFor(() => {
- // Default state: require_email_verified=true, auto_link=false → standard description
- expect(
- screen.getByText(/only.*accept.*email.*verified/i)
- ).toBeInTheDocument();
- });
- });
- it('shows "Disable auto-link first" description when auto_link is enabled', async () => {
- server.use(http.get('/api/v1/auth/oidc/providers/all', () => HttpResponse.json([])));
- const user = userEvent.setup();
- render(<OIDCProviderSettings />);
- await waitFor(() => {
- expect(screen.getAllByRole('button', { name: /Add Provider/i })[0]).toBeInTheDocument();
- });
- await user.click(screen.getAllByRole('button', { name: /Add Provider/i })[0]);
- await waitFor(() => {
- expect(screen.getByText(/Auto.*Link/i)).toBeInTheDocument();
- });
- // Find the Auto Link switch by aria-label or by position
- const switches = screen.getAllByRole('switch');
- // Switches order in form: Enabled, AutoCreate, AutoLink, RequireEmailVerified
- // AutoLink is the 3rd switch (index 2)
- const autoLinkSwitch = switches[2];
- await user.click(autoLinkSwitch);
- await waitFor(() => {
- expect(
- screen.getByText(/disable auto.?link first/i)
- ).toBeInTheDocument();
- });
- });
- it('shows warning text when require_email_verified is toggled off', async () => {
- server.use(http.get('/api/v1/auth/oidc/providers/all', () => HttpResponse.json([])));
- const user = userEvent.setup();
- render(<OIDCProviderSettings />);
- await waitFor(() => {
- expect(screen.getAllByRole('button', { name: /Add Provider/i })[0]).toBeInTheDocument();
- });
- await user.click(screen.getAllByRole('button', { name: /Add Provider/i })[0]);
- await waitFor(() => {
- expect(screen.getByText(/Require Email Verified/i)).toBeInTheDocument();
- });
- // RequireEmailVerified is the 4th switch (index 3)
- const switches = screen.getAllByRole('switch');
- const reqEvSwitch = switches[3];
- await user.click(reqEvSwitch);
- await waitFor(() => {
- expect(
- screen.getByText(/warning.*accept.*without.*verif/i)
- ).toBeInTheDocument();
- });
- });
- it('shows security warning when auto_link is enabled with a custom email claim', async () => {
- server.use(http.get('/api/v1/auth/oidc/providers/all', () => HttpResponse.json([])));
- const user = userEvent.setup();
- render(<OIDCProviderSettings />);
- await waitFor(() => {
- expect(screen.getAllByRole('button', { name: /Add Provider/i })[0]).toBeInTheDocument();
- });
- await user.click(screen.getAllByRole('button', { name: /Add Provider/i })[0]);
- await waitFor(() => {
- expect(screen.getByText(/Auto.*Link/i)).toBeInTheDocument();
- });
- // Enable auto_link (switch index 2)
- const autoLinkSwitch = screen.getAllByRole('switch')[2];
- await user.click(autoLinkSwitch);
- // Change email claim to a custom value via fireEvent to bypass the onChange fallback
- const emailClaimInput = screen.getByPlaceholderText('email');
- fireEvent.change(emailClaimInput, { target: { value: 'preferred_username' } });
- await waitFor(() => {
- expect(screen.getByText(/tenant-administered/i)).toBeInTheDocument();
- });
- });
- });
- describe('Provider info view', () => {
- it('renders email_claim and require_email_verified fields in provider details', async () => {
- render(<OIDCProviderSettings />);
- await waitFor(() => {
- expect(screen.getByText('TestIdP')).toBeInTheDocument();
- });
- // The provider card shows field labels in the details section
- expect(screen.getByText(/Email Claim/i)).toBeInTheDocument();
- expect(screen.getByText(/Require Email Verified/i)).toBeInTheDocument();
- });
- it('renders Default Group label in provider details', async () => {
- render(<OIDCProviderSettings />);
- await waitFor(() => {
- expect(screen.getByText('TestIdP')).toBeInTheDocument();
- });
- expect(screen.getByText(/Default Group/i)).toBeInTheDocument();
- });
- it('shows Viewers fallback label when default_group_id is null', async () => {
- render(<OIDCProviderSettings />);
- await waitFor(() => {
- expect(screen.getByText('TestIdP')).toBeInTheDocument();
- });
- // null default_group_id should display the Viewers fallback text
- expect(screen.getByText(/Viewers.*default/i)).toBeInTheDocument();
- });
- it('shows group name when default_group_id matches a known group', async () => {
- server.use(
- http.get('/api/v1/auth/oidc/providers/all', () =>
- HttpResponse.json([{ ...mockProviders[0], default_group_id: 2 }])
- )
- );
- render(<OIDCProviderSettings />);
- await waitFor(() => {
- expect(screen.getByText('TestIdP')).toBeInTheDocument();
- });
- // default_group_id=2 matches Operators in the global MSW mock
- expect(screen.getByText('Operators')).toBeInTheDocument();
- });
- });
- describe('ProviderForm — default group dropdown', () => {
- it('renders a Default Group select in the create form', async () => {
- server.use(http.get('/api/v1/auth/oidc/providers/all', () => HttpResponse.json([])));
- render(<OIDCProviderSettings />);
- await waitFor(() => {
- expect(screen.getAllByRole('button', { name: /Add Provider/i })[0]).toBeInTheDocument();
- });
- await userEvent.click(screen.getAllByRole('button', { name: /Add Provider/i })[0]);
- await waitFor(() => {
- expect(screen.getByText(/Default Group/i)).toBeInTheDocument();
- });
- // Dropdown should render with Viewers fallback option
- const select = screen.getByRole('combobox');
- expect(select).toBeInTheDocument();
- expect(screen.getByText(/Viewers.*default/i)).toBeInTheDocument();
- });
- it('populates Default Group dropdown with groups from API', async () => {
- server.use(http.get('/api/v1/auth/oidc/providers/all', () => HttpResponse.json([])));
- render(<OIDCProviderSettings />);
- await waitFor(() => {
- expect(screen.getAllByRole('button', { name: /Add Provider/i })[0]).toBeInTheDocument();
- });
- await userEvent.click(screen.getAllByRole('button', { name: /Add Provider/i })[0]);
- await waitFor(() => {
- // Global MSW mock returns Administrators, Operators, Viewers
- const options = screen.getAllByRole('option');
- const optionTexts = options.map((o) => o.textContent);
- expect(optionTexts).toContain('Operators');
- expect(optionTexts).toContain('Administrators');
- });
- });
- });
- // #1333: icon proxy — preview uses the backend proxy URL (never icon_url
- // directly) and the admin gets explicit Refresh / Remove buttons.
- describe('Icon proxy (#1333)', () => {
- it('renders icon preview via the backend proxy URL when has_icon is true', async () => {
- server.use(
- http.get('/api/v1/auth/oidc/providers/all', () =>
- HttpResponse.json([
- {
- ...mockProviders[0],
- id: 42,
- icon_url: 'https://idp.example.com/icon.png',
- has_icon: true,
- },
- ])
- )
- );
- render(<OIDCProviderSettings />);
- await waitFor(() => {
- expect(screen.getByText('TestIdP')).toBeInTheDocument();
- });
- const img = screen.getByAltText('TestIdP') as HTMLImageElement;
- expect(img.getAttribute('src')).toBe('/api/v1/auth/oidc/providers/42/icon');
- });
- it('exposes Refresh and Remove buttons when has_icon is true', async () => {
- server.use(
- http.get('/api/v1/auth/oidc/providers/all', () =>
- HttpResponse.json([
- { ...mockProviders[0], id: 99, icon_url: 'https://idp.example.com/i.png', has_icon: true },
- ])
- )
- );
- render(<OIDCProviderSettings />);
- await waitFor(() => {
- expect(screen.getByTestId('refresh-icon-99')).toBeInTheDocument();
- });
- expect(screen.getByTestId('remove-icon-99')).toBeInTheDocument();
- });
- it('hides Remove button when has_icon is false', async () => {
- server.use(
- http.get('/api/v1/auth/oidc/providers/all', () =>
- HttpResponse.json([
- // icon_url set but no cached bytes → Refresh visible, Remove hidden.
- { ...mockProviders[0], id: 100, icon_url: 'https://idp.example.com/i.png', has_icon: false },
- ])
- )
- );
- render(<OIDCProviderSettings />);
- await waitFor(() => {
- expect(screen.getByTestId('refresh-icon-100')).toBeInTheDocument();
- });
- expect(screen.queryByTestId('remove-icon-100')).not.toBeInTheDocument();
- });
- it('hides both buttons when icon_url is not set', async () => {
- server.use(
- http.get('/api/v1/auth/oidc/providers/all', () =>
- HttpResponse.json([{ ...mockProviders[0], id: 101, icon_url: null, has_icon: false }])
- )
- );
- render(<OIDCProviderSettings />);
- await waitFor(() => {
- expect(screen.getByText('TestIdP')).toBeInTheDocument();
- });
- expect(screen.queryByTestId('refresh-icon-101')).not.toBeInTheDocument();
- expect(screen.queryByTestId('remove-icon-101')).not.toBeInTheDocument();
- });
- it('swaps in Globe fallback when icon image fails to load', async () => {
- // I3 (#1333 review): admin preview must show a meaningful fallback
- // instead of an unexplained gap (display: none) when the proxy
- // endpoint returns 404 (e.g. race with DELETE /icon).
- server.use(
- http.get('/api/v1/auth/oidc/providers/all', () =>
- HttpResponse.json([
- { ...mockProviders[0], id: 102, icon_url: 'https://idp.example.com/i.png', has_icon: true },
- ])
- )
- );
- render(<OIDCProviderSettings />);
- const img = (await screen.findByAltText('TestIdP')) as HTMLImageElement;
- fireEvent.error(img);
- // After error: <img> removed, Globe-fallback rendered. Confirm by
- // asserting the alt text is gone and the Globe SVG is present.
- await waitFor(() => {
- expect(screen.queryByAltText('TestIdP')).not.toBeInTheDocument();
- });
- });
- });
- });
|