ConnectedAppsSection.test.tsx 3.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. /**
  2. * Settings → API Keys → Connected apps.
  3. */
  4. import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
  5. import { screen, waitFor, within } from '@testing-library/react';
  6. import userEvent from '@testing-library/user-event';
  7. import { render } from '../utils';
  8. import { api, type ConnectedApp } from '../../api/client';
  9. const mockUseAuth = { authEnabled: true };
  10. vi.mock('../../contexts/AuthContext', async (importOriginal) => {
  11. const actual = await importOriginal<typeof import('../../contexts/AuthContext')>();
  12. return { ...actual, useAuth: () => mockUseAuth };
  13. });
  14. import { ConnectedAppsSection } from '../../components/ConnectedAppsSection';
  15. function app(overrides: Partial<ConnectedApp> = {}): ConnectedApp {
  16. return {
  17. id: 1,
  18. name: 'Order Desk',
  19. client_id: 'bba_123',
  20. redirect_uri: 'http://orders.local:8090/auth/callback',
  21. enabled: true,
  22. created_at: '2026-09-26T10:00:00Z',
  23. last_used_at: null,
  24. ...overrides,
  25. };
  26. }
  27. beforeEach(() => {
  28. mockUseAuth.authEnabled = true;
  29. });
  30. afterEach(() => {
  31. vi.restoreAllMocks();
  32. });
  33. describe('ConnectedAppsSection', () => {
  34. it('lists registered apps by client ID, never with a secret', async () => {
  35. vi.spyOn(api, 'listConnectedApps').mockResolvedValue([app()]);
  36. render(<ConnectedAppsSection />);
  37. expect(await screen.findByText('Order Desk')).toBeInTheDocument();
  38. expect(screen.getByText('bba_123')).toBeInTheDocument();
  39. expect(screen.queryByText(/bbs_/)).not.toBeInTheDocument();
  40. });
  41. it('shows the secret once after adding an app', async () => {
  42. vi.spyOn(api, 'listConnectedApps').mockResolvedValue([]);
  43. const create = vi
  44. .spyOn(api, 'createConnectedApp')
  45. .mockResolvedValue(app({ client_secret: 'bbs_only_once' }));
  46. render(<ConnectedAppsSection />);
  47. await screen.findByText('No connected apps yet.');
  48. await userEvent.type(screen.getByLabelText('App name'), 'Order Desk');
  49. await userEvent.type(screen.getByLabelText('Callback URL'), 'http://orders.local:8090/auth/callback');
  50. await userEvent.click(screen.getByRole('button', { name: 'Add app' }));
  51. expect(create).toHaveBeenCalledWith({
  52. name: 'Order Desk',
  53. redirect_uri: 'http://orders.local:8090/auth/callback',
  54. });
  55. const dialog = await screen.findByRole('dialog');
  56. expect(within(dialog).getByText('bbs_only_once')).toBeInTheDocument();
  57. await userEvent.click(within(dialog).getByRole('button', { name: "I've saved it" }));
  58. await waitFor(() => expect(screen.queryByText('bbs_only_once')).not.toBeInTheDocument());
  59. });
  60. it('asks before replacing the secret', async () => {
  61. vi.spyOn(api, 'listConnectedApps').mockResolvedValue([app()]);
  62. const rotate = vi.spyOn(api, 'rotateConnectedAppSecret').mockResolvedValue(app({ client_secret: 'bbs_new' }));
  63. render(<ConnectedAppsSection />);
  64. await userEvent.click(await screen.findByRole('button', { name: 'New secret' }));
  65. expect(rotate).not.toHaveBeenCalled();
  66. const confirm = screen.getByRole('dialog');
  67. await userEvent.click(within(confirm).getByRole('button', { name: 'New secret' }));
  68. expect(rotate).toHaveBeenCalledWith(1);
  69. expect(await screen.findByText('bbs_new')).toBeInTheDocument();
  70. });
  71. it('explains that authentication has to be on first', async () => {
  72. mockUseAuth.authEnabled = false;
  73. const list = vi.spyOn(api, 'listConnectedApps');
  74. render(<ConnectedAppsSection />);
  75. expect(screen.getByText(/need Bambuddy authentication/)).toBeInTheDocument();
  76. expect(list).not.toHaveBeenCalled();
  77. });
  78. });