From 85a5807c1ff46e95bfb13ec8d784a0bc7378960d Mon Sep 17 00:00:00 2001 From: peterstone2017 Date: Fri, 1 Apr 2022 12:01:12 -0500 Subject: [PATCH 01/17] initial add cookies attempt --- azure_functions_worker/bindings/datumdef.py | 90 +++++++++++++- .../bindings/nullable_converters.py | 113 ++++++++++++++++++ azure_functions_worker/dispatcher.py | 6 - azure_functions_worker/protos/__init__.py | 12 +- .../function.json | 15 +++ .../multiple-set-cookie-resp-headers/main.py | 23 ++++ .../function.json | 15 +++ .../main.py | 18 +++ .../function.json | 15 +++ .../main.py | 18 +++ .../function.json | 15 +++ .../main.py | 15 +++ .../function.json | 15 +++ .../main.py | 15 +++ .../function.json | 15 +++ .../set-cookie-resp-header-empty/main.py | 15 +++ tests/unittests/test_http_functions.py | 51 ++++++++ 17 files changed, 456 insertions(+), 10 deletions(-) create mode 100644 azure_functions_worker/bindings/nullable_converters.py create mode 100644 tests/unittests/http_functions/multiple-set-cookie-resp-headers/function.json create mode 100644 tests/unittests/http_functions/multiple-set-cookie-resp-headers/main.py create mode 100644 tests/unittests/http_functions/response_cookie_header_nullable_bool_err/function.json create mode 100644 tests/unittests/http_functions/response_cookie_header_nullable_bool_err/main.py create mode 100644 tests/unittests/http_functions/response_cookie_header_nullable_double_err/function.json create mode 100644 tests/unittests/http_functions/response_cookie_header_nullable_double_err/main.py create mode 100644 tests/unittests/http_functions/response_cookie_header_nullable_timestamp_err/function.json create mode 100644 tests/unittests/http_functions/response_cookie_header_nullable_timestamp_err/main.py create mode 100644 tests/unittests/http_functions/set-cookie-resp-header-default-values/function.json create mode 100644 tests/unittests/http_functions/set-cookie-resp-header-default-values/main.py create mode 100644 tests/unittests/http_functions/set-cookie-resp-header-empty/function.json create mode 100644 tests/unittests/http_functions/set-cookie-resp-header-empty/main.py diff --git a/azure_functions_worker/bindings/datumdef.py b/azure_functions_worker/bindings/datumdef.py index 2257d38b0..573d418fc 100644 --- a/azure_functions_worker/bindings/datumdef.py +++ b/azure_functions_worker/bindings/datumdef.py @@ -1,10 +1,21 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - +import logging from typing import Any, Optional import json from .. import protos from ..logging import logger +from typing import Union, List +from datetime import datetime +from google.protobuf.timestamp_pb2 import Timestamp +try: + from http.cookies import SimpleCookie +except ImportError: + from Cookie import SimpleCookie +from dateutil import parser +from dateutil.parser import ParserError +from .nullable_converters import to_nullable_bool, to_nullable_string, \ + to_nullable_double, to_nullable_timestamp class Datum: @@ -99,8 +110,8 @@ def from_rpc_shared_memory( shmem: protos.RpcSharedMemory, shmem_mgr) -> Optional['Datum']: """ - Reads the specified shared memory region and converts the read data into - a datum object of the corresponding type. + Reads the specified shared memory region and converts the read data + into a datum object of the corresponding type. """ if shmem is None: logger.warning('Cannot read from shared memory. ' @@ -183,6 +194,7 @@ def datum_as_proto(datum: Datum) -> protos.TypedData: k: v.value for k, v in datum.value['headers'].items() }, + cookies=parse_to_rpc_http_cookie_list(datum.value['cookies']), enable_content_negotiation=False, body=datum_as_proto(datum.value['body']), )) @@ -190,3 +202,75 @@ def datum_as_proto(datum: Datum) -> protos.TypedData: raise NotImplementedError( 'unexpected Datum type: {!r}'.format(datum.type) ) + +def parse_to_rpc_http_cookie_list(cookies: Optional[List[SimpleCookie]]): + if cookies is None: + return cookies + + rpc_http_cookies = [] + + for cookie in cookies: + for name, cookie_entity in cookie.items(): + rpc_http_cookies.append( + protos.RpcHttpCookie(name=name, + value=cookie_entity.value, + domain=to_nullable_string( + cookie_entity['domain'], + 'cookie.domain'), + path= + to_nullable_string( + cookie_entity['path'], + 'cookie.path'), + expires= + to_nullable_timestamp( + parse_cookie_attr_expires( + cookie_entity), + 'cookie.expires'), + secure=to_nullable_bool( + bool(cookie_entity['secure']), + 'cookie.secure'), + http_only=to_nullable_bool( + bool(cookie_entity['httponly']), + 'cookie.httpOnly'), + same_site=parse_cookie_attr_same_site( + cookie_entity), + max_age=to_nullable_double( + cookie_entity['max-age'], + 'cookie.maxAge'))) + + return rpc_http_cookies + + +def parse_cookie_attr_expires(cookie_entity): + expires = cookie_entity['expires'] + + if expires is not None and len(expires) != 0: + try: + return parser.parse(expires) + except ParserError: + logging.error( + f"Can not parse value {expires} of expires in the cookie " + f"due to invalid format.") + raise + except OverflowError: + logging.error( + f"Can not parse value {expires} of expires in the cookie " + f"because the parsed date exceeds the largest valid C " + f"integer on your system.") + raise + + return None + + +def parse_cookie_attr_same_site(cookie_entity): + same_site = getattr(protos.RpcHttpCookie.SameSite, "None") + raw_same_site_str = cookie_entity['samesite'].lower() + + if raw_same_site_str == 'lax': + same_site = protos.RpcHttpCookie.SameSite.Lax + elif raw_same_site_str == 'strict': + same_site = protos.RpcHttpCookie.SameSite.Strict + elif raw_same_site_str == 'none': + same_site = protos.RpcHttpCookie.SameSite.ExplicitNone + + return same_site \ No newline at end of file diff --git a/azure_functions_worker/bindings/nullable_converters.py b/azure_functions_worker/bindings/nullable_converters.py new file mode 100644 index 000000000..5690cebeb --- /dev/null +++ b/azure_functions_worker/bindings/nullable_converters.py @@ -0,0 +1,113 @@ +from datetime import datetime +from typing import Optional, Union + +from google.protobuf.timestamp_pb2 import Timestamp + +from azure_functions_worker import protos, logging +from azure_functions_worker.logging import logger + + +def to_nullable_string(nullable: Optional[str], property_name: str) -> \ + Optional[protos.NullableString]: + """Converts string input to an 'NullableString' to be sent through the + RPC layer. Input that is not a string but is also not null or undefined + logs a function app level warning. + + :param nullable Input to be converted to an NullableString if it is a + valid string + :param property_name The name of the property that the caller will + assign the output to. Used for debugging. + """ + if isinstance(nullable, str): + return protos.NullableString(value=nullable) + + if nullable is not None: + raise TypeError( + f"A 'str' type was expected instead of a '${type(nullable)}' " + f"type. Cannot parse value {nullable} of '${property_name}'.") + + return None + + +def to_nullable_bool(nullable: Optional[bool], property_name: str) -> Optional[ + protos.NullableBool]: + """Converts boolean input to an 'NullableBool' to be sent through the + RPC layer. Input that is not a boolean but is also not null or undefined + logs a function app level warning. + + :param nullable Input to be converted to an NullableBool if it is a + valid boolean + :param property_name The name of the property that the caller will + assign the output to. Used for debugging. + """ + if isinstance(nullable, bool): + return protos.NullableBool(value=nullable) + + if nullable is not None: + raise TypeError( + f"A 'bool' type was expected instead of a '{type(nullable)}' " + f"type. Cannot parse value {nullable} of '{property_name}'.") + + return None + + +def to_nullable_double(nullable: Optional[Union[str, int, float]], + property_name: str) -> \ + Optional[protos.NullableDouble]: + """Converts int or float or str that parses to a number to an + 'NullableDouble' to be sent through the RPC layer. Input that is not a + valid number but is also not null or undefined logs a function app level + warning. + :param nullable Input to be converted to an NullableDouble if it is a + valid number + :param property_name The name of the property that the caller will + assign the output to. Used for debugging. + """ + if isinstance(nullable, int) or isinstance(nullable, float): + return protos.NullableDouble(value=nullable) + elif isinstance(nullable, str): + if len(nullable) == 0: + return None + + try: + return protos.NullableDouble(value=float(nullable)) + except Exception: + raise TypeError( + f"Cannot parse value {nullable} of '{property_name}' to " + f"float.") + + if nullable is not None: + raise TypeError( + f"A 'int' or 'float'" + f" type was expected instead of a '{type(nullable)}' " + f"type. Cannot parse value {nullable} of '{property_name}'.") + + return None + + +def to_nullable_timestamp(date_time: Optional[Union[datetime, int]], + property_name: str) -> protos.NullableTimestamp: + """Converts Date or number input to an 'NullableTimestamp' to be sent + through the RPC layer. Input that is not a Date or number but is also + not null or undefined logs a function app level warning. + + :param date_time Input to be converted to an NullableTimestamp if it is + valid input + :param property_name The name of the property that the caller will + assign the output to. Used for debugging. + """ + logger.info(date_time) + logger.info(type(date_time)) + if date_time is not None: + try: + time_in_seconds = date_time if type( + date_time) == 'int' else date_time.timestamp() + + return protos.NullableTimestamp( + value=Timestamp(seconds=int(time_in_seconds))) + except Exception: + raise TypeError( + f"A 'datetime' or 'int'" + f" type was expected instead of a '{type(date_time)}' " + f"type. Cannot parse value {date_time} of '{property_name}'.") + return None diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py index 03c29478b..50ed72a09 100644 --- a/azure_functions_worker/dispatcher.py +++ b/azure_functions_worker/dispatcher.py @@ -211,12 +211,6 @@ def on_logging(self, record: logging.LogRecord, formatted_msg: str) -> None: if invocation_id is not None: log['invocation_id'] = invocation_id - # XXX: When an exception field is set in RpcLog, WebHost doesn't - # wait for the call result and simply aborts the execution. - # - # if record.exc_info and record.exc_info[1] is not None: - # log['exception'] = self._serialize_exception(record.exc_info[1]) - self._grpc_resp_queue.put_nowait( protos.StreamingMessage( request_id=self.request_id, diff --git a/azure_functions_worker/protos/__init__.py b/azure_functions_worker/protos/__init__.py index 827a0df72..45c31d375 100644 --- a/azure_functions_worker/protos/__init__.py +++ b/azure_functions_worker/protos/__init__.py @@ -24,8 +24,18 @@ ParameterBinding, TypedData, RpcHttp, + RpcHttpCookie, RpcLog, RpcSharedMemory, RpcDataType, CloseSharedMemoryResourcesRequest, - CloseSharedMemoryResourcesResponse) + CloseSharedMemoryResourcesResponse, + FunctionsMetadataRequest, + FunctionMetadataResponse) + +from .shared.NullableTypes_pb2 import ( + NullableString, + NullableBool, + NullableDouble, + NullableTimestamp +) diff --git a/tests/unittests/http_functions/multiple-set-cookie-resp-headers/function.json b/tests/unittests/http_functions/multiple-set-cookie-resp-headers/function.json new file mode 100644 index 000000000..5d4d8285f --- /dev/null +++ b/tests/unittests/http_functions/multiple-set-cookie-resp-headers/function.json @@ -0,0 +1,15 @@ +{ + "scriptFile": "main.py", + "bindings": [ + { + "type": "httpTrigger", + "direction": "in", + "name": "req" + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} diff --git a/tests/unittests/http_functions/multiple-set-cookie-resp-headers/main.py b/tests/unittests/http_functions/multiple-set-cookie-resp-headers/main.py new file mode 100644 index 000000000..096f573dd --- /dev/null +++ b/tests/unittests/http_functions/multiple-set-cookie-resp-headers/main.py @@ -0,0 +1,23 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import logging + +import azure.functions as func + + +def main(req: func.HttpRequest): + logging.info('Python HTTP trigger function processed a request.') + resp = func.HttpResponse( + "This HTTP triggered function executed successfully.") + + resp.headers.add("Set-Cookie", + 'foo3=42; Domain=example.com; Expires=Thu, 12-Jan-2017 ' + '13:55:08 GMT; Path=/; Max-Age=10000000; Secure; ' + 'HttpOnly') + resp.headers.add("Set-Cookie", + 'foo3=43; Domain=example.com; Expires=Thu, 12-Jan-2018 ' + '13:55:08 GMT; Path=/; Max-Age=10000000; Secure; ' + 'HttpOnly') + resp.headers.add("HELLO", 'world') + + return resp \ No newline at end of file diff --git a/tests/unittests/http_functions/response_cookie_header_nullable_bool_err/function.json b/tests/unittests/http_functions/response_cookie_header_nullable_bool_err/function.json new file mode 100644 index 000000000..5d4d8285f --- /dev/null +++ b/tests/unittests/http_functions/response_cookie_header_nullable_bool_err/function.json @@ -0,0 +1,15 @@ +{ + "scriptFile": "main.py", + "bindings": [ + { + "type": "httpTrigger", + "direction": "in", + "name": "req" + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} diff --git a/tests/unittests/http_functions/response_cookie_header_nullable_bool_err/main.py b/tests/unittests/http_functions/response_cookie_header_nullable_bool_err/main.py new file mode 100644 index 000000000..630a33dff --- /dev/null +++ b/tests/unittests/http_functions/response_cookie_header_nullable_bool_err/main.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import logging + +import azure.functions as func + + +def main(req: func.HttpRequest): + logging.info('Python HTTP trigger function processed a request.') + resp = func.HttpResponse( + "This HTTP triggered function executed successfully.") + + resp.headers.add("Set-Cookie", + 'foo3=42; Domain=example.com; Expires=Thu, 12-Jan-2017 ' + '13:55:08 GMT; Path=/; Max-Age=10000000; SecureFalse; ' + 'HttpOnly') + + return resp diff --git a/tests/unittests/http_functions/response_cookie_header_nullable_double_err/function.json b/tests/unittests/http_functions/response_cookie_header_nullable_double_err/function.json new file mode 100644 index 000000000..5d4d8285f --- /dev/null +++ b/tests/unittests/http_functions/response_cookie_header_nullable_double_err/function.json @@ -0,0 +1,15 @@ +{ + "scriptFile": "main.py", + "bindings": [ + { + "type": "httpTrigger", + "direction": "in", + "name": "req" + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} diff --git a/tests/unittests/http_functions/response_cookie_header_nullable_double_err/main.py b/tests/unittests/http_functions/response_cookie_header_nullable_double_err/main.py new file mode 100644 index 000000000..81601b8b9 --- /dev/null +++ b/tests/unittests/http_functions/response_cookie_header_nullable_double_err/main.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import logging + +import azure.functions as func + + +def main(req: func.HttpRequest): + logging.info('Python HTTP trigger function processed a request.') + resp = func.HttpResponse( + "This HTTP triggered function executed successfully.") + + resp.headers.add("Set-Cookie", + 'foo3=42; Domain=example.com; Expires=Thu, 12-Jan-2017 ' + '13:55:08 GMT; Path=/; Max-Age=Dummy; SecureFalse; ' + 'HttpOnly') + + return resp diff --git a/tests/unittests/http_functions/response_cookie_header_nullable_timestamp_err/function.json b/tests/unittests/http_functions/response_cookie_header_nullable_timestamp_err/function.json new file mode 100644 index 000000000..5d4d8285f --- /dev/null +++ b/tests/unittests/http_functions/response_cookie_header_nullable_timestamp_err/function.json @@ -0,0 +1,15 @@ +{ + "scriptFile": "main.py", + "bindings": [ + { + "type": "httpTrigger", + "direction": "in", + "name": "req" + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} diff --git a/tests/unittests/http_functions/response_cookie_header_nullable_timestamp_err/main.py b/tests/unittests/http_functions/response_cookie_header_nullable_timestamp_err/main.py new file mode 100644 index 000000000..6a7c8cfef --- /dev/null +++ b/tests/unittests/http_functions/response_cookie_header_nullable_timestamp_err/main.py @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import logging + +import azure.functions as func + + +def main(req: func.HttpRequest): + logging.info('Python HTTP trigger function processed a request.') + resp = func.HttpResponse( + "This HTTP triggered function executed successfully.") + + resp.headers.add("Set-Cookie", 'foo=bar; Domain=123; Expires=Dummy') + + return resp diff --git a/tests/unittests/http_functions/set-cookie-resp-header-default-values/function.json b/tests/unittests/http_functions/set-cookie-resp-header-default-values/function.json new file mode 100644 index 000000000..5d4d8285f --- /dev/null +++ b/tests/unittests/http_functions/set-cookie-resp-header-default-values/function.json @@ -0,0 +1,15 @@ +{ + "scriptFile": "main.py", + "bindings": [ + { + "type": "httpTrigger", + "direction": "in", + "name": "req" + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} diff --git a/tests/unittests/http_functions/set-cookie-resp-header-default-values/main.py b/tests/unittests/http_functions/set-cookie-resp-header-default-values/main.py new file mode 100644 index 000000000..a29b693b9 --- /dev/null +++ b/tests/unittests/http_functions/set-cookie-resp-header-default-values/main.py @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import logging + +import azure.functions as func + + +def main(req: func.HttpRequest): + logging.info('Python HTTP trigger function processed a request.') + resp = func.HttpResponse( + "This HTTP triggered function executed successfully.") + + resp.headers.add("Set-Cookie", 'foo=bar') + + return resp diff --git a/tests/unittests/http_functions/set-cookie-resp-header-empty/function.json b/tests/unittests/http_functions/set-cookie-resp-header-empty/function.json new file mode 100644 index 000000000..5d4d8285f --- /dev/null +++ b/tests/unittests/http_functions/set-cookie-resp-header-empty/function.json @@ -0,0 +1,15 @@ +{ + "scriptFile": "main.py", + "bindings": [ + { + "type": "httpTrigger", + "direction": "in", + "name": "req" + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} diff --git a/tests/unittests/http_functions/set-cookie-resp-header-empty/main.py b/tests/unittests/http_functions/set-cookie-resp-header-empty/main.py new file mode 100644 index 000000000..3e33bc8e4 --- /dev/null +++ b/tests/unittests/http_functions/set-cookie-resp-header-empty/main.py @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import logging + +import azure.functions as func + + +def main(req: func.HttpRequest): + logging.info('Python HTTP trigger function processed a request.') + resp = func.HttpResponse( + "This HTTP triggered function executed successfully.") + + resp.headers.add("Set-Cookie", '') + + return resp diff --git a/tests/unittests/test_http_functions.py b/tests/unittests/test_http_functions.py index 3b2fe8c00..90670a64b 100644 --- a/tests/unittests/test_http_functions.py +++ b/tests/unittests/test_http_functions.py @@ -352,6 +352,57 @@ def test_print_to_console_stdout(self): self.assertEqual(r.status_code, 200) self.assertEqual(r.text, 'OK-print-logging') + def test_multiple_cookie_header_in_response(self): + r = self.webhost.request('GET', 'multiple-set-cookie-resp-headers') + self.assertEqual(r.status_code, 200) + self.assertEqual(r.headers.get( + 'Set-Cookie'), + "foo3=42; expires=Thu, 12 Jan 2017 13:55:08 GMT; " + "max-age=10000000; domain=example.com; path=/; secure; httponly, " + "foo3=43; expires=Fri, 12 Jan 2018 13:55:08 GMT; " + "max-age=10000000; domain=example.com; path=/; secure; httponly") + + def test_set_cookie_header_in_response_empty_value(self): + r = self.webhost.request('GET', 'set-cookie-resp-header-empty') + self.assertEqual(r.status_code, 200) + self.assertEqual(r.headers.get('Set-Cookie'), None) + + def test_set_cookie_header_in_response_default_value(self): + r = self.webhost.request('GET', + 'set-cookie-resp-header-default-values') + self.assertEqual(r.status_code, 200) + self.assertEqual(r.headers.get('Set-Cookie'), + 'foo=bar; domain=; path=') + + def test_response_cookie_header_nullable_timestamp_err(self): + r = self.webhost.request( + 'GET', + 'response_cookie_header_nullable_timestamp_err') + self.assertEqual(r.status_code, 500) + + def check_log_response_cookie_header_nullable_timestamp_err(self, + host_out: + typing.List[ + str]): + self.assertIn( + "Can not parse value Dummy of expires in the cookie due to " + "invalid format.", + host_out) + + def test_response_cookie_header_nullable_bool_err(self): + r = self.webhost.request( + 'GET', + 'response_cookie_header_nullable_bool_err') + self.assertEqual(r.status_code, 200) + self.assertFalse("Set-Cookie" in r.headers) + + def test_response_cookie_header_nullable_double_err(self): + r = self.webhost.request( + 'GET', + 'response_cookie_header_nullable_double_err') + self.assertEqual(r.status_code, 200) + self.assertFalse("Set-Cookie" in r.headers) + @pytest.mark.flaky(reruns=3) def check_log_print_to_console_stdout(self, host_out: typing.List[str]): # System logs stdout should not exist in host_out From 5efa8549900bba2165aaa208afc5189f7c46806f Mon Sep 17 00:00:00 2001 From: peterstone2017 Date: Fri, 8 Apr 2022 10:50:30 -0500 Subject: [PATCH 02/17] exclude workindexing proto api from this pr --- azure_functions_worker/protos/__init__.py | 4 +--- tests/test_random.py | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 tests/test_random.py diff --git a/azure_functions_worker/protos/__init__.py b/azure_functions_worker/protos/__init__.py index 45c31d375..843904ae1 100644 --- a/azure_functions_worker/protos/__init__.py +++ b/azure_functions_worker/protos/__init__.py @@ -29,9 +29,7 @@ RpcSharedMemory, RpcDataType, CloseSharedMemoryResourcesRequest, - CloseSharedMemoryResourcesResponse, - FunctionsMetadataRequest, - FunctionMetadataResponse) + CloseSharedMemoryResourcesResponse) from .shared.NullableTypes_pb2 import ( NullableString, diff --git a/tests/test_random.py b/tests/test_random.py new file mode 100644 index 000000000..48aa7ee0c --- /dev/null +++ b/tests/test_random.py @@ -0,0 +1,21 @@ +import unittest + +from azure_functions_worker.bindings.datumdef import parse_cookie_attr_expires, \ + parse_to_rpc_http_cookie_list + +try: + from http.cookies import SimpleCookie +except ImportError: + from Cookie import SimpleCookie +from dateutil import parser + +headers = ['foo=bar; Path=/some/path; Secure', + 'foo2=42; Domain=123; Expires=Thu, 12-Jan-2017 13:55:08 GMT; ' + 'Path=/; Max-Age=dd;'] + +cookies = SimpleCookie('\r\n'.join(headers)) + + +class TestHttpFunctions(unittest.TestCase): + def test_multiple_cookie_header_in_response(self): + print(parse_to_rpc_http_cookie_list([cookies])) From f9d066ea078fd3a87d4fb4006cb2835dedd40eb3 Mon Sep 17 00:00:00 2001 From: peterstone2017 Date: Mon, 11 Apr 2022 17:23:44 -0500 Subject: [PATCH 03/17] Add parsing and nullable tests --- azure_functions_worker/bindings/datumdef.py | 40 +++--- .../bindings/nullable_converters.py | 18 ++- .../multiple-set-cookie-resp-headers/main.py | 2 +- tests/unittests/test_datumref.py | 120 ++++++++++++++++++ tests/unittests/test_http_functions.py | 6 +- tests/unittests/test_nullable_converters.py | 93 ++++++++++++++ 6 files changed, 244 insertions(+), 35 deletions(-) create mode 100644 tests/unittests/test_datumref.py create mode 100644 tests/unittests/test_nullable_converters.py diff --git a/azure_functions_worker/bindings/datumdef.py b/azure_functions_worker/bindings/datumdef.py index 573d418fc..8449219d7 100644 --- a/azure_functions_worker/bindings/datumdef.py +++ b/azure_functions_worker/bindings/datumdef.py @@ -5,9 +5,7 @@ import json from .. import protos from ..logging import logger -from typing import Union, List -from datetime import datetime -from google.protobuf.timestamp_pb2 import Timestamp +from typing import List try: from http.cookies import SimpleCookie except ImportError: @@ -203,6 +201,7 @@ def datum_as_proto(datum: Datum) -> protos.TypedData: 'unexpected Datum type: {!r}'.format(datum.type) ) + def parse_to_rpc_http_cookie_list(cookies: Optional[List[SimpleCookie]]): if cookies is None: return cookies @@ -217,15 +216,11 @@ def parse_to_rpc_http_cookie_list(cookies: Optional[List[SimpleCookie]]): domain=to_nullable_string( cookie_entity['domain'], 'cookie.domain'), - path= - to_nullable_string( - cookie_entity['path'], - 'cookie.path'), - expires= - to_nullable_timestamp( + path=to_nullable_string( + cookie_entity['path'], 'cookie.path'), + expires=to_nullable_timestamp( parse_cookie_attr_expires( - cookie_entity), - 'cookie.expires'), + cookie_entity), 'cookie.expires'), secure=to_nullable_bool( bool(cookie_entity['secure']), 'cookie.secure'), @@ -264,13 +259,16 @@ def parse_cookie_attr_expires(cookie_entity): def parse_cookie_attr_same_site(cookie_entity): same_site = getattr(protos.RpcHttpCookie.SameSite, "None") - raw_same_site_str = cookie_entity['samesite'].lower() - - if raw_same_site_str == 'lax': - same_site = protos.RpcHttpCookie.SameSite.Lax - elif raw_same_site_str == 'strict': - same_site = protos.RpcHttpCookie.SameSite.Strict - elif raw_same_site_str == 'none': - same_site = protos.RpcHttpCookie.SameSite.ExplicitNone - - return same_site \ No newline at end of file + try: + raw_same_site_str = cookie_entity['samesite'].lower() + + if raw_same_site_str == 'lax': + same_site = protos.RpcHttpCookie.SameSite.Lax + elif raw_same_site_str == 'strict': + same_site = protos.RpcHttpCookie.SameSite.Strict + elif raw_same_site_str == 'none': + same_site = protos.RpcHttpCookie.SameSite.ExplicitNone + except Exception: + return same_site + + return same_site diff --git a/azure_functions_worker/bindings/nullable_converters.py b/azure_functions_worker/bindings/nullable_converters.py index 5690cebeb..e1c75aecc 100644 --- a/azure_functions_worker/bindings/nullable_converters.py +++ b/azure_functions_worker/bindings/nullable_converters.py @@ -3,8 +3,7 @@ from google.protobuf.timestamp_pb2 import Timestamp -from azure_functions_worker import protos, logging -from azure_functions_worker.logging import logger +from azure_functions_worker import protos def to_nullable_string(nullable: Optional[str], property_name: str) -> \ @@ -23,14 +22,14 @@ def to_nullable_string(nullable: Optional[str], property_name: str) -> \ if nullable is not None: raise TypeError( - f"A 'str' type was expected instead of a '${type(nullable)}' " - f"type. Cannot parse value {nullable} of '${property_name}'.") + f"A 'str' type was expected instead of a '{type(nullable)}' " + f"type. Cannot parse value {nullable} of '{property_name}'.") return None -def to_nullable_bool(nullable: Optional[bool], property_name: str) -> Optional[ - protos.NullableBool]: +def to_nullable_bool(nullable: Optional[bool], property_name: str) -> \ + Optional[protos.NullableBool]: """Converts boolean input to an 'NullableBool' to be sent through the RPC layer. Input that is not a boolean but is also not null or undefined logs a function app level warning. @@ -96,12 +95,11 @@ def to_nullable_timestamp(date_time: Optional[Union[datetime, int]], :param property_name The name of the property that the caller will assign the output to. Used for debugging. """ - logger.info(date_time) - logger.info(type(date_time)) if date_time is not None: try: - time_in_seconds = date_time if type( - date_time) == 'int' else date_time.timestamp() + time_in_seconds = date_time if isinstance(date_time, + int) else \ + date_time.timestamp() return protos.NullableTimestamp( value=Timestamp(seconds=int(time_in_seconds))) diff --git a/tests/unittests/http_functions/multiple-set-cookie-resp-headers/main.py b/tests/unittests/http_functions/multiple-set-cookie-resp-headers/main.py index 096f573dd..450496fb4 100644 --- a/tests/unittests/http_functions/multiple-set-cookie-resp-headers/main.py +++ b/tests/unittests/http_functions/multiple-set-cookie-resp-headers/main.py @@ -20,4 +20,4 @@ def main(req: func.HttpRequest): 'HttpOnly') resp.headers.add("HELLO", 'world') - return resp \ No newline at end of file + return resp diff --git a/tests/unittests/test_datumref.py b/tests/unittests/test_datumref.py new file mode 100644 index 000000000..adb860f2f --- /dev/null +++ b/tests/unittests/test_datumref.py @@ -0,0 +1,120 @@ +import unittest +from http.cookies import SimpleCookie + +from dateutil import parser +from dateutil.parser import ParserError + +from azure_functions_worker import protos +from azure_functions_worker.bindings.datumdef import \ + parse_cookie_attr_expires, \ + parse_cookie_attr_same_site, parse_to_rpc_http_cookie_list +from azure_functions_worker.bindings.nullable_converters import \ + to_nullable_bool, to_nullable_string, to_nullable_double, \ + to_nullable_timestamp +from azure_functions_worker.protos import RpcHttpCookie + + +class TestDatumRef(unittest.TestCase): + def test_parse_cookie_attr_expires_none(self): + self.assertEquals(parse_cookie_attr_expires({"expires": None}), None) + + def test_parse_cookie_attr_expires_zero_length(self): + self.assertEquals(parse_cookie_attr_expires({"expires": ""}), None) + + def test_parse_cookie_attr_expires_valid(self): + self.assertEquals(parse_cookie_attr_expires( + {"expires": "Thu, 12-Jan-2017 13:55:08 GMT"}), + parser.parse("Thu, 12-Jan-2017 13:55:08 GMT")) + + def test_parse_cookie_attr_expires_parse_error(self): + with self.assertRaises(ParserError): + parse_cookie_attr_expires( + {"expires": "Thu, 12-Jan-2017 13:550:08 GMT"}) + + def test_parse_cookie_attr_same_site_default(self): + self.assertEquals(parse_cookie_attr_same_site( + {}), + getattr(protos.RpcHttpCookie.SameSite, "None")) + + def test_parse_cookie_attr_same_site_lax(self): + self.assertEquals(parse_cookie_attr_same_site( + {'samesite': 'lax'}), + getattr(protos.RpcHttpCookie.SameSite, "Lax")) + + def test_parse_cookie_attr_same_site_strict(self): + self.assertEquals(parse_cookie_attr_same_site( + {'samesite': 'strict'}), + getattr(protos.RpcHttpCookie.SameSite, "Strict")) + + def test_parse_cookie_attr_same_site_explicit_none(self): + self.assertEquals(parse_cookie_attr_same_site( + {'samesite': 'none'}), + getattr(protos.RpcHttpCookie.SameSite, "ExplicitNone")) + + def test_parse_to_rpc_http_cookie_list_none(self): + self.assertEquals(parse_to_rpc_http_cookie_list(None), None) + + def test_parse_to_rpc_http_cookie_list_valid(self): + headers = [ + 'foo=bar; Path=/some/path; Secure; HttpOnly; Domain=123; ' + 'SameSite=Lax; Max-Age=12345; Expires=Thu, 12-Jan-2017 13:55:08 ' + 'GMT;', + 'foo2=bar; Path=/some/path2; Secure; HttpOnly; Domain=123; ' + 'SameSite=Lax; Max-Age=12345; Expires=Thu, 12-Jan-2017 13:55:08 ' + 'GMT;'] + + cookies = SimpleCookie('\r\n'.join(headers)) + + cookie1 = RpcHttpCookie(name="foo", + value="bar", + domain=to_nullable_string("123", + "cookie.domain"), + path=to_nullable_string("/some/path", + "cookie.path"), + expires=to_nullable_timestamp( + parse_cookie_attr_expires( + { + "expires": "Thu, " + "12-Jan-2017 13:55:08" + " GMT"}), + 'cookie.expires'), + secure=to_nullable_bool( + bool("True"), + 'cookie.secure'), + http_only=to_nullable_bool( + bool("True"), + 'cookie.httpOnly'), + same_site=parse_cookie_attr_same_site( + {"samesite": "Lax"}), + max_age=to_nullable_double( + 12345, + 'cookie.maxAge')) + + cookie2 = RpcHttpCookie(name="foo2", + value="bar", + domain=to_nullable_string("123", + "cookie.domain"), + path=to_nullable_string("/some/path2", + "cookie.path"), + expires=to_nullable_timestamp( + parse_cookie_attr_expires( + { + "expires": "Thu, " + "12-Jan-2017 13:55:08" + " GMT"}), + 'cookie.expires'), + secure=to_nullable_bool( + bool("True"), + 'cookie.secure'), + http_only=to_nullable_bool( + bool("True"), + 'cookie.httpOnly'), + same_site=parse_cookie_attr_same_site( + {"samesite": "Lax"}), + max_age=to_nullable_double( + 12345, + 'cookie.maxAge')) + + rpc_cookies = parse_to_rpc_http_cookie_list([cookies]) + self.assertEquals(cookie1, rpc_cookies[0]) + self.assertEquals(cookie2, rpc_cookies[1]) diff --git a/tests/unittests/test_http_functions.py b/tests/unittests/test_http_functions.py index 90670a64b..1f1b44b42 100644 --- a/tests/unittests/test_http_functions.py +++ b/tests/unittests/test_http_functions.py @@ -381,9 +381,9 @@ def test_response_cookie_header_nullable_timestamp_err(self): self.assertEqual(r.status_code, 500) def check_log_response_cookie_header_nullable_timestamp_err(self, - host_out: - typing.List[ - str]): + host_out: + typing.List[ + str]): self.assertIn( "Can not parse value Dummy of expires in the cookie due to " "invalid format.", diff --git a/tests/unittests/test_nullable_converters.py b/tests/unittests/test_nullable_converters.py new file mode 100644 index 000000000..617d440a1 --- /dev/null +++ b/tests/unittests/test_nullable_converters.py @@ -0,0 +1,93 @@ +import datetime +import unittest + +import pytest +from google.protobuf.timestamp_pb2 import Timestamp + +from azure_functions_worker import protos +from azure_functions_worker.bindings.nullable_converters import \ + to_nullable_string, to_nullable_bool, to_nullable_double, \ + to_nullable_timestamp + +try: + from http.cookies import SimpleCookie +except ImportError: + from Cookie import SimpleCookie + +headers = ['foo=bar; Path=/some/path; Secure', + 'foo2=42; Domain=123; Expires=Thu, 12-Jan-2017 13:55:08 GMT; ' + 'Path=/; Max-Age=dd;'] + +cookies = SimpleCookie('\r\n'.join(headers)) + + +class TestNullableConverters(unittest.TestCase): + def test_to_nullable_string_none(self): + self.assertEqual(to_nullable_string(None, "name"), None) + + def test_to_nullable_string_valid(self): + self.assertEqual(to_nullable_string("dummy", "name"), + protos.NullableString(value="dummy")) + + def test_to_nullable_string_wrong_type(self): + with pytest.raises(Exception) as e: + self.assertEqual(to_nullable_string(123, "name"), + protos.NullableString(value="dummy")) + self.assertEqual(type(e), TypeError) + + def test_to_nullable_bool_none(self): + self.assertEqual(to_nullable_bool(None, "name"), None) + + def test_to_nullable_bool_valid(self): + self.assertEqual(to_nullable_bool(True, "name"), + protos.NullableBool(value=True)) + + def test_to_nullable_bool_wrong_type(self): + with pytest.raises(Exception) as e: + to_nullable_bool("True", "name") + self.assertEqual( + "A 'str' type was expected instead of a '' " + "type. Cannot parse value 123 of 'name'.", + e.msg) + self.assertEqual(type(e), TypeError) + + def test_to_nullable_double_str(self): + self.assertEqual(to_nullable_double("12", "name"), + protos.NullableDouble(value=12)) + + def test_to_nullable_double_int(self): + self.assertEqual(to_nullable_double(12, "name"), + protos.NullableDouble(value=12)) + + def test_to_nullable_double_float(self): + self.assertEqual(to_nullable_double(12.0, "name"), + protos.NullableDouble(value=12)) + + def test_to_nullable_double_none(self): + self.assertEqual(to_nullable_double(None, "name"), None) + + def test_to_nullable_double_wrong_type(self): + with pytest.raises(Exception) as e: + to_nullable_double(object(), "name") + self.assertIn( + "A 'int' or 'float' type was expected instead of a", e.msg) + self.assertEqual(type(e), TypeError) + + def test_to_nullable_timestamp_int(self): + self.assertEqual(to_nullable_timestamp(1000, "datetime"), + protos.NullableTimestamp( + value=Timestamp(seconds=int(1000)))) + + def test_to_nullable_timestamp_datetime(self): + now = datetime.datetime.now() + self.assertEqual(to_nullable_timestamp(now, "datetime"), + protos.NullableTimestamp( + value=Timestamp(seconds=int(now.timestamp())))) + + def test_to_nullable_timestamp_wrong_type(self): + with self.assertRaises(Exception) as e: + to_nullable_timestamp("now", "datetime") + self.assertEqual(type(e), TypeError) + + def test_to_nullable_timestamp_none(self): + self.assertEqual(to_nullable_timestamp(None, "timestamp"), None) From 113d761763fa01e6d295f80aa7d13c494d273f0e Mon Sep 17 00:00:00 2001 From: peterstone2017 Date: Fri, 1 Apr 2022 12:01:12 -0500 Subject: [PATCH 04/17] initial add cookies attempt --- azure_functions_worker/bindings/datumdef.py | 90 +++++++++++++- .../bindings/nullable_converters.py | 113 ++++++++++++++++++ azure_functions_worker/dispatcher.py | 6 - azure_functions_worker/protos/__init__.py | 12 +- .../function.json | 15 +++ .../multiple-set-cookie-resp-headers/main.py | 23 ++++ .../function.json | 15 +++ .../main.py | 18 +++ .../function.json | 15 +++ .../main.py | 18 +++ .../function.json | 15 +++ .../main.py | 15 +++ .../function.json | 15 +++ .../main.py | 15 +++ .../function.json | 15 +++ .../set-cookie-resp-header-empty/main.py | 15 +++ tests/unittests/test_http_functions.py | 51 ++++++++ 17 files changed, 456 insertions(+), 10 deletions(-) create mode 100644 azure_functions_worker/bindings/nullable_converters.py create mode 100644 tests/unittests/http_functions/multiple-set-cookie-resp-headers/function.json create mode 100644 tests/unittests/http_functions/multiple-set-cookie-resp-headers/main.py create mode 100644 tests/unittests/http_functions/response_cookie_header_nullable_bool_err/function.json create mode 100644 tests/unittests/http_functions/response_cookie_header_nullable_bool_err/main.py create mode 100644 tests/unittests/http_functions/response_cookie_header_nullable_double_err/function.json create mode 100644 tests/unittests/http_functions/response_cookie_header_nullable_double_err/main.py create mode 100644 tests/unittests/http_functions/response_cookie_header_nullable_timestamp_err/function.json create mode 100644 tests/unittests/http_functions/response_cookie_header_nullable_timestamp_err/main.py create mode 100644 tests/unittests/http_functions/set-cookie-resp-header-default-values/function.json create mode 100644 tests/unittests/http_functions/set-cookie-resp-header-default-values/main.py create mode 100644 tests/unittests/http_functions/set-cookie-resp-header-empty/function.json create mode 100644 tests/unittests/http_functions/set-cookie-resp-header-empty/main.py diff --git a/azure_functions_worker/bindings/datumdef.py b/azure_functions_worker/bindings/datumdef.py index 2257d38b0..573d418fc 100644 --- a/azure_functions_worker/bindings/datumdef.py +++ b/azure_functions_worker/bindings/datumdef.py @@ -1,10 +1,21 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - +import logging from typing import Any, Optional import json from .. import protos from ..logging import logger +from typing import Union, List +from datetime import datetime +from google.protobuf.timestamp_pb2 import Timestamp +try: + from http.cookies import SimpleCookie +except ImportError: + from Cookie import SimpleCookie +from dateutil import parser +from dateutil.parser import ParserError +from .nullable_converters import to_nullable_bool, to_nullable_string, \ + to_nullable_double, to_nullable_timestamp class Datum: @@ -99,8 +110,8 @@ def from_rpc_shared_memory( shmem: protos.RpcSharedMemory, shmem_mgr) -> Optional['Datum']: """ - Reads the specified shared memory region and converts the read data into - a datum object of the corresponding type. + Reads the specified shared memory region and converts the read data + into a datum object of the corresponding type. """ if shmem is None: logger.warning('Cannot read from shared memory. ' @@ -183,6 +194,7 @@ def datum_as_proto(datum: Datum) -> protos.TypedData: k: v.value for k, v in datum.value['headers'].items() }, + cookies=parse_to_rpc_http_cookie_list(datum.value['cookies']), enable_content_negotiation=False, body=datum_as_proto(datum.value['body']), )) @@ -190,3 +202,75 @@ def datum_as_proto(datum: Datum) -> protos.TypedData: raise NotImplementedError( 'unexpected Datum type: {!r}'.format(datum.type) ) + +def parse_to_rpc_http_cookie_list(cookies: Optional[List[SimpleCookie]]): + if cookies is None: + return cookies + + rpc_http_cookies = [] + + for cookie in cookies: + for name, cookie_entity in cookie.items(): + rpc_http_cookies.append( + protos.RpcHttpCookie(name=name, + value=cookie_entity.value, + domain=to_nullable_string( + cookie_entity['domain'], + 'cookie.domain'), + path= + to_nullable_string( + cookie_entity['path'], + 'cookie.path'), + expires= + to_nullable_timestamp( + parse_cookie_attr_expires( + cookie_entity), + 'cookie.expires'), + secure=to_nullable_bool( + bool(cookie_entity['secure']), + 'cookie.secure'), + http_only=to_nullable_bool( + bool(cookie_entity['httponly']), + 'cookie.httpOnly'), + same_site=parse_cookie_attr_same_site( + cookie_entity), + max_age=to_nullable_double( + cookie_entity['max-age'], + 'cookie.maxAge'))) + + return rpc_http_cookies + + +def parse_cookie_attr_expires(cookie_entity): + expires = cookie_entity['expires'] + + if expires is not None and len(expires) != 0: + try: + return parser.parse(expires) + except ParserError: + logging.error( + f"Can not parse value {expires} of expires in the cookie " + f"due to invalid format.") + raise + except OverflowError: + logging.error( + f"Can not parse value {expires} of expires in the cookie " + f"because the parsed date exceeds the largest valid C " + f"integer on your system.") + raise + + return None + + +def parse_cookie_attr_same_site(cookie_entity): + same_site = getattr(protos.RpcHttpCookie.SameSite, "None") + raw_same_site_str = cookie_entity['samesite'].lower() + + if raw_same_site_str == 'lax': + same_site = protos.RpcHttpCookie.SameSite.Lax + elif raw_same_site_str == 'strict': + same_site = protos.RpcHttpCookie.SameSite.Strict + elif raw_same_site_str == 'none': + same_site = protos.RpcHttpCookie.SameSite.ExplicitNone + + return same_site \ No newline at end of file diff --git a/azure_functions_worker/bindings/nullable_converters.py b/azure_functions_worker/bindings/nullable_converters.py new file mode 100644 index 000000000..5690cebeb --- /dev/null +++ b/azure_functions_worker/bindings/nullable_converters.py @@ -0,0 +1,113 @@ +from datetime import datetime +from typing import Optional, Union + +from google.protobuf.timestamp_pb2 import Timestamp + +from azure_functions_worker import protos, logging +from azure_functions_worker.logging import logger + + +def to_nullable_string(nullable: Optional[str], property_name: str) -> \ + Optional[protos.NullableString]: + """Converts string input to an 'NullableString' to be sent through the + RPC layer. Input that is not a string but is also not null or undefined + logs a function app level warning. + + :param nullable Input to be converted to an NullableString if it is a + valid string + :param property_name The name of the property that the caller will + assign the output to. Used for debugging. + """ + if isinstance(nullable, str): + return protos.NullableString(value=nullable) + + if nullable is not None: + raise TypeError( + f"A 'str' type was expected instead of a '${type(nullable)}' " + f"type. Cannot parse value {nullable} of '${property_name}'.") + + return None + + +def to_nullable_bool(nullable: Optional[bool], property_name: str) -> Optional[ + protos.NullableBool]: + """Converts boolean input to an 'NullableBool' to be sent through the + RPC layer. Input that is not a boolean but is also not null or undefined + logs a function app level warning. + + :param nullable Input to be converted to an NullableBool if it is a + valid boolean + :param property_name The name of the property that the caller will + assign the output to. Used for debugging. + """ + if isinstance(nullable, bool): + return protos.NullableBool(value=nullable) + + if nullable is not None: + raise TypeError( + f"A 'bool' type was expected instead of a '{type(nullable)}' " + f"type. Cannot parse value {nullable} of '{property_name}'.") + + return None + + +def to_nullable_double(nullable: Optional[Union[str, int, float]], + property_name: str) -> \ + Optional[protos.NullableDouble]: + """Converts int or float or str that parses to a number to an + 'NullableDouble' to be sent through the RPC layer. Input that is not a + valid number but is also not null or undefined logs a function app level + warning. + :param nullable Input to be converted to an NullableDouble if it is a + valid number + :param property_name The name of the property that the caller will + assign the output to. Used for debugging. + """ + if isinstance(nullable, int) or isinstance(nullable, float): + return protos.NullableDouble(value=nullable) + elif isinstance(nullable, str): + if len(nullable) == 0: + return None + + try: + return protos.NullableDouble(value=float(nullable)) + except Exception: + raise TypeError( + f"Cannot parse value {nullable} of '{property_name}' to " + f"float.") + + if nullable is not None: + raise TypeError( + f"A 'int' or 'float'" + f" type was expected instead of a '{type(nullable)}' " + f"type. Cannot parse value {nullable} of '{property_name}'.") + + return None + + +def to_nullable_timestamp(date_time: Optional[Union[datetime, int]], + property_name: str) -> protos.NullableTimestamp: + """Converts Date or number input to an 'NullableTimestamp' to be sent + through the RPC layer. Input that is not a Date or number but is also + not null or undefined logs a function app level warning. + + :param date_time Input to be converted to an NullableTimestamp if it is + valid input + :param property_name The name of the property that the caller will + assign the output to. Used for debugging. + """ + logger.info(date_time) + logger.info(type(date_time)) + if date_time is not None: + try: + time_in_seconds = date_time if type( + date_time) == 'int' else date_time.timestamp() + + return protos.NullableTimestamp( + value=Timestamp(seconds=int(time_in_seconds))) + except Exception: + raise TypeError( + f"A 'datetime' or 'int'" + f" type was expected instead of a '{type(date_time)}' " + f"type. Cannot parse value {date_time} of '{property_name}'.") + return None diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py index 03c29478b..50ed72a09 100644 --- a/azure_functions_worker/dispatcher.py +++ b/azure_functions_worker/dispatcher.py @@ -211,12 +211,6 @@ def on_logging(self, record: logging.LogRecord, formatted_msg: str) -> None: if invocation_id is not None: log['invocation_id'] = invocation_id - # XXX: When an exception field is set in RpcLog, WebHost doesn't - # wait for the call result and simply aborts the execution. - # - # if record.exc_info and record.exc_info[1] is not None: - # log['exception'] = self._serialize_exception(record.exc_info[1]) - self._grpc_resp_queue.put_nowait( protos.StreamingMessage( request_id=self.request_id, diff --git a/azure_functions_worker/protos/__init__.py b/azure_functions_worker/protos/__init__.py index 827a0df72..45c31d375 100644 --- a/azure_functions_worker/protos/__init__.py +++ b/azure_functions_worker/protos/__init__.py @@ -24,8 +24,18 @@ ParameterBinding, TypedData, RpcHttp, + RpcHttpCookie, RpcLog, RpcSharedMemory, RpcDataType, CloseSharedMemoryResourcesRequest, - CloseSharedMemoryResourcesResponse) + CloseSharedMemoryResourcesResponse, + FunctionsMetadataRequest, + FunctionMetadataResponse) + +from .shared.NullableTypes_pb2 import ( + NullableString, + NullableBool, + NullableDouble, + NullableTimestamp +) diff --git a/tests/unittests/http_functions/multiple-set-cookie-resp-headers/function.json b/tests/unittests/http_functions/multiple-set-cookie-resp-headers/function.json new file mode 100644 index 000000000..5d4d8285f --- /dev/null +++ b/tests/unittests/http_functions/multiple-set-cookie-resp-headers/function.json @@ -0,0 +1,15 @@ +{ + "scriptFile": "main.py", + "bindings": [ + { + "type": "httpTrigger", + "direction": "in", + "name": "req" + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} diff --git a/tests/unittests/http_functions/multiple-set-cookie-resp-headers/main.py b/tests/unittests/http_functions/multiple-set-cookie-resp-headers/main.py new file mode 100644 index 000000000..096f573dd --- /dev/null +++ b/tests/unittests/http_functions/multiple-set-cookie-resp-headers/main.py @@ -0,0 +1,23 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import logging + +import azure.functions as func + + +def main(req: func.HttpRequest): + logging.info('Python HTTP trigger function processed a request.') + resp = func.HttpResponse( + "This HTTP triggered function executed successfully.") + + resp.headers.add("Set-Cookie", + 'foo3=42; Domain=example.com; Expires=Thu, 12-Jan-2017 ' + '13:55:08 GMT; Path=/; Max-Age=10000000; Secure; ' + 'HttpOnly') + resp.headers.add("Set-Cookie", + 'foo3=43; Domain=example.com; Expires=Thu, 12-Jan-2018 ' + '13:55:08 GMT; Path=/; Max-Age=10000000; Secure; ' + 'HttpOnly') + resp.headers.add("HELLO", 'world') + + return resp \ No newline at end of file diff --git a/tests/unittests/http_functions/response_cookie_header_nullable_bool_err/function.json b/tests/unittests/http_functions/response_cookie_header_nullable_bool_err/function.json new file mode 100644 index 000000000..5d4d8285f --- /dev/null +++ b/tests/unittests/http_functions/response_cookie_header_nullable_bool_err/function.json @@ -0,0 +1,15 @@ +{ + "scriptFile": "main.py", + "bindings": [ + { + "type": "httpTrigger", + "direction": "in", + "name": "req" + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} diff --git a/tests/unittests/http_functions/response_cookie_header_nullable_bool_err/main.py b/tests/unittests/http_functions/response_cookie_header_nullable_bool_err/main.py new file mode 100644 index 000000000..630a33dff --- /dev/null +++ b/tests/unittests/http_functions/response_cookie_header_nullable_bool_err/main.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import logging + +import azure.functions as func + + +def main(req: func.HttpRequest): + logging.info('Python HTTP trigger function processed a request.') + resp = func.HttpResponse( + "This HTTP triggered function executed successfully.") + + resp.headers.add("Set-Cookie", + 'foo3=42; Domain=example.com; Expires=Thu, 12-Jan-2017 ' + '13:55:08 GMT; Path=/; Max-Age=10000000; SecureFalse; ' + 'HttpOnly') + + return resp diff --git a/tests/unittests/http_functions/response_cookie_header_nullable_double_err/function.json b/tests/unittests/http_functions/response_cookie_header_nullable_double_err/function.json new file mode 100644 index 000000000..5d4d8285f --- /dev/null +++ b/tests/unittests/http_functions/response_cookie_header_nullable_double_err/function.json @@ -0,0 +1,15 @@ +{ + "scriptFile": "main.py", + "bindings": [ + { + "type": "httpTrigger", + "direction": "in", + "name": "req" + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} diff --git a/tests/unittests/http_functions/response_cookie_header_nullable_double_err/main.py b/tests/unittests/http_functions/response_cookie_header_nullable_double_err/main.py new file mode 100644 index 000000000..81601b8b9 --- /dev/null +++ b/tests/unittests/http_functions/response_cookie_header_nullable_double_err/main.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import logging + +import azure.functions as func + + +def main(req: func.HttpRequest): + logging.info('Python HTTP trigger function processed a request.') + resp = func.HttpResponse( + "This HTTP triggered function executed successfully.") + + resp.headers.add("Set-Cookie", + 'foo3=42; Domain=example.com; Expires=Thu, 12-Jan-2017 ' + '13:55:08 GMT; Path=/; Max-Age=Dummy; SecureFalse; ' + 'HttpOnly') + + return resp diff --git a/tests/unittests/http_functions/response_cookie_header_nullable_timestamp_err/function.json b/tests/unittests/http_functions/response_cookie_header_nullable_timestamp_err/function.json new file mode 100644 index 000000000..5d4d8285f --- /dev/null +++ b/tests/unittests/http_functions/response_cookie_header_nullable_timestamp_err/function.json @@ -0,0 +1,15 @@ +{ + "scriptFile": "main.py", + "bindings": [ + { + "type": "httpTrigger", + "direction": "in", + "name": "req" + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} diff --git a/tests/unittests/http_functions/response_cookie_header_nullable_timestamp_err/main.py b/tests/unittests/http_functions/response_cookie_header_nullable_timestamp_err/main.py new file mode 100644 index 000000000..6a7c8cfef --- /dev/null +++ b/tests/unittests/http_functions/response_cookie_header_nullable_timestamp_err/main.py @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import logging + +import azure.functions as func + + +def main(req: func.HttpRequest): + logging.info('Python HTTP trigger function processed a request.') + resp = func.HttpResponse( + "This HTTP triggered function executed successfully.") + + resp.headers.add("Set-Cookie", 'foo=bar; Domain=123; Expires=Dummy') + + return resp diff --git a/tests/unittests/http_functions/set-cookie-resp-header-default-values/function.json b/tests/unittests/http_functions/set-cookie-resp-header-default-values/function.json new file mode 100644 index 000000000..5d4d8285f --- /dev/null +++ b/tests/unittests/http_functions/set-cookie-resp-header-default-values/function.json @@ -0,0 +1,15 @@ +{ + "scriptFile": "main.py", + "bindings": [ + { + "type": "httpTrigger", + "direction": "in", + "name": "req" + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} diff --git a/tests/unittests/http_functions/set-cookie-resp-header-default-values/main.py b/tests/unittests/http_functions/set-cookie-resp-header-default-values/main.py new file mode 100644 index 000000000..a29b693b9 --- /dev/null +++ b/tests/unittests/http_functions/set-cookie-resp-header-default-values/main.py @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import logging + +import azure.functions as func + + +def main(req: func.HttpRequest): + logging.info('Python HTTP trigger function processed a request.') + resp = func.HttpResponse( + "This HTTP triggered function executed successfully.") + + resp.headers.add("Set-Cookie", 'foo=bar') + + return resp diff --git a/tests/unittests/http_functions/set-cookie-resp-header-empty/function.json b/tests/unittests/http_functions/set-cookie-resp-header-empty/function.json new file mode 100644 index 000000000..5d4d8285f --- /dev/null +++ b/tests/unittests/http_functions/set-cookie-resp-header-empty/function.json @@ -0,0 +1,15 @@ +{ + "scriptFile": "main.py", + "bindings": [ + { + "type": "httpTrigger", + "direction": "in", + "name": "req" + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} diff --git a/tests/unittests/http_functions/set-cookie-resp-header-empty/main.py b/tests/unittests/http_functions/set-cookie-resp-header-empty/main.py new file mode 100644 index 000000000..3e33bc8e4 --- /dev/null +++ b/tests/unittests/http_functions/set-cookie-resp-header-empty/main.py @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import logging + +import azure.functions as func + + +def main(req: func.HttpRequest): + logging.info('Python HTTP trigger function processed a request.') + resp = func.HttpResponse( + "This HTTP triggered function executed successfully.") + + resp.headers.add("Set-Cookie", '') + + return resp diff --git a/tests/unittests/test_http_functions.py b/tests/unittests/test_http_functions.py index 3b2fe8c00..90670a64b 100644 --- a/tests/unittests/test_http_functions.py +++ b/tests/unittests/test_http_functions.py @@ -352,6 +352,57 @@ def test_print_to_console_stdout(self): self.assertEqual(r.status_code, 200) self.assertEqual(r.text, 'OK-print-logging') + def test_multiple_cookie_header_in_response(self): + r = self.webhost.request('GET', 'multiple-set-cookie-resp-headers') + self.assertEqual(r.status_code, 200) + self.assertEqual(r.headers.get( + 'Set-Cookie'), + "foo3=42; expires=Thu, 12 Jan 2017 13:55:08 GMT; " + "max-age=10000000; domain=example.com; path=/; secure; httponly, " + "foo3=43; expires=Fri, 12 Jan 2018 13:55:08 GMT; " + "max-age=10000000; domain=example.com; path=/; secure; httponly") + + def test_set_cookie_header_in_response_empty_value(self): + r = self.webhost.request('GET', 'set-cookie-resp-header-empty') + self.assertEqual(r.status_code, 200) + self.assertEqual(r.headers.get('Set-Cookie'), None) + + def test_set_cookie_header_in_response_default_value(self): + r = self.webhost.request('GET', + 'set-cookie-resp-header-default-values') + self.assertEqual(r.status_code, 200) + self.assertEqual(r.headers.get('Set-Cookie'), + 'foo=bar; domain=; path=') + + def test_response_cookie_header_nullable_timestamp_err(self): + r = self.webhost.request( + 'GET', + 'response_cookie_header_nullable_timestamp_err') + self.assertEqual(r.status_code, 500) + + def check_log_response_cookie_header_nullable_timestamp_err(self, + host_out: + typing.List[ + str]): + self.assertIn( + "Can not parse value Dummy of expires in the cookie due to " + "invalid format.", + host_out) + + def test_response_cookie_header_nullable_bool_err(self): + r = self.webhost.request( + 'GET', + 'response_cookie_header_nullable_bool_err') + self.assertEqual(r.status_code, 200) + self.assertFalse("Set-Cookie" in r.headers) + + def test_response_cookie_header_nullable_double_err(self): + r = self.webhost.request( + 'GET', + 'response_cookie_header_nullable_double_err') + self.assertEqual(r.status_code, 200) + self.assertFalse("Set-Cookie" in r.headers) + @pytest.mark.flaky(reruns=3) def check_log_print_to_console_stdout(self, host_out: typing.List[str]): # System logs stdout should not exist in host_out From 3fe9f263cfbb428bfbd676b60229307724935eed Mon Sep 17 00:00:00 2001 From: peterstone2017 Date: Fri, 8 Apr 2022 10:50:30 -0500 Subject: [PATCH 05/17] exclude workindexing proto api from this pr --- azure_functions_worker/protos/__init__.py | 4 +--- tests/test_random.py | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 tests/test_random.py diff --git a/azure_functions_worker/protos/__init__.py b/azure_functions_worker/protos/__init__.py index 45c31d375..843904ae1 100644 --- a/azure_functions_worker/protos/__init__.py +++ b/azure_functions_worker/protos/__init__.py @@ -29,9 +29,7 @@ RpcSharedMemory, RpcDataType, CloseSharedMemoryResourcesRequest, - CloseSharedMemoryResourcesResponse, - FunctionsMetadataRequest, - FunctionMetadataResponse) + CloseSharedMemoryResourcesResponse) from .shared.NullableTypes_pb2 import ( NullableString, diff --git a/tests/test_random.py b/tests/test_random.py new file mode 100644 index 000000000..48aa7ee0c --- /dev/null +++ b/tests/test_random.py @@ -0,0 +1,21 @@ +import unittest + +from azure_functions_worker.bindings.datumdef import parse_cookie_attr_expires, \ + parse_to_rpc_http_cookie_list + +try: + from http.cookies import SimpleCookie +except ImportError: + from Cookie import SimpleCookie +from dateutil import parser + +headers = ['foo=bar; Path=/some/path; Secure', + 'foo2=42; Domain=123; Expires=Thu, 12-Jan-2017 13:55:08 GMT; ' + 'Path=/; Max-Age=dd;'] + +cookies = SimpleCookie('\r\n'.join(headers)) + + +class TestHttpFunctions(unittest.TestCase): + def test_multiple_cookie_header_in_response(self): + print(parse_to_rpc_http_cookie_list([cookies])) From 1df97897dff32bb27097f2293b8f1f4215f01abb Mon Sep 17 00:00:00 2001 From: peterstone2017 Date: Mon, 11 Apr 2022 17:50:26 -0500 Subject: [PATCH 06/17] delete random test --- tests/test_random.py | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 tests/test_random.py diff --git a/tests/test_random.py b/tests/test_random.py deleted file mode 100644 index 48aa7ee0c..000000000 --- a/tests/test_random.py +++ /dev/null @@ -1,21 +0,0 @@ -import unittest - -from azure_functions_worker.bindings.datumdef import parse_cookie_attr_expires, \ - parse_to_rpc_http_cookie_list - -try: - from http.cookies import SimpleCookie -except ImportError: - from Cookie import SimpleCookie -from dateutil import parser - -headers = ['foo=bar; Path=/some/path; Secure', - 'foo2=42; Domain=123; Expires=Thu, 12-Jan-2017 13:55:08 GMT; ' - 'Path=/; Max-Age=dd;'] - -cookies = SimpleCookie('\r\n'.join(headers)) - - -class TestHttpFunctions(unittest.TestCase): - def test_multiple_cookie_header_in_response(self): - print(parse_to_rpc_http_cookie_list([cookies])) From 07e762165af006e74d9c34bb11af7725a9a8582e Mon Sep 17 00:00:00 2001 From: peterstone2017 Date: Tue, 3 May 2022 15:53:44 -0500 Subject: [PATCH 07/17] update multi-ccokie tests;add pystein tests --- .../http_functions_stein/function_app.py | 85 +++++++++++++++++++ .../function.json | 0 .../main.py | 0 .../function.json | 0 .../main.py | 0 .../function.json | 0 .../main.py | 0 tests/unittests/test_http_functions.py | 6 +- 8 files changed, 88 insertions(+), 3 deletions(-) rename tests/unittests/http_functions/{multiple-set-cookie-resp-headers => multiple_set_cookie_resp_headers}/function.json (100%) rename tests/unittests/http_functions/{multiple-set-cookie-resp-headers => multiple_set_cookie_resp_headers}/main.py (100%) rename tests/unittests/http_functions/{set-cookie-resp-header-default-values => set_cookie_resp_header_default_values}/function.json (100%) rename tests/unittests/http_functions/{set-cookie-resp-header-default-values => set_cookie_resp_header_default_values}/main.py (100%) rename tests/unittests/http_functions/{set-cookie-resp-header-empty => set_cookie_resp_header_empty}/function.json (100%) rename tests/unittests/http_functions/{set-cookie-resp-header-empty => set_cookie_resp_header_empty}/main.py (100%) diff --git a/tests/unittests/http_functions/http_functions_stein/function_app.py b/tests/unittests/http_functions/http_functions_stein/function_app.py index 7abf2be96..7faac469e 100644 --- a/tests/unittests/http_functions/http_functions_stein/function_app.py +++ b/tests/unittests/http_functions/http_functions_stein/function_app.py @@ -318,3 +318,88 @@ def user_event_loop(req: func.HttpRequest) -> func.HttpResponse: loop.run_until_complete(try_log()) loop.close() return 'OK-user-event-loop' + + +@app.route(route="multiple_set_cookie_resp_headers") +def multiple_set_cookie_resp_headers( + req: func.HttpRequest) -> func.HttpResponse: + logging.info('Python HTTP trigger function processed a request.') + resp = func.HttpResponse( + "This HTTP triggered function executed successfully.") + + resp.headers.add("Set-Cookie", + 'foo3=42; Domain=example.com; Expires=Thu, 12-Jan-2017 ' + '13:55:08 GMT; Path=/; Max-Age=10000000; Secure; ' + 'HttpOnly') + resp.headers.add("Set-Cookie", + 'foo3=43; Domain=example.com; Expires=Thu, 12-Jan-2018 ' + '13:55:08 GMT; Path=/; Max-Age=10000000; Secure; ' + 'HttpOnly') + resp.headers.add("HELLO", 'world') + + return resp + + +@app.route(route="response_cookie_header_nullable_bool_err") +def response_cookie_header_nullable_bool_err( + req: func.HttpRequest) -> func.HttpResponse: + logging.info('Python HTTP trigger function processed a request.') + resp = func.HttpResponse( + "This HTTP triggered function executed successfully.") + + resp.headers.add("Set-Cookie", + 'foo3=42; Domain=example.com; Expires=Thu, 12-Jan-2017 ' + '13:55:08 GMT; Path=/; Max-Age=10000000; SecureFalse; ' + 'HttpOnly') + + return resp + + +@app.route(route="response_cookie_header_nullable_double_err") +def response_cookie_header_nullable_double_err( + req: func.HttpRequest) -> func.HttpResponse: + logging.info('Python HTTP trigger function processed a request.') + resp = func.HttpResponse( + "This HTTP triggered function executed successfully.") + + resp.headers.add("Set-Cookie", + 'foo3=42; Domain=example.com; Expires=Thu, 12-Jan-2017 ' + '13:55:08 GMT; Path=/; Max-Age=Dummy; SecureFalse; ' + 'HttpOnly') + + return resp + + +@app.route(route="response_cookie_header_nullable_timestamp_err") +def response_cookie_header_nullable_timestamp_err( + req: func.HttpRequest) -> func.HttpResponse: + logging.info('Python HTTP trigger function processed a request.') + resp = func.HttpResponse( + "This HTTP triggered function executed successfully.") + + resp.headers.add("Set-Cookie", 'foo=bar; Domain=123; Expires=Dummy') + + return resp + + +@app.route(route="set_cookie_resp_header_default_values") +def set_cookie_resp_header_default_values( + req: func.HttpRequest) -> func.HttpResponse: + logging.info('Python HTTP trigger function processed a request.') + resp = func.HttpResponse( + "This HTTP triggered function executed successfully.") + + resp.headers.add("Set-Cookie", 'foo=bar') + + return resp + +@app.route(route="set_cookie_resp_header_empty") +def set_cookie_resp_header_empty( + req: func.HttpRequest) -> func.HttpResponse: + logging.info('Python HTTP trigger function processed a request.') + resp = func.HttpResponse( + "This HTTP triggered function executed successfully.") + + resp.headers.add("Set-Cookie", '') + + return resp \ No newline at end of file diff --git a/tests/unittests/http_functions/multiple-set-cookie-resp-headers/function.json b/tests/unittests/http_functions/multiple_set_cookie_resp_headers/function.json similarity index 100% rename from tests/unittests/http_functions/multiple-set-cookie-resp-headers/function.json rename to tests/unittests/http_functions/multiple_set_cookie_resp_headers/function.json diff --git a/tests/unittests/http_functions/multiple-set-cookie-resp-headers/main.py b/tests/unittests/http_functions/multiple_set_cookie_resp_headers/main.py similarity index 100% rename from tests/unittests/http_functions/multiple-set-cookie-resp-headers/main.py rename to tests/unittests/http_functions/multiple_set_cookie_resp_headers/main.py diff --git a/tests/unittests/http_functions/set-cookie-resp-header-default-values/function.json b/tests/unittests/http_functions/set_cookie_resp_header_default_values/function.json similarity index 100% rename from tests/unittests/http_functions/set-cookie-resp-header-default-values/function.json rename to tests/unittests/http_functions/set_cookie_resp_header_default_values/function.json diff --git a/tests/unittests/http_functions/set-cookie-resp-header-default-values/main.py b/tests/unittests/http_functions/set_cookie_resp_header_default_values/main.py similarity index 100% rename from tests/unittests/http_functions/set-cookie-resp-header-default-values/main.py rename to tests/unittests/http_functions/set_cookie_resp_header_default_values/main.py diff --git a/tests/unittests/http_functions/set-cookie-resp-header-empty/function.json b/tests/unittests/http_functions/set_cookie_resp_header_empty/function.json similarity index 100% rename from tests/unittests/http_functions/set-cookie-resp-header-empty/function.json rename to tests/unittests/http_functions/set_cookie_resp_header_empty/function.json diff --git a/tests/unittests/http_functions/set-cookie-resp-header-empty/main.py b/tests/unittests/http_functions/set_cookie_resp_header_empty/main.py similarity index 100% rename from tests/unittests/http_functions/set-cookie-resp-header-empty/main.py rename to tests/unittests/http_functions/set_cookie_resp_header_empty/main.py diff --git a/tests/unittests/test_http_functions.py b/tests/unittests/test_http_functions.py index 5e42320c4..378857d6b 100644 --- a/tests/unittests/test_http_functions.py +++ b/tests/unittests/test_http_functions.py @@ -350,7 +350,7 @@ def test_print_to_console_stdout(self): self.assertEqual(r.text, 'OK-print-logging') def test_multiple_cookie_header_in_response(self): - r = self.webhost.request('GET', 'multiple-set-cookie-resp-headers') + r = self.webhost.request('GET', 'multiple_set_cookie_resp_headers') self.assertEqual(r.status_code, 200) self.assertEqual(r.headers.get( 'Set-Cookie'), @@ -360,13 +360,13 @@ def test_multiple_cookie_header_in_response(self): "max-age=10000000; domain=example.com; path=/; secure; httponly") def test_set_cookie_header_in_response_empty_value(self): - r = self.webhost.request('GET', 'set-cookie-resp-header-empty') + r = self.webhost.request('GET', 'set_cookie_resp_header_empty') self.assertEqual(r.status_code, 200) self.assertEqual(r.headers.get('Set-Cookie'), None) def test_set_cookie_header_in_response_default_value(self): r = self.webhost.request('GET', - 'set-cookie-resp-header-default-values') + 'set_cookie_resp_header_default_values') self.assertEqual(r.status_code, 200) self.assertEqual(r.headers.get('Set-Cookie'), 'foo=bar; domain=; path=') From b1a6dc1277a8bba5308edea869601361e34e0a04 Mon Sep 17 00:00:00 2001 From: peterstone2017 Date: Tue, 3 May 2022 16:07:57 -0500 Subject: [PATCH 08/17] change to self.assertEqual --- tests/unittests/test_datumref.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/unittests/test_datumref.py b/tests/unittests/test_datumref.py index adb860f2f..2020ca72b 100644 --- a/tests/unittests/test_datumref.py +++ b/tests/unittests/test_datumref.py @@ -16,13 +16,13 @@ class TestDatumRef(unittest.TestCase): def test_parse_cookie_attr_expires_none(self): - self.assertEquals(parse_cookie_attr_expires({"expires": None}), None) + self.assertEqual(parse_cookie_attr_expires({"expires": None}), None) def test_parse_cookie_attr_expires_zero_length(self): - self.assertEquals(parse_cookie_attr_expires({"expires": ""}), None) + self.assertEqual(parse_cookie_attr_expires({"expires": ""}), None) def test_parse_cookie_attr_expires_valid(self): - self.assertEquals(parse_cookie_attr_expires( + self.assertEqual(parse_cookie_attr_expires( {"expires": "Thu, 12-Jan-2017 13:55:08 GMT"}), parser.parse("Thu, 12-Jan-2017 13:55:08 GMT")) @@ -32,27 +32,27 @@ def test_parse_cookie_attr_expires_parse_error(self): {"expires": "Thu, 12-Jan-2017 13:550:08 GMT"}) def test_parse_cookie_attr_same_site_default(self): - self.assertEquals(parse_cookie_attr_same_site( + self.assertEqual(parse_cookie_attr_same_site( {}), getattr(protos.RpcHttpCookie.SameSite, "None")) def test_parse_cookie_attr_same_site_lax(self): - self.assertEquals(parse_cookie_attr_same_site( + self.assertEqual(parse_cookie_attr_same_site( {'samesite': 'lax'}), getattr(protos.RpcHttpCookie.SameSite, "Lax")) def test_parse_cookie_attr_same_site_strict(self): - self.assertEquals(parse_cookie_attr_same_site( + self.assertEqual(parse_cookie_attr_same_site( {'samesite': 'strict'}), getattr(protos.RpcHttpCookie.SameSite, "Strict")) def test_parse_cookie_attr_same_site_explicit_none(self): - self.assertEquals(parse_cookie_attr_same_site( + self.assertEqual(parse_cookie_attr_same_site( {'samesite': 'none'}), getattr(protos.RpcHttpCookie.SameSite, "ExplicitNone")) def test_parse_to_rpc_http_cookie_list_none(self): - self.assertEquals(parse_to_rpc_http_cookie_list(None), None) + self.assertEqual(parse_to_rpc_http_cookie_list(None), None) def test_parse_to_rpc_http_cookie_list_valid(self): headers = [ @@ -116,5 +116,5 @@ def test_parse_to_rpc_http_cookie_list_valid(self): 'cookie.maxAge')) rpc_cookies = parse_to_rpc_http_cookie_list([cookies]) - self.assertEquals(cookie1, rpc_cookies[0]) - self.assertEquals(cookie2, rpc_cookies[1]) + self.assertEqual(cookie1, rpc_cookies[0]) + self.assertEqual(cookie2, rpc_cookies[1]) From a57156c11994a2ce6880b6e8e5bae666ce797d03 Mon Sep 17 00:00:00 2001 From: peterstone2017 Date: Tue, 3 May 2022 18:31:10 -0500 Subject: [PATCH 09/17] fix flakey8 --- .../http_functions/http_functions_stein/function_app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unittests/http_functions/http_functions_stein/function_app.py b/tests/unittests/http_functions/http_functions_stein/function_app.py index 7faac469e..4dd703034 100644 --- a/tests/unittests/http_functions/http_functions_stein/function_app.py +++ b/tests/unittests/http_functions/http_functions_stein/function_app.py @@ -393,6 +393,7 @@ def set_cookie_resp_header_default_values( return resp + @app.route(route="set_cookie_resp_header_empty") def set_cookie_resp_header_empty( req: func.HttpRequest) -> func.HttpResponse: @@ -402,4 +403,4 @@ def set_cookie_resp_header_empty( resp.headers.add("Set-Cookie", '') - return resp \ No newline at end of file + return resp From 47f51f5388494fe40c2a9d893b44cc0622c90078 Mon Sep 17 00:00:00 2001 From: peterstone2017 Date: Tue, 3 May 2022 18:38:01 -0500 Subject: [PATCH 10/17] fix flakey8 --- azure_functions_worker/protos/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/azure_functions_worker/protos/__init__.py b/azure_functions_worker/protos/__init__.py index 26d9106de..45c31d375 100644 --- a/azure_functions_worker/protos/__init__.py +++ b/azure_functions_worker/protos/__init__.py @@ -39,4 +39,3 @@ NullableDouble, NullableTimestamp ) - From a5e26bf3cd676685b46cc52fa7d19fd018664b04 Mon Sep 17 00:00:00 2001 From: peterstone2017 Date: Tue, 3 May 2022 23:35:28 -0500 Subject: [PATCH 11/17] make dateutil install required --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 95484f0ef..5e44b7e49 100644 --- a/setup.py +++ b/setup.py @@ -105,13 +105,13 @@ "grpcio~=1.43.0", "grpcio-tools~=1.43.0", "protobuf~=3.19.3", - "azure-functions==1.11.0" + "azure-functions==1.11.0", + "python-dateutil~=2.8.2" ] EXTRA_REQUIRES = { "dev": [ "azure-eventhub~=5.7.0", # Used for EventHub E2E tests - "python-dateutil~=2.8.2", "pycryptodome~=3.10.1", "flake8~=4.0.1", "mypy", From 48c08d0eb97ed27ed3302bace683b926f5567f1b Mon Sep 17 00:00:00 2001 From: peterstone2017 Date: Thu, 5 May 2022 19:18:22 -0500 Subject: [PATCH 12/17] skip setting multi cookie headers test for py 3.7 --- tests/unittests/test_datumref.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/unittests/test_datumref.py b/tests/unittests/test_datumref.py index 2020ca72b..5d5f1b56c 100644 --- a/tests/unittests/test_datumref.py +++ b/tests/unittests/test_datumref.py @@ -1,5 +1,7 @@ +import sys import unittest from http.cookies import SimpleCookie +from unittest import skipIf from dateutil import parser from dateutil.parser import ParserError @@ -54,6 +56,8 @@ def test_parse_cookie_attr_same_site_explicit_none(self): def test_parse_to_rpc_http_cookie_list_none(self): self.assertEqual(parse_to_rpc_http_cookie_list(None), None) + @skipIf(sys.version_info < (3, 8, 0), + "Skip the tests for Python 3.7 and below") def test_parse_to_rpc_http_cookie_list_valid(self): headers = [ 'foo=bar; Path=/some/path; Secure; HttpOnly; Domain=123; ' From a0f946de54ab062fb7549e49056fc8d403359df0 Mon Sep 17 00:00:00 2001 From: peterstone2017 Date: Fri, 17 Jun 2022 11:30:57 -0500 Subject: [PATCH 13/17] skip 3.7 multi cookie tests --- tests/unittests/test_http_functions.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/unittests/test_http_functions.py b/tests/unittests/test_http_functions.py index 378857d6b..14a6619fd 100644 --- a/tests/unittests/test_http_functions.py +++ b/tests/unittests/test_http_functions.py @@ -4,7 +4,9 @@ import hashlib import os import pathlib +import sys import typing +from unittest import skipIf import pytest @@ -349,6 +351,8 @@ def test_print_to_console_stdout(self): self.assertEqual(r.status_code, 200) self.assertEqual(r.text, 'OK-print-logging') + @skipIf(sys.version_info < (3, 8, 0), + "Skip the tests for Python 3.7 and below") def test_multiple_cookie_header_in_response(self): r = self.webhost.request('GET', 'multiple_set_cookie_resp_headers') self.assertEqual(r.status_code, 200) @@ -359,11 +363,15 @@ def test_multiple_cookie_header_in_response(self): "foo3=43; expires=Fri, 12 Jan 2018 13:55:08 GMT; " "max-age=10000000; domain=example.com; path=/; secure; httponly") + @skipIf(sys.version_info < (3, 8, 0), + "Skip the tests for Python 3.7 and below") def test_set_cookie_header_in_response_empty_value(self): r = self.webhost.request('GET', 'set_cookie_resp_header_empty') self.assertEqual(r.status_code, 200) self.assertEqual(r.headers.get('Set-Cookie'), None) + @skipIf(sys.version_info < (3, 8, 0), + "Skip the tests for Python 3.7 and below") def test_set_cookie_header_in_response_default_value(self): r = self.webhost.request('GET', 'set_cookie_resp_header_default_values') @@ -371,6 +379,8 @@ def test_set_cookie_header_in_response_default_value(self): self.assertEqual(r.headers.get('Set-Cookie'), 'foo=bar; domain=; path=') + @skipIf(sys.version_info < (3, 8, 0), + "Skip the tests for Python 3.7 and below") def test_response_cookie_header_nullable_timestamp_err(self): r = self.webhost.request( 'GET', @@ -386,6 +396,8 @@ def check_log_response_cookie_header_nullable_timestamp_err(self, "invalid format.", host_out) + @skipIf(sys.version_info < (3, 8, 0), + "Skip the tests for Python 3.7 and below") def test_response_cookie_header_nullable_bool_err(self): r = self.webhost.request( 'GET', @@ -393,6 +405,8 @@ def test_response_cookie_header_nullable_bool_err(self): self.assertEqual(r.status_code, 200) self.assertFalse("Set-Cookie" in r.headers) + @skipIf(sys.version_info < (3, 8, 0), + "Skip the tests for Python 3.7 and below") def test_response_cookie_header_nullable_double_err(self): r = self.webhost.request( 'GET', From 6d461d57b38f1d6f5c9632474b9404dc9090855a Mon Sep 17 00:00:00 2001 From: peterstone2017 Date: Thu, 30 Jun 2022 13:17:53 -0500 Subject: [PATCH 14/17] skip linux consumption tests until dateutil goes in --- tests/endtoend/test_linux_consumption.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/endtoend/test_linux_consumption.py b/tests/endtoend/test_linux_consumption.py index cf9039567..769d2380b 100644 --- a/tests/endtoend/test_linux_consumption.py +++ b/tests/endtoend/test_linux_consumption.py @@ -2,7 +2,7 @@ # Licensed under the MIT License. import os import sys -from unittest import TestCase, skipIf +from unittest import TestCase, skip from requests import Request @@ -14,9 +14,7 @@ _DEFAULT_HOST_VERSION = "3" -@skipIf(is_python_version('3.10'), - "Skip the tests for Python 3.10 currently as the mesh images for " - "Python 3.10 aren't available currently.") +@skip class TestLinuxConsumption(TestCase): """Test worker behaviors on specific scenarios. From fcafe6ab46b4df94e3c1323a3cda133c431b65bb Mon Sep 17 00:00:00 2001 From: peterstone2017 Date: Thu, 30 Jun 2022 13:30:06 -0500 Subject: [PATCH 15/17] flakey8 fix --- tests/endtoend/test_linux_consumption.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/endtoend/test_linux_consumption.py b/tests/endtoend/test_linux_consumption.py index 769d2380b..6f566b3cf 100644 --- a/tests/endtoend/test_linux_consumption.py +++ b/tests/endtoend/test_linux_consumption.py @@ -9,7 +9,6 @@ from azure_functions_worker.testutils_lc import ( LinuxConsumptionWebHostController ) -from azure_functions_worker.utils.common import is_python_version _DEFAULT_HOST_VERSION = "3" From a42bed15cc31fab182f405a8b1cef9845b8ac094 Mon Sep 17 00:00:00 2001 From: peterstone2017 Date: Fri, 1 Jul 2022 15:52:54 -0500 Subject: [PATCH 16/17] update cookie tests --- tests/unittests/test_datumref.py | 5 ++++ tests/unittests/test_nullable_converters.py | 33 +++++++++++++++------ 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/tests/unittests/test_datumref.py b/tests/unittests/test_datumref.py index 5d5f1b56c..62768592f 100644 --- a/tests/unittests/test_datumref.py +++ b/tests/unittests/test_datumref.py @@ -33,6 +33,11 @@ def test_parse_cookie_attr_expires_parse_error(self): parse_cookie_attr_expires( {"expires": "Thu, 12-Jan-2017 13:550:08 GMT"}) + def test_parse_cookie_attr_expires_overflow_error(self): + with self.assertRaises(OverflowError): + parse_cookie_attr_expires( + {"expires": "Thu, 12-Jan-9999999999999999 13:550:08 GMT"}) + def test_parse_cookie_attr_same_site_default(self): self.assertEqual(parse_cookie_attr_same_site( {}), diff --git a/tests/unittests/test_nullable_converters.py b/tests/unittests/test_nullable_converters.py index 617d440a1..e147f15ed 100644 --- a/tests/unittests/test_nullable_converters.py +++ b/tests/unittests/test_nullable_converters.py @@ -45,16 +45,28 @@ def test_to_nullable_bool_valid(self): def test_to_nullable_bool_wrong_type(self): with pytest.raises(Exception) as e: to_nullable_bool("True", "name") - self.assertEqual( - "A 'str' type was expected instead of a '' " - "type. Cannot parse value 123 of 'name'.", - e.msg) - self.assertEqual(type(e), TypeError) + + self.assertEqual(e.type, TypeError) + self.assertEqual(e.value.args[0], + "A 'bool' type was expected instead of a '' type. " + "Cannot parse value True of 'name'.") def test_to_nullable_double_str(self): self.assertEqual(to_nullable_double("12", "name"), protos.NullableDouble(value=12)) + def test_to_nullable_double_empty_str(self): + self.assertEqual(to_nullable_double("", "name"), None) + + def test_to_nullable_double_invalid_str(self): + with pytest.raises(TypeError) as e: + to_nullable_double("222d", "name") + + self.assertEqual(e.type, TypeError) + self.assertEqual(e.value.args[0], + "Cannot parse value 222d of 'name' to float.") + def test_to_nullable_double_int(self): self.assertEqual(to_nullable_double(12, "name"), protos.NullableDouble(value=12)) @@ -69,9 +81,12 @@ def test_to_nullable_double_none(self): def test_to_nullable_double_wrong_type(self): with pytest.raises(Exception) as e: to_nullable_double(object(), "name") - self.assertIn( - "A 'int' or 'float' type was expected instead of a", e.msg) - self.assertEqual(type(e), TypeError) + + self.assertIn( + "A 'int' or 'float' type was expected instead of a '' type", + e.value.args[0]) + self.assertEqual(e.type, TypeError) def test_to_nullable_timestamp_int(self): self.assertEqual(to_nullable_timestamp(1000, "datetime"), @@ -87,7 +102,7 @@ def test_to_nullable_timestamp_datetime(self): def test_to_nullable_timestamp_wrong_type(self): with self.assertRaises(Exception) as e: to_nullable_timestamp("now", "datetime") - self.assertEqual(type(e), TypeError) + self.assertEqual(e.type, TypeError) def test_to_nullable_timestamp_none(self): self.assertEqual(to_nullable_timestamp(None, "timestamp"), None) From e8459860e35b0c3be7696f3d1d43260738e4100f Mon Sep 17 00:00:00 2001 From: peterstone2017 Date: Fri, 1 Jul 2022 15:55:44 -0500 Subject: [PATCH 17/17] fix test --- tests/unittests/test_nullable_converters.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/unittests/test_nullable_converters.py b/tests/unittests/test_nullable_converters.py index e147f15ed..6ac617459 100644 --- a/tests/unittests/test_nullable_converters.py +++ b/tests/unittests/test_nullable_converters.py @@ -100,9 +100,8 @@ def test_to_nullable_timestamp_datetime(self): value=Timestamp(seconds=int(now.timestamp())))) def test_to_nullable_timestamp_wrong_type(self): - with self.assertRaises(Exception) as e: + with self.assertRaises(TypeError): to_nullable_timestamp("now", "datetime") - self.assertEqual(e.type, TypeError) def test_to_nullable_timestamp_none(self): self.assertEqual(to_nullable_timestamp(None, "timestamp"), None)