switch tests to 'jest' (#2193)

This commit is contained in:
Andrew Stanton-Nurse 2018-05-04 12:51:33 -07:00 committed by GitHub
parent c009e15b0c
commit e3e80b957c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 7134 additions and 1325 deletions

View File

@ -57,14 +57,14 @@
</ItemGroup>
<Copy SourceFiles="@(MsgPack5Files)" DestinationFolder="$(MSBuildProjectDirectory)/wwwroot/lib/msgpack5" />
<ItemGroup>
<JasmineFiles Include="$(MSBuildThisFileDirectory)../node_modules/jasmine-core/lib/jasmine-core/*.js" />
<JasmineFiles Include="$(MSBuildThisFileDirectory)../node_modules/jasmine-core/lib/jasmine-core/*.css" />
<JasmineFiles Include="$(MSBuildThisFileDirectory)node_modules/jasmine-core/lib/jasmine-core/*.js" />
<JasmineFiles Include="$(MSBuildThisFileDirectory)node_modules/jasmine-core/lib/jasmine-core/*.css" />
</ItemGroup>
<Copy SourceFiles="@(JasmineFiles)" DestinationFolder="$(MSBuildProjectDirectory)/wwwroot/lib/jasmine" />
<ItemGroup>
<SignalRJSClientFiles Include="$(MSBuildThisFileDirectory)../signalr/dist/browser/*" />
<SignalRJSClientFiles Include="$(MSBuildThisFileDirectory)../signalr-protocol-msgpack/dist/browser/*" />
<SignalRJSClientFiles Include="$(MSBuildThisFileDirectory)node_modules/@aspnet/signalr/dist/browser/*" />
<SignalRJSClientFiles Include="$(MSBuildThisFileDirectory)node_modules/@aspnet/signalr-protocol-msgpack/dist/browser/*" />
</ItemGroup>
<Copy SourceFiles="@(SignalRJSClientFiles)" DestinationFolder="$(MSBuildThisFileDirectory)/wwwroot/lib/signalr" />
</Target>

View File

@ -4,12 +4,131 @@
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@aspnet/signalr": {
"version": "file:../signalr",
"dependencies": {
"es6-promise": {
"version": "4.2.2",
"bundled": true
}
}
},
"@aspnet/signalr-protocol-msgpack": {
"version": "file:../signalr-protocol-msgpack",
"requires": {
"msgpack5": "4.0.2"
},
"dependencies": {
"@types/bl": {
"version": "0.8.31",
"bundled": true,
"requires": {
"@types/node": "8.5.5"
}
},
"@types/msgpack5": {
"version": "3.4.1",
"bundled": true,
"requires": {
"@types/bl": "0.8.31"
}
},
"@types/node": {
"version": "8.5.5",
"bundled": true
},
"base64-js": {
"version": "1.2.1",
"bundled": true
},
"bl": {
"version": "1.2.2",
"bundled": true,
"requires": {
"readable-stream": "2.3.6",
"safe-buffer": "5.1.1"
}
},
"buffer": {
"version": "5.0.8",
"bundled": true,
"requires": {
"base64-js": "1.2.1",
"ieee754": "1.1.8"
}
},
"core-util-is": {
"version": "1.0.2",
"bundled": true
},
"ieee754": {
"version": "1.1.8",
"bundled": true
},
"inherits": {
"version": "2.0.3",
"bundled": true
},
"isarray": {
"version": "1.0.0",
"bundled": true
},
"msgpack5": {
"version": "4.0.2",
"bundled": true,
"requires": {
"bl": "1.2.2",
"inherits": "2.0.3",
"readable-stream": "2.3.6",
"safe-buffer": "5.1.1"
}
},
"process-nextick-args": {
"version": "2.0.0",
"bundled": true
},
"readable-stream": {
"version": "2.3.6",
"bundled": true,
"requires": {
"core-util-is": "1.0.2",
"inherits": "2.0.3",
"isarray": "1.0.0",
"process-nextick-args": "2.0.0",
"safe-buffer": "5.1.1",
"string_decoder": "1.1.1",
"util-deprecate": "1.0.2"
}
},
"safe-buffer": {
"version": "5.1.1",
"bundled": true
},
"string_decoder": {
"version": "1.1.1",
"bundled": true,
"requires": {
"safe-buffer": "5.1.1"
}
},
"util-deprecate": {
"version": "1.0.2",
"bundled": true
}
}
},
"@types/debug": {
"version": "0.0.30",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-0.0.30.tgz",
"integrity": "sha512-orGL5LXERPYsLov6CWs3Fh6203+dXzJkR7OnddIr2514Hsecwc8xRpzCapshBbKFImCsvS/mk6+FWiN5LyZJAQ==",
"dev": true
},
"@types/jasmine": {
"version": "2.8.6",
"resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-2.8.6.tgz",
"integrity": "sha512-clg9raJTY0EOo5pVZKX3ZlMjlYzVU73L71q5OV1jhE2Uezb7oF94jh4CvwrW6wInquQAdhOxJz5VDF2TLUGmmA==",
"dev": true
},
"@types/node": {
"version": "9.4.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-9.4.6.tgz",
@ -52,6 +171,22 @@
"integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=",
"dev": true
},
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"requires": {
"balanced-match": "1.0.0",
"concat-map": "0.0.1"
}
},
"chalk": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz",
@ -78,6 +213,12 @@
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
"dev": true
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true
},
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
@ -117,6 +258,26 @@
"integrity": "sha1-LUH1Y+H+QA7Uli/hpNXGp1Od9/Y=",
"dev": true
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true
},
"glob": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
"dev": true,
"requires": {
"fs.realpath": "1.0.0",
"inflight": "1.0.6",
"inherits": "2.0.3",
"minimatch": "3.0.4",
"once": "1.4.0",
"path-is-absolute": "1.0.1"
}
},
"has-flag": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz",
@ -132,6 +293,38 @@
"parse-passwd": "1.0.0"
}
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"dev": true,
"requires": {
"once": "1.4.0",
"wrappy": "1.0.2"
}
},
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
"dev": true
},
"jasmine": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.1.0.tgz",
"integrity": "sha1-K9Wf1+xuwOistk4J9Fpo7SrRlSo=",
"dev": true,
"requires": {
"glob": "7.1.2",
"jasmine-core": "3.1.0"
}
},
"jasmine-core": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.1.0.tgz",
"integrity": "sha1-pHheE11d9lAk38kiSVPfWFvSdmw=",
"dev": true
},
"js-yaml": {
"version": "3.11.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.11.0.tgz",
@ -148,6 +341,15 @@
"integrity": "sha512-j3dZCri3cCd23wgPqK/0/KvTN8R+W6fXDqQe8BNLbTpONjbA8SPaRr+q0BQq9bx3Q/+g68/gDIh9FW3by702Tg==",
"dev": true
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"requires": {
"brace-expansion": "1.1.11"
}
},
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
@ -187,12 +389,27 @@
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"dev": true
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"requires": {
"wrappy": "1.0.2"
}
},
"parse-passwd": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz",
"integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=",
"dev": true
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"dev": true
},
"safe-buffer": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
@ -308,6 +525,12 @@
"homedir-polyfill": "1.0.1"
}
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true
},
"yallist": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz",

View File

