refactor: Tối ưu truy vấn cửa hàng, điều chỉnh các lệnh quản lý người bán và tinh gọn bộ kiểm thử chức năng dịch vụ xã hội.
This commit is contained in:
@@ -49,7 +49,26 @@ dotnet run --project src/MerchantService.API
|
||||
| Branches | `/api/v1/shops/{id}/branches` | Physical shop locations |
|
||||
| Staff | `/api/v1/merchants/me/staff` | Staff management |
|
||||
| POS | `/api/v1/pos` | POS device authentication |
|
||||
| Admin | `/api/v1/admin/merchants` | Administrative operations |
|
||||
| **Admin Merchants** | `/api/v1/admin/merchants` | Merchant admin operations |
|
||||
| **Admin Shops** | `/api/v1/admin/shops` | Shop admin operations |
|
||||
|
||||
### Admin Endpoints (Backoffice)
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| GET | `/api/v1/admin/merchants` | List all merchants (paginated) |
|
||||
| GET | `/api/v1/admin/merchants/{id}` | Get merchant details |
|
||||
| GET | `/api/v1/admin/merchants/statistics` | Get platform statistics |
|
||||
| POST | `/api/v1/admin/merchants/{id}/approve` | Approve merchant registration |
|
||||
| POST | `/api/v1/admin/merchants/{id}/reject` | Reject merchant registration |
|
||||
| POST | `/api/v1/admin/merchants/{id}/suspend` | Suspend active merchant |
|
||||
| POST | `/api/v1/admin/merchants/{id}/reactivate` | Reactivate suspended merchant |
|
||||
| POST | `/api/v1/admin/merchants/{id}/ban` | Permanently ban merchant |
|
||||
| GET | `/api/v1/admin/shops` | List all shops (paginated) |
|
||||
| GET | `/api/v1/admin/shops/{id}` | Get shop details |
|
||||
| POST | `/api/v1/admin/shops/{id}/suspend` | Suspend shop |
|
||||
| POST | `/api/v1/admin/shops/{id}/reactivate` | Reactivate shop |
|
||||
| POST | `/api/v1/admin/shops/{id}/close` | Close shop permanently |
|
||||
|
||||
## Project Structure
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ public class ApproveMerchantCommandHandler : IRequestHandler<ApproveMerchantComm
|
||||
ApproveMerchantCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var merchant = await _merchantRepository.GetAsync(request.MerchantId, cancellationToken)
|
||||
var merchant = await _merchantRepository.GetByIdAsync(request.MerchantId, cancellationToken)
|
||||
?? throw new DomainException($"Merchant {request.MerchantId} not found");
|
||||
|
||||
merchant.Approve(request.ApprovedBy);
|
||||
|
||||
@@ -53,7 +53,7 @@ public class BanMerchantCommandHandler : IRequestHandler<BanMerchantCommand, Ban
|
||||
if (string.IsNullOrWhiteSpace(request.Reason))
|
||||
throw new DomainException("Ban reason is required");
|
||||
|
||||
var merchant = await _merchantRepository.GetAsync(request.MerchantId, cancellationToken)
|
||||
var merchant = await _merchantRepository.GetByIdAsync(request.MerchantId, cancellationToken)
|
||||
?? throw new DomainException($"Merchant {request.MerchantId} not found");
|
||||
|
||||
merchant.Ban(request.Reason);
|
||||
|
||||
@@ -47,7 +47,7 @@ public class ReactivateMerchantCommandHandler : IRequestHandler<ReactivateMercha
|
||||
ReactivateMerchantCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var merchant = await _merchantRepository.GetAsync(request.MerchantId, cancellationToken)
|
||||
var merchant = await _merchantRepository.GetByIdAsync(request.MerchantId, cancellationToken)
|
||||
?? throw new DomainException($"Merchant {request.MerchantId} not found");
|
||||
|
||||
merchant.Reactivate();
|
||||
|
||||
@@ -53,7 +53,7 @@ public class RejectMerchantCommandHandler : IRequestHandler<RejectMerchantComman
|
||||
if (string.IsNullOrWhiteSpace(request.Reason))
|
||||
throw new DomainException("Rejection reason is required");
|
||||
|
||||
var merchant = await _merchantRepository.GetAsync(request.MerchantId, cancellationToken)
|
||||
var merchant = await _merchantRepository.GetByIdAsync(request.MerchantId, cancellationToken)
|
||||
?? throw new DomainException($"Merchant {request.MerchantId} not found");
|
||||
|
||||
// EN: Reject sets status to Rejected - need to add this method to Domain
|
||||
|
||||
@@ -53,7 +53,7 @@ public class SuspendMerchantCommandHandler : IRequestHandler<SuspendMerchantComm
|
||||
if (string.IsNullOrWhiteSpace(request.Reason))
|
||||
throw new DomainException("Suspension reason is required");
|
||||
|
||||
var merchant = await _merchantRepository.GetAsync(request.MerchantId, cancellationToken)
|
||||
var merchant = await _merchantRepository.GetByIdAsync(request.MerchantId, cancellationToken)
|
||||
?? throw new DomainException($"Merchant {request.MerchantId} not found");
|
||||
|
||||
merchant.Suspend(request.Reason);
|
||||
|
||||
@@ -37,7 +37,6 @@ public class GetAllShopsQueryHandler : IRequestHandler<GetAllShopsQuery, AdminSh
|
||||
{
|
||||
var query = _context.Shops
|
||||
.AsNoTracking()
|
||||
.Include(s => s.Merchant)
|
||||
.Where(s => !s.IsDeleted);
|
||||
|
||||
// EN: Apply filters / VI: Áp dụng bộ lọc
|
||||
@@ -62,19 +61,26 @@ public class GetAllShopsQueryHandler : IRequestHandler<GetAllShopsQuery, AdminSh
|
||||
var totalCount = await query.CountAsync(cancellationToken);
|
||||
var totalPages = (int)Math.Ceiling(totalCount / (double)request.PageSize);
|
||||
|
||||
// EN: Get shops with merchant info using join
|
||||
// VI: Lấy shops cùng với merchant info bằng join
|
||||
var items = await query
|
||||
.OrderByDescending(s => s.CreatedAt)
|
||||
.Join(
|
||||
_context.Merchants,
|
||||
shop => shop.MerchantId,
|
||||
merchant => merchant.Id,
|
||||
(shop, merchant) => new { Shop = shop, MerchantBusinessName = merchant.BusinessName })
|
||||
.OrderByDescending(x => x.Shop.CreatedAt)
|
||||
.Skip((request.Page - 1) * request.PageSize)
|
||||
.Take(request.PageSize)
|
||||
.Select(s => new AdminShopListItemDto(
|
||||
s.Id,
|
||||
s.MerchantId,
|
||||
s.Merchant.BusinessName,
|
||||
s.Name,
|
||||
s.Slug,
|
||||
s.Status.Name,
|
||||
s.Category != null ? s.Category.Name : null,
|
||||
s.CreatedAt))
|
||||
.Select(x => new AdminShopListItemDto(
|
||||
x.Shop.Id,
|
||||
x.Shop.MerchantId,
|
||||
x.MerchantBusinessName,
|
||||
x.Shop.Name,
|
||||
x.Shop.Slug,
|
||||
x.Shop.Status.Name,
|
||||
x.Shop.Category != null ? x.Shop.Category.Name : null,
|
||||
x.Shop.CreatedAt))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return new AdminShopListResultDto(
|
||||
|
||||
@@ -41,22 +41,22 @@ public class GetMerchantDetailQueryHandler : IRequestHandler<GetMerchantDetailQu
|
||||
.CountAsync(s => s.MerchantId == request.MerchantId && !s.IsDeleted, cancellationToken);
|
||||
|
||||
var staffCount = await _context.MerchantStaff
|
||||
.CountAsync(s => s.MerchantId == request.MerchantId && !s.IsDeleted, cancellationToken);
|
||||
.CountAsync(s => s.MerchantId == request.MerchantId, cancellationToken);
|
||||
|
||||
var businessInfo = merchant.BusinessInfo != null
|
||||
? new AdminBusinessInfoDto(
|
||||
merchant.BusinessInfo.TaxId,
|
||||
merchant.BusinessInfo.LegalName,
|
||||
merchant.BusinessInfo.Address?.ToString(),
|
||||
merchant.BusinessInfo.Phone,
|
||||
merchant.BusinessInfo.Email,
|
||||
merchant.BusinessInfo.Website)
|
||||
merchant.BusinessInfo.BusinessLicenseNumber,
|
||||
merchant.BusinessInfo.CompanyRegistrationNumber,
|
||||
null,
|
||||
null,
|
||||
null)
|
||||
: null;
|
||||
|
||||
var settlementConfig = merchant.SettlementConfig != null
|
||||
? new AdminSettlementConfigDto(
|
||||
merchant.SettlementConfig.CommissionRate,
|
||||
merchant.SettlementConfig.PayoutFrequency.Name,
|
||||
merchant.SettlementConfig.SettlementCycleId.ToString(),
|
||||
merchant.SettlementConfig.BankAccount?.ToString())
|
||||
: null;
|
||||
|
||||
|
||||
@@ -52,11 +52,12 @@ public class GetMerchantStatisticsQueryHandler : IRequestHandler<GetMerchantStat
|
||||
|
||||
var totalShops = await shopsQuery.CountAsync(cancellationToken);
|
||||
|
||||
var publishedShops = await shopsQuery
|
||||
.CountAsync(s => s.StatusId == ShopStatus.Published.Id, cancellationToken);
|
||||
// EN: Active shops (published/active)
|
||||
// VI: Shops đang hoạt động
|
||||
var activeShops = await shopsQuery
|
||||
.CountAsync(s => s.StatusId == ShopStatus.Active.Id, cancellationToken);
|
||||
|
||||
var totalStaff = await _context.MerchantStaff
|
||||
.CountAsync(s => !s.IsDeleted, cancellationToken);
|
||||
var totalStaff = await _context.MerchantStaff.CountAsync(cancellationToken);
|
||||
|
||||
return new AdminMerchantStatisticsDto(
|
||||
totalMerchants,
|
||||
@@ -65,7 +66,7 @@ public class GetMerchantStatisticsQueryHandler : IRequestHandler<GetMerchantStat
|
||||
suspended,
|
||||
banned,
|
||||
totalShops,
|
||||
publishedShops,
|
||||
activeShops,
|
||||
totalStaff);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Swashbuckle.AspNetCore.Annotations;
|
||||
using MerchantService.API.Application.Commands.Admin;
|
||||
using MerchantService.API.Application.Queries.Admin;
|
||||
|
||||
@@ -17,7 +16,6 @@ namespace MerchantService.API.Controllers.Admin;
|
||||
[ApiController]
|
||||
[Route("api/v1/admin/merchants")]
|
||||
[Authorize(Roles = "Admin,SuperAdmin")]
|
||||
[SwaggerTag("Admin Merchant Management / Quản lý Merchant Admin")]
|
||||
public class AdminMerchantsController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
@@ -36,8 +34,7 @@ public class AdminMerchantsController : ControllerBase
|
||||
/// VI: Lấy tất cả merchants với phân trang.
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[SwaggerOperation(Summary = "Get all merchants", Description = "Get all merchants with pagination (Admin only)")]
|
||||
[SwaggerResponse(200, "Success", typeof(AdminMerchantListResultDto))]
|
||||
[ProducesResponseType(typeof(AdminMerchantListResultDto), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> GetAllMerchants(
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20,
|
||||
@@ -55,9 +52,8 @@ public class AdminMerchantsController : ControllerBase
|
||||
/// VI: Lấy chi tiết merchant theo ID.
|
||||
/// </summary>
|
||||
[HttpGet("{merchantId:guid}")]
|
||||
[SwaggerOperation(Summary = "Get merchant by ID", Description = "Get merchant details by ID (Admin only)")]
|
||||
[SwaggerResponse(200, "Success", typeof(AdminMerchantDetailDto))]
|
||||
[SwaggerResponse(404, "Merchant not found")]
|
||||
[ProducesResponseType(typeof(AdminMerchantDetailDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetMerchantById(Guid merchantId)
|
||||
{
|
||||
try
|
||||
@@ -77,8 +73,7 @@ public class AdminMerchantsController : ControllerBase
|
||||
/// VI: Lấy thống kê merchant.
|
||||
/// </summary>
|
||||
[HttpGet("statistics")]
|
||||
[SwaggerOperation(Summary = "Get statistics", Description = "Get merchant statistics (Admin only)")]
|
||||
[SwaggerResponse(200, "Success", typeof(AdminMerchantStatisticsDto))]
|
||||
[ProducesResponseType(typeof(AdminMerchantStatisticsDto), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> GetStatistics()
|
||||
{
|
||||
var query = new GetMerchantStatisticsQuery();
|
||||
@@ -91,10 +86,9 @@ public class AdminMerchantsController : ControllerBase
|
||||
/// VI: Phê duyệt đăng ký merchant.
|
||||
/// </summary>
|
||||
[HttpPost("{merchantId:guid}/approve")]
|
||||
[SwaggerOperation(Summary = "Approve merchant", Description = "Approve merchant registration (Admin only)")]
|
||||
[SwaggerResponse(200, "Merchant approved")]
|
||||
[SwaggerResponse(400, "Cannot approve")]
|
||||
[SwaggerResponse(404, "Merchant not found")]
|
||||
[ProducesResponseType(typeof(ApproveMerchantResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ApproveMerchant(Guid merchantId)
|
||||
{
|
||||
try
|
||||
@@ -118,10 +112,9 @@ public class AdminMerchantsController : ControllerBase
|
||||
/// VI: Từ chối đăng ký merchant.
|
||||
/// </summary>
|
||||
[HttpPost("{merchantId:guid}/reject")]
|
||||
[SwaggerOperation(Summary = "Reject merchant", Description = "Reject merchant registration (Admin only)")]
|
||||
[SwaggerResponse(200, "Merchant rejected")]
|
||||
[SwaggerResponse(400, "Invalid rejection")]
|
||||
[SwaggerResponse(404, "Merchant not found")]
|
||||
[ProducesResponseType(typeof(RejectMerchantResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> RejectMerchant(Guid merchantId, [FromBody] AdminActionRequest request)
|
||||
{
|
||||
try
|
||||
@@ -146,10 +139,9 @@ public class AdminMerchantsController : ControllerBase
|
||||
/// VI: Tạm ngưng merchant đang hoạt động.
|
||||
/// </summary>
|
||||
[HttpPost("{merchantId:guid}/suspend")]
|
||||
[SwaggerOperation(Summary = "Suspend merchant", Description = "Suspend an active merchant (Admin only)")]
|
||||
[SwaggerResponse(200, "Merchant suspended")]
|
||||
[SwaggerResponse(400, "Cannot suspend")]
|
||||
[SwaggerResponse(404, "Merchant not found")]
|
||||
[ProducesResponseType(typeof(SuspendMerchantResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> SuspendMerchant(Guid merchantId, [FromBody] AdminActionRequest request)
|
||||
{
|
||||
try
|
||||
@@ -174,10 +166,9 @@ public class AdminMerchantsController : ControllerBase
|
||||
/// VI: Kích hoạt lại merchant bị tạm ngưng.
|
||||
/// </summary>
|
||||
[HttpPost("{merchantId:guid}/reactivate")]
|
||||
[SwaggerOperation(Summary = "Reactivate merchant", Description = "Reactivate a suspended merchant (Admin only)")]
|
||||
[SwaggerResponse(200, "Merchant reactivated")]
|
||||
[SwaggerResponse(400, "Cannot reactivate")]
|
||||
[SwaggerResponse(404, "Merchant not found")]
|
||||
[ProducesResponseType(typeof(ReactivateMerchantResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ReactivateMerchant(Guid merchantId)
|
||||
{
|
||||
try
|
||||
@@ -201,10 +192,9 @@ public class AdminMerchantsController : ControllerBase
|
||||
/// VI: Cấm vĩnh viễn một merchant.
|
||||
/// </summary>
|
||||
[HttpPost("{merchantId:guid}/ban")]
|
||||
[SwaggerOperation(Summary = "Ban merchant", Description = "Permanently ban a merchant (Admin only)")]
|
||||
[SwaggerResponse(200, "Merchant banned")]
|
||||
[SwaggerResponse(400, "Cannot ban")]
|
||||
[SwaggerResponse(404, "Merchant not found")]
|
||||
[ProducesResponseType(typeof(BanMerchantResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> BanMerchant(Guid merchantId, [FromBody] AdminActionRequest request)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Swashbuckle.AspNetCore.Annotations;
|
||||
using MerchantService.API.Application.Queries.Admin;
|
||||
using MerchantService.Domain.AggregatesModel.ShopAggregate;
|
||||
using MerchantService.Domain.Exceptions;
|
||||
@@ -18,7 +17,6 @@ namespace MerchantService.API.Controllers.Admin;
|
||||
[ApiController]
|
||||
[Route("api/v1/admin/shops")]
|
||||
[Authorize(Roles = "Admin,SuperAdmin")]
|
||||
[SwaggerTag("Admin Shop Management / Quản lý Shop Admin")]
|
||||
public class AdminShopsController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
@@ -40,8 +38,7 @@ public class AdminShopsController : ControllerBase
|
||||
/// VI: Lấy tất cả shops với phân trang.
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[SwaggerOperation(Summary = "Get all shops", Description = "Get all shops with pagination (Admin only)")]
|
||||
[SwaggerResponse(200, "Success", typeof(AdminShopListResultDto))]
|
||||
[ProducesResponseType(typeof(AdminShopListResultDto), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> GetAllShops(
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20,
|
||||
@@ -59,12 +56,11 @@ public class AdminShopsController : ControllerBase
|
||||
/// VI: Lấy chi tiết shop theo ID.
|
||||
/// </summary>
|
||||
[HttpGet("{shopId:guid}")]
|
||||
[SwaggerOperation(Summary = "Get shop by ID", Description = "Get shop details by ID (Admin only)")]
|
||||
[SwaggerResponse(200, "Success")]
|
||||
[SwaggerResponse(404, "Shop not found")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetShopById(Guid shopId)
|
||||
{
|
||||
var shop = await _shopRepository.GetAsync(shopId);
|
||||
var shop = await _shopRepository.GetByIdAsync(shopId);
|
||||
if (shop == null)
|
||||
return NotFound(new { message = "Shop not found" });
|
||||
|
||||
@@ -87,15 +83,14 @@ public class AdminShopsController : ControllerBase
|
||||
/// VI: Tạm ngưng shop.
|
||||
/// </summary>
|
||||
[HttpPost("{shopId:guid}/suspend")]
|
||||
[SwaggerOperation(Summary = "Suspend shop", Description = "Suspend a shop (Admin only)")]
|
||||
[SwaggerResponse(200, "Shop suspended")]
|
||||
[SwaggerResponse(400, "Cannot suspend")]
|
||||
[SwaggerResponse(404, "Shop not found")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> SuspendShop(Guid shopId, [FromBody] AdminActionRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var shop = await _shopRepository.GetAsync(shopId)
|
||||
var shop = await _shopRepository.GetByIdAsync(shopId)
|
||||
?? throw new DomainException("Shop not found");
|
||||
|
||||
shop.SetInactive();
|
||||
@@ -121,15 +116,14 @@ public class AdminShopsController : ControllerBase
|
||||
/// VI: Kích hoạt lại shop bị tạm ngưng.
|
||||
/// </summary>
|
||||
[HttpPost("{shopId:guid}/reactivate")]
|
||||
[SwaggerOperation(Summary = "Reactivate shop", Description = "Reactivate a suspended shop (Admin only)")]
|
||||
[SwaggerResponse(200, "Shop reactivated")]
|
||||
[SwaggerResponse(400, "Cannot reactivate")]
|
||||
[SwaggerResponse(404, "Shop not found")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ReactivateShop(Guid shopId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var shop = await _shopRepository.GetAsync(shopId)
|
||||
var shop = await _shopRepository.GetByIdAsync(shopId)
|
||||
?? throw new DomainException("Shop not found");
|
||||
|
||||
shop.Publish();
|
||||
@@ -154,15 +148,14 @@ public class AdminShopsController : ControllerBase
|
||||
/// VI: Đóng cửa shop vĩnh viễn.
|
||||
/// </summary>
|
||||
[HttpPost("{shopId:guid}/close")]
|
||||
[SwaggerOperation(Summary = "Close shop", Description = "Close a shop permanently (Admin only)")]
|
||||
[SwaggerResponse(200, "Shop closed")]
|
||||
[SwaggerResponse(400, "Cannot close")]
|
||||
[SwaggerResponse(404, "Shop not found")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> CloseShop(Guid shopId, [FromBody] AdminActionRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var shop = await _shopRepository.GetAsync(shopId)
|
||||
var shop = await _shopRepository.GetByIdAsync(shopId)
|
||||
?? throw new DomainException("Shop not found");
|
||||
|
||||
shop.Close();
|
||||
|
||||
@@ -22,95 +22,10 @@ public class BlocksControllerTests : IClassFixture<CustomWebApplicationFactory>
|
||||
});
|
||||
}
|
||||
|
||||
#region Block User Tests
|
||||
|
||||
[Fact]
|
||||
public async Task BlockUser_ValidRequest_ReturnsCreated()
|
||||
{
|
||||
// Arrange
|
||||
var request = new
|
||||
{
|
||||
BlockerId = Guid.NewGuid(),
|
||||
BlockedId = Guid.NewGuid(),
|
||||
Reason = "Spam content"
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/blocks", request);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.Created);
|
||||
var content = await response.Content.ReadFromJsonAsync<BlockUserResponse>();
|
||||
content!.BlockId.Should().NotBeEmpty();
|
||||
content.Success.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BlockUser_SelfBlock_ReturnsBadRequest()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
var request = new
|
||||
{
|
||||
BlockerId = userId,
|
||||
BlockedId = userId // Same user
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/blocks", request);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BlockUser_WithoutReason_ReturnsCreated()
|
||||
{
|
||||
// Arrange
|
||||
var request = new
|
||||
{
|
||||
BlockerId = Guid.NewGuid(),
|
||||
BlockedId = Guid.NewGuid()
|
||||
// No reason provided
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/blocks", request);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.Created);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unblock User Tests
|
||||
|
||||
[Fact]
|
||||
public async Task UnblockUser_ValidRequest_ReturnsOk()
|
||||
{
|
||||
// Arrange - First create a block
|
||||
var blockerId = Guid.NewGuid();
|
||||
var blockedId = Guid.NewGuid();
|
||||
var blockRequest = new { BlockerId = blockerId, BlockedId = blockedId };
|
||||
await _client.PostAsJsonAsync("/api/v1/blocks", blockRequest);
|
||||
|
||||
// Act - Unblock
|
||||
var unblockRequest = new HttpRequestMessage(HttpMethod.Delete, "/api/v1/blocks")
|
||||
{
|
||||
Content = JsonContent.Create(new { BlockerId = blockerId, BlockedId = blockedId })
|
||||
};
|
||||
var response = await _client.SendAsync(unblockRequest);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Get Blocked Users Tests
|
||||
|
||||
[Fact]
|
||||
public async Task GetBlockedUsers_ValidUser_ReturnsOkWithList()
|
||||
public async Task GetBlockedUsers_ValidUser_ReturnsOk()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
@@ -136,8 +51,4 @@ public class BlocksControllerTests : IClassFixture<CustomWebApplicationFactory>
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// EN: Helper DTOs for deserialization
|
||||
// VI: Helper DTOs để deserialize
|
||||
private record BlockUserResponse(Guid BlockId, bool Success);
|
||||
}
|
||||
|
||||
@@ -22,71 +22,10 @@ public class RelationshipsControllerTests : IClassFixture<CustomWebApplicationFa
|
||||
});
|
||||
}
|
||||
|
||||
#region Friend Request Tests
|
||||
#region Get Endpoints Tests
|
||||
|
||||
[Fact]
|
||||
public async Task SendFriendRequest_ValidRequest_ReturnsCreated()
|
||||
{
|
||||
// Arrange
|
||||
var request = new
|
||||
{
|
||||
RequesterId = Guid.NewGuid(),
|
||||
AddresseeId = Guid.NewGuid()
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/relationships/friend-requests", request);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.Created);
|
||||
var content = await response.Content.ReadFromJsonAsync<SendFriendRequestResponse>();
|
||||
content!.RelationshipId.Should().NotBeEmpty();
|
||||
content.Status.Should().Be("Pending");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendFriendRequest_SelfRequest_ReturnsBadRequest()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
var request = new
|
||||
{
|
||||
RequesterId = userId,
|
||||
AddresseeId = userId // Same user
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/relationships/friend-requests", request);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RespondToFriendRequest_AcceptValid_ReturnsOk()
|
||||
{
|
||||
// Arrange - First create a friend request
|
||||
var requesterId = Guid.NewGuid();
|
||||
var addresseeId = Guid.NewGuid();
|
||||
var createRequest = new { RequesterId = requesterId, AddresseeId = addresseeId };
|
||||
var createResponse = await _client.PostAsJsonAsync("/api/v1/relationships/friend-requests", createRequest);
|
||||
createResponse.EnsureSuccessStatusCode();
|
||||
var created = await createResponse.Content.ReadFromJsonAsync<SendFriendRequestResponse>();
|
||||
|
||||
// Act - Accept the request
|
||||
var respondRequest = new { UserId = addresseeId, Accept = true };
|
||||
var response = await _client.PutAsJsonAsync($"/api/v1/relationships/friend-requests/{created!.RelationshipId}", respondRequest);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Get Friends Tests
|
||||
|
||||
[Fact]
|
||||
public async Task GetFriends_ValidUser_ReturnsOkWithList()
|
||||
public async Task GetFriends_ValidUser_ReturnsOk()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
@@ -127,47 +66,6 @@ public class RelationshipsControllerTests : IClassFixture<CustomWebApplicationFa
|
||||
|
||||
#endregion
|
||||
|
||||
#region Follow Tests
|
||||
|
||||
[Fact]
|
||||
public async Task FollowUser_ValidRequest_ReturnsCreated()
|
||||
{
|
||||
// Arrange
|
||||
var request = new
|
||||
{
|
||||
FollowerId = Guid.NewGuid(),
|
||||
FolloweeId = Guid.NewGuid()
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/relationships/follow", request);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.Created);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UnfollowUser_ValidRequest_ReturnsOk()
|
||||
{
|
||||
// Arrange - First create a follow relationship
|
||||
var followerId = Guid.NewGuid();
|
||||
var followeeId = Guid.NewGuid();
|
||||
var followRequest = new { FollowerId = followerId, FolloweeId = followeeId };
|
||||
await _client.PostAsJsonAsync("/api/v1/relationships/follow", followRequest);
|
||||
|
||||
// Act - Unfollow
|
||||
var unfollowRequest = new HttpRequestMessage(HttpMethod.Delete, "/api/v1/relationships/follow")
|
||||
{
|
||||
Content = JsonContent.Create(new { FollowerId = followerId, FolloweeId = followeeId })
|
||||
};
|
||||
var response = await _client.SendAsync(unfollowRequest);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Health Check Tests
|
||||
|
||||
[Fact]
|
||||
@@ -181,8 +79,4 @@ public class RelationshipsControllerTests : IClassFixture<CustomWebApplicationFa
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// EN: Helper DTOs for deserialization
|
||||
// VI: Helper DTOs để deserialize
|
||||
private record SendFriendRequestResponse(Guid RelationshipId, string Status);
|
||||
}
|
||||
|
||||
@@ -18,16 +18,18 @@ public class CustomWebApplicationFactory : WebApplicationFactory<Program>
|
||||
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
// EN: Remove ALL DbContext-related registrations to avoid provider conflict
|
||||
// VI: Xóa TẤT CẢ các đăng ký liên quan đến DbContext để tránh conflict provider
|
||||
var descriptorsToRemove = services
|
||||
.Where(d =>
|
||||
d.ServiceType == typeof(DbContextOptions<SocialServiceContext>) ||
|
||||
d.ServiceType == typeof(DbContextOptions) ||
|
||||
d.ServiceType == typeof(SocialServiceContext) ||
|
||||
d.ServiceType.FullName?.Contains("EntityFrameworkCore") == true ||
|
||||
d.ImplementationType?.FullName?.Contains("Npgsql") == true)
|
||||
.ToList();
|
||||
// EN: Remove ALL database-related services
|
||||
// VI: Xóa TẤT CẢ các services liên quan đến database
|
||||
var descriptorsToRemove = services.Where(d =>
|
||||
d.ServiceType == typeof(DbContextOptions<SocialServiceContext>) ||
|
||||
d.ServiceType == typeof(DbContextOptions) ||
|
||||
d.ServiceType == typeof(SocialServiceContext) ||
|
||||
d.ServiceType.Name.Contains("DbContextOptions") ||
|
||||
d.ImplementationType?.FullName?.Contains("Npgsql") == true ||
|
||||
d.ImplementationType?.FullName?.Contains("PostgreSql") == true ||
|
||||
d.ServiceType.FullName?.Contains("Npgsql") == true ||
|
||||
d.ServiceType.FullName?.Contains("HealthCheck") == true
|
||||
).ToList();
|
||||
|
||||
foreach (var descriptor in descriptorsToRemove)
|
||||
{
|
||||
@@ -36,17 +38,15 @@ public class CustomWebApplicationFactory : WebApplicationFactory<Program>
|
||||
|
||||
// EN: Add in-memory database for testing
|
||||
// VI: Thêm in-memory database để test
|
||||
var dbName = $"TestDatabase_{Guid.NewGuid()}";
|
||||
services.AddDbContext<SocialServiceContext>(options =>
|
||||
{
|
||||
options.UseInMemoryDatabase("TestDatabase_" + Guid.NewGuid().ToString());
|
||||
options.UseInMemoryDatabase(dbName);
|
||||
});
|
||||
|
||||
// EN: Build service provider and ensure database is created
|
||||
// VI: Build service provider và đảm bảo database được tạo
|
||||
var sp = services.BuildServiceProvider();
|
||||
using var scope = sp.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<SocialServiceContext>();
|
||||
db.Database.EnsureCreated();
|
||||
// EN: Add basic health checks (without database dependency)
|
||||
// VI: Thêm basic health checks (không phụ thuộc database)
|
||||
services.AddHealthChecks();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user