From 4178ff57aaed7c135c5ad703f8ff3e05c1505b14 Mon Sep 17 00:00:00 2001 From: Melissa Neubert Date: Tue, 2 Nov 2021 15:19:43 -0700 Subject: [PATCH] Recording delete APIs in CallingServerClient (#18493) * Initial delete & tests * Tests for delete apis * PR feedback * Fix formatting * PR updates * Update api file Co-authored-by: Melissa Neubert --- .../delete_live_tests/recording_delete.json | 23 +++++ .../recording_invalid_file_delete.json | 23 +++++ .../recording_unauthorized_delete.json | 23 +++++ .../delete_live_tests/recording_delete.js | 18 ++++ .../recording_invalid_file_delete.js | 18 ++++ .../recording_unauthorized_delete.js | 18 ++++ .../review/communication-callingserver.api.md | 6 +- .../src/ContentDownloader.ts | 20 +---- .../src/callingServerClient.ts | 84 ++++++++++++++++-- .../communication-callingserver/src/models.ts | 4 + .../src/utils/utils.ts | 37 ++++++++ .../test/public/deleteContent.spec.ts | 86 +++++++++++++++++++ 12 files changed, 337 insertions(+), 23 deletions(-) create mode 100644 sdk/communication/communication-callingserver/recordings/browsers/delete_live_tests/recording_delete.json create mode 100644 sdk/communication/communication-callingserver/recordings/browsers/delete_live_tests/recording_invalid_file_delete.json create mode 100644 sdk/communication/communication-callingserver/recordings/browsers/delete_live_tests/recording_unauthorized_delete.json create mode 100644 sdk/communication/communication-callingserver/recordings/node/delete_live_tests/recording_delete.js create mode 100644 sdk/communication/communication-callingserver/recordings/node/delete_live_tests/recording_invalid_file_delete.js create mode 100644 sdk/communication/communication-callingserver/recordings/node/delete_live_tests/recording_unauthorized_delete.js create mode 100644 sdk/communication/communication-callingserver/test/public/deleteContent.spec.ts diff --git a/sdk/communication/communication-callingserver/recordings/browsers/delete_live_tests/recording_delete.json b/sdk/communication/communication-callingserver/recordings/browsers/delete_live_tests/recording_delete.json new file mode 100644 index 000000000000..32931dae0f85 --- /dev/null +++ b/sdk/communication/communication-callingserver/recordings/browsers/delete_live_tests/recording_delete.json @@ -0,0 +1,23 @@ +{ + "recordings": [ + { + "method": "DELETE", + "url": "https://endpoint/v1/objects/0-wus-d6-fdf8ff0fdcd668bca8c52c0b1ee79b05", + "query": {}, + "requestBody": null, + "status": 200, + "response": "", + "responseHeaders": { + "content-length": "0", + "date": "Tue, 02 Nov 2021 17:49:08 GMT", + "server": "Microsoft-HTTPAPI/2.0", + "strict-transport-security": "max-age=31536000; includeSubDomains" + } + } + ], + "uniqueTestInfo": { + "uniqueName": {}, + "newDate": {} + }, + "hash": "e810745688333ce018b48166e9e854e3" +} \ No newline at end of file diff --git a/sdk/communication/communication-callingserver/recordings/browsers/delete_live_tests/recording_invalid_file_delete.json b/sdk/communication/communication-callingserver/recordings/browsers/delete_live_tests/recording_invalid_file_delete.json new file mode 100644 index 000000000000..97de0a6c7512 --- /dev/null +++ b/sdk/communication/communication-callingserver/recordings/browsers/delete_live_tests/recording_invalid_file_delete.json @@ -0,0 +1,23 @@ +{ + "recordings": [ + { + "method": "GET", + "url": "https://endpoint/v1/objects/0-wus-d4-ca4017a32f8514aa9f054f0917270000", + "query": {}, + "requestBody": null, + "status": 404, + "response": "", + "responseHeaders": { + "content-length": "0", + "date": "Tue, 02 Nov 2021 17:47:04 GMT", + "server": "Microsoft-HTTPAPI/2.0", + "strict-transport-security": "max-age=31536000; includeSubDomains" + } + } + ], + "uniqueTestInfo": { + "uniqueName": {}, + "newDate": {} + }, + "hash": "a29d365489ae98459d067c46c720becc" +} \ No newline at end of file diff --git a/sdk/communication/communication-callingserver/recordings/browsers/delete_live_tests/recording_unauthorized_delete.json b/sdk/communication/communication-callingserver/recordings/browsers/delete_live_tests/recording_unauthorized_delete.json new file mode 100644 index 000000000000..0a8d608ae5ee --- /dev/null +++ b/sdk/communication/communication-callingserver/recordings/browsers/delete_live_tests/recording_unauthorized_delete.json @@ -0,0 +1,23 @@ +{ + "recordings": [ + { + "method": "DELETE", + "url": "https://endpoint/v1/objects/0-wus-d6-fdf8ff0fdcd668bca8c52c0b1ee79b05", + "query": {}, + "requestBody": null, + "status": 401, + "response": "", + "responseHeaders": { + "content-length": "0", + "date": "Tue, 02 Nov 2021 17:47:04 GMT", + "server": "Microsoft-HTTPAPI/2.0", + "strict-transport-security": "max-age=31536000; includeSubDomains" + } + } + ], + "uniqueTestInfo": { + "uniqueName": {}, + "newDate": {} + }, + "hash": "1b018902befb4c128b53cbe8d7af4a82" +} \ No newline at end of file diff --git a/sdk/communication/communication-callingserver/recordings/node/delete_live_tests/recording_delete.js b/sdk/communication/communication-callingserver/recordings/node/delete_live_tests/recording_delete.js new file mode 100644 index 000000000000..ebd767717c33 --- /dev/null +++ b/sdk/communication/communication-callingserver/recordings/node/delete_live_tests/recording_delete.js @@ -0,0 +1,18 @@ +let nock = require('nock'); + +module.exports.hash = "2edca15d57c1511a41d4cbd310f4422b"; + +module.exports.testInfo = {"uniqueName":{},"newDate":{}} + +nock('https://endpoint', {"encodedQueryParams":true}) + .delete('/v1/objects/0-wus-d6-fdf8ff0fdcd668bca8c52c0b1ee79b05') + .reply(200, "", [ + 'Content-Length', + '0', + 'Server', + 'Microsoft-HTTPAPI/2.0', + 'Strict-Transport-Security', + 'max-age=31536000; includeSubDomains', + 'Date', + 'Tue, 02 Nov 2021 17:43:52 GMT' +]); diff --git a/sdk/communication/communication-callingserver/recordings/node/delete_live_tests/recording_invalid_file_delete.js b/sdk/communication/communication-callingserver/recordings/node/delete_live_tests/recording_invalid_file_delete.js new file mode 100644 index 000000000000..ba24187cda73 --- /dev/null +++ b/sdk/communication/communication-callingserver/recordings/node/delete_live_tests/recording_invalid_file_delete.js @@ -0,0 +1,18 @@ +let nock = require('nock'); + +module.exports.hash = "0d9469b3f72006adfc5ffce15953963a"; + +module.exports.testInfo = {"uniqueName":{},"newDate":{}} + +nock('https://endpoint', {"encodedQueryParams":true}) + .get('/v1/objects/0-wus-d4-ca4017a32f8514aa9f054f0917270000') + .reply(404, "", [ + 'Content-Length', + '0', + 'Server', + 'Microsoft-HTTPAPI/2.0', + 'Strict-Transport-Security', + 'max-age=31536000; includeSubDomains', + 'Date', + 'Tue, 02 Nov 2021 17:46:09 GMT' +]); diff --git a/sdk/communication/communication-callingserver/recordings/node/delete_live_tests/recording_unauthorized_delete.js b/sdk/communication/communication-callingserver/recordings/node/delete_live_tests/recording_unauthorized_delete.js new file mode 100644 index 000000000000..1305650d372d --- /dev/null +++ b/sdk/communication/communication-callingserver/recordings/node/delete_live_tests/recording_unauthorized_delete.js @@ -0,0 +1,18 @@ +let nock = require('nock'); + +module.exports.hash = "44aeeb9e1bce10c53420368854607266"; + +module.exports.testInfo = {"uniqueName":{},"newDate":{}} + +nock('https://endpoint', {"encodedQueryParams":true}) + .delete('/v1/objects/0-wus-d6-fdf8ff0fdcd668bca8c52c0b1ee79b05') + .reply(401, "", [ + 'Content-Length', + '0', + 'Server', + 'Microsoft-HTTPAPI/2.0', + 'Strict-Transport-Security', + 'max-age=31536000; includeSubDomains', + 'Date', + 'Tue, 02 Nov 2021 17:46:09 GMT' +]); diff --git a/sdk/communication/communication-callingserver/review/communication-callingserver.api.md b/sdk/communication/communication-callingserver/review/communication-callingserver.api.md index d6c1f190931f..0fa0d6f4709c 100644 --- a/sdk/communication/communication-callingserver/review/communication-callingserver.api.md +++ b/sdk/communication/communication-callingserver/review/communication-callingserver.api.md @@ -85,6 +85,7 @@ export class CallingServerClient { cancelMediaOperation(callLocator: CallLocator, mediaOperationId: string, options?: CancelMediaOperationOptions): Promise; cancelParticipantMediaOperation(callLocator: CallLocator, participant: CommunicationIdentifier, mediaOperationId: string, options?: CancelMediaOperationOptions): Promise; createCallConnection(source: CommunicationIdentifier, targets: CommunicationIdentifier[], options: CreateCallConnectionOptions): Promise; + delete(deleteUri: string, options?: DeleteOptions): Promise; download(uri: string, offset?: number, options?: DownloadOptions): Promise; getCallConnection(callConnectionId: string): CallConnection; // Warning: (ae-forgotten-export) The symbol "CallRecordingProperties" needs to be exported by the entry point index.d.ts @@ -102,7 +103,7 @@ export class CallingServerClient { // Warning: (ae-forgotten-export) The symbol "StartCallRecordingResult" needs to be exported by the entry point index.d.ts startRecording(callLocator: CallLocator, recordingStateCallbackUri: string, options?: StartRecordingOptions): Promise; stopRecording(recordingId: string, options?: StopRecordingOptions): Promise; -} + } // @public export interface CallingServerClientOptions extends PipelineOptions { @@ -164,6 +165,9 @@ export interface CreateCallConnectionOptions extends OperationOptions { subject?: string; } +// @public +export type DeleteOptions = OperationOptions; + // @public (undocumented) export interface DownloadContentOptions extends DownloadOptions { range?: string; diff --git a/sdk/communication/communication-callingserver/src/ContentDownloader.ts b/sdk/communication/communication-callingserver/src/ContentDownloader.ts index 23527da31125..ea6d2d8cc0af 100644 --- a/sdk/communication/communication-callingserver/src/ContentDownloader.ts +++ b/sdk/communication/communication-callingserver/src/ContentDownloader.ts @@ -7,8 +7,6 @@ import * as Parameters from "./parameters"; import * as Mappers from "./generated/src/models/mappers"; import * as ExtraMappers from "./mappers"; import { CallingServerApiClientContext } from "./generated/src/callingServerApiClientContext"; -import { URLBuilder } from "@azure/core-http"; -import { OperationQueryParameter } from "@azure/core-http"; import { createSpan } from "./tracing"; import { SpanStatusCode } from "@azure/core-tracing"; @@ -20,6 +18,7 @@ import { OperationSpec } from "@azure/core-http"; import { ContentDownloadResponse } from "."; +import { CallingServerUtils } from "./utils/utils"; export class ContentDownloader { private readonly client: CallingServerApiClientContext; @@ -64,10 +63,7 @@ export class ContentDownloader { const operationArguments: OperationArguments = { options: operationOptionsToRequestOptionsBase(options || {}) }; - - const q = URLBuilder.parse(contentUri); - const formattedUrl = q.getPath()!.startsWith("/") ? q.getPath()!.substr(1) : q.getPath()!; - const stringToSign = this.client.endpoint + formattedUrl; + const stringToSign = CallingServerUtils.getStringToSign(this.client.endpoint, contentUri); return this.client.sendOperationRequest( operationArguments, getDownloadContentOperationSpec(contentUri, stringToSign) @@ -79,17 +75,7 @@ export class ContentDownloader { const serializer = new Serializer(Mappers, /* isXml */ false); function getDownloadContentOperationSpec(url: string, stringToSign: string): OperationSpec { - const stringToSignHeader: OperationQueryParameter = { - parameterPath: "UriToSignWith", - mapper: { - defaultValue: stringToSign, - isConstant: true, - serializedName: "UriToSignWith", - type: { - name: "String" - } - } - }; + const stringToSignHeader = CallingServerUtils.getStringToSignHeader(stringToSign); const downloadContentOperationSpec: OperationSpec = { path: "", diff --git a/sdk/communication/communication-callingserver/src/callingServerClient.ts b/sdk/communication/communication-callingserver/src/callingServerClient.ts index fd1dc24e4466..80bfd83f7568 100644 --- a/sdk/communication/communication-callingserver/src/callingServerClient.ts +++ b/sdk/communication/communication-callingserver/src/callingServerClient.ts @@ -17,7 +17,8 @@ import { PauseRecordingOptions, ResumeRecordingOptions, StopRecordingOptions, - GetRecordingPropertiesOptions + GetRecordingPropertiesOptions, + DeleteOptions } from "./models"; import { CallConnections, ServerCalls } from "./generated/src/operations"; import { @@ -35,6 +36,7 @@ import { StartCallRecordingWithCallLocatorRequest, CallRecordingProperties } from "./generated/src/models"; +import * as Mappers from "./generated/src/models/mappers"; import { TokenCredential } from "@azure/core-auth"; import { @@ -50,7 +52,10 @@ import { InternalPipelineOptions, createPipelineFromOptions, operationOptionsToRequestOptionsBase, - RestResponse + RestResponse, + OperationArguments, + OperationSpec, + Serializer } from "@azure/core-http"; import { SpanStatusCode } from "@azure/core-tracing"; import { CallingServerApiClient } from "./generated/src/callingServerApiClient"; @@ -85,7 +90,7 @@ export class CallingServerClient { private readonly callingServerServiceClient: CallingServerApiClient; private readonly callConnectionRestClient: CallConnections; private readonly serverCallRestClient: ServerCalls; - private readonly downloadCallingServerApiClient: CallingServerApiClientContext; + private readonly storageApiClient: CallingServerApiClientContext; /** * Initializes a new instance of the CallingServerClient class. @@ -138,7 +143,7 @@ export class CallingServerClient { this.callingServerServiceClient = new CallingServerApiClient(url, pipeline); this.callConnectionRestClient = this.callingServerServiceClient.callConnections; this.serverCallRestClient = this.callingServerServiceClient.serverCalls; - this.downloadCallingServerApiClient = new CallingServerApiClientContext(url, pipeline); + this.storageApiClient = new CallingServerApiClientContext(url, pipeline); } /** @@ -150,7 +155,7 @@ export class CallingServerClient { } public initializeContentDownloader(): ContentDownloader { - return new ContentDownloader(this.downloadCallingServerApiClient); + return new ContentDownloader(this.storageApiClient); } /** @@ -824,4 +829,73 @@ export class CallingServerClient { span.end(); } } + + /** + * Deletes the content pointed to the uri passed as a parameter. + * + * * Returns a RestResponse indicating the result of the delete operation. + * + * @param deleteUri - Endpoint where the content exists. + * + * Example usage: + * + * ```js + * // Delete content + * const deleteUri = "https://deleteUri.com"; + * const deleteResponse = await callingServerClient.delete(deleteUri); + * + * ``` + */ + public async delete(deleteUri: string, options: DeleteOptions = {}): Promise { + const { span, updatedOptions } = createSpan("ServerCallRestClient-delete", options); + + const operationArguments: OperationArguments = { + options: operationOptionsToRequestOptionsBase(updatedOptions) + }; + + try { + const stringToSign = CallingServerUtils.getStringToSign( + this.storageApiClient.endpoint, + deleteUri + ); + return this.storageApiClient.sendOperationRequest( + operationArguments, + getDeleteOperationSpec(deleteUri, stringToSign) + ) as Promise; + } catch (e) { + span.setStatus({ + code: SpanStatusCode.ERROR, + message: e.message + }); + throw e; + } finally { + span.end(); + } + } +} + +function getDeleteOperationSpec(url: string, stringToSign: string): OperationSpec { + // Operation Specifications + const serializer = new Serializer(Mappers, /* isXml */ false); + const stringToSignHeader = CallingServerUtils.getStringToSignHeader(stringToSign); + const hostHeader = CallingServerUtils.getMsHostHeaders(stringToSign); + + const deleteOperationSpec: OperationSpec = { + path: "", + baseUrl: url, + httpMethod: "DELETE", + responses: { + 200: {}, + default: { + bodyMapper: Mappers.CommunicationErrorResponse + } + }, + requestBody: undefined, + queryParameters: [], + urlParameters: [], + headerParameters: [stringToSignHeader, hostHeader], + serializer + }; + + return deleteOperationSpec; } diff --git a/sdk/communication/communication-callingserver/src/models.ts b/sdk/communication/communication-callingserver/src/models.ts index 2d7df30f097a..04630834e3f6 100644 --- a/sdk/communication/communication-callingserver/src/models.ts +++ b/sdk/communication/communication-callingserver/src/models.ts @@ -131,6 +131,10 @@ export type StopRecordingOptions = OperationOptions; * Options to get recording properties. */ export type GetRecordingPropertiesOptions = OperationOptions; +/** + * Options to delete recording. + */ +export type DeleteOptions = OperationOptions; /** * Call Locator. diff --git a/sdk/communication/communication-callingserver/src/utils/utils.ts b/sdk/communication/communication-callingserver/src/utils/utils.ts index 540c53100081..dd72df897ae6 100644 --- a/sdk/communication/communication-callingserver/src/utils/utils.ts +++ b/sdk/communication/communication-callingserver/src/utils/utils.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +import { OperationQueryParameter } from "@azure/core-http"; import { URLBuilder } from "@azure/core-http"; export class CallingServerUtils { @@ -13,4 +14,40 @@ export class CallingServerUtils { } return url.getScheme() === "http" || url.getScheme() === "https"; } + + public static getStringToSignHeader(stringToSign: string): OperationQueryParameter { + return { + parameterPath: "UriToSignWith", + mapper: { + defaultValue: stringToSign, + isConstant: true, + serializedName: "UriToSignWith", + type: { + name: "String" + } + } + }; + } + + public static getMsHostHeaders(hostName: string): OperationQueryParameter { + const q = URLBuilder.parse(hostName!); + const hostAndPort = q.getHost()! + (q.getPort() !== undefined ? q.getPort() : ""); + return { + parameterPath: "x-ms-host", + mapper: { + defaultValue: hostAndPort, + isConstant: true, + serializedName: "x-ms-host", + type: { + name: "String" + } + } + }; + } + + public static getStringToSign(resourceEndpoint: string, requestUri: string): string { + const q = URLBuilder.parse(requestUri); + const formattedUrl = q.getPath()!.startsWith("/") ? q.getPath()!.substr(1) : q.getPath()!; + return resourceEndpoint + formattedUrl; + } } diff --git a/sdk/communication/communication-callingserver/test/public/deleteContent.spec.ts b/sdk/communication/communication-callingserver/test/public/deleteContent.spec.ts new file mode 100644 index 000000000000..53b1128b0737 --- /dev/null +++ b/sdk/communication/communication-callingserver/test/public/deleteContent.spec.ts @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { + env, + isPlaybackMode, + record, + Recorder, + RecorderEnvironmentSetup +} from "@azure-tools/test-recorder"; +import { assert } from "chai"; +import { CallingServerClient } from "../../src"; +import { Context } from "mocha"; +import { RestError } from "@azure/core-http"; + +const replaceableVariables: { [k: string]: string } = { + COMMUNICATION_LIVETEST_DYNAMIC_CONNECTION_STRING: "endpoint=https://endpoint/;accesskey=banana" +}; + +const environmentSetup: RecorderEnvironmentSetup = { + replaceableVariables, + customizationsOnRecordings: [ + (recording: string): string => recording.replace(/(https:\/\/)([^/',]*)/, "$1endpoint"), + (recording: string): string => recording.replace("endpoint:443", "endpoint") + ], + queryParametersToSkip: [] +}; + +describe("Delete Live Tests", function() { + let recorder: Recorder; + const uri = "https://endpoint/v1/objects/0-wus-d6-fdf8ff0fdcd668bca8c52c0b1ee79b05"; + const invalidUri = "https://endpoint/v1/objects/0-wus-d4-ca4017a32f8514aa9f054f0917270000"; + + const callingServerServiceClient = new CallingServerClient( + env.COMMUNICATION_LIVETEST_DYNAMIC_CONNECTION_STRING || + "endpoint=https://endpoint/;accesskey=banana" + ); + + beforeEach(async function(this: Context) { + recorder = record(this, environmentSetup); + }); + + afterEach(async function(this: Context) { + if (!this.currentTest?.isPending()) { + await recorder.stop(); + } + }); + + it("delete", async function(this: Context) { + if (!isPlaybackMode()) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + + const result = await callingServerServiceClient.delete(uri); + assert.equal(200, result._response.status); + }); + + it("unauthorized delete", async function(this: Context) { + if (!isPlaybackMode()) { + // tslint:disable-next-l ine:no-invalid-this + this.skip(); + } + + try { + const unauthorizedCallingServerServiceClient = new CallingServerClient( + "endpoint=https://test.communication.azure.com/;accesskey=1234" + ); + await unauthorizedCallingServerServiceClient.delete(uri); + } catch (e) { + assert.equal((e as RestError).statusCode, 401); + } + }); + + it("invalid file delete", async function(this: Context) { + if (!isPlaybackMode()) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + try { + await callingServerServiceClient.download(invalidUri); + } catch (e) { + assert.equal((e as RestError).statusCode, 404); + } + }); +});