@ -4,12 +4,17 @@
"version": "1.0.0",
"description": "",
"main": "index.js",
"dependencies": {},
"dependencies": {
"@aspnet/signalr": "file:../signalr",
"@aspnet/signalr-protocol-msgpack": "file:../signalr-protocol-msgpack"
},
"devDependencies": {
"@types/debug": "0.0.30",
"@types/jasmine": "^2.8.6",
"@types/node": "^9.4.6",
"debug": "^3.1.0",
"es6-promise": "^4.2.2",
"jasmine": "^3.1.0",
"tap-parser": "^7.0.0",
"tee": "^0.2.0",
"ts-node": "^4.1.0"

View File

@ -8,7 +8,7 @@ import commonjs from 'rollup-plugin-commonjs'
import resolve from 'rollup-plugin-node-resolve'
export default {
input: path.join(__dirname, "obj", "js", "FunctionalTests", "ts", "index.js"),
input: path.join(__dirname, "obj", "js", "index.js"),
output: {
file: path.join(__dirname, "wwwroot", "dist", "signalr-functional-tests.js"),
format: "iife",

View File

@ -6,7 +6,7 @@ import { eachTransport, ECHOENDPOINT_URL } from "./Common";
import { TestLogger } from "./TestLogger";
// We want to continue testing HttpConnection, but we don't export it anymore. So just pull it in directly from the source file.
import { HttpConnection } from "../../signalr/src/HttpConnection";
import { HttpConnection } from "@aspnet/signalr/dist/esm/HttpConnection";
const commonOptions: IHttpConnectionOptions = {
logMessageContent: true,

View File

@ -116,9 +116,9 @@ describe("hubConnection", () => {
done();
});
const received = [];
const received: string[] = [];
hubConnection.start().then(() => {
hubConnection.stream("Stream").subscribe({
hubConnection.stream<string>("Stream").subscribe({
complete() {
expect(received).toEqual(["a", "b", "c"]);
hubConnection.stop();
@ -686,7 +686,7 @@ describe("hubConnection", () => {
}
});
function getJwtToken(url): Promise<string> {
function getJwtToken(url: string): Promise<string> {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();

View File

@ -2,7 +2,7 @@ import { ILogger, LogLevel } from "@aspnet/signalr";
// Since JavaScript modules are file-based, we can just pull in utilities from the
// main library directly even if they aren't exported.
import { ConsoleLogger } from "../../signalr/src/Utils";
import { ConsoleLogger } from "@aspnet/signalr/dist/esm/Utils";
export class TestLog {
public messages: Array<[Date, LogLevel, string]> = [];

View File

@ -4,6 +4,7 @@
console.log("SignalR Functional Tests Loaded");
import "es6-promise/dist/es6-promise.auto.js";
import "./ConnectionTests";
import "./HubConnectionTests";
import "./WebDriverReporter";
import "./WebSocketTests";

View File

@ -1,27 +1,12 @@
{
"compileOnSave": false,
"extends": "../tsconfig.base.json",
"compilerOptions": {
"noImplicitAny": false,
"noEmitOnError": true,
"removeComments": false,
"sourceMap": true,
"target": "es5",
"module": "es2015",
"outDir": "./obj/js",
"baseUrl": ".",
"paths": {
"@aspnet/signalr": [ "../signalr/dist/esm/index" ],
"@aspnet/signalr-protocol-msgpack": [ "../signalr-protocol-msgpack/dist/esm/index" ]
},
"lib": [ "es2015.promise", "es5", "dom", "es2015.collection" ]
"typeRoots": [
"./node_modules/@types/"
]
},
"include": [
"./ts/**/*",
"../signalr/dist/esm/**/*.d.ts"
],
"exclude": [
"node_modules",
"wwwroot",
"js"
"./ts/**/*"
]
}
}

File diff suppressed because it is too large Load Diff

View File

@ -5,21 +5,47 @@
"main": "index.js",
"scripts": {
"build": "cd ./signalr && npm run build && cd ../signalr-protocol-msgpack && npm run build",
"test": "cd ./signalr && npm run test && cd ../signalr-protocol-msgpack && npm run test"
"test": "jest"
},
"author": "Microsoft",
"license": "Apache-2.0",
"devDependencies": {
"@types/jasmine": "^2.8.3",
"@types/jest": "^22.2.3",
"@types/node": "^8.5.2",
"jasmine": "^2.8.0",
"jest": "^22.4.3",
"rimraf": "^2.6.2",
"rollup": "^0.53.4",
"rollup-plugin-commonjs": "^8.2.6",
"rollup-plugin-node-resolve": "^3.0.2",
"rollup-plugin-sourcemaps": "^0.4.2",
"ts-jest": "^22.4.4",
"tslint": "^5.9.1",
"typescript": "^2.7.1",
"uglify-js": "^3.3.5"
},
"jest": {
"globals": {
"ts-jest": {
"tsConfigFile": "./tsconfig.jest.json",
"skipBabel": true,
"enableTsDiagnostics": true
}
},
"transform": {
"^.+\\.tsx?$": "ts-jest"
},
"testEnvironment": "node",
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
"moduleNameMapper": {
"^@aspnet/signalr$": "<rootDir>/signalr/src/index.ts"
},
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
]
}
}

View File

@ -14,12 +14,11 @@
"clean": "node ../node_modules/rimraf/bin.js ./dist",
"build": "npm run clean && npm run build:lint && npm run build:esm && npm run build:cjs && npm run build:browser && npm run build:uglify",
"build:lint": "node ../node_modules/tslint/bin/tslint -c ../tslint.json -p ./tsconfig.json",
"build:esm": "node ../node_modules/typescript/bin/tsc --project ./tsconfig.json --module es2015 --outDir ./dist/esm --target ES5 -d",
"build:cjs": "node ../node_modules/typescript/bin/tsc --project ./tsconfig.json --module commonjs --outDir ./dist/cjs --target ES5",
"build:esm": "node ../node_modules/typescript/bin/tsc --project ./tsconfig.json --module es2015 --outDir ./dist/esm -d",
"build:cjs": "node ../node_modules/typescript/bin/tsc --project ./tsconfig.json --module commonjs --outDir ./dist/cjs",
"build:browser": "node ../node_modules/rollup/bin/rollup -c",
"build:uglify": "node ../node_modules/uglify-js/bin/uglifyjs --source-map \"url='signalr-protocol-msgpack.min.js.map',content='./dist/browser/signalr-protocol-msgpack.js.map'\" --comments -o ./dist/browser/signalr-protocol-msgpack.min.js ./dist/browser/signalr-protocol-msgpack.js",
"pretest": "node ../node_modules/rimraf/bin.js ./spec/obj && node ../node_modules/typescript/bin/tsc --project ./spec/tsconfig.json && cd ./spec/obj && npm init -y && npm install ../../../signalr",
"test": "node ../node_modules/jasmine/bin/jasmine.js ./spec/obj/spec/**/*.spec.js"
"test": "echo \"Run 'npm test' in the 'clients\\ts' folder to test this package\" && exit 1"
},
"keywords": [
"signalr",

View File

@ -1,18 +0,0 @@
{
"compileOnSave": false,
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"moduleResolution": "node",
"outDir": "./obj",
"lib": [ "es2015", "dom" ],
"baseUrl": ".",
"paths": {
"@aspnet/*": [ "../../*" ]
}
},
"include": [
"./**/*",
"../../typings/**/*"
]
}

View File

@ -18,16 +18,16 @@ describe("Binary Message Formatter", () => {
});
([
[[0x80], new Error("Cannot read message size.")],
[[0x02, 0x01, 0x80, 0x80], new Error("Cannot read message size.")],
[[0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80], new Error("Cannot read message size.")], // the size of the second message is cut
[[0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01], new Error("Incomplete message.")], // second message has only size
[[0xff, 0xff, 0xff, 0xff, 0xff], new Error("Messages bigger than 2GB are not supported.")],
[[0x80, 0x80, 0x80, 0x80, 0x08], new Error("Messages bigger than 2GB are not supported.")],
[[0x80, 0x80, 0x80, 0x80, 0x80], new Error("Messages bigger than 2GB are not supported.")],
[[0x02, 0x00], new Error("Incomplete message.")],
[[0xff, 0xff, 0xff, 0xff, 0x07], new Error("Incomplete message.")],
] as Array<[number[], Error]>).forEach(([payload, expectedError]) => {
[[0x80], "Cannot read message size."],
[[0x02, 0x01, 0x80, 0x80], "Cannot read message size."],
[[0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80], "Cannot read message size."], // the size of the second message is cut
[[0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01], "Incomplete message."], // second message has only size
[[0xff, 0xff, 0xff, 0xff, 0xff], "Messages bigger than 2GB are not supported."],
[[0x80, 0x80, 0x80, 0x80, 0x08], "Messages bigger than 2GB are not supported."],
[[0x80, 0x80, 0x80, 0x80, 0x80], "Messages bigger than 2GB are not supported."],
[[0x02, 0x00], "Incomplete message."],
[[0xff, 0xff, 0xff, 0xff, 0x07], "Incomplete message."],
] as Array<[number[], string]>).forEach(([payload, expectedError]) => {
it(`should fail to parse '${payload}'`, () => {
expect(() => BinaryMessageFormat.parse(new Uint8Array(payload).buffer)).toThrow(expectedError);
});

View File

@ -146,16 +146,16 @@ describe("MessageHubProtocol", () => {
}));
([
["message with no payload", [0x00], new Error("Invalid payload.")],
["message with empty array", [0x01, 0x90], new Error("Invalid payload.")],
["message without outer array", [0x01, 0xc2], new Error("Invalid payload.")],
["message with invalid headers", [0x03, 0x92, 0x01, 0x05], new Error("Invalid headers.")],
["Invocation message with invalid invocation id", [0x03, 0x92, 0x01, 0x80], new Error("Invalid payload for Invocation message.")],
["StreamItem message with invalid invocation id", [0x03, 0x92, 0x02, 0x80], new Error("Invalid payload for StreamItem message.")],
["Completion message with invalid invocation id", [0x04, 0x93, 0x03, 0x80, 0xa0], new Error("Invalid payload for Completion message.")],
["Completion message with missing result", [0x05, 0x94, 0x03, 0x80, 0xa0, 0x01], new Error("Invalid payload for Completion message.")],
["Completion message with missing error", [0x05, 0x94, 0x03, 0x80, 0xa0, 0x03], new Error("Invalid payload for Completion message.")],
] as Array<[string, number[], Error]>).forEach(([name, payload, expectedError]) =>
["message with no payload", [0x00], "Invalid payload."],
["message with empty array", [0x01, 0x90], "Invalid payload."],
["message without outer array", [0x01, 0xc2], "Invalid payload."],
["message with invalid headers", [0x03, 0x92, 0x01, 0x05], "Invalid headers."],
["Invocation message with invalid invocation id", [0x03, 0x92, 0x01, 0x80], "Invalid payload for Invocation message."],
["StreamItem message with invalid invocation id", [0x03, 0x92, 0x02, 0x80], "Invalid payload for StreamItem message."],
["Completion message with invalid invocation id", [0x04, 0x93, 0x03, 0x80, 0xa0], "Invalid payload for Completion message."],
["Completion message with missing result", [0x05, 0x94, 0x03, 0x80, 0xa0, 0x01], "Invalid payload for Completion message."],
["Completion message with missing error", [0x05, 0x94, 0x03, 0x80, 0xa0, 0x03], "Invalid payload for Completion message."],
] as Array<[string, number[], string]>).forEach(([name, payload, expectedError]) =>
it("throws for " + name, () => {
expect(() => new MessagePackHubProtocol().parseMessages(new Uint8Array(payload).buffer, NullLogger.instance))
.toThrow(expectedError);

View File

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

View File

@ -1,15 +1,7 @@
{
"extends": "../tsconfig-base.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@aspnet/signalr": [
"../signalr"
]
}
},
"extends": "../tsconfig.base.json",
"include": [
"./src/**/*",
"../typings/**/*"
"./src/**/*"
]
}

View File

@ -11,15 +11,14 @@
"test": "spec"
},
"scripts": {
"clean": "node ../node_modules/rimraf/bin.js ./dist ./.rpt2_cache",
"clean": "node ../node_modules/rimraf/bin.js ./dist",
"build": "npm run clean && npm run build:lint && npm run build:esm && npm run build:cjs && npm run build:browser && npm run build:uglify",
"build:lint": "node ../node_modules/tslint/bin/tslint -c ../tslint.json -p ./tsconfig.json",
"build:esm": "node ../node_modules/typescript/bin/tsc --project ./tsconfig.json --module es2015 --outDir ./dist/esm --target ES5 -d && node ./build/process-dts.js",
"build:cjs": "node ../node_modules/typescript/bin/tsc --project ./tsconfig.json --module commonjs --outDir ./dist/cjs --target ES5",
"build:esm": "node ../node_modules/typescript/bin/tsc --project ./tsconfig.json --module es2015 --outDir ./dist/esm -d && node ./build/process-dts.js",
"build:cjs": "node ../node_modules/typescript/bin/tsc --project ./tsconfig.json --module commonjs --outDir ./dist/cjs",
"build:browser": "node ../node_modules/rollup/bin/rollup -c",
"build:uglify": "node ../node_modules/uglify-js/bin/uglifyjs --source-map \"url='signalr.min.js.map',content='./dist/browser/signalr.js.map'\" --comments -o ./dist/browser/signalr.min.js ./dist/browser/signalr.js",
"pretest": "node ../node_modules/rimraf/bin.js ./spec/obj && node ../node_modules/typescript/bin/tsc --project ./spec/tsconfig.json",
"test": "node ../node_modules/jasmine/bin/jasmine.js ./spec/obj/spec/**/*.spec.js"
"test": "echo \"Run 'npm test' in the 'clients\\ts' folder to test this package\" && exit 1"
},
"repository": {
"type": "git",

View File

@ -1,931 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
import { HubConnection } from "../src/HubConnection";
import { IConnection } from "../src/IConnection";
import { HubMessage, IHubProtocol, MessageType } from "../src/IHubProtocol";
import { ILogger, LogLevel } from "../src/ILogger";
import { TransferFormat } from "../src/ITransport";
import { JsonHubProtocol } from "../src/JsonHubProtocol";
import { NullLogger } from "../src/Loggers";
import { IStreamSubscriber } from "../src/Stream";
import { TextMessageFormat } from "../src/TextMessageFormat";
import { asyncit as it, captureException, delay, PromiseSource } from "./Utils";
function createHubConnection(connection: IConnection, logger?: ILogger, protocol?: IHubProtocol) {
return HubConnection.create(connection, logger || NullLogger.instance, protocol || new JsonHubProtocol());
}
describe("HubConnection", () => {
describe("start", () => {
it("sends negotiation message", async () => {
const connection = new TestConnection();
const hubConnection = createHubConnection(connection);
await hubConnection.start();
expect(connection.sentData.length).toBe(1);
expect(JSON.parse(connection.sentData[0])).toEqual({
protocol: "json",
version: 1,
});
await hubConnection.stop();
});
});
describe("send", () => {
it("sends a non blocking invocation", async () => {
const connection = new TestConnection();
const hubConnection = createHubConnection(connection);
const invokePromise = hubConnection.send("testMethod", "arg", 42)
.catch((_) => { }); // Suppress exception and unhandled promise rejection warning.
// Verify the message is sent
expect(connection.sentData.length).toBe(1);
expect(JSON.parse(connection.sentData[0])).toEqual({
arguments: [
"arg",
42,
],
target: "testMethod",
type: MessageType.Invocation,
});
// Close the connection
hubConnection.stop();
});
});
describe("invoke", () => {
it("sends an invocation", async () => {
const connection = new TestConnection();
const hubConnection = createHubConnection(connection);
const invokePromise = hubConnection.invoke("testMethod", "arg", 42)
.catch((_) => { }); // Suppress exception and unhandled promise rejection warning.
// Verify the message is sent
expect(connection.sentData.length).toBe(1);
expect(JSON.parse(connection.sentData[0])).toEqual({
arguments: [
"arg",
42,
],
invocationId: connection.lastInvocationId,
target: "testMethod",
type: MessageType.Invocation,
});
// Close the connection
hubConnection.stop();
});
it("can process handshake from text", async () => {
let protocolCalled = false;
const mockProtocol = new TestProtocol(TransferFormat.Text);
mockProtocol.onreceive = (d) => {
protocolCalled = true;
};
const connection = new TestConnection();
const hubConnection = createHubConnection(connection, null, mockProtocol);
const data = "{}" + TextMessageFormat.RecordSeparator;
connection.receiveText(data);
// message only contained handshake response
expect(protocolCalled).toEqual(false);
});
it("can process handshake from binary", async () => {
let protocolCalled = false;
const mockProtocol = new TestProtocol(TransferFormat.Binary);
mockProtocol.onreceive = (d) => {
protocolCalled = true;
};
const connection = new TestConnection();
const hubConnection = createHubConnection(connection, null, mockProtocol);
// handshake response + message separator
const data = [0x7b, 0x7d, 0x1e];
connection.receiveBinary(new Uint8Array(data).buffer);
// message only contained handshake response
expect(protocolCalled).toEqual(false);
});
it("can process handshake and additional messages from binary", async () => {
let receivedProcotolData: ArrayBuffer;
const mockProtocol = new TestProtocol(TransferFormat.Binary);
mockProtocol.onreceive = (d) => receivedProcotolData = d as ArrayBuffer;
const connection = new TestConnection();
const hubConnection = createHubConnection(connection, null, mockProtocol);
// handshake response + message separator + message pack message
const data = [
0x7b, 0x7d, 0x1e, 0x65, 0x95, 0x03, 0x80, 0xa1, 0x30, 0x01, 0xd9, 0x5d, 0x54, 0x68, 0x65, 0x20, 0x63, 0x6c,
0x69, 0x65, 0x6e, 0x74, 0x20, 0x61, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20,
0x69, 0x6e, 0x76, 0x6f, 0x6b, 0x65, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x69,
0x6e, 0x67, 0x20, 0x27, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x27, 0x20, 0x6d,
0x65, 0x74, 0x68, 0x6f, 0x64, 0x20, 0x69, 0x6e, 0x20, 0x61, 0x20, 0x6e, 0x6f, 0x6e, 0x2d, 0x73, 0x74, 0x72,
0x65, 0x61, 0x6d, 0x69, 0x6e, 0x67, 0x20, 0x66, 0x61, 0x73, 0x68, 0x69, 0x6f, 0x6e, 0x2e,
];
connection.receiveBinary(new Uint8Array(data).buffer);
// left over data is the message pack message
expect(receivedProcotolData.byteLength).toEqual(102);
});
it("can process handshake and additional messages from text", async () => {
let receivedProcotolData: string;
const mockProtocol = new TestProtocol(TransferFormat.Text);
mockProtocol.onreceive = (d) => receivedProcotolData = d as string;
const connection = new TestConnection();
const hubConnection = createHubConnection(connection, null, mockProtocol);
const data = "{}" + TextMessageFormat.RecordSeparator + "{\"type\":6}" + TextMessageFormat.RecordSeparator;
connection.receiveText(data);
expect(receivedProcotolData).toEqual("{\"type\":6}" + TextMessageFormat.RecordSeparator);
});
it("rejects the promise when an error is received", async () => {
const connection = new TestConnection();
const hubConnection = createHubConnection(connection);
connection.receiveHandshakeResponse();
const invokePromise = hubConnection.invoke("testMethod", "arg", 42);
connection.receive({ type: MessageType.Completion, invocationId: connection.lastInvocationId, error: "foo" });
const ex = await captureException(async () => invokePromise);
expect(ex.message).toBe("foo");
});
it("resolves the promise when a result is received", async () => {
const connection = new TestConnection();
const hubConnection = createHubConnection(connection);
connection.receiveHandshakeResponse();
const invokePromise = hubConnection.invoke("testMethod", "arg", 42);
connection.receive({ type: MessageType.Completion, invocationId: connection.lastInvocationId, result: "foo" });
expect(await invokePromise).toBe("foo");
});
it("completes pending invocations when stopped", async () => {
const connection = new TestConnection();
const hubConnection = createHubConnection(connection);
connection.receiveHandshakeResponse();
const invokePromise = hubConnection.invoke("testMethod");
hubConnection.stop();
const ex = await captureException(async () => await invokePromise);
expect(ex.message).toBe("Invocation canceled due to connection being closed.");
});
it("completes pending invocations when connection is lost", async () => {
const connection = new TestConnection();
const hubConnection = createHubConnection(connection);
connection.receiveHandshakeResponse();
const invokePromise = hubConnection.invoke("testMethod");
// Typically this would be called by the transport
connection.onclose(new Error("Connection lost"));
const ex = await captureException(async () => await invokePromise);
expect(ex.message).toBe("Connection lost");
});
});
describe("on", () => {
it("invocations ignored in callbacks not registered", async () => {
const warnings: string[] = [];
const logger = {
log: (logLevel: LogLevel, message: string) => {
if (logLevel === LogLevel.Warning) {
warnings.push(message);
}
},
} as ILogger;
const connection = new TestConnection();
const hubConnection = createHubConnection(connection, logger);
connection.receiveHandshakeResponse();
connection.receive({
arguments: ["test"],
nonblocking: true,
target: "message",
type: MessageType.Invocation,
});
expect(warnings).toEqual(["No client method with the name 'message' found."]);
});
it("invocations ignored in callbacks that have registered then unregistered", async () => {
const warnings: string[] = [];
const logger = {
log: (logLevel: LogLevel, message: string) => {
if (logLevel === LogLevel.Warning) {
warnings.push(message);
}
},
} as ILogger;
const connection = new TestConnection();
const hubConnection = createHubConnection(connection, logger);
connection.receiveHandshakeResponse();
const handler = () => { };
hubConnection.on("message", handler);
hubConnection.off("message", handler);
connection.receive({
arguments: ["test"],
invocationId: "0",
nonblocking: true,
target: "message",
type: MessageType.Invocation,
});
expect(warnings).toEqual(["No client method with the name 'message' found."]);
});
it("all handlers can be unregistered with just the method name", async () => {
const connection = new TestConnection();
const hubConnection = createHubConnection(connection);
connection.receiveHandshakeResponse();
let count = 0;
const handler = () => { count++; };
const secondHandler = () => { count++; };
hubConnection.on("inc", handler);
hubConnection.on("inc", secondHandler);
connection.receive({
arguments: [],
invocationId: "0",
nonblocking: true,
target: "inc",
type: MessageType.Invocation,
});
hubConnection.off("inc");
connection.receive({
arguments: [],
invocationId: "0",
nonblocking: true,
target: "inc",
type: MessageType.Invocation,
});
expect(count).toBe(2);
});
it("a single handler can be unregistered with the method name and handler", async () => {
const connection = new TestConnection();
const hubConnection = createHubConnection(connection);
connection.receiveHandshakeResponse();
let count = 0;
const handler = () => { count++; };
const secondHandler = () => { count++; };
hubConnection.on("inc", handler);
hubConnection.on("inc", secondHandler);
connection.receive({
arguments: [],
invocationId: "0",
nonblocking: true,
target: "inc",
type: MessageType.Invocation,
});
hubConnection.off("inc", handler);
connection.receive({
arguments: [],
invocationId: "0",
nonblocking: true,
target: "inc",
type: MessageType.Invocation,
});
expect(count).toBe(3);
});
it("can't register the same handler multiple times", async () => {
const connection = new TestConnection();
const hubConnection = createHubConnection(connection);
connection.receiveHandshakeResponse();
let count = 0;
const handler = () => { count++; };
hubConnection.on("inc", handler);
hubConnection.on("inc", handler);
connection.receive({
arguments: [],
invocationId: "0",
nonblocking: true,
target: "inc",
type: MessageType.Invocation,
});
expect(count).toBe(1);
});
it("callback invoked when servers invokes a method on the client", async () => {
const connection = new TestConnection();
const hubConnection = createHubConnection(connection);
connection.receiveHandshakeResponse();
let value = "";
hubConnection.on("message", (v) => value = v);
connection.receive({
arguments: ["test"],
invocationId: "0",
nonblocking: true,
target: "message",
type: MessageType.Invocation,
});
expect(value).toBe("test");
});
it("stop on handshake error", async () => {
const connection = new TestConnection();
const hubConnection = createHubConnection(connection);
let closeError: Error = null;
hubConnection.onclose((e) => closeError = e);
connection.receiveHandshakeResponse("Error!");
expect(closeError.message).toEqual("Server returned handshake error: Error!");
});
it("stop on close message", async () => {
const connection = new TestConnection();
const hubConnection = createHubConnection(connection);
let isClosed = false;
let closeError: Error = null;
hubConnection.onclose((e) => {
isClosed = true;
closeError = e;
});
connection.receiveHandshakeResponse();
connection.receive({
type: MessageType.Close,
});
expect(isClosed).toEqual(true);
expect(closeError).toEqual(null);
});
it("stop on error close message", async () => {
const connection = new TestConnection();
const hubConnection = createHubConnection(connection);
let isClosed = false;
let closeError: Error = null;
hubConnection.onclose((e) => {
isClosed = true;
closeError = e;
});
connection.receiveHandshakeResponse();
connection.receive({
error: "Error!",
type: MessageType.Close,
});
expect(isClosed).toEqual(true);
expect(closeError.message).toEqual("Server returned an error on close: Error!");
});
it("can have multiple callbacks", async () => {
const connection = new TestConnection();
const hubConnection = createHubConnection(connection);
connection.receiveHandshakeResponse();
let numInvocations1 = 0;
let numInvocations2 = 0;
hubConnection.on("message", () => numInvocations1++);
hubConnection.on("message", () => numInvocations2++);
connection.receive({
arguments: [],
invocationId: "0",
nonblocking: true,
target: "message",
type: MessageType.Invocation,
});
expect(numInvocations1).toBe(1);
expect(numInvocations2).toBe(1);
});
it("can unsubscribe from on", async () => {
const connection = new TestConnection();
const hubConnection = createHubConnection(connection);
connection.receiveHandshakeResponse();
let numInvocations = 0;
const callback = () => numInvocations++;
hubConnection.on("message", callback);
connection.receive({
arguments: [],
invocationId: "0",
nonblocking: true,
target: "message",
type: MessageType.Invocation,
});
hubConnection.off("message", callback);
connection.receive({
arguments: [],
invocationId: "0",
nonblocking: true,
target: "message",
type: MessageType.Invocation,
});
expect(numInvocations).toBe(1);
});
it("unsubscribing from non-existing callbacks no-ops", async () => {
const connection = new TestConnection();
const hubConnection = createHubConnection(connection);
hubConnection.off("_", () => { });
hubConnection.on("message", (t) => { });
hubConnection.on("message", () => { });
});
it("using null/undefined for methodName or method no-ops", async () => {
const warnings: string[] = [];
const logger = {
log(logLevel: LogLevel, message: string) {
if (logLevel === LogLevel.Warning) {
warnings.push(message);
}
},
} as ILogger;
const connection = new TestConnection();
const hubConnection = createHubConnection(connection, logger);
connection.receiveHandshakeResponse();
hubConnection.on(null, undefined);
hubConnection.on(undefined, null);
hubConnection.on("message", null);
hubConnection.on("message", undefined);
hubConnection.on(null, () => { });
hubConnection.on(undefined, () => { });
// invoke a method to make sure we are not trying to use null/undefined
connection.receive({
arguments: [],
invocationId: "0",
nonblocking: true,
target: "message",
type: MessageType.Invocation,
});
expect(warnings).toEqual(["No client method with the name 'message' found."]);
hubConnection.off(null, undefined);
hubConnection.off(undefined, null);
hubConnection.off("message", null);
hubConnection.off("message", undefined);
hubConnection.off(null, () => { });
hubConnection.off(undefined, () => { });
});
});
describe("stream", () => {
it("sends an invocation", async () => {
const connection = new TestConnection();
const hubConnection = createHubConnection(connection);
const invokePromise = hubConnection.stream("testStream", "arg", 42);
// Verify the message is sent
expect(connection.sentData.length).toBe(1);
expect(JSON.parse(connection.sentData[0])).toEqual({
arguments: [
"arg",
42,
],
invocationId: connection.lastInvocationId,
target: "testStream",
type: MessageType.StreamInvocation,
});
// Close the connection
hubConnection.stop();
});
it("completes with an error when an error is yielded", async () => {
const connection = new TestConnection();
const hubConnection = createHubConnection(connection);
connection.receiveHandshakeResponse();
const observer = new TestObserver();
hubConnection.stream<any>("testMethod", "arg", 42)
.subscribe(observer);
connection.receive({ type: MessageType.Completion, invocationId: connection.lastInvocationId, error: "foo" });
const ex = await captureException(async () => await observer.completed);
expect(ex.message).toEqual("Error: foo");
});
it("completes the observer when a completion is received", async () => {
const connection = new TestConnection();
const hubConnection = createHubConnection(connection);
connection.receiveHandshakeResponse();
const observer = new TestObserver();
hubConnection.stream<any>("testMethod", "arg", 42)
.subscribe(observer);
connection.receive({ type: MessageType.Completion, invocationId: connection.lastInvocationId });
expect(await observer.completed).toEqual([]);
});
it("completes pending streams when stopped", async () => {
const connection = new TestConnection();
const hubConnection = createHubConnection(connection);
const observer = new TestObserver();
hubConnection.stream<any>("testMethod")
.subscribe(observer);
hubConnection.stop();
const ex = await captureException(async () => await observer.completed);
expect(ex.message).toEqual("Error: Invocation canceled due to connection being closed.");
});
it("completes pending streams when connection is lost", async () => {
const connection = new TestConnection();
const hubConnection = createHubConnection(connection);
const observer = new TestObserver();
hubConnection.stream<any>("testMethod")
.subscribe(observer);
// Typically this would be called by the transport
connection.onclose(new Error("Connection lost"));
const ex = await captureException(async () => await observer.completed);
expect(ex.message).toEqual("Error: Connection lost");
});
it("yields items as they arrive", async () => {
const connection = new TestConnection();
const hubConnection = createHubConnection(connection);
connection.receiveHandshakeResponse();
const observer = new TestObserver();
hubConnection.stream<any>("testMethod")
.subscribe(observer);
connection.receive({ type: MessageType.StreamItem, invocationId: connection.lastInvocationId, item: 1 });
expect(observer.itemsReceived).toEqual([1]);
connection.receive({ type: MessageType.StreamItem, invocationId: connection.lastInvocationId, item: 2 });
expect(observer.itemsReceived).toEqual([1, 2]);
connection.receive({ type: MessageType.StreamItem, invocationId: connection.lastInvocationId, item: 3 });
expect(observer.itemsReceived).toEqual([1, 2, 3]);
connection.receive({ type: MessageType.Completion, invocationId: connection.lastInvocationId });
expect(await observer.completed).toEqual([1, 2, 3]);
});
it("does not require error function registered", async () => {
const connection = new TestConnection();
const hubConnection = createHubConnection(connection);
const observer = hubConnection.stream("testMethod").subscribe(NullSubscriber.instance);
// Typically this would be called by the transport
// triggers observer.error()
connection.onclose(new Error("Connection lost"));
});
it("does not require complete function registered", async () => {
const connection = new TestConnection();
const hubConnection = createHubConnection(connection);
const observer = hubConnection.stream("testMethod").subscribe(NullSubscriber.instance);
// Send completion to trigger observer.complete()
// Expectation is connection.receive will not to throw
connection.receive({ type: MessageType.Completion, invocationId: connection.lastInvocationId });
});
it("can be canceled", () => {
const connection = new TestConnection();
const hubConnection = createHubConnection(connection);
connection.receiveHandshakeResponse();
const observer = new TestObserver();
const subscription = hubConnection.stream("testMethod")
.subscribe(observer);
connection.receive({ type: MessageType.StreamItem, invocationId: connection.lastInvocationId, item: 1 });
expect(observer.itemsReceived).toEqual([1]);
subscription.dispose();
connection.receive({ type: MessageType.StreamItem, invocationId: connection.lastInvocationId, item: 2 });
// Observer should no longer receive messages
expect(observer.itemsReceived).toEqual([1]);
// Verify the cancel is sent
expect(connection.sentData.length).toBe(2);
expect(JSON.parse(connection.sentData[1])).toEqual({
invocationId: connection.lastInvocationId,
type: MessageType.CancelInvocation,
});
});
});
describe("onClose", () => {
it("can have multiple callbacks", async () => {
const connection = new TestConnection();
const hubConnection = createHubConnection(connection);
let invocations = 0;
hubConnection.onclose((e) => invocations++);
hubConnection.onclose((e) => invocations++);
// Typically this would be called by the transport
connection.onclose();
expect(invocations).toBe(2);
});
it("callbacks receive error", async () => {
const connection = new TestConnection();
const hubConnection = createHubConnection(connection);
let error: Error;
hubConnection.onclose((e) => error = e);
// Typically this would be called by the transport
connection.onclose(new Error("Test error."));
expect(error.message).toBe("Test error.");
});
it("ignores null callbacks", async () => {
const connection = new TestConnection();
const hubConnection = createHubConnection(connection);
hubConnection.onclose(null);
hubConnection.onclose(undefined);
// Typically this would be called by the transport
connection.onclose();
// expect no errors
});
});
describe("keepAlive", () => {
it("can receive ping messages", async () => {
// Receive the ping mid-invocation so we can see that the rest of the flow works fine
const connection = new TestConnection();
const hubConnection = createHubConnection(connection);
const invokePromise = hubConnection.invoke("testMethod", "arg", 42);
connection.receive({ type: MessageType.Ping });
connection.receive({ type: MessageType.Completion, invocationId: connection.lastInvocationId, result: "foo" });
expect(await invokePromise).toBe("foo");
});
it("does not terminate if messages are received", async () => {
const connection = new TestConnection();
const hubConnection = createHubConnection(connection);
hubConnection.serverTimeoutInMilliseconds = 100;
const p = new PromiseSource<Error>();
hubConnection.onclose((e) => p.resolve(e));
await hubConnection.start();
await connection.receive({ type: MessageType.Ping });
await delay(50);
await connection.receive({ type: MessageType.Ping });
await delay(50);
await connection.receive({ type: MessageType.Ping });
await delay(50);
await connection.receive({ type: MessageType.Ping });
await delay(50);
connection.stop();
const error = await p.promise;
expect(error).toBeUndefined();
});
it("does not timeout if message was received before HubConnection.start", async () => {
const connection = new TestConnection();
const hubConnection = createHubConnection(connection);
hubConnection.serverTimeoutInMilliseconds = 100;
const p = new PromiseSource<Error>();
hubConnection.onclose((e) => p.resolve(e));
// send message before start to trigger timeout handler
// testing for regression where we didn't cleanup timer if request received before start created a timer
await connection.receive({ type: MessageType.Ping });
await hubConnection.start();
await connection.receive({ type: MessageType.Ping });
await delay(50);
await connection.receive({ type: MessageType.Ping });
await delay(50);
await connection.receive({ type: MessageType.Ping });
await delay(50);
connection.stop();
const error = await p.promise;
expect(error).toBeUndefined();
});
it("terminates if no messages received within timeout interval", async () => {
const connection = new TestConnection();
const hubConnection = createHubConnection(connection);
hubConnection.serverTimeoutInMilliseconds = 100;
const p = new PromiseSource<Error>();
hubConnection.onclose((e) => p.resolve(e));
await hubConnection.start();
const error = await p.promise;
expect(error).toEqual(new Error("Server timeout elapsed without receiving a message from the server."));
});
});
});
class TestConnection implements IConnection {
public readonly features: any = {};
public start(): Promise<void> {
return Promise.resolve();
}
public send(data: any): Promise<void> {
const invocation = TextMessageFormat.parse(data)[0];
const invocationId = JSON.parse(invocation).invocationId;
if (invocationId) {
this.lastInvocationId = invocationId;
}
if (this.sentData) {
this.sentData.push(invocation);
} else {
this.sentData = [invocation];
}
return Promise.resolve();
}
public stop(error?: Error): Promise<void> {
if (this.onclose) {
this.onclose(error);
}
return Promise.resolve();
}
public receiveHandshakeResponse(error?: string): void {
this.receive({ error });
}
public receive(data: any): void {
const payload = JSON.stringify(data);
this.onreceive(TextMessageFormat.write(payload));
}
public receiveText(data: string) {
this.onreceive(data);
}
public receiveBinary(data: ArrayBuffer) {
this.onreceive(data);
}
public onreceive: (data: string | ArrayBuffer) => void;
public onclose: (error?: Error) => void;
public sentData: any[];
public lastInvocationId: string;
}
class TestProtocol implements IHubProtocol {
public readonly name: string = "TestProtocol";
public readonly version: number = 1;
public readonly transferFormat: TransferFormat;
public onreceive: (data: string | ArrayBuffer) => void;
constructor(transferFormat: TransferFormat) {
this.transferFormat = transferFormat;
}
public parseMessages(input: any): HubMessage[] {
if (this.onreceive) {
this.onreceive(input);
}
return [];
}
public writeMessage(message: HubMessage): any {
}
}
class TestObserver implements IStreamSubscriber<any> {
public readonly closed: boolean;
public itemsReceived: [any];
private itemsSource: PromiseSource<[any]>;
get completed(): Promise<[any]> {
return this.itemsSource.promise;
}
constructor() {
this.itemsReceived = [] as [any];
this.itemsSource = new PromiseSource<[any]>();
}
public next(value: any) {
this.itemsReceived.push(value);
}
public error(err: any) {
this.itemsSource.reject(new Error(err));
}
public complete() {
this.itemsSource.resolve(this.itemsReceived);
}
}
class NullSubscriber<T> implements IStreamSubscriber<T> {
public static instance: NullSubscriber<any> = new NullSubscriber();
private constructor() {
}
public next(value: T): void {
}
public error(err: any): void {
}
public complete(): void {
}
}

View File

@ -1,15 +0,0 @@
{
"compileOnSave": false,
"compilerOptions": {
"module": "commonjs",
"target": "es2016",
"sourceMap": true,
"moduleResolution": "node",
"outDir": "./obj",
"lib": [ "es2016", "dom" ]
},
"include": [
"./**/*",
"../../typings/**/*"
]
}

View File

@ -17,14 +17,14 @@ const enum ConnectionState {
Disconnected,
}
interface INegotiateResponse {
connectionId: string;
availableTransports: IAvailableTransport[];
url: string;
accessToken: string;
export interface INegotiateResponse {
connectionId?: string;
availableTransports?: IAvailableTransport[];
url?: string;
accessToken?: string;
}
interface IAvailableTransport {
export interface IAvailableTransport {
transport: keyof typeof HttpTransportType;
transferFormats: Array<keyof typeof TransferFormat>;
}

View File

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
import { AbortController } from "../src/AbortController";
import { asyncit as it } from "./Utils";
describe("AbortSignal", () => {
describe("aborted", () => {

View File

@ -3,7 +3,6 @@
import { HttpRequest } from "../src/HttpClient";
import { TestHttpClient } from "./TestHttpClient";
import { asyncit as it } from "./Utils";
describe("HttpClient", () => {
describe("get", () => {

View File

@ -2,30 +2,44 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
import { HttpResponse } from "../src/HttpClient";
import { HttpConnection } from "../src/HttpConnection";
import { HttpConnection, INegotiateResponse } from "../src/HttpConnection";
import { IHttpConnectionOptions } from "../src/IHttpConnectionOptions";
import { HttpTransportType, ITransport, TransferFormat } from "../src/ITransport";
import { HttpError } from "../src/Errors";
import { LogLevel } from "../src/ILogger";
import { eachEndpointUrl, eachTransport } from "./Common";
import { TestHttpClient } from "./TestHttpClient";
import { PromiseSource } from "./Utils";
const commonOptions: IHttpConnectionOptions = {
logger: null,
};
const defaultConnectionId = "abc123";
const defaultNegotiateResponse: INegotiateResponse = {
availableTransports: [
{ transport: "WebSockets", transferFormats: ["Text", "Binary"] },
{ transport: "ServerSentEvents", transferFormats: ["Text"] },
{ transport: "LongPolling", transferFormats: ["Text", "Binary"] },
],
connectionId: defaultConnectionId,
};
describe("HttpConnection", () => {
it("cannot be created with relative url if document object is not present", () => {
expect(() => new HttpConnection("/test", commonOptions))
.toThrow(new Error("Cannot resolve '/test'."));
.toThrow("Cannot resolve '/test'.");
});
it("cannot be created with relative url if window object is not present", () => {
(global as any).window = {};
expect(() => new HttpConnection("/test", commonOptions))
.toThrow(new Error("Cannot resolve '/test'."));
.toThrow("Cannot resolve '/test'.");
delete (global as any).window;
});
it("starting connection fails if getting id fails", async (done) => {
it("starting connection fails if getting id fails", async () => {
const options: IHttpConnectionOptions = {
...commonOptions,
httpClient: new TestHttpClient()
@ -35,45 +49,48 @@ describe("HttpConnection", () => {
const connection = new HttpConnection("http://tempuri.org", options);
try {
await connection.start(TransferFormat.Text);
fail();
done();
} catch (e) {
expect(e).toBe("error");
done();
}
expect(connection.start(TransferFormat.Text))
.rejects
.toThrow("error");
});
it("cannot start a running connection", async (done) => {
it("cannot start a running connection", async () => {
const negotiating = new PromiseSource();
const options: IHttpConnectionOptions = {
...commonOptions,
httpClient: new TestHttpClient()
.on("POST", (r) => {
connection.start(TransferFormat.Text)
.then(() => {
fail();
done();
})
.catch((error: Error) => {
expect(error.message).toBe("Cannot start a connection that is not in the 'Disconnected' state.");
done();
});
return Promise.reject("error");
negotiating.resolve();
return defaultNegotiateResponse;
}),
transport: {
connect(url: string, transferFormat: TransferFormat) {
return Promise.resolve();
},
send(data: any) {
return Promise.resolve();
},
stop() {
return Promise.resolve();
},
onclose: null,
onreceive: null,
},
} as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options);
try {
await connection.start(TransferFormat.Text);
} catch (e) {
// This exception is thrown after the actual verification is completed.
// The connection is not setup to be running so just ignore the error.
await expect(connection.start(TransferFormat.Text))
.rejects
.toThrow("Cannot start a connection that is not in the 'Disconnected' state.");
} finally {
await connection.stop();
}
});
it("can start a stopped connection", async (done) => {
it("can start a stopped connection", async () => {
let negotiateCalls = 0;
const options: IHttpConnectionOptions = {
...commonOptions,
@ -87,22 +104,16 @@ describe("HttpConnection", () => {
const connection = new HttpConnection("http://tempuri.org", options);
try {
await connection.start(TransferFormat.Text);
} catch (e) {
expect(e).toBe("reached negotiate");
}
await expect(connection.start(TransferFormat.Text))
.rejects
.toThrow("reached negotiate");
try {
await connection.start(TransferFormat.Text);
} catch (e) {
expect(e).toBe("reached negotiate");
}
done();
await expect(connection.start(TransferFormat.Text))
.rejects
.toThrow("reached negotiate");
});
it("can stop a starting connection", async (done) => {
it("can stop a starting connection", async () => {
const options: IHttpConnectionOptions = {
...commonOptions,
httpClient: new TestHttpClient()
@ -118,22 +129,15 @@ describe("HttpConnection", () => {
const connection = new HttpConnection("http://tempuri.org", options);
try {
await connection.start(TransferFormat.Text);
done();
} catch (e) {
fail();
done();
}
await connection.start(TransferFormat.Text);
});
it("can stop a non-started connection", async (done) => {
it("can stop a non-started connection", async () => {
const connection = new HttpConnection("http://tempuri.org", commonOptions);
await connection.stop();
done();
});
it("start throws after all transports fail", async (done) => {
it("start throws after all transports fail", async () => {
const options: IHttpConnectionOptions = {
...commonOptions,
httpClient: new TestHttpClient()
@ -142,25 +146,21 @@ describe("HttpConnection", () => {
} as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org?q=myData", options);
try {
await connection.start(TransferFormat.Text);
fail();
done();
} catch (e) {
expect(e.message).toBe("Unable to initialize any of the available transports.");
}
done();
await expect(connection.start(TransferFormat.Text))
.rejects
.toThrow("Unable to initialize any of the available transports.");
});
it("preserves user's query string", async (done) => {
let connectUrl: string;
it("preserves user's query string", async () => {
const connectUrl = new PromiseSource<string>();
const fakeTransport: ITransport = {
connect(url: string): Promise<void> {
connectUrl = url;
return Promise.reject("");
connectUrl.resolve(url);
return Promise.resolve();
},
send(data: any): Promise<void> {
return Promise.reject("");
return Promise.resolve();
},
stop(): Promise<void> {
return Promise.resolve();
@ -178,60 +178,50 @@ describe("HttpConnection", () => {
} as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org?q=myData", options);
try {
await connection.start(TransferFormat.Text);
fail();
done();
} catch (e) {
}
const startPromise = connection.start(TransferFormat.Text);
expect(connectUrl).toBe("http://tempuri.org?q=myData&id=42");
done();
expect(await connectUrl).toBe("http://tempuri.org?q=myData&id=42");
await startPromise;
} finally {
await connection.stop();
}
});
eachEndpointUrl((givenUrl: string, expectedUrl: string) => {
it("negotiate request puts 'negotiate' at the end of the path", async (done) => {
let negotiateUrl: string;
let connection: HttpConnection;
it(`negotiate request for '${givenUrl}' puts 'negotiate' at the end of the path`, async () => {
const negotiateUrl = new PromiseSource<string>();
const options: IHttpConnectionOptions = {
...commonOptions,
httpClient: new TestHttpClient()
.on("POST", (r) => {
negotiateUrl = r.url;
connection.stop();
return "{}";
negotiateUrl.resolve(r.url);
throw new HttpError("We don't care how this turns out", 500);
})
.on("GET", (r) => {
connection.stop();
return "";
}),
return new HttpResponse(204);
})
.on("DELETE", (r) => new HttpResponse(202)),
} as IHttpConnectionOptions;
connection = new HttpConnection(givenUrl, options);
const connection = new HttpConnection(givenUrl, options);
try {
await connection.start(TransferFormat.Text);
done();
} catch (e) {
fail();
done();
}
const startPromise = connection.start(TransferFormat.Text);
expect(negotiateUrl).toBe(expectedUrl);
expect(await negotiateUrl).toBe(expectedUrl);
await expect(startPromise).rejects;
} finally {
await connection.stop();
}
});
});
eachTransport((requestedTransport: HttpTransportType) => {
it(`cannot be started if requested ${HttpTransportType[requestedTransport]} transport not available on server`, async () => {
const negotiateResponse = {
availableTransports: [
{ transport: "WebSockets", transferFormats: [ "Text", "Binary" ] },
{ transport: "ServerSentEvents", transferFormats: [ "Text" ] },
{ transport: "LongPolling", transferFormats: [ "Text", "Binary" ] },
],
connectionId: "abc123",
};
// Clone the default response
const negotiateResponse = { ...defaultNegotiateResponse };
// Remove the requested transport from the response
negotiateResponse.availableTransports = negotiateResponse.availableTransports
@ -247,12 +237,9 @@ describe("HttpConnection", () => {
const connection = new HttpConnection("http://tempuri.org", options);
try {
await connection.start(TransferFormat.Text);
fail("Expected connection.start to throw!");
} catch (e) {
expect(e.message).toBe("Unable to initialize any of the available transports.");
}
await expect(connection.start(TransferFormat.Text))
.rejects
.toThrow("Unable to initialize any of the available transports.");
});
for (const [val, name] of [[null, "null"], [undefined, "undefined"], [0, "0"]]) {
@ -316,7 +303,7 @@ describe("HttpConnection", () => {
});
});
it("cannot be started if no transport available on server and no transport requested", async (done) => {
it("cannot be started if no transport available on server and no transport requested", async () => {
const options: IHttpConnectionOptions = {
...commonOptions,
httpClient: new TestHttpClient()
@ -325,17 +312,12 @@ describe("HttpConnection", () => {
} as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options);
try {
await connection.start(TransferFormat.Text);
fail();
done();
} catch (e) {
expect(e.message).toBe("Unable to initialize any of the available transports.");
done();
}
await expect(connection.start(TransferFormat.Text))
.rejects
.toThrow("Unable to initialize any of the available transports.");
});
it("does not send negotiate request if WebSockets transport requested explicitly and skipNegotiation is true", async (done) => {
it("does not send negotiate request if WebSockets transport requested explicitly and skipNegotiation is true", async () => {
const options: IHttpConnectionOptions = {
...commonOptions,
httpClient: new TestHttpClient(),
@ -344,19 +326,12 @@ describe("HttpConnection", () => {
} as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options);
try {
await connection.start(TransferFormat.Text);
fail();
done();
} catch (e) {
// WebSocket is created when the transport is connecting which happens after
// negotiate request would be sent. No better/easier way to test this.
expect(e.message).toBe("'WebSocket' is not supported in your environment.");
done();
}
await expect(connection.start(TransferFormat.Text))
.rejects
.toThrow("'WebSocket' is not supported in your environment.");
});
it("does not start non WebSockets transport requested explicitly and skipNegotiation is true", async (done) => {
it("does not start non WebSockets transport requested explicitly and skipNegotiation is true", async () => {
const options: IHttpConnectionOptions = {
...commonOptions,
httpClient: new TestHttpClient(),
@ -365,19 +340,12 @@ describe("HttpConnection", () => {
} as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options);
try {
await connection.start(TransferFormat.Text);
fail();
done();
} catch (e) {
// WebSocket is created when the transport is connecting which happens after
// negotiate request would be sent. No better/easier way to test this.
expect(e.message).toBe("Negotiation can only be skipped when using the WebSocket transport directly.");
done();
}
await expect(connection.start(TransferFormat.Text))
.rejects
.toThrow("Negotiation can only be skipped when using the WebSocket transport directly.");
});
it("redirects to url when negotiate returns it", async (done) => {
it("redirects to url when negotiate returns it", async () => {
let firstNegotiate = true;
let firstPoll = true;
const httpClient = new TestHttpClient()
@ -397,7 +365,8 @@ describe("HttpConnection", () => {
return "";
}
return new HttpResponse(204, "No Content", "");
});
})
.on("DELETE", (r) => new HttpResponse(202));
const options: IHttpConnectionOptions = {
...commonOptions,
@ -405,23 +374,21 @@ describe("HttpConnection", () => {
transport: HttpTransportType.LongPolling,
} as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options);
try {
const connection = new HttpConnection("http://tempuri.org", options);
await connection.start(TransferFormat.Text);
} catch (e) {
fail(e);
done();
}
expect(httpClient.sentRequests.length).toBe(4);
expect(httpClient.sentRequests[0].url).toBe("http://tempuri.org/negotiate");
expect(httpClient.sentRequests[1].url).toBe("https://another.domain.url/chat/negotiate");
expect(httpClient.sentRequests[2].url).toMatch(/^https:\/\/another\.domain\.url\/chat\?id=0rge0d00-0040-0030-0r00-000q00r00e00/i);
expect(httpClient.sentRequests[3].url).toMatch(/^https:\/\/another\.domain\.url\/chat\?id=0rge0d00-0040-0030-0r00-000q00r00e00/i);
done();
expect(httpClient.sentRequests.length).toBe(4);
expect(httpClient.sentRequests[0].url).toBe("http://tempuri.org/negotiate");
expect(httpClient.sentRequests[1].url).toBe("https://another.domain.url/chat/negotiate");
expect(httpClient.sentRequests[2].url).toMatch(/^https:\/\/another\.domain\.url\/chat\?id=0rge0d00-0040-0030-0r00-000q00r00e00/i);
expect(httpClient.sentRequests[3].url).toMatch(/^https:\/\/another\.domain\.url\/chat\?id=0rge0d00-0040-0030-0r00-000q00r00e00/i);
} finally {
await connection.stop();
}
});
it("fails to start if negotiate redirects more than 100 times", async (done) => {
it("fails to start if negotiate redirects more than 100 times", async () => {
const httpClient = new TestHttpClient()
.on("POST", /negotiate$/, (r) => ({ url: "https://another.domain.url/chat" }));
@ -431,17 +398,13 @@ describe("HttpConnection", () => {
transport: HttpTransportType.LongPolling,
} as IHttpConnectionOptions;
try {
const connection = new HttpConnection("http://tempuri.org", options);
await connection.start(TransferFormat.Text);
fail();
} catch (e) {
expect(e.message).toBe("Negotiate redirection limit exceeded.");
done();
}
const connection = new HttpConnection("http://tempuri.org", options);
await expect(connection.start(TransferFormat.Text))
.rejects
.toThrow("Negotiate redirection limit exceeded.");
});
it("redirects to url when negotiate returns it with access token", async (done) => {
it("redirects to url when negotiate returns it with access token", async () => {
let firstNegotiate = true;
let firstPoll = true;
const httpClient = new TestHttpClient()
@ -475,7 +438,8 @@ describe("HttpConnection", () => {
return "";
}
return new HttpResponse(204, "No Content", "");
});
})
.on("DELETE", (r) => new HttpResponse(202));
const options: IHttpConnectionOptions = {
...commonOptions,
@ -484,23 +448,21 @@ describe("HttpConnection", () => {
transport: HttpTransportType.LongPolling,
} as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options);
try {
const connection = new HttpConnection("http://tempuri.org", options);
await connection.start(TransferFormat.Text);
} catch (e) {
fail(e);
done();
}
expect(httpClient.sentRequests.length).toBe(4);
expect(httpClient.sentRequests[0].url).toBe("http://tempuri.org/negotiate");
expect(httpClient.sentRequests[1].url).toBe("https://another.domain.url/chat/negotiate");
expect(httpClient.sentRequests[2].url).toMatch(/^https:\/\/another\.domain\.url\/chat\?id=0rge0d00-0040-0030-0r00-000q00r00e00/i);
expect(httpClient.sentRequests[3].url).toMatch(/^https:\/\/another\.domain\.url\/chat\?id=0rge0d00-0040-0030-0r00-000q00r00e00/i);
done();
expect(httpClient.sentRequests.length).toBe(4);
expect(httpClient.sentRequests[0].url).toBe("http://tempuri.org/negotiate");
expect(httpClient.sentRequests[1].url).toBe("https://another.domain.url/chat/negotiate");
expect(httpClient.sentRequests[2].url).toMatch(/^https:\/\/another\.domain\.url\/chat\?id=0rge0d00-0040-0030-0r00-000q00r00e00/i);
expect(httpClient.sentRequests[3].url).toMatch(/^https:\/\/another\.domain\.url\/chat\?id=0rge0d00-0040-0030-0r00-000q00r00e00/i);
} finally {
await connection.stop();
}
});
it("authorization header removed when token factory returns null and using LongPolling", async (done) => {
it("authorization header removed when token factory returns null and using LongPolling", async () => {
const availableTransport = { transport: "LongPolling", transferFormats: ["Text"] };
let httpClientGetCount = 0;
@ -535,23 +497,22 @@ describe("HttpConnection", () => {
}
throw new Error("fail");
}
}),
})
.on("DELETE", (r) => new HttpResponse(202)),
} as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options);
try {
await connection.start(TransferFormat.Text);
expect(httpClientGetCount).toBeGreaterThanOrEqual(2);
expect(accessTokenFactoryCount).toBeGreaterThanOrEqual(2);
done();
} catch (e) {
fail(e);
done();
} finally {
await connection.stop();
}
});
it("sets inherentKeepAlive feature when using LongPolling", async (done) => {
it("sets inherentKeepAlive feature when using LongPolling", async () => {
const availableTransport = { transport: "LongPolling", transferFormats: ["Text"] };
let httpClientGetCount = 0;
@ -567,22 +528,20 @@ describe("HttpConnection", () => {
} else {
throw new Error("fail");
}
}),
})
.on("DELETE", (r) => new HttpResponse(202)),
} as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options);
try {
await connection.start(TransferFormat.Text);
expect(connection.features.inherentKeepAlive).toBe(true);
done();
} catch (e) {
fail(e);
done();
} finally {
await connection.stop();
}
});
it("does not select ServerSentEvents transport when not available in environment", async (done) => {
it("does not select ServerSentEvents transport when not available in environment", async () => {
const serverSentEventsTransport = { transport: "ServerSentEvents", transferFormats: ["Text"] };
const options: IHttpConnectionOptions = {
@ -593,19 +552,12 @@ describe("HttpConnection", () => {
const connection = new HttpConnection("http://tempuri.org", options);
try {
await connection.start(TransferFormat.Text);
fail();
done();
} catch (e) {
// ServerSentEvents is only transport returned from server but is not selected
// because there is no support in the environment, leading to the following error
expect(e.message).toBe("Unable to initialize any of the available transports.");
done();
}
await expect(connection.start(TransferFormat.Text))
.rejects
.toThrow("Unable to initialize any of the available transports.");
});
it("does not select WebSockets transport when not available in environment", async (done) => {
it("does not select WebSockets transport when not available in environment", async () => {
const webSocketsTransport = { transport: "WebSockets", transferFormats: ["Text"] };
const options: IHttpConnectionOptions = {
@ -616,16 +568,9 @@ describe("HttpConnection", () => {
const connection = new HttpConnection("http://tempuri.org", options);
try {
await connection.start(TransferFormat.Text);
fail();
done();
} catch (e) {
// WebSockets is only transport returned from server but is not selected
// because there is no support in the environment, leading to the following error
expect(e.message).toBe("Unable to initialize any of the available transports.");
done();
}
expect(connection.start(TransferFormat.Text))
.rejects
.toThrow("Unable to initialize any of the available transports.");
});
describe(".constructor", () => {

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,7 @@ import { HttpTransportType, TransferFormat } from "../src/ITransport";
import { NullLogger } from "../src/Loggers";
import { TestHttpClient } from "./TestHttpClient";
import { asyncit as it, PromiseSource } from "./Utils";
import { PromiseSource } from "./Utils";
const allTransportsNegotiateResponse = {
availableTransports: [
@ -37,17 +37,17 @@ describe("HubConnectionBuilder", () => {
eachMissingValue((val, name) => {
it(`configureLogging throws if logger is ${name}`, () => {
const builder = new HubConnectionBuilder();
expect(() => builder.configureLogging(val)).toThrow(new Error("The 'logging' argument is required."));
expect(() => builder.configureLogging(val)).toThrow("The 'logging' argument is required.");
});
it(`withUrl throws if url is ${name}`, () => {
const builder = new HubConnectionBuilder();
expect(() => builder.withUrl(val)).toThrow(new Error("The 'url' argument is required."));
expect(() => builder.withUrl(val)).toThrow("The 'url' argument is required.");
});
it(`withHubProtocol throws if protocol is ${name}`, () => {
const builder = new HubConnectionBuilder();
expect(() => builder.withHubProtocol(val)).toThrow(new Error("The 'protocol' argument is required."));
expect(() => builder.withHubProtocol(val)).toThrow("The 'protocol' argument is required.");
});
});

View File

@ -149,16 +149,16 @@ describe("JsonHubProtocol", () => {
([
["message with empty payload", `{}${TextMessageFormat.RecordSeparator}`, new Error("Invalid payload.")],
["Invocation message with invalid invocation id", `{"type":1,"invocationId":1,"target":"method"}${TextMessageFormat.RecordSeparator}`, new Error("Invalid payload for Invocation message.")],
["Invocation message with empty string invocation id", `{"type":1,"invocationId":"","target":"method"}${TextMessageFormat.RecordSeparator}`, new Error("Invalid payload for Invocation message.")],
["Invocation message with invalid target", `{"type":1,"invocationId":"1","target":1}${TextMessageFormat.RecordSeparator}`, new Error("Invalid payload for Invocation message.")],
["StreamItem message with missing invocation id", `{"type":2}${TextMessageFormat.RecordSeparator}`, new Error("Invalid payload for StreamItem message.")],
["StreamItem message with invalid invocation id", `{"type":2,"invocationId":1}${TextMessageFormat.RecordSeparator}`, new Error("Invalid payload for StreamItem message.")],
["Completion message with missing invocation id", `{"type":3}${TextMessageFormat.RecordSeparator}`, new Error("Invalid payload for Completion message.")],
["Completion message with invalid invocation id", `{"type":3,"invocationId":1}${TextMessageFormat.RecordSeparator}`, new Error("Invalid payload for Completion message.")],
["Completion message with result and error", `{"type":3,"invocationId":"1","result":2,"error":"error"}${TextMessageFormat.RecordSeparator}`, new Error("Invalid payload for Completion message.")],
["Completion message with non-string error", `{"type":3,"invocationId":"1","error":21}${TextMessageFormat.RecordSeparator}`, new Error("Invalid payload for Completion message.")],
] as Array<[string, string, Error]>).forEach(([name, payload, expectedError]) =>
["Invocation message with invalid invocation id", `{"type":1,"invocationId":1,"target":"method"}${TextMessageFormat.RecordSeparator}`, "Invalid payload for Invocation message."],
["Invocation message with empty string invocation id", `{"type":1,"invocationId":"","target":"method"}${TextMessageFormat.RecordSeparator}`, "Invalid payload for Invocation message."],
["Invocation message with invalid target", `{"type":1,"invocationId":"1","target":1}${TextMessageFormat.RecordSeparator}`, "Invalid payload for Invocation message."],
["StreamItem message with missing invocation id", `{"type":2}${TextMessageFormat.RecordSeparator}`, "Invalid payload for StreamItem message."],
["StreamItem message with invalid invocation id", `{"type":2,"invocationId":1}${TextMessageFormat.RecordSeparator}`, "Invalid payload for StreamItem message."],
["Completion message with missing invocation id", `{"type":3}${TextMessageFormat.RecordSeparator}`, "Invalid payload for Completion message."],
["Completion message with invalid invocation id", `{"type":3,"invocationId":1}${TextMessageFormat.RecordSeparator}`, "Invalid payload for Completion message."],
["Completion message with result and error", `{"type":3,"invocationId":"1","result":2,"error":"error"}${TextMessageFormat.RecordSeparator}`, "Invalid payload for Completion message."],
["Completion message with non-string error", `{"type":3,"invocationId":"1","error":21}${TextMessageFormat.RecordSeparator}`, "Invalid payload for Completion message."],
] as Array<[string, string, string]>).forEach(([name, payload, expectedError]) =>
it("throws for " + name, () => {
expect(() => new JsonHubProtocol().parseMessages(payload, NullLogger.instance))
.toThrow(expectedError);

View File

@ -1,3 +1,4 @@
import { HttpError, TimeoutError } from "../src/Errors";
import { HttpResponse } from "../src/HttpClient";
import { LogLevel } from "../src/ILogger";
import { TransferFormat } from "../src/ITransport";
@ -6,9 +7,7 @@ import { LongPollingTransport } from "../src/LongPollingTransport";
import { ConsoleLogger } from "../src/Utils";
import { TestHttpClient } from "./TestHttpClient";
import { asyncit as it, PromiseSource, delay } from "./Utils";
import { HttpError, TimeoutError } from "../src/Errors";
import { AbortSignal } from "../src/AbortController";
import { delay, PromiseSource } from "./Utils";
describe("LongPollingTransport", () => {
it("shuts down poll after timeout even if server doesn't shut it down on receiving the DELETE", async () => {
@ -70,7 +69,10 @@ describe("LongPollingTransport", () => {
});
for (const result of [200, 204, 300, new HttpError("Boom", 500), new TimeoutError()]) {
const resultName = typeof result === "number" ? result.toString() : result.constructor.name;
// Function has a name property but TypeScript doesn't know about it.
const resultName = typeof result === "number" ? result.toString() : (result.constructor as any).name;
it(`does not fire shutdown timer when poll terminates with ${resultName}`, async () => {
let firstPoll = true;
const deleteReceived = new PromiseSource();
@ -119,4 +121,4 @@ describe("LongPollingTransport", () => {
expect(transport.pollAborted).toBe(false);
});
}
});
});

View File

@ -3,7 +3,7 @@
import { HttpClient, HttpRequest, HttpResponse } from "../src/HttpClient";
type TestHttpHandlerResult = string | HttpResponse | any;
export type TestHttpHandlerResult = string | HttpResponse | any;
export type TestHttpHandler = (request: HttpRequest, next?: (request: HttpRequest) => Promise<HttpResponse>) => Promise<TestHttpHandlerResult> | TestHttpHandlerResult;
export class TestHttpClient extends HttpClient {

View File

@ -17,10 +17,10 @@ describe("TextMessageFormat", () => {
});
([
["", new Error("Message is incomplete.")],
["ABC", new Error("Message is incomplete.")],
["ABC\u001eXYZ", new Error("Message is incomplete.")],
] as Array<[string, Error]>).forEach(([payload, expectedError]) => {
["", "Message is incomplete."],
["ABC", "Message is incomplete."],
["ABC\u001eXYZ", "Message is incomplete."],
] as Array<[string, string]>).forEach(([payload, expectedError]) => {
it(`should fail to parse '${payload}'`, () => {
expect(() => TextMessageFormat.parse(payload)).toThrow(expectedError);
});

View File

@ -3,42 +3,13 @@
jasmine.DEFAULT_TIMEOUT_INTERVAL = 20000;
export function asyncit(expectation: string, assertion?: () => Promise<any> | void, timeout?: number): void {
let testFunction: (done: DoneFn) => void;
if (assertion) {
testFunction = (done) => {
const promise = assertion();
if (promise) {
promise.then(() => done())
.catch((err) => {
fail(err);
done();
});
} else {
done();
}
};
}
it(expectation, testFunction, timeout);
}
export async function captureException(fn: () => Promise<any>): Promise<Error> {
try {
await fn();
return null;
} catch (e) {
return e;
}
}
export function delay(durationInMilliseconds: number): Promise<void> {
const source = new PromiseSource<void>();
setTimeout(() => source.resolve(), durationInMilliseconds);
return source.promise;
}
export class PromiseSource<T = void> {
export class PromiseSource<T = void> implements Promise<T> {
public promise: Promise<T>;
private resolver: (value?: T | PromiseLike<T>) => void;
@ -58,4 +29,12 @@ export class PromiseSource<T = void> {
public reject(reason?: any) {
this.rejecter(reason);
}
// Look like a promise so we can be awaited directly;
public then<TResult1 = T, TResult2 = never>(onfulfilled?: (value: T) => TResult1 | PromiseLike<TResult1>, onrejected?: (reason: any) => TResult2 | PromiseLike<TResult2>): Promise<TResult1 | TResult2> {
return this.promise.then(onfulfilled, onrejected);
}
public catch<TResult = never>(onrejected?: (reason: any) => TResult | PromiseLike<TResult>): Promise<T | TResult> {
return this.promise.catch(onrejected);
}
}

View File

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

View File

@ -1,5 +1,5 @@
{
"extends": "../tsconfig-base.json",
"extends": "../tsconfig.base.json",
"include": [
"./src/**/*"
]

View File

@ -2,10 +2,10 @@
"compileOnSave": false,
"compilerOptions": {
"module": "es2015",
"target": "es2016",
"outDir": "./obj/js",
"target": "es5",
"sourceMap": true,
"moduleResolution": "node",
"importHelpers": true,
"inlineSources": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
@ -14,6 +14,15 @@
"suppressImplicitAnyIndexErrors": true,
"noEmitOnError": true,
"stripInternal": true,
"lib": [ "es5", "es2015.promise", "es2015.iterable", "dom" ]
"lib": [ "es5", "es2015.promise", "es2015.iterable", "dom" ],
"baseUrl": ".",
"paths": {
"@aspnet/signalr": [
"./signalr"
],
"@aspnet/signalr-protocol-msgpack": [
"./signalr-protocol-msgpack"
]
}
}
}

View File

@ -0,0 +1,9 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"module": "commonjs"
},
"include": [
"./*/tests/**/*"
]
}