feat: Add unittest for webapp using Jest (#541)

* update dependencies packages

* fix parse command line option

* enable esmodule on jest

* define webapp client test job on yamato

* define signaling handler on server

* setup server test

* add jest-websocket-mock

* fix handler argument

* implement basic test for http/wbsocket signaling handler

* fix lint

* fix tsconfig.json

* change linux vm image for webapp job

* fix jest command for win

* fix upm-ci-webapp.yml

* wait more time for running test server

* make client directory for webapp

* add cross env for client test

* fix httphandler on private mode

* fix review
This commit is contained in:
Takashi Kannan 2021-10-04 14:28:23 +09:00 committed by GitHub
parent b7e8b08729
commit 4eb8c6052f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 14219 additions and 9173 deletions

3
.gitignore vendored
View File

@ -353,6 +353,7 @@ Plugin/webrtc
Assets/Samples.meta
Assets/Samples/
# Exclude Webapp coverage result
WebApp/**/coverage/*
.DS_Store

View File

@ -5,20 +5,24 @@ platforms:
flavor: b1.xlarge
pack_command: pack_webapp.cmd
test_command: test_webapp.cmd
client_test_command: test_webapp_client.cmd
- name: macos
type: Unity::VM::osx
image: package-ci/mac:latest
flavor: m1.mac
pack_command: ./pack_webapp.sh
test_command: ./test_webapp.sh
client_test_command: ./test_webapp_client.sh
- name: linux
type: Unity::VM
image: cds-ops/ubuntu-18.04-base:stable
image: package-ci/ubuntu:latest
flavor: b1.xlarge
pack_command: ./pack_webapp.sh
test_command: ./test_webapp.sh
client_test_command: ./test_webapp_client.sh
projects:
- packagename: com.unity.webapp.renderstreaming
- name: renderstreaming
packagename: com.unity.webapp.renderstreaming
---
{% for project in projects %}
{% for platform in platforms %}
@ -52,6 +56,22 @@ test_{{ platform.name }}:
- "WebApp/coverage/**/*"
dependencies:
- .yamato/upm-ci-webapp.yml#pack_{{ platform.name }}
test_client_{{ platform.name }}:
name : Test Client {{ project.packagename }} on {{ platform.name }}
agent:
type: {{ platform.type }}
image: {{ platform.image }}
flavor: {{ platform.flavor }}
commands:
- {{ platform.client_test_command }}
artifacts:
logs:
paths:
- "WebApp/output.log"
- "WebApp/coverage/**/*"
dependencies:
- .yamato/upm-ci-webapp.yml#pack_{{ platform.name }}
{% endfor %}
trigger_webapp_test_{{ project.name }}:
@ -61,6 +81,7 @@ trigger_webapp_test_{{ project.name }}:
dependencies:
{% for platform in platforms %}
- .yamato/upm-ci-webapp.yml#test_{{ platform.name }}
- .yamato/upm-ci-webapp.yml#test_client_{{ platform.name }}
{% endfor %}
{% endfor %}

View File

@ -8,9 +8,10 @@ module.exports = {
parserOptions: {
sourceType: "module",
project: "./tsconfig.lint.json",
},
},
plugins: ["@typescript-eslint"],
rules: {
"@typescript-eslint/no-var-requires": "off"
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/no-explicit-any": "off",
}
};

View File

@ -0,0 +1,194 @@
/*
* For a detailed explanation regarding each configuration property, visit:
* https://jestjs.io/docs/configuration
*/
export default {
// All imported modules in your tests should be mocked automatically
// automock: false,
// Stop running tests after `n` failures
// bail: 0,
// The directory where Jest should store its cached dependency information
// cacheDirectory: "/private/var/folders/wt/swsbjj0x061bdb0y4dqc0g4c0000gn/T/jest_dx",
// Automatically clear mock calls and instances between every test
// clearMocks: false,
// Indicates whether the coverage information should be collected while executing the test
collectCoverage: true,
// An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: undefined,
// The directory where Jest should output its coverage files
coverageDirectory: "coverage",
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "/node_modules/"
// ],
// Indicates which provider should be used to instrument code for coverage
coverageProvider: "v8",
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: undefined,
// A path to a custom dependency extractor
// dependencyExtractor: undefined,
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: undefined,
// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: undefined,
// A set of global variables that need to be available in all test environments
// globals: {},
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// An array of file extensions your modules use
moduleFileExtensions: [
"js",
"jsx",
"ts",
"tsx",
"json",
"node"
],
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
// moduleNameMapper: {},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
// Activates notifications for test results
// notify: false,
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration
// preset: undefined,
// Run tests from one or more projects
// projects: undefined,
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// Automatically reset mock state between every test
// resetMocks: false,
// Reset the module registry before running each individual test
// resetModules: false,
// A path to a custom resolver
// resolver: undefined,
// Automatically restore mock state between every test
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,
// A list of paths to directories that Jest should use to search for files in
// roots: [
// "<rootDir>"
// ],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
// The number of seconds after which a test is considered as slow and reported as such in the results.
// slowTestThreshold: 5,
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
// The test environment that will be used for testing
testEnvironment: "jsdom",
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Adds a location field to test results
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
testMatch: [
"**/__tests__/**/*.[jt]s?(x)",
"**/?(*.)+(spec|test).[tj]s?(x)"
],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "/node_modules/"
// ],
// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],
// This option allows the use of a custom results processor
// testResultsProcessor: undefined,
// This option allows use of a custom test runner
// testRunner: "jest-circus/runner",
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
testURL: "http://localhost",
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
// timers: "real",
// A map from regular expressions to paths to transformers
// transform: undefined
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "/node_modules/",
// "\\.pnp\\.[^\\/]+$"
// ],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run
// verbose: undefined,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling
// watchman: true,
};

3053
WebApp/client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,12 @@
{
"name": "webclient",
"version": "3.0.0",
"private": true,
"scripts": {
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
},
"devDependencies": {
"jest": "^27.2.0"
},
"type": "module"
}

View File

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Before

Width:  |  Height:  |  Size: 472 KiB

After

Width:  |  Height:  |  Size: 472 KiB

View File

