using CloudServicesCrud;
using Grpc.Core;
using Grpc.Net.Client;
using Microsoft.Extensions.Configuration;
using Xunit;
namespace DynamicCrud.Tests.Grpc;
///
/// Canal gRPC compartido contra el DAO desplegado. Estas pruebas son de caja negra:
/// no tocan la base de datos ni levantan el servidor en proceso, solo hablan gRPC
/// contra el endpoint que se les indique (típicamente un kubectl port-forward a QA).
///
public class GrpcFixture : IDisposable
{
/// Endpoint por defecto: el port-forward al puerto TLS del pod.
private const string DefaultEndpoint = "https://127.0.0.1:8440";
public GrpcChannel Channel { get; }
public string Endpoint { get; }
public GrpcFixture()
{
var config = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("appsettings.test.json", optional: true)
.AddEnvironmentVariables()
.Build();
Endpoint = config["Dao:GrpcEndpoint"] ?? DefaultEndpoint;
// El certificado del servidor está emitido para su nombre dentro del cluster, no para
// 127.0.0.1, así que a través de un port-forward la validación siempre fallaría.
// Esto es el equivalente de `grpcurl -insecure`: cifra, pero no valida el nombre.
var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback =
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
};
// El pod también expone un puerto en h2c plano (50051). Si el endpoint apunta ahí,
// hay que habilitar HTTP/2 sin TLS explícitamente.
if (Endpoint.StartsWith("http://", StringComparison.OrdinalIgnoreCase))
AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
Channel = GrpcChannel.ForAddress(Endpoint, new GrpcChannelOptions
{
HttpHandler = handler,
// Sin esto, un endpoint caído deja cada prueba colgada hasta el timeout global.
MaxReceiveMessageSize = 32 * 1024 * 1024
});
EnsureReachable();
}
///
/// Falla temprano y con un mensaje accionable si el endpoint no responde. Sin esto,
/// un port-forward caído produce decenas de timeouts crípticos en vez de una causa.
///
private void EnsureReachable()
{
var client = new CloudServicesCrud.DynamicCrudService.DynamicCrudServiceClient(Channel);
try
{
// GetFields es la llamada más barata que existe: resuelve el tipo por reflexión
// y no toca la base de datos.
client.GetFields(
new GetFieldsRequest { Model = "region" },
deadline: DateTime.UtcNow.AddSeconds(10));
}
catch (RpcException ex)
{
throw new InvalidOperationException(
$"No se pudo contactar el DAO en {Endpoint} ({ex.StatusCode}). " +
"Levanta el port-forward antes de correr las pruebas:\n" +
" kubectl port-forward svc/csharp-dao-service 8440:8440 -n \n" +
"Para apuntar a otro endpoint, define la variable de entorno Dao__GrpcEndpoint " +
"(por ejemplo http://127.0.0.1:50051 para el puerto sin TLS).", ex);
}
}
public void Dispose() => Channel.Dispose();
}
[CollectionDefinition(GrpcCollection.Name)]
public class GrpcCollection : ICollectionFixture
{
public const string Name = "grpc-dao";
}