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

216 lines
7.9 KiB
C#

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
using VendingMachine.Application.DTOs.Vending;
using VendingMachine.Domain.Entities;
using VendingMachine.Infrastructure.Data;
using VendingMachine.Infrastructure.Services;
using Xunit;
namespace VendingMachine.Tests;
/// <summary>
/// 集成测试:贩卖机扫码-锁定-支付回调-解锁 完整流程
/// </summary>
public class VendingFlowIntegrationTests : IDisposable
{
private readonly AppDbContext _db;
private readonly PointsService _pointsService;
private readonly VendingMachineService _vendingService;
private readonly InMemoryRedisStore _redis;
public VendingFlowIntegrationTests()
{
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.ConfigureWarnings(w => w.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.InMemoryEventId.TransactionIgnoredWarning))
.Options;
_db = new AppDbContext(options);
_redis = new InMemoryRedisStore();
_pointsService = new PointsService(_db, NullLogger<PointsService>.Instance);
_vendingService = new VendingMachineService(
_db, _redis, _pointsService, NullLogger<VendingMachineService>.Instance);
}
/// <summary>
/// 完整贩卖机流程:生成二维码 → 扫码获取用户信息(锁定)→ 支付回调(积分发放+优惠券核销+解锁)
/// </summary>
[Fact]
public async Task FullVendingFlow_Qrcode_Lock_PaymentCallback_Unlock()
{
// === 种子数据:会员用户 + 积分配置 + 可用优惠券 ===
var user = new User
{
Phone = "1234567890",
AreaCode = "+86",
Nickname = "用户888888",
IsMember = true,
MembershipType = MembershipType.Monthly,
MembershipExpireAt = DateTime.UtcNow.AddDays(20),
PointsBalance = 500
};
_db.Users.Add(user);
var pointsConfig = new PointsConfig { Id = 1, ConversionRate = 1m };
_db.PointsConfigs.Add(pointsConfig);
var couponTemplate = new CouponTemplate
{
Id = "tpl-10off",
NameZhCn = "满100减10",
Type = CouponType.ThresholdDiscount,
ThresholdAmount = 100m,
DiscountAmount = 10m,
PointsCost = 0,
ExpireAt = DateTime.UtcNow.AddDays(30),
IsActive = true
};
_db.CouponTemplates.Add(couponTemplate);
var userCoupon = new UserCoupon
{
CouponTemplateId = "tpl-10off",
UserId = user.Uid,
Status = CouponStatus.Available,
ExpireAt = DateTime.UtcNow.AddDays(30)
};
_db.UserCoupons.Add(userCoupon);
await _db.SaveChangesAsync();
var couponId = userCoupon.Id;
// === 第一步:生成会员二维码 ===
var qrcodeResult = await _vendingService.GenerateQrcodeAsync(user.Uid);
Assert.True(qrcodeResult.Success);
Assert.False(string.IsNullOrEmpty(qrcodeResult.Data!.Token));
Assert.True(qrcodeResult.Data.ExpiresAt > 0);
var qrcodeToken = qrcodeResult.Data.Token;
// === 第二步:贩卖机扫码获取用户信息(同时锁定用户) ===
var userInfoResult = await _vendingService.GetUserByQrcodeAsync(
new QrcodeRequest { QrcodeToken = qrcodeToken }, "machine-001");
Assert.True(userInfoResult.Success);
Assert.Equal(user.Uid, userInfoResult.Data!.UserId);
Assert.True(userInfoResult.Data.IsMember);
Assert.False(userInfoResult.Data.IsLocked);
Assert.NotEmpty(userInfoResult.Data.Coupons);
// === 第三步:验证用户已被锁定(其他贩卖机请求应返回锁定状态) ===
// 需要重新生成二维码(上一个已被消费)
var qrcode2 = await _vendingService.GenerateQrcodeAsync(user.Uid);
var lockedResult = await _vendingService.GetUserByQrcodeAsync(
new QrcodeRequest { QrcodeToken = qrcode2.Data!.Token }, "machine-002");
Assert.True(lockedResult.Success);
Assert.True(lockedResult.Data!.IsLocked); // 被 machine-001 锁定
// === 第四步:支付回调(成功支付,使用优惠券) ===
var initialBalance = (await _db.Users.FirstAsync(u => u.Uid == user.Uid)).PointsBalance;
var paymentResult = await _vendingService.ReportPaymentAsync(new VendingPaymentPayload
{
UserId = user.Uid,
MachineId = "machine-001",
PaymentAmount = 150m,
UsedCouponId = couponId,
PaymentStatus = "success",
TransactionId = "txn-001"
});
Assert.True(paymentResult.Success);
// === 第五步:验证积分已发放 ===
var updatedUser = await _db.Users.FirstAsync(u => u.Uid == user.Uid);
// 150 * 1 = 150 积分
Assert.Equal(initialBalance + 150, updatedUser.PointsBalance);
// === 第六步:验证优惠券已核销 ===
var usedCoupon = await _db.UserCoupons.FirstAsync(uc => uc.Id == couponId);
Assert.Equal(CouponStatus.Used, usedCoupon.Status);
Assert.NotNull(usedCoupon.UsedAt);
// === 第七步:验证用户锁定已解除 ===
var lockValue = await _redis.GetAsync($"vending:lock:{user.Uid}");
Assert.Null(lockValue);
}
/// <summary>
/// 非会员用户无法生成二维码
/// </summary>
[Fact]
public async Task GenerateQrcode_NonMember_ShouldFail()
{
var user = new User
{
Phone = "9999999999",
AreaCode = "+86",
Nickname = "用户000000",
IsMember = false
};
_db.Users.Add(user);
await _db.SaveChangesAsync();
var result = await _vendingService.GenerateQrcodeAsync(user.Uid);
Assert.False(result.Success);
Assert.Contains("非会员", result.Message);
}
/// <summary>
/// 过期二维码应被拒绝
/// </summary>
[Fact]
public async Task GetUserByQrcode_ExpiredToken_ShouldFail()
{
var result = await _vendingService.GetUserByQrcodeAsync(
new QrcodeRequest { QrcodeToken = "invalid-or-expired-token" }, "machine-001");
Assert.False(result.Success);
Assert.Contains("无效", result.Message);
}
/// <summary>
/// 支付失败时也应解除用户锁定
/// </summary>
[Fact]
public async Task PaymentCallback_Failed_ShouldStillUnlockUser()
{
var user = new User
{
Phone = "5555555555",
AreaCode = "+86",
Nickname = "用户555555",
IsMember = true,
MembershipType = MembershipType.Monthly,
MembershipExpireAt = DateTime.UtcNow.AddDays(10),
PointsBalance = 100
};
_db.Users.Add(user);
_db.PointsConfigs.Add(new PointsConfig { Id = 1, ConversionRate = 1m });
await _db.SaveChangesAsync();
// 模拟用户被锁定
await _redis.SetAsync($"vending:lock:{user.Uid}", "machine-003", TimeSpan.FromMinutes(10));
// 支付失败回调
var result = await _vendingService.ReportPaymentAsync(new VendingPaymentPayload
{
UserId = user.Uid,
MachineId = "machine-003",
PaymentAmount = 50m,
PaymentStatus = "failed",
TransactionId = "txn-fail-001"
});
Assert.True(result.Success);
// 验证锁定已解除
var lockValue = await _redis.GetAsync($"vending:lock:{user.Uid}");
Assert.Null(lockValue);
// 验证积分未变化(支付失败不发放积分)
var updatedUser = await _db.Users.FirstAsync(u => u.Uid == user.Uid);
Assert.Equal(100, updatedUser.PointsBalance);
}
public void Dispose()
{
_db.Dispose();
}
}