Skip to content

Commit e4ed362

Browse files
Copilotdavidfowl
andcommitted
Update tests for user secrets refactoring
- Update SecretsStoreTests to use UserSecretsManagerFactory - Update UserSecretsParameterDefaultTests with concurrent write tests - Update DefaultUserSecretsManagerTests to use JsonFlattener - Update VersionCheckServiceTests to inject IUserSecretsManager - Add TestUserSecretsManager for test isolation - Fix DeploymentStateManagerBase.cs extra braces issue Co-authored-by: davidfowl <[email protected]>
1 parent 7d743a8 commit e4ed362

File tree

5 files changed

+210
-30
lines changed

5 files changed

+210
-30
lines changed

src/Aspire.Hosting/Publishing/Internal/DeploymentStateManagerBase.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,6 @@ private sealed class SectionMetadata(long version)
7676
/// <param name="source">The flattened JsonObject to unflatten.</param>
7777
/// <returns>An unflattened JsonObject with nested structure.</returns>
7878
public static JsonObject UnflattenJsonObject(JsonObject source) => JsonFlattener.UnflattenJsonObject(source);
79-
}
80-
}
81-
}
8279

8380
/// <summary>
8481
/// Loads the deployment state from storage, using caching to avoid repeated loads.

tests/Aspire.Hosting.Azure.Tests/DefaultUserSecretsManagerTests.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public void FlattenJsonObject_HandlesNestedStructures()
3030
};
3131

3232
// Act
33-
var result = DeploymentStateManagerBase<UserSecretsDeploymentStateManager>.FlattenJsonObject(userSecrets);
33+
var result = JsonFlattener.FlattenJsonObject(userSecrets);
3434

3535
// Assert
3636
Assert.Equal("existing-flat-value", result["Azure:SubscriptionId"]!.ToString());
@@ -54,7 +54,7 @@ public void FlattenJsonObject_HandlesSimpleFlatStructure()
5454
};
5555

5656
// Act
57-
var result = DeploymentStateManagerBase<UserSecretsDeploymentStateManager>.FlattenJsonObject(userSecrets);
57+
var result = JsonFlattener.FlattenJsonObject(userSecrets);
5858

5959
// Assert
6060
Assert.Equal(3, result.Count);
@@ -70,7 +70,7 @@ public void FlattenJsonObject_HandlesEmptyObject()
7070
var userSecrets = new JsonObject();
7171

7272
// Act
73-
var result = DeploymentStateManagerBase<UserSecretsDeploymentStateManager>.FlattenJsonObject(userSecrets);
73+
var result = JsonFlattener.FlattenJsonObject(userSecrets);
7474

7575
// Assert
7676
Assert.Empty(result);
@@ -95,7 +95,7 @@ public void FlattenJsonObject_HandlesDeeplyNestedStructures()
9595
};
9696

9797
// Act
98-
var result = DeploymentStateManagerBase<UserSecretsDeploymentStateManager>.FlattenJsonObject(userSecrets);
98+
var result = JsonFlattener.FlattenJsonObject(userSecrets);
9999

100100
// Assert
101101
Assert.Single(result);
@@ -120,7 +120,7 @@ public void FlattenJsonObject_PreservesNullAndPrimitiveValues()
120120
};
121121

122122
// Act
123-
var result = DeploymentStateManagerBase<UserSecretsDeploymentStateManager>.FlattenJsonObject(userSecrets);
123+
var result = JsonFlattener.FlattenJsonObject(userSecrets);
124124

125125
// Assert
126126
Assert.Equal("text", result["StringValue"]!.ToString());
@@ -143,7 +143,7 @@ public void FlattenJsonObject_HandlesArraysWithPrimitiveValues()
143143
};
144144

145145
// Act
146-
var result = DeploymentStateManagerBase<UserSecretsDeploymentStateManager>.FlattenJsonObject(userSecrets);
146+
var result = JsonFlattener.FlattenJsonObject(userSecrets);
147147

148148
// Assert
149149
Assert.Equal("value1", result["SimpleArray:0"]!.ToString());
@@ -183,7 +183,7 @@ public void FlattenJsonObject_HandlesArraysWithObjects()
183183
};
184184

185185
// Act
186-
var result = DeploymentStateManagerBase<UserSecretsDeploymentStateManager>.FlattenJsonObject(userSecrets);
186+
var result = JsonFlattener.FlattenJsonObject(userSecrets);
187187

