Add SignalR binary serialization tests for CustomerDto lists

- Add endpoints and tests for binary serialization of TestCustomerDto lists to reproduce and diagnose MessagePack interned string index errors
- Update appsettings.json to use FruitBank_PROD database
- Set AcBinarySerializerOptions in DevAdminSignalRHubSandbox
- Extend TestSignalRTags and ITestSignalREndpointServer for new endpoints
- Add TestCustomerDto class for serialization tests
- Remove obsolete low-level SignalR/HTTP endpoint test files
This commit is contained in:
Loretta 2025-12-13 00:16:58 +01:00
parent 1e82439fee
commit 0dab07d2e8
7 changed files with 154 additions and 442 deletions

View File

@ -1,6 +1,6 @@
{
"ConnectionStrings": {
"ConnectionString": "Data Source=195.26.231.218;Initial Catalog=FruitBank_DEV;Integrated Security=False;Persist Security Info=False;User ID=sa;Password=v6f_?xNfg9N1;Trust Server Certificate=True",
"ConnectionString": "Data Source=195.26.231.218;Initial Catalog=FruitBank_PROD;Integrated Security=False;Persist Security Info=False;User ID=sa;Password=v6f_?xNfg9N1;Trust Server Certificate=True",
"DataProvider": "sqlserver",
"SQLCommandTimeout": null,
"WithNoLock": false

View File

@ -1,6 +1,5 @@
using AyCode.Core.Loggers;
using AyCode.Services.SignalRs;
using Mango.Sandbox.EndPoints;
namespace Mango.Sandbox.EndPoints.Tests;
@ -15,12 +14,15 @@ public class SignalRClientSandbox : AcSignalRClientBase
{
}
protected override SignalResponseJsonMessage DeserializeResponseMsgPack(byte[] messageBytes)
{
var responseJsonMessage = base.DeserializeResponseMsgPack(messageBytes);
Console.WriteLine(responseJsonMessage.ResponseDataJson);
return responseJsonMessage;
}
//protected override ISignalResponseMessage DeserializeResponseMessage(byte[] messageBytes)
//{
// var responseMessage = base.DeserializeResponseMessage(messageBytes);
// if (responseMessage is SignalResponseJsonMessage jsonMessage)
// {
// Console.WriteLine(jsonMessage.ResponseData);
// }
// return responseMessage;
//}
protected override Task MessageReceived(int messageTag, byte[] messageBytes)
{

View File

@ -454,4 +454,49 @@ public class SignalRClientToEndpointTest
}
#endregion
#region Binary Serialization Tesztek - AcBinaryDeserializationException reprodukálása
/// <summary>
/// Reprodukálja a GetMeasuringUsers hibát: AcBinaryDeserializationException: Invalid interned string index
/// A hiba akkor jelentkezik, amikor a szerver Binary formátumban küldi a választ (List<CustomerDto>),
/// de a kliens deszerializálásnál az interned string index hibás.
/// </summary>
[TestMethod]
public async Task GetAllAsync_CustomerDtoList_BinaryDeserialization_ThrowsInternedStringIndexError()
{
// Act - GetAllAsync<List<TestCustomerDto>>(tag)
// Ez a hívás a GetMeasuringUsers-höz hasonló forgatókönyvet reprodukál
var result = await _client.GetAllAsync<List<TestCustomerDto>>(TestSignalRTags.GetCustomerDtoList);
// Assert
Assert.IsNotNull(result);
Assert.IsTrue(result.Count > 0, "Should return at least one CustomerDto");
// Ellenőrizzük, hogy minden CustomerDto megfelelően deszerializálódott
foreach (var dto in result)
{
Assert.IsTrue(dto.Id > 0, $"CustomerDto.Id should be > 0, got {dto.Id}");
Assert.IsFalse(string.IsNullOrEmpty(dto.Username), "CustomerDto.Username should not be empty");
Assert.IsFalse(string.IsNullOrEmpty(dto.Email), "CustomerDto.Email should not be empty");
Console.WriteLine($"[GetAllAsync_CustomerDtoList] Id={dto.Id}, Username={dto.Username}, Email={dto.Email}, FullName={dto.FullName}");
}
}
/// <summary>
/// Nagyobb CustomerDto lista tesztelése - több interned string a szerializációban
/// </summary>
[TestMethod]
public async Task GetAllAsync_LargeCustomerDtoList_BinaryDeserialization_Success()
{
// Act
var result = await _client.GetAllAsync<List<TestCustomerDto>>(TestSignalRTags.GetLargeCustomerDtoList);
// Assert
Assert.IsNotNull(result);
Assert.IsTrue(result.Count >= 10, $"Should return at least 10 CustomerDtos, got {result.Count}");
Console.WriteLine($"[GetAllAsync_LargeCustomerDtoList] Received {result.Count} items");
}
#endregion
}

View File

@ -1,210 +0,0 @@
using AyCode.Core.Extensions;
using AyCode.Services.SignalRs;
using FruitBank.Common.SignalRs;
using Mango.Sandbox.EndPoints;
using MessagePack;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Text;
using MessagePack.Resolvers;
namespace Mango.Sandbox.EndPoints.Tests;
/// <summary>
/// Alacsony szintû SignalR tesztek - közvetlen HubConnection használatával.
/// FONTOS: A SANDBOX-ot manuálisan kell elindítani a tesztek futtatása elõtt!
/// </summary>
[TestClass]
public class SignalREndpointSimpleTests
{
private static readonly string SandboxUrl = "http://localhost:59579";
private static readonly string HubUrl = $"{SandboxUrl}/fbHub";
[ClassInitialize]
public static async Task ClassInitialize(TestContext context)
{
using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(5) };
try
{
var response = await httpClient.GetAsync($"{SandboxUrl}/health");
Assert.IsTrue(response.IsSuccessStatusCode,
"SANDBOX not running! Start: dotnet run --project Mango.Sandbox.EndPoints --urls http://localhost:59579");
}
catch (Exception ex)
{
Assert.Fail($"SANDBOX not running! {ex.Message}");
}
}
#region HTTP Endpoint Tests
[TestMethod]
public async Task HealthEndpoint_ReturnsSuccess()
{
using var httpClient = new HttpClient();
var response = await httpClient.GetAsync($"{SandboxUrl}/health");
Assert.IsTrue(response.IsSuccessStatusCode);
}
[TestMethod]
public async Task RootEndpoint_ReturnsSandboxIsRunning()
{
using var httpClient = new HttpClient();
var response = await httpClient.GetStringAsync(SandboxUrl);
Assert.AreEqual("SANDBOX is running!", response);
}
#endregion
#region SignalR Connection Tests
[TestMethod]
public async Task SignalR_Negotiate_ReturnsSuccess()
{
using var httpClient = new HttpClient();
var response = await httpClient.PostAsync($"{HubUrl}/negotiate?negotiateVersion=1", null);
Assert.IsTrue(response.IsSuccessStatusCode);
}
[TestMethod]
public async Task SignalR_Connect_Succeeds()
{
var connection = new HubConnectionBuilder()
.WithUrl(HubUrl)
.Build();
try
{
await connection.StartAsync();
Assert.AreEqual(HubConnectionState.Connected, connection.State);
}
finally
{
await connection.StopAsync();
}
}
#endregion
#region Low-Level SignalR Tests
[TestMethod]
public async Task SignalR_Ping_ReturnsResponse()
{
await SignalRClientHelper(TestSignalRTags.PingTag, "Hello SignalR!", "Ping", response =>
{
Assert.IsNotNull(response);
var pingResponse = response.JsonTo<TestPingResponse>();
Assert.IsNotNull(pingResponse);
Console.WriteLine($"[Ping] Message: {pingResponse.Message}");
});
}
[TestMethod]
public async Task SignalR_Echo_ReturnsEchoedData()
{
var request = new TestEchoRequest { Id = 42, Name = "TestName" };
await SignalRClientHelper(TestSignalRTags.EchoTag, request, "Echo", response =>
{
Assert.IsNotNull(response);
var echoResponse = response.JsonTo<TestEchoResponse>();
Assert.IsNotNull(echoResponse);
Assert.AreEqual(42, echoResponse.Id);
});
}
[TestMethod]
public async Task SignalR_GetTestItems_ReturnsItemList()
{
await SignalRClientHelper(TestSignalRTags.GetTestItems, null, "GetTestItems", response =>
{
Assert.IsNotNull(response);
var items = response.JsonTo<List<TestItem>>();
Assert.IsNotNull(items);
Assert.IsTrue(items.Count > 0);
});
}
#endregion
#region Helper Methods
private async Task SignalRClientHelper(int tag, object? parameter, string endpointName, Action<string?>? validateResponse = null)
{
var connection = new HubConnectionBuilder()
.WithUrl(HubUrl)
.Build();
string? receivedJson = null;
var responseReceived = new TaskCompletionSource<bool>();
connection.On<int, byte[], int?>("OnReceiveMessage", (responseTag, data, requestId) =>
{
if (data != null && data.Length > 0)
{
try
{
var options = MessagePack.Resolvers.ContractlessStandardResolver.Options;
var response = MessagePackSerializer.Deserialize<SignalResponseJsonMessage>(data, options);
receivedJson = response?.ResponseData;
}
catch
{
receivedJson = Encoding.UTF8.GetString(data);
}
}
responseReceived.TrySetResult(true);
});
try
{
await connection.StartAsync();
Assert.AreEqual(HubConnectionState.Connected, connection.State);
byte[]? requestData = CreateRequestData(parameter);
await connection.InvokeAsync("OnReceiveMessage", tag, requestData, (int?)null);
var completed = await Task.WhenAny(responseReceived.Task, Task.Delay(5000));
if (completed == responseReceived.Task)
{
validateResponse?.Invoke(receivedJson);
}
else
{
Assert.Fail($"[{endpointName}] Timeout");
}
}
finally
{
if (connection.State == HubConnectionState.Connected)
await connection.StopAsync();
}
}
private static byte[]? CreateRequestData(object? parameter)
{
if (parameter == null) return null;
var isPrimitive = parameter is string or int or long or double or float or decimal or bool or DateTime;
if (isPrimitive)
{
var idMessage = new IdMessage(parameter);
return new SignalPostJsonDataMessage<IdMessage>(idMessage).ToMessagePack(ContractlessStandardResolver.Options);
}
else
{
return new SignalPostJsonDataMessage<object>(parameter).ToMessagePack(ContractlessStandardResolver.Options);
}
}
#endregion
}
/// <summary>
/// Wrapper a Task eredményekhez - a szerver Task-ot ad vissza {"Result":...} formátumban
/// </summary>
public class TaskResultWrapper<T>
{
public T? Result { get; set; }
}

