Skip to content

Commit 6d00085

Browse files
authored
Add new SameSite property and tests (#286) (#311)
* 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 * fix merge * fix node 12
1 parent 72e54ed commit 6d00085

File tree

10 files changed

+191
-107
lines changed

10 files changed

+191
-107
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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ jobs:
9292
FUNCTIONS_WORKER_RUNTIME: 'node'
9393
languageWorkers:node:workerDirectory: $(System.DefaultWorkingDirectory)
9494
- task: PublishTestResults@2
95+
condition: always()
9596
inputs:
9697
testRunner: VSTest
9798
testResultsFiles: '**/*.trx'

src/converters/RpcHttpConverters.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,13 @@ function toRpcHttpCookie(inputCookie: Cookie): rpc.IRpcHttpCookie {
101101
// Resolve SameSite enum, a one-off
102102
let rpcSameSite: rpc.RpcHttpCookie.SameSite = rpc.RpcHttpCookie.SameSite.None;
103103
if (inputCookie && inputCookie.sameSite) {
104-
if (inputCookie.sameSite.toLocaleLowerCase() === "lax") {
105-
rpcSameSite = rpc.RpcHttpCookie.SameSite.Lax;
106-
} else if (inputCookie.sameSite.toLocaleLowerCase() === "strict") {
107-
rpcSameSite = rpc.RpcHttpCookie.SameSite.Strict;
104+
let sameSite = inputCookie.sameSite.toLocaleLowerCase();
105+
if (sameSite === "lax") {
106+
rpcSameSite = rpc.RpcHttpCookie.SameSite.Lax;
107+
} else if (sameSite === "strict") {
108+
rpcSameSite = rpc.RpcHttpCookie.SameSite.Strict;
109+
} else if (sameSite === "none") {
110+
rpcSameSite = rpc.RpcHttpCookie.SameSite.ExplicitNone;
108111
}
109112
}
110113

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: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,45 @@ describe('Rpc Converters', () => {
4343
expect((<any>rpcCookies[2].expires).value.seconds).to.equal(819199440);
4444
});
4545

46-
it('throws on invalid cookie input', () => {
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+
84+
it('throws on invalid input', () => {
4785
expect(() => {
4886
let cookieInputs = [
4987
{
Lines changed: 51 additions & 0 deletions
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
}

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

Lines changed: 0 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,6 @@
44
using System;
55
using System.Diagnostics;
66
using System.Threading.Tasks;
7-
using System.Net;
8-
using System.Net.Http;
9-
using System.Net.Http.Headers;
10-
using Newtonsoft.Json.Linq;
11-
using System.Text;
12-
using System.Linq;
137

148
namespace Azure.Functions.NodeJs.Tests.E2E
159
{
@@ -34,88 +28,5 @@ public static async Task RetryAsync(Func<Task<bool>> condition, int timeout = 60
3428
}
3529
}
3630
}
37-
38-
public static async Task<bool> InvokeHttpTrigger(string functionName, string queryString, HttpStatusCode expectedStatusCode, string expectedMessage, int expectedCode = 0)
39-
{
40-
string uri = $"api/{functionName}{queryString}";
41-
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri);
42-
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/plain"));
43-
44-
var httpClient = new HttpClient();
45-
httpClient.BaseAddress = new Uri(Constants.FunctionsHostUrl);
46-
var response = await httpClient.SendAsync(request);
47-
if (expectedStatusCode != response.StatusCode && expectedCode != (int)response.StatusCode)
48-
{
49-
return false;
50-
}
51-
52-
if (!string.IsNullOrEmpty(expectedMessage))
53-
{
54-
string actualMessage = await response.Content.ReadAsStringAsync();
55-
return actualMessage.Contains(expectedMessage);
56-
}
57-
return true;
58-
}
59-
60-
public static async Task<bool> InvokeHttpTriggerWithBody(string functionName, string body, HttpStatusCode expectedStatusCode, string mediaType, int expectedCode = 0)
61-
{
62-
// Arrange
63-
HttpRequestMessage request = new HttpRequestMessage
64-
{
65-
RequestUri = new Uri($"{Constants.FunctionsHostUrl}/api/{functionName}"),
66-
Method = HttpMethod.Post,
67-
Content = new StringContent(body),
68-
};
69-
request.Content.Headers.ContentType = new MediaTypeHeaderValue(mediaType);
70-
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(mediaType));
71-
72-
// Act
73-
HttpResponseMessage response = null;
74-
using (var httpClient = new HttpClient())
75-
{
76-
response = await httpClient.SendAsync(request);
77-
}
78-
79-
// Verify
80-
if (expectedStatusCode != response.StatusCode && expectedCode != (int)response.StatusCode)
81-
{
82-
return false;
83-
}
84-
85-
return VerifyBodyAndRawBody(JObject.Parse(await response.Content.ReadAsStringAsync()), body, mediaType);
86-
}
87-
88-
private static bool VerifyBodyAndRawBody(JObject result, string input, string mediaType)
89-
{
90-
if (mediaType.Equals("application/json", StringComparison.OrdinalIgnoreCase))
91-
{
92-
try
93-
{
94-
return ((string)result["reqRawBody"]).Equals(input) && (JToken.DeepEquals((JObject)result["reqBody"], JObject.Parse(input)));
95-
}
96-
catch (InvalidCastException) // Invalid JSON
97-
{
98-
return ((string)result["reqRawBody"]).Equals(input) && ((string)result["reqBody"]).Equals(input);
99-
}
100-
}
101-
else if(IsMediaTypeOctetOrMultipart(mediaType))
102-
{
103-
JObject reqBody = (JObject)result["reqBody"];
104-
byte[] responseBytes = reqBody["data"].ToObject<byte[]>();
105-
return responseBytes.SequenceEqual(Encoding.UTF8.GetBytes(input)) && ((string)result["reqRawBody"]).Equals(input);
106-
}
107-
else if(mediaType.Equals("text/plain", StringComparison.OrdinalIgnoreCase))
108-
{
109-
return ((string)result["reqRawBody"]).Equals(input) && ((string)result["reqBody"]).Equals(input);
110-
}
111-
112-
return false;
113-
}
114-
115-
private static bool IsMediaTypeOctetOrMultipart(string mediaType)
116-
{
117-
return mediaType != null && (string.Equals(mediaType, "application/octet-stream", StringComparison.OrdinalIgnoreCase) ||
118-
mediaType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0);
119-
}
12031
}
12132
}
Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
module.exports = async function (context, req) {
22
context.log('JavaScript HTTP trigger function processed a request.');
3-
4-
return {
3+
// TODO: Add this scenario
4+
// {
5+
// name: "mycookie6-samesite-none",
6+
// value: "myvalue",
7+
// sameSite: "None"
8+
// },
9+
context.res = {
10+
status: 200,
511
cookies: [
612
{
713
name: "mycookie",
@@ -10,16 +16,25 @@ module.exports = async function (context, req) {
1016
},
1117
{
1218
name: "mycookie2",
13-
value: "myvalue2",
19+
value: "myvalue",
1420
path: "/",
1521
maxAge: "200000"
1622
},
1723
{
1824
name: "mycookie3-expires",
1925
value: "myvalue3-expires",
2026
maxAge: 0
27+
},
28+
{
29+
name: "mycookie4-samesite-lax",
30+
value: "myvalue",
31+
sameSite: "Lax"
32+
},
33+
{
34+
name: "mycookie5-samesite-strict",
35+
value: "myvalue",
36+
sameSite: "Strict"
2137
}
22-
],
23-
body: JSON.stringify(req.headers["cookie"])
38+
]
2439
}
2540
};

0 commit comments

Comments
 (0)