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/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 diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index ac9471a7..00000000 --- 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 00000000..0a8d78b0 --- /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 1a7fcfc0..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,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 603ef16b..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" @@ -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 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/LoopSocket.ts b/src/test/include/LoopSocket.ts new file mode 100644 index 00000000..0bc91210 --- /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(_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 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/consts.ts b/src/test/include/consts.ts new file mode 100644 index 00000000..5e3a138a --- /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 = { + 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/include/loopSocket.js b/src/test/include/loopSocket.js deleted file mode 100644 index c2a68dcd..00000000 --- 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 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/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 8c7c88d0..a35f4792 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', () => { '' ); - 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 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-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..eee4e5c2 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\\\\\\"" ' + ';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); }); @@ -417,24 +421,22 @@ 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 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[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', () => { diff --git a/src/test/test-subscriber-notifier.ts b/src/test/test-subscriber-notifier.ts new file mode 100644 index 00000000..b96d8fda --- /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(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(); + })); +});