188188
// Assert
189189
Assert.Equal("Item1", result["ObjectArray:0:Name"]!.ToString());
@@ -206,7 +206,7 @@ public void FlattenJsonObject_HandlesEmptyArrays()
206206
};
207207

208208
// Act
209-
var result = DeploymentStateManagerBase<UserSecretsDeploymentStateManager>.FlattenJsonObject(userSecrets);
209+
var result = JsonFlattener.FlattenJsonObject(userSecrets);
210210

211211
// Assert
212212
Assert.Single(result);

tests/Aspire.Hosting.Tests/SecretsStoreTests.cs

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33

44
using System.Reflection;
55
using System.Reflection.Emit;
6+
using Aspire.Hosting.Publishing.Internal;
7+
using Aspire.Hosting.UserSecrets;
68
using Microsoft.Extensions.Configuration;
79
using Microsoft.Extensions.Configuration.UserSecrets;
8-
using Microsoft.Extensions.SecretManager.Tools.Internal;
910

1011
namespace Aspire.Hosting.Tests;
1112

@@ -26,7 +27,8 @@ public void GetOrSetUserSecret_SavesValueToUserSecrets()
2627
var configuration = new ConfigurationManager();
2728
var value = TokenGenerator.GenerateToken();
2829

29-
SecretsStore.GetOrSetUserSecret(configuration, testAssembly, key, () => value);
30+
var manager = UserSecretsManagerFactory.Instance.Create(testAssembly);
31+
manager?.GetOrSetSecret(configuration, key, () => value);
3032
var userSecrets = GetUserSecrets(userSecretsId);
3133

3234
var configValue = configuration[key];
@@ -50,7 +52,8 @@ public void GetOrSetUserSecret_ReadsValueFromConfiguration()
5052
var valueInConfig = TokenGenerator.GenerateToken();
5153
configuration[key] = valueInConfig;
5254

53-
SecretsStore.GetOrSetUserSecret(configuration, testAssembly, key, TokenGenerator.GenerateToken);
55+
var manager = UserSecretsManagerFactory.Instance.Create(testAssembly);
56+
manager?.GetOrSetSecret(configuration, key, TokenGenerator.GenerateToken);
5457
var userSecrets = GetUserSecrets(userSecretsId);
5558

5659
Assert.False(userSecrets.TryGetValue(key, out var savedValue));
@@ -60,20 +63,33 @@ public void GetOrSetUserSecret_ReadsValueFromConfiguration()
6063

6164
private static Dictionary<string, string?> GetUserSecrets(string userSecretsId)
6265
{
63-
var secretsStore = new SecretsStore(userSecretsId);
64-
return secretsStore.AsEnumerable().ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
66+
var manager = UserSecretsManagerFactory.Instance.GetOrCreateFromId(userSecretsId);
67+
if (manager == null || !File.Exists(manager.FilePath))
68+
{
69+
return new Dictionary<string, string?>();
70+
}
71+
72+
var config = new ConfigurationBuilder()
73+
.AddJsonFile(manager.FilePath, optional: true)
74+
.Build();
75+
76+
return config.AsEnumerable()
77+
.Where(kvp => kvp.Value != null)
78+
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
6579
}
6680

6781
private static void ClearUsersSecrets(string userSecretsId)
6882
{
69-
var secretsStore = new SecretsStore(userSecretsId);
70-
secretsStore.Clear();
71-
secretsStore.Save();
83+
var filePath = UserSecretsPathHelper.GetSecretsPathFromSecretsId(userSecretsId);
84+
if (File.Exists(filePath))
85+
{
86+
File.Delete(filePath);
87+
}
7288
}
7389