View File

@ -1,218 +0,0 @@
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Diagnostics;
using System.Text;
using System.Text.Json;
namespace Mango.Sandbox.EndPoints.Tests;
/// <summary>
/// SignalR Endpoint tesztek.
/// FONTOS: A SANDBOX-ot manuálisan kell elindítani a tesztek futtatása elõtt!
// Indítás: dotnet run --project Mango.Sandbox.EndPoints --urls http://localhost:59579
/// </summary>
[TestClass]
public class SignalREndpointWithNopEnvTests
{
private static readonly string SandboxUrl = "http://localhost:59579";
private static readonly string HubUrl = $"{SandboxUrl}/fbHub";
// SignalR Tags from FruitBank.Common.SignalRs.SignalRTags
private const int GetMeasuringUsersTag = 70;
private const int GetStockQuantityHistoryDtosTag = 40;
private const int GetStockQuantityHistoryDtosByProductIdTag = 41;
private const int GetShippingDocumentsByShippingIdTag = 60;
private const int GetOrderDtoByIdTag = 21;
private const int GetStockTakingItemsByIdTag = 81;
//[ClassInitialize]
//public static async Task ClassInitialize(TestContext context)
//{
// using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(5) };
// try
// {
// var response = await httpClient.GetAsync($"{SandboxUrl}/health");
// Assert.IsTrue(response.IsSuccessStatusCode,
// "SANDBOX is not running! Start it manually: dotnet run --project Mango.Sandbox.EndPoints --urls http://localhost:59579");
// }
// catch (Exception ex)
// {
// Assert.Fail($"SANDBOX is not running! Error: {ex.Message}\n" +
// "Start it manually: dotnet run --project Mango.Sandbox.EndPoints --urls http://localhost:59579");
// }
//}
//#region HTTP Endpoint Tests
//[TestMethod]
//public async Task HealthEndpoint_ReturnsSuccess()
//{
// using var httpClient = new HttpClient();
// var response = await httpClient.GetAsync($"{SandboxUrl}/health");
// Assert.IsTrue(response.IsSuccessStatusCode, $"Health endpoint returned {response.StatusCode}");
//}
//[TestMethod]
//public async Task RootEndpoint_ReturnsSandboxIsRunning()
//{
// using var httpClient = new HttpClient();
// var response = await httpClient.GetStringAsync(SandboxUrl);
// Assert.AreEqual("SANDBOX is running!", response);
//}
//#endregion
//#region SignalR Connection Tests
//[TestMethod]
//public async Task SignalR_Negotiate_ReturnsSuccess()
//{
// using var httpClient = new HttpClient();
// var response = await httpClient.PostAsync($"{HubUrl}/negotiate?negotiateVersion=1", null);
// Assert.IsTrue(response.IsSuccessStatusCode, $"SignalR negotiate returned {response.StatusCode}");
//}
//[TestMethod]
//public async Task SignalR_Connect_Succeeds()
//{
// var connection = new HubConnectionBuilder()
// .WithUrl(HubUrl)
// .Build();
// try
// {
// await connection.StartAsync();
// Assert.AreEqual(HubConnectionState.Connected, connection.State);
// }
// finally
// {
// await connection.StopAsync();
// }
//}
//#endregion
//#region SignalR Business Endpoint Tests
//[TestMethod]
//public async Task SignalR_GetMeasuringUsers_ReturnsJson()
//{
// await TestSignalREndpoint(GetMeasuringUsersTag, null, "GetMeasuringUsers");
//}
//[TestMethod]
//public async Task SignalR_GetStockQuantityHistoryDtos_ReturnsJson()
//{
// await TestSignalREndpoint(GetStockQuantityHistoryDtosTag, null, "GetStockQuantityHistoryDtos");
//}
//[TestMethod]
//public async Task SignalR_GetStockQuantityHistoryDtosByProductId_ReturnsJson()
//{
// // ProductId = 10
// await TestSignalREndpoint(GetStockQuantityHistoryDtosByProductIdTag, 10, "GetStockQuantityHistoryDtosByProductId");
//}
//[TestMethod]
//public async Task SignalR_GetShippingDocumentsByShippingId_ReturnsJson()
//{
// // ShippingId = 5
// await TestSignalREndpoint(GetShippingDocumentsByShippingIdTag, 5, "GetShippingDocumentsByShippingId");
//}
//[TestMethod]
//public async Task SignalR_GetOrderDtoById_ReturnsJson()
//{
// // OrderId = 15
// await TestSignalREndpoint(GetOrderDtoByIdTag, 15, "GetOrderDtoById");
//}
//[TestMethod]
//public async Task SignalR_GetStockTakingItemsById_ReturnsJson()
//{
// // StockTakingItemId = 200
// await TestSignalREndpoint(GetStockTakingItemsByIdTag, 200, "GetStockTakingItemsById");
//}
//#endregion
//#region Helper Methods
//private async Task TestSignalREndpoint(int tag, object? parameter, string endpointName)
//{
// var connection = new HubConnectionBuilder()
// .WithUrl(HubUrl)
// .Build();
// string? receivedJson = null;
// int receivedTag = -1;
// var responseReceived = new TaskCompletionSource<bool>();
// connection.On<int, byte[]>("ReceiveMessage", (responseTag, data) =>
// {
// receivedTag = responseTag;
// if (data != null && data.Length > 0)
// {
// receivedJson = Encoding.UTF8.GetString(data);
// }
// responseReceived.TrySetResult(true);
// });
// try
// {
// await connection.StartAsync();
// Assert.AreEqual(HubConnectionState.Connected, connection.State, $"Failed to connect to SignalR hub for {endpointName}");
// // Készítsük el a request data-t
// byte[] requestData = parameter != null
// ? Encoding.UTF8.GetBytes(JsonSerializer.Serialize(parameter))
// : Array.Empty<byte>();
// await connection.InvokeAsync("ReceiveMessage", tag, requestData);
// var completed = await Task.WhenAny(responseReceived.Task, Task.Delay(15000));
// if (completed == responseReceived.Task)
// {
// Console.WriteLine($"[{endpointName}] Response tag: {receivedTag}");
// Console.WriteLine($"[{endpointName}] Response JSON: {receivedJson?.Substring(0, Math.Min(500, receivedJson?.Length ?? 0))}...");
// // Ellenõrizzük, hogy valid JSON-e (ha van adat)
// if (!string.IsNullOrEmpty(receivedJson))
// {
// try
// {
// var jsonDoc = JsonDocument.Parse(receivedJson);
// Assert.IsTrue(
// jsonDoc.RootElement.ValueKind == JsonValueKind.Array ||
// jsonDoc.RootElement.ValueKind == JsonValueKind.Object ||
// jsonDoc.RootElement.ValueKind == JsonValueKind.Null,
// $"[{endpointName}] Response is not a valid JSON");
// }
// catch (JsonException ex)
// {
// Assert.Fail($"[{endpointName}] Invalid JSON response: {ex.Message}");
// }
// }
// }
// else
// {
// Assert.AreEqual(HubConnectionState.Connected, connection.State,
// $"[{endpointName}] Connection was closed - check SANDBOX logs for DI errors");
// }
// }
// catch (Exception ex)
// {
// Assert.Fail($"[{endpointName}] SignalR error: {ex.Message}. Check SANDBOX logs for missing DI registrations.");
// }
// finally
// {
// if (connection.State == HubConnectionState.Connected)
// {
// await connection.StopAsync();
// }
// }
//}
//
//#endregion
}

