Skip to content

Commit cad9b26

Browse files
msJinLeiCopilot
andauthored
Add Interface to Send Auth Info to Telemetry (#421)
* Add test case of authentication telemetry * Add Interface to Send Auth Info to Telemetry * Address review comments * Address review comments * Address review comments * Add CmdletContext * Improve the interface * Revise the comments * Fix the test * Forget to commit the change * Fix concurrency issue Fix concurrency issue and add test cases for authentication telemetry * Address review comments * Address review comments Update src/Authentication.Abstractions/Interfaces/IAuthenticationFactory.cs Co-authored-by: Copilot <[email protected]> * Address review comments Remove variables of error counting in IAzurePSCmdletDataVault * Update src/Authentication.Abstractions/Interfaces/IClientFactory.cs Co-authored-by: Copilot <[email protected]> * Update src/Authentication.Abstractions/Interfaces/IAuthenticationFactory.cs Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]>
1 parent 1062bcb commit cad9b26

28 files changed

+889
-31
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
// ----------------------------------------------------------------------------------
2+
//
3+
// Copyright Microsoft Corporation
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
// ----------------------------------------------------------------------------------
14+
using Microsoft.Azure.Commands.Common.Authentication.Abstractions;
15+
using Microsoft.Azure.Commands.Common.Authentication.Abstractions.Interfaces;
16+
17+
using Moq;
18+
19+
using System;
20+
using System.Collections.Generic;
21+
using System.Linq;
22+
using System.Threading.Tasks;
23+
24+
using Xunit;
25+
26+
namespace Authentication.Abstractions.Test
27+
{
28+
public class AuthenticationTelemetryTest
29+
{
30+
// Theory for PushTelemetryRecord tests
31+
[Theory]
32+
[InlineData(true, true, true, 1, 1, 0)] // Valid context, valid record -> success
33+
[InlineData(false, true, false, 0, 0, 1)] // Invalid context, valid record -> failure
34+
[InlineData(true, false, false, 0, 0, 1)] // Valid context, null record -> failure
35+
[InlineData(null, true, false, 0, 0, 1)] // Null context, valid record -> failure
36+
public void PushTelemetryRecord_Tests(bool? isContextValid, bool hasRecord, bool expectedResult,
37+
int expectedKeysCurrent, int expectedKeysAll, int expectedEmptyCount)
38+
{
39+
// Arrange
40+
var telemetry = new AuthenticationTelemetry();
41+
ICmdletContext context = null;
42+
AuthTelemetryRecord record = hasRecord ? new AuthTelemetryRecord() : null;
43+
44+
if (isContextValid.HasValue)
45+
{
46+
var mockContext = new Mock<ICmdletContext>();
47+
mockContext.Setup(c => c.IsValid).Returns(isContextValid.Value);
48+
if (isContextValid.Value)
49+
{
50+
mockContext.Setup(c => c.CmdletId).Returns("TestCmdlet");
51+
}
52+
context = mockContext.Object;
53+
}
54+
55+
// Act
56+
var result = telemetry.PushDataRecord(context, record);
57+
58+
// Assert
59+
Assert.Equal(expectedResult, result);
60+
}
61+
62+
// Test data for PopTelemetryRecord tests
63+
public static IEnumerable<object[]> PopTelemetryRecordTestData =>
64+
new List<object[]>
65+
{
66+
// Parameters: isContextValid, cmdletId, pushBeforePop, expectedNotNull, expectedKeysCount, expectedAllCount, expectedEmptyCount, expectedKeyNotFoundCount
67+
new object[] { true, "TestCmdlet", true, true, 0, 1, 0, 0 }, // Valid context with existing record
68+
new object[] { null, null, false, false, 0, 0, 1, 0 }, // Null context
69+
new object[] { false, null, false, false, 0, 0, 1, 0 }, // Invalid context
70+
new object[] { true, "TestCmdlet", false, false, 0, 0, 0, 1 } // Valid context with non-existent key
71+
};
72+
73+
[Theory]
74+
[MemberData(nameof(PopTelemetryRecordTestData))]
75+
public void PopTelemetryRecord_Tests(bool? isContextValid, string cmdletId, bool pushBeforePop,
76+
bool expectedNotNull, int expectedKeysCount, int expectedAllCount,
77+
int expectedEmptyCount, int expectedKeyNotFoundCount)
78+
{
79+
// Arrange
80+
var telemetry = new AuthenticationTelemetry();
81+
ICmdletContext context = null;
82+
83+
if (isContextValid.HasValue)
84+
{
85+
var mockContext = new Mock<ICmdletContext>();
86+
mockContext.Setup(c => c.IsValid).Returns(isContextValid.Value);
87+
if (!string.IsNullOrEmpty(cmdletId))
88+
{
89+
mockContext.Setup(c => c.CmdletId).Returns(cmdletId);
90+
}
91+
context = mockContext.Object;
92+
}
93+
94+
// Push a record first if needed
95+
if (pushBeforePop && context != null)
96+
{
97+
var record = new AuthTelemetryRecord { TokenCredentialName = "TestCredential" };
98+
Assert.True(telemetry.PushDataRecord(context, record));
99+
}
100+
101+
// Act
102+
var result = telemetry.PopDataRecords(context);
103+
104+
// Assert
105+
Assert.Equal(expectedNotNull, result != null);
106+
if (expectedNotNull)
107+
{
108+
Assert.Single(result);
109+
Assert.Equal("TestCredential", result.FirstOrDefault()?.TokenCredentialName);
110+
}
111+
}
112+
113+
// Test data for GetTelemetryRecord tests
114+
public static IEnumerable<object[]> GetTelemetryRecordTestData =>
115+
new List<object[]>
116+
{
117+
// Parameters: isContextValid, cmdletId, recordCount, expectedNotNull
118+
new object[] { true, "TestCmdlet", 1, true }, // Valid context with single record
119+
new object[] { true, "TestCmdlet", 3, true }, // Valid context with multiple records
120+
new object[] { null, null, 0, false }, // Null context
121+
new object[] { false, null, 0, false }, // Invalid context
122+
new object[] { true, "TestCmdlet", 0, false } // Valid context with no records
123+
};
124+
125+
[Theory]
126+
[MemberData(nameof(GetTelemetryRecordTestData))]
127+
public void GetTelemetryRecord_Tests(bool? isContextValid, string cmdletId, int recordCount,
128+
bool expectedNotNull)
129+
{
130+
// Arrange
131+
var telemetry = new AuthenticationTelemetry();
132+
ICmdletContext context = null;
133+
134+
if (isContextValid.HasValue)
135+
{
136+
var mockContext = new Mock<ICmdletContext>();
137+
mockContext.Setup(c => c.IsValid).Returns(isContextValid.Value);
138+
if (!string.IsNullOrEmpty(cmdletId))
139+
{
140+
mockContext.Setup(c => c.CmdletId).Returns(cmdletId);
141+
}
142+
context = mockContext.Object;
143+
}
144+
145+
// Push records if needed
146+
for (int i = 0; i < recordCount; i++)
147+
{
148+
var record = new AuthTelemetryRecord { TokenCredentialName = $"TestCredential{i}" };
149+
Assert.True(telemetry.PushDataRecord(context, record));
150+
}
151+
152+
// Act
153+
var result = telemetry.GetTelemetryRecord(context);
154+
155+
// Assert
156+
Assert.Equal(expectedNotNull, result != null);
157+
158+
if (expectedNotNull)
159+
{
160+
// Verify the AuthenticationTelemetryData contains our records
161+
Assert.NotNull(result.Primary);
162+
Assert.Equal("TestCredential0", result.Primary.TokenCredentialName);
163+
164+
if (recordCount > 1)
165+
{
166+
Assert.NotEmpty(result.Secondary);
167+
Assert.Equal(recordCount - 1, result.Secondary.Count);
168+
169+
// Verify each record in the tail
170+
for (int i = 1; i < recordCount; i++)
171+
{
172+
Assert.Equal($"TestCredential{i}", result.Secondary[i - 1].TokenCredentialName);
173+
}
174+
}
175+
else
176+
{
177+
Assert.Empty(result.Secondary);
178+
}
179+
}
180+
}
181+
[Fact]
182+
public void TelemetryRecord_ConcurrentTests()
183+
{
184+
// Arrange
185+
var telemetry = new AuthenticationTelemetry();
186+
const int pusherThreadCount = 10;
187+
188+
// Create delegate to push records and get telemetry data
189+
Func<ICmdletContext, AuthenticationTelemetryData> PushAndGetFromMultipleThreads = (context) =>
190+
{
191+
// Create tasks for pushing records (10 threads)
192+
var pushTasks = new Task[pusherThreadCount];
193+
for (int t = 0; t < pusherThreadCount; t++)
194+
{
195+
var threadId = t;
196+
pushTasks[t] = Task.Run(() =>
197+
{
198+
// Each thread pushes one unique record
199+
var record = new AuthTelemetryRecord { TokenCredentialName = $"TestCredential-{context.CmdletId}-{threadId}" };
200+
Assert.True(telemetry.PushDataRecord(context, record));
201+
});
202+
}
203+
Task.WaitAll(pushTasks); // Wait for all push tasks to complete
204+
return telemetry.GetTelemetryRecord(context);
205+
};
206+
207+
// Create two contexts
208+
var mockContext1 = new Mock<ICmdletContext>();
209+
mockContext1.Setup(c => c.IsValid).Returns(true);
210+
mockContext1.Setup(c => c.CmdletId).Returns("TestCmdlet1");
211+
var context1 = mockContext1.Object;
212+
213+
var mockContext2 = new Mock<ICmdletContext>();
214+
mockContext2.Setup(c => c.IsValid).Returns(true);
215+
mockContext2.Setup(c => c.CmdletId).Returns("TestCmdlet2");
216+
var context2 = mockContext2.Object;
217+
218+
// Act
219+
// Run tasks in parallel
220+
var task1 = Task<AuthenticationTelemetryData>.Run(() => PushAndGetFromMultipleThreads(context1));
221+
var task2 = Task<AuthenticationTelemetryData>.Run(() => PushAndGetFromMultipleThreads(context2));
222+
223+
// Wait for both tasks to complete
224+
Task.WaitAll(task1, task2);
225+
226+
// Get results
227+
var results1 = task1.Result;
228+
var results2 = task2.Result;
229+
230+
// Assert
231+
// Check that we have results from both contexts
232+
Assert.NotNull(results1);
233+
Assert.True(results1.Primary?.TokenCredentialName.StartsWith("TestCredential-TestCmdlet1"));
234+
Assert.Equal(9, results1.Secondary?.Count);
235+
Assert.NotNull(results2);
236+
Assert.True(results2.Primary?.TokenCredentialName.StartsWith("TestCredential-TestCmdlet2"));
237+
Assert.Equal(9, results2.Secondary?.Count);
238+
239+
// Verify all records were retrieved (nothing left)
240+
Assert.Null(telemetry.GetTelemetryRecord(context1));
241+
Assert.Null(telemetry.GetTelemetryRecord(context2));
242+
}
243+
}
244+
}

