158 lines
5.9 KiB
TypeScript
158 lines
5.9 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { cleanup, render, screen, within } from '@testing-library/react';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import {
|
|
InstanceSettingsPage,
|
|
sanitizeFleetSnapshot,
|
|
type FleetDataSource,
|
|
type FleetSnapshot,
|
|
} from './instance-settings-page.js';
|
|
|
|
afterEach(cleanup);
|
|
|
|
function deferredSource() {
|
|
let resolve!: (value: FleetSnapshot) => void;
|
|
let reject!: (reason: unknown) => void;
|
|
const load = vi.fn<FleetDataSource['load']>(
|
|
() =>
|
|
new Promise((done, fail) => {
|
|
resolve = done;
|
|
reject = fail;
|
|
}),
|
|
);
|
|
return {
|
|
source: { load },
|
|
load,
|
|
resolve: (value: FleetSnapshot) => resolve(value),
|
|
reject: (reason: unknown) => reject(reason),
|
|
};
|
|
}
|
|
|
|
const snapshot: FleetSnapshot = {
|
|
instances: [
|
|
{
|
|
id: 'west/one',
|
|
name: 'West modem',
|
|
url: 'https://operator:secret@west.example:8443/admin?token=secret',
|
|
description: 'must not appear',
|
|
tags: ['private-tag'],
|
|
},
|
|
{ id: 'offline', url: 'javascript:alert(1)' },
|
|
],
|
|
statuses: new Map([
|
|
[
|
|
'west/one',
|
|
{
|
|
reachable: true,
|
|
authenticated: true,
|
|
latencyMs: 12,
|
|
summary: {
|
|
capabilities: ['messages', 'calls', 7],
|
|
freshness: 'fresh',
|
|
password: 'never-render',
|
|
credentialConfigured: true,
|
|
version: 'also-not-part-of-this-page',
|
|
},
|
|
},
|
|
],
|
|
['offline', { reachable: false }],
|
|
]),
|
|
};
|
|
|
|
describe('Settings instances page', () => {
|
|
it('rejects identity values instead of trimming, truncating, or admitting controls', () => {
|
|
const longId = 'x'.repeat(257);
|
|
const source = (id: string, name?: string) => ({
|
|
instances: [{ id, url: 'https://example.test', ...(name === undefined ? {} : { name }) }],
|
|
statuses: new Map(),
|
|
});
|
|
|
|
for (const id of [' padded', 'padded ', longId, 'line\nbreak', 'null\0byte']) {
|
|
expect(sanitizeFleetSnapshot(source(id))).toEqual({ instances: [], statuses: new Map() });
|
|
}
|
|
expect(sanitizeFleetSnapshot(source('stable-id', ` ${'n'.repeat(300)} `))?.instances).toEqual(
|
|
[{ id: 'stable-id', url: 'https://example.test', name: 'n'.repeat(256) }],
|
|
);
|
|
});
|
|
|
|
it('is explicitly unavailable without an injected fleet source', () => {
|
|
render(<InstanceSettingsPage />);
|
|
expect(screen.getByRole('heading', { name: 'Instances' })).toBeTruthy();
|
|
expect(screen.getByRole('status', { name: 'Instances unavailable' }).textContent).toMatch(
|
|
/no fleet data source/i,
|
|
);
|
|
expect(screen.getByRole('link', { name: 'Add instance' }).getAttribute('href')).toBe(
|
|
'/instances/new',
|
|
);
|
|
});
|
|
|
|
it('loads and renders an accessible, safe settings list using only fleet fields', async () => {
|
|
const pending = deferredSource();
|
|
render(<InstanceSettingsPage dataSource={pending.source} />);
|
|
expect(screen.getByRole('status', { name: 'Instances loading status' })).toBeTruthy();
|
|
expect(pending.load).toHaveBeenCalledWith(expect.any(AbortSignal));
|
|
pending.resolve(snapshot);
|
|
|
|
const list = await screen.findByRole('list', { name: 'Configured instances' });
|
|
const west = within(list).getByRole('listitem', { name: 'West modem' });
|
|
expect(within(west).getByRole('link', { name: 'West modem' }).getAttribute('href')).toBe(
|
|
'/settings/instances/west%2Fone',
|
|
);
|
|
const origin = within(west).getByRole('link', { name: 'Open West modem origin' });
|
|
expect(origin.getAttribute('href')).toBe('https://west.example:8443');
|
|
expect(origin.getAttribute('rel')).toBe('noopener noreferrer');
|
|
for (const value of ['Online', 'Authenticated', 'Fresh', 'messages, calls'])
|
|
expect(within(west).getByText(value)).toBeTruthy();
|
|
|
|
const offline = within(list).getByRole('listitem', { name: 'offline' });
|
|
expect(within(offline).getByText('Invalid origin')).toBeTruthy();
|
|
expect(within(offline).getByText('Offline')).toBeTruthy();
|
|
expect(within(offline).queryByText(/authentication|freshness|capabilities/i)).toBeNull();
|
|
|
|
expect(document.body.textContent).not.toMatch(
|
|
/secret|must not appear|private-tag|never-render|credentialConfigured|also-not-part/,
|
|
);
|
|
});
|
|
|
|
it('shows a fixed error, retries, and never exposes rejection details', async () => {
|
|
const user = userEvent.setup();
|
|
const load = vi
|
|
.fn<FleetDataSource['load']>()
|
|
.mockRejectedValueOnce(new Error('password=top-secret'))
|
|
.mockResolvedValueOnce({ instances: [], statuses: new Map() });
|
|
render(<InstanceSettingsPage dataSource={{ load }} />);
|
|
const alert = await screen.findByRole('alert');
|
|
expect(alert.textContent).toBe('Instances could not be loaded.Retry loading instances');
|
|
expect(document.body.textContent).not.toContain('top-secret');
|
|
await user.click(screen.getByRole('button', { name: 'Retry loading instances' }));
|
|
expect(await screen.findByText('No instances are configured.')).toBeTruthy();
|
|
expect(load).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('aborts superseded loads and fences late results', async () => {
|
|
const first = deferredSource();
|
|
const second = deferredSource();
|
|
let calls = 0;
|
|
const load = vi.fn<FleetDataSource['load']>((signal) =>
|
|
(calls++ === 0 ? first : second).source.load(signal),
|
|
);
|
|
const source = { load };
|
|
const { rerender } = render(<InstanceSettingsPage dataSource={source} refreshSignal={0} />);
|
|
const oldSignal = load.mock.calls[0]?.[0];
|
|
rerender(<InstanceSettingsPage dataSource={source} refreshSignal={1} />);
|
|
expect(oldSignal?.aborted).toBe(true);
|
|
first.resolve({
|
|
instances: [{ id: 'stale', url: 'https://stale.example' }],
|
|
statuses: new Map(),
|
|
});
|
|
second.resolve({
|
|
instances: [{ id: 'current', url: 'https://current.example' }],
|
|
statuses: new Map(),
|
|
});
|
|
expect(await screen.findByRole('link', { name: 'current' })).toBeTruthy();
|
|
expect(screen.queryByRole('link', { name: 'stale' })).toBeNull();
|
|
});
|
|
});
|