Files
pos-system/services/order-service-net/tests/OrderService.FunctionalTests/Controllers/OrdersControllerTests.cs
Ho Ngoc Hai 1d12a7980b feat: add order lifecycle integration tests (29 tests) and staging K8s deployment manifests
Testing (P0-7):
- 29 functional tests for order-service API (create/pay/complete/cancel lifecycle)
- CustomWebApplicationFactory with InMemory DB, mocked wallet/SignalR/tenant
- TestAuthHandler for JWT auth in tests
- Full lifecycle tests: cash flow and online payment flow end-to-end

Staging Deployment (P0-8):
- K8s manifests for 8 MVP services + Redis + POS web (namespace, configmap, secrets)
- Traefik Ingress with path-based routing and TLS via cert-manager
- HPA auto-scaling (2-4 replicas, CPU/memory thresholds)
- deploy-staging.sh script with --dry-run and --service flags
- CI/CD: deploy-staging.yml and docker-build.yml with matrix strategy
- Consistent patterns: port 8080, 3 health probes, RollingUpdate

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:56:03 +07:00

777 lines
29 KiB
C#

// EN: Functional tests for OrdersController covering the full order lifecycle.
// VI: Functional tests cho OrdersController bao phủ toàn bộ vòng đời order.
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using FluentAssertions;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using OrderService.Domain.AggregatesModel.OrderAggregate;
using OrderService.Infrastructure;
using Xunit;
namespace OrderService.FunctionalTests.Controllers;
/// <summary>
/// EN: Functional tests for the Orders API endpoints.
/// Tests the full order lifecycle: create -> pay -> complete/cancel.
/// Uses InMemory database and mocked external services.
/// VI: Functional tests cho Orders API endpoints.
/// Test toàn bộ vòng đời order: tạo -> thanh toán -> hoàn thành/hủy.
/// Sử dụng InMemory database và mock external services.
/// </summary>
[Collection("OrderService")]
public class OrdersControllerTests : IClassFixture<CustomWebApplicationFactory>
{
private readonly HttpClient _client;
private readonly CustomWebApplicationFactory _factory;
/// <summary>
/// EN: Default shop ID used in tests (matches TestAuthHandler.TestShopId).
/// VI: Shop ID mặc định sử dụng trong tests (khớp với TestAuthHandler.TestShopId).
/// </summary>
private static readonly Guid TestShopId = TestAuthHandler.TestShopId;
/// <summary>
/// EN: JSON serializer options matching the API's camelCase convention.
/// VI: Tùy chọn JSON serializer khớp với quy ước camelCase của API.
/// </summary>
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
public OrdersControllerTests(CustomWebApplicationFactory factory)
{
_factory = factory;
_client = factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false,
});
}
// ============================================================
// EN: Helper methods
// VI: Các phương thức helper
// ============================================================
/// <summary>
/// EN: Create a valid order request payload.
/// VI: Tạo payload request order hợp lệ.
/// </summary>
private static object CreateValidOrderRequest(Guid? shopId = null, decimal unitPrice = 50_000m)
{
return new
{
shopId = shopId ?? TestShopId,
items = new[]
{
new
{
productId = Guid.NewGuid(),
productName = "Test Product",
productType = "Physical",
quantity = 2,
unitPrice = unitPrice,
trackInventory = true
}
}
};
}
/// <summary>
/// EN: Create an order via API and return the order ID.
/// VI: Tạo order qua API và trả về order ID.
/// </summary>
private async Task<Guid> CreateOrderAndGetIdAsync(Guid? shopId = null, decimal unitPrice = 50_000m)
{
var request = CreateValidOrderRequest(shopId, unitPrice);
var response = await _client.PostAsJsonAsync("/api/v1/orders", request);
response.EnsureSuccessStatusCode();
var body = await response.Content.ReadFromJsonAsync<JsonElement>(JsonOptions);
return body.GetProperty("orderId").GetGuid();
}
/// <summary>
/// EN: Pay for an order via API (cash payment).
/// VI: Thanh toán order qua API (tiền mặt).
/// </summary>
private async Task PayOrderCashAsync(Guid orderId, decimal amountTendered = 200_000m)
{
var payRequest = new { paymentMethod = "cash", amountTendered = amountTendered };
var response = await _client.PostAsJsonAsync(
$"/api/v1/orders/{orderId}/pay?shopId={TestShopId}", payRequest);
response.EnsureSuccessStatusCode();
}
// ============================================================
// EN: CREATE ORDER tests
// VI: Tests TẠO ORDER
// ============================================================
[Fact]
public async Task CreateOrder_WithValidData_Returns201()
{
// Arrange
var request = CreateValidOrderRequest();
// Act
var response = await _client.PostAsJsonAsync("/api/v1/orders", request);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.Created);
var body = await response.Content.ReadFromJsonAsync<JsonElement>(JsonOptions);
body.GetProperty("orderId").GetGuid().Should().NotBeEmpty();
body.GetProperty("totalAmount").GetDecimal().Should().Be(100_000m);
body.GetProperty("status").GetString().Should().Be("Validated");
}
[Fact]
public async Task CreateOrder_WithMultipleItems_Returns201WithCorrectTotal()
{
// Arrange
var request = new
{
shopId = TestShopId,
items = new[]
{
new { productId = Guid.NewGuid(), productName = "Item A", productType = "Physical", quantity = 1, unitPrice = 30_000m, trackInventory = true },
new { productId = Guid.NewGuid(), productName = "Item B", productType = "Service", quantity = 3, unitPrice = 15_000m, trackInventory = false },
}
};
// Act
var response = await _client.PostAsJsonAsync("/api/v1/orders", request);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.Created);
var body = await response.Content.ReadFromJsonAsync<JsonElement>(JsonOptions);
// 30_000 * 1 + 15_000 * 3 = 75_000
body.GetProperty("totalAmount").GetDecimal().Should().Be(75_000m);
}
[Fact]
public async Task CreateOrder_WithEmptyShopId_ReturnsError()
{
// Arrange
var request = new
{
shopId = Guid.Empty,
items = new[]
{
new { productId = Guid.NewGuid(), productName = "Test", productType = "Physical", quantity = 1, unitPrice = 10_000m, trackInventory = true }
}
};
// Act
var response = await _client.PostAsJsonAsync("/api/v1/orders", request);
// Assert — FluentValidation throws ValidationException via MediatR pipeline;
// Hellang ProblemDetails maps this to 500 (no explicit mapping for FluentValidation.ValidationException).
// 400 would be preferred, but requires ProblemDetails.Map<ValidationException> config.
response.IsSuccessStatusCode.Should().BeFalse();
response.StatusCode.Should().BeOneOf(HttpStatusCode.BadRequest, HttpStatusCode.InternalServerError);
}
[Fact]
public async Task CreateOrder_WithNoItems_ReturnsError()
{
// Arrange
var request = new
{
shopId = TestShopId,
items = Array.Empty<object>()
};
// Act
var response = await _client.PostAsJsonAsync("/api/v1/orders", request);
// Assert
response.IsSuccessStatusCode.Should().BeFalse();
response.StatusCode.Should().BeOneOf(HttpStatusCode.BadRequest, HttpStatusCode.InternalServerError);
}
[Fact]
public async Task CreateOrder_WithInvalidProductType_ReturnsError()
{
// Arrange
var request = new
{
shopId = TestShopId,
items = new[]
{
new { productId = Guid.NewGuid(), productName = "Test", productType = "InvalidType", quantity = 1, unitPrice = 10_000m, trackInventory = true }
}
};
// Act
var response = await _client.PostAsJsonAsync("/api/v1/orders", request);
// Assert
response.IsSuccessStatusCode.Should().BeFalse();
response.StatusCode.Should().BeOneOf(HttpStatusCode.BadRequest, HttpStatusCode.InternalServerError);
}
[Fact]
public async Task CreateOrder_WithZeroQuantity_ReturnsError()
{
// Arrange
var request = new
{
shopId = TestShopId,
items = new[]
{
new { productId = Guid.NewGuid(), productName = "Test", productType = "Physical", quantity = 0, unitPrice = 10_000m, trackInventory = true }
}
};
// Act
var response = await _client.PostAsJsonAsync("/api/v1/orders", request);
// Assert
response.IsSuccessStatusCode.Should().BeFalse();
response.StatusCode.Should().BeOneOf(HttpStatusCode.BadRequest, HttpStatusCode.InternalServerError);
}
[Fact]
public async Task CreateOrder_WithDiscount_Returns201WithDiscountedTotal()
{
// Arrange
var request = new
{
shopId = TestShopId,
items = new[]
{
new { productId = Guid.NewGuid(), productName = "Test Product", productType = "Physical", quantity = 2, unitPrice = 50_000m, trackInventory = true }
},
discountAmount = 20_000m,
discountType = "promotion",
discountReference = "PROMO-001"
};
// Act
var response = await _client.PostAsJsonAsync("/api/v1/orders", request);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.Created);
var body = await response.Content.ReadFromJsonAsync<JsonElement>(JsonOptions);
// 50_000 * 2 - 20_000 = 80_000
body.GetProperty("totalAmount").GetDecimal().Should().Be(80_000m);
}
// ============================================================
// EN: PAY ORDER tests
// VI: Tests THANH TOAN ORDER
// ============================================================
[Fact]
public async Task PayOrder_CashPayment_Returns200WithChange()
{
// Arrange
var orderId = await CreateOrderAndGetIdAsync(unitPrice: 50_000m);
var payRequest = new { paymentMethod = "cash", amountTendered = 200_000m };
// Act
var response = await _client.PostAsJsonAsync(
$"/api/v1/orders/{orderId}/pay?shopId={TestShopId}", payRequest);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var body = await response.Content.ReadFromJsonAsync<JsonElement>(JsonOptions);
body.GetProperty("success").GetBoolean().Should().BeTrue();
body.GetProperty("data").GetProperty("changeAmount").GetDecimal().Should().Be(100_000m);
body.GetProperty("data").GetProperty("transactionId").GetString().Should().StartWith("CASH-");
}
[Fact]
public async Task PayOrder_CardPayment_Returns200()
{
// Arrange
var orderId = await CreateOrderAndGetIdAsync();
var payRequest = new { paymentMethod = "card" };
// Act
var response = await _client.PostAsJsonAsync(
$"/api/v1/orders/{orderId}/pay?shopId={TestShopId}", payRequest);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var body = await response.Content.ReadFromJsonAsync<JsonElement>(JsonOptions);
body.GetProperty("success").GetBoolean().Should().BeTrue();
body.GetProperty("data").GetProperty("transactionId").GetString().Should().StartWith("CARD-");
}
[Fact]
public async Task PayOrder_OnlinePayment_Returns200WithPaymentUrl()
{
// Arrange
var orderId = await CreateOrderAndGetIdAsync();
var payRequest = new
{
paymentMethod = "vnpay",
returnUrl = "https://myshop.test/return"
};
// Act
var response = await _client.PostAsJsonAsync(
$"/api/v1/orders/{orderId}/pay?shopId={TestShopId}", payRequest);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var body = await response.Content.ReadFromJsonAsync<JsonElement>(JsonOptions);
body.GetProperty("success").GetBoolean().Should().BeTrue();
body.GetProperty("data").GetProperty("paymentUrl").GetString().Should().StartWith("https://mock-gateway.test/pay");
body.GetProperty("data").GetProperty("status").GetString().Should().Be("PaymentPending");
}
[Fact]
public async Task PayOrder_CashWithInsufficientAmount_Returns400()
{
// Arrange
var orderId = await CreateOrderAndGetIdAsync(unitPrice: 50_000m);
// 2 items * 50_000 = 100_000, but tendering only 50_000
var payRequest = new { paymentMethod = "cash", amountTendered = 50_000m };
// Act
var response = await _client.PostAsJsonAsync(
$"/api/v1/orders/{orderId}/pay?shopId={TestShopId}", payRequest);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
var body = await response.Content.ReadFromJsonAsync<JsonElement>(JsonOptions);
body.GetProperty("success").GetBoolean().Should().BeFalse();
}
[Fact]
public async Task PayOrder_AlreadyPaid_Returns500()
{
// Arrange — create and pay an order
var orderId = await CreateOrderAndGetIdAsync();
await PayOrderCashAsync(orderId);
// Act — try to pay again
var payRequest = new { paymentMethod = "cash", amountTendered = 200_000m };
var response = await _client.PostAsJsonAsync(
$"/api/v1/orders/{orderId}/pay?shopId={TestShopId}", payRequest);
// Assert — should fail (order is already Processing, not Validated)
response.StatusCode.Should().BeOneOf(
HttpStatusCode.BadRequest,
HttpStatusCode.InternalServerError);
}
// ============================================================
// EN: COMPLETE ORDER tests
// VI: Tests HOAN THANH ORDER
// ============================================================
[Fact]
public async Task CompleteOrder_AfterPayment_Returns200()
{
// Arrange — create, pay (transitions to Processing)
var orderId = await CreateOrderAndGetIdAsync();
await PayOrderCashAsync(orderId);
// Act — complete the order
var response = await _client.PostAsJsonAsync(
$"/api/v1/orders/{orderId}/complete?shopId={TestShopId}",
new { });
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var body = await response.Content.ReadFromJsonAsync<JsonElement>(JsonOptions);
body.GetProperty("success").GetBoolean().Should().BeTrue();
body.GetProperty("status").GetString().Should().Be("Completed");
}
[Fact]
public async Task CompleteOrder_WithoutPayment_Returns500()
{
// Arrange — create order but don't pay (status = Validated)
var orderId = await CreateOrderAndGetIdAsync();
// Act
var response = await _client.PostAsJsonAsync(
$"/api/v1/orders/{orderId}/complete?shopId={TestShopId}",
new { });
// Assert — should fail because order is not in Processing state
response.StatusCode.Should().BeOneOf(
HttpStatusCode.BadRequest,
HttpStatusCode.InternalServerError);
}
// ============================================================
// EN: CANCEL ORDER tests
// VI: Tests HUY ORDER
// ============================================================
[Fact]
public async Task CancelOrder_PendingOrder_Returns200()
{
// Arrange — create an order (status = Validated)
var orderId = await CreateOrderAndGetIdAsync();
// Act
var cancelRequest = new { reason = "Customer changed their mind" };
var response = await _client.PostAsJsonAsync(
$"/api/v1/orders/{orderId}/cancel?shopId={TestShopId}", cancelRequest);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var body = await response.Content.ReadFromJsonAsync<JsonElement>(JsonOptions);
body.GetProperty("success").GetBoolean().Should().BeTrue();
body.GetProperty("status").GetString().Should().Be("Cancelled");
}
[Fact]
public async Task CancelOrder_AlreadyCompleted_Returns500()
{
// Arrange — create, pay, and complete the order
var orderId = await CreateOrderAndGetIdAsync();
await PayOrderCashAsync(orderId);
var completeResponse = await _client.PostAsJsonAsync(
$"/api/v1/orders/{orderId}/complete?shopId={TestShopId}",
new { });
completeResponse.EnsureSuccessStatusCode();
// Act — try to cancel a completed order
var cancelRequest = new { reason = "Too late" };
var response = await _client.PostAsJsonAsync(
$"/api/v1/orders/{orderId}/cancel?shopId={TestShopId}", cancelRequest);
// Assert — should fail (cannot cancel completed order)
response.StatusCode.Should().BeOneOf(
HttpStatusCode.BadRequest,
HttpStatusCode.InternalServerError);
}
[Fact]
public async Task CancelOrder_AlreadyCancelled_Returns500()
{
// Arrange — create and cancel an order
var orderId = await CreateOrderAndGetIdAsync();
var cancelRequest = new { reason = "First cancellation" };
var firstResponse = await _client.PostAsJsonAsync(
$"/api/v1/orders/{orderId}/cancel?shopId={TestShopId}", cancelRequest);
firstResponse.EnsureSuccessStatusCode();
// Act — try to cancel again
var secondCancelRequest = new { reason = "Second cancellation attempt" };
var response = await _client.PostAsJsonAsync(
$"/api/v1/orders/{orderId}/cancel?shopId={TestShopId}", secondCancelRequest);
// Assert — should fail (already cancelled)
response.StatusCode.Should().BeOneOf(
HttpStatusCode.BadRequest,
HttpStatusCode.InternalServerError);
}
[Fact]
public async Task CancelOrder_WithEmptyReason_ReturnsError()
{
// Arrange
var orderId = await CreateOrderAndGetIdAsync();
// Act
var cancelRequest = new { reason = "" };
var response = await _client.PostAsJsonAsync(
$"/api/v1/orders/{orderId}/cancel?shopId={TestShopId}", cancelRequest);
// Assert — FluentValidation rejects empty reason;
// mapped as 500 via ProblemDetails (no explicit ValidationException mapping).
response.IsSuccessStatusCode.Should().BeFalse();
response.StatusCode.Should().BeOneOf(HttpStatusCode.BadRequest, HttpStatusCode.InternalServerError);
}
// ============================================================
// EN: PAYMENT CALLBACK tests
// VI: Tests CALLBACK THANH TOAN
// ============================================================
[Fact]
public async Task PaymentCallback_SuccessfulPayment_Returns200()
{
// Arrange — create order and initiate online payment (status -> PaymentPending)
var orderId = await CreateOrderAndGetIdAsync();
var payRequest = new
{
paymentMethod = "vnpay",
returnUrl = "https://myshop.test/return"
};
var payResponse = await _client.PostAsJsonAsync(
$"/api/v1/orders/{orderId}/pay?shopId={TestShopId}", payRequest);
payResponse.EnsureSuccessStatusCode();
// Act — simulate successful gateway callback
var callbackRequest = new
{
gatewayTransactionId = "VNP-TXN-12345",
isSuccess = true,
gatewayResponseCode = "00"
};
var response = await _client.PostAsJsonAsync(
$"/api/v1/orders/{orderId}/payment-callback", callbackRequest);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var body = await response.Content.ReadFromJsonAsync<JsonElement>(JsonOptions);
body.GetProperty("success").GetBoolean().Should().BeTrue();
body.GetProperty("data").GetProperty("status").GetString().Should().Be("Processing");
}
[Fact]
public async Task PaymentCallback_FailedPayment_CancelsOrder()
{
// Arrange — create order and initiate online payment
var orderId = await CreateOrderAndGetIdAsync();
var payRequest = new
{
paymentMethod = "momo",
returnUrl = "https://myshop.test/return"
};
var payResponse = await _client.PostAsJsonAsync(
$"/api/v1/orders/{orderId}/pay?shopId={TestShopId}", payRequest);
payResponse.EnsureSuccessStatusCode();
// Act — simulate failed gateway callback
var callbackRequest = new
{
gatewayTransactionId = "MOMO-TXN-FAIL",
isSuccess = false,
gatewayResponseCode = "99"
};
var response = await _client.PostAsJsonAsync(
$"/api/v1/orders/{orderId}/payment-callback", callbackRequest);
// Assert — payment failed but callback endpoint succeeds, order is cancelled
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
var body = await response.Content.ReadFromJsonAsync<JsonElement>(JsonOptions);
body.GetProperty("success").GetBoolean().Should().BeFalse();
}
// ============================================================
// EN: FULL LIFECYCLE tests
// VI: Tests VONG DOI DAY DU
// ============================================================
[Fact]
public async Task FullLifecycle_CashPayment_CreatePayComplete()
{
// Step 1: Create order
var request = new
{
shopId = TestShopId,
items = new[]
{
new { productId = Guid.NewGuid(), productName = "Bia Saigon", productType = "Physical", quantity = 5, unitPrice = 15_000m, trackInventory = true },
new { productId = Guid.NewGuid(), productName = "Pho", productType = "PreparedFood", quantity = 2, unitPrice = 45_000m, trackInventory = false }
}
};
var createResponse = await _client.PostAsJsonAsync("/api/v1/orders", request);
createResponse.StatusCode.Should().Be(HttpStatusCode.Created);
var createBody = await createResponse.Content.ReadFromJsonAsync<JsonElement>(JsonOptions);
var orderId = createBody.GetProperty("orderId").GetGuid();
// 15_000 * 5 + 45_000 * 2 = 165_000
createBody.GetProperty("totalAmount").GetDecimal().Should().Be(165_000m);
createBody.GetProperty("status").GetString().Should().Be("Validated");
// Step 2: Pay with cash
var payRequest = new { paymentMethod = "cash", amountTendered = 200_000m };
var payResponse = await _client.PostAsJsonAsync(
$"/api/v1/orders/{orderId}/pay?shopId={TestShopId}", payRequest);
payResponse.StatusCode.Should().Be(HttpStatusCode.OK);
var payBody = await payResponse.Content.ReadFromJsonAsync<JsonElement>(JsonOptions);
payBody.GetProperty("success").GetBoolean().Should().BeTrue();
payBody.GetProperty("data").GetProperty("changeAmount").GetDecimal().Should().Be(35_000m);
// Step 3: Complete order
var completeResponse = await _client.PostAsJsonAsync(
$"/api/v1/orders/{orderId}/complete?shopId={TestShopId}", new { });
completeResponse.StatusCode.Should().Be(HttpStatusCode.OK);
var completeBody = await completeResponse.Content.ReadFromJsonAsync<JsonElement>(JsonOptions);
completeBody.GetProperty("success").GetBoolean().Should().BeTrue();
completeBody.GetProperty("status").GetString().Should().Be("Completed");
}
[Fact]
public async Task FullLifecycle_OnlinePayment_CreatePayCallbackComplete()
{
// Step 1: Create order
var orderId = await CreateOrderAndGetIdAsync(unitPrice: 100_000m);
// Step 2: Initiate online payment
var payRequest = new
{
paymentMethod = "vnpay",
returnUrl = "https://myshop.test/return"
};
var payResponse = await _client.PostAsJsonAsync(
$"/api/v1/orders/{orderId}/pay?shopId={TestShopId}", payRequest);
payResponse.StatusCode.Should().Be(HttpStatusCode.OK);
var payBody = await payResponse.Content.ReadFromJsonAsync<JsonElement>(JsonOptions);
payBody.GetProperty("success").GetBoolean().Should().BeTrue();
payBody.GetProperty("data").GetProperty("paymentUrl").GetString().Should().Contain("mock-gateway");
payBody.GetProperty("data").GetProperty("status").GetString().Should().Be("PaymentPending");
// Step 3: Gateway callback (success)
var callbackRequest = new
{
gatewayTransactionId = "VNP-SUCCESS-001",
isSuccess = true,
gatewayResponseCode = "00"
};
var callbackResponse = await _client.PostAsJsonAsync(
$"/api/v1/orders/{orderId}/payment-callback", callbackRequest);
callbackResponse.StatusCode.Should().Be(HttpStatusCode.OK);
var callbackBody = await callbackResponse.Content.ReadFromJsonAsync<JsonElement>(JsonOptions);
callbackBody.GetProperty("success").GetBoolean().Should().BeTrue();
callbackBody.GetProperty("data").GetProperty("status").GetString().Should().Be("Processing");
// Step 4: Complete order
var completeResponse = await _client.PostAsJsonAsync(
$"/api/v1/orders/{orderId}/complete?shopId={TestShopId}", new { });
completeResponse.StatusCode.Should().Be(HttpStatusCode.OK);
var completeBody = await completeResponse.Content.ReadFromJsonAsync<JsonElement>(JsonOptions);
completeBody.GetProperty("status").GetString().Should().Be("Completed");
}
// ============================================================
// EN: HEALTH CHECK tests
// VI: Tests KIEM TRA SUC KHOE
// ============================================================
[Fact]
public async Task HealthCheck_Live_Returns200()
{
// Act
var response = await _client.GetAsync("/health/live");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
}
[Fact]
public async Task HealthCheck_Root_Returns200()
{
// Act
var response = await _client.GetAsync("/health");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
}
// ============================================================
// EN: EDGE CASE tests
// VI: Tests TRUONG HOP CANH
// ============================================================
[Fact]
public async Task PayOrder_QrPayment_TreatedAsCard()
{
// Arrange
var orderId = await CreateOrderAndGetIdAsync();
var payRequest = new { paymentMethod = "qr" };
// Act
var response = await _client.PostAsJsonAsync(
$"/api/v1/orders/{orderId}/pay?shopId={TestShopId}", payRequest);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var body = await response.Content.ReadFromJsonAsync<JsonElement>(JsonOptions);
body.GetProperty("success").GetBoolean().Should().BeTrue();
body.GetProperty("data").GetProperty("transactionId").GetString().Should().StartWith("CARD-");
}
[Fact]
public async Task PayOrder_TransferPayment_TreatedAsCard()
{
// Arrange
var orderId = await CreateOrderAndGetIdAsync();
var payRequest = new { paymentMethod = "transfer" };
// Act
var response = await _client.PostAsJsonAsync(
$"/api/v1/orders/{orderId}/pay?shopId={TestShopId}", payRequest);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var body = await response.Content.ReadFromJsonAsync<JsonElement>(JsonOptions);
body.GetProperty("success").GetBoolean().Should().BeTrue();
}
[Fact]
public async Task CreateOrder_WithTableId_Returns201()
{
// Arrange
var tableId = Guid.NewGuid();
var request = new
{
shopId = TestShopId,
customerId = Guid.NewGuid(),
tableId = tableId,
items = new[]
{
new { productId = Guid.NewGuid(), productName = "Pho Bo", productType = "PreparedFood", quantity = 1, unitPrice = 55_000m, trackInventory = false }
}
};
// Act
var response = await _client.PostAsJsonAsync("/api/v1/orders", request);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.Created);
var body = await response.Content.ReadFromJsonAsync<JsonElement>(JsonOptions);
body.GetProperty("orderId").GetGuid().Should().NotBeEmpty();
}
[Fact]
public async Task PayOrder_CashExactAmount_ZeroChange()
{
// Arrange — order total = 2 * 50_000 = 100_000
var orderId = await CreateOrderAndGetIdAsync(unitPrice: 50_000m);
var payRequest = new { paymentMethod = "cash", amountTendered = 100_000m };
// Act
var response = await _client.PostAsJsonAsync(
$"/api/v1/orders/{orderId}/pay?shopId={TestShopId}", payRequest);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var body = await response.Content.ReadFromJsonAsync<JsonElement>(JsonOptions);
body.GetProperty("success").GetBoolean().Should().BeTrue();
body.GetProperty("data").GetProperty("changeAmount").GetDecimal().Should().Be(0m);
}
}