From c11d8ade44f24757baff473c4955df853cff24d9 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sun, 1 Nov 2026 09:50:35 +0100 Subject: [PATCH] test: cover the plugin, .eml preview, drag-out, export file and install banner paths of the device policies --- .../__tests__/device-gates-paths.test.tsx | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 components/device/__tests__/device-gates-paths.test.tsx diff --git a/components/device/__tests__/device-gates-paths.test.tsx b/components/device/__tests__/device-gates-paths.test.tsx new file mode 100644 index 000000000..12125fa46 --- /dev/null +++ b/components/device/__tests__/device-gates-paths.test.tsx @@ -0,0 +1,129 @@ +import { act, fireEvent, render, renderHook, screen } from '@testing-library/react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('@/hooks/use-config', () => ({ + useConfig: () => ({ appName: 'Bulwark', faviconUrl: '', appLogoLightUrl: '', appLogoDarkUrl: '' }), +})); +vi.mock('@/stores/toast-store', () => ({ toast: { info: vi.fn(), error: vi.fn(), success: vi.fn(), warning: vi.fn() } })); +vi.mock('@/contexts/drag-drop-context', () => ({ + useDragDropContext: () => ({ startDrag: vi.fn(), endDrag: vi.fn(), isDragging: false, draggedEmails: [] }), +})); +vi.mock('@/hooks/use-attachment-drag', () => ({ isDragOutSupported: () => true })); + +import { PWAInstallPrompt } from '@/components/pwa-install-prompt'; +import { EmlPreview, type ParsedEml } from '@/components/files/eml-preview'; +import { dispatchApiCall } from '@/lib/plugin-sandbox/host-api'; +import { resetPluginRateLimits } from '@/lib/plugin-sandbox/host-api-input'; +import { memorySink } from '@/lib/mail-export/sinks'; +import { useEmailDrag } from '@/hooks/use-email-drag'; +import { DEFAULT_POLICY } from '@/lib/admin/types'; +import { DEFAULT_ORGANISATION_POLICY } from '@/lib/admin/organisation-policy'; +import type { InstalledPlugin } from '@/lib/plugin-types'; +import type { Email } from '@/lib/jmap/types'; +import { useAuthStore } from '@/stores/auth-store'; +import { useMailTransferStore } from '@/stores/mail-transfer-store'; +import { usePolicyStore } from '@/stores/policy-store'; +import { useUIStore } from '@/stores/ui-store'; + +// The other paths of the device policies (Organisation > Devices): the +// plugin APIs, .eml previews, dragging messages out, the export file, and +// the install prompt after the user dismissed Bulwark's own card. + +function setDevices(devices: Record) { + const policy = { ...DEFAULT_POLICY, organisation: { ...DEFAULT_ORGANISATION_POLICY, devices: { ...DEFAULT_ORGANISATION_POLICY.devices, ...devices } } }; + usePolicyStore.setState({ policy, rawPolicy: policy, subject: null, loaded: true }); +} + +const initialPolicy = usePolicyStore.getState(); +const initialAuth = useAuthStore.getState(); +let click: ReturnType; +beforeEach(() => { + localStorage.clear(); + resetPluginRateLimits(); + useAuthStore.setState({ activeAccountId: 'acc-a', username: 'alice@example.org', getAllConnectedClients: (() => new Map()) as never }); + click = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {}); + vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:x'); + vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {}); +}); +afterEach(() => { + usePolicyStore.setState(initialPolicy, true); + useAuthStore.setState(initialAuth, true); + useMailTransferStore.setState({ job: null, request: null, visible: false }); + vi.restoreAllMocks(); +}); + +const PLUGIN: InstalledPlugin = { + id: 'p', name: 'P', version: '1.0.0', author: '', description: '', type: 'ui-extension', + permissions: ['ui:download-file'], grantedPermissions: ['ui:download-file'], entrypoint: 'index.js', enabled: true, status: 'running', settings: {}, +}; + +describe('plugins', () => { + it('can’t save a file under "No downloads" or "No export"', async () => { + setDevices({ noDownloads: true }); + await expect(dispatchApiCall(PLUGIN, 'ui.downloadFile', [{ content: 'x', filename: 'a.txt', contentType: 'text/plain' }])).rejects.toThrow(/turned off by the organisation/); + setDevices({ noExport: true }); + await expect(dispatchApiCall(PLUGIN, 'ui.downloadFile', [{ content: 'x', filename: 'a.txt', contentType: 'text/plain' }])).rejects.toThrow(/turned off by the organisation/); + expect(click).not.toHaveBeenCalled(); + setDevices({}); + await dispatchApiCall(PLUGIN, 'ui.downloadFile', [{ content: 'x', filename: 'a.txt', contentType: 'text/plain' }]); + expect(click).toHaveBeenCalledTimes(1); + }); + + it('can’t print under "No printing"', async () => { + setDevices({ noPrint: true }); + await expect(dispatchApiCall(PLUGIN, 'ui.print', [{ html: '

x

' }])).rejects.toThrow(/printing is turned off/); + expect(document.querySelector('iframe')).toBeNull(); + }); +}); + +describe('.eml previews', () => { + const eml: ParsedEml = { + subject: 'Fwd', from: null, to: [], date: null, text: 'see attached', html: null, + attachments: [ + { filename: 'data.zip', mimeType: 'application/zip', content: new Uint8Array([1]) }, + { filename: 'chart.png', mimeType: 'image/png', content: new Uint8Array([1]) }, + ], + } as unknown as ParsedEml; + + it('offer no download of their attachments under "No downloads"', () => { + render( {}} downloadsOff />); + expect(screen.queryByTitle('download')).toBeNull(); + fireEvent.click(screen.getByText('data.zip')); + expect(click).not.toHaveBeenCalled(); + }); +}); + +describe('dragging messages out', () => { + it('fetches nothing to drag out under "No export"; moving between folders still drags', () => { + setDevices({ noExport: true }); + const fetchBlobAsObjectUrl = vi.fn(async () => 'blob:eml'); + useAuthStore.setState({ client: { fetchBlobAsObjectUrl, getAccountId: () => 'a' } as never }); + useUIStore.setState({ isMobile: false }); + const email = { id: 'e', blobId: 'b', threadId: 't', subject: 'S', receivedAt: '2026-11-01T00:00:00Z', mailboxIds: {}, keywords: {} } as unknown as Email; + const { result } = renderHook(() => useEmailDrag({ email, sourceMailboxId: 'inbox' })); + expect(result.current.dragHandlers.draggable).toBe(true); + act(() => { (result.current.dragHandlers as { onPointerEnter?: () => void }).onPointerEnter?.(); }); + expect(fetchBlobAsObjectUrl).not.toHaveBeenCalled(); + }); +}); + +describe('the export file', () => { + it('isn’t handed out once "No export" is on', async () => { + setDevices({ noExport: true }); + const sink = memorySink('all.mbox', 'application/mbox'); + await sink.write(new Uint8Array([70, 114, 111, 109])); + await sink.close(); + expect(click).not.toHaveBeenCalled(); + }); +}); + +describe('install prompt', () => { + it('holds back the browser’s banner under the policy even after the user dismissed Bulwark’s card', () => { + localStorage.setItem('pwa-install-dismissed', '1'); + setDevices({ noInstallPrompt: true }); + render(); + const event = new Event('beforeinstallprompt', { cancelable: true }); + act(() => { window.dispatchEvent(event); }); + expect(event.defaultPrevented).toBe(true); + }); +});