View File

@ -1,4 +1,5 @@
using AyCode.Core.Enums;
using AyCode.Core.Extensions;
using AyCode.Core.Loggers;
using AyCode.Models.Server.DynamicMethods;
using AyCode.Services.Server.SignalRs;
@ -23,16 +24,16 @@ public class DevAdminSignalRHubSandbox : AcWebSignalRHubWithSessionBase<SignalRT
public DevAdminSignalRHubSandbox(IConfiguration configuration, ITestSignalREndpointServer testSignalREndpoint, IEnumerable<IAcLogWriterBase> logWriters)
: base(configuration, new Logger<DevAdminSignalRHubSandbox>(logWriters.ToArray()))
{
SerializerOptions = new AcBinarySerializerOptions();
DynamicMethodCallModels.Add(new AcDynamicMethodCallModel<SignalRAttribute>(testSignalREndpoint));
}
protected override Task SendMessageToClient(IAcSignalRHubItemServer sendTo, int messageTag, ISignalRMessage message, int? requestId = null)
{
Console.WriteLine(((SignalResponseJsonMessage)message).ResponseDataJson);
//protected override Task SendMessageToClient(IAcSignalRHubItemServer sendTo, int messageTag, ISignalRMessage message, int? requestId = null)
//{
// Console.WriteLine(((SignalResponseJsonMessage)message).ResponseDataJson);
return base.SendMessageToClient(sendTo, messageTag, message, requestId);
}
// return base.SendMessageToClient(sendTo, messageTag, message, requestId);
//}
}
// ===========================================

