feat(smartradius): Implement full RADIUS server and client with RFC 2865/2866 compliance, including packet handling, authenticators, attributes, secrets manager, client APIs, and comprehensive tests and documentation
This commit is contained in:
246
test/server/test.accounting.ts
Normal file
246
test/server/test.accounting.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import {
|
||||
RadiusPacket,
|
||||
RadiusAuthenticator,
|
||||
ERadiusCode,
|
||||
ERadiusAttributeType,
|
||||
EAcctStatusType,
|
||||
EAcctTerminateCause,
|
||||
EAcctAuthentic,
|
||||
} from '../../ts_server/index.js';
|
||||
|
||||
tap.test('should create Accounting-Request packet with Start status', async () => {
|
||||
const identifier = 1;
|
||||
const secret = 'testing123';
|
||||
|
||||
const packet = RadiusPacket.createAccountingRequest(identifier, secret, [
|
||||
{ type: ERadiusAttributeType.AcctStatusType, value: EAcctStatusType.Start },
|
||||
{ type: ERadiusAttributeType.AcctSessionId, value: 'session-001' },
|
||||
{ type: ERadiusAttributeType.UserName, value: 'testuser' },
|
||||
{ type: ERadiusAttributeType.NasIpAddress, value: '192.168.1.1' },
|
||||
]);
|
||||
|
||||
const decoded = RadiusPacket.decode(packet);
|
||||
expect(decoded.code).toEqual(ERadiusCode.AccountingRequest);
|
||||
expect(decoded.identifier).toEqual(identifier);
|
||||
});
|
||||
|
||||
tap.test('should create Accounting-Request packet with Stop status', async () => {
|
||||
const identifier = 2;
|
||||
const secret = 'testing123';
|
||||
|
||||
const packet = RadiusPacket.createAccountingRequest(identifier, secret, [
|
||||
{ type: ERadiusAttributeType.AcctStatusType, value: EAcctStatusType.Stop },
|
||||
{ type: ERadiusAttributeType.AcctSessionId, value: 'session-001' },
|
||||
{ type: ERadiusAttributeType.UserName, value: 'testuser' },
|
||||
{ type: ERadiusAttributeType.AcctSessionTime, value: 3600 }, // 1 hour
|
||||
{ type: ERadiusAttributeType.AcctInputOctets, value: 1024000 },
|
||||
{ type: ERadiusAttributeType.AcctOutputOctets, value: 2048000 },
|
||||
{ type: ERadiusAttributeType.AcctInputPackets, value: 1000 },
|
||||
{ type: ERadiusAttributeType.AcctOutputPackets, value: 2000 },
|
||||
{ type: ERadiusAttributeType.AcctTerminateCause, value: EAcctTerminateCause.UserRequest },
|
||||
]);
|
||||
|
||||
const decoded = RadiusPacket.decodeAndParse(packet);
|
||||
expect(decoded.code).toEqual(ERadiusCode.AccountingRequest);
|
||||
|
||||
const statusType = decoded.parsedAttributes.find(
|
||||
(a) => a.type === ERadiusAttributeType.AcctStatusType
|
||||
);
|
||||
expect(statusType?.value).toEqual(EAcctStatusType.Stop);
|
||||
});
|
||||
|
||||
tap.test('should create Accounting-Request packet with Interim-Update status', async () => {
|
||||
const identifier = 3;
|
||||
const secret = 'testing123';
|
||||
|
||||
const packet = RadiusPacket.createAccountingRequest(identifier, secret, [
|
||||
{ type: ERadiusAttributeType.AcctStatusType, value: EAcctStatusType.InterimUpdate },
|
||||
{ type: ERadiusAttributeType.AcctSessionId, value: 'session-001' },
|
||||
{ type: ERadiusAttributeType.UserName, value: 'testuser' },
|
||||
{ type: ERadiusAttributeType.AcctSessionTime, value: 1800 }, // 30 min
|
||||
{ type: ERadiusAttributeType.AcctInputOctets, value: 512000 },
|
||||
{ type: ERadiusAttributeType.AcctOutputOctets, value: 1024000 },
|
||||
]);
|
||||
|
||||
const decoded = RadiusPacket.decodeAndParse(packet);
|
||||
expect(decoded.code).toEqual(ERadiusCode.AccountingRequest);
|
||||
|
||||
const statusType = decoded.parsedAttributes.find(
|
||||
(a) => a.type === ERadiusAttributeType.AcctStatusType
|
||||
);
|
||||
expect(statusType?.value).toEqual(EAcctStatusType.InterimUpdate);
|
||||
});
|
||||
|
||||
tap.test('should verify Accounting-Request authenticator', async () => {
|
||||
const identifier = 1;
|
||||
const secret = 'testing123';
|
||||
|
||||
const packet = RadiusPacket.createAccountingRequest(identifier, secret, [
|
||||
{ type: ERadiusAttributeType.AcctStatusType, value: EAcctStatusType.Start },
|
||||
{ type: ERadiusAttributeType.AcctSessionId, value: 'session-001' },
|
||||
]);
|
||||
|
||||
// Verify the authenticator
|
||||
const isValid = RadiusAuthenticator.verifyAccountingRequestAuthenticator(packet, secret);
|
||||
expect(isValid).toBeTruthy();
|
||||
|
||||
// Should fail with wrong secret
|
||||
const isInvalid = RadiusAuthenticator.verifyAccountingRequestAuthenticator(packet, 'wrongsecret');
|
||||
expect(isInvalid).toBeFalsy();
|
||||
});
|
||||
|
||||
tap.test('should create Accounting-Response packet', async () => {
|
||||
const identifier = 1;
|
||||
const requestAuthenticator = Buffer.alloc(16, 0x42);
|
||||
const secret = 'testing123';
|
||||
|
||||
const packet = RadiusPacket.createAccountingResponse(
|
||||
identifier,
|
||||
requestAuthenticator,
|
||||
secret,
|
||||
[] // Usually no attributes in response
|
||||
);
|
||||
|
||||
const decoded = RadiusPacket.decode(packet);
|
||||
expect(decoded.code).toEqual(ERadiusCode.AccountingResponse);
|
||||
expect(decoded.identifier).toEqual(identifier);
|
||||
});
|
||||
|
||||
tap.test('should verify Accounting-Response authenticator', async () => {
|
||||
const identifier = 1;
|
||||
const requestAuthenticator = Buffer.alloc(16, 0x42);
|
||||
const secret = 'testing123';
|
||||
|
||||
const response = RadiusPacket.createAccountingResponse(
|
||||
identifier,
|
||||
requestAuthenticator,
|
||||
secret,
|
||||
[]
|
||||
);
|
||||
|
||||
// Verify response authenticator
|
||||
const isValid = RadiusAuthenticator.verifyResponseAuthenticator(
|
||||
response,
|
||||
requestAuthenticator,
|
||||
secret
|
||||
);
|
||||
expect(isValid).toBeTruthy();
|
||||
});
|
||||
|
||||
tap.test('should parse all accounting attributes correctly', async () => {
|
||||
const identifier = 1;
|
||||
const secret = 'testing123';
|
||||
|
||||
const packet = RadiusPacket.createAccountingRequest(identifier, secret, [
|
||||
{ type: ERadiusAttributeType.AcctStatusType, value: EAcctStatusType.Stop },
|
||||
{ type: ERadiusAttributeType.AcctDelayTime, value: 5 },
|
||||
{ type: ERadiusAttributeType.AcctInputOctets, value: 1000000 },
|
||||
{ type: ERadiusAttributeType.AcctOutputOctets, value: 2000000 },
|
||||
{ type: ERadiusAttributeType.AcctSessionId, value: 'sess-123' },
|
||||
{ type: ERadiusAttributeType.AcctAuthentic, value: EAcctAuthentic.Radius },
|
||||
{ type: ERadiusAttributeType.AcctSessionTime, value: 7200 },
|
||||
{ type: ERadiusAttributeType.AcctInputPackets, value: 5000 },
|
||||
{ type: ERadiusAttributeType.AcctOutputPackets, value: 10000 },
|
||||
{ type: ERadiusAttributeType.AcctTerminateCause, value: EAcctTerminateCause.SessionTimeout },
|
||||
{ type: ERadiusAttributeType.AcctMultiSessionId, value: 'multi-sess-456' },
|
||||
{ type: ERadiusAttributeType.AcctLinkCount, value: 2 },
|
||||
]);
|
||||
|
||||
const decoded = RadiusPacket.decodeAndParse(packet);
|
||||
|
||||
// Check each attribute
|
||||
const attrs = decoded.parsedAttributes;
|
||||
|
||||
const statusType = attrs.find((a) => a.type === ERadiusAttributeType.AcctStatusType);
|
||||
expect(statusType?.value).toEqual(EAcctStatusType.Stop);
|
||||
|
||||
const delayTime = attrs.find((a) => a.type === ERadiusAttributeType.AcctDelayTime);
|
||||
expect(delayTime?.value).toEqual(5);
|
||||
|
||||
const inputOctets = attrs.find((a) => a.type === ERadiusAttributeType.AcctInputOctets);
|
||||
expect(inputOctets?.value).toEqual(1000000);
|
||||
|
||||
const outputOctets = attrs.find((a) => a.type === ERadiusAttributeType.AcctOutputOctets);
|
||||
expect(outputOctets?.value).toEqual(2000000);
|
||||
|
||||
const sessionId = attrs.find((a) => a.type === ERadiusAttributeType.AcctSessionId);
|
||||
expect(sessionId?.value).toEqual('sess-123');
|
||||
|
||||
const authentic = attrs.find((a) => a.type === ERadiusAttributeType.AcctAuthentic);
|
||||
expect(authentic?.value).toEqual(EAcctAuthentic.Radius);
|
||||
|
||||
const sessionTime = attrs.find((a) => a.type === ERadiusAttributeType.AcctSessionTime);
|
||||
expect(sessionTime?.value).toEqual(7200);
|
||||
|
||||
const terminateCause = attrs.find((a) => a.type === ERadiusAttributeType.AcctTerminateCause);
|
||||
expect(terminateCause?.value).toEqual(EAcctTerminateCause.SessionTimeout);
|
||||
});
|
||||
|
||||
tap.test('should handle Accounting-On/Off status types', async () => {
|
||||
const secret = 'testing123';
|
||||
|
||||
// Accounting-On (NAS restart/reboot notification)
|
||||
const acctOnPacket = RadiusPacket.createAccountingRequest(1, secret, [
|
||||
{ type: ERadiusAttributeType.AcctStatusType, value: EAcctStatusType.AccountingOn },
|
||||
{ type: ERadiusAttributeType.NasIpAddress, value: '192.168.1.1' },
|
||||
]);
|
||||
|
||||
let decoded = RadiusPacket.decodeAndParse(acctOnPacket);
|
||||
let statusType = decoded.parsedAttributes.find(
|
||||
(a) => a.type === ERadiusAttributeType.AcctStatusType
|
||||
);
|
||||
expect(statusType?.value).toEqual(EAcctStatusType.AccountingOn);
|
||||
|
||||
// Accounting-Off
|
||||
const acctOffPacket = RadiusPacket.createAccountingRequest(2, secret, [
|
||||
{ type: ERadiusAttributeType.AcctStatusType, value: EAcctStatusType.AccountingOff },
|
||||
{ type: ERadiusAttributeType.NasIpAddress, value: '192.168.1.1' },
|
||||
]);
|
||||
|
||||
decoded = RadiusPacket.decodeAndParse(acctOffPacket);
|
||||
statusType = decoded.parsedAttributes.find(
|
||||
(a) => a.type === ERadiusAttributeType.AcctStatusType
|
||||
);
|
||||
expect(statusType?.value).toEqual(EAcctStatusType.AccountingOff);
|
||||
});
|
||||
|
||||
tap.test('should handle all termination causes', async () => {
|
||||
const secret = 'testing123';
|
||||
const terminationCauses = [
|
||||
EAcctTerminateCause.UserRequest,
|
||||
EAcctTerminateCause.LostCarrier,
|
||||
EAcctTerminateCause.LostService,
|
||||
EAcctTerminateCause.IdleTimeout,
|
||||
EAcctTerminateCause.SessionTimeout,
|
||||
EAcctTerminateCause.AdminReset,
|
||||
EAcctTerminateCause.AdminReboot,
|
||||
EAcctTerminateCause.PortError,
|
||||
EAcctTerminateCause.NasError,
|
||||
EAcctTerminateCause.NasRequest,
|
||||
EAcctTerminateCause.NasReboot,
|
||||
EAcctTerminateCause.PortUnneeded,
|
||||
EAcctTerminateCause.PortPreempted,
|
||||
EAcctTerminateCause.PortSuspended,
|
||||
EAcctTerminateCause.ServiceUnavailable,
|
||||
EAcctTerminateCause.Callback,
|
||||
EAcctTerminateCause.UserError,
|
||||
EAcctTerminateCause.HostRequest,
|
||||
];
|
||||
|
||||
for (const cause of terminationCauses) {
|
||||
const packet = RadiusPacket.createAccountingRequest(1, secret, [
|
||||
{ type: ERadiusAttributeType.AcctStatusType, value: EAcctStatusType.Stop },
|
||||
{ type: ERadiusAttributeType.AcctSessionId, value: 'session-001' },
|
||||
{ type: ERadiusAttributeType.AcctTerminateCause, value: cause },
|
||||
]);
|
||||
|
||||
const decoded = RadiusPacket.decodeAndParse(packet);
|
||||
const termCause = decoded.parsedAttributes.find(
|
||||
(a) => a.type === ERadiusAttributeType.AcctTerminateCause
|
||||
);
|
||||
expect(termCause?.value).toEqual(cause);
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
Reference in New Issue
Block a user