feat(editor): F4.5c the DHCP server and the port forwarding in the device sheet
Measured first: SheetRun now types into FILIUS' DHCP dialog and keeps rows
in its port forwarding dialog. Java's DHCP server keeps its bounds as typed
(".0.0.1" too), never takes an empty gateway or DNS server, hands out the
device's gateway and DNS server unless its own (a home router always its
LAN address, where "manual settings" do nothing), drops a second static
assignment of a MAC address silently and lets "_" through its MAC pattern;
the port forwarding dialog keeps any row, its firmware forwards only the
valid ones. Here a value passes its rule or is not taken, with a word why.
- doc: DHCP server addresses and flags, static assignments (a MAC address
once, ignoring case), port forwarding rows (TCP or UDP, musterPort, the
LAN address); the sheet shows what the server hands out and which rows
Java's firmware forwards (the core's parsePortForwardingRow, exported)
- editor: Java's dialogs as pages of the sheet with a back button, the
three row tables on one RowsEditor (the forwarding table rebuilt on it);
a sheet per device, so an open page closes when another device is chosen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,7 @@
|
||||
|
||||
import { IP_ADDRESS_PATTERN, javaIpCheck } from '@mdg-filius/core';
|
||||
|
||||
export type FieldRule = 'name' | 'ipAddress' | 'ipAddressOrEmpty' | 'subnetMask' | 'ssid' | 'seconds' | 'port';
|
||||
export type FieldRule = 'name' | 'ipAddress' | 'ipAddressOrEmpty' | 'subnetMask' | 'ssid' | 'seconds' | 'port' | 'macAddress';
|
||||
|
||||
/** What a field's text comes to: the value the device takes, or that it takes none. */
|
||||
export type CheckedField = { readonly value: string } | { readonly invalid: true };
|
||||
@@ -31,6 +31,12 @@ const SUBNET_MASK_PATTERN = /^(0*([1-9]|1?[0-9]{1,2}|2[0-4]?[0-9]|25[0-5]))\.((0
|
||||
const SSID_PATTERN = /^[ -~]+$/;
|
||||
/** EingabenUeberpruefung.musterPort: 1 to 65535, no leading zero. */
|
||||
const PORT_PATTERN = /^([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{1,4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/;
|
||||
/**
|
||||
* EingabenUeberpruefung.musterMacAddress, whose class [0-9a-fA-f] also lets
|
||||
* the six characters between Z and a through ("AA:BB:CC:DD:EE:_F"); here
|
||||
* hexadecimal digits only.
|
||||
*/
|
||||
const MAC_ADDRESS_PATTERN = /^[0-9A-Fa-f]{2}(:[0-9A-Fa-f]{2}){5}$/;
|
||||
/** EingabenUeberpruefung.musterNurZahlen, at least one digit (Java's Long.parseLong refuses none). */
|
||||
const DIGITS_PATTERN = /^[0-9]+$/;
|
||||
|
||||
@@ -65,5 +71,8 @@ export function checkField(rule: FieldRule, text: string): CheckedField {
|
||||
}
|
||||
case 'port':
|
||||
return PORT_PATTERN.test(text.trim()) ? { value: text.trim() } : INVALID;
|
||||
case 'macAddress':
|
||||
// Upper case, as the model keeps MAC addresses (Java keeps them as typed and compares ignoring case).
|
||||
return MAC_ADDRESS_PATTERN.test(text.trim()) ? { value: text.trim().toUpperCase() } : INVALID;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,12 +20,27 @@
|
||||
// Nor: an interface address that passes the panel's pattern but not
|
||||
// IP.ipCheck (".0.0.1") becomes null in Java, and a router's panel then
|
||||
// throws; a table cell Java refuses is stored as null. Here neither is taken.
|
||||
// Nor that a DHCP server keeps its bounds as typed (".0.0.1" too), drops a
|
||||
// second static assignment of a MAC address without a word, and that a home
|
||||
// router keeps any port forwarding row, valid or not (its firmware then
|
||||
// forwards only the valid ones): here a value passes its rule or is not
|
||||
// taken, and the sheet says why.
|
||||
|
||||
import * as Y from 'yjs';
|
||||
import { DEFAULT_REMOTE_LINK_LATENCY_MILLISECONDS, defaultSwitchConfiguration, forwardsIPv4ByDefault, type Id, type ManualRoute, type NodeKind } from '@mdg-filius/core';
|
||||
import {
|
||||
DEFAULT_REMOTE_LINK_LATENCY_MILLISECONDS,
|
||||
defaultSwitchConfiguration,
|
||||
dhcpServerConfiguration,
|
||||
forwardsIPv4ByDefault,
|
||||
type DhcpStaticAssignment,
|
||||
type GatewayPortForwardingRow,
|
||||
type Id,
|
||||
type ManualRoute,
|
||||
type NodeKind,
|
||||
} from '@mdg-filius/core';
|
||||
import { modemPair } from '@mdg-filius/fls';
|
||||
import { connect, type ConnectError } from './commands.js';
|
||||
import { checkField, type CheckedField } from './fields.js';
|
||||
import { checkField, type CheckedField, type FieldRule } from './fields.js';
|
||||
import { inOrder, ROOTS } from './projectDoc.js';
|
||||
import { toY } from './values.js';
|
||||
|
||||
@@ -319,3 +334,150 @@ export function removeRoute(doc: Y.Doc, id: Id, index: number, origin: unknown):
|
||||
if (routes.length === 0) settingsOf(node).delete('routes');
|
||||
}, origin);
|
||||
}
|
||||
|
||||
/** Checks a row's cells by their rules: the values, or the columns that do not pass. */
|
||||
function checkRow<Column extends string>(
|
||||
columns: readonly Column[],
|
||||
rule: (column: Column) => FieldRule,
|
||||
cells: Readonly<Record<Column, string>>,
|
||||
): { readonly values: Record<Column, string> } | { readonly invalid: readonly Column[] } {
|
||||
const checked = columns.map((column) => [column, checkField(rule(column), cells[column])] as const);
|
||||
const invalid = checked.filter(([, each]) => 'invalid' in each).map(([column]) => column);
|
||||
if (invalid.length > 0) return { invalid };
|
||||
return { values: Object.fromEntries(checked.map(([column, each]) => [column, (each as { value: string }).value])) as Record<Column, string> };
|
||||
}
|
||||
|
||||
/** A device's DHCP server, made as Java's constructor leaves it when the device has none in the document. */
|
||||
function dhcpServerOf(node: Y.Map<unknown>): Y.Map<unknown> {
|
||||
let server = settingsOf(node).get('dhcpServer') as Y.Map<unknown> | undefined;
|
||||
if (server === undefined) {
|
||||
server = toY({ ...dhcpServerConfiguration(), staticAssignments: [] }) as Y.Map<unknown>;
|
||||
settingsOf(node).set('dhcpServer', server);
|
||||
}
|
||||
return server;
|
||||
}
|
||||
|
||||
export type DhcpServerField = 'lowerBoundIPAddress' | 'upperBoundIPAddress' | 'gatewayIPAddress' | 'dnsServerIPAddress';
|
||||
|
||||
/** An address of a DHCP server (JDHCPKonfiguration: musterIpAdresse, never empty), as Java's IP.ipCheck writes it. */
|
||||
export function setDhcpServerAddress(doc: Y.Doc, id: Id, field: DhcpServerField, text: string, origin: unknown): CheckedField {
|
||||
return setChecked(doc, id, checkField('ipAddress', text), (node, value) => dhcpServerOf(node).set(field, value), origin);
|
||||
}
|
||||
|
||||
/** A DHCP server switched on or off, or handing out its own gateway and DNS server instead of the device's. */
|
||||
export function setDhcpServerFlag(doc: Y.Doc, id: Id, flag: 'isActive' | 'useOwnSettings', on: boolean, origin: unknown): void {
|
||||
const node = nodes(doc).get(id);
|
||||
if (node !== undefined) doc.transact(() => dhcpServerOf(node).set(flag, on), origin);
|
||||
}
|
||||
|
||||
export type StaticAssignmentColumn = 'macAddress' | 'ipAddress';
|
||||
export const STATIC_ASSIGNMENT_COLUMNS: readonly StaticAssignmentColumn[] = ['macAddress', 'ipAddress'];
|
||||
const staticRule = (column: StaticAssignmentColumn): FieldRule => (column === 'macAddress' ? 'macAddress' : 'ipAddress');
|
||||
|
||||
const staticAssignmentsOf = (node: Y.Map<unknown>) => {
|
||||
const server = dhcpServerOf(node);
|
||||
let list = server.get('staticAssignments') as Y.Array<Y.Map<unknown>> | undefined;
|
||||
if (list === undefined) {
|
||||
list = new Y.Array<Y.Map<unknown>>();
|
||||
server.set('staticAssignments', list);
|
||||
}
|
||||
return list;
|
||||
};
|
||||
|
||||
/** Whether another static assignment has this MAC address (Java compares ignoring case). */
|
||||
function macTaken(list: Y.Array<Y.Map<unknown>>, mac: string, except: number): boolean {
|
||||
return list.toArray().some((entry, index) => index !== except && (entry.get('macAddress') as string).toUpperCase() === mac.toUpperCase());
|
||||
}
|
||||
|
||||
/** A static assignment of the DHCP server: once both cells pass and the MAC address has none yet. */
|
||||
export function addDhcpStaticAssignment(
|
||||
doc: Y.Doc,
|
||||
id: Id,
|
||||
cells: Readonly<Record<StaticAssignmentColumn, string>>,
|
||||
assignmentId: Id,
|
||||
origin: unknown,
|
||||
): { readonly assignment: DhcpStaticAssignment } | { readonly invalid: readonly StaticAssignmentColumn[] } | { readonly error: 'macTaken' } {
|
||||
const checked = checkRow(STATIC_ASSIGNMENT_COLUMNS, staticRule, cells);
|
||||
const node = nodes(doc).get(id);
|
||||
if ('invalid' in checked || node === undefined) return 'invalid' in checked ? checked : { invalid: [] };
|
||||
const existing = settingsOf(node).get('dhcpServer') as Y.Map<unknown> | undefined;
|
||||
const list = existing?.get('staticAssignments') as Y.Array<Y.Map<unknown>> | undefined;
|
||||
if (list !== undefined && macTaken(list, checked.values.macAddress, -1)) return { error: 'macTaken' };
|
||||
const assignment = { id: assignmentId, ...checked.values };
|
||||
doc.transact(() => staticAssignmentsOf(node).push([toY(assignment) as Y.Map<unknown>]), origin);
|
||||
return { assignment };
|
||||
}
|
||||
|
||||
/** A cell of a static assignment; a MAC address another assignment has is not taken. */
|
||||
export function setDhcpStaticCell(doc: Y.Doc, id: Id, index: number, column: StaticAssignmentColumn, text: string, origin: unknown): CheckedField {
|
||||
const node = nodes(doc).get(id);
|
||||
const list = (node?.get('settings') as Y.Map<unknown> | undefined)?.get('dhcpServer') as Y.Map<unknown> | undefined;
|
||||
const entries = list?.get('staticAssignments') as Y.Array<Y.Map<unknown>> | undefined;
|
||||
const checked = checkField(staticRule(column), text);
|
||||
if (entries === undefined || index < 0 || index >= entries.length) return checked;
|
||||
if ('value' in checked && column === 'macAddress' && macTaken(entries, checked.value, index)) return { invalid: true };
|
||||
return setChecked(doc, id, checked, () => entries.get(index).set(column, (checked as { value: string }).value), origin);
|
||||
}
|
||||
|
||||
export function removeDhcpStaticAssignment(doc: Y.Doc, id: Id, index: number, origin: unknown): void {
|
||||
const node = nodes(doc).get(id);
|
||||
const entries = ((node?.get('settings') as Y.Map<unknown> | undefined)?.get('dhcpServer') as Y.Map<unknown> | undefined)?.get('staticAssignments') as Y.Array<Y.Map<unknown>> | undefined;
|
||||
if (entries === undefined || index < 0 || index >= entries.length) return;
|
||||
doc.transact(() => entries.delete(index, 1), origin);
|
||||
}
|
||||
|
||||
export type PortForwardingColumn = keyof GatewayPortForwardingRow;
|
||||
export const PORT_FORWARDING_COLUMNS: readonly PortForwardingColumn[] = ['protocolValue', 'publicPortValue', 'lanIPAddress', 'lanPortValue'];
|
||||
/** Java's protocols of a port forwarding row, as its dialog offers them. */
|
||||
export const PORT_FORWARDING_PROTOCOLS = ['TCP', 'UDP'] as const;
|
||||
|
||||
/** Each column's rule (GatewayFirmware.validateData): a protocol, ports by musterPort, the LAN address by musterIpAdresse. */
|
||||
const forwardingRule = (column: PortForwardingColumn): FieldRule => (column === 'lanIPAddress' ? 'ipAddress' : column === 'protocolValue' ? 'name' : 'port');
|
||||
|
||||
function checkForwardingRow(cells: Readonly<Record<PortForwardingColumn, string>>) {
|
||||
const checked = checkRow(PORT_FORWARDING_COLUMNS, forwardingRule, cells);
|
||||
const protocolTaken = (PORT_FORWARDING_PROTOCOLS as readonly string[]).includes(cells.protocolValue);
|
||||
if ('invalid' in checked) return protocolTaken ? checked : { invalid: ['protocolValue' as const, ...checked.invalid] };
|
||||
return protocolTaken ? checked : { invalid: ['protocolValue' as const] };
|
||||
}
|
||||
|
||||
const forwardingOf = (node: Y.Map<unknown>) => {
|
||||
let list = settingsOf(node).get('portForwarding') as Y.Array<Y.Map<unknown>> | undefined;
|
||||
if (list === undefined) {
|
||||
list = new Y.Array<Y.Map<unknown>>();
|
||||
settingsOf(node).set('portForwarding', list);
|
||||
}
|
||||
return list;
|
||||
};
|
||||
|
||||
/** A home router's port forwarding row, once all four cells pass (Java keeps any row; its firmware forwards only the valid ones). */
|
||||
export function addPortForwarding(
|
||||
doc: Y.Doc,
|
||||
id: Id,
|
||||
cells: Readonly<Record<PortForwardingColumn, string>>,
|
||||
origin: unknown,
|
||||
): { readonly row: GatewayPortForwardingRow } | { readonly invalid: readonly PortForwardingColumn[] } {
|
||||
const checked = checkForwardingRow(cells);
|
||||
const node = nodes(doc).get(id);
|
||||
if ('invalid' in checked || node === undefined) return 'invalid' in checked ? checked : { invalid: [] };
|
||||
const row = checked.values as GatewayPortForwardingRow;
|
||||
doc.transact(() => forwardingOf(node).push([toY({ ...row }) as Y.Map<unknown>]), origin);
|
||||
return { row };
|
||||
}
|
||||
|
||||
export function setPortForwardingCell(doc: Y.Doc, id: Id, index: number, column: PortForwardingColumn, text: string, origin: unknown): CheckedField {
|
||||
const checked: CheckedField =
|
||||
column === 'protocolValue' ? ((PORT_FORWARDING_PROTOCOLS as readonly string[]).includes(text) ? { value: text } : { invalid: true }) : checkField(forwardingRule(column), text);
|
||||
return setChecked(doc, id, checked, (node, value) => (settingsOf(node).get('portForwarding') as Y.Array<Y.Map<unknown>> | undefined)?.get(index)?.set(column, value), origin);
|
||||
}
|
||||
|
||||
/** Removes a port forwarding row; the list goes with its last row, as the project keeps it. */
|
||||
export function removePortForwarding(doc: Y.Doc, id: Id, index: number, origin: unknown): void {
|
||||
const node = nodes(doc).get(id);
|
||||
const list = node === undefined ? undefined : (settingsOf(node).get('portForwarding') as Y.Array<Y.Map<unknown>> | undefined);
|
||||
if (node === undefined || list === undefined || index < 0 || index >= list.length) return;
|
||||
doc.transact(() => {
|
||||
list.delete(index, 1);
|
||||
if (list.length === 0) settingsOf(node).delete('portForwarding');
|
||||
}, origin);
|
||||
}
|
||||
|
||||
@@ -7,8 +7,12 @@ import * as Y from 'yjs';
|
||||
import {
|
||||
defaultSwitchConfiguration,
|
||||
DEFAULT_SWITCH_RETENTION_MILLISECONDS,
|
||||
dhcpServerConfiguration,
|
||||
forwardsIPv4ByDefault,
|
||||
parsePortForwardingRow,
|
||||
routeTableRows,
|
||||
type DhcpServerConfiguration,
|
||||
type GatewayPortForwardingRow,
|
||||
stableGeneratedMacAddress,
|
||||
type HostLabelPolicy,
|
||||
type ManualRoute,
|
||||
@@ -20,6 +24,25 @@ import { inOrder, ROOTS } from './projectDoc.js';
|
||||
import { fromY } from './values.js';
|
||||
import { nodeLabel } from './view.js';
|
||||
|
||||
/** A device's DHCP server as Java's dialog shows it (JDHCPKonfiguration). */
|
||||
export interface DhcpServerSheet {
|
||||
readonly isActive: boolean;
|
||||
readonly lowerBoundIPAddress: string;
|
||||
readonly upperBoundIPAddress: string;
|
||||
/** The device's own mask (the LAN card's on a home router), shown, not changed. */
|
||||
readonly subnetMask: string;
|
||||
/** What the server hands out (DHCPServer.determineGatewayip): its own setting, the device's gateway (0.0.0.0 without one), a home router's LAN address. */
|
||||
readonly gateway: string;
|
||||
readonly gatewayEditable: boolean;
|
||||
/** determineDnsserverip: its own setting (always on a home router), else the device's DNS server or 0.0.0.0. */
|
||||
readonly dnsServer: string;
|
||||
readonly dnsServerEditable: boolean;
|
||||
readonly useOwnSettings: boolean;
|
||||
/** A home router's dialog leaves the choice without effect; the sheet offers it on a PC or notebook only. */
|
||||
readonly ownSettingsOffered: boolean;
|
||||
readonly staticAssignments: readonly { readonly macAddress: string; readonly ipAddress: string }[];
|
||||
}
|
||||
|
||||
export interface HostSheet {
|
||||
readonly kind: 'pc' | 'notebook';
|
||||
readonly name: string;
|
||||
@@ -40,6 +63,7 @@ export interface HostSheet {
|
||||
readonly wirelessPossible: boolean;
|
||||
/** The switches' SSIDs, in the order of the devices, each once: the networks to choose from. */
|
||||
readonly networks: readonly string[];
|
||||
readonly dhcpServer: DhcpServerSheet;
|
||||
}
|
||||
|
||||
export interface SwitchSheet {
|
||||
@@ -89,6 +113,9 @@ export interface HomeRouterSheet {
|
||||
readonly dhcpClient: boolean;
|
||||
readonly wan: InterfaceSheet;
|
||||
readonly lan: InterfaceSheet;
|
||||
readonly dhcpServer: DhcpServerSheet;
|
||||
/** The port forwarding rows, each with whether Java's firmware forwards it (GatewayFirmware.validateData). */
|
||||
readonly portForwarding: readonly (GatewayPortForwardingRow & { readonly forwarded: boolean })[];
|
||||
}
|
||||
|
||||
export interface NameSheet {
|
||||
@@ -123,6 +150,27 @@ function interfacesOf(doc: Y.Doc, id: string, node: Y.Map<unknown>): InterfaceSh
|
||||
});
|
||||
}
|
||||
|
||||
/** The DHCP server's sheet, from the device's own address settings (or a home router's LAN card). */
|
||||
function dhcpServerSheet(node: Y.Map<unknown>, own: { readonly subnetMask: string; readonly gateway: string; readonly dnsServer: string }, home: boolean): DhcpServerSheet {
|
||||
const stored = (fromY(settingsOf(node).get('dhcpServer')) ?? {}) as Partial<DhcpServerConfiguration>;
|
||||
const server = dhcpServerConfiguration(stored);
|
||||
const unset = (value: string) => (value === '' ? '0.0.0.0' : value);
|
||||
const ownSettings = !home && server.useOwnSettings;
|
||||
return {
|
||||
isActive: server.isActive,
|
||||
lowerBoundIPAddress: server.lowerBoundIPAddress,
|
||||
upperBoundIPAddress: server.upperBoundIPAddress,
|
||||
subnetMask: own.subnetMask,
|
||||
gateway: home ? own.gateway : ownSettings ? server.gatewayIPAddress : unset(own.gateway),
|
||||
gatewayEditable: ownSettings,
|
||||
dnsServer: home || ownSettings ? server.dnsServerIPAddress : unset(own.dnsServer),
|
||||
dnsServerEditable: home || ownSettings,
|
||||
useOwnSettings: server.useOwnSettings,
|
||||
ownSettingsOffered: !home,
|
||||
staticAssignments: server.staticAssignments.map(({ macAddress, ipAddress }) => ({ macAddress, ipAddress })),
|
||||
};
|
||||
}
|
||||
|
||||
/** The sheet of a device, or undefined when the document has no such device. */
|
||||
export function deviceSheet(doc: Y.Doc, id: string): DeviceSheet | undefined {
|
||||
const all = doc.getMap<Y.Map<unknown>>(ROOTS.nodes);
|
||||
@@ -160,6 +208,7 @@ export function deviceSheet(doc: Y.Doc, id: string): DeviceSheet | undefined {
|
||||
ssid: (wireless?.get('ssid') as string | undefined) ?? '',
|
||||
wirelessPossible: isWireless || !plugged,
|
||||
networks: [...networks],
|
||||
dhcpServer: dhcpServerSheet(node, { subnetMask: text(device, 'subnetMask'), gateway: text(device, 'defaultGateway'), dnsServer: text(device, 'dnsServer') }, false),
|
||||
};
|
||||
}
|
||||
case 'networkSwitch': {
|
||||
@@ -192,6 +241,7 @@ export function deviceSheet(doc: Y.Doc, id: string): DeviceSheet | undefined {
|
||||
}
|
||||
case 'gateway': {
|
||||
const [wan, lan] = interfacesOf(doc, id, node);
|
||||
const rows = (fromY(settingsOf(node).get('portForwarding')) ?? []) as unknown as GatewayPortForwardingRow[];
|
||||
return {
|
||||
kind,
|
||||
name,
|
||||
@@ -200,6 +250,9 @@ export function deviceSheet(doc: Y.Doc, id: string): DeviceSheet | undefined {
|
||||
dhcpClient: setting(node, 'dhcpClient')?.get('isEnabled') === true,
|
||||
wan: wan!,
|
||||
lan: lan!,
|
||||
// Java's home router hands out its LAN address as the gateway, with the LAN card's mask.
|
||||
dhcpServer: dhcpServerSheet(node, { subnetMask: lan!.subnetMask, gateway: lan!.ipAddress, dnsServer: '' }, true),
|
||||
portForwarding: rows.map((row) => ({ ...row, forwarded: parsePortForwardingRow(row) !== undefined })),
|
||||
};
|
||||
}
|
||||
case 'unsupported':
|
||||
|
||||
@@ -116,3 +116,22 @@ homeRouter lanIp 192.168.1.256
|
||||
homeRouter lanIp 192.168.1.1
|
||||
homeRouter lanMask 255.255.255.128
|
||||
homeRouter lanMask abc
|
||||
dhcpHost lower 192.168.0.110
|
||||
dhcpHost lower 010.000.000.001
|
||||
dhcpHost lower .0.0.1
|
||||
dhcpHost lower 10..0.0.1
|
||||
dhcpHost lower 256.0.0.1
|
||||
dhcpHost lower
|
||||
dhcpHost lower 192.168.0.110
|
||||
dhcpHost upper 192.168.0.250
|
||||
dhcpHost upper abc
|
||||
dhcpHost gateway 10.0.0.9
|
||||
dhcpHost gateway
|
||||
dhcpHost gateway 10.0.0
|
||||
dhcpHost dns 10.0.0.8
|
||||
dhcpHost dns
|
||||
dhcpHost dns 10.0.0.256
|
||||
dhcpHome dns 42.0.0.53
|
||||
dhcpHome dns 42.0.0
|
||||
dhcpHome lower 192.168.0.110
|
||||
dhcpHome gateway 10.0.0.9
|
||||
|
||||
@@ -1035,10 +1035,181 @@
|
||||
"marked": true,
|
||||
"stored": "255.255.255.0",
|
||||
"shown": "255.255.255.0"
|
||||
},
|
||||
{
|
||||
"device": "dhcpHost",
|
||||
"field": "lower",
|
||||
"input": "192.168.0.110",
|
||||
"start": "192.168.0.100",
|
||||
"marked": false,
|
||||
"stored": "192.168.0.110",
|
||||
"shown": "192.168.0.110"
|
||||
},
|
||||
{
|
||||
"device": "dhcpHost",
|
||||
"field": "lower",
|
||||
"input": "010.000.000.001",
|
||||
"start": "192.168.0.100",
|
||||
"marked": false,
|
||||
"stored": "010.000.000.001",
|
||||
"shown": "010.000.000.001"
|
||||
},
|
||||
{
|
||||
"device": "dhcpHost",
|
||||
"field": "lower",
|
||||
"input": ".0.0.1",
|
||||
"start": "192.168.0.100",
|
||||
"marked": false,
|
||||
"stored": ".0.0.1",
|
||||
"shown": ".0.0.1"
|
||||
},
|
||||
{
|
||||
"device": "dhcpHost",
|
||||
"field": "lower",
|
||||
"input": "10..0.0.1",
|
||||
"start": "192.168.0.100",
|
||||
"marked": true,
|
||||
"stored": "192.168.0.100",
|
||||
"shown": "192.168.0.100"
|
||||
},
|
||||
{
|
||||
"device": "dhcpHost",
|
||||
"field": "lower",
|
||||
"input": "256.0.0.1",
|
||||
"start": "192.168.0.100",
|
||||
"marked": true,
|
||||
"stored": "192.168.0.100",
|
||||
"shown": "192.168.0.100"
|
||||
},
|
||||
{
|
||||
"device": "dhcpHost",
|
||||
"field": "lower",
|
||||
"input": "",
|
||||
"start": "192.168.0.100",
|
||||
"marked": true,
|
||||
"stored": "192.168.0.100",
|
||||
"shown": "192.168.0.100"
|
||||
},
|
||||
{
|
||||
"device": "dhcpHost",
|
||||
"field": "lower",
|
||||
"input": " 192.168.0.110",
|
||||
"start": "192.168.0.100",
|
||||
"marked": true,
|
||||
"stored": "192.168.0.100",
|
||||
"shown": "192.168.0.100"
|
||||
},
|
||||
{
|
||||
"device": "dhcpHost",
|
||||
"field": "upper",
|
||||
"input": "192.168.0.250",
|
||||
"start": "192.168.0.200",
|
||||
"marked": false,
|
||||
"stored": "192.168.0.250",
|
||||
"shown": "192.168.0.250"
|
||||
},
|
||||
{
|
||||
"device": "dhcpHost",
|
||||
"field": "upper",
|
||||
"input": "abc",
|
||||
"start": "192.168.0.200",
|
||||
"marked": true,
|
||||
"stored": "192.168.0.200",
|
||||
"shown": "192.168.0.200"
|
||||
},
|
||||
{
|
||||
"device": "dhcpHost",
|
||||
"field": "gateway",
|
||||
"input": "10.0.0.9",
|
||||
"start": "10.0.0.1",
|
||||
"marked": false,
|
||||
"stored": "10.0.0.9",
|
||||
"shown": "10.0.0.9"
|
||||
},
|
||||
{
|
||||
"device": "dhcpHost",
|
||||
"field": "gateway",
|
||||
"input": "",
|
||||
"start": "10.0.0.1",
|
||||
"marked": true,
|
||||
"stored": "10.0.0.1",
|
||||
"shown": "10.0.0.1"
|
||||
},
|
||||
{
|
||||
"device": "dhcpHost",
|
||||
"field": "gateway",
|
||||
"input": "10.0.0",
|
||||
"start": "10.0.0.1",
|
||||
"marked": true,
|
||||
"stored": "10.0.0.1",
|
||||
"shown": "10.0.0.1"
|
||||
},
|
||||
{
|
||||
"device": "dhcpHost",
|
||||
"field": "dns",
|
||||
"input": "10.0.0.8",
|
||||
"start": "10.0.0.2",
|
||||
"marked": false,
|
||||
"stored": "10.0.0.8",
|
||||
"shown": "10.0.0.8"
|
||||
},
|
||||
{
|
||||
"device": "dhcpHost",
|
||||
"field": "dns",
|
||||
"input": "",
|
||||
"start": "10.0.0.2",
|
||||
"marked": true,
|
||||
"stored": "10.0.0.2",
|
||||
"shown": "10.0.0.2"
|
||||
},
|
||||
{
|
||||
"device": "dhcpHost",
|
||||
"field": "dns",
|
||||
"input": "10.0.0.256",
|
||||
"start": "10.0.0.2",
|
||||
"marked": true,
|
||||
"stored": "10.0.0.2",
|
||||
"shown": "10.0.0.2"
|
||||
},
|
||||
{
|
||||
"device": "dhcpHome",
|
||||
"field": "dns",
|
||||
"input": "42.0.0.53",
|
||||
"start": "10.0.0.2",
|
||||
"marked": false,
|
||||
"stored": "42.0.0.53",
|
||||
"shown": "42.0.0.53"
|
||||
},
|
||||
{
|
||||
"device": "dhcpHome",
|
||||
"field": "dns",
|
||||
"input": "42.0.0",
|
||||
"start": "10.0.0.2",
|
||||
"marked": true,
|
||||
"stored": "10.0.0.2",
|
||||
"shown": "10.0.0.2"
|
||||
},
|
||||
{
|
||||
"device": "dhcpHome",
|
||||
"field": "lower",
|
||||
"input": "192.168.0.110",
|
||||
"start": "192.168.0.100",
|
||||
"marked": false,
|
||||
"stored": "192.168.0.110",
|
||||
"shown": "192.168.0.110"
|
||||
},
|
||||
{
|
||||
"device": "dhcpHome",
|
||||
"field": "gateway",
|
||||
"input": "10.0.0.9",
|
||||
"start": "10.0.0.1",
|
||||
"marked": false,
|
||||
"stored": "10.0.0.1",
|
||||
"shown": "10.0.0.1"
|
||||
}
|
||||
],
|
||||
"flows": {
|
||||
"useMacAsNameThenOff": "CE:0D:5A:24:8A:C6",
|
||||
"useMacAsNameThenOff": "26:0A:1B:EE:FE:4B",
|
||||
"useIpAsNameThenOff": "PC-A",
|
||||
"dhcpClientStopsServer": true,
|
||||
"dhcpClient": true,
|
||||
@@ -1390,6 +1561,119 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"dhcp": {
|
||||
"dhcpHostShown": {
|
||||
"tfUntergrenze": [
|
||||
"192.168.0.100",
|
||||
true
|
||||
],
|
||||
"tfObergrenze": [
|
||||
"192.168.0.200",
|
||||
true
|
||||
],
|
||||
"tfNetzmaske": [
|
||||
"255.255.255.0",
|
||||
false
|
||||
],
|
||||
"tfGateway": [
|
||||
"192.168.0.254",
|
||||
false
|
||||
],
|
||||
"tfDNSServer": [
|
||||
"192.168.0.253",
|
||||
false
|
||||
]
|
||||
},
|
||||
"dhcpHomeShown": {
|
||||
"tfUntergrenze": [
|
||||
"192.168.0.100",
|
||||
true
|
||||
],
|
||||
"tfObergrenze": [
|
||||
"192.168.0.200",
|
||||
true
|
||||
],
|
||||
"tfNetzmaske": [
|
||||
"255.255.255.0",
|
||||
false
|
||||
],
|
||||
"tfGateway": [
|
||||
"192.168.0.10",
|
||||
false
|
||||
],
|
||||
"tfDNSServer": [
|
||||
"10.0.0.2",
|
||||
true
|
||||
]
|
||||
},
|
||||
"hostWithoutGatewayShown": [
|
||||
"0.0.0.0",
|
||||
"0.0.0.0"
|
||||
],
|
||||
"staticAssignments": [
|
||||
[
|
||||
"aa:bb:cc:dd:ee:01",
|
||||
"192.168.0.50"
|
||||
],
|
||||
[
|
||||
"AA:BB:CC:DD:EE:_F",
|
||||
"192.168.0.52"
|
||||
],
|
||||
[
|
||||
"AA:BB:CC:DD:EE:03",
|
||||
"010.0.0.1"
|
||||
]
|
||||
],
|
||||
"addable": [
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"forwarding": {
|
||||
"rows": [
|
||||
[
|
||||
"TCP",
|
||||
"80",
|
||||
"192.168.0.20",
|
||||
"8080"
|
||||
],
|
||||
[
|
||||
"UDP",
|
||||
"0",
|
||||
"192.168.0.21",
|
||||
"53"
|
||||
],
|
||||
[
|
||||
"TCP",
|
||||
"abc",
|
||||
"192.168.0.22",
|
||||
"80"
|
||||
],
|
||||
[
|
||||
"tcp",
|
||||
"443",
|
||||
"192.168.0.300",
|
||||
"443"
|
||||
],
|
||||
[
|
||||
"17",
|
||||
"5353",
|
||||
"192.168.0.23",
|
||||
"05353"
|
||||
]
|
||||
],
|
||||
"valid": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"edits": {
|
||||
"invalidCell": {
|
||||
"manual": [
|
||||
|
||||
@@ -9,8 +9,10 @@ import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import * as Y from 'yjs';
|
||||
import { javaIpCheck } from '@mdg-filius/core';
|
||||
import { openProjectArchive, saveProjectArchive } from '@mdg-filius/fls';
|
||||
import {
|
||||
toY,
|
||||
addDevice,
|
||||
checkField,
|
||||
connect,
|
||||
@@ -29,7 +31,15 @@ import {
|
||||
setSwitchRetention,
|
||||
setSwitchSsid,
|
||||
addRoute,
|
||||
addDhcpStaticAssignment,
|
||||
addPortForwarding,
|
||||
removeDhcpStaticAssignment,
|
||||
removePortForwarding,
|
||||
removeRoute,
|
||||
setDhcpServerAddress,
|
||||
setDhcpServerFlag,
|
||||
setDhcpStaticCell,
|
||||
setPortForwardingCell,
|
||||
setInterfaceAddress,
|
||||
setRip,
|
||||
setRouteCell,
|
||||
@@ -43,7 +53,7 @@ import {
|
||||
} from '../src/index.js';
|
||||
|
||||
interface JavaCase {
|
||||
readonly device: 'host' | 'switch' | 'modem' | 'router' | 'homeRouter';
|
||||
readonly device: 'host' | 'switch' | 'modem' | 'router' | 'homeRouter' | 'dhcpHost' | 'dhcpHome';
|
||||
readonly field: string;
|
||||
readonly input: string;
|
||||
readonly start: string;
|
||||
@@ -72,6 +82,14 @@ const java = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)
|
||||
};
|
||||
readonly tables: Readonly<Record<string, readonly JavaRow[]>>;
|
||||
readonly edits: Readonly<Record<string, { readonly manual: readonly (readonly (string | null)[])[]; readonly rowsShown?: number }>>;
|
||||
readonly dhcp: {
|
||||
readonly dhcpHostShown: Readonly<Record<string, readonly [string, boolean]>>;
|
||||
readonly dhcpHomeShown: Readonly<Record<string, readonly [string, boolean]>>;
|
||||
readonly hostWithoutGatewayShown: readonly [string, string];
|
||||
readonly staticAssignments: readonly (readonly [string, string])[];
|
||||
readonly addable: readonly boolean[];
|
||||
};
|
||||
readonly forwarding: { readonly rows: readonly (readonly [string, string, string, string])[]; readonly valid: readonly boolean[] };
|
||||
};
|
||||
|
||||
const RULES: Readonly<Record<string, FieldRule>> = {
|
||||
@@ -95,8 +113,18 @@ const RULES: Readonly<Record<string, FieldRule>> = {
|
||||
'homeRouter wanMask': 'subnetMask',
|
||||
'homeRouter lanIp': 'ipAddress',
|
||||
'homeRouter lanMask': 'subnetMask',
|
||||
// The DHCP dialog checks every address with musterIpAdresse: none may be empty.
|
||||
'dhcpHost lower': 'ipAddress',
|
||||
'dhcpHost upper': 'ipAddress',
|
||||
'dhcpHost gateway': 'ipAddress',
|
||||
'dhcpHost dns': 'ipAddress',
|
||||
'dhcpHome lower': 'ipAddress',
|
||||
'dhcpHome dns': 'ipAddress',
|
||||
};
|
||||
|
||||
/** Fields Java's dialog shows but never takes: a home router hands out its LAN address as the gateway. */
|
||||
const NOT_OFFERED = new Set(['dhcpHome gateway']);
|
||||
|
||||
/** Fields whose text is judged without the spaces around it. */
|
||||
const TRIMMED = new Set(
|
||||
Object.entries(RULES)
|
||||
@@ -105,10 +133,13 @@ const TRIMMED = new Set(
|
||||
);
|
||||
|
||||
/** Whether Java's device took the text: its value changed (to an address: null is a card that lost its own), or was the text already. */
|
||||
const tookIt = (each: JavaCase) => each.stored !== null && (each.stored !== each.start || each.input === each.start);
|
||||
const tookIt = (each: JavaCase) => each.stored !== null && (each.stored !== each.start || each.input === each.start) && (!keptAsTyped(each) || javaIpCheck(each.stored) !== null);
|
||||
|
||||
/** What Java's model holds in the units of the sheet's value: seconds for the retention time. */
|
||||
const javaValue = (each: JavaCase) => (each.field === 'retention' ? String(Number(each.stored) / 1000) : each.stored);
|
||||
/** A DHCP server keeps its addresses as typed ("010.0.0.1"); the model keeps IP.ipCheck's form. */
|
||||
const keptAsTyped = (each: JavaCase) => each.device.startsWith('dhcp');
|
||||
|
||||
/** What Java's model holds in the units of the sheet's value: seconds for the retention time, an address in IP.ipCheck's form. */
|
||||
const javaValue = (each: JavaCase) => (each.field === 'retention' ? String(Number(each.stored) / 1000) : keptAsTyped(each) ? javaIpCheck(each.stored!) : each.stored);
|
||||
|
||||
/** The case Java judged for a text: the trimmed text's own case where spaces are left out. */
|
||||
function judged(each: JavaCase): JavaCase {
|
||||
@@ -121,13 +152,14 @@ function judged(each: JavaCase): JavaCase {
|
||||
|
||||
describe("a field's rule, against FILIUS' settings panels", () => {
|
||||
it('has a case for every field the sheet checks', () => {
|
||||
expect(new Set(java.cases.map((each) => `${each.device} ${each.field}`))).toEqual(new Set(Object.keys(RULES)));
|
||||
expect(new Set(java.cases.map((each) => `${each.device} ${each.field}`))).toEqual(new Set([...Object.keys(RULES), ...NOT_OFFERED]));
|
||||
expect(java.cases.length).toBeGreaterThan(70);
|
||||
});
|
||||
|
||||
it('takes a text exactly when Java neither marks nor refuses it, as the value Java keeps', () => {
|
||||
for (const each of java.cases) {
|
||||
const key = `${each.device} ${each.field}`;
|
||||
if (NOT_OFFERED.has(key)) continue;
|
||||
const verdict = judged(each);
|
||||
// Taken: the value changed, or the text was the value already.
|
||||
const took = tookIt(verdict);
|
||||
@@ -143,9 +175,13 @@ describe("a field's rule, against FILIUS' settings panels", () => {
|
||||
});
|
||||
|
||||
it("names where Java's two judgements disagree, which here are one", () => {
|
||||
const disagreeing = java.cases.filter((each) => RULES[`${each.device} ${each.field}`] !== 'name' && each.field !== 'port' && each.marked === tookIt(each));
|
||||
const disagreeing = java.cases.filter(
|
||||
(each) => !NOT_OFFERED.has(`${each.device} ${each.field}`) && RULES[`${each.device} ${each.field}`] !== 'name' && each.field !== 'port' && each.marked === tookIt(each),
|
||||
);
|
||||
const kept = (each: JavaCase) => each.stored !== null && each.stored !== each.start && !tookIt(each);
|
||||
// Marked red, yet taken; or black, yet refused (the gateway's spaces Java trims to empty) — or lost: a card without its address.
|
||||
const verdictOf = (each: JavaCase) => (each.marked ? 'marked, taken' : each.stored === null ? `lost${each.error === undefined ? '' : `, ${each.error}`}` : 'refused');
|
||||
const verdictOf = (each: JavaCase) =>
|
||||
each.marked ? 'marked, taken' : each.stored === null ? `lost${each.error === undefined ? '' : `, ${each.error}`}` : kept(each) ? 'kept, no address' : 'refused';
|
||||
expect(disagreeing.map((each) => `${each.device} ${each.field} ${JSON.stringify(each.input)} ${verdictOf(each)}`)).toEqual([
|
||||
'host ip "10..0.0.1" marked, taken',
|
||||
'host ip ".0.0.1" refused',
|
||||
@@ -167,6 +203,7 @@ describe("a field's rule, against FILIUS' settings panels", () => {
|
||||
'router gateway "192.168..0.1" marked, taken',
|
||||
'homeRouter gateway "" marked, taken',
|
||||
'homeRouter wanIp ".1.2.3" lost',
|
||||
'dhcpHost lower ".0.0.1" kept, no address',
|
||||
]);
|
||||
const ports = java.cases.filter((each) => each.field === 'port' && each.stored !== each.start);
|
||||
expect(ports.map((each) => each.input)).toEqual(['0', '65535', '65536', '-1', '080', '80']);
|
||||
@@ -208,6 +245,19 @@ describe("a PC's sheet", () => {
|
||||
ssid: '',
|
||||
wirelessPossible: true,
|
||||
networks: [],
|
||||
dhcpServer: {
|
||||
isActive: false,
|
||||
lowerBoundIPAddress: '0.0.0.0',
|
||||
upperBoundIPAddress: '0.0.0.0',
|
||||
subnetMask: '255.255.255.0',
|
||||
gateway: '0.0.0.0',
|
||||
gatewayEditable: false,
|
||||
dnsServer: '0.0.0.0',
|
||||
dnsServerEditable: false,
|
||||
useOwnSettings: false,
|
||||
ownSettingsOffered: true,
|
||||
staticAssignments: [],
|
||||
},
|
||||
});
|
||||
expect(sheet.macAddress).toMatch(/^([0-9A-F]{2}:){5}[0-9A-F]{2}$/);
|
||||
expect(deviceSheet(doc, 'nobody')).toBeUndefined();
|
||||
@@ -706,3 +756,168 @@ describe("a home router's sheet", () => {
|
||||
expect(reopened.deviceConfigurations.get(homeAgain!.id)?.defaultGateway).toBe('42.0.0.1');
|
||||
});
|
||||
});
|
||||
|
||||
describe("a DHCP server's sheet", () => {
|
||||
/** The devices of SheetRun's DHCP flows: a host 192.168.0.10/24 with gateway and DNS, a home router; the server with known settings. */
|
||||
function servers() {
|
||||
const doc = new Y.Doc();
|
||||
const pc = add(doc, 'pc');
|
||||
setHostAddress(doc, pc, 'defaultGateway', '192.168.0.254', 'me');
|
||||
setHostAddress(doc, pc, 'dnsServer', '192.168.0.253', 'me');
|
||||
const homeId = add(doc, 'gateway');
|
||||
for (const id of [pc, homeId]) {
|
||||
setDhcpServerAddress(doc, id, 'lowerBoundIPAddress', '192.168.0.100', 'me');
|
||||
setDhcpServerAddress(doc, id, 'upperBoundIPAddress', '192.168.0.200', 'me');
|
||||
setDhcpServerAddress(doc, id, 'gatewayIPAddress', '10.0.0.1', 'me');
|
||||
setDhcpServerAddress(doc, id, 'dnsServerIPAddress', '10.0.0.2', 'me');
|
||||
}
|
||||
return { doc, pc, homeId };
|
||||
}
|
||||
|
||||
it("shows what Java's dialog shows: the device's gateway and DNS server unless its own, a home router's LAN address", () => {
|
||||
const { doc, pc, homeId } = servers();
|
||||
const shown = (sheet: ReturnType<typeof host>['dhcpServer']) => ({
|
||||
tfUntergrenze: [sheet.lowerBoundIPAddress, true],
|
||||
tfObergrenze: [sheet.upperBoundIPAddress, true],
|
||||
tfNetzmaske: [sheet.subnetMask, false],
|
||||
tfGateway: [sheet.gateway, sheet.gatewayEditable],
|
||||
tfDNSServer: [sheet.dnsServer, sheet.dnsServerEditable],
|
||||
});
|
||||
expect(shown(host(doc, pc).dhcpServer)).toEqual(java.dhcp.dhcpHostShown);
|
||||
expect(shown(home(doc, homeId).dhcpServer)).toEqual(java.dhcp.dhcpHomeShown);
|
||||
expect(home(doc, homeId).dhcpServer.ownSettingsOffered).toBe(false);
|
||||
// A home router's choice has no effect (Java's dialog keeps the flag, the gateway stays its LAN address).
|
||||
setDhcpServerFlag(doc, homeId, 'useOwnSettings', true, 'me');
|
||||
expect(home(doc, homeId).dhcpServer).toMatchObject({ useOwnSettings: true, gateway: '192.168.0.10', gatewayEditable: false });
|
||||
const bare = add(doc, 'pc');
|
||||
expect([host(doc, bare).dhcpServer.gateway, host(doc, bare).dhcpServer.dnsServer]).toEqual(java.dhcp.hostWithoutGatewayShown);
|
||||
setDhcpServerFlag(doc, pc, 'useOwnSettings', true, 'me');
|
||||
expect(host(doc, pc).dhcpServer).toMatchObject({ gateway: '10.0.0.1', gatewayEditable: true, dnsServer: '10.0.0.2', dnsServerEditable: true, useOwnSettings: true });
|
||||
setDhcpServerFlag(doc, pc, 'isActive', true, 'me');
|
||||
expect(readProject(doc).dhcpServerConfigurations.get(pc)).toMatchObject({ isActive: true, useOwnSettings: true, lowerBoundIPAddress: '192.168.0.100' });
|
||||
});
|
||||
|
||||
it("keeps a static assignment once both cells pass and its MAC address is free, unlike Java's silent drops", () => {
|
||||
const { doc, pc } = servers();
|
||||
// Java's add button takes the duplicate and the "_F" too (musterMacAddress); its server drops the duplicate on saving.
|
||||
expect(java.dhcp.addable).toEqual([true, true, true, false, true, false]);
|
||||
const rows = [
|
||||
['aa:bb:cc:dd:ee:01', '192.168.0.50'],
|
||||
['AA:BB:CC:DD:EE:01', '192.168.0.51'],
|
||||
['AA:BB:CC:DD:EE:_F', '192.168.0.52'],
|
||||
['AA:BB:CC:DD:EE:02', '192.168.0.256'],
|
||||
['AA:BB:CC:DD:EE:03', '010.0.0.1'],
|
||||
['AABBCCDDEE04', '192.168.0.54'],
|
||||
] as const;
|
||||
const outcomes = rows.map(([macAddress, ipAddress], index) => {
|
||||
const result = addDhcpStaticAssignment(doc, pc, { macAddress, ipAddress }, `assignment-${index}`, 'me');
|
||||
return 'assignment' in result ? 'taken' : 'error' in result ? result.error : `invalid ${result.invalid.join(' ')}`;
|
||||
});
|
||||
expect(outcomes).toEqual(['taken', 'macTaken', 'invalid macAddress', 'invalid ipAddress', 'taken', 'invalid macAddress']);
|
||||
// Java kept the first, the "_F" its pattern lets through, and "010.0.0.1" as typed; here upper case and IP.ipCheck's form.
|
||||
expect(java.dhcp.staticAssignments).toEqual([
|
||||
['aa:bb:cc:dd:ee:01', '192.168.0.50'],
|
||||
['AA:BB:CC:DD:EE:_F', '192.168.0.52'],
|
||||
['AA:BB:CC:DD:EE:03', '010.0.0.1'],
|
||||
]);
|
||||
expect(host(doc, pc).dhcpServer.staticAssignments).toEqual([
|
||||
{ macAddress: 'AA:BB:CC:DD:EE:01', ipAddress: '192.168.0.50' },
|
||||
{ macAddress: 'AA:BB:CC:DD:EE:03', ipAddress: '10.0.0.1' },
|
||||
]);
|
||||
expect(setDhcpStaticCell(doc, pc, 1, 'macAddress', 'aa:bb:cc:dd:ee:01', 'me')).toEqual({ invalid: true });
|
||||
expect(setDhcpStaticCell(doc, pc, 1, 'macAddress', 'aa:bb:cc:dd:ee:09', 'me')).toEqual({ value: 'AA:BB:CC:DD:EE:09' });
|
||||
expect(setDhcpStaticCell(doc, pc, 0, 'ipAddress', '192.168.0.60', 'me')).toEqual({ value: '192.168.0.60' });
|
||||
expect(setDhcpStaticCell(doc, pc, 5, 'ipAddress', '192.168.0.61', 'me')).toEqual({ value: '192.168.0.61' });
|
||||
removeDhcpStaticAssignment(doc, pc, 7, 'me');
|
||||
removeDhcpStaticAssignment(doc, pc, 0, 'me');
|
||||
expect(readProject(doc).dhcpServerConfigurations.get(pc)?.staticAssignments).toEqual([{ id: 'assignment-4', macAddress: 'AA:BB:CC:DD:EE:09', ipAddress: '10.0.0.1' }]);
|
||||
expect(addDhcpStaticAssignment(doc, 'nobody', { macAddress: 'AA:BB:CC:DD:EE:0A', ipAddress: '10.0.0.3' }, 'x', 'me')).toEqual({ invalid: [] });
|
||||
});
|
||||
|
||||
it("compares MAC addresses ignoring case, as Java does, a file's lower case too, and lets a row keep its own", () => {
|
||||
const doc = new Y.Doc();
|
||||
const pc = add(doc, 'pc');
|
||||
addDhcpStaticAssignment(doc, pc, { macAddress: 'AA:BB:CC:DD:EE:01', ipAddress: '10.0.0.5' }, 'a', 'me');
|
||||
// A file keeps a MAC address as Java's dialog took it, in lower case.
|
||||
const entry = ((doc.getMap<Y.Map<unknown>>(ROOTS.nodes).get(pc)!.get('settings') as Y.Map<Y.Map<unknown>>).get('dhcpServer')!.get('staticAssignments') as Y.Array<Y.Map<unknown>>).get(0);
|
||||
entry.set('macAddress', 'aa:bb:cc:dd:ee:01');
|
||||
expect(addDhcpStaticAssignment(doc, pc, { macAddress: 'AA:BB:CC:DD:EE:01', ipAddress: '10.0.0.6' }, 'b', 'me')).toEqual({ error: 'macTaken' });
|
||||
expect(setDhcpStaticCell(doc, pc, 0, 'macAddress', 'AA:BB:CC:DD:EE:01', 'me')).toEqual({ value: 'AA:BB:CC:DD:EE:01' });
|
||||
});
|
||||
|
||||
it('takes a static assignment on a device without a server in the document, made as Java makes it', () => {
|
||||
const doc = new Y.Doc();
|
||||
const pc = add(doc, 'pc');
|
||||
expect('assignment' in addDhcpStaticAssignment(doc, pc, { macAddress: 'AA:BB:CC:DD:EE:01', ipAddress: '10.0.0.5' }, 'a', 'me')).toBe(true);
|
||||
expect(readProject(doc).dhcpServerConfigurations.get(pc)).toMatchObject({
|
||||
isActive: false,
|
||||
lowerBoundIPAddress: '0.0.0.0',
|
||||
staticAssignments: [{ id: 'a', macAddress: 'AA:BB:CC:DD:EE:01', ipAddress: '10.0.0.5' }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("a home router's port forwarding", () => {
|
||||
const JAVA_ROWS = () => java.forwarding.rows.map(([protocolValue, publicPortValue, lanIPAddress, lanPortValue]) => ({ protocolValue, publicPortValue, lanIPAddress, lanPortValue }));
|
||||
|
||||
it('takes a row once all four cells pass, where Java keeps any row and its firmware forwards only the valid ones', () => {
|
||||
const doc = new Y.Doc();
|
||||
const id = add(doc, 'gateway');
|
||||
const outcomes = JAVA_ROWS().map((row) => {
|
||||
const result = addPortForwarding(doc, id, row, 'me');
|
||||
return 'row' in result ? 'taken' : result.invalid.join(' ');
|
||||
});
|
||||
// Java's firmware forwards the first only ("17" it would take, but not the leading zero of "05353").
|
||||
expect(java.forwarding.valid).toEqual([true, false, false, false, false]);
|
||||
expect(outcomes).toEqual(['taken', 'publicPortValue', 'publicPortValue', 'protocolValue lanIPAddress', 'protocolValue lanPortValue']);
|
||||
expect(home(doc, id).portForwarding).toEqual([{ ...JAVA_ROWS()[0]!, forwarded: true }]);
|
||||
expect(addPortForwarding(doc, 'nobody', JAVA_ROWS()[0]!, 'me')).toEqual({ invalid: [] });
|
||||
// Java's dialog offers TCP and UDP; Java's firmware would take "tcp", "6" or "17" from a file.
|
||||
for (const protocolValue of ['tcp', '6', 'ICMP']) expect(addPortForwarding(doc, id, { ...JAVA_ROWS()[0]!, protocolValue }, 'me')).toEqual({ invalid: ['protocolValue'] });
|
||||
});
|
||||
|
||||
it("marks the rows a file brought that Java's firmware would not forward, and edits and removes rows", () => {
|
||||
const doc = new Y.Doc();
|
||||
const id = add(doc, 'gateway');
|
||||
const settings = doc.getMap<Y.Map<unknown>>(ROOTS.nodes).get(id)!.get('settings') as Y.Map<unknown>;
|
||||
const list = new Y.Array<Y.Map<unknown>>();
|
||||
settings.set('portForwarding', list);
|
||||
list.push(JAVA_ROWS().map((row) => toY({ ...row }) as Y.Map<unknown>));
|
||||
expect(home(doc, id).portForwarding.map((row) => row.forwarded)).toEqual(java.forwarding.valid);
|
||||
expect(setPortForwardingCell(doc, id, 1, 'publicPortValue', '0053', 'me')).toEqual({ invalid: true });
|
||||
expect(setPortForwardingCell(doc, id, 1, 'publicPortValue', ' 53', 'me')).toEqual({ value: '53' });
|
||||
expect(setPortForwardingCell(doc, id, 1, 'protocolValue', 'ICMP', 'me')).toEqual({ invalid: true });
|
||||
expect(setPortForwardingCell(doc, id, 0, 'protocolValue', 'UDP', 'me')).toEqual({ value: 'UDP' });
|
||||
expect(home(doc, id).portForwarding.map((row) => [row.protocolValue, row.publicPortValue, row.forwarded])).toEqual([
|
||||
['UDP', '80', true],
|
||||
['UDP', '53', true],
|
||||
['TCP', 'abc', false],
|
||||
['tcp', '443', false],
|
||||
['17', '5353', false],
|
||||
]);
|
||||
for (const index of [9, 4, 3, 2, 1]) removePortForwarding(doc, id, index, 'me');
|
||||
expect(readProject(doc).portForwardingRowsByNodeId.get(id)).toHaveLength(1);
|
||||
removePortForwarding(doc, id, 0, 'me');
|
||||
expect(settings.has('portForwarding')).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps the DHCP server and the port forwarding through a saved file', () => {
|
||||
const doc = new Y.Doc();
|
||||
const id = add(doc, 'gateway');
|
||||
setDhcpServerAddress(doc, id, 'lowerBoundIPAddress', '192.168.0.100', 'me');
|
||||
setDhcpServerAddress(doc, id, 'dnsServerIPAddress', '8.8.8.8', 'me');
|
||||
setDhcpServerFlag(doc, id, 'isActive', true, 'me');
|
||||
addDhcpStaticAssignment(doc, id, { macAddress: 'AA:BB:CC:DD:EE:01', ipAddress: '192.168.0.50' }, 'a', 'me');
|
||||
addPortForwarding(doc, id, JAVA_ROWS()[0]!, 'me');
|
||||
let counter = 0;
|
||||
const reopened = openProjectArchive(saveProjectArchive(readProject(doc)).archive, () => `50000000-0000-0000-0000-${(++counter).toString(16).padStart(12, '0')}`);
|
||||
const again = reopened.graph.nodes[0]!.id;
|
||||
expect(reopened.dhcpServerConfigurations.get(again)).toMatchObject({
|
||||
isActive: true,
|
||||
lowerBoundIPAddress: '192.168.0.100',
|
||||
dnsServerIPAddress: '8.8.8.8',
|
||||
staticAssignments: [{ macAddress: 'AA:BB:CC:DD:EE:01', ipAddress: '192.168.0.50' }],
|
||||
});
|
||||
expect(reopened.portForwardingRowsByNodeId.get(again)).toEqual([JAVA_ROWS()[0]]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user