import { App as AntApp, ConfigProvider } from 'antd'; import { act, fireEvent, 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, WebmailLogin } 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('replaces only the selected mailbox-user grants in bulk and reports detailed results', async () => { const browser = userEvent.setup(); const entries = [ accessEntryFixture(11, 'support-1@example.test'), accessEntryFixture(12, 'support-2@example.test') ]; vi.spyOn(api, 'adminUsers').mockResolvedValue({ users: adminUsers }); vi.spyOn(api, 'adminMailboxAccess').mockResolvedValue({ mailboxes: entries }); const bulkUpdate = vi.spyOn(api, 'bulkUpdateAdminMailboxAccess').mockResolvedValue({ mailboxes: entries, summary: { mailboxCount: 2, userCount: 2, changedGrantCount: 2, skippedOwnerCount: 2 } }); renderMailboxAccessRoute(); const search = await screen.findByRole('searchbox', { name: '搜索邮箱、所有者或邮箱地址' }); await browser.click(await screen.findByRole('checkbox', { name: `选择邮箱 · ${entries[0].mailbox.address}` })); await browser.click(screen.getByRole('checkbox', { name: `选择邮箱 · ${entries[1].mailbox.address}` })); expect(screen.getByText('已选择邮箱 2')).toBeTruthy(); await browser.click(screen.getByRole('button', { name: '批量授权 (2)' })); const dialog = await screen.findByRole('dialog', { name: '批量配置邮箱权限' }); expect(within(dialog).getByText(entries[0].mailbox.address)).toBeTruthy(); expect(within(dialog).getByText(entries[1].mailbox.address)).toBeTruthy(); expect(within(dialog).getByRole('radiogroup', { name: '批量操作' }).getAttribute('aria-required')).toBe('true'); expect(within(dialog).getByText(/现有权限替换为下方权限/)).toBeTruthy(); await browser.click(within(dialog).getByRole('combobox', { name: '授权用户' })); await browser.click(await screen.findByText('owner · owner@example.test', { selector: '.ant-select-item-option-content' })); await browser.click(within(dialog).getByRole('combobox', { name: '授权用户' })); await browser.click(await screen.findByText('reader · reader@example.test', { selector: '.ant-select-item-option-content' })); expect(within(dialog).getByText('2 个邮箱 × 2 个用户 = 4 个组合')).toBeTruthy(); expect(within(dialog).getByText('预计跳过邮箱所有者组合 2')).toBeTruthy(); await browser.click(within(dialog).getByRole('checkbox', { name: '收取邮件' })); const viewPermission = within(dialog).getByRole('checkbox', { name: '查看配置' }) as HTMLInputElement; expect(viewPermission.checked).toBe(true); expect(viewPermission.disabled).toBe(true); expect(within(dialog).getByText(/收取邮件或发送邮件会自动包含查看配置权限/)).toBeTruthy(); await browser.click(within(dialog).getByRole('button', { name: '应用权限' })); await waitFor(() => expect(bulkUpdate).toHaveBeenCalledWith({ mailboxIds: [11, 12], userIds: [1, 2], operation: 'upsert', permissions: { view: true, receive: true, send: false } })); expect(await screen.findByText('批量邮箱权限已更新: 已更改 2 · 未变化 0 · 跳过所有者 2')).toBeTruthy(); expect(screen.queryByRole('dialog', { name: '批量配置邮箱权限' })).toBeNull(); expect((screen.getByRole('button', { name: '批量授权' }) as HTMLButtonElement).disabled).toBe(true); await waitFor(() => expect(document.activeElement).toBe(search)); }); it('offers disabled users only for removal and drops them before switching back to grant mode', async () => { const browser = userEvent.setup(); const entry = accessEntryFixture(13, 'disabled-removal@example.test'); vi.spyOn(api, 'adminUsers').mockResolvedValue({ users: adminUsers }); vi.spyOn(api, 'adminMailboxAccess').mockResolvedValue({ mailboxes: [entry] }); const bulkUpdate = vi.spyOn(api, 'bulkUpdateAdminMailboxAccess'); renderMailboxAccessRoute(); await browser.click(await screen.findByRole('checkbox', { name: `选择邮箱 · ${entry.mailbox.address}` })); await browser.click(screen.getByRole('button', { name: '批量授权 (1)' })); const dialog = await screen.findByRole('dialog', { name: '批量配置邮箱权限' }); const disabledUserLabel = 'disabled · disabled@example.test · 已禁用'; await browser.click(within(dialog).getByRole('combobox', { name: '授权用户' })); expect(screen.getByText('pending · pending@example.test', { selector: '.ant-select-item-option-content' })).toBeTruthy(); expect(screen.queryByText(disabledUserLabel, { selector: '.ant-select-item-option-content' })).toBeNull(); await browser.keyboard('{Escape}'); await browser.click(within(dialog).getByText('移除授权')); expect(within(dialog).getByText(/移除授权时可选择已禁用用户/)).toBeTruthy(); await browser.click(within(dialog).getByRole('combobox', { name: '授权用户' })); await browser.click(await screen.findByText(disabledUserLabel, { selector: '.ant-select-item-option-content' })); await browser.keyboard('{Escape}'); expect((within(dialog).getByRole('button', { name: '移除授权' }) as HTMLButtonElement).disabled).toBe(false); await browser.click(within(dialog).getByText('设置或替换权限')); expect((within(dialog).getByRole('button', { name: '应用权限' }) as HTMLButtonElement).disabled).toBe(true); await browser.click(within(dialog).getByRole('combobox', { name: '授权用户' })); expect(screen.queryByText(disabledUserLabel, { selector: '.ant-select-item-option-content' })).toBeNull(); expect(bulkUpdate).not.toHaveBeenCalled(); }); it('previews only five selected mailboxes, clears hidden selections, and prunes removed IDs after reload', async () => { const browser = userEvent.setup(); const entries = Array.from({ length: 6 }, (_item, index) => ( accessEntryFixture(20 + index, `mailbox-${index + 1}@example.test`) )); vi.spyOn(api, 'adminUsers').mockResolvedValue({ users: adminUsers }); const list = vi.spyOn(api, 'adminMailboxAccess') .mockResolvedValueOnce({ mailboxes: entries }) .mockResolvedValueOnce({ mailboxes: entries.slice(0, 5) }); renderMailboxAccessRoute(); await browser.click(await screen.findByRole('checkbox', { name: '选择全部筛选结果 (6)' })); expect(screen.getByText('已选择邮箱 6')).toBeTruthy(); await browser.click(screen.getByRole('button', { name: '批量授权 (6)' })); const dialog = await screen.findByRole('dialog', { name: '批量配置邮箱权限' }); for (const entry of entries.slice(0, 5)) { expect(within(dialog).getByText(entry.mailbox.address)).toBeTruthy(); } expect(within(dialog).queryByText(entries[5].mailbox.address)).toBeNull(); expect(within(dialog).getByText('+1')).toBeTruthy(); await browser.click(within(dialog).getByRole('button', { name: '取消' })); await waitFor(() => expect(screen.queryByRole('dialog', { name: '批量配置邮箱权限' })).toBeNull()); const search = screen.getByRole('searchbox', { name: '搜索邮箱、所有者或邮箱地址' }); await browser.type(search, 'mailbox-6'); expect(await screen.findByText('当前筛选中 1')).toBeTruthy(); await browser.click(screen.getByRole('button', { name: '清空选择' })); expect(screen.queryByText('已选择邮箱 6')).toBeNull(); await browser.click(screen.getByRole('checkbox', { name: `选择邮箱 · ${entries[5].mailbox.address}` })); expect(screen.getByText('已选择邮箱 1')).toBeTruthy(); await browser.click(screen.getByRole('button', { name: /刷新/ })); await waitFor(() => expect(list).toHaveBeenCalledTimes(2)); await waitFor(() => expect(screen.queryByText('已选择邮箱 1')).toBeNull()); expect((screen.getByRole('button', { name: '批量授权' }) as HTMLButtonElement).disabled).toBe(true); }); it('requires a second confirmation for bulk removal and preserves the form after failure', async () => { const browser = userEvent.setup(); const entry = accessEntryFixture(31, 'remove@example.test'); vi.spyOn(api, 'adminUsers').mockResolvedValue({ users: adminUsers }); vi.spyOn(api, 'adminMailboxAccess').mockResolvedValue({ mailboxes: [entry] }); let rejectBulk!: (reason?: unknown) => void; const bulkUpdate = vi.spyOn(api, 'bulkUpdateAdminMailboxAccess').mockReturnValue(new Promise((_resolve, reject) => { rejectBulk = reject; })); renderMailboxAccessRoute(); await browser.click(await screen.findByRole('checkbox', { name: `选择邮箱 · ${entry.mailbox.address}` })); await browser.click(screen.getByRole('button', { name: '批量授权 (1)' })); const dialog = await screen.findByRole('dialog', { name: '批量配置邮箱权限' }); await browser.click(within(dialog).getByText('移除授权')); await browser.click(within(dialog).getByRole('combobox', { name: '授权用户' })); await browser.click(await screen.findByText('reader · reader@example.test', { selector: '.ant-select-item-option-content' })); await waitFor(() => expect(within(dialog).getByText('1 个邮箱 × 1 个用户 = 1 个组合')).toBeTruthy()); await browser.click(within(dialog).getByRole('button', { name: '移除授权' })); expect(bulkUpdate).not.toHaveBeenCalled(); const confirmationTitle = await screen.findByText( '确认批量移除授权', { selector: '.ant-modal-confirm-title' } ); const confirmation = confirmationTitle.closest('.ant-modal') as HTMLElement; expect(confirmation).toBeTruthy(); expect(within(confirmation).getByText('1 个邮箱 × 1 个用户 = 1 个组合')).toBeTruthy(); await browser.click(within(confirmation).getByRole('button', { name: '移除授权' })); await waitFor(() => expect(bulkUpdate).toHaveBeenCalledWith({ mailboxIds: [31], userIds: [2], operation: 'remove' })); expect((within(dialog).getByRole('combobox', { name: '授权用户' }) as HTMLInputElement).disabled).toBe(true); expect((within(dialog).getByRole('button', { name: '取消' }) as HTMLButtonElement).disabled).toBe(true); expect((within(confirmation).getByRole('button', { name: '取消' }) as HTMLButtonElement).disabled).toBe(true); fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' }); expect(document.body.contains(dialog)).toBe(true); await act(async () => { rejectBulk(new Error('批量授权暂时不可用')); await Promise.resolve(); }); await waitFor(() => expect(screen.queryByText( '确认批量移除授权', { selector: '.ant-modal-confirm-title' } )).toBeNull()); const retainedDialog = screen.getByRole('dialog', { name: '批量配置邮箱权限' }); expect(within(retainedDialog).getByText('批量授权暂时不可用')).toBeTruthy(); expect((within(retainedDialog).getByRole('radio', { name: '移除授权' }) as HTMLInputElement).checked).toBe(true); await browser.click(within(retainedDialog).getByRole('combobox', { name: '授权用户' })); const retainedUserOption = await screen.findByText( 'reader · reader@example.test', { selector: '.ant-select-item-option-content' } ); expect(retainedUserOption.closest('.ant-select-item-option')?.classList.contains( 'ant-select-item-option-selected' )).toBe(true); expect(within(retainedDialog).getByText('已选择邮箱 1')).toBeTruthy(); }); it('blocks a selection larger than the mailbox batch limit', async () => { const browser = userEvent.setup(); vi.spyOn(window, 'matchMedia').mockImplementation((query) => mediaQueryList(query === '(min-width: 768px)', query)); const entries = Array.from({ length: 501 }, (_item, index) => ( accessEntryFixture(1000 + index, `limit-${index + 1}@example.test`) )); vi.spyOn(api, 'adminUsers').mockResolvedValue({ users: adminUsers }); vi.spyOn(api, 'adminMailboxAccess').mockResolvedValue({ mailboxes: entries }); const bulkUpdate = vi.spyOn(api, 'bulkUpdateAdminMailboxAccess'); renderMailboxAccessRoute(); await browser.click(await screen.findByRole('checkbox', { name: '选择全部筛选结果 (501)' })); expect(await screen.findByText('单次最多选择邮箱数: 500')).toBeTruthy(); expect((screen.getByRole('button', { name: '批量授权 (501)' }) as HTMLButtonElement).disabled).toBe(true); expect(bulkUpdate).not.toHaveBeenCalled(); }); 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(within(viewOnlyCard as HTMLElement).queryByRole('button', { name: /一键登录 Webmail/ })).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'); }); it('exchanges an assigned mailbox for a hidden Webmail POST without exposing the ticket', async () => { const browser = userEvent.setup(); const owner = 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 inactive = { ...mailboxFixture(4, 'inactive@example.test', 'assigned', { view: true, receive: true, send: false }, 1), status: 'disabled' }; vi.spyOn(api, 'inboundMailboxes').mockResolvedValue({ mailboxes: [owner, assigned, viewOnly, inactive] }); const webmailLogin: WebmailLogin = { action: 'https://mail.us.ss5.xyz/', ticket: 'mht_secret-ticket', expiresAt: '2026-07-18T01:00:00.000Z' }; let resolveLogin!: (value: { webmailLogin: WebmailLogin }) => void; const login = vi.spyOn(api, 'createWebmailLogin').mockReturnValue(new Promise((resolve) => { resolveLogin = resolve; })); let submittedForm: HTMLFormElement | null = null; const requestSubmit = vi.spyOn(HTMLFormElement.prototype, 'requestSubmit').mockImplementation(function (this: HTMLFormElement) { submittedForm = this.cloneNode(true) as HTMLFormElement; }); const router = createMemoryRouter([{ path: '/account', element: }], { initialEntries: ['/account'] }); renderWithRouter(router, userContext); const assignedCard = (await screen.findByText(assigned.address)).closest('.ant-card') as HTMLElement; const ownerCard = screen.getByText(owner.address).closest('.ant-card') as HTMLElement; const viewOnlyCard = screen.getByText(viewOnly.address).closest('.ant-card') as HTMLElement; const inactiveCard = screen.getByText(inactive.address).closest('.ant-card') as HTMLElement; const assignedButton = within(assignedCard).getByRole('button', { name: /一键登录 Webmail/ }); const ownerButton = within(ownerCard).getByRole('button', { name: /一键登录 Webmail/ }); expect(within(viewOnlyCard).queryByRole('button', { name: /一键登录 Webmail/ })).toBeNull(); const inactiveButton = within(inactiveCard).getByRole('button', { name: /一键登录 Webmail/ }) as HTMLButtonElement; expect(inactiveButton.disabled).toBe(true); expect(inactiveButton.title).toContain('邮箱已停用'); const setItem = vi.spyOn(Storage.prototype, 'setItem'); await browser.click(assignedButton); await waitFor(() => expect(login).toHaveBeenCalledWith(assigned.id)); expect(assignedButton.classList.contains('ant-btn-loading')).toBe(true); expect(ownerButton.classList.contains('ant-btn-loading')).toBe(false); await act(async () => resolveLogin({ webmailLogin })); await waitFor(() => expect(requestSubmit).toHaveBeenCalledTimes(1)); expect(submittedForm).not.toBeNull(); expect(submittedForm!.getAttribute('method')).toBe('POST'); expect(submittedForm!.action).toBe(webmailLogin.action); expect(submittedForm!.target).toBe('_self'); expect(Object.fromEntries(new FormData(submittedForm!))).toEqual({ mailhub_ticket: webmailLogin.ticket, _task: 'mail', _mbox: 'INBOX' }); expect(document.querySelector(`form[action="${webmailLogin.action}"]`)).toBeNull(); expect(`${router.state.location.pathname}${router.state.location.search}`).toBe('/account'); expect(window.location.href).not.toContain(webmailLogin.ticket); expect(setItem.mock.calls.flat().join(' ')).not.toContain(webmailLogin.ticket); }); it('shows a Webmail login error without navigating away', async () => { const browser = userEvent.setup(); const mailbox = mailboxFixture(1, 'owned@example.test', 'owner', { view: true, receive: true, send: true }, 2); vi.spyOn(api, 'inboundMailboxes').mockResolvedValue({ mailboxes: [mailbox] }); vi.spyOn(api, 'createWebmailLogin').mockRejectedValue(new Error('Webmail 暂时不可用')); const router = createMemoryRouter([{ path: '/account', element: }], { initialEntries: ['/account'] }); renderWithRouter(router, userContext); await browser.click(await screen.findByRole('button', { name: /一键登录 Webmail/ })); expect(await screen.findByText('Webmail 暂时不可用')).toBeTruthy(); expect(router.state.location.pathname).toBe('/account'); }); it('hides the Webmail shortcut until the server explicitly enables SSO', async () => { const mailbox = mailboxFixture(1, 'owned@example.test', 'owner', { view: true, receive: true, send: true }, 2); vi.spyOn(api, 'inboundMailboxes').mockResolvedValue({ mailboxes: [mailbox] }); const router = createMemoryRouter([{ path: '/account', element: }], { initialEntries: ['/account'] }); renderWithRouter(router, { ...userContext, config: { ...config, webmailSsoEnabled: false } }); expect(await screen.findByText(mailbox.address)).toBeTruthy(); expect(screen.queryByRole('button', { name: /一键登录 Webmail/ })).toBeNull(); }); }); 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 accessEntryFixture(id: number, address: string): AdminMailboxAccessEntry { const mailbox = mailboxFixture( id, address, 'owner', { view: true, receive: true, send: true }, adminUsers[0].id ); return { mailbox, owner: adminUsers[0], grants: [{ user: adminUsers[2], permissions: { view: true, receive: false, send: true }, createdAt: '2026-07-18T00:00:00.000Z', updatedAt: '2026-07-18T00:00:00.000Z' }] }; } function renderMailboxAccessRoute() { const router = createMemoryRouter([{ path: '/admin/:section', element: }], { initialEntries: ['/admin/mailbox-access'] }); renderWithRouter(router, adminContext); return router; } function mediaQueryList(matches: boolean, media: string): MediaQueryList { return { matches, media, onchange: null, addListener: () => undefined, removeListener: () => undefined, addEventListener: () => undefined, removeEventListener: () => undefined, dispatchEvent: () => false }; } 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() }, { id: 3, username: 'sender', email: 'sender@example.test', role: 'user', status: 'active', resourceCounts: resourceCounts() }, { id: 4, username: 'disabled', email: 'disabled@example.test', role: 'user', status: 'disabled', resourceCounts: resourceCounts() }, { id: 5, username: 'pending', email: 'pending@example.test', role: 'user', status: 'pending_email', 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', webmailSsoEnabled: true, 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' });