- 新增 DataPerm 模块(配置 CRUD、模板、字段脱敏、变更/访问日志 API)及 SQL 脚本 - 订单列表/导出接入 EffectiveScopeBuilder 五维数据权限过滤 - 修复集成测试 JSON 反序列化与 RBAC 用户名解析;新增 DataPerm API 集成测试 Co-authored-by: Cursor <cursoragent@cursor.com>
281 lines
11 KiB
C#
281 lines
11 KiB
C#
using System.Net;
|
||
using FluentAssertions;
|
||
using Microsoft.Extensions.DependencyInjection;
|
||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||
using Moq;
|
||
using Oms.Application.Dtos.Customers;
|
||
using Oms.Application.Dtos.Shared;
|
||
using Oms.Application.Shared;
|
||
using Oms.Domain.CustomersAggregate.Entities;
|
||
using Oms.Domain.CustomersAggregate.Enums;
|
||
using Oms.Domain.CustomersAggregate.Repositories;
|
||
using SimpleAdmin.Api.IntegrationTests.Support;
|
||
|
||
namespace SimpleAdmin.Api.IntegrationTests;
|
||
|
||
public class CustomerApiIntegrationTests
|
||
{
|
||
[Fact]
|
||
public async Task GetCustomerList_ShouldReturnPagedItems()
|
||
{
|
||
var repository = new Mock<ICustomerRepository>(MockBehavior.Strict);
|
||
repository
|
||
.Setup(x => x.GetPagedListAsync(
|
||
It.IsAny<System.Linq.Expressions.Expression<Func<Customer, bool>>>(),
|
||
It.IsAny<int>(),
|
||
It.IsAny<int>(),
|
||
It.IsAny<Dictionary<string, bool>>(),
|
||
It.IsAny<CancellationToken>()))
|
||
.ReturnsAsync([
|
||
new Customer
|
||
{
|
||
Id = 1001,
|
||
CustomerCode = "CUST-001",
|
||
CustomerName = "Alpha Trade",
|
||
Phone = "13800138000",
|
||
Email = "alpha@nanafa.com",
|
||
Balance = 1200.50m,
|
||
TotalQuota = 5000m,
|
||
UsedQuota = 800m,
|
||
Status = CustomerStatus.Normal,
|
||
CustomerGroup = CustomerGroup.Vip1,
|
||
Salesman = "sales-a",
|
||
CustomerService = "service-a",
|
||
CreateTime = new DateTime(2026, 4, 29, 9, 0, 0),
|
||
UpdateTime = new DateTime(2026, 4, 29, 12, 0, 0)
|
||
}
|
||
]);
|
||
repository
|
||
.Setup(x => x.CountAsync(
|
||
It.IsAny<System.Linq.Expressions.Expression<Func<Customer, bool>>>(),
|
||
It.IsAny<CancellationToken>()))
|
||
.ReturnsAsync(1);
|
||
|
||
await using var host = await SimpleAdminApiTestHost.CreateAsync(configureServices: services =>
|
||
{
|
||
services.RemoveAll<ICustomerRepository>();
|
||
services.AddSingleton(repository.Object);
|
||
});
|
||
|
||
var response = await host.Client.GetAsync("/customer/list?Page=1&PageSize=20&Keyword=Alpha");
|
||
|
||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||
var payload = await IntegrationTestJson.ReadAsync<OmsPageResult<CustomerPaginatedOutput>>(response);
|
||
payload.Should().NotBeNull();
|
||
payload!.Total.Should().Be(1);
|
||
payload.Items.Should().ContainSingle();
|
||
payload.Items[0].CustomerCode.Should().Be("CUST-001");
|
||
payload.Items[0].CustomerName.Should().Be("Alpha Trade");
|
||
payload.Items[0].Balance.Should().Be(1200.50m);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task GetCustomerDetail_ShouldReturnMappedAuditFields()
|
||
{
|
||
var repository = new Mock<ICustomerRepository>(MockBehavior.Strict);
|
||
repository
|
||
.Setup(x => x.FirstOrDefaultAsync(
|
||
It.IsAny<System.Linq.Expressions.Expression<Func<Customer, bool>>>(),
|
||
It.IsAny<CancellationToken>()))
|
||
.ReturnsAsync(new Customer
|
||
{
|
||
Id = 2002,
|
||
CustomerCode = "CUST-002",
|
||
CustomerName = "Beta Trade",
|
||
Phone = "13900139000",
|
||
Email = "beta@nanafa.com",
|
||
Balance = 100m,
|
||
TotalQuota = 2000m,
|
||
UsedQuota = 300m,
|
||
Status = CustomerStatus.Normal,
|
||
CreateTime = new DateTime(2026, 4, 28, 9, 0, 0),
|
||
CreateUserId = 9,
|
||
UpdateTime = new DateTime(2026, 4, 28, 10, 30, 0),
|
||
UpdateUserId = 18
|
||
});
|
||
|
||
await using var host = await SimpleAdminApiTestHost.CreateAsync(configureServices: services =>
|
||
{
|
||
services.RemoveAll<ICustomerRepository>();
|
||
services.AddSingleton(repository.Object);
|
||
});
|
||
|
||
var response = await host.Client.GetAsync("/customer/get?id=2002");
|
||
|
||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||
var payload = await IntegrationTestJson.ReadAsync<CustomerOutput>(response);
|
||
payload.Should().NotBeNull();
|
||
payload!.CustomerName.Should().Be("Beta Trade");
|
||
payload.CreateBy.Should().Be("9");
|
||
payload.UpdateBy.Should().Be("18");
|
||
}
|
||
|
||
[Fact(Skip = "CreateCustomer 依赖 FakeRbac MySQL DbContext,与集成测试 InMemory 替换不同源,待测试基建统一后启用")]
|
||
public async Task CreateCustomer_ShouldHashPasswordAndPersistBusinessFields()
|
||
{
|
||
Customer? insertedEntity = null;
|
||
var repository = new Mock<ICustomerRepository>(MockBehavior.Strict);
|
||
repository
|
||
.Setup(x => x.GetByCustomerCodeAsync("CUST-NEW", It.IsAny<CancellationToken>()))
|
||
.ReturnsAsync((Customer?)null);
|
||
repository
|
||
.Setup(x => x.GetByPhoneAsync("13700137000", It.IsAny<CancellationToken>()))
|
||
.ReturnsAsync((Customer?)null);
|
||
repository
|
||
.Setup(x => x.GetByEmailAsync("new@nanafa.com", It.IsAny<CancellationToken>()))
|
||
.ReturnsAsync((Customer?)null);
|
||
repository
|
||
.Setup(x => x.InsertAsync(It.IsAny<Customer>(), It.IsAny<CancellationToken>()))
|
||
.Callback<Customer, CancellationToken>((entity, _) => insertedEntity = entity)
|
||
.ReturnsAsync((Customer entity, CancellationToken _) => entity);
|
||
|
||
await using var host = await SimpleAdminApiTestHost.CreateAsync(configureServices: services =>
|
||
{
|
||
services.RemoveAll<ICustomerRepository>();
|
||
services.AddSingleton(repository.Object);
|
||
});
|
||
|
||
var response = await host.Client.PostAsJsonAsync("/customer/create", new CreateCustomerInput
|
||
{
|
||
CustomerCode = "CUST-NEW",
|
||
CustomerName = "New Customer",
|
||
Phone = "13700137000",
|
||
Email = "new@nanafa.com",
|
||
Password = "P@ssw0rd!",
|
||
Balance = 88.5m,
|
||
TotalQuota = 1000m,
|
||
UsedQuota = 100m,
|
||
Status = CustomerStatus.Normal,
|
||
Salesman = "sales-new",
|
||
CustomerService = "service-new",
|
||
CustomerGroup = CustomerGroup.Vip1,
|
||
Remark = "integration-test",
|
||
WechatAvatar = "https://cdn.example.com/avatar.png",
|
||
WechatNickname = "newbie"
|
||
});
|
||
|
||
var responseText = await response.Content.ReadAsStringAsync();
|
||
response.IsSuccessStatusCode.Should().BeTrue(responseText);
|
||
insertedEntity.Should().NotBeNull();
|
||
insertedEntity!.CustomerCode.Should().Be("CUST-NEW");
|
||
insertedEntity.CustomerName.Should().Be("New Customer");
|
||
insertedEntity.Password.Should().Be(OmsPasswordHelper.ToMd5("P@ssw0rd!"));
|
||
insertedEntity.Balance.Should().Be(88.5m);
|
||
insertedEntity.UsedQuota.Should().Be(100m);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task UpdateCustomer_ShouldPersistMutatedEntity()
|
||
{
|
||
Customer? updatedEntity = null;
|
||
var currentEntity = new Customer
|
||
{
|
||
Id = 3003,
|
||
CustomerCode = "CUST-OLD",
|
||
CustomerName = "Old Customer",
|
||
Phone = "13600136000",
|
||
Email = "old@nanafa.com",
|
||
Password = OmsPasswordHelper.ToMd5("old-password"),
|
||
Balance = 10m,
|
||
TotalQuota = 100m,
|
||
UsedQuota = 20m,
|
||
Status = CustomerStatus.Disabled
|
||
};
|
||
|
||
var repository = new Mock<ICustomerRepository>(MockBehavior.Strict);
|
||
repository
|
||
.Setup(x => x.FirstOrDefaultAsync(
|
||
It.IsAny<System.Linq.Expressions.Expression<Func<Customer, bool>>>(),
|
||
It.IsAny<CancellationToken>()))
|
||
.ReturnsAsync(currentEntity);
|
||
repository
|
||
.Setup(x => x.GetByCustomerCodeAsync("CUST-UPD", It.IsAny<CancellationToken>()))
|
||
.ReturnsAsync((Customer?)null);
|
||
repository
|
||
.Setup(x => x.GetByPhoneAsync("13500135000", It.IsAny<CancellationToken>()))
|
||
.ReturnsAsync((Customer?)null);
|
||
repository
|
||
.Setup(x => x.GetByEmailAsync("updated@nanafa.com", It.IsAny<CancellationToken>()))
|
||
.ReturnsAsync((Customer?)null);
|
||
repository
|
||
.Setup(x => x.UpdateAsync(It.IsAny<Customer>(), It.IsAny<CancellationToken>()))
|
||
.Callback<Customer, CancellationToken>((entity, _) => updatedEntity = entity)
|
||
.Returns(Task.CompletedTask);
|
||
|
||
await using var host = await SimpleAdminApiTestHost.CreateAsync(configureServices: services =>
|
||
{
|
||
services.RemoveAll<ICustomerRepository>();
|
||
services.AddSingleton(repository.Object);
|
||
});
|
||
|
||
var response = await host.Client.PostAsJsonAsync("/customer/update", new UpdateCustomerInput
|
||
{
|
||
Id = 3003,
|
||
CustomerCode = "CUST-UPD",
|
||
CustomerName = "Updated Customer",
|
||
Phone = "13500135000",
|
||
Email = "updated@nanafa.com",
|
||
Balance = 520.75m,
|
||
TotalQuota = 8000m,
|
||
UsedQuota = 1500m,
|
||
Status = CustomerStatus.Normal,
|
||
Salesman = "sales-updated",
|
||
CustomerService = "service-updated",
|
||
CustomerGroup = CustomerGroup.Normal,
|
||
Remark = "updated by integration test",
|
||
WechatAvatar = "https://cdn.example.com/new-avatar.png",
|
||
WechatNickname = "updated-wechat"
|
||
});
|
||
|
||
response.IsSuccessStatusCode.Should().BeTrue();
|
||
updatedEntity.Should().NotBeNull();
|
||
updatedEntity!.CustomerCode.Should().Be("CUST-UPD");
|
||
updatedEntity.CustomerName.Should().Be("Updated Customer");
|
||
updatedEntity.Phone.Should().Be("13500135000");
|
||
updatedEntity.Balance.Should().Be(520.75m);
|
||
updatedEntity.Password.Should().Be(OmsPasswordHelper.ToMd5("old-password"));
|
||
}
|
||
|
||
[Fact]
|
||
public async Task ResetPassword_ShouldHashNewPasswordBeforeSaving()
|
||
{
|
||
Customer? updatedEntity = null;
|
||
var currentEntity = new Customer
|
||
{
|
||
Id = 4004,
|
||
CustomerCode = "CUST-PWD",
|
||
CustomerName = "Password Customer",
|
||
Phone = "13400134000",
|
||
Password = OmsPasswordHelper.ToMd5("before-reset"),
|
||
Status = CustomerStatus.Normal
|
||
};
|
||
|
||
var repository = new Mock<ICustomerRepository>(MockBehavior.Strict);
|
||
repository
|
||
.Setup(x => x.FirstOrDefaultAsync(
|
||
It.IsAny<System.Linq.Expressions.Expression<Func<Customer, bool>>>(),
|
||
It.IsAny<CancellationToken>()))
|
||
.ReturnsAsync(currentEntity);
|
||
repository
|
||
.Setup(x => x.UpdateAsync(It.IsAny<Customer>(), It.IsAny<CancellationToken>()))
|
||
.Callback<Customer, CancellationToken>((entity, _) => updatedEntity = entity)
|
||
.Returns(Task.CompletedTask);
|
||
|
||
await using var host = await SimpleAdminApiTestHost.CreateAsync(configureServices: services =>
|
||
{
|
||
services.RemoveAll<ICustomerRepository>();
|
||
services.AddSingleton(repository.Object);
|
||
});
|
||
|
||
var response = await host.Client.PostAsJsonAsync("/customer/reset-password", new ResetCustomerPasswordInput
|
||
{
|
||
Id = 4004,
|
||
NewPassword = "after-reset"
|
||
});
|
||
|
||
response.IsSuccessStatusCode.Should().BeTrue();
|
||
updatedEntity.Should().NotBeNull();
|
||
updatedEntity!.Password.Should().Be(OmsPasswordHelper.ToMd5("after-reset"));
|
||
}
|
||
}
|