using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; using VendingMachine.Application.DTOs.Membership; using VendingMachine.Application.DTOs.User; using VendingMachine.Domain.Entities; using VendingMachine.Infrastructure.Data; using VendingMachine.Infrastructure.Services; using Xunit; namespace VendingMachine.Tests; /// /// 集成测试:用户注册-登录-购买会员-获取积分-兑换优惠券 完整流程 /// public class UserJourneyIntegrationTests : IDisposable { private readonly AppDbContext _db; private readonly UserService _userService; private readonly MembershipService _membershipService; private readonly PointsService _pointsService; private readonly CouponService _couponService; private readonly InMemoryVerificationCodeStore _codeStore; public UserJourneyIntegrationTests() { // 使用 InMemory 数据库(忽略事务警告) var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .ConfigureWarnings(w => w.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.InMemoryEventId.TransactionIgnoredWarning)) .Options; _db = new AppDbContext(options); // JWT 配置 var config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { ["Jwt:Secret"] = "TestSecretKeyForIntegrationTests_AtLeast32Chars!", ["Jwt:Issuer"] = "Test", ["Jwt:Audience"] = "Test", ["Jwt:ExpirationMinutes"] = "60" }) .Build(); _codeStore = new InMemoryVerificationCodeStore(); _userService = new UserService(_db, _codeStore, config, NullLogger.Instance); _membershipService = new MembershipService(_db, NullLogger.Instance); _pointsService = new PointsService(_db, NullLogger.Instance); _couponService = new CouponService(_db, NullLogger.Instance); } /// /// 完整用户旅程:注册 → 登录 → 购买会员 → 消费获取积分 → 兑换优惠券 /// [Fact] public async Task FullUserJourney_Register_Login_BuyMembership_EarnPoints_RedeemCoupon() { // === 第一步:种子数据 === var monthlyProduct = new MembershipProduct { Id = "monthly-30", Type = MembershipType.Monthly, Price = 99m, Currency = "TWD", DurationDays = 30, GoogleProductId = "com.vending.monthly", AppleProductId = "com.vending.monthly", DescriptionZhCn = "单月会员" }; _db.MembershipProducts.Add(monthlyProduct); var pointsConfig = new PointsConfig { Id = 1, ConversionRate = 2m }; _db.PointsConfigs.Add(pointsConfig); var couponTemplate = new CouponTemplate { Id = "coupon-5off", NameZhCn = "满50减5", NameZhTw = "滿50減5", NameEn = "5 off 50", Type = CouponType.ThresholdDiscount, ThresholdAmount = 50m, DiscountAmount = 5m, PointsCost = 100, ExpireAt = DateTime.UtcNow.AddDays(30), IsActive = true, IsStamp = false }; _db.CouponTemplates.Add(couponTemplate); await _db.SaveChangesAsync(); // === 第二步:发送验证码 === var sendResult = await _userService.SendVerificationCodeAsync( new SendCodeRequest { Phone = "1234567890", AreaCode = "+86" }); Assert.True(sendResult.Success); // 获取存储的验证码 var storedCode = await _codeStore.GetCodeAsync("sms:code:+861234567890"); Assert.NotNull(storedCode); // === 第三步:登录(自动注册新用户) === var loginResult = await _userService.LoginAsync(new LoginRequest { Phone = "1234567890", AreaCode = "+86", Code = storedCode, AgreementAccepted = true }); Assert.True(loginResult.Success); Assert.NotNull(loginResult.Data); Assert.False(string.IsNullOrEmpty(loginResult.Data.Token)); var uid = loginResult.Data.UserInfo.Uid; Assert.StartsWith("用户", loginResult.Data.UserInfo.Nickname); // === 第四步:获取用户信息 === var userInfo = await _userService.GetUserInfoAsync(uid); Assert.True(userInfo.Success); Assert.False(userInfo.Data!.IsMember); Assert.Equal(0, userInfo.Data.PointsBalance); // === 第五步:购买单月会员 === var purchaseResult = await _membershipService.PurchaseAsync(uid, new PurchaseRequest { ProductId = "monthly-30", Receipt = "valid-receipt-token", Platform = "android" }); Assert.True(purchaseResult.Success); Assert.Equal("monthly", purchaseResult.Data!.MembershipType); // 验证会员状态已更新 var memberInfo = await _membershipService.GetMembershipInfoAsync(uid); Assert.True(memberInfo.Success); Assert.True(memberInfo.Data!.IsMember); // === 第六步:模拟贩卖机消费获取积分 === var pointsResult = await _pointsService.AddPointsFromPaymentAsync(uid, 100m); Assert.True(pointsResult.Success); Assert.Equal(200, pointsResult.Data); // 100 * 2 = 200 积分 // 验证积分余额 var balance = await _pointsService.GetBalanceAsync(uid); Assert.Equal(200, balance.Data); // === 第七步:兑换优惠券(需要 100 积分) === var redeemResult = await _couponService.RedeemCouponAsync(uid, "coupon-5off"); Assert.True(redeemResult.Success); Assert.True(redeemResult.Data!.Redeemed); // 验证积分已扣减 var balanceAfter = await _pointsService.GetBalanceAsync(uid); Assert.Equal(100, balanceAfter.Data); // 200 - 100 = 100 // 验证优惠券已添加到用户 var myCoupons = await _couponService.GetMyCouponsAsync(uid, null, "zh-CN"); Assert.True(myCoupons.Success); Assert.Single(myCoupons.Data!); Assert.Equal("available", myCoupons.Data[0].Status); } /// /// 协议未勾选时登录应被阻止 /// [Fact] public async Task Login_WithoutAgreement_ShouldFail() { // 先发送验证码 await _userService.SendVerificationCodeAsync( new SendCodeRequest { Phone = "9876543210", AreaCode = "+886" }); var code = await _codeStore.GetCodeAsync("sms:code:+8869876543210"); var result = await _userService.LoginAsync(new LoginRequest { Phone = "9876543210", AreaCode = "+886", Code = code!, AgreementAccepted = false }); Assert.False(result.Success); Assert.Contains("协议", result.Message); } /// /// 积分不足时兑换优惠券应失败 /// [Fact] public async Task RedeemCoupon_InsufficientPoints_ShouldFail() { // 创建用户(无积分) var user = new User { Phone = "1111111111", AreaCode = "+86", Nickname = "用户123456", IsMember = true, MembershipType = MembershipType.Monthly, PointsBalance = 50 }; _db.Users.Add(user); var coupon = new CouponTemplate { Id = "expensive-coupon", NameZhCn = "大额优惠券", Type = CouponType.DirectDiscount, DiscountAmount = 20m, PointsCost = 500, ExpireAt = DateTime.UtcNow.AddDays(30), IsActive = true }; _db.CouponTemplates.Add(coupon); await _db.SaveChangesAsync(); var result = await _couponService.RedeemCouponAsync(user.Uid, "expensive-coupon"); Assert.False(result.Success); Assert.Contains("积分不足", result.Message); // 积分应保持不变 var balance = await _pointsService.GetBalanceAsync(user.Uid); Assert.Equal(50, balance.Data); } public void Dispose() { _db.Dispose(); } }