Files
3x-ui/frontend/src/test/dns-tab.test.tsx
T
MHSanaei b6c859f5b9 feat(xray): edit Lua DNS and routing scripts
xray-core 26.10.10 can hand DNS and routing to Lua: dns.script and
routing.script name a .lua file (absolute, or found next to the Xray
binary) that defines HandleDNSQuery / HandleRoute, and a DNS server's id
is how a DNS script picks it. The keys round-tripped through the JSON
template but had no field.

- DNS: a Lua Script field, and an ID field in the server modal;
- Basics: a Routing Script field beside the routing strategy;
- Routing: a warning above the rules while a script is set, since the
  core then routes only through the script and skips every rule, the
  panel's api rule included.

The descriptions say the script must still send the api inbound to the
api outbound, and that Xray will not start if the file is missing. An
empty field drops the key, because the core rejects an empty path.
2026-10-11 15:37:20 +02:00

155 lines
5.7 KiB
TypeScript

import { useEffect, useState } from 'react';
import { describe, expect, it, vi } from 'vitest';
import { fireEvent, screen } from '@testing-library/react';
import DnsTab from '@/pages/xray/dns/DnsTab';
import type { SetTemplate, XraySettingsValue } from '@/hooks/useXraySetting';
import { renderWithProviders } from './test-utils';
function withHosts(hosts: Record<string, string>): XraySettingsValue {
return {
dns: {
hosts,
servers: [],
},
} as unknown as XraySettingsValue;
}
describe('DnsTab', () => {
it('keeps an empty row after adding a host', () => {
function Harness() {
const [templateSettings, setTemplateSettings] = useState<XraySettingsValue | null>(
withHosts({ 'first.example': '1.1.1.1' }),
);
const updateTemplate: SetTemplate = (next) => {
setTemplateSettings((current) => (typeof next === 'function' ? next(current) : next));
};
return <DnsTab templateSettings={templateSettings} setTemplateSettings={updateTemplate} />;
}
renderWithProviders(<Harness />);
fireEvent.click(screen.getByRole('tab', { name: /Hosts$/ }));
fireEvent.click(screen.getByRole('button', { name: /Add Host$/ }));
expect(screen.getAllByLabelText('Domain (e.g. domain:example.com)')).toHaveLength(2);
});
it('keeps a row visible while its domain is incomplete', () => {
function Harness() {
const [templateSettings, setTemplateSettings] = useState<XraySettingsValue | null>(
withHosts({ 'first.example': '1.1.1.1' }),
);
const updateTemplate: SetTemplate = (next) => {
setTemplateSettings((current) => (typeof next === 'function' ? next(current) : next));
};
return <DnsTab templateSettings={templateSettings} setTemplateSettings={updateTemplate} />;
}
renderWithProviders(<Harness />);
fireEvent.click(screen.getByRole('tab', { name: /Hosts$/ }));
fireEvent.change(screen.getByLabelText('Domain (e.g. domain:example.com)'), {
target: { value: '' },
});
expect(
(screen.getByLabelText('Domain (e.g. domain:example.com)') as HTMLInputElement).value,
).toBe('');
});
it('shows hosts from an externally refreshed configuration', () => {
function Harness() {
const [templateSettings, setTemplateSettings] = useState<XraySettingsValue | null>(
withHosts({ 'first.example': '1.1.1.1' }),
);
const updateTemplate: SetTemplate = (next) => {
setTemplateSettings((current) => (typeof next === 'function' ? next(current) : next));
};
return (
<>
<button
type="button"
onClick={() => setTemplateSettings(withHosts({ 'second.example': '2.2.2.2' }))}
>
Refresh hosts
</button>
<DnsTab templateSettings={templateSettings} setTemplateSettings={updateTemplate} />
</>
);
}
renderWithProviders(<Harness />);
fireEvent.click(screen.getByRole('tab', { name: /Hosts$/ }));
expect(
(screen.getByLabelText('Domain (e.g. domain:example.com)') as HTMLInputElement).value,
).toBe('first.example');
fireEvent.click(screen.getByRole('button', { name: 'Refresh hosts' }));
expect(
(screen.getByLabelText('Domain (e.g. domain:example.com)') as HTMLInputElement).value,
).toBe('second.example');
});
it('clears an incomplete host draft when DNS is disabled', () => {
function Harness() {
const [templateSettings, setTemplateSettings] = useState<XraySettingsValue | null>(
withHosts({ 'first.example': '1.1.1.1' }),
);
const updateTemplate: SetTemplate = (next) => {
setTemplateSettings((current) => (typeof next === 'function' ? next(current) : next));
};
return (
<>
<button type="button" onClick={() => setTemplateSettings({})}>
Disable DNS
</button>
<button type="button" onClick={() => setTemplateSettings(withHosts({}))}>
Enable DNS
</button>
<DnsTab templateSettings={templateSettings} setTemplateSettings={updateTemplate} />
</>
);
}
renderWithProviders(<Harness />);
fireEvent.click(screen.getByRole('tab', { name: /Hosts$/ }));
fireEvent.change(screen.getByLabelText('Domain (e.g. domain:example.com)'), {
target: { value: '' },
});
fireEvent.click(screen.getByRole('button', { name: 'Disable DNS' }));
fireEvent.click(screen.getByRole('button', { name: 'Enable DNS' }));
fireEvent.click(screen.getByRole('tab', { name: /Hosts$/ }));
expect(screen.queryByLabelText('Domain (e.g. domain:example.com)')).toBeNull();
});
// xray-core 26.10.10 dns.script is a file path; an empty one fails the core build.
it('writes the Lua script path and drops the key once cleared', () => {
const seen = vi.fn();
const latest = () => seen.mock.lastCall?.[0] as { dns: Record<string, unknown> };
function Harness() {
const [templateSettings, setTemplateSettings] = useState<XraySettingsValue | null>(
withHosts({}),
);
useEffect(() => seen(templateSettings), [templateSettings]);
const updateTemplate: SetTemplate = (next) => {
setTemplateSettings((current) => (typeof next === 'function' ? next(current) : next));
};
return <DnsTab templateSettings={templateSettings} setTemplateSettings={updateTemplate} />;
}
renderWithProviders(<Harness />);
const script = screen.getByRole('textbox', { name: 'Lua Script' });
fireEvent.change(script, { target: { value: '/etc/xray/dns.lua' } });
expect(latest().dns.script).toBe('/etc/xray/dns.lua');
fireEvent.change(script, { target: { value: '' } });
expect(latest().dns).not.toHaveProperty('script');
});
});