src/Authentication.Abstractions.Test/AzureSessionTest.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
using System.Collections.Generic;
2121
using System.Diagnostics;
2222
using System.Linq;
23-
using System.Threading;
2423
using System.Threading.Tasks;
2524

2625
using Xunit;
@@ -44,7 +43,7 @@ public AzureSessionTest()
4443
{
4544
try
4645
{
47-
IAzureSession oldSession = AzureSession.Instance;
46+
oldSession = AzureSession.Instance;
4847

4948
}
5049
catch (Exception)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// ----------------------------------------------------------------------------------
2+
//
3+
// Copyright Microsoft Corporation
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
// ----------------------------------------------------------------------------------
14+
15+
using Newtonsoft.Json;
16+
17+
using System;
18+
using System.Collections.Concurrent;
19+
using System.Collections.Generic;
20+
21+
namespace Microsoft.Azure.Commands.Common.Authentication.Abstractions
22+
{
23+
/// <summary>
24+
/// Represents a telemetry record for authentication.
25+
/// </summary>
26+
public class AuthTelemetryRecord : IAuthTelemetryRecord
27+
{
28+
/// <summary>
29+
/// Gets or sets the class name of the TokenCredential, which stands for the authentication method.
30+
/// </summary>
31+
public string TokenCredentialName { get; set; }
32+
33+
/// <summary>
34+
/// Gets or sets a value indicating whether the authentication process succeeded or not.
35+
/// </summary>
36+
public bool AuthenticationSuccess { get; set; } = false;
37+
38+
/// <summary>
39+
/// Gets the additional properties for AuthenticationInfo.
40+
/// </summary>
41+
[JsonIgnore]
42+
public IDictionary<string, string> ExtendedProperties { get; } = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
43+
44+
/// <summary>
45+
/// Initializes a new instance of the <see cref="AuthTelemetryRecord"/> class.
46+
/// </summary>
47+
public AuthTelemetryRecord()
48+
{
49+
TokenCredentialName = null;
50+
}
51+
52+
/// <summary>
53+
/// Initializes a new instance of the <see cref="AuthTelemetryRecord"/> class based on another instance of <see cref="IAuthTelemetryRecord"/>.
54+
/// </summary>
55+
/// <param name="other">The other instance of <see cref="IAuthTelemetryRecord"/>.</param>
56+
/// <param name="isSuccess">A value indicating whether the authentication was successful or not.</param>
57+
public AuthTelemetryRecord(IAuthTelemetryRecord other, bool? isSuccess = null)
58+
{
59+
this.TokenCredentialName = other.TokenCredentialName;
60+
this.AuthenticationSuccess = isSuccess ?? other.AuthenticationSuccess;
61+
foreach(var property in other.ExtendedProperties)
62+
{
63+
this.SetProperty(property.Key, property.Value);
64+
}
65+
}
66+
67+
/// <summary>
68+
/// Represents the key to indicate whether the token cache is enabled or not.
69+
/// </summary>
70+
public const string TokenCacheEnabled = "TokenCacheEnabled";
71+
72+
/// <summary>
73+
/// Represents the prefix of properties of the first record of authentication telemetry record.
74+
/// </summary>
75+
public const string AuthTelemetryPropertyPrimaryPrefix = "auth-info-primary";
76+
77+
/// <summary>
78+
/// Represents the key of the left records of authentication telemetry.
79+
/// </summary>
80+
public const string AuthTelemetryPropertySecondaryKey = "auth-info-secondary";
81+
}
82+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using Microsoft.Azure.Commands.Common.Authentication.Abstractions.Interfaces;
2+
3+
// ----------------------------------------------------------------------------------
4+
//
5+
// Copyright Microsoft Corporation
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
// ----------------------------------------------------------------------------------
16+
17+
namespace Microsoft.Azure.Commands.Common.Authentication.Abstractions
18+
{
19+
/// <summary>
20+
/// Represents a class for handling authentication telemetry.
21+
/// </summary>
22+
public class AuthenticationTelemetry : AzurePSCmdletConcurrentVault<AuthTelemetryRecord>
23+
{
24+
/// <summary>
25+
/// The name of the class.
26+
/// </summary>
27+
public const string Name = nameof(AuthenticationTelemetry);
28+
29+
/// <summary>
30+
/// Gets the telemetry record for the specified cmdlet context.
31+
/// </summary>
32+
/// <param name="cmdletContext">The cmdlet context.</param>
33+
/// <returns>The authentication telemetry data.</returns>
34+
public AuthenticationTelemetryData GetTelemetryRecord(ICmdletContext cmdletContext)
35+
{
36+
var records = PopDataRecords(cmdletContext);
37+
return records == null ? null : new AuthenticationTelemetryData(records);
38+
}
39+
}
40+
}

0 commit comments

Comments
 (0)