mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 08:13:50 +00:00
Some user-facing strings still said "Bulwark" on a deployment with its own
APP_NAME: Settings > General > Default apps (protocol handlers), the "Open
Bulwark" notification, the rate-limit banner and toast, the plugin connect
dialog, one plugin permission text and the connector page.
These strings now take an {appName} placeholder, filled by productName():
the configured APP_NAME, or "Bulwark" when it is unset or the default
"Webmail" (the same rule as the TOTP issuer and the About card, #1160).
A stock install reads as before.
The settings search (the settings page and api.app.searchSettings) indexes
these texts with the name filled in, so "bulwark" or the configured name
finds Default apps and no result carries the raw placeholder.
Locales: the 13 keys in every locale that has "Bulwark" in them. Languages
that inflect the name (cs, et, hu, ko, ro, sk, tr) use a neutral form such
as "{appName} uygulamasında" instead of a declined "Bulwark'ta"; a native
check of those is welcome.
Not changed on purpose: the mobile app, push relay, Lite, demo and admin
texts, which speak about Bulwark itself.
Closes #1314
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
106 lines
5.4 KiB
TypeScript
106 lines
5.4 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import type { InstalledPlugin } from '../plugin-types';
|
|
|
|
// api.app.getInfo: what a plugin may know about the app it runs in (an AI
|
|
// assistant answering "what webmail is this?"), with no permission.
|
|
|
|
const config = { appName: 'Rath Mail', stalwartFeaturesEnabled: false, demoMode: false };
|
|
const fetchConfig = vi.fn(async () => config);
|
|
vi.mock('@/hooks/use-config', () => ({ fetchConfig: () => fetchConfig() }));
|
|
|
|
const { dispatchApiCall } = await import('../plugin-sandbox/host-api');
|
|
|
|
const plugin = (permissions: string[] = []) => ({
|
|
id: 'p', name: 'P', version: '1.0.0', author: 'a', description: 'd', type: 'hook',
|
|
permissions, managed: permissions.length > 0, entrypoint: 'index.js', enabled: true, status: 'running', settings: {}, httpPostAllowedOrigins: [],
|
|
} as unknown as InstalledPlugin);
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
fetchConfig.mockClear();
|
|
});
|
|
|
|
describe('api.app.getInfo', () => {
|
|
it('gives the product, the administrator\'s name for it, the version and the edition, without a permission', async () => {
|
|
vi.stubEnv('NEXT_PUBLIC_APP_VERSION', '1.17.0');
|
|
expect(await dispatchApiCall(plugin(), 'app.getInfo', [])).toEqual({
|
|
product: 'Bulwark Mail',
|
|
name: 'Rath Mail',
|
|
version: '1.17.0',
|
|
edition: 'full',
|
|
stalwartFeatures: false,
|
|
demoMode: false,
|
|
});
|
|
});
|
|
|
|
it('still answers when the config can not be read', async () => {
|
|
fetchConfig.mockRejectedValueOnce(new Error('offline'));
|
|
const info = await dispatchApiCall(plugin(), 'app.getInfo', []) as { name: string; product: string };
|
|
expect(info.product).toBe('Bulwark Mail');
|
|
expect(info.name).toBe('Webmail');
|
|
});
|
|
});
|
|
|
|
describe('api.app.searchSettings', () => {
|
|
const setUp = async ({ locale = 'en', vacation = true } = {}) => {
|
|
const { useLocaleStore } = await import('@/stores/locale-store');
|
|
const { useAuthStore } = await import('@/stores/auth-store');
|
|
useLocaleStore.setState({ locale } as never);
|
|
useAuthStore.setState({ client: {
|
|
supportsSieve: () => true, supportsVacationResponse: () => vacation, supportsCalendars: () => true, supportsFiles: () => false,
|
|
} } as never);
|
|
};
|
|
|
|
it('finds a setting from an English query, named in the user\'s language', async () => {
|
|
await setUp({ locale: 'de' });
|
|
const results = await dispatchApiCall(plugin(), 'app.searchSettings', ['undo send', { limit: 3 }]) as { pageId: string; page: string; label: string }[];
|
|
expect(results.length).toBeGreaterThan(0);
|
|
expect(results[0].pageId).toBe('composing');
|
|
expect(results[0].page).toBe('Verfassen');
|
|
const de = (await import('@/locales/de/common.json')).default as unknown as { settings: { email_behavior: { send_delay: { label: string } } } };
|
|
expect(results.map((r) => r.label)).toContain(de.settings.email_behavior.send_delay.label);
|
|
});
|
|
|
|
it('leaves out pages the server does not offer', async () => {
|
|
await setUp({ vacation: false });
|
|
const results = await dispatchApiCall(plugin(), 'app.searchSettings', ['out of office automatic reply']) as { pageId: string }[];
|
|
expect(results.some((r) => r.pageId === 'vacation')).toBe(false);
|
|
await setUp({ vacation: true });
|
|
const again = await dispatchApiCall(plugin(), 'app.searchSettings', ['out of office automatic reply']) as { pageId: string }[];
|
|
expect(again.some((r) => r.pageId === 'vacation')).toBe(true);
|
|
});
|
|
|
|
it('returns texts with the configured name, never the raw placeholder (#1314)', async () => {
|
|
await setUp();
|
|
const results = await dispatchApiCall(plugin(), 'app.searchSettings', ['Rath Mail email links']) as { pageId: string; description?: string }[];
|
|
expect(results.some((r) => r.pageId === 'general' && r.description?.includes('Rath Mail'))).toBe(true);
|
|
expect(JSON.stringify(results)).not.toContain('{appName}');
|
|
});
|
|
|
|
it('says Bulwark when the config can not be read', async () => {
|
|
await setUp();
|
|
fetchConfig.mockRejectedValueOnce(new Error('offline'));
|
|
const results = await dispatchApiCall(plugin(), 'app.searchSettings', ['bulwark email links']) as { pageId: string; description?: string }[];
|
|
expect(fetchConfig).toHaveBeenCalledTimes(1);
|
|
expect(results.some((r) => r.pageId === 'general' && r.description?.includes('Bulwark'))).toBe(true);
|
|
expect(JSON.stringify(results)).not.toContain('{appName}');
|
|
});
|
|
|
|
it('wants a query', async () => {
|
|
await expect(dispatchApiCall(plugin(), 'app.searchSettings', [''])).rejects.toThrow(/non-empty string/);
|
|
});
|
|
});
|
|
|
|
describe('api.app.getSettings', () => {
|
|
it('needs settings:read, and shares plain preferences but nothing about other people', async () => {
|
|
const { useSettingsStore } = await import('@/stores/settings-store');
|
|
useSettingsStore.setState({ sendDelaySeconds: 10, markAsReadDelay: 2000, mailLayout: 'focus', trustedSenders: ['boss@example.org'], pushRelayUrl: 'https://relay.example' } as never);
|
|
await expect(dispatchApiCall(plugin(), 'app.getSettings', [])).rejects.toThrow();
|
|
const settings = await dispatchApiCall(plugin(['settings:read']), 'app.getSettings', []) as Record<string, unknown>;
|
|
expect(settings).toMatchObject({ sendDelaySeconds: 10, markAsReadDelay: 2000, mailLayout: 'focus' });
|
|
expect(settings).not.toHaveProperty('trustedSenders');
|
|
expect(settings).not.toHaveProperty('pushRelayUrl');
|
|
expect(JSON.stringify(settings)).not.toContain('boss@example.org');
|
|
});
|
|
});
|