fix: stop a stuck certificate call holding mail rendering, and let a server's certificate access be taken back

This commit is contained in:
Linus Rath
2026-11-03 11:31:14 +01:00
parent 2db3c4b217
commit 1d4587f7ea
39 changed files with 474 additions and 47 deletions
@@ -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>
);
}
+2
View File
@@ -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')}>