Commit 2f44b02 for jssip.net
commit 2f44b02d57ec497b7bab38c3412cf845f819369c
Author: José Luis Millán <jmillan@aliax.net>
Date: Tue Jan 27 16:02:26 2026 +0100
Rewrite tests to TypeScript (#958)
diff --git a/.gitignore b/.gitignore
index 216bf0e..8d4f7a5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,9 @@
+## Node.
/node_modules
/lib
/dist
+
+## Others.
+/docs
+/coverage
+/.cache
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a29ac42..20c6828 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
diff --git a/jest.config.js b/jest.config.js
deleted file mode 100644
index ac9471a..0000000
--- a/jest.config.js
+++ /dev/null
@@ -1,3 +0,0 @@
-module.exports = {
- testRegex: 'src/test/test-.*\\.js',
-};
diff --git a/jest.config.mjs b/jest.config.mjs
new file mode 100644
index 0000000..0a8d78b
--- /dev/null
+++ b/jest.config.mjs
@@ -0,0 +1,18 @@
+const config = {
+ verbose: true,
+ preset: 'ts-jest',
+ testEnvironment: 'node',
+ testRegex: 'src/test/test-.*\\.ts',
+ transform: {
+ '^.+\\.ts$': [
+ 'ts-jest',
+ {
+ tsconfig: 'tsconfig.json',
+ },
+ ],
+ },
+ coveragePathIgnorePatterns: ['src/test'],
+ cacheDirectory: '.cache/jest',
+};
+
+export default config;
diff --git a/npm-scripts.mjs b/npm-scripts.mjs
index 1a7fcfc..c47c08d 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,9 +103,14 @@ function lint(fix = false) {
function test() {
logInfo('test()');
- // TODO: remove when tests are written in TS.
- buildTypescript();
- 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 603ef16..d8909bd 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"
@@ -50,6 +51,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",
@@ -59,8 +61,10 @@
"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",
"typescript": "^5.9.3",
"typescript-eslint": "^8.53.1"
}
diff --git a/src/NameAddrHeader.d.ts b/src/NameAddrHeader.d.ts
index 59e15cb..c897ae4 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<T = any>(key: string): T;
diff --git a/src/URI.d.ts b/src/URI.d.ts
index 05fcd0d..eb1502c 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<T = unknown>(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/LoopSocket.ts b/src/test/include/LoopSocket.ts
new file mode 100644
index 0000000..0bc9121
--- /dev/null
+++ b/src/test/include/LoopSocket.ts
@@ -0,0 +1,58 @@
+// LoopSocket send message itself.
+// Used P2P logic: message call-id is modified in each leg.
+
+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(): void {}
+
+ send(message: string): boolean {
+ const new_message = this.modifyCallId(message);
+
+ setTimeout(() => {
+ this.ondata(new_message);
+ }, 0);
+
+ return true;
+ }
+
+ isConnected(): boolean {
+ return true;
+ }
+
+ isConnecting(): boolean {
+ return false;
+ }
+
+ onconnect(): void {}
+
+ ondisconnect(): void {}
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ ondata<T>(_event: T): void {}
+
+ // Call-ID: add or drop word '_second'.
+ 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);
+ } else {
+ callId += '_second';
+ }
+
+ return `${message.substring(0, begin)}Call-ID: ${callId}${message.substring(end)}`;
+ }
+}
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 85a9094..2d57b49 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<string, unknown>)['WebSocket'] = function (this: {
+ close: () => void;
+}) {
this.close = function () {};
};
// Define global.navigator for bowser module.
-global.navigator = {
+(globalThis as Record<string, unknown>)['navigator'] = {
userAgent: '',
};
diff --git a/src/test/include/consts.ts b/src/test/include/consts.ts
new file mode 100644
index 0000000..5e3a138
--- /dev/null
+++ b/src/test/include/consts.ts
@@ -0,0 +1,56 @@
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export const SOCKET_DESCRIPTION: Record<string, any> = {
+ 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<string, any> = {
+ 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<string, any> = {
+ 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<string, any> = {
+ 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/loopSocket.js b/src/test/include/loopSocket.js
deleted file mode 100644
index c2a68dc..0000000
--- a/src/test/include/loopSocket.js
+++ /dev/null
@@ -1,42 +0,0 @@
-// 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() {
- setTimeout(() => {
- this.onconnect();
- }, 0);
- }
-
- disconnect() {}
-
- send(message) {
- const message2 = this._modifyCallId(message);
-
- setTimeout(() => {
- this.ondata(message2);
- }, 0);
-
- return true;
- }
-
- // Call-ID: add or drop word '_second'.
- _modifyCallId(message) {
- const ixBegin = message.indexOf('Call-ID');
- const ixEnd = message.indexOf('\r', ixBegin);
- let callId = message.substring(ixBegin + 9, ixEnd);
-
- if (callId.endsWith('_second')) {
- callId = callId.substring(0, callId.length - 7);
- } else {
- callId += '_second';
- }
-
- 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 2e426a5..0000000
--- 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/test-UA-no-WebRTC.js b/src/test/test-UA-no-WebRTC.ts
similarity index 60%
rename from src/test/test-UA-no-WebRTC.js
rename to src/test/test-UA-no-WebRTC.ts
index 8c7c88d..a35f479 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 consts from './include/consts';
+
+// 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 config = consts.UA_CONFIGURATION;
+ const wsSocket = new WebSocketInterface(consts.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();
@@ -43,19 +44,19 @@ describe('UA No WebRTC', () => {
'<sip:anonymous@anonymous.invalid;transport=ws;ob>'
);
- 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
)
) {
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}`
+ consts.UA_CONFIGURATION_AFTER_START[parameter]
);
break;
}
@@ -64,17 +65,17 @@ 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}`
+ consts.UA_CONFIGURATION_AFTER_START[parameter]
);
}
}
}
}
- const transport = testUA.UA_TRANSPORT_AFTER_START;
- const sockets = transport.sockets;
+ const transport = consts.UA_TRANSPORT_AFTER_START;
+ 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 9c562d6..0000000
--- 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-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 9b72b32..a92f9c3 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 æßð" <sip:alice@jssip.net>');
@@ -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 2068420..28a980b 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 1fb874a..07c43fd 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 d6c6825..eee4e5c 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 consts from './include/consts';
+
+// 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\\\\\\"" ' +
'<SIP:%61liCE@versaTICA.Com:6060;TRansport=TCp;Foo=ABc;baz?X-Header-1=AaA1&X-Header-2=BbB&x-header-1=AAA2>;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()}"<sip:foo@bar.com>`;
- 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<SIP:%61liCE@versaTICA.Com:6060;TRansport=TCp;Foo=ABc;baz?X-Header-1=AaA1&X-Header-2=BbB&x-header-1=AAA2>;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"<SIP:%61liCE@versaTICA.Com:6060;TRansport=TCp;Foo=ABc;baz?X-Header-1=AaA1&X-Header-2=BbB&x-header-1=AAA2>;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 =
'<SIP:%61liCE@versaTICA.Com:6060;TRansport=TCp;Foo=ABc;baz?X-Header-1=AaA1&X-Header-2=BbB&x-header-1=AAA2>;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:+1234@ALIAX.net;Transport=WS>;+sip.Instance="abCD", sip:bob@biloxi.COM;headerParam, <sip:DOMAIN.com:5>';
- 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('"@ł€ĸłæß" <sip:bob@biloxi.com>;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 = '<sip:bob@versatica.com?Accept-Contact=sip:bobsdesk.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('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);
});
@@ -417,24 +421,22 @@ Max-Forwards: 70\r\n\
Privacy: id\r\n\
P-Preferred-Identity: "Cullen Jennings" <sip:fluffy@cisco.com>\r\n\r\n';
- const config = testUA.UA_CONFIGURATION;
- const wsSocket = new JsSIP.WebSocketInterface(
- testUA.SOCKET_DESCRIPTION.url
- );
+ const config = consts.UA_CONFIGURATION;
+ const wsSocket = new WebSocketInterface(consts.SOCKET_DESCRIPTION['url']);
- config.sockets = wsSocket;
+ config['sockets'] = wsSocket;
- const ua = new JsSIP.UA(config);
+ const ua = new UA(config as ConstructorParameters<typeof UA>[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 0d432ea..3231862 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', () => {
diff --git a/src/test/test-subscriber-notifier.ts b/src/test/test-subscriber-notifier.ts
new file mode 100644
index 0000000..b96d8fd
--- /dev/null
+++ b/src/test/test-subscriber-notifier.ts
@@ -0,0 +1,221 @@
+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;
+
+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<void>(resolve => {
+ let step = STEP.INIT;
+
+ 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(++step).toBe(STEP.SUBSCRIBER_ON_ACTIVE);
+ });
+
+ subscriber.on(
+ 'notify',
+ (
+ isFinal: boolean,
+ notify: {
+ method: string;
+ getHeader: (name: string) => string;
+ parseHeader: (name: string) => { state: string };
+ },
+ body?: string,
+ contType?: string
+ ) => {
+ step++;
+ // 'receive notify'
+ 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'
+ 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 (step === STEP.SUBSCRIBER_ON_NOTIFY_1) {
+ subscriber.terminate(WEATHER_REQUEST);
+ }
+ }
+ );
+
+ subscriber.on(
+ 'terminated',
+ (
+ terminationCode: number,
+ reason: string | undefined,
+ retryAfter: number | undefined
+ ) => {
+ expect(++step).toBe(STEP.SUBSCRIBER_TERMINATED);
+ expect(terminationCode).toBe(subscriber.C.FINAL_NOTIFY_RECEIVED);
+ expect(reason).toBeUndefined();
+ expect(retryAfter).toBeUndefined();
+
+ ua.stop();
+ }
+ );
+
+ subscriber.on('accepted', () => {
+ expect(++step).toBe(STEP.SUBSCRIBER_ON_ACCEPTED);
+ });
+
+ subscriber.subscribe(WEATHER_REQUEST);
+
+ expect(++step).toBe(STEP.SUBSCRIBE_SENT);
+ }
+
+ // 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(++step).toBe(
+ isUnsubscribe
+ ? STEP.NOTIFIER_ON_UNSUBSCRIBE
+ : STEP.NOTIFIER_ON_SUBSCRIBE
+ );
+ 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(++step).toBe(STEP.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(++step).toBe(STEP.UA_ON_NEWSUBSCRIBE);
+
+ 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(++step).toBe(STEP.SOCKET_CONNECTED);
+
+ createSubscriber(ua);
+ });
+
+ ua.on('disconnected', () => {
+ resolve();
+ });
+
+ ua.start();
+ }));
+});