using Store.Users.Entities; using Store.Users.Services; using MAX.Framework.Commons; namespace Store.Users.Tests; public sealed class UserAuthenticationTests { [Theory] [InlineData("", "password")] [InlineData(" ", "password")] [InlineData("main", "")] [InlineData("missing", "legacy-password")] [InlineData("main", "wrong")] [InlineData("main' OR 1=1 --", "legacy-password")] public async Task InvalidCredentials_ReturnNullWithoutChangingPassword(string name, string password) { using var context = new UserTestContext(); Assert.Null(await context.Authentication.Authenticate(name, password)); Assert.Equal("legacy-password", (await context.Read("main")).Password); } [Fact] public async Task LegacyPassword_RemainsUnchangedAfterRepeatedLogins() { using var context = new UserTestContext(); var info = await context.Authentication.Authenticate(" main ", "legacy-password"); Assert.Equal("main", info!.UserName); var user = await context.Read("main"); Assert.Equal("legacy-password", user.Password); Assert.True(UserPassword.Verify(user, "legacy-password")); Assert.NotNull(await context.Authentication.Authenticate("main", "legacy-password")); Assert.Equal("legacy-password", (await context.Read("main")).Password); } [Fact] public async Task HashedPassword_RemainsUnchangedAfterLogin() { using var context = new UserTestContext(); context.CurrentUser.UserName = "main"; await context.Users.ChangePassword(new() { OldPassword = "legacy-password", NewPassword = "new-password" }, default); var hash = (await context.Read("main")).Password; Assert.NotNull(await context.Authentication.Authenticate("main", "new-password")); Assert.Equal(hash, (await context.Read("main")).Password); } [Fact] public void PasswordHashes_AreSalted_AndRejectWrongPassword() { var user = new UserEntity(); user.Password = UserPassword.Hash(user, "test-password"); Assert.NotEqual(user.Password, UserPassword.Hash(user, "test-password")); Assert.True(UserPassword.Verify(user, "test-password")); Assert.False(UserPassword.Verify(user, "wrong-password")); } [Theory] [InlineData("AQAAAAinvalid!")] [InlineData("AAinvalid!")] [InlineData("")] public void MalformedPasswordHash_IsRejected(string hash) => Assert.False(UserPassword.Verify(new UserEntity { Password = hash }, "test-password")); [Fact] public async Task WrongOldPassword_DoesNotChangePasswordOrSignOut() { using var context = new UserTestContext(); context.CurrentUser.UserName = "main"; var error = await Assert.ThrowsAsync(() => context.Users.ChangePassword( new() { OldPassword = "wrong", NewPassword = "new-password" }, default)); Assert.Equal("原密码错误", error.Message); Assert.Equal("legacy-password", (await context.Read("main")).Password); Assert.Empty(context.Sessions.SignedOut); } [Fact] public async Task ChangePassword_InvalidatesOldPasswordAndSession() { using var context = new UserTestContext(); context.CurrentUser.UserName = "main"; await context.Users.ChangePassword(new() { OldPassword = "legacy-password", NewPassword = "new-password" }, default); Assert.Contains("main", context.Sessions.SignedOut); Assert.Null(await context.Authentication.Authenticate("main", "legacy-password")); Assert.NotNull(await context.Authentication.Authenticate("main", "new-password")); } [Fact] public async Task CancelledAuthentication_ThrowsBeforeChangingPassword() { using var context = new UserTestContext(); using var cancellation = new CancellationTokenSource(); cancellation.Cancel(); await Assert.ThrowsAnyAsync(() => context.Authentication.Authenticate("main", "legacy-password", cancellation.Token)); Assert.Equal("legacy-password", (await context.Read("main")).Password); } }