* Enable CORS

* Add empty line

* Add mock http proxy

* Fix few tests

* Remove axios workaround that was fixed in current release

* Fix all tests except download/upload progress

* Fix stream HTTP tests

* Delete testserver

* Remove reporting skipped tests as warning

* Remove .only

* Add nodeIt

* Fix Constants.ts

* Remove it.skip()
This commit is contained in:
Kamil Pajdzik 2019-01-22 11:46:55 -08:00 коммит произвёл GitHub
Родитель 2bc787a678
Коммит 4c2b1c5390
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
11 изменённых файлов: 390 добавлений и 248 удалений

Просмотреть файл

@ -2,11 +2,8 @@ import { checkEverything } from "@ts-common/azure-js-dev-tools";
import { checkConstantsVersion } from "./checkConstantsVersion";
checkEverything({
checkForSkipCallsOptions: {
skipIsWarning: true
},
additionalChecks: {
name: "Constants.ts Version",
check: checkConstantsVersion
}
});
});

Просмотреть файл

@ -49,9 +49,13 @@ module.exports = function (config: any) {
concurrency: Infinity,
customLaunchers: {
ChromeNoSecurity: {
base: "ChromeHeadless",
flags: ["--disable-web-security"]
},
ChromeDebugging: {
base: "Chrome",
flags: [`http://localhost:${defaults.port}/debug.html`, "--auto-open-devtools-for-tabs"]
flags: [`http://localhost:${defaults.port}/debug.html`, "--auto-open-devtools-for-tabs", "--disable-web-security"]
}
},
});

Просмотреть файл

@ -12,9 +12,7 @@ import { RestError } from "./restError";
import { WebResource, HttpRequestBody } from "./webResource";
import { ProxySettings } from "./serviceClient";
const axiosClient = axios.create();
// Workaround for https://github.com/axios/axios/issues/1158
axiosClient.interceptors.request.use(config => ({ ...config, method: config.method && config.method.toUpperCase() as any }));
export const axiosClient = axios.create();
/**
* A HttpClient implementation that uses axios to send HTTP requests.

Просмотреть файл

@ -8,7 +8,7 @@ export const Constants = {
* @const
* @type {string}
*/
msRestVersion: "1.5.0",
msRestVersion: "1.5.1",
/**
* Specifies HTTP.

Просмотреть файл

@ -1,3 +1,3 @@
{
"reporterEnabled": "list, mocha-junit-reporter"
}
}

Просмотреть файл

@ -5,7 +5,7 @@
"email": "azsdkteam@microsoft.com",
"url": "https://github.com/Azure/ms-rest-js"
},
"version": "1.5.0",
"version": "1.5.1",
"description": "Isomorphic client Runtime for Typescript/node.js/browser javascript client libraries generated using AutoRest",
"tags": [
"isomorphic",
@ -73,6 +73,7 @@
"@types/xhr-mock": "^2.0.0",
"@types/xml2js": "^0.4.3",
"abortcontroller-polyfill": "^1.1.9",
"axios-mock-adapter": "^1.16.0",
"chai": "^4.2.0",
"express": "^4.16.3",
"glob": "^7.1.2",
@ -111,6 +112,7 @@
"webpack": "^4.27.1",
"webpack-cli": "^3.1.2",
"webpack-dev-middleware": "^3.1.2",
"xhr-mock": "^2.4.1",
"yarn": "^1.6.0"
},
"homepage": "https://github.com/Azure/ms-rest-js",
@ -132,7 +134,7 @@
"test": "run-p test:tslint test:unit test:karma",
"test:tslint": "tslint -p . -c tslint.json --exclude \"./test/**/*.ts\"",
"test:unit": "nyc mocha",
"test:karma": "npm run build:test-browser && node ./node_modules/karma/bin/karma start karma.conf.ts --single-run",
"test:karma": "npm run build:test-browser && node ./node_modules/karma/bin/karma start karma.conf.ts --browsers ChromeNoSecurity --single-run ",
"test:karma:debug": "npm run build:test-browser && node ./node_modules/karma/bin/karma start karma.conf.ts --log-level debug --browsers ChromeDebugging --debug --auto-watch",
"publish-preview": "mocha --no-colors && shx rm -rf dist/test && node ./.scripts/publish",
"local": "ts-node ./.scripts/local.ts",

