feat/Update_Controllers_to_Allow_for_Premium_Membership

This commit is contained in:
cesnimda
2026-08-03 09:17:28 +02:00
parent de937d25dc
commit c3f4a57195
187 changed files with 26062 additions and 991 deletions
@@ -188,16 +188,21 @@ public sealed class AuthAndSystemControllerTests
var emailSender = new Mock<IAppEmailSender>();
var controller = new AuthController(config, userManager.Object, tokenService.Object, emailSender.Object, Mock.Of<IGoogleTokenValidator>(), Mock.Of<IMicrosoftTokenValidator>(), NullLogger<AuthController>.Instance, Mock.Of<ITwoFactorPendingTokenService>(), TestHostFactory.CreateInMemoryDb())
using var db = TestHostFactory.CreateInMemoryDb();
var controller = new AuthController(config, userManager.Object, tokenService.Object, emailSender.Object, Mock.Of<IGoogleTokenValidator>(), Mock.Of<IMicrosoftTokenValidator>(), NullLogger<AuthController>.Instance, Mock.Of<ITwoFactorPendingTokenService>(), db)
{
ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() }
};
var result = await controller.Register(new AuthController.RegisterRequest("new.user@example.com", "password123"), CancellationToken.None);
Assert.IsType<OkObjectResult>(result);
var accepted = Assert.IsType<ObjectResult>(result);
Assert.Equal(StatusCodes.Status202Accepted, accepted.StatusCode);
Assert.True(Assert.IsType<AuthController.RegistrationPendingResult>(accepted.Value).VerificationRequired);
Assert.NotNull(created);
Assert.False(created!.EmailConfirmed);
Assert.Empty(db.UserSessions);
tokenService.Verify(x => x.CreateAccessTokenAsync(It.IsAny<ApplicationUser>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()), Times.Never);
emailSender.Verify(x => x.SendAsync("new.user@example.com", It.IsAny<string>(), It.Is<string>(b => b.Contains("verify-email")), It.IsAny<CancellationToken>()), Times.Once);
}
@@ -414,13 +419,92 @@ public sealed class AuthAndSystemControllerTests
var result = await controller.UpdateProfile(new AuthController.UpdateProfileRequest(" new@example.com ", " newuser ", " Ada ", " Lovelace ", " Ada L. ", null, null));
Assert.IsType<NoContentResult>(result);
Assert.Equal("new@example.com", user.Email);
Assert.Equal("old@example.com", user.Email);
Assert.Equal("newuser", user.UserName);
Assert.Equal("Ada", user.FirstName);
Assert.Equal("Lovelace", user.LastName);
Assert.Equal("Ada L.", user.DisplayName);
}
[Fact]
public async Task Request_email_change_keeps_current_email_and_sends_confirmation_to_new_address()
{
var user = new ApplicationUser { Id = "user-1", Email = "old@example.com", UserName = "old@example.com", EmailConfirmed = true, SecurityStamp = "old-stamp" };
var users = CreateUserManager();
users.Setup(x => x.GetUserAsync(It.IsAny<System.Security.Claims.ClaimsPrincipal>())).ReturnsAsync(user);
users.Setup(x => x.HasPasswordAsync(user)).ReturnsAsync(true);
users.Setup(x => x.CheckPasswordAsync(user, "correct-password")).ReturnsAsync(true);
users.Setup(x => x.NormalizeEmail(It.IsAny<string>())).Returns((string value) => value.ToUpperInvariant());
users.Setup(x => x.FindByEmailAsync("new@example.com")).ReturnsAsync((ApplicationUser?)null);
users.Setup(x => x.UpdateAsync(user)).ReturnsAsync(IdentityResult.Success);
users.Setup(x => x.GenerateChangeEmailTokenAsync(user, "new@example.com")).ReturnsAsync("change-token");
var email = new Mock<IAppEmailSender>();
var controller = new AuthController(BuildConfig(), users.Object, Mock.Of<ITokenService>(), email.Object, Mock.Of<IGoogleTokenValidator>(), Mock.Of<IMicrosoftTokenValidator>(), NullLogger<AuthController>.Instance, Mock.Of<ITwoFactorPendingTokenService>(), TestHostFactory.CreateInMemoryDb())
{
ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() }
};
var result = await controller.RequestEmailChange(new AuthController.RequestEmailChangeRequest("new@example.com", "correct-password"), CancellationToken.None);
Assert.IsType<AcceptedResult>(result);
Assert.Equal("old@example.com", user.Email);
Assert.Equal("new@example.com", user.PendingEmail);
Assert.NotNull(user.PendingEmailRequestedAtUtc);
Assert.NotEqual("old-stamp", user.SecurityStamp);
email.Verify(x => x.SendAsync("new@example.com", It.IsAny<string>(), It.Is<string>(body => body.Contains("confirm-email-change")), It.IsAny<CancellationToken>()), Times.Once);
email.Verify(x => x.SendAsync("old@example.com", It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task Confirm_email_change_rejects_a_superseded_address()
{
var user = new ApplicationUser { Id = "user-1", Email = "old@example.com", PendingEmail = "latest@example.com" };
var users = CreateUserManager();
users.Setup(x => x.FindByIdAsync("user-1")).ReturnsAsync(user);
users.Setup(x => x.NormalizeEmail(It.IsAny<string>())).Returns((string value) => value.ToUpperInvariant());
var controller = new AuthController(BuildConfig(), users.Object, Mock.Of<ITokenService>(), Mock.Of<IAppEmailSender>(), Mock.Of<IGoogleTokenValidator>(), Mock.Of<IMicrosoftTokenValidator>(), NullLogger<AuthController>.Instance, Mock.Of<ITwoFactorPendingTokenService>(), TestHostFactory.CreateInMemoryDb());
var result = await controller.ConfirmEmailChange(new AuthController.ConfirmEmailChangeRequest("user-1", "superseded@example.com", "old-token"), CancellationToken.None);
Assert.IsType<BadRequestObjectResult>(result);
users.Verify(x => x.ChangeEmailAsync(It.IsAny<ApplicationUser>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never);
}
[Fact]
public async Task Confirm_email_change_updates_default_username_and_revokes_sessions_and_devices()
{
var user = new ApplicationUser { Id = "user-1", Email = "old@example.com", UserName = "old@example.com", PendingEmail = "new@example.com", PendingEmailRequestedAtUtc = DateTimeOffset.UtcNow };
var users = CreateUserManager();
users.Setup(x => x.FindByIdAsync("user-1")).ReturnsAsync(user);
users.Setup(x => x.NormalizeEmail(It.IsAny<string>())).Returns((string value) => value.ToUpperInvariant());
users.Setup(x => x.NormalizeName(It.IsAny<string>())).Returns((string value) => value.ToUpperInvariant());
users.Setup(x => x.ChangeEmailAsync(user, "new@example.com", "change-token"))
.Callback(() => { user.Email = "new@example.com"; user.EmailConfirmed = true; })
.ReturnsAsync(IdentityResult.Success);
users.Setup(x => x.SetUserNameAsync(user, "new@example.com"))
.Callback(() => user.UserName = "new@example.com")
.ReturnsAsync(IdentityResult.Success);
users.Setup(x => x.UpdateAsync(user)).ReturnsAsync(IdentityResult.Success);
using var db = TestHostFactory.CreateInMemoryDb();
db.UserSessions.Add(new UserSession { Id = "sid-1", UserId = user.Id, CreatedAtUtc = DateTimeOffset.UtcNow, LastSeenAtUtc = DateTimeOffset.UtcNow, ExpiresAtUtc = DateTimeOffset.UtcNow.AddHours(1) });
db.TrustedDevices.Add(new TrustedDevice { UserId = user.Id, TokenHash = "hash", CreatedAtUtc = DateTimeOffset.UtcNow, LastSeenAtUtc = DateTimeOffset.UtcNow, ExpiresAtUtc = DateTimeOffset.UtcNow.AddDays(1) });
await db.SaveChangesAsync();
var controller = new AuthController(BuildConfig(), users.Object, Mock.Of<ITokenService>(), Mock.Of<IAppEmailSender>(), Mock.Of<IGoogleTokenValidator>(), Mock.Of<IMicrosoftTokenValidator>(), NullLogger<AuthController>.Instance, Mock.Of<ITwoFactorPendingTokenService>(), db)
{
ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() }
};
var result = await controller.ConfirmEmailChange(new AuthController.ConfirmEmailChangeRequest("user-1", "new@example.com", "change-token"), CancellationToken.None);
Assert.IsType<NoContentResult>(result);
Assert.Equal("new@example.com", user.Email);
Assert.Equal("new@example.com", user.UserName);
Assert.Null(user.PendingEmail);
Assert.NotNull((await db.UserSessions.SingleAsync()).RevokedAtUtc);
Assert.Empty(await db.TrustedDevices.ToListAsync());
}
private static AuthController BuildProfileController(ApplicationUser user)
{
var userManager = CreateUserManager();
@@ -505,9 +589,10 @@ public sealed class AuthAndSystemControllerTests
[Fact]
public async Task Request_password_reset_returns_service_unavailable_when_email_send_fails()
{
var user = new ApplicationUser { Email = "person@example.com", UserName = "person@example.com" };
var user = new ApplicationUser { Email = "person@example.com", UserName = "person@example.com", EmailConfirmed = true };
var userManager = CreateUserManager();
userManager.Setup(x => x.FindByEmailAsync("person@example.com")).ReturnsAsync(user);
userManager.Setup(x => x.HasPasswordAsync(user)).ReturnsAsync(true);
userManager.Setup(x => x.GeneratePasswordResetTokenAsync(user)).ReturnsAsync("reset-token");
var emailSender = new Mock<IAppEmailSender>();
@@ -578,6 +663,8 @@ public sealed class AuthAndSystemControllerTests
[Fact]
public async Task Exchange_microsoft_token_creates_new_user_when_registration_allowed()
{
const string tenantId = "11111111-1111-1111-1111-111111111111";
const string objectId = "22222222-2222-2222-2222-222222222222";
var userManager = CreateUserManager();
userManager.Setup(x => x.Users).Returns(new TestAsyncEnumerable<ApplicationUser>(new List<ApplicationUser>()));
userManager.Setup(x => x.FindByEmailAsync("new.hire@example.com")).ReturnsAsync((ApplicationUser?)null);
@@ -594,7 +681,7 @@ public sealed class AuthAndSystemControllerTests
var microsoftValidator = new Mock<IMicrosoftTokenValidator>();
microsoftValidator
.Setup(x => x.ValidateAsync("microsoft-token", It.IsAny<CancellationToken>()))
.ReturnsAsync(new MicrosoftTokenPrincipal("ms-subject", "new.hire@example.com", true, "New", "Hire", "New Hire"));
.ReturnsAsync(new MicrosoftTokenPrincipal(objectId, "new.hire@example.com", false, "New", "Hire", "New Hire", tenantId, objectId));
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?> { ["Auth:AllowRegistration"] = "true" })
@@ -616,12 +703,17 @@ public sealed class AuthAndSystemControllerTests
Assert.Equal("microsoft", payload.Provider);
Assert.NotNull(created);
Assert.Equal("new.hire@example.com", created!.Email);
Assert.Equal("ms-subject", created.MicrosoftSubject);
Assert.False(created.EmailConfirmed);
Assert.Equal(tenantId, created.MicrosoftTenantId);
Assert.Equal(objectId, created.MicrosoftObjectId);
Assert.Null(created.MicrosoftSubject);
}
[Fact]
public async Task Exchange_microsoft_token_rejects_unmatched_account_when_registration_disabled()
{
const string tenantId = "11111111-1111-1111-1111-111111111111";
const string objectId = "22222222-2222-2222-2222-222222222222";
var userManager = CreateUserManager();
userManager.Setup(x => x.Users).Returns(new TestAsyncEnumerable<ApplicationUser>(new List<ApplicationUser>()));
userManager.Setup(x => x.FindByEmailAsync("nobody@example.com")).ReturnsAsync((ApplicationUser?)null);
@@ -629,7 +721,7 @@ public sealed class AuthAndSystemControllerTests
var microsoftValidator = new Mock<IMicrosoftTokenValidator>();
microsoftValidator
.Setup(x => x.ValidateAsync("microsoft-token", It.IsAny<CancellationToken>()))
.ReturnsAsync(new MicrosoftTokenPrincipal("ms-subject", "nobody@example.com", true, null, null, null));
.ReturnsAsync(new MicrosoftTokenPrincipal(objectId, "nobody@example.com", false, null, null, null, tenantId, objectId));
var controller = new AuthController(BuildConfig(), userManager.Object, Mock.Of<ITokenService>(), Mock.Of<IAppEmailSender>(), Mock.Of<IGoogleTokenValidator>(), microsoftValidator.Object, NullLogger<AuthController>.Instance, Mock.Of<ITwoFactorPendingTokenService>(), TestHostFactory.CreateInMemoryDb())
{
@@ -645,6 +737,158 @@ public sealed class AuthAndSystemControllerTests
userManager.Verify(x => x.CreateAsync(It.IsAny<ApplicationUser>()), Times.Never);
}
[Fact]
public async Task Exchange_microsoft_token_never_auto_links_an_existing_email()
{
const string tenantId = "11111111-1111-1111-1111-111111111111";
const string objectId = "22222222-2222-2222-2222-222222222222";
var existing = new ApplicationUser { Id = "local-1", Email = "same@example.com", UserName = "same@example.com", EmailConfirmed = true };
var users = CreateUserManager();
users.Setup(x => x.Users).Returns(new TestAsyncEnumerable<ApplicationUser>(new List<ApplicationUser>()));
users.Setup(x => x.FindByEmailAsync("same@example.com")).ReturnsAsync(existing);
var microsoft = new Mock<IMicrosoftTokenValidator>();
microsoft.Setup(x => x.ValidateAsync("token", It.IsAny<CancellationToken>()))
.ReturnsAsync(new MicrosoftTokenPrincipal(objectId, "same@example.com", false, null, null, null, tenantId, objectId));
var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?> { ["Auth:AllowRegistration"] = "true" }).Build();
var controller = new AuthController(config, users.Object, Mock.Of<ITokenService>(), Mock.Of<IAppEmailSender>(), Mock.Of<IGoogleTokenValidator>(), microsoft.Object, NullLogger<AuthController>.Instance, Mock.Of<ITwoFactorPendingTokenService>(), TestHostFactory.CreateInMemoryDb())
{
ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() }
};
var result = await controller.ExchangeMicrosoftToken(new AuthController.MicrosoftTokenRequest("token"), CancellationToken.None);
Assert.IsType<ConflictObjectResult>(result);
Assert.Null(existing.MicrosoftTenantId);
users.Verify(x => x.UpdateAsync(existing), Times.Never);
users.Verify(x => x.CreateAsync(It.IsAny<ApplicationUser>()), Times.Never);
}
[Fact]
public async Task Exchange_microsoft_token_uses_the_full_tenant_object_pair()
{
const string tenantA = "11111111-1111-1111-1111-111111111111";
const string tenantB = "33333333-3333-3333-3333-333333333333";
const string objectId = "22222222-2222-2222-2222-222222222222";
var userA = new ApplicationUser { Id = "user-a", Email = "a@example.com", UserName = "a", EmailConfirmed = true, MicrosoftTenantId = tenantA, MicrosoftObjectId = objectId };
var userB = new ApplicationUser { Id = "user-b", Email = "b@example.com", UserName = "b", EmailConfirmed = true, MicrosoftTenantId = tenantB, MicrosoftObjectId = objectId };
var users = CreateUserManager();
users.Setup(x => x.Users).Returns(new TestAsyncEnumerable<ApplicationUser>(new[] { userA, userB }));
users.Setup(x => x.UpdateAsync(userB)).ReturnsAsync(IdentityResult.Success);
var tokens = new Mock<ITokenService>();
tokens.Setup(x => x.CreateAccessTokenAsync(userB, It.IsAny<string?>(), It.IsAny<CancellationToken>())).ReturnsAsync("app-token");
var microsoft = new Mock<IMicrosoftTokenValidator>();
microsoft.Setup(x => x.ValidateAsync("token", It.IsAny<CancellationToken>()))
.ReturnsAsync(new MicrosoftTokenPrincipal(objectId, "shared@example.com", false, null, null, null, tenantB, objectId));
var controller = new AuthController(BuildConfig(), users.Object, tokens.Object, Mock.Of<IAppEmailSender>(), Mock.Of<IGoogleTokenValidator>(), microsoft.Object, NullLogger<AuthController>.Instance, Mock.Of<ITwoFactorPendingTokenService>(), TestHostFactory.CreateInMemoryDb())
{
ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() }
};
var result = await controller.ExchangeMicrosoftToken(new AuthController.MicrosoftTokenRequest("token"), CancellationToken.None);
Assert.IsType<OkObjectResult>(result);
tokens.Verify(x => x.CreateAccessTokenAsync(userB, It.IsAny<string?>(), It.IsAny<CancellationToken>()), Times.Once);
tokens.Verify(x => x.CreateAccessTokenAsync(userA, It.IsAny<string?>(), It.IsAny<CancellationToken>()), Times.Never);
}
[Fact]
public async Task Exchange_legacy_microsoft_link_sends_proof_but_does_not_sign_in()
{
const string tenantId = "11111111-1111-1111-1111-111111111111";
const string objectId = "22222222-2222-2222-2222-222222222222";
var legacy = new ApplicationUser { Id = "legacy-1", Email = "legacy@example.com", EmailConfirmed = true, MicrosoftSubject = objectId, MicrosoftEmail = "legacy@example.com" };
var users = CreateUserManager();
users.Setup(x => x.Users).Returns(new TestAsyncEnumerable<ApplicationUser>(new[] { legacy }));
users.Setup(x => x.GenerateUserTokenAsync(legacy, TokenOptions.DefaultProvider, It.Is<string>(p => p.Contains(tenantId) && p.Contains(objectId)))).ReturnsAsync("recovery-token");
var microsoft = new Mock<IMicrosoftTokenValidator>();
microsoft.Setup(x => x.ValidateAsync("token", It.IsAny<CancellationToken>()))
.ReturnsAsync(new MicrosoftTokenPrincipal(objectId, "legacy@example.com", false, null, null, null, tenantId, objectId));
var email = new Mock<IAppEmailSender>();
var appTokens = new Mock<ITokenService>();
var controller = new AuthController(BuildConfig(), users.Object, appTokens.Object, email.Object, Mock.Of<IGoogleTokenValidator>(), microsoft.Object, NullLogger<AuthController>.Instance, Mock.Of<ITwoFactorPendingTokenService>(), TestHostFactory.CreateInMemoryDb())
{
ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() }
};
var result = await controller.ExchangeMicrosoftToken(new AuthController.MicrosoftTokenRequest("token"), CancellationToken.None);
Assert.IsType<AcceptedResult>(result);
Assert.Null(legacy.MicrosoftTenantId);
appTokens.Verify(x => x.CreateAccessTokenAsync(It.IsAny<ApplicationUser>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()), Times.Never);
email.Verify(x => x.SendAsync("legacy@example.com", It.IsAny<string>(), It.Is<string>(body => body.Contains("microsoft-legacy-relink")), It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task Link_microsoft_requires_current_password_and_uses_canonical_pair_only()
{
const string tenantId = "11111111-1111-1111-1111-111111111111";
const string objectId = "22222222-2222-2222-2222-222222222222";
var user = new ApplicationUser { Id = "user-1", Email = "local@example.com", EmailConfirmed = true };
var users = CreateUserManager();
users.Setup(x => x.GetUserAsync(It.IsAny<System.Security.Claims.ClaimsPrincipal>())).ReturnsAsync(user);
users.Setup(x => x.HasPasswordAsync(user)).ReturnsAsync(true);
users.Setup(x => x.CheckPasswordAsync(user, "correct-password")).ReturnsAsync(true);
users.Setup(x => x.Users).Returns(new TestAsyncEnumerable<ApplicationUser>(Array.Empty<ApplicationUser>()));
users.Setup(x => x.UpdateAsync(user)).ReturnsAsync(IdentityResult.Success);
var microsoft = new Mock<IMicrosoftTokenValidator>();
microsoft.Setup(x => x.ValidateAsync("token", It.IsAny<CancellationToken>()))
.ReturnsAsync(new MicrosoftTokenPrincipal(objectId, "metadata@example.com", false, null, null, null, tenantId, objectId));
var controller = new AuthController(BuildConfig(), users.Object, Mock.Of<ITokenService>(), Mock.Of<IAppEmailSender>(), Mock.Of<IGoogleTokenValidator>(), microsoft.Object, NullLogger<AuthController>.Instance, Mock.Of<ITwoFactorPendingTokenService>(), TestHostFactory.CreateInMemoryDb())
{
ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() }
};
var result = await controller.LinkMicrosoft(new AuthController.MicrosoftTokenRequest("token", CurrentPassword: "correct-password"), CancellationToken.None);
Assert.IsType<OkObjectResult>(result.Result);
Assert.Equal(tenantId, user.MicrosoftTenantId);
Assert.Equal(objectId, user.MicrosoftObjectId);
Assert.Null(user.MicrosoftSubject);
}
[Fact]
public async Task Unlink_microsoft_refuses_a_passwordless_last_credential()
{
var user = new ApplicationUser { Id = "user-1", MicrosoftTenantId = "11111111-1111-1111-1111-111111111111", MicrosoftObjectId = "22222222-2222-2222-2222-222222222222" };
var users = CreateUserManager();
users.Setup(x => x.GetUserAsync(It.IsAny<System.Security.Claims.ClaimsPrincipal>())).ReturnsAsync(user);
users.Setup(x => x.HasPasswordAsync(user)).ReturnsAsync(false);
var controller = new AuthController(BuildConfig(), users.Object, Mock.Of<ITokenService>(), Mock.Of<IAppEmailSender>(), Mock.Of<IGoogleTokenValidator>(), Mock.Of<IMicrosoftTokenValidator>(), NullLogger<AuthController>.Instance, Mock.Of<ITwoFactorPendingTokenService>(), TestHostFactory.CreateInMemoryDb());
var result = await controller.UnlinkMicrosoft(new AuthController.MicrosoftUnlinkRequest("irrelevant"), CancellationToken.None);
Assert.IsType<BadRequestObjectResult>(result);
Assert.NotNull(user.MicrosoftTenantId);
users.Verify(x => x.UpdateAsync(user), Times.Never);
}
[Fact]
public async Task Confirm_legacy_relink_requires_matching_microsoft_proof()
{
const string tenantId = "11111111-1111-1111-1111-111111111111";
const string objectId = "22222222-2222-2222-2222-222222222222";
var user = new ApplicationUser { Id = "legacy-1", Email = "legacy@example.com", EmailConfirmed = true, MicrosoftSubject = objectId };
var users = CreateUserManager();
users.Setup(x => x.FindByIdAsync(user.Id)).ReturnsAsync(user);
users.Setup(x => x.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, It.Is<string>(p => p.Contains(tenantId) && p.Contains(objectId)), "recovery-token")).ReturnsAsync(true);
users.Setup(x => x.Users).Returns(new TestAsyncEnumerable<ApplicationUser>(new[] { user }));
users.Setup(x => x.UpdateAsync(user)).ReturnsAsync(IdentityResult.Success);
var microsoft = new Mock<IMicrosoftTokenValidator>();
microsoft.Setup(x => x.ValidateAsync("fresh-token", It.IsAny<CancellationToken>()))
.ReturnsAsync(new MicrosoftTokenPrincipal(objectId, "metadata@example.com", false, null, null, null, tenantId, objectId));
var controller = new AuthController(BuildConfig(), users.Object, Mock.Of<ITokenService>(), Mock.Of<IAppEmailSender>(), Mock.Of<IGoogleTokenValidator>(), microsoft.Object, NullLogger<AuthController>.Instance, Mock.Of<ITwoFactorPendingTokenService>(), TestHostFactory.CreateInMemoryDb())
{
ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() }
};
var result = await controller.ConfirmMicrosoftLegacyRelink(new AuthController.ConfirmMicrosoftLegacyRelinkRequest(user.Id, tenantId, objectId, "recovery-token", "fresh-token"), CancellationToken.None);
Assert.IsType<NoContentResult>(result);
Assert.Equal(tenantId, user.MicrosoftTenantId);
Assert.Equal(objectId, user.MicrosoftObjectId);
Assert.Equal("metadata@example.com", user.MicrosoftEmail);
}
[Fact]
public void Me_result_includes_google_link_details_for_local_users()
{