Skip to content

Commit 2512b46

Browse files
YunchuWangpeterstone2017
andauthored
log warning for function app without http functions (#138)
* log warning for function app without http functions * add tests * fix flakey * addr comment Co-authored-by: peterstone2017 <[email protected]>
1 parent d2eab5c commit 2512b46

File tree

3 files changed

+96
-4
lines changed

3 files changed

+96
-4
lines changed

azure/functions/decorators/function_app.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Copyright (c) Microsoft Corporation. All rights reserved.
22
# Licensed under the MIT License.
33
import json
4+
import logging
45
from abc import ABC
56
from typing import Callable, Dict, List, Optional, Union, Iterable
67

@@ -46,6 +47,7 @@ def __init__(self, func: Callable, script_file: str):
4647
self._bindings: List[Binding] = []
4748
self.function_script_file = script_file
4849
self.http_type = 'function'
50+
self._is_http_function = False
4951

5052
def add_binding(self, binding: Binding) -> None:
5153
"""Add a binding instance to the function.
@@ -72,7 +74,6 @@ def add_trigger(self, trigger: Trigger) -> None:
7274
f"being added is {trigger.get_dict_repr()}")
7375

7476
self._trigger = trigger
75-
7677
# We still add the trigger info to the bindings to ensure that
7778
# function.json is complete
7879
self._bindings.append(trigger)
@@ -93,6 +94,9 @@ def set_http_type(self, http_type: str) -> None:
9394
"""
9495
self.http_type = http_type
9596

97+
def is_http_function(self) -> bool:
98+
return self._is_http_function
99+
96100
def get_trigger(self) -> Optional[Trigger]:
97101
"""Get attached trigger instance of the function.
98102
@@ -212,6 +216,7 @@ def _validate_function(self,
212216
getattr(trigger, 'init_params').add('auth_level')
213217
setattr(trigger, 'auth_level',
214218
parse_singular_param_to_enum(auth_level, AuthLevel))
219+
self._function._is_http_function = True
215220

216221
def build(self, auth_level: Optional[AuthLevel] = None) -> Function:
217222
"""
@@ -1614,14 +1619,29 @@ def __init__(self, auth_level: Union[AuthLevel, str], *args, **kwargs):
16141619
"""
16151620
DecoratorApi.__init__(self, *args, **kwargs)
16161621
HttpFunctionsAuthLevelMixin.__init__(self, auth_level, *args, **kwargs)
1622+
self._require_auth_level: Optional[bool] = None
16171623

16181624
def get_functions(self) -> List[Function]:
16191625
"""Get the function objects in the function app.
16201626
16211627
:return: List of functions in the function app.
16221628
"""
1623-
return [function_builder.build(self.auth_level) for function_builder
1624-
in self._function_builders]
1629+
functions = [function_builder.build(self.auth_level)
1630+
for function_builder in self._function_builders]
1631+
1632+
if not self._require_auth_level:
1633+
self._require_auth_level = any(
1634+
function.is_http_function() for function in functions)
1635+
1636+
if not self._require_auth_level:
1637+
logging.warning(
1638+
'Auth level is not applied to non http '
1639+
'function app. Ref: '
1640+
'https://docs.microsoft.com/azure/azure-functions/functions'
1641+
'-bindings-http-webhook-trigger?tabs=in-process'
1642+
'%2Cfunctionsv2&pivots=programming-language-python#http-auth')
1643+
1644+
return functions
16251645

16261646
def register_functions(self, function_container: DecoratorApi) -> None:
16271647
"""Register a list of functions in the function app.

tests/decorators/test_decorators.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -867,6 +867,21 @@ def dummy():
867867
"connectionStringSetting": "dummy_str"
868868
})
869869

870+
def test_not_http_function(self):
871+
app = self.func_app
872+
873+
@app.cosmos_db_trigger(arg_name="trigger",
874+
database_name="dummy_db",
875+
collection_name="dummy_collection",
876+
connection_string_setting="dummy_str")
877+
def dummy():
878+
pass
879+
880+
funcs = app.get_functions()
881+
self.assertEqual(len(funcs), 1)
882+
883+
self.assertFalse(funcs[0].is_http_function())
884+
870885
def test_cosmosdb_input_binding(self):
871886
app = self.func_app
872887

@@ -1060,6 +1075,8 @@ def default_auth_level():
10601075
http_func_1 = funcs[0]
10611076
http_func_2 = funcs[1]
10621077

