vending-machine/backend/tests/VendingMachine.Tests/UserJourneyIntegrationTests.cs
2026-04-03 06:07:13 +08:00

230 lines
8.3 KiB
C#

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;
/// <summary>
/// 集成测试:用户注册-登录-购买会员-获取积分-兑换优惠券 完整流程
/// </summary>
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<AppDbContext>()
.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<string, string?>
{
["Jwt:Secret"] = "TestSecretKeyForIntegrationTests_AtLeast32Chars!",
["Jwt:Issuer"] = "Test",
["Jwt:Audience"] = "Test",
["Jwt:ExpirationMinutes"] = "60"
})
.Build();
_codeStore = new InMemoryVerificationCodeStore();
_userService = new UserService(_db, _codeStore, config, NullLogger<UserService>.Instance);
_membershipService = new MembershipService(_db, NullLogger<MembershipService>.Instance);
_pointsService = new PointsService(_db, NullLogger<PointsService>.Instance);
_couponService = new CouponService(_db, NullLogger<CouponService>.Instance);
}
/// <summary>
/// 完整用户旅程:注册 → 登录 → 购买会员 → 消费获取积分 → 兑换优惠券
/// </summary>
[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);
}
/// <summary>
/// 协议未勾选时登录应被阻止
/// </summary>
[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);
}
/// <summary>
/// 积分不足时兑换优惠券应失败
/// </summary>
[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();
}
}