/* tslint: disable */ import * as OTClient from '@opentok/client'; import { primary as sessionCredentials, faultyLogging as badLoggingCredentials, } from './credentials.json'; import { NetworkTestError, MissingOpenTokInstanceError, MissingSessionCredentialsError, IncompleteSessionCredentialsError, InvalidOnUpdateCallback, } from '../src/NetworkTest/errors'; import { ConnectivityError } from '../src/NetworkTest/testConnectivity/errors'; import NetworkTest, { ErrorNames } from '../src/NetworkTest'; import { ConnectivityTestResults } from '../src/NetworkTest/testConnectivity/index'; import { QualityTestError } from '../src/NetworkTest/testQuality/errors/index'; type CustomMatcher = jasmine.CustomMatcher; const malformedCredentials = { apiKey: '1234', invalidProp: '1234', token: '1234' }; const badCredentials = { apiKey: '1234', sessionId: '1234', token: '1234' }; const networkTest = new NetworkTest(OTClient, sessionCredentials); const networkTestWithOptions = new NetworkTest(OTClient, sessionCredentials, { audioOnly: true, timeout: 5000, }); const badCredentialsNetworkTest = new NetworkTest(OTClient, badCredentials); const validOnUpdateCallback = (stats: OT.SubscriberStats) => stats; const customMatchers: jasmine.CustomMatcherFactories = { toBeInstanceOf: (): CustomMatcher => { return { compare: (actual: any, expected: any): jasmine.CustomMatcherResult => { const pass: boolean = actual instanceof expected; const message: string = pass ? '' : `Expected ${actual} to be an instance of ${expected}`; return { pass, message }; }, }; }, toBeABoolean: (): CustomMatcher => { return { compare: (actual: any, expected: any): jasmine.CustomMatcherResult => { const pass: boolean = typeof actual === 'boolean'; const message: string = pass ? '' : `Expected ${actual} to be an instance of ${expected}`; return { pass, message }; }, }; }, }; describe('NetworkTest', () => { beforeAll(() => { jasmine.addMatchers(customMatchers); }); afterEach((done) => { if (networkTest) { networkTest.stop(); } // A bit of a hack. But this prevents tests from failing if a previous test's Session didn't disconnect: setTimeout(() => { done(); }, 1000); }); it('its constructor requires OT and valid session credentials', () => { expect(() => new NetworkTest(sessionCredentials)).toThrow(new MissingOpenTokInstanceError()); expect(() => new NetworkTest({}, sessionCredentials)).toThrow(new MissingOpenTokInstanceError()); expect(() => new NetworkTest(OTClient)).toThrow(new MissingSessionCredentialsError()); expect(() => new NetworkTest(OTClient, malformedCredentials)).toThrow(new IncompleteSessionCredentialsError()); expect(new NetworkTest(OTClient, sessionCredentials)).toBeInstanceOf(NetworkTest); }); it('it contains a valid ErrorNames module', () => { expect(ErrorNames.MISSING_OPENTOK_INSTANCE).toBe('MissingOpenTokInstanceError'); }); describe('Connectivity Test', () => { const testConnectFailure = (errorName, expectedType) => { return new Promise((resolve) => { const realInitSession = OT.initSession; spyOn(OT, 'initSession').and.callFake((apiKey, sessionId) => { const session = realInitSession(apiKey, sessionId); spyOn(session, 'connect').and.callFake((token, callback) => { const error = new Error(); error.name = errorName; callback(error); }); return session; }); const netTest = new NetworkTest(OT, sessionCredentials); netTest.testConnectivity() .then((results: ConnectivityTestResults) => { expect(results.failedTests).toBeInstanceOf(Array); if (results.failedTests.find(f => f.type === expectedType)) { resolve(); } }); }); }; describe('Test Results', () => { it('should contain success and failedTests properties', (done) => { networkTest.testConnectivity() .then((results: ConnectivityTestResults) => { expect(results.success).toBeABoolean; expect(results.failedTests).toBeInstanceOf(Array); done(); }); }, 10000); it('should return a failed test case if invalid session credentials are used', (done) => { const validateResults = (results: ConnectivityTestResults) => { expect(results.success).toBe(false); expect(results.failedTests).toBeInstanceOf(Array); const [initialFailure, secondaryFailure] = results.failedTests; expect(initialFailure.type).toBe('messaging'); expect(initialFailure.error.name).toBe(ErrorNames.CONNECT_TO_SESSION_TOKEN_ERROR); expect(secondaryFailure.type).toBe('media'); expect(secondaryFailure.error.name).toBe(ErrorNames.FAILED_MESSAGING_SERVER_TEST); }; const validateError = (error?: ConnectivityError) => { expect(error).toBeUndefined(); }; badCredentialsNetworkTest.testConnectivity() .then(validateResults) .catch(validateError) .finally(done); }); it('should result in a failed test if the logging server cannot be reached', (done) => { const badLoggingOT = { ...OTClient, ...{ properties: { ...OTClient.properties, loggingURL: OTClient.properties.loggingURL.replace('tokbox', 'bad-tokbox'), }, }, }; const badLoggingNetworkTest = new NetworkTest(badLoggingOT, badLoggingCredentials); badLoggingNetworkTest.testConnectivity() .then((results: ConnectivityTestResults) => { expect(results.failedTests).toBeInstanceOf(Array); if (results.failedTests.find(f => f.type === 'logging')) { done(); } }); }, 10000); it('should result in a failed test if the API server cannot be reached', (done) => { testConnectFailure('OT_CONNECT_FAILED', 'api').then(done); }, 1000); it('results in a failed test when session.connect() gets an invalid HTTP status', (done) => { testConnectFailure('OT_INVALID_HTTP_STATUS', 'api').then(done); }, 1000); it('results in a failed test if session.connect() gets an authentication error', (done) => { testConnectFailure('OT_AUTHENTICATION_ERROR', 'messaging').then(done); }, 1000); it('results in a failed test if OT.getDevices() returns an error', (done) => { spyOn(OT, 'getDevices').and.callFake((callback) => { callback(new Error()); }); networkTest.testConnectivity() .then((results: ConnectivityTestResults) => { expect(results.success).toBe(false); expect(results.failedTests).toBeInstanceOf(Array); if (results.failedTests.find(f => f.type === 'OpenTok.js')) { done(); } }); }, 10000); it('results in a failed test if there are no cameras or mics', (done) => { spyOn(OT, 'getDevices').and.callFake((callback) => { callback(null, []); }); networkTest.testConnectivity() .then((results: ConnectivityTestResults) => { expect(results.success).toBe(false); expect(results.failedTests).toBeInstanceOf(Array); if (results.failedTests.find(f => f.type === 'OpenTok.js')) { done(); } }); }, 10000); it('results in a failed test if session.connect() gets an authentication error', (done) => { testConnectFailure('OT_AUTHENTICATION_ERROR', 'messaging').then(done); }, 1000); it('results in a failed test if OT.initPublisher() returns an error', (done) => { spyOn(OT, 'initPublisher').and.callFake((target, options, callback) => { callback(new Error()); }); networkTest.testConnectivity() .then((results: ConnectivityTestResults) => { expect(results.success).toBe(false); expect(results.failedTests).toBeInstanceOf(Array); if (results.failedTests.find(f => f.type === 'OpenTok.js')) { done(); } }); }, 10000); it('results in a failed test if Session.subscribe() returns an error', (done) => { const realInitSession = OT.initSession; spyOn(OT, 'initSession').and.callFake((apiKey, sessionId) => { const session = realInitSession(apiKey, sessionId); spyOn(session, 'subscribe').and.callFake((stream, target, config, callback) => { const error = new Error(); callback(error); }); return session; }); networkTest.testConnectivity() .then((results: ConnectivityTestResults) => { expect(results.success).toBe(false); expect(results.failedTests).toBeInstanceOf(Array); if (results.failedTests.find(f => f.type === 'media')) { done(); } }); }, 10000); }); describe('Quality Test', () => { const validateResultsUndefined = (results: QualityTestResults) => { expect(results).toBe(undefined); }; const validateUnsupportedBrowserError = (error?: QualityTestError) => { expect(error.name).toBe(ErrorNames.UNSUPPORTED_BROWSER); }; const testConnectFailure = (otErrorName, netTestErrorName) => { const realInitSession = OT.initSession; spyOn(OT, 'initSession').and.callFake((apiKey, sessionId) => { const session = realInitSession(apiKey, sessionId); spyOn(session, 'connect').and.callFake((token, callback) => { const error = new Error(); error.name = otErrorName; callback(error); }); return session; }); const validateResults = (results: QualityTestResults) => { expect(results).toBe(undefined); }; const validateError = (error?: QualityTestError) => { expect(error.name).toBe(netTestErrorName); }; networkTest.testQuality(null) .then(validateResults) .catch(validateError); }; const validateStandardResults = (results: QualityTestResults) => { const { audio, video } = results; expect(audio.bitrate).toEqual(jasmine.any(Number)); expect(audio.supported).toEqual(jasmine.any(Boolean)); expect(audio.reason || '').toEqual(jasmine.any(String)); expect(audio.packetLossRatio).toEqual(jasmine.any(Number)); expect(audio.mos).toEqual(jasmine.any(Number)); expect(video.supported).toEqual(jasmine.any(Boolean)); if (video.supported) { expect(video.bitrate).toEqual(jasmine.any(Number)); expect(video.packetLossRatio).toEqual(jasmine.any(Number)); expect(video.frameRate).toEqual(jasmine.any(Number)); expect(video.recommendedResolution).toEqual(jasmine.any(String)); expect(video.recommendedFrameRate).toEqual(jasmine.any(Number)); expect(video.mos).toEqual(jasmine.any(Number)); } else { expect(video.reason).toEqual(jasmine.any(String)); } }; it('validates its onUpdate callback', () => { // eslint-disable-next-line expect(() => networkTest.testQuality('bad-callback').toThrow(new InvalidOnUpdateCallback())); expect(() => networkTest.testConnectivity(validOnUpdateCallback) .not.toThrowError(NetworkTestError)); }); it('should return an error if invalid session credentials are used', (done) => { const validateResults = (results: QualityTestResults) => { expect(results).toBe(undefined); }; const validateError = (error?: QualityTestError) => { expect(error.name).toBe(ErrorNames.CONNECT_TO_SESSION_TOKEN_ERROR); }; badCredentialsNetworkTest.testQuality(null) .then(validateResults) .catch(validateError) .finally(done); }); it('should return an error if session.connect() gets an authentication error', () => { testConnectFailure('OT_AUTHENTICATION_ERROR', ErrorNames.CONNECT_TO_SESSION_TOKEN_ERROR); }); it('should return an error if session.connect() gets a session ID error', () => { testConnectFailure('OT_INVALID_SESSION_ID', ErrorNames.CONNECT_TO_SESSION_ID_ERROR); }); it('should return an error if session.connect() gets a network error', () => { testConnectFailure('OT_CONNECT_FAILED', ErrorNames.CONNECT_TO_SESSION_NETWORK_ERROR); }); it('results in a failed test if OT.getDevices() returns an error', (done) => { spyOn(OT, 'getDevices').and.callFake((callback) => { callback(new Error()); }); networkTest.testQuality() .catch((error?: QualityTestError) => { expect(error?.name).toBe(ErrorNames.FAILED_TO_OBTAIN_MEDIA_DEVICES); done(); }); }, 10000); it('results in a failed test if there are no mics', (done) => { const realOTGetDevices = OT.getDevices; spyOn(OT, 'getDevices').and.callFake((callbackFn) => { realOTGetDevices((error, devices) => { const onlyVideoDevices = devices?.filter(device => device.kind !== 'audioInput'); callbackFn(error, onlyVideoDevices); }); }); networkTest.testQuality() .catch((error?: QualityTestError) => { expect(error?.name).toBe(ErrorNames.NO_AUDIO_CAPTURE_DEVICES); done(); }); }, 10000); it('should return valid test results or an error', (done) => { const validateError = (error?: QualityTestError) => { expect(error?.name).toBe(ErrorNames.QUALITY_TEST_ERROR); }; const onUpdate = (stats: Stats) => validOnUpdateCallback(stats); networkTest.testQuality(onUpdate) .then(validateStandardResults) .catch(validateError) .finally(done); }, 40000); it('should run a valid test or error when give audiOnly and timeout options', (done) => { const validateResults = (results: QualityTestResults) => { const { audio, video } = results; expect(audio.bitrate).toEqual(jasmine.any(Number)); expect(audio.supported).toEqual(jasmine.any(Boolean)); expect(audio.reason || '').toEqual(jasmine.any(String)); expect(audio.packetLossRatio).toEqual(jasmine.any(Number)); expect(audio.mos).toEqual(jasmine.any(Number)); expect(video.supported).toEqual(false); }; const validateError = (error?: QualityTestError) => { expect(error?.name).toBe(ErrorNames.QUALITY_TEST_ERROR); }; const onUpdate = (stats: Stats) => validOnUpdateCallback(stats); networkTestWithOptions.testQuality(onUpdate) .then(validateResults) .catch(validateError) .finally(done); }, 10000); it('should stop the quality test when you call the stop() method', (done) => { const validateError = (error?: QualityTestError) => { expect(error?.name).toBe(ErrorNames.QUALITY_TEST_ERROR); }; const onUpdate = (stats: Stats) => { validOnUpdateCallback(stats); networkTest.stop(); // The test will wait for adequate stats before stopping }; networkTest.testQuality(onUpdate) .then(validateStandardResults) .catch(validateError) .finally(done); }, 10000); it('should return valid test results or an error when there is no camera', (done) => { const realOTGetDevices = OT.getDevices; spyOn(OT, 'getDevices').and.callFake((callbackFn) => { realOTGetDevices((error, devices) => { const onlyAudioDevices = devices?.filter(device => device.kind !== 'videoInput'); callbackFn(error, onlyAudioDevices); }); }); const validateResults = (results: QualityTestResults) => { const { audio, video } = results; expect(audio.bitrate).toEqual(jasmine.any(Number)); expect(audio.supported).toEqual(jasmine.any(Boolean)); expect(audio.packetLossRatio).toEqual(jasmine.any(Number)); expect(audio.mos).toEqual(jasmine.any(Number)); expect(video.supported).toEqual(false); expect(video.reason).toEqual('No camera was found.'); }; const validateError = (error?: QualityTestError) => { expect(error).toBe(QualityTestError); }; const onUpdate = (stats: Stats) => validOnUpdateCallback(stats); networkTest.testQuality(onUpdate) .then(validateResults) .catch(validateError) .finally(done); }, 10000); it('should return an error if the window.navigator is undefined', () => { spyOnProperty(window, 'navigator', 'get').and.returnValue(undefined); networkTest.testQuality(null) .then(validateResultsUndefined) .catch(validateUnsupportedBrowserError); }); it('should return an unsupported browser error if the browser is an older version of Edge', () => { spyOnProperty(window, 'navigator', 'get').and.returnValue({ mediaDevices: {}, webkitGetUserMedia: null, mozGetUserMedia: null, userAgent: 'Edge/12.10240', }); networkTest.testQuality(null) .then(validateResultsUndefined) .catch(validateUnsupportedBrowserError); }, 10000); it('should run the test if the browser is a Chromium-based version of Edge', (done) => { const mozGetUserMedia = navigator.mozGetUserMedia; const webkitGetUserMedia = navigator.webkitGetUserMedia; navigator.mozGetUserMedia = null; navigator.webkitGetUserMedia = {}; spyOnProperty(window.navigator, 'userAgent', 'get').and.returnValue('Edg'); networkTestWithOptions.testQuality() .then(() => { navigator.mozGetUserMedia = mozGetUserMedia; navigator.webkitGetUserMedia = webkitGetUserMedia; done(); }); }, 10000); it('results in a failed test if OT.initPublisher() returns an error', (done) => { spyOn(OT, 'initPublisher').and.callFake((target, options, callback) => { callback(new Error()); }); networkTest.testQuality().catch((error?: QualityTestError) => { expect(error?.name).toBe(ErrorNames.INIT_PUBLISHER_ERROR); done(); }); }, 10000); it('results in a failed test if Session.subscribe() returns an error', (done) => { const realInitSession = OT.initSession; spyOn(OT, 'initSession').and.callFake((apiKey, sessionId) => { const session = realInitSession(apiKey, sessionId); spyOn(session, 'subscribe').and.callFake((stream, target, config, callback) => { const error = new Error(); callback(error); }); return session; }); networkTest.testQuality().catch((error?: QualityTestError) => { expect(error?.name).toBe(ErrorNames.SUBSCRIBE_TO_SESSION_ERROR); done(); }); }, 10000); }); }); });