OIDCProviderSettingsWiring.test.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. /**
  2. * OIDC provider list: edit, delete, enable toggle and icon actions send
  3. * their request for the provider that was clicked, the edit leaves the
  4. * stored client secret alone unless one is typed, and cancelling the delete
  5. * confirm sends nothing.
  6. */
  7. import { describe, it, expect, beforeEach } from 'vitest';
  8. import { screen, waitFor } from '@testing-library/react';
  9. import userEvent from '@testing-library/user-event';
  10. import { http, HttpResponse } from 'msw';
  11. import { render } from '../utils';
  12. import { server } from '../mocks/server';
  13. import { OIDCProviderSettings } from '../../components/OIDCProviderSettings';
  14. type Captured = { method: string; path: string; body: Record<string, unknown> | null };
  15. let captured: Captured[];
  16. function provider(id: number, name: string, extra: Record<string, unknown> = {}) {
  17. return {
  18. id,
  19. name,
  20. issuer_url: `https://${name.toLowerCase()}.idp.example`,
  21. client_id: `${name.toLowerCase()}-client`,
  22. scopes: 'openid email profile',
  23. is_enabled: true,
  24. auto_create_users: false,
  25. auto_link_existing_accounts: false,
  26. email_claim: 'email',
  27. require_email_verified: true,
  28. group_claim: 'groups',
  29. group_mapping: {},
  30. icon_url: `https://${name.toLowerCase()}.idp.example/icon.png`,
  31. has_icon: true,
  32. default_group_id: null,
  33. is_autologin: false,
  34. is_env_managed: false,
  35. created_at: '2026-01-01T00:00:00Z',
  36. updated_at: '2026-01-01T00:00:00Z',
  37. ...extra,
  38. };
  39. }
  40. async function capture(request: Request) {
  41. const text = await request.text();
  42. captured.push({
  43. method: request.method,
  44. path: new URL(request.url).pathname,
  45. body: text ? JSON.parse(text) : null,
  46. });
  47. }
  48. beforeEach(() => {
  49. captured = [];
  50. server.use(
  51. http.get('*/api/v1/auth/oidc/providers/all', () =>
  52. HttpResponse.json([provider(7, 'Alpha'), provider(9, 'Beta')]),
  53. ),
  54. http.get('*/api/v1/groups/', () => HttpResponse.json([])),
  55. http.put('*/api/v1/auth/oidc/providers/:id', async ({ request }) => {
  56. await capture(request);
  57. return HttpResponse.json(provider(9, 'Beta'));
  58. }),
  59. http.delete('*/api/v1/auth/oidc/providers/:id', async ({ request }) => {
  60. await capture(request);
  61. return HttpResponse.json({ message: 'deleted' });
  62. }),
  63. http.delete('*/api/v1/auth/oidc/providers/:id/icon', async ({ request }) => {
  64. await capture(request);
  65. return new HttpResponse(null, { status: 204 });
  66. }),
  67. http.post('*/api/v1/auth/oidc/providers/:id/icon/refresh', async ({ request }) => {
  68. await capture(request);
  69. return HttpResponse.json(provider(9, 'Beta'));
  70. }),
  71. );
  72. });
  73. describe('OIDCProviderSettings wiring', () => {
  74. it('editing a provider PUTs to its id without client_secret when the secret is left blank', async () => {
  75. const user = userEvent.setup();
  76. render(<OIDCProviderSettings />);
  77. await user.click(await screen.findByTestId('edit-provider-9'));
  78. const scopes = screen.getByDisplayValue('openid email profile');
  79. await user.clear(scopes);
  80. await user.type(scopes, 'openid email');
  81. await user.click(screen.getByRole('button', { name: /^save$/i }));
  82. await waitFor(() => expect(captured).toHaveLength(1));
  83. expect(captured[0].method).toBe('PUT');
  84. expect(captured[0].path).toBe('/api/v1/auth/oidc/providers/9');
  85. expect(captured[0].body).toMatchObject({
  86. name: 'Beta',
  87. issuer_url: 'https://beta.idp.example',
  88. client_id: 'beta-client',
  89. scopes: 'openid email',
  90. });
  91. expect(captured[0].body).not.toHaveProperty('client_secret');
  92. });
  93. it('a typed client secret is sent with the edit', async () => {
  94. const user = userEvent.setup();
  95. render(<OIDCProviderSettings />);
  96. await user.click(await screen.findByTestId('edit-provider-9'));
  97. await user.type(document.querySelector('input[type="password"]') as HTMLInputElement, 'test-secret-1');
  98. await user.click(screen.getByRole('button', { name: /^save$/i }));
  99. await waitFor(() => expect(captured).toHaveLength(1));
  100. expect(captured[0].body).toMatchObject({ client_secret: 'test-secret-1' });
  101. });
  102. it('delete asks first, then DELETEs the clicked provider', async () => {
  103. const user = userEvent.setup();
  104. render(<OIDCProviderSettings />);
  105. // Second row, so a delete of "the first provider" would not pass
  106. await user.click(await screen.findByTestId('delete-provider-9'));
  107. expect(captured).toHaveLength(0);
  108. await user.click(screen.getByRole('button', { name: /^delete$/i }));
  109. await waitFor(() => expect(captured).toHaveLength(1));
  110. expect(captured[0]).toMatchObject({ method: 'DELETE', path: '/api/v1/auth/oidc/providers/9' });
  111. });
  112. it('cancelling the delete confirm sends nothing', async () => {
  113. const user = userEvent.setup();
  114. render(<OIDCProviderSettings />);
  115. await user.click(await screen.findByTestId('delete-provider-7'));
  116. await user.click(screen.getByRole('button', { name: /^cancel$/i }));
  117. await waitFor(() => expect(screen.queryByRole('button', { name: /^delete$/i })).not.toBeInTheDocument());
  118. expect(captured).toHaveLength(0);
  119. });
  120. it('refresh icon and remove icon call the clicked provider id', async () => {
  121. const user = userEvent.setup();
  122. render(<OIDCProviderSettings />);
  123. await user.click(await screen.findByTestId('refresh-icon-9'));
  124. await waitFor(() => expect(captured).toHaveLength(1));
  125. expect(captured[0]).toMatchObject({ method: 'POST', path: '/api/v1/auth/oidc/providers/9/icon/refresh' });
  126. await user.click(screen.getByTestId('remove-icon-7'));
  127. await waitFor(() => expect(captured).toHaveLength(2));
  128. expect(captured[1]).toMatchObject({ method: 'DELETE', path: '/api/v1/auth/oidc/providers/7/icon' });
  129. });
  130. it('the enable toggle sends only {is_enabled:false} for that provider', async () => {
  131. const user = userEvent.setup();
  132. render(<OIDCProviderSettings />);
  133. await screen.findByTestId('edit-provider-9');
  134. // One toggle per provider, in list order
  135. const toggles = screen.getAllByRole('switch');
  136. await user.click(toggles[1]);
  137. await waitFor(() => expect(captured).toHaveLength(1));
  138. expect(captured[0]).toEqual({
  139. method: 'PUT',
  140. path: '/api/v1/auth/oidc/providers/9',
  141. body: { is_enabled: false },
  142. });
  143. });
  144. });