305 lines
9.0 KiB
TypeScript
305 lines
9.0 KiB
TypeScript
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<string, { password: string; sessionTimeout?: number }> = {
|
|
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();
|