import { expect, tap } from '@git.zone/tstest/tapbundle'; import { RadiusClient } from '../../ts_client/index.js'; import { RadiusServer, ERadiusCode, EAcctStatusType, EAcctTerminateCause, RadiusAuthenticator, } from '../../ts_server/index.js'; // Integration test: full client-server communication let server: RadiusServer; const TEST_SECRET = 'integration-secret'; const TEST_AUTH_PORT = 18200; const TEST_ACCT_PORT = 18210; // Track accounting records const accountingRecords: any[] = []; // User database const users: Record = { alice: { password: 'alice123', sessionTimeout: 7200 }, bob: { password: 'bob456', sessionTimeout: 3600 }, charlie: { password: 'charlie789' }, }; tap.test('setup - start integration test server', async () => { server = new RadiusServer({ authPort: TEST_AUTH_PORT, acctPort: TEST_ACCT_PORT, defaultSecret: TEST_SECRET, authenticationHandler: async (request) => { const user = users[request.username]; if (!user) { return { code: ERadiusCode.AccessReject, replyMessage: 'Unknown user', }; } // PAP authentication if (request.password !== undefined) { if (request.password === user.password) { return { code: ERadiusCode.AccessAccept, replyMessage: `Welcome, ${request.username}!`, sessionTimeout: user.sessionTimeout, framedIpAddress: '10.0.0.' + (Object.keys(users).indexOf(request.username) + 10), }; } return { code: ERadiusCode.AccessReject, replyMessage: 'Invalid password', }; } // CHAP authentication if (request.chapPassword && request.chapChallenge) { const isValid = RadiusAuthenticator.verifyChapResponse( request.chapPassword, request.chapChallenge, user.password ); if (isValid) { return { code: ERadiusCode.AccessAccept, replyMessage: `Welcome, ${request.username}! (CHAP)`, sessionTimeout: user.sessionTimeout, }; } return { code: ERadiusCode.AccessReject, replyMessage: 'CHAP authentication failed', }; } return { code: ERadiusCode.AccessReject, replyMessage: 'No authentication method provided', }; }, accountingHandler: async (request) => { accountingRecords.push({ statusType: request.statusType, sessionId: request.sessionId, username: request.username, sessionTime: request.sessionTime, inputOctets: request.inputOctets, outputOctets: request.outputOctets, terminateCause: request.terminateCause, timestamp: Date.now(), }); return { success: true }; }, }); await server.start(); }); tap.test('integration: PAP authentication for multiple users', async () => { const client = new RadiusClient({ host: '127.0.0.1', authPort: TEST_AUTH_PORT, acctPort: TEST_ACCT_PORT, secret: TEST_SECRET, timeout: 2000, }); await client.connect(); // Alice let response = await client.authenticatePap('alice', 'alice123'); expect(response.accepted).toBeTruthy(); expect(response.replyMessage).toInclude('alice'); expect(response.sessionTimeout).toEqual(7200); expect(response.framedIpAddress).toEqual('10.0.0.10'); // Bob response = await client.authenticatePap('bob', 'bob456'); expect(response.accepted).toBeTruthy(); expect(response.replyMessage).toInclude('bob'); expect(response.sessionTimeout).toEqual(3600); // Charlie (no session timeout) response = await client.authenticatePap('charlie', 'charlie789'); expect(response.accepted).toBeTruthy(); expect(response.replyMessage).toInclude('charlie'); expect(response.sessionTimeout).toBeUndefined(); // Unknown user response = await client.authenticatePap('unknown', 'password'); expect(response.rejected).toBeTruthy(); expect(response.replyMessage).toEqual('Unknown user'); // Wrong password response = await client.authenticatePap('alice', 'wrongpassword'); expect(response.rejected).toBeTruthy(); expect(response.replyMessage).toEqual('Invalid password'); await client.disconnect(); }); tap.test('integration: CHAP authentication', async () => { const client = new RadiusClient({ host: '127.0.0.1', authPort: TEST_AUTH_PORT, acctPort: TEST_ACCT_PORT, secret: TEST_SECRET, timeout: 2000, }); await client.connect(); // CHAP with correct password let response = await client.authenticateChap('bob', 'bob456'); expect(response.accepted).toBeTruthy(); expect(response.replyMessage).toInclude('CHAP'); // CHAP with wrong password response = await client.authenticateChap('bob', 'wrongpass'); expect(response.rejected).toBeTruthy(); await client.disconnect(); }); tap.test('integration: full session lifecycle with accounting', async () => { const client = new RadiusClient({ host: '127.0.0.1', authPort: TEST_AUTH_PORT, acctPort: TEST_ACCT_PORT, secret: TEST_SECRET, timeout: 2000, }); await client.connect(); const sessionId = `session-${Date.now()}`; // 1. Authenticate const authResponse = await client.authenticatePap('alice', 'alice123'); expect(authResponse.accepted).toBeTruthy(); // 2. Accounting Start let acctResponse = await client.accountingStart(sessionId, 'alice'); expect(acctResponse.success).toBeTruthy(); // 3. Interim Update acctResponse = await client.accountingUpdate(sessionId, { username: 'alice', sessionTime: 300, inputOctets: 100000, outputOctets: 200000, }); expect(acctResponse.success).toBeTruthy(); // 4. Accounting Stop acctResponse = await client.accountingStop(sessionId, { username: 'alice', sessionTime: 600, inputOctets: 250000, outputOctets: 500000, terminateCause: EAcctTerminateCause.UserRequest, }); expect(acctResponse.success).toBeTruthy(); // Verify accounting records const sessionRecords = accountingRecords.filter((r) => r.sessionId === sessionId); expect(sessionRecords.length).toEqual(3); const startRecord = sessionRecords.find((r) => r.statusType === EAcctStatusType.Start); expect(startRecord).toBeDefined(); expect(startRecord!.username).toEqual('alice'); const updateRecord = sessionRecords.find((r) => r.statusType === EAcctStatusType.InterimUpdate); expect(updateRecord).toBeDefined(); expect(updateRecord!.sessionTime).toEqual(300); const stopRecord = sessionRecords.find((r) => r.statusType === EAcctStatusType.Stop); expect(stopRecord).toBeDefined(); expect(stopRecord!.sessionTime).toEqual(600); expect(stopRecord!.terminateCause).toEqual(EAcctTerminateCause.UserRequest); await client.disconnect(); }); tap.test('integration: multiple concurrent clients', async () => { const clients = []; const results = []; // Create 5 clients for (let i = 0; i < 5; i++) { const client = new RadiusClient({ host: '127.0.0.1', authPort: TEST_AUTH_PORT, acctPort: TEST_ACCT_PORT, secret: TEST_SECRET, timeout: 2000, }); await client.connect(); clients.push(client); } // Send authentication requests concurrently const promises = clients.map((client, i) => client.authenticatePap(i % 2 === 0 ? 'alice' : 'bob', i % 2 === 0 ? 'alice123' : 'bob456') ); const responses = await Promise.all(promises); // All should succeed for (const response of responses) { expect(response.accepted).toBeTruthy(); } // Cleanup for (const client of clients) { await client.disconnect(); } }); tap.test('integration: server statistics', async () => { const stats = server.getStats(); // Should have recorded some requests expect(stats.authRequests).toBeGreaterThan(0); expect(stats.authAccepts).toBeGreaterThan(0); expect(stats.authRejects).toBeGreaterThan(0); expect(stats.acctRequests).toBeGreaterThan(0); expect(stats.acctResponses).toBeGreaterThan(0); // Should have no invalid packets (we sent valid ones) expect(stats.authInvalidPackets).toEqual(0); expect(stats.acctInvalidPackets).toEqual(0); }); tap.test('integration: duplicate request handling', async () => { // This tests that the server handles duplicate requests correctly // by caching responses within the detection window const client = new RadiusClient({ host: '127.0.0.1', authPort: TEST_AUTH_PORT, acctPort: TEST_ACCT_PORT, secret: TEST_SECRET, timeout: 2000, }); await client.connect(); // Send same request multiple times quickly const responses = await Promise.all([ client.authenticatePap('alice', 'alice123'), client.authenticatePap('alice', 'alice123'), client.authenticatePap('alice', 'alice123'), ]); // All should succeed with same result for (const response of responses) { expect(response.accepted).toBeTruthy(); } await client.disconnect(); }); tap.test('teardown - stop integration test server', async () => { await server.stop(); }); export default tap.start();