diff --git a/lib/azureServiceClient.ts b/lib/azureServiceClient.ts index 4b91323..fc6d68c 100644 --- a/lib/azureServiceClient.ts +++ b/lib/azureServiceClient.ts @@ -1,14 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. -import * as msRest from "ms-rest-js"; -import PollingState from "./pollingState"; -import Constants, { LongRunningOperationStates as LroStates } from "./util/constants"; +import { HttpOperationResponse, RequestOptionsBase, RequestPrepareOptions, ServiceClient, ServiceClientCredentials, ServiceClientOptions, WebResource } from "ms-rest-js"; +import { createLROPollStrategy, LROPollStrategy } from "./lroPollStrategy"; +import * as Constants from "./util/constants"; /** * Options to be provided while creating the client. */ -export interface AzureServiceClientOptions extends msRest.ServiceClientOptions { +export interface AzureServiceClientOptions extends ServiceClientOptions { /** * @property {string} [options.acceptLanguage] - Gets or sets the preferred language for the response. Default value is: "en-US". */ @@ -30,11 +30,14 @@ export interface AzureServiceClientOptions extends msRest.ServiceClientOptions { * UserTokenCredentials object used for authentication. * @param {AzureServiceClientOptions} options - The parameter options used by AzureServiceClient */ -export class AzureServiceClient extends msRest.ServiceClient { - acceptLanguage: string = Constants.DEFAULT_LANGUAGE; - longRunningOperationRetryTimeout = 30; +export class AzureServiceClient extends ServiceClient { + public acceptLanguage: string = Constants.DEFAULT_LANGUAGE; + /** + * The retry timeout in seconds for Long Running Operations. Default value is 30. + */ + public longRunningOperationRetryTimeout?: number; - constructor(credentials: msRest.ServiceClientCredentials, options?: AzureServiceClientOptions) { + constructor(credentials: ServiceClientCredentials, options?: AzureServiceClientOptions) { super(credentials, options = updateOptionsWithDefaultValues(options)); if (options.acceptLanguage != undefined) { @@ -51,57 +54,30 @@ export class AzureServiceClient extends msRest.ServiceClient { /** * Provides a mechanism to make a request that will poll and provide the final result. * @param {msRest.RequestPrepareOptions|msRest.WebResource} request - The request object - * @param {msRest.RequestOptionsBase} [options] Additional options to be sent while making the request + * @param {AzureRequestOptionsBase} [options] Additional options to be sent while making the request * @returns {Promise} The HttpOperationResponse containing the final polling request, response and the responseBody. */ - sendLongRunningRequest(request: msRest.RequestPrepareOptions | msRest.WebResource, options?: msRest.RequestOptionsBase): Promise { - return this.sendRequest(request).then(response => this.getLongRunningOperationResult(response, options)); + async sendLongRunningRequest(request: RequestPrepareOptions | WebResource, options?: RequestOptionsBase): Promise { + return this.sendRequest(request).then((response: HttpOperationResponse) => this.getLongRunningOperationResult(response, options)); } /** * Poll Azure long running PUT, PATCH, POST or DELETE operations. - * @param {msRest.HttpOperationResponse} resultOfInitialRequest - result/response of the initial request which is a part of the asynchronous polling operation. - * @param {msRest.RequestOptionsBase} [options] - custom request options. - * @returns {Promise} result - The final response after polling is complete. + * @param {HttpOperationResponse} initialResponse - response of the initial request which is a part of the asynchronous polling operation. + * @param {AzureRequestOptionsBase} [options] - custom request options. + * @returns {Promise} The final response after polling is complete. */ - async getLongRunningOperationResult(resultOfInitialRequest: msRest.HttpOperationResponse, options?: msRest.RequestOptionsBase): Promise { - const initialRequestMethod: msRest.HttpMethods = resultOfInitialRequest.request.method; - - if (checkResponseStatusCodeFailed(resultOfInitialRequest)) { - throw new Error(`Unexpected polling status code from long running operation ` + - `"${resultOfInitialRequest.status}" for method "${initialRequestMethod}".`); - } - const pollingState = new PollingState(resultOfInitialRequest, this.longRunningOperationRetryTimeout); - pollingState.optionsOfInitialRequest = options as msRest.RequestOptionsBase; - - const resourceUrl: string = resultOfInitialRequest.request.url; - while (terminalStates.indexOf(pollingState.status) === -1) { - await msRest.delay(pollingState.getTimeout()); - if (pollingState.azureAsyncOperationHeaderLink) { - await updateStateFromAzureAsyncOperationHeader(this, pollingState); - } else if (pollingState.locationHeaderLink) { - await updateStateFromLocationHeader(this, initialRequestMethod, pollingState); - } else if (initialRequestMethod === "PUT") { - await updateStateFromGetResourceOperation(this, resourceUrl, pollingState); - } else { - throw new Error("Location header is missing from long running operation."); - } - } - - if (pollingState.status === "Succeeded") { - if ((pollingState.azureAsyncOperationHeaderLink || !pollingState.resource) && - (initialRequestMethod === "PUT" || initialRequestMethod === "PATCH")) { - await updateStateFromGetResourceOperation(this, resourceUrl, pollingState); - } - return pollingState.getOperationResponse(); + async getLongRunningOperationResult(initialResponse: HttpOperationResponse, options?: RequestOptionsBase): Promise { + const lroPollStrategy: LROPollStrategy = createLROPollStrategy(initialResponse, this, options); + const succeeded: boolean = await lroPollStrategy.pollUntilFinished(); + if (succeeded) { + return lroPollStrategy.getOperationResponse(); } else { - throw pollingState.getRestError(); + throw lroPollStrategy.getRestError(); } } } -const terminalStates: LroStates[] = ["Succeeded", "Failed", "Canceled"]; - export function updateOptionsWithDefaultValues(options?: AzureServiceClientOptions): AzureServiceClientOptions { if (!options) { options = {}; @@ -110,133 +86,4 @@ export function updateOptionsWithDefaultValues(options?: AzureServiceClientOptio options.generateClientRequestIdHeader = true; } return options; -} - -/** - * Verified whether an unexpected polling status code for long running operation was received for the response of the initial request. - * @param {msRest.HttpOperationResponse} initialResponse - Response to the initial request that was sent as a part of the asynchronous operation. - */ -export function checkResponseStatusCodeFailed(initialResponse: msRest.HttpOperationResponse): boolean { - const statusCode = initialResponse.status; - const method = initialResponse.request.method; - if (statusCode === 200 || statusCode === 202 || - (statusCode === 201 && method === "PUT") || - (statusCode === 204 && (method === "DELETE" || method === "POST"))) { - return false; - } else { - return true; - } -} - -/** - * Retrieve operation status by polling from "azure-asyncoperation" header. - * @param {PollingState} pollingState - The object to persist current operation state. - * @param {boolean} inPostOrDelete - Invoked by Post Or Delete operation. - */ -export function updateStateFromAzureAsyncOperationHeader(client: AzureServiceClient, pollingState: PollingState): Promise { - return getStatus(client, pollingState.azureAsyncOperationHeaderLink as string, pollingState.optionsOfInitialRequest).then(result => { - const parsedResponse = result.parsedBody as { [key: string]: any }; - if (!parsedResponse) { - throw new Error("The response from long running operation does not contain a body."); - } else if (!parsedResponse.status) { - throw new Error(`The response "${result.bodyAsText}" from long running operation does not contain the status property.`); - } - pollingState.status = parsedResponse.status; - pollingState.error = parsedResponse.error; - pollingState.updateResponse(result); - pollingState.request = result.request; - pollingState.resource = undefined; - pollingState.resource = result.parsedBody; - }); -} - -/** - * Retrieve PUT operation status by polling from "location" header. - * @param {string} method - The HTTP method. - * @param {PollingState} pollingState - The object to persist current operation state. - */ -export function updateStateFromLocationHeader(client: AzureServiceClient, method: msRest.HttpMethods, pollingState: PollingState): Promise { - return getStatus(client, pollingState.locationHeaderLink as string, pollingState.optionsOfInitialRequest).then(result => { - const parsedResponse = result.parsedBody as { [key: string]: any }; - - pollingState.updateResponse(result); - pollingState.request = result.request; - const statusCode = result.status; - if (statusCode === 202) { - pollingState.status = "InProgress"; - } else if (statusCode === 200 || - (statusCode === 201 && (method === "PUT" || method === "PATCH")) || - (statusCode === 204 && (method === "DELETE" || method === "POST"))) { - pollingState.status = "Succeeded"; - pollingState.resource = parsedResponse; - // we might not throw an error, but initialize here just in case. - pollingState.error = new msRest.RestError(`Long running operation failed with status "${pollingState.status}".`); - pollingState.error.code = pollingState.status; - } else { - throw new Error(`The response with status code ${statusCode} from polling for ` + - `long running operation url "${pollingState.locationHeaderLink}" is not valid.`); - } - }); -} - -/** - * Polling for resource status. - * @param {string} resourceUrl - The url of resource. - * @param {PollingState} pollingState - The object to persist current operation state. - */ -export function updateStateFromGetResourceOperation(client: AzureServiceClient, resourceUrl: string, pollingState: PollingState): Promise { - return getStatus(client, resourceUrl, pollingState.optionsOfInitialRequest).then(result => { - if (!result.parsedBody) { - throw new Error("The response from long running operation does not contain a body."); - } - - const parsedResponse = result.parsedBody as { [key: string]: any }; - pollingState.status = "Succeeded"; - if (parsedResponse && parsedResponse.properties && parsedResponse.properties.provisioningState) { - pollingState.status = parsedResponse.properties.provisioningState; - } - pollingState.updateResponse(result); - pollingState.request = result.request; - pollingState.resource = parsedResponse; - // we might not throw an error, but initialize here just in case. - pollingState.error = new msRest.RestError(`Long running operation failed with status "${pollingState.status}".`); - pollingState.error.code = pollingState.status; - }); -} - -/** - * Retrieves operation status by querying the operation URL. - * @param {string} operationUrl - URL used to poll operation result. - * @param {object} options - Options that can be set on the request object - */ -export function getStatus(client: AzureServiceClient, operationUrl: string, options?: msRest.RequestOptionsBase): Promise { - // Construct URL - const requestUrl = operationUrl.replace(" ", "%20"); - // Create HTTP request object - const httpRequest: msRest.RequestPrepareOptions = { - method: "GET", - url: requestUrl, - headers: {} - }; - if (options && options.customHeaders) { - const customHeaders = options.customHeaders; - for (const headerName of Object.keys(customHeaders)) { - httpRequest.headers![headerName] = customHeaders[headerName]; - } - } - return client.sendRequest(httpRequest).then(operationResponse => { - const statusCode = operationResponse.status; - const responseBody = operationResponse.parsedBody; - if (statusCode !== 200 && statusCode !== 201 && statusCode !== 202 && statusCode !== 204) { - const error = new msRest.RestError(`Invalid status code with response body "${operationResponse.bodyAsText}" occurred ` + - `when polling for operation status.`); - error.statusCode = statusCode; - error.request = msRest.stripRequest(operationResponse.request); - error.response = operationResponse; - error.body = responseBody; - throw error; - } - - return operationResponse; - }); } \ No newline at end of file diff --git a/lib/lroPollStrategy.ts b/lib/lroPollStrategy.ts new file mode 100644 index 0000000..4ebb909 --- /dev/null +++ b/lib/lroPollStrategy.ts @@ -0,0 +1,354 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +import { delay, HttpMethods, HttpOperationResponse, RequestOptionsBase, RequestPrepareOptions, RestError, stripRequest, WebResource } from "ms-rest-js"; +import { AzureServiceClient } from "./azureServiceClient"; +import { LongRunningOperationStates } from "./util/constants"; + +/** + * A long-running operation polling strategy base class that other polling strategies should extend. + */ +export abstract class LROPollStrategy { + private readonly _initialRequestUrl: string; + protected readonly _initialRequestMethod: HttpMethods; + protected _status: LongRunningOperationStates; + protected _mostRecentRequest: WebResource; + protected _response: HttpOperationResponse; + protected _resource: any; + + constructor(initialResponse: HttpOperationResponse, private readonly _azureServiceClient: AzureServiceClient, private readonly _options?: RequestOptionsBase) { + this._response = initialResponse; + + this._mostRecentRequest = initialResponse.request; + this._initialRequestUrl = this._mostRecentRequest.url; + this._initialRequestMethod = this._mostRecentRequest.method; + + this._resource = getResponseBody(initialResponse); + this._status = getStatusFromResponse(initialResponse, this._resource); + this._resource = getResponseBody(initialResponse); + } + + public async pollUntilFinished(): Promise { + while (!isFinished(this._status)) { + const delayInSeconds: number = getDelayInSeconds(this._azureServiceClient, this._response); + await delay(delayInSeconds * 1000); + + await this.sendPollRequest(); + } + return this._status === "Succeeded"; + } + + protected abstract sendPollRequest(): Promise; + + protected shouldDoFinalGetResourceRequest(): boolean { + return !this._resource && (this._initialRequestMethod === "PUT" || this._initialRequestMethod === "PATCH"); + } + + public async getOperationResponse(): Promise { + if (this.shouldDoFinalGetResourceRequest()) { + await this.updateStateFromGetResourceOperation(); + } + const result: HttpOperationResponse = { + ...this._response, + headers: this._response.headers.clone() + }; + if (this._resource && typeof this._resource.valueOf() === "string") { + result.bodyAsText = this._resource; + result.parsedBody = JSON.parse(this._resource); + } else { + result.bodyAsText = JSON.stringify(this._resource); + result.parsedBody = this._resource; + } + return result; + } + + public getRestError(): RestError { + const error = new RestError(""); + error.request = stripRequest(this._mostRecentRequest); + error.response = this._response; + error.message = `Long running operation failed with status: "${this._status}".`; + error.body = this._resource; + if (error.body) { + const innerError: any = error.body.error; + if (innerError) { + if (innerError.message) { + error.message = `Long running operation failed with error: "${innerError.message}".`; + } + if (innerError.code) { + error.code = innerError.code; + } + } + } + return error; + } + + protected updateStateFromGetResourceOperation(): Promise { + return this.getStatus(this._initialRequestUrl).then(result => { + if (!result.parsedBody) { + throw new Error("The response from long running operation does not contain a body."); + } + + this._status = getProvisioningState(result.parsedBody) || "Succeeded"; + this._response = result; + this._mostRecentRequest = result.request; + this._resource = getResponseBody(result); + }); + } + + /** + * Retrieves operation status by querying the operation URL. + * @param {string} statusUrl URL used to poll operation result. + */ + protected getStatus(statusUrl: string): Promise { + const requestUrl: string = statusUrl.replace(" ", "%20"); + const httpRequest: RequestPrepareOptions = { + method: "GET", + url: requestUrl, + headers: {} + }; + if (this._options && this._options.customHeaders) { + const customHeaders = this._options.customHeaders; + for (const headerName of Object.keys(customHeaders)) { + httpRequest.headers![headerName] = customHeaders[headerName]; + } + } + return this._azureServiceClient.sendRequest(httpRequest).then(operationResponse => { + const statusCode: number = operationResponse.status; + const responseBody: any = operationResponse.parsedBody; + if (statusCode !== 200 && statusCode !== 201 && statusCode !== 202 && statusCode !== 204) { + const error = new RestError(`Invalid status code with response body "${operationResponse.bodyAsText}" occurred when polling for operation status.`); + error.statusCode = statusCode; + error.request = stripRequest(operationResponse.request); + error.response = operationResponse; + error.body = responseBody; + throw error; + } + + return operationResponse; + }); + } +} + +export function getDelayInSeconds(azureServiceClient: AzureServiceClient, previousResponse: HttpOperationResponse): number { + let delayInSeconds = 30; + if (azureServiceClient.longRunningOperationRetryTimeout != undefined) { + delayInSeconds = azureServiceClient.longRunningOperationRetryTimeout; + } else { + const retryAfterHeaderValue: string | undefined = previousResponse.headers.get("retry-after"); + if (retryAfterHeaderValue) { + const retryAfterDelayInSeconds: number = parseInt(retryAfterHeaderValue); + if (!Number.isNaN(retryAfterDelayInSeconds)) { + delayInSeconds = retryAfterDelayInSeconds; + } + } + } + return delayInSeconds; +} + +function getProvisioningState(responseBody: any): LongRunningOperationStates | undefined { + return responseBody && responseBody.properties && responseBody.properties.provisioningState; +} + +function getResponseBody(response: HttpOperationResponse): any { + let result: any; + try { + if (response.bodyAsText && response.bodyAsText.length > 0) { + result = JSON.parse(response.bodyAsText); + } else { + result = response.parsedBody; + } + } catch (error) { + const deserializationError = new RestError(`Error "${error}" occurred in parsing the responseBody " + + "while creating the PollingState for Long Running Operation- "${response.bodyAsText}"`); + deserializationError.request = response.request; + deserializationError.response = response; + throw deserializationError; + } + return result; +} + +function getStatusFromResponse(response: HttpOperationResponse, responseBody?: any): LongRunningOperationStates { + if (responseBody == undefined) { + responseBody = getResponseBody(response); + } + + let result: LongRunningOperationStates; + switch (response.status) { + case 202: + result = "InProgress"; + break; + + case 204: + result = "Succeeded"; + break; + + case 201: + result = getProvisioningState(responseBody) || "InProgress"; + break; + + case 200: + result = getProvisioningState(responseBody) || "Succeeded"; + break; + + default: + result = "Failed"; + break; + } + return result; +} + +const terminalStates: LongRunningOperationStates[] = ["Succeeded", "Failed", "Canceled"]; + +/** + * Get whether or not a long-running operation with the provided status is finished. + * @param status The current status of a long-running operation. + * @returns Whether or not a long-running operation with the provided status is finished. + */ +function isFinished(status: LongRunningOperationStates): boolean { + return terminalStates.indexOf(status) !== -1; +} + +/** + * Create a new long-running operation polling strategy based on the provided initial response. + * @param initialResponse The initial response to the long-running operation's initial request. + * @param azureServiceClient The AzureServiceClient that was used to send the initial request. + * @param options Any options that were provided to the initial request. + */ +export function createLROPollStrategy(initialResponse: HttpOperationResponse, azureServiceClient: AzureServiceClient, options?: RequestOptionsBase): LROPollStrategy { + if (checkResponseStatusCodeFailed(initialResponse)) { + throw new Error(`Unexpected polling status code from long running operation ` + + `"${initialResponse.status}" for method "${initialResponse.request.method}".`); + } + + let result: LROPollStrategy; + + if (getAzureAsyncOperationHeaderValue(initialResponse)) { + result = new AzureAsyncOperationLROPollStrategy(initialResponse, azureServiceClient, options); + } else if (getLocationHeaderValue(initialResponse)) { + result = new LocationLROPollStrategy(initialResponse, azureServiceClient, options); + } else if (initialResponse.request.method === "PUT" || isFinished(getStatusFromResponse(initialResponse))) { + result = new GetResourceLROPollStrategy(initialResponse, azureServiceClient, options); + } else { + throw new Error("Can't determine long running operation polling strategy from initial response."); + } + + return result; +} + +/** + * Verified whether an unexpected polling status code for long running operation was received for the response of the initial request. + * @param {msRest.HttpOperationResponse} initialResponse - Response to the initial request that was sent as a part of the asynchronous operation. + */ +function checkResponseStatusCodeFailed(initialResponse: HttpOperationResponse): boolean { + const statusCode = initialResponse.status; + const method: HttpMethods = initialResponse.request.method; + if (statusCode === 200 || statusCode === 202 || + (statusCode === 201 && method === "PUT") || + (statusCode === 204 && (method === "DELETE" || method === "POST"))) { + return false; + } else { + return true; + } +} + +function getLocationHeaderValue(response: HttpOperationResponse): string | undefined { + return response.headers.get("location"); +} + +/** + * A long-running operation polling strategy that is based on the location header. + */ +class LocationLROPollStrategy extends LROPollStrategy { + private _locationHeaderValue?: string; + + constructor(initialResponse: HttpOperationResponse, azureServiceClient: AzureServiceClient, options?: RequestOptionsBase) { + super(initialResponse, azureServiceClient, options); + + this._locationHeaderValue = getLocationHeaderValue(initialResponse)!; + } + + /** + * Retrieve PUT operation status by polling from "location" header. + * @param {string} method - The HTTP method. + * @param {PollingState} pollingState - The object to persist current operation state. + */ + protected sendPollRequest(): Promise { + return this.getStatus(this._locationHeaderValue!).then((result: HttpOperationResponse) => { + const locationHeaderValue: string | undefined = getLocationHeaderValue(result); + if (locationHeaderValue) { + this._locationHeaderValue = locationHeaderValue; + } + + this._response = result; + this._mostRecentRequest = result.request; + + const statusCode: number = result.status; + if (statusCode === 202) { + this._status = "InProgress"; + } else if (statusCode === 200 || + (statusCode === 201 && (this._initialRequestMethod === "PUT" || this._initialRequestMethod === "PATCH")) || + (statusCode === 204 && (this._initialRequestMethod === "DELETE" || this._initialRequestMethod === "POST"))) { + this._status = "Succeeded"; + this._resource = getResponseBody(result); + } else { + throw new Error(`The response with status code ${statusCode} from polling for long running operation url "${this._locationHeaderValue}" is not valid.`); + } + }); + } +} + +function getAzureAsyncOperationHeaderValue(response: HttpOperationResponse): string | undefined { + return response.headers.get("azure-asyncoperation"); +} + +/** + * A long-running operation polling strategy that is based on the azure-asyncoperation header. + */ +class AzureAsyncOperationLROPollStrategy extends LROPollStrategy { + private _azureAsyncOperationHeaderValue: string; + + public constructor(initialResponse: HttpOperationResponse, azureServiceClient: AzureServiceClient, options?: RequestOptionsBase) { + super(initialResponse, azureServiceClient, options); + + this._azureAsyncOperationHeaderValue = getAzureAsyncOperationHeaderValue(initialResponse)!; + } + + /** + * Retrieve operation status by polling from "azure-asyncoperation" header. + * @param {PollingState} pollingState - The object to persist current operation state. + * @param {boolean} inPostOrDelete - Invoked by Post Or Delete operation. + */ + protected sendPollRequest(): Promise { + return this.getStatus(this._azureAsyncOperationHeaderValue!).then((response: HttpOperationResponse) => { + const parsedResponse: any = response.parsedBody; + if (!parsedResponse) { + throw new Error("The response from long running operation does not contain a body."); + } else if (!parsedResponse.status) { + throw new Error(`The response "${response.bodyAsText}" from long running operation does not contain the status property.`); + } + + const azureAsyncOperationHeaderValue: string | undefined = getAzureAsyncOperationHeaderValue(response); + if (azureAsyncOperationHeaderValue) { + this._azureAsyncOperationHeaderValue = azureAsyncOperationHeaderValue; + } + + this._status = parsedResponse.status; + this._response = response; + this._mostRecentRequest = response.request; + this._resource = getResponseBody(response); + }); + } + + protected shouldDoFinalGetResourceRequest(): boolean { + return this._initialRequestMethod === "PUT" || this._initialRequestMethod === "PATCH"; + } +} + +/** + * A long-running operation polling strategy that is based on the resource's provisioning state. + */ +class GetResourceLROPollStrategy extends LROPollStrategy { + protected sendPollRequest(): Promise { + return this.updateStateFromGetResourceOperation(); + } +} \ No newline at end of file diff --git a/lib/msRestAzure.ts b/lib/msRestAzure.ts index 4d779c3..771643c 100644 --- a/lib/msRestAzure.ts +++ b/lib/msRestAzure.ts @@ -1,9 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. -import { AzureServiceClientOptions, AzureServiceClient } from "./azureServiceClient"; -import Constants from "./util/constants"; -import { CloudError, CloudErrorMapper } from "./cloudError"; -import { BaseResource, BaseResourceMapper } from "./baseResource"; -import { CognitiveServicesCredentials } from "./credentials/cognitiveServicesCredentials"; -export { AzureServiceClient, AzureServiceClientOptions, Constants, CloudError, CloudErrorMapper, BaseResource, BaseResourceMapper, CognitiveServicesCredentials }; +export { AzureServiceClient, AzureServiceClientOptions } from "./azureServiceClient"; +export { BaseResource, BaseResourceMapper } from "./baseResource"; +export { CloudError, CloudErrorMapper } from "./cloudError"; +export { CognitiveServicesCredentials } from "./credentials/cognitiveServicesCredentials"; +export { DEFAULT_LANGUAGE, LongRunningOperationStates, msRestAzureVersion } from "./util/constants"; diff --git a/lib/pollingState.ts b/lib/pollingState.ts deleted file mode 100644 index 1a9f82b..0000000 --- a/lib/pollingState.ts +++ /dev/null @@ -1,194 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. - -import { LongRunningOperationStates as LroStates } from "./util/constants"; -import * as msRest from "ms-rest-js"; - -/** - * @class - * Initializes a new instance of the PollingState class. - */ -export default class PollingState { - /** - * @param {msRest.HttpOperationResponse} [response] - Response of the initial request that was made as a part of the asynchronous operation. - */ - resultOfInitialRequest: msRest.HttpOperationResponse; - /** - * @param {msRest.RequestOptionsBase} [optionsOfInitialRequest] - Request options that were provided as a part of the initial request. - */ - optionsOfInitialRequest!: msRest.RequestOptionsBase; - /** - * @param {msRest.WebResource} [request] - provides information about the request made for polling. - */ - request: msRest.WebResource; - /** - * @param {Response} [response] - The response object to extract longrunning operation status. - */ - response!: msRest.HttpOperationResponse; - /** - * @param {any} [resource] - Provides information about the response body received in the polling request. Particularly useful when polling via provisioningState. - */ - resource: any; - /** - * @param {number} [retryTimeout] - The timeout in seconds to retry on intermediate operation results. Default Value is 30. - */ - retryTimeout = 30; - /** - * @param {string} [azureAsyncOperationHeaderLink] - The url that is present in "azure-asyncoperation" response header. - */ - azureAsyncOperationHeaderLink?: string; - /** - * @param {string} [locationHeaderLink] - The url that is present in "Location" response header. - */ - locationHeaderLink?: string; - /** - * @param {string} [status] - The status of polling. "Succeeded, Failed, Cancelled, Updating, Creating, etc." - */ - status: LroStates; - /** - * @param {msRest.RestError} [error] - Provides information about the error that happened while polling. - */ - error?: msRest.RestError; - - constructor(resultOfInitialRequest: msRest.HttpOperationResponse, retryTimeout: number) { - this.resultOfInitialRequest = resultOfInitialRequest; - this.retryTimeout = retryTimeout; - this.updateResponse(resultOfInitialRequest); - this.request = resultOfInitialRequest.request; - // Parse response.body & assign it as the resource. - try { - if (resultOfInitialRequest.bodyAsText && resultOfInitialRequest.bodyAsText.length > 0) { - this.resource = JSON.parse(resultOfInitialRequest.bodyAsText); - } else { - this.resource = resultOfInitialRequest.parsedBody; - } - } catch (error) { - const deserializationError = new msRest.RestError(`Error "${error}" occurred in parsing the responseBody " + - "while creating the PollingState for Long Running Operation- "${resultOfInitialRequest.bodyAsText}"`); - deserializationError.request = resultOfInitialRequest.request; - deserializationError.response = resultOfInitialRequest; - throw deserializationError; - } - const resource = this.resource; - let status: LroStates; - switch (this.response.status) { - case 202: - status = "InProgress"; - break; - - case 204: - status = "Succeeded"; - break; - - case 201: - if (resource && resource.properties && resource.properties.provisioningState) { - status = resource.properties.provisioningState; - } else { - status = "InProgress"; - } - break; - - case 200: - if (resource && resource.properties && resource.properties.provisioningState) { - status = resource.properties.provisioningState; - } else { - status = "Succeeded"; - } - break; - - default: - status = "Failed"; - break; - } - this.status = status; - } - - /** - * Update cached data using the provided response object - * @param {Response} [response] - provider response object. - */ - updateResponse(response: msRest.HttpOperationResponse) { - this.response = response; - if (response && response.headers) { - const asyncOperationHeader: string | undefined = response.headers.get("azure-asyncoperation"); - const locationHeader: string | undefined = response.headers.get("location"); - if (asyncOperationHeader) { - this.azureAsyncOperationHeaderLink = asyncOperationHeader; - } - - if (locationHeader) { - this.locationHeaderLink = locationHeader; - } - } - } - - /** - * Gets timeout in milliseconds. - * @returns {number} timeout - */ - getTimeout() { - const retryTimeout = this.retryTimeout; - if (retryTimeout || retryTimeout === 0) { - return retryTimeout * 1000; - } - if (this.response) { - const retryAfter: string | undefined = this.response.headers.get("retry-after"); - if (retryAfter) { - return parseInt(retryAfter) * 1000; - } - } - return 30 * 1000; - } - - /** - * Returns long running operation result. - * @returns {msRest.HttpOperationResponse} HttpOperationResponse - */ - getOperationResponse(): msRest.HttpOperationResponse { - const result = { ...this.response, headers: this.response.headers.clone() }; - const resource = this.resource; - if (resource && typeof resource.valueOf() === "string") { - result.bodyAsText = resource; - result.parsedBody = JSON.parse(resource); - } else { - result.parsedBody = resource; - result.bodyAsText = JSON.stringify(resource); - } - return result; - } - - /** - * Returns an Error on operation failure. - * @param {Error} err - The error object. - * @returns {msRest.RestError} The RestError defined in the runtime. - */ - getRestError(err?: Error): msRest.RestError { - let errMsg: string; - let errCode: string | undefined = undefined; - - const error = new msRest.RestError(""); - error.request = msRest.stripRequest(this.request); - error.response = this.response; - const parsedResponse = this.resource as { [key: string]: any }; - - if (err && err.message) { - errMsg = `Long running operation failed with error: "${err.message}".`; - } else { - errMsg = `Long running operation failed with status: "${this.status}".`; - } - - if (parsedResponse) { - if (parsedResponse.error && parsedResponse.error.message) { - errMsg = `Long running operation failed with error: "${parsedResponse.error.message}".`; - } - if (parsedResponse.error && parsedResponse.error.code) { - errCode = parsedResponse.error.code as string; - } - } - - error.message = errMsg; - if (errCode) error.code = errCode; - error.body = parsedResponse; - return error; - } -} \ No newline at end of file diff --git a/lib/util/constants.ts b/lib/util/constants.ts index 598201e..dfb1d06 100644 --- a/lib/util/constants.ts +++ b/lib/util/constants.ts @@ -10,22 +10,17 @@ */ export type LongRunningOperationStates = "InProgress" | "Succeeded" | "Failed" | "Canceled"; -const Constants = { +/** + * The default language in the request header. + * + * @const + * @type {string} + */ +export const DEFAULT_LANGUAGE = "en-us"; - /** - * The default language in the request header. - * - * @const - * @type {string} - */ - DEFAULT_LANGUAGE: "en-us", - - /** - * The ms-rest-azure version. - * @const - * @type {string} - */ - msRestAzureVersion: "0.1.0" -}; - -export default Constants; +/** + * The ms-rest-azure version. + * @const + * @type {string} + */ +export const msRestAzureVersion = "0.1.0"; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 99f0d50..082f672 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "ms-rest-azure-js", - "version": "0.11.101", + "version": "0.11.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2938,9 +2938,9 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "ms-rest-js": { - "version": "0.16.371", - "resolved": "https://registry.npmjs.org/ms-rest-js/-/ms-rest-js-0.16.371.tgz", - "integrity": "sha1-iQHQQc8HynVA0YzfmmwCe2Z7ImQ=", + "version": "0.16.372", + "resolved": "https://registry.npmjs.org/ms-rest-js/-/ms-rest-js-0.16.372.tgz", + "integrity": "sha1-8xchzTHDXqv9piaAG/fJRSqJfJE=", "requires": { "@types/express": "^4.11.1", "@types/form-data": "^2.2.1", @@ -6306,9 +6306,9 @@ } }, "tsutils": { - "version": "2.28.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.28.0.tgz", - "integrity": "sha512-bh5nAtW0tuhvOJnx1GLRn5ScraRLICGyJV5wJhtRWOLsxW70Kk5tZtpK3O/hW6LDnqKS9mlUMPZj9fEMJ0gxqA==", + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", "dev": true, "requires": { "tslib": "^1.8.1" diff --git a/package.json b/package.json index b693a63..5db84ae 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "types": "./typings/lib/msRestAzure.d.ts", "license": "MIT", "dependencies": { - "ms-rest-js": "~0.16.371", + "ms-rest-js": "~0.16.372", "tslib": "^1.9.2" }, "devDependencies": { diff --git a/test/azureServiceClientTests.ts b/test/azureServiceClientTests.ts index 8f38e86..4b7c972 100644 --- a/test/azureServiceClientTests.ts +++ b/test/azureServiceClientTests.ts @@ -2,8 +2,7 @@ // Licensed under the MIT License. See License.txt in the project root for license information. import * as assert from "assert"; -import * as msRest from "ms-rest-js"; -import { HttpHeaders, HttpOperationResponse, TokenCredentials, WebResource } from "ms-rest-js"; +import { HttpHeaders, HttpOperationResponse, RequestOptionsBase, RestError, TokenCredentials, WebResource } from "ms-rest-js"; import { AzureServiceClient, AzureServiceClientOptions, updateOptionsWithDefaultValues } from "../lib/azureServiceClient"; import * as msAssert from "./msAssert"; @@ -12,14 +11,14 @@ describe("AzureServiceClient", () => { it("with no options provided", () => { const client = new AzureServiceClient(new TokenCredentials("my-fake-token")); assert.strictEqual(client.acceptLanguage, "en-us"); - assert.strictEqual(client.longRunningOperationRetryTimeout, 30); + assert.strictEqual(client.longRunningOperationRetryTimeout, undefined); assert.deepStrictEqual(client.userAgentInfo, { value: ["ms-rest-js/0.1.0", "ms-rest-azure/0.1.0"] }); }); it("with acceptLanguage provided", () => { const client = new AzureServiceClient(new TokenCredentials("my-fake-token"), { acceptLanguage: "my-fake-language" }); assert.strictEqual(client.acceptLanguage, "my-fake-language"); - assert.strictEqual(client.longRunningOperationRetryTimeout, 30); + assert.strictEqual(client.longRunningOperationRetryTimeout, undefined); assert.deepStrictEqual(client.userAgentInfo, { value: ["ms-rest-js/0.1.0", "ms-rest-azure/0.1.0"] }); }); @@ -49,7 +48,7 @@ describe("AzureServiceClient", () => { } ]); const httpRequest = new WebResource("https://fake.azure.com/longRunningOperation", "GET"); - const error: msRest.RestError = await msAssert.throwsAsync(serviceClient.sendLongRunningRequest(httpRequest)); + const error: RestError = await msAssert.throwsAsync(serviceClient.sendLongRunningRequest(httpRequest)); assert.strictEqual(error.message, `Error "SyntaxError: Unexpected token < in JSON at position 0" occurred while parsing the response body - hello.`); assert.strictEqual(error.request!.headers.get("authorization"), "Bearer my-fake-token"); }); @@ -166,7 +165,7 @@ describe("AzureServiceClient", () => { { status: 200, body: {} } ]); const httpRequest = new WebResource("https://fake.azure.com/longRunningOperation", "PUT"); - const options: msRest.RequestOptionsBase = { + const options: RequestOptionsBase = { customHeaders: { a: "1" } @@ -184,7 +183,7 @@ describe("AzureServiceClient", () => { { status: 200, body: { properties: { provisioningState: "Failed" } } } ]); const httpRequest = new WebResource("https://fake.azure.com/longRunningOperation", "PUT"); - const error: msRest.RestError = await msAssert.throwsAsync(serviceClient.sendLongRunningRequest(httpRequest)); + const error: RestError = await msAssert.throwsAsync(serviceClient.sendLongRunningRequest(httpRequest)); assert.strictEqual(error.message, `Long running operation failed with status: "Failed".`); assert.strictEqual(error.code, undefined); }); @@ -210,7 +209,7 @@ describe("AzureServiceClient", () => { const httpRequest = new WebResource("https://fake.azure.com/longRunningOperation", "GET"); await msAssert.throwsAsync( serviceClient.sendLongRunningRequest(httpRequest), - new Error(`Location header is missing from long running operation.`)); + new Error(`Can't determine long running operation polling strategy from initial response.`)); }); it("with 202 status and PATCH method", async () => { @@ -218,7 +217,7 @@ describe("AzureServiceClient", () => { const httpRequest = new WebResource("https://fake.azure.com/longRunningOperation", "PATCH"); await msAssert.throwsAsync( serviceClient.sendLongRunningRequest(httpRequest), - new Error(`Location header is missing from long running operation.`)); + new Error(`Can't determine long running operation polling strategy from initial response.`)); }); it("with 202 status, PUT method, and undefined final response body", async () => { @@ -237,7 +236,7 @@ describe("AzureServiceClient", () => { const httpRequest = new WebResource("https://fake.azure.com/longRunningOperation", "POST"); await msAssert.throwsAsync( serviceClient.sendLongRunningRequest(httpRequest), - new Error(`Location header is missing from long running operation.`)); + new Error(`Can't determine long running operation polling strategy from initial response.`)); }); it("with 202 status, POST method, azure-asyncoperation header, and undefined final response body", async () => { @@ -478,7 +477,7 @@ describe("AzureServiceClient", () => { } ]); const httpRequest = new WebResource("https://fake.azure.com/longRunningOperation", "PUT"); - const error: msRest.RestError = await msAssert.throwsAsync(serviceClient.sendLongRunningRequest(httpRequest)); + const error: RestError = await msAssert.throwsAsync(serviceClient.sendLongRunningRequest(httpRequest)); assert.strictEqual(error.message, `Invalid status code with response body "undefined" occurred when polling for operation status.`); assert.strictEqual(error.statusCode, 404); assert.strictEqual(error.request!.headers.contains("authorization"), false); @@ -490,7 +489,7 @@ describe("AzureServiceClient", () => { const httpRequest = new WebResource("https://fake.azure.com/longRunningOperation", "DELETE"); await msAssert.throwsAsync( serviceClient.sendLongRunningRequest(httpRequest), - new Error(`Location header is missing from long running operation.`)); + new Error(`Can't determine long running operation polling strategy from initial response.`)); }); it("with 204 status and GET method", async () => { diff --git a/test/credentials/cognitiveServicesCredentialsTests.ts b/test/credentials/cognitiveServicesCredentialsTests.ts new file mode 100644 index 0000000..9bada6d --- /dev/null +++ b/test/credentials/cognitiveServicesCredentialsTests.ts @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +import * as assert from "assert"; +import { WebResource } from "ms-rest-js"; +import { CognitiveServicesCredentials } from "../../lib/credentials/cognitiveServicesCredentials"; +import * as msAssert from "../msAssert"; + +describe("CognitiveServicesCredentials", () => { + describe("constructor()", () => { + it("with undefined subscription key", () => { + msAssert.throws(() => new CognitiveServicesCredentials(undefined as any), + new Error("subscriptionKey cannot be null or undefined and must be of type string.")); + }); + + it("with null subscription key", () => { + // tslint:disable-next-line:no-null-keyword + msAssert.throws(() => new CognitiveServicesCredentials(null as any), + new Error("subscriptionKey cannot be null or undefined and must be of type string.")); + }); + + it("with number subscription key", () => { + msAssert.throws(() => new CognitiveServicesCredentials(50 as any), + new Error("subscriptionKey cannot be null or undefined and must be of type string.")); + }); + + it("with empty subscription key", () => { + msAssert.throws(() => new CognitiveServicesCredentials(""), + new Error("subscriptionKey cannot be null or undefined and must be of type string.")); + }); + + it("with non-empty subscription key", async () => { + const credentials = new CognitiveServicesCredentials("fake-subscription-key"); + const httpRequest = new WebResource(); + const signedHttpRequest: WebResource = await credentials.signRequest(httpRequest); + assert.strictEqual(signedHttpRequest, httpRequest); + assert.deepEqual(signedHttpRequest.headers.rawHeaders(), { + "Ocp-Apim-Subscription-Key": "fake-subscription-key", + "X-BingApis-SDK-Client": "node-SDK" + }); + }); + }); +}); \ No newline at end of file diff --git a/test/lroPollStrategyTests.ts b/test/lroPollStrategyTests.ts new file mode 100644 index 0000000..59ba2f0 --- /dev/null +++ b/test/lroPollStrategyTests.ts @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +import * as assert from "assert"; +import { HttpHeaders, HttpOperationResponse, TokenCredentials, WebResource } from "ms-rest-js"; +import { AzureServiceClient } from "../lib/azureServiceClient"; +import { getDelayInSeconds } from "../lib/lroPollStrategy"; + +describe("LROPollStrategy", () => { + describe("getDelayInMilliseconds()", () => { + it("with no AzureServiceClient.longRunningOperationRetryTimeout value and no retry-after header", () => { + const azureServiceClient = new AzureServiceClient(new TokenCredentials("my-fake-token")); + const previousResponse: HttpOperationResponse = { + request: new WebResource(), + status: 200, + headers: new HttpHeaders() + }; + assert.strictEqual(getDelayInSeconds(azureServiceClient, previousResponse), 30); + }); + + it("with 11 AzureServiceClient.longRunningOperationRetryTimeout and no retry-after header", () => { + const azureServiceClient = new AzureServiceClient(new TokenCredentials("my-fake-token"), { longRunningOperationRetryTimeout: 11 }); + const previousResponse: HttpOperationResponse = { + request: new WebResource(), + status: 200, + headers: new HttpHeaders() + }; + assert.strictEqual(getDelayInSeconds(azureServiceClient, previousResponse), 11); + }); + + it("with no AzureServiceClient.longRunningOperationRetryTimeout value and 12 retry-after header", () => { + const azureServiceClient = new AzureServiceClient(new TokenCredentials("my-fake-token")); + const previousResponse: HttpOperationResponse = { + request: new WebResource(), + status: 200, + headers: new HttpHeaders({ "retry-after": "12" }) + }; + assert.strictEqual(getDelayInSeconds(azureServiceClient, previousResponse), 12); + }); + + it("with no AzureServiceClient.longRunningOperationRetryTimeout value and spam retry-after header", () => { + const azureServiceClient = new AzureServiceClient(new TokenCredentials("my-fake-token")); + const previousResponse: HttpOperationResponse = { + request: new WebResource(), + status: 200, + headers: new HttpHeaders({ "retry-after": "spam" }) + }; + assert.strictEqual(getDelayInSeconds(azureServiceClient, previousResponse), 30); + }); + + it("with 11 AzureServiceClient.longRunningOperationRetryTimeout and 12 retry-after header", () => { + const azureServiceClient = new AzureServiceClient(new TokenCredentials("my-fake-token"), { longRunningOperationRetryTimeout: 11 }); + const previousResponse: HttpOperationResponse = { + request: new WebResource(), + status: 200, + headers: new HttpHeaders({ "retry-after": "12" }) + }; + assert.strictEqual(getDelayInSeconds(azureServiceClient, previousResponse), 11); + }); + }); +}); \ No newline at end of file diff --git a/test/msAssert.ts b/test/msAssert.ts index 8040465..660b805 100644 --- a/test/msAssert.ts +++ b/test/msAssert.ts @@ -4,10 +4,37 @@ import * as assert from "assert"; /** - * Assert that the provided asyncAction throws an Error. If the expectedError is undefined, then + * Assert that the provided syncFunction throws an Error. If the expectedError is undefined, then * this function will just assert that an Error was thrown. If the expectedError is defined, then * this function will assert that the Error that was thrown is equal to the provided expectedError. - * @param asyncFunction The async function that is expected to thrown an Error. + * @param syncFunction The synchronous function that is expected to thrown an Error. + * @param expectedError The Error that is expected to be thrown. + */ +export function throws(syncFunction: () => void, expectedError?: ((error: Error) => void) | Error): Error { + let thrownError: Error | undefined; + + try { + syncFunction(); + } catch (error) { + thrownError = error; + } + + if (!thrownError) { + assert.throws(() => { }); + } else if (expectedError instanceof Error) { + assert.deepStrictEqual(thrownError, expectedError); + } else if (expectedError) { + expectedError(thrownError); + } + + return thrownError!; +} + +/** + * Assert that the provided asyncFunction throws an Error. If the expectedError is undefined, then + * this function will just assert that an Error was thrown. If the expectedError is defined, then + * this function will assert that the Error that was thrown is equal to the provided expectedError. + * @param asyncFunction The asynchronous function that is expected to thrown an Error. * @param expectedError The Error that is expected to be thrown. */ export async function throwsAsync(asyncFunction: (() => Promise) | Promise, expectedError?: ((error: Error) => void) | Error): Promise {