Files
webmail/components/plugins/__tests__/plugin-connect-dialog.test.tsx
T
ghoztyandClaude Opus 5.5 30f51e5ca4 feat: name the app with the configured APP_NAME in user-facing strings
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>
2026-10-22 04:14:54 +03:00

103 lines
4.9 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
const apiFetch = vi.fn();
vi.mock('next-intl', () => {
const t = (key: string, values?: Record<string, unknown>) => (values ? `${key} ${JSON.stringify(values)}` : key);
return { useTranslations: () => t, useLocale: () => 'en' };
});
vi.mock('@/lib/browser-navigation', async (importOriginal) => ({
...(await importOriginal<typeof import('@/lib/browser-navigation')>()),
apiFetch: (...args: unknown[]) => apiFetch(...args),
}));
const appName = vi.hoisted(() => ({ value: 'Webmail' }));
vi.mock('@/hooks/use-config', () => ({ useConfig: () => ({ appName: appName.value }) }));
import { PluginConnectDialog } from '../plugin-connect-dialog';
import { originFromInput, requestConnect, type ConnectResult } from '@/lib/plugin-sandbox/connect-dialog';
beforeEach(() => {
apiFetch.mockReset();
apiFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 }));
appName.value = 'Webmail';
});
function open(mode: 'password' | 'signin' = 'password') {
let result: Promise<ConnectResult>;
act(() => {
result = requestConnect({
pluginId: 'nextcloud', pluginName: 'Nextcloud', credentialId: 'cloud', mode,
spec: { label: 'Nextcloud', scope: 'user', inject: { type: 'basic' } },
});
});
return result!;
}
describe('connect dialog', () => {
it('sends the password to the server and tells the plugin only that it worked', async () => {
render(<PluginConnectDialog />);
const result = open();
fireEvent.change(screen.getByText('server').parentElement!.querySelector('input')!, { target: { value: 'cloud.example.org/index.php' } });
fireEvent.change(screen.getByText('username').parentElement!.querySelector('input')!, { target: { value: 'alice' } });
fireEvent.change(screen.getByText('secret').parentElement!.querySelector('input')!, { target: { value: 'app-pw' } });
expect(screen.getByText(/intro_password /)).toHaveTextContent('cloud.example.org');
fireEvent.click(screen.getByRole('button', { name: 'connect' }));
await expect(result).resolves.toEqual({ connected: true, origin: 'https://cloud.example.org' });
const [url, init] = apiFetch.mock.calls[0];
expect(url).toBe('/api/plugin-secrets');
expect(JSON.parse(init.body)).toMatchObject({
pluginId: 'nextcloud', credentialId: 'cloud', origin: 'https://cloud.example.org', username: 'alice', secret: 'app-pw',
});
expect(JSON.stringify(await result)).not.toContain('app-pw');
});
it('only approves the server for a sign-in flow', async () => {
render(<PluginConnectDialog />);
const result = open('signin');
expect(screen.queryByText('secret')).toBeNull();
fireEvent.change(screen.getByText('server').parentElement!.querySelector('input')!, { target: { value: 'https://cloud.example.org' } });
fireEvent.click(screen.getByRole('button', { name: 'continue' }));
await expect(result).resolves.toEqual({ connected: false, origin: 'https://cloud.example.org' });
expect(JSON.parse(apiFetch.mock.calls[0][1].body)).not.toHaveProperty('secret');
});
it('names the app as configured, and Bulwark on a stock install', async () => {
const first = render(<PluginConnectDialog />);
const result = open('signin');
expect(screen.getByText(/intro_signin /)).toHaveTextContent('"appName":"Bulwark"');
first.unmount();
appName.value = 'Acme Mail';
render(<PluginConnectDialog />);
expect(screen.getByText(/intro_signin /)).toHaveTextContent('"appName":"Acme Mail"');
act(() => { fireEvent.keyDown(window, { key: 'Escape' }); });
await expect(result).resolves.toEqual({ connected: false });
});
it('shows the server error and stays open', async () => {
apiFetch.mockResolvedValueOnce(new Response(JSON.stringify({ error: 'The server address must start with https://' }), { status: 400 }));
render(<PluginConnectDialog />);
const result = open();
fireEvent.change(screen.getByText('server').parentElement!.querySelector('input')!, { target: { value: 'cloud.example.org' } });
fireEvent.change(screen.getByText('username').parentElement!.querySelector('input')!, { target: { value: 'alice' } });
fireEvent.change(screen.getByText('secret').parentElement!.querySelector('input')!, { target: { value: 'pw' } });
fireEvent.click(screen.getByRole('button', { name: 'connect' }));
await waitFor(() => expect(screen.getByRole('alert')).toHaveTextContent('must start with https'));
fireEvent.click(screen.getByRole('button', { name: 'cancel' }));
await expect(result).resolves.toEqual({ connected: false });
});
});
describe('originFromInput', () => {
it('adds https and keeps only the origin', () => {
expect(originFromInput('cloud.example.org/nextcloud')).toBe('https://cloud.example.org');
expect(originFromInput('https://u:p@cloud.example.org')).toBeNull();
expect(originFromInput('javascript:alert(1)')).toBeNull();
});
});