Files
Psychotoxical-psysonic/src/components/settings/AddServerForm.test.tsx
T

176 lines
6.8 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { renderWithProviders } from '@/test/helpers/renderWithProviders';
import { AddServerForm } from './AddServerForm';
import { encodeServerMagicString } from '@/utils/server/serverMagicString';
// resolve_host_addresses Tauri command — hint-only, must not block save.
vi.mock('@/api/network', () => ({
resolveHostAddresses: vi.fn(async () => [] as string[]),
}));
// showToast mocked so we can assert two-LAN validation surfaced the error.
vi.mock('@/utils/ui/toast', () => ({
showToast: vi.fn(),
}));
import { showToast } from '@/utils/ui/toast';
describe('AddServerForm — dual-address behaviour', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('saves a single-address profile without alternateUrl / shareUsesLocalUrl', async () => {
const onSave = vi.fn();
renderWithProviders(<AddServerForm onSave={onSave} onCancel={vi.fn()} />);
const user = userEvent.setup();
const inputs = screen.getAllByRole('textbox');
// [0] name, [1] primary url, [2] alternate url, [3] username, [4] magic string
await user.type(inputs[1]!, 'https://music.example.com');
await user.type(inputs[3]!, 'tester');
await user.type(screen.getByPlaceholderText('••••••••'), 'pw');
await user.click(screen.getByRole('button', { name: /add/i }));
expect(onSave).toHaveBeenCalledTimes(1);
const arg = onSave.mock.calls[0]![0];
expect(arg.url).toBe('https://music.example.com');
expect(arg.username).toBe('tester');
expect(arg.password).toBe('pw');
expect(arg).not.toHaveProperty('alternateUrl');
expect(arg).not.toHaveProperty('shareUsesLocalUrl');
});
it('saves both addresses when the user fills the second field', async () => {
const onSave = vi.fn();
renderWithProviders(<AddServerForm onSave={onSave} onCancel={vi.fn()} />);
const user = userEvent.setup();
const inputs = screen.getAllByRole('textbox');
// [0] name, [1] primary url, [2] alternate url, [3] username
await user.type(inputs[1]!, 'https://music.example.com');
await user.type(inputs[2]!, 'http://192.168.0.10:4533');
await user.type(inputs[3]!, 'tester');
await user.type(screen.getByPlaceholderText('••••••••'), 'pw');
await user.click(screen.getByRole('button', { name: /add/i }));
expect(onSave).toHaveBeenCalledTimes(1);
const arg = onSave.mock.calls[0]![0];
expect(arg.url).toBe('https://music.example.com');
expect(arg.alternateUrl).toBe('http://192.168.0.10:4533');
expect(arg.shareUsesLocalUrl).toBe(false);
});
it('blocks save with a toast when both addresses classify as LAN', async () => {
const onSave = vi.fn();
renderWithProviders(<AddServerForm onSave={onSave} onCancel={vi.fn()} />);
const user = userEvent.setup();
const inputs = screen.getAllByRole('textbox');
await user.type(inputs[1]!, 'http://10.0.0.5');
await user.type(inputs[2]!, 'http://192.168.0.10');
await user.type(inputs[3]!, 'tester');
await user.type(screen.getByPlaceholderText('••••••••'), 'pw');
await user.click(screen.getByRole('button', { name: /add/i }));
// Save is blocked, error toast surfaced with the two-LAN string.
expect(onSave).not.toHaveBeenCalled();
expect(showToast).toHaveBeenCalledWith(
expect.stringMatching(/both addresses are local/i),
expect.any(Number),
'error',
);
});
it('decodes a v2 magic string and forwards alternateUrl + shareUsesLocalUrl on save', async () => {
const onSave = vi.fn();
renderWithProviders(<AddServerForm onSave={onSave} onCancel={vi.fn()} />);
const user = userEvent.setup();
const magicString = encodeServerMagicString({
url: 'https://music.example.com',
alternateUrl: 'http://192.168.0.10:4533',
shareUsesLocalUrl: true,
username: 'tester',
password: 'pw',
});
// The magic-string input is the last textbox shown for new-profile mode.
const inputs = screen.getAllByRole('textbox');
const magicInput = inputs[inputs.length - 1]!;
await user.type(magicInput, magicString);
await user.click(screen.getByRole('button', { name: /add/i }));
expect(onSave).toHaveBeenCalledTimes(1);
const arg = onSave.mock.calls[0]![0];
expect(arg.url).toBe('https://music.example.com');
expect(arg.alternateUrl).toBe('http://192.168.0.10:4533');
expect(arg.shareUsesLocalUrl).toBe(true);
expect(arg.username).toBe('tester');
expect(arg.password).toBe('pw');
});
it('strips alternateUrl + share flag when the user empties the second field', async () => {
const onSave = vi.fn();
renderWithProviders(
<AddServerForm
onSave={onSave}
onCancel={vi.fn()}
editingServer={{
id: 'srv-1',
name: 'Home',
url: 'https://music.example.com',
alternateUrl: 'http://192.168.0.10',
shareUsesLocalUrl: true,
username: 'tester',
password: 'pw',
}}
/>,
);
const user = userEvent.setup();
// Locate the alternate-url field (second URL-shaped input, prefilled).
const altInput = screen.getByDisplayValue('http://192.168.0.10');
await user.clear(altInput);
await user.click(screen.getByRole('button', { name: /save/i }));
expect(onSave).toHaveBeenCalledTimes(1);
const arg = onSave.mock.calls[0]![0];
expect(arg.url).toBe('https://music.example.com');
expect(arg).not.toHaveProperty('alternateUrl');
expect(arg).not.toHaveProperty('shareUsesLocalUrl');
});
});
describe('AddServerForm — custom HTTP headers', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('includes configured custom headers on save', async () => {
const onSave = vi.fn();
renderWithProviders(<AddServerForm onSave={onSave} onCancel={vi.fn()} />);
const user = userEvent.setup();
const inputs = screen.getAllByRole('textbox');
await user.type(inputs[1]!, 'https://music.example.com');
await user.type(inputs[3]!, 'tester');
await user.type(screen.getByPlaceholderText('••••••••'), 'pw');
await user.click(screen.getByRole('button', { name: /custom http headers/i }));
const headerNameInputs = screen.getAllByPlaceholderText(/header name/i);
const headerValueInputs = screen.getAllByPlaceholderText(/header value/i);
await user.type(headerNameInputs[0]!, 'CF-Access-Client-Secret');
await user.type(headerValueInputs[0]!, 'gate-secret');
await user.click(screen.getByRole('button', { name: 'Add' }));
expect(onSave).toHaveBeenCalledTimes(1);
const arg = onSave.mock.calls[0]![0];
expect(arg.customHeaders).toEqual([{ name: 'CF-Access-Client-Secret', value: 'gate-secret' }]);
expect(arg.customHeadersApplyTo).toBe('public');
});
});