Skip to content

Commit e4b3662

Browse files
Implement requested changes: Move MockAnonymousUserService to tests and add PromoteAnonUser method
Co-authored-by: christiannagel <1908285+christiannagel@users.noreply.github.com>
1 parent ebc4cf9 commit e4b3662

7 files changed

Lines changed: 177 additions & 13 deletions

File tree

src/services/identity/Codebreaker.Identity.Tests/AnonymousUserServiceTests.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ public async Task DeleteAnonUsers_ShouldDeleteStaleAnonymousUsers()
4444
// In a real implementation, we would use a test double or wrapper for GraphServiceClient
4545
}
4646

47+
[Fact(Skip = "Requires mocking of GraphServiceClient which is challenging")]
48+
public async Task PromoteAnonUser_ShouldPromoteAnonymousUser()
49+
{
50+
// This test would require extensive mocking of GraphServiceClient
51+
// In a real implementation, we would use a test double or wrapper for GraphServiceClient
52+
}
53+
4754
[Fact]
4855
public void ServiceInitialization_WithValidOptions_ShouldNotThrow()
4956
{
@@ -84,12 +91,14 @@ private class GraphAnonymousUserServiceWrapper(
8491
IOptions<AnonymousUserOptions> options,
8592
ILogger<GraphAnonymousUserService> logger) : GraphAnonymousUserService(options, logger)
8693
{
87-
8894
// We don't test the actual Graph API methods here
8995
public new Task<AnonymousUser> CreateAnonUser(string userName) =>
9096
Task.FromResult(new AnonymousUser());
9197

9298
public new Task<int> DeleteAnonUsers() =>
9399
Task.FromResult(0);
100+
101+
public new Task<AnonymousUser> PromoteAnonUser(string anonymousUserId, string email, string displayName, string password) =>
102+
Task.FromResult(new AnonymousUser());
94103
}
95104
}

src/services/identity/Codebreaker.Identity/Services/MockAnonymousUserService.cs renamed to src/services/identity/Codebreaker.Identity.Tests/MockAnonymousUserService.cs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
using Codebreaker.Identity.Models;
2+
using Codebreaker.Identity.Services;
23
using Microsoft.Extensions.Logging;
34

4-
namespace Codebreaker.Identity.Services;
5+
namespace Codebreaker.Identity.Tests;
56

67
/// <summary>
78
/// A mock implementation of <see cref="IAnonymousUserService"/> for testing
@@ -54,6 +55,26 @@ public Task<int> DeleteAnonUsers()
5455
return Task.FromResult(count);
5556
}
5657

58+
/// <inheritdoc />
59+
public Task<AnonymousUser> PromoteAnonUser(string anonymousUserId, string email, string displayName, string password)
60+
{
61+
_logger.LogInformation("Promoting mock anonymous user: {UserId}", anonymousUserId);
62+
63+
var user = _users.FirstOrDefault(u => u.Id == anonymousUserId);
64+
if (user == null)
65+
{
66+
throw new InvalidOperationException($"User with ID {anonymousUserId} not found");
67+
}
68+
69+
// Update the user properties
70+
user.Email = email;
71+
user.DisplayName = displayName;
72+
user.Password = password;
73+
user.LastLoginAt = DateTimeOffset.UtcNow;
74+
75+
return Task.FromResult(user);
76+
}
77+
5778
/// <summary>
5879
/// Gets the current anonymous users
5980
/// </summary>

src/services/identity/Codebreaker.Identity.Tests/MockAnonymousUserServiceTests.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,47 @@ public async Task DeleteAnonUsers_ShouldRemoveStaleUsers()
9797
Assert.Equal(recentUser.Id, remainingUsers[0].Id);
9898
}
9999

100+
[Fact]
101+
public async Task PromoteAnonUser_ShouldUpdateUserProperties()
102+
{
103+
// Arrange
104+
var service = new MockAnonymousUserService(_mockLogger.Object);
105+
var user = await service.CreateAnonUser("AnonUser");
106+
107+
string newEmail = "registered@example.com";
108+
string newDisplayName = "Registered User";
109+
string newPassword = "SecurePassword123!";
110+
111+
// Act
112+
var result = await service.PromoteAnonUser(user.Id, newEmail, newDisplayName, newPassword);
113+
114+
// Assert
115+
Assert.NotNull(result);
116+
Assert.Equal(user.Id, result.Id); // ID should remain the same
117+
Assert.Equal(newEmail, result.Email);
118+
Assert.Equal(newDisplayName, result.DisplayName);
119+
Assert.Equal(newPassword, result.Password);
120+
Assert.NotNull(result.LastLoginAt); // Should set last login time
121+
122+
// Verify the user was updated in the collection
123+
var users = service.GetUsers();
124+
Assert.Single(users);
125+
Assert.Equal(newEmail, users[0].Email);
126+
Assert.Equal(newDisplayName, users[0].DisplayName);
127+
}
128+
129+
[Fact]
130+
public async Task PromoteAnonUser_WithInvalidId_ShouldThrowException()
131+
{
132+
// Arrange
133+
var service = new MockAnonymousUserService(_mockLogger.Object);
134+
string invalidId = "non-existent-id";
135+
136+
// Act & Assert
137+
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
138+
await service.PromoteAnonUser(invalidId, "email@example.com", "Display Name", "password"));
139+
}
140+
100141
[Fact]
101142
public void GetUsers_ShouldReturnReadOnlyListOfUsers()
102143
{
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using Codebreaker.Identity.Services;
2+
using Microsoft.Extensions.DependencyInjection;
3+
4+
namespace Codebreaker.Identity.Tests;
5+
6+
/// <summary>
7+
/// Extension methods for testing
8+
/// </summary>
9+
public static class TestExtensions
10+
{
11+
/// <summary>
12+
/// Adds a mock anonymous user service for testing purposes
13+
/// </summary>
14+
/// <param name="services">The service collection</param>
15+
/// <returns>The service collection</returns>
16+
public static IServiceCollection AddMockAnonymousUserService(this IServiceCollection services)
17+
{
18+
services.AddScoped<IAnonymousUserService, MockAnonymousUserService>();
19+
return services;
20+
}
21+
}

src/services/identity/Codebreaker.Identity/Extensions/ServiceCollectionExtensions.cs

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,4 @@ public static IServiceCollection AddAnonymousUserService(
3434
services.AddScoped<IAnonymousUserService, GraphAnonymousUserService>();
3535
return services;
3636
}
37-
38-
/// <summary>
39-
/// Adds a mock anonymous user service for testing purposes
40-
/// </summary>
41-
/// <param name="services">The service collection</param>
42-
/// <returns>The service collection</returns>
43-
public static IServiceCollection AddMockAnonymousUserService(this IServiceCollection services)
44-
{
45-
services.AddScoped<IAnonymousUserService, MockAnonymousUserService>();
46-
return services;
47-
}
4837
}

