diff --git a/src/WebJobs.Script.Grpc/azure-functions-language-worker-protobuf/README.md b/src/WebJobs.Script.Grpc/azure-functions-language-worker-protobuf/README.md index a490b9603f..acc87ce7fe 100644 --- a/src/WebJobs.Script.Grpc/azure-functions-language-worker-protobuf/README.md +++ b/src/WebJobs.Script.Grpc/azure-functions-language-worker-protobuf/README.md @@ -38,7 +38,7 @@ From within the Azure Functions language worker repo: ## CSharp ``` -set NUGET_PATH=%UserProfile%\.nuget\packages +set NUGET_PATH="%UserProfile%\.nuget\packages" set GRPC_TOOLS_PATH=%NUGET_PATH%\grpc.tools\\tools\windows_x86 set PROTO_PATH=.\azure-functions-language-worker-protobuf\src\proto set PROTO=.\azure-functions-language-worker-protobuf\src\proto\FunctionRpc.proto diff --git a/src/WebJobs.Script.Grpc/azure-functions-language-worker-protobuf/src/proto/FunctionRpc.proto b/src/WebJobs.Script.Grpc/azure-functions-language-worker-protobuf/src/proto/FunctionRpc.proto index 58dd7eb7f5..fbb585b235 100644 --- a/src/WebJobs.Script.Grpc/azure-functions-language-worker-protobuf/src/proto/FunctionRpc.proto +++ b/src/WebJobs.Script.Grpc/azure-functions-language-worker-protobuf/src/proto/FunctionRpc.proto @@ -11,6 +11,7 @@ package AzureFunctionsRpcMessages; import "google/protobuf/duration.proto"; import "identity/ClaimsIdentityRpc.proto"; +import "shared/NullableTypes.proto"; // Interface exported by the server. service FunctionRpc { @@ -375,6 +376,44 @@ message RpcException { string message = 2; } +// Http cookie type. Note that only name and value are used for Http requests +message RpcHttpCookie { + // Enum that lets servers require that a cookie shouoldn't be sent with cross-site requests + enum SameSite { + None = 0; + Lax = 1; + Strict = 2; + } + + // Cookie name + string name = 1; + + // Cookie value + string value = 2; + + // Specifies allowed hosts to receive the cookie + NullableString domain = 3; + + // Specifies URL path that must exist in the requested URL + NullableString path = 4; + + // Sets the cookie to expire at a specific date instead of when the client closes. + // It is generally recommended that you use "Max-Age" over "Expires". + NullableTimestamp expires = 5; + + // Sets the cookie to only be sent with an encrypted request + NullableBool secure = 6; + + // Sets the cookie to be inaccessible to JavaScript's Document.cookie API + NullableBool http_only = 7; + + // Allows servers to assert that a cookie ought not to be sent along with cross-site requests + SameSite same_site = 8; + + // Number of seconds until the cookie expires. A zero or negative number will expire the cookie immediately. + NullableDouble max_age = 9; +} + // TODO - solidify this or remove it message RpcHttp { string method = 1; @@ -387,4 +426,5 @@ message RpcHttp { bool enable_content_negotiation= 16; TypedData rawBody = 17; repeated RpcClaimsIdentity identities = 18; + repeated RpcHttpCookie cookies = 19; } diff --git a/src/WebJobs.Script.Grpc/azure-functions-language-worker-protobuf/src/proto/identity/ClaimsIdentityRpc.proto b/src/WebJobs.Script.Grpc/azure-functions-language-worker-protobuf/src/proto/identity/ClaimsIdentityRpc.proto index 01af545667..b9219615eb 100644 --- a/src/WebJobs.Script.Grpc/azure-functions-language-worker-protobuf/src/proto/identity/ClaimsIdentityRpc.proto +++ b/src/WebJobs.Script.Grpc/azure-functions-language-worker-protobuf/src/proto/identity/ClaimsIdentityRpc.proto @@ -1,7 +1,7 @@ syntax = "proto3"; // protobuf vscode extension: https://marketplace.visualstudio.com/items?itemName=zxh404.vscode-proto3 -import "shared/NullableString.proto"; +import "shared/NullableTypes.proto"; // Light-weight representation of a .NET System.Security.Claims.ClaimsIdentity object. // This is the same serialization as found in EasyAuth, and needs to be kept in sync with diff --git a/src/WebJobs.Script.Grpc/azure-functions-language-worker-protobuf/src/proto/shared/NullableString.proto b/src/WebJobs.Script.Grpc/azure-functions-language-worker-protobuf/src/proto/shared/NullableString.proto deleted file mode 100644 index 7fd81b4dd7..0000000000 --- a/src/WebJobs.Script.Grpc/azure-functions-language-worker-protobuf/src/proto/shared/NullableString.proto +++ /dev/null @@ -1,8 +0,0 @@ -syntax = "proto3"; -// protobuf vscode extension: https://marketplace.visualstudio.com/items?itemName=zxh404.vscode-proto3 - -message NullableString { - oneof string { - string value = 1; - } -} diff --git a/src/WebJobs.Script.Grpc/azure-functions-language-worker-protobuf/src/proto/shared/NullableTypes.proto b/src/WebJobs.Script.Grpc/azure-functions-language-worker-protobuf/src/proto/shared/NullableTypes.proto new file mode 100644 index 0000000000..51786101a5 --- /dev/null +++ b/src/WebJobs.Script.Grpc/azure-functions-language-worker-protobuf/src/proto/shared/NullableTypes.proto @@ -0,0 +1,28 @@ +syntax = "proto3"; +// protobuf vscode extension: https://marketplace.visualstudio.com/items?itemName=zxh404.vscode-proto3 + +import "google/protobuf/timestamp.proto"; + +message NullableString { + oneof string { + string value = 1; + } +} + +message NullableDouble { + oneof double { + double value = 1; + } +} + +message NullableBool { + oneof bool { + bool value = 1; + } +} + +message NullableTimestamp { + oneof timestamp { + google.protobuf.Timestamp value = 1; + } +} diff --git a/src/WebJobs.Script/Binding/Http/HttpBinding.cs b/src/WebJobs.Script/Binding/Http/HttpBinding.cs index 5ba560919b..dfc5ebc119 100644 --- a/src/WebJobs.Script/Binding/Http/HttpBinding.cs +++ b/src/WebJobs.Script/Binding/Http/HttpBinding.cs @@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.WebApiCompatShim; using Microsoft.Azure.WebJobs.Script.Description; +using Microsoft.Azure.WebJobs.Script.Rpc; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -83,17 +84,19 @@ internal static IActionResult CreateResult(HttpRequest request, object content) int statusCode = StatusCodes.Status200OK; IDictionary responseHeaders = null; bool enableContentNegotiation = false; + List> cookies = new List>(); if (responseObject != null) { - ParseResponseObject(responseObject, ref content, out responseHeaders, out statusCode, out enableContentNegotiation); + ParseResponseObject(responseObject, ref content, out responseHeaders, out statusCode, out cookies, out enableContentNegotiation); } - return CreateResult(request, statusCode, content, responseHeaders, enableContentNegotiation); + return CreateResult(request, statusCode, content, responseHeaders, cookies, enableContentNegotiation); } - internal static void ParseResponseObject(IDictionary responseObject, ref object content, out IDictionary headers, out int statusCode, out bool enableContentNegotiation) + internal static void ParseResponseObject(IDictionary responseObject, ref object content, out IDictionary headers, out int statusCode, out List> cookies, out bool enableContentNegotiation) { headers = null; + cookies = null; statusCode = StatusCodes.Status200OK; enableContentNegotiation = false; @@ -101,12 +104,12 @@ internal static void ParseResponseObject(IDictionary responseObj // Sniff the object to see if it looks like a response object // by convention object bodyValue = null; - if (responseObject.TryGetValue("body", out bodyValue, ignoreCase: true)) + if (responseObject.TryGetValue(LanguageWorkerConstants.RpcHttpBody, out bodyValue, ignoreCase: true)) { // the response content becomes the specified body value content = bodyValue; - if (responseObject.TryGetValue("headers", out IDictionary headersValue, ignoreCase: true)) + if (responseObject.TryGetValue(LanguageWorkerConstants.RpcHttpHeaders, out IDictionary headersValue, ignoreCase: true)) { headers = headersValue; } @@ -116,10 +119,15 @@ internal static void ParseResponseObject(IDictionary responseObj statusCode = responseStatusCode.Value; } - if (responseObject.TryGetValue("enableContentNegotiation", out bool enableContentNegotiationValue, ignoreCase: true)) + if (responseObject.TryGetValue(LanguageWorkerConstants.RpcHttpEnableContentNegotiation, out bool enableContentNegotiationValue, ignoreCase: true)) { enableContentNegotiation = enableContentNegotiationValue; } + + if (responseObject.TryGetValue(LanguageWorkerConstants.RpcHttpCookies, out List> cookiesValue, ignoreCase: true)) + { + cookies = cookiesValue; + } } } @@ -127,8 +135,8 @@ internal static bool TryParseStatusCode(IDictionary responseObje { statusCode = StatusCodes.Status200OK; - if (!responseObject.TryGetValue("statusCode", out object statusValue, ignoreCase: true) && - !responseObject.TryGetValue("status", out statusValue, ignoreCase: true)) + if (!responseObject.TryGetValue(LanguageWorkerConstants.RpcHttpStatusCode, out object statusValue, ignoreCase: true) && + !responseObject.TryGetValue(LanguageWorkerConstants.RpcHttpStatus, out statusValue, ignoreCase: true)) { return false; } @@ -161,7 +169,7 @@ statusValue is long || return false; } - private static IActionResult CreateResult(HttpRequest request, int statusCode, object content, IDictionary headers, bool enableContentNegotiation) + private static IActionResult CreateResult(HttpRequest request, int statusCode, object content, IDictionary headers, List> cookies, bool enableContentNegotiation) { if (enableContentNegotiation) { @@ -171,7 +179,11 @@ private static IActionResult CreateResult(HttpRequest request, int statusCode, o } else { - return new RawScriptResult(statusCode, content) { Headers = headers }; + return new RawScriptResult(statusCode, content) + { + Headers = headers, + Cookies = cookies + }; } } diff --git a/src/WebJobs.Script/Binding/Http/RawScriptResult.cs b/src/WebJobs.Script/Binding/Http/RawScriptResult.cs index 77443378c4..762341bb22 100644 --- a/src/WebJobs.Script/Binding/Http/RawScriptResult.cs +++ b/src/WebJobs.Script/Binding/Http/RawScriptResult.cs @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.WebUtilities; using Microsoft.Azure.WebJobs.Script.WebHost.Formatters; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace Microsoft.Azure.WebJobs.Script.Binding { @@ -36,6 +37,8 @@ public RawScriptResult(int? statusCode, object content) public IDictionary Headers { get; set; } + public List> Cookies { get; set; } + public async Task ExecuteResultAsync(ActionContext context) { HttpResponse response = context.HttpContext.Response; @@ -73,6 +76,22 @@ public async Task ExecuteResultAsync(ActionContext context) response.StatusCode = StatusCode.Value; } + if (Cookies != null) + { + foreach (var cookie in Cookies) + { + // Item3 (CookieOptions) should not be null, but this will behave correctly if it is + if (cookie.Item3 != null) + { + response.Cookies.Append(cookie.Item1, cookie.Item2, cookie.Item3); + } + else + { + response.Cookies.Append(cookie.Item1, cookie.Item2); + } + } + } + await WriteResponseBodyAsync(response, Content); } diff --git a/src/WebJobs.Script/Rpc/LanguageWorkerChannel.cs b/src/WebJobs.Script/Rpc/LanguageWorkerChannel.cs index 32a04ef8ef..2483400464 100644 --- a/src/WebJobs.Script/Rpc/LanguageWorkerChannel.cs +++ b/src/WebJobs.Script/Rpc/LanguageWorkerChannel.cs @@ -469,15 +469,22 @@ internal void InvokeResponse(InvocationResponse invokeResponse) if (_executingInvocations.TryRemove(invokeResponse.InvocationId, out ScriptInvocationContext context) && invokeResponse.Result.IsSuccess(context.ResultSource)) { - IDictionary bindingsDictionary = invokeResponse.OutputData - .ToDictionary(binding => binding.Name, binding => binding.Data.ToObject()); + try + { + IDictionary bindingsDictionary = invokeResponse.OutputData + .ToDictionary(binding => binding.Name, binding => binding.Data.ToObject()); - var result = new ScriptInvocationResult() + var result = new ScriptInvocationResult() + { + Outputs = bindingsDictionary, + Return = invokeResponse?.ReturnValue?.ToObject() + }; + context.ResultSource.SetResult(result); + } + catch (Exception responseEx) { - Outputs = bindingsDictionary, - Return = invokeResponse?.ReturnValue?.ToObject() - }; - context.ResultSource.SetResult(result); + context.ResultSource.TrySetException(responseEx); + } } } diff --git a/src/WebJobs.Script/Rpc/LanguageWorkerConstants.cs b/src/WebJobs.Script/Rpc/LanguageWorkerConstants.cs index 45a34c5533..13691ca3bc 100644 --- a/src/WebJobs.Script/Rpc/LanguageWorkerConstants.cs +++ b/src/WebJobs.Script/Rpc/LanguageWorkerConstants.cs @@ -35,8 +35,16 @@ public static class LanguageWorkerConstants public const int DefaultMaxMessageLengthBytes = 128 * 1024 * 1024; - //Logs + // Logs public const string LanguageWorkerConsoleLogPrefix = "LanguageWorkerConsoleLog"; public const string FunctionConsoleLogCategoryName = "Host.Function.Console"; + + // Rpc Http Constants + public const string RpcHttpBody = "body"; + public const string RpcHttpHeaders = "headers"; + public const string RpcHttpEnableContentNegotiation = "enableContentNegotiation"; + public const string RpcHttpCookies = "cookies"; + public const string RpcHttpStatusCode = "statusCode"; + public const string RpcHttpStatus = "status"; } } diff --git a/src/WebJobs.Script/Rpc/MessageExtensions/Utilities.cs b/src/WebJobs.Script/Rpc/MessageExtensions/Utilities.cs index 01ae5f35a0..8de1596216 100644 --- a/src/WebJobs.Script/Rpc/MessageExtensions/Utilities.cs +++ b/src/WebJobs.Script/Rpc/MessageExtensions/Utilities.cs @@ -1,11 +1,12 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. +using System; using System.Collections.Generic; using System.Dynamic; using System.Linq; +using Microsoft.AspNetCore.Http; using Microsoft.Azure.WebJobs.Script.Grpc.Messages; -using Microsoft.Azure.WebJobs.Script.Rpc; namespace Microsoft.Azure.WebJobs.Script.Rpc { @@ -25,11 +26,70 @@ public static object ConvertFromHttpMessageToExpando(RpcHttp inputMessage) expando.headers = inputMessage.Headers.ToDictionary(p => p.Key, p => (object)p.Value); expando.enableContentNegotiation = inputMessage.EnableContentNegotiation; + expando.cookies = new List>(); + foreach (RpcHttpCookie cookie in inputMessage.Cookies) + { + expando.cookies.Add(RpcHttpCookieConverter(cookie)); + } + if (inputMessage.Body != null) { expando.body = inputMessage.Body.ToObject(); } return expando; } + + public static Tuple RpcHttpCookieConverter(RpcHttpCookie cookie) + { + var cookieOptions = new CookieOptions(); + if (cookie.Domain != null) + { + cookieOptions.Domain = cookie.Domain.Value; + } + + if (cookie.Path != null) + { + cookieOptions.Path = cookie.Path.Value; + } + + if (cookie.Secure != null) + { + cookieOptions.Secure = cookie.Secure.Value; + } + + cookieOptions.SameSite = RpcSameSiteEnumConverter(cookie.SameSite); + + if (cookie.HttpOnly != null) + { + cookieOptions.HttpOnly = cookie.HttpOnly.Value; + } + + if (cookie.Expires != null) + { + cookieOptions.Expires = cookie.Expires.Value.ToDateTimeOffset(); + } + + if (cookie.MaxAge != null) + { + cookieOptions.MaxAge = TimeSpan.FromSeconds(cookie.MaxAge.Value); + } + + return new Tuple(cookie.Name, cookie.Value, cookieOptions); + } + + private static SameSiteMode RpcSameSiteEnumConverter(RpcHttpCookie.Types.SameSite sameSite) + { + switch (sameSite) + { + case RpcHttpCookie.Types.SameSite.Strict: + return SameSiteMode.Strict; + case RpcHttpCookie.Types.SameSite.Lax: + return SameSiteMode.Lax; + case RpcHttpCookie.Types.SameSite.None: + return SameSiteMode.None; + default: + return SameSiteMode.None; + } + } } } \ No newline at end of file diff --git a/test/WebJobs.Script.Tests/Binding/ActionResults/RawScriptResultTests.cs b/test/WebJobs.Script.Tests/Binding/ActionResults/RawScriptResultTests.cs index 1974dec211..1a79dee5bd 100644 --- a/test/WebJobs.Script.Tests/Binding/ActionResults/RawScriptResultTests.cs +++ b/test/WebJobs.Script.Tests/Binding/ActionResults/RawScriptResultTests.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. +using System; using System.Collections.Generic; using System.IO; using System.Text; @@ -51,6 +52,37 @@ public async Task HandlesStringContent() Assert.Equal(200, context.HttpContext.Response.StatusCode); } + [Fact] + public async Task AddsHttpCookies() + { + var result = new RawScriptResult(null, null) + { + Headers = new Dictionary(), + Cookies = new List>() + { + new Tuple("firstCookie", "cookieValue", new CookieOptions() + { + SameSite = SameSiteMode.None + }), + new Tuple("secondCookie", "cookieValue2", new CookieOptions() + { + Path = "/", + HttpOnly = true, + MaxAge = TimeSpan.FromSeconds(20) + }) + } + }; + + var context = new ActionContext() { HttpContext = new DefaultHttpContext() }; + context.HttpContext.Response.Body = new MemoryStream(); + await result.ExecuteResultAsync(context); + context.HttpContext.Response.Headers.TryGetValue("Set-Cookie", out StringValues cookies); + + Assert.Equal(2, cookies.Count); + Assert.Equal("firstCookie=cookieValue; path=/", cookies[0]); + Assert.Equal("secondCookie=cookieValue2; max-age=20; path=/; samesite=lax; httponly", cookies[1]); + } + [Fact] public async Task HandlesStringContent_WithHeader() { diff --git a/test/WebJobs.Script.Tests/Binding/HttpBindingTests.cs b/test/WebJobs.Script.Tests/Binding/HttpBindingTests.cs index a227570a04..1e591c1190 100644 --- a/test/WebJobs.Script.Tests/Binding/HttpBindingTests.cs +++ b/test/WebJobs.Script.Tests/Binding/HttpBindingTests.cs @@ -4,17 +4,9 @@ using System; using System.Collections.Generic; using System.Dynamic; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; -using System.Web.Http; +using System.Linq; using Microsoft.AspNetCore.Http; using Microsoft.Azure.WebJobs.Script.Binding; -using Microsoft.Azure.WebJobs.Script.WebHost; -using Microsoft.WebJobs.Script.Tests; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using Xunit; namespace Microsoft.Azure.WebJobs.Script.Tests @@ -37,12 +29,15 @@ public void ParseResponseObject_ReturnsExpectedResult() object content = null; int statusCode = StatusCodes.Status200OK; - HttpBinding.ParseResponseObject(responseObject, ref content, out IDictionary headers, out statusCode, out bool enableContentNegotiationResponse); + List> cookies = null; + HttpBinding.ParseResponseObject(responseObject, ref content, out IDictionary headers, out statusCode, out cookies, out bool enableContentNegotiationResponse); Assert.Equal("Test Body", content); Assert.Same(headers, headers); Assert.Equal(StatusCodes.Status202Accepted, statusCode); Assert.False(enableContentNegotiationResponse); + // No cookies found or set + Assert.True(cookies == null || !cookies.Any()); // verify case insensitivity responseObject = new ExpandoObject(); @@ -56,12 +51,51 @@ public void ParseResponseObject_ReturnsExpectedResult() headers = null; statusCode = StatusCodes.Status200OK; enableContentNegotiationResponse = false; - HttpBinding.ParseResponseObject(responseObject, ref content, out headers, out statusCode, out enableContentNegotiationResponse); + HttpBinding.ParseResponseObject(responseObject, ref content, out headers, out statusCode, out cookies, out enableContentNegotiationResponse); Assert.Equal("Test Body", content); Assert.Same(headers, headers); Assert.Equal(StatusCodes.Status202Accepted, statusCode); Assert.True(enableContentNegotiationResponse); + // No cookies found or set + Assert.True(cookies == null || !cookies.Any()); + } + + [Fact] + public void ParseResponseObject_WithCookies_ReturnsExpectedResult() + { + var cookieProperties = new Tuple("hello", "world", new CookieOptions() + { + Domain = "/", + MaxAge = TimeSpan.FromSeconds(60), + HttpOnly = true + }); + + IList> cookieContents = new List>() + { + cookieProperties + }; + + dynamic responseObject = new ExpandoObject(); + responseObject.Body = "Test Body"; + responseObject.Cookies = cookieContents; + responseObject.StatusCode = "202"; // verify string works as well + + object content = null; + var statusCode = StatusCodes.Status200OK; + HttpBinding.ParseResponseObject(responseObject, ref content, out IDictionary headers, out statusCode, out List> cookies, out bool enableContentNegotiationResponse); + + Assert.Equal("Test Body", content); + Assert.Same(cookieContents, cookies); + Assert.Equal(cookieContents.Count, cookies.Count); + var firstCookie = cookies.First(); + Assert.Same(cookieProperties, firstCookie); + Assert.Same(cookieProperties.Item1, firstCookie.Item1); + Assert.Same(cookieProperties.Item2, firstCookie.Item2); + Assert.Same(cookieProperties.Item3, firstCookie.Item3); + Assert.Equal(StatusCodes.Status202Accepted, statusCode); + Assert.False(enableContentNegotiationResponse); + Assert.Null(headers); } [Fact] @@ -75,9 +109,9 @@ public void ParseResponseObject_StatusWithNullBody_ReturnsExpectedResult() IDictionary headers = null; int statusCode = StatusCodes.Status200OK; bool enableContentNegotiationResponse = false; - HttpBinding.ParseResponseObject(responseObject, ref content, out headers, out statusCode, out enableContentNegotiationResponse); + HttpBinding.ParseResponseObject(responseObject, ref content, out headers, out statusCode, out List> cookies, out enableContentNegotiationResponse); - Assert.Equal(null, content); + Assert.Null(content); Assert.Equal(StatusCodes.Status202Accepted, statusCode); } diff --git a/test/WebJobs.Script.Tests/Rpc/RpcMessageConversionExtensionsTests.cs b/test/WebJobs.Script.Tests/Rpc/RpcMessageConversionExtensionsTests.cs index 250b4cbb82..8f251054bb 100644 --- a/test/WebJobs.Script.Tests/Rpc/RpcMessageConversionExtensionsTests.cs +++ b/test/WebJobs.Script.Tests/Rpc/RpcMessageConversionExtensionsTests.cs @@ -1,9 +1,12 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. +using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; +using Google.Protobuf.Collections; +using Google.Protobuf.WellKnownTypes; using Microsoft.AspNetCore.Http; using Microsoft.Azure.WebJobs.Script.Description; using Microsoft.Azure.WebJobs.Script.Grpc.Messages; @@ -92,6 +95,95 @@ public void ToBindingInfo_Defaults_EmptyDataType() Assert.Equal(bindingInfo.DataType, BindingInfo.Types.DataType.Undefined); } + [Theory] + [InlineData("testuser", "testvalue", RpcHttpCookie.Types.SameSite.Lax, null, null, null, null, null, null)] + [InlineData("testuser", "testvalue", RpcHttpCookie.Types.SameSite.Strict, null, null, null, null, null, null)] + [InlineData("testuser", "testvalue", RpcHttpCookie.Types.SameSite.None, null, null, null, null, null, null)] + [InlineData("testuser", "testvalue", RpcHttpCookie.Types.SameSite.Lax, "4/17/2019", null, null, null, null, null)] + [InlineData("testuser", "testvalue", RpcHttpCookie.Types.SameSite.Lax, null, "bing.com", null, null, null, null)] + [InlineData("testuser", "testvalue", RpcHttpCookie.Types.SameSite.Lax, null, null, true, null, null, null)] + [InlineData("testuser", "testvalue", RpcHttpCookie.Types.SameSite.Lax, null, null, null, 60 * 60 * 24, null, null)] + [InlineData("testuser", "testvalue", RpcHttpCookie.Types.SameSite.Lax, null, null, null, null, "/example/route", null)] + [InlineData("testuser", "testvalue", RpcHttpCookie.Types.SameSite.Lax, null, null, null, null, null, true)] + public void SetCookie_ReturnsExpectedResult(string name, string value, RpcHttpCookie.Types.SameSite sameSite, string expires, + string domain, bool? httpOnly, double? maxAge, string path, bool? secure) + { + // Mock rpc cookie + var rpcCookie = new RpcHttpCookie() + { + Name = name, + Value = value, + SameSite = sameSite + }; + + if (!string.IsNullOrEmpty(domain)) + { + rpcCookie.Domain = new NullableString() + { + Value = domain + }; + } + + if (!string.IsNullOrEmpty(path)) + { + rpcCookie.Path = new NullableString() + { + Value = path + }; + } + + if (maxAge.HasValue) + { + rpcCookie.MaxAge = new NullableDouble() + { + Value = maxAge.Value + }; + } + + DateTimeOffset? expiresDateTime = null; + if (!string.IsNullOrEmpty(expires)) + { + if (DateTimeOffset.TryParse(expires, out DateTimeOffset result)) + { + expiresDateTime = result; + rpcCookie.Expires = new NullableTimestamp() + { + Value = result.ToTimestamp() + }; + } + } + + if (httpOnly.HasValue) + { + rpcCookie.HttpOnly = new NullableBool() + { + Value = httpOnly.Value + }; + } + + if (secure.HasValue) + { + rpcCookie.Secure = new NullableBool() + { + Value = secure.Value + }; + } + + var appendCookieArguments = Utilities.RpcHttpCookieConverter(rpcCookie); + Assert.Equal(appendCookieArguments.Item1, name); + Assert.Equal(appendCookieArguments.Item2, value); + + var cookieOptions = appendCookieArguments.Item3; + Assert.Equal(cookieOptions.Domain, domain); + Assert.Equal(cookieOptions.Path, path ?? "/"); + + Assert.Equal(cookieOptions.MaxAge?.TotalSeconds, maxAge); + Assert.Equal(cookieOptions.Expires?.UtcDateTime.ToString(), expiresDateTime?.UtcDateTime.ToString()); + + Assert.Equal(cookieOptions.Secure, secure ?? false); + Assert.Equal(cookieOptions.HttpOnly, httpOnly ?? false); + } + [Fact] public void HttpObjects_ClaimsPrincipal() {