@ -0,0 +1,13 @@
// test for client
import { default as Signaling, WebSocketSignaling } from "../public/js/signaling.js";
test('basic', () => {
var http = new Signaling();
var ws = new WebSocketSignaling();
expect('hello').toBe('hello');
});
test('basic2', () => {
expect(1 + 1).toBe(2);
});

View File

@ -1,10 +1,196 @@
/*
* For a detailed explanation regarding each configuration property, visit:
* https://jestjs.io/docs/configuration
*/
module.exports = {
roots: ['<rootDir>/src/', '<rootDir>/test/'],
// All imported modules in your tests should be mocked automatically
// automock: false,
// Stop running tests after `n` failures
// bail: 0,
// The directory where Jest should store its cached dependency information
// cacheDirectory: "/private/var/folders/wt/swsbjj0x061bdb0y4dqc0g4c0000gn/T/jest_dx",
// Automatically clear mock calls and instances between every test
// clearMocks: false,
// Indicates whether the coverage information should be collected while executing the test
collectCoverage: true,
// An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: undefined,
// The directory where Jest should output its coverage files
coverageDirectory: "coverage",
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "/node_modules/"
// ],
// Indicates which provider should be used to instrument code for coverage
coverageProvider: "v8",
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: undefined,
// A path to a custom dependency extractor
// dependencyExtractor: undefined,
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: undefined,
// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: undefined,
// A set of global variables that need to be available in all test environments
// globals: {},
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// An array of file extensions your modules use
moduleFileExtensions: [
"js",
"jsx",
"ts",
"tsx",
"json",
"node"
],
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
// moduleNameMapper: {},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
// Activates notifications for test results
// notify: false,
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration
// preset: undefined,
// Run tests from one or more projects
// projects: undefined,
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// Automatically reset mock state between every test
// resetMocks: false,
// Reset the module registry before running each individual test
// resetModules: false,
// A path to a custom resolver
// resolver: undefined,
// Automatically restore mock state between every test
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,
// A list of paths to directories that Jest should use to search for files in
// roots: [
// "<rootDir>"
// ],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
// The number of seconds after which a test is considered as slow and reported as such in the results.
// slowTestThreshold: 5,
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
// The test environment that will be used for testing
testEnvironment: "node",
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Adds a location field to test results
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
testMatch: [
"**/__tests__/**/*.[jt]s?(x)",
"**/?(*.)+(spec|test).[tj]s?(x)"
],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "/node_modules/"
// ],
// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],
// This option allows the use of a custom results processor
// testResultsProcessor: undefined,
// This option allows use of a custom test runner
// testRunner: "jest-circus/runner",
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
testURL: "http://localhost",
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
// timers: "real",
// A map from regular expressions to paths to transformers
transform: {
'^.+\\.tsx?$': 'ts-jest'
},
testRegex: '(/__tests__/.*|\\.(test|spec))\\.[tj]sx?$',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
coverageDirectory: './coverage/',
collectCoverage: true
}
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "/node_modules/",
// "\\.pnp\\.[^\\/]+$"
// ],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run
// verbose: undefined,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling
// watchman: true,
};

