xref: /unit/test/test_asgi_application.py (revision 1848:4bd548074e2c)
1import re
2import time
3from distutils.version import LooseVersion
4
5import pytest
6
7from unit.applications.lang.python import TestApplicationPython
8from unit.option import option
9
10
11class TestASGIApplication(TestApplicationPython):
12    prerequisites = {
13        'modules': {'python': lambda v: LooseVersion(v) >= LooseVersion('3.5')}
14    }
15    load_module = 'asgi'
16
17    def findall(self, pattern):
18        with open(option.temp_dir + '/unit.log', 'r', errors='ignore') as f:
19            return re.findall(pattern, f.read())
20
21    def test_asgi_application_variables(self):
22        self.load('variables')
23
24        body = 'Test body string.'
25
26        resp = self.http(
27            b"""POST / HTTP/1.1
28Host: localhost
29Content-Length: %d
30Custom-Header: blah
31Custom-hEader: Blah
32Content-Type: text/html
33Connection: close
34custom-header: BLAH
35
36%s"""
37            % (len(body), body.encode()),
38            raw=True,
39        )
40
41        assert resp['status'] == 200, 'status'
42        headers = resp['headers']
43        header_server = headers.pop('Server')
44        assert re.search(r'Unit/[\d\.]+', header_server), 'server header'
45
46        date = headers.pop('Date')
47        assert date[-4:] == ' GMT', 'date header timezone'
48        assert (
49            abs(self.date_to_sec_epoch(date) - self.sec_epoch()) < 5
50        ), 'date header'
51
52        assert headers == {
53            'Connection': 'close',
54            'content-length': str(len(body)),
55            'content-type': 'text/html',
56            'request-method': 'POST',
57            'request-uri': '/',
58            'http-host': 'localhost',
59            'http-version': '1.1',
60            'custom-header': 'blah, Blah, BLAH',
61            'asgi-version': '3.0',
62            'asgi-spec-version': '2.1',
63            'scheme': 'http',
64        }, 'headers'
65        assert resp['body'] == body, 'body'
66
67    def test_asgi_application_query_string(self):
68        self.load('query_string')
69
70        resp = self.get(url='/?var1=val1&var2=val2')
71
72        assert (
73            resp['headers']['query-string'] == 'var1=val1&var2=val2'
74        ), 'query-string header'
75
76    def test_asgi_application_query_string_space(self):
77        self.load('query_string')
78
79        resp = self.get(url='/ ?var1=val1&var2=val2')
80        assert (
81            resp['headers']['query-string'] == 'var1=val1&var2=val2'
82        ), 'query-string space'
83
84        resp = self.get(url='/ %20?var1=val1&var2=val2')
85        assert (
86            resp['headers']['query-string'] == 'var1=val1&var2=val2'
87        ), 'query-string space 2'
88
89        resp = self.get(url='/ %20 ?var1=val1&var2=val2')
90        assert (
91            resp['headers']['query-string'] == 'var1=val1&var2=val2'
92        ), 'query-string space 3'
93
94        resp = self.get(url='/blah %20 blah? var1= val1 & var2=val2')
95        assert (
96            resp['headers']['query-string'] == ' var1= val1 & var2=val2'
97        ), 'query-string space 4'
98
99    def test_asgi_application_query_string_empty(self):
100        self.load('query_string')
101
102        resp = self.get(url='/?')
103
104        assert resp['status'] == 200, 'query string empty status'
105        assert resp['headers']['query-string'] == '', 'query string empty'
106
107    def test_asgi_application_query_string_absent(self):
108        self.load('query_string')
109
110        resp = self.get()
111
112        assert resp['status'] == 200, 'query string absent status'
113        assert resp['headers']['query-string'] == '', 'query string absent'
114
115    @pytest.mark.skip('not yet')
116    def test_asgi_application_server_port(self):
117        self.load('server_port')
118
119        assert (
120            self.get()['headers']['Server-Port'] == '7080'
121        ), 'Server-Port header'
122
123    @pytest.mark.skip('not yet')
124    def test_asgi_application_working_directory_invalid(self):
125        self.load('empty')
126
127        assert 'success' in self.conf(
128            '"/blah"', 'applications/empty/working_directory'
129        ), 'configure invalid working_directory'
130
131        assert self.get()['status'] == 500, 'status'
132
133    def test_asgi_application_204_transfer_encoding(self):
134        self.load('204_no_content')
135
136        assert (
137            'Transfer-Encoding' not in self.get()['headers']
138        ), '204 header transfer encoding'
139
140    def test_asgi_application_shm_ack_handle(self):
141        # Minimum possible limit
142        shm_limit = 10 * 1024 * 1024
143
144        self.load('mirror', limits={"shm": shm_limit})
145
146        # Should exceed shm_limit
147        max_body_size = 12 * 1024 * 1024
148
149        assert 'success' in self.conf(
150            '{"http":{"max_body_size": ' + str(max_body_size) + ' }}',
151            'settings',
152        )
153
154        assert self.get()['status'] == 200, 'init'
155
156        body = '0123456789AB' * 1024 * 1024  # 12 Mb
157        resp = self.post(
158            headers={
159                'Host': 'localhost',
160                'Connection': 'close',
161                'Content-Type': 'text/html',
162            },
163            body=body,
164            read_buffer_size=1024 * 1024,
165        )
166
167        assert resp['body'] == body, 'keep-alive 1'
168
169    def test_asgi_keepalive_body(self):
170        self.load('mirror')
171
172        assert self.get()['status'] == 200, 'init'
173
174        body = '0123456789' * 500
175        (resp, sock) = self.post(
176            headers={
177                'Host': 'localhost',
178                'Connection': 'keep-alive',
179                'Content-Type': 'text/html',
180            },
181            start=True,
182            body=body,
183            read_timeout=1,
184        )
185
186        assert resp['body'] == body, 'keep-alive 1'
187
188        body = '0123456789'
189        resp = self.post(
190            headers={
191                'Host': 'localhost',
192                'Connection': 'close',
193                'Content-Type': 'text/html',
194            },
195            sock=sock,
196            body=body,
197        )
198
199        assert resp['body'] == body, 'keep-alive 2'
200
201    def test_asgi_keepalive_reconfigure(self):
202        self.load('mirror')
203
204        assert self.get()['status'] == 200, 'init'
205
206        body = '0123456789'
207        conns = 3
208        socks = []
209
210        for i in range(conns):
211            (resp, sock) = self.post(
212                headers={
213                    'Host': 'localhost',
214                    'Connection': 'keep-alive',
215                    'Content-Type': 'text/html',
216                },
217                start=True,
218                body=body,
219                read_timeout=1,
220            )
221
222            assert resp['body'] == body, 'keep-alive open'
223
224            self.load('mirror', processes=i + 1)
225
226            socks.append(sock)
227
228        for i in range(conns):
229            (resp, sock) = self.post(
230                headers={
231                    'Host': 'localhost',
232                    'Connection': 'keep-alive',
233                    'Content-Type': 'text/html',
234                },
235                start=True,
236                sock=socks[i],
237                body=body,
238                read_timeout=1,
239            )
240
241            assert resp['body'] == body, 'keep-alive request'
242
243            self.load('mirror', processes=i + 1)
244
245        for i in range(conns):
246            resp = self.post(
247                headers={
248                    'Host': 'localhost',
249                    'Connection': 'close',
250                    'Content-Type': 'text/html',
251                },
252                sock=socks[i],
253                body=body,
254            )
255
256            assert resp['body'] == body, 'keep-alive close'
257
258            self.load('mirror', processes=i + 1)
259
260    def test_asgi_keepalive_reconfigure_2(self):
261        self.load('mirror')
262
263        assert self.get()['status'] == 200, 'init'
264
265        body = '0123456789'
266
267        (resp, sock) = self.post(
268            headers={
269                'Host': 'localhost',
270                'Connection': 'keep-alive',
271                'Content-Type': 'text/html',
272            },
273            start=True,
274            body=body,
275            read_timeout=1,
276        )
277
278        assert resp['body'] == body, 'reconfigure 2 keep-alive 1'
279
280        self.load('empty')
281
282        assert self.get()['status'] == 200, 'init'
283
284        (resp, sock) = self.post(
285            headers={
286                'Host': 'localhost',
287                'Connection': 'close',
288                'Content-Type': 'text/html',
289            },
290            start=True,
291            sock=sock,
292            body=body,
293        )
294
295        assert resp['status'] == 200, 'reconfigure 2 keep-alive 2'
296        assert resp['body'] == '', 'reconfigure 2 keep-alive 2 body'
297
298        assert 'success' in self.conf(
299            {"listeners": {}, "applications": {}}
300        ), 'reconfigure 2 clear configuration'
301
302        resp = self.get(sock=sock)
303
304        assert resp == {}, 'reconfigure 2 keep-alive 3'
305
306    def test_asgi_keepalive_reconfigure_3(self):
307        self.load('empty')
308
309        assert self.get()['status'] == 200, 'init'
310
311        (_, sock) = self.http(
312            b"""GET / HTTP/1.1
313""",
314            start=True,
315            raw=True,
316            no_recv=True,
317        )
318
319        assert self.get()['status'] == 200
320
321        assert 'success' in self.conf(
322            {"listeners": {}, "applications": {}}
323        ), 'reconfigure 3 clear configuration'
324
325        resp = self.http(
326            b"""Host: localhost
327Connection: close
328
329""",
330            sock=sock,
331            raw=True,
332        )
333
334        assert resp['status'] == 200, 'reconfigure 3'
335
336    def test_asgi_process_switch(self):
337        self.load('delayed', processes=2)
338
339        self.get(
340            headers={
341                'Host': 'localhost',
342                'Content-Length': '0',
343                'X-Delay': '5',
344                'Connection': 'close',
345            },
346            no_recv=True,
347        )
348
349        headers_delay_1 = {
350            'Connection': 'close',
351            'Host': 'localhost',
352            'Content-Length': '0',
353            'X-Delay': '1',
354        }
355
356        self.get(headers=headers_delay_1, no_recv=True)
357
358        time.sleep(0.5)
359
360        for _ in range(10):
361            self.get(headers=headers_delay_1, no_recv=True)
362
363        self.get(headers=headers_delay_1)
364
365    def test_asgi_application_loading_error(self, skip_alert):
366        skip_alert(r'Python failed to import module "blah"')
367
368        self.load('empty', module="blah")
369
370        assert self.get()['status'] == 503, 'loading error'
371
372    def test_asgi_application_threading(self):
373        """wait_for_record() timeouts after 5s while every thread works at
374        least 3s.  So without releasing GIL test should fail.
375        """
376
377        self.load('threading')
378
379        for _ in range(10):
380            self.get(no_recv=True)
381
382        assert (
383            self.wait_for_record(r'\(5\) Thread: 100', wait=50) is not None
384        ), 'last thread finished'
385
386    def test_asgi_application_threads(self):
387        self.load('threads', threads=2)
388
389        socks = []
390
391        for i in range(2):
392            (_, sock) = self.get(
393                headers={
394                    'Host': 'localhost',
395                    'X-Delay': '3',
396                    'Connection': 'close',
397                },
398                no_recv=True,
399                start=True,
400            )
401
402            socks.append(sock)
403
404            time.sleep(1.0)  # required to avoid greedy request reading
405
406        threads = set()
407
408        for sock in socks:
409            resp = self.recvall(sock).decode('utf-8')
410
411            self.log_in(resp)
412
413            resp = self._resp_to_dict(resp)
414
415            assert resp['status'] == 200, 'status'
416
417            threads.add(resp['headers']['x-thread'])
418
419            sock.close()
420
421        assert len(socks) == len(threads), 'threads differs'
422
423    def test_asgi_application_legacy(self):
424        self.load('legacy')
425
426        resp = self.get(
427            headers={
428                'Host': 'localhost',
429                'Content-Length': '0',
430                'Connection': 'close',
431            },
432        )
433
434        assert resp['status'] == 200, 'status'
435
436    def test_asgi_application_legacy_force(self):
437        self.load('legacy_force', protocol='asgi')
438
439        resp = self.get(
440            headers={
441                'Host': 'localhost',
442                'Content-Length': '0',
443                'Connection': 'close',
444            },
445        )
446
447        assert resp['status'] == 200, 'status'
448