7490
private static void DeleteUserSecretsFile(string userSecretsId)
7591
{
76-
var userSecretsPath = PathHelper.GetSecretsPathFromSecretsId(userSecretsId);
92+
var userSecretsPath = UserSecretsPathHelper.GetSecretsPathFromSecretsId(userSecretsId);
7793
if (File.Exists(userSecretsPath))
7894
{
7995
File.Delete(userSecretsPath);

tests/Aspire.Hosting.Tests/UserSecretsParameterDefaultTests.cs

Lines changed: 155 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
using System.Reflection.Emit;
66
using System.Text;
77
using Aspire.Hosting.Publishing;
8+
using Aspire.Hosting.Publishing.Internal;
9+
using Aspire.Hosting.UserSecrets;
10+
using Microsoft.Extensions.Configuration;
811
using Microsoft.Extensions.Configuration.UserSecrets;
9-
using Microsoft.Extensions.SecretManager.Tools.Internal;
1012

1113
namespace Aspire.Hosting.Tests;
1214

@@ -49,7 +51,7 @@ public void UserSecretsParameterDefault_GetDefaultValue_DoesntThrowIfSecretsFile
4951
{
5052
var userSecretsId = Guid.NewGuid().ToString("N");
5153
DeleteUserSecretsFile(userSecretsId);
52-
var userSecretsPath = PathHelper.GetSecretsPathFromSecretsId(userSecretsId);
54+
var userSecretsPath = UserSecretsPathHelper.GetSecretsPathFromSecretsId(userSecretsId);
5355
if (File.Exists(userSecretsPath))
5456
{
5557
File.Delete(userSecretsPath);
@@ -71,6 +73,123 @@ public void UserSecretsParameterDefault_GetDefaultValue_DoesntThrowIfSecretsFile
7173
var _ = userSecretDefault.GetDefaultValue();
7274
}
7375

76+
[Fact]
77+
public async Task TrySetUserSecret_ConcurrentWrites_PreservesAllSecrets()
78+
{
79+
var userSecretsId = Guid.NewGuid().ToString("N");
80+
ClearUsersSecrets(userSecretsId);
81+
82+
var testAssembly = AssemblyBuilder.DefineDynamicAssembly(
83+
new("TestAssembly"), AssemblyBuilderAccess.RunAndCollect, [new CustomAttributeBuilder(s_userSecretsIdAttrCtor, [userSecretsId])]);
84+
85+
// Simulate concurrent writes from multiple threads (like SQL Server and RabbitMQ generating passwords)
86+
var tasks = new List<Task<bool>>();
87+
var secretsToWrite = new Dictionary<string, string>
88+
{
89+
["Parameters:sqlserver-password"] = "SqlPassword123!",
90+
["Parameters:rabbitmq-password"] = "RabbitPassword456!",
91+
["Parameters:redis-password"] = "RedisPassword789!",
92+
["Parameters:postgres-password"] = "PostgresPassword012!",
93+
};
94+
95+
foreach (var kvp in secretsToWrite)
96+
{
97+
var key = kvp.Key;
98+
var value = kvp.Value;
99+
tasks.Add(Task.Run(() =>
100+
{
101+
var manager = UserSecretsManagerFactory.Instance.Create(testAssembly);
102+
return manager?.TrySetSecret(key, value) ?? false;
103+
}));
104+
}
105+
106+
var results = await Task.WhenAll(tasks);
107+
108+
// All writes should succeed
109+
Assert.All(results, Assert.True);
110+
111+
// All secrets should be preserved
112+
var userSecrets = GetUserSecrets(userSecretsId);
113+
foreach (var kvp in secretsToWrite)
114+
{
115+
Assert.True(userSecrets.ContainsKey(kvp.Key), $"Secret '{kvp.Key}' was not found in user secrets");
116+
Assert.Equal(kvp.Value, userSecrets[kvp.Key]);
117+
}
118+
119+
DeleteUserSecretsFile(userSecretsId);
120+
}
121+
122+
[Fact]
123+
public async Task TrySetUserSecret_SqlServerAndRabbitMQ_BothSecretsPreserved()
124+
{
125+
// This test specifically reproduces the issue described in the bug report
126+
var userSecretsId = Guid.NewGuid().ToString("N");
127+
ClearUsersSecrets(userSecretsId);
128+
129+
var testAssembly = AssemblyBuilder.DefineDynamicAssembly(
130+
new("TestAssembly"), AssemblyBuilderAccess.RunAndCollect, [new CustomAttributeBuilder(s_userSecretsIdAttrCtor, [userSecretsId])]);
131+
132+
// Simulate SQL Server and RabbitMQ generating passwords concurrently
133+
var sqlTask = Task.Run(() =>
134+
{
135+
var manager = UserSecretsManagerFactory.Instance.Create(testAssembly);
136+
return manager?.TrySetSecret("Parameters:sql-password", "SqlPassword123!") ?? false;
137+
});
138+
var rabbitTask = Task.Run(() =>
139+
{
140+
var manager = UserSecretsManagerFactory.Instance.Create(testAssembly);
141+
return manager?.TrySetSecret("Parameters:rabbit-password", "RabbitPassword456!") ?? false;
142+
});
143+
144+
var results = await Task.WhenAll(sqlTask, rabbitTask);
145+
146+
// Both writes should succeed
147+
Assert.All(results, Assert.True);
148+
149+
// Both secrets should be in the file
150+
var userSecrets = GetUserSecrets(userSecretsId);
151+
Assert.True(userSecrets.ContainsKey("Parameters:sql-password"), "SQL Server password was not found");
152+
Assert.True(userSecrets.ContainsKey("Parameters:rabbit-password"), "RabbitMQ password was not found");
153+
Assert.Equal("SqlPassword123!", userSecrets["Parameters:sql-password"]);
154+
Assert.Equal("RabbitPassword456!", userSecrets["Parameters:rabbit-password"]);
155+
156+
DeleteUserSecretsFile(userSecretsId);
157+
}
158+
159+
[Fact]
160+
public async Task TrySetUserSecret_ConcurrentWritesSameKey_LastWriteWins()
161+
{
162+
var userSecretsId = Guid.NewGuid().ToString("N");
163+
ClearUsersSecrets(userSecretsId);
164+
165+
var testAssembly = AssemblyBuilder.DefineDynamicAssembly(
166+
new("TestAssembly"), AssemblyBuilderAccess.RunAndCollect, [new CustomAttributeBuilder(s_userSecretsIdAttrCtor, [userSecretsId])]);
167+
168+
// Simulate concurrent writes to the same key
169+
var tasks = new List<Task<bool>>();
170+
for (int i = 0; i < 10; i++)
171+
{
172+
var value = $"Value{i}";
173+
tasks.Add(Task.Run(() =>
174+
{
175+
var manager = UserSecretsManagerFactory.Instance.Create(testAssembly);
176+
return manager?.TrySetSecret("Parameters:test-key", value) ?? false;
177+
}));
178+
}
179+
180+
var results = await Task.WhenAll(tasks);
181+
182+
// All writes should succeed
183+
Assert.All(results, Assert.True);
184+
185+
// The key should exist with one of the values
186+
var userSecrets = GetUserSecrets(userSecretsId);
187+
Assert.True(userSecrets.ContainsKey("Parameters:test-key"));
188+
Assert.NotNull(userSecrets["Parameters:test-key"]);
189+
190+
DeleteUserSecretsFile(userSecretsId);
191+
}
192+
74193
private static void EnsureUserSecretsDirectory(string secretsFilePath)
75194
{
76195
var directoryName = Path.GetDirectoryName(secretsFilePath);
@@ -82,20 +201,47 @@ private static void EnsureUserSecretsDirectory(string secretsFilePath)
82201

83202
private static Dictionary<string, string?> GetUserSecrets(string userSecretsId)
84203
{
85-
var secretsStore = new SecretsStore(userSecretsId);
86-
return secretsStore.AsEnumerable().ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
204+
var manager = UserSecretsManagerFactory.Instance.GetOrCreateFromId(userSecretsId);
205+
if (manager == null)
206+
{
207+
return new Dictionary<string, string?>();
208+
}
209+
210+
// Read the secrets file directly
211+
var secrets = new Dictionary<string, string?>();
212+
if (File.Exists(manager.FilePath))
213+
{
214+
var json = File.ReadAllText(manager.FilePath);
215+
if (!string.IsNullOrWhiteSpace(json))
216+
{
217+
var config = new ConfigurationBuilder()
218+
.AddJsonFile(manager.FilePath, optional: true)
219+
.Build();
220+
221+
foreach (var kvp in config.AsEnumerable())
222+
{
223+
if (kvp.Value != null)
224+
{
225+
secrets[kvp.Key] = kvp.Value;
226+
}
227+
}
228+
}
229+
}
230+
return secrets;
87231
}
88232

89233
private static void ClearUsersSecrets(string userSecretsId)
90234
{
91-
var secretsStore = new SecretsStore(userSecretsId);
92-
secretsStore.Clear();
93-
secretsStore.Save();
235+
var filePath = UserSecretsPathHelper.GetSecretsPathFromSecretsId(userSecretsId);
236+
if (File.Exists(filePath))
237+
{
238+
File.Delete(filePath);
239+
}
94240
}
95241

96242
private static void DeleteUserSecretsFile(string userSecretsId)
97243
{
98-
var userSecretsPath = PathHelper.GetSecretsPathFromSecretsId(userSecretsId);
244+
var userSecretsPath = UserSecretsPathHelper.GetSecretsPathFromSecretsId(userSecretsId);
99245
if (File.Exists(userSecretsPath))
100246
{
101247
File.Delete(userSecretsPath);
@@ -115,3 +261,4 @@ public override void WriteToManifest(ManifestPublishingContext context)
115261
}
116262
}
117263
}
264+

0 commit comments

Comments
 (0)