230 lines
8.3 KiB
C#
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();
|
|
}
|
|
}
|