From 4e20a8a4c177c8a498aa63f12852abe7c21ca645 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Luis=20Mill=C3=A1n?= Date: Tue, 27 Jan 2026 13:01:41 +0100 Subject: [PATCH 1/8] Rewrite tests to TypeScript --- jest.config.js | 12 +- npm-scripts.mjs | 2 - package.json | 2 + src/NameAddrHeader.d.ts | 2 +- src/URI.d.ts | 4 +- src/test/include/{common.js => common.ts} | 10 +- .../include/{loopSocket.js => loopSocket.ts} | 38 +++- src/test/include/testUA.js | 54 ----- src/test/include/testUA.ts | 56 +++++ ...t-UA-no-WebRTC.js => test-UA-no-WebRTC.ts} | 42 ++-- src/test/test-UA-subscriber-notifier.js | 175 --------------- src/test/test-UA-subscriber-notifier.ts | 201 ++++++++++++++++++ src/test/{test-classes.js => test-classes.ts} | 14 +- ...cation.js => test-digestAuthentication.ts} | 5 +- ...alizeTarget.js => test-normalizeTarget.ts} | 23 +- src/test/{test-parser.js => test-parser.ts} | 114 +++++----- ...{test-properties.js => test-properties.ts} | 7 +- 17 files changed, 417 insertions(+), 344 deletions(-) rename src/test/include/{common.js => common.ts} (50%) rename src/test/include/{loopSocket.js => loopSocket.ts} (55%) delete mode 100644 src/test/include/testUA.js create mode 100644 src/test/include/testUA.ts rename src/test/{test-UA-no-WebRTC.js => test-UA-no-WebRTC.ts} (66%) delete mode 100644 src/test/test-UA-subscriber-notifier.js create mode 100644 src/test/test-UA-subscriber-notifier.ts rename src/test/{test-classes.js => test-classes.ts} (92%) rename src/test/{test-digestAuthentication.js => test-digestAuthentication.ts} (94%) rename src/test/{test-normalizeTarget.js => test-normalizeTarget.ts} (74%) rename src/test/{test-parser.js => test-parser.ts} (80%) rename src/test/{test-properties.js => test-properties.ts} (57%) diff --git a/jest.config.js b/jest.config.js index ac9471a7..a85f45b2 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,3 +1,13 @@ module.exports = { - testRegex: 'src/test/test-.*\\.js', + preset: 'ts-jest', + testEnvironment: 'node', + testRegex: 'src/test/test-.*\\.ts', + transform: { + '^.+\\.ts$': [ + 'ts-jest', + { + tsconfig: 'tsconfig.json', + }, + ], + }, }; diff --git a/npm-scripts.mjs b/npm-scripts.mjs index 1a7fcfc0..0fe9a02a 100644 --- a/npm-scripts.mjs +++ b/npm-scripts.mjs @@ -92,8 +92,6 @@ function lint(fix = false) { function test() { logInfo('test()'); - // TODO: remove when tests are written in TS. - buildTypescript(); executeCmd('jest'); } diff --git a/package.json b/package.json index 603ef16b..8207ba6f 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@eslint/js": "^9.39.2", "@types/debug": "^4.1.12", "@types/events": "^3.0.3", + "@types/jest": "^30.0.0", "@types/node": "^25.0.10", "cpx": "^1.5.0", "esbuild": "^0.27.2", @@ -61,6 +62,7 @@ "jest": "^30.2.0", "pegjs": "^0.7.0", "prettier": "^3.8.1", + "ts-jest": "^29.4.6", "typescript": "^5.9.3", "typescript-eslint": "^8.53.1" } diff --git a/src/NameAddrHeader.d.ts b/src/NameAddrHeader.d.ts index 59e15cb0..c897ae4f 100644 --- a/src/NameAddrHeader.d.ts +++ b/src/NameAddrHeader.d.ts @@ -9,7 +9,7 @@ export class NameAddrHeader { constructor(uri: URI, display_name?: string, parameters?: Parameters); - setParam(key: string, value?: string): void; + setParam(key: string, value?: string | number | null): void; // eslint-disable-next-line @typescript-eslint/no-explicit-any getParam(key: string): T; diff --git a/src/URI.d.ts b/src/URI.d.ts index 05fcd0db..eb1502c6 100644 --- a/src/URI.d.ts +++ b/src/URI.d.ts @@ -21,7 +21,7 @@ export class URI { headers?: Headers ); - setParam(key: string, value?: string): void; + setParam(key: string, value?: string | number | null): void; getParam(key: string): T; @@ -45,7 +45,7 @@ export class URI { toString(): string; - toAor(): string; + toAor(show_port?: boolean): string; static parse(uri: string): Grammar | undefined; } diff --git a/src/test/include/common.js b/src/test/include/common.ts similarity index 50% rename from src/test/include/common.js rename to src/test/include/common.ts index 85a90947..2d57b490 100644 --- a/src/test/include/common.js +++ b/src/test/include/common.ts @@ -1,18 +1,20 @@ -/* eslint no-console: 0*/ +/* eslint no-console: 0 */ // Show uncaught errors. -process.on('uncaughtException', function (error) { +process.on('uncaughtException', function (error: Error) { console.error('uncaught exception:'); console.error(error.stack); process.exit(1); }); // Define global.WebSocket. -global.WebSocket = function () { +(globalThis as Record)['WebSocket'] = function (this: { + close: () => void; +}) { this.close = function () {}; }; // Define global.navigator for bowser module. -global.navigator = { +(globalThis as Record)['navigator'] = { userAgent: '', }; diff --git a/src/test/include/loopSocket.js b/src/test/include/loopSocket.ts similarity index 55% rename from src/test/include/loopSocket.js rename to src/test/include/loopSocket.ts index c2a68dcd..9750b32d 100644 --- a/src/test/include/loopSocket.js +++ b/src/test/include/loopSocket.ts @@ -1,21 +1,22 @@ // LoopSocket send message itself. // Used P2P logic: message call-id is modified in each leg. -module.exports = class LoopSocket { - constructor() { - this.url = 'ws://localhost:12345'; - this.via_transport = 'WS'; - this.sip_uri = 'sip:localhost:12345;transport=ws'; - } - connect() { +import type { Socket } from '../../Socket'; + +export default class LoopSocket implements Socket { + url = 'ws://localhost:12345'; + via_transport = 'WS'; + sip_uri = 'sip:localhost:12345;transport=ws'; + + connect(): void { setTimeout(() => { this.onconnect(); }, 0); } - disconnect() {} + disconnect(): void {} - send(message) { + send(message: string): boolean { const message2 = this._modifyCallId(message); setTimeout(() => { @@ -25,8 +26,23 @@ module.exports = class LoopSocket { return true; } + isConnected(): boolean { + return true; + } + + isConnecting(): boolean { + return false; + } + + onconnect(): void {} + + ondisconnect(): void {} + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ondata(_event: T): void {} + // Call-ID: add or drop word '_second'. - _modifyCallId(message) { + private _modifyCallId(message: string): string { const ixBegin = message.indexOf('Call-ID'); const ixEnd = message.indexOf('\r', ixBegin); let callId = message.substring(ixBegin + 9, ixEnd); @@ -39,4 +55,4 @@ module.exports = class LoopSocket { return `${message.substring(0, ixBegin)}Call-ID: ${callId}${message.substring(ixEnd)}`; } -}; +} diff --git a/src/test/include/testUA.js b/src/test/include/testUA.js deleted file mode 100644 index 2e426a55..00000000 --- a/src/test/include/testUA.js +++ /dev/null @@ -1,54 +0,0 @@ -module.exports = { - SOCKET_DESCRIPTION: { - via_transport: 'WS', - sip_uri: 'sip:localhost:12345;transport=ws', - url: 'ws://localhost:12345', - }, - - UA_CONFIGURATION: { - uri: 'sip:f%61keUA@jssip.net', - password: '1234ññññ', - display_name: 'Fake UA ð→€ł !!!', - authorization_user: 'fakeUA', - instance_id: 'uuid:8f1fa16a-1165-4a96-8341-785b1ef24f12', - registrar_server: 'registrar.jssip.NET:6060;TRansport=TCP', - register_expires: 600, - register: false, - connection_recovery_min_interval: 2, - connection_recovery_max_interval: 30, - use_preloaded_route: true, - no_answer_timeout: 60000, - session_timers: true, - }, - - UA_CONFIGURATION_AFTER_START: { - uri: 'sip:fakeUA@jssip.net', - password: '1234ññññ', - display_name: 'Fake UA ð→€ł !!!', - authorization_user: 'fakeUA', - instance_id: '8f1fa16a-1165-4a96-8341-785b1ef24f12', // Without 'uuid:'. - registrar_server: 'sip:registrar.jssip.net:6060;transport=tcp', - register_expires: 600, - register: false, - use_preloaded_route: true, - no_answer_timeout: 60000 * 1000, // Internally converted to miliseconds. - session_timers: true, - }, - - UA_TRANSPORT_AFTER_START: { - sockets: [ - { - socket: { - via_transport: 'WS', - sip_uri: 'sip:localhost:12345;transport=ws', - url: 'ws://localhost:12345', - }, - weight: 0, - }, - ], - recovery_options: { - min_interval: 2, - max_interval: 30, - }, - }, -}; diff --git a/src/test/include/testUA.ts b/src/test/include/testUA.ts new file mode 100644 index 00000000..5e3a138a --- /dev/null +++ b/src/test/include/testUA.ts @@ -0,0 +1,56 @@ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const SOCKET_DESCRIPTION: Record = { + via_transport: 'WS', + sip_uri: 'sip:localhost:12345;transport=ws', + url: 'ws://localhost:12345', +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const UA_CONFIGURATION: Record = { + uri: 'sip:f%61keUA@jssip.net', + password: '1234ññññ', + display_name: 'Fake UA ð→€ł !!!', + authorization_user: 'fakeUA', + instance_id: 'uuid:8f1fa16a-1165-4a96-8341-785b1ef24f12', + registrar_server: 'registrar.jssip.NET:6060;TRansport=TCP', + register_expires: 600, + register: false, + connection_recovery_min_interval: 2, + connection_recovery_max_interval: 30, + use_preloaded_route: true, + no_answer_timeout: 60000, + session_timers: true, +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const UA_CONFIGURATION_AFTER_START: Record = { + uri: 'sip:fakeUA@jssip.net', + password: '1234ññññ', + display_name: 'Fake UA ð→€ł !!!', + authorization_user: 'fakeUA', + instance_id: '8f1fa16a-1165-4a96-8341-785b1ef24f12', // Without 'uuid:'. + registrar_server: 'sip:registrar.jssip.net:6060;transport=tcp', + register_expires: 600, + register: false, + use_preloaded_route: true, + no_answer_timeout: 60000 * 1000, // Internally converted to miliseconds. + session_timers: true, +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const UA_TRANSPORT_AFTER_START: Record = { + sockets: [ + { + socket: { + via_transport: 'WS', + sip_uri: 'sip:localhost:12345;transport=ws', + url: 'ws://localhost:12345', + }, + weight: 0, + }, + ], + recovery_options: { + min_interval: 2, + max_interval: 30, + }, +}; diff --git a/src/test/test-UA-no-WebRTC.js b/src/test/test-UA-no-WebRTC.ts similarity index 66% rename from src/test/test-UA-no-WebRTC.js rename to src/test/test-UA-no-WebRTC.ts index 8c7c88d0..a6a063a0 100644 --- a/src/test/test-UA-no-WebRTC.js +++ b/src/test/test-UA-no-WebRTC.ts @@ -1,27 +1,28 @@ /* eslint no-console: 0*/ -require('./include/common'); -const testUA = require('./include/testUA'); -const JsSIP = require('../..'); +import './include/common'; +import * as testUA from './include/testUA'; + +// eslint-disable-next-line @typescript-eslint/no-require-imports +const JsSIP = require('../JsSIP.js'); +const { UA, WebSocketInterface, Exceptions, C } = JsSIP; describe('UA No WebRTC', () => { test('UA wrong configuration', () => { - expect(() => new JsSIP.UA({ lalala: 'lololo' })).toThrow( - JsSIP.Exceptions.ConfigurationError + expect(() => new UA({ lalala: 'lololo' } as never)).toThrow( + Exceptions.ConfigurationError ); }); test('UA no WS connection', () => { const config = testUA.UA_CONFIGURATION; - const wsSocket = new JsSIP.WebSocketInterface( - testUA.SOCKET_DESCRIPTION.url - ); + const wsSocket = new WebSocketInterface(testUA.SOCKET_DESCRIPTION['url']); - config.sockets = wsSocket; + config['sockets'] = wsSocket; - const ua = new JsSIP.UA(config); + const ua = new UA(config); - expect(ua instanceof JsSIP.UA).toBeTruthy(); + expect(ua instanceof UA).toBeTruthy(); ua.start(); @@ -53,9 +54,9 @@ describe('UA No WebRTC', () => { switch (parameter) { case 'uri': case 'registrar_server': { + // eslint-disable-next-line jest/no-conditional-expect expect(ua.configuration[parameter].toString()).toBe( - testUA.UA_CONFIGURATION_AFTER_START[parameter], - `testing parameter ${parameter}` + testUA.UA_CONFIGURATION_AFTER_START[parameter] ); break; } @@ -64,9 +65,9 @@ describe('UA No WebRTC', () => { break; } default: { + // eslint-disable-next-line jest/no-conditional-expect expect(ua.configuration[parameter]).toBe( - testUA.UA_CONFIGURATION_AFTER_START[parameter], - `testing parameter ${parameter}` + testUA.UA_CONFIGURATION_AFTER_START[parameter] ); } } @@ -74,7 +75,7 @@ describe('UA No WebRTC', () => { } const transport = testUA.UA_TRANSPORT_AFTER_START; - const sockets = transport.sockets; + const sockets = transport['sockets']; const socket = sockets[0].socket; expect(sockets.length).toEqual(ua.transport.sockets.length); @@ -83,12 +84,15 @@ describe('UA No WebRTC', () => { expect(socket.sip_uri).toEqual(ua.transport.sip_uri); expect(socket.url).toEqual(ua.transport.url); - expect(transport.recovery_options).toEqual(ua.transport.recovery_options); + expect(transport['recovery_options']).toEqual( + ua.transport.recovery_options + ); ua.sendMessage('test', 'FAIL WITH CONNECTION_ERROR PLEASE', { eventHandlers: { - failed: function (e) { - expect(e.cause).toEqual(JsSIP.C.causes.CONNECTION_ERROR); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + failed: function (e: any) { + expect(e.cause).toEqual(C.causes.CONNECTION_ERROR); }, }, }); diff --git a/src/test/test-UA-subscriber-notifier.js b/src/test/test-UA-subscriber-notifier.js deleted file mode 100644 index 9c562d6d..00000000 --- a/src/test/test-UA-subscriber-notifier.js +++ /dev/null @@ -1,175 +0,0 @@ -require('./include/common'); -const JsSIP = require('../..'); -const LoopSocket = require('./include/loopSocket'); - -describe('subscriber/notifier communication', () => { - test('should handle subscriber/notifier communication', () => - new Promise(resolve => { - let eventSequence = 0; - - const TARGET = 'ikq'; - const REQUEST_URI = 'sip:ikq@example.com'; - const CONTACT_URI = 'sip:ikq@abcdefabcdef.invalid;transport=ws'; - const SUBSCRIBE_ACCEPT = 'application/text, text/plain'; - const EVENT_NAME = 'weather'; - const CONTENT_TYPE = 'text/plain'; - const WEATHER_REQUEST = 'Please report the weather condition'; - const WEATHER_REPORT = '+20..+24°C, no precipitation, light wind'; - - /** - * @param {JsSIP.UA} ua - */ - function createSubscriber(ua) { - const options = { - expires: 3600, - contentType: CONTENT_TYPE, - params: null, - }; - - const subscriber = ua.subscribe( - TARGET, - EVENT_NAME, - SUBSCRIBE_ACCEPT, - options - ); - - subscriber.on('active', () => { - // 'receive notify with subscription-state: active' - expect(++eventSequence).toBe(6); - }); - - subscriber.on('notify', (isFinal, notify, body, contType) => { - eventSequence++; - // 'receive notify' - expect(eventSequence === 7 || eventSequence === 11).toBe(true); - - expect(notify.method).toBe('NOTIFY'); - expect(notify.getHeader('contact')).toBe(`<${CONTACT_URI}>`); // 'notify contact' - expect(body).toBe(WEATHER_REPORT); // 'notify body' - expect(contType).toBe(CONTENT_TYPE); // 'notify content-type' - - const subsState = notify.parseHeader('subscription-state').state; - - expect( - subsState === 'pending' || - subsState === 'active' || - subsState === 'terminated' - ).toBe(true); // 'notify subscription-state' - - // After receiving the first notify, send un-subscribe. - if (eventSequence === 7) { - ++eventSequence; // 'send un-subscribe' - - subscriber.terminate(WEATHER_REQUEST); - } - }); - - subscriber.on('terminated', (terminationCode, reason, retryAfter) => { - expect(++eventSequence).toBe(12); // 'subscriber terminated' - expect(terminationCode).toBe(subscriber.C.FINAL_NOTIFY_RECEIVED); - expect(reason).toBeUndefined(); - expect(retryAfter).toBeUndefined(); - - ua.stop(); - }); - - subscriber.on('accepted', () => { - expect(++eventSequence).toBe(5); // 'initial subscribe accepted' - }); - - expect(++eventSequence).toBe(2); // 'send subscribe' - - subscriber.subscribe(WEATHER_REQUEST); - } - - /** - * @param {JsSIP.UA} ua - */ - function createNotifier(ua, subscribe) { - const notifier = ua.notify(subscribe, CONTENT_TYPE, { pending: false }); - - // Receive subscribe (includes initial) - notifier.on('subscribe', (isUnsubscribe, subs, body, contType) => { - expect(subscribe.method).toBe('SUBSCRIBE'); - expect(subscribe.getHeader('contact')).toBe(`<${CONTACT_URI}>`); // 'subscribe contact' - expect(subscribe.getHeader('accept')).toBe(SUBSCRIBE_ACCEPT); // 'subscribe accept' - expect(body).toBe(WEATHER_REQUEST); // 'subscribe body' - expect(contType).toBe(CONTENT_TYPE); // 'subscribe content-type' - - expect(++eventSequence).toBe(isUnsubscribe ? 9 : 4); - if (isUnsubscribe) { - // 'send final notify' - notifier.terminate(WEATHER_REPORT); - } else { - // 'send notify' - notifier.notify(WEATHER_REPORT); - } - }); - - // Example only. Never reached. - notifier.on('expired', () => { - notifier.terminate(WEATHER_REPORT, 'timeout'); - }); - - notifier.on('terminated', () => { - expect(++eventSequence).toBe(10); // 'notifier terminated' - }); - - notifier.start(); - } - - // Start JsSIP UA with loop socket. - const config = { - sockets: new LoopSocket(), // message sending itself, with modified Call-ID - uri: REQUEST_URI, - contact_uri: CONTACT_URI, - register: false, - }; - - const ua = new JsSIP.UA(config); - - // Uncomment to see SIP communication - // JsSIP.debug.enable('JsSIP:*'); - - ua.on('newSubscribe', e => { - expect(++eventSequence).toBe(3); // 'receive initial subscribe' - - const subs = e.request; - const ev = subs.parseHeader('event'); - - expect(subs.ruri.toString()).toBe(REQUEST_URI); // 'initial subscribe uri' - expect(ev.event).toBe(EVENT_NAME); // 'subscribe event' - - if (ev.event !== EVENT_NAME) { - subs.reply(489); // "Bad Event" - - return; - } - - const accepts = subs.getHeaders('accept'); - const canUse = accepts && accepts.some(v => v.includes(CONTENT_TYPE)); - - expect(canUse).toBe(true); // 'notifier can use subscribe accept header' - - if (!canUse) { - subs.reply(406); // "Not Acceptable" - - return; - } - - createNotifier(ua, subs); - }); - - ua.on('connected', () => { - expect(++eventSequence).toBe(1); // 'socket connected' - - createSubscriber(ua); - }); - - ua.on('disconnected', () => { - resolve(); - }); - - ua.start(); - })); -}); diff --git a/src/test/test-UA-subscriber-notifier.ts b/src/test/test-UA-subscriber-notifier.ts new file mode 100644 index 00000000..3647cf84 --- /dev/null +++ b/src/test/test-UA-subscriber-notifier.ts @@ -0,0 +1,201 @@ +import './include/common'; +import LoopSocket from './include/loopSocket'; + +// eslint-disable-next-line @typescript-eslint/no-require-imports +const JsSIP = require('../JsSIP.js'); +const { UA } = JsSIP; + +describe('subscriber/notifier communication', () => { + test('should handle subscriber/notifier communication', () => + new Promise(resolve => { + let eventSequence = 0; + + const TARGET = 'ikq'; + const REQUEST_URI = 'sip:ikq@example.com'; + const CONTACT_URI = 'sip:ikq@abcdefabcdef.invalid;transport=ws'; + const SUBSCRIBE_ACCEPT = 'application/text, text/plain'; + const EVENT_NAME = 'weather'; + const CONTENT_TYPE = 'text/plain'; + const WEATHER_REQUEST = 'Please report the weather condition'; + const WEATHER_REPORT = '+20..+24°C, no precipitation, light wind'; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + function createSubscriber(ua: any): void { + const options = { + expires: 3600, + contentType: CONTENT_TYPE, + }; + + const subscriber = ua.subscribe( + TARGET, + EVENT_NAME, + SUBSCRIBE_ACCEPT, + options + ); + + subscriber.on('active', () => { + // 'receive notify with subscription-state: active' + expect(++eventSequence).toBe(6); + }); + + subscriber.on( + 'notify', + ( + isFinal: boolean, + notify: { + method: string; + getHeader: (name: string) => string; + parseHeader: (name: string) => { state: string }; + }, + body?: string, + contType?: string + ) => { + eventSequence++; + // 'receive notify' + expect(eventSequence === 7 || eventSequence === 11).toBe(true); + + expect(notify.method).toBe('NOTIFY'); + expect(notify.getHeader('contact')).toBe(`<${CONTACT_URI}>`); // 'notify contact' + expect(body).toBe(WEATHER_REPORT); // 'notify body' + expect(contType).toBe(CONTENT_TYPE); // 'notify content-type' + + const subsState = notify.parseHeader('subscription-state').state; + + expect( + subsState === 'pending' || + subsState === 'active' || + subsState === 'terminated' + ).toBe(true); // 'notify subscription-state' + + // After receiving the first notify, send un-subscribe. + if (eventSequence === 7) { + ++eventSequence; // 'send un-subscribe' + + subscriber.terminate(WEATHER_REQUEST); + } + } + ); + + subscriber.on( + 'terminated', + ( + terminationCode: number, + reason: string | undefined, + retryAfter: number | undefined + ) => { + expect(++eventSequence).toBe(12); // 'subscriber terminated' + expect(terminationCode).toBe(subscriber.C.FINAL_NOTIFY_RECEIVED); + expect(reason).toBeUndefined(); + expect(retryAfter).toBeUndefined(); + + ua.stop(); + } + ); + + subscriber.on('accepted', () => { + expect(++eventSequence).toBe(5); // 'initial subscribe accepted' + }); + + expect(++eventSequence).toBe(2); // 'send subscribe' + + subscriber.subscribe(WEATHER_REQUEST); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + function createNotifier(ua: any, subscribe: any): void { + const notifier = ua.notify(subscribe, CONTENT_TYPE, { pending: false }); + + // Receive subscribe (includes initial) + notifier.on( + 'subscribe', + ( + isUnsubscribe: boolean, + subs: unknown, + body?: string, + contType?: string + ) => { + expect(subscribe.method).toBe('SUBSCRIBE'); + expect(subscribe.getHeader('contact')).toBe(`<${CONTACT_URI}>`); // 'subscribe contact' + expect(subscribe.getHeader('accept')).toBe(SUBSCRIBE_ACCEPT); // 'subscribe accept' + expect(body).toBe(WEATHER_REQUEST); // 'subscribe body' + expect(contType).toBe(CONTENT_TYPE); // 'subscribe content-type' + + expect(++eventSequence).toBe(isUnsubscribe ? 9 : 4); + if (isUnsubscribe) { + // 'send final notify' + notifier.terminate(WEATHER_REPORT); + } else { + // 'send notify' + notifier.notify(WEATHER_REPORT); + } + } + ); + + // Example only. Never reached. + notifier.on('expired', () => { + notifier.terminate(WEATHER_REPORT, 'timeout'); + }); + + notifier.on('terminated', () => { + expect(++eventSequence).toBe(10); // 'notifier terminated' + }); + + notifier.start(); + } + + // Start JsSIP UA with loop socket. + const config = { + sockets: new LoopSocket(), // message sending itself, with modified Call-ID + uri: REQUEST_URI, + contact_uri: CONTACT_URI, + register: false, + }; + + const ua = new UA(config); + + // Uncomment to see SIP communication + // JsSIP.debug.enable('JsSIP:*'); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ua.on('newSubscribe', (e: any) => { + expect(++eventSequence).toBe(3); // 'receive initial subscribe' + + const subs = e.request; + const ev = subs.parseHeader('event'); + + expect(subs.ruri.toString()).toBe(REQUEST_URI); // 'initial subscribe uri' + expect(ev.event).toBe(EVENT_NAME); // 'subscribe event' + + if (ev.event !== EVENT_NAME) { + subs.reply(489); // "Bad Event" + + return; + } + + const accepts = subs.getHeaders('accept'); + const canUse = accepts?.some((v: string) => v.includes(CONTENT_TYPE)); + + expect(canUse).toBe(true); // 'notifier can use subscribe accept header' + + if (!canUse) { + subs.reply(406); // "Not Acceptable" + + return; + } + + createNotifier(ua, subs); + }); + + ua.on('connected', () => { + expect(++eventSequence).toBe(1); // 'socket connected' + + createSubscriber(ua); + }); + + ua.on('disconnected', () => { + resolve(); + }); + + ua.start(); + })); +}); diff --git a/src/test/test-classes.js b/src/test/test-classes.ts similarity index 92% rename from src/test/test-classes.js rename to src/test/test-classes.ts index 9b72b326..a92f9c3a 100644 --- a/src/test/test-classes.js +++ b/src/test/test-classes.ts @@ -1,9 +1,12 @@ -require('./include/common'); -const JsSIP = require('../..'); +import './include/common'; + +// eslint-disable-next-line @typescript-eslint/no-require-imports +const JsSIP = require('../JsSIP.js'); +const { URI, NameAddrHeader } = JsSIP; describe('URI Tests', () => { test('new URI', () => { - const uri = new JsSIP.URI(null, 'alice', 'jssip.net', 6060); + const uri = new URI(null, 'alice', 'jssip.net', 6060); expect(uri.scheme).toBe('sip'); expect(uri.user).toBe('alice'); @@ -115,8 +118,8 @@ describe('URI Tests', () => { describe('NameAddr Tests', () => { test('new NameAddr', () => { - const uri = new JsSIP.URI('sip', 'alice', 'jssip.net'); - const name = new JsSIP.NameAddrHeader(uri, 'Alice æßð'); + const uri = new URI('sip', 'alice', 'jssip.net'); + const name = new NameAddrHeader(uri, 'Alice æßð'); expect(name.display_name).toBe('Alice æßð'); expect(name.toString()).toBe('"Alice æßð" '); @@ -147,6 +150,5 @@ describe('NameAddr Tests', () => { expect(name2.toString()).toBe(name.toString()); name2.display_name = '@ł€'; expect(name2.display_name).toBe('@ł€'); - expect(name.user).toBeUndefined(); }); }); diff --git a/src/test/test-digestAuthentication.js b/src/test/test-digestAuthentication.ts similarity index 94% rename from src/test/test-digestAuthentication.js rename to src/test/test-digestAuthentication.ts index 20684208..28a980be 100644 --- a/src/test/test-digestAuthentication.js +++ b/src/test/test-digestAuthentication.ts @@ -1,5 +1,6 @@ -require('./include/common'); -const DigestAuthentication = require('../DigestAuthentication.js'); +import './include/common'; +// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-explicit-any +const DigestAuthentication: any = require('../DigestAuthentication.js'); // Results of this tests originally obtained from RFC 2617 and: // 'https://pernau.at/kd/sipdigest.php' diff --git a/src/test/test-normalizeTarget.js b/src/test/test-normalizeTarget.ts similarity index 74% rename from src/test/test-normalizeTarget.js rename to src/test/test-normalizeTarget.ts index 1fb874a0..07c43fd6 100644 --- a/src/test/test-normalizeTarget.js +++ b/src/test/test-normalizeTarget.ts @@ -1,15 +1,18 @@ -require('./include/common'); -const JsSIP = require('../..'); +import './include/common'; + +// eslint-disable-next-line @typescript-eslint/no-require-imports +const JsSIP = require('../JsSIP.js'); +const { URI, Utils } = JsSIP; describe('normalizeTarget', () => { test('valid targets', () => { const domain = 'jssip.net'; - function test_ok(given_data, expected) { - const uri = JsSIP.Utils.normalizeTarget(given_data, domain); + function test_ok(given_data: string, expected: string): void { + const uri = Utils.normalizeTarget(given_data, domain); - expect(uri instanceof JsSIP.URI).toBeTruthy(); - expect(uri.toString()).toEqual(expected); + expect(uri instanceof URI).toBeTruthy(); + expect(uri!.toString()).toEqual(expected); } test_ok('%61lice', 'sip:alice@jssip.net'); @@ -39,8 +42,10 @@ describe('normalizeTarget', () => { test('invalid targets', () => { const domain = 'jssip.net'; - function test_error(given_data) { - expect(JsSIP.Utils.normalizeTarget(given_data, domain)).toBe(undefined); + function test_error(given_data: unknown): void { + expect(Utils.normalizeTarget(given_data as string, domain)).toBe( + undefined + ); } test_error(null); @@ -52,6 +57,6 @@ describe('normalizeTarget', () => { test_error('ibc@iñaki.com'); test_error('ibc@aliax.net;;;;;'); - expect(JsSIP.Utils.normalizeTarget('alice')).toBe(undefined); + expect(Utils.normalizeTarget('alice')).toBe(undefined); }); }); diff --git a/src/test/test-parser.js b/src/test/test-parser.ts similarity index 80% rename from src/test/test-parser.js rename to src/test/test-parser.ts index d6c68251..3ed3122c 100644 --- a/src/test/test-parser.js +++ b/src/test/test-parser.ts @@ -1,16 +1,20 @@ -require('./include/common'); -const JsSIP = require('../..'); -const testUA = require('./include/testUA'); -const Parser = require('../Parser'); +import './include/common'; +import * as testUA from './include/testUA'; + +// eslint-disable-next-line @typescript-eslint/no-require-imports +const JsSIP = require('../JsSIP.js'); +const { URI, NameAddrHeader, Grammar, WebSocketInterface, UA } = JsSIP; +// eslint-disable-next-line @typescript-eslint/no-require-imports +const Parser = require('../Parser.js'); describe('parser', () => { test('parse URI', () => { const data = 'SIP:%61liCE@versaTICA.Com:6060;TRansport=TCp;Foo=ABc;baz?X-Header-1=AaA1&X-Header-2=BbB&x-header-1=AAA2'; - const uri = JsSIP.URI.parse(data); + const uri = URI.parse(data); // Parsed data. - expect(uri instanceof JsSIP.URI).toBeTruthy(); + expect(uri instanceof URI).toBeTruthy(); expect(uri.scheme).toBe('sip'); expect(uri.user).toBe('aliCE'); expect(uri.host).toBe('versatica.com'); @@ -49,10 +53,10 @@ describe('parser', () => { const data = ' "Iñaki ðđøþ foo \\"bar\\" \\\\\\\\ \\\\ \\\\d \\\\\\\\d \\\\\' \\\\\\"sdf\\\\\\"" ' + ';QWE=QWE;ASd'; - const name = JsSIP.NameAddrHeader.parse(data); + const name = NameAddrHeader.parse(data); // Parsed data. - expect(name instanceof JsSIP.NameAddrHeader).toBeTruthy(); + expect(name instanceof NameAddrHeader).toBeTruthy(); expect(name.display_name).toBe( 'Iñaki ðđøþ foo "bar" \\\\ \\ \\d \\\\d \\\' \\"sdf\\"' ); @@ -64,7 +68,7 @@ describe('parser', () => { const uri = name.uri; - expect(uri instanceof JsSIP.URI).toBeTruthy(); + expect(uri instanceof URI).toBeTruthy(); expect(uri.scheme).toBe('sip'); expect(uri.user).toBe('aliCE'); expect(uri.host).toBe('versatica.com'); @@ -94,15 +98,15 @@ describe('parser', () => { test('parse invalid NameAddr with non UTF-8 characters', () => { const buffer = Buffer.from([0xc0]); const data = `"${buffer.toString()}"`; - const name = JsSIP.NameAddrHeader.parse(data); + const name = NameAddrHeader.parse(data); // Parsed data. - expect(name instanceof JsSIP.NameAddrHeader).toBeTruthy(); + expect(name instanceof NameAddrHeader).toBeTruthy(); expect(name.display_name).toBe(buffer.toString()); const uri = name.uri; - expect(uri instanceof JsSIP.URI).toBeTruthy(); + expect(uri instanceof URI).toBeTruthy(); expect(uri.scheme).toBe('sip'); expect(uri.user).toBe('foo'); expect(uri.host).toBe('bar.com'); @@ -112,37 +116,37 @@ describe('parser', () => { test('parse NameAddr with token display_name', () => { const data = 'Foo Foo Bar\tBaz;QWE=QWE;ASd'; - const name = JsSIP.NameAddrHeader.parse(data); + const name = NameAddrHeader.parse(data); // Parsed data. - expect(name instanceof JsSIP.NameAddrHeader).toBeTruthy(); + expect(name instanceof NameAddrHeader).toBeTruthy(); expect(name.display_name).toBe('Foo Foo Bar Baz'); }); test('parse NameAddr with no space between DQUOTE and LAQUOT', () => { const data = '"Foo";QWE=QWE;ASd'; - const name = JsSIP.NameAddrHeader.parse(data); + const name = NameAddrHeader.parse(data); // Parsed data. - expect(name instanceof JsSIP.NameAddrHeader).toBeTruthy(); + expect(name instanceof NameAddrHeader).toBeTruthy(); expect(name.display_name).toBe('Foo'); }); test('parse NameAddr with no display_name', () => { const data = ';QWE=QWE;ASd'; - const name = JsSIP.NameAddrHeader.parse(data); + const name = NameAddrHeader.parse(data); // Parsed data. - expect(name instanceof JsSIP.NameAddrHeader).toBeTruthy(); + expect(name instanceof NameAddrHeader).toBeTruthy(); expect(name.display_name).toBe(undefined); }); test('parse multiple Contact', () => { const data = '"Iñaki @ł€" ;+sip.Instance="abCD", sip:bob@biloxi.COM;headerParam, '; - const contacts = JsSIP.Grammar.parse(data, 'Contact'); + const contacts = Grammar.parse(data, 'Contact'); expect(contacts instanceof Array).toBeTruthy(); expect(contacts.length).toBe(3); @@ -151,13 +155,13 @@ describe('parser', () => { const c3 = contacts[2].parsed; // Parsed data. - expect(c1 instanceof JsSIP.NameAddrHeader).toBeTruthy(); + expect(c1 instanceof NameAddrHeader).toBeTruthy(); expect(c1.display_name).toBe('Iñaki @ł€'); expect(c1.hasParam('+sip.instance')).toBe(true); expect(c1.hasParam('nooo')).toBe(false); expect(c1.getParam('+SIP.instance')).toBe('"abCD"'); expect(c1.getParam('nooo')).toBe(undefined); - expect(c1.uri instanceof JsSIP.URI).toBeTruthy(); + expect(c1.uri instanceof URI).toBeTruthy(); expect(c1.uri.scheme).toBe('sip'); expect(c1.uri.user).toBe('+1234'); expect(c1.uri.host).toBe('aliax.net'); @@ -184,10 +188,10 @@ describe('parser', () => { ); // Parsed data. - expect(c2 instanceof JsSIP.NameAddrHeader).toBeTruthy(); + expect(c2 instanceof NameAddrHeader).toBeTruthy(); expect(c2.display_name).toBe(undefined); expect(c2.hasParam('HEADERPARAM')).toBe(true); - expect(c2.uri instanceof JsSIP.URI).toBeTruthy(); + expect(c2.uri instanceof URI).toBeTruthy(); expect(c2.uri.scheme).toBe('sip'); expect(c2.uri.user).toBe('bob'); expect(c2.uri.host).toBe('biloxi.com'); @@ -200,9 +204,9 @@ describe('parser', () => { expect(c2.toString()).toBe('"@ł€ĸłæß" ;headerparam'); // Parsed data. - expect(c3 instanceof JsSIP.NameAddrHeader).toBeTruthy(); + expect(c3 instanceof NameAddrHeader).toBeTruthy(); expect(c3.displayName).toBe(undefined); - expect(c3.uri instanceof JsSIP.URI).toBeTruthy(); + expect(c3.uri instanceof URI).toBeTruthy(); expect(c3.uri.scheme).toBe('sip'); expect(c3.uri.user).toBe(undefined); expect(c3.uri.host).toBe('domain.com'); @@ -221,7 +225,7 @@ describe('parser', () => { test('parse Via', () => { let data = 'SIP / 3.0 \r\n / UDP [1:ab::FF]:6060 ;\r\n BRanch=1234;Param1=Foo;paRAM2;param3=Bar'; - let via = JsSIP.Grammar.parse(data, 'Via'); + let via = Grammar.parse(data, 'Via'); expect(via.protocol).toBe('SIP'); expect(via.transport).toBe('UDP'); @@ -237,7 +241,7 @@ describe('parser', () => { data = 'SIP / 3.0 \r\n / UDP [1:ab::FF]:6060 ;\r\n BRanch=1234;rport=1111;Param1=Foo;paRAM2;param3=Bar'; - via = JsSIP.Grammar.parse(data, 'Via'); + via = Grammar.parse(data, 'Via'); expect(via.protocol).toBe('SIP'); expect(via.transport).toBe('UDP'); @@ -254,7 +258,7 @@ describe('parser', () => { data = 'SIP / 3.0 \r\n / UDP [1:ab::FF]:6060 ;\r\n BRanch=1234;rport;Param1=Foo;paRAM2;param3=Bar'; - via = JsSIP.Grammar.parse(data, 'Via'); + via = Grammar.parse(data, 'Via'); expect(via.protocol).toBe('SIP'); expect(via.transport).toBe('UDP'); @@ -272,7 +276,7 @@ describe('parser', () => { test('parse CSeq', () => { const data = '123456 CHICKEN'; - const cseq = JsSIP.Grammar.parse(data, 'CSeq'); + const cseq = Grammar.parse(data, 'CSeq'); expect(cseq.value).toBe(123456); expect(cseq.method).toBe('CHICKEN'); @@ -281,7 +285,7 @@ describe('parser', () => { test('parse authentication challenge', () => { const data = 'Digest realm = "[1:ABCD::abc]", nonce = "31d0a89ed7781ce6877de5cb032bf114", qop="AUTH,autH-INt", algorithm = md5 , stale = TRUE , opaque = "00000188"'; - const auth = JsSIP.Grammar.parse(data, 'challenge'); + const auth = Grammar.parse(data, 'challenge'); expect(auth.realm).toBe('[1:ABCD::abc]'); expect(auth.nonce).toBe('31d0a89ed7781ce6877de5cb032bf114'); @@ -293,7 +297,7 @@ describe('parser', () => { test('parse Event', () => { const data = 'Presence;Param1=QWe;paraM2'; - const event = JsSIP.Grammar.parse(data, 'Event'); + const event = Grammar.parse(data, 'Event'); expect(event.event).toBe('presence'); expect(event.params).toEqual({ param1: 'QWe', param2: undefined }); @@ -303,13 +307,13 @@ describe('parser', () => { let data, session_expires; data = '180;refresher=uac'; - session_expires = JsSIP.Grammar.parse(data, 'Session_Expires'); + session_expires = Grammar.parse(data, 'Session_Expires'); expect(session_expires.expires).toBe(180); expect(session_expires.refresher).toBe('uac'); data = '210 ; refresher = UAS ; foo = bar'; - session_expires = JsSIP.Grammar.parse(data, 'Session_Expires'); + session_expires = Grammar.parse(data, 'Session_Expires'); expect(session_expires.expires).toBe(210); expect(session_expires.refresher).toBe('uas'); @@ -319,14 +323,14 @@ describe('parser', () => { let data, reason; data = 'SIP ; cause = 488 ; text = "Wrong SDP"'; - reason = JsSIP.Grammar.parse(data, 'Reason'); + reason = Grammar.parse(data, 'Reason'); expect(reason.protocol).toBe('sip'); expect(reason.cause).toBe(488); expect(reason.text).toBe('Wrong SDP'); data = 'ISUP; cause=500 ; LALA = foo'; - reason = JsSIP.Grammar.parse(data, 'Reason'); + reason = Grammar.parse(data, 'Reason'); expect(reason.protocol).toBe('isup'); expect(reason.cause).toBe(500); @@ -338,33 +342,33 @@ describe('parser', () => { let data, parsed; data = 'versatica.com'; - expect((parsed = JsSIP.Grammar.parse(data, 'host'))).not.toBe(-1); + expect((parsed = Grammar.parse(data, 'host'))).not.toBe(-1); expect(parsed.host_type).toBe('domain'); data = 'myhost123'; - expect((parsed = JsSIP.Grammar.parse(data, 'host'))).not.toBe(-1); + expect((parsed = Grammar.parse(data, 'host'))).not.toBe(-1); expect(parsed.host_type).toBe('domain'); data = '1.2.3.4'; - expect((parsed = JsSIP.Grammar.parse(data, 'host'))).not.toBe(-1); + expect((parsed = Grammar.parse(data, 'host'))).not.toBe(-1); expect(parsed.host_type).toBe('IPv4'); data = '[1:0:fF::432]'; - expect((parsed = JsSIP.Grammar.parse(data, 'host'))).not.toBe(-1); + expect((parsed = Grammar.parse(data, 'host'))).not.toBe(-1); expect(parsed.host_type).toBe('IPv6'); data = '1.2.3.444'; - expect((parsed = JsSIP.Grammar.parse(data, 'host'))).toBe(-1); + expect((parsed = Grammar.parse(data, 'host'))).toBe(-1); data = 'iñaki.com'; - expect((parsed = JsSIP.Grammar.parse(data, 'host'))).toBe(-1); + expect((parsed = Grammar.parse(data, 'host'))).toBe(-1); data = '1.2.3.bar.qwe-asd.foo'; - expect((parsed = JsSIP.Grammar.parse(data, 'host'))).not.toBe(-1); + expect((parsed = Grammar.parse(data, 'host'))).not.toBe(-1); expect(parsed.host_type).toBe('domain'); data = '1.2.3.4.bar.qwe-asd.foo'; - expect((parsed = JsSIP.Grammar.parse(data, 'host'))).not.toBe(-1); + expect((parsed = Grammar.parse(data, 'host'))).not.toBe(-1); expect(parsed.host_type).toBe('domain'); }); @@ -372,13 +376,13 @@ describe('parser', () => { let data, parsed; data = 'sip:alice@versatica.com'; - expect((parsed = JsSIP.Grammar.parse(data, 'Refer_To'))).not.toBe(-1); + expect((parsed = Grammar.parse(data, 'Refer_To'))).not.toBe(-1); expect(parsed.uri.scheme).toBe('sip'); expect(parsed.uri.user).toBe('alice'); expect(parsed.uri.host).toBe('versatica.com'); data = ''; - expect((parsed = JsSIP.Grammar.parse(data, 'Refer_To'))).not.toBe(-1); + expect((parsed = Grammar.parse(data, 'Refer_To'))).not.toBe(-1); expect(parsed.uri.scheme).toBe('sip'); expect(parsed.uri.user).toBe('bob'); expect(parsed.uri.host).toBe('versatica.com'); @@ -390,7 +394,7 @@ describe('parser', () => { const data = '5t2gpbrbi72v79p1i8mr;to-tag=03aq91cl9n;from-tag=kun98clbf7'; - expect((parsed = JsSIP.Grammar.parse(data, 'Replaces'))).not.toBe(-1); + expect((parsed = Grammar.parse(data, 'Replaces'))).not.toBe(-1); expect(parsed.call_id).toBe('5t2gpbrbi72v79p1i8mr'); expect(parsed.to_tag).toBe('03aq91cl9n'); expect(parsed.from_tag).toBe('kun98clbf7'); @@ -400,7 +404,7 @@ describe('parser', () => { const data = 'SIP/2.0 420 Bad Extension'; let parsed; - expect((parsed = JsSIP.Grammar.parse(data, 'Status_Line'))).not.toBe(-1); + expect((parsed = Grammar.parse(data, 'Status_Line'))).not.toBe(-1); expect(parsed.status_code).toBe(420); }); @@ -418,23 +422,21 @@ Privacy: id\r\n\ P-Preferred-Identity: "Cullen Jennings" \r\n\r\n'; const config = testUA.UA_CONFIGURATION; - const wsSocket = new JsSIP.WebSocketInterface( - testUA.SOCKET_DESCRIPTION.url - ); + const wsSocket = new WebSocketInterface(testUA.SOCKET_DESCRIPTION['url']); - config.sockets = wsSocket; + config['sockets'] = wsSocket; - const ua = new JsSIP.UA(config); + const ua = new UA(config as ConstructorParameters[0]); const message = Parser.parseMessage(data, ua); expect(message.hasHeader('P-Preferred-Identity')).toBe(true); const pai = message.getHeader('P-Preferred-Identity'); - const nameAddress = JsSIP.NameAddrHeader.parse(pai); + const nameAddress = NameAddrHeader.parse(pai); - expect(nameAddress instanceof JsSIP.NameAddrHeader).toBeTruthy(); - expect(nameAddress.uri.user).toBe('fluffy'); - expect(nameAddress.uri.host).toBe('cisco.com'); + expect(nameAddress instanceof NameAddrHeader).toBeTruthy(); + expect(nameAddress!.uri.user).toBe('fluffy'); + expect(nameAddress!.uri.host).toBe('cisco.com'); expect(message.hasHeader('Privacy')).toBe(true); expect(message.getHeader('Privacy')).toBe('id'); diff --git a/src/test/test-properties.js b/src/test/test-properties.ts similarity index 57% rename from src/test/test-properties.js rename to src/test/test-properties.ts index 0d432ea3..3231862d 100644 --- a/src/test/test-properties.js +++ b/src/test/test-properties.ts @@ -1,5 +1,8 @@ -require('./include/common'); -const JsSIP = require('../..'); +import './include/common'; + +// eslint-disable-next-line @typescript-eslint/no-require-imports +const JsSIP = require('../JsSIP.js'); +// eslint-disable-next-line @typescript-eslint/no-require-imports const pkg = require('../../package.json'); describe('Properties', () => { From 01a76ddbe88c05d025b5c925fa7503f3f43d77a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Luis=20Mill=C3=A1n?= Date: Tue, 27 Jan 2026 14:38:53 +0100 Subject: [PATCH 2/8] jest.config.mjs --- jest.config.js => jest.config.mjs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) rename jest.config.js => jest.config.mjs (76%) diff --git a/jest.config.js b/jest.config.mjs similarity index 76% rename from jest.config.js rename to jest.config.mjs index a85f45b2..59bb1960 100644 --- a/jest.config.js +++ b/jest.config.mjs @@ -1,4 +1,5 @@ -module.exports = { +const config = { + verbose: true, preset: 'ts-jest', testEnvironment: 'node', testRegex: 'src/test/test-.*\\.ts', @@ -11,3 +12,5 @@ module.exports = { ], }, }; + +export default config; From 6db0250d0ca18ce5bd4efc3fbfcc0479b9038fdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Luis=20Mill=C3=A1n?= Date: Tue, 27 Jan 2026 14:44:59 +0100 Subject: [PATCH 3/8] test coverage --- .gitignore | 6 ++++++ jest.config.mjs | 2 ++ npm-scripts.mjs | 20 +++++++++++++++++++- package.json | 2 ++ 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 216bf0eb..8d4f7a5c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,9 @@ +## Node. /node_modules /lib /dist + +## Others. +/docs +/coverage +/.cache diff --git a/jest.config.mjs b/jest.config.mjs index 59bb1960..0a8d78b0 100644 --- a/jest.config.mjs +++ b/jest.config.mjs @@ -11,6 +11,8 @@ const config = { }, ], }, + coveragePathIgnorePatterns: ['src/test'], + cacheDirectory: '.cache/jest', }; export default config; diff --git a/npm-scripts.mjs b/npm-scripts.mjs index 0fe9a02a..c47c08d1 100644 --- a/npm-scripts.mjs +++ b/npm-scripts.mjs @@ -30,21 +30,31 @@ async function run() { switch (task) { case 'grammar': { grammar(); + break; } case 'lint': { lint(); + break; } case 'lint:fix': { lint(true); + break; } case 'test': { test(); + + break; + } + + case 'coverage': { + coverage(); + break; } @@ -72,6 +82,7 @@ async function run() { // eslint-disable-next-line no-console console.log('update tryit-jssip and JsSIP website'); + break; } @@ -92,7 +103,14 @@ function lint(fix = false) { function test() { logInfo('test()'); - executeCmd('jest'); + executeCmd(`jest --silent false --detectOpenHandles ${taskArgs}`); +} + +function coverage() { + logInfo('coverage()'); + + executeCmd(`jest --coverage ${taskArgs}`); + executeCmd('open-cli coverage/lcov-report/index.html'); } function grammar() { diff --git a/package.json b/package.json index 8207ba6f..d8909bd2 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "lint": "node npm-scripts.mjs lint", "lint:fix": "node npm-scripts.mjs lint:fix", "test": "node npm-scripts.mjs test", + "coverage": "node npm-scripts.mjs coverage", "build": "node npm-scripts.mjs build", "typescript:build": "node npm-scripts.mjs typescript:build", "release": "node npm-scripts.js release" @@ -60,6 +61,7 @@ "eslint-plugin-prettier": "^5.5.5", "globals": "^17.0.0", "jest": "^30.2.0", + "open-cli": "^8.0.0", "pegjs": "^0.7.0", "prettier": "^3.8.1", "ts-jest": "^29.4.6", From 85e5927cf57b9eb310b93c4788600d6de0ef4227 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Luis=20Mill=C3=A1n?= Date: Tue, 27 Jan 2026 15:23:40 +0100 Subject: [PATCH 4/8] rename subscriber notifier test name --- ...test-UA-subscriber-notifier.ts => test-subscriber-notifier.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/test/{test-UA-subscriber-notifier.ts => test-subscriber-notifier.rs} (100%) diff --git a/src/test/test-UA-subscriber-notifier.ts b/src/test/test-subscriber-notifier.rs similarity index 100% rename from src/test/test-UA-subscriber-notifier.ts rename to src/test/test-subscriber-notifier.rs From bce41855028e3c0ba84e72810d932ab7922fa04f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Luis=20Mill=C3=A1n?= Date: Tue, 27 Jan 2026 15:26:37 +0100 Subject: [PATCH 5/8] rename test file --- src/test/include/{testUA.ts => consts.ts} | 0 src/test/test-UA-no-WebRTC.ts | 16 ++++++++-------- src/test/test-parser.ts | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) rename src/test/include/{testUA.ts => consts.ts} (100%) diff --git a/src/test/include/testUA.ts b/src/test/include/consts.ts similarity index 100% rename from src/test/include/testUA.ts rename to src/test/include/consts.ts diff --git a/src/test/test-UA-no-WebRTC.ts b/src/test/test-UA-no-WebRTC.ts index a6a063a0..a35f4792 100644 --- a/src/test/test-UA-no-WebRTC.ts +++ b/src/test/test-UA-no-WebRTC.ts @@ -1,7 +1,7 @@ /* eslint no-console: 0*/ import './include/common'; -import * as testUA from './include/testUA'; +import * as consts from './include/consts'; // eslint-disable-next-line @typescript-eslint/no-require-imports const JsSIP = require('../JsSIP.js'); @@ -15,8 +15,8 @@ describe('UA No WebRTC', () => { }); test('UA no WS connection', () => { - const config = testUA.UA_CONFIGURATION; - const wsSocket = new WebSocketInterface(testUA.SOCKET_DESCRIPTION['url']); + const config = consts.UA_CONFIGURATION; + const wsSocket = new WebSocketInterface(consts.SOCKET_DESCRIPTION['url']); config['sockets'] = wsSocket; @@ -44,10 +44,10 @@ describe('UA No WebRTC', () => { '' ); - for (const parameter in testUA.UA_CONFIGURATION_AFTER_START) { + for (const parameter in consts.UA_CONFIGURATION_AFTER_START) { if ( Object.prototype.hasOwnProperty.call( - testUA.UA_CONFIGURATION_AFTER_START, + consts.UA_CONFIGURATION_AFTER_START, parameter ) ) { @@ -56,7 +56,7 @@ describe('UA No WebRTC', () => { case 'registrar_server': { // eslint-disable-next-line jest/no-conditional-expect expect(ua.configuration[parameter].toString()).toBe( - testUA.UA_CONFIGURATION_AFTER_START[parameter] + consts.UA_CONFIGURATION_AFTER_START[parameter] ); break; } @@ -67,14 +67,14 @@ describe('UA No WebRTC', () => { default: { // eslint-disable-next-line jest/no-conditional-expect expect(ua.configuration[parameter]).toBe( - testUA.UA_CONFIGURATION_AFTER_START[parameter] + consts.UA_CONFIGURATION_AFTER_START[parameter] ); } } } } - const transport = testUA.UA_TRANSPORT_AFTER_START; + const transport = consts.UA_TRANSPORT_AFTER_START; const sockets = transport['sockets']; const socket = sockets[0].socket; diff --git a/src/test/test-parser.ts b/src/test/test-parser.ts index 3ed3122c..eee4e5c2 100644 --- a/src/test/test-parser.ts +++ b/src/test/test-parser.ts @@ -1,5 +1,5 @@ import './include/common'; -import * as testUA from './include/testUA'; +import * as consts from './include/consts'; // eslint-disable-next-line @typescript-eslint/no-require-imports const JsSIP = require('../JsSIP.js'); @@ -421,8 +421,8 @@ Max-Forwards: 70\r\n\ Privacy: id\r\n\ P-Preferred-Identity: "Cullen Jennings" \r\n\r\n'; - const config = testUA.UA_CONFIGURATION; - const wsSocket = new WebSocketInterface(testUA.SOCKET_DESCRIPTION['url']); + const config = consts.UA_CONFIGURATION; + const wsSocket = new WebSocketInterface(consts.SOCKET_DESCRIPTION['url']); config['sockets'] = wsSocket; From acf77d88a6320271b2d57d526999ebe731d804dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Luis=20Mill=C3=A1n?= Date: Tue, 27 Jan 2026 15:28:29 +0100 Subject: [PATCH 6/8] rename test file --- src/test/include/{loopSocket.ts => LoopSocket.ts} | 14 +++++++------- ...ber-notifier.rs => test-subscriber-notifier.ts} | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) rename src/test/include/{loopSocket.ts => LoopSocket.ts} (71%) rename src/test/{test-subscriber-notifier.rs => test-subscriber-notifier.ts} (99%) diff --git a/src/test/include/loopSocket.ts b/src/test/include/LoopSocket.ts similarity index 71% rename from src/test/include/loopSocket.ts rename to src/test/include/LoopSocket.ts index 9750b32d..0bc91210 100644 --- a/src/test/include/loopSocket.ts +++ b/src/test/include/LoopSocket.ts @@ -17,10 +17,10 @@ export default class LoopSocket implements Socket { disconnect(): void {} send(message: string): boolean { - const message2 = this._modifyCallId(message); + const new_message = this.modifyCallId(message); setTimeout(() => { - this.ondata(message2); + this.ondata(new_message); }, 0); return true; @@ -42,10 +42,10 @@ export default class LoopSocket implements Socket { ondata(_event: T): void {} // Call-ID: add or drop word '_second'. - private _modifyCallId(message: string): string { - const ixBegin = message.indexOf('Call-ID'); - const ixEnd = message.indexOf('\r', ixBegin); - let callId = message.substring(ixBegin + 9, ixEnd); + private modifyCallId(message: string): string { + const begin = message.indexOf('Call-ID'); + const end = message.indexOf('\r', begin); + let callId = message.substring(begin + 9, end); if (callId.endsWith('_second')) { callId = callId.substring(0, callId.length - 7); @@ -53,6 +53,6 @@ export default class LoopSocket implements Socket { callId += '_second'; } - return `${message.substring(0, ixBegin)}Call-ID: ${callId}${message.substring(ixEnd)}`; + return `${message.substring(0, begin)}Call-ID: ${callId}${message.substring(end)}`; } } diff --git a/src/test/test-subscriber-notifier.rs b/src/test/test-subscriber-notifier.ts similarity index 99% rename from src/test/test-subscriber-notifier.rs rename to src/test/test-subscriber-notifier.ts index 3647cf84..f0759a6f 100644 --- a/src/test/test-subscriber-notifier.rs +++ b/src/test/test-subscriber-notifier.ts @@ -1,5 +1,5 @@ import './include/common'; -import LoopSocket from './include/loopSocket'; +import LoopSocket from './include/LoopSocket'; // eslint-disable-next-line @typescript-eslint/no-require-imports const JsSIP = require('../JsSIP.js'); From cf46e363bac6d15881a3799a4c7b18951bfc0aca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Luis=20Mill=C3=A1n?= Date: Tue, 27 Jan 2026 15:35:38 +0100 Subject: [PATCH 7/8] CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a29ac420..20c68283 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Modernize eslint. - Use prettier. - Prepare environment for TS. +- Rewrite tests to TS (#958). ### 3.12.0 From 1d91c4edb5023ffdef9c9d612d75afbf4d9aecb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Luis=20Mill=C3=A1n?= Date: Tue, 27 Jan 2026 16:00:20 +0100 Subject: [PATCH 8/8] Rework test --- src/test/test-subscriber-notifier.ts | 50 +++++++++++++++++++--------- 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/src/test/test-subscriber-notifier.ts b/src/test/test-subscriber-notifier.ts index f0759a6f..b96d8fda 100644 --- a/src/test/test-subscriber-notifier.ts +++ b/src/test/test-subscriber-notifier.ts @@ -5,10 +5,25 @@ import LoopSocket from './include/LoopSocket'; const JsSIP = require('../JsSIP.js'); const { UA } = JsSIP; +const enum STEP { + INIT = 0, + SOCKET_CONNECTED = 1, + SUBSCRIBE_SENT = 2, + UA_ON_NEWSUBSCRIBE = 3, + NOTIFIER_ON_SUBSCRIBE = 4, + SUBSCRIBER_ON_ACCEPTED = 5, + SUBSCRIBER_ON_ACTIVE = 6, + SUBSCRIBER_ON_NOTIFY_1 = 7, + NOTIFIER_ON_UNSUBSCRIBE = 8, + NOTIFIER_TERMINATED = 9, + SUBSCRIBER_ON_NOTIFY_2 = 10, + SUBSCRIBER_TERMINATED = 11, +} + describe('subscriber/notifier communication', () => { test('should handle subscriber/notifier communication', () => new Promise(resolve => { - let eventSequence = 0; + let step = STEP.INIT; const TARGET = 'ikq'; const REQUEST_URI = 'sip:ikq@example.com'; @@ -35,7 +50,7 @@ describe('subscriber/notifier communication', () => { subscriber.on('active', () => { // 'receive notify with subscription-state: active' - expect(++eventSequence).toBe(6); + expect(++step).toBe(STEP.SUBSCRIBER_ON_ACTIVE); }); subscriber.on( @@ -50,9 +65,12 @@ describe('subscriber/notifier communication', () => { body?: string, contType?: string ) => { - eventSequence++; + step++; // 'receive notify' - expect(eventSequence === 7 || eventSequence === 11).toBe(true); + expect( + step === STEP.SUBSCRIBER_ON_NOTIFY_1 || + step === STEP.SUBSCRIBER_ON_NOTIFY_2 + ).toBe(true); expect(notify.method).toBe('NOTIFY'); expect(notify.getHeader('contact')).toBe(`<${CONTACT_URI}>`); // 'notify contact' @@ -68,9 +86,7 @@ describe('subscriber/notifier communication', () => { ).toBe(true); // 'notify subscription-state' // After receiving the first notify, send un-subscribe. - if (eventSequence === 7) { - ++eventSequence; // 'send un-subscribe' - + if (step === STEP.SUBSCRIBER_ON_NOTIFY_1) { subscriber.terminate(WEATHER_REQUEST); } } @@ -83,7 +99,7 @@ describe('subscriber/notifier communication', () => { reason: string | undefined, retryAfter: number | undefined ) => { - expect(++eventSequence).toBe(12); // 'subscriber terminated' + expect(++step).toBe(STEP.SUBSCRIBER_TERMINATED); expect(terminationCode).toBe(subscriber.C.FINAL_NOTIFY_RECEIVED); expect(reason).toBeUndefined(); expect(retryAfter).toBeUndefined(); @@ -93,12 +109,12 @@ describe('subscriber/notifier communication', () => { ); subscriber.on('accepted', () => { - expect(++eventSequence).toBe(5); // 'initial subscribe accepted' + expect(++step).toBe(STEP.SUBSCRIBER_ON_ACCEPTED); }); - expect(++eventSequence).toBe(2); // 'send subscribe' - subscriber.subscribe(WEATHER_REQUEST); + + expect(++step).toBe(STEP.SUBSCRIBE_SENT); } // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -120,7 +136,11 @@ describe('subscriber/notifier communication', () => { expect(body).toBe(WEATHER_REQUEST); // 'subscribe body' expect(contType).toBe(CONTENT_TYPE); // 'subscribe content-type' - expect(++eventSequence).toBe(isUnsubscribe ? 9 : 4); + expect(++step).toBe( + isUnsubscribe + ? STEP.NOTIFIER_ON_UNSUBSCRIBE + : STEP.NOTIFIER_ON_SUBSCRIBE + ); if (isUnsubscribe) { // 'send final notify' notifier.terminate(WEATHER_REPORT); @@ -137,7 +157,7 @@ describe('subscriber/notifier communication', () => { }); notifier.on('terminated', () => { - expect(++eventSequence).toBe(10); // 'notifier terminated' + expect(++step).toBe(STEP.NOTIFIER_TERMINATED); }); notifier.start(); @@ -158,7 +178,7 @@ describe('subscriber/notifier communication', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any ua.on('newSubscribe', (e: any) => { - expect(++eventSequence).toBe(3); // 'receive initial subscribe' + expect(++step).toBe(STEP.UA_ON_NEWSUBSCRIBE); const subs = e.request; const ev = subs.parseHeader('event'); @@ -187,7 +207,7 @@ describe('subscriber/notifier communication', () => { }); ua.on('connected', () => { - expect(++eventSequence).toBe(1); // 'socket connected' + expect(++step).toBe(STEP.SOCKET_CONNECTED); createSubscriber(ua); });