18776
WebApp/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,8 +4,8 @@
"private": true,
"scripts": {
"prestart": "npm install",
"build": "tsc -p tsconfig.json",
"test": "jest --env=node --colors --coverage test",
"build": "tsc -p tsconfig.build.json",
"test": "jest --colors test/*.ts",
"newman": "newman run test/renderstreaming.postman_collection.json",
"start": "node ./build/index.js",
"dev": "ts-node ./src/index.ts",
@ -13,34 +13,37 @@
"pack": "pkg ."
},
"dependencies": {
"@types/express": "^4.16.1",
"@types/node": "^11.12.0",
"@types/ws": "^7.2.2",
"@types/express": "^4.17.13",
"@types/node": "^11.15.54",
"@types/ws": "^7.4.7",
"debug": "~2.6.9",
"express": "~4.16.0",
"express": "~4.16.4",
"morgan": "^1.10.0",
"uuid": "^3.4.0",
"ws": "^7.4.6"
"ws": "^7.5.5"
},
"devDependencies": {
"@types/jest": "^24.0.12",
"@types/morgan": "^1.9.2",
"@typescript-eslint/eslint-plugin": "^2.3.3",
"@typescript-eslint/parser": "^2.3.3",
"eslint": "^6.5.1",
"jest": "^24.8.0",
"newman": "^4.5.5",
"pkg": "^4.4.0",
"ts-jest": "^24.0.2",
"ts-node": "^8.1.0",
"typescript": "^3.3.4000"
"@jest-mock/express": "^1.4.5",
"@types/jest": "^27.0.2",
"@types/morgan": "^1.9.3",
"@typescript-eslint/eslint-plugin": "^2.34.0",
"@typescript-eslint/parser": "^2.34.0",
"eslint": "^6.8.0",
"jest": "^27.2.0",
"jest-websocket-mock": "^2.2.1",
"mock-socket": "^9.0.5",
"newman": "^5.3.0",
"pkg": "^4.5.1",
"ts-jest": "^27.0.5",
"ts-node": "^10.2.1",
"typescript": "^3.9.10"
},
"bin": {
"webserver": "build/index.js"
},
"pkg": {
"assets": [
"public/**/*"
"client/public/**/*"
],
"targets": [
"node10"

View File

@ -0,0 +1,277 @@
import { Request, Response } from 'express';
import Offer from './offer';
import Answer from './answer';
import Candidate from './candidate';
let isPrivate: boolean;
// [{sessonId:[connectionId,...]}]
const clients: Map<string, Set<string>> = new Map<string, Set<string>>();
// [{connectionId:[sessionId1, sessionId2]}]
const connectionPair: Map<string, [string, string]> = new Map<string, [string, string]>(); // key = connectionId
// [{sessionId:[{connectionId:Offer},...]}]
const offers: Map<string, Map<string, Offer>> = new Map<string, Map<string, Offer>>(); // key = sessionId
// [{sessionId:[{connectionId:Answer},...]}]
const answers: Map<string, Map<string, Answer>> = new Map<string, Map<string, Answer>>(); // key = sessionId
// [{sessionId:[{connectionId:Candidate},...]}]
const candidates: Map<string, Map<string, Candidate[]>> = new Map<string, Map<string, Candidate[]>>(); // key = sessionId
function getOrCreateConnectionIds(sessionId: string): Set<string> {
let connectionIds = null;
if (!clients.has(sessionId)) {
connectionIds = new Set<string>();
clients.set(sessionId, connectionIds);
}
connectionIds = clients.get(sessionId);
return connectionIds;
}
function reset(mode: string): void {
isPrivate = mode == "private";
clients.clear();
connectionPair.clear();
offers.clear();
answers.clear();
candidates.clear();
}
function checkSessionId(req: Request, res: Response, next): void {
if (req.url === '/') {
next();
return;
}
const id: string = req.header('session-id');
if (!clients.has(id)) {
res.sendStatus(404);
return;
}
next();
}
function getConnection(req: Request, res: Response): void {
const sessionId: string = req.header('session-id');
const arrayConnection = Array.from(clients.get(sessionId))
const obj = arrayConnection.map((v) => ({ connectionId: v }));
res.json({ connections: obj });
}
function getOffer(req: Request, res: Response): void {
// get `fromtime` parameter from request query
const fromTime: number = req.query.fromtime ? Number(req.query.fromtime) : 0;
const sessionId: string = req.header('session-id');
let arrayOffers: [string, Offer][] = [];
if (offers.size != 0) {
if (isPrivate) {
if (offers.has(sessionId)) {
arrayOffers = Array.from(offers.get(sessionId));
}
} else {
const otherSessionMap = Array.from(offers).filter(x => x[0] != sessionId);
arrayOffers = [].concat(...Array.from(otherSessionMap, x => Array.from(x[1], y => [y[0], y[1]])));
}
}
if (fromTime > 0) {
arrayOffers = arrayOffers.filter((v) => v[1].datetime > fromTime);
}
const obj = arrayOffers.map((v) => ({ connectionId: v[0], sdp: v[1].sdp, polite: v[1].polite }));
res.json({ offers: obj });
}
function getAnswer(req: Request, res: Response): void {
// get `fromtime` parameter from request query
const fromTime: number = req.query.fromtime ? Number(req.query.fromtime) : 0;
const sessionId: string = req.header('session-id');
let arrayOffers: [string, Answer][] = [];
if (answers.size != 0 && answers.has(sessionId)) {
arrayOffers = Array.from(answers.get(sessionId));
}
if (fromTime > 0) {
arrayOffers = arrayOffers.filter((v) => v[1].datetime > fromTime);
}
const obj = arrayOffers.map((v) => ({ connectionId: v[0], sdp: v[1].sdp }));
res.json({ answers: obj });
}
function getCandidate(req: Request, res: Response): void {
// get `fromtime` parameter from request query
const fromTime: number = req.query.fromtime ? Number(req.query.fromtime) : 0;
const sessionId: string = req.header('session-id');
const connectionIds = Array.from(clients.get(sessionId));
const arr = [];
for (const connectionId of connectionIds) {
const pair = connectionPair.get(connectionId);
if (pair == null) {
continue;
}
const otherSessionId = sessionId === pair[0] ? pair[1] : pair[0];
if (!candidates.get(otherSessionId) || !candidates.get(otherSessionId).get(connectionId)) {
continue;
}
const arrayCandidates = candidates.get(otherSessionId).get(connectionId)
.filter((v) => v.datetime > fromTime)
.map((v) => ({ candidate: v.candidate, sdpMLineIndex: v.sdpMLineIndex, sdpMid: v.sdpMid }));
if (arrayCandidates.length === 0) {
continue;
}
arr.push({ connectionId, candidates: arrayCandidates });
}
res.json({ candidates: arr });
}
function createSession(sessionId: string, res: Response): void {
clients.set(sessionId, new Set<string>());
offers.set(sessionId, new Map<string, Offer>());
answers.set(sessionId, new Map<string, Answer>());
candidates.set(sessionId, new Map<string, Candidate[]>());
res.json({ sessionId: sessionId });
}
function deleteSession(req: Request, res: Response): void {
const id: string = req.header('session-id');
offers.delete(id);
answers.delete(id);
candidates.delete(id);
clients.delete(id);
res.sendStatus(200);
}
function createConnection(req: Request, res: Response): void {
const sessionId: string = req.header('session-id');
const { connectionId } = req.body;
if (connectionId == null) {
res.status(400).send({ error: new Error(`connectionId is required`) });
return;
}
let polite = true;
if (isPrivate) {
if (connectionPair.has(connectionId)) {
const pair = connectionPair.get(connectionId);
if (pair[0] != null && pair[1] != null) {
const err = new Error(`${connectionId}: This connection id is already used.`);
console.log(err);
res.status(400).send({ error: err });
return;
} else if (pair[0] != null) {
connectionPair.set(connectionId, [pair[0], sessionId]);
const map = getOrCreateConnectionIds(pair[0]);
map.add(connectionId);
}
} else {
connectionPair.set(connectionId, [sessionId, null]);
polite = false;
}
}
const connectionIds = getOrCreateConnectionIds(sessionId);
connectionIds.add(connectionId);
res.json({ connectionId: connectionId, polite: polite });
}
function deleteConnection(req: Request, res: Response): void {
const sessionId: string = req.header('session-id');
const { connectionId } = req.body;
clients.get(sessionId).delete(connectionId);
if (connectionPair.has(connectionId)) {
const pair = connectionPair.get(connectionId);
const otherSessionId = pair[0] == sessionId ? pair[1] : pair[0];
if (otherSessionId) {
if (clients.has(otherSessionId)) {
clients.get(otherSessionId).delete(connectionId);
}
}
}
connectionPair.delete(connectionId);
offers.get(sessionId).delete(connectionId);
answers.get(sessionId).delete(connectionId);
candidates.get(sessionId).delete(connectionId);
res.json({ connectionId: connectionId });
}
function postOffer(req: Request, res: Response): void {
const sessionId: string = req.header('session-id');
const { connectionId } = req.body;
let keySessionId = null;
let polite = false;
if (isPrivate) {
if (connectionPair.has(connectionId)) {
const pair = connectionPair.get(connectionId);
keySessionId = pair[0] == sessionId ? pair[1] : pair[0];
if (keySessionId != null) {
polite = true;
const map = offers.get(keySessionId);
map.set(connectionId, new Offer(req.body.sdp, Date.now(), polite))
}
}
res.sendStatus(200);
return;
}
connectionPair.set(connectionId, [sessionId, null]);
keySessionId = sessionId;
const map = offers.get(keySessionId);
map.set(connectionId, new Offer(req.body.sdp, Date.now(), polite))
res.sendStatus(200);
}
function postAnswer(req: Request, res: Response): void {
const sessionId: string = req.header('session-id');
const { connectionId } = req.body;
const connectionIds = getOrCreateConnectionIds(sessionId);
connectionIds.add(connectionId);
if (!connectionPair.has(connectionId)) {
res.sendStatus(200);
return;
}
// add connectionPair
const pair = connectionPair.get(connectionId);
const otherSessionId = pair[0] == sessionId ? pair[1] : pair[0];
if (!isPrivate) {
connectionPair.set(connectionId, [otherSessionId, sessionId]);
}
const map = answers.get(otherSessionId);
map.set(connectionId, new Answer(req.body.sdp, Date.now()));
// update datetime for candidates
const mapCandidates = candidates.get(otherSessionId);
if (mapCandidates) {
const arrayCandidates = mapCandidates.get(connectionId);
if (arrayCandidates) {
for (const candidate of arrayCandidates) {
candidate.datetime = Date.now();
}
}
}
res.sendStatus(200);
}
function postCandidate(req: Request, res: Response): void {
const sessionId: string = req.header('session-id');
const { connectionId } = req.body;
const map = candidates.get(sessionId);
if (!map.has(connectionId)) {
map.set(connectionId, []);
}
const arr = map.get(connectionId);
const candidate = new Candidate(req.body.candidate, req.body.sdpMLineIndex, req.body.sdpMid, Date.now());
arr.push(candidate);
res.sendStatus(200);
}
export { reset, checkSessionId, getConnection, getOffer, getAnswer, getCandidate, createSession, deleteSession, createConnection, deleteConnection, postOffer, postAnswer, postCandidate }

View File

@ -0,0 +1,153 @@
import Offer from './offer';
import Answer from './answer';
import Candidate from './candidate';
let isPrivate: boolean;
// [{sessonId:[connectionId,...]}]
const clients: Map<WebSocket, Set<string>> = new Map<WebSocket, Set<string>>();
// [{connectionId:[sessionId1, sessionId2]}]
const connectionPair: Map<string, [WebSocket, WebSocket]> = new Map<string, [WebSocket, WebSocket]>();
function getOrCreateConnectionIds(session: WebSocket): Set<string> {
let connectionIds = null;
if (!clients.has(session)) {
connectionIds = new Set<string>();
clients.set(session, connectionIds);
}
connectionIds = clients.get(session);
return connectionIds;
}
function reset(mode: string): void {
isPrivate = mode == "private";
}
function add(ws: WebSocket): void {
clients.set(ws, new Set<string>());
}
function remove(ws: WebSocket): void {
const connectionIds = clients.get(ws);
connectionIds.forEach(connectionId => {
const pair = connectionPair.get(connectionId);
if (pair) {
const otherSessionWs = pair[0] == ws ? pair[1] : pair[0];
if (otherSessionWs) {
otherSessionWs.send(JSON.stringify({ type: "disconnect", connectionId: connectionId }));
}
}
connectionPair.delete(connectionId);
});
clients.delete(ws);
}
function onConnect(ws: WebSocket, connectionId: string): void {
let polite = true;
if (isPrivate) {
if (connectionPair.has(connectionId)) {
const pair = connectionPair.get(connectionId);
if (pair[0] != null && pair[1] != null) {
ws.send(JSON.stringify({ type: "error", message: `${connectionId}: This connection id is already used.` }));
return;
} else if (pair[0] != null) {
connectionPair.set(connectionId, [pair[0], ws]);
}
} else {
connectionPair.set(connectionId, [ws, null]);
polite = false;
}
}
const connectionIds = getOrCreateConnectionIds(ws);
connectionIds.add(connectionId);
ws.send(JSON.stringify({ type: "connect", connectionId: connectionId, polite: polite }));
}
function onDisconnect(ws: WebSocket, connectionId: string): void {
const connectionIds = clients.get(ws);
connectionIds.delete(connectionId);
if (connectionPair.has(connectionId)) {
const pair = connectionPair.get(connectionId);
const otherSessionWs = pair[0] == ws ? pair[1] : pair[0];
if (otherSessionWs) {
otherSessionWs.send(JSON.stringify({ type: "disconnect", connectionId: connectionId }));
}
}
connectionPair.delete(connectionId);
ws.send(JSON.stringify({ type: "disconnect", connectionId: connectionId }));
}
function onOffer(ws: WebSocket, message: any): void {
const connectionId = message.connectionId as string;
const newOffer = new Offer(message.sdp, Date.now(), false);
if (isPrivate) {
if (connectionPair.has(connectionId)) {
const pair = connectionPair.get(connectionId);
const otherSessionWs = pair[0] == ws ? pair[1] : pair[0];
if (otherSessionWs) {
newOffer.polite = true;
otherSessionWs.send(JSON.stringify({ from: connectionId, to: "", type: "offer", data: newOffer }));
}
}
return;
}
connectionPair.set(connectionId, [ws, null]);
clients.forEach((_v, k) => {
if (k == ws) {
return;
}
k.send(JSON.stringify({ from: connectionId, to: "", type: "offer", data: newOffer }));
});
}
function onAnswer(ws: WebSocket, message: any): void {
const connectionId = message.connectionId as string;
const connectionIds = getOrCreateConnectionIds(ws);
connectionIds.add(connectionId);
const newAnswer = new Answer(message.sdp, Date.now());
if (!connectionPair.has(connectionId)) {
return;
}
const pair = connectionPair.get(connectionId);
const otherSessionWs = pair[0] == ws ? pair[1] : pair[0];
if (!isPrivate) {
connectionPair.set(connectionId, [otherSessionWs, ws]);
}
otherSessionWs.send(JSON.stringify({ from: connectionId, to: "", type: "answer", data: newAnswer }));
}
function onCandidate(ws: WebSocket, message: any): void {
const connectionId = message.connectionId;
const candidate = new Candidate(message.candidate, message.sdpMLineIndex, message.sdpMid, Date.now());
if (isPrivate) {
if (connectionPair.has(connectionId)) {
const pair = connectionPair.get(connectionId);
const otherSessionWs = pair[0] == ws ? pair[1] : pair[0];
if (otherSessionWs) {
otherSessionWs.send(JSON.stringify({ from: connectionId, to: "", type: "candidate", data: candidate }));
}
}
return;
}
clients.forEach((_v, k) => {
if (k === ws) {
return;
}
k.send(JSON.stringify({ from: connectionId, to: "", type: "candidate", data: candidate }));
});
}
export { reset, add, remove, onConnect, onDisconnect, onOffer, onAnswer, onCandidate }

View File

@ -1,3 +1,4 @@
import { Command } from 'commander';
import * as express from 'express';
import * as https from 'https';
import { Server } from 'http';
@ -10,12 +11,12 @@ import Options from './class/options';
export class RenderStreaming {
public static run(argv: string[]): RenderStreaming {
const program = require('commander');
const program = new Command();
const readOptions = (): Options => {
if (Array.isArray(argv)) {
program
.usage('[options] <apps...>')
.option('-p, --port <n>', 'Port to start the server on', process.env.PORT || 80)
.option('-p, --port <n>', 'Port to start the server on', process.env.PORT || `80`)
.option('-s, --secure', 'Enable HTTPS (you need server.key and server.cert)', process.env.SECURE || false)
.option('-k, --keyfile <path>', 'https key file (default server.key)', process.env.KEYFILE || 'server.key')
.option('-c, --certfile <path>', 'https cert file (default server.cert)', process.env.CERTFILE || 'server.cert')
@ -23,14 +24,15 @@ export class RenderStreaming {
.option('-m, --mode <type>', 'Choose Communication mode public or private (default public)', process.env.MODE || 'public')
.option('-l, --logging <type>', 'Choose http logging type combined, dev, short, tiny or none.(default dev)', process.env.LOGGING || 'dev')
.parse(argv);
const option = program.opts();
return {
port: program.port,
secure: program.secure == undefined ? false : program.secure,
keyfile: program.keyfile,
certfile: program.certfile,
websocket: program.websocket == undefined ? false : program.websocket,
mode: program.mode,
logging: program.logging,
port: option.port,
secure: option.secure == undefined ? false : option.secure,
keyfile: option.keyfile,
certfile: option.certfile,
websocket: option.websocket == undefined ? false : option.websocket,
mode: option.mode,
logging: option.logging,
};
}
};

View File

@ -6,10 +6,11 @@ import * as morgan from 'morgan';
import signaling from './signaling';
import { log, LogLevel } from './log';
import Options from './class/options';
import { reset as resetHandler }from './class/httphandler';
export const createServer = (config: Options): express.Application => {
const app: express.Application = express();
app.set('isPrivate', config.mode == "private");
resetHandler(config.mode);
// logging http access
if (config.logging != "none") {
app.use(morgan(config.logging));
@ -19,9 +20,9 @@ export const createServer = (config: Options): express.Application => {
app.use(bodyParser.json());
app.get('/config', (req, res) => res.json({ useWebSocket: config.websocket, startupMode: config.mode, logging: config.logging }));
app.use('/signaling', signaling);
app.use(express.static(path.join(__dirname, '../public')));
app.use(express.static(path.join(__dirname, '../client/public')));
app.get('/', (req, res) => {
const indexPagePath: string = path.join(__dirname, '../public/index.html');
const indexPagePath: string = path.join(__dirname, '../client/public/index.html');
fs.access(indexPagePath, (err) => {
if (err) {
log(LogLevel.warn, `Can't find file ' ${indexPagePath}`);

View File

@ -1,272 +1,22 @@
import { Request, Response, Router } from 'express';
import * as express from 'express';
import { v4 as uuid } from 'uuid';
import Offer from './class/offer';
import Answer from './class/answer';
import Candidate from './class/candidate';
import * as handler from'./class/httphandler';
const express = require('express');
const router: Router = express.Router();
// [{sessonId:[connectionId,...]}]
const clients: Map<string, Set<string>> = new Map<string, Set<string>>();
// [{connectionId:[sessionId1, sessionId2]}]
const connectionPair: Map<string, [string, string]> = new Map<string, [string, string]>(); // key = connectionId
// [{sessionId:[{connectionId:Offer},...]}]
const offers: Map<string, Map<string, Offer>> = new Map<string, Map<string, Offer>>(); // key = sessionId
// [{sessionId:[{connectionId:Answer},...]}]
const answers: Map<string, Map<string, Answer>> = new Map<string, Map<string, Answer>>(); // key = sessionId
// [{sessionId:[{connectionId:Candidate},...]}]
const candidates: Map<string, Map<string, Candidate[]>> = new Map<string, Map<string, Candidate[]>>(); // key = sessionId
function getOrCreateConnectionIds(sessionId): Set<string> {
let connectionIds = null;
if (!clients.has(sessionId)) {
connectionIds = new Set<string>();
clients.set(sessionId, connectionIds);
}
connectionIds = clients.get(sessionId);
return connectionIds;
}
router.use((req: Request, res: Response, next) => {
if (req.url === '/') {
next();
return;
}
const id: string = req.header('session-id');
if (!clients.has(id)) {
res.sendStatus(404);
return;
}
next();
});
router.get('/connection', (req: Request, res: Response) => {
const sessionId: string = req.header('session-id');
const arrayConnection = Array.from(clients.get(sessionId))
const obj = arrayConnection.map((v) => ({ connectionId: v }));
res.json({ connections: obj });
});
router.get('/offer', (req: Request, res: Response) => {
// get `fromtime` parameter from request query
const fromTime: number = req.query.fromtime ? Number(req.query.fromtime) : 0;
const sessionId: string = req.header('session-id');
let arrayOffers: [string, Offer][] = [];
if (offers.size != 0) {
if (req.app.get('isPrivate')) {
if (offers.has(sessionId)) {
arrayOffers = Array.from(offers.get(sessionId));
}
} else {
const otherSessionMap = Array.from(offers).filter(x => x[0] != sessionId);
arrayOffers = [].concat(...Array.from(otherSessionMap, x => Array.from(x[1], y => [y[0], y[1]])));
}
}
if (fromTime > 0) {
arrayOffers = arrayOffers.filter((v) => v[1].datetime > fromTime);
}
const obj = arrayOffers.map((v) => ({ connectionId: v[0], sdp: v[1].sdp, polite: v[1].polite }));
res.json({ offers: obj });
});
router.get('/answer', (req: Request, res: Response) => {
// get `fromtime` parameter from request query
const fromTime: number = req.query.fromtime ? Number(req.query.fromtime) : 0;
const sessionId: string = req.header('session-id');
let arrayOffers: [string, Answer][] = [];
if (answers.size != 0 && answers.has(sessionId)) {
arrayOffers = Array.from(answers.get(sessionId));
}
if (fromTime > 0) {
arrayOffers = arrayOffers.filter((v) => v[1].datetime > fromTime);
}
const obj = arrayOffers.map((v) => ({ connectionId: v[0], sdp: v[1].sdp }));
res.json({ answers: obj });
});
router.get('/candidate', (req: Request, res: Response) => {
// get `fromtime` parameter from request query
const fromTime: number = req.query.fromtime ? Number(req.query.fromtime) : 0;
const sessionId: string = req.header('session-id');
const connectionIds = Array.from(clients.get(sessionId));
const arr = [];
for (const connectionId of connectionIds) {
const pair = connectionPair.get(connectionId);
if (pair == null) {
continue;
}
const otherSessionId = sessionId === pair[0] ? pair[1] : pair[0];
if (!candidates.get(otherSessionId) || !candidates.get(otherSessionId).get(connectionId)) {
continue;
}
const arrayCandidates = candidates.get(otherSessionId).get(connectionId)
.filter((v) => v.datetime > fromTime)
.map((v) => ({ candidate: v.candidate, sdpMLineIndex: v.sdpMLineIndex, sdpMid: v.sdpMid }));
if (arrayCandidates.length === 0) {
continue;
}
arr.push({ connectionId, candidates: arrayCandidates });
}
res.json({ candidates: arr });
});
router.put('', (req: Request, res: Response) => {
const id: string = uuid();
clients.set(id, new Set<string>());
offers.set(id, new Map<string, Offer>());
answers.set(id, new Map<string, Answer>());
candidates.set(id, new Map<string, Candidate[]>());
res.json({ sessionId: id });
});
router.delete('', (req: Request, res: Response) => {
const id: string = req.header('session-id');
offers.delete(id);
answers.delete(id);
candidates.delete(id);
clients.delete(id);
res.sendStatus(200);
});
router.put('/connection', (req: Request, res: Response) => {
const sessionId: string = req.header('session-id');
const { connectionId } = req.body;
if (connectionId == null) {
res.status(400).send({ error: new Error(`connectionId is required`) });
return;
}
let polite = true;
if (req.app.get('isPrivate')) {
if (connectionPair.has(connectionId)) {
const pair = connectionPair.get(connectionId);
if (pair[0] != null && pair[1] != null) {
const err = new Error(`${connectionId}: This connection id is already used.`);
console.log(err);
res.status(400).send({ error: err });
return;
} else if (pair[0] != null) {
connectionPair.set(connectionId, [pair[0], sessionId]);
const map = getOrCreateConnectionIds(pair[0]);
map.add(connectionId);
}
} else {
connectionPair.set(connectionId, [sessionId, null]);
polite = false;
}
}
const connectionIds = getOrCreateConnectionIds(sessionId);
connectionIds.add(connectionId);
res.json({ connectionId: connectionId, polite: polite });
});
router.delete('/connection', (req: Request, res: Response) => {
const sessionId: string = req.header('session-id');
const { connectionId } = req.body;
clients.get(sessionId).delete(connectionId);
if (connectionPair.has(connectionId)) {
const pair = connectionPair.get(connectionId);
const otherSessionId = pair[0] == sessionId ? pair[1] : pair[0];
if (otherSessionId) {
if (clients.has(otherSessionId)) {
clients.get(otherSessionId).delete(connectionId);
}
}
}
connectionPair.delete(connectionId);
offers.get(sessionId).delete(connectionId);
answers.get(sessionId).delete(connectionId);
candidates.get(sessionId).delete(connectionId);
res.json({ connectionId: connectionId });
});
router.post('/offer', (req: Request, res: Response) => {
const sessionId: string = req.header('session-id');
const { connectionId } = req.body;
let keySessionId = null;
let polite = false;
if (res.app.get('isPrivate')) {
if (connectionPair.has(connectionId)) {
const pair = connectionPair.get(connectionId);
keySessionId = pair[0] == sessionId ? pair[1] : pair[0];
if (keySessionId != null) {
polite = true;
const map = offers.get(keySessionId);
map.set(connectionId, new Offer(req.body.sdp, Date.now(), polite))
}
}
res.sendStatus(200);
return;
}
connectionPair.set(connectionId, [sessionId, null]);
keySessionId = sessionId;
const map = offers.get(keySessionId);
map.set(connectionId, new Offer(req.body.sdp, Date.now(), polite))
res.sendStatus(200);
});
router.post('/answer', (req: Request, res: Response) => {
const sessionId: string = req.header('session-id');
const { connectionId } = req.body;
const connectionIds = getOrCreateConnectionIds(sessionId);
connectionIds.add(connectionId);
if (!connectionPair.has(connectionId)) {
res.sendStatus(200);
return;
}
// add connectionPair
const pair = connectionPair.get(connectionId);
const otherSessionId = pair[0] == sessionId ? pair[1] : pair[0];
if (!res.app.get('isPrivate')) {
connectionPair.set(connectionId, [otherSessionId, sessionId]);
}
const map = answers.get(otherSessionId);
map.set(connectionId, new Answer(req.body.sdp, Date.now()));
// update datetime for candidates
const mapCandidates = candidates.get(otherSessionId);
if (mapCandidates) {
const arrayCandidates = mapCandidates.get(connectionId);
if (arrayCandidates) {
for (const candidate of arrayCandidates) {
candidate.datetime = Date.now();
}
}
}
res.sendStatus(200);
});
router.post('/candidate', (req: Request, res: Response) => {
const sessionId: string = req.header('session-id');
const { connectionId } = req.body;
const map = candidates.get(sessionId);
if (!map.has(connectionId)) {
map.set(connectionId, []);
}
const arr = map.get(connectionId);
const candidate = new Candidate(req.body.candidate, req.body.sdpMLineIndex, req.body.sdpMid, Date.now());
arr.push(candidate);
res.sendStatus(200);
const router: express.Router = express.Router();
router.use(handler.checkSessionId);
router.get('/connection', handler.getConnection);
router.get('/offer', handler.getOffer);
router.get('/answer', handler.getAnswer);
router.get('/candidate', handler.getCandidate);
router.put('', (req: express.Request, res: express.Response) => {
const sessionId = uuid();
handler.createSession(sessionId, res);
});
router.delete('', handler.deleteSession);
router.put('/connection', handler.createConnection);
router.delete('/connection', handler.deleteConnection);
router.post('/offer', handler.postOffer);
router.post('/answer', handler.postAnswer);
router.post('/candidate', handler.postCandidate);
export default router;

View File

@ -1,57 +1,25 @@
import * as websocket from "ws";
import { Server } from 'http';
import Offer from './class/offer';
import Answer from './class/answer';
import Candidate from './class/candidate';
// [{sessonId:[connectionId,...]}]
const clients: Map<WebSocket, Set<string>> = new Map<WebSocket, Set<string>>();
// [{connectionId:[sessionId1, sessionId2]}]
const connectionPair: Map<string, [WebSocket, WebSocket]> = new Map<string, [WebSocket, WebSocket]>();
function getOrCreateConnectionIds(settion: WebSocket): Set<string> {
let connectionIds = null;
if (!clients.has(settion)) {
connectionIds = new Set<string>();
clients.set(settion, connectionIds);
}
connectionIds = clients.get(settion);
return connectionIds;
}
import * as handler from "./class/websockethandler"
export default class WSSignaling {
server: Server;
wss: websocket.Server;
isPrivate: boolean;
constructor(server: Server, mode: string) {
this.server = server;
this.wss = new websocket.Server({ server });
this.isPrivate = mode == "private";
handler.reset(mode);
this.wss.on('connection', (ws: WebSocket) => {
clients.set(ws, new Set<string>());
handler.add(ws);
ws.onclose = (_event: CloseEvent) => {
const connectionIds = clients.get(ws);
connectionIds.forEach(connectionId => {
const pair = connectionPair.get(connectionId);
if (pair) {
const otherSessionWs = pair[0] == ws ? pair[1] : pair[0];
if (otherSessionWs) {
otherSessionWs.send(JSON.stringify({ type: "disconnect", connectionId: connectionId }));
}
}
connectionPair.delete(connectionId);
});
clients.delete(ws);
ws.onclose = (): void => {
handler.remove(ws);
}
ws.onmessage = (event: MessageEvent) => {
ws.onmessage = (event: MessageEvent): void => {
// type: connect, disconnect JSON Schema
// connectionId: connect or disconnect connectionId
@ -70,19 +38,19 @@ export default class WSSignaling {
switch (msg.type) {
case "connect":
this.onConnect(ws, msg.connectionId);
handler.onConnect(ws, msg.connectionId);
break;
case "disconnect":
this.onDisconnect(ws, msg.connectionId);
handler.onDisconnect(ws, msg.connectionId);
break;
case "offer":
this.onOffer(ws, msg.data);
handler.onOffer(ws, msg.data);
break;
case "answer":
this.onAnswer(ws, msg.data);
handler.onAnswer(ws, msg.data);
break;
case "candidate":
this.onCandidate(ws, msg.data);
handler.onCandidate(ws, msg.data);
break;
default:
break;
@ -90,110 +58,4 @@ export default class WSSignaling {
};
});
}
private onConnect(ws: WebSocket, connectionId: string) {
let polite = true;
if (this.isPrivate) {
if (connectionPair.has(connectionId)) {
const pair = connectionPair.get(connectionId);
if (pair[0] != null && pair[1] != null) {
ws.send(JSON.stringify({ type: "error", message: `${connectionId}: This connection id is already used.` }));
return;
} else if (pair[0] != null) {
connectionPair.set(connectionId, [pair[0], ws]);
}
} else {
connectionPair.set(connectionId, [ws, null]);
polite = false;
}
}
const connectionIds = getOrCreateConnectionIds(ws);
connectionIds.add(connectionId);
ws.send(JSON.stringify({ type: "connect", connectionId: connectionId, polite: polite }));
}
private onDisconnect(ws: WebSocket, connectionId: string) {
const connectionIds = clients.get(ws);
connectionIds.delete(connectionId);
if (connectionPair.has(connectionId)) {
const pair = connectionPair.get(connectionId);
const otherSessionWs = pair[0] == ws ? pair[1] : pair[0];
if (otherSessionWs) {
otherSessionWs.send(JSON.stringify({ type: "disconnect", connectionId: connectionId }));
}
}
connectionPair.delete(connectionId);
ws.send(JSON.stringify({ type: "disconnect", connectionId: connectionId }));
}
private onOffer(ws: WebSocket, message: any) {
const connectionId = message.connectionId as string;
const newOffer = new Offer(message.sdp, Date.now(), false);
if (this.isPrivate) {
if (connectionPair.has(connectionId)) {
const pair = connectionPair.get(connectionId);
const otherSessionWs = pair[0] == ws ? pair[1] : pair[0];
if (otherSessionWs) {
newOffer.polite = true;
otherSessionWs.send(JSON.stringify({ from: connectionId, to: "", type: "offer", data: newOffer }));
}
}
return;
}
connectionPair.set(connectionId, [ws, null]);
clients.forEach((_v, k) => {
if (k == ws) {
return;
}
k.send(JSON.stringify({ from: connectionId, to: "", type: "offer", data: newOffer }));
});
}
private onAnswer(ws: WebSocket, message: any) {
const connectionId = message.connectionId as string;
const connectionIds = getOrCreateConnectionIds(ws);
connectionIds.add(connectionId);
const newAnswer = new Answer(message.sdp, Date.now());
if (!connectionPair.has(connectionId)) {
return;
}
const pair = connectionPair.get(connectionId);
const otherSessionWs = pair[0] == ws ? pair[1] : pair[0];
if (!this.isPrivate) {
connectionPair.set(connectionId, [otherSessionWs, ws]);
}
otherSessionWs.send(JSON.stringify({ from: connectionId, to: "", type: "answer", data: newAnswer }));
}
private onCandidate(ws: WebSocket, message: any) {
const connectionId = message.connectionId;
const candidate = new Candidate(message.candidate, message.sdpMLineIndex, message.sdpMid, Date.now());
if (this.isPrivate) {
if (connectionPair.has(connectionId)) {
const pair = connectionPair.get(connectionId);
const otherSessionWs = pair[0] == ws ? pair[1] : pair[0];
if (otherSessionWs) {
otherSessionWs.send(JSON.stringify({ from: connectionId, to: "", type: "candidate", data: candidate }));
}
}
return;
}
clients.forEach((_v, k) => {
if (k === ws) {
return;
}
k.send(JSON.stringify({ from: connectionId, to: "", type: "candidate", data: candidate }));
});
}
}

View File

@ -1,7 +1,154 @@
test('basic', () => {
expect('hello').toBe('hello');
// test for server
import { getMockReq, getMockRes } from '@jest-mock/express';
import * as httpHandler from '../src/class/httphandler';
import WS from "jest-websocket-mock";
import * as wsHandler from '../src/class/websockethandler';
describe('http signaling test in public mode', () => {
const sessionId = "abcd1234";
const headerMock = (): string => sessionId;
const connectionId = "12345";
const { res, mockClear } = getMockRes()
const req = getMockReq({ header: headerMock, body: { connectionId: connectionId } });
beforeAll(() => {
httpHandler.reset("public");
});
beforeEach(() => {
mockClear();
});
test('create session', async () => {
await httpHandler.createSession(sessionId, res);
expect(res.json).toHaveBeenCalledWith({ sessionId: sessionId });
});
test('create connection', async () => {
await httpHandler.createConnection(req, res);
expect(res.json).toHaveBeenCalledWith({ connectionId: connectionId, polite: true });
});
test('delete connection', async () => {
await httpHandler.deleteConnection(req, res);
expect(res.json).toHaveBeenCalledWith({ connectionId: connectionId });
});
test('delete session', async () => {
const req = getMockReq({ header: headerMock });
await httpHandler.deleteSession(req, res)
expect(res.sendStatus).toBeCalledWith(200);
});
});
test('basic2', () => {
expect(1+1).toBe(2);
describe('http signaling test in private mode', () => {
const sessionId = "abcd1234";
const headerMock = (): string => sessionId;
const connectionId = "12345";
const { res, mockClear } = getMockRes()
const req = getMockReq({ header: headerMock, body: { connectionId: connectionId } });
beforeAll(() => {
httpHandler.reset("private");
});
beforeEach(() => {
mockClear();
});
test('create session', async () => {
await httpHandler.createSession(sessionId, res);
expect(res.json).toHaveBeenCalledWith({ sessionId: sessionId });
});
test('create connection', async () => {
await httpHandler.createConnection(req, res);
expect(res.json).toHaveBeenCalledWith({ connectionId: connectionId, polite: false });
});
test('delete connection', async () => {
await httpHandler.deleteConnection(req, res);
expect(res.json).toHaveBeenCalledWith({ connectionId: connectionId });
});
test('delete session', async () => {
const req = getMockReq({ header: headerMock });
await httpHandler.deleteSession(req, res)
expect(res.sendStatus).toBeCalledWith(200);
});
});
describe('websocket signaling test in public mode', () => {
let server: WS;
let client: WebSocket;
const connectionId = "12345";
beforeAll(async () => {
wsHandler.reset("public");
server = new WS("ws://localhost:1234", { jsonProtocol: true });
client = new WebSocket("ws://localhost:1234");
await server.connected;
});
afterAll(() => {
WS.clean();
});
test('create session', async () => {
await wsHandler.add(client);
});
test('create connection', async () => {
await wsHandler.onConnect(client, connectionId);
await expect(server).toReceiveMessage({ type: "connect", connectionId: connectionId, polite: true });
});
test('delete connection', async () => {
await wsHandler.onDisconnect(client, connectionId);
await expect(server).toReceiveMessage({ type: "disconnect", connectionId: connectionId });
});
test('delete session', async () => {
await wsHandler.remove(client);
});
});
describe('websocket signaling test in private mode', () => {
let server: WS;
let client: WebSocket;
const connectionId = "12345";
beforeAll(async () => {
wsHandler.reset("private");
server = new WS("ws://localhost:1234", { jsonProtocol: true });
client = new WebSocket("ws://localhost:1234");
await server.connected;
});
afterAll(() => {
WS.clean();
});
test('create session', async () => {
await wsHandler.add(client);
});
test('create connection', async () => {
await wsHandler.onConnect(client, connectionId);
await expect(server).toReceiveMessage({ type: "connect", connectionId: connectionId, polite: false });
});
test('delete connection', async () => {
await wsHandler.onDisconnect(client, connectionId);
await expect(server).toReceiveMessage({ type: "disconnect", connectionId: connectionId });
});
test('delete session', async () => {
await wsHandler.remove(client);
});
});

View File

@ -0,0 +1,4 @@
{
"include": ["src/**/*"],
"extends": "./tsconfig.json"
}

View File

@ -1,5 +1,5 @@
{
"include": ["src/**/*"],
"include": ["src/**/*", "test/**/*.ts"],
"exclude": ["node_modules", "**/*.spec.ts"],
"compilerOptions": {
"module": "commonjs",
@ -9,4 +9,4 @@
"outDir":"build",
"rootDir":"src"
}
}
}

View File

@ -3,5 +3,5 @@ npm install
npm run lint
npm run test
npm run dev -- -p 8080 &
sleep 1
sleep 5
npm run newman -- -e ./test/env_macos.postman_environment.json

3
test_webapp_client.cmd Normal file
View File

@ -0,0 +1,3 @@
cd WebApp\client
call npm install
call npm run test

3
test_webapp_client.sh Executable file
View File

@ -0,0 +1,3 @@
cd WebApp/client
npm install
npm run test