Skip to content

Commit c2f2d91

Browse files
authored
Add new SameSite property and tests (#286)
* Add new SameSite property and tests * publish test results * update image and publish * try again * put test outputs in correct location * always publish results * fixing http tests * fixing tests * change test output cookie order * disabling samesite none cookie test until e2e is repaired
1 parent f562028 commit c2f2d91

File tree

12 files changed

+200
-113
lines changed

12 files changed

+200
-113
lines changed

azure-functions-language-worker-protobuf/src/proto/FunctionRpc.proto

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,11 +431,12 @@ message RpcException {
431431

432432
// Http cookie type. Note that only name and value are used for Http requests
433433
message RpcHttpCookie {
434-
// Enum that lets servers require that a cookie shouoldn't be sent with cross-site requests
434+
// Enum that lets servers require that a cookie shouldn't be sent with cross-site requests
435435
enum SameSite {
436436
None = 0;
437437
Lax = 1;
438438
Strict = 2;
439+
ExplicitNone = 3;
439440
}
440441

441442
// Cookie name

azure-pipelines.yml

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,13 @@ jobs:
3333
IMAGE_TYPE: 'vs2017-win2016'
3434
NODE_VERSION: $(NODE_12)
3535
MAC_NODE8:
36-
IMAGE_TYPE: 'macos-10.13'
36+
IMAGE_TYPE: 'macos-10.14'
3737
NODE_VERSION: $(NODE_8)
3838
MAC_NODE10:
39-
IMAGE_TYPE: 'macos-10.13'
39+
IMAGE_TYPE: 'macos-10.14'
4040
NODE_VERSION: $(NODE_10)
4141
MAC_NODE12:
42-
IMAGE_TYPE: 'macos-10.13'
42+
IMAGE_TYPE: 'macos-10.14'
4343
NODE_VERSION: $(NODE_12)
4444
pool:
4545
vmImage: $(IMAGE_TYPE)
@@ -63,8 +63,6 @@ jobs:
6363
NODE_VERSION: $(NODE_8)
6464
NODE10:
6565
NODE_VERSION: $(NODE_10)
66-
NODE12:
67-
NODE_VERSION: $(NODE_12)
6866
pool:
6967
vmImage: 'vs2017-win2016'
7068
steps:
@@ -81,12 +79,18 @@ jobs:
8179
displayName: 'setup tests'
8280
- powershell: |
8381
.\run-e2e-tests.ps1
82+
displayName: 'run tests'
8483
env:
8584
AzureWebJobsStorage: $(AzureWebJobsStorage)
8685
AzureWebJobsEventHubSender: $(AzureWebJobsEventHubSender)
8786
AzureWebJobsCosmosDBConnectionString: $(AzureWebJobsCosmosDBConnectionString)
8887
FUNCTIONS_WORKER_RUNTIME: 'node'
8988
languageWorkers:node:workerDirectory: $(System.DefaultWorkingDirectory)
89+
- task: PublishTestResults@2
90+
condition: always()
91+
inputs:
92+
testRunner: VSTest
93+
testResultsFiles: '**/*.trx'
9094

9195
- job: BuildArtifacts
9296
condition: and(succeeded(), or(eq(variables['Build.SourceBranch'], 'refs/heads/master'), eq(variables['Build.SourceBranch'], 'refs/heads/v2.x')))

run-e2e-tests.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ function RunTest([string] $project, [string] $description,[bool] $skipBuild = $f
33
Write-Host "-----------------------------------------------------------------------------" -ForegroundColor DarkCyan
44
Write-Host
55

6-
$cmdargs = "test", "$project", "-v", "q", "-l", "trx", "-r","..\..\..\testResults"
6+
$cmdargs = "test", "$project", "-v", "q", "-l", "trx", "-r",".\testResults"
77

88
if ($filter) {
99
$cmdargs += "--filter", "$filter"

src/converters/RpcHttpConverters.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,13 @@ function toRpcHttpCookie(inputCookie: Cookie): rpc.IRpcHttpCookie {
9696
// Resolve SameSite enum, a one-off
9797
let rpcSameSite: rpc.RpcHttpCookie.SameSite = rpc.RpcHttpCookie.SameSite.None;
9898
if (inputCookie && inputCookie.sameSite) {
99-
if (inputCookie.sameSite.toLocaleLowerCase() === "lax") {
100-
rpcSameSite = rpc.RpcHttpCookie.SameSite.Lax;
101-
} else if (inputCookie.sameSite.toLocaleLowerCase() === "strict") {
102-
rpcSameSite = rpc.RpcHttpCookie.SameSite.Strict;
99+
let sameSite = inputCookie.sameSite.toLocaleLowerCase();
100+
if (sameSite === "lax") {
101+
rpcSameSite = rpc.RpcHttpCookie.SameSite.Lax;
102+
} else if (sameSite === "strict") {
103+
rpcSameSite = rpc.RpcHttpCookie.SameSite.Strict;
104+
} else if (sameSite === "none") {
105+
rpcSameSite = rpc.RpcHttpCookie.SameSite.ExplicitNone;
103106
}
104107
}
105108

src/public/Interfaces.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ export interface Cookie {
137137
httpOnly?: boolean;
138138

139139
/** Can restrict the cookie to not be sent with cross-site requests */
140-
sameSite?: "Strict" | "Lax" | undefined;
140+
sameSite?: "Strict" | "Lax" | "None" | undefined;
141141

142142
/** Number of seconds until the cookie expires. A zero or negative number will expire the cookie immediately. */
143143
maxAge?: number;

test/RpcHttpConverters.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,44 @@ describe('Rpc Converters', () => {
4343
expect((<any>rpcCookies[2].expires).value.seconds).to.equal(819199440);
4444
});
4545

46+
it('converts http cookie SameSite', () => {
47+
let cookieInputs: Cookie[] =
48+
[
49+
{
50+
name: "none-cookie",
51+
value: "myvalue",
52+
sameSite: "None"
53+
},
54+
{
55+
name: "lax-cookie",
56+
value: "myvalue",
57+
sameSite: "Lax"
58+
},
59+
{
60+
name: "strict-cookie",
61+
value: "myvalue",
62+
sameSite: "Strict"
63+
},
64+
{
65+
name: "default-cookie",
66+
value: "myvalue"
67+
}
68+
];
69+
70+
let rpcCookies = toRpcHttpCookieList(<Cookie[]>cookieInputs);
71+
expect(rpcCookies[0].name).to.equal("none-cookie");
72+
expect(rpcCookies[0].sameSite).to.equal(rpc.RpcHttpCookie.SameSite.ExplicitNone);
73+
74+
expect(rpcCookies[1].name).to.equal("lax-cookie");
75+
expect(rpcCookies[1].sameSite).to.equal(rpc.RpcHttpCookie.SameSite.Lax);
76+
77+
expect(rpcCookies[2].name).to.equal("strict-cookie");
78+
expect(rpcCookies[2].sameSite).to.equal(rpc.RpcHttpCookie.SameSite.Strict);
79+
80+
expect(rpcCookies[3].name).to.equal("default-cookie");
81+
expect(rpcCookies[3].sameSite).to.equal(rpc.RpcHttpCookie.SameSite.None);
82+
});
83+
4684
it('throws on invalid input', () => {
4785
expect(() => {
4886
let cookieInputs = [

test/end-to-end/Azure.Functions.NodejsWorker.E2E/Azure.Functions.NodejsWorker.E2E/Helpers/CosmosDBHelpers.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ await Utilities.RetryAsync(async () =>
5454
{
5555
try
5656
{
57-
retrievedDocument = await _docDbClient.ReadDocumentAsync(docUri);
57+
retrievedDocument = await _docDbClient.ReadDocumentAsync(docUri, new RequestOptions { PartitionKey = new PartitionKey(docId) });
5858
return true;
5959
}
6060
catch (DocumentClientException ex) when (ex.Error.Code == "NotFound")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Net;
6+
using System.Net.Http;
7+
using System.Net.Http.Headers;
8+
using System.Threading.Tasks;
9+
10+
namespace Azure.Functions.NodeJs.Tests.E2E
11+
{
12+
class HttpHelpers
13+
{
14+
public static async Task<HttpResponseMessage> InvokeHttpTrigger(string functionName, string queryString = "")
15+
{
16+
// Basic http request
17+
HttpRequestMessage request = GetTestRequest(functionName, queryString);
18+
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/plain"));
19+
return await GetResponseMessage(request);
20+
}
21+
22+
public static async Task<HttpResponseMessage> InvokeHttpTriggerWithBody(string functionName, string body, HttpStatusCode expectedStatusCode, string mediaType, int expectedCode = 0)
23+
{
24+
HttpRequestMessage request = GetTestRequest(functionName);
25+
request.Content = new StringContent(body);
26+
request.Content.Headers.ContentType = new MediaTypeHeaderValue(mediaType);
27+
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(mediaType));
28+
return await GetResponseMessage(request);
29+
}
30+
31+
private static HttpRequestMessage GetTestRequest(string functionName, string queryString = "")
32+
{
33+
return new HttpRequestMessage
34+
{
35+
RequestUri = new Uri($"{Constants.FunctionsHostUrl}/api/{functionName}{queryString}"),
36+
Method = HttpMethod.Post
37+
};
38+
}
39+
40+
private static async Task<HttpResponseMessage> GetResponseMessage(HttpRequestMessage request)
41+
{
42+
HttpResponseMessage response = null;
43+
using (var httpClient = new HttpClient())
44+
{
45+
response = await httpClient.SendAsync(request);
46+
}
47+
48+
return response;
49+
}
50+
}
51+
}

test/end-to-end/Azure.Functions.NodejsWorker.E2E/Azure.Functions.NodejsWorker.E2E/HttpEndToEndTests.cs

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

4+
using Newtonsoft.Json.Linq;
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Linq;
48
using System.Net;
9+
using System.Net.Http;
10+
using System.Text;
511
using System.Threading.Tasks;
612
using Xunit;
713

@@ -25,7 +31,15 @@ public HttpEndToEndTests(FunctionAppFixture fixture)
2531
public async Task HttpTriggerTests(string functionName, string queryString, HttpStatusCode expectedStatusCode, string expectedMessage)
2632
{
2733
// TODO: Verify exception on 500 after https://github.com/Azure/azure-functions-host/issues/3589
28-
Assert.True(await Utilities.InvokeHttpTrigger(functionName, queryString, expectedStatusCode, expectedMessage));
34+
HttpResponseMessage response = await HttpHelpers.InvokeHttpTrigger(functionName, queryString);
35+
string actualMessage = await response.Content.ReadAsStringAsync();
36+
37+
Assert.Equal(expectedStatusCode, response.StatusCode);
38+
39+
if (!string.IsNullOrEmpty(expectedMessage)) {
40+
Assert.False(string.IsNullOrEmpty(actualMessage));
41+
Assert.True(actualMessage.Contains(expectedMessage));
42+
}
2943
}
3044

3145
[Theory]
@@ -34,15 +48,65 @@ public async Task HttpTriggerTests(string functionName, string queryString, Http
3448
[InlineData("HttpTriggerBodyAndRawBody", "{\"a\":1}", "application/octet-stream", HttpStatusCode.OK)]
3549
[InlineData("HttpTriggerBodyAndRawBody", "abc", "text/plain", HttpStatusCode.OK)]
3650

37-
public async Task HttpTriggerTestsWithCustomMediaType(string functionName, string queryString, string mediaType, HttpStatusCode expectedStatusCode)
51+
public async Task HttpTriggerTestsWithCustomMediaType(string functionName, string body, string mediaType, HttpStatusCode expectedStatusCode)
3852
{
39-
Assert.True(await Utilities.InvokeHttpTriggerWithBody(functionName, queryString, expectedStatusCode, mediaType));
53+
HttpResponseMessage response = await HttpHelpers.InvokeHttpTriggerWithBody(functionName, body, expectedStatusCode, mediaType);
54+
JObject responseBody = JObject.Parse(await response.Content.ReadAsStringAsync());
55+
56+
Assert.Equal(expectedStatusCode, response.StatusCode);
57+
VerifyBodyAndRawBody(responseBody, body, mediaType);
4058
}
4159

42-
[Fact(Skip = "Not yet enabled.")]
60+
[Fact]
4361
public async Task HttpTriggerWithCookieTests()
4462
{
45-
Assert.True(await Utilities.InvokeHttpTrigger("HttpTriggerSetsCookie", "", HttpStatusCode.OK, "mycookie=myvalue, mycookie2=myvalue2"));
63+
HttpResponseMessage response = await HttpHelpers.InvokeHttpTrigger("HttpTriggerSetsCookie");
64+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
65+
List<string> cookies = response.Headers.SingleOrDefault(header => header.Key == "Set-Cookie").Value.ToList();
66+
Assert.Equal(5, cookies.Count);
67+
Assert.Equal("mycookie=myvalue; max-age=200000; path=/", cookies[0]);
68+
Assert.Equal("mycookie2=myvalue; max-age=200000; path=/", cookies[1]);
69+
Assert.Equal("mycookie3-expires=myvalue3-expires; max-age=0; path=/", cookies[2]);
70+
Assert.Equal("mycookie4-samesite-lax=myvalue; path=/; samesite=lax", cookies[3]);
71+
Assert.Equal("mycookie5-samesite-strict=myvalue; path=/; samesite=strict", cookies[4]);
72+
// Assert.Equal("mycookie4-samesite-none=myvalue; path=/; samesite=none", cookies[5]);
73+
}
74+
75+
private static void VerifyBodyAndRawBody(JObject result, string input, string mediaType)
76+
{
77+
if (mediaType.Equals("application/json", StringComparison.OrdinalIgnoreCase))
78+
{
79+
try
80+
{
81+
Assert.Equal(input, (string)result["reqRawBody"]);
82+
Assert.True(JToken.DeepEquals((JObject)result["reqBody"], JObject.Parse(input)));
83+
}
84+
catch (InvalidCastException) // Invalid JSON
85+
{
86+
Assert.Equal(input, (string)result["reqRawBody"]);
87+
Assert.Equal(input, (string)result["reqBody"]);
88+
}
89+
}
90+
else if (IsMediaTypeOctetOrMultipart(mediaType))
91+
{
92+
JObject reqBody = (JObject)result["reqBody"];
93+
byte[] responseBytes = reqBody["data"].ToObject<byte[]>();
94+
Assert.True(responseBytes.SequenceEqual(Encoding.UTF8.GetBytes(input)));
95+
Assert.Equal(input, (string)result["reqRawBody"]);
96+
}
97+
else if (mediaType.Equals("text/plain", StringComparison.OrdinalIgnoreCase))
98+
{
99+
Assert.Equal(input, (string)result["reqRawBody"]);
100+
Assert.Equal(input, (string)result["reqBody"]);
101+
} else {
102+
Assert.Equal("Supported media types are 'text/plain' 'application/octet-stream', 'multipart/*', and 'application/json'", $"Found mediaType '{mediaType}'");
103+
}
104+
}
105+
106+
private static bool IsMediaTypeOctetOrMultipart(string mediaType)
107+
{
108+
return mediaType != null && (string.Equals(mediaType, "application/octet-stream", StringComparison.OrdinalIgnoreCase)
109+
|| mediaType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0);
46110
}
47111
}
48112
}

0 commit comments

Comments
 (0)