feat(core): event queue, Ethernet, switch, ARP, IPv4, ICMP, routing and firewall
First slice of the TypeScript port of the filius-ipad runtime engine, with 29 tests ported from the Swift suite. Parts not ported yet throw NotPortedError instead of guessing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
// Ported from filius-ipad ios/FiliusPadTests/TopologyJavaRouteTableTests.swift.
|
||||
// Left out: column titles and geometry (editor), and the reducer tests
|
||||
// (they move with the document layer, packages/doc).
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
appendingBlankDraftIfNeeded,
|
||||
bestMatchingManualRoute,
|
||||
blankRouteDraft,
|
||||
isValidJavaIPAddress,
|
||||
isValidJavaSubnetMask,
|
||||
manualRouteFromDraft,
|
||||
routeTableRows,
|
||||
visibleRouteRows,
|
||||
type ManualRoute,
|
||||
} from '../src/index.js';
|
||||
|
||||
function route(destinationNetwork: string, subnetMask: string, gateway: string, interfaceIPAddress: string): ManualRoute {
|
||||
return { destinationNetwork, subnetMask, gateway, interfaceIPAddress };
|
||||
}
|
||||
|
||||
describe('Java route table', () => {
|
||||
it('projects multiple interfaces in Java order with Java values', () => {
|
||||
const rows = routeTableRows(
|
||||
[
|
||||
{ ipAddress: '10.0.0.1', subnetMask: '255.255.255.0' },
|
||||
{ ipAddress: '192.168.0.1', subnetMask: '255.255.255.0' },
|
||||
],
|
||||
[
|
||||
route('172.16.0.0', '255.255.0.0', '10.0.0.2', '10.0.0.1'),
|
||||
route('203.0.113.0', '255.255.255.0', '192.168.0.2', '192.168.0.1'),
|
||||
],
|
||||
'10.0.0.254',
|
||||
);
|
||||
|
||||
expect(rows.map((row) => row.destinationNetwork)).toEqual([
|
||||
'192.168.0.1', '10.0.0.1',
|
||||
'192.168.0.0', '10.0.0.0',
|
||||
'127.0.0.0',
|
||||
'172.16.0.0', '203.0.113.0',
|
||||
'0.0.0.0',
|
||||
]);
|
||||
expect(rows.map((row) => row.origin)).toEqual([
|
||||
'localHost', 'localHost', 'connected', 'connected', 'localhost', 'manual', 'manual', 'defaultGateway',
|
||||
]);
|
||||
expect(rows.map((row) => row.isEditable)).toEqual([false, false, false, false, false, true, true, false]);
|
||||
expect(rows[0]).toMatchObject({ subnetMask: '255.255.255.255', nextHop: '127.0.0.1', interfaceIPAddress: '127.0.0.1' });
|
||||
expect(rows[2]?.nextHop).toBe('192.168.0.1');
|
||||
expect(rows[4]?.nextHop).toBe('127.0.0.1');
|
||||
expect(rows.at(-1)).toMatchObject({ nextHop: '10.0.0.254', interfaceIPAddress: '10.0.0.1' });
|
||||
});
|
||||
|
||||
it('puts the default route on the last matching interface and falls back to the primary one', () => {
|
||||
const interfaces = [
|
||||
{ ipAddress: '10.0.0.1', subnetMask: '255.0.0.0' },
|
||||
{ ipAddress: '10.1.0.1', subnetMask: '255.0.0.0' },
|
||||
];
|
||||
|
||||
const matching = routeTableRows(interfaces, [], '10.2.0.1');
|
||||
expect(matching.at(-1)).toMatchObject({ origin: 'defaultGateway', interfaceIPAddress: '10.1.0.1' });
|
||||
|
||||
const fallback = routeTableRows(interfaces, [], '192.168.0.1');
|
||||
expect(fallback.at(-1)?.interfaceIPAddress).toBe('10.0.0.1');
|
||||
});
|
||||
|
||||
it('shows only manual rows when generated rows are hidden', () => {
|
||||
const rows = routeTableRows(
|
||||
[{ ipAddress: '10.0.0.1', subnetMask: '255.255.255.0' }],
|
||||
[route('172.16.0.0', '255.255.0.0', '10.0.0.2', '10.0.0.1')],
|
||||
'10.0.0.254',
|
||||
);
|
||||
|
||||
expect(visibleRouteRows(rows, false).map((row) => row.origin)).toEqual(['manual']);
|
||||
expect(visibleRouteRows(rows, true)).toEqual(rows);
|
||||
});
|
||||
|
||||
it('accepts leading zeros and non-contiguous masks like Java', () => {
|
||||
expect(isValidJavaIPAddress('001.002.003.004')).toBe(true);
|
||||
expect(isValidJavaSubnetMask('255.0.255.0')).toBe(true);
|
||||
expect(isValidJavaIPAddress('256.0.0.1')).toBe(false);
|
||||
expect(isValidJavaSubnetMask('255.255.255')).toBe(false);
|
||||
expect(isValidJavaIPAddress(' 10.0.0.1')).toBe(false);
|
||||
});
|
||||
|
||||
it('commits a draft only when all four cells are valid', () => {
|
||||
const draft = blankRouteDraft('00000000-0000-0000-0000-000000000013');
|
||||
draft.destinationNetwork = '10.0.0.0';
|
||||
draft.subnetMask = '255.0.255.0';
|
||||
draft.nextGateway = '10.0.0.2';
|
||||
|
||||
expect(manualRouteFromDraft(draft)).toBeUndefined();
|
||||
|
||||
draft.interfaceIPAddress = '10.0.0.1';
|
||||
expect(manualRouteFromDraft(draft)).toEqual(route('10.0.0.0', '255.0.255.0', '10.0.0.2', '10.0.0.1'));
|
||||
});
|
||||
|
||||
it('keeps at most one pending blank draft', () => {
|
||||
let counter = 0;
|
||||
const newId = () => `draft-${(counter += 1)}`;
|
||||
const first = appendingBlankDraftIfNeeded([], newId);
|
||||
const second = appendingBlankDraftIfNeeded(first, newId);
|
||||
|
||||
expect(first).toHaveLength(1);
|
||||
expect(second).toEqual(first);
|
||||
expect(second[0]?.isPendingInsertion).toBe(true);
|
||||
});
|
||||
|
||||
it('picks the greatest numeric mask and keeps the first row on a tie', () => {
|
||||
const broad = route('10.0.0.0', '255.0.0.0', '1.1.1.1', '10.0.0.1');
|
||||
const firstSpecific = route('10.1.0.0', '255.255.0.0', '2.2.2.2', '10.0.0.1');
|
||||
const tiedSpecific = route('10.1.0.0', '255.255.0.0', '3.3.3.3', '10.0.0.1');
|
||||
|
||||
expect(bestMatchingManualRoute('10.1.2.3', [broad, firstSpecific, tiedSpecific])).toBe(firstSpecific);
|
||||
});
|
||||
|
||||
it('never includes learned RIP rows in the static projection', () => {
|
||||
const rows = routeTableRows([{ ipAddress: '10.0.0.1', subnetMask: '255.255.255.0' }], [], undefined);
|
||||
|
||||
expect(rows.some((row) => row.origin === 'rip')).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user