UNPKG

9.36 kBPlain TextView Raw
1/**
2 * -------------------------------------------------------------------------------------------
3 * Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
4 * See License in the project root for license information.
5 * -------------------------------------------------------------------------------------------
6 */
7
8/**
9 * @module ChaosHandler
10 */
11
12import { Context } from "../IContext";
13import { RequestMethod } from "../RequestMethod";
14import { Middleware } from "./IMiddleware";
15import { MiddlewareControl } from "./MiddlewareControl";
16import { generateUUID } from "./MiddlewareUtil";
17import { httpStatusCode, methodStatusCode } from "./options/ChaosHandlerData";
18import { ChaosHandlerOptions } from "./options/ChaosHandlerOptions";
19import { ChaosStrategy } from "./options/ChaosStrategy";
20
21/**
22 * Class representing ChaosHandler
23 * @class
24 * Class
25 * @implements Middleware
26 */
27export class ChaosHandler implements Middleware {
28 /**
29 * A member holding options to customize the handler behavior
30 *
31 * @private
32 */
33 private options: ChaosHandlerOptions;
34
35 /**
36 * container for the manual map that has been written by the client
37 *
38 * @private
39 */
40 private manualMap: Map<string, Map<string, number>>;
41
42 /**
43 * @private
44 * A member to hold next middleware in the middleware chain
45 */
46 private nextMiddleware: Middleware;
47
48 /**
49 * @public
50 * @constructor
51 * To create an instance of Testing Handler
52 * @param {ChaosHandlerOptions} [options = new ChaosHandlerOptions()] - The testing handler options instance
53 * @param manualMap - The Map passed by user containing url-statusCode info
54 * @returns An instance of Testing Handler
55 */
56 public constructor(options: ChaosHandlerOptions = new ChaosHandlerOptions(), manualMap?: Map<string, Map<string, number>>) {
57 this.options = options;
58 this.manualMap = manualMap;
59 }
60
61 /**
62 * Generates responseHeader
63 * @private
64 * @param {ChaosHandlerOptions} chaosHandlerOptions - The ChaosHandlerOptions object
65 * @param {string} requestID - request id
66 * @param {string} requestDate - date of the request
67 * @returns response Header
68 */
69 private createResponseHeaders(chaosHandlerOptions: ChaosHandlerOptions, requestID: string, requestDate: string) {
70 const responseHeader: Headers = chaosHandlerOptions.headers ? new Headers(chaosHandlerOptions.headers) : new Headers();
71 responseHeader.append("Cache-Control", "no-store");
72 responseHeader.append("request-id", requestID);
73 responseHeader.append("client-request-id", requestID);
74 responseHeader.append("x-ms-ags-diagnostic", "");
75 responseHeader.append("Date", requestDate);
76 responseHeader.append("Strict-Transport-Security", "");
77
78 if (chaosHandlerOptions.statusCode === 429) {
79 // throttling case has to have a timeout scenario
80 responseHeader.append("retry-after", "3");
81 }
82
83 return responseHeader;
84 }
85
86 /**
87 * Generates responseBody
88 * @private
89 * @param {ChaosHandlerOptions} chaosHandlerOptions - The ChaosHandlerOptions object
90 * @param {string} requestID - request id
91 * @param {string} requestDate - date of the request
92 * * @returns response body
93 */
94 private createResponseBody(chaosHandlerOptions: ChaosHandlerOptions, requestID: string, requestDate: string) {
95 if (chaosHandlerOptions.responseBody) {
96 return chaosHandlerOptions.responseBody;
97 }
98 let body: any;
99 if (chaosHandlerOptions.statusCode >= 400) {
100 const codeMessage: string = httpStatusCode[chaosHandlerOptions.statusCode];
101 const errMessage: string = chaosHandlerOptions.statusMessage;
102
103 body = {
104 error: {
105 code: codeMessage,
106 message: errMessage,
107 innerError: {
108 "request-id": requestID,
109 date: requestDate,
110 },
111 },
112 };
113 } else {
114 body = {};
115 }
116 return body;
117 }
118
119 /**
120 * creates a response
121 * @private
122 * @param {ChaosHandlerOptions} chaosHandlerOptions - The ChaosHandlerOptions object
123 * @param {Context} context - Contains the context of the request
124 */
125 private createResponse(chaosHandlerOptions: ChaosHandlerOptions, context: Context) {
126 const requestURL = context.request as string;
127 const requestID = generateUUID();
128 const requestDate = new Date();
129 const responseHeader = this.createResponseHeaders(chaosHandlerOptions, requestID, requestDate.toString());
130 const responseBody = this.createResponseBody(chaosHandlerOptions, requestID, requestDate.toString());
131 const init: any = { url: requestURL, status: chaosHandlerOptions.statusCode, statusText: chaosHandlerOptions.statusMessage, headers: responseHeader };
132 context.response = new Response(typeof responseBody === "string" ? responseBody : JSON.stringify(responseBody), init);
133 }
134
135 /**
136 * Decides whether to send the request to the graph or not
137 * @private
138 * @param {ChaosHandlerOptions} chaosHandlerOptions - A ChaosHandlerOptions object
139 * @param {Context} context - Contains the context of the request
140 * @returns nothing
141 */
142 private async sendRequest(chaosHandlerOptions: ChaosHandlerOptions, context: Context): Promise<void> {
143 this.setStatusCode(chaosHandlerOptions, context.request as string, context.options.method as RequestMethod);
144 if ((chaosHandlerOptions.chaosStrategy === ChaosStrategy.MANUAL && !this.nextMiddleware) || Math.floor(Math.random() * 100) < chaosHandlerOptions.chaosPercentage) {
145 this.createResponse(chaosHandlerOptions, context);
146 } else if (this.nextMiddleware) {
147 await this.nextMiddleware.execute(context);
148 }
149 }
150
151 /**
152 * Fetches a random status code for the RANDOM mode from the predefined array
153 * @private
154 * @param {string} requestMethod - the API method for the request
155 * @returns a random status code from a given set of status codes
156 */
157 private getRandomStatusCode(requestMethod: RequestMethod): number {
158 const statusCodeArray: number[] = methodStatusCode[requestMethod] as number[];
159 return statusCodeArray[Math.floor(Math.random() * statusCodeArray.length)];
160 }
161
162 /**
163 * To fetch the relative URL out of the complete URL using a predefined regex pattern
164 * @private
165 * @param {string} urlMethod - the complete URL
166 * @returns the string as relative URL
167 */
168 private getRelativeURL(urlMethod: string): string {
169 const pattern = /https?:\/\/graph\.microsoft\.com\/[^/]+(.+?)(\?|$)/;
170 let relativeURL: string;
171 if (pattern.exec(urlMethod) !== null) {
172 relativeURL = pattern.exec(urlMethod)[1];
173 }
174 return relativeURL;
175 }
176
177 /**
178 * To fetch the status code from the map(if needed), then returns response by calling createResponse
179 * @private
180 * @param {ChaosHandlerOptions} chaosHandlerOptions - The ChaosHandlerOptions object
181 * @param {string} requestURL - the URL for the request
182 * @param {string} requestMethod - the API method for the request
183 */
184 private setStatusCode(chaosHandlerOptions: ChaosHandlerOptions, requestURL: string, requestMethod: RequestMethod) {
185 if (chaosHandlerOptions.chaosStrategy === ChaosStrategy.MANUAL) {
186 if (chaosHandlerOptions.statusCode === undefined) {
187 // manual mode with no status code, can be a global level or request level without statusCode
188 const relativeURL: string = this.getRelativeURL(requestURL);
189 if (this.manualMap.get(relativeURL) !== undefined) {
190 // checking Manual Map for exact match
191 if (this.manualMap.get(relativeURL).get(requestMethod) !== undefined) {
192 chaosHandlerOptions.statusCode = this.manualMap.get(relativeURL).get(requestMethod);
193 }
194 // else statusCode would be undefined
195 } else {
196 // checking for regex match if exact match doesn't work
197 this.manualMap.forEach((value: Map<string, number>, key: string) => {
198 const regexURL = new RegExp(key + "$");
199 if (regexURL.test(relativeURL)) {
200 if (this.manualMap.get(key).get(requestMethod) !== undefined) {
201 chaosHandlerOptions.statusCode = this.manualMap.get(key).get(requestMethod);
202 }
203 // else statusCode would be undefined
204 }
205 });
206 }
207
208 // Case of redirection or request url not in map ---> statusCode would be undefined
209 }
210 } else {
211 // Handling the case of Random here
212 chaosHandlerOptions.statusCode = this.getRandomStatusCode(requestMethod);
213 // else statusCode would be undefined
214 }
215 }
216
217 /**
218 * To get the options for execution of the middleware
219 * @private
220 * @param {Context} context - The context object
221 * @returns options for middleware execution
222 */
223 private getOptions(context: Context): ChaosHandlerOptions {
224 let options: ChaosHandlerOptions;
225 if (context.middlewareControl instanceof MiddlewareControl) {
226 options = context.middlewareControl.getMiddlewareOptions(ChaosHandlerOptions) as ChaosHandlerOptions;
227 }
228 if (typeof options === "undefined") {
229 options = Object.assign(new ChaosHandlerOptions(), this.options);
230 }
231
232 return options;
233 }
234
235 /**
236 * To execute the current middleware
237 * @public
238 * @async
239 * @param {Context} context - The context object of the request
240 * @returns A Promise that resolves to nothing
241 */
242 public async execute(context: Context): Promise<void> {
243 const chaosHandlerOptions: ChaosHandlerOptions = this.getOptions(context);
244 return await this.sendRequest(chaosHandlerOptions, context);
245 }
246
247 /**
248 * @public
249 * To set the next middleware in the chain
250 * @param {Middleware} next - The middleware instance
251 * @returns Nothing
252 */
253 public setNext(next: Middleware): void {
254 this.nextMiddleware = next;
255 }
256}