admin-resources.test.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  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 { createMemoryRouter, RouterProvider } from 'react-router-dom';
  5. import { afterEach, describe, expect, it, vi } from 'vitest';
  6. import { AppContext, type AppContextValue } from '../../src/frontend/app-context';
  7. import { I18nProvider } from '../../src/frontend/i18n/react';
  8. import { api } from '../../src/frontend/services/api';
  9. import { mailhubTheme } from '../../src/frontend/theme';
  10. import AdminPage from '../../src/pages/Admin';
  11. import type { AdminResourceInventory, AdminUser, Domain, RuntimeConfig } from '../../src/frontend/types';
  12. describe('Admin resource migration', () => {
  13. afterEach(() => vi.restoreAllMocks());
  14. it('submits a domain transfer after selecting the domain and target user', async () => {
  15. const user = userEvent.setup();
  16. const transferDomain = vi.spyOn(api, 'transferAdminDomain').mockResolvedValue({ domain });
  17. vi.spyOn(api, 'adminUsers').mockResolvedValue({ users });
  18. vi.spyOn(api, 'adminResources').mockResolvedValue({ inventory });
  19. renderAdminResourcesRoute();
  20. const transferCard = (await screen.findByText('迁移域名')).closest('.ant-card');
  21. expect(transferCard).not.toBeNull();
  22. await selectOption(user, within(transferCard!).getByLabelText('域名'), 'taobiba.com · chendeben');
  23. await selectOption(user, within(transferCard!).getByLabelText('目标用户'), 'chan (#7)');
  24. await user.click(within(transferCard!).getByRole('button', { name: '执行迁移' }));
  25. const dialog = await screen.findByRole('dialog');
  26. expect(dialog.textContent).toContain('确认迁移该域名?');
  27. await user.click(within(dialog).getByRole('button', { name: /确\s*认/ }));
  28. await waitFor(() => expect(transferDomain).toHaveBeenCalledWith(12, {
  29. targetUserId: 7,
  30. dnsCredentialMode: 'domain_only'
  31. }));
  32. });
  33. it('submits a DNS credential transfer after confirming the action', async () => {
  34. const user = userEvent.setup();
  35. const transferCredential = vi.spyOn(api, 'transferAdminDnsCredential').mockResolvedValue({
  36. credential: inventory.users[0].dnsCredentials[0]
  37. });
  38. vi.spyOn(api, 'adminUsers').mockResolvedValue({ users });
  39. vi.spyOn(api, 'adminResources').mockResolvedValue({ inventory });
  40. renderAdminResourcesRoute();
  41. const transferCard = (await screen.findByText('迁移 DNS 凭据')).closest('.ant-card');
  42. expect(transferCard).not.toBeNull();
  43. await selectOption(user, within(transferCard!).getByLabelText('DNS 凭据'), 'DNSPod · taobiba.com · chendeben');
  44. await selectOption(user, within(transferCard!).getByLabelText('目标用户'), 'chan (#7)');
  45. await user.click(within(transferCard!).getByRole('button', { name: '执行迁移' }));
  46. const dialog = await screen.findByRole('dialog');
  47. expect(dialog.textContent).toContain('确认迁移该 DNS 凭据?');
  48. await user.click(within(dialog).getByRole('button', { name: /确\s*认/ }));
  49. await waitFor(() => expect(transferCredential).toHaveBeenCalledWith(8, { targetUserId: 7 }));
  50. });
  51. });
  52. async function selectOption(user: ReturnType<typeof userEvent.setup>, input: HTMLElement, option: string) {
  53. await user.click(input);
  54. await user.click(await screen.findByText(option, { selector: '.ant-select-item-option-content' }));
  55. }
  56. function renderAdminResourcesRoute() {
  57. const router = createMemoryRouter([{ path: '/admin/:section', element: <AdminPage /> }], {
  58. initialEntries: ['/admin/resources']
  59. });
  60. return render(
  61. <ConfigProvider theme={{ ...mailhubTheme, token: { ...mailhubTheme.token, motion: false } }}>
  62. <AntApp>
  63. <I18nProvider>
  64. <AppContext.Provider value={appContext}>
  65. <RouterProvider router={router} />
  66. </AppContext.Provider>
  67. </I18nProvider>
  68. </AntApp>
  69. </ConfigProvider>
  70. );
  71. }
  72. const resourceCounts = {
  73. domains: 1,
  74. dnsCredentials: 1,
  75. apiTokens: 0,
  76. inboundMailboxes: 0,
  77. inboundMessages: 0,
  78. sendEvents: 0,
  79. smtpCredential: 0
  80. };
  81. const users: AdminUser[] = [
  82. { id: 1, username: 'chendeben', email: 'owner@example.test', role: 'admin', status: 'active', resourceCounts },
  83. { id: 7, username: 'chan', email: 'chan@example.test', role: 'user', status: 'active', resourceCounts }
  84. ];
  85. const domain: Domain = {
  86. id: 12,
  87. userId: 1,
  88. dnsCredentialId: 8,
  89. smtpRelayId: null,
  90. domain: 'taobiba.com',
  91. selector: 'mailhub',
  92. verificationToken: 'verify-token',
  93. dkimPublic: 'public-key',
  94. senderHost: 'mail.taobiba.com',
  95. sendingIp: '192.0.2.10',
  96. spfExtra: '',
  97. dmarcPolicy: 'none',
  98. dmarcRua: '',
  99. catchAllAddress: '',
  100. mailboxSignupEnabled: false,
  101. status: {},
  102. createdAt: '2026-07-19T00:00:00.000Z',
  103. updatedAt: '2026-07-19T00:00:00.000Z'
  104. };
  105. const inventory: AdminResourceInventory = {
  106. users: [
  107. {
  108. user: users[0],
  109. domains: [domain],
  110. dnsCredentials: [{
  111. id: 8,
  112. userId: 1,
  113. name: 'DNSPod',
  114. provider: 'dnspod',
  115. zoneName: 'taobiba.com',
  116. defaultTtl: 600,
  117. credentialSet: true,
  118. createdAt: '2026-07-19T00:00:00.000Z',
  119. updatedAt: '2026-07-19T00:00:00.000Z'
  120. }],
  121. smtpCredential: null,
  122. apiTokens: [],
  123. inboundMailboxes: [],
  124. inboundMessageCount: 0,
  125. sendEventCount: 0
  126. }
  127. ],
  128. warnings: []
  129. };
  130. const runtimeConfig: RuntimeConfig = {
  131. appBaseUrl: 'https://mail.example.test',
  132. mailHostname: 'mail.example.test',
  133. sendingIp: '192.0.2.10',
  134. defaultSpfMechanisms: '',
  135. dmarcPolicy: 'none',
  136. dmarcRua: '',
  137. registrationRequiresApproval: false,
  138. sendRequiresVerified: true,
  139. engagementTrackingEnabled: true,
  140. listUnsubscribeMailto: '',
  141. listUnsubscribeUrl: '',
  142. listUnsubscribePostEnabled: false,
  143. feedbackIdEnabled: false,
  144. reportAbuseTo: '',
  145. csaComplaintsTo: '',
  146. bounceAddress: '',
  147. bounceEnvelopeEnabled: false
  148. };
  149. const appContext: AppContextValue = {
  150. user: users[0],
  151. config: runtimeConfig,
  152. refreshBootstrap: vi.fn(async () => undefined),
  153. logout: vi.fn(async () => undefined)
  154. };