diff --git a/package-lock.json b/package-lock.json index 1822fc0..4b6c7e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "cidr-regex": "^4.1.1", "dotenv": "^16.4.1", "ip-address": "^9.0.5", "ip-cidr": "^4.0.0" @@ -504,6 +505,18 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/cidr-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/cidr-regex/-/cidr-regex-4.1.1.tgz", + "integrity": "sha512-ekKcVp+iRB9zlKFXyx7io7nINgb0oRjgRdXNEodp1OuxRui8FXr/CA40Tz1voWUp9DPPrMyQKy01vJhDo4N1lw==", + "license": "BSD-2-Clause", + "dependencies": { + "ip-regex": "^5.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1711,6 +1724,18 @@ "node": ">=16.14.0" } }, + "node_modules/ip-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-5.0.0.tgz", + "integrity": "sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-array-buffer": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", diff --git a/package.json b/package.json index e760141..cfcbf32 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "semistandard": "^17.0.0" }, "dependencies": { + "cidr-regex": "^4.1.1", "dotenv": "^16.4.1", "ip-address": "^9.0.5", "ip-cidr": "^4.0.0" diff --git a/tests/network/lan.spec.js b/tests/network/lan.spec.js index 2d0f338..12f8d3e 100644 --- a/tests/network/lan.spec.js +++ b/tests/network/lan.spec.js @@ -1,5 +1,5 @@ import { test, expect } from '@playwright/test'; -import { getDefaultNetmask, isValidMacAddress, isValidPrivateIPV4 } from '../../utils/utils.js'; +import { clickSelect2Dropdown, getDefaultNetmask, hasToastText, isValidCidr, isValidMacAddress, isValidPrivateIPV4, isValidateDhcpInput, splitIPv6 } from '../../utils/utils.js'; test.describe('Network & Services - Network - LAN', () => { test.beforeEach(async ({ page }) => { @@ -66,7 +66,22 @@ test.describe('Network & Services - Network - LAN', () => { } else if (label === 'RX' || label === 'TX') { expect(typeof parseInt(value)).toBe('number'); } else if (label === 'IPv4') { - expect(isValidPrivateIPV4(value)).toBeTruthy(); + expect(isValidPrivateIPV4(value)).toBe(true); + } else if (label === 'IPv6 (Prefixes)') { + const splittedValue = splitIPv6(value); + if (!splittedValue) { + expect(value).toBe(false); + } else if (splittedValue.length === 1) { + const isValid = isValidCidr(splittedValue[0]); + expect(isValid).toBe(true); + } else if (splittedValue.length === 2) { + const ipv6One = splittedValue[0]; + const isValidIPv6One = isValidCidr(ipv6One); + const ipv6Two = splittedValue[1]; + const isValidIPv6Two = isValidCidr(ipv6Two); + + expect(await isValidIPv6One && isValidIPv6Two).toBe(true); + } } } } @@ -115,4 +130,65 @@ test.describe('Network & Services - Network - LAN', () => { expect(await page.isVisible('label:has-text("DHCP Start")')).toBe(false); } }); + + test('DHCP validation', async ({ page }) => { + await page.waitForSelector('#ipAddress'); + const ipAddress = await page.locator('#ipAddress'); + const ipValue = await ipAddress.inputValue().toString(); + + const networkMask = await page.locator('.lan-netmask-select'); + const networkValue = await networkMask.inputValue().toString(); + + const dhcpStart = await page.locator('#dhcpStart'); + const dhcpStartValue = await dhcpStart.inputValue(); + + const dhcpMax = await page.locator('#dhcpMax'); + const dhcpMaxValue = await dhcpMax.inputValue(); + + const isValid = isValidateDhcpInput(ipValue, networkValue, dhcpStartValue, dhcpMaxValue); + + if (isValid) { + expect(isValid).toBe(true); + } else { + expect(isValid).toBe(false); + } + }); + + test.describe('Bad IPv4 Form Submits', () => { + test('Bad IPv4', async ({ page }) => { + const ip = await page.locator('#ipAddress'); + await ip.fill('1.1.1.1.1.1.1.1.1.1.1'); + await page.click('button.button.green:has-text("Save")'); + expect(await hasToastText(page, 'Invalid IP Address')).toBeTruthy(); + }); + + test('Bad Netmask', async ({ page }) => { + await clickSelect2Dropdown(page, '.lan-netmask-select'); + await page.getByRole('searchbox', { name: 'Search' }).fill('15151515515151515'); + await page.keyboard.press('Enter'); + await page.click('button.button.green:has-text("Save")'); + expect(await hasToastText(page, 'Invalid Netmask')).toBeTruthy(); + }); + + test('Bad DHCP Start', async ({ page }) => { + await page.locator('#dhcpStart').fill('55555555555555'); + await page.click('button.button.green:has-text("Save")'); + expect(await hasToastText(page, 'Invalid DHCP Range')).toBeTruthy(); + }); + + test('Bad DHCP Max Leases', async ({ page }) => { + await page.locator('#dhcpMax').fill('55555555555555'); + await page.click('button.button.green:has-text("Save")'); + expect(await hasToastText(page, 'Invalid DHCP Range')).toBeTruthy(); + }); + }); + + test('Lease Time', async ({ page }) => { + await page.waitForTimeout(2000); + const leaseTimeInput = await page.$('#leaseTime'); + const value = await leaseTimeInput.inputValue(); + const leaseTimeValue = Number(value); + expect(leaseTimeValue).toBeGreaterThanOrEqual(1); + expect(leaseTimeValue).toBeLessThanOrEqual(12); + }); }); diff --git a/utils/utils.js b/utils/utils.js index 1e66457..1af5f62 100644 --- a/utils/utils.js +++ b/utils/utils.js @@ -46,6 +46,7 @@ export function isValidCidr(cidr, type = 0) { export async function clickSelect2Dropdown(page, selectSelector) { const select2ContainerSelector = `${selectSelector} + .select2-container`; + await page.waitForTimeout(2000); const select2ContainerExists = (await page.$(select2ContainerSelector)) !== null; @@ -60,11 +61,13 @@ export async function clickSelect2Dropdown(page, selectSelector) { export async function hasToastText(page, text) { try { - return ( - await ( - await page.waitForSelector(".swal2-popup", { state: "visible" }) - ).textContent() - ).includes(text); + await page.waitForSelector(".swal2-popup", { state: "visible" }); + const message = await page.waitForSelector("#swal2-html-container", { + state: "visible", + }); + + const textContent = await message.textContent(); + return textContent.includes(text); } catch (error) { return false; } @@ -136,3 +139,54 @@ export function getDefaultNetmask(ip) { return false; } } + +export function calculateDhcpLimits(ip, mask) { + function toBinary(octet) { + return ("00000000" + parseInt(octet, 10).toString(2)).substr(-8); + } + + const maskBinary = mask + .split(".") + .map((octet) => toBinary(octet)) + .join(""); + const subnetBits = maskBinary.split("1").length - 1; + const totalHosts = Math.pow(2, 32 - subnetBits) - 2; + + return { + minStart: 1, + maxStart: totalHosts, + maxLeases: totalHosts, + }; +} + +export function isValidateDhcpInput(ip, mask, dhcpStart, maxLeases) { + const limits = calculateDhcpLimits(ip, mask); + const isValidStart = + dhcpStart >= limits.minStart && dhcpStart <= limits.maxStart; + const isValidLeases = + maxLeases <= limits.maxLeases && + dhcpStart + maxLeases - 1 <= limits.maxStart; + + return isValidStart && isValidLeases; +} + +export function splitIPv6(ipv6Prefixes) { + if (ipv6Prefixes == "") { + return false; + } + + const splitPrefixes = ipv6Prefixes.split(/(?<=\/\d+)/); + + const formattedPrefixes = []; + + for (let i = 0; i < splitPrefixes.length; i += 2) { + const prefix = splitPrefixes[i].trim(); + const subnet = splitPrefixes[i + 1] ? splitPrefixes[i + 1].trim() : ""; + + if (prefix && subnet) { + formattedPrefixes.push(`${prefix}${subnet}`); + } + } + + return formattedPrefixes; +}