// The Gnutella window in the room, on three computers of one network: its // view (the known peers, the results as the table shows them, the files of // /peer2peer, the number of peers), its actions, a downloaded file in the // document, and what a run leaves in the project. The program's steps // against Java's window are packages/core/test/gnutellaWindow.test.ts. import { describe, expect, it } from 'vitest'; import * as Y from 'yjs'; import { addDevice, changeFiles, connect, emptyProject, fileText, readProject, setHostAddress, setSoftware, writeProject } from '@mdg-filius/doc'; import type { GnutellaView, HostMessage } from '@mdg-filius/protocol'; import { SimulationRoom, TICK_MILLISECONDS } from '../src/index.js'; function setUp() { const doc = new Y.Doc(); writeProject(doc, emptyProject()); let counter = 0; const newId = () => `port-${++counter}`; addDevice(doc, { kind: 'networkSwitch', id: 'switch', name: 'Switch', x: 100, y: 100, newId }, 'me'); for (const [id, address] of [ ['a', '192.168.0.20'], ['b', '192.168.0.30'], ['c', '192.168.0.31'], ] as const) { addDevice(doc, { kind: 'pc', id, name: id, x: 0, y: 0, newId }, 'me'); connect(doc, { node: id }, { node: 'switch' }, `cable-${id}`, 'me'); setHostAddress(doc, id, 'ipAddress', address, 'me'); setSoftware(doc, id, { install: ['gnutella'], remove: [] }, 'de', 'me'); } changeFiles(doc, 'c', 'me', (files) => files.writeTextFile('/peer2peer/notizen.txt', 'Notiz')); let now = 0; const room = new SimulationRoom(doc, { now: () => now, seed: () => 3 }); const views = new Map(); const errors: string[] = []; const lines: unknown[] = []; const client = room.connect((message: HostMessage) => { if (message.kind === 'view' && message.app === 'gnutella') views.set(message.node, message.view as GnutellaView | null); if (message.kind === 'app' && message.app === 'gnutella') lines.push(...message.lines); if (message.kind === 'error') errors.push(message.code); }); const send = (fields: object) => room.receive(client, { v: 1, ...fields }); send({ kind: 'sub', topics: ['app:a/gnutella', 'app:b/gnutella', 'app:switch/gnutella'] }); send({ kind: 'start' }); const run = (milliseconds: number) => { for (let elapsed = 0; elapsed < milliseconds; elapsed += TICK_MILLISECONDS) { now += TICK_MILLISECONDS; room.tick(); } }; run(TICK_MILLISECONDS); const act = (node: string, action: string, args: object = {}) => send({ kind: 'input', node, app: 'gnutella', action, args, reqId: action }); return { doc, views, errors, lines, act, run, send }; } describe('the Gnutella window in the room', () => { it('shows the peers, the results and the files, and takes its actions', () => { const { doc, views, errors, lines, act, run } = setUp(); const view = () => views.get('a')!; expect(view()).toEqual({ peers: [], results: [], files: [], maximumKnownPeers: 3 }); act('b', 'join', { address: '192.168.0.31' }); run(3000); act('a', 'join', { address: '192.168.0.30' }); run(3000); // The ping goes on to .31, whose pong makes it a peer too. expect(view().peers).toEqual(['192.168.0.30', '192.168.0.31']); act('a', 'search', { criteria: 'notiz' }); run(3000); expect(view().results).toEqual([['192.168.0.31', 'notizen.txt', ' 5 B']]); act('a', 'download', { row: 0 }); run(3000); expect(view().files).toEqual([['notizen.txt', 'txt']]); // In the document's file at once. expect(fileText(doc, 'a', '/peer2peer/notizen.txt')?.toString()).toBe('Notiz'); act('a', 'clearResults'); expect(view().results).toEqual([]); // "Stopp!" at once: the hits on their way are lost. act('a', 'search', { criteria: 'notiz' }); act('a', 'cancelSearch'); run(3000); expect(view().results).toEqual([]); act('a', 'setMaximumPeers', { count: 12 }); expect(view().maximumKnownPeers).toBe(12); act('a', 'leave'); expect(view().peers).toEqual([]); expect(errors).toEqual([]); // The window has no lines: none are sent. expect(lines).toEqual([]); }); it('leaves in the project what the run made of the number of peers', () => { const { doc, act, send } = setUp(); act('a', 'setMaximumPeers', { count: 7 }); send({ kind: 'stop' }); expect(readProject(doc).gnutellaConfigurations.get('a')).toEqual({ maximumKnownPeers: 7 }); expect(readProject(doc).gnutellaConfigurations.get('b')).toEqual({ maximumKnownPeers: 3 }); }); it('refuses what the window never hands over', () => { const { views, act, errors } = setUp(); act('a', 'join', { address: '192.168.0.300' }); act('a', 'join', { address: 5 }); act('a', 'search', { criteria: 3 }); act('a', 'download', { row: 0 }); act('a', 'download', { row: -1 }); act('a', 'download', { row: 0.5 }); act('a', 'setMaximumPeers', { count: -1 }); act('a', 'setMaximumPeers', { count: '4' }); act('a', 'setMaximumPeers', { count: 2_147_483_648 }); act('a', 'restart'); act('switch', 'leave'); expect(errors).toEqual(['badArgs', 'badArgs', 'badArgs', 'badArgs', 'badArgs', 'badArgs', 'badArgs', 'badArgs', 'badArgs', 'unknownAction', 'unknownApp']); expect(views.get('switch')).toBeNull(); }); });