mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-05 08:16:22 +00:00
fix: stop a stuck certificate call holding mail rendering, and let a server's certificate access be taken back
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { DesktopKeystoreSettings } from '../desktop-keystore-settings';
|
||||
|
||||
// Taking back a server's use of the certificates on this computer. The stored yes lives
|
||||
// in the shell; this row is the way out, and only shows when there is something to take back.
|
||||
|
||||
const toast = vi.hoisted(() => ({ success: vi.fn(), error: vi.fn(), info: vi.fn(), warning: vi.fn() }));
|
||||
vi.mock('@/stores/toast-store', () => ({ toast }));
|
||||
vi.mock('../settings-section', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('../settings-section')>()),
|
||||
SettingsSection: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
function bridge(state: string) {
|
||||
const revokeAccess = vi.fn(async () => {});
|
||||
(window as unknown as { bulwarkDesktop?: unknown }).bulwarkDesktop = {
|
||||
version: 1,
|
||||
platform: 'win32',
|
||||
keystore: { accessState: vi.fn(async () => state), revokeAccess },
|
||||
};
|
||||
return revokeAccess;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
delete (window as unknown as { bulwarkDesktop?: unknown }).bulwarkDesktop;
|
||||
toast.success.mockClear();
|
||||
});
|
||||
|
||||
describe('the keystore access row', () => {
|
||||
it('takes back a stored yes through the shell, and goes away', async () => {
|
||||
const revoke = bridge('stored');
|
||||
render(<DesktopKeystoreSettings />);
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'revoke' }));
|
||||
await waitFor(() => expect(revoke).toHaveBeenCalledTimes(1));
|
||||
await waitFor(() => expect(screen.queryByRole('button', { name: 'revoke' })).toBeNull());
|
||||
expect(toast.success).toHaveBeenCalledWith('revoked');
|
||||
});
|
||||
|
||||
it.each(['off', 'policy', 'app'])('shows nothing when the state is %s', async (state) => {
|
||||
bridge(state);
|
||||
const { container } = render(<DesktopKeystoreSettings />);
|
||||
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||
expect(container.textContent).toBe('');
|
||||
});
|
||||
|
||||
it('shows nothing in a browser', async () => {
|
||||
const { container } = render(<DesktopKeystoreSettings />);
|
||||
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||
expect(container.textContent).toBe('');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { toast } from "@/stores/toast-store";
|
||||
import { SettingsSection, SettingItem } from "./settings-section";
|
||||
|
||||
/**
|
||||
* In the desktop app on an instance: the user's yes to this server using certificates on
|
||||
* this computer, stored in the shell, and the way to take it back. Nothing shows in a
|
||||
* browser, in standalone, or when an organisation's policy set it.
|
||||
*/
|
||||
export function DesktopKeystoreSettings() {
|
||||
const t = useTranslations("desktop_keystore");
|
||||
const [stored, setStored] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let live = true;
|
||||
window.bulwarkDesktop?.keystore?.accessState?.().then(
|
||||
(state) => { if (live) setStored(state === "stored"); },
|
||||
() => {},
|
||||
);
|
||||
return () => { live = false; };
|
||||
}, []);
|
||||
|
||||
if (!stored) return null;
|
||||
|
||||
const revoke = async () => {
|
||||
await window.bulwarkDesktop?.keystore?.revokeAccess();
|
||||
setStored(false);
|
||||
toast.success(t("revoked"));
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsSection id="desktop-keystore" title={t("title")} description={t("description")}>
|
||||
<SettingItem label={t("label")} description={t("label_description")}>
|
||||
<Button variant="outline" onClick={() => void revoke()}>{t("revoke")}</Button>
|
||||
</SettingItem>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { useTranslations } from 'next-intl';
|
||||
import { useSettingsStore } from '@/stores/settings-store';
|
||||
import { SettingsSection, SettingItem, ToggleSwitch } from './settings-section';
|
||||
import { LanguageSettings } from './language-settings';
|
||||
import { DesktopKeystoreSettings } from './desktop-keystore-settings';
|
||||
import { ProtocolHandlerSettings } from './protocol-handler-settings';
|
||||
import { useTour } from '@/components/tour/tour-provider';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -25,6 +26,7 @@ export function GeneralSettings({ supportsCalendar }: { supportsCalendar: boolea
|
||||
<LanguageSettings />
|
||||
|
||||
{!IS_LITE && <ProtocolHandlerSettings supportsCalendar={supportsCalendar} />}
|
||||
<DesktopKeystoreSettings />
|
||||
|
||||
<SettingsSection id="tour" title={tTour('settings_section')}>
|
||||
<SettingItem label={tTour('restart_title')} description={tTour('restart_desc')}>
|
||||
|
||||
Reference in New Issue
Block a user