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