View File

@ -396,6 +396,75 @@ public class TestSignalREndpoint : ITestSignalREndpointServer
}
#endregion
#region Binary Serialization Tesztek - CustomerDto reprodukálás
/// <summary>
/// CustomerDto lista visszaadása - reprodukálja a GetMeasuringUsers forgatókönyvet
/// </summary>
[SignalR(TestSignalRTags.GetCustomerDtoList)]
public Task<List<TestCustomerDto>> GetCustomerDtoList()
{
var customers = new List<TestCustomerDto>
{
new()
{
Id = 1,
Username = "user1",
Email = "user1@test.com",
FirstName = "First1",
LastName = "Last1",
RegisteredInStoreId = 1,
Deleted = false
},
new()
{
Id = 2,
Username = "user2",
Email = "user2@test.com",
FirstName = "First2",
LastName = "Last2",
RegisteredInStoreId = 1,
Deleted = false
},
new()
{
Id = 3,
Username = "user3",
Email = "user3@test.com",
FirstName = "First3",
LastName = "Last3",
RegisteredInStoreId = 2,
Deleted = true
}
};
return Task.FromResult(customers);
}
/// <summary>
/// Nagyobb CustomerDto lista - több interned string a szerializációban
/// </summary>
[SignalR(TestSignalRTags.GetLargeCustomerDtoList)]
public Task<List<TestCustomerDto>> GetLargeCustomerDtoList()
{
var customers = new List<TestCustomerDto>();
for (int i = 1; i <= 30; i++)
{
customers.Add(new TestCustomerDto
{
Id = i,
Username = $"user{i}",
Email = $"user{i}@test.com",
FirstName = $"FirstName{i}",
LastName = $"LastName{i}",
RegisteredInStoreId = (i % 3) + 1,
Deleted = i % 5 == 0
});
}
return Task.FromResult(customers);
}
#endregion
}
/// <summary>
@ -449,6 +518,10 @@ public static class TestSignalRTags
public const int DtoAndListParam = 9061;
public const int ThreeComplexParams = 9062;
public const int FiveParams = 9063;
// Binary serialization tesztek - CustomerDto reprodukálás
public const int GetCustomerDtoList = 9070;
public const int GetLargeCustomerDtoList = 9071;
}
/// <summary>
@ -500,6 +573,10 @@ public interface ITestSignalREndpointServer
Task<string> HandleDtoAndList(TestOrderItem item, List<int> numbers);
Task<string> HandleThreeComplexParams(TestOrderItem item, List<string> tags, SharedTag sharedTag);
Task<string> HandleFiveParams(int a, string b, bool c, Guid d, decimal e);
// Binary serialization tesztek - CustomerDto reprodukálás
Task<List<TestCustomerDto>> GetCustomerDtoList();
Task<List<TestCustomerDto>> GetLargeCustomerDtoList();
}
#region DTOs
@ -584,4 +661,19 @@ public class SharedTag
public bool IsActive { get; set; }
}
/// <summary>
/// Teszt CustomerDto a felhasználók teszteléséhez
/// </summary>
public class TestCustomerDto
{
public int Id { get; set; }
public string Username { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public string FullName => $"{LastName} {FirstName}";
public int RegisteredInStoreId { get; set; }
public bool Deleted { get; set; }
}
#endregion