diff --git a/Directory.Build.props b/Directory.Build.props index b8e6f2fcd..7d05141e9 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -9,6 +9,9 @@ $(WarningsNotAsErrors);CS1591 + + + preview diff --git a/Grpc.AspNetCore.sln b/Grpc.AspNetCore.sln index 32cad6ca8..bbddb2873 100644 --- a/Grpc.AspNetCore.sln +++ b/Grpc.AspNetCore.sln @@ -56,11 +56,18 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "testassets", "testassets", EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Proto", "Proto", "{BF1393D4-6099-4EF9-85BB-7EE6CBEB920C}" ProjectSection(SolutionItems) = preProject + testassets\Proto\authorize.proto = testassets\Proto\authorize.proto testassets\Proto\chat.proto = testassets\Proto\chat.proto testassets\Proto\compression.proto = testassets\Proto\compression.proto testassets\Proto\count.proto = testassets\Proto\count.proto + testassets\Proto\empty.proto = testassets\Proto\empty.proto testassets\Proto\greet.proto = testassets\Proto\greet.proto + testassets\Proto\lifetime.proto = testassets\Proto\lifetime.proto testassets\Proto\message.proto = testassets\Proto\message.proto + testassets\Proto\messages.proto = testassets\Proto\messages.proto + testassets\Proto\nested.proto = testassets\Proto\nested.proto + testassets\Proto\singleton.proto = testassets\Proto\singleton.proto + testassets\Proto\test.proto = testassets\Proto\test.proto EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FunctionalTestsWebsite", "testassets\FunctionalTestsWebsite\FunctionalTestsWebsite.csproj", "{7B95289B-4992-4C0D-B26F-8EC58F81FC96}" @@ -110,6 +117,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Grpc.AspNetCore.Server.Refl EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reflector", "examples\Clients\Reflector\Reflector.csproj", "{86AD33E9-2C07-45BD-B599-420C2618188D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InteropTestsClient", "testassets\InteropTestsClient\InteropTestsClient.csproj", "{291E5BA5-608D-406D-A2DB-389412D907F3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Grpc.NetCore.HttpClient.Tests", "test\Grpc.NetCore.HttpClient.Tests\Grpc.NetCore.HttpClient.Tests.csproj", "{2D26FB7A-72AD-41B9-9B06-44F50A8F8A49}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -188,6 +199,14 @@ Global {86AD33E9-2C07-45BD-B599-420C2618188D}.Debug|Any CPU.Build.0 = Debug|Any CPU {86AD33E9-2C07-45BD-B599-420C2618188D}.Release|Any CPU.ActiveCfg = Release|Any CPU {86AD33E9-2C07-45BD-B599-420C2618188D}.Release|Any CPU.Build.0 = Release|Any CPU + {2D26FB7A-72AD-41B9-9B06-44F50A8F8A49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2D26FB7A-72AD-41B9-9B06-44F50A8F8A49}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2D26FB7A-72AD-41B9-9B06-44F50A8F8A49}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2D26FB7A-72AD-41B9-9B06-44F50A8F8A49}.Release|Any CPU.Build.0 = Release|Any CPU + {291E5BA5-608D-406D-A2DB-389412D907F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {291E5BA5-608D-406D-A2DB-389412D907F3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {291E5BA5-608D-406D-A2DB-389412D907F3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {291E5BA5-608D-406D-A2DB-389412D907F3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -221,6 +240,8 @@ Global {39320CA8-D8F0-45B6-B704-A04C16870226} = {310E5783-455A-4D09-A7AE-39DC2AB09504} {55813F20-1269-4B19-B03E-7E4A90148F92} = {8C62055F-8CD7-4859-9001-634D544DF2AE} {86AD33E9-2C07-45BD-B599-420C2618188D} = {F6E0F9D7-64E5-4C7B-A9BC-3C2AD687710B} + {2D26FB7A-72AD-41B9-9B06-44F50A8F8A49} = {CECC4AE8-9C4E-4727-939B-517CC2E58D65} + {291E5BA5-608D-406D-A2DB-389412D907F3} = {59C7B1F0-EE4D-4098-8596-0ADDBC305234} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {CD5C2B19-49B4-480A-990C-36D98A719B07} diff --git a/build/dependencies.props b/build/dependencies.props index edae8b3f7..090dc3daa 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -1,6 +1,7 @@ 0.11.3 + 3.0.0 3.7.0 1.20.0-pre3 1.20.0-pre3 @@ -12,7 +13,7 @@ 16.0.0-preview-20181205-02 1.0.0-beta2-18618-05 4.10.0 - 12.0.1 + 12.0.2 3.11.0 3.12.0 4.6.0-preview.19073.11 diff --git a/build/sources.props b/build/sources.props index 104d54c7c..2d2fb609b 100644 --- a/build/sources.props +++ b/build/sources.props @@ -4,6 +4,7 @@ $(RestoreSources); https://api.nuget.org/v3/index.json; https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json; + https://dotnetfeed.blob.core.windows.net/dotnet-core/index.json; $(RestoreSources); diff --git a/examples/Clients/Counter/Counter.csproj b/examples/Clients/Counter/Counter.csproj index e18ad41a9..10d1873c5 100644 --- a/examples/Clients/Counter/Counter.csproj +++ b/examples/Clients/Counter/Counter.csproj @@ -3,7 +3,6 @@ Exe netcoreapp3.0 - latest @@ -22,4 +21,5 @@ + diff --git a/examples/Clients/Counter/Program.cs b/examples/Clients/Counter/Program.cs index 0fd565f12..e6c4e8937 100644 --- a/examples/Clients/Counter/Program.cs +++ b/examples/Clients/Counter/Program.cs @@ -17,6 +17,7 @@ #endregion using System; +using System.IO; using System.Runtime.InteropServices; using System.Threading.Tasks; using Common; @@ -36,9 +37,24 @@ static async Task Main(string[] args) var channel = new Channel("localhost:50051", credentials); var client = new Counter.CounterClient(channel); - var reply = client.IncrementCount(new Google.Protobuf.WellKnownTypes.Empty()); + await UnaryCallExample(client); + + await ClientStreamingCallExample(client); + + Console.WriteLine("Shutting down"); + await channel.ShutdownAsync(); + Console.WriteLine("Press any key to exit..."); + Console.ReadKey(); + } + + private static async Task UnaryCallExample(Counter.CounterClient client) + { + var reply = await client.IncrementCountAsync(new Google.Protobuf.WellKnownTypes.Empty()); Console.WriteLine("Count: " + reply.Count); + } + private static async Task ClientStreamingCallExample(Counter.CounterClient client) + { using (var call = client.AccumulateCount()) { for (int i = 0; i < 3; i++) @@ -50,13 +66,10 @@ static async Task Main(string[] args) } await call.RequestStream.CompleteAsync(); - Console.WriteLine($"Count: {(await call.ResponseAsync).Count}"); - } - Console.WriteLine("Shutting down"); - await channel.ShutdownAsync(); - Console.WriteLine("Press any key to exit..."); - Console.ReadKey(); + var response = await call; + Console.WriteLine($"Count: {response.Count}"); + } } } } diff --git a/examples/Clients/Greeter/Greeter.csproj b/examples/Clients/Greeter/Greeter.csproj index 20faebda9..c11bb2333 100644 --- a/examples/Clients/Greeter/Greeter.csproj +++ b/examples/Clients/Greeter/Greeter.csproj @@ -3,7 +3,6 @@ Exe netcoreapp3.0 - latest @@ -22,4 +21,5 @@ + diff --git a/examples/Clients/Mailer/Mailer.csproj b/examples/Clients/Mailer/Mailer.csproj index 4f96ecea4..621e8dc73 100644 --- a/examples/Clients/Mailer/Mailer.csproj +++ b/examples/Clients/Mailer/Mailer.csproj @@ -3,7 +3,6 @@ Exe netcoreapp3.0 - latest diff --git a/examples/Clients/Mailer/Program.cs b/examples/Clients/Mailer/Program.cs index 0b454e061..7bcefb37e 100644 --- a/examples/Clients/Mailer/Program.cs +++ b/examples/Clients/Mailer/Program.cs @@ -43,7 +43,7 @@ static async Task Main(string[] args) await channel.ConnectAsync(); Console.WriteLine("Connected"); - Console.WriteLine("Press escape to exit. Press any other key to forward mail."); + Console.WriteLine("Press escape to disconnect. Press any other key to forward mail."); var client = new Mailer.MailerClient(channel); using (var mailbox = client.Mailbox(headers: new Metadata { new Metadata.Entry("mailbox-name", mailboxName) })) @@ -78,6 +78,9 @@ static async Task Main(string[] args) Console.WriteLine("Disconnecting"); await channel.ShutdownAsync(); + + Console.WriteLine("Disconnected. Press any key to exit."); + Console.ReadKey(); } private static string GetMailboxName(string[] args) diff --git a/examples/Clients/Reflector/Reflector.csproj b/examples/Clients/Reflector/Reflector.csproj index ea7110ca9..d23ab6d4b 100644 --- a/examples/Clients/Reflector/Reflector.csproj +++ b/examples/Clients/Reflector/Reflector.csproj @@ -3,7 +3,6 @@ Exe netcoreapp3.0 - latest diff --git a/examples/Server/Services/Greeter.cs b/examples/Server/Services/Greeter.cs index 7fa9a7984..695f9c26a 100644 --- a/examples/Server/Services/Greeter.cs +++ b/examples/Server/Services/Greeter.cs @@ -34,9 +34,6 @@ public GreeterService(ILoggerFactory loggerFactory) //Server side handler of the SayHello RPC public override Task SayHello(HelloRequest request, ServerCallContext context) { - var httpContext = context.GetHttpContext(); - _logger.LogInformation($"Connection id: {httpContext.Connection.Id}"); - _logger.LogInformation($"Sending hello to {request.Name}"); return Task.FromResult(new HelloReply { Message = "Hello " + request.Name }); } diff --git a/global.json b/global.json index 04bcb8999..aa30320ef 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "3.0.100-preview4-011108" + "version": "3.0.100-preview6-011568" } } diff --git a/perf/benchmarkapps/BenchmarkClient/BenchmarkClient.csproj b/perf/benchmarkapps/BenchmarkClient/BenchmarkClient.csproj index 5ed86e53e..494916146 100644 --- a/perf/benchmarkapps/BenchmarkClient/BenchmarkClient.csproj +++ b/perf/benchmarkapps/BenchmarkClient/BenchmarkClient.csproj @@ -3,7 +3,6 @@ Exe netcoreapp3.0 - latest diff --git a/perf/benchmarkapps/BenchmarkServer/BenchmarkServer.csproj b/perf/benchmarkapps/BenchmarkServer/BenchmarkServer.csproj index b769e7b29..a5788cf7c 100644 --- a/perf/benchmarkapps/BenchmarkServer/BenchmarkServer.csproj +++ b/perf/benchmarkapps/BenchmarkServer/BenchmarkServer.csproj @@ -4,7 +4,6 @@ netcoreapp3.0 $(BenchmarksTargetFramework) Exe - latest InProcess @@ -35,5 +34,6 @@ + diff --git a/src/Grpc.AspNetCore.Server/Internal/GrpcProtocolHelpers.cs b/src/Grpc.AspNetCore.Server/Internal/GrpcProtocolHelpers.cs index c62aa9ab0..162fd1bb2 100644 --- a/src/Grpc.AspNetCore.Server/Internal/GrpcProtocolHelpers.cs +++ b/src/Grpc.AspNetCore.Server/Internal/GrpcProtocolHelpers.cs @@ -205,7 +205,7 @@ public static void SetStatusTrailers(HttpResponse response, Status status) { // Use SetTrailer here because we want to overwrite any that was set earlier SetTrailer(response, GrpcProtocolConstants.StatusTrailer, status.StatusCode.ToTrailerString()); - SetTrailer(response, GrpcProtocolConstants.MessageTrailer, status.Detail); + SetTrailer(response, GrpcProtocolConstants.MessageTrailer, !string.IsNullOrEmpty(status.Detail) ? status.Detail : null); } private static void SetTrailer(HttpResponse response, string trailerName, StringValues trailerValues) diff --git a/src/Grpc.NetCore.HttpClient/ClientAsyncStreamReader.cs b/src/Grpc.NetCore.HttpClient/ClientAsyncStreamReader.cs deleted file mode 100644 index a608f9b56..000000000 --- a/src/Grpc.NetCore.HttpClient/ClientAsyncStreamReader.cs +++ /dev/null @@ -1,65 +0,0 @@ -#region Copyright notice and license - -// Copyright 2019 The gRPC Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#endregion - -using System; -using System.IO; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Grpc.Core; - -namespace Grpc.NetCore.HttpClient -{ - internal class ClientAsyncStreamReader : IAsyncStreamReader - { - private Task _sendTask; - private Stream _responseStream; - private Func _deserializer; - - public ClientAsyncStreamReader(Task sendTask, Func deserializer) - { - _sendTask = sendTask; - _deserializer = deserializer; - } - - public TResponse Current { get; private set; } - - public void Dispose() - { - } - - public async Task MoveNext(CancellationToken cancellationToken) - { - try - { - if (_responseStream == null) - { - var responseMessage = await _sendTask; - _responseStream = await responseMessage.Content.ReadAsStreamAsync(); - } - - Current = _responseStream.ReadSingleMessage(_deserializer); - return true; - } - catch - { - return false; - } - } - } -} diff --git a/src/Grpc.NetCore.HttpClient/Grpc.NetCore.HttpClient.csproj b/src/Grpc.NetCore.HttpClient/Grpc.NetCore.HttpClient.csproj index 88ce1c68f..57a36a628 100644 --- a/src/Grpc.NetCore.HttpClient/Grpc.NetCore.HttpClient.csproj +++ b/src/Grpc.NetCore.HttpClient/Grpc.NetCore.HttpClient.csproj @@ -12,15 +12,18 @@ - netstandard2.0 + + netcoreapp3.0 8.0 true true + + + - diff --git a/src/Grpc.NetCore.HttpClient/GrpcClientFactory.cs b/src/Grpc.NetCore.HttpClient/GrpcClientFactory.cs index 394f0fc3e..cca471861 100644 --- a/src/Grpc.NetCore.HttpClient/GrpcClientFactory.cs +++ b/src/Grpc.NetCore.HttpClient/GrpcClientFactory.cs @@ -42,14 +42,37 @@ public static TClient Create(string baseAddress, X509Certificate certif // Needs to be set before creating the HttpClientHandler AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2Support", true); - var handler = new HttpClientHandler(); - handler.ServerCertificateCustomValidationCallback = (httpRequestMessage, cert, cetChain, policyErrors) => true; + var httpClientHandler = new HttpClientHandler(); + httpClientHandler.ServerCertificateCustomValidationCallback = (httpRequestMessage, cert, cetChain, policyErrors) => true; if (certificate != null) { - handler.ClientCertificates.Add(certificate); + httpClientHandler.ClientCertificates.Add(certificate); } - return Cache.Instance.Activator(new HttpClientCallInvoker(handler, new Uri(baseAddress, UriKind.RelativeOrAbsolute))); + return CreateCore(baseAddress, httpClientHandler); + } + + /// + /// Creates a gRPC client using the specified address and handler. + /// + /// The type of the gRPC client. This type will typically be defined using generated code from a *.proto file. + /// The base address to use when making gRPC requests. + /// The . + /// A gRPC client. + public static TClient Create(string baseAddress, HttpClientHandler httpClientHandler) where TClient : ClientBase + { + return CreateCore(baseAddress, httpClientHandler); + } + + private static TClient CreateCore(string baseAddress, HttpClientHandler httpClientHandler) where TClient : ClientBase + { + // Needs to be set before creating the HttpClientHandler + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2Support", true); + + var httpClient = new System.Net.Http.HttpClient(httpClientHandler); + httpClient.BaseAddress = new Uri(baseAddress, UriKind.RelativeOrAbsolute); + + return Cache.Instance.Activator(new HttpClientCallInvoker(httpClient)); } private class Cache diff --git a/src/Grpc.NetCore.HttpClient/HttpClientCallInvoker.cs b/src/Grpc.NetCore.HttpClient/HttpClientCallInvoker.cs index 63dbae751..1e9f37f2f 100644 --- a/src/Grpc.NetCore.HttpClient/HttpClientCallInvoker.cs +++ b/src/Grpc.NetCore.HttpClient/HttpClientCallInvoker.cs @@ -21,6 +21,7 @@ using System.Threading; using System.Threading.Tasks; using Grpc.Core; +using Grpc.NetCore.HttpClient.Internal; namespace Grpc.NetCore.HttpClient { @@ -31,16 +32,8 @@ public class HttpClientCallInvoker : CallInvoker { private System.Net.Http.HttpClient _client; - /// - /// Initializes a new instance of the class. - /// - /// The primary client handler to use for gRPC requests. - /// The base address to use when making gRPC requests. - public HttpClientCallInvoker(HttpClientHandler handler, Uri baseAddress) - { - _client = new System.Net.Http.HttpClient(handler); - _client.BaseAddress = baseAddress; - } + // Override the current time for unit testing + internal ISystemClock Clock = SystemClock.Instance; /// /// Initializes a new instance of the class. @@ -80,22 +73,16 @@ public HttpClientCallInvoker(System.Net.Http.HttpClient client) /// public override AsyncClientStreamingCall AsyncClientStreamingCall(Method method, string host, CallOptions options) { - var pipeContent = new PipeContent(); - var message = new HttpRequestMessage(HttpMethod.Post, method.FullName); - message.Content = pipeContent; - message.Version = new Version(2, 0); - - var sendTask = SendRequestMessageAsync(() => Task.CompletedTask, _client, message); + var call = CreateGrpcCall(method, options); + call.StartClientStreaming(_client); return new AsyncClientStreamingCall( - requestStream: new PipeClientStreamWriter(pipeContent.PipeWriter, method.RequestMarshaller.Serializer, options.WriteOptions), - responseAsync: GetResponseAsync(sendTask, method.ResponseMarshaller.Deserializer), - responseHeadersAsync: GetResponseHeadersAsync(sendTask), - // Cannot implement due to trailers being unimplemented - getStatusFunc: () => new Status(), - // Cannot implement due to trailers being unimplemented - getTrailersFunc: () => new Metadata(), - disposeAction: () => { }); + requestStream: call.ClientStreamWriter, + responseAsync: call.GetResponseAsync(), + responseHeadersAsync: call.GetResponseHeadersAsync(), + getStatusFunc: call.GetStatus, + getTrailersFunc: call.GetTrailers, + disposeAction: call.Dispose); } /// @@ -105,22 +92,16 @@ public override AsyncClientStreamingCall AsyncClientStreami /// public override AsyncDuplexStreamingCall AsyncDuplexStreamingCall(Method method, string host, CallOptions options) { - var pipeContent = new PipeContent(); - var message = new HttpRequestMessage(HttpMethod.Post, method.FullName); - message.Content = pipeContent; - message.Version = new Version(2, 0); - - var sendTask = SendRequestMessageAsync(() => Task.CompletedTask, _client, message); + var call = CreateGrpcCall(method, options); + call.StartDuplexStreaming(_client); return new AsyncDuplexStreamingCall( - requestStream: new PipeClientStreamWriter(pipeContent.PipeWriter, method.RequestMarshaller.Serializer, options.WriteOptions), - responseStream: new ClientAsyncStreamReader(sendTask, method.ResponseMarshaller.Deserializer), - responseHeadersAsync: GetResponseHeadersAsync(sendTask), - // Cannot implement due to trailers being unimplemented - getStatusFunc: () => new Status(), - // Cannot implement due to trailers being unimplemented - getTrailersFunc: () => new Metadata(), - disposeAction: () => { }); + requestStream: call.ClientStreamWriter, + responseStream: call.ClientStreamReader, + responseHeadersAsync: call.GetResponseHeadersAsync(), + getStatusFunc: call.GetStatus, + getTrailersFunc: call.GetTrailers, + disposeAction: call.Dispose); } /// @@ -129,28 +110,15 @@ public override AsyncDuplexStreamingCall AsyncDuplexStreami /// public override AsyncServerStreamingCall AsyncServerStreamingCall(Method method, string host, CallOptions options, TRequest request) { - var content = new PipeContent(); - var message = new HttpRequestMessage(HttpMethod.Post, method.FullName); - message.Content = content; - message.Version = new Version(2, 0); - - // Write request body - var sendTask = SendRequestMessageAsync( - async () => - { - await content.PipeWriter.WriteMessageCoreAsync(method.RequestMarshaller.Serializer(request), true); - content.PipeWriter.Complete(); - }, - _client, message); + var call = CreateGrpcCall(method, options); + call.StartServerStreaming(_client, request); return new AsyncServerStreamingCall( - responseStream: new ClientAsyncStreamReader(sendTask, method.ResponseMarshaller.Deserializer), - responseHeadersAsync: GetResponseHeadersAsync(sendTask), - // Cannot implement due to trailers being unimplemented - getStatusFunc: () => new Status(), - // Cannot implement due to trailers being unimplemented - getTrailersFunc: () => new Metadata(), - disposeAction: () => { }); + responseStream: call.ClientStreamReader, + responseHeadersAsync: call.GetResponseHeadersAsync(), + getStatusFunc: call.GetStatus, + getTrailersFunc: call.GetTrailers, + disposeAction: call.Dispose); } /// @@ -158,28 +126,15 @@ public override AsyncServerStreamingCall AsyncServerStreamingCall public override AsyncUnaryCall AsyncUnaryCall(Method method, string host, CallOptions options, TRequest request) { - var content = new PipeContent(); - var message = new HttpRequestMessage(HttpMethod.Post, method.FullName); - message.Content = content; - message.Version = new Version(2, 0); - - // Write request body - var sendTask = SendRequestMessageAsync( - async () => - { - await content.PipeWriter.WriteMessageCoreAsync(method.RequestMarshaller.Serializer(request), true); - content.PipeWriter.Complete(); - }, - _client, message); + var call = CreateGrpcCall(method, options); + call.StartUnary(_client, request); return new AsyncUnaryCall( - responseAsync: GetResponseAsync(sendTask, method.ResponseMarshaller.Deserializer), - responseHeadersAsync: GetResponseHeadersAsync(sendTask), - // Cannot implement due to trailers being unimplemented - getStatusFunc: () => new Status(), - // Cannot implement due to trailers being unimplemented - getTrailersFunc: () => new Metadata(), - disposeAction: () => { }); + responseAsync: call.GetResponseAsync(), + responseHeadersAsync: call.GetResponseHeadersAsync(), + getStatusFunc: call.GetStatus, + getTrailersFunc: call.GetTrailers, + disposeAction: call.Dispose); } /// @@ -187,74 +142,13 @@ public override AsyncUnaryCall AsyncUnaryCall(Me /// public override TResponse BlockingUnaryCall(Method method, string host, CallOptions options, TRequest request) { - return AsyncUnaryCall(method, host, options, request)?.GetAwaiter().GetResult(); - } - - private static async Task SendRequestMessageAsync(Func writeMessageTask, System.Net.Http.HttpClient client, HttpRequestMessage message) - { - await writeMessageTask(); - return await client.SendAsync(message, HttpCompletionOption.ResponseHeadersRead); - } - - private static async Task GetResponseAsync(Task sendTask, Func deserializer) - { - // We can't use pipes here since we can't control how much is read and response trailers causes InvalidOperationException - var response = await sendTask; - var responseStream = await response.Content.ReadAsStreamAsync(); - - return responseStream.ReadSingleMessage(deserializer); + var call = AsyncUnaryCall(method, host, options, request); + return call.ResponseAsync.GetAwaiter().GetResult(); } - private static async Task GetResponseHeadersAsync(Task sendTask) + private GrpcCall CreateGrpcCall(Method method, CallOptions options) { - var response = await sendTask; - - var headers = new Metadata(); - - foreach (var header in response.Headers) - { - // ASP.NET Core includes pseudo headers in the set of request headers - // whereas, they are not in gRPC implementations. We will filter them - // out when we construct the list of headers on the context. - if (header.Key.StartsWith(":", StringComparison.Ordinal)) - { - continue; - } - else if (header.Key.EndsWith(Metadata.BinaryHeaderSuffix, StringComparison.OrdinalIgnoreCase)) - { - headers.Add(header.Key, ParseBinaryHeader(string.Join(",", header.Value))); - } - else - { - headers.Add(header.Key, string.Join(",", header.Value)); - } - } - return null; - } - - private static byte[] ParseBinaryHeader(string base64) - { - string decodable; - switch (base64.Length % 4) - { - case 0: - // base64 has the required padding - decodable = base64; - break; - case 2: - // 2 chars padding - decodable = base64 + "=="; - break; - case 3: - // 3 chars padding - decodable = base64 + "="; - break; - default: - // length%4 == 1 should be illegal - throw new FormatException("Invalid base64 header value"); - } - - return Convert.FromBase64String(decodable); + return new GrpcCall(method, options, Clock); } } } diff --git a/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs b/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs new file mode 100644 index 000000000..db76ef9ba --- /dev/null +++ b/src/Grpc.NetCore.HttpClient/Internal/GrpcCall.cs @@ -0,0 +1,486 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; +using Grpc.Core; + +namespace Grpc.NetCore.HttpClient.Internal +{ + internal class GrpcCall : IDisposable + { + private readonly CancellationTokenSource _callCts; + private readonly CancellationTokenRegistration? _ctsRegistration; + private readonly ISystemClock _clock; + private readonly TimeSpan? _timeout; + private readonly Timer _deadlineTimer; + private Metadata _trailers; + private CancellationTokenRegistration? _writerCtsRegistration; + private string _headerValidationError; + + public bool DeadlineReached { get; private set; } + public bool Disposed { get; private set; } + public bool ResponseFinished { get; private set; } + public HttpResponseMessage HttpResponse { get; private set; } + public CallOptions Options { get; } + public Method Method { get; } + public Task SendTask { get; private set; } + public HttpContentClientStreamWriter ClientStreamWriter { get; private set; } + public HttpContentClientStreamReader ClientStreamReader { get; private set; } + + public GrpcCall(Method method, CallOptions options, ISystemClock clock) + { + // Validate deadline before creating any objects that require cleanup + ValidateDeadline(options.Deadline); + + _callCts = new CancellationTokenSource(); + Method = method; + Options = options; + _clock = clock; + + if (options.CancellationToken.CanBeCanceled) + { + // The cancellation token will cancel the call CTS + _ctsRegistration = options.CancellationToken.Register(CancelCall); + } + + if (options.Deadline != null && options.Deadline != DateTime.MaxValue) + { + var timeout = options.Deadline.Value - _clock.UtcNow; + _timeout = (timeout > TimeSpan.Zero) ? timeout : TimeSpan.Zero; + } + + if (_timeout != null) + { + // Deadline timer will cancel the call CTS + _deadlineTimer = new Timer(DeadlineExceeded, null, _timeout.Value, Timeout.InfiniteTimeSpan); + } + } + + private void ValidateDeadline(DateTime? deadline) + { + if (deadline != null && deadline != DateTime.MaxValue && deadline != DateTime.MinValue && deadline.Value.Kind != DateTimeKind.Utc) + { + throw new InvalidOperationException("Deadline must have a kind DateTimeKind.Utc or be equal to DateTime.MaxValue or DateTime.MinValue."); + } + } + + public CancellationToken CancellationToken + { + get { return _callCts.Token; } + } + + public bool IsCancellationRequested + { + get { return _callCts.IsCancellationRequested; } + } + + public void StartUnary(System.Net.Http.HttpClient client, TRequest request) + { + var message = CreateHttpRequestMessage(); + SetMessageContent(request, message); + StartSend(client, message); + } + + public void StartClientStreaming(System.Net.Http.HttpClient client) + { + var message = CreateHttpRequestMessage(); + ClientStreamWriter = CreateWriter(message); + StartSend(client, message); + } + + public void StartServerStreaming(System.Net.Http.HttpClient client, TRequest request) + { + var message = CreateHttpRequestMessage(); + SetMessageContent(request, message); + StartSend(client, message); + ClientStreamReader = new HttpContentClientStreamReader(this); + } + + public void StartDuplexStreaming(System.Net.Http.HttpClient client) + { + var message = CreateHttpRequestMessage(); + ClientStreamWriter = CreateWriter(message); + StartSend(client, message); + ClientStreamReader = new HttpContentClientStreamReader(this); + } + + /// + /// Dispose can be called by: + /// 1. The user. AsyncUnaryCall.Dispose et al will call this Dispose + /// 2. will call dispose if errors fail validation + /// 3. will call dispose + /// + public void Dispose() + { + if (!Disposed) + { + Disposed = true; + + if (!ResponseFinished) + { + // If the response is not finished then cancel any pending actions: + // 1. Call HttpClient.SendAsync + // 2. Response Stream.ReadAsync + // 3. Client stream + // - Getting the Stream from the Request.HttpContent + // - Holding the Request.HttpContent.SerializeToStream open + // - Writing to the client stream + _callCts.Cancel(); + } + + _ctsRegistration?.Dispose(); + _writerCtsRegistration?.Dispose(); + _deadlineTimer?.Dispose(); + HttpResponse?.Dispose(); + ClientStreamReader?.Dispose(); + ClientStreamWriter?.Dispose(); + + // To avoid racing with Dispose, skip disposing the call CTS + // This avoid Dispose potentially calling cancel on a disposed CTS + // The call CTS is not exposed externally and all dependent registrations + // are cleaned up + } + } + + public void EnsureNotDisposed() + { + if (Disposed) + { + throw new ObjectDisposedException(nameof(GrpcCall)); + } + } + + public void EnsureHeadersValid() + { + if (_headerValidationError != null) + { + throw new InvalidOperationException(_headerValidationError); + } + } + + public Exception CreateCanceledStatusException() + { + if (_headerValidationError != null) + { + return new InvalidOperationException(_headerValidationError); + } + + var statusCode = DeadlineReached ? StatusCode.DeadlineExceeded : StatusCode.Cancelled; + return new RpcException(new Status(statusCode, string.Empty)); + } + + /// + /// Marks the response as finished, i.e. all response content has been read and trailers are available. + /// Can be called by for unary and client streaming calls, or + /// + /// for server streaming and duplex streaming calls. + /// + public void FinishResponse() + { + ResponseFinished = true; + + try + { + // Get status from response before dispose + // This may throw an error if the grpc-status is missing or malformed + var status = GetStatusCore(HttpResponse); + + if (status.StatusCode != StatusCode.OK) + { + throw new RpcException(status); + } + } + finally + { + // Clean up call resources once this call is finished + // Call may not be explicitly disposed when used with unary methods + // e.g. var reply = await client.SayHelloAsync(new HelloRequest()); + Dispose(); + } + } + + public async Task GetResponseHeadersAsync() + { + try + { + await SendTask.ConfigureAwait(false); + + // The task of this method is cached so there is no need to cache the headers here + return GrpcProtocolHelpers.BuildMetadata(HttpResponse.Headers); + } + catch (OperationCanceledException) + { + EnsureNotDisposed(); + throw CreateCanceledStatusException(); + } + } + + public Status GetStatus() + { + ValidateTrailersAvailable(); + + return GetStatusCore(HttpResponse); + } + + public async Task GetResponseAsync() + { + try + { + await SendTask.ConfigureAwait(false); + + // Trailers are only available once the response body had been read + var responseStream = await HttpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); + var message = await responseStream.ReadSingleMessageAsync(Method.ResponseMarshaller.Deserializer, _callCts.Token).ConfigureAwait(false); + FinishResponse(); + + if (message == null) + { + throw new InvalidOperationException("Call did not return a response message"); + } + + // The task of this method is cached so there is no need to cache the message here + return message; + } + catch (OperationCanceledException) + { + EnsureNotDisposed(); + throw CreateCanceledStatusException(); + } + } + + private void ValidateHeaders() + { + if (HttpResponse.StatusCode != HttpStatusCode.OK) + { + _headerValidationError = "Bad gRPC response. Expected HTTP status code 200. Got status code: " + (int)HttpResponse.StatusCode; + } + else if (HttpResponse.Content.Headers.ContentType == null) + { + _headerValidationError = "Bad gRPC response. Response did not have a content-type header."; + } + else + { + var grpcEncoding = HttpResponse.Content.Headers.ContentType.ToString(); + if (!GrpcProtocolHelpers.IsGrpcContentType(grpcEncoding)) + { + _headerValidationError = "Bad gRPC response. Invalid content-type value: " + grpcEncoding; + } + } + + if (_headerValidationError != null) + { + // Response is not valid gRPC + // Clean up/cancel any pending operations + Dispose(); + + throw new InvalidOperationException(_headerValidationError); + } + + // Success! + } + + public Metadata GetTrailers() + { + if (_trailers == null) + { + ValidateTrailersAvailable(); + + _trailers = GrpcProtocolHelpers.BuildMetadata(HttpResponse.TrailingHeaders); + } + + return _trailers; + } + + private void SetMessageContent(TRequest request, HttpRequestMessage message) + { + message.Content = new PushStreamContent( + (stream) => + { + return SerializationHelpers.WriteMessage(stream, request, Method.RequestMarshaller.Serializer, Options.CancellationToken); + }, + GrpcProtocolConstants.GrpcContentTypeHeaderValue); + } + + private void CancelCall() + { + _callCts.Cancel(); + } + + private void StartSend(System.Net.Http.HttpClient client, HttpRequestMessage message) + { + SendTask = SendAsync(client, message); + } + + private async Task SendAsync(System.Net.Http.HttpClient client, HttpRequestMessage message) + { + HttpResponse = await client.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, _callCts.Token).ConfigureAwait(false); + ValidateHeaders(); + } + + private HttpContentClientStreamWriter CreateWriter(HttpRequestMessage message) + { + var writeStreamTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var completeTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + // Canceling call will cancel pending writes to the stream + _writerCtsRegistration = _callCts.Token.Register(() => + { + completeTcs.TrySetCanceled(); + writeStreamTcs.TrySetCanceled(); + }); + + message.Content = new PushStreamContent( + (stream) => + { + writeStreamTcs.TrySetResult(stream); + return completeTcs.Task; + }, + GrpcProtocolConstants.GrpcContentTypeHeaderValue); + + var writer = new HttpContentClientStreamWriter(this, writeStreamTcs.Task, completeTcs); + return writer; + } + + private HttpRequestMessage CreateHttpRequestMessage() + { + var message = new HttpRequestMessage(HttpMethod.Post, Method.FullName); + message.Version = new Version(2, 0); + message.Headers.UserAgent.Add(GrpcProtocolConstants.UserAgentHeader); + + if (Options.Headers != null && Options.Headers.Count > 0) + { + foreach (var entry in Options.Headers) + { + // Deadline is set via CallOptions.Deadline + if (entry.Key == GrpcProtocolConstants.TimeoutHeader) + { + continue; + } + + var value = entry.IsBinary ? Convert.ToBase64String(entry.ValueBytes) : entry.Value; + message.Headers.Add(entry.Key, value); + } + } + + if (_timeout != null) + { + message.Headers.Add(GrpcProtocolConstants.TimeoutHeader, GrpcProtocolHelpers.EncodeTimeout(Convert.ToInt64(_timeout.Value.TotalMilliseconds))); + } + + return message; + } + + private void DeadlineExceeded(object state) + { + // Deadline is only exceeded if the timeout has passed and + // the response has not been finished or canceled + if (!_callCts.IsCancellationRequested && !ResponseFinished) + { + // Flag is used to determine status code when generating exceptions + DeadlineReached = true; + + _callCts.Cancel(); + } + } + + private static Status GetStatusCore(HttpResponseMessage httpResponseMessage) + { + string grpcStatus = GetHeaderValue(httpResponseMessage.TrailingHeaders, GrpcProtocolConstants.StatusTrailer); + // grpc-status is a required trailer + if (grpcStatus == null) + { + throw new InvalidOperationException("Response did not have a grpc-status trailer."); + } + + int statusValue; + if (!int.TryParse(grpcStatus, out statusValue)) + { + throw new InvalidOperationException("Unexpected grpc-status value: " + grpcStatus); + } + + // grpc-message is optional + string grpcMessage = GetHeaderValue(httpResponseMessage.TrailingHeaders, GrpcProtocolConstants.MessageTrailer); + if (!string.IsNullOrEmpty(grpcMessage)) + { + // https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#responses + // The value portion of Status-Message is conceptually a Unicode string description of the error, + // physically encoded as UTF-8 followed by percent-encoding. + grpcMessage = Uri.UnescapeDataString(grpcMessage); + } + + return new Status((StatusCode)statusValue, grpcMessage); + } + + private static string GetHeaderValue(HttpHeaders headers, string name) + { + if (!headers.TryGetValues(name, out var values)) + { + return null; + } + + // HttpHeaders appears to always return an array, but fallback to converting values to one just in case + var valuesArray = values as string[] ?? values.ToArray(); + + switch (valuesArray.Length) + { + case 0: + return null; + case 1: + return valuesArray[0]; + default: + throw new InvalidOperationException($"Multiple {name} headers."); + } + } + + private void ValidateTrailersAvailable() + { + // Response headers have been returned and are not a valid grpc response + EnsureHeadersValid(); + + // Response is finished + if (ResponseFinished) + { + return; + } + + // Async call could have been disposed + EnsureNotDisposed(); + + // Call could have been canceled or deadline exceeded + if (_callCts.IsCancellationRequested) + { + throw CreateCanceledStatusException(); + } + + // HttpClient.SendAsync could have failed + if (SendTask.IsFaulted) + { + throw new InvalidOperationException("Can't get the call trailers because an error occured when making the request.", SendTask.Exception); + } + + throw new InvalidOperationException("Can't get the call trailers because the call is not complete."); + } + } +} diff --git a/src/Grpc.NetCore.HttpClient/Internal/GrpcProtocolConstants.cs b/src/Grpc.NetCore.HttpClient/Internal/GrpcProtocolConstants.cs new file mode 100644 index 000000000..0fa4ffcc8 --- /dev/null +++ b/src/Grpc.NetCore.HttpClient/Internal/GrpcProtocolConstants.cs @@ -0,0 +1,62 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System.Diagnostics; +using System.Linq; +using System.Net.Http.Headers; +using System.Reflection; + +namespace Grpc.NetCore.HttpClient.Internal +{ + internal static class GrpcProtocolConstants + { + internal const string GrpcContentType = "application/grpc"; + internal static readonly MediaTypeHeaderValue GrpcContentTypeHeaderValue = new MediaTypeHeaderValue("application/grpc"); + + internal const string TimeoutHeader = "grpc-timeout"; + internal const string MessageEncodingHeader = "grpc-encoding"; + + internal const string StatusTrailer = "grpc-status"; + internal const string MessageTrailer = "grpc-message"; + + internal const string MessageAcceptEncodingHeader = "grpc-accept-encoding"; + + internal static readonly ProductInfoHeaderValue UserAgentHeader; + + static GrpcProtocolConstants() + { + var userAgent = "grpc-dotnet"; + + var assemblyVersion = typeof(GrpcProtocolConstants) + .Assembly + .GetCustomAttributes() + .FirstOrDefault(); + + Debug.Assert(assemblyVersion != null); + + // assembly version attribute should always be present + // but in case it isn't then don't include version in user-agent + if (assemblyVersion != null) + { + userAgent += "/" + assemblyVersion.InformationalVersion; + } + + UserAgentHeader = ProductInfoHeaderValue.Parse(userAgent); + } + } +} diff --git a/src/Grpc.NetCore.HttpClient/Internal/GrpcProtocolHelpers.cs b/src/Grpc.NetCore.HttpClient/Internal/GrpcProtocolHelpers.cs new file mode 100644 index 000000000..df354592f --- /dev/null +++ b/src/Grpc.NetCore.HttpClient/Internal/GrpcProtocolHelpers.cs @@ -0,0 +1,188 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.Net.Http.Headers; +using Grpc.Core; + +namespace Grpc.NetCore.HttpClient.Internal +{ + internal static class GrpcProtocolHelpers + { + public static bool IsGrpcContentType(string contentType) + { + if (contentType == null) + { + return false; + } + + if (!contentType.StartsWith(GrpcProtocolConstants.GrpcContentType, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (contentType.Length == GrpcProtocolConstants.GrpcContentType.Length) + { + // Exact match + return true; + } + + // Support variations on the content-type (e.g. +proto, +json) + char nextChar = contentType[GrpcProtocolConstants.GrpcContentType.Length]; + if (nextChar == ';') + { + return true; + } + if (nextChar == '+') + { + // Accept any message format. Marshaller could be set to support third-party formats + return true; + } + + return false; + } + + public static byte[] ParseBinaryHeader(string base64) + { + string decodable; + switch (base64.Length % 4) + { + case 0: + // base64 has the required padding + decodable = base64; + break; + case 2: + // 2 chars padding + decodable = base64 + "=="; + break; + case 3: + // 3 chars padding + decodable = base64 + "="; + break; + default: + // length%4 == 1 should be illegal + throw new FormatException("Invalid base64 header value"); + } + + return Convert.FromBase64String(decodable); + } + + public static Metadata BuildMetadata(HttpResponseHeaders responseHeaders) + { + var headers = new Metadata(); + + foreach (var header in responseHeaders) + { + // ASP.NET Core includes pseudo headers in the set of request headers + // whereas, they are not in gRPC implementations. We will filter them + // out when we construct the list of headers on the context. + if (header.Key.StartsWith(":", StringComparison.Ordinal)) + { + continue; + } + else if (header.Key.EndsWith(Metadata.BinaryHeaderSuffix, StringComparison.OrdinalIgnoreCase)) + { + headers.Add(header.Key, GrpcProtocolHelpers.ParseBinaryHeader(string.Join(",", header.Value))); + } + else + { + headers.Add(header.Key, string.Join(",", header.Value)); + } + } + + return headers; + } + + private const int MillisecondsPerSecond = 1000; + + /* round an integer up to the next value with three significant figures */ + private static long TimeoutRoundUpToThreeSignificantFigures(long x) + { + if (x < 1000) return x; + if (x < 10000) return RoundUp(x, 10); + if (x < 100000) return RoundUp(x, 100); + if (x < 1000000) return RoundUp(x, 1000); + if (x < 10000000) return RoundUp(x, 10000); + if (x < 100000000) return RoundUp(x, 100000); + if (x < 1000000000) return RoundUp(x, 1000000); + return RoundUp(x, 10000000); + + static long RoundUp(long x, long divisor) + { + return (x / divisor + Convert.ToInt32(x % divisor != 0)) * divisor; + } + } + + private static string FormatTimeout(long value, char ext) + { + return value.ToString() + ext; + } + + private static string EncodeTimeoutSeconds(long sec) + { + if (sec % 3600 == 0) + { + return FormatTimeout(sec / 3600, 'H'); + } + else if (sec % 60 == 0) + { + return FormatTimeout(sec / 60, 'M'); + } + else + { + return FormatTimeout(sec, 'S'); + } + } + + private static string EncodeTimeoutMilliseconds(long x) + { + x = TimeoutRoundUpToThreeSignificantFigures(x); + if (x < MillisecondsPerSecond) + { + return FormatTimeout(x, 'm'); + } + else + { + if (x % MillisecondsPerSecond == 0) + { + return EncodeTimeoutSeconds(x / MillisecondsPerSecond); + } + else + { + return FormatTimeout(x, 'm'); + } + } + } + + public static string EncodeTimeout(long timeout) + { + if (timeout <= 0) + { + return "1n"; + } + else if (timeout < 1000 * MillisecondsPerSecond) + { + return EncodeTimeoutMilliseconds(timeout); + } + else + { + return EncodeTimeoutSeconds(timeout / MillisecondsPerSecond + Convert.ToInt32(timeout % MillisecondsPerSecond != 0)); + } + } + } +} diff --git a/src/Grpc.NetCore.HttpClient/Internal/HttpContentClientStreamReader.cs b/src/Grpc.NetCore.HttpClient/Internal/HttpContentClientStreamReader.cs new file mode 100644 index 000000000..2e1d5b463 --- /dev/null +++ b/src/Grpc.NetCore.HttpClient/Internal/HttpContentClientStreamReader.cs @@ -0,0 +1,140 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Grpc.Core; + +namespace Grpc.NetCore.HttpClient.Internal +{ + internal class HttpContentClientStreamReader : IAsyncStreamReader + { + private static readonly Task FinishedTask = Task.FromResult(false); + + private readonly GrpcCall _call; + private readonly object _moveNextLock; + + private HttpResponseMessage _httpResponse; + private Stream _responseStream; + private Task _moveNextTask; + + public HttpContentClientStreamReader(GrpcCall call) + { + _call = call; + _moveNextLock = new object(); + } + + public TResponse Current { get; private set; } + + public void Dispose() + { + } + + public Task MoveNext(CancellationToken cancellationToken) + { + // HTTP response has finished + if (_call.ResponseFinished) + { + return FinishedTask; + } + + if (_call.IsCancellationRequested) + { + throw _call.CreateCanceledStatusException(); + } + + lock (_moveNextLock) + { + // Pending move next need to be awaited first + if (IsMoveNextInProgressUnsynchronized) + { + return Task.FromException(new InvalidOperationException("Cannot read next message because the previous read is in progress.")); + } + + // Save move next task to track whether it is complete + _moveNextTask = MoveNextCore(cancellationToken); + } + + return _moveNextTask; + } + + private async Task MoveNextCore(CancellationToken cancellationToken) + { + CancellationTokenSource cts = null; + try + { + // Linking tokens is expensive. Only create a linked token if the token passed in requires it + if (cancellationToken.CanBeCanceled) + { + cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _call.CancellationToken); + cancellationToken = cts.Token; + } + else + { + cancellationToken = _call.CancellationToken; + } + + cancellationToken.ThrowIfCancellationRequested(); + + if (_httpResponse == null) + { + await _call.SendTask.ConfigureAwait(false); + _httpResponse = _call.HttpResponse; + } + if (_responseStream == null) + { + _responseStream = await _httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); + } + + Current = await _responseStream.ReadStreamedMessageAsync(_call.Method.ResponseMarshaller.Deserializer, cancellationToken).ConfigureAwait(false); + if (Current == null) + { + // No more content in response so mark as finished + _call.FinishResponse(); + return false; + } + + return true; + } + catch (OperationCanceledException) + { + throw _call.CreateCanceledStatusException(); + } + finally + { + cts?.Dispose(); + } + } + + /// + /// A value indicating whether there is an async move next already in progress. + /// Should only check this property when holding the move next lock. + /// + private bool IsMoveNextInProgressUnsynchronized + { + get + { + var moveNextTask = _moveNextTask; + return moveNextTask != null && !moveNextTask.IsCompleted; + } + } + } +} diff --git a/src/Grpc.NetCore.HttpClient/Internal/HttpContentClientStreamWriter.cs b/src/Grpc.NetCore.HttpClient/Internal/HttpContentClientStreamWriter.cs new file mode 100644 index 000000000..49847f351 --- /dev/null +++ b/src/Grpc.NetCore.HttpClient/Internal/HttpContentClientStreamWriter.cs @@ -0,0 +1,127 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Grpc.Core; + +namespace Grpc.NetCore.HttpClient.Internal +{ + internal class HttpContentClientStreamWriter : IClientStreamWriter + { + private readonly GrpcCall _call; + private readonly Task _writeStreamTask; + private readonly TaskCompletionSource _completeTcs; + private readonly object _writeLock; + private Task _writeTask; + + public HttpContentClientStreamWriter(GrpcCall call, Task writeStreamTask, TaskCompletionSource completeTcs) + { + _call = call; + _writeStreamTask = writeStreamTask; + _completeTcs = completeTcs; + _writeLock = new object(); + WriteOptions = _call.Options.WriteOptions; + } + + public WriteOptions WriteOptions { get; set; } + + public Task CompleteAsync() + { + lock (_writeLock) + { + // Pending writes need to be awaited first + if (IsWriteInProgressUnsynchronized) + { + return Task.FromException(new InvalidOperationException("Cannot complete client stream writer because the previous write is in progress.")); + } + + // Notify that the client stream is complete + _completeTcs.TrySetResult(true); + } + + return Task.CompletedTask; + } + + public Task WriteAsync(TRequest message) + { + if (message == null) + { + throw new ArgumentNullException(nameof(message)); + } + + lock (_writeLock) + { + // CompleteAsync has already been called + if (_completeTcs.Task.IsCompletedSuccessfully) + { + return Task.FromException(new InvalidOperationException("Cannot write message because the client stream writer is complete.")); + } + else if (_completeTcs.Task.IsCanceled) + { + throw _call.CreateCanceledStatusException(); + } + + // Pending writes need to be awaited first + if (IsWriteInProgressUnsynchronized) + { + return Task.FromException(new InvalidOperationException("Cannot write message because the previous write is in progress.")); + } + + // Save write task to track whether it is complete + _writeTask = WriteAsyncCore(message); + } + + return _writeTask; + } + + public void Dispose() + { + } + + private async Task WriteAsyncCore(TRequest message) + { + try + { + // Wait until the client stream has started + var writeStream = await _writeStreamTask.ConfigureAwait(false); + + await SerializationHelpers.WriteMessage(writeStream, message, _call.Method.RequestMarshaller.Serializer, _call.CancellationToken).ConfigureAwait(false); + } + catch (TaskCanceledException) + { + throw _call.CreateCanceledStatusException(); + } + } + + /// + /// A value indicating whether there is an async write already in progress. + /// Should only check this property when holding the write lock. + /// + private bool IsWriteInProgressUnsynchronized + { + get + { + var writeTask = _writeTask; + return writeTask != null && !writeTask.IsCompleted; + } + } + } +} diff --git a/src/Grpc.NetCore.HttpClient/Internal/ISystemClock.cs b/src/Grpc.NetCore.HttpClient/Internal/ISystemClock.cs new file mode 100644 index 000000000..016c52ee0 --- /dev/null +++ b/src/Grpc.NetCore.HttpClient/Internal/ISystemClock.cs @@ -0,0 +1,27 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; + +namespace Grpc.NetCore.HttpClient.Internal +{ + internal interface ISystemClock + { + DateTime UtcNow { get; } + } +} diff --git a/src/Grpc.NetCore.HttpClient/Internal/PushStreamContent.cs b/src/Grpc.NetCore.HttpClient/Internal/PushStreamContent.cs new file mode 100644 index 000000000..51e28f87e --- /dev/null +++ b/src/Grpc.NetCore.HttpClient/Internal/PushStreamContent.cs @@ -0,0 +1,53 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; + +namespace Grpc.NetCore.HttpClient.Internal +{ + internal class PushStreamContent : HttpContent + { + private readonly Func _onStreamAvailable; + + public PushStreamContent(Func onStreamAvailable, MediaTypeHeaderValue mediaType) + { + _onStreamAvailable = onStreamAvailable; + Headers.ContentType = mediaType; + } + + protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) + { + return _onStreamAvailable(stream); + } + + protected override bool TryComputeLength(out long length) + { + // We can't know the length of the content being pushed to the output stream. + length = -1; + return false; + } + + // Hacky. ReadAsStreamAsync does not complete until SerializeToStreamAsync finishes + internal Task PushComplete => ReadAsStreamAsync(); + } +} \ No newline at end of file diff --git a/src/Grpc.NetCore.HttpClient/Internal/SerialiationHelpers.cs b/src/Grpc.NetCore.HttpClient/Internal/SerialiationHelpers.cs new file mode 100644 index 000000000..3715cbc54 --- /dev/null +++ b/src/Grpc.NetCore.HttpClient/Internal/SerialiationHelpers.cs @@ -0,0 +1,61 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.Buffers.Binary; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Grpc.NetCore.HttpClient.Internal +{ + internal static class SerializationHelpers + { + public static async Task WriteMessage(Stream stream, TMessage message, Func serializer, CancellationToken cancellationToken) + { + var data = serializer(message); + + await WriteHeaderAsync(stream, data.Length, false, cancellationToken).ConfigureAwait(false); + await stream.WriteAsync(data, cancellationToken).ConfigureAwait(false); + } + + private const int MessageDelimiterSize = 4; // how many bytes it takes to encode "Message-Length" + private const int HeaderSize = MessageDelimiterSize + 1; // message length + compression flag + + public static Task WriteHeaderAsync(Stream stream, int length, bool compress, CancellationToken cancellationToken) + { + var headerData = new byte[HeaderSize]; + + // Compression flag + headerData[0] = compress ? (byte)1 : (byte)0; + + // Message length + EncodeMessageLength(length, headerData.AsSpan(1)); + + return stream.WriteAsync(headerData, 0, headerData.Length, cancellationToken); + } + + private static void EncodeMessageLength(int messageLength, Span destination) + { + Debug.Assert(destination.Length >= MessageDelimiterSize, "Buffer too small to encode message length."); + + BinaryPrimitives.WriteUInt32BigEndian(destination, (uint)messageLength); + } + } +} diff --git a/src/Grpc.NetCore.HttpClient/Internal/StreamExtensions.cs b/src/Grpc.NetCore.HttpClient/Internal/StreamExtensions.cs new file mode 100644 index 000000000..28f066ecb --- /dev/null +++ b/src/Grpc.NetCore.HttpClient/Internal/StreamExtensions.cs @@ -0,0 +1,111 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.Buffers.Binary; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Grpc.NetCore.HttpClient +{ + internal static class StreamExtensions + { + public static Task ReadSingleMessageAsync(this Stream responseStream, Func deserializer, CancellationToken cancellationToken) + { + return responseStream.ReadMessageCoreAsync(deserializer, cancellationToken, true, true); + } + + public static Task ReadStreamedMessageAsync(this Stream responseStream, Func deserializer, CancellationToken cancellationToken) + { + return responseStream.ReadMessageCoreAsync(deserializer, cancellationToken, true, false); + } + + private static async Task ReadMessageCoreAsync(this Stream responseStream, Func deserializer, CancellationToken cancellationToken, bool canBeEmpty, bool singleMessage) + { + cancellationToken.ThrowIfCancellationRequested(); + + var header = new byte[5]; + + int read; + var received = 0; + while ((read = await responseStream.ReadAsync(header, received, header.Length - received, cancellationToken).ConfigureAwait(false)) > 0) + { + received += read; + + if (received == header.Length) + { + break; + } + } + + if (received < header.Length) + { + if (received == 0 && canBeEmpty) + { + return default; + } + + throw new InvalidDataException("Unexpected end of content while reading the message header."); + } + + var length = BinaryPrimitives.ReadUInt32BigEndian(header.AsSpan(1)); + if (length > int.MaxValue) + { + throw new InvalidDataException("Message too large."); + } + + byte[] messageData; + if (length > 0) + { + received = 0; + messageData = new byte[length]; + while ((read = await responseStream.ReadAsync(messageData, received, messageData.Length - received, cancellationToken).ConfigureAwait(false)) > 0) + { + received += read; + + if (received == messageData.Length) + { + break; + } + } + } + else + { + messageData = Array.Empty(); + } + + cancellationToken.ThrowIfCancellationRequested(); + + var message = deserializer(messageData); + + if (singleMessage) + { + // Check that there is no additional content in the stream for a single message + // There is no ReadByteAsync on stream. Reuse header array with ReadAsync, we don't need it anymore + if (await responseStream.ReadAsync(header, 0, 1).ConfigureAwait(false) > 0) + { + throw new InvalidDataException("Unexpected data after finished reading message."); + } + } + + return message; + } + + } +} diff --git a/src/Grpc.NetCore.HttpClient/Internal/SystemClock.cs b/src/Grpc.NetCore.HttpClient/Internal/SystemClock.cs new file mode 100644 index 000000000..87321c263 --- /dev/null +++ b/src/Grpc.NetCore.HttpClient/Internal/SystemClock.cs @@ -0,0 +1,29 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; + +namespace Grpc.NetCore.HttpClient.Internal +{ + internal class SystemClock : ISystemClock + { + public static readonly SystemClock Instance = new SystemClock(); + + public DateTime UtcNow => DateTime.UtcNow; + } +} diff --git a/src/Grpc.NetCore.HttpClient/PipeClientStreamWriter.cs b/src/Grpc.NetCore.HttpClient/PipeClientStreamWriter.cs deleted file mode 100644 index 18605f46e..000000000 --- a/src/Grpc.NetCore.HttpClient/PipeClientStreamWriter.cs +++ /dev/null @@ -1,56 +0,0 @@ -#region Copyright notice and license - -// Copyright 2019 The gRPC Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#endregion - -using System; -using System.IO.Pipelines; -using System.Threading.Tasks; -using Grpc.Core; - -namespace Grpc.NetCore.HttpClient -{ - internal class PipeClientStreamWriter : IClientStreamWriter - { - private readonly PipeWriter _writer; - private readonly Func _serializer; - - public PipeClientStreamWriter(PipeWriter Writer, Func serializer, WriteOptions options) - { - _writer = Writer; - _serializer = serializer; - WriteOptions = options; - } - - public WriteOptions WriteOptions { get; set; } - - public Task CompleteAsync() - { - _writer.Complete(); - return Task.CompletedTask; - } - - public Task WriteAsync(TRequest message) - { - if (message == null) - { - throw new ArgumentNullException(nameof(message)); - } - - return _writer.WriteMessageCoreAsync(_serializer(message), flush: true); - } - } -} diff --git a/src/Grpc.NetCore.HttpClient/PipeContent.cs b/src/Grpc.NetCore.HttpClient/PipeContent.cs deleted file mode 100644 index 6982828f0..000000000 --- a/src/Grpc.NetCore.HttpClient/PipeContent.cs +++ /dev/null @@ -1,79 +0,0 @@ -#region Copyright notice and license - -// Copyright 2019 The gRPC Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#endregion - -using System.Buffers; -using System.IO; -using System.IO.Pipelines; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading.Tasks; - -namespace Grpc.NetCore.HttpClient -{ - internal class PipeContent : HttpContent - { - private Pipe _pipe = new Pipe(); - - public PipeWriter PipeWriter => _pipe.Writer; - private PipeReader PipeReader => _pipe.Reader; - - public PipeContent() - { - Headers.ContentType = new MediaTypeHeaderValue("application/grpc"); - } - - protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) - { - while (true) - { - var result = await PipeReader.ReadAsync(); - var buffer = result.Buffer; - - try - { - if (result.IsCanceled) - { - throw new TaskCanceledException(); - } - - if (!buffer.IsEmpty) - { - var data = buffer.ToArray(); - stream.Write(data, 0, data.Length); - } - - if (result.IsCompleted) - { - break; - } - } - finally - { - PipeReader.AdvanceTo(buffer.End); - } - } - } - - protected override bool TryComputeLength(out long length) - { - length = 0; - return false; - } - } -} diff --git a/src/Grpc.NetCore.HttpClient/PipeWriterExtensions.cs b/src/Grpc.NetCore.HttpClient/PipeWriterExtensions.cs deleted file mode 100644 index 64867f326..000000000 --- a/src/Grpc.NetCore.HttpClient/PipeWriterExtensions.cs +++ /dev/null @@ -1,63 +0,0 @@ -#region Copyright notice and license - -// Copyright 2019 The gRPC Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#endregion - -using System.Buffers; -using System.Buffers.Binary; -using System.IO.Pipelines; -using System.Threading.Tasks; - -namespace Grpc.NetCore.HttpClient -{ - internal static class PipeWriterExtensions - { - private const int MessageDelimiterSize = 4; // how many bytes it takes to encode "Message-Length" - private const int HeaderSize = MessageDelimiterSize + 1; // message length + compression flag - - public static Task WriteMessageCoreAsync(this PipeWriter pipeWriter, byte[] messageData, bool flush) - { - WriteHeader(pipeWriter, messageData.Length); - pipeWriter.Write(messageData); - - if (flush) - { - var valueTask = pipeWriter.FlushAsync(); - - if (valueTask.IsCompletedSuccessfully) - { - // We do this to reset the underlying value task (which happens in GetResult()) - valueTask.GetAwaiter().GetResult(); - return Task.CompletedTask; - } - - return valueTask.AsTask(); - } - - return Task.CompletedTask; - } - - private static void WriteHeader(PipeWriter pipeWriter, int length) - { - var headerData = pipeWriter.GetSpan(HeaderSize); - // Messages are currently always uncompressed - headerData[0] = 0; - BinaryPrimitives.WriteUInt32BigEndian(headerData.Slice(1), (uint)length); - - pipeWriter.Advance(HeaderSize); - } - } -} diff --git a/src/Grpc.NetCore.HttpClient/Properties/AssemblyInfo.cs b/src/Grpc.NetCore.HttpClient/Properties/AssemblyInfo.cs index 9e89fd1e0..c81c37157 100644 --- a/src/Grpc.NetCore.HttpClient/Properties/AssemblyInfo.cs +++ b/src/Grpc.NetCore.HttpClient/Properties/AssemblyInfo.cs @@ -24,6 +24,12 @@ "27fc95aff3dc604a6971417453f9483c7b5e836756d5b271bf8f2403fe186e31956148c03d804487cf642f8cc0" + "71394ee9672dfe5b55ea0f95dfd5a7f77d22c962ccf51320d3")] +[assembly: InternalsVisibleTo("Grpc.NetCore.HttpClient.Tests,PublicKey=" + + "00240000048000009400000006020000002400005253413100040000010001002f5797a92c6fcde81bd4098f43" + + "0442bb8e12768722de0b0cb1b15e955b32a11352740ee59f2c94c48edc8e177d1052536b8ac651bce11ce5da3a" + + "27fc95aff3dc604a6971417453f9483c7b5e836756d5b271bf8f2403fe186e31956148c03d804487cf642f8cc0" + + "71394ee9672dfe5b55ea0f95dfd5a7f77d22c962ccf51320d3")] + // For Moq. This assembly needs access to internal types via InternalVisibleTo to be able to mock them [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602" + "000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02ba" + diff --git a/src/Grpc.NetCore.HttpClient/StreamExtensions.cs b/src/Grpc.NetCore.HttpClient/StreamExtensions.cs deleted file mode 100644 index e6f35daef..000000000 --- a/src/Grpc.NetCore.HttpClient/StreamExtensions.cs +++ /dev/null @@ -1,48 +0,0 @@ -#region Copyright notice and license - -// Copyright 2019 The gRPC Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#endregion - -using System; -using System.Buffers.Binary; -using System.IO; - -namespace Grpc.NetCore.HttpClient -{ - internal static class StreamExtensions - { - public static TResponse ReadSingleMessage(this Stream responseStream, Func deserializer) - { - if (responseStream.ReadByte() != 0) - { - throw new InvalidOperationException("Compressed response not yet supported"); - } - - var lengthBytes = new byte[4]; - responseStream.Read(lengthBytes, 0, 4); - var length = BinaryPrimitives.ReadUInt32BigEndian(lengthBytes); - if (length > int.MaxValue) - { - throw new InvalidOperationException("message too large"); - } - - var responseBytes = new byte[length]; - responseStream.Read(responseBytes, 0, (int)length); - - return deserializer(responseBytes); - } - } -} diff --git a/test/FunctionalTests/AuthorizationTests.cs b/test/FunctionalTests/AuthorizationTests.cs index be65d5423..f44cc3cde 100644 --- a/test/FunctionalTests/AuthorizationTests.cs +++ b/test/FunctionalTests/AuthorizationTests.cs @@ -25,6 +25,7 @@ using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Internal; using Grpc.Core; +using Grpc.Tests.Shared; using NUnit.Framework; namespace Grpc.AspNetCore.FunctionalTests diff --git a/test/FunctionalTests/ClientStreamingMethodTests.cs b/test/FunctionalTests/ClientStreamingMethodTests.cs index a505967a9..053220cc3 100644 --- a/test/FunctionalTests/ClientStreamingMethodTests.cs +++ b/test/FunctionalTests/ClientStreamingMethodTests.cs @@ -27,9 +27,9 @@ using Google.Protobuf.WellKnownTypes; using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Internal; -using Grpc.AspNetCore.Server.Tests; using Grpc.Core; using NUnit.Framework; +using Grpc.Tests.Shared; namespace Grpc.AspNetCore.FunctionalTests { @@ -157,7 +157,7 @@ static async Task AccumulateCount(IAsyncStreamReader AccumulateCount(IAsyncStreamReader(); + var reply = await response.GetSuccessfulGrpcMessageAsync().DefaultTimeout(); Assert.AreEqual(3, reply.Count); Assert.AreEqual(StatusCode.OK.ToTrailerString(), Fixture.TrailersContainer.Trailers[GrpcProtocolConstants.StatusTrailer].Single()); diff --git a/test/FunctionalTests/CompressionTests.cs b/test/FunctionalTests/CompressionTests.cs index c1e479a43..16c802d21 100644 --- a/test/FunctionalTests/CompressionTests.cs +++ b/test/FunctionalTests/CompressionTests.cs @@ -29,7 +29,7 @@ using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Compression; using Grpc.AspNetCore.Server.Internal; -using Grpc.AspNetCore.Server.Tests; +using Grpc.Tests.Shared; using Grpc.Core; using NUnit.Framework; diff --git a/test/FunctionalTests/DeadlineTests.cs b/test/FunctionalTests/DeadlineTests.cs index 5ad8e8780..6e6edbc61 100644 --- a/test/FunctionalTests/DeadlineTests.cs +++ b/test/FunctionalTests/DeadlineTests.cs @@ -26,6 +26,7 @@ using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Internal; using Grpc.Core; +using Grpc.Tests.Shared; using NUnit.Framework; namespace Grpc.AspNetCore.FunctionalTests @@ -49,7 +50,7 @@ public Task WriteUntilDeadline_SuccessResponsesStreamed_Deadline() => } // Ensure deadline timer has run - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); context.CancellationToken.Register(() => tcs.SetResult(null)); await tcs.Task; }); diff --git a/test/FunctionalTests/DuplexStreamingMethodTests.cs b/test/FunctionalTests/DuplexStreamingMethodTests.cs index d44a90fff..ad62c0855 100644 --- a/test/FunctionalTests/DuplexStreamingMethodTests.cs +++ b/test/FunctionalTests/DuplexStreamingMethodTests.cs @@ -26,8 +26,8 @@ using FunctionalTestsWebsite.Services; using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Internal; -using Grpc.AspNetCore.Server.Tests; using Grpc.Core; +using Grpc.Tests.Shared; using NUnit.Framework; namespace Grpc.AspNetCore.FunctionalTests @@ -65,7 +65,6 @@ public async Task MultipleMessagesFromOneClient_SuccessResponses() var pipeReader = new StreamPipeReader(responseStream); var message1Task = MessageHelpers.AssertReadStreamMessageAsync(pipeReader); - Assert.IsTrue(message1Task.IsCompleted); var message1 = await message1Task.DefaultTimeout(); Assert.AreEqual("John", message1.Name); Assert.AreEqual("Hello Jill", message1.Message); diff --git a/test/FunctionalTests/Grpc.AspNetCore.FunctionalTests.csproj b/test/FunctionalTests/Grpc.AspNetCore.FunctionalTests.csproj index 7c30c07d0..099da3d6a 100644 --- a/test/FunctionalTests/Grpc.AspNetCore.FunctionalTests.csproj +++ b/test/FunctionalTests/Grpc.AspNetCore.FunctionalTests.csproj @@ -11,6 +11,7 @@ + diff --git a/test/FunctionalTests/HttpContextTests.cs b/test/FunctionalTests/HttpContextTests.cs index 18ae0b944..331fe1aaa 100644 --- a/test/FunctionalTests/HttpContextTests.cs +++ b/test/FunctionalTests/HttpContextTests.cs @@ -22,7 +22,7 @@ using Greet; using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Internal; -using Grpc.AspNetCore.Server.Tests; +using Grpc.Tests.Shared; using Grpc.Core; using NUnit.Framework; diff --git a/test/FunctionalTests/Infrastructure/HttpResponseMessageExtensions.cs b/test/FunctionalTests/Infrastructure/HttpResponseMessageExtensions.cs index 3660d4778..7dad7ed19 100644 --- a/test/FunctionalTests/Infrastructure/HttpResponseMessageExtensions.cs +++ b/test/FunctionalTests/Infrastructure/HttpResponseMessageExtensions.cs @@ -16,11 +16,11 @@ #endregion -using System.Linq; using System.Net; using System.Net.Http; using System.Threading.Tasks; using Google.Protobuf; +using Grpc.Tests.Shared; using NUnit.Framework; namespace Grpc.AspNetCore.FunctionalTests.Infrastructure diff --git a/test/FunctionalTests/LifetimeTests.cs b/test/FunctionalTests/LifetimeTests.cs index afa949125..339db2a7a 100644 --- a/test/FunctionalTests/LifetimeTests.cs +++ b/test/FunctionalTests/LifetimeTests.cs @@ -25,7 +25,7 @@ using Google.Protobuf.WellKnownTypes; using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Internal; -using Grpc.AspNetCore.Server.Tests; +using Grpc.Tests.Shared; using Grpc.Core; using Lifetime; using NUnit.Framework; diff --git a/test/FunctionalTests/MaxMessageSizeTests.cs b/test/FunctionalTests/MaxMessageSizeTests.cs index b32a95254..ad53ff799 100644 --- a/test/FunctionalTests/MaxMessageSizeTests.cs +++ b/test/FunctionalTests/MaxMessageSizeTests.cs @@ -23,7 +23,7 @@ using Greet; using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Internal; -using Grpc.AspNetCore.Server.Tests; +using Grpc.Tests.Shared; using Grpc.Core; using NUnit.Framework; diff --git a/test/FunctionalTests/NestedTests.cs b/test/FunctionalTests/NestedTests.cs index 0c3601b5a..dafa43abe 100644 --- a/test/FunctionalTests/NestedTests.cs +++ b/test/FunctionalTests/NestedTests.cs @@ -24,7 +24,7 @@ using System.Threading.Tasks; using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Internal; -using Grpc.AspNetCore.Server.Tests; +using Grpc.Tests.Shared; using Grpc.Core; using Nested; using NUnit.Framework; @@ -35,6 +35,7 @@ namespace Grpc.AspNetCore.FunctionalTests public class NestedTests : FunctionalTestBase { [Test] + [Ignore("Failing because TestHost does not return trailers. Blocked on https://github.com/aspnet/AspNetCore/issues/6880")] public async Task CallNestedService_SuccessResponse() { // Arrange diff --git a/test/FunctionalTests/ServerStreamingMethodTests.cs b/test/FunctionalTests/ServerStreamingMethodTests.cs index a6d0ce687..f9cad3381 100644 --- a/test/FunctionalTests/ServerStreamingMethodTests.cs +++ b/test/FunctionalTests/ServerStreamingMethodTests.cs @@ -26,6 +26,7 @@ using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Internal; using Grpc.Core; +using Grpc.Tests.Shared; using NUnit.Framework; namespace Grpc.AspNetCore.FunctionalTests diff --git a/test/FunctionalTests/UnaryMethodTests.cs b/test/FunctionalTests/UnaryMethodTests.cs index 035078caa..f10e81ebe 100644 --- a/test/FunctionalTests/UnaryMethodTests.cs +++ b/test/FunctionalTests/UnaryMethodTests.cs @@ -28,8 +28,8 @@ using Greet; using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Internal; -using Grpc.AspNetCore.Server.Tests; using Grpc.Core; +using Grpc.Tests.Shared; using NUnit.Framework; namespace Grpc.AspNetCore.FunctionalTests diff --git a/test/FunctionalTests/UnimplementedTests.cs b/test/FunctionalTests/UnimplementedTests.cs index d22594344..e9eb31579 100644 --- a/test/FunctionalTests/UnimplementedTests.cs +++ b/test/FunctionalTests/UnimplementedTests.cs @@ -25,6 +25,7 @@ using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Internal; using Grpc.Core; +using Grpc.Tests.Shared; using NUnit.Framework; namespace Grpc.AspNetCore.FunctionalTests diff --git a/test/Grpc.AspNetCore.Server.Tests/Grpc.AspNetCore.Server.Tests.csproj b/test/Grpc.AspNetCore.Server.Tests/Grpc.AspNetCore.Server.Tests.csproj index 7a262e836..2e66bacdd 100644 --- a/test/Grpc.AspNetCore.Server.Tests/Grpc.AspNetCore.Server.Tests.csproj +++ b/test/Grpc.AspNetCore.Server.Tests/Grpc.AspNetCore.Server.Tests.csproj @@ -3,22 +3,17 @@ netcoreapp3.0 false - latest - + - - - - diff --git a/test/Grpc.AspNetCore.Server.Tests/HttpContextStreamReaderTests.cs b/test/Grpc.AspNetCore.Server.Tests/HttpContextStreamReaderTests.cs index a58613e67..9c00f7849 100644 --- a/test/Grpc.AspNetCore.Server.Tests/HttpContextStreamReaderTests.cs +++ b/test/Grpc.AspNetCore.Server.Tests/HttpContextStreamReaderTests.cs @@ -23,6 +23,7 @@ using Greet; using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Internal; +using Grpc.Tests.Shared; using Microsoft.AspNetCore.Http; using NUnit.Framework; diff --git a/test/Grpc.AspNetCore.Server.Tests/HttpContextStreamWriterTests.cs b/test/Grpc.AspNetCore.Server.Tests/HttpContextStreamWriterTests.cs index 91360240c..cfb54575d 100644 --- a/test/Grpc.AspNetCore.Server.Tests/HttpContextStreamWriterTests.cs +++ b/test/Grpc.AspNetCore.Server.Tests/HttpContextStreamWriterTests.cs @@ -24,6 +24,7 @@ using Grpc.AspNetCore.FunctionalTests.Infrastructure; using Grpc.AspNetCore.Server.Internal; using Grpc.Core; +using Grpc.Tests.Shared; using Microsoft.AspNetCore.Http; using NUnit.Framework; diff --git a/test/Grpc.AspNetCore.Server.Tests/PipeExtensionsTests.cs b/test/Grpc.AspNetCore.Server.Tests/PipeExtensionsTests.cs index 26b71e420..32be8bd8a 100644 --- a/test/Grpc.AspNetCore.Server.Tests/PipeExtensionsTests.cs +++ b/test/Grpc.AspNetCore.Server.Tests/PipeExtensionsTests.cs @@ -27,6 +27,7 @@ using Grpc.AspNetCore.Server.Compression; using Grpc.AspNetCore.Server.Internal; using Grpc.Core; +using Grpc.Tests.Shared; using Microsoft.AspNetCore.Http; using NUnit.Framework; diff --git a/test/Grpc.AspNetCore.Server.Tests/Reflection/ReflectionGrpcServiceActivatorTests.cs b/test/Grpc.AspNetCore.Server.Tests/Reflection/ReflectionGrpcServiceActivatorTests.cs index 1b26d3652..f1b187caa 100644 --- a/test/Grpc.AspNetCore.Server.Tests/Reflection/ReflectionGrpcServiceActivatorTests.cs +++ b/test/Grpc.AspNetCore.Server.Tests/Reflection/ReflectionGrpcServiceActivatorTests.cs @@ -26,6 +26,7 @@ using Grpc.AspNetCore.Server.Reflection.Internal; using Grpc.Core; using Grpc.Reflection.V1Alpha; +using Grpc.Tests.Shared; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; diff --git a/test/Grpc.NetCore.HttpClient.Tests/AsyncClientStreamingCallTests.cs b/test/Grpc.NetCore.HttpClient.Tests/AsyncClientStreamingCallTests.cs new file mode 100644 index 000000000..0d7623199 --- /dev/null +++ b/test/Grpc.NetCore.HttpClient.Tests/AsyncClientStreamingCallTests.cs @@ -0,0 +1,212 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; +using Greet; +using Grpc.Core; +using Grpc.NetCore.HttpClient.Internal; +using Grpc.NetCore.HttpClient.Tests.Infrastructure; +using Grpc.Tests.Shared; +using NUnit.Framework; + +namespace Grpc.NetCore.HttpClient.Tests +{ + [TestFixture] + public class AsyncClientStreamingCallTests + { + [Test] + public async Task AsyncClientStreamingCall_Success_HttpRequestMessagePopulated() + { + // Arrange + HttpRequestMessage httpRequestMessage = null; + + var httpClient = TestHelpers.CreateTestClient(async request => + { + httpRequestMessage = request; + + HelloReply reply = new HelloReply + { + Message = "Hello world" + }; + + var streamContent = await TestHelpers.CreateResponseContent(reply).DefaultTimeout(); + return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + + await call.RequestStream.CompleteAsync().DefaultTimeout(); + + var response = await call; + + // Assert + Assert.AreEqual("Hello world", response.Message); + + Assert.IsNotNull(httpRequestMessage); + Assert.AreEqual(new Version(2, 0), httpRequestMessage.Version); + Assert.AreEqual(HttpMethod.Post, httpRequestMessage.Method); + Assert.AreEqual(new Uri("https://localhost/ServiceName/MethodName"), httpRequestMessage.RequestUri); + Assert.AreEqual(new MediaTypeHeaderValue("application/grpc"), httpRequestMessage.Content.Headers.ContentType); + } + + [Test] + public async Task AsyncClientStreamingCall_Success_RequestContentSent() + { + // Arrange + PushStreamContent content = null; + + var httpClient = TestHelpers.CreateTestClient(async request => + { + content = (PushStreamContent)request.Content; + await content.PushComplete.DefaultTimeout(); + + HelloReply reply = new HelloReply + { + Message = "Hello world" + }; + + var streamContent = await TestHelpers.CreateResponseContent(reply).DefaultTimeout(); + + return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + + // Assert + Assert.IsNotNull(call); + Assert.IsNotNull(content); + + var responseTask = call.ResponseAsync; + Assert.IsFalse(responseTask.IsCompleted, "Response not returned until client stream is complete."); + + var streamTask = content.ReadAsStreamAsync(); + + await call.RequestStream.WriteAsync(new HelloRequest { Name = "1" }).DefaultTimeout(); + await call.RequestStream.WriteAsync(new HelloRequest { Name = "2" }).DefaultTimeout(); + + await call.RequestStream.CompleteAsync().DefaultTimeout(); + + var requestContent = await streamTask.DefaultTimeout(); + var requestMessage = await requestContent.ReadStreamedMessageAsync(TestHelpers.ServiceMethod.RequestMarshaller.Deserializer, CancellationToken.None).DefaultTimeout(); + Assert.AreEqual("1", requestMessage.Name); + requestMessage = await requestContent.ReadStreamedMessageAsync(TestHelpers.ServiceMethod.RequestMarshaller.Deserializer, CancellationToken.None).DefaultTimeout(); + Assert.AreEqual("2", requestMessage.Name); + + var responseMessage = await responseTask.DefaultTimeout(); + Assert.AreEqual("Hello world", responseMessage.Message); + } + + [Test] + public void ClientStreamWriter_WriteWhilePendingWrite_ErrorThrown() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(request => + { + var streamContent = new StreamContent(new SyncPointMemoryStream()); + return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent)); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + + // Assert + var writeTask1 = call.RequestStream.WriteAsync(new HelloRequest { Name = "1" }); + Assert.IsFalse(writeTask1.IsCompleted); + + var writeTask2 = call.RequestStream.WriteAsync(new HelloRequest { Name = "2" }); + var ex = Assert.ThrowsAsync(() => writeTask2.DefaultTimeout()); + + Assert.AreEqual("Cannot write message because the previous write is in progress.", ex.Message); + } + + [Test] + public void ClientStreamWriter_CompleteWhilePendingWrite_ErrorThrown() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(request => + { + var streamContent = new StreamContent(new SyncPointMemoryStream()); + return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent)); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + + // Assert + var writeTask1 = call.RequestStream.WriteAsync(new HelloRequest { Name = "1" }); + Assert.IsFalse(writeTask1.IsCompleted); + + var completeTask = call.RequestStream.CompleteAsync(); + var ex = Assert.ThrowsAsync(() => completeTask.DefaultTimeout()); + + Assert.AreEqual("Cannot complete client stream writer because the previous write is in progress.", ex.Message); + } + + [Test] + public async Task ClientStreamWriter_WriteWhileComplete_ErrorThrown() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(request => + { + var streamContent = new StreamContent(new SyncPointMemoryStream()); + return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent)); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + await call.RequestStream.CompleteAsync(); + + // Assert + var ex = Assert.ThrowsAsync(() => call.RequestStream.WriteAsync(new HelloRequest { Name = "1" }).DefaultTimeout()); + + Assert.AreEqual("Cannot write message because the client stream writer is complete.", ex.Message); + } + + [Test] + public void ClientStreamWriter_WriteWithInvalidHttpStatus_ErrorThrown() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(request => + { + var streamContent = new StreamContent(new SyncPointMemoryStream()); + return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.NotFound, streamContent)); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + + // Assert + var ex = Assert.ThrowsAsync(() => call.RequestStream.WriteAsync(new HelloRequest { Name = "1" }).DefaultTimeout()); + + Assert.AreEqual("Bad gRPC response. Expected HTTP status code 200. Got status code: 404", ex.Message); + } + } +} diff --git a/test/Grpc.NetCore.HttpClient.Tests/AsyncDuplexStreamingCallTests.cs b/test/Grpc.NetCore.HttpClient.Tests/AsyncDuplexStreamingCallTests.cs new file mode 100644 index 000000000..b806d8568 --- /dev/null +++ b/test/Grpc.NetCore.HttpClient.Tests/AsyncDuplexStreamingCallTests.cs @@ -0,0 +1,169 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Greet; +using Grpc.Core; +using Grpc.NetCore.HttpClient.Internal; +using Grpc.NetCore.HttpClient.Tests.Infrastructure; +using Grpc.Tests.Shared; +using NUnit.Framework; + +namespace Grpc.NetCore.HttpClient.Tests +{ + [TestFixture] + public class AsyncDuplexStreamingCallTests + { + [Test] + public async Task AsyncDuplexStreamingCall_NoContent_NoMessagesReturned() + { + // Arrange + HttpRequestMessage httpRequestMessage = null; + + var httpClient = TestHelpers.CreateTestClient(request => + { + httpRequestMessage = request; + + HelloReply reply = new HelloReply + { + Message = "Hello world" + }; + + return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK, new ByteArrayContent(Array.Empty()))); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncDuplexStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + + var responseStream = call.ResponseStream; + + // Assert + Assert.IsNull(responseStream.Current); + Assert.IsFalse(await responseStream.MoveNext(CancellationToken.None).DefaultTimeout()); + Assert.IsNull(responseStream.Current); + } + + [Test] + public async Task AsyncServerStreamingCall_MessagesReturnedTogether_MessagesReceived() + { + // Arrange + HttpRequestMessage httpRequestMessage = null; + + var httpClient = TestHelpers.CreateTestClient(request => + { + httpRequestMessage = request; + + HelloReply reply = new HelloReply + { + Message = "Hello world" + }; + + return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK, new ByteArrayContent(Array.Empty()))); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncDuplexStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + + var responseStream = call.ResponseStream; + + // Assert + Assert.IsNull(responseStream.Current); + Assert.IsFalse(await responseStream.MoveNext(CancellationToken.None).DefaultTimeout()); + Assert.IsNull(responseStream.Current); + } + + [Test] + public async Task AsyncDuplexStreamingCall_MessagesStreamed_MessagesReceived() + { + // Arrange + var streamContent = new SyncPointMemoryStream(); + + PushStreamContent content = null; + + var httpClient = TestHelpers.CreateTestClient(async request => + { + content = (PushStreamContent)request.Content; + await content.PushComplete.DefaultTimeout(); + + return ResponseUtils.CreateResponse(HttpStatusCode.OK, new StreamContent(streamContent)); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncDuplexStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + + var requestStream = call.RequestStream; + var responseStream = call.ResponseStream; + + // Assert + await call.RequestStream.WriteAsync(new HelloRequest { Name = "1" }).DefaultTimeout(); + await call.RequestStream.WriteAsync(new HelloRequest { Name = "2" }).DefaultTimeout(); + + await call.RequestStream.CompleteAsync().DefaultTimeout(); + + var requestContent = await content.ReadAsStreamAsync().DefaultTimeout(); + var requestMessage = await requestContent.ReadStreamedMessageAsync(TestHelpers.ServiceMethod.RequestMarshaller.Deserializer, CancellationToken.None).DefaultTimeout(); + Assert.AreEqual("1", requestMessage.Name); + requestMessage = await requestContent.ReadStreamedMessageAsync(TestHelpers.ServiceMethod.RequestMarshaller.Deserializer, CancellationToken.None).DefaultTimeout(); + Assert.AreEqual("2", requestMessage.Name); + + Assert.IsNull(responseStream.Current); + + var moveNextTask1 = responseStream.MoveNext(CancellationToken.None); + Assert.IsFalse(moveNextTask1.IsCompleted); + + await streamContent.AddDataAndWait(await TestHelpers.GetResponseDataAsync(new HelloReply + { + Message = "Hello world 1" + }).DefaultTimeout()).DefaultTimeout(); + + Assert.IsTrue(await moveNextTask1.DefaultTimeout()); + Assert.IsNotNull(responseStream.Current); + Assert.AreEqual("Hello world 1", responseStream.Current.Message); + + var moveNextTask2 = responseStream.MoveNext(CancellationToken.None); + Assert.IsFalse(moveNextTask2.IsCompleted); + + await streamContent.AddDataAndWait(await TestHelpers.GetResponseDataAsync(new HelloReply + { + Message = "Hello world 2" + }).DefaultTimeout()).DefaultTimeout(); + + Assert.IsTrue(await moveNextTask2.DefaultTimeout()); + Assert.IsNotNull(responseStream.Current); + Assert.AreEqual("Hello world 2", responseStream.Current.Message); + + var moveNextTask3 = responseStream.MoveNext(CancellationToken.None); + Assert.IsFalse(moveNextTask3.IsCompleted); + + await streamContent.AddDataAndWait(Array.Empty()).DefaultTimeout(); + + Assert.IsFalse(await moveNextTask3.DefaultTimeout()); + + var moveNextTask4 = responseStream.MoveNext(CancellationToken.None); + Assert.IsTrue(moveNextTask4.IsCompleted); + Assert.IsFalse(await moveNextTask3.DefaultTimeout()); + } + } +} diff --git a/test/Grpc.NetCore.HttpClient.Tests/AsyncServerStreamingCallTests.cs b/test/Grpc.NetCore.HttpClient.Tests/AsyncServerStreamingCallTests.cs new file mode 100644 index 000000000..9d71fc2be --- /dev/null +++ b/test/Grpc.NetCore.HttpClient.Tests/AsyncServerStreamingCallTests.cs @@ -0,0 +1,180 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Greet; +using Grpc.Core; +using Grpc.NetCore.HttpClient.Tests.Infrastructure; +using Grpc.Tests.Shared; +using NUnit.Framework; + +namespace Grpc.NetCore.HttpClient.Tests +{ + [TestFixture] + public class AsyncServerStreamingCallTests + { + [Test] + public async Task AsyncServerStreamingCall_NoContent_NoMessagesReturned() + { + // Arrange + HttpRequestMessage httpRequestMessage = null; + + var httpClient = TestHelpers.CreateTestClient(request => + { + httpRequestMessage = request; + + HelloReply reply = new HelloReply + { + Message = "Hello world" + }; + + return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK, new ByteArrayContent(Array.Empty()))); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncServerStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + + var responseStream = call.ResponseStream; + + // Assert + Assert.IsNull(responseStream.Current); + Assert.IsFalse(await responseStream.MoveNext(CancellationToken.None).DefaultTimeout()); + Assert.IsNull(responseStream.Current); + } + + [Test] + public async Task AsyncServerStreamingCall_MessagesReturnedTogether_MessagesReceived() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(async request => + { + var streamContent = await TestHelpers.CreateResponseContent( + new HelloReply + { + Message = "Hello world 1" + }, + new HelloReply + { + Message = "Hello world 2" + }).DefaultTimeout(); + + return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncServerStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + + var responseStream = call.ResponseStream; + + // Assert + Assert.IsNull(responseStream.Current); + + Assert.IsTrue(await responseStream.MoveNext(CancellationToken.None).DefaultTimeout()); + Assert.IsNotNull(responseStream.Current); + Assert.AreEqual("Hello world 1", responseStream.Current.Message); + + Assert.IsTrue(await responseStream.MoveNext(CancellationToken.None).DefaultTimeout()); + Assert.IsNotNull(responseStream.Current); + Assert.AreEqual("Hello world 2", responseStream.Current.Message); + + Assert.IsFalse(await responseStream.MoveNext(CancellationToken.None).DefaultTimeout()); + } + + [Test] + public async Task AsyncServerStreamingCall_MessagesStreamed_MessagesReceived() + { + // Arrange + var streamContent = new SyncPointMemoryStream(); + + var httpClient = TestHelpers.CreateTestClient(request => + { + return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK, new StreamContent(streamContent))); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncServerStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + + var responseStream = call.ResponseStream; + + // Assert + Assert.IsNull(responseStream.Current); + + var moveNextTask1 = responseStream.MoveNext(CancellationToken.None); + Assert.IsFalse(moveNextTask1.IsCompleted); + + await streamContent.AddDataAndWait(await TestHelpers.GetResponseDataAsync(new HelloReply + { + Message = "Hello world 1" + }).DefaultTimeout()).DefaultTimeout(); + + Assert.IsTrue(await moveNextTask1.DefaultTimeout()); + Assert.IsNotNull(responseStream.Current); + Assert.AreEqual("Hello world 1", responseStream.Current.Message); + + var moveNextTask2 = responseStream.MoveNext(CancellationToken.None); + Assert.IsFalse(moveNextTask2.IsCompleted); + + await streamContent.AddDataAndWait(await TestHelpers.GetResponseDataAsync(new HelloReply + { + Message = "Hello world 2" + }).DefaultTimeout()).DefaultTimeout(); + + Assert.IsTrue(await moveNextTask2.DefaultTimeout()); + Assert.IsNotNull(responseStream.Current); + Assert.AreEqual("Hello world 2", responseStream.Current.Message); + + var moveNextTask3 = responseStream.MoveNext(CancellationToken.None); + Assert.IsFalse(moveNextTask3.IsCompleted); + + await streamContent.AddDataAndWait(Array.Empty()).DefaultTimeout(); + + Assert.IsFalse(await moveNextTask3.DefaultTimeout()); + + var moveNextTask4 = responseStream.MoveNext(CancellationToken.None); + Assert.IsTrue(moveNextTask4.IsCompleted); + Assert.IsFalse(await moveNextTask3.DefaultTimeout()); + } + + [Test] + public void ClientStreamReader_WriteWithInvalidHttpStatus_ErrorThrown() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(request => + { + var streamContent = new StreamContent(new SyncPointMemoryStream()); + return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.NotFound, streamContent)); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncServerStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + + // Assert + var ex = Assert.ThrowsAsync(async () => await call.ResponseStream.MoveNext(CancellationToken.None).DefaultTimeout()); + + Assert.AreEqual("Bad gRPC response. Expected HTTP status code 200. Got status code: 404", ex.Message); + } + } +} diff --git a/test/Grpc.NetCore.HttpClient.Tests/AsyncUnaryCallTests.cs b/test/Grpc.NetCore.HttpClient.Tests/AsyncUnaryCallTests.cs new file mode 100644 index 000000000..079bdb5f2 --- /dev/null +++ b/test/Grpc.NetCore.HttpClient.Tests/AsyncUnaryCallTests.cs @@ -0,0 +1,130 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; +using Greet; +using Grpc.Core; +using Grpc.NetCore.HttpClient.Internal; +using Grpc.NetCore.HttpClient.Tests.Infrastructure; +using Grpc.Tests.Shared; +using NUnit.Framework; + +namespace Grpc.NetCore.HttpClient.Tests +{ + [TestFixture] + public class AsyncUnaryCallTests + { + [Test] + public async Task AsyncUnaryCall_Success_HttpRequestMessagePopulated() + { + // Arrange + HttpRequestMessage httpRequestMessage = null; + + var httpClient = TestHelpers.CreateTestClient(async request => + { + httpRequestMessage = request; + + HelloReply reply = new HelloReply + { + Message = "Hello world" + }; + + var streamContent = await TestHelpers.CreateResponseContent(reply).DefaultTimeout(); + + return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var rs = await invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + + // Assert + Assert.AreEqual("Hello world", rs.Message); + + Assert.IsNotNull(httpRequestMessage); + Assert.AreEqual(new Version(2, 0), httpRequestMessage.Version); + Assert.AreEqual(HttpMethod.Post, httpRequestMessage.Method); + Assert.AreEqual(new Uri("https://localhost/ServiceName/MethodName"), httpRequestMessage.RequestUri); + Assert.AreEqual(new MediaTypeHeaderValue("application/grpc"), httpRequestMessage.Content.Headers.ContentType); + + var userAgent = httpRequestMessage.Headers.UserAgent.Single(); + Assert.AreEqual(GrpcProtocolConstants.UserAgentHeader, userAgent); + Assert.AreEqual("grpc-dotnet", userAgent.Product.Name); + Assert.IsTrue(!string.IsNullOrEmpty(userAgent.Product.Version)); + } + + [Test] + public async Task AsyncUnaryCall_Success_RequestContentSent() + { + // Arrange + HttpContent content = null; + + var httpClient = TestHelpers.CreateTestClient(async request => + { + content = request.Content; + + HelloReply reply = new HelloReply + { + Message = "Hello world" + }; + + var streamContent = await TestHelpers.CreateResponseContent(reply).DefaultTimeout(); + + return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var rs = await invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest { Name = "World" }); + + // Assert + Assert.AreEqual("Hello world", rs.Message); + + Assert.IsNotNull(content); + + var requestContent = await content.ReadAsStreamAsync().DefaultTimeout(); + var requestMessage = await requestContent.ReadSingleMessageAsync(TestHelpers.ServiceMethod.RequestMarshaller.Deserializer, CancellationToken.None).DefaultTimeout(); + + Assert.AreEqual("World", requestMessage.Name); + } + + [Test] + public void AsyncUnaryCall_NonOkStatusTrailer_ThrowRpcError() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(request => + { + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, new ByteArrayContent(Array.Empty()), StatusCode.Unimplemented); + return Task.FromResult(response); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var ex = Assert.ThrowsAsync(async () => await invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest())); + + // Assert + Assert.AreEqual(StatusCode.Unimplemented, ex.StatusCode); + } + } +} diff --git a/test/Grpc.NetCore.HttpClient.Tests/CancellationTests.cs b/test/Grpc.NetCore.HttpClient.Tests/CancellationTests.cs new file mode 100644 index 000000000..833b60e80 --- /dev/null +++ b/test/Grpc.NetCore.HttpClient.Tests/CancellationTests.cs @@ -0,0 +1,115 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using NUnit.Framework; +using Greet; +using static Greet.Greeter; +using Grpc.Core; +using Google.Protobuf; +using System.Net.Http; +using Grpc.NetCore.HttpClient.Tests.Infrastructure; +using System.Net; +using System.Threading.Tasks; +using System; +using System.IO; +using System.Threading; +using System.Net.Http.Headers; +using System.Text; +using System.Linq; +using Grpc.NetCore.HttpClient.Internal; +using Grpc.Tests.Shared; + +namespace Grpc.NetCore.HttpClient.Tests +{ + [TestFixture] + public class CancellationTests + { + [Test] + public void AsyncClientStreamingCall_CancellationDuringSend_ResponseThrowsCancelledStatus() + { + // Arrange + var cts = new CancellationTokenSource(); + var invoker = CreateTimedoutCallInvoker(); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(cancellationToken: cts.Token)); + + // Assert + var responseTask = call.ResponseAsync; + Assert.IsFalse(responseTask.IsCompleted, "Response not returned until client stream is complete."); + + cts.Cancel(); + + var ex = Assert.ThrowsAsync(async () => await responseTask.DefaultTimeout()); + Assert.AreEqual(StatusCode.Cancelled, ex.Status.StatusCode); + } + + [Test] + public void AsyncClientStreamingCall_CancellationDuringSend_ResponseHeadersThrowsCancelledStatus() + { + // Arrange + var cts = new CancellationTokenSource(); + var invoker = CreateTimedoutCallInvoker(); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(cancellationToken: cts.Token)); + + // Assert + var responseHeadersTask = call.ResponseHeadersAsync; + Assert.IsFalse(responseHeadersTask.IsCompleted, "Headers not returned until client stream is complete."); + + cts.Cancel(); + + var ex = Assert.ThrowsAsync(async () => await responseHeadersTask.DefaultTimeout()); + Assert.AreEqual(StatusCode.Cancelled, ex.Status.StatusCode); + } + + [Test] + public void AsyncClientStreamingCall_CancellationDuringSend_TrailersThrowsCancelledStatus() + { + // Arrange + var cts = new CancellationTokenSource(); + var invoker = CreateTimedoutCallInvoker(); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(cancellationToken: cts.Token)); + + // Assert + cts.Cancel(); + + var ex = Assert.Throws(() => call.GetTrailers()); + + Assert.AreEqual(StatusCode.Cancelled, ex.Status.StatusCode); + } + + private static HttpClientCallInvoker CreateTimedoutCallInvoker() + { + PushStreamContent content = null; + + var httpClient = TestHelpers.CreateTestClient(async request => + { + content = (PushStreamContent)request.Content; + await content.PushComplete.DefaultTimeout(); + + return ResponseUtils.CreateResponse(HttpStatusCode.OK); + }); + var invoker = new HttpClientCallInvoker(httpClient); + return invoker; + } + } +} diff --git a/test/Grpc.NetCore.HttpClient.Tests/DeadlineTests.cs b/test/Grpc.NetCore.HttpClient.Tests/DeadlineTests.cs new file mode 100644 index 000000000..d0e651c16 --- /dev/null +++ b/test/Grpc.NetCore.HttpClient.Tests/DeadlineTests.cs @@ -0,0 +1,270 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Greet; +using Grpc.Core; +using Grpc.NetCore.HttpClient.Internal; +using Grpc.NetCore.HttpClient.Tests.Infrastructure; +using Grpc.Tests.Shared; +using NUnit.Framework; + +namespace Grpc.NetCore.HttpClient.Tests +{ + [TestFixture] + public class DeadlineTests + { + [Test] + public async Task AsyncUnaryCall_SetSecondDeadline_RequestMessageContainsDeadlineHeader() + { + // Arrange + HttpRequestMessage httpRequestMessage = null; + + var httpClient = TestHelpers.CreateTestClient(async request => + { + httpRequestMessage = request; + + var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout(); + return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + }); + var invoker = new HttpClientCallInvoker(httpClient); + invoker.Clock = new TestSystemClock(new DateTime(2019, 11, 29, 1, 1, 1, DateTimeKind.Utc)); + + // Act + await invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(deadline: invoker.Clock.UtcNow.AddSeconds(1)), new HelloRequest()); + + // Assert + Assert.IsNotNull(httpRequestMessage); + Assert.AreEqual("1S", httpRequestMessage.Headers.GetValues(GrpcProtocolConstants.TimeoutHeader).Single()); + } + + [Test] + public async Task AsyncUnaryCall_SetMaxValueDeadline_RequestMessageHasNoDeadlineHeader() + { + // Arrange + HttpRequestMessage httpRequestMessage = null; + + var httpClient = TestHelpers.CreateTestClient(async request => + { + httpRequestMessage = request; + + var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout(); + return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + await invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(deadline: DateTime.MaxValue), new HelloRequest()); + + // Assert + Assert.IsNotNull(httpRequestMessage); + Assert.AreEqual(0, httpRequestMessage.Headers.Count(h => string.Equals(h.Key, GrpcProtocolConstants.TimeoutHeader, StringComparison.OrdinalIgnoreCase))); + } + + [Test] + public async Task AsyncUnaryCall_SendDeadlineHeaderAndDeadlineValue_DeadlineValueIsUsed() + { + // Arrange + HttpRequestMessage httpRequestMessage = null; + + var httpClient = TestHelpers.CreateTestClient(async request => + { + httpRequestMessage = request; + + HelloReply reply = new HelloReply + { + Message = "Hello world" + }; + + var streamContent = await TestHelpers.CreateResponseContent(reply).DefaultTimeout(); + + return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + }); + var invoker = new HttpClientCallInvoker(httpClient); + invoker.Clock = new TestSystemClock(new DateTime(2019, 11, 29, 1, 1, 1, DateTimeKind.Utc)); + + var headers = new Metadata(); + headers.Add("grpc-timeout", "1D"); + + // Act + var rs = await invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(headers: headers, deadline: invoker.Clock.UtcNow.AddSeconds(1)), new HelloRequest()); + + // Assert + Assert.AreEqual("Hello world", rs.Message); + + Assert.IsNotNull(httpRequestMessage); + Assert.AreEqual("1S", httpRequestMessage.Headers.GetValues(GrpcProtocolConstants.TimeoutHeader).Single()); + } + + [Test] + public void AsyncClientStreamingCall_DeadlineDuringSend_ResponseThrowsDeadlineExceededStatus() + { + // Arrange + PushStreamContent content = null; + + var httpClient = TestHelpers.CreateTestClient(async request => + { + content = (PushStreamContent)request.Content; + await content.PushComplete.DefaultTimeout(); + + return ResponseUtils.CreateResponse(HttpStatusCode.OK); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(deadline: DateTime.UtcNow.AddSeconds(0.5))); + + // Assert + var responseTask = call.ResponseAsync; + Assert.IsFalse(responseTask.IsCompleted, "Response not returned until client stream is complete."); + + var ex = Assert.ThrowsAsync(async () => await responseTask.DefaultTimeout()); + Assert.AreEqual(StatusCode.DeadlineExceeded, ex.Status.StatusCode); + } + + [Test] + public void AsyncClientStreamingCall_DeadlineBeforeWrite_ResponseThrowsDeadlineExceededStatus() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(request => + { + return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK)); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(deadline: DateTime.UtcNow)); + + // Assert + var ex = Assert.ThrowsAsync(async () => await call.RequestStream.WriteAsync(new HelloRequest()).DefaultTimeout()); + Assert.AreEqual(StatusCode.DeadlineExceeded, ex.Status.StatusCode); + } + + [Test] + public void AsyncClientStreamingCall_DeadlineDuringWrite_ResponseThrowsDeadlineExceededStatus() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(request => + { + var stream = new SyncPointMemoryStream(); + var content = new StreamContent(stream); + return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK, content, grpcStatusCode: null)); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(deadline: DateTime.UtcNow.AddSeconds(0.5))); + + // Assert + var ex = Assert.ThrowsAsync(async () => await call.RequestStream.WriteAsync(new HelloRequest()).DefaultTimeout()); + Assert.AreEqual(StatusCode.DeadlineExceeded, ex.Status.StatusCode); + } + + [Test] + public void AsyncServerStreamingCall_DeadlineDuringWrite_ResponseThrowsDeadlineExceededStatus() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(request => + { + var stream = new SyncPointMemoryStream(); + var content = new StreamContent(stream); + return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK, content)); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncServerStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(deadline: DateTime.UtcNow.AddSeconds(0.5)), new HelloRequest()); + + // Assert + var ex = Assert.ThrowsAsync(async () => await call.ResponseStream.MoveNext(CancellationToken.None)); + Assert.AreEqual(StatusCode.DeadlineExceeded, ex.Status.StatusCode); + } + + [Test] + public async Task AsyncUnaryCall_SuccessAndReadValuesAfterDeadline_ValuesReturned() + { + // Arrange + HttpRequestMessage httpRequestMessage = null; + + var httpClient = TestHelpers.CreateTestClient(async request => + { + httpRequestMessage = request; + + var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout(); + return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + }); + var invoker = new HttpClientCallInvoker(httpClient); + invoker.Clock = new TestSystemClock(new DateTime(2019, 11, 29, 1, 1, 1, DateTimeKind.Utc)); + + // Act + var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(deadline: invoker.Clock.UtcNow.AddSeconds(0.5)), new HelloRequest()); + + // Assert + var result = await call; + Assert.IsNotNull(result); + + // Wait for deadline to trigger + await Task.Delay(1000); + + Assert.IsNotNull(await call.ResponseHeadersAsync); + + Assert.IsNotNull(call.GetTrailers()); + + Assert.AreEqual(StatusCode.OK, call.GetStatus().StatusCode); + } + + [Test] + public void AsyncUnaryCall_SetNonUtcDeadline_ThrowError() + { + // Arrange + HttpRequestMessage httpRequestMessage = null; + + var httpClient = TestHelpers.CreateTestClient(async request => + { + httpRequestMessage = request; + + var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout(); + return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var ex = Assert.ThrowsAsync(async () => await invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(deadline: new DateTime(2000, DateTimeKind.Local)), new HelloRequest())); + + // Assert + Assert.AreEqual("Deadline must have a kind DateTimeKind.Utc or be equal to DateTime.MaxValue or DateTime.MinValue.", ex.Message); + } + + private class TestSystemClock : ISystemClock + { + public TestSystemClock(DateTime utcNow) + { + UtcNow = utcNow; + } + + public DateTime UtcNow { get; } + } + } +} diff --git a/test/Grpc.NetCore.HttpClient.Tests/GetStatusTests.cs b/test/Grpc.NetCore.HttpClient.Tests/GetStatusTests.cs new file mode 100644 index 000000000..ad64cfd23 --- /dev/null +++ b/test/Grpc.NetCore.HttpClient.Tests/GetStatusTests.cs @@ -0,0 +1,159 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using NUnit.Framework; +using Greet; +using static Greet.Greeter; +using Grpc.Core; +using Google.Protobuf; +using System.Net.Http; +using Grpc.NetCore.HttpClient.Tests.Infrastructure; +using System.Net; +using System.Threading.Tasks; +using System; +using System.IO; +using System.Threading; +using System.Net.Http.Headers; +using System.Text; +using System.Linq; +using Grpc.NetCore.HttpClient.Internal; +using Grpc.Tests.Shared; + +namespace Grpc.NetCore.HttpClient.Tests +{ + [TestFixture] + public class GetStatusTests + { + [Test] + public void AsyncUnaryCall_ValidStatusReturned_ReturnsStatus() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(async request => + { + var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout(); + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent, grpcStatusCode: StatusCode.Aborted); + response.TrailingHeaders.Add(GrpcProtocolConstants.MessageTrailer, "value"); + return response; + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + + // Assert + var ex = Assert.ThrowsAsync(async () => await call.ResponseAsync.DefaultTimeout()); + Assert.AreEqual(StatusCode.Aborted, ex.StatusCode); + + var status = call.GetStatus(); + Assert.AreEqual(StatusCode.Aborted, status.StatusCode); + Assert.AreEqual("value", status.Detail); + } + + [Test] + public void AsyncUnaryCall_PercentEncodedMessage_MessageDecoded() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(async request => + { + var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout(); + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent, grpcStatusCode: StatusCode.Aborted); + response.TrailingHeaders.Add(GrpcProtocolConstants.MessageTrailer, "%C2%A3"); + return response; + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + + // Assert + var ex = Assert.ThrowsAsync(async () => await call.ResponseAsync.DefaultTimeout()); + Assert.AreEqual(StatusCode.Aborted, ex.StatusCode); + + var status = call.GetStatus(); + Assert.AreEqual(StatusCode.Aborted, status.StatusCode); + Assert.AreEqual("£", status.Detail); + } + + [Test] + public void AsyncUnaryCall_MultipleStatusHeaders_ThrowError() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(async request => + { + var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout(); + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent, grpcStatusCode: StatusCode.Aborted); + response.TrailingHeaders.Add(GrpcProtocolConstants.MessageTrailer, "one"); + response.TrailingHeaders.Add(GrpcProtocolConstants.MessageTrailer, "two"); + return response; + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + + // Assert + var ex = Assert.ThrowsAsync(async () => await call.ResponseAsync.DefaultTimeout()); + Assert.AreEqual("Multiple grpc-message headers.", ex.Message); + } + + [Test] + public void AsyncUnaryCall_MissingStatus_ThrowError() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(async request => + { + var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout(); + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent, grpcStatusCode: null); + return response; + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + + // Assert + Assert.ThrowsAsync(async () => await call.ResponseAsync.DefaultTimeout()); + + var ex = Assert.Throws(() => call.GetStatus()); + Assert.AreEqual("Response did not have a grpc-status trailer.", ex.Message); + } + + [Test] + public void AsyncUnaryCall_InvalidStatus_ThrowError() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(async request => + { + var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout(); + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent, grpcStatusCode: null); + response.TrailingHeaders.Add(GrpcProtocolConstants.StatusTrailer, "value"); + return response; + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + + // Assert + Assert.ThrowsAsync(async () => await call.ResponseAsync.DefaultTimeout()); + + var ex = Assert.Throws(() => call.GetStatus()); + Assert.AreEqual("Unexpected grpc-status value: value", ex.Message); + } + } +} diff --git a/test/Grpc.NetCore.HttpClient.Tests/GetTrailersTests.cs b/test/Grpc.NetCore.HttpClient.Tests/GetTrailersTests.cs new file mode 100644 index 000000000..7bd144e71 --- /dev/null +++ b/test/Grpc.NetCore.HttpClient.Tests/GetTrailersTests.cs @@ -0,0 +1,340 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using NUnit.Framework; +using Greet; +using static Greet.Greeter; +using Grpc.Core; +using Google.Protobuf; +using System.Net.Http; +using Grpc.NetCore.HttpClient.Tests.Infrastructure; +using System.Net; +using System.Threading.Tasks; +using System; +using System.IO; +using System.Threading; +using System.Net.Http.Headers; +using System.Text; +using System.Linq; +using Grpc.NetCore.HttpClient.Internal; +using Grpc.Tests.Shared; + +namespace Grpc.NetCore.HttpClient.Tests +{ + [TestFixture] + public class GetTrailersTests + { + [Test] + public async Task AsyncUnaryCall_MessageReturned_ReturnsTrailers() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(async request => + { + var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout(); + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + response.Headers.Add("custom", "ABC"); + response.TrailingHeaders.Add("custom-header", "value"); + return response; + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + var message = await call; + var trailers1 = call.GetTrailers(); + var trailers2 = call.GetTrailers(); + + // Assert + Assert.AreSame(trailers1, trailers2); + Assert.AreEqual("value", trailers1.Single(t => t.Key == "custom-header").Value); + } + + [Test] + public async Task AsyncUnaryCall_HeadersReturned_ReturnsTrailers() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(async request => + { + var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout(); + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + response.Headers.Add("custom", "ABC"); + response.TrailingHeaders.Add("custom-header", "value"); + return response; + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + var responseHeaders = await call.ResponseHeadersAsync.DefaultTimeout(); + var trailers = call.GetTrailers(); + + // Assert + Assert.AreEqual("value", trailers.Single(t => t.Key == "custom-header").Value); + } + + [Test] + public void AsyncUnaryCall_UnfinishedCall_ThrowsError() + { + // Arrange + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var httpClient = TestHelpers.CreateTestClient(async request => + { + await tcs.Task.DefaultTimeout(); + return null; + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + var ex = Assert.Throws(() => call.GetTrailers()); + + // Assert + Assert.AreEqual("Can't get the call trailers because the call is not complete.", ex.Message); + } + + [Test] + public void AsyncUnaryCall_ErrorCall_ThrowsError() + { + // Arrange + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var httpClient = TestHelpers.CreateTestClient(request => + { + return Task.FromException(new Exception("An error!")); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + var ex = Assert.Throws(() => call.GetTrailers()); + + // Assert + Assert.AreEqual("Can't get the call trailers because an error occured when making the request.", ex.Message); + Assert.AreEqual("An error!", ex.InnerException.InnerException.Message); + } + + [Test] + public void AsyncClientStreamingCall_UnfinishedCall_ThrowsError() + { + // Arrange + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var httpClient = TestHelpers.CreateTestClient(async request => + { + await tcs.Task.DefaultTimeout(); + return null; + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + var ex = Assert.Throws(() => call.GetTrailers()); + + // Assert + Assert.AreEqual("Can't get the call trailers because the call is not complete.", ex.Message); + } + + [Test] + public void AsyncServerStreamingCall_UnfinishedCall_ThrowsError() + { + // Arrange + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var httpClient = TestHelpers.CreateTestClient(async request => + { + await tcs.Task.DefaultTimeout(); + return null; + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncServerStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + var ex = Assert.Throws(() => call.GetTrailers()); + + // Assert + Assert.AreEqual("Can't get the call trailers because the call is not complete.", ex.Message); + } + + [Test] + public async Task AsyncServerStreamingCall_UnfinishedReader_ThrowsError() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(async request => + { + var streamContent = await TestHelpers.CreateResponseContent( + new HelloReply + { + Message = "Hello world 1" + }, + new HelloReply + { + Message = "Hello world 2" + }).DefaultTimeout(); + + return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncServerStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + var responseStream = call.ResponseStream; + + Assert.IsTrue(await responseStream.MoveNext(CancellationToken.None).DefaultTimeout()); + var ex = Assert.Throws(() => call.GetTrailers()); + + // Assert + Assert.AreEqual("Can't get the call trailers because the call is not complete.", ex.Message); + } + + [Test] + public async Task AsyncServerStreamingCall_FinishedReader_ReturnsTrailers() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(async request => + { + var streamContent = await TestHelpers.CreateResponseContent( + new HelloReply + { + Message = "Hello world 1" + }, + new HelloReply + { + Message = "Hello world 2" + }).DefaultTimeout(); + + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + response.TrailingHeaders.Add("custom-header", "value"); + return response; + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncServerStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + var responseStream = call.ResponseStream; + + Assert.IsTrue(await responseStream.MoveNext(CancellationToken.None).DefaultTimeout()); + Assert.IsTrue(await responseStream.MoveNext(CancellationToken.None).DefaultTimeout()); + Assert.IsFalse(await responseStream.MoveNext(CancellationToken.None).DefaultTimeout()); + var trailers = call.GetTrailers(); + + // Assert + Assert.AreEqual("value", trailers.Single(t => t.Key == "custom-header").Value); + } + + [Test] + public async Task AsyncClientStreamingCall_CompleteWriter_ReturnsTrailers() + { + // Arrange + var trailingHeadersWrittenTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var httpClient = TestHelpers.CreateTestClient(request => + { + var content = (PushStreamContent)request.Content; + var stream = new SyncPointMemoryStream(); + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, new StreamContent(stream)); + + _ = Task.Run(async () => + { + // Add a response message after the client has completed + await content.PushComplete.DefaultTimeout(); + + var messageData = await TestHelpers.GetResponseDataAsync(new HelloReply { Message = "Hello world" }).DefaultTimeout(); + await stream.AddDataAndWait(messageData).DefaultTimeout(); + await stream.AddDataAndWait(Array.Empty()).DefaultTimeout(); + + response.TrailingHeaders.Add("custom-header", "value"); + trailingHeadersWrittenTcs.SetResult(true); + }); + + return Task.FromResult(response); + }); + + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + await call.RequestStream.CompleteAsync().DefaultTimeout(); + await Task.WhenAll(call.ResponseAsync, trailingHeadersWrittenTcs.Task).DefaultTimeout(); + var trailers = call.GetTrailers(); + + // Assert + Assert.AreEqual("value", trailers.Single(t => t.Key == "custom-header").Value); + } + + [Test] + public void AsyncClientStreamingCall_UncompleteWriter_ThrowsError() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(request => + { + var stream = new SyncPointMemoryStream(); + + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, new StreamContent(stream), grpcStatusCode: null); + return Task.FromResult(response); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + var ex = Assert.Throws(() => call.GetTrailers()); + + // Assert + Assert.AreEqual("Can't get the call trailers because the call is not complete.", ex.Message); + } + + [Test] + public void AsyncClientStreamingCall_NotFoundStatus_ThrowsError() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(request => + { + var response = ResponseUtils.CreateResponse(HttpStatusCode.NotFound); + return Task.FromResult(response); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + var ex = Assert.Throws(() => call.GetTrailers()); + + // Assert + Assert.AreEqual("Bad gRPC response. Expected HTTP status code 200. Got status code: 404", ex.Message); + } + + [Test] + public void AsyncClientStreamingCall_InvalidContentType_ThrowsError() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(request => + { + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK); + response.Content.Headers.ContentType = new MediaTypeHeaderValue("text/plain"); + return Task.FromResult(response); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + var ex = Assert.Throws(() => call.GetTrailers()); + + // Assert + Assert.AreEqual("Bad gRPC response. Invalid content-type value: text/plain", ex.Message); + } + } +} diff --git a/test/Grpc.NetCore.HttpClient.Tests/Grpc.NetCore.HttpClient.Tests.csproj b/test/Grpc.NetCore.HttpClient.Tests/Grpc.NetCore.HttpClient.Tests.csproj new file mode 100644 index 000000000..c2dfb4bba --- /dev/null +++ b/test/Grpc.NetCore.HttpClient.Tests/Grpc.NetCore.HttpClient.Tests.csproj @@ -0,0 +1,29 @@ + + + + netcoreapp3.0 + false + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/Grpc.NetCore.HttpClient.Tests/GrpcClientFactoryTests.cs b/test/Grpc.NetCore.HttpClient.Tests/GrpcClientFactoryTests.cs new file mode 100644 index 000000000..1e5a4b518 --- /dev/null +++ b/test/Grpc.NetCore.HttpClient.Tests/GrpcClientFactoryTests.cs @@ -0,0 +1,38 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using NUnit.Framework; +using Greet; +using static Greet.Greeter; + +namespace Grpc.NetCore.HttpClient.Tests +{ + [TestFixture] + public class GrpcClientFactoryTests + { + [Test] + public void Create_WithBaseAddress_ReturnInstance() + { + // Arrange & Act + var client = GrpcClientFactory.Create("http://localhost"); + + // Assert + Assert.IsNotNull(client); + } + } +} diff --git a/test/Grpc.NetCore.HttpClient.Tests/GrpcProtocolHelpersTests.cs b/test/Grpc.NetCore.HttpClient.Tests/GrpcProtocolHelpersTests.cs new file mode 100644 index 000000000..1ca736199 --- /dev/null +++ b/test/Grpc.NetCore.HttpClient.Tests/GrpcProtocolHelpersTests.cs @@ -0,0 +1,59 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using NUnit.Framework; +using Greet; +using static Greet.Greeter; +using System; +using Grpc.NetCore.HttpClient.Internal; + +namespace Grpc.NetCore.HttpClient.Tests +{ + [TestFixture] + public class GrpcProtocolHelpersTests + { + private const int MillisecondsPerSecond = 1000; + + [TestCase(-1, "1n")] + [TestCase(-10, "1n")] + [TestCase(1, "1m")] + [TestCase(10, "10m")] + [TestCase(100, "100m")] + [TestCase(890, "890m")] + [TestCase(900, "900m")] + [TestCase(901, "901m")] + [TestCase(1000, "1S")] + [TestCase(2000, "2S")] + [TestCase(2500, "2500m")] + [TestCase(59900, "59900m")] + [TestCase(50000, "50S")] + [TestCase(59000, "59S")] + [TestCase(60000, "1M")] + [TestCase(80000, "80S")] + [TestCase(90000, "90S")] + [TestCase(120000, "2M")] + [TestCase(20 * 60 * MillisecondsPerSecond, "20M")] + [TestCase(60 * 60 * MillisecondsPerSecond, "1H")] + [TestCase(10 * 60 * 60 * MillisecondsPerSecond, "10H")] + public void EncodeTimeout(int milliseconds, string expected) + { + var encoded = GrpcProtocolHelpers.EncodeTimeout(milliseconds); + Assert.AreEqual(expected, encoded); + } + } +} diff --git a/test/Grpc.NetCore.HttpClient.Tests/HeadersTests.cs b/test/Grpc.NetCore.HttpClient.Tests/HeadersTests.cs new file mode 100644 index 000000000..651f62675 --- /dev/null +++ b/test/Grpc.NetCore.HttpClient.Tests/HeadersTests.cs @@ -0,0 +1,110 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Greet; +using Grpc.Core; +using Grpc.NetCore.HttpClient.Tests.Infrastructure; +using Grpc.Tests.Shared; +using Microsoft.Net.Http.Headers; +using NUnit.Framework; + +namespace Grpc.NetCore.HttpClient.Tests +{ + [TestFixture] + public class HeadersTests + { + [Test] + public async Task AsyncUnaryCall_SendHeaders_RequestMessageContainsHeaders() + { + // Arrange + HttpRequestMessage httpRequestMessage = null; + + var httpClient = TestHelpers.CreateTestClient(async request => + { + httpRequestMessage = request; + + HelloReply reply = new HelloReply + { + Message = "Hello world" + }; + + var streamContent = await TestHelpers.CreateResponseContent(reply).DefaultTimeout(); + + return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + var headers = new Metadata(); + headers.Add("custom", "ascii"); + headers.Add("custom-bin", Encoding.UTF8.GetBytes("Hello world")); + + // Act + var rs = await invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(headers: headers), new HelloRequest()); + + // Assert + Assert.AreEqual("Hello world", rs.Message); + + Assert.IsNotNull(httpRequestMessage); + Assert.AreEqual("ascii", httpRequestMessage.Headers.GetValues("custom").Single()); + Assert.AreEqual("Hello world", Encoding.UTF8.GetString(Convert.FromBase64String(httpRequestMessage.Headers.GetValues("custom-bin").Single()))); + } + + [Test] + public async Task AsyncUnaryCall_NoHeaders_RequestMessageHasNoHeaders() + { + // Arrange + HttpRequestMessage httpRequestMessage = null; + + var httpClient = TestHelpers.CreateTestClient(async request => + { + httpRequestMessage = request; + + HelloReply reply = new HelloReply + { + Message = "Hello world" + }; + + var streamContent = await TestHelpers.CreateResponseContent(reply).DefaultTimeout(); + + return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + var headers = new Metadata(); + + // Act + var rs = await invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(headers: headers), new HelloRequest()); + + // Assert + Assert.AreEqual("Hello world", rs.Message); + + Assert.IsNotNull(httpRequestMessage); + + // User-Agent is always sent + Assert.AreEqual(0, httpRequestMessage.Headers.Count(h => !string.Equals(h.Key, HeaderNames.UserAgent, StringComparison.OrdinalIgnoreCase))); + } + } +} diff --git a/test/Grpc.NetCore.HttpClient.Tests/HttpContentClientStreamReaderTests.cs b/test/Grpc.NetCore.HttpClient.Tests/HttpContentClientStreamReaderTests.cs new file mode 100644 index 000000000..d7579454a --- /dev/null +++ b/test/Grpc.NetCore.HttpClient.Tests/HttpContentClientStreamReaderTests.cs @@ -0,0 +1,115 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Greet; +using Grpc.Core; +using Grpc.NetCore.HttpClient.Internal; +using Grpc.NetCore.HttpClient.Tests.Infrastructure; +using Grpc.Tests.Shared; +using NUnit.Framework; + +namespace Grpc.NetCore.HttpClient.Tests +{ + [TestFixture] + public class HttpContentClientStreamReaderTests + { + [Test] + public void MoveNext_TokenCanceledBeforeCall_ThrowError() + { + // Arrange + var cts = new CancellationTokenSource(); + cts.Cancel(); + + var httpClient = TestHelpers.CreateTestClient(request => + { + var stream = new SyncPointMemoryStream(); + var content = new StreamContent(stream); + return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK, content)); + }); + + var call = new GrpcCall(TestHelpers.ServiceMethod, new CallOptions(), SystemClock.Instance); + call.StartServerStreaming(httpClient, new HelloRequest()); + + // Act + var moveNextTask1 = call.ClientStreamReader.MoveNext(cts.Token); + + // Assert + Assert.IsTrue(moveNextTask1.IsCompleted); + var ex = Assert.ThrowsAsync(async () => await moveNextTask1.DefaultTimeout()); + Assert.AreEqual(StatusCode.Cancelled, ex.StatusCode); + } + + [Test] + public void MoveNext_TokenCanceledDuringCall_ThrowError() + { + // Arrange + var cts = new CancellationTokenSource(); + + var httpClient = TestHelpers.CreateTestClient(request => + { + var stream = new SyncPointMemoryStream(); + var content = new StreamContent(stream); + return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK, content)); + }); + + var call = new GrpcCall(TestHelpers.ServiceMethod, new CallOptions(), SystemClock.Instance); + call.StartServerStreaming(httpClient, new HelloRequest()); + + // Act + var moveNextTask1 = call.ClientStreamReader.MoveNext(cts.Token); + + // Assert + Assert.IsFalse(moveNextTask1.IsCompleted); + + cts.Cancel(); + + var ex = Assert.ThrowsAsync(async () => await moveNextTask1.DefaultTimeout()); + Assert.AreEqual(StatusCode.Cancelled, ex.StatusCode); + } + + [Test] + public void MoveNext_MultipleCallsWithoutAwait_ThrowError() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(request => + { + var stream = new SyncPointMemoryStream(); + var content = new StreamContent(stream); + return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK, content)); + }); + + var call = new GrpcCall(TestHelpers.ServiceMethod, new CallOptions(), SystemClock.Instance); + call.StartServerStreaming(httpClient, new HelloRequest()); + + // Act + var moveNextTask1 = call.ClientStreamReader.MoveNext(CancellationToken.None); + var moveNextTask2 = call.ClientStreamReader.MoveNext(CancellationToken.None); + + // Assert + Assert.IsFalse(moveNextTask1.IsCompleted); + + var ex = Assert.ThrowsAsync(async () => await moveNextTask2.DefaultTimeout()); + Assert.AreEqual("Cannot read next message because the previous read is in progress.", ex.Message); + } + } +} diff --git a/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/ResponseUtils.cs b/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/ResponseUtils.cs new file mode 100644 index 000000000..87a21705c --- /dev/null +++ b/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/ResponseUtils.cs @@ -0,0 +1,80 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System.Net.Http; +using System; +using System.Net; +using System.IO; +using System.Threading.Tasks; +using System.Threading; +using System.Diagnostics; +using System.Buffers.Binary; +using Grpc.NetCore.HttpClient.Internal; +using Grpc.Core; + +namespace Grpc.NetCore.HttpClient.Tests.Infrastructure +{ + internal static class ResponseUtils + { + public static HttpResponseMessage CreateResponse(HttpStatusCode statusCode) => + CreateResponse(statusCode, string.Empty); + + public static HttpResponseMessage CreateResponse(HttpStatusCode statusCode, string payload) => + CreateResponse(statusCode, new StringContent(payload)); + + public static HttpResponseMessage CreateResponse(HttpStatusCode statusCode, HttpContent payload, StatusCode? grpcStatusCode = StatusCode.OK) + { + payload.Headers.ContentType = GrpcProtocolConstants.GrpcContentTypeHeaderValue; + + var message = new HttpResponseMessage(statusCode) + { + Content = payload + }; + + if (grpcStatusCode != null) + { + message.TrailingHeaders.Add(GrpcProtocolConstants.StatusTrailer, grpcStatusCode.Value.ToString("D")); + } + + return message; + } + + private const int MessageDelimiterSize = 4; // how many bytes it takes to encode "Message-Length" + private const int HeaderSize = MessageDelimiterSize + 1; // message length + compression flag + + public static Task WriteHeaderAsync(Stream stream, int length, bool compress, CancellationToken cancellationToken) + { + var headerData = new byte[HeaderSize]; + + // Compression flag + headerData[0] = compress ? (byte)1 : (byte)0; + + // Message length + EncodeMessageLength(length, headerData.AsSpan(1)); + + return stream.WriteAsync(headerData, 0, headerData.Length, cancellationToken); + } + + private static void EncodeMessageLength(int messageLength, Span destination) + { + Debug.Assert(destination.Length >= MessageDelimiterSize, "Buffer too small to encode message length."); + + BinaryPrimitives.WriteUInt32BigEndian(destination, (uint)messageLength); + } + } +} diff --git a/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/TestHelpers.cs b/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/TestHelpers.cs new file mode 100644 index 000000000..261622d2d --- /dev/null +++ b/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/TestHelpers.cs @@ -0,0 +1,82 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.IO; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; +using Google.Protobuf; +using Greet; +using Grpc.Core; + +namespace Grpc.NetCore.HttpClient.Tests.Infrastructure +{ + public static class TestHelpers + { + public static readonly Marshaller HelloRequestMarshaller = Marshallers.Create(r => r.ToByteArray(), data => HelloRequest.Parser.ParseFrom(data)); + public static readonly Marshaller HelloReplyMarshaller = Marshallers.Create(r => r.ToByteArray(), data => HelloReply.Parser.ParseFrom(data)); + + public static readonly Method ServiceMethod = new Method(MethodType.Unary, "ServiceName", "MethodName", HelloRequestMarshaller, HelloReplyMarshaller); + + public static System.Net.Http.HttpClient CreateTestClient(Func> sendAsync) + { + var handler = TestHttpMessageHandler.Create(sendAsync); + var httpClient = new System.Net.Http.HttpClient(handler); + httpClient.BaseAddress = new Uri("https://localhost"); + + return httpClient; + } + + public static System.Net.Http.HttpClient CreateTestClient(Func> sendAsync) + { + var handler = TestHttpMessageHandler.Create(sendAsync); + var httpClient = new System.Net.Http.HttpClient(handler); + httpClient.BaseAddress = new Uri("https://localhost"); + + return httpClient; + } + + public static async Task CreateResponseContent(params TResponse[] responses) where TResponse : IMessage + { + var ms = new MemoryStream(); + foreach (var response in responses) + { + await WriteResponseAsync(ms, response); + } + ms.Seek(0, SeekOrigin.Begin); + var streamContent = new StreamContent(ms); + streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/grpc"); + return streamContent; + } + + public static async Task WriteResponseAsync(Stream ms, TResponse response) where TResponse : IMessage + { + await ResponseUtils.WriteHeaderAsync(ms, response.CalculateSize(), false, CancellationToken.None); + await ms.WriteAsync(response.ToByteArray()); + } + + public static async Task GetResponseDataAsync(TResponse response) where TResponse : IMessage + { + var ms = new MemoryStream(); + await WriteResponseAsync(ms, response); + return ms.ToArray(); + } + } +} diff --git a/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/TestHttpMessageHandler.cs b/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/TestHttpMessageHandler.cs new file mode 100644 index 000000000..895cb42e4 --- /dev/null +++ b/test/Grpc.NetCore.HttpClient.Tests/Infrastructure/TestHttpMessageHandler.cs @@ -0,0 +1,50 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System; + +namespace Grpc.NetCore.HttpClient.Tests.Infrastructure +{ + public class TestHttpMessageHandler : HttpMessageHandler + { + private readonly Func> _sendAsync; + + public TestHttpMessageHandler(Func> sendAsync) + { + _sendAsync = sendAsync; + } + + public static TestHttpMessageHandler Create(Func> sendAsync) + { + return new TestHttpMessageHandler((request, cancellationToken) => sendAsync(request)); + } + + public static TestHttpMessageHandler Create(Func> sendAsync) + { + return new TestHttpMessageHandler(sendAsync); + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return _sendAsync(request, cancellationToken); + } + } +} diff --git a/test/Grpc.NetCore.HttpClient.Tests/Proto/greet.proto b/test/Grpc.NetCore.HttpClient.Tests/Proto/greet.proto new file mode 100644 index 000000000..85fb59318 --- /dev/null +++ b/test/Grpc.NetCore.HttpClient.Tests/Proto/greet.proto @@ -0,0 +1,35 @@ +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package Greet; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply) {} + rpc SayHellos (HelloRequest) returns (stream HelloReply) {} +} + +service SecondGreeter { + rpc SayHello (HelloRequest) returns (HelloReply) {} + rpc SayHellos (HelloRequest) returns (stream HelloReply) {} +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} diff --git a/test/Grpc.NetCore.HttpClient.Tests/ResponseAsyncTests.cs b/test/Grpc.NetCore.HttpClient.Tests/ResponseAsyncTests.cs new file mode 100644 index 000000000..0f3fd9dd2 --- /dev/null +++ b/test/Grpc.NetCore.HttpClient.Tests/ResponseAsyncTests.cs @@ -0,0 +1,152 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Greet; +using Grpc.Core; +using Grpc.NetCore.HttpClient.Tests.Infrastructure; +using Grpc.Tests.Shared; +using NUnit.Framework; + +namespace Grpc.NetCore.HttpClient.Tests +{ + [TestFixture] + public class ResponseAsyncTests + { + [Test] + public async Task AsyncUnaryCall_AwaitMultipleTimes_SameMessageReturned() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(async request => + { + HelloReply reply = new HelloReply + { + Message = "Hello world" + }; + + var streamContent = await TestHelpers.CreateResponseContent(reply).DefaultTimeout(); + + return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest { Name = "World" }); + + var response1 = await call; + var response2 = await call; + var response3 = await call.ResponseAsync.DefaultTimeout(); + var response4 = await call.ResponseAsync.DefaultTimeout(); + + // Assert + Assert.AreEqual("Hello world", response1.Message); + + Assert.AreEqual(response1, response2); + Assert.AreEqual(response1, response3); + Assert.AreEqual(response1, response4); + } + + [Test] + public async Task AsyncUnaryCall_DisposeAfterHeadersAndBeforeMessage_ThrowsError() + { + // Arrange + var stream = new SyncPointMemoryStream(); + + var httpClient = TestHelpers.CreateTestClient(request => + { + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, new StreamContent(stream)); + response.Headers.Add("custom", "value!"); + return Task.FromResult(response); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest { Name = "World" }); + var responseHeaders = await call.ResponseHeadersAsync.DefaultTimeout(); + call.Dispose(); + + // Assert + Assert.ThrowsAsync(async () => await call.ResponseAsync.DefaultTimeout()); + + var header = responseHeaders.Single(h => h.Key == "custom"); + Assert.AreEqual("value!", header.Value); + } + + [Test] + public void AsyncUnaryCall_ErrorSendingRequest_ThrowsError() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(request => + { + return Task.FromException(new Exception("An error!")); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + var ex = Assert.CatchAsync(() => call.ResponseAsync); + + // Assert + Assert.AreEqual("An error!", ex.Message); + } + + [Test] + public void AsyncClientStreamingCall_NotFoundStatus_ThrowsError() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(request => + { + var response = ResponseUtils.CreateResponse(HttpStatusCode.NotFound); + return Task.FromResult(response); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + var ex = Assert.ThrowsAsync(async () => await call.ResponseAsync.DefaultTimeout()); + + // Assert + Assert.AreEqual("Bad gRPC response. Expected HTTP status code 200. Got status code: 404", ex.Message); + } + + [Test] + public void AsyncClientStreamingCall_InvalidContentType_ThrowsError() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(request => + { + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK); + response.Content.Headers.ContentType = new MediaTypeHeaderValue("text/plain"); + return Task.FromResult(response); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + var ex = Assert.ThrowsAsync(async () => await call.ResponseAsync.DefaultTimeout()); + + // Assert + Assert.AreEqual("Bad gRPC response. Invalid content-type value: text/plain", ex.Message); + } + } +} diff --git a/test/Grpc.NetCore.HttpClient.Tests/ResponseHeadersAsyncTests.cs b/test/Grpc.NetCore.HttpClient.Tests/ResponseHeadersAsyncTests.cs new file mode 100644 index 000000000..4fcb0bb4b --- /dev/null +++ b/test/Grpc.NetCore.HttpClient.Tests/ResponseHeadersAsyncTests.cs @@ -0,0 +1,225 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Greet; +using Grpc.Core; +using Grpc.NetCore.HttpClient.Tests.Infrastructure; +using Grpc.Tests.Shared; +using NUnit.Framework; + +namespace Grpc.NetCore.HttpClient.Tests +{ + [TestFixture] + public class ResponseHeadersAsyncTests + { + [Test] + public async Task AsyncUnaryCall_Success_ResponseHeadersPopulated() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(async request => + { + HelloReply reply = new HelloReply + { + Message = "Hello world" + }; + + var streamContent = await TestHelpers.CreateResponseContent(reply).DefaultTimeout(); + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + response.Headers.Server.Add(new ProductInfoHeaderValue("TestName", "1.0")); + response.Headers.Add("custom", "ABC"); + response.Headers.Add("binary-bin", Convert.ToBase64String(Encoding.UTF8.GetBytes("Hello world"))); + return response; + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncUnaryCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + var responseHeaders1 = await call.ResponseHeadersAsync.DefaultTimeout(); + var responseHeaders2 = await call.ResponseHeadersAsync.DefaultTimeout(); + + // Assert + Assert.AreSame(responseHeaders1, responseHeaders2); + + var header = responseHeaders1.Single(h => h.Key == "server"); + Assert.AreEqual("TestName/1.0", header.Value); + + header = responseHeaders1.Single(h => h.Key == "custom"); + Assert.AreEqual("ABC", header.Value); + + header = responseHeaders1.Single(h => h.Key == "binary-bin"); + Assert.AreEqual(true, header.IsBinary); + CollectionAssert.AreEqual(Encoding.UTF8.GetBytes("Hello world"), header.ValueBytes); + } + + [Test] + public async Task AsyncClientStreamingCall_Success_ResponseHeadersPopulated() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(async request => + { + var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout(); + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + response.Headers.Add("custom", "ABC"); + return response; + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + var responseHeaders = await call.ResponseHeadersAsync.DefaultTimeout(); + + // Assert + var header = responseHeaders.Single(h => h.Key == "custom"); + Assert.AreEqual("ABC", header.Value); + } + + [Test] + public async Task AsyncDuplexStreamingCall_Success_ResponseHeadersPopulated() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(async request => + { + var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout(); + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + response.Headers.Add("custom", "ABC"); + return response; + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncDuplexStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + var responseHeaders = await call.ResponseHeadersAsync.DefaultTimeout(); + + // Assert + var header = responseHeaders.Single(h => h.Key == "custom"); + Assert.AreEqual("ABC", header.Value); + } + + [Test] + public async Task AsyncServerStreamingCall_Success_ResponseHeadersPopulated() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(async request => + { + var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout(); + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + response.Headers.Add("custom", "ABC"); + return response; + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncServerStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + var responseHeaders = await call.ResponseHeadersAsync.DefaultTimeout(); + + // Assert + var header = responseHeaders.Single(h => h.Key == "custom"); + Assert.AreEqual("ABC", header.Value); + } + + [Test] + public void AsyncServerStreamingCall_ErrorSendingRequest_ReturnsError() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(request => + { + return Task.FromException(new Exception("An error!")); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncServerStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + var ex = Assert.CatchAsync(() => call.ResponseHeadersAsync); + + // Assert + Assert.AreEqual("An error!", ex.Message); + } + + [Test] + public void AsyncServerStreamingCall_DisposeBeforeHeadersReceived_ReturnsError() + { + // Arrange + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var httpClient = TestHelpers.CreateTestClient(async (request, ct) => + { + await tcs.Task.DefaultTimeout(); + ct.ThrowIfCancellationRequested(); + var streamContent = await TestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout(); + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent); + response.Headers.Add("custom", "ABC"); + return response; + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncServerStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions(), new HelloRequest()); + call.Dispose(); + tcs.TrySetResult(true); + + // Assert + Assert.ThrowsAsync(() => call.ResponseHeadersAsync); + } + + [Test] + public void AsyncClientStreamingCall_NotFoundStatus_ThrowsError() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(request => + { + var response = ResponseUtils.CreateResponse(HttpStatusCode.NotFound); + return Task.FromResult(response); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + var ex = Assert.ThrowsAsync(async () => await call.ResponseHeadersAsync.DefaultTimeout()); + + // Assert + Assert.AreEqual("Bad gRPC response. Expected HTTP status code 200. Got status code: 404", ex.Message); + } + + [Test] + public void AsyncClientStreamingCall_InvalidContentType_ThrowsError() + { + // Arrange + var httpClient = TestHelpers.CreateTestClient(request => + { + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK); + response.Content.Headers.ContentType = new MediaTypeHeaderValue("text/plain"); + return Task.FromResult(response); + }); + var invoker = new HttpClientCallInvoker(httpClient); + + // Act + var call = invoker.AsyncClientStreamingCall(TestHelpers.ServiceMethod, null, new CallOptions()); + var ex = Assert.ThrowsAsync(async () => await call.ResponseHeadersAsync.DefaultTimeout()); + + // Assert + Assert.AreEqual("Bad gRPC response. Invalid content-type value: text/plain", ex.Message); + } + } +} diff --git a/test/Shared/HttpContextServerCallContextHelpers.cs b/test/Shared/HttpContextServerCallContextHelpers.cs index c9b5e8369..7e7439328 100644 --- a/test/Shared/HttpContextServerCallContextHelpers.cs +++ b/test/Shared/HttpContextServerCallContextHelpers.cs @@ -22,7 +22,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Grpc.AspNetCore.FunctionalTests.Infrastructure +namespace Grpc.Tests.Shared { internal static class HttpContextServerCallContextHelper { diff --git a/test/Shared/MessageHelpers.cs b/test/Shared/MessageHelpers.cs index 7b0b51b0d..fdc51558d 100644 --- a/test/Shared/MessageHelpers.cs +++ b/test/Shared/MessageHelpers.cs @@ -27,7 +27,7 @@ using Grpc.AspNetCore.Server.Internal; using Microsoft.AspNetCore.Http; -namespace Grpc.AspNetCore.FunctionalTests.Infrastructure +namespace Grpc.Tests.Shared { internal static class MessageHelpers { diff --git a/test/Shared/SyncPoint.cs b/test/Shared/SyncPoint.cs index 1d722a50e..0269812dc 100644 --- a/test/Shared/SyncPoint.cs +++ b/test/Shared/SyncPoint.cs @@ -20,7 +20,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Grpc.AspNetCore.Server.Tests +namespace Grpc.Tests.Shared { public class SyncPoint { diff --git a/test/Shared/SyncPointMemoryStream.cs b/test/Shared/SyncPointMemoryStream.cs index c9e368f3f..cfd81d1cf 100644 --- a/test/Shared/SyncPointMemoryStream.cs +++ b/test/Shared/SyncPointMemoryStream.cs @@ -22,7 +22,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Grpc.AspNetCore.Server.Tests +namespace Grpc.Tests.Shared { /// /// A memory stream that waits for data when reading and allows the sender of data to wait for it to be read. diff --git a/test/FunctionalTests/Infrastructure/TaskExtensions.cs b/test/Shared/TaskExtensions.cs similarity index 98% rename from test/FunctionalTests/Infrastructure/TaskExtensions.cs rename to test/Shared/TaskExtensions.cs index 894c82a84..ea708499e 100644 --- a/test/FunctionalTests/Infrastructure/TaskExtensions.cs +++ b/test/Shared/TaskExtensions.cs @@ -22,7 +22,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Grpc.AspNetCore.FunctionalTests.Infrastructure +namespace Grpc.Tests.Shared { internal static class TaskExtensions { diff --git a/testassets/FunctionalTestsWebsite/Startup.cs b/testassets/FunctionalTestsWebsite/Startup.cs index 8abffa8e3..b260a20e6 100644 --- a/testassets/FunctionalTestsWebsite/Startup.cs +++ b/testassets/FunctionalTestsWebsite/Startup.cs @@ -67,7 +67,6 @@ public void ConfigureServices(IServiceCollection services) policy.RequireClaim(ClaimTypes.NameIdentifier); }); }); - services.AddAuthorizationPolicyEvaluator(); services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { diff --git a/testassets/InteropTestsClient/Certs/README.md b/testassets/InteropTestsClient/Certs/README.md new file mode 100644 index 000000000..79206a486 --- /dev/null +++ b/testassets/InteropTestsClient/Certs/README.md @@ -0,0 +1,8 @@ +Keys taken from https://github.com/grpc/grpc/tree/master/src/core/tsi/test_creds +so that interop server in this project is compatible with interop clients +implemented in other gRPC languages. + +The server1.pem and server1.key were combined into server1.pfx. The password is 1111. These certs are not secure, do not use in production. +``` +openssl pkcs12 -export -out server1.pfx -inkey server1.key -in server1.pem -certfile ca.pem +``` diff --git a/testassets/InteropTestsClient/Certs/ca.pem b/testassets/InteropTestsClient/Certs/ca.pem new file mode 100644 index 000000000..6c8511a73 --- /dev/null +++ b/testassets/InteropTestsClient/Certs/ca.pem @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICSjCCAbOgAwIBAgIJAJHGGR4dGioHMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQxDzANBgNVBAMTBnRlc3RjYTAeFw0xNDExMTEyMjMxMjla +Fw0yNDExMDgyMjMxMjlaMFYxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0 +YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxDzANBgNVBAMT +BnRlc3RjYTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwEDfBV5MYdlHVHJ7 ++L4nxrZy7mBfAVXpOc5vMYztssUI7mL2/iYujiIXM+weZYNTEpLdjyJdu7R5gGUu +g1jSVK/EPHfc74O7AyZU34PNIP4Sh33N+/A5YexrNgJlPY+E3GdVYi4ldWJjgkAd +Qah2PH5ACLrIIC6tRka9hcaBlIECAwEAAaMgMB4wDAYDVR0TBAUwAwEB/zAOBgNV +HQ8BAf8EBAMCAgQwDQYJKoZIhvcNAQELBQADgYEAHzC7jdYlzAVmddi/gdAeKPau +sPBG/C2HCWqHzpCUHcKuvMzDVkY/MP2o6JIW2DBbY64bO/FceExhjcykgaYtCH/m +oIU63+CFOTtR7otyQAWHqXa7q4SbCDlG7DyRFxqG0txPtGvy12lgldA2+RgcigQG +Dfcog5wrJytaQ6UA0wE= +-----END CERTIFICATE----- diff --git a/testassets/InteropTestsClient/Certs/server1.key b/testassets/InteropTestsClient/Certs/server1.key new file mode 100644 index 000000000..143a5b876 --- /dev/null +++ b/testassets/InteropTestsClient/Certs/server1.key @@ -0,0 +1,16 @@ +-----BEGIN PRIVATE KEY----- +MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAOHDFScoLCVJpYDD +M4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlumN+fm+AjPEK5GHhGn1BgzkWF+slf +3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wHNVX77fBZOgp9VlSMVfyd9N8YwbBY +AckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAECgYAn7qGnM2vbjJNBm0VZCkOkTIWm +V10okw7EPJrdL2mkre9NasghNXbE1y5zDshx5Nt3KsazKOxTT8d0Jwh/3KbaN+YY +tTCbKGW0pXDRBhwUHRcuRzScjli8Rih5UOCiZkhefUTcRb6xIhZJuQy71tjaSy0p +dHZRmYyBYO2YEQ8xoQJBAPrJPhMBkzmEYFtyIEqAxQ/o/A6E+E4w8i+KM7nQCK7q +K4JXzyXVAjLfyBZWHGM2uro/fjqPggGD6QH1qXCkI4MCQQDmdKeb2TrKRh5BY1LR +81aJGKcJ2XbcDu6wMZK4oqWbTX2KiYn9GB0woM6nSr/Y6iy1u145YzYxEV/iMwff +DJULAkB8B2MnyzOg0pNFJqBJuH29bKCcHa8gHJzqXhNO5lAlEbMK95p/P2Wi+4Hd +aiEIAF1BF326QJcvYKmwSmrORp85AkAlSNxRJ50OWrfMZnBgzVjDx3xG6KsFQVk2 +ol6VhqL6dFgKUORFUWBvnKSyhjJxurlPEahV6oo6+A+mPhFY8eUvAkAZQyTdupP3 +XEFQKctGz+9+gKkemDp7LBBMEMBXrGTLPhpEfcjv/7KPdnFHYmhYeBTBnuVmTVWe +F98XJ7tIFfJq +-----END PRIVATE KEY----- diff --git a/testassets/InteropTestsClient/Certs/server1.pem b/testassets/InteropTestsClient/Certs/server1.pem new file mode 100644 index 000000000..f3d43fcc5 --- /dev/null +++ b/testassets/InteropTestsClient/Certs/server1.pem @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICnDCCAgWgAwIBAgIBBzANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJBVTET +MBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQ +dHkgTHRkMQ8wDQYDVQQDEwZ0ZXN0Y2EwHhcNMTUxMTA0MDIyMDI0WhcNMjUxMTAx +MDIyMDI0WjBlMQswCQYDVQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNV +BAcTB0NoaWNhZ28xFTATBgNVBAoTDEV4YW1wbGUsIENvLjEaMBgGA1UEAxQRKi50 +ZXN0Lmdvb2dsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOHDFSco +LCVJpYDDM4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlumN+fm+AjPEK5GHhGn1Bg +zkWF+slf3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wHNVX77fBZOgp9VlSMVfyd +9N8YwbBYAckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAGjazBpMAkGA1UdEwQCMAAw +CwYDVR0PBAQDAgXgME8GA1UdEQRIMEaCECoudGVzdC5nb29nbGUuZnKCGHdhdGVy +em9vaS50ZXN0Lmdvb2dsZS5iZYISKi50ZXN0LnlvdXR1YmUuY29thwTAqAEDMA0G +CSqGSIb3DQEBCwUAA4GBAJFXVifQNub1LUP4JlnX5lXNlo8FxZ2a12AFQs+bzoJ6 +hM044EDjqyxUqSbVePK0ni3w1fHQB5rY9yYC5f8G7aqqTY1QOhoUk8ZTSTRpnkTh +y4jjdvTZeLDVBlueZUTDRmy2feY5aZIU18vFDK08dTG0A87pppuv1LNIR3loveU8 +-----END CERTIFICATE----- diff --git a/testassets/InteropTestsClient/Certs/server1.pfx b/testassets/InteropTestsClient/Certs/server1.pfx new file mode 100644 index 000000000..252acc126 Binary files /dev/null and b/testassets/InteropTestsClient/Certs/server1.pfx differ diff --git a/testassets/InteropTestsClient/IChannel.cs b/testassets/InteropTestsClient/IChannel.cs new file mode 100644 index 000000000..47a2c949b --- /dev/null +++ b/testassets/InteropTestsClient/IChannel.cs @@ -0,0 +1,61 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Grpc.Core; + +namespace InteropTestsClient +{ + public interface IChannel + { + Task ShutdownAsync(); + } + + public class HttpClientChannel : IChannel + { + public HttpClientHandler HttpClientHandler { get; } + + public HttpClientChannel(HttpClientHandler httpClient) + { + HttpClientHandler = httpClient; + } + + public Task ShutdownAsync() + { + HttpClientHandler.Dispose(); + return Task.CompletedTask; + } + } + + public class CoreChannel : IChannel + { + public Channel Channel { get; } + + public CoreChannel(Channel channel) + { + Channel = channel; + } + + public Task ShutdownAsync() + { + return Channel.ShutdownAsync(); + } + } +} diff --git a/testassets/InteropTestsClient/InteropClient.cs b/testassets/InteropTestsClient/InteropClient.cs new file mode 100644 index 000000000..2702f1960 --- /dev/null +++ b/testassets/InteropTestsClient/InteropClient.cs @@ -0,0 +1,772 @@ +#region Copyright notice and license + +// Copyright 2015-2016 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using CommandLine; +using Google.Apis.Auth.OAuth2; +using Google.Protobuf; +using Grpc.Auth; +using Grpc.Core; +using Grpc.Core.Logging; +using Grpc.Core.Utils; +using Grpc.NetCore.HttpClient; +using Grpc.Testing; +using Newtonsoft.Json.Linq; +using NUnit.Framework; + +namespace InteropTestsClient +{ + public class InteropClient + { + internal const string CompressionRequestAlgorithmMetadataKey = "grpc-internal-encoding-request"; + + private class ClientOptions + { + [Option("client_type" +#if DEBUG + , Default = "httpclient" +#endif + )] + public string ClientType { get; set; } + + [Option("server_host", Default = "localhost")] + public string ServerHost { get; set; } + + [Option("server_host_override")] + public string ServerHostOverride { get; set; } + + [Option("server_port" +#if DEBUG + , Default = 50052 +#endif + )] + public int ServerPort { get; set; } + + [Option("test_case" +#if DEBUG + , Default = "large_unary" +#endif + )] + public string TestCase { get; set; } + + // Deliberately using nullable bool type to allow --use_tls=true syntax (as opposed to --use_tls) + [Option("use_tls", Default = false)] + public bool? UseTls { get; set; } + + // Deliberately using nullable bool type to allow --use_test_ca=true syntax (as opposed to --use_test_ca) + [Option("use_test_ca", Default = false)] + public bool? UseTestCa { get; set; } + + [Option("default_service_account", Required = false)] + public string DefaultServiceAccount { get; set; } + + [Option("oauth_scope", Required = false)] + public string OAuthScope { get; set; } + + [Option("service_account_key_file", Required = false)] + public string ServiceAccountKeyFile { get; set; } + } + + ClientOptions options; + + private InteropClient(ClientOptions options) + { + this.options = options; + } + + public static void Run(string[] args) + { + GrpcEnvironment.SetLogger(new ConsoleLogger()); + var parserResult = Parser.Default.ParseArguments(args) + .WithNotParsed(errors => Environment.Exit(1)) + .WithParsed(options => + { + Console.WriteLine("Use TLS: " + options.UseTls); + Console.WriteLine("Server host: " + options.ServerHost); + Console.WriteLine("Server port: " + options.ServerPort); + + var interopClient = new InteropClient(options); + interopClient.Run().Wait(); + }); + } + + private async Task Run() + { + IChannel channel = IsHttpClient() ? await HttpClientCreateChannel() : await CoreCreateChannel(); + await RunTestCaseAsync(channel, options); + await channel.ShutdownAsync(); + } + + private Task HttpClientCreateChannel() + { + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2Support", true); + + var httpClientHandler = new HttpClientHandler(); + httpClientHandler.ServerCertificateCustomValidationCallback = (httpRequestMessage, cert, cetChain, policyErrors) => true; + + return Task.FromResult(new HttpClientChannel(httpClientHandler)); + } + + private async Task CoreCreateChannel() + { + var credentials = await CreateCredentialsAsync(); + + List channelOptions = null; + if (!string.IsNullOrEmpty(options.ServerHostOverride)) + { + channelOptions = new List + { + new ChannelOption(ChannelOptions.SslTargetNameOverride, options.ServerHostOverride) + }; + } + var channel = new Channel(options.ServerHost, options.ServerPort, credentials, channelOptions); + await channel.ConnectAsync(); + return new CoreChannel(channel); + } + + private bool IsHttpClient() => string.Equals(options.ClientType, "httpclient", StringComparison.OrdinalIgnoreCase); + + private async Task CreateCredentialsAsync() + { + var credentials = ChannelCredentials.Insecure; + if (options.UseTls.Value) + { + credentials = options.UseTestCa.Value ? TestCredentials.CreateSslCredentials() : new SslCredentials(); + } + + if (options.TestCase == "jwt_token_creds") + { + var googleCredential = await GoogleCredential.GetApplicationDefaultAsync(); + Assert.IsTrue(googleCredential.IsCreateScopedRequired); + credentials = ChannelCredentials.Create(credentials, googleCredential.ToCallCredentials()); + } + + if (options.TestCase == "compute_engine_creds") + { + var googleCredential = await GoogleCredential.GetApplicationDefaultAsync(); + Assert.IsFalse(googleCredential.IsCreateScopedRequired); + credentials = ChannelCredentials.Create(credentials, googleCredential.ToCallCredentials()); + } + return credentials; + } + + private TClient CreateClient(IChannel channel) where TClient : ClientBase + { + if (channel is CoreChannel coreChannel) + { + return (TClient)Activator.CreateInstance(typeof(TClient), coreChannel.Channel); + } + else if (channel is HttpClientChannel httpClientChannel) + { + return GrpcClientFactory.Create($"http://{options.ServerHost}:{options.ServerPort}", certificate: null); + } + else + { + throw new Exception("Unexpected channel type."); + } + } + + private async Task RunTestCaseAsync(IChannel channel, ClientOptions options) + { + var client = CreateClient(channel); + switch (options.TestCase) + { + case "empty_unary": + RunEmptyUnary(client); + break; + case "large_unary": + RunLargeUnary(client); + break; + case "client_streaming": + await RunClientStreamingAsync(client); + break; + case "server_streaming": + await RunServerStreamingAsync(client); + break; + case "ping_pong": + await RunPingPongAsync(client); + break; + case "empty_stream": + await RunEmptyStreamAsync(client); + break; + case "compute_engine_creds": + RunComputeEngineCreds(client, options.DefaultServiceAccount, options.OAuthScope); + break; + case "jwt_token_creds": + RunJwtTokenCreds(client); + break; + case "oauth2_auth_token": + await RunOAuth2AuthTokenAsync(client, options.OAuthScope); + break; + case "per_rpc_creds": + await RunPerRpcCredsAsync(client, options.OAuthScope); + break; + case "cancel_after_begin": + await RunCancelAfterBeginAsync(client); + break; + case "cancel_after_first_response": + await RunCancelAfterFirstResponseAsync(client); + break; + case "timeout_on_sleeping_server": + await RunTimeoutOnSleepingServerAsync(client); + break; + case "custom_metadata": + await RunCustomMetadataAsync(client); + break; + case "status_code_and_message": + await RunStatusCodeAndMessageAsync(client); + break; + case "unimplemented_service": + RunUnimplementedService(CreateClient(channel)); + break; + case "unimplemented_method": + RunUnimplementedMethod(client); + break; + case "client_compressed_unary": + RunClientCompressedUnary(client); + break; + case "client_compressed_streaming": + await RunClientCompressedStreamingAsync(client); + break; + default: + throw new ArgumentException("Unknown test case " + options.TestCase); + } + } + + public static void RunEmptyUnary(TestService.TestServiceClient client) + { + Console.WriteLine("running empty_unary"); + var response = client.EmptyCall(new Empty()); + Assert.IsNotNull(response); + Console.WriteLine("Passed!"); + } + + public static void RunLargeUnary(TestService.TestServiceClient client) + { + Console.WriteLine("running large_unary"); + var request = new SimpleRequest + { + ResponseSize = 314159, + Payload = CreateZerosPayload(271828) + }; + var response = client.UnaryCall(request); + + Assert.AreEqual(314159, response.Payload.Body.Length); + Console.WriteLine("Passed!"); + } + + public static async Task RunClientStreamingAsync(TestService.TestServiceClient client) + { + Console.WriteLine("running client_streaming"); + + var bodySizes = new List { 27182, 8, 1828, 45904 }.Select((size) => new StreamingInputCallRequest { Payload = CreateZerosPayload(size) }); + + using (var call = client.StreamingInputCall()) + { + await call.RequestStream.WriteAllAsync(bodySizes); + + var response = await call.ResponseAsync; + Assert.AreEqual(74922, response.AggregatedPayloadSize); + } + Console.WriteLine("Passed!"); + } + + public static async Task RunServerStreamingAsync(TestService.TestServiceClient client) + { + Console.WriteLine("running server_streaming"); + + var bodySizes = new List { 31415, 9, 2653, 58979 }; + + var request = new StreamingOutputCallRequest + { + ResponseParameters = { bodySizes.Select((size) => new ResponseParameters { Size = size }) } + }; + + using (var call = client.StreamingOutputCall(request)) + { + var responseList = await call.ResponseStream.ToListAsync(); + CollectionAssert.AreEqual(bodySizes, responseList.Select((item) => item.Payload.Body.Length)); + } + Console.WriteLine("Passed!"); + } + + public static async Task RunPingPongAsync(TestService.TestServiceClient client) + { + Console.WriteLine("running ping_pong"); + + using (var call = client.FullDuplexCall()) + { + await call.RequestStream.WriteAsync(new StreamingOutputCallRequest + { + ResponseParameters = { new ResponseParameters { Size = 31415 } }, + Payload = CreateZerosPayload(27182) + }); + + Assert.IsTrue(await call.ResponseStream.MoveNext()); + Assert.AreEqual(31415, call.ResponseStream.Current.Payload.Body.Length); + + await call.RequestStream.WriteAsync(new StreamingOutputCallRequest + { + ResponseParameters = { new ResponseParameters { Size = 9 } }, + Payload = CreateZerosPayload(8) + }); + + Assert.IsTrue(await call.ResponseStream.MoveNext()); + Assert.AreEqual(9, call.ResponseStream.Current.Payload.Body.Length); + + await call.RequestStream.WriteAsync(new StreamingOutputCallRequest + { + ResponseParameters = { new ResponseParameters { Size = 2653 } }, + Payload = CreateZerosPayload(1828) + }); + + Assert.IsTrue(await call.ResponseStream.MoveNext()); + Assert.AreEqual(2653, call.ResponseStream.Current.Payload.Body.Length); + + await call.RequestStream.WriteAsync(new StreamingOutputCallRequest + { + ResponseParameters = { new ResponseParameters { Size = 58979 } }, + Payload = CreateZerosPayload(45904) + }); + + Assert.IsTrue(await call.ResponseStream.MoveNext()); + Assert.AreEqual(58979, call.ResponseStream.Current.Payload.Body.Length); + + await call.RequestStream.CompleteAsync(); + + Assert.IsFalse(await call.ResponseStream.MoveNext()); + } + Console.WriteLine("Passed!"); + } + + public static async Task RunEmptyStreamAsync(TestService.TestServiceClient client) + { + Console.WriteLine("running empty_stream"); + using (var call = client.FullDuplexCall()) + { + await call.RequestStream.CompleteAsync(); + + var responseList = await call.ResponseStream.ToListAsync(); + Assert.AreEqual(0, responseList.Count); + } + Console.WriteLine("Passed!"); + } + + public static void RunComputeEngineCreds(TestService.TestServiceClient client, string defaultServiceAccount, string oauthScope) + { + Console.WriteLine("running compute_engine_creds"); + + var request = new SimpleRequest + { + ResponseSize = 314159, + Payload = CreateZerosPayload(271828), + FillUsername = true, + FillOauthScope = true + }; + + // not setting credentials here because they were set on channel already + var response = client.UnaryCall(request); + + Assert.AreEqual(314159, response.Payload.Body.Length); + Assert.False(string.IsNullOrEmpty(response.OauthScope)); + Assert.True(oauthScope.Contains(response.OauthScope)); + Assert.AreEqual(defaultServiceAccount, response.Username); + Console.WriteLine("Passed!"); + } + + public static void RunJwtTokenCreds(TestService.TestServiceClient client) + { + Console.WriteLine("running jwt_token_creds"); + + var request = new SimpleRequest + { + ResponseSize = 314159, + Payload = CreateZerosPayload(271828), + FillUsername = true, + }; + + // not setting credentials here because they were set on channel already + var response = client.UnaryCall(request); + + Assert.AreEqual(314159, response.Payload.Body.Length); + Assert.AreEqual(GetEmailFromServiceAccountFile(), response.Username); + Console.WriteLine("Passed!"); + } + + public static async Task RunOAuth2AuthTokenAsync(TestService.TestServiceClient client, string oauthScope) + { + Console.WriteLine("running oauth2_auth_token"); + ITokenAccess credential = (await GoogleCredential.GetApplicationDefaultAsync()).CreateScoped(new[] { oauthScope }); + string oauth2Token = await credential.GetAccessTokenForRequestAsync(); + + var credentials = GoogleGrpcCredentials.FromAccessToken(oauth2Token); + var request = new SimpleRequest + { + FillUsername = true, + FillOauthScope = true + }; + + var response = client.UnaryCall(request, new CallOptions(credentials: credentials)); + + Assert.False(string.IsNullOrEmpty(response.OauthScope)); + Assert.True(oauthScope.Contains(response.OauthScope)); + Assert.AreEqual(GetEmailFromServiceAccountFile(), response.Username); + Console.WriteLine("Passed!"); + } + + public static async Task RunPerRpcCredsAsync(TestService.TestServiceClient client, string oauthScope) + { + Console.WriteLine("running per_rpc_creds"); + ITokenAccess googleCredential = await GoogleCredential.GetApplicationDefaultAsync(); + + var credentials = googleCredential.ToCallCredentials(); + var request = new SimpleRequest + { + FillUsername = true, + }; + + var response = client.UnaryCall(request, new CallOptions(credentials: credentials)); + + Assert.AreEqual(GetEmailFromServiceAccountFile(), response.Username); + Console.WriteLine("Passed!"); + } + + public static async Task RunCancelAfterBeginAsync(TestService.TestServiceClient client) + { + Console.WriteLine("running cancel_after_begin"); + + var cts = new CancellationTokenSource(); + using (var call = client.StreamingInputCall(cancellationToken: cts.Token)) + { + // TODO(jtattermusch): we need this to ensure call has been initiated once we cancel it. + await Task.Delay(1000); + cts.Cancel(); + + var ex = Assert.ThrowsAsync(async () => await call.ResponseAsync); + Assert.AreEqual(StatusCode.Cancelled, ex.Status.StatusCode); + } + Console.WriteLine("Passed!"); + } + + public static async Task RunCancelAfterFirstResponseAsync(TestService.TestServiceClient client) + { + Console.WriteLine("running cancel_after_first_response"); + + var cts = new CancellationTokenSource(); + using (var call = client.FullDuplexCall(cancellationToken: cts.Token)) + { + await call.RequestStream.WriteAsync(new StreamingOutputCallRequest + { + ResponseParameters = { new ResponseParameters { Size = 31415 } }, + Payload = CreateZerosPayload(27182) + }); + + Assert.IsTrue(await call.ResponseStream.MoveNext()); + Assert.AreEqual(31415, call.ResponseStream.Current.Payload.Body.Length); + + cts.Cancel(); + + try + { + // cannot use Assert.ThrowsAsync because it uses Task.Wait and would deadlock. + await call.ResponseStream.MoveNext(); + Assert.Fail(); + } + catch (RpcException ex) + { + Assert.AreEqual(StatusCode.Cancelled, ex.Status.StatusCode); + } + } + Console.WriteLine("Passed!"); + } + + public static async Task RunTimeoutOnSleepingServerAsync(TestService.TestServiceClient client) + { + Console.WriteLine("running timeout_on_sleeping_server"); + + var deadline = DateTime.UtcNow.AddMilliseconds(1); + using (var call = client.FullDuplexCall(deadline: deadline)) + { + try + { + await call.RequestStream.WriteAsync(new StreamingOutputCallRequest { Payload = CreateZerosPayload(27182) }); + } + catch (InvalidOperationException) + { + // Deadline was reached before write has started. Eat the exception and continue. + } + catch (RpcException) + { + // Deadline was reached before write has started. Eat the exception and continue. + } + + try + { + await call.ResponseStream.MoveNext(); + Assert.Fail(); + } + catch (RpcException ex) + { + // We can't guarantee the status code always DeadlineExceeded. See issue #2685. + Assert.Contains(ex.Status.StatusCode, new[] { StatusCode.DeadlineExceeded, StatusCode.Internal }); + } + } + Console.WriteLine("Passed!"); + } + + public static async Task RunCustomMetadataAsync(TestService.TestServiceClient client) + { + Console.WriteLine("running custom_metadata"); + { + // step 1: test unary call + var request = new SimpleRequest + { + ResponseSize = 314159, + Payload = CreateZerosPayload(271828) + }; + + var call = client.UnaryCallAsync(request, headers: CreateTestMetadata()); + await call.ResponseAsync; + + var responseHeaders = await call.ResponseHeadersAsync; + var responseTrailers = call.GetTrailers(); + + Assert.AreEqual("test_initial_metadata_value", responseHeaders.First((entry) => entry.Key == "x-grpc-test-echo-initial").Value); + CollectionAssert.AreEqual(new byte[] { 0xab, 0xab, 0xab }, responseTrailers.First((entry) => entry.Key == "x-grpc-test-echo-trailing-bin").ValueBytes); + } + + { + // step 2: test full duplex call + var request = new StreamingOutputCallRequest + { + ResponseParameters = { new ResponseParameters { Size = 31415 } }, + Payload = CreateZerosPayload(27182) + }; + + var call = client.FullDuplexCall(headers: CreateTestMetadata()); + + await call.RequestStream.WriteAsync(request); + await call.RequestStream.CompleteAsync(); + await call.ResponseStream.ToListAsync(); + + var responseHeaders = await call.ResponseHeadersAsync; + var responseTrailers = call.GetTrailers(); + + Assert.AreEqual("test_initial_metadata_value", responseHeaders.First((entry) => entry.Key == "x-grpc-test-echo-initial").Value); + CollectionAssert.AreEqual(new byte[] { 0xab, 0xab, 0xab }, responseTrailers.First((entry) => entry.Key == "x-grpc-test-echo-trailing-bin").ValueBytes); + } + + Console.WriteLine("Passed!"); + } + + public static async Task RunStatusCodeAndMessageAsync(TestService.TestServiceClient client) + { + Console.WriteLine("running status_code_and_message"); + var echoStatus = new EchoStatus + { + Code = 2, + Message = "test status message" + }; + + { + // step 1: test unary call + var request = new SimpleRequest { ResponseStatus = echoStatus }; + + var e = Assert.Throws(() => client.UnaryCall(request)); + Assert.AreEqual(StatusCode.Unknown, e.Status.StatusCode); + Assert.AreEqual(echoStatus.Message, e.Status.Detail); + } + + { + // step 2: test full duplex call + var request = new StreamingOutputCallRequest { ResponseStatus = echoStatus }; + + var call = client.FullDuplexCall(); + await call.RequestStream.WriteAsync(request); + await call.RequestStream.CompleteAsync(); + + try + { + // cannot use Assert.ThrowsAsync because it uses Task.Wait and would deadlock. + await call.ResponseStream.ToListAsync(); + Assert.Fail(); + } + catch (RpcException e) + { + Assert.AreEqual(StatusCode.Unknown, e.Status.StatusCode); + Assert.AreEqual(echoStatus.Message, e.Status.Detail); + } + } + + Console.WriteLine("Passed!"); + } + + public static void RunUnimplementedService(UnimplementedService.UnimplementedServiceClient client) + { + Console.WriteLine("running unimplemented_service"); + var e = Assert.Throws(() => client.UnimplementedCall(new Empty())); + + Assert.AreEqual(StatusCode.Unimplemented, e.Status.StatusCode); + Console.WriteLine("Passed!"); + } + + public static void RunUnimplementedMethod(TestService.TestServiceClient client) + { + Console.WriteLine("running unimplemented_method"); + var e = Assert.Throws(() => client.UnimplementedCall(new Empty())); + + Assert.AreEqual(StatusCode.Unimplemented, e.Status.StatusCode); + Console.WriteLine("Passed!"); + } + + public static void RunClientCompressedUnary(TestService.TestServiceClient client) + { + Console.WriteLine("running client_compressed_unary"); + var probeRequest = new SimpleRequest + { + ExpectCompressed = new BoolValue + { + Value = true // lie about compression + }, + ResponseSize = 314159, + Payload = CreateZerosPayload(271828) + }; + var e = Assert.Throws(() => client.UnaryCall(probeRequest, CreateClientCompressionMetadata(false))); + Assert.AreEqual(StatusCode.InvalidArgument, e.Status.StatusCode); + + var compressedRequest = new SimpleRequest + { + ExpectCompressed = new BoolValue + { + Value = true + }, + ResponseSize = 314159, + Payload = CreateZerosPayload(271828) + }; + var response1 = client.UnaryCall(compressedRequest, CreateClientCompressionMetadata(true)); + Assert.AreEqual(314159, response1.Payload.Body.Length); + + var uncompressedRequest = new SimpleRequest + { + ExpectCompressed = new BoolValue + { + Value = false + }, + ResponseSize = 314159, + Payload = CreateZerosPayload(271828) + }; + var response2 = client.UnaryCall(uncompressedRequest, CreateClientCompressionMetadata(false)); + Assert.AreEqual(314159, response2.Payload.Body.Length); + + Console.WriteLine("Passed!"); + } + + public static async Task RunClientCompressedStreamingAsync(TestService.TestServiceClient client) + { + Console.WriteLine("running client_compressed_streaming"); + try + { + var probeCall = client.StreamingInputCall(CreateClientCompressionMetadata(false)); + await probeCall.RequestStream.WriteAsync(new StreamingInputCallRequest + { + ExpectCompressed = new BoolValue + { + Value = true + }, + Payload = CreateZerosPayload(27182) + }); + + // cannot use Assert.ThrowsAsync because it uses Task.Wait and would deadlock. + await probeCall; + Assert.Fail(); + } + catch (RpcException e) + { + Assert.AreEqual(StatusCode.InvalidArgument, e.Status.StatusCode); + } + + var call = client.StreamingInputCall(CreateClientCompressionMetadata(true)); + await call.RequestStream.WriteAsync(new StreamingInputCallRequest + { + ExpectCompressed = new BoolValue + { + Value = true + }, + Payload = CreateZerosPayload(27182) + }); + + call.RequestStream.WriteOptions = new WriteOptions(WriteFlags.NoCompress); + await call.RequestStream.WriteAsync(new StreamingInputCallRequest + { + ExpectCompressed = new BoolValue + { + Value = false + }, + Payload = CreateZerosPayload(45904) + }); + await call.RequestStream.CompleteAsync(); + + var response = await call.ResponseAsync; + Assert.AreEqual(73086, response.AggregatedPayloadSize); + + Console.WriteLine("Passed!"); + } + + private static Payload CreateZerosPayload(int size) + { + return new Payload { Body = ByteString.CopyFrom(new byte[size]) }; + } + + private static Metadata CreateClientCompressionMetadata(bool compressed) + { + var algorithmName = compressed ? "gzip" : "identity"; + return new Metadata + { + { new Metadata.Entry(CompressionRequestAlgorithmMetadataKey, algorithmName) } + }; + } + + // extracts the client_email field from service account file used for auth test cases + private static string GetEmailFromServiceAccountFile() + { + string keyFile = Environment.GetEnvironmentVariable("GOOGLE_APPLICATION_CREDENTIALS"); + Assert.IsNotNull(keyFile); + var jobject = JObject.Parse(File.ReadAllText(keyFile)); + string email = jobject.GetValue("client_email").Value(); + Assert.IsTrue(email.Length > 0); // spec requires nonempty client email. + return email; + } + + private static Metadata CreateTestMetadata() + { + return new Metadata + { + {"x-grpc-test-echo-initial", "test_initial_metadata_value"}, + {"x-grpc-test-echo-trailing-bin", new byte[] {0xab, 0xab, 0xab}} + }; + } + } +} \ No newline at end of file diff --git a/testassets/InteropTestsClient/InteropTestsClient.csproj b/testassets/InteropTestsClient/InteropTestsClient.csproj new file mode 100644 index 000000000..55d7f44be --- /dev/null +++ b/testassets/InteropTestsClient/InteropTestsClient.csproj @@ -0,0 +1,24 @@ + + + + Exe + netcoreapp3.0 + + + + + + + + + + + + + + + + + + + diff --git a/testassets/InteropTestsClient/Program.cs b/testassets/InteropTestsClient/Program.cs new file mode 100644 index 000000000..3e204d853 --- /dev/null +++ b/testassets/InteropTestsClient/Program.cs @@ -0,0 +1,28 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +namespace InteropTestsClient +{ + public class Program + { + public static void Main(string[] args) + { + InteropClient.Run(args); + } + } +} diff --git a/testassets/InteropTestsClient/RunTests.ps1 b/testassets/InteropTestsClient/RunTests.ps1 new file mode 100644 index 000000000..9ef6fd741 --- /dev/null +++ b/testassets/InteropTestsClient/RunTests.ps1 @@ -0,0 +1,35 @@ +$allTests = + "empty_unary", + "large_unary", + "client_streaming", + "server_streaming", + #"ping_pong", + "empty_stream", + + #"compute_engine_creds", + #"jwt_token_creds", + #"oauth2_auth_token", + #"per_rpc_creds", + + "cancel_after_begin", + #"cancel_after_first_response", + "timeout_on_sleeping_server", + "custom_metadata", + "status_code_and_message", + "unimplemented_service", + "unimplemented_method" + #, + #"client_compressed_unary", + #"client_compressed_streaming" + +Write-Host "Running $($allTests.Count) tests" -ForegroundColor Cyan +Write-Host + +foreach ($test in $allTests) +{ + Write-Host "Running $test" -ForegroundColor Cyan + dotnet run --use_tls false --server_port 50052 --client_type httpclient --test_case $test + Write-Host +} + +Write-Host "Done" -ForegroundColor Cyan \ No newline at end of file diff --git a/testassets/InteropTestsClient/TestCredentials.cs b/testassets/InteropTestsClient/TestCredentials.cs new file mode 100644 index 000000000..088fcd89b --- /dev/null +++ b/testassets/InteropTestsClient/TestCredentials.cs @@ -0,0 +1,75 @@ +#region Copyright notice and license + +// Copyright 2015 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System.IO; +using System.Reflection; +using Grpc.Core; + +namespace InteropTestsClient +{ + /// + /// SSL Credentials for testing. + /// + public static class TestCredentials + { + public const string DefaultHostOverride = "foo.test.google.fr"; + + public static string ClientCertAuthorityPath + { + get + { + return GetPath("data/ca.pem"); + } + } + + public static string ServerCertChainPath + { + get + { + return GetPath("data/server1.pem"); + } + } + + public static string ServerPrivateKeyPath + { + get + { + return GetPath("data/server1.key"); + } + } + + public static SslCredentials CreateSslCredentials() + { + return new SslCredentials(File.ReadAllText(ClientCertAuthorityPath)); + } + + public static SslServerCredentials CreateSslServerCredentials() + { + var keyCertPair = new KeyCertificatePair( + File.ReadAllText(ServerCertChainPath), + File.ReadAllText(ServerPrivateKeyPath)); + return new SslServerCredentials(new[] { keyCertPair }); + } + + private static string GetPath(string relativePath) + { + var assemblyDir = Path.GetDirectoryName(typeof(TestCredentials).GetTypeInfo().Assembly.Location); + return Path.Combine(assemblyDir, relativePath); + } + } +} diff --git a/testassets/InteropTestsWebsite/Program.cs b/testassets/InteropTestsWebsite/Program.cs index a53520ead..7c627785b 100644 --- a/testassets/InteropTestsWebsite/Program.cs +++ b/testassets/InteropTestsWebsite/Program.cs @@ -16,6 +16,7 @@ #endregion +using System; using System.Runtime.InteropServices; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; @@ -43,6 +44,8 @@ public static IWebHostBuilder CreateWebHostBuilder(string[] args) => options.Limits.MinRequestBodyDataRate = null; options.ListenAnyIP(port, listenOptions => { + Console.WriteLine($"Enabling connection encryption: {useTls}"); + if (useTls) { listenOptions.UseHttps(Resources.ServerPFXPath, "1111"); diff --git a/testassets/InteropTestsWebsite/TestServiceImpl.cs b/testassets/InteropTestsWebsite/TestServiceImpl.cs index d9c46cb93..cfc5ab706 100644 --- a/testassets/InteropTestsWebsite/TestServiceImpl.cs +++ b/testassets/InteropTestsWebsite/TestServiceImpl.cs @@ -39,6 +39,7 @@ public override async Task UnaryCall(SimpleRequest request, Serv { await EnsureEchoMetadataAsync(context); EnsureEchoStatus(request.ResponseStatus, context); + EnsureCompression(request.ExpectCompressed, context); var response = new SimpleResponse { Payload = CreateZerosPayload(request.ResponseSize) }; return response; @@ -63,6 +64,8 @@ public override async Task StreamingInputCall(IAsync int sum = 0; await requestStream.ForEachAsync(request => { + EnsureCompression(request.ExpectCompressed, context); + sum += request.Payload.Body.Length; return Task.CompletedTask; }); @@ -116,5 +119,20 @@ private static void EnsureEchoStatus(EchoStatus responseStatus, ServerCallContex context.Status = new Status(statusCode, responseStatus.Message); } } + + private static void EnsureCompression(BoolValue expectCompressed, ServerCallContext context) + { + if (expectCompressed != null) + { + string encoding = context.RequestHeaders.SingleOrDefault(h => h.Key == "grpc-encoding")?.Value; + if (expectCompressed.Value) + { + if (encoding == null || encoding == "identity") + { + throw new RpcException(new Status(StatusCode.InvalidArgument, string.Empty)); + } + } + } + } } } diff --git a/testassets/Proto/empty.proto b/testassets/Proto/empty.proto new file mode 100644 index 000000000..6a0aa88df --- /dev/null +++ b/testassets/Proto/empty.proto @@ -0,0 +1,28 @@ + +// Copyright 2015 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package grpc.testing; + +// An empty message that you can re-use to avoid defining duplicated empty +// messages in your project. A typical example is to use it as argument or the +// return value of a service API. For instance: +// +// service Foo { +// rpc Bar (grpc.testing.Empty) returns (grpc.testing.Empty) { }; +// }; +// +message Empty {} diff --git a/testassets/Proto/messages.proto b/testassets/Proto/messages.proto new file mode 100644 index 000000000..7b1b7286d --- /dev/null +++ b/testassets/Proto/messages.proto @@ -0,0 +1,165 @@ + +// Copyright 2015-2016 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Message definitions to be used by integration test service definitions. + +syntax = "proto3"; + +package grpc.testing; + +// TODO(dgq): Go back to using well-known types once +// https://github.com/grpc/grpc/issues/6980 has been fixed. +// import "google/protobuf/wrappers.proto"; +message BoolValue { + // The bool value. + bool value = 1; +} + +// The type of payload that should be returned. +enum PayloadType { + // Compressable text format. + COMPRESSABLE = 0; +} + +// A block of data, to simply increase gRPC message size. +message Payload { + // The type of data in body. + PayloadType type = 1; + // Primary contents of payload. + bytes body = 2; +} + +// A protobuf representation for grpc status. This is used by test +// clients to specify a status that the server should attempt to return. +message EchoStatus { + int32 code = 1; + string message = 2; +} + +// Unary request. +message SimpleRequest { + // Desired payload type in the response from the server. + // If response_type is RANDOM, server randomly chooses one from other formats. + PayloadType response_type = 1; + + // Desired payload size in the response from the server. + int32 response_size = 2; + + // Optional input payload sent along with the request. + Payload payload = 3; + + // Whether SimpleResponse should include username. + bool fill_username = 4; + + // Whether SimpleResponse should include OAuth scope. + bool fill_oauth_scope = 5; + + // Whether to request the server to compress the response. This field is + // "nullable" in order to interoperate seamlessly with clients not able to + // implement the full compression tests by introspecting the call to verify + // the response's compression status. + BoolValue response_compressed = 6; + + // Whether server should return a given status + EchoStatus response_status = 7; + + // Whether the server should expect this request to be compressed. + BoolValue expect_compressed = 8; +} + +// Unary response, as configured by the request. +message SimpleResponse { + // Payload to increase message size. + Payload payload = 1; + // The user the request came from, for verifying authentication was + // successful when the client expected it. + string username = 2; + // OAuth scope. + string oauth_scope = 3; +} + +// Client-streaming request. +message StreamingInputCallRequest { + // Optional input payload sent along with the request. + Payload payload = 1; + + // Whether the server should expect this request to be compressed. This field + // is "nullable" in order to interoperate seamlessly with servers not able to + // implement the full compression tests by introspecting the call to verify + // the request's compression status. + BoolValue expect_compressed = 2; + + // Not expecting any payload from the response. +} + +// Client-streaming response. +message StreamingInputCallResponse { + // Aggregated size of payloads received from the client. + int32 aggregated_payload_size = 1; +} + +// Configuration for a particular response. +message ResponseParameters { + // Desired payload sizes in responses from the server. + int32 size = 1; + + // Desired interval between consecutive responses in the response stream in + // microseconds. + int32 interval_us = 2; + + // Whether to request the server to compress the response. This field is + // "nullable" in order to interoperate seamlessly with clients not able to + // implement the full compression tests by introspecting the call to verify + // the response's compression status. + BoolValue compressed = 3; +} + +// Server-streaming request. +message StreamingOutputCallRequest { + // Desired payload type in the response from the server. + // If response_type is RANDOM, the payload from each response in the stream + // might be of different types. This is to simulate a mixed type of payload + // stream. + PayloadType response_type = 1; + + // Configuration for each expected response message. + repeated ResponseParameters response_parameters = 2; + + // Optional input payload sent along with the request. + Payload payload = 3; + + // Whether server should return a given status + EchoStatus response_status = 7; +} + +// Server-streaming response, as configured by the request and parameters. +message StreamingOutputCallResponse { + // Payload to increase response size. + Payload payload = 1; +} + +// For reconnect interop test only. +// Client tells server what reconnection parameters it used. +message ReconnectParams { + int32 max_reconnect_backoff_ms = 1; +} + +// For reconnect interop test only. +// Server tells client whether its reconnects are following the spec and the +// reconnect backoffs it saw. +message ReconnectInfo { + bool passed = 1; + repeated int32 backoff_ms = 2; +} diff --git a/testassets/Proto/test.proto b/testassets/Proto/test.proto new file mode 100644 index 000000000..86d6ab605 --- /dev/null +++ b/testassets/Proto/test.proto @@ -0,0 +1,79 @@ + +// Copyright 2015-2016 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// An integration test service that covers all the method signature permutations +// of unary/streaming requests/responses. + +syntax = "proto3"; + +import "empty.proto"; +import "messages.proto"; + +package grpc.testing; + +// A simple service to test the various types of RPCs and experiment with +// performance with various types of payload. +service TestService { + // One empty request followed by one empty response. + rpc EmptyCall(grpc.testing.Empty) returns (grpc.testing.Empty); + + // One request followed by one response. + rpc UnaryCall(SimpleRequest) returns (SimpleResponse); + + // One request followed by one response. Response has cache control + // headers set such that a caching HTTP proxy (such as GFE) can + // satisfy subsequent requests. + rpc CacheableUnaryCall(SimpleRequest) returns (SimpleResponse); + + // One request followed by a sequence of responses (streamed download). + // The server returns the payload with client desired type and sizes. + rpc StreamingOutputCall(StreamingOutputCallRequest) + returns (stream StreamingOutputCallResponse); + + // A sequence of requests followed by one response (streamed upload). + // The server returns the aggregated size of client payload as the result. + rpc StreamingInputCall(stream StreamingInputCallRequest) + returns (StreamingInputCallResponse); + + // A sequence of requests with each request served by the server immediately. + // As one request could lead to multiple responses, this interface + // demonstrates the idea of full duplexing. + rpc FullDuplexCall(stream StreamingOutputCallRequest) + returns (stream StreamingOutputCallResponse); + + // A sequence of requests followed by a sequence of responses. + // The server buffers all the client requests and then serves them in order. A + // stream of responses are returned to the client when the server starts with + // first request. + rpc HalfDuplexCall(stream StreamingOutputCallRequest) + returns (stream StreamingOutputCallResponse); + + // The test server will not implement this method. It will be used + // to test the behavior when clients call unimplemented methods. + rpc UnimplementedCall(grpc.testing.Empty) returns (grpc.testing.Empty); +} + +// A simple service NOT implemented at servers so clients can test for +// that case. +service UnimplementedService { + // A call that no server should implement + rpc UnimplementedCall(grpc.testing.Empty) returns (grpc.testing.Empty); +} + +// A service used to control reconnect server. +service ReconnectService { + rpc Start(grpc.testing.ReconnectParams) returns (grpc.testing.Empty); + rpc Stop(grpc.testing.Empty) returns (grpc.testing.ReconnectInfo); +}