This commit is contained in:
Ildar Kamalov 2025-08-11 14:25:16 +03:00
parent 7ecd6426d9
commit c7ca1e8eaf
17 changed files with 54 additions and 380 deletions

View File

@ -124,7 +124,12 @@ export const Menu = ({ headerMenu }: Props) => {
</nav>
<div className={s.referenceWrapper}>
<div className={cn(s.menuLinkWrapper)}>
<a href={apiClient.getLogoutUrl()} target="_blank" rel="noopener noreferrer" className={s.menuLink}>
<a
href={apiClient.getLogoutUrl()}
target="_blank"
rel="noopener noreferrer"
className={s.menuLink}
id="sign_out">
<Icon className={s.linkIcon} icon="logout" />
<span className={theme.common.textOverflow}>{intl.getMessage('logout')}</span>
</a>

View File

@ -131,7 +131,7 @@ export const Form = ({ initialValues, processing, processingReset, onSubmit, onR
<div className={theme.form.buttonGroup}>
<Button
type="submit"
data-testid="logs_config_save"
id="logs_config_save"
variant="primary"
size="small"
disabled={disableSubmit}
@ -141,7 +141,7 @@ export const Form = ({ initialValues, processing, processingReset, onSubmit, onR
<Button
type="button"
data-testid="logs_config_clear"
id="logs_config_clear"
variant="secondary"
size="small"
onClick={onReset}

View File

@ -61,7 +61,6 @@ export const Auth = ({ onAuthSubmit }: Props) => {
{...field}
type="text"
id="install_username"
data-testid="install_username"
label={t('install_auth_username')}
placeholder={t('install_auth_username_enter')}
errorMessage={fieldState.error?.message}
@ -86,7 +85,6 @@ export const Auth = ({ onAuthSubmit }: Props) => {
{...field}
type="password"
id="install_password"
data-testid="install_password"
label={t('install_auth_password')}
placeholder={t('install_auth_password_enter')}
errorMessage={fieldState.error?.message}
@ -111,7 +109,6 @@ export const Auth = ({ onAuthSubmit }: Props) => {
{...field}
type="password"
id="install_confirm_password"
data-testid="install_confirm_password"
label={t('install_auth_confirm')}
placeholder={t('install_auth_confirm')}
errorMessage={fieldState.error?.message}

View File

@ -35,7 +35,7 @@ class Controls extends Component<ControlsProps> {
case 3:
return (
<Button
data-testid="install_back"
id="install_back"
type="button"
size="small"
variant="secondary"
@ -56,7 +56,7 @@ class Controls extends Component<ControlsProps> {
case 1:
return (
<Button
data-testid="install_get_started"
id="install_get_started"
type="button"
onClick={nextStep}
size="small"
@ -69,7 +69,7 @@ class Controls extends Component<ControlsProps> {
case 3:
return (
<Button
data-testid="install_next"
id="install_next"
type="submit"
size="small"
variant="primary"
@ -81,7 +81,7 @@ class Controls extends Component<ControlsProps> {
case 4:
return (
<Button
data-testid="install_next"
id="install_next"
type="button"
size="small"
variant="primary"
@ -93,7 +93,7 @@ class Controls extends Component<ControlsProps> {
case 5:
return (
<Button
data-testid="install_open_dashboard"
id="install_open_dashboard"
type="button"
size="small"
variant="primary"

View File

@ -252,7 +252,7 @@ export const Settings = ({ handleSubmit, handleFix, validateForm, config, interf
name="web.ip"
control={control}
render={({ field }) => (
<select {...field} data-testid="install_web_ip">
<select {...field} id="install_web_ip">
<option value={ALL_INTERFACES_IP}>
{t('install_settings_all_interfaces')}
</option>
@ -281,7 +281,7 @@ export const Settings = ({ handleSubmit, handleFix, validateForm, config, interf
<Input
{...field}
type="number"
data-testid="install_web_port"
id="install_web_port"
placeholder={STANDARD_WEB_PORT.toString()}
errorMessage={fieldState.error?.message}
onChange={(e) => {
@ -301,7 +301,7 @@ export const Settings = ({ handleSubmit, handleFix, validateForm, config, interf
{isWebFixAvailable && (
<button
type="button"
data-testid="install_web_fix"
id="install_web_fix"
className="btn btn-secondary btn-sm ml-2"
onClick={() => handleAutofix('web')}>
<Trans>fix</Trans>
@ -342,7 +342,7 @@ export const Settings = ({ handleSubmit, handleFix, validateForm, config, interf
name="dns.ip"
control={control}
render={({ field }) => (
<select {...field} data-testid="install_dns_ip">
<select {...field} id="install_dns_ip">
<option value={ALL_INTERFACES_IP}>
{t('install_settings_all_interfaces')}
</option>
@ -372,7 +372,7 @@ export const Settings = ({ handleSubmit, handleFix, validateForm, config, interf
<Input
{...field}
type="number"
data-testid="install_dns_port"
id="install_dns_port"
errorMessage={fieldState.error?.message}
placeholder={STANDARD_WEB_PORT.toString()}
onChange={(e) => {
@ -393,7 +393,7 @@ export const Settings = ({ handleSubmit, handleFix, validateForm, config, interf
{isDnsFixAvailable && (
<button
type="button"
data-testid="install_dns_fix"
id="install_dns_fix"
className="btn btn-secondary btn-sm ml-2"
onClick={() => handleAutofix('dns')}>
<Trans>fix</Trans>

View File

@ -41,7 +41,6 @@ const Form = ({ onSubmit, processing }: LoginFormProps) => {
<Input
{...field}
id="username"
data-testid="username"
type="text"
label={t('username_label')}
placeholder={t('username_placeholder')}
@ -62,7 +61,6 @@ const Form = ({ onSubmit, processing }: LoginFormProps) => {
<Input
{...field}
id="password"
data-testid="password"
type="password"
label={t('password_label')}
placeholder={t('password_placeholder')}
@ -74,13 +72,7 @@ const Form = ({ onSubmit, processing }: LoginFormProps) => {
</div>
<div className="form-footer">
<Button
id="sign_in"
data-testid="sign_in"
type="submit"
variant="primary"
size="small"
disabled={processing || !isValid}>
<Button id="sign_in" type="submit" variant="primary" size="small" disabled={processing || !isValid}>
{t('sign_in')}
</Button>
</div>

View File

@ -4,31 +4,20 @@ import { ADMIN_USERNAME, ADMIN_PASSWORD } from '../constants';
test.describe('Control Panel', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login.html');
await page.getByTestId('username').click();
await page.getByTestId('username').fill(ADMIN_USERNAME);
await page.getByTestId('password').click();
await page.getByTestId('password').fill(ADMIN_PASSWORD);
await page.locator('#username').click();
await page.locator('#username').fill(ADMIN_USERNAME);
await page.locator('#password').click();
await page.locator('#password').fill(ADMIN_PASSWORD);
await page.keyboard.press('Tab');
await page.getByTestId('sign_in').click();
await page.locator('#sign_in').click();
await page.waitForURL((url) => !url.href.endsWith('/login.html'));
});
test('should sign out successfully', async ({ page }) => {
await page.getByTestId('sign_out').click();
await page.locator('#sign_out').click();
await page.waitForURL((url) => url.href.endsWith('/login.html'));
await expect(page.getByTestId('sign_in')).toBeVisible();
});
test('should change theme to dark and then light', async ({ page }) => {
await page.getByTestId('theme_dark').click();
await expect(page.locator('body[data-theme="dark"]')).toBeVisible();
await page.getByTestId('theme_light').click();
await expect(page.locator('body:not([data-theme="dark"])')).toBeVisible();
await expect(page.locator('#sign_in')).toBeVisible();
});
});

View File

@ -1,64 +0,0 @@
import { test, expect } from '@playwright/test';
import { ADMIN_PASSWORD, ADMIN_USERNAME } from '../constants';
import { getDHCPConfig } from '../helpers/network';
const dhcpConfig = getDHCPConfig();
const INTERFACE_NAME = dhcpConfig.interfaceName;
const RANGE_START = dhcpConfig.rangeStart;
const RANGE_END = dhcpConfig.rangeEnd;
const SUBNET_MASK = dhcpConfig.subnetMask;
const LEASE_TIME = '86400';
test.describe('DHCP Configuration', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login.html');
await page.getByTestId('username').click();
await page.getByTestId('username').fill(ADMIN_USERNAME);
await page.getByTestId('password').click();
await page.getByTestId('password').fill(ADMIN_PASSWORD);
await page.keyboard.press('Tab');
await page.getByTestId('sign_in').click();
await page.waitForURL((url) => !url.href.endsWith('/login.html'));
await page.goto(`/#dhcp`);
});
test('should select the correct DHCP interface', async ({ page }) => {
await page.getByTestId('interface_name').selectOption(INTERFACE_NAME);
expect(await page.locator('select[name="interface_name"]').inputValue()).toBe(INTERFACE_NAME);
});
test('should configure DHCP IPv4 settings correctly', async ({ page }) => {
await page.getByTestId('interface_name').selectOption(INTERFACE_NAME);
await page.getByTestId('v4_gateway_ip').click();
await page.getByTestId('v4_gateway_ip').fill('192.168.1.99');
await page.getByTestId('v4_subnet_mask').click();
await page.getByTestId('v4_subnet_mask').fill(SUBNET_MASK);
await page.getByTestId('v4_range_start').click();
await page.getByTestId('v4_range_start').fill(RANGE_START);
await page.getByTestId('v4_range_end').click();
await page.getByTestId('v4_range_end').fill(RANGE_END);
await page.getByTestId('v4_lease_duration').click();
await page.getByTestId('v4_lease_duration').fill(LEASE_TIME);
await page.getByTestId('v4_save').click();
});
test('should show error for invalid DHCP IPv4 range', async ({ page }) => {
await page.getByTestId('interface_name').selectOption(INTERFACE_NAME);
await page.getByTestId('v4_range_start').click();
await page.getByTestId('v4_range_start').fill(RANGE_END);
await page.getByTestId('v4_range_end').click();
await page.getByTestId('v4_range_end').fill(RANGE_START);
await page.keyboard.press('Tab');
expect(await page.getByText('Must be greater than range').isVisible()).toBe(true);
});
test('should show error for invalid DHCP IPv4 address', async ({ page }) => {
await page.getByTestId('interface_name').selectOption(INTERFACE_NAME);
await page.getByTestId('v4_gateway_ip').click();
await page.getByTestId('v4_gateway_ip').fill('192.168.1.200s');
await page.keyboard.press('Tab');
expect(await page.getByText('Invalid IPv4 address').isVisible()).toBe(true);
});
});

View File

@ -1,52 +0,0 @@
import { test, expect, type Page } from '@playwright/test';
import { ADMIN_USERNAME, ADMIN_PASSWORD } from '../constants';
test.describe('DNS Settings', () => {
test.beforeEach(async ({ page }) => {
// Login before each test
await page.goto('/login.html');
await page.getByTestId('username').click();
await page.getByTestId('username').fill(ADMIN_USERNAME);
await page.getByTestId('password').click();
await page.getByTestId('password').fill(ADMIN_PASSWORD);
await page.keyboard.press('Tab');
await page.getByTestId('sign_in').click();
await page.waitForURL((url) => !url.href.endsWith('/login.html'));
});
const runDNSSettingsTest = async (page: Page, address: string) => {
await page.goto('/#dns');
const currentDns = await page.getByTestId('upstream_dns').inputValue();
await page.getByTestId('upstream_dns').fill(address);
await page.getByTestId('dns_upstream_test').click();
await page.waitForTimeout(2000);
await expect(page.getByTestId('upstream_dns')).toHaveValue(address);
await page.getByTestId('upstream_dns').fill(currentDns);
await page.getByTestId('dns_upstream_save').click({ force: true });
};
test('test for Default DNS', async ({ page }) => {
await runDNSSettingsTest(page, 'https://dns10.quad9.net/dns-query');
});
test('test for Plain DNS', async ({ page }) => {
await runDNSSettingsTest(page, '94.140.14.140');
});
test('test for DNS-over-HTTPS', async ({ page }) => {
await runDNSSettingsTest(page, 'https://unfiltered.adguard-dns.com/dns-query');
});
test('test for DNS-over-TLS', async ({ page }) => {
await runDNSSettingsTest(page, 'tls://unfiltered.adguard-dns.com');
});
test('test for DNS-over-QUIC', async ({ page }) => {
await runDNSSettingsTest(page, 'quic://unfiltered.adguard-dns.com');
});
});

View File

@ -1,73 +0,0 @@
import { test, expect, type Page } from '@playwright/test';
import { execSync } from 'child_process';
import { ADMIN_USERNAME, ADMIN_PASSWORD } from '../constants';
test.describe('Filtering', () => {
test.beforeEach(async ({ page }) => {
// Login before each test
await page.goto('/login.html');
await page.getByTestId('username').click();
await page.getByTestId('username').fill(ADMIN_USERNAME);
await page.getByTestId('password').click();
await page.getByTestId('password').fill(ADMIN_PASSWORD);
await page.keyboard.press('Tab');
await page.getByTestId('sign_in').click();
await page.waitForURL((url) => !url.href.endsWith('/login.html'));
});
const runTerminalCommand = (command: string) => {
try {
console.info(`Executing command: ${command}`);
const output = execSync(command, { encoding: 'utf-8', stdio: 'pipe' }).trim();
console.info('Command executed successfully.');
console.debug(`Command output:\n${output}`);
return output;
} catch (error: any) {
console.error(`Command execution failed with error:\n${error.message}`);
throw new Error(`Failed to execute command: ${command}\nError: ${error.message}`);
}
}
const runCustomRuleTest = async (page: Page, domain_to_block: string) => {
await page.goto('/#custom_rules');
await page.getByTestId('custom_rule_textarea').fill(domain_to_block);
await page.getByTestId('apply_custom_rule').click();
const nslookupBlockedResult = await runTerminalCommand(`nslookup ${domain_to_block} 127.0.0.1`).toString();
console.info(`nslookup blocked CNAME result: '${nslookupBlockedResult}'`);
const currentRules = await page.getByTestId('custom_rule_textarea').inputValue();
console.debug(`Current rules before removal:\n${currentRules}`);
if (currentRules.includes(domain_to_block)) {
const updatedRules = currentRules
.split('\n')
.filter((line) => line.trim() !== domain_to_block.trim())
.join('\n');
await page.getByTestId('custom_rule_textarea').fill(updatedRules);
console.info(`Rule '${domain_to_block}' removed successfully.`);
console.info('Applying the updated filtering rules after removal.');
await page.getByTestId('apply_custom_rule').click();
await page.waitForLoadState('domcontentloaded');
console.info(`Filtering rules successfully updated after removing '${domain_to_block}'.`);
} else {
console.warn(`Rule '${domain_to_block}' not found. No changes were made.`);
}
const nslookupUnblockedResult = await runTerminalCommand(`nslookup ${domain_to_block} 127.0.0.1`).toString();
console.info(`nslookup unblocked CNAME result: '${nslookupUnblockedResult}'`);
};
test('Test blocking rule for apple.com', async ({ page }) => {
await runCustomRuleTest(page, 'apple.com');
});
});

View File

@ -5,19 +5,19 @@ import { ADMIN_USERNAME, ADMIN_PASSWORD } from '../constants';
test.describe('General Settings', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login.html');
await page.getByTestId('username').click();
await page.getByTestId('username').fill(ADMIN_USERNAME);
await page.getByTestId('password').click();
await page.getByTestId('password').fill(ADMIN_PASSWORD);
await page.locator('#username').click();
await page.locator('#username').fill(ADMIN_USERNAME);
await page.locator('#password').click();
await page.locator('#password').fill(ADMIN_PASSWORD);
await page.keyboard.press('Tab');
await page.getByTestId('sign_in').click();
await page.locator('#sign_in').click();
await page.waitForURL((url) => !url.href.endsWith('/login.html'));
});
test('should toggle browsing security feature and verify DNS changes', async ({ page }) => {
await page.goto('/#settings');
const browsingSecurity = await page.getByTestId('safebrowsing');
const browsingSecurity = await page.locator('#safebrowsing');
const browsingSecurityLabel = await browsingSecurity.locator('xpath=following-sibling::*[1]');
const initialState = await browsingSecurity.isChecked();
@ -45,7 +45,7 @@ test.describe('General Settings', () => {
test('should toggle parental control feature and verify DNS changes', async ({ page }) => {
await page.goto('/#settings');
const parentalControl = page.getByTestId('parental');
const parentalControl = page.locator('#parental');
const parentalControlLabel = await parentalControl.locator('xpath=following-sibling::*[1]');
const initialState = await parentalControl.isChecked();
@ -73,7 +73,7 @@ test.describe('General Settings', () => {
test('should toggle safe search feature', async ({ page }) => {
await page.goto('/#settings');
const safeSearch = page.getByTestId('safesearch');
const safeSearch = page.locator('#safesearch');
const safeSearchLabel = await safeSearch.locator('xpath=following-sibling::*[1]');
const initialState = await safeSearch.isChecked();

View File

@ -10,16 +10,16 @@ async function globalSetup(config: FullConfig) {
try {
await page.goto('/');
await page.getByTestId('install_get_started').click();
await page.getByTestId('install_web_port').fill(PORT.toString());
await page.getByTestId('install_next').click();
await page.getByTestId('install_username').fill(ADMIN_USERNAME);
await page.getByTestId('install_password').fill(ADMIN_PASSWORD);
await page.getByTestId('install_confirm_password').click();
await page.getByTestId('install_confirm_password').fill(ADMIN_PASSWORD);
await page.getByTestId('install_next').click();
await page.getByTestId('install_next').click();
await page.getByTestId('install_open_dashboard').click();
await page.locator('#install_get_started').click();
await page.locator('#install_web_port').fill(PORT.toString());
await page.locator('#install_next').click();
await page.locator('#install_username').fill(ADMIN_USERNAME);
await page.locator('#install_password').fill(ADMIN_PASSWORD);
await page.locator('#install_confirm_password').click();
await page.locator('#install_confirm_password').fill(ADMIN_PASSWORD);
await page.locator('#install_next').click();
await page.locator('#install_next').click();
await page.locator('#install_open_dashboard').click();
await page.waitForURL((url) => !url.href.endsWith('/install.html'));
} catch (error) {
console.error('Error during global setup:', error);

View File

@ -5,12 +5,12 @@ import { ADMIN_PASSWORD, ADMIN_USERNAME } from '../constants';
test.describe('Login', () => {
test('should successfully log in with valid credentials', async ({ page }) => {
await page.goto('/login.html');
await page.getByTestId('username').click();
await page.getByTestId('username').fill(ADMIN_USERNAME);
await page.getByTestId('password').click();
await page.getByTestId('password').fill(ADMIN_PASSWORD);
await page.locator('#username').click();
await page.locator('#username').fill(ADMIN_USERNAME);
await page.locator('#password').click();
await page.locator('#password').fill(ADMIN_PASSWORD);
await page.keyboard.press('Tab');
await page.getByTestId('sign_in').click();
await page.locator('#sign_in').click();
await page.waitForURL((url) => !url.href.endsWith('/login.html'));
});
});

View File

@ -1,124 +0,0 @@
import { test, expect } from '@playwright/test';
import { ADMIN_USERNAME, ADMIN_PASSWORD } from '../constants';
test.describe('QueryLog', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login.html');
await page.getByTestId('username').click();
await page.getByTestId('username').fill(ADMIN_USERNAME);
await page.getByTestId('password').click();
await page.getByTestId('password').fill(ADMIN_PASSWORD);
await page.keyboard.press('Tab');
await page.getByTestId('sign_in').click();
await page.waitForURL((url) => !url.href.endsWith('/login.html'));
});
test('Search of queryLog should work correctly', async ({ page }) => {
await page.route('/control/querylog', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(
{
"data": [
{
"answer": [
{
"type": "A",
"value": "77.88.44.242",
"ttl": 294
},
{
"type": "A",
"value": "5.255.255.242",
"ttl": 294
},
{
"type": "A",
"value": "77.88.55.242",
"ttl": 294
}
],
"answer_dnssec": false,
"cached": false,
"client": "127.0.0.1",
"client_info": {
"whois": {},
"name": "localhost",
"disallowed_rule": "127.0.0.1",
"disallowed": false
},
"client_proto": "",
"elapsedMs": "78.163167",
"question": {
"class": "IN",
"name": "ya.ru",
"type": "A"
},
"reason": "NotFilteredNotFound",
"rules": [],
"status": "NOERROR",
"time": "2024-07-17T16:02:37.500662+02:00",
"upstream": "https://dns10.quad9.net:443/dns-query"
},
{
"answer": [
{
"type": "A",
"value": "77.88.55.242",
"ttl": 351
},
{
"type": "A",
"value": "77.88.44.242",
"ttl": 351
},
{
"type": "A",
"value": "5.255.255.242",
"ttl": 351
}
],
"answer_dnssec": false,
"cached": false,
"client": "127.0.0.1",
"client_info": {
"whois": {},
"name": "localhost",
"disallowed_rule": "127.0.0.1",
"disallowed": false
},
"client_proto": "",
"elapsedMs": "5051.070708",
"question": {
"class": "IN",
"name": "ya.ru",
"type": "A"
},
"reason": "NotFilteredNotFound",
"rules": [],
"status": "NOERROR",
"time": "2024-07-17T16:02:37.4983+02:00",
"upstream": "https://dns10.quad9.net:443/dns-query"
}
],
"oldest": "2024-07-17T16:02:37.4983+02:00"
}
),
});
});
await page.goto('/#logs');
await page.getByTestId('querylog_search').fill('127.0.0.1');
const [request] = await Promise.all([
page.waitForRequest((req) => req.url().includes('/control/querylog')),
]);
if (request) {
expect(request.url()).toContain('search=127.0.0.1');
expect(await page.getByTestId('querylog_cell').first().isVisible()).toBe(true);
}
});
});

View File

@ -5,6 +5,7 @@ import { CleanWebpackPlugin } from 'clean-webpack-plugin';
import CopyPlugin from 'copy-webpack-plugin';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import * as url from 'url';
// eslint-disable-next-line import/extensions
import { BUILD_ENVS } from './constants.js';
// eslint-disable-next-line no-underscore-dangle

View File

@ -1,7 +1,9 @@
import { merge } from 'webpack-merge';
import yaml from 'js-yaml';
import fs from 'fs';
// eslint-disable-next-line import/extensions
import { BASE_URL } from './constants.js';
// eslint-disable-next-line import/extensions
import common from './webpack.common.js';
const ZERO_HOST = '0.0.0.0';

View File

@ -1,4 +1,5 @@
import { merge } from 'webpack-merge';
/* eslint-disable-next-line import/extensions */
import common from './webpack.common.js';
export default merge(common, {