From f1996efb0114526b302d6484e0f1fb98c0d10834 Mon Sep 17 00:00:00 2001 From: Abrax20 Date: Wed, 28 Aug 2019 20:15:15 +0200 Subject: [PATCH 1/3] FIx Batching Documentation code The code example in the Batching Documentation does not work. The batchResponse is not set in the example code. --- docs/content/Batching.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/Batching.md b/docs/content/Batching.md index 9c40b280d..68405ac54 100644 --- a/docs/content/Batching.md +++ b/docs/content/Batching.md @@ -56,7 +56,7 @@ const serialBatching = async function(elem) { let batchResponseContent = new MicrosoftGraph.BatchResponseContent(response); //Getting response by id - console.log(batchResponse.getResponseById(downloadId)); + console.log(batchResponseContent.getResponseById(downloadId)); //Getting all the responses console.log(batchResponseContent.getResponses()); From 248945c6fc746be4af77e3e1a22019627968fbc9 Mon Sep 17 00:00:00 2001 From: nikithauc Date: Thu, 17 Sep 2020 14:19:02 -0700 Subject: [PATCH 2/3] Correcting missing import, incorrect reference in example (#326) --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cc7ee95f5..fdb2f6b8a 100644 --- a/README.md +++ b/README.md @@ -95,8 +95,8 @@ npm install msal@ ```typescript import { UserAgentApplication } from "msal"; - -import { ImplicitMSALAuthenticationProvider } from "./node_modules/@microsoft/microsoft-graph-client/lib/src/ImplicitMSALAuthenticationProvider"; +import { ImplicitMSALAuthenticationProvider } from "@microsoft/microsoft-graph-client/lib/src/ImplicitMSALAuthenticationProvider"; +import { MSALAuthenticationProviderOptions } from '@microsoft/microsoft-graph-client/lib/src/MSALAuthenticationProviderOptions'; // An Optional options for initializing the MSAL @see https://github.com/AzureAD/microsoft-authentication-library-for-js/wiki/MSAL-basics#configuration-options const msalConfig = { @@ -110,7 +110,7 @@ const graphScopes = ["user.read", "mail.send"]; // An array of graph scopes // Important Note: This library implements loginPopup and acquireTokenPopup flow, remember this while initializing the msal // Initialize the MSAL @see https://github.com/AzureAD/microsoft-authentication-library-for-js#1-instantiate-the-useragentapplication const msalApplication = new UserAgentApplication(msalConfig); -const options = new MicrosoftGraph.MSALAuthenticationProviderOptions(graphScopes); +const options = new MSALAuthenticationProviderOptions(graphScopes); const authProvider = new ImplicitMSALAuthenticationProvider(msalApplication, options); ``` From b05fc9c15e05baeacc14df308070512ff8f0d19d Mon Sep 17 00:00:00 2001 From: nikithauc Date: Fri, 9 Oct 2020 10:11:32 -0700 Subject: [PATCH 3/3] Release - 2.1.0 (#338) * updated broken link in README * Post request with empty body, now working fine. * updated the tests for this case * made change to use undefined instead of null as classname * added functionality to simplify building middleware chain * Updated GraphRequest.ts for .count() scenario Updated GraphRequest.ts to handle .count() when no parameter is specified * Bumped version to '2.1.0-Preview.1' * Adding the Modifying middleware chain samples * - fixes broken link to client instance * Updated the example to the correct return type * fix casing on filename * Update package.json * Designed ChaosHandler * Removing the set and get middleware chain method * Update README * Update README * Bump lodash from 4.17.15 to 4.17.19 in /samples/browser Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19) Signed-off-by: dependabot[bot] * Changed parsepath and query functions to split query params on first equals sign considering nested queries. Added comments to query functions * Correcting spelling in comments, setting the param key directlyand replacing ternary condition with if else * Adding a buildheaders function to conditionally set content type and checking if body is null when serializing content * Altering condition to be optimal Co-authored-by: Mustafa Zengin * Adding formdata to the node project, setting the esModuleInterop config as true for allowing import of npm modules, adding unit test for application/xhtml+xml content type post * Adding a private query parsing function and more validations * Adding tests for url parsing and comments to the private url parsing functions * Adding more information in the return comments for functions returning the GraphRequest instance * Adding unit test for serializecontent testing if content is null, changed content type setting for put and patch * Changing the conditional expression to check for undefined or null headers Co-authored-by: Mustafa Zengin * Adding the missing return Co-authored-by: Mustafa Zengin * Removing the try catch block from the test case * Removing the try catch block from the test case * Returning if the content-type is present else setting content-type as default * Uninstalling formdata, restoring the tsconfigs and removing the condition to check if form-data is undefined in GraphRequest.ts * Restoring file * Reverting the package json changes * Bump yargs-parser from 13.1.1 to 13.1.2 in /samples/browser (#321) Bumps [yargs-parser](https://github.com/yargs/yargs-parser) from 13.1.1 to 13.1.2. - [Release notes](https://github.com/yargs/yargs-parser/releases) - [Changelog](https://github.com/yargs/yargs-parser/blob/master/docs/CHANGELOG-full.md) - [Commits](https://github.com/yargs/yargs-parser/commits) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump mixin-deep from 1.3.1 to 1.3.2 in /scripts (#301) Bumps [mixin-deep](https://github.com/jonschlinkert/mixin-deep) from 1.3.1 to 1.3.2. - [Release notes](https://github.com/jonschlinkert/mixin-deep/releases) - [Commits](https://github.com/jonschlinkert/mixin-deep/compare/1.3.1...1.3.2) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Vincent Biret Co-authored-by: nikithauc * Bump http-proxy from 1.17.0 to 1.18.1 in /samples/browser (#319) Bumps [http-proxy](https://github.com/http-party/node-http-proxy) from 1.17.0 to 1.18.1. - [Release notes](https://github.com/http-party/node-http-proxy/releases) - [Changelog](https://github.com/http-party/node-http-proxy/blob/master/CHANGELOG.md) - [Commits](https://github.com/http-party/node-http-proxy/compare/1.17.0...1.18.1) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Update OneDriveLargeFileUploadTask.ts (#325) * Update OneDriveLargeFileUploadTask.ts Trying to upload files with the '#' or '%' character in the filename ends with network errors of the form - 400 status response "The parameter item does not exist in method getByPath" Encoding the entire URL does not encode URL components correctly. When the SDK's client tries to encode these URL components in the parameters on the call to OneDriveLargeFileUploadTask.create, then the resultant file in OneDrive has the URL encoded filename (as opposed to the user-friendly/decoded filename). Therefore, fix the encoding in the SDK's URL components itself, directly. * Update OneDriveLargeFileUploadTask.ts Fix typo * Update src/tasks/OneDriveLargeFileUploadTask.ts Updated comment Co-authored-by: Mustafa Zengin * Update OneDriveLargeFileUploadTask.ts Updated comment Co-authored-by: Mustafa Zengin * Use correct class name (#284) Co-authored-by: Vincent Biret Co-authored-by: nikithauc * Typo in documentation example. (#270) Co-authored-by: Vincent Biret Co-authored-by: nikithauc * Enhancement/#311 page iterator request options (#318) * Adding request options property to PageIterator * Adding tests for page iterator task to test passing along the headers * using headersinit type for headers * Specifying parameter definition * using constants * Updating function documentation * remove response type, test passing fetchoptions * testing requestOptions set in pageiterator * typos, optional parameters comments * Sorting parameter list - chaoshandleroptions (#334) * Client init with middleware array (#333) * Passing midddleware array in client options * Tests for middleware array * Removing try catch * ifnode condition,chain middleware test * Adding missing exports * Array initialization Co-authored-by: Vincent Biret * merging with dev * Using spread operator * Checking if middleware is not empty Co-authored-by: Vincent Biret * Make GraphError real Error (#335) Co-authored-by: nikithauc * Using ternary shorthand, upgrade preview 2.1.0-2 (#337) * Bump version to 2.1.0 * changing.substring parameter, comments to check middleware * adding tests, urlotherqueryoptions type Co-authored-by: Abhinav Srivastava Co-authored-by: muthurathinam Co-authored-by: Vincent Biret Co-authored-by: warreee Co-authored-by: Gideon Goldberg Co-authored-by: Muthurathinam <6259786+muthurathinam@users.noreply.github.com> Co-authored-by: Nikola Metulev Co-authored-by: Behnam Mohammadi Co-authored-by: DeVere Dyett Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Mustafa Zengin Co-authored-by: Michael Mainer Co-authored-by: Vincent Biret Co-authored-by: Hari Sridharan Co-authored-by: Mustafa Zengin Co-authored-by: mattdenkers Co-authored-by: lewgordon <50742795+lewgordon@users.noreply.github.com> Co-authored-by: Olivier Cuypers --- README.md | 1 + docs/Actions.md | 4 +- docs/ChaosHandlerSamples.md | 102 +++++++ docs/CustomAuthenticationProvider.md | 4 +- docs/CustomMiddlewareChain.md | 2 +- docs/GettingRawResponse.md | 2 +- docs/tasks/LargeFileUploadTask.md | 2 +- package-lock.json | 2 +- package.json | 3 +- samples/browser/package-lock.json | 34 ++- scripts/package-lock.json | 19 +- spec/core/Client.ts | 40 +++ spec/core/GraphErrorHandler.ts | 2 +- spec/core/GraphRequestUtil.ts | 12 +- spec/core/HTTPClient.ts | 31 ++ spec/core/urlGeneration.ts | 53 ++++ spec/core/urlParsing.ts | 17 +- spec/development/workload/OneNote.ts | 32 +- spec/development/workload/PageIterator.ts | 123 ++++++++ spec/middleware/ChaosHandler.ts | 254 ++++++++++++++++ spec/middleware/MiddlewareFactory.ts | 27 ++ src/Client.ts | 2 +- src/GraphError.ts | 13 +- src/GraphErrorHandler.ts | 6 +- src/GraphRequest.ts | 210 +++++++++---- src/GraphRequestUtil.ts | 2 +- src/HTTPClient.ts | 44 ++- src/HTTPClientFactory.ts | 9 +- src/IClientOptions.ts | 4 +- src/Version.ts | 2 +- src/browser/index.ts | 6 +- src/index.ts | 6 +- src/middleware/ChaosHandler.ts | 286 ++++++++++++++++++ src/middleware/MiddlewareFactory.ts | 62 ++++ src/middleware/options/ChaosHandlerData.ts | 88 ++++++ src/middleware/options/ChaosHandlerOptions.ts | 79 +++++ src/middleware/options/ChaosStrategy.ts | 19 ++ src/tasks/OneDriveLargeFileUploadTask.ts | 7 +- src/tasks/PageIterator.ts | 40 ++- 39 files changed, 1517 insertions(+), 134 deletions(-) create mode 100644 docs/ChaosHandlerSamples.md create mode 100644 spec/development/workload/PageIterator.ts create mode 100644 spec/middleware/ChaosHandler.ts create mode 100644 spec/middleware/MiddlewareFactory.ts create mode 100644 src/middleware/ChaosHandler.ts create mode 100644 src/middleware/MiddlewareFactory.ts create mode 100644 src/middleware/options/ChaosHandlerData.ts create mode 100644 src/middleware/options/ChaosHandlerOptions.ts create mode 100644 src/middleware/options/ChaosStrategy.ts diff --git a/README.md b/README.md index fdb2f6b8a..94c25c7a5 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,7 @@ npm install msal@ ```typescript import { UserAgentApplication } from "msal"; + import { ImplicitMSALAuthenticationProvider } from "@microsoft/microsoft-graph-client/lib/src/ImplicitMSALAuthenticationProvider"; import { MSALAuthenticationProviderOptions } from '@microsoft/microsoft-graph-client/lib/src/MSALAuthenticationProviderOptions'; diff --git a/docs/Actions.md b/docs/Actions.md index 4f901b71b..701f21b2d 100644 --- a/docs/Actions.md +++ b/docs/Actions.md @@ -29,13 +29,13 @@ client .then((stream) => { let writeStream = fs.createWriteStream(`../`); // Eg: test.pdf stream.pipe(writeStream).on("error", (err) => { - throw error; + throw err; }); writeStream.on("finish", () => { console.log("Downloaded"); }); writeStream.on("error", (err) => { - throw error; + throw err; }); }) .catch((error) => { diff --git a/docs/ChaosHandlerSamples.md b/docs/ChaosHandlerSamples.md new file mode 100644 index 000000000..d87544c26 --- /dev/null +++ b/docs/ChaosHandlerSamples.md @@ -0,0 +1,102 @@ +# Testing Handler + +### How to include + +> Uses [Custom Middleware Chain](https://github.com/microsoftgraph/msgraph-sdk-javascript/blob/dev/docs/CustomMiddlewareChain.md), it's not included in default middleware chain + +### Modes in Chaos Handler + +- Manual mode - Setting the Response code manually. - Global/Client Level - Provide a map declared manually containing response code for the requests. - Request Level - Providing response code per request. This would be overriding the Global level response code (if any). +- Random mode - We get a random Response code from a set of response code defined for each method. + +**A request Passes through to the Graph, if there is no entry for the request** + +**Note - Always add ChaosHandler before HttpMessageHandler in the midllewareChain** + +### Samples + +```js +require("isomorphic-fetch"); +const MicrosoftGraph = require("../../lib/src/index.js"); +const secrets = require("./secrets"); +const fs = require("fs"); +// Initialising the client +const client = MicrosoftGraph.Client.init({ + defaultVersion: "v1.0", + debugLogging: true, + authProvider: (done) => { + done(null, secrets.accessToken); + }, +}); + +/* +Create a custom MiddlewareChain passing information in this way + +const manualMap = new Map([["/me/messages/.*", new Map([["GET", 429], ["PATCH", 429]])], ["/me", new Map([["POST", 502]])]]); +const chaosHandler = new MicrosoftGraph.ChaosHandler(new MicrosoftGraph.ChaosHandlerOptions(MicrosoftGraph.ChaosStrategy.MANUAL), manualMap); +*/ + +// This request would use the Map (Manual mode) +const mail = { + subject: "Chaos Handler Samples", + toRecipients: [ + { + emailAddress: { + address: "admin@M365x003297.OnMicrosoft.com", + }, + }, + ], + body: { + content: "

Testing Handler Samples Sample


https://github.com/microsoftgraph/msgraph-sdk-javascript", + contentType: "html", + }, +}; +client + .api("/users/me/sendMail") + .post({ + message: mail, + }) + .then((res) => { + console.log(res, "This is for sendMail"); + }) + .catch((err) => { + console.log(err, "This is for sendMail in error case"); + }); + +// OverRiding to Random mode, providing the chaos percentage as 60(percentage times the error would be generated from handler) +client + .api("/me") + .middlewareOptions([new MicrosoftGraph.ChaosHandlerOptions(MicrosoftGraph.ChaosStrategy.RANDOM, "I generated the error", undefined, 60)]) + .get() + .then((res) => { + console.log(res); + }) + .catch((err) => { + console.log(err); + }); + +// This request is passed to the graph and gets a response from the graph, as no entry for /me GET request in the Map +client + .api("/me") + .get() + .then((res) => { + console.log("Found", res, "users"); + }) + .catch((err) => { + console.log(err, "!!!!!!!!!"); + }); + +// Using Manual Map with regex matching +client + .api("/me/messages/hjdlfslod-fdssdkjfs-6zdkmghs-sadhsu2") + .header("content-type", "application/json") + .update({ + birthday: "1908-12-22T00:00:00Z", + }) + .then((res) => { + console.log("This is regex matching... Updated Bday"); + }) + .catch((err) => { + console.log(err, "matched"); + }); +``` diff --git a/docs/CustomAuthenticationProvider.md b/docs/CustomAuthenticationProvider.md index 0b590b130..71a26354e 100644 --- a/docs/CustomAuthenticationProvider.md +++ b/docs/CustomAuthenticationProvider.md @@ -18,7 +18,7 @@ class MyAuthenticationProvider implements AuthenticationProvider { * This should return a Promise that resolves to an accessToken (in case of success) or rejects with error (in case of failure) * Basically this method will contain the implementation for getting and refreshing accessTokens */ - public async getAccessToken(): Promise {} + public async getAccessToken(): Promise {} } ``` @@ -30,7 +30,7 @@ Pass instance of MyAuthenticationProvider while initializing. import { MyAuthenticationProvider } from "./MyAuthenticationProvider"; let clientOptions: ClientOptions = { - authProvider: new MyCustomAuthenticationProvider(), + authProvider: new MyAuthenticationProvider(), }; const client = Client.initWithMiddleware(clientOptions); ``` diff --git a/docs/CustomMiddlewareChain.md b/docs/CustomMiddlewareChain.md index e42bdd59c..d86163088 100644 --- a/docs/CustomMiddlewareChain.md +++ b/docs/CustomMiddlewareChain.md @@ -144,4 +144,4 @@ export class MyLoggingHandler implements Middleware { } ``` -Refer [MiddlewareOptions](../src/middleware/option/IMiddlewareOptions.ts) interface to know its structure. +Refer [MiddlewareOptions](../src/middleware/options/IMiddlewareOptions.ts) interface to know its structure. diff --git a/docs/GettingRawResponse.md b/docs/GettingRawResponse.md index 142cd063e..cf3169fec 100644 --- a/docs/GettingRawResponse.md +++ b/docs/GettingRawResponse.md @@ -4,7 +4,7 @@ Steps for getting the raw response [i.e [Response Object](https://developer.mozi ## Initialize the Client -Refer [this documentation](../CreatingClientInstance.md) for initializing the client. +Refer [this documentation](./CreatingClientInstance.md) for initializing the client. ## Getting Raw Response by setting ResponseType diff --git a/docs/tasks/LargeFileUploadTask.md b/docs/tasks/LargeFileUploadTask.md index d1b7af09b..981eff3f7 100644 --- a/docs/tasks/LargeFileUploadTask.md +++ b/docs/tasks/LargeFileUploadTask.md @@ -104,7 +104,7 @@ _You can pass in the customized options using LargeFileUploadTask_ ```typescript async function largeFileUpload(client, file) { - const filename = file.name; + const fileName = file.name; const driveId = ""; const path = ""; try { diff --git a/package-lock.json b/package-lock.json index ba1e60c7b..2ce564910 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@microsoft/microsoft-graph-client", - "version": "2.0.0", + "version": "2.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 5c170f7dc..3f8644c8d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,8 @@ { "name": "@microsoft/microsoft-graph-client", - "version": "2.0.0", + "version": "2.1.0", "description": "Microsoft Graph Client Library", + "license": "MIT", "main": "lib/src/index.js", "module": "lib/es/index.js", "typings": "lib/src/index", diff --git a/samples/browser/package-lock.json b/samples/browser/package-lock.json index ba9e1a6af..08cddce18 100644 --- a/samples/browser/package-lock.json +++ b/samples/browser/package-lock.json @@ -223,12 +223,6 @@ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", "dev": true }, - "eventemitter3": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", - "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==", - "dev": true - }, "finalhandler": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", @@ -408,14 +402,22 @@ } }, "http-proxy": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.17.0.tgz", - "integrity": "sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", "dev": true, "requires": { - "eventemitter3": "^3.0.0", + "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", "requires-port": "^1.0.0" + }, + "dependencies": { + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + } } }, "inflight": { @@ -479,9 +481,9 @@ } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", "dev": true }, "mime": { @@ -1070,9 +1072,9 @@ } }, "yargs-parser": { - "version": "13.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", - "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", "dev": true, "requires": { "camelcase": "^5.0.0", diff --git a/scripts/package-lock.json b/scripts/package-lock.json index e66812be4..fceb874a9 100644 --- a/scripts/package-lock.json +++ b/scripts/package-lock.json @@ -1172,7 +1172,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -1587,7 +1588,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -1643,6 +1645,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -1686,12 +1689,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -2579,9 +2584,9 @@ "dev": true }, "mixin-deep": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", - "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", "dev": true, "requires": { "for-in": "^1.0.2", diff --git a/spec/core/Client.ts b/spec/core/Client.ts index 64165e8e7..55d98cc9e 100644 --- a/spec/core/Client.ts +++ b/spec/core/Client.ts @@ -8,10 +8,15 @@ import { assert } from "chai"; import "isomorphic-fetch"; +import { CustomAuthenticationProvider, TelemetryHandler } from "../../src"; import { Client } from "../../src/Client"; import { AuthProvider } from "../../src/IAuthProvider"; import { ClientOptions } from "../../src/IClientOptions"; import { Options } from "../../src/IOptions"; +import { AuthenticationHandler } from "../../src/middleware/AuthenticationHandler"; +import { ChaosHandler } from "../../src/middleware/ChaosHandler"; +import { ChaosHandlerOptions } from "../../src/middleware/options/ChaosHandlerOptions"; +import { ChaosStrategy } from "../../src/middleware/options/ChaosStrategy"; import { DummyAuthenticationProvider } from "../DummyAuthenticationProvider"; import { DummyHTTPMessageHandler } from "../DummyHTTPMessageHandler"; @@ -61,6 +66,41 @@ describe("Client.ts", () => { assert.equal(error.name, "InvalidMiddlewareChain"); } }); + + it("Init middleware using a middleware array", async () => { + const provider: AuthProvider = (done) => { + done(null, "dummy_token"); + }; + const authHandler = new AuthenticationHandler(new CustomAuthenticationProvider(provider)); + const responseBody = "Test response body"; + const options = new ChaosHandlerOptions(ChaosStrategy.MANUAL, "Testing middleware array", 200, 0, responseBody); + const middlewareArray = [authHandler, new ChaosHandler(options)]; + const client = Client.initWithMiddleware({ middleware: middlewareArray }); + + const response = await client.api("me").get(); + assert.equal(response, responseBody); + }); + + it("Init middleware using a chained middleware array", async () => { + const provider: AuthProvider = (done) => { + done(null, "dummy_token"); + }; + const authHandler = new AuthenticationHandler(new CustomAuthenticationProvider(provider)); + + const responseBody = "Test response body"; + const options = new ChaosHandlerOptions(ChaosStrategy.MANUAL, "Testing chained middleware array", 200, 0, responseBody); + const chaosHandler = new ChaosHandler(options); + const telemetryHandler = new TelemetryHandler(); + + authHandler.setNext(telemetryHandler); + telemetryHandler.setNext(chaosHandler); + + const middlewareArray = [authHandler]; + const client = Client.initWithMiddleware({ middleware: middlewareArray }); + + const response = await client.api("me").get(); + assert.equal(response, responseBody); + }); }); describe("init", () => { diff --git a/spec/core/GraphErrorHandler.ts b/spec/core/GraphErrorHandler.ts index f16ae3ebf..e16c05a4a 100644 --- a/spec/core/GraphErrorHandler.ts +++ b/spec/core/GraphErrorHandler.ts @@ -97,9 +97,9 @@ describe("GraphErrorHandler.ts", () => { it("Should construct some default error", async () => { const gError = await GraphErrorHandler.getError(); + assert.equal(gError.message, ""); assert.equal(gError.statusCode, -1); assert.equal(gError.code, null); - assert.equal(gError.message, null); assert.equal(gError.body, null); assert.equal(gError.requestId, null); }); diff --git a/spec/core/GraphRequestUtil.ts b/spec/core/GraphRequestUtil.ts index 6ce3dff82..19369d355 100644 --- a/spec/core/GraphRequestUtil.ts +++ b/spec/core/GraphRequestUtil.ts @@ -61,10 +61,20 @@ describe("GraphRequestUtil.ts", () => { node2.link = node1; try { serializeContent(node1); - throw new Error("Something wrong with the serialize content, it should stringify cyclic referenced objects"); + throw new Error("Something wrong with the serialize content, it should not stringify cyclic referenced objects"); } catch (error) { assert.equal(error.message, "Unable to stringify the content"); } }); + + it("Should return undefined for the case of undefined content value", () => { + const val = undefined; + assert.equal(serializeContent(val), undefined); + }); + + it("Should return 'null' for the case of null content value", () => { + const val = null; + assert.equal(serializeContent(val), "null"); + }); }); }); diff --git a/spec/core/HTTPClient.ts b/spec/core/HTTPClient.ts index 8c00d9aee..33d6933f7 100644 --- a/spec/core/HTTPClient.ts +++ b/spec/core/HTTPClient.ts @@ -21,7 +21,38 @@ describe("HTTPClient.ts", () => { assert.isDefined(httpClient["middleware"]); assert.equal(httpClient["middleware"], httpMessageHandler); }); + + it("Should create an instance and populate middleware member when passing a middleware array", () => { + const client = new HTTPClient(...[httpMessageHandler]); + assert.isDefined(client["middleware"]); + assert.equal(client["middleware"], httpMessageHandler); + }); + + it("Should throw an error if middleware is undefined", () => { + try { + const client = new HTTPClient(); + } catch (error) { + assert.equal(error.name, "InvalidMiddlewareChain"); + } + }); + + it("Should throw an error if middleware is passed as null", () => { + try { + const client = new HTTPClient(null); + } catch (error) { + assert.equal(error.name, "InvalidMiddlewareChain"); + } + }); + + it("Should throw an error if middleware is passed as an empty array", () => { + try { + const client = new HTTPClient(...[]); + } catch (error) { + assert.equal(error.name, "InvalidMiddlewareChain"); + } + }); }); + /* tslint:enable: no-string-literal */ describe("sendRequest", async () => { diff --git a/spec/core/urlGeneration.ts b/spec/core/urlGeneration.ts index 830b52d90..74ca64ce6 100644 --- a/spec/core/urlGeneration.ts +++ b/spec/core/urlGeneration.ts @@ -91,6 +91,59 @@ cases.push({ .query("$search=senior"), }); +cases.push({ + url: "https://graph.microsoft.com/beta/me/people?$select=displayName,title,id&$count=false&$expand=a($expand=a,b)", + request: client + .api("/me/people") + .version("beta") + .select(["displayName", "title"]) + .count(true) + .expand("a($expand=a,b)") + .query("$select=id") + .query("$count=false"), +}); + +cases.push({ + url: "https://graph.microsoft.com/v1.0/me/people?$select=displayName,title,id&select=value", + request: client + .api("/me/people") + .version("v1.0") + .select(["displayName", "title"]) + .query({ select: "value" }) + .query({ $select: "id" }), +}); + +// handling an invalid input +cases.push({ + url: "https://graph.microsoft.com/v1.0/me/people?$select=displayName,title&select=value&test", + request: client + .api("/me/people") + .version("v1.0") + .select(["displayName", "title"]) + .query({ select: "value" }) + .query("test"), +}); + +// handling an invalid input +cases.push({ + url: "https://graph.microsoft.com/v1.0/me/people?$expand=address($select=home,$expand=city)&$select=home,displayName,title&select=value&test", + request: client + .api("/me/people?$expand=address($select=home,$expand=city)&$select=home") + .version("v1.0") + .select(["displayName", "title"]) + .query({ select: "value" }) + .query("test"), +}); + +cases.push({ + url: "https://graph.microsoft.com/v1.0/me/people?$expand=home($select=home)&name=test", + request: client.api("/me/people").query("?name=test&$expand=home($select=home)"), +}); +cases.push({ + url: "https://graph.microsoft.com/v1.0/me/people?$expand=home($select=home)&name=test", + request: client.api("/me/people?name=test&$expand=home($select=home)"), +}); + cases.push({ url: "https://graph.microsoft.com/v1.0/me/drive/root?$expand=children($select=name),permissions", request: client diff --git a/spec/core/urlParsing.ts b/spec/core/urlParsing.ts index c25f4c797..c1241eac4 100644 --- a/spec/core/urlParsing.ts +++ b/spec/core/urlParsing.ts @@ -28,6 +28,21 @@ const testCases = { "me?$select=displayName": "https://graph.microsoft.com/v1.0/me?$select=displayName", "me?select=displayName": "https://graph.microsoft.com/v1.0/me?select=displayName", "https://graph.microsoft.com/beta/me?select=displayName": "https://graph.microsoft.com/beta/me?select=displayName", + + // test for nested query parameters + "https://graph.microsoft.com/beta/identityGovernance/entitlementManagement/accessPackages/?$expand=accessPackageAssignmentPolicies,accessPackageResourceRoleScopes($expand=accessPackageResourceRole,accessPackageResourceScope)": "https://graph.microsoft.com/beta/identityGovernance/entitlementManagement/accessPackages/?$expand=accessPackageAssignmentPolicies,accessPackageResourceRoleScopes($expand=accessPackageResourceRole,accessPackageResourceScope)", + "me?$select=displayName&$select=id": "https://graph.microsoft.com/v1.0/me?$select=displayName,id", + "/me?$filter=b&$filter=a": "https://graph.microsoft.com/v1.0/me?$filter=a", + "https://graph.microsoft.com/v1.0/me?$top=4&$expand=4&$iscount=true&$top=2": "https://graph.microsoft.com/v1.0/me?$top=2&$expand=4&$iscount=true", + "/items?$expand=fields($select=Title)&$expand=name($select=firstName)": "https://graph.microsoft.com/v1.0/items?$expand=fields($select=Title),name($select=firstName)", + + // Passing invalid parameters + "/me?test&123": "https://graph.microsoft.com/v1.0/me?test&123", + "/me?$select($select=name)": "https://graph.microsoft.com/v1.0/me?$select($select=name)", + "/me/?$filter=any(Actors, Name eq 'John Belushi')": "https://graph.microsoft.com/v1.0/me/?$filter=any(Actors, Name eq 'John Belushi')", + "/me/$filter=any(Actors, it/ID eq Director/ID)": "https://graph.microsoft.com/v1.0/me/$filter=any(Actors, it/ID eq Director/ID)", + "/me?$whatif": "https://graph.microsoft.com/v1.0/me?$whatif", + "/me/?$filter=any(Actors a, any(a/Movies m, a/ID eq m/Director/ID))": "https://graph.microsoft.com/v1.0/me/?$filter=any(Actors a, any(a/Movies m, a/ID eq m/Director/ID))", }; describe("urlParsing.ts", () => { @@ -42,5 +57,5 @@ describe("urlParsing.ts", () => { } } }); - /* tslint:enable: no-string-literal */ }); +/* tslint:enable: no-string-literal */ diff --git a/spec/development/workload/OneNote.ts b/spec/development/workload/OneNote.ts index 4f394a596..87d47e5e3 100644 --- a/spec/development/workload/OneNote.ts +++ b/spec/development/workload/OneNote.ts @@ -70,19 +70,27 @@ describe("OneNote", function() { } }); it("Create a OneNote page with html page content", async () => { - try { - const formData = new FormData(); - formData.append("Presentation", fs.createReadStream("./spec/sample_files/onenotepage.html")); - const json = await client.api(`/me/onenote/sections/${section.id}/pages`).post(formData); - const createdPageFromHTML = json as OnenotePage; + const formData = new FormData(); + formData.append("Presentation", fs.createReadStream("./spec/sample_files/onenotepage.html")); + const json = await client.api(`/me/onenote/sections/${section.id}/pages`).post(formData); + const createdPageFromHTML = json as OnenotePage; - assert.isDefined(createdPage.id); - assert.isDefined(createdPage.contentUrl); - assert.equal("New Page", createdPageFromHTML.title); - assert.isUndefined(createdPage["random fake property that should be null"]); - } catch (error) { - throw error; - } + assert.isDefined(createdPage.id); + assert.isDefined(createdPage.contentUrl); + assert.equal("New Page", createdPageFromHTML.title); + assert.isUndefined(createdPage["random fake property that should be null"]); + }); + + it("Create a OneNote page with application/xhtml+xml page content", async () => { + const body = "A page with a block of HTML

This page contains some formatted text.

"; + const json = await client + .api(`/me/onenote/sections/${section.id}/pages`) + .header("content-type", "application/xhtml+xml") + .post(body); + const createdPageFromHTML = json as OnenotePage; + assert.isDefined(createdPage.id); + assert.isDefined(createdPage.contentUrl); + assert.isUndefined(createdPage["random fake property that should be null"]); }); it("create a OneNote page with html page content and file attachment", async () => { diff --git a/spec/development/workload/PageIterator.ts b/spec/development/workload/PageIterator.ts new file mode 100644 index 000000000..69a793bf9 --- /dev/null +++ b/spec/development/workload/PageIterator.ts @@ -0,0 +1,123 @@ +/** + * ------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. + * See License in the project root for license information. + * ------------------------------------------------------------------------------------------- + */ + +import { assert } from "chai"; +import { Event } from "microsoft-graph"; + +import { PageIterator, PageIteratorCallback, GraphRequestOptions, PageCollection } from "../../../src/tasks/PageIterator"; +import { getClient } from "../test-helper"; +import { ChaosHandler } from "../../../src/middleware/ChaosHandler"; +import { ChaosHandlerOptions } from "../../../src/middleware/options/ChaosHandlerOptions"; +import { ChaosStrategy } from "../../../src/middleware/options/ChaosStrategy"; +import { Client, ClientOptions } from "../../../src"; + +const client = getClient(); +describe("PageIterator", function() { + const pstHeader = { Prefer: 'outlook.timezone= "pacific standard time"' }; + const utc = "UTC"; + const pst = "Pacific Standard Time"; + const testURL = "/me/events"; + + before(async function() { + this.timeout(20000); + + const response = await client.api(testURL).get(); + const numberOfEvents = 4; + const existingEventsCount = response.value.length; + + if (existingEventsCount >= numberOfEvents) { + return; + } + const eventSubject = '"subject": "Test event '; + const eventTimeZone = '"timeZone": "UTC"'; + const eventStartDateTime = '"start": { "dateTime":"' + new Date().toISOString() + '",' + eventTimeZone + "}"; + const eventEndDateTime = '"end": { "dateTime":"' + new Date().toISOString() + '",' + eventTimeZone + "}"; + + for (let i = 1; i <= numberOfEvents - existingEventsCount; i++) { + const eventBody = "{" + eventSubject + "" + 1 + '",' + eventStartDateTime + "," + eventEndDateTime + "}"; + const response = await client.api(testURL).post(eventBody); + if (response.error) { + throw response.error; + } + } + }); + + it("same headers passed with pageIterator", async () => { + const response = await client + .api(`${testURL}?$top=2`) + .headers(pstHeader) + .select("id,start,end") + .get(); + + const callback: PageIteratorCallback = (eventResponse) => { + const event = eventResponse as Event; + assert.equal(event.start.timeZone, pst); + return true; + }; + var requestOptions: GraphRequestOptions = { options: { headers: pstHeader } }; + if (response["@odata.nextLink"]) { + const pageIterator = new PageIterator(client, response, callback, requestOptions); + await pageIterator.iterate(); + assert.isTrue(pageIterator.isComplete()); + } + }).timeout(30 * 1000); + + it("different headers passed with pageIterator", async () => { + const response = await client + .api(`${testURL}?$top=2`) + .headers({ Prefer: `outlook.timezone= "${utc}"` }) + .select("id,start,end") + .get(); + + let counter = 0; + const callback: PageIteratorCallback = (eventResponse) => { + const event = eventResponse as Event; + if (counter < 2) { + assert.equal(event.start.timeZone, utc); + counter++; + } else { + assert.equal(event.start.timeZone, pst); + } + return true; + }; + + var requestOptions = { headers: pstHeader }; + if (response["@odata.nextLink"]) { + const pageIterator = new PageIterator(client, response, callback, requestOptions); + await pageIterator.iterate(); + assert.isTrue(pageIterator.isComplete()); + } + }).timeout(30 * 1000); + + it("setting middleware with pageIterator", async () => { + const middleware = new ChaosHandler(); + const getPageCollection = () => { + return { + value: [], + "@odata.nextLink": "nextURL", + additionalContent: "additional content", + }; + }; + const clientOptions: ClientOptions = { + middleware, + }; + const responseBody = { value: [{ event1: "value1" }, { event2: "value2" }] }; + let counter = 1; + const callback: PageIteratorCallback = (data) => { + assert.equal(data["event" + counter], "value" + counter); + counter++; + return true; + }; + + const middlewareOptions = [new ChaosHandlerOptions(ChaosStrategy.MANUAL, "middleware options for pageIterator", 200, 0, responseBody)]; + const requestOptions = { middlewareOptions }; + + const client = Client.initWithMiddleware(clientOptions); + const pageIterator = new PageIterator(client, getPageCollection(), callback, requestOptions); + await pageIterator.iterate(); + }); +}); diff --git a/spec/middleware/ChaosHandler.ts b/spec/middleware/ChaosHandler.ts new file mode 100644 index 000000000..782ecf58e --- /dev/null +++ b/spec/middleware/ChaosHandler.ts @@ -0,0 +1,254 @@ +import { assert } from "chai"; + +import { Context } from "../../src/IContext"; +import { ChaosHandler } from "../../src/middleware/ChaosHandler"; +import { MiddlewareControl } from "../../src/middleware/MiddlewareControl"; +import { ChaosHandlerOptions } from "../../src/middleware/options/ChaosHandlerOptions"; +import { ChaosStrategy } from "../../src/middleware/options/ChaosStrategy"; +import { RequestMethod } from "../../src/RequestMethod"; +import { DummyHTTPMessageHandler } from "../DummyHTTPMessageHandler"; + +const chaosHandlerOptions = new ChaosHandlerOptions(); +const chaosHandler = new ChaosHandler(); + +describe("ChaosHandler.ts", () => { + /* tslint:disable: no-string-literal */ + describe("constructor", () => { + it("Should create an instance with given options", () => { + const handler = new ChaosHandler(chaosHandlerOptions); + assert.isDefined(handler["options"]); + }); + + it("Should create an instance with default set of options", () => { + const handler = new ChaosHandler(); + assert.isDefined(handler["options"]); + }); + }); + + describe("createResponseHeaders", () => { + it("Should have request-id for every random statusCode", () => { + const responseHeader = chaosHandler["createResponseHeaders"](204, "xxxxxxxxxxxxxxxx", new Date().toString()); + assert.isDefined(responseHeader.get("request-id")); + }); + + it("Should have retry-after for 429 case", () => { + const responseHeader = chaosHandler["createResponseHeaders"](429, "xxxxxxxxxxxxxxxx", new Date().toString()); + assert.isDefined(responseHeader.get("retry-after")); + }); + }); + + describe("createResponseBody", () => { + it("Should return error in response body for error scenarios", () => { + const responseBody = chaosHandler["createResponseBody"](404, "Not Found", "xxxxxxxxxxxxxx", new Date().toString()); + assert.isDefined(responseBody["error"]); + }); + + it("Should return empty response body for success scenarios", () => { + const responseBody = chaosHandler["createResponseBody"](200, "Not Found", "xxxxxxxxxxxxxx", new Date().toString()); + assert.equal(Object.keys(responseBody).length, 0); + }); + }); + + describe("createResponse", () => { + const cxt: Context = { + request: "https://graph.microsoft.com/v1.0/me", + options: { + method: "GET", + }, + }; + + it("Should return a valid response object for MANUAL case", () => { + chaosHandler["createResponse"](new ChaosHandlerOptions(ChaosStrategy.MANUAL, "Manual response", 404), cxt); + assert.isDefined(cxt.response); + }); + + it("Should return a valid response object for RANDOM case", () => { + chaosHandler["createResponse"](new ChaosHandlerOptions(ChaosStrategy.RANDOM), cxt); + assert.isDefined(cxt.response); + }); + }); + + describe("sendRequest", async () => { + const cxt: Context = { + request: "https://graph.microsoft.com/v1.0/me", + options: { + method: "GET", + }, + }; + + const manualMap: Map> = new Map([["/me", new Map([["GET", 500]])]]); + const tempManualOptions: ChaosHandlerOptions = new ChaosHandlerOptions(ChaosStrategy.MANUAL); + const tempChaosHandler = new ChaosHandler(tempManualOptions, manualMap); + + const dummyHTTPHandler = new DummyHTTPMessageHandler(); + const handler = new ChaosHandler(); + handler.setNext(dummyHTTPHandler); + + it("Should return a response after creating it", async () => { + tempChaosHandler["sendRequest"](tempManualOptions, cxt); + assert.isDefined(cxt.response); + }); + + it("Should send the request to the graph", async () => { + handler["sendRequest"](new ChaosHandlerOptions(ChaosStrategy.RANDOM, "I generated the error", undefined, 100), cxt); + assert.isDefined(cxt.response); + }); + }); + + describe("getRandomStatusCode", () => { + it("Should return a status code for GET method", () => { + assert.isDefined(chaosHandler["getRandomStatusCode"](RequestMethod.GET)); + }); + + it("Should return a status code for POST method", () => { + assert.isDefined(chaosHandler["getRandomStatusCode"](RequestMethod.POST)); + }); + + it("Should return a status code for PUT method", () => { + assert.isDefined(chaosHandler["getRandomStatusCode"](RequestMethod.PUT)); + }); + + it("Should return a status code for PATCH method", () => { + assert.isDefined(chaosHandler["getRandomStatusCode"](RequestMethod.PATCH)); + }); + + it("Should return a status code for DELETE method", () => { + assert.isDefined(chaosHandler["getRandomStatusCode"](RequestMethod.DELETE)); + }); + }); + + describe("getRelativeURL", () => { + it("Should return a relative URL for the complete URL", () => { + assert.equal(chaosHandler["getRelativeURL"]("https://graph.microsoft.com/v1.0/me"), "/me"); + }); + + it("Should return a relative URL for the complete URL with filter", () => { + assert.equal(chaosHandler["getRelativeURL"]("https://graph.microsoft.com/v1.0/me/messages?filter=emailAddress eq 'jon@contoso.com'"), "/me/messages"); + }); + + it("Should return a relative URL for the complete URL with ids", () => { + assert.equal(chaosHandler["getRelativeURL"]("https://graph.microsoft.com/v1.0/me/messages/q1abcxx-xxxxxx-xxxxabc"), "/me/messages/q1abcxx-xxxxxx-xxxxabc"); + }); + + it("Should return a relative URL for the complete URL in case of beta", () => { + assert.equal(chaosHandler["getRelativeURL"]("https://graph.microsoft.com/beta/me/messages"), "/me/messages"); + }); + }); + + describe("setStatusCode", () => { + const manualMap: Map> = new Map([["/me/messages/.*", new Map([["GET", 500], ["PATCH", 201]])], ["/me", new Map([["GET", 500], ["PATCH", 201]])]]); + const tempManualOptions: ChaosHandlerOptions = new ChaosHandlerOptions(ChaosStrategy.MANUAL); + const tempManualOptionsRegex: ChaosHandlerOptions = new ChaosHandlerOptions(ChaosStrategy.MANUAL); + const tempChaosHandlerManual = new ChaosHandler(tempManualOptions, manualMap); + const tempChaosHandlerManualRegex = new ChaosHandler(tempManualOptionsRegex, manualMap); + + it("Should set a statusCode for MANUAL mode", () => { + const tempOptions = new ChaosHandlerOptions(ChaosStrategy.MANUAL, "Set status code", 404); + chaosHandler["setStatusCode"](tempOptions, "https://graph.microsoft.com/v1.0/me", RequestMethod.GET); + assert.isDefined(tempOptions.statusCode); + }); + + it("Should set a statusCode for RANDOM mode", () => { + const tempOptions = new ChaosHandlerOptions(ChaosStrategy.RANDOM, "I generated the error", undefined, 100); + chaosHandler["setStatusCode"](tempOptions, "https://graph.microsoft.com/v1.0/me", RequestMethod.POST); + assert.isDefined(tempOptions.statusCode); + }); + + it("Should set a statusCode for MANUAL mode with manualMap", () => { + tempChaosHandlerManual["setStatusCode"](tempManualOptions, "https://graph.microsoft.com/v1.0/me", RequestMethod.PATCH); + assert.equal(tempManualOptions.statusCode, 201); + }); + + it("Should set a statusCode for MANUAL mode with manualMap matching regex", () => { + tempChaosHandlerManualRegex["setStatusCode"](tempManualOptionsRegex, "https://graph.microsoft.com/v1.0/me/messages/abc123-xxxxx-xxxxx", RequestMethod.GET); + assert.equal(tempManualOptionsRegex.statusCode, 500); + }); + }); + + describe("getOptions", () => { + it("Should return the options in the context object", () => { + const options = new ChaosHandlerOptions(ChaosStrategy.MANUAL, "Get options", 405); + const cxt: Context = { + request: "url", + middlewareControl: new MiddlewareControl([options]), + }; + const o = chaosHandler["getOptions"](cxt); + assert.equal(o.chaosStrategy, ChaosStrategy.MANUAL); + assert.equal(o.statusCode, 405); + }); + + it("Should return the default set of options with RANDOM in the middleware", () => { + const cxt: Context = { + request: "url", + }; + const o = chaosHandler["getOptions"](cxt); + assert.equal(o.chaosStrategy, ChaosStrategy.RANDOM); + assert.equal(o.statusCode, undefined); + }); + + it("Should return the default set of options with DEFAULT in the middleware", () => { + const tempChaosHandler = new ChaosHandler(new ChaosHandlerOptions(ChaosStrategy.MANUAL)); + const cxt: Context = { + request: "url", + }; + const o = tempChaosHandler["getOptions"](cxt); + assert.equal(o.chaosStrategy, ChaosStrategy.MANUAL); + assert.equal(o.statusCode, undefined); + }); + }); + + describe("execute", async () => { + const manualMap: Map> = new Map([["/me", new Map([["GET", 500], ["PATCH", 201]])]]); + const dummyHTTPHandler = new DummyHTTPMessageHandler(); + const tempChaosHandlerDefault = new ChaosHandler(new ChaosHandlerOptions()); + const tempChaosHandlerRandom = new ChaosHandler(new ChaosHandlerOptions(ChaosStrategy.RANDOM)); + const tempChaosHandlerManual = new ChaosHandler(new ChaosHandlerOptions(ChaosStrategy.MANUAL), manualMap); + tempChaosHandlerDefault.setNext(dummyHTTPHandler); + tempChaosHandlerRandom.setNext(dummyHTTPHandler); + tempChaosHandlerManual.setNext(dummyHTTPHandler); + + it("Should return response for Default Case", async () => { + const options = new ChaosHandlerOptions(ChaosStrategy.RANDOM); + const cxt: Context = { + request: "https://graph.microsoft.com/v1.0/me", + options: { + method: "GET", + }, + middlewareControl: new MiddlewareControl([options]), + }; + assert.isDefined(tempChaosHandlerDefault["execute"](cxt)); + }); + + it("Should return response for Random case", async () => { + const cxt: Context = { + request: "https://graph.microsoft.com/v1.0/me", + options: { + method: "GET", + }, + }; + assert.isDefined(tempChaosHandlerRandom["execute"](cxt)); + }); + + it("Should return response for Manual Global case", async () => { + const cxt: Context = { + request: "https://graph.microsoft.com/v1.0/me", + options: { + method: "GET", + }, + }; + assert.isDefined(tempChaosHandlerManual["execute"](cxt)); + }); + + it("Should return response for Manual Request Level case", async () => { + const options = new ChaosHandlerOptions(ChaosStrategy.MANUAL, "Manual Request level case", 200); + const cxt: Context = { + request: "https://graph.microsoft.com/v1.0/me", + options: { + method: "GET", + }, + middlewareControl: new MiddlewareControl([options]), + }; + assert.isDefined(tempChaosHandlerManual["execute"](cxt)); + }); + }); +}); diff --git a/spec/middleware/MiddlewareFactory.ts b/spec/middleware/MiddlewareFactory.ts new file mode 100644 index 000000000..33d71dcda --- /dev/null +++ b/spec/middleware/MiddlewareFactory.ts @@ -0,0 +1,27 @@ +/** + * ------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. + * See License in the project root for license information. + * ------------------------------------------------------------------------------------------- + */ + +import { assert } from "chai"; + +import { AuthenticationHandler, CustomAuthenticationProvider, HTTPMessageHandler, RedirectHandler, RetryHandler, TelemetryHandler } from "../../src"; +import { AuthProvider } from "../../src/IAuthProvider"; +import { MiddlewareFactory } from "../../src/middleware/MiddlewareFactory"; + +describe("MiddlewareFactory", () => { + it("Should return the default pipeline", () => { + const provider: AuthProvider = (done) => { + done(null, "dummy_token"); + }; + const defaultMiddleWareArray = MiddlewareFactory.getDefaultMiddlewareChain(new CustomAuthenticationProvider(provider)); + + assert.isTrue(defaultMiddleWareArray[0] instanceof AuthenticationHandler); + assert.isTrue(defaultMiddleWareArray[1] instanceof RetryHandler); + assert.isTrue(defaultMiddleWareArray[2] instanceof RedirectHandler); + assert.isTrue(defaultMiddleWareArray[3] instanceof TelemetryHandler); + assert.isTrue(defaultMiddleWareArray[4] instanceof HTTPMessageHandler); + }); +}); diff --git a/src/Client.ts b/src/Client.ts index 0cd9b04c8..ede9074a4 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -93,7 +93,7 @@ export class Client { } else if (clientOptions.authProvider !== undefined) { httpClient = HTTPClientFactory.createWithAuthenticationProvider(clientOptions.authProvider); } else if (clientOptions.middleware !== undefined) { - httpClient = new HTTPClient(clientOptions.middleware); + httpClient = new HTTPClient(...[].concat(clientOptions.middleware)); } else { const error = new Error(); error.name = "InvalidMiddlewareChain"; diff --git a/src/GraphError.ts b/src/GraphError.ts index 08dc7bde2..d3433e879 100644 --- a/src/GraphError.ts +++ b/src/GraphError.ts @@ -17,7 +17,7 @@ * Some fields are renamed ie, "request-id" => requestId so you can use dot notation */ -export class GraphError { +export class GraphError extends Error { /** * @public * A member holding status code of the error @@ -30,12 +30,6 @@ export class GraphError { */ public code: string | null; - /** - * @public - * A member holding error message - */ - public message: string | null; - /** * @public * A member holding request-id i.e identifier of the request @@ -61,12 +55,13 @@ export class GraphError { * @param {number} [statusCode = -1] - The status code of the error * @returns An instance of GraphError */ - public constructor(statusCode: number = -1) { + public constructor(statusCode: number = -1, message?: string, baseError?: Error) { + super(message || (baseError && baseError.message)); this.statusCode = statusCode; this.code = null; - this.message = null; this.requestId = null; this.date = new Date(); this.body = null; + this.stack = baseError ? baseError.stack : this.stack; } } diff --git a/src/GraphErrorHandler.ts b/src/GraphErrorHandler.ts index e5506663d..310596968 100644 --- a/src/GraphErrorHandler.ts +++ b/src/GraphErrorHandler.ts @@ -27,12 +27,11 @@ export class GraphErrorHandler { * @returns The GraphError instance */ private static constructError(error: Error, statusCode?: number): GraphError { - const gError = new GraphError(statusCode); + const gError = new GraphError(statusCode, "", error); if (error.name !== undefined) { gError.code = error.name; } gError.body = error.toString(); - gError.message = error.message; gError.date = new Date(); return gError; } @@ -60,9 +59,8 @@ export class GraphErrorHandler { */ private static constructErrorFromResponse(error: any, statusCode: number): GraphError { error = error.error; - const gError = new GraphError(statusCode); + const gError = new GraphError(statusCode, error.message); gError.code = error.code; - gError.message = error.message; if (error.innerError !== undefined) { gError.requestId = error.innerError["request-id"]; gError.date = new Date(error.innerError.date); diff --git a/src/GraphRequest.ts b/src/GraphRequest.ts index 79762b462..7ec736be1 100644 --- a/src/GraphRequest.ts +++ b/src/GraphRequest.ts @@ -22,7 +22,6 @@ import { MiddlewareControl } from "./middleware/MiddlewareControl"; import { MiddlewareOptions } from "./middleware/options/IMiddlewareOptions"; import { RequestMethod } from "./RequestMethod"; import { ResponseType } from "./ResponseType"; - /** * @interface * Signature to representing key value pairs @@ -42,6 +41,7 @@ interface KeyValuePairObjectStringNumber { * @property {string} [path] - The path of the resource request * @property {KeyValuePairObjectStringNumber} oDataQueryParams - The oData Query Params * @property {KeyValuePairObjectStringNumber} otherURLQueryParams - The other query params for a request + * @property {string[]} otherURLQueryOptions - The non key-value query parameters. Example- '/me?$whatif' */ export interface URLComponents { host: string; @@ -49,6 +49,7 @@ export interface URLComponents { path?: string; oDataQueryParams: KeyValuePairObjectStringNumber; otherURLQueryParams: KeyValuePairObjectStringNumber; + otherURLQueryOptions?: string[]; } /** @@ -79,9 +80,7 @@ export class GraphRequest { * @private * A member to hold custom header options for a request */ - private _headers: { - [key: string]: string; - }; + private _headers: HeadersInit; /** * @private @@ -118,6 +117,7 @@ export class GraphRequest { version: this.config.defaultVersion, oDataQueryParams: {}, otherURLQueryParams: {}, + otherURLQueryOptions: [], }; this._headers = {}; this._options = {}; @@ -170,14 +170,7 @@ export class GraphRequest { // Capture query string into oDataQueryParams and otherURLQueryParams const queryParams = path.substring(queryStrPos + 1, path.length).split("&"); for (const queryParam of queryParams) { - const qParams = queryParam.split("="); - const key = qParams[0]; - const value = qParams[1]; - if (oDataQueryNames.indexOf(key) !== -1) { - this.urlComponents.oDataQueryParams[key] = value; - } else { - this.urlComponents.otherURLQueryParams[key] = value; - } + this.parseQueryParameter(queryParam); } } }; @@ -244,9 +237,102 @@ export class GraphRequest { } } } + + if (urlComponents.otherURLQueryOptions.length !== 0) { + for (const str of urlComponents.otherURLQueryOptions) { + query.push(str); + } + } return query.length > 0 ? "?" + query.join("&") : ""; } + /** + * @private + * Parses the query parameters to set the urlComponents property of the GraphRequest object + * @param {string|KeyValuePairObjectStringNumber} queryDictionaryOrString - The query parameter + * @returns The same GraphRequest instance that is being called with + */ + private parseQueryParameter(queryDictionaryOrString: string | KeyValuePairObjectStringNumber): GraphRequest { + if (typeof queryDictionaryOrString === "string") { + if (queryDictionaryOrString.charAt(0) === "?") { + queryDictionaryOrString = queryDictionaryOrString.substring(1); + } + + if (queryDictionaryOrString.indexOf("&") !== -1) { + const queryParams = queryDictionaryOrString.split("&"); + for (const str of queryParams) { + this.parseQueryParamenterString(str); + } + } else { + this.parseQueryParamenterString(queryDictionaryOrString); + } + } else if (queryDictionaryOrString.constructor === Object) { + for (const key in queryDictionaryOrString) { + if (queryDictionaryOrString.hasOwnProperty(key)) { + this.setURLComponentsQueryParamater(key, queryDictionaryOrString[key]); + } + } + } + + return this; + } + + /** + * @private + * Parses the query parameter of string type to set the urlComponents property of the GraphRequest object + * @param {string} queryParameter - the query parameters + * returns nothing + */ + private parseQueryParamenterString(queryParameter: string): void { + /* The query key-value pair must be split on the first equals sign to avoid errors in parsing nested query parameters. + Example-> "/me?$expand=home($select=city)" */ + if (this.isValidQueryKeyValuePair(queryParameter)) { + const indexOfFirstEquals = queryParameter.indexOf("="); + const paramKey = queryParameter.substring(0, indexOfFirstEquals); + const paramValue = queryParameter.substring(indexOfFirstEquals + 1); + this.setURLComponentsQueryParamater(paramKey, paramValue); + } else { + /* Push values which are not of key-value structure. + Example-> Handle an invalid input->.query(test), .query($select($select=name)) and let the Graph API respond with the error in the URL*/ + this.urlComponents.otherURLQueryOptions.push(queryParameter); + } + } + + /** + * @private + * Sets values into the urlComponents property of GraphRequest object. + * @param {string} paramKey - the query parameter key + * @param {string} paramValue - the query paramter value + * @returns nothing + */ + private setURLComponentsQueryParamater(paramKey: string, paramValue: string | number): void { + if (oDataQueryNames.indexOf(paramKey) !== -1) { + const currentValue = this.urlComponents.oDataQueryParams[paramKey]; + const isValueAppendable = currentValue && (paramKey === "$expand" || paramKey === "$select" || paramKey === "$orderby"); + this.urlComponents.oDataQueryParams[paramKey] = isValueAppendable ? currentValue + "," + paramValue : paramValue; + } else { + this.urlComponents.otherURLQueryParams[paramKey] = paramValue; + } + } + /** + * @private + * Check if the query parameter string has a valid key-value structure + * @param {string} queryString - the query parameter string. Example -> "name=value" + * #returns true if the query string has a valid key-value structure else false + */ + private isValidQueryKeyValuePair(queryString: string): boolean { + const indexofFirstEquals = queryString.indexOf("="); + if (indexofFirstEquals === -1) { + return false; + } + const indexofOpeningParanthesis = queryString.indexOf("("); + if (indexofOpeningParanthesis !== -1 && queryString.indexOf("(") < indexofFirstEquals) { + // Example -> .query($select($expand=true)); + return false; + } + return true; + } + /** * @private * Updates the custom headers and options for a request @@ -302,6 +388,27 @@ export class GraphRequest { } } + /** + * @private + * Checks if the content-type is present in the _headers property. If not present, defaults the content-type to application/json + * @param none + * @returns nothing + */ + private setHeaderContentType(): void { + if (!this._headers) { + this.header("Content-Type", "application/json"); + return; + } + const headerKeys = Object.keys(this._headers); + for (const headerKey of headerKeys) { + if (headerKey.toLowerCase() === "content-type") { + return; + } + } + // Default the content-type to application/json in case the content-type is not present in the header + this.header("Content-Type", "application/json"); + } + /** * @public * Sets the custom header for a request @@ -317,10 +424,10 @@ export class GraphRequest { /** * @public * Sets the custom headers for a request - * @param {KeyValuePairObjectStringNumber} headers - The headers key value pair object + * @param {KeyValuePairObjectStringNumber | HeadersInit} headers - The request headers * @returns The same GraphRequest instance that is being called with */ - public headers(headers: KeyValuePairObjectStringNumber): GraphRequest { + public headers(headers: KeyValuePairObjectStringNumber | HeadersInit): GraphRequest { for (const key in headers) { if (headers.hasOwnProperty(key)) { this._headers[key] = headers[key] as string; @@ -393,7 +500,7 @@ export class GraphRequest { * @public * To add properties for select OData Query param * @param {string|string[]} properties - The Properties value - * @returns The same GraphRequest instance that is being called with + * @returns The same GraphRequest instance that is being called with, after adding the properties for $select query */ /* * Accepts .select("displayName,birthday") @@ -410,7 +517,7 @@ export class GraphRequest { * @public * To add properties for expand OData Query param * @param {string|string[]} properties - The Properties value - * @returns The same GraphRequest instance that is being called with + * @returns The same GraphRequest instance that is being called with, after adding the properties for $expand query */ public expand(properties: string | string[]): GraphRequest { this.addCsvQueryParameter("$expand", properties, arguments); @@ -421,7 +528,7 @@ export class GraphRequest { * @public * To add properties for orderby OData Query param * @param {string|string[]} properties - The Properties value - * @returns The same GraphRequest instance that is being called with + * @returns The same GraphRequest instance that is being called with, after adding the properties for $orderby query */ public orderby(properties: string | string[]): GraphRequest { this.addCsvQueryParameter("$orderby", properties, arguments); @@ -430,9 +537,9 @@ export class GraphRequest { /** * @public - * To add query string for filter OData Query param + * To add query string for filter OData Query param. The request URL accepts only one $filter Odata Query option and its value is set to the most recently passed filter query string. * @param {string} filterStr - The filter query string - * @returns The same GraphRequest instance that is being called with + * @returns The same GraphRequest instance that is being called with, after adding the $filter query */ public filter(filterStr: string): GraphRequest { this.urlComponents.oDataQueryParams.$filter = filterStr; @@ -441,9 +548,9 @@ export class GraphRequest { /** * @public - * To add criterion for search OData Query param + * To add criterion for search OData Query param. The request URL accepts only one $search Odata Query option and its value is set to the most recently passed search criterion string. * @param {string} searchStr - The search criterion string - * @returns The same GraphRequest instance that is being called with + * @returns The same GraphRequest instance that is being called with, after adding the $search query criteria */ public search(searchStr: string): GraphRequest { this.urlComponents.oDataQueryParams.$search = searchStr; @@ -452,9 +559,9 @@ export class GraphRequest { /** * @public - * To add number for top OData Query param + * To add number for top OData Query param. The request URL accepts only one $top Odata Query option and its value is set to the most recently passed number value. * @param {number} n - The number value - * @returns The same GraphRequest instance that is being called with + * @returns The same GraphRequest instance that is being called with, after adding the number for $top query */ public top(n: number): GraphRequest { this.urlComponents.oDataQueryParams.$top = n; @@ -463,9 +570,9 @@ export class GraphRequest { /** * @public - * To add number for skip OData Query param + * To add number for skip OData Query param. The request URL accepts only one $skip Odata Query option and its value is set to the most recently passed number value. * @param {number} n - The number value - * @returns The same GraphRequest instance that is being called with + * @returns The same GraphRequest instance that is being called with, after adding the number for the $skip query */ public skip(n: number): GraphRequest { this.urlComponents.oDataQueryParams.$skip = n; @@ -474,9 +581,9 @@ export class GraphRequest { /** * @public - * To add token string for skipToken OData Query param + * To add token string for skipToken OData Query param. The request URL accepts only one $skipToken Odata Query option and its value is set to the most recently passed token value. * @param {string} token - The token value - * @returns The same GraphRequest instance that is being called with + * @returns The same GraphRequest instance that is being called with, after adding the token string for $skipToken query option */ public skipToken(token: string): GraphRequest { this.urlComponents.oDataQueryParams.$skipToken = token; @@ -485,11 +592,11 @@ export class GraphRequest { /** * @public - * To add boolean for count OData Query param + * To add boolean for count OData Query param. The URL accepts only one $count Odata Query option and its value is set to the most recently passed boolean value. * @param {boolean} isCount - The count boolean - * @returns The same GraphRequest instance that is being called with + * @returns The same GraphRequest instance that is being called with, after adding the boolean value for the $count query option */ - public count(isCount: boolean): GraphRequest { + public count(isCount: boolean = false): GraphRequest { this.urlComponents.oDataQueryParams.$count = isCount.toString(); return this; } @@ -498,23 +605,14 @@ export class GraphRequest { * @public * Appends query string to the urlComponent * @param {string|KeyValuePairObjectStringNumber} queryDictionaryOrString - The query value - * @returns The same GraphRequest instance that is being called with + * @returns The same GraphRequest instance that is being called with, after appending the query string to the url component + */ + /* + * Accepts .query("displayName=xyz") + * and .select({ name: "value" }) */ public query(queryDictionaryOrString: string | KeyValuePairObjectStringNumber): GraphRequest { - const otherURLQueryParams = this.urlComponents.otherURLQueryParams; - if (typeof queryDictionaryOrString === "string") { - const querySplit = queryDictionaryOrString.split("="); - const queryKey = querySplit[0]; - const queryValue = querySplit[1]; - otherURLQueryParams[queryKey] = queryValue; - } else { - for (const key in queryDictionaryOrString) { - if (queryDictionaryOrString.hasOwnProperty(key)) { - otherURLQueryParams[key] = queryDictionaryOrString[key]; - } - } - } - return this; + return this.parseQueryParameter(queryDictionaryOrString); } /** @@ -550,13 +648,15 @@ export class GraphRequest { const options: FetchOptions = { method: RequestMethod.POST, body: serializeContent(content), - headers: - typeof FormData !== "undefined" && content instanceof FormData - ? {} - : { - "Content-Type": "application/json", - }, }; + const className: string = content && content.constructor && content.constructor.name; + if (className === "FormData") { + // Content-Type headers should not be specified in case the of FormData type content + options.headers = {}; + } else { + this.setHeaderContentType(); + options.headers = this._headers; + } try { const response = await this.send(url, options, callback); return response; @@ -591,12 +691,10 @@ export class GraphRequest { */ public async put(content: any, callback?: GraphRequestCallback): Promise { const url = this.buildFullUrl(); + this.setHeaderContentType(); const options: FetchOptions = { method: RequestMethod.PUT, body: serializeContent(content), - headers: { - "Content-Type": "application/json", - }, }; try { const response = await this.send(url, options, callback); @@ -616,12 +714,10 @@ export class GraphRequest { */ public async patch(content: any, callback?: GraphRequestCallback): Promise { const url = this.buildFullUrl(); + this.setHeaderContentType(); const options: FetchOptions = { method: RequestMethod.PATCH, body: serializeContent(content), - headers: { - "Content-Type": "application/json", - }, }; try { const response = await this.send(url, options, callback); diff --git a/src/GraphRequestUtil.ts b/src/GraphRequestUtil.ts index c58062ad7..a9ab02bb1 100644 --- a/src/GraphRequestUtil.ts +++ b/src/GraphRequestUtil.ts @@ -41,7 +41,7 @@ export const urlJoin = (urlSegments: string[]): string => { */ export const serializeContent = (content: any): any => { - const className: string = content.constructor.name; + const className: string = content && content.constructor && content.constructor.name; if (className === "Buffer" || className === "Blob" || className === "File" || className === "FormData" || typeof content === "string") { return content; } diff --git a/src/HTTPClient.ts b/src/HTTPClient.ts index 2276fb227..c4e866d95 100644 --- a/src/HTTPClient.ts +++ b/src/HTTPClient.ts @@ -27,10 +27,48 @@ export class HTTPClient { * @public * @constructor * Creates an instance of a HTTPClient - * @param {Middleware} middleware - The first middleware of the middleware chain + * @param {...Middleware} middleware - The first middleware of the middleware chain or a sequence of all the Middleware handlers */ - public constructor(middleware: Middleware) { - this.middleware = middleware; + public constructor(...middleware: Middleware[]) { + if (!middleware || !middleware.length) { + const error = new Error(); + error.name = "InvalidMiddlewareChain"; + error.message = "Please provide a default middleware chain or custom middleware chain"; + throw error; + } + this.setMiddleware(...middleware); + } + + /** + * @private + * Processes the middleware parameter passed to set this.middleware property + * The calling function should validate if middleware is not undefined or not empty. + * @param {...Middleware} middleware - The middleware passed + * @returns Nothing + */ + private setMiddleware(...middleware: Middleware[]): void { + if (middleware.length > 1) { + this.parseMiddleWareArray(middleware); + } else { + this.middleware = middleware[0]; + } + } + + /** + * @private + * Processes the middleware array to construct the chain + * and sets this.middleware property to the first middlware handler of the array + * The calling function should validate if middleware is not undefined or not empty + * @param {Middleware[]} middlewareArray - The array of middleware handlers + * @returns Nothing + */ + private parseMiddleWareArray(middlewareArray: Middleware[]) { + middlewareArray.forEach((element, index) => { + if (index < middlewareArray.length - 1) { + element.setNext(middlewareArray[index + 1]); + } + }); + this.middleware = middlewareArray[0]; } /** diff --git a/src/HTTPClientFactory.ts b/src/HTTPClientFactory.ts index 673b90dbe..d97fce35f 100644 --- a/src/HTTPClientFactory.ts +++ b/src/HTTPClientFactory.ts @@ -26,7 +26,7 @@ import { TelemetryHandler } from "./middleware/TelemetryHandler"; * @returns A boolean representing the environment is node or not */ const isNodeEnvironment = (): boolean => { - return new Function("try {return this === global;}catch(e){return false;}")(); // tslint:disable-line: function-constructor + return typeof process === "object" && typeof require === "function"; }; /** @@ -69,10 +69,11 @@ export class HTTPClientFactory { * @public * @static * Creates a middleware chain with the given one - * @param {Middleware} middleware - The first middleware of the middleware chain + * @property {...Middleware} middleware - The first middleware of the middleware chain or a sequence of all the Middleware handlers * @returns A HTTPClient instance */ - public static createWithMiddleware(middleware: Middleware): HTTPClient { - return new HTTPClient(middleware); + public static createWithMiddleware(...middleware: Middleware[]): HTTPClient { + // Middleware should not empty or undefined. This is check is present in the HTTPClient constructor. + return new HTTPClient(...middleware); } } diff --git a/src/IClientOptions.ts b/src/IClientOptions.ts index f87c10454..c3b0c2ea2 100644 --- a/src/IClientOptions.ts +++ b/src/IClientOptions.ts @@ -17,7 +17,7 @@ import { Middleware } from "./middleware/IMiddleware"; * @property {boolean} [debugLogging] - The boolean to enable/disable debug logging * @property {string} [defaultVersion] - The default version that needs to be used while making graph api request * @property {FetchOptions} [fetchOptions] - The options for fetch request - * @property {Middleware} [middleware] - The first middleware of the middleware chain + * @property {Middleware| Middleware[]} [middleware] - The first middleware of the middleware chain or an array of the Middleware handlers */ export interface ClientOptions { authProvider?: AuthenticationProvider; @@ -25,5 +25,5 @@ export interface ClientOptions { debugLogging?: boolean; defaultVersion?: string; fetchOptions?: FetchOptions; - middleware?: Middleware; + middleware?: Middleware | Middleware[]; } diff --git a/src/Version.ts b/src/Version.ts index b67e5277e..597ea1575 100644 --- a/src/Version.ts +++ b/src/Version.ts @@ -12,4 +12,4 @@ * @module Version */ -export const PACKAGE_VERSION = "2.0.0"; +export const PACKAGE_VERSION = "2.1.0"; diff --git a/src/browser/index.ts b/src/browser/index.ts index dd5a0bec3..e6f54ae11 100644 --- a/src/browser/index.ts +++ b/src/browser/index.ts @@ -13,17 +13,21 @@ export * from "../middleware/HTTPMessageHandler"; export * from "../middleware/IMiddleware"; export * from "../middleware/RetryHandler"; export * from "../middleware/TelemetryHandler"; - +export * from "../middleware/MiddlewareFactory"; export * from "../middleware/options/AuthenticationHandlerOptions"; export * from "../middleware/options/IMiddlewareOptions"; export * from "../middleware/options/RetryHandlerOptions"; export * from "../middleware/options/TelemetryHandlerOptions"; +export * from "../middleware/options/ChaosHandlerOptions"; +export * from "../middleware/options/ChaosStrategy"; +export * from "../middleware/ChaosHandler"; export * from "../tasks/LargeFileUploadTask"; export * from "../tasks/OneDriveLargeFileUploadTask"; export * from "../tasks/PageIterator"; export * from "../Client"; +export * from "../CustomAuthenticationProvider"; export * from "../GraphError"; export * from "../GraphRequest"; export * from "../IAuthProvider"; diff --git a/src/index.ts b/src/index.ts index 6f0ccb39b..d0250f0e3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,18 +14,22 @@ export * from "./middleware/IMiddleware"; export * from "./middleware/RetryHandler"; export * from "./middleware/RedirectHandler"; export * from "./middleware/TelemetryHandler"; - +export * from "./middleware/MiddlewareFactory"; export * from "./middleware/options/AuthenticationHandlerOptions"; export * from "./middleware/options/IMiddlewareOptions"; export * from "./middleware/options/RetryHandlerOptions"; export * from "./middleware/options/RedirectHandlerOptions"; export * from "./middleware/options/TelemetryHandlerOptions"; +export * from "./middleware/options/ChaosHandlerOptions"; +export * from "./middleware/options/ChaosStrategy"; +export * from "./middleware/ChaosHandler"; export * from "./tasks/LargeFileUploadTask"; export * from "./tasks/OneDriveLargeFileUploadTask"; export * from "./tasks/PageIterator"; export * from "./Client"; +export * from "./CustomAuthenticationProvider"; export * from "./GraphError"; export * from "./GraphRequest"; export * from "./IAuthProvider"; diff --git a/src/middleware/ChaosHandler.ts b/src/middleware/ChaosHandler.ts new file mode 100644 index 000000000..5944cad79 --- /dev/null +++ b/src/middleware/ChaosHandler.ts @@ -0,0 +1,286 @@ +/** + * ------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. + * See License in the project root for license information. + * ------------------------------------------------------------------------------------------- + */ + +/** + * @module ChaosHandler + */ + +import { Context } from "../IContext"; +import { RequestMethod } from "../RequestMethod"; + +import { Middleware } from "./IMiddleware"; +import { MiddlewareControl } from "./MiddlewareControl"; +import { generateUUID } from "./MiddlewareUtil"; +import { httpStatusCode, methodStatusCode } from "./options/ChaosHandlerData"; +import { ChaosHandlerOptions } from "./options/ChaosHandlerOptions"; +import { ChaosStrategy } from "./options/ChaosStrategy"; + +/** + * Class representing ChaosHandler + * @class + * Class + * @implements Middleware + */ +export class ChaosHandler implements Middleware { + /** + * A member holding options to customize the handler behavior + * + * @private + */ + private options: ChaosHandlerOptions; + + /** + * container for the manual map that has been written by the client + * + * @private + */ + private manualMap: Map>; + + /** + * @private + * A member to hold next middleware in the middleware chain + */ + private nextMiddleware: Middleware; + + /** + * @public + * @constructor + * To create an instance of Testing Handler + * @param {ChaosHandlerOptions} [options = new ChaosHandlerOptions()] - The testing handler options instance + * @param manualMap - The Map passed by user containing url-statusCode info + * @returns An instance of Testing Handler + */ + public constructor(options: ChaosHandlerOptions = new ChaosHandlerOptions(), manualMap?: Map>) { + this.options = options; + this.manualMap = manualMap; + } + + /** + * Generates responseHeader + * @private + * @param {number} statusCode - the status code to be returned for the request + * @param {string} requestID - request id + * @param {string} requestDate - date of the request + * @returns response Header + */ + private createResponseHeaders(statusCode: number, requestID: string, requestDate: string) { + const responseHeader: Headers = new Headers(); + + responseHeader.append("Cache-Control", "no-store"); + responseHeader.append("request-id", requestID); + responseHeader.append("client-request-id", requestID); + responseHeader.append("x-ms-ags-diagnostic", ""); + responseHeader.append("Date", requestDate); + responseHeader.append("Strict-Transport-Security", ""); + + if (statusCode === 429) { + // throttling case has to have a timeout scenario + responseHeader.append("retry-after", "300"); + } + return responseHeader; + } + + /** + * Generates responseBody + * @private + * @param {number} statusCode - the status code to be returned for the request + * @param {string} statusMessage - the status message to be returned for the request + * @param {string} requestID - request id + * @param {string} requestDate - date of the request + * @param {any?} requestBody - the request body to be returned for the request + * @returns response body + */ + private createResponseBody(statusCode: number, statusMessage: string, requestID: string, requestDate: string, responseBody?: any) { + if (responseBody) { + return responseBody; + } + let body: any; + if (statusCode >= 400) { + const codeMessage: string = httpStatusCode[statusCode]; + const errMessage: string = statusMessage; + + body = { + error: { + code: codeMessage, + message: errMessage, + innerError: { + "request-id": requestID, + date: requestDate, + }, + }, + }; + } else { + body = {}; + } + return body; + } + + /** + * creates a response + * @private + * @param {ChaosHandlerOptions} ChaosHandlerOptions - The ChaosHandlerOptions object + * @param {Context} context - Contains the context of the request + */ + private createResponse(chaosHandlerOptions: ChaosHandlerOptions, context: Context) { + try { + let responseBody: any; + let responseHeader: Headers; + let requestID: string; + let requestDate: Date; + const requestURL = context.request as string; + + requestID = generateUUID(); + requestDate = new Date(); + responseHeader = this.createResponseHeaders(chaosHandlerOptions.statusCode, requestID, requestDate.toString()); + responseBody = this.createResponseBody(chaosHandlerOptions.statusCode, chaosHandlerOptions.statusMessage, requestID, requestDate.toString(), chaosHandlerOptions.responseBody); + const init: any = { url: requestURL, status: chaosHandlerOptions.statusCode, statusText: chaosHandlerOptions.statusMessage, headers: responseHeader }; + context.response = new Response(responseBody, init); + } catch (error) { + throw error; + } + } + + /** + * Decides whether to send the request to the graph or not + * @private + * @param {ChaosHandlerOptions} chaosHandlerOptions - A ChaosHandlerOptions object + * @param {Context} context - Contains the context of the request + * @returns nothing + */ + private async sendRequest(chaosHandlerOptions: ChaosHandlerOptions, context: Context): Promise { + try { + this.setStatusCode(chaosHandlerOptions, context.request as string, context.options.method as RequestMethod); + if (!chaosHandlerOptions.statusCode) { + await this.nextMiddleware.execute(context); + } else { + this.createResponse(chaosHandlerOptions, context); + } + } catch (error) { + throw error; + } + } + + /** + * Fetches a random status code for the RANDOM mode from the predefined array + * @private + * @param {string} requestMethod - the API method for the request + * @returns a random status code from a given set of status codes + */ + private getRandomStatusCode(requestMethod: RequestMethod): number { + try { + const statusCodeArray: number[] = methodStatusCode[requestMethod] as number[]; + return statusCodeArray[Math.floor(Math.random() * statusCodeArray.length)]; + } catch (error) { + throw error; + } + } + + /** + * To fetch the relative URL out of the complete URL using a predefined regex pattern + * @private + * @param {string} urlMethod - the complete URL + * @returns the string as relative URL + */ + private getRelativeURL(urlMethod: string): string { + const pattern: RegExp = /https?:\/\/graph\.microsoft\.com\/[^/]+(.+?)(\?|$)/; + let relativeURL: string; + if (pattern.exec(urlMethod) !== null) { + relativeURL = pattern.exec(urlMethod)[1]; + } + return relativeURL; + } + + /** + * To fetch the status code from the map(if needed), then returns response by calling createResponse + * @private + * @param {ChaosHandlerOptions} ChaosHandlerOptions - The ChaosHandlerOptions object + * @param {string} requestURL - the URL for the request + * @param {string} requestMethod - the API method for the request + */ + private setStatusCode(chaosHandlerOptions: ChaosHandlerOptions, requestURL: string, requestMethod: RequestMethod) { + try { + if (chaosHandlerOptions.chaosStrategy === ChaosStrategy.MANUAL) { + if (chaosHandlerOptions.statusCode === undefined) { + // manual mode with no status code, can be a global level or request level without statusCode + const relativeURL: string = this.getRelativeURL(requestURL); + if (this.manualMap.get(relativeURL) !== undefined) { + // checking Manual Map for exact match + if (this.manualMap.get(relativeURL).get(requestMethod) !== undefined) { + chaosHandlerOptions.statusCode = this.manualMap.get(relativeURL).get(requestMethod); + } + // else statusCode would be undefined + } else { + // checking for regex match if exact match doesn't work + this.manualMap.forEach((value: Map, key: string) => { + const regexURL: RegExp = new RegExp(key + "$"); + if (regexURL.test(relativeURL)) { + if (this.manualMap.get(key).get(requestMethod) !== undefined) { + chaosHandlerOptions.statusCode = this.manualMap.get(key).get(requestMethod); + } + // else statusCode would be undefined + } + }); + } + + // Case of redirection or request url not in map ---> statusCode would be undefined + } + } else { + // Handling the case of Random here + if (Math.floor(Math.random() * 100) < chaosHandlerOptions.chaosPercentage) { + chaosHandlerOptions.statusCode = this.getRandomStatusCode(requestMethod); + } + // else statusCode would be undefined + } + } catch (error) { + throw error; + } + } + + /** + * To get the options for execution of the middleware + * @private + * @param {Context} context - The context object + * @returns options for middleware execution + */ + private getOptions(context: Context): ChaosHandlerOptions { + let options: ChaosHandlerOptions; + if (context.middlewareControl instanceof MiddlewareControl) { + options = context.middlewareControl.getMiddlewareOptions(ChaosHandlerOptions) as ChaosHandlerOptions; + } + if (typeof options === "undefined") { + options = Object.assign(new ChaosHandlerOptions(), this.options); + } + + return options; + } + + /** + * To execute the current middleware + * @public + * @async + * @param {Context} context - The context object of the request + * @returns A Promise that resolves to nothing + */ + public async execute(context: Context): Promise { + try { + const chaosHandlerOptions: ChaosHandlerOptions = this.getOptions(context); + return await this.sendRequest(chaosHandlerOptions, context); + } catch (error) { + throw error; + } + } + + /** + * @public + * To set the next middleware in the chain + * @param {Middleware} next - The middleware instance + * @returns Nothing + */ + public setNext(next: Middleware): void { + this.nextMiddleware = next; + } +} diff --git a/src/middleware/MiddlewareFactory.ts b/src/middleware/MiddlewareFactory.ts new file mode 100644 index 000000000..619e55d63 --- /dev/null +++ b/src/middleware/MiddlewareFactory.ts @@ -0,0 +1,62 @@ +/** + * ------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. + * See License in the project root for license information. + * ------------------------------------------------------------------------------------------- + */ + +/** + * @module MiddlewareFactory + */ + +import { AuthenticationProvider } from "../IAuthenticationProvider"; + +import { AuthenticationHandler } from "./AuthenticationHandler"; +import { HTTPMessageHandler } from "./HTTPMessageHandler"; +import { Middleware } from "./IMiddleware"; +import { RedirectHandlerOptions } from "./options/RedirectHandlerOptions"; +import { RetryHandlerOptions } from "./options/RetryHandlerOptions"; +import { RedirectHandler } from "./RedirectHandler"; +import { RetryHandler } from "./RetryHandler"; +import { TelemetryHandler } from "./TelemetryHandler"; + +/** + * @private + * To check whether the environment is node or not + * @returns A boolean representing the environment is node or not + */ +const isNodeEnvironment = (): boolean => { + return typeof process === "object" && typeof require === "function"; +}; + +/** + * @class + * Class containing function(s) related to the middleware pipelines. + */ +export class MiddlewareFactory { + /** + * @public + * @static + * Returns the default middleware chain an array with the middleware handlers + * @param {AuthenticationProvider} authProvider - The authentication provider instance + * @returns an array of the middleware handlers of the default middleware chain + */ + public static getDefaultMiddlewareChain(authProvider: AuthenticationProvider): Middleware[] { + const middleware: Middleware[] = []; + const authenticationHandler = new AuthenticationHandler(authProvider); + const retryHandler = new RetryHandler(new RetryHandlerOptions()); + const telemetryHandler = new TelemetryHandler(); + const httpMessageHandler = new HTTPMessageHandler(); + + middleware.push(authenticationHandler); + middleware.push(retryHandler); + if (isNodeEnvironment()) { + const redirectHandler = new RedirectHandler(new RedirectHandlerOptions()); + middleware.push(redirectHandler); + } + middleware.push(telemetryHandler); + middleware.push(httpMessageHandler); + + return middleware; + } +} diff --git a/src/middleware/options/ChaosHandlerData.ts b/src/middleware/options/ChaosHandlerData.ts new file mode 100644 index 000000000..2b3a5924f --- /dev/null +++ b/src/middleware/options/ChaosHandlerData.ts @@ -0,0 +1,88 @@ +/** + * ------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. + * See License in the project root for license information. + * ------------------------------------------------------------------------------------------- + */ + +/** + * @module ChaosHandlerData + */ + +/** + * Contains RequestMethod to corresponding array of possible status codes, used for Random mode + */ +export const methodStatusCode: { [key: string]: number[] } = { + GET: [429, 500, 502, 503, 504], + POST: [429, 500, 502, 503, 504, 507], + PUT: [429, 500, 502, 503, 504, 507], + PATCH: [429, 500, 502, 503, 504], + DELETE: [429, 500, 502, 503, 504, 507], +}; + +/** + * Contains statusCode to statusMessage map + */ +export const httpStatusCode: { [key: number]: string } = { + 100: "Continue", + 101: "Switching Protocols", + 102: "Processing", + 103: "Early Hints", + 200: "OK", + 201: "Created", + 202: "Accepted", + 203: "Non-Authoritative Information", + 204: "No Content", + 205: "Reset Content", + 206: "Partial Content", + 207: "Multi-Status", + 208: "Already Reported", + 226: "IM Used", + 300: "Multiple Choices", + 301: "Moved Permanently", + 302: "Found", + 303: "See Other", + 304: "Not Modified", + 305: "Use Proxy", + 307: "Temporary Redirect", + 308: "Permanent Redirect", + 400: "Bad Request", + 401: "Unauthorized", + 402: "Payment Required", + 403: "Forbidden", + 404: "Not Found", + 405: "Method Not Allowed", + 406: "Not Acceptable", + 407: "Proxy Authentication Required", + 408: "Request Timeout", + 409: "Conflict", + 410: "Gone", + 411: "Length Required", + 412: "Precondition Failed", + 413: "Payload Too Large", + 414: "URI Too Long", + 415: "Unsupported Media Type", + 416: "Range Not Satisfiable", + 417: "Expectation Failed", + 421: "Misdirected Request", + 422: "Unprocessable Entity", + 423: "Locked", + 424: "Failed Dependency", + 425: "Too Early", + 426: "Upgrade Required", + 428: "Precondition Required", + 429: "Too Many Requests", + 431: "Request Header Fields Too Large", + 451: "Unavailable For Legal Reasons", + 500: "Internal Server Error", + 501: "Not Implemented", + 502: "Bad Gateway", + 503: "Service Unavailable", + 504: "Gateway Timeout", + 505: "HTTP Version Not Supported", + 506: "Variant Also Negotiates", + 507: "Insufficient Storage", + 508: "Loop Detected", + 510: "Not Extended", + 511: "Network Authentication Required", +}; diff --git a/src/middleware/options/ChaosHandlerOptions.ts b/src/middleware/options/ChaosHandlerOptions.ts new file mode 100644 index 000000000..836a1550d --- /dev/null +++ b/src/middleware/options/ChaosHandlerOptions.ts @@ -0,0 +1,79 @@ +/** + * ------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. + * See License in the project root for license information. + * ------------------------------------------------------------------------------------------- + */ + +/** + * @module ChaosHandlerOptions + */ + +import { ChaosStrategy } from "./ChaosStrategy"; +import { MiddlewareOptions } from "./IMiddlewareOptions"; + +/** + * Class representing ChaosHandlerOptions + * @class + * Class + * @implements MiddlewareOptions + */ +export class ChaosHandlerOptions implements MiddlewareOptions { + /** + * Specifies the startegy used for the Testing Handler -> RANDOM/MANUAL + * + * @public + */ + public chaosStrategy: ChaosStrategy; + + /** + * Status code to be returned in the response + * + * @public + */ + public statusCode: number; + + /** + * The Message to be returned in the response + * + * @public + */ + public statusMessage: string; + + /** + * The percentage of randomness/chaos in the handler + * + * Setting the default value as 10% + * @public + */ + public chaosPercentage: number; + + /** + * The response body to be returned in the response + * + * @public + */ + public responseBody: any; + + /** + * @public + * @constructor + * To create an instance of Testing Handler Options + * @param {ChaosStrategy} ChaosStrategy - Specifies the startegy used for the Testing Handler -> RAMDOM/MANUAL + * @param {string} statusMessage - The Message to be returned in the response + * @param {number?} statusCode - The statusCode to be returned in the response + * @param {number?} chaosPercentage - The percentage of randomness/chaos in the handler + * @param {any?} responseBody - The response body to be returned in the response + * @returns An instance of ChaosHandlerOptions + */ + public constructor(chaosStrategy: ChaosStrategy = ChaosStrategy.RANDOM, statusMessage: string = "Some error Happened", statusCode?: number, chaosPercentage?: number, responseBody?: any) { + this.chaosStrategy = chaosStrategy; + this.statusCode = statusCode; + this.statusMessage = statusMessage; + this.chaosPercentage = chaosPercentage !== undefined ? chaosPercentage : 10; + this.responseBody = responseBody; + if (this.chaosPercentage > 100) { + throw new Error("Error Pecentage can not be more than 100"); + } + } +} diff --git a/src/middleware/options/ChaosStrategy.ts b/src/middleware/options/ChaosStrategy.ts new file mode 100644 index 000000000..5450f86df --- /dev/null +++ b/src/middleware/options/ChaosStrategy.ts @@ -0,0 +1,19 @@ +/** + * ------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. + * See License in the project root for license information. + * ------------------------------------------------------------------------------------------- + */ + +/** + * @module ChaosStrategy + */ + +/** + * Strategy used for Testing Handler + * @enum + */ +export enum ChaosStrategy { + MANUAL, + RANDOM, +} diff --git a/src/tasks/OneDriveLargeFileUploadTask.ts b/src/tasks/OneDriveLargeFileUploadTask.ts index 2716f4a22..44d52027c 100644 --- a/src/tasks/OneDriveLargeFileUploadTask.ts +++ b/src/tasks/OneDriveLargeFileUploadTask.ts @@ -59,7 +59,12 @@ export class OneDriveLargeFileUploadTask extends LargeFileUploadTask { if (path[path.length - 1] !== "/") { path = `${path}/`; } - return encodeURI(`/me/drive/root:${path}${fileName}:/createUploadSession`); + // we choose to encode each component of the file path separately because when encoding full URI + // with encodeURI, special characters like # or % in the file name doesn't get encoded as desired + return `/me/drive/root:${path + .split("/") + .map((p) => encodeURIComponent(p)) + .join("/")}${encodeURIComponent(fileName)}:/createUploadSession`; } /** diff --git a/src/tasks/PageIterator.ts b/src/tasks/PageIterator.ts index 3fa90e4f8..b23cc68a1 100644 --- a/src/tasks/PageIterator.ts +++ b/src/tasks/PageIterator.ts @@ -9,7 +9,10 @@ * @module PageIterator */ +import { FetchOptions } from "../IFetchOptions"; import { Client } from "../index"; +import { MiddlewareOptions } from "../middleware/options/IMiddlewareOptions"; +import { ResponseType } from "../ResponseType"; /** * Signature representing PageCollection @@ -25,6 +28,19 @@ export interface PageCollection { [Key: string]: any; } +/** + * Signature to define the request options to be sent during request. + * The values of the GraphRequestOptions properties are passed to the Graph Request object. + * @property {HeadersInit} headers - the header options for the request + * @property {MiddlewareOptions[]} middlewareoptions - The middleware options for the request + * @property {FetchOptions} options - The fetch options for the request + */ +export interface GraphRequestOptions { + headers?: HeadersInit; + middlewareOptions?: MiddlewareOptions[]; + options?: FetchOptions; +} + /** * Signature representing callback for page iterator * @property {Function} callback - The callback function which should return boolean to continue the continue/stop the iteration. @@ -73,6 +89,11 @@ export class PageIterator { */ private complete: boolean; + /** + * Information to be added to the request + */ + private requestOptions: GraphRequestOptions; + /** * @public * @constructor @@ -80,15 +101,17 @@ export class PageIterator { * @param {Client} client - The graph client instance * @param {PageCollection} pageCollection - The page collection object * @param {PageIteratorCallback} callBack - The callback function + * @param {GraphRequestOptions} requestOptions - The request options * @returns An instance of a PageIterator */ - public constructor(client: Client, pageCollection: PageCollection, callback: PageIteratorCallback) { + public constructor(client: Client, pageCollection: PageCollection, callback: PageIteratorCallback, requestOptions?: GraphRequestOptions) { this.client = client; this.collection = pageCollection.value; this.nextLink = pageCollection["@odata.nextLink"]; this.deltaLink = pageCollection["@odata.deltaLink"]; this.callback = callback; this.complete = false; + this.requestOptions = requestOptions; } /** @@ -116,7 +139,20 @@ export class PageIterator { */ private async fetchAndUpdateNextPageData(): Promise { try { - const response: PageCollection = await this.client.api(this.nextLink).get(); + let graphRequest = this.client.api(this.nextLink); + if (this.requestOptions) { + if (this.requestOptions.headers) { + graphRequest = graphRequest.headers(this.requestOptions.headers); + } + if (this.requestOptions.middlewareOptions) { + graphRequest = graphRequest.middlewareOptions(this.requestOptions.middlewareOptions); + } + if (this.requestOptions.options) { + graphRequest = graphRequest.options(this.requestOptions.options); + } + } + + const response: PageCollection = await graphRequest.get(); this.collection = response.value; this.nextLink = response["@odata.nextLink"]; this.deltaLink = response["@odata.deltaLink"];