10
10
from azure .functions ._http_asgi import (
11
11
AsgiMiddleware
12
12
)
13
+ import pytest
13
14
14
15
15
16
class MockAsgiApplication :
@@ -18,71 +19,109 @@ class MockAsgiApplication:
18
19
response_headers = [
19
20
[b"content-type" , b"text/plain" ],
20
21
]
22
+ startup_called = False
23
+ shutdown_called = False
24
+
25
+ def __init__ (self , fail_startup = False , fail_shutdown = False ):
26
+ self .fail_startup = fail_startup
27
+ self .fail_shutdown = fail_shutdown
21
28
22
29
async def __call__ (self , scope , receive , send ):
23
30
self .received_scope = scope
24
- # Verify against ASGI specification
25
- assert scope ['type' ] == 'http'
26
- assert isinstance (scope ['type' ], str )
27
31
32
+ # Verify against ASGI specification
28
33
assert scope ['asgi.spec_version' ] in ['2.0' , '2.1' ]
29
34
assert isinstance (scope ['asgi.spec_version' ], str )
30
35
31
36
assert scope ['asgi.version' ] in ['2.0' , '2.1' , '2.2' ]
32
37
assert isinstance (scope ['asgi.version' ], str )
33
38
34
- assert scope ['http_version' ] in ['1.0' , '1.1' , '2' ]
35
- assert isinstance (scope ['http_version' ], str )
36
-
37
- assert scope ['method' ] in ['POST' , 'GET' , 'PUT' , 'DELETE' , 'PATCH' ]
38
- assert isinstance (scope ['method' ], str )
39
-
40
- assert scope ['scheme' ] in ['http' , 'https' ]
41
- assert isinstance (scope ['scheme' ], str )
42
-
43
- assert isinstance (scope ['path' ], str )
44
- assert isinstance (scope ['raw_path' ], bytes )
45
- assert isinstance (scope ['query_string' ], bytes )
46
- assert isinstance (scope ['root_path' ], str )
47
-
48
- assert hasattr (scope ['headers' ], '__iter__' )
49
- for k , v in scope ['headers' ]:
50
- assert isinstance (k , bytes )
51
- assert isinstance (v , bytes )
52
-
53
- assert scope ['client' ] is None or hasattr (scope ['client' ], '__iter__' )
54
- if scope ['client' ]:
55
- assert len (scope ['client' ]) == 2
56
- assert isinstance (scope ['client' ][0 ], str )
57
- assert isinstance (scope ['client' ][1 ], int )
58
-
59
- assert scope ['server' ] is None or hasattr (scope ['server' ], '__iter__' )
60
- if scope ['server' ]:
61
- assert len (scope ['server' ]) == 2
62
- assert isinstance (scope ['server' ][0 ], str )
63
- assert isinstance (scope ['server' ][1 ], int )
64
-
65
- self .received_request = await receive ()
66
- assert self .received_request ['type' ] == 'http.request'
67
- assert isinstance (self .received_request ['body' ], bytes )
68
- assert isinstance (self .received_request ['more_body' ], bool )
69
-
70
- await send (
71
- {
72
- "type" : "http.response.start" ,
73
- "status" : self .response_code ,
74
- "headers" : self .response_headers ,
75
- }
76
- )
77
- await send (
78
- {
79
- "type" : "http.response.body" ,
80
- "body" : self .response_body ,
81
- }
82
- )
39
+ assert isinstance (scope ['type' ], str )
83
40
84
- self .next_request = await receive ()
85
- assert self .next_request ['type' ] == 'http.disconnect'
41
+ if scope ['type' ] == 'lifespan' :
42
+ self .startup_called = True
43
+ startup_message = await receive ()
44
+ assert startup_message ['type' ] == 'lifespan.startup'
45
+ if self .fail_startup :
46
+ if isinstance (self .fail_startup , str ):
47
+ await send ({
48
+ "type" : "lifespan.startup.failed" ,
49
+ "message" : self .fail_startup })
50
+ else :
51
+ await send ({"type" : "lifespan.startup.failed" })
52
+ else :
53
+ await send ({"type" : "lifespan.startup.complete" })
54
+ shutdown_message = await receive ()
55
+ assert shutdown_message ['type' ] == 'lifespan.shutdown'
56
+ if self .fail_shutdown :
57
+ if isinstance (self .fail_shutdown , str ):
58
+ await send ({
59
+ "type" : "lifespan.shutdown.failed" ,
60
+ "message" : self .fail_shutdown })
61
+ else :
62
+ await send ({"type" : "lifespan.shutdown.failed" })
63
+ else :
64
+ await send ({"type" : "lifespan.shutdown.complete" })
65
+
66
+ self .shutdown_called = True
67
+
68
+ elif scope ['type' ] == 'http' :
69
+ assert scope ['http_version' ] in ['1.0' , '1.1' , '2' ]
70
+ assert isinstance (scope ['http_version' ], str )
71
+
72
+ assert scope ['method' ] in ['POST' , 'GET' , 'PUT' , 'DELETE' , 'PATCH' ]
73
+ assert isinstance (scope ['method' ], str )
74
+
75
+ assert scope ['scheme' ] in ['http' , 'https' ]
76
+ assert isinstance (scope ['scheme' ], str )
77
+
78
+ assert isinstance (scope ['path' ], str )
79
+ assert isinstance (scope ['raw_path' ], bytes )
80
+ assert isinstance (scope ['query_string' ], bytes )
81
+ assert isinstance (scope ['root_path' ], str )
82
+
83
+ assert hasattr (scope ['headers' ], '__iter__' )
84
+ for k , v in scope ['headers' ]:
85
+ assert isinstance (k , bytes )
86
+ assert isinstance (v , bytes )
87
+
88
+ assert scope ['client' ] is None or hasattr (scope ['client' ],
89
+ '__iter__' )
90
+ if scope ['client' ]:
91
+ assert len (scope ['client' ]) == 2
92
+ assert isinstance (scope ['client' ][0 ], str )
93
+ assert isinstance (scope ['client' ][1 ], int )
94
+
95
+ assert scope ['server' ] is None or hasattr (scope ['server' ],
96
+ '__iter__' )
97
+ if scope ['server' ]:
98
+ assert len (scope ['server' ]) == 2
99
+ assert isinstance (scope ['server' ][0 ], str )
100
+ assert isinstance (scope ['server' ][1 ], int )
101
+
102
+ self .received_request = await receive ()
103
+ assert self .received_request ['type' ] == 'http.request'
104
+ assert isinstance (self .received_request ['body' ], bytes )
105
+ assert isinstance (self .received_request ['more_body' ], bool )
106
+
107
+ await send (
108
+ {
109
+ "type" : "http.response.start" ,
110
+ "status" : self .response_code ,
111
+ "headers" : self .response_headers ,
112
+ }
113
+ )
114
+ await send (
115
+ {
116
+ "type" : "http.response.body" ,
117
+ "body" : self .response_body ,
118
+ }
119
+ )
120
+
121
+ self .next_request = await receive ()
122
+ assert self .next_request ['type' ] == 'http.disconnect'
123
+ else :
124
+ raise AssertionError (f"unexpected type { scope ['type' ]} " )
86
125
87
126
88
127
class TestHttpAsgiMiddleware (unittest .TestCase ):
@@ -221,3 +260,45 @@ async def main(req, context):
221
260
# Verify asserted
222
261
self .assertEqual (response .status_code , 200 )
223
262
self .assertEqual (response .get_body (), test_body )
263
+
264
+ def test_function_app_lifecycle_events (self ):
265
+ mock_app = MockAsgiApplication ()
266
+ middleware = AsgiMiddleware (mock_app )
267
+ asyncio .get_event_loop ().run_until_complete (
268
+ middleware .notify_startup ()
269
+ )
270
+ assert mock_app .startup_called
271
+
272
+ asyncio .get_event_loop ().run_until_complete (
273
+ middleware .notify_shutdown ()
274
+ )
275
+ assert mock_app .shutdown_called
276
+
277
+ def test_function_app_lifecycle_events_with_failures (self ):
278
+ apps = [
279
+ MockAsgiApplication (False , True ),
280
+ MockAsgiApplication (True , False ),
281
+ MockAsgiApplication (True , True ),
282
+ MockAsgiApplication ("bork" , False ),
283
+ MockAsgiApplication (False , "bork" ),
284
+ MockAsgiApplication ("bork" , "bork" ),
285
+ ]
286
+ for mock_app in apps :
287
+ middleware = AsgiMiddleware (mock_app )
288
+ asyncio .get_event_loop ().run_until_complete (
289
+ middleware .notify_startup ()
290
+ )
291
+ assert mock_app .startup_called
292
+
293
+ asyncio .get_event_loop ().run_until_complete (
294
+ middleware .notify_shutdown ()
295
+ )
296
+ assert mock_app .shutdown_called
297
+
298
+ def test_calling_shutdown_without_startup_errors (self ):
299
+ mock_app = MockAsgiApplication ()
300
+ middleware = AsgiMiddleware (mock_app )
301
+ with pytest .raises (RuntimeError ):
302
+ asyncio .get_event_loop ().run_until_complete (
303
+ middleware .notify_shutdown ()
304
+ )
0 commit comments