opentok-network-test-js
Version:
Precall network test for applications using the OpenTok platform.
491 lines (431 loc) • 19.6 kB
text/typescript
/* 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);
});
});
});