using MS.Microservice.Core.Domain.Repository; using MS.Microservice.Core.Functional; using MS.Microservice.Domain.Aggregates.IdentityModel; using MS.Microservice.Domain.Aggregates.IdentityModel.Repository; using MS.Microservice.Domain.Consts; using MS.Microservice.Domain.Exception; using MS.Microservice.Domain.Services; using NSubstitute; using System.Linq.Expressions; using Xunit; namespace MS.Microservice.Infrastructure.Tests { /// /// 覆盖创建用户的函数式领域流程: /// 使用 Option 显式表达“用户已存在 / 用户不存在”的分支。 /// public class UserDomainServiceTests { private sealed class PersistedUser : User { public PersistedUser(int id, string account) : base(account, "Password123", "salt", false, "13800138000", 1, 1, "demo@example.com", "Demo", "", "") { Id = id; } } [Fact] public async Task CreateUserEitherAsync_WhenUserAlreadyExists_ReturnsLeft() { var repository = Substitute.For(); var service = new UserDomainService(repository); var existingUser = new PersistedUser(1, "demo"); repository.FindOptionAsync(Arg.Any>>(), Arg.Any()) .Returns((Option)existingUser); var candidate = new User("demo", "Password123", "salt", false, "13800138001", 1, 1, "demo2@example.com", "Demo2", "", ""); var result = await service.CreateUserEitherAsync(candidate); Assert.True(result.IsLeft); Assert.Equal("conflict", result.Left.Code); Assert.Equal(ExceptionConsts.UserExisted, result.Left.Message); } [Fact] public async Task CreateUserEitherAsync_WhenUserDoesNotExist_PersistsAndReturnsTrue() { var repository = Substitute.For(); var unitOfWork = Substitute.For(); repository.UnitOfWork.Returns(unitOfWork); repository.FindOptionAsync(Arg.Any>>(), Arg.Any()) .Returns((Option)F.None); repository.InsertEitherAsync(Arg.Any(), Arg.Any()) .Returns(call => (Either)F.Right(call.Arg())); unitOfWork.SaveChangesAsync(Arg.Any()) .Returns(1); var service = new UserDomainService(repository); var candidate = new User("demo", "placeholder", string.Empty, false, "13800138000", 1, 1, "demo@example.com", "Demo", "", ""); candidate.SetPasswordHash("versioned-password-hash"); var result = await service.CreateUserEitherAsync(candidate); Assert.True(result.IsRight); Assert.True(result.Right); Assert.Equal("versioned-password-hash", candidate.Password); Assert.Equal(User.ModernPasswordSaltMarker, candidate.Salt); await repository.Received(1).InsertEitherAsync(candidate, Arg.Any()); await unitOfWork.Received(1).SaveChangesAsync(Arg.Any()); } [Fact] public async Task CreateUserEitherAsync_WhenPasswordIsNotVersioned_ReturnsValidationError() { var repository = Substitute.For(); repository.FindOptionAsync(Arg.Any>>(), Arg.Any()) .Returns((Option)F.None); var service = new UserDomainService(repository); var candidate = new User("demo", "Password123", "a1b2", false, "13800138000", 1, 1, "demo@example.com", "Demo", "", ""); var result = await service.CreateUserEitherAsync(candidate); Assert.True(result.IsLeft); Assert.Equal("validation", result.Left.Code); await repository.DidNotReceiveWithAnyArgs().InsertEitherAsync(default!, default); } [Fact] public async Task UpdatePasswordHashAsync_SetsVersionedHashAndPersistsUser() { var repository = Substitute.For(); var unitOfWork = Substitute.For(); repository.UnitOfWork.Returns(unitOfWork); repository.UpdateAsync(Arg.Any(), Arg.Any()) .Returns(call => call.Arg()); unitOfWork.SaveEntitiesAsync(Arg.Any()) .Returns(true); var service = new UserDomainService(repository); var user = new PersistedUser(1, "demo"); var result = await service.UpdatePasswordHashAsync(user, "versioned-password-hash"); Assert.True(result); Assert.Equal("versioned-password-hash", user.Password); Assert.Equal(User.ModernPasswordSaltMarker, user.Salt); await repository.Received(1).UpdateAsync(user, Arg.Any()); await unitOfWork.Received(1).SaveEntitiesAsync(Arg.Any()); } } }