src/services/identity/Codebreaker.Identity/Services/GraphAnonymousUserService.cs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,79 @@ createdAtObj is string createdAtStr &&
177177
}
178178
}
179179

180+
/// <inheritdoc />
181+
public async Task<AnonymousUser> PromoteAnonUser(string anonymousUserId, string email, string displayName, string password)
182+
{
183+
_logger.LogInformation("Promoting anonymous user {UserId} to registered user", anonymousUserId);
184+
185+
try
186+
{
187+
// First, verify the user exists and is actually an anonymous user
188+
var user = await _graphClient.Users[anonymousUserId].GetAsync(requestConfig =>
189+
{
190+
requestConfig.QueryParameters.Select = ["id", "displayName", "userPrincipalName", "mail", "extension_AnonymousUser"];
191+
});
192+
193+
if (user == null)
194+
{
195+
throw new InvalidOperationException($"User with ID {anonymousUserId} not found");
196+
}
197+
198+
// Verify this is an anonymous user
199+
if (user.AdditionalData == null ||
200+
!user.AdditionalData.TryGetValue("extension_AnonymousUser", out var isAnonObj) ||
201+
isAnonObj is not bool isAnon ||
202+
!isAnon)
203+
{
204+
throw new InvalidOperationException($"User with ID {anonymousUserId} is not an anonymous user");
205+
}
206+
207+
// Update the user properties but keep the same ID
208+
await _graphClient.Users[anonymousUserId].PatchAsync(new User
209+
{
210+
DisplayName = displayName,
211+
UserPrincipalName = email,
212+
Mail = email,
213+
PasswordProfile = new PasswordProfile
214+
{
215+
ForceChangePasswordNextSignIn = false,
216+
Password = password
217+
},
218+
// Remove the anonymous user flag and add promoted flag
219+
AdditionalData = new Dictionary<string, object>
220+
{
221+
{ "extension_AnonymousUser", false },
222+
{ "extension_PromotedAt", DateTimeOffset.UtcNow.ToString("o") }
223+
}
224+
});
225+
226+
// Get the updated user to return
227+
var updatedUser = await _graphClient.Users[anonymousUserId].GetAsync();
228+
229+
if (updatedUser == null)
230+
{
231+
throw new InvalidOperationException($"Failed to retrieve updated user with ID {anonymousUserId}");
232+
}
233+
234+
// Return the updated user details
235+
return new AnonymousUser
236+
{
237+
Id = updatedUser.Id ?? anonymousUserId,
238+
UserName = updatedUser.UserPrincipalName ?? email,
239+
DisplayName = updatedUser.DisplayName ?? displayName,
240+
Email = updatedUser.Mail ?? email,
241+
Password = password,
242+
CreatedAt = DateTimeOffset.Parse(user.AdditionalData?["extension_CreatedAt"]?.ToString() ?? DateTimeOffset.UtcNow.ToString()),
243+
LastLoginAt = updatedUser.SignInActivity?.LastSignInDateTime ?? DateTimeOffset.UtcNow
244+
};
245+
}
246+
catch (Exception ex)
247+
{
248+
_logger.LogError(ex, "Failed to promote anonymous user {UserId}: {Message}", anonymousUserId, ex.Message);
249+
throw;
250+
}
251+
}
252+
180253
private static string GenerateSecurePassword(int length)
181254
{
182255
const string uppercaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

src/services/identity/Codebreaker.Identity/Services/IAnonymousUserService.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,14 @@ public interface IAnonymousUserService
1919
/// </summary>
2020
/// <returns>The number of deleted users</returns>
2121
Task<int> DeleteAnonUsers();
22+
23+
/// <summary>
24+
/// Promotes an anonymous user to a registered user
25+
/// </summary>
26+
/// <param name="anonymousUserId">The ID of the anonymous user</param>
27+
/// <param name="email">The email address for the registered user</param>
28+
/// <param name="displayName">The display name for the registered user</param>
29+
/// <param name="password">The password for the registered user</param>
30+
/// <returns>The updated user object</returns>
31+
Task<AnonymousUser> PromoteAnonUser(string anonymousUserId, string email, string displayName, string password);
2232
}

0 commit comments

Comments
 (0)