Просмотреть файл

@ -1,14 +1,18 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { assert, AssertionError } from "chai";
import "chai/register-should";
import { createReadStream } from "fs";
import { join } from "path";
import { DefaultHttpClient } from "../lib/defaultHttpClient";
import { RestError } from "../lib/restError";
import { isNode } from "../lib/util/utils";
import { WebResource, HttpRequestBody } from "../lib/webResource";
import { WebResource, HttpRequestBody, TransferProgressEvent } from "../lib/webResource";
import { getHttpMock } from "./mockHttp";
const nodeIt = isNode ? it : it.skip;
const httpMock = getHttpMock();
function getAbortController(): AbortController {
let controller: AbortController;
@ -21,11 +25,229 @@ function getAbortController(): AbortController {
return controller;
}
const baseURL = "https://example.com";
describe("defaultHttpClient", function () {
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
beforeEach(() => httpMock.setup());
afterEach(() => httpMock.teardown());
it("should return a response instead of throwing for awaited 404", async function () {
const resourceUrl = "/nonexistent/";
httpMock.get(resourceUrl, async () => {
return { status: 404 };
});
const request = new WebResource(resourceUrl, "GET");
const httpClient = new DefaultHttpClient();
const response = await httpClient.sendRequest(request);
response.status.should.equal(404);
});
it("should allow canceling requests", async function () {
const resourceUrl = `/fileupload`;
httpMock.post(resourceUrl, async () => {
await sleep(10000);
assert.fail();
return { status: 201 };
});
const controller = getAbortController();
const veryBigPayload = "very long string";
const request = new WebResource(resourceUrl, "POST", veryBigPayload, undefined, undefined, true, undefined, controller.signal);
const client = new DefaultHttpClient();
const promise = client.sendRequest(request);
controller.abort();
try {
await promise;
assert.fail("");
} catch (err) {
err.should.not.be.instanceof(AssertionError);
}
});
nodeIt("should not overwrite a user-provided cookie (nodejs only)", async function () {
// Cookie is only allowed to be set by the browser based on an actual response Set-Cookie header
httpMock.get("http://my.fake.domain/set-cookie", {
status: 200,
headers: {
"Set-Cookie": "data=123456"
}
});
httpMock.get("http://my.fake.domain/cookie", async (_url, _method, _body, headers) => {
return {
status: 200,
headers: headers
};
});
const client = new DefaultHttpClient();
const request1 = new WebResource("http://my.fake.domain/set-cookie");
const response1 = await client.sendRequest(request1);
response1.headers.get("Set-Cookie")!.should.equal("data=123456");
const request2 = new WebResource("http://my.fake.domain/cookie");
const response2 = await client.sendRequest(request2);
response2.headers.get("Cookie")!.should.equal("data=123456");
const request3 = new WebResource("http://my.fake.domain/cookie", "GET", undefined, undefined, { Cookie: "data=abcdefg" });
const response3 = await client.sendRequest(request3);
response3.headers.get("Cookie")!.should.equal("data=abcdefg");
});
it("should allow canceling multiple requests with one token", async function () {
httpMock.post("/fileupload", async () => {
await sleep(1000);
assert.fail();
return { status: 201 };
});
const controller = getAbortController();
const buf = "Very large string";
const requests = [
new WebResource("/fileupload", "POST", buf, undefined, undefined, true, undefined, controller.signal),
new WebResource("/fileupload", "POST", buf, undefined, undefined, true, undefined, controller.signal)
];
const client = new DefaultHttpClient();
const promises = requests.map(r => client.sendRequest(r));
controller.abort();
// Ensure each promise is individually rejected
for (const promise of promises) {
try {
await promise;
assert.fail();
} catch (err) {
err.should.not.be.instanceof(AssertionError);
}
}
});
describe("should report upload and download progress", () => {
type Notified = { notified: boolean };
const listener = (operationStatus: Notified, ev: TransferProgressEvent) => {
operationStatus.notified = true;
if (typeof ProgressEvent !== "undefined") {
ev.should.not.be.instanceof(ProgressEvent);
}
ev.loadedBytes.should.be.a("Number");
};
it("for simple bodies", async function () {
httpMock.post("/fileupload", async (_url, _method, body) => {
return { status: 201, body: body, headers: { "Content-Length": "200" } };
});
const upload: Notified = { notified: false };
const download: Notified = { notified: false };
const body = "Very large string to upload";
const request = new WebResource("/fileupload", "POST", body, undefined, undefined, false, undefined, undefined, 0,
ev => listener(upload, ev),
ev => listener(download, ev));
const client = new DefaultHttpClient();
const response = await client.sendRequest(request);
response.should.exist;
upload.notified.should.be.true;
download.notified.should.be.true;
});
it("for blob or stream bodies", async function () {
let payload: HttpRequestBody;
if (isNode) {
payload = () => createReadStream(__filename);
} else {
payload = new Blob([new Uint8Array(1024 * 1024)]);
}
const size = isNode ? payload.toString().length : undefined;
httpMock.post("/fileupload", async (_url, _method, _body) => {
return { status: 201, body: payload, headers: { "Content-Type": "text/javascript", "Content-length": size } };
});
const upload: Notified = { notified: false };
const download: Notified = { notified: false };
const request = new WebResource("/fileupload", "POST", payload, undefined, undefined, true, undefined, undefined, 0,
ev => listener(upload, ev),
ev => listener(download, ev));
const client = new DefaultHttpClient();
const response = await client.sendRequest(request);
if (response.blobBody) {
await response.blobBody;
} else if ((typeof response.readableStreamBody === "function")) {
const streamBody = (response.readableStreamBody as Function)();
streamBody.on("data", () => { });
await new Promise((resolve, reject) => {
streamBody.on("end", resolve);
streamBody.on("error", reject);
});
}
upload.notified.should.be.true;
download.notified.should.be.true;
});
});
it("should honor request timeouts", async function () {
httpMock.timeout("GET", "/slow");
const request = new WebResource("/slow", "GET", undefined, undefined, undefined, false, false, undefined, 100);
const client = new DefaultHttpClient();
try {
await client.sendRequest(request);
throw new Error("request did not fail as expected");
} catch (err) {
err.message.should.match(/timeout/);
}
});
it("should give a graceful error for nonexistent hosts", async function () {
const requestUrl = "http://foo.notawebsite/";
httpMock.passThrough(requestUrl);
const request = new WebResource(requestUrl);
const client = new DefaultHttpClient();
try {
await client.sendRequest(request);
throw new Error("request did not fail as expected");
} catch (err) {
err.should.be.instanceof(RestError);
err.code.should.equal("REQUEST_SEND_ERROR");
}
});
it("should interpret undefined as an empty body", async function () {
const requestUrl = "/expect-empty";
httpMock.put(requestUrl, async (_url, _method, body, _headers) => {
if (!body) {
return {
status: 200
};
} else {
return {
status: 400,
body: `Expected empty body but got "${JSON.stringify(body)}"`
};
}
});
const request = new WebResource(requestUrl, "PUT");
const client = new DefaultHttpClient();
const response = await client.sendRequest(request);
response.status.should.equal(200, response.bodyAsText!);
});
describe.skip("defaultHttpClient", function () {
it("should send HTTP requests", async function () {
const request = new WebResource("https://example.com/", "GET");
httpMock.passThrough();
const request = new WebResource("https://example.com", "GET");
request.headers.set("Access-Control-Allow-Headers", "Content-Type");
request.headers.set("Access-Control-Allow-Methods", "GET");
request.headers.set("Access-Control-Allow-Origin", "https://example.com");
const httpClient = new DefaultHttpClient();
@ -33,8 +255,6 @@ describe.skip("defaultHttpClient", function () {
assert.deepEqual(response.request, request);
assert.strictEqual(response.status, 200);
assert(response.headers);
// content-length varies based on OS line endings
assert.strictEqual(response.headers.get("content-length"), response.bodyAsText!.length.toString());
assert.strictEqual(response.headers.get("content-type")!.split(";")[0], "text/html");
const responseBody: string | null | undefined = response.bodyAsText;
const expectedResponseBody =
@ -93,168 +313,4 @@ describe.skip("defaultHttpClient", function () {
responseBody && responseBody.replace(/\s/g, ""),
expectedResponseBody.replace(/\s/g, ""));
});
it("should return a response instead of throwing for awaited 404", async function () {
const resourceUrl = `${baseURL}/nonexistent`;
const request = new WebResource(resourceUrl, "GET");
const httpClient = new DefaultHttpClient();
const response = await httpClient.sendRequest(request);
response.should.exist;
});
it("should allow canceling requests", async function () {
const controller = getAbortController();
const veryBigPayload = "very long string";
const request = new WebResource(`${baseURL}/fileupload`, "POST", veryBigPayload, undefined, undefined, true, undefined, controller.signal);
const client = new DefaultHttpClient();
const promise = client.sendRequest(request);
controller.abort();
try {
await promise;
assert.fail("");
} catch (err) {
err.should.not.be.instanceof(AssertionError);
}
});
it("should not overwrite a user-provided cookie (nodejs only)", async function () {
// Cookie is only allowed to be set by the browser based on an actual response Set-Cookie header
if (!isNode) {
this.skip();
}
const client = new DefaultHttpClient();
const request1 = new WebResource(`${baseURL}/set-cookie`);
await client.sendRequest(request1);
const request2 = new WebResource(`${baseURL}/cookie`);
const response2 = await client.sendRequest(request2);
response2.headers.get("Cookie")!.should.equal("data=123456");
const request3 = new WebResource(`${baseURL}/cookie`, "GET", undefined, undefined, { Cookie: "data=abcdefg" });
const response3 = await client.sendRequest(request3);
response3.headers.get("Cookie")!.should.equal("data=abcdefg");
});
it("should allow canceling multiple requests with one token", async function () {
const controller = getAbortController();
const buf = new Uint8Array(1024 * 1024 * 1);
const requests = [
new WebResource(`${baseURL}/fileupload`, "POST", buf, undefined, undefined, true, undefined, controller.signal),
new WebResource(`${baseURL}/fileupload`, "POST", buf, undefined, undefined, true, undefined, controller.signal)
];
const client = new DefaultHttpClient();
const promises = requests.map(r => client.sendRequest(r));
controller.abort();
// Ensure each promise is individually rejected
for (const promise of promises) {
try {
await promise;
assert.fail("");
} catch (err) {
err.should.not.be.instanceof(AssertionError);
}
}
});
it("should report upload and download progress for simple bodies", async function () {
let uploadNotified = false;
let downloadNotified = false;
const body = isNode ? Buffer.alloc(1024 * 1024) : new Uint8Array(1024 * 1024);
const request = new WebResource(`${baseURL}/fileupload`, "POST", body, undefined, undefined, false, undefined, undefined, 0,
ev => {
uploadNotified = true;
if (typeof ProgressEvent !== "undefined") {
ev.should.not.be.instanceof(ProgressEvent);
}
ev.loadedBytes.should.be.a("Number");
},
ev => {
downloadNotified = true;
if (typeof ProgressEvent !== "undefined") {
ev.should.not.be.instanceof(ProgressEvent);
}
ev.loadedBytes.should.be.a("Number");
});
const client = new DefaultHttpClient();
await client.sendRequest(request);
assert(uploadNotified);
assert(downloadNotified);
});
it("should report upload and download progress for blob or stream bodies", async function () {
let uploadNotified = false;
let downloadNotified = false;
let body: HttpRequestBody;
if (isNode) {
body = () => createReadStream(join(__dirname, "..", "resources", "example-index.html"));
} else {
body = new Blob([new Uint8Array(1024 * 1024)]);
}
const request = new WebResource(`${baseURL}/fileupload`, "POST", body, undefined, undefined, true, undefined, undefined, 0,
ev => {
uploadNotified = true;
if (typeof ProgressEvent !== "undefined") {
ev.should.not.be.instanceof(ProgressEvent);
}
ev.loadedBytes.should.be.a("Number");
},
ev => {
downloadNotified = true;
if (typeof ProgressEvent !== "undefined") {
ev.should.not.be.instanceof(ProgressEvent);
}
ev.loadedBytes.should.be.a("Number");
});
const client = new DefaultHttpClient();
const response = await client.sendRequest(request);
const streamBody = response.readableStreamBody;
if (response.blobBody) {
await response.blobBody;
} else if (streamBody) {
streamBody.on("data", () => { });
await new Promise((resolve, reject) => {
streamBody.on("end", resolve);
streamBody.on("error", reject);
});
}
assert(uploadNotified);
assert(downloadNotified);
});
it("should honor request timeouts", async function () {
const request = new WebResource(`${baseURL}/slow`, "GET", undefined, undefined, undefined, false, false, undefined, 100);
const client = new DefaultHttpClient();
try {
await client.sendRequest(request);
throw new Error("request did not fail as expected");
} catch (err) {
err.message.should.match(/timeout/);
}
});
it("should give a graceful error for nonexistent hosts", async function () {
const request = new WebResource(`http://foo.notawebsite/`);
const client = new DefaultHttpClient();
try {
await client.sendRequest(request);
throw new Error("request did not fail as expected");
} catch (err) {
err.should.be.instanceof(RestError);
err.code.should.equal("REQUEST_SEND_ERROR");
}
});
it("should interpret undefined as an empty body", async function () {
const request = new WebResource(`${baseURL}/expect-empty`, "PUT");
const client = new DefaultHttpClient();
const response = await client.sendRequest(request);
response.status.should.equal(200, response.bodyAsText!);
});
});

132
test/mockHttp.ts Normal file
Просмотреть файл

@ -0,0 +1,132 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import xhrMock, { proxy } from "xhr-mock";
import MockAdapter from "axios-mock-adapter";
import { isNode, HttpMethods } from "../lib/msRest";
import { AxiosRequestConfig } from "axios";
export type UrlFilter = string | RegExp;
export type MockResponseData = {
status?: number;
body?: any;
headers?: any;
};
export type MockResponseFunction = (url?: string, method?: string, body?: any, headers?: any) => Promise<MockResponseData>;
export type MockResponse = MockResponseData | MockResponseFunction;
interface HttpMockFacade {
setup(): void;
teardown(): void;
passThrough(url?: UrlFilter): void;
timeout(method: HttpMethods, url?: UrlFilter): void;
mockHttpMethod(method: HttpMethods, url: UrlFilter, response: MockResponse): void;
get(url: UrlFilter, response: MockResponse): void;
post(url: UrlFilter, response: MockResponse): void;
put(url: UrlFilter, response: MockResponse): void;
}
export function getHttpMock(): HttpMockFacade {
return (isNode ? new NodeHttpMock() : new BrowserHttpMock());
}
class NodeHttpMock implements HttpMockFacade {
private _mockAdapter: MockAdapter;
constructor() {
const axiosClient = require("../lib/axiosHttpClient").axiosClient;
this._mockAdapter = new MockAdapter(axiosClient);
}
setup(): void {
}
teardown(): void {
}
mockHttpMethod(method: HttpMethods, url: UrlFilter, response: MockResponse): void {
const methodName = "on" + method.charAt(0) + method.slice(1).toLowerCase();
const mockCall: { reply: (statusOrCallback: number | Function, data?: any, headers?: any) => MockAdapter } = (this._mockAdapter as any)[methodName](url);
if (typeof response === "function") {
mockCall.reply(async (config: AxiosRequestConfig) => {
const result = await response(config.url, config.method, config.data, config.headers);
return [result.status, result.body, result.headers];
});
} else {
mockCall.reply(response.status || 200, response.body || {}, response.headers || {});
}
}
get(url: UrlFilter, response: MockResponse): void {
return this.mockHttpMethod("GET", url, response);
}
post(url: UrlFilter, response: MockResponse): void {
return this.mockHttpMethod("POST", url, response);
}
put(url: UrlFilter, response: MockResponse): void {
return this.mockHttpMethod("PUT", url, response);
}
passThrough(url?: UrlFilter): void {
this._mockAdapter.onAny(url).passThrough();
}
timeout(_method: HttpMethods, url?: UrlFilter): void {
this._mockAdapter.onAny(url).timeout();
}
}
class BrowserHttpMock implements HttpMockFacade {
setup(): void {
xhrMock.setup();
}
teardown(): void {
xhrMock.teardown();
}
mockHttpMethod(method: HttpMethods, url: UrlFilter, response: MockResponse): void {
if (typeof response === "function") {
xhrMock.use(method, url, async (req, res) => {
const result = await response(req.url().toString(), req.method().toString(), req.body(), req.headers());
return res.status(result.status || 200).body(result.body || {}).headers(result.headers || {});
});
} else {
xhrMock.use(method, url, {
status: response.status,
body: response.body
});
}
}
get(url: UrlFilter, response: MockResponse): void {
this.mockHttpMethod("GET", url, response);
}
post(url: UrlFilter, response: MockResponse): void {
this.mockHttpMethod("POST", url, response);
}
put(url: UrlFilter, response: MockResponse): void {
this.mockHttpMethod("PUT", url, response);
}
passThrough(url?: UrlFilter): void {
if (url) {
console.warn("Browser mock doesn't support filtered passThrough calls.");
}
xhrMock.use(proxy);
}
timeout(method: HttpMethods, url: UrlFilter): void {
return this.mockHttpMethod(method, url, () => new Promise(() => { }));
}
}

Просмотреть файл

@ -1,63 +0,0 @@
import path = require("path");
import webpackMiddleware = require("webpack-dev-middleware");
import webpack = require("webpack");
import express = require("express");
import testconfig = require("../webpack.testconfig");
const port = parseInt(process.env.PORT!) || 3001;
const app = express();
if (process.argv.indexOf("--no-webpack") === -1) {
app.use(webpackMiddleware(webpack(testconfig), {
publicPath: "/"
}));
}
app.use(express.static(path.join(__dirname, "../")));
app.use(express.static(path.join(__dirname, "../test/resources/")));
app.post("/fileupload", function(req, res) {
req.pipe(res);
});
app.get("/set-cookie", function(_req, res) {
res.setHeader("Set-Cookie", "data=123456");
res.end();
});
app.get("/cookie", function(req, res) {
res.setHeader("Cookie", req.header("Cookie")!);
res.end();
});
app.get("/slow", function(_req, res) {
setTimeout(() => {
res.status(200);
res.end();
}, 2000);
});
app.put("/expect-empty", function (req, res) {
let bufs: Buffer[] = [];
req.on('data', (data: Buffer) => {
bufs.push(data);
});
req.on('end', () => {
const buf = Buffer.concat(bufs);
if (buf.length === 0) {
res.status(200);
res.end();
} else {
res.status(400);
res.end("Expected empty body but got " + buf.toString('utf-8'));
}
});
req.on('error', err => {
res.status(500);
res.end(err);
});
});
app.listen(port, function() {
console.log(`ms-rest-js testserver (${process.pid}) listening on port ${port}...`);
});

Просмотреть файл

@ -35,4 +35,4 @@
"./samples/**/*.ts",
"./test/**/*.ts"
]
}
}

Просмотреть файл

@ -6,6 +6,22 @@ const config: webpack.Configuration = {
entry: glob.sync(path.join(__dirname, "test/**/*.ts")),
mode: "development",
devtool: "source-map",
stats: {
colors: true,
hash: false,
version: false,
timings: false,
assets: false,
chunks: false,
modules: false,
reasons: false,
children: false,
source: false,
errors: true,
errorDetails: false,
warnings: false,
publicPath: false
},
output: {
filename: "msRest.browser.test.js",
path: path.resolve(__dirname, "test")
@ -31,7 +47,7 @@ const config: webpack.Configuration = {
},
node: {
fs: "empty",
net: false,
net: "empty",
path: "empty",
dns: false,
tls: false,