From 404e4cf288d8de4743c0726a85898365305b2d55 Mon Sep 17 00:00:00 2001 From: Aigerim Beishenbekova Date: Wed, 18 Dec 2024 14:20:40 +0000 Subject: [PATCH 1/4] added routing logic --- .../Azure.Communication.Common/CHANGELOG.md | 1 + .../Azure.Communication.Common/README.md | 29 ++++- ...ntraCommunicationTokenCredentialOptions.cs | 9 +- .../src/EntraTokenCredential.cs | 44 ++++++- .../Identity/EntraTokenCredentialTest.cs | 112 +++++++++++------- 5 files changed, 136 insertions(+), 59 deletions(-) diff --git a/sdk/communication/Azure.Communication.Common/CHANGELOG.md b/sdk/communication/Azure.Communication.Common/CHANGELOG.md index 9b0bede7dc11c..911d0e6bd1e0d 100644 --- a/sdk/communication/Azure.Communication.Common/CHANGELOG.md +++ b/sdk/communication/Azure.Communication.Common/CHANGELOG.md @@ -3,6 +3,7 @@ ## 1.4.0-beta.1 (Unreleased) ### Features Added +- Introduced support for `Azure.Core.TokenCredential` with `EntraCommunicationTokenCredentialOptions`, enabling Entra users to authorize Communication Services and allowing Azure Communication Services to receive calls from Teams resource accounts. ### Breaking Changes diff --git a/sdk/communication/Azure.Communication.Common/README.md b/sdk/communication/Azure.Communication.Common/README.md index e62022ba2c0ea..ec65f02141280 100644 --- a/sdk/communication/Azure.Communication.Common/README.md +++ b/sdk/communication/Azure.Communication.Common/README.md @@ -119,10 +119,31 @@ var entraTokenCredential = new InteractiveBrowserCredential(options); var entraTokenCredentialOptions = new EntraCommunicationTokenCredentialOptions( resourceEndpoint: "https://.communication.azure.com", - entraTokenCredential: entraTokenCredential -){ - Scopes = new[] { "https://communication.azure.com/clients/VoIP" } -}; + entraTokenCredential: entraTokenCredential, + scopes: new[] { "https://communication.azure.com/clients/VoIP" } +); + +var credential = new CommunicationTokenCredential(entraTokenCredentialOptions); + +``` + +The same approach can be used to authorize your Azure Communication Services resource to receive calls from the Teams resource account. +This requires providing the `https://auth.msft.communication.azure.com/TeamsExtension.ManageCalls` scope +```C# +var options = new InteractiveBrowserCredentialOptions + { + TenantId = "", + ClientId = "", + RedirectUri = new Uri(""), + AuthorityHost = new Uri("https://login.microsoftonline.com/") + }; +var entraTokenCredential = new InteractiveBrowserCredential(options); + +var entraTokenCredentialOptions = new EntraCommunicationTokenCredentialOptions( + resourceEndpoint: "https://.communication.azure.com", + entraTokenCredential: entraTokenCredential, + scopes: new[] { "https://auth.msft.communication.azure.com/TeamsExtension.ManageCalls" } +); var credential = new CommunicationTokenCredential(entraTokenCredentialOptions); diff --git a/sdk/communication/Azure.Communication.Common/src/EntraCommunicationTokenCredentialOptions.cs b/sdk/communication/Azure.Communication.Common/src/EntraCommunicationTokenCredentialOptions.cs index 034cd1f03d827..3863072ffcb67 100644 --- a/sdk/communication/Azure.Communication.Common/src/EntraCommunicationTokenCredentialOptions.cs +++ b/sdk/communication/Azure.Communication.Common/src/EntraCommunicationTokenCredentialOptions.cs @@ -11,7 +11,6 @@ namespace Azure.Communication /// public class EntraCommunicationTokenCredentialOptions { - private static string[] DefaultScopes = { "https://communication.azure.com/clients/.default" }; /// /// The URI of the Azure Communication Services resource. /// @@ -25,23 +24,25 @@ public class EntraCommunicationTokenCredentialOptions /// /// The scopes required for the Entra user token. These scopes determine the permissions granted to the token. For example, ["https://communication.azure.com/clients/VoIP"]. /// - public string[] Scopes { get; set; } + public string[] Scopes { get; } /// /// Initializes a new instance of . /// /// The URI of the Azure Communication Services resource.For example, https://myResource.communication.azure.com. /// The credential capable of fetching an Entra user token. + /// The scopes required for the Entra user token. public EntraCommunicationTokenCredentialOptions( string resourceEndpoint, - TokenCredential entraTokenCredential) + TokenCredential entraTokenCredential, + string[] scopes) { Argument.AssertNotNullOrEmpty(resourceEndpoint, nameof(resourceEndpoint)); Argument.AssertNotNull(entraTokenCredential, nameof(entraTokenCredential)); this.ResourceEndpoint = resourceEndpoint; this.TokenCredential = entraTokenCredential; - this.Scopes = DefaultScopes; + this.Scopes = scopes; } } } diff --git a/sdk/communication/Azure.Communication.Common/src/EntraTokenCredential.cs b/sdk/communication/Azure.Communication.Common/src/EntraTokenCredential.cs index 9660851235dba..4b451ed7ac15b 100644 --- a/sdk/communication/Azure.Communication.Common/src/EntraTokenCredential.cs +++ b/sdk/communication/Azure.Communication.Common/src/EntraTokenCredential.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -15,8 +16,12 @@ namespace Azure.Communication /// internal sealed class EntraTokenCredential : ICommunicationTokenCredential { + private const string TeamsExtentionsScopePrefix = "https://auth.msft.communication.azure.com/"; + private const string EntraScopePrefix = "https://communication.azure.com/clients/"; + private HttpPipeline _pipeline; private string _resourceEndpoint; + private string[] _scopes { get; set; } private readonly ThreadSafeRefreshableAccessTokenCache _accessTokenCache; /// @@ -27,6 +32,7 @@ internal sealed class EntraTokenCredential : ICommunicationTokenCredential public EntraTokenCredential(EntraCommunicationTokenCredentialOptions options, HttpPipelineTransport pipelineTransport = null) { this._resourceEndpoint = options.ResourceEndpoint; + this._scopes = options.Scopes; _pipeline = CreatePipelineFromOptions(options, pipelineTransport); _accessTokenCache = new ThreadSafeRefreshableAccessTokenCache( ExchangeEntraToken, @@ -99,14 +105,9 @@ private async ValueTask ExchangeEntraTokenAsync(bool async, Cancell private HttpMessage CreateRequestMessage() { - var uri = new RequestUriBuilder(); - uri.Reset(new Uri(_resourceEndpoint)); - uri.AppendPath("/access/entra/:exchangeAccessToken", false); - uri.AppendQuery("api-version", "2024-04-01-preview", true); - var message = _pipeline.CreateMessage(); var request = message.Request; - request.Uri = uri; + request.Uri = CreateRequestUri(); request.Method = RequestMethod.Post; request.Headers.Add("Accept", "application/json"); request.Headers.Add("Content-Type", "application/json"); @@ -115,6 +116,37 @@ private HttpMessage CreateRequestMessage() return message; } + private RequestUriBuilder CreateRequestUri() + { + var uri = new RequestUriBuilder(); + uri.Reset(new Uri(_resourceEndpoint)); + + var (endpoint, apiVersion) = DetermineEndpointAndApiVersion(); + uri.AppendPath(endpoint, false); + uri.AppendQuery("api-version", apiVersion, true); + return uri; + } + + private (string Endpoint, string ApiVersion) DetermineEndpointAndApiVersion() + { + if (_scopes == null || !_scopes.Any()) + { + throw new ArgumentException($"Scopes validation failed. Ensure all scopes start with either {TeamsExtentionsScopePrefix} or {EntraScopePrefix}.", nameof(_scopes)); + } + else if (_scopes.All(item => item.StartsWith(TeamsExtentionsScopePrefix))) + { + return ("/access/teamsPhone/:exchangeTeamsAccessToken", "2025-03-02-preview"); + } + else if (_scopes.All(item => item.StartsWith(EntraScopePrefix))) + { + return ("/access/entra/:exchangeAccessToken", "2024-04-01-preview"); + } + else + { + throw new ArgumentException($"Scopes validation failed. Ensure all scopes start with either {TeamsExtentionsScopePrefix} or {EntraScopePrefix}.", nameof(_scopes)); + } + } + private AccessToken ParseAccessTokenFromResponse(Response response) { switch (response.Status) diff --git a/sdk/communication/Azure.Communication.Common/tests/Identity/EntraTokenCredentialTest.cs b/sdk/communication/Azure.Communication.Common/tests/Identity/EntraTokenCredentialTest.cs index c7cea3fb737d3..ea234866f1725 100644 --- a/sdk/communication/Azure.Communication.Common/tests/Identity/EntraTokenCredentialTest.cs +++ b/sdk/communication/Azure.Communication.Common/tests/Identity/EntraTokenCredentialTest.cs @@ -24,7 +24,8 @@ public class EntraTokenCredentialTest protected string TokenResponse = string.Format(TokenResponseTemplate, SampleToken, SampleTokenExpiry); private Mock _mockTokenCredential = null!; - private string[] _scopes = new string[] { "https://communication.azure.com/clients/VoIP" }; + private const string entraScope = "https://communication.azure.com/clients/VoIP"; + private const string opsScope = "https://auth.msft.communication.azure.com/TeamsExtension.ManageCalls"; private string _resourceEndpoint = "https://myResource.communication.azure.com"; [SetUp] @@ -38,46 +39,34 @@ public void Setup() } [Test] - public void EntraTokenCredential_Init_ThrowsErrorWithNulls() + [TestCase(new string[] { entraScope }, typeof(ArgumentException))] + [TestCase(new string[] { opsScope }, typeof(ArgumentException))] + public void EntraTokenCredential_Init_ThrowsErrorWithNulls(string[] scopes, Type exception) { Assert.Throws(() => new EntraCommunicationTokenCredentialOptions( null, - _mockTokenCredential.Object) - { - Scopes = _scopes - }); + _mockTokenCredential.Object, + scopes)); Assert.Throws(() => new EntraCommunicationTokenCredentialOptions( "", - _mockTokenCredential.Object) - { - Scopes = _scopes - }); + _mockTokenCredential.Object, + scopes)); Assert.Throws(() => new EntraCommunicationTokenCredentialOptions( _resourceEndpoint, - null) - { - Scopes = _scopes - }); - } - - [Test] - public void EntraTokenCredential_InitWithoutScopes_InitsWithDefaultScope() - { - var credential = new EntraCommunicationTokenCredentialOptions( - _resourceEndpoint, - _mockTokenCredential.Object); - var scopes = new[] { "https://communication.azure.com/clients/.default" }; - Assert.AreEqual(credential.Scopes, scopes); + null, + scopes)); } [Test] - public void EntraTokenCredential_Init_FetchesTokenImmediately() + [TestCase(new string[] { entraScope }, typeof(ArgumentException))] + [TestCase(new string[] { opsScope }, typeof(ArgumentException))] + public void EntraTokenCredential_Init_FetchesTokenImmediately(string[] scopes, Type exception) { // Arrange var expiryTime = DateTimeOffset.Parse(SampleTokenExpiry, null, System.Globalization.DateTimeStyles.RoundtripKind); - var options = CreateEntraTokenCredentialOptions(); + var options = CreateEntraTokenCredentialOptions(scopes); var mockTransport = CreateMockTransport(new[] { CreateMockResponse(200, TokenResponse) }); var entraTokenCredential = new EntraTokenCredential(options, mockTransport); // Assert @@ -85,12 +74,14 @@ public void EntraTokenCredential_Init_FetchesTokenImmediately() } [Test] - public async Task EntraTokenCredential_GetToken_ReturnsToken() + [TestCase("/access/entra/:exchangeAccessToken", new string[] { entraScope }, typeof(ArgumentException))] + [TestCase("/access/teamsPhone/:exchangeTeamsAccessToken", new string[] { opsScope }, typeof(ArgumentException))] + public async Task EntraTokenCredential_GetToken_ReturnsToken(string expectedEndpoint, string[] scopes, Type exception) { // Arrange var expiryTime = DateTimeOffset.Parse(SampleTokenExpiry, null, System.Globalization.DateTimeStyles.RoundtripKind); - var options = CreateEntraTokenCredentialOptions(); - var mockTransport = CreateMockTransport(new[] { CreateMockResponse(200, TokenResponse) }); + var options = CreateEntraTokenCredentialOptions(scopes); + var mockTransport = (MockTransport) CreateMockTransport(new[] { CreateMockResponse(200, TokenResponse) }); var entraTokenCredential = new EntraTokenCredential(options, mockTransport); // Act @@ -99,11 +90,14 @@ public async Task EntraTokenCredential_GetToken_ReturnsToken() // Assert Assert.AreEqual(SampleToken, token.Token); Assert.AreEqual(token.ExpiresOn, expiryTime); + Assert.AreEqual(expectedEndpoint, mockTransport.SingleRequest.Uri.Path, expectedEndpoint); _mockTokenCredential.Verify(tc => tc.GetTokenAsync(It.IsAny(), It.IsAny()), Times.Once); } [Test] - public async Task EntraTokenCredential_GetToken_InternalEntraTokenChangeInvalidatesCachedToken() + [TestCase(new string[] { entraScope }, typeof(ArgumentException))] + [TestCase(new string[] { opsScope }, typeof(ArgumentException))] + public async Task EntraTokenCredential_GetToken_InternalEntraTokenChangeInvalidatesCachedToken(string[] scopes, Type exception) { // Arrange var expiryTime = DateTimeOffset.Parse(SampleTokenExpiry, null, System.Globalization.DateTimeStyles.RoundtripKind); @@ -114,7 +108,7 @@ public async Task EntraTokenCredential_GetToken_InternalEntraTokenChangeInvalida .ReturnsAsync(new AccessToken("Entra token for call from constructor", refreshOn)) .ReturnsAsync(new AccessToken("Entra token for the first getToken call token", expiryTime)); - var options = CreateEntraTokenCredentialOptions(); + var options = CreateEntraTokenCredentialOptions(scopes); var latestTokenResponse = string.Format(TokenResponseTemplate, newToken, SampleTokenExpiry); var mockTransport = CreateMockTransport(new[] { CreateMockResponse(200, TokenResponse), CreateMockResponse(200, latestTokenResponse) }); var entraTokenCredential = new EntraTokenCredential(options, mockTransport); @@ -127,10 +121,12 @@ public async Task EntraTokenCredential_GetToken_InternalEntraTokenChangeInvalida } [Test] - public async Task EntraTokenCredential_GetToken_MultipleCallsReturnsCachedToken() + [TestCase(new string[] { entraScope }, typeof(ArgumentException))] + [TestCase(new string[] { opsScope }, typeof(ArgumentException))] + public async Task EntraTokenCredential_GetToken_MultipleCallsReturnsCachedToken(string[] scopes, Type exception) { // Arrange - var options = CreateEntraTokenCredentialOptions(); + var options = CreateEntraTokenCredentialOptions(scopes); var mockTransport = CreateMockTransport(new[] { CreateMockResponse(200, TokenResponse) }); var entraTokenCredential = new EntraTokenCredential(options, mockTransport); @@ -146,10 +142,12 @@ public async Task EntraTokenCredential_GetToken_MultipleCallsReturnsCachedToken( } [Test] - public void EntraTokenCredential_GetToken_ThrowsFailedResponse() + [TestCase(new string[] { entraScope }, typeof(ArgumentException))] + [TestCase(new string[] { opsScope }, typeof(ArgumentException))] + public void EntraTokenCredential_GetToken_ThrowsFailedResponse(string[] scopes, Type exception) { // Arrange - var options = CreateEntraTokenCredentialOptions(); + var options = CreateEntraTokenCredentialOptions(scopes); var errorMessage = "{\"error\":{\"code\":\"BadRequest\",\"message\":\"Invalid request.\"}}"; var mockResponses = new[] { @@ -164,10 +162,12 @@ public void EntraTokenCredential_GetToken_ThrowsFailedResponse() } [Test] - public void EntraTokenCredential_GetToken_ThrowsInvalidJson() + [TestCase(new string[] { entraScope }, typeof(ArgumentException))] + [TestCase(new string[] { opsScope }, typeof(ArgumentException))] + public void EntraTokenCredential_GetToken_ThrowsInvalidJson(string[] scopes, Type exception) { // Arrange - var options = CreateEntraTokenCredentialOptions(); + var options = CreateEntraTokenCredentialOptions(scopes); var errorMessage = "{\"error\":{\"code\":\"BadRequest\",\"message\":\"Invalid request.\"}}"; var mockResponses = new[] { @@ -183,10 +183,12 @@ public void EntraTokenCredential_GetToken_ThrowsInvalidJson() } [Test] - public void EntraTokenCredential_GetToken_RetriesThreeTimesOnTransientError() + [TestCase(new string[] { entraScope }, typeof(ArgumentException))] + [TestCase(new string[] { opsScope }, typeof(ArgumentException))] + public void EntraTokenCredential_GetToken_RetriesThreeTimesOnTransientError(string[] scopes, Type exception) { // Arrange - var options = CreateEntraTokenCredentialOptions(); + var options = CreateEntraTokenCredentialOptions(scopes); var lastRetryErrorMessage = "Last Retry Error Message"; var mockResponses = new MockResponse[] { @@ -205,21 +207,41 @@ public void EntraTokenCredential_GetToken_RetriesThreeTimesOnTransientError() var entraTokenCredential = new EntraTokenCredential(options, mockTransport); // Act & Assert - var exception = Assert.ThrowsAsync(async () => await entraTokenCredential.GetTokenAsync(CancellationToken.None)); - Assert.AreEqual(lastRetryErrorMessage, lastRetryErrorMessage); + var ex = Assert.ThrowsAsync(async () => await entraTokenCredential.GetTokenAsync(CancellationToken.None)); + StringAssert.Contains(lastRetryErrorMessage, ex?.Message); } - private EntraCommunicationTokenCredentialOptions CreateEntraTokenCredentialOptions() + [Test] + [TestCase(new string[] { entraScope, opsScope }, typeof(ArgumentException))] + [TestCase(new string[] { opsScope, entraScope }, typeof(ArgumentException))] + [TestCase(new string[] { "invalidScope" }, typeof(ArgumentException))] + [TestCase(new string[] { "" }, typeof(ArgumentException))] + [TestCase(new string[] { }, typeof(ArgumentException))] + public void EntraTokenCredential_GetToken_ThrowsForInvalidScopes(string[] scopes, Type exception) { - return new EntraCommunicationTokenCredentialOptions(_resourceEndpoint, _mockTokenCredential.Object) + // Arrange + var options = CreateEntraTokenCredentialOptions(scopes); + var mockResponses = new MockResponse[] { - Scopes = _scopes + CreateMockResponse(200, TokenResponse) }; + + var mockTransport = CreateMockTransport(mockResponses); + var entraTokenCredential = new EntraTokenCredential(options, mockTransport); + + // Act & Assert + var ex = Assert.ThrowsAsync(async () => await entraTokenCredential.GetTokenAsync(CancellationToken.None)); + StringAssert.Contains("Scopes validation failed. Ensure all scopes start with either", ex?.Message); + } + + private EntraCommunicationTokenCredentialOptions CreateEntraTokenCredentialOptions(string[] scopes) + { + return new EntraCommunicationTokenCredentialOptions(_resourceEndpoint, _mockTokenCredential.Object, scopes); } private MockResponse CreateMockResponse(int statusCode, string body) { - return new MockResponse(statusCode).WithContent(body); + return new MockResponse(statusCode).WithJson(body); } private HttpPipelineTransport CreateMockTransport(MockResponse[] mockResponses) From 4802ff3571542724a4dc81d6fd22ec2bc4a9283d Mon Sep 17 00:00:00 2001 From: Aigerim Beishenbekova Date: Wed, 18 Dec 2024 15:36:35 +0000 Subject: [PATCH 2/4] fixed comments --- .../Azure.Communication.Common/CHANGELOG.md | 2 +- .../Azure.Communication.Common/README.md | 2 +- .../src/EntraTokenCredential.cs | 20 ++++++---- .../Identity/EntraTokenCredentialTest.cs | 40 +++++++++---------- 4 files changed, 34 insertions(+), 30 deletions(-) diff --git a/sdk/communication/Azure.Communication.Common/CHANGELOG.md b/sdk/communication/Azure.Communication.Common/CHANGELOG.md index 911d0e6bd1e0d..884c23905795b 100644 --- a/sdk/communication/Azure.Communication.Common/CHANGELOG.md +++ b/sdk/communication/Azure.Communication.Common/CHANGELOG.md @@ -3,7 +3,7 @@ ## 1.4.0-beta.1 (Unreleased) ### Features Added -- Introduced support for `Azure.Core.TokenCredential` with `EntraCommunicationTokenCredentialOptions`, enabling Entra users to authorize Communication Services and allowing Azure Communication Services to receive calls from Teams resource accounts. +- Introduced support for `Azure.Core.TokenCredential` with `EntraCommunicationTokenCredentialOptions`, enabling Entra users to authorize Communication Services and allowing an Entra user with a Teams license to use Teams Phone Extensibility features through the Azure Communication Services resource. ### Breaking Changes diff --git a/sdk/communication/Azure.Communication.Common/README.md b/sdk/communication/Azure.Communication.Common/README.md index ec65f02141280..1791e9ee44472 100644 --- a/sdk/communication/Azure.Communication.Common/README.md +++ b/sdk/communication/Azure.Communication.Common/README.md @@ -127,7 +127,7 @@ var credential = new CommunicationTokenCredential(entraTokenCredentialOptions); ``` -The same approach can be used to authorize your Azure Communication Services resource to receive calls from the Teams resource account. +The same approach can be used for authorizing an Entra user with a Teams license to use Teams Phone Extensibility features through your Azure Communication Services resource. This requires providing the `https://auth.msft.communication.azure.com/TeamsExtension.ManageCalls` scope ```C# var options = new InteractiveBrowserCredentialOptions diff --git a/sdk/communication/Azure.Communication.Common/src/EntraTokenCredential.cs b/sdk/communication/Azure.Communication.Common/src/EntraTokenCredential.cs index 4b451ed7ac15b..cec20ec093b93 100644 --- a/sdk/communication/Azure.Communication.Common/src/EntraTokenCredential.cs +++ b/sdk/communication/Azure.Communication.Common/src/EntraTokenCredential.cs @@ -16,8 +16,12 @@ namespace Azure.Communication /// internal sealed class EntraTokenCredential : ICommunicationTokenCredential { - private const string TeamsExtentionsScopePrefix = "https://auth.msft.communication.azure.com/"; - private const string EntraScopePrefix = "https://communication.azure.com/clients/"; + private const string TeamsExtensionScopePrefix = "https://auth.msft.communication.azure.com/"; + private const string ComunicationClientsScopePrefix = "https://communication.azure.com/clients/"; + private const string TeamsExtensionEndpoint = "/access/teamsPhone/:exchangeTeamsAccessToken"; + private const string TeamsExtensionApiVersion = "2025-03-02-preview"; + private const string ComunicationClientsEndpoint = "/access/entra/:exchangeAccessToken"; + private const string ComunicationClientsApiVersion = "2024-04-01-preview"; private HttpPipeline _pipeline; private string _resourceEndpoint; @@ -131,19 +135,19 @@ private RequestUriBuilder CreateRequestUri() { if (_scopes == null || !_scopes.Any()) { - throw new ArgumentException($"Scopes validation failed. Ensure all scopes start with either {TeamsExtentionsScopePrefix} or {EntraScopePrefix}.", nameof(_scopes)); + throw new ArgumentException($"Scopes validation failed. Ensure all scopes start with either {TeamsExtensionScopePrefix} or {ComunicationClientsScopePrefix}.", nameof(_scopes)); } - else if (_scopes.All(item => item.StartsWith(TeamsExtentionsScopePrefix))) + else if (_scopes.All(item => item.StartsWith(TeamsExtensionScopePrefix))) { - return ("/access/teamsPhone/:exchangeTeamsAccessToken", "2025-03-02-preview"); + return (TeamsExtensionEndpoint, TeamsExtensionApiVersion); } - else if (_scopes.All(item => item.StartsWith(EntraScopePrefix))) + else if (_scopes.All(item => item.StartsWith(ComunicationClientsScopePrefix))) { - return ("/access/entra/:exchangeAccessToken", "2024-04-01-preview"); + return (ComunicationClientsEndpoint, ComunicationClientsApiVersion); } else { - throw new ArgumentException($"Scopes validation failed. Ensure all scopes start with either {TeamsExtentionsScopePrefix} or {EntraScopePrefix}.", nameof(_scopes)); + throw new ArgumentException($"Scopes validation failed. Ensure all scopes start with either {TeamsExtensionScopePrefix} or {ComunicationClientsScopePrefix}.", nameof(_scopes)); } } diff --git a/sdk/communication/Azure.Communication.Common/tests/Identity/EntraTokenCredentialTest.cs b/sdk/communication/Azure.Communication.Common/tests/Identity/EntraTokenCredentialTest.cs index ea234866f1725..57a0b8f83d59b 100644 --- a/sdk/communication/Azure.Communication.Common/tests/Identity/EntraTokenCredentialTest.cs +++ b/sdk/communication/Azure.Communication.Common/tests/Identity/EntraTokenCredentialTest.cs @@ -24,8 +24,8 @@ public class EntraTokenCredentialTest protected string TokenResponse = string.Format(TokenResponseTemplate, SampleToken, SampleTokenExpiry); private Mock _mockTokenCredential = null!; - private const string entraScope = "https://communication.azure.com/clients/VoIP"; - private const string opsScope = "https://auth.msft.communication.azure.com/TeamsExtension.ManageCalls"; + private const string communicationClientsScope = "https://communication.azure.com/clients/VoIP"; + private const string teamsExtensionScope = "https://auth.msft.communication.azure.com/TeamsExtension.ManageCalls"; private string _resourceEndpoint = "https://myResource.communication.azure.com"; [SetUp] @@ -39,8 +39,8 @@ public void Setup() } [Test] - [TestCase(new string[] { entraScope }, typeof(ArgumentException))] - [TestCase(new string[] { opsScope }, typeof(ArgumentException))] + [TestCase(new string[] { communicationClientsScope }, typeof(ArgumentException))] + [TestCase(new string[] { teamsExtensionScope }, typeof(ArgumentException))] public void EntraTokenCredential_Init_ThrowsErrorWithNulls(string[] scopes, Type exception) { Assert.Throws(() => new EntraCommunicationTokenCredentialOptions( @@ -60,8 +60,8 @@ public void EntraTokenCredential_Init_ThrowsErrorWithNulls(string[] scopes, Type } [Test] - [TestCase(new string[] { entraScope }, typeof(ArgumentException))] - [TestCase(new string[] { opsScope }, typeof(ArgumentException))] + [TestCase(new string[] { communicationClientsScope }, typeof(ArgumentException))] + [TestCase(new string[] { teamsExtensionScope }, typeof(ArgumentException))] public void EntraTokenCredential_Init_FetchesTokenImmediately(string[] scopes, Type exception) { // Arrange @@ -74,8 +74,8 @@ public void EntraTokenCredential_Init_FetchesTokenImmediately(string[] scopes, T } [Test] - [TestCase("/access/entra/:exchangeAccessToken", new string[] { entraScope }, typeof(ArgumentException))] - [TestCase("/access/teamsPhone/:exchangeTeamsAccessToken", new string[] { opsScope }, typeof(ArgumentException))] + [TestCase("/access/entra/:exchangeAccessToken", new string[] { communicationClientsScope }, typeof(ArgumentException))] + [TestCase("/access/teamsPhone/:exchangeTeamsAccessToken", new string[] { teamsExtensionScope }, typeof(ArgumentException))] public async Task EntraTokenCredential_GetToken_ReturnsToken(string expectedEndpoint, string[] scopes, Type exception) { // Arrange @@ -95,8 +95,8 @@ public async Task EntraTokenCredential_GetToken_ReturnsToken(string expectedEndp } [Test] - [TestCase(new string[] { entraScope }, typeof(ArgumentException))] - [TestCase(new string[] { opsScope }, typeof(ArgumentException))] + [TestCase(new string[] { communicationClientsScope }, typeof(ArgumentException))] + [TestCase(new string[] { teamsExtensionScope }, typeof(ArgumentException))] public async Task EntraTokenCredential_GetToken_InternalEntraTokenChangeInvalidatesCachedToken(string[] scopes, Type exception) { // Arrange @@ -121,8 +121,8 @@ public async Task EntraTokenCredential_GetToken_InternalEntraTokenChangeInvalida } [Test] - [TestCase(new string[] { entraScope }, typeof(ArgumentException))] - [TestCase(new string[] { opsScope }, typeof(ArgumentException))] + [TestCase(new string[] { communicationClientsScope }, typeof(ArgumentException))] + [TestCase(new string[] { teamsExtensionScope }, typeof(ArgumentException))] public async Task EntraTokenCredential_GetToken_MultipleCallsReturnsCachedToken(string[] scopes, Type exception) { // Arrange @@ -142,8 +142,8 @@ public async Task EntraTokenCredential_GetToken_MultipleCallsReturnsCachedToken( } [Test] - [TestCase(new string[] { entraScope }, typeof(ArgumentException))] - [TestCase(new string[] { opsScope }, typeof(ArgumentException))] + [TestCase(new string[] { communicationClientsScope }, typeof(ArgumentException))] + [TestCase(new string[] { teamsExtensionScope }, typeof(ArgumentException))] public void EntraTokenCredential_GetToken_ThrowsFailedResponse(string[] scopes, Type exception) { // Arrange @@ -162,8 +162,8 @@ public void EntraTokenCredential_GetToken_ThrowsFailedResponse(string[] scopes, } [Test] - [TestCase(new string[] { entraScope }, typeof(ArgumentException))] - [TestCase(new string[] { opsScope }, typeof(ArgumentException))] + [TestCase(new string[] { communicationClientsScope }, typeof(ArgumentException))] + [TestCase(new string[] { teamsExtensionScope }, typeof(ArgumentException))] public void EntraTokenCredential_GetToken_ThrowsInvalidJson(string[] scopes, Type exception) { // Arrange @@ -183,8 +183,8 @@ public void EntraTokenCredential_GetToken_ThrowsInvalidJson(string[] scopes, Typ } [Test] - [TestCase(new string[] { entraScope }, typeof(ArgumentException))] - [TestCase(new string[] { opsScope }, typeof(ArgumentException))] + [TestCase(new string[] { communicationClientsScope }, typeof(ArgumentException))] + [TestCase(new string[] { teamsExtensionScope }, typeof(ArgumentException))] public void EntraTokenCredential_GetToken_RetriesThreeTimesOnTransientError(string[] scopes, Type exception) { // Arrange @@ -212,8 +212,8 @@ public void EntraTokenCredential_GetToken_RetriesThreeTimesOnTransientError(stri } [Test] - [TestCase(new string[] { entraScope, opsScope }, typeof(ArgumentException))] - [TestCase(new string[] { opsScope, entraScope }, typeof(ArgumentException))] + [TestCase(new string[] { communicationClientsScope, teamsExtensionScope }, typeof(ArgumentException))] + [TestCase(new string[] { teamsExtensionScope, communicationClientsScope }, typeof(ArgumentException))] [TestCase(new string[] { "invalidScope" }, typeof(ArgumentException))] [TestCase(new string[] { "" }, typeof(ArgumentException))] [TestCase(new string[] { }, typeof(ArgumentException))] From 0c1f6373ba807c8830891c66555445787f5594ae Mon Sep 17 00:00:00 2001 From: Aigerim Beishenbekova Date: Wed, 18 Dec 2024 16:07:48 +0000 Subject: [PATCH 3/4] reverted back optional scope param --- .../Azure.Communication.Common/README.md | 25 ++++---- ...ntraCommunicationTokenCredentialOptions.cs | 9 ++- .../Identity/EntraTokenCredentialTest.cs | 60 +++++++++++++++---- 3 files changed, 67 insertions(+), 27 deletions(-) diff --git a/sdk/communication/Azure.Communication.Common/README.md b/sdk/communication/Azure.Communication.Common/README.md index 1791e9ee44472..b36c8e354c648 100644 --- a/sdk/communication/Azure.Communication.Common/README.md +++ b/sdk/communication/Azure.Communication.Common/README.md @@ -105,23 +105,23 @@ using var tokenCredential = new CommunicationTokenCredential( ### Create a credential with a token credential capable of obtaining an Entra user token For scenarios where an Entra user can be used with Communication Services, you need to initialize any implementation of [Azure.Core.TokenCredential](https://docs.microsoft.com/dotnet/api/azure.core.tokencredential?view=azure-dotnet) and provide it to the ``EntraCommunicationTokenCredentialOptions``. -Along with this, you must provide the URI of the Azure Communication Services resource and the scopes required for the Entra user token. These scopes determine the permissions granted to the token: - +Along with this, you must provide the URI of the Azure Communication Services resource and the scopes required for the Entra user token. These scopes determine the permissions granted to the token. +If the scopes are not provided, by default, it sets the scopes to `https://communication.azure.com/clients/.default`. ```C# var options = new InteractiveBrowserCredentialOptions { TenantId = "", ClientId = "", - RedirectUri = new Uri(""), - AuthorityHost = new Uri("https://login.microsoftonline.com/") + RedirectUri = new Uri("") }; var entraTokenCredential = new InteractiveBrowserCredential(options); var entraTokenCredentialOptions = new EntraCommunicationTokenCredentialOptions( resourceEndpoint: "https://.communication.azure.com", - entraTokenCredential: entraTokenCredential, - scopes: new[] { "https://communication.azure.com/clients/VoIP" } -); + entraTokenCredential: entraTokenCredential) + { + Scopes = new[] { "https://communication.azure.com/clients/VoIP" } + }; var credential = new CommunicationTokenCredential(entraTokenCredentialOptions); @@ -134,16 +134,17 @@ var options = new InteractiveBrowserCredentialOptions { TenantId = "", ClientId = "", - RedirectUri = new Uri(""), - AuthorityHost = new Uri("https://login.microsoftonline.com/") + RedirectUri = new Uri("") }; var entraTokenCredential = new InteractiveBrowserCredential(options); var entraTokenCredentialOptions = new EntraCommunicationTokenCredentialOptions( resourceEndpoint: "https://.communication.azure.com", - entraTokenCredential: entraTokenCredential, - scopes: new[] { "https://auth.msft.communication.azure.com/TeamsExtension.ManageCalls" } -); + entraTokenCredential: entraTokenCredential) + ) + { + Scopes = new[] { "https://auth.msft.communication.azure.com/TeamsExtension.ManageCalls" } + }; var credential = new CommunicationTokenCredential(entraTokenCredentialOptions); diff --git a/sdk/communication/Azure.Communication.Common/src/EntraCommunicationTokenCredentialOptions.cs b/sdk/communication/Azure.Communication.Common/src/EntraCommunicationTokenCredentialOptions.cs index 3863072ffcb67..034cd1f03d827 100644 --- a/sdk/communication/Azure.Communication.Common/src/EntraCommunicationTokenCredentialOptions.cs +++ b/sdk/communication/Azure.Communication.Common/src/EntraCommunicationTokenCredentialOptions.cs @@ -11,6 +11,7 @@ namespace Azure.Communication /// public class EntraCommunicationTokenCredentialOptions { + private static string[] DefaultScopes = { "https://communication.azure.com/clients/.default" }; /// /// The URI of the Azure Communication Services resource. /// @@ -24,25 +25,23 @@ public class EntraCommunicationTokenCredentialOptions /// /// The scopes required for the Entra user token. These scopes determine the permissions granted to the token. For example, ["https://communication.azure.com/clients/VoIP"]. /// - public string[] Scopes { get; } + public string[] Scopes { get; set; } /// /// Initializes a new instance of . /// /// The URI of the Azure Communication Services resource.For example, https://myResource.communication.azure.com. /// The credential capable of fetching an Entra user token. - /// The scopes required for the Entra user token. public EntraCommunicationTokenCredentialOptions( string resourceEndpoint, - TokenCredential entraTokenCredential, - string[] scopes) + TokenCredential entraTokenCredential) { Argument.AssertNotNullOrEmpty(resourceEndpoint, nameof(resourceEndpoint)); Argument.AssertNotNull(entraTokenCredential, nameof(entraTokenCredential)); this.ResourceEndpoint = resourceEndpoint; this.TokenCredential = entraTokenCredential; - this.Scopes = scopes; + this.Scopes = DefaultScopes; } } } diff --git a/sdk/communication/Azure.Communication.Common/tests/Identity/EntraTokenCredentialTest.cs b/sdk/communication/Azure.Communication.Common/tests/Identity/EntraTokenCredentialTest.cs index 57a0b8f83d59b..ce0bf12825b91 100644 --- a/sdk/communication/Azure.Communication.Common/tests/Identity/EntraTokenCredentialTest.cs +++ b/sdk/communication/Azure.Communication.Common/tests/Identity/EntraTokenCredentialTest.cs @@ -24,7 +24,9 @@ public class EntraTokenCredentialTest protected string TokenResponse = string.Format(TokenResponseTemplate, SampleToken, SampleTokenExpiry); private Mock _mockTokenCredential = null!; + private const string comunicationClientsEndpoint = "/access/entra/:exchangeAccessToken"; private const string communicationClientsScope = "https://communication.azure.com/clients/VoIP"; + private const string teamsExtensionEndpoint = "/access/teamsPhone/:exchangeTeamsAccessToken"; private const string teamsExtensionScope = "https://auth.msft.communication.azure.com/TeamsExtension.ManageCalls"; private string _resourceEndpoint = "https://myResource.communication.azure.com"; @@ -45,18 +47,34 @@ public void EntraTokenCredential_Init_ThrowsErrorWithNulls(string[] scopes, Type { Assert.Throws(() => new EntraCommunicationTokenCredentialOptions( null, - _mockTokenCredential.Object, - scopes)); + _mockTokenCredential.Object) + { + Scopes = scopes + }); Assert.Throws(() => new EntraCommunicationTokenCredentialOptions( "", - _mockTokenCredential.Object, - scopes)); + _mockTokenCredential.Object) + { + Scopes = scopes + }); Assert.Throws(() => new EntraCommunicationTokenCredentialOptions( _resourceEndpoint, - null, - scopes)); + null) + { + Scopes = scopes + }); + } + + [Test] + public void EntraTokenCredential_InitWithoutScopes_InitsWithDefaultScope() + { + var credential = new EntraCommunicationTokenCredentialOptions( + _resourceEndpoint, + _mockTokenCredential.Object); + var scopes = new[] { "https://communication.azure.com/clients/.default" }; + Assert.AreEqual(credential.Scopes, scopes); } [Test] @@ -74,8 +92,8 @@ public void EntraTokenCredential_Init_FetchesTokenImmediately(string[] scopes, T } [Test] - [TestCase("/access/entra/:exchangeAccessToken", new string[] { communicationClientsScope }, typeof(ArgumentException))] - [TestCase("/access/teamsPhone/:exchangeTeamsAccessToken", new string[] { teamsExtensionScope }, typeof(ArgumentException))] + [TestCase(comunicationClientsEndpoint, new string[] { communicationClientsScope }, typeof(ArgumentException))] + [TestCase(teamsExtensionEndpoint, new string[] { teamsExtensionScope }, typeof(ArgumentException))] public async Task EntraTokenCredential_GetToken_ReturnsToken(string expectedEndpoint, string[] scopes, Type exception) { // Arrange @@ -90,7 +108,26 @@ public async Task EntraTokenCredential_GetToken_ReturnsToken(string expectedEndp // Assert Assert.AreEqual(SampleToken, token.Token); Assert.AreEqual(token.ExpiresOn, expiryTime); - Assert.AreEqual(expectedEndpoint, mockTransport.SingleRequest.Uri.Path, expectedEndpoint); + Assert.AreEqual(expectedEndpoint, mockTransport.SingleRequest.Uri.Path); + _mockTokenCredential.Verify(tc => tc.GetTokenAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Test] + public async Task EntraTokenCredential_InitWithoutScopes_ReturnsComunicationClientsToken() + { + // Arrange + var expiryTime = DateTimeOffset.Parse(SampleTokenExpiry, null, System.Globalization.DateTimeStyles.RoundtripKind); + var options = new EntraCommunicationTokenCredentialOptions(_resourceEndpoint, _mockTokenCredential.Object); + var mockTransport = (MockTransport)CreateMockTransport(new[] { CreateMockResponse(200, TokenResponse) }); + var entraTokenCredential = new EntraTokenCredential(options, mockTransport); + + // Act + var token = await entraTokenCredential.GetTokenAsync(CancellationToken.None); + + // Assert + Assert.AreEqual(SampleToken, token.Token); + Assert.AreEqual(token.ExpiresOn, expiryTime); + Assert.AreEqual(comunicationClientsEndpoint, mockTransport.SingleRequest.Uri.Path); _mockTokenCredential.Verify(tc => tc.GetTokenAsync(It.IsAny(), It.IsAny()), Times.Once); } @@ -236,7 +273,10 @@ public void EntraTokenCredential_GetToken_ThrowsForInvalidScopes(string[] scopes private EntraCommunicationTokenCredentialOptions CreateEntraTokenCredentialOptions(string[] scopes) { - return new EntraCommunicationTokenCredentialOptions(_resourceEndpoint, _mockTokenCredential.Object, scopes); + return new EntraCommunicationTokenCredentialOptions(_resourceEndpoint, _mockTokenCredential.Object) + { + Scopes = scopes + }; } private MockResponse CreateMockResponse(int statusCode, string body) From 1c9ab5a45fd83f67df4fd5e6f25a33ab53ff2f0f Mon Sep 17 00:00:00 2001 From: Aigerim Beishenbekova Date: Tue, 31 Dec 2024 11:36:47 +0000 Subject: [PATCH 4/4] fixed tests --- .../src/EntraTokenCredential.cs | 2 +- .../Identity/EntraTokenCredentialTest.cs | 83 ++++++++++--------- 2 files changed, 43 insertions(+), 42 deletions(-) diff --git a/sdk/communication/Azure.Communication.Common/src/EntraTokenCredential.cs b/sdk/communication/Azure.Communication.Common/src/EntraTokenCredential.cs index cec20ec093b93..90567bb6e5246 100644 --- a/sdk/communication/Azure.Communication.Common/src/EntraTokenCredential.cs +++ b/sdk/communication/Azure.Communication.Common/src/EntraTokenCredential.cs @@ -18,7 +18,7 @@ internal sealed class EntraTokenCredential : ICommunicationTokenCredential { private const string TeamsExtensionScopePrefix = "https://auth.msft.communication.azure.com/"; private const string ComunicationClientsScopePrefix = "https://communication.azure.com/clients/"; - private const string TeamsExtensionEndpoint = "/access/teamsPhone/:exchangeTeamsAccessToken"; + private const string TeamsExtensionEndpoint = "/access/teamsPhone/:exchangeAccessToken"; private const string TeamsExtensionApiVersion = "2025-03-02-preview"; private const string ComunicationClientsEndpoint = "/access/entra/:exchangeAccessToken"; private const string ComunicationClientsApiVersion = "2024-04-01-preview"; diff --git a/sdk/communication/Azure.Communication.Common/tests/Identity/EntraTokenCredentialTest.cs b/sdk/communication/Azure.Communication.Common/tests/Identity/EntraTokenCredentialTest.cs index ce0bf12825b91..f07ef406b4f68 100644 --- a/sdk/communication/Azure.Communication.Common/tests/Identity/EntraTokenCredentialTest.cs +++ b/sdk/communication/Azure.Communication.Common/tests/Identity/EntraTokenCredentialTest.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Azure.Core; @@ -26,10 +27,24 @@ public class EntraTokenCredentialTest private Mock _mockTokenCredential = null!; private const string comunicationClientsEndpoint = "/access/entra/:exchangeAccessToken"; private const string communicationClientsScope = "https://communication.azure.com/clients/VoIP"; - private const string teamsExtensionEndpoint = "/access/teamsPhone/:exchangeTeamsAccessToken"; + private const string teamsExtensionEndpoint = "/access/teamsPhone/:exchangeAccessToken"; private const string teamsExtensionScope = "https://auth.msft.communication.azure.com/TeamsExtension.ManageCalls"; private string _resourceEndpoint = "https://myResource.communication.azure.com"; + private static readonly object[] validScopes = + { + new object[] { new string[] { communicationClientsScope }}, + new object[] { new string[] { teamsExtensionScope } } + }; + private static readonly object[] invalidScopes = + { + new object[] { new string[] { communicationClientsScope, teamsExtensionScope } }, + new object[] { new string[] { teamsExtensionScope, communicationClientsScope } }, + new object[] { new string[] { "invalidScope" } }, + new object[] { new string[] { "" } }, + new object[] { new string[] { } } + }; + [SetUp] public void Setup() { @@ -40,10 +55,8 @@ public void Setup() .ReturnsAsync(new AccessToken(SampleToken, expiryTime)); } - [Test] - [TestCase(new string[] { communicationClientsScope }, typeof(ArgumentException))] - [TestCase(new string[] { teamsExtensionScope }, typeof(ArgumentException))] - public void EntraTokenCredential_Init_ThrowsErrorWithNulls(string[] scopes, Type exception) + [Test, TestCaseSource(nameof(validScopes))] + public void EntraTokenCredential_Init_ThrowsErrorWithNulls(string[] scopes) { Assert.Throws(() => new EntraCommunicationTokenCredentialOptions( null, @@ -77,10 +90,8 @@ public void EntraTokenCredential_InitWithoutScopes_InitsWithDefaultScope() Assert.AreEqual(credential.Scopes, scopes); } - [Test] - [TestCase(new string[] { communicationClientsScope }, typeof(ArgumentException))] - [TestCase(new string[] { teamsExtensionScope }, typeof(ArgumentException))] - public void EntraTokenCredential_Init_FetchesTokenImmediately(string[] scopes, Type exception) + [Test, TestCaseSource(nameof(validScopes))] + public void EntraTokenCredential_Init_FetchesTokenImmediately(string[] scopes) { // Arrange var expiryTime = DateTimeOffset.Parse(SampleTokenExpiry, null, System.Globalization.DateTimeStyles.RoundtripKind); @@ -91,10 +102,8 @@ public void EntraTokenCredential_Init_FetchesTokenImmediately(string[] scopes, T _mockTokenCredential.Verify(tc => tc.GetTokenAsync(It.IsAny(), It.IsAny()), Times.Once); } - [Test] - [TestCase(comunicationClientsEndpoint, new string[] { communicationClientsScope }, typeof(ArgumentException))] - [TestCase(teamsExtensionEndpoint, new string[] { teamsExtensionScope }, typeof(ArgumentException))] - public async Task EntraTokenCredential_GetToken_ReturnsToken(string expectedEndpoint, string[] scopes, Type exception) + [Test, TestCaseSource(nameof(validScopes))] + public async Task EntraTokenCredential_GetToken_ReturnsToken(string[] scopes) { // Arrange var expiryTime = DateTimeOffset.Parse(SampleTokenExpiry, null, System.Globalization.DateTimeStyles.RoundtripKind); @@ -108,7 +117,14 @@ public async Task EntraTokenCredential_GetToken_ReturnsToken(string expectedEndp // Assert Assert.AreEqual(SampleToken, token.Token); Assert.AreEqual(token.ExpiresOn, expiryTime); - Assert.AreEqual(expectedEndpoint, mockTransport.SingleRequest.Uri.Path); + if (scopes.Contains(teamsExtensionScope)) + { + Assert.AreEqual(teamsExtensionEndpoint, mockTransport.SingleRequest.Uri.Path); + } + else + { + Assert.AreEqual(comunicationClientsEndpoint, mockTransport.SingleRequest.Uri.Path); + } _mockTokenCredential.Verify(tc => tc.GetTokenAsync(It.IsAny(), It.IsAny()), Times.Once); } @@ -131,10 +147,8 @@ public async Task EntraTokenCredential_InitWithoutScopes_ReturnsComunicationClie _mockTokenCredential.Verify(tc => tc.GetTokenAsync(It.IsAny(), It.IsAny()), Times.Once); } - [Test] - [TestCase(new string[] { communicationClientsScope }, typeof(ArgumentException))] - [TestCase(new string[] { teamsExtensionScope }, typeof(ArgumentException))] - public async Task EntraTokenCredential_GetToken_InternalEntraTokenChangeInvalidatesCachedToken(string[] scopes, Type exception) + [Test, TestCaseSource(nameof(validScopes))] + public async Task EntraTokenCredential_GetToken_InternalEntraTokenChangeInvalidatesCachedToken(string[] scopes) { // Arrange var expiryTime = DateTimeOffset.Parse(SampleTokenExpiry, null, System.Globalization.DateTimeStyles.RoundtripKind); @@ -157,10 +171,8 @@ public async Task EntraTokenCredential_GetToken_InternalEntraTokenChangeInvalida _mockTokenCredential.Verify(tc => tc.GetTokenAsync(It.IsAny(), It.IsAny()), Times.Exactly(2)); } - [Test] - [TestCase(new string[] { communicationClientsScope }, typeof(ArgumentException))] - [TestCase(new string[] { teamsExtensionScope }, typeof(ArgumentException))] - public async Task EntraTokenCredential_GetToken_MultipleCallsReturnsCachedToken(string[] scopes, Type exception) + [Test, TestCaseSource(nameof(validScopes))] + public async Task EntraTokenCredential_GetToken_MultipleCallsReturnsCachedToken(string[] scopes) { // Arrange var options = CreateEntraTokenCredentialOptions(scopes); @@ -178,10 +190,8 @@ public async Task EntraTokenCredential_GetToken_MultipleCallsReturnsCachedToken( _mockTokenCredential.Verify(tc => tc.GetTokenAsync(It.IsAny(), It.IsAny()), Times.Once); } - [Test] - [TestCase(new string[] { communicationClientsScope }, typeof(ArgumentException))] - [TestCase(new string[] { teamsExtensionScope }, typeof(ArgumentException))] - public void EntraTokenCredential_GetToken_ThrowsFailedResponse(string[] scopes, Type exception) + [Test, TestCaseSource(nameof(validScopes))] + public void EntraTokenCredential_GetToken_ThrowsFailedResponse(string[] scopes) { // Arrange var options = CreateEntraTokenCredentialOptions(scopes); @@ -198,10 +208,8 @@ public void EntraTokenCredential_GetToken_ThrowsFailedResponse(string[] scopes, Assert.ThrowsAsync(async () => await entraTokenCredential.GetTokenAsync(CancellationToken.None)); } - [Test] - [TestCase(new string[] { communicationClientsScope }, typeof(ArgumentException))] - [TestCase(new string[] { teamsExtensionScope }, typeof(ArgumentException))] - public void EntraTokenCredential_GetToken_ThrowsInvalidJson(string[] scopes, Type exception) + [Test, TestCaseSource(nameof(validScopes))] + public void EntraTokenCredential_GetToken_ThrowsInvalidJson(string[] scopes) { // Arrange var options = CreateEntraTokenCredentialOptions(scopes); @@ -219,10 +227,8 @@ public void EntraTokenCredential_GetToken_ThrowsInvalidJson(string[] scopes, Typ Assert.ThrowsAsync(async () => await entraTokenCredential.GetTokenAsync(CancellationToken.None)); } - [Test] - [TestCase(new string[] { communicationClientsScope }, typeof(ArgumentException))] - [TestCase(new string[] { teamsExtensionScope }, typeof(ArgumentException))] - public void EntraTokenCredential_GetToken_RetriesThreeTimesOnTransientError(string[] scopes, Type exception) + [Test, TestCaseSource(nameof(validScopes))] + public void EntraTokenCredential_GetToken_RetriesThreeTimesOnTransientError(string[] scopes) { // Arrange var options = CreateEntraTokenCredentialOptions(scopes); @@ -248,13 +254,8 @@ public void EntraTokenCredential_GetToken_RetriesThreeTimesOnTransientError(stri StringAssert.Contains(lastRetryErrorMessage, ex?.Message); } - [Test] - [TestCase(new string[] { communicationClientsScope, teamsExtensionScope }, typeof(ArgumentException))] - [TestCase(new string[] { teamsExtensionScope, communicationClientsScope }, typeof(ArgumentException))] - [TestCase(new string[] { "invalidScope" }, typeof(ArgumentException))] - [TestCase(new string[] { "" }, typeof(ArgumentException))] - [TestCase(new string[] { }, typeof(ArgumentException))] - public void EntraTokenCredential_GetToken_ThrowsForInvalidScopes(string[] scopes, Type exception) + [Test, TestCaseSource(nameof(invalidScopes))] + public void EntraTokenCredential_GetToken_ThrowsForInvalidScopes(string[] scopes) { // Arrange var options = CreateEntraTokenCredentialOptions(scopes);