- packages/logger: upgrade tsconfig target to ES2022 to support Array.at() - packages/http-client: exclude test files from tsc build to prevent noUnusedLocals errors - packages/http-client/test: use vi.hoisted() for mock functions (vi.mock hoisting fix) - services/goodgo-mcp-server/tests: use vi.hoisted() for all 4 test files (catalog, inventory, analytics, recipe) - web-client-tpos: remove stale @using WebClientTpos.Client.Components.Auth from 10 auth pages (moved to blazor-ui RCL) - web-client-tpos: remove AttachToken() calls in PosDataService (auth via BFF httpOnly cookie) - web-client-tpos: fix IamApiService.SetAuthHeader() and MerchantApiService.AttachTokenAsync() — make no-op, remove _auth dependency - web-client-tpos: fix Profile.razor — remove AttachToken() method and calls - web-client-tpos: fix OnboardingReady.razor — escape @keyframes → @@keyframes in Razor style block - web-client-tpos: fix PosDataService.GetListFromApiAsync() — check array before property lookup to fix plain array deserialization - web-client-tpos/tests: update AuthStateServiceTests to new AuthStateService.Login(email, role) signature (no token param) - web-client-tpos/tests: update PosDataServiceTests to new PosDataService(http) constructor (no authState param) All 113 Node.js tests pass. All 30 .NET component tests pass. All .NET builds succeed. Co-Authored-By: Paperclip <noreply@paperclip.ing>
263 lines
8.3 KiB
C#
263 lines
8.3 KiB
C#
using System.Net;
|
|
using System.Text;
|
|
using FluentAssertions;
|
|
using WebClientTpos.Client.Services;
|
|
using Xunit;
|
|
|
|
namespace WebClientTpos.ComponentTests.Services;
|
|
|
|
/// <summary>
|
|
/// EN: Unit tests for PosDataService — smart 4-format JSON deserialization and HTTP helpers.
|
|
/// VI: Unit tests cho PosDataService — deserialize JSON thông minh 4 định dạng và các HTTP helper.
|
|
/// </summary>
|
|
public class PosDataServiceTests
|
|
{
|
|
// EN: Simple item DTO used across tests.
|
|
// VI: DTO item đơn giản dùng trong các test.
|
|
private record ItemDto(Guid Id, string Name);
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Helper — build PosDataService with a mocked HttpMessageHandler
|
|
// -----------------------------------------------------------------------
|
|
|
|
private static (PosDataService sut, AuthStateService authState) BuildSut(
|
|
string responseBody,
|
|
HttpStatusCode statusCode = HttpStatusCode.OK)
|
|
{
|
|
// EN: Auth is via BFF httpOnly cookie — AuthStateService only tracks email/role/expiry.
|
|
// VI: Auth qua BFF httpOnly cookie — AuthStateService chỉ theo dõi email/role/expiry.
|
|
var authState = new AuthStateService();
|
|
authState.Login("test@goodgo.vn", "owner");
|
|
|
|
var handler = new FakeHttpMessageHandler(responseBody, statusCode);
|
|
var httpClient = new HttpClient(handler) { BaseAddress = new Uri("http://localhost/") };
|
|
|
|
var sut = new PosDataService(httpClient);
|
|
return (sut, authState);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// GetListFromApiAsync — 4-format deserialization
|
|
// -----------------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public async Task GetListFromApiAsync_PlainArray_ShouldDeserializeCorrectly()
|
|
{
|
|
// Arrange — Case 1: plain JSON array
|
|
var json = """
|
|
[
|
|
{"id":"00000000-0000-0000-0000-000000000001","name":"Espresso"},
|
|
{"id":"00000000-0000-0000-0000-000000000002","name":"Cappuccino"}
|
|
]
|
|
""";
|
|
var (sut, _) = BuildSut(json);
|
|
|
|
// Act
|
|
var result = await sut.GetListFromApiAsync<ItemDto>("api/bff/products");
|
|
|
|
// Assert
|
|
result.Should().HaveCount(2);
|
|
result[0].Name.Should().Be("Espresso");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetListFromApiAsync_PagedResultWrapper_ShouldDeserializeCorrectly()
|
|
{
|
|
// Arrange — Case 2: { "items": [...] }
|
|
var json = """
|
|
{
|
|
"items": [
|
|
{"id":"00000000-0000-0000-0000-000000000003","name":"Latte"},
|
|
{"id":"00000000-0000-0000-0000-000000000004","name":"Mocha"}
|
|
],
|
|
"totalCount": 2,
|
|
"pageNumber": 1,
|
|
"pageSize": 10
|
|
}
|
|
""";
|
|
var (sut, _) = BuildSut(json);
|
|
|
|
// Act
|
|
var result = await sut.GetListFromApiAsync<ItemDto>("api/bff/products");
|
|
|
|
// Assert
|
|
result.Should().HaveCount(2);
|
|
result[1].Name.Should().Be("Mocha");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetListFromApiAsync_ApiResponseWithPagedData_ShouldDeserializeCorrectly()
|
|
{
|
|
// Arrange — Case 3: { "data": { "items": [...] } }
|
|
var json = """
|
|
{
|
|
"success": true,
|
|
"data": {
|
|
"items": [
|
|
{"id":"00000000-0000-0000-0000-000000000005","name":"Flat White"}
|
|
],
|
|
"totalCount": 1
|
|
}
|
|
}
|
|
""";
|
|
var (sut, _) = BuildSut(json);
|
|
|
|
// Act
|
|
var result = await sut.GetListFromApiAsync<ItemDto>("api/bff/products");
|
|
|
|
// Assert
|
|
result.Should().HaveCount(1);
|
|
result[0].Name.Should().Be("Flat White");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetListFromApiAsync_ApiResponseWithDirectArray_ShouldDeserializeCorrectly()
|
|
{
|
|
// Arrange — Case 4: { "data": [...] }
|
|
var json = """
|
|
{
|
|
"success": true,
|
|
"data": [
|
|
{"id":"00000000-0000-0000-0000-000000000006","name":"Americano"}
|
|
]
|
|
}
|
|
""";
|
|
var (sut, _) = BuildSut(json);
|
|
|
|
// Act
|
|
var result = await sut.GetListFromApiAsync<ItemDto>("api/bff/products");
|
|
|
|
// Assert
|
|
result.Should().HaveCount(1);
|
|
result[0].Name.Should().Be("Americano");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetListFromApiAsync_GenericWrapper_ShouldDeserializeFirstArrayProperty()
|
|
{
|
|
// Arrange — Case 5: { "products": [...] } (generic single-array-property)
|
|
var json = """
|
|
{
|
|
"products": [
|
|
{"id":"00000000-0000-0000-0000-000000000007","name":"Cold Brew"}
|
|
]
|
|
}
|
|
""";
|
|
var (sut, _) = BuildSut(json);
|
|
|
|
// Act
|
|
var result = await sut.GetListFromApiAsync<ItemDto>("api/bff/products");
|
|
|
|
// Assert
|
|
result.Should().HaveCount(1);
|
|
result[0].Name.Should().Be("Cold Brew");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetListFromApiAsync_EmptyJson_ShouldReturnEmptyList()
|
|
{
|
|
// Arrange
|
|
var (sut, _) = BuildSut(" ");
|
|
|
|
// Act
|
|
var result = await sut.GetListFromApiAsync<ItemDto>("api/bff/products");
|
|
|
|
// Assert
|
|
result.Should().BeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetListFromApiAsync_NonSuccessStatus_ShouldReturnEmptyList()
|
|
{
|
|
// Arrange
|
|
var (sut, _) = BuildSut("{}", HttpStatusCode.InternalServerError);
|
|
|
|
// Act
|
|
var result = await sut.GetListFromApiAsync<ItemDto>("api/bff/products");
|
|
|
|
// Assert
|
|
result.Should().BeEmpty();
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// PostAsync
|
|
// -----------------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public async Task PostAsync_OnSuccess_ShouldReturnTrue()
|
|
{
|
|
// Arrange
|
|
var (sut, _) = BuildSut("{\"success\":true}", HttpStatusCode.OK);
|
|
|
|
// Act
|
|
var result = await sut.PostAsync("api/bff/orders", new { quantity = 1 });
|
|
|
|
// Assert
|
|
result.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PostAsync_OnBadRequest_ShouldReturnFalse()
|
|
{
|
|
// Arrange
|
|
var (sut, _) = BuildSut("{\"success\":false}", HttpStatusCode.BadRequest);
|
|
|
|
// Act
|
|
var result = await sut.PostAsync("api/bff/orders", new { quantity = -1 });
|
|
|
|
// Assert
|
|
result.Should().BeFalse();
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// PutAsync
|
|
// -----------------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public async Task PutAsync_OnSuccess_ShouldReturnTrue()
|
|
{
|
|
var (sut, _) = BuildSut("{}", HttpStatusCode.OK);
|
|
var result = await sut.PutAsync("api/bff/products/1", new { price = 40000 });
|
|
result.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PutAsync_OnNotFound_ShouldReturnFalse()
|
|
{
|
|
var (sut, _) = BuildSut("{}", HttpStatusCode.NotFound);
|
|
var result = await sut.PutAsync("api/bff/products/nonexistent", new { price = 0 });
|
|
result.Should().BeFalse();
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Test helper — FakeHttpMessageHandler
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// <summary>
|
|
/// EN: In-memory HTTP handler for unit testing HttpClient-based services.
|
|
/// VI: HTTP handler in-memory để unit test các service dùng HttpClient.
|
|
/// </summary>
|
|
internal class FakeHttpMessageHandler : HttpMessageHandler
|
|
{
|
|
private readonly string _responseBody;
|
|
private readonly HttpStatusCode _statusCode;
|
|
|
|
public FakeHttpMessageHandler(string responseBody, HttpStatusCode statusCode = HttpStatusCode.OK)
|
|
{
|
|
_responseBody = responseBody;
|
|
_statusCode = statusCode;
|
|
}
|
|
|
|
protected override Task<HttpResponseMessage> SendAsync(
|
|
HttpRequestMessage request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var response = new HttpResponseMessage(_statusCode)
|
|
{
|
|
Content = new StringContent(_responseBody, Encoding.UTF8, "application/json"),
|
|
};
|
|
return Task.FromResult(response);
|
|
}
|
|
}
|