api-token-secret.test.tsx 3.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. import { App as AntApp, ConfigProvider } from 'antd';
  2. import { render, screen, waitFor, within } from '@testing-library/react';
  3. import userEvent from '@testing-library/user-event';
  4. import { describe, expect, it, vi } from 'vitest';
  5. import { AppContext, type AppContextValue } from '../../src/frontend/app-context';
  6. import { I18nProvider } from '../../src/frontend/i18n/react';
  7. import { api } from '../../src/frontend/services/api';
  8. import { mailhubTheme } from '../../src/frontend/theme';
  9. import type { ApiToken, RuntimeConfig } from '../../src/frontend/types';
  10. import ApiTokens from '../../src/pages/ApiTokens';
  11. describe('API token one-time secret', () => {
  12. it('removes the full token after the explicit acknowledgement', async () => {
  13. const user = userEvent.setup();
  14. const summary: ApiToken = {
  15. id: 9,
  16. name: 'CI sender',
  17. tokenPrefix: 'mh_12345678',
  18. scopes: ['send'],
  19. status: 'active',
  20. createdAt: '2026-07-14T00:00:00.000Z'
  21. };
  22. const fullToken = 'mh_12345678.full-secret-value';
  23. vi.spyOn(api, 'apiTokens')
  24. .mockResolvedValueOnce({ tokens: [] })
  25. .mockResolvedValue({ tokens: [summary] });
  26. const createToken = vi.spyOn(api, 'createApiToken').mockResolvedValue({
  27. token: { ...summary, token: fullToken }
  28. });
  29. renderPage();
  30. await screen.findByText('创建 Token 后可复制完整密钥;历史 Token 只显示前缀,示例中使用占位符。');
  31. await user.click(screen.getAllByRole('button', { name: /创建 Token/ })[0]);
  32. const editor = await screen.findByRole('dialog');
  33. await user.type(within(editor).getByLabelText('名称'), 'CI sender');
  34. await user.click(within(editor).getByRole('button', { name: /创建 Token/ }));
  35. await waitFor(() => expect(createToken).toHaveBeenCalledWith({
  36. name: 'CI sender',
  37. scopes: ['send'],
  38. expiresAt: null
  39. }));
  40. expect(await screen.findByText(fullToken)).not.toBeNull();
  41. const reveal = screen.getByRole('dialog', { name: 'API Token 已创建' });
  42. await user.click(within(reveal).getByRole('button', { name: /确.*认/ }));
  43. await waitFor(() => expect(screen.queryByRole('dialog', { name: 'API Token 已创建' })).toBeNull());
  44. expect(await screen.findByText('mh_12345678...')).not.toBeNull();
  45. expect(screen.queryByRole('dialog', { name: 'API Token 已创建' })).toBeNull();
  46. });
  47. });
  48. function renderPage() {
  49. const context: AppContextValue = {
  50. user: { id: 1, username: 'operator', email: 'operator@example.test', role: 'admin', status: 'active' },
  51. config,
  52. refreshBootstrap: vi.fn(async () => undefined),
  53. logout: vi.fn(async () => undefined)
  54. };
  55. return render(
  56. <ConfigProvider theme={{ ...mailhubTheme, token: { ...mailhubTheme.token, motion: false } }}>
  57. <AntApp>
  58. <I18nProvider>
  59. <AppContext.Provider value={context}>
  60. <ApiTokens />
  61. </AppContext.Provider>
  62. </I18nProvider>
  63. </AntApp>
  64. </ConfigProvider>
  65. );
  66. }
  67. const config: RuntimeConfig = {
  68. appBaseUrl: 'https://mail.example.test',
  69. mailHostname: 'mail.example.test',
  70. sendingIp: '192.0.2.10',
  71. defaultSpfMechanisms: '',
  72. dmarcPolicy: 'none',
  73. dmarcRua: '',
  74. sendRequiresVerified: true,
  75. engagementTrackingEnabled: true,
  76. listUnsubscribeMailto: '',
  77. listUnsubscribeUrl: '',
  78. listUnsubscribePostEnabled: false,
  79. feedbackIdEnabled: false,
  80. reportAbuseTo: '',
  81. csaComplaintsTo: '',
  82. bounceAddress: '',
  83. bounceEnvelopeEnabled: false
  84. };