1078+
self.assertTrue(http_func_1.is_http_function())
1079+
self.assertTrue(http_func_2.is_http_function())
10631080
self.assertEqual(http_func_1.get_user_function().__name__,
10641081
"specify_auth_level")
10651082
self.assertEqual(http_func_2.get_user_function().__name__,
@@ -1351,6 +1368,8 @@ def dummy():
13511368
func = self._get_user_function(app)
13521369

13531370
self.assertEqual(len(func.get_bindings()), 1)
1371+
self.assertTrue(func.is_http_function())
1372+
13541373
output = func.get_bindings()[0]
13551374

13561375
self.assertEqual(output.get_dict_repr(), {

tests/decorators/test_function_app.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
BindingDirection, SCRIPT_FILE_NAME
1212
from azure.functions.decorators.function_app import FunctionBuilder, \
1313
FunctionApp, Function, Blueprint, DecoratorApi, AsgiFunctionApp, \
14-
WsgiFunctionApp, HttpFunctionsAuthLevelMixin, FunctionRegister
14+
WsgiFunctionApp, HttpFunctionsAuthLevelMixin, FunctionRegister, TriggerApi
1515
from azure.functions.decorators.http import HttpTrigger, HttpOutput, \
1616
HttpMethod
1717
from tests.decorators.test_core import DummyTrigger
@@ -459,9 +459,48 @@ class DummyFunctionApp(FunctionRegister):
459459
self.assertIsNotNone(getattr(app, "function_name", None))
460460
self.assertIsNotNone(getattr(app, "_validate_type", None))
461461
self.assertIsNotNone(getattr(app, "_configure_function_builder", None))
462+
self.assertIsNone(getattr(app, "_require_auth_level"))
462463
self.assertTrue(hasattr(app, "auth_level"))
463464
self.assertEqual(app.auth_level, AuthLevel.ANONYMOUS)
464465

466+
def test_function_register_http_function_app(self):
467+
class DummyFunctionApp(FunctionRegister, TriggerApi):
468+
pass
469+
470+
app = DummyFunctionApp(auth_level=AuthLevel.ANONYMOUS)
471+
472+
@app.route("name1")
473+
def hello1(name: str):
474+
return "hello"
475+
476+
@app.schedule(arg_name="name", schedule="10****")
477+
def hello2(name: str):
478+
return "hello"
479+
480+
@app.route("name1")
481+
def hello3(name: str):
482+
return "hello"
483+
484+
self.assertIsNone(app._require_auth_level, None)
485+
app.get_functions()
486+
self.assertTrue(app._require_auth_level)
487+
488+
def test_function_register_non_http_function_app(self):
489+
class DummyFunctionApp(FunctionRegister, TriggerApi):
490+
pass
491+
492+
app = DummyFunctionApp(auth_level=AuthLevel.ANONYMOUS)
493+
blueprint = Blueprint()
494+
495+
@blueprint.schedule(arg_name="name", schedule="10****")
496+
def hello(name: str):
497+
return name
498+
499+
app.register_blueprint(blueprint)
500+
501+
app.get_functions()
502+
self.assertFalse(app._require_auth_level)
503+
465504
def test_function_register_register_function_register_error(self):
466505
class DummyFunctionApp(FunctionRegister):
467506
pass
@@ -506,6 +545,13 @@ def test_asgi_function_app_custom(self):
506545
http_auth_level=AuthLevel.ANONYMOUS)
507546
self.assertEqual(app.auth_level, AuthLevel.ANONYMOUS)
508547

548+
def test_asgi_function_app_is_http_function(self):
549+
app = AsgiFunctionApp(app=object())
550+
funcs = app.get_functions()
551+
552+
self.assertEqual(len(funcs), 1)
553+
self.assertTrue(funcs[0].is_http_function())
554+
509555
def test_wsgi_function_app_default(self):
510556
app = WsgiFunctionApp(app=object())
511557
self.assertEqual(app.auth_level, AuthLevel.FUNCTION)
@@ -514,3 +560,10 @@ def test_wsgi_function_app_custom(self):
514560
app = WsgiFunctionApp(app=object(),
515561
http_auth_level=AuthLevel.ANONYMOUS)
516562
self.assertEqual(app.auth_level, AuthLevel.ANONYMOUS)
563+
564+
def test_wsgi_function_app_is_http_function(self):
565+
app = WsgiFunctionApp(app=object())
566+
funcs = app.get_functions()
567+
568+
self.assertEqual(len(funcs), 1)
569+
self.assertTrue(funcs[0].is_http_function())

0 commit comments

Comments
 (0)