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"; }