import { App as AntApp, ConfigProvider } from 'antd'; import { render, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { createMemoryRouter, RouterProvider, useLocation } from 'react-router-dom'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { AppContext, type AppContextValue } from '../../src/frontend/app-context'; import { I18nProvider } from '../../src/frontend/i18n/react'; import { api } from '../../src/frontend/services/api'; import { mailhubTheme } from '../../src/frontend/theme'; import type { AdminMailboxAccessEntry, AdminUser, InboundMailbox, MailboxAccessType, MailboxPermissions, RuntimeConfig, User } from '../../src/frontend/types'; import Account from '../../src/pages/Account'; import AdminPage from '../../src/pages/Admin'; describe('Mailbox access UI', () => { afterEach(() => vi.restoreAllMocks()); it('keeps the admin mailbox editor in the URL and saves normalized grants', async () => { const browser = userEvent.setup(); const entry = accessEntry(); vi.spyOn(api, 'adminUsers').mockResolvedValue({ users: adminUsers }); vi.spyOn(api, 'adminMailboxAccess').mockResolvedValue({ mailboxes: [entry] }); const save = vi.spyOn(api, 'saveAdminMailboxAccess').mockImplementation(async (_id, grants) => ({ mailbox: { ...entry, grants: grants.map((grant) => ({ user: adminUsers.find((user) => user.id === grant.userId)!, permissions: grant, createdAt: '2026-07-18T00:00:00.000Z', updatedAt: '2026-07-18T00:00:00.000Z' })) } })); const router = createMemoryRouter([{ path: '/admin/:section', element: }], { initialEntries: ['/admin/mailbox-access'] }); renderWithRouter(router, adminContext); const configure = await screen.findByRole('button', { name: /配置权限/ }); expect(configure.style.minHeight).toBe('44px'); await browser.click(configure); await waitFor(() => expect(new URLSearchParams(router.state.location.search).get('mailboxId')).toBe('10')); const drawer = await screen.findByRole('dialog'); expect(within(drawer).getByText(/owner@example\.test/)).toBeTruthy(); expect(within(drawer).getByText('所有者权限')).toBeTruthy(); await browser.click(within(drawer).getByRole('checkbox', { name: '收取邮件' })); await browser.click(within(drawer).getByRole('checkbox', { name: '查看配置' })); await browser.click(within(drawer).getByRole('button', { name: /保\s*存/ })); expect(await screen.findByText('每个用户至少选择一项权限,或移除该授权行。')).toBeTruthy(); expect(save).not.toHaveBeenCalled(); await browser.click(within(drawer).getByRole('checkbox', { name: '发送邮件' })); expect((within(drawer).getByRole('checkbox', { name: '查看配置' }) as HTMLInputElement).checked).toBe(true); await browser.click(within(drawer).getByRole('button', { name: /保\s*存/ })); await waitFor(() => expect(save).toHaveBeenCalledWith(10, [{ userId: 2, view: true, receive: false, send: true }])); expect(await screen.findByText('邮箱权限已保存')).toBeTruthy(); await browser.click(within(drawer).getByRole('button', { name: 'Close' })); await waitFor(() => expect(new URLSearchParams(router.state.location.search).has('mailboxId')).toBe(false)); await waitFor(() => expect(document.activeElement).toBe(configure)); }); it('shows owned and assigned mailboxes in account center with permission-based actions', async () => { const browser = userEvent.setup(); const owned = mailboxFixture(1, 'owned@example.test', 'owner', { view: true, receive: true, send: true }, 2); const assigned = mailboxFixture(2, 'assigned@example.test', 'assigned', { view: true, receive: true, send: false }, 1); const viewOnly = mailboxFixture(3, 'view-only@example.test', 'assigned', { view: true, receive: false, send: false }, 1); const list = vi.spyOn(api, 'inboundMailboxes').mockResolvedValue({ mailboxes: [owned, assigned, viewOnly] }); const router = createMemoryRouter([ { path: '/account', element: }, { path: '/inbox', element: } ], { initialEntries: ['/account'] }); renderWithRouter(router, userContext); expect(await screen.findByRole('heading', { name: '账号与邮箱权限' })).toBeTruthy(); expect(list).toHaveBeenCalledWith('effective'); expect(screen.getAllByText('管理员分配').length).toBeGreaterThan(0); const viewOnlyCard = screen.getByText(viewOnly.address).closest('.ant-card'); expect(viewOnlyCard).toBeTruthy(); expect(within(viewOnlyCard as HTMLElement).queryByRole('button', { name: /打开收件箱/ })).toBeNull(); expect(screen.getAllByRole('button', { name: /管理邮箱/ })).toHaveLength(1); const assignedCard = screen.getByText(assigned.address).closest('.ant-card'); expect(assignedCard).toBeTruthy(); await browser.click(within(assignedCard as HTMLElement).getByRole('button', { name: /打开收件箱/ })); expect(screen.getByTestId('location').textContent).toBe('/inbox?mailboxId=2&folder=INBOX'); }); }); function renderWithRouter(router: ReturnType, context: AppContextValue) { return render( ); } function LocationProbe() { const location = useLocation(); return
{location.pathname}{location.search}
; } function accessEntry(): AdminMailboxAccessEntry { const mailbox = mailboxFixture(10, 'support@example.test', 'owner', { view: true, receive: true, send: true }, 1); return { mailbox, owner: adminUsers[0], grants: [{ user: adminUsers[1], permissions: { view: true, receive: true, send: false }, createdAt: '2026-07-18T00:00:00.000Z', updatedAt: '2026-07-18T00:00:00.000Z' }] }; } function mailboxFixture( id: number, address: string, type: MailboxAccessType, permissions: MailboxPermissions, ownerUserId: number ): InboundMailbox { const [, domain] = address.split('@'); return { id, userId: ownerUserId, ownerUserId, domainId: id, domain, address, localPart: address.split('@')[0], displayName: '', aliases: [], forwardTo: [], keepForwarded: true, quotaMb: 1024, passwordSet: true, passwordRecoverable: false, status: 'active', messageCount: permissions.receive ? 3 : null, unreadCount: permissions.receive ? 1 : null, lastMessageAt: permissions.receive ? '2026-07-18T00:00:00.000Z' : null, access: { type, permissions }, createdAt: '2026-07-18T00:00:00.000Z', updatedAt: '2026-07-18T00:00:00.000Z' }; } const adminUsers: AdminUser[] = [ { id: 1, username: 'owner', email: 'owner@example.test', role: 'admin', status: 'active', resourceCounts: resourceCounts() }, { id: 2, username: 'reader', email: 'reader@example.test', role: 'user', status: 'active', resourceCounts: resourceCounts() } ]; function contextFor(user: User): AppContextValue { return { user, config, refreshBootstrap: vi.fn(async () => undefined), logout: vi.fn(async () => undefined) }; } function resourceCounts() { return { domains: 0, dnsCredentials: 0, apiTokens: 0, inboundMailboxes: 0, inboundMessages: 0, sendEvents: 0, smtpCredential: 0 }; } const config: RuntimeConfig = { appBaseUrl: 'https://mail.example.test', mailHostname: 'mail.example.test', sendingIp: '192.0.2.10', defaultSpfMechanisms: '', dmarcPolicy: 'none', dmarcRua: '', registrationRequiresApproval: false, sendRequiresVerified: true, engagementTrackingEnabled: true, listUnsubscribeMailto: '', listUnsubscribeUrl: '', listUnsubscribePostEnabled: false, feedbackIdEnabled: false, reportAbuseTo: '', csaComplaintsTo: '', bounceAddress: '', bounceEnvelopeEnabled: false }; const adminContext: AppContextValue = contextFor(adminUsers[0]); const userContext: AppContextValue = contextFor({ id: 2, username: 'reader', email: 'reader@example.test', role: 'user', status: 'active' });