Completed
Push — master ( efe9c1...526fb9 )
by Ali-Akber
11s
created

FlaskExtTests.test_custom_key_prefix()   B

Complexity

Conditions 5

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
c 1
b 0
f 0
dl 0
loc 24
rs 8.1671
1
"""
2
3
"""
4
import json
5
import logging
6
import time
7
import unittest
8
9
import hiro
10
import mock
11
import redis
12
import datetime
13
from flask import Flask, Blueprint, request, current_app, make_response
14
from flask.ext import restful
15
from flask.ext.restful import Resource
16
from flask.views import View, MethodView
17
from limits.errors import ConfigurationError
18
from limits.storage import MemcachedStorage
19
from limits.strategies import MovingWindowRateLimiter
20
21
from flask.ext.limiter.extension import C, Limiter, HEADERS
22
from flask.ext.limiter.util import get_remote_address, get_ipaddr
23
24
25
class FlaskExtTests(unittest.TestCase):
26
    def setUp(self):
27
        redis.Redis().flushall()
28
29
    def build_app(self, config={}, **limiter_args):
30
        app = Flask(__name__)
31
        for k, v in config.items():
32
            app.config.setdefault(k, v)
33
        limiter_args.setdefault('key_func', get_remote_address)
34
        limiter = Limiter(app, **limiter_args)
35
        mock_handler = mock.Mock()
36
        mock_handler.level = logging.INFO
37
        limiter.logger.addHandler(mock_handler)
38
        return app, limiter
39
40
    def test_invalid_strategy(self):
41
        app = Flask(__name__)
42
        app.config.setdefault(C.STRATEGY, "fubar")
43
        self.assertRaises(ConfigurationError, Limiter, app, key_func=get_remote_address)
44
45
    def test_invalid_storage_string(self):
46
        app = Flask(__name__)
47
        app.config.setdefault(C.STORAGE_URL, "fubar://localhost:1234")
48
        self.assertRaises(ConfigurationError, Limiter, app, key_func=get_remote_address)
49
50
    def test_constructor_arguments_over_config(self):
51
        app = Flask(__name__)
52
        app.config.setdefault(C.STRATEGY, "fixed-window-elastic-expiry")
53
        limiter = Limiter(strategy='moving-window', key_func=get_remote_address)
54
        limiter.init_app(app)
55
        app.config.setdefault(C.STORAGE_URL, "redis://localhost:6379")
56
        self.assertEqual(type(limiter._limiter), MovingWindowRateLimiter)
57
        limiter = Limiter(storage_uri='memcached://localhost:11211', key_func=get_remote_address)
58
        limiter.init_app(app)
59
        self.assertEqual(type(limiter._storage), MemcachedStorage)
60
61 View Code Duplication
    def test_error_message(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
62
        app, limiter = self.build_app({
63
            C.GLOBAL_LIMITS: "1 per day"
64
        })
65
66
        @app.route("/")
67
        def null():
68
            return ""
69
70
        with app.test_client() as cli:
71
            cli.get("/")
72
            self.assertTrue("1 per 1 day" in cli.get("/").data.decode())
73
74
            @app.errorhandler(429)
75
            def ratelimit_handler(e):
76
                return make_response('{"error" : "rate limit %s"}' % str(e.description), 429)
77
78
            self.assertEqual({'error': 'rate limit 1 per 1 day'}, json.loads(cli.get("/").data.decode()))
79
80
    def test_reset(self):
81
        app, limiter = self.build_app({
82
            C.GLOBAL_LIMITS: "1 per day"
83
        })
84
85
        @app.route("/")
86
        def null():
87
            return "Hello Reset"
88
89
        with app.test_client() as cli:
90
            cli.get("/")
91
            self.assertTrue("1 per 1 day" in cli.get("/").data.decode())
92
            limiter.reset()
93
            self.assertEqual("Hello Reset", cli.get("/").data.decode())
94
            self.assertTrue("1 per 1 day" in cli.get("/").data.decode())
95
96
    def test_swallow_error(self):
97
        app, limiter = self.build_app({
98
            C.GLOBAL_LIMITS: "1 per day",
99
            C.SWALLOW_ERRORS: True
100
        })
101
102
        @app.route("/")
103
        def null():
104
            return "ok"
105
106
        with app.test_client() as cli:
107
            with mock.patch("limits.strategies.FixedWindowRateLimiter.hit") as hit:
108
                def raiser(*a, **k):
109
                    raise Exception
110
111
                hit.side_effect = raiser
112
                self.assertTrue("ok" in cli.get("/").data.decode())
113
114 View Code Duplication
    def test_no_swallow_error(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
115
        app, limiter = self.build_app({
116
            C.GLOBAL_LIMITS: "1 per day",
117
        })
118
119
        @app.route("/")
120
        def null():
121
            return "ok"
122
123
        @app.errorhandler(500)
124
        def e500(e):
125
            return str(e), 500
126
127
        with app.test_client() as cli:
128
            with mock.patch("limits.strategies.FixedWindowRateLimiter.hit") as hit:
129
                def raiser(*a, **k):
130
                    raise Exception("underlying")
131
132
                hit.side_effect = raiser
133
                self.assertEqual(500, cli.get("/").status_code)
134
                self.assertEqual("underlying", cli.get("/").data.decode())
135
136 View Code Duplication
    def test_combined_rate_limits(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
137
        app, limiter = self.build_app({
138
            C.GLOBAL_LIMITS: "1 per hour; 10 per day"
139
        })
140
141
        @app.route("/t1")
142
        @limiter.limit("100 per hour;10/minute")
143
        def t1():
144
            return "t1"
145
146
        @app.route("/t2")
147
        def t2():
148
            return "t2"
149
150
        with hiro.Timeline().freeze() as timeline:
151
            with app.test_client() as cli:
152
                self.assertEqual(200, cli.get("/t1").status_code)
153
                self.assertEqual(200, cli.get("/t2").status_code)
154
                self.assertEqual(429, cli.get("/t2").status_code)
155
156
    def test_key_func(self):
157
        app, limiter = self.build_app()
158
159
        @app.route("/t1")
160
        @limiter.limit("100 per minute", lambda: "test")
161
        def t1():
162
            return "test"
163
164
        with hiro.Timeline().freeze() as timeline:
165
            with app.test_client() as cli:
166
                for i in range(0, 100):
167
                    self.assertEqual(200,
168
                                     cli.get("/t1", headers={"X_FORWARDED_FOR": "127.0.0.2"}).status_code
169
                                     )
170
                self.assertEqual(429, cli.get("/t1").status_code)
171
172
    def test_multiple_decorators(self):
173
        app, limiter = self.build_app(key_func=get_ipaddr)
174
175
        @app.route("/t1")
176
        @limiter.limit("100 per minute", lambda: "test")  # effectively becomes a limit for all users
177
        @limiter.limit("50/minute")  # per ip as per default key_func
178
        def t1():
179
            return "test"
180
181
        with hiro.Timeline().freeze() as timeline:
182
            with app.test_client() as cli:
183
                for i in range(0, 100):
184
                    self.assertEqual(200 if i < 50 else 429,
185
                                     cli.get("/t1", headers={"X_FORWARDED_FOR": "127.0.0.2"}).status_code
186
                                     )
187
                for i in range(50):
188
                    self.assertEqual(200, cli.get("/t1").status_code)
189
                self.assertEqual(429, cli.get("/t1").status_code)
190
                self.assertEqual(429, cli.get("/t1", headers={"X_FORWARDED_FOR": "127.0.0.3"}).status_code)
191
192 View Code Duplication
    def test_logging(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
193
        app = Flask(__name__)
194
        limiter = Limiter(app, key_func=get_remote_address)
195
        mock_handler = mock.Mock()
196
        mock_handler.level = logging.INFO
197
        limiter.logger.addHandler(mock_handler)
198
199
        @app.route("/t1")
200
        @limiter.limit("1/minute")
201
        def t1():
202
            return "test"
203
204
        with app.test_client() as cli:
205
            self.assertEqual(200, cli.get("/t1").status_code)
206
            self.assertEqual(429, cli.get("/t1").status_code)
207
        self.assertEqual(mock_handler.handle.call_count, 1)
208
209 View Code Duplication
    def test_reuse_logging(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
210
        app = Flask(__name__)
211
        app_handler = mock.Mock()
212
        app_handler.level = logging.INFO
213
        app.logger.addHandler(app_handler)
214
        limiter = Limiter(app, key_func=get_remote_address)
215
        for handler in app.logger.handlers:
216
            limiter.logger.addHandler(handler)
217
218
        @app.route("/t1")
219
        @limiter.limit("1/minute")
220
        def t1():
221
            return "42"
222
223
        with app.test_client() as cli:
224
            cli.get("/t1")
225
            cli.get("/t1")
226
227
        self.assertEqual(app_handler.handle.call_count, 1)
228
229 View Code Duplication
    def test_exempt_routes(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
230
        app, limiter = self.build_app(default_limits=["1/minute"])
231
232
        @app.route("/t1")
233
        def t1():
234
            return "test"
235
236
        @app.route("/t2")
237
        @limiter.exempt
238
        def t2():
239
            return "test"
240
241
        with app.test_client() as cli:
242
            self.assertEqual(cli.get("/t1").status_code, 200)
243
            self.assertEqual(cli.get("/t1").status_code, 429)
244
            self.assertEqual(cli.get("/t2").status_code, 200)
245
            self.assertEqual(cli.get("/t2").status_code, 200)
246
247 View Code Duplication
    def test_blueprint(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
248
        app, limiter = self.build_app(default_limits=["1/minute"])
249
        bp = Blueprint("main", __name__)
250
251
        @bp.route("/t1")
252
        def t1():
253
            return "test"
254
255
        @bp.route("/t2")
256
        @limiter.limit("10 per minute")
257
        def t2():
258
            return "test"
259
260
        app.register_blueprint(bp)
261
262
        with app.test_client() as cli:
263
            self.assertEqual(cli.get("/t1").status_code, 200)
264
            self.assertEqual(cli.get("/t1").status_code, 429)
265
            for i in range(0, 10):
266
                self.assertEqual(cli.get("/t2").status_code, 200)
267
            self.assertEqual(cli.get("/t2").status_code, 429)
268
269
    def test_register_blueprint(self):
270
        app, limiter = self.build_app(default_limits=["1/minute"])
271
        bp_1 = Blueprint("bp1", __name__)
272
        bp_2 = Blueprint("bp2", __name__)
273
        bp_3 = Blueprint("bp3", __name__)
274
        bp_4 = Blueprint("bp4", __name__)
275
276
        @bp_1.route("/t1")
277
        def t1():
278
            return "test"
279
280
        @bp_1.route("/t2")
281
        def t2():
282
            return "test"
283
284
        @bp_2.route("/t3")
285
        def t3():
286
            return "test"
287
288
        @bp_3.route("/t4")
289
        def t4():
290
            return "test"
291
292
        @bp_4.route("/t5")
293
        def t4():
294
            return "test"
295
296
        def dy_limit():
297
            return "1/second"
298
299
        app.register_blueprint(bp_1)
300
        app.register_blueprint(bp_2)
301
        app.register_blueprint(bp_3)
302
        app.register_blueprint(bp_4)
303
304
        limiter.limit("1/second")(bp_1)
305
        limiter.exempt(bp_3)
306
        limiter.limit(dy_limit)(bp_4)
307
308
        with hiro.Timeline().freeze() as timeline:
309
            with app.test_client() as cli:
310
                self.assertEqual(cli.get("/t1").status_code, 200)
311
                self.assertEqual(cli.get("/t1").status_code, 429)
312
                timeline.forward(1)
313
                self.assertEqual(cli.get("/t1").status_code, 200)
314
                self.assertEqual(cli.get("/t2").status_code, 200)
315
                self.assertEqual(cli.get("/t2").status_code, 429)
316
                timeline.forward(1)
317
                self.assertEqual(cli.get("/t2").status_code, 200)
318
319
                self.assertEqual(cli.get("/t3").status_code, 200)
320
                for i in range(0, 10):
321
                    timeline.forward(1)
322
                    self.assertEqual(cli.get("/t3").status_code, 429)
323
324
                for i in range(0, 10):
325
                    self.assertEqual(cli.get("/t4").status_code, 200)
326
327
                self.assertEqual(cli.get("/t5").status_code, 200)
328
                self.assertEqual(cli.get("/t5").status_code, 429)
329
330 View Code Duplication
    def test_disabled_flag(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
331
        app, limiter = self.build_app(
332
                config={C.ENABLED: False},
333
                default_limits=["1/minute"]
334
        )
335
336
        @app.route("/t1")
337
        def t1():
338
            return "test"
339
340
        @app.route("/t2")
341
        @limiter.limit("10 per minute")
342
        def t2():
343
            return "test"
344
345
        with app.test_client() as cli:
346
            self.assertEqual(cli.get("/t1").status_code, 200)
347
            self.assertEqual(cli.get("/t1").status_code, 200)
348
            for i in range(0, 10):
349
                self.assertEqual(cli.get("/t2").status_code, 200)
350
            self.assertEqual(cli.get("/t2").status_code, 200)
351
352
    def test_fallback_to_memory_config(self):
353
        _, limiter = self.build_app(
354
                config={C.ENABLED: True},
355
                default_limits=["5/minute"],
356
                storage_uri="redis://localhost:6379",
357
                in_memory_fallback=["1/minute"]
358
        )
359
        self.assertEqual(len(limiter._in_memory_fallback), 1)
360
361
        _, limiter = self.build_app(
362
                config={C.ENABLED: True, C.IN_MEMORY_FALLBACK: "1/minute"},
363
                default_limits=["5/minute"],
364
                storage_uri="redis://localhost:6379",
365
        )
366
        self.assertEqual(len(limiter._in_memory_fallback), 1)
367
368
    def test_fallback_to_memory_backoff_check(self):
369
        app, limiter = self.build_app(
370
                config={C.ENABLED: True},
371
                default_limits=["5/minute"],
372
                storage_uri="redis://localhost:6379",
373
                in_memory_fallback=["1/minute"]
374
        )
375
376
        @app.route("/t1")
377
        def t1():
378
            return "test"
379
380
        with app.test_client() as cli:
381
            def raiser(*a):
382
                raise Exception("redis dead")
383
384
            with hiro.Timeline() as timeline:
385
                with mock.patch(
386
                        "redis.client.Redis.execute_command"
387
                ) as exec_command:
388
                    exec_command.side_effect = raiser
389
                    self.assertEqual(cli.get("/t1").status_code, 200)
390
                    self.assertEqual(cli.get("/t1").status_code, 429)
391
                    timeline.forward(1)
392
                    self.assertEqual(cli.get("/t1").status_code, 429)
393
                    timeline.forward(2)
394
                    self.assertEqual(cli.get("/t1").status_code, 429)
395
                    timeline.forward(4)
396
                    self.assertEqual(cli.get("/t1").status_code, 429)
397
                    timeline.forward(8)
398
                    self.assertEqual(cli.get("/t1").status_code, 429)
399
                    timeline.forward(16)
400
                    self.assertEqual(cli.get("/t1").status_code, 429)
401
                    timeline.forward(32)
402
                    self.assertEqual(cli.get("/t1").status_code, 200)
403
                # redis back to normal, but exponential backoff will only
404
                # result in it being marked after pow(2,0) seconds and next
405
                # check
406
                self.assertEqual(cli.get("/t1").status_code, 429)
407
                timeline.forward(1)
408
                self.assertEqual(cli.get("/t1").status_code, 200)
409
                self.assertEqual(cli.get("/t1").status_code, 200)
410
                self.assertEqual(cli.get("/t1").status_code, 200)
411
                self.assertEqual(cli.get("/t1").status_code, 200)
412
                self.assertEqual(cli.get("/t1").status_code, 200)
413
                self.assertEqual(cli.get("/t1").status_code, 429)
414
415
    def test_fallback_to_memory(self):
416
        app, limiter = self.build_app(
417
                config={C.ENABLED: True},
418
                default_limits=["5/minute"],
419
                storage_uri="redis://localhost:6379",
420
                in_memory_fallback=["1/minute"]
421
        )
422
423
        @app.route("/t1")
424
        def t1():
425
            return "test"
426
427
        @app.route("/t2")
428
        @limiter.limit("3 per minute")
429
        def t2():
430
            return "test"
431
432
        with app.test_client() as cli:
433
            self.assertEqual(cli.get("/t1").status_code, 200)
434
            self.assertEqual(cli.get("/t1").status_code, 200)
435
            self.assertEqual(cli.get("/t1").status_code, 200)
436
            self.assertEqual(cli.get("/t1").status_code, 200)
437
            self.assertEqual(cli.get("/t1").status_code, 200)
438
            self.assertEqual(cli.get("/t1").status_code, 429)
439
            self.assertEqual(cli.get("/t2").status_code, 200)
440
            self.assertEqual(cli.get("/t2").status_code, 200)
441
            self.assertEqual(cli.get("/t2").status_code, 200)
442
            self.assertEqual(cli.get("/t2").status_code, 429)
443
444
            def raiser(*a):
445
                raise Exception("redis dead")
446
447
            with mock.patch(
448
                    "redis.client.Redis.execute_command"
449
            ) as exec_command:
450
                exec_command.side_effect = raiser
451
                self.assertEqual(cli.get("/t1").status_code, 200)
452
                self.assertEqual(cli.get("/t1").status_code, 429)
453
                self.assertEqual(cli.get("/t2").status_code, 200)
454
                self.assertEqual(cli.get("/t2").status_code, 429)
455
            # redis back to normal, go back to regular limits
456
            with hiro.Timeline() as timeline:
457
                timeline.forward(1)
458
                limiter._storage.storage.flushall()
459
                self.assertEqual(cli.get("/t2").status_code, 200)
460
                self.assertEqual(cli.get("/t2").status_code, 200)
461
                self.assertEqual(cli.get("/t2").status_code, 200)
462
                self.assertEqual(cli.get("/t2").status_code, 429)
463
464
    def test_decorated_dynamic_limits(self):
465
        app, limiter = self.build_app({"X": "2 per second"}, default_limits=["1/second"])
466
467
        def request_context_limit():
468
            limits = {
469
                "127.0.0.1": "10 per minute",
470
                "127.0.0.2": "1 per minute"
471
            }
472
            remote_addr = (request.access_route and request.access_route[0]) or request.remote_addr or '127.0.0.1'
473
            limit = limits.setdefault(remote_addr, '1 per minute')
474
            return limit
475
476
        @app.route("/t1")
477
        @limiter.limit("20/day")
478
        @limiter.limit(lambda: current_app.config.get("X"))
479
        @limiter.limit(request_context_limit)
480
        def t1():
481
            return "42"
482
483
        @app.route("/t2")
484
        @limiter.limit(lambda: current_app.config.get("X"))
485
        def t2():
486
            return "42"
487
488
        R1 = {"X_FORWARDED_FOR": "127.0.0.1, 127.0.0.0"}
489
        R2 = {"X_FORWARDED_FOR": "127.0.0.2"}
490
491
        with app.test_client() as cli:
492
            with hiro.Timeline().freeze() as timeline:
493
                for i in range(0, 10):
494
                    self.assertEqual(cli.get("/t1", headers=R1).status_code, 200)
495
                    timeline.forward(1)
496
                self.assertEqual(cli.get("/t1", headers=R1).status_code, 429)
497
                self.assertEqual(cli.get("/t1", headers=R2).status_code, 200)
498
                self.assertEqual(cli.get("/t1", headers=R2).status_code, 429)
499
                timeline.forward(60)
500
                self.assertEqual(cli.get("/t1", headers=R2).status_code, 200)
501
                self.assertEqual(cli.get("/t2").status_code, 200)
502
                self.assertEqual(cli.get("/t2").status_code, 200)
503
                self.assertEqual(cli.get("/t2").status_code, 429)
504
                timeline.forward(1)
505
                self.assertEqual(cli.get("/t2").status_code, 200)
506
507 View Code Duplication
    def test_invalid_decorated_dynamic_limits(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
508
        app = Flask(__name__)
509
        app.config.setdefault("X", "2 per sec")
510
        limiter = Limiter(app, default_limits=["1/second"], key_func=get_remote_address)
511
        mock_handler = mock.Mock()
512
        mock_handler.level = logging.INFO
513
        limiter.logger.addHandler(mock_handler)
514
515
        @app.route("/t1")
516
        @limiter.limit(lambda: current_app.config.get("X"))
517
        def t1():
518
            return "42"
519
520
        with app.test_client() as cli:
521
            with hiro.Timeline().freeze() as timeline:
522
                self.assertEqual(cli.get("/t1").status_code, 200)
523
                self.assertEqual(cli.get("/t1").status_code, 429)
524
        # 2 for invalid limit, 1 for warning.
525
        self.assertEqual(mock_handler.handle.call_count, 3)
526
        self.assertTrue("failed to load ratelimit" in mock_handler.handle.call_args_list[0][0][0].msg)
527
        self.assertTrue("failed to load ratelimit" in mock_handler.handle.call_args_list[1][0][0].msg)
528
        self.assertTrue("exceeded at endpoint" in mock_handler.handle.call_args_list[2][0][0].msg)
529
530 View Code Duplication
    def test_invalid_decorated_static_limits(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
531
        app = Flask(__name__)
532
        limiter = Limiter(app, default_limits=["1/second"], key_func=get_remote_address)
533
        mock_handler = mock.Mock()
534
        mock_handler.level = logging.INFO
535
        limiter.logger.addHandler(mock_handler)
536
537
        @app.route("/t1")
538
        @limiter.limit("2/sec")
539
        def t1():
540
            return "42"
541
542
        with app.test_client() as cli:
543
            with hiro.Timeline().freeze() as timeline:
544
                self.assertEqual(cli.get("/t1").status_code, 200)
545
                self.assertEqual(cli.get("/t1").status_code, 429)
546
        self.assertTrue("failed to configure" in mock_handler.handle.call_args_list[0][0][0].msg)
547
        self.assertTrue("exceeded at endpoint" in mock_handler.handle.call_args_list[1][0][0].msg)
548
549 View Code Duplication
    def test_invalid_decorated_static_limit_blueprint(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
550
        app = Flask(__name__)
551
        limiter = Limiter(app, default_limits=["1/second"], key_func=get_remote_address)
552
        mock_handler = mock.Mock()
553
        mock_handler.level = logging.INFO
554
        limiter.logger.addHandler(mock_handler)
555
        bp = Blueprint("bp1", __name__)
556
557
        @bp.route("/t1")
558
        def t1():
559
            return "42"
560
561
        limiter.limit("2/sec")(bp)
562
        app.register_blueprint(bp)
563
564
        with app.test_client() as cli:
565
            with hiro.Timeline().freeze() as timeline:
566
                self.assertEqual(cli.get("/t1").status_code, 200)
567
                self.assertEqual(cli.get("/t1").status_code, 429)
568
        self.assertTrue("failed to configure" in mock_handler.handle.call_args_list[0][0][0].msg)
569
        self.assertTrue("exceeded at endpoint" in mock_handler.handle.call_args_list[1][0][0].msg)
570
571 View Code Duplication
    def test_invalid_decorated_dynamic_limits_blueprint(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
572
        app = Flask(__name__)
573
        app.config.setdefault("X", "2 per sec")
574
        limiter = Limiter(app, default_limits=["1/second"], key_func=get_remote_address)
575
        mock_handler = mock.Mock()
576
        mock_handler.level = logging.INFO
577
        limiter.logger.addHandler(mock_handler)
578
        bp = Blueprint("bp1", __name__)
579
580
        @bp.route("/t1")
581
        def t1():
582
            return "42"
583
584
        limiter.limit(lambda: current_app.config.get("X"))(bp)
585
        app.register_blueprint(bp)
586
587
        with app.test_client() as cli:
588
            with hiro.Timeline().freeze() as timeline:
589
                self.assertEqual(cli.get("/t1").status_code, 200)
590
                self.assertEqual(cli.get("/t1").status_code, 429)
591
        self.assertEqual(mock_handler.handle.call_count, 3)
592
        self.assertTrue("failed to load ratelimit" in mock_handler.handle.call_args_list[0][0][0].msg)
593
        self.assertTrue("failed to load ratelimit" in mock_handler.handle.call_args_list[1][0][0].msg)
594
        self.assertTrue("exceeded at endpoint" in mock_handler.handle.call_args_list[2][0][0].msg)
595
596
    def test_multiple_apps(self):
597
        app1 = Flask(__name__)
598
        app2 = Flask(__name__)
599
600
        limiter = Limiter(default_limits=["1/second"], key_func=get_remote_address)
601
        limiter.init_app(app1)
602
        limiter.init_app(app2)
603
604
        @app1.route("/ping")
605
        def ping():
606
            return "PONG"
607
608
        @app1.route("/slowping")
609
        @limiter.limit("1/minute")
610
        def slow_ping():
611
            return "PONG"
612
613
        @app2.route("/ping")
614
        @limiter.limit("2/second")
615
        def ping_2():
616
            return "PONG"
617
618
        @app2.route("/slowping")
619
        @limiter.limit("2/minute")
620
        def slow_ping_2():
621
            return "PONG"
622
623
        with hiro.Timeline().freeze() as timeline:
624
            with app1.test_client() as cli:
625
                self.assertEqual(cli.get("/ping").status_code, 200)
626
                self.assertEqual(cli.get("/ping").status_code, 429)
627
                timeline.forward(1)
628
                self.assertEqual(cli.get("/ping").status_code, 200)
629
                self.assertEqual(cli.get("/slowping").status_code, 200)
630
                timeline.forward(59)
631
                self.assertEqual(cli.get("/slowping").status_code, 429)
632
                timeline.forward(1)
633
                self.assertEqual(cli.get("/slowping").status_code, 200)
634
            with app2.test_client() as cli:
635
                self.assertEqual(cli.get("/ping").status_code, 200)
636
                self.assertEqual(cli.get("/ping").status_code, 200)
637
                self.assertEqual(cli.get("/ping").status_code, 429)
638
                timeline.forward(1)
639
                self.assertEqual(cli.get("/ping").status_code, 200)
640
                self.assertEqual(cli.get("/slowping").status_code, 200)
641
                timeline.forward(59)
642
                self.assertEqual(cli.get("/slowping").status_code, 200)
643
                self.assertEqual(cli.get("/slowping").status_code, 429)
644
                timeline.forward(1)
645
                self.assertEqual(cli.get("/slowping").status_code, 200)
646
647
    def test_headers_no_breach(self):
648
        app = Flask(__name__)
649
        limiter = Limiter(
650
            app, default_limits=["10/minute"], headers_enabled=True,
651
            key_func=get_remote_address
652
        )
653
654
        @app.route("/t1")
655
        def t1():
656
            return "test"
657
658
        @app.route("/t2")
659
        @limiter.limit("2/second; 5 per minute; 10/hour")
660
        def t2():
661
            return "test"
662
663
        with hiro.Timeline().freeze():
664
            with app.test_client() as cli:
665
                resp = cli.get("/t1")
666
                self.assertEqual(
667
                        resp.headers.get('X-RateLimit-Limit'),
668
                        '10'
669
                )
670
                self.assertEqual(
671
                        resp.headers.get('X-RateLimit-Remaining'),
672
                        '9'
673
                )
674
                self.assertEqual(
675
                        resp.headers.get('X-RateLimit-Reset'),
676
                        str(int(time.time() + 60))
677
                )
678
                self.assertEqual(
679
                        resp.headers.get('Retry-After'),
680
                        str(59)
681
                )
682
                resp = cli.get("/t2")
683
                self.assertEqual(
684
                        resp.headers.get('X-RateLimit-Limit'),
685
                        '2'
686
                )
687
                self.assertEqual(
688
                        resp.headers.get('X-RateLimit-Remaining'),
689
                        '1'
690
                )
691
                self.assertEqual(
692
                        resp.headers.get('X-RateLimit-Reset'),
693
                        str(int(time.time() + 1))
694
                )
695
696
                self.assertEqual(
697
                    resp.headers.get('Retry-After'),
698
                    str(0)
699
                )
700
701 View Code Duplication
    def test_headers_breach(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
702
        app = Flask(__name__)
703
        limiter = Limiter(
704
            app, default_limits=["10/minute"],
705
            headers_enabled=True, key_func=get_remote_address
706
        )
707
708
        @app.route("/t1")
709
        @limiter.limit("2/second; 10 per minute; 20/hour")
710
        def t():
711
            return "test"
712
713
        with hiro.Timeline().freeze() as timeline:
714
            with app.test_client() as cli:
715
                for i in range(11):
716
                    resp = cli.get("/t1")
717
                    timeline.forward(1)
718
719
                self.assertEqual(
720
                        resp.headers.get('X-RateLimit-Limit'),
721
                        '10'
722
                )
723
                self.assertEqual(
724
                        resp.headers.get('X-RateLimit-Remaining'),
725
                        '0'
726
                )
727
                self.assertEqual(
728
                        resp.headers.get('X-RateLimit-Reset'),
729
                        str(int(time.time() + 49))
730
                )
731
                self.assertEqual(
732
                        resp.headers.get('Retry-After'),
733
                        str(int(49))
734
                )
735
736
    def test_custom_headers_from_setter(self):
737
        app = Flask(__name__)
738
        limiter = Limiter(
739
            app, default_limits=["10/minute"], headers_enabled=True,
740
            key_func=get_remote_address, retry_after='http-date'
741
        )
742
        limiter._header_mapping[HEADERS.RESET] = 'X-Reset'
743
        limiter._header_mapping[HEADERS.LIMIT] = 'X-Limit'
744
        limiter._header_mapping[HEADERS.REMAINING] = 'X-Remaining'
745
746
        @app.route("/t1")
747
        @limiter.limit("2/second; 10 per minute; 20/hour")
748
        def t():
749
            return "test"
750
751
        with hiro.Timeline().freeze(0) as timeline:
752
            with app.test_client() as cli:
753
                for i in range(11):
754
                    resp = cli.get("/t1")
755
                    timeline.forward(1)
756
757
                self.assertEqual(
758
                        resp.headers.get('X-Limit'),
759
                        '10'
760
                )
761
                self.assertEqual(
762
                        resp.headers.get('X-Remaining'),
763
                        '0'
764
                )
765
                self.assertEqual(
766
                        resp.headers.get('X-Reset'),
767
                        str(int(time.time() + 49))
768
                )
769
                self.assertEqual(
770
                    resp.headers.get('Retry-After'),
771
                    'Thu, 01 Jan 1970 00:01:00 GMT'
772
                )
773
774 View Code Duplication
    def test_custom_headers_from_config(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
775
        app = Flask(__name__)
776
        app.config.setdefault(C.HEADER_LIMIT, "X-Limit")
777
        app.config.setdefault(C.HEADER_REMAINING, "X-Remaining")
778
        app.config.setdefault(C.HEADER_RESET, "X-Reset")
779
        limiter = Limiter(
780
            app, default_limits=["10/minute"], headers_enabled=True,
781
            key_func=get_remote_address
782
        )
783
784
785
        @app.route("/t1")
786
        @limiter.limit("2/second; 10 per minute; 20/hour")
787
        def t():
788
            return "test"
789
790
        with hiro.Timeline().freeze() as timeline:
791
            with app.test_client() as cli:
792
                for i in range(11):
793
                    resp = cli.get("/t1")
794
                    timeline.forward(1)
795
796
                self.assertEqual(
797
                        resp.headers.get('X-Limit'),
798
                        '10'
799
                )
800
                self.assertEqual(
801
                        resp.headers.get('X-Remaining'),
802
                        '0'
803
                )
804
                self.assertEqual(
805
                        resp.headers.get('X-Reset'),
806
                        str(int(time.time() + 49))
807
                )
808
809
    def test_named_shared_limit(self):
810
        app, limiter = self.build_app()
811
        shared_limit_a = limiter.shared_limit("1/minute", scope='a')
812
        shared_limit_b = limiter.shared_limit("1/minute", scope='b')
813
814
        @app.route("/t1")
815
        @shared_limit_a
816
        def route1():
817
            return "route1"
818
819
        @app.route("/t2")
820
        @shared_limit_a
821
        def route2():
822
            return "route2"
823
824
        @app.route("/t3")
825
        @shared_limit_b
826
        def route3():
827
            return "route3"
828
829
        with hiro.Timeline().freeze() as timeline:
830
            with app.test_client() as cli:
831
                self.assertEqual(200, cli.get("/t1").status_code)
832
                self.assertEqual(200, cli.get("/t3").status_code)
833
                self.assertEqual(429, cli.get("/t2").status_code)
834
835
    def test_application_shared_limit(self):
836
        app, limiter = self.build_app(application_limits=["2/minute"])
837
        @app.route("/t1")
838
        def t1():
839
            return "route1"
840
841
        @app.route("/t2")
842
        def t2():
843
            return "route2"
844
845
        with hiro.Timeline().freeze():
846
            with app.test_client() as cli:
847
                self.assertEqual(200, cli.get("/t1").status_code)
848
                self.assertEqual(200, cli.get("/t2").status_code)
849
                self.assertEqual(429, cli.get("/t1").status_code)
850
851
    def test_dynamic_shared_limit(self):
852
        app, limiter = self.build_app()
853
        fn_a = mock.Mock()
854
        fn_b = mock.Mock()
855
        fn_a.return_value = "foo"
856
        fn_b.return_value = "bar"
857
858
        dy_limit_a = limiter.shared_limit("1/minute", scope=fn_a)
859
        dy_limit_b = limiter.shared_limit("1/minute", scope=fn_b)
860
861
        @app.route("/t1")
862
        @dy_limit_a
863
        def t1():
864
            return "route1"
865
866
        @app.route("/t2")
867
        @dy_limit_a
868
        def t2():
869
            return "route2"
870
871 View Code Duplication
        @app.route("/t3")
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
872
        @dy_limit_b
873
        def t3():
874
            return "route3"
875
876
        with hiro.Timeline().freeze():
877
            with app.test_client() as cli:
878
                self.assertEqual(200, cli.get("/t1").status_code)
879
                self.assertEqual(200, cli.get("/t3").status_code)
880
                self.assertEqual(429, cli.get("/t2").status_code)
881
                self.assertEqual(429, cli.get("/t3").status_code)
882
                self.assertEqual(2, fn_a.call_count)
883
                self.assertEqual(2, fn_b.call_count)
884
                fn_b.assert_called_with("t3")
885
                fn_a.assert_has_calls([mock.call("t1"), mock.call("t2")])
886
887
    def test_conditional_limits(self):
888
        """Test that the conditional activation of the limits work."""
889
        app = Flask(__name__)
890
        limiter = Limiter(app, key_func=get_remote_address)
891
892
        @app.route("/limited")
893
        @limiter.limit("1 per day")
894
        def limited_route():
895
            return "passed"
896
897
        @app.route("/unlimited")
898
        @limiter.limit("1 per day", exempt_when=lambda: True)
899
        def never_limited_route():
900
            return "should always pass"
901
902
        is_exempt = False
903
904
        @app.route("/conditional")
905
        @limiter.limit("1 per day", exempt_when=lambda: is_exempt)
906
        def conditionally_limited_route():
907 View Code Duplication
            return "conditional"
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
908
909
        with app.test_client() as cli:
910
            self.assertEqual(cli.get("/limited").status_code, 200)
911
            self.assertEqual(cli.get("/limited").status_code, 429)
912
913
            self.assertEqual(cli.get("/unlimited").status_code, 200)
914
            self.assertEqual(cli.get("/unlimited").status_code, 200)
915
916
            self.assertEqual(cli.get("/conditional").status_code, 200)
917
            self.assertEqual(cli.get("/conditional").status_code, 429)
918
            is_exempt = True
919
            self.assertEqual(cli.get("/conditional").status_code, 200)
920
            is_exempt = False
921
            self.assertEqual(cli.get("/conditional").status_code, 429)
922
923
    def test_conditional_shared_limits(self):
924
        """Test that conditional shared limits work."""
925
        app = Flask(__name__)
926
        limiter = Limiter(app, key_func=get_remote_address)
927
928
        @app.route("/limited")
929
        @limiter.shared_limit("1 per day", "test_scope")
930
        def limited_route():
931
            return "passed"
932
933
        @app.route("/unlimited")
934
        @limiter.shared_limit("1 per day", "test_scope", exempt_when=lambda: True)
935
        def never_limited_route():
936
            return "should always pass"
937
938
        is_exempt = False
939
940
        @app.route("/conditional")
941
        @limiter.shared_limit("1 per day", "test_scope", exempt_when=lambda: is_exempt)
942
        def conditionally_limited_route():
943
            return "conditional"
944
945
        with app.test_client() as cli:
946
            self.assertEqual(cli.get("/unlimited").status_code, 200)
947
            self.assertEqual(cli.get("/unlimited").status_code, 200)
948
949
            self.assertEqual(cli.get("/limited").status_code, 200)
950
            self.assertEqual(cli.get("/limited").status_code, 429)
951
952
            self.assertEqual(cli.get("/conditional").status_code, 429)
953
            is_exempt = True
954
            self.assertEqual(cli.get("/conditional").status_code, 200)
955
            is_exempt = False
956
            self.assertEqual(cli.get("/conditional").status_code, 429)
957
958
    def test_whitelisting(self):
959
960
        app = Flask(__name__)
961
        limiter = Limiter(
962
            app, default_limits=["1/minute"], headers_enabled=True,
963
            key_func=get_remote_address
964
        )
965
966
        @app.route("/")
967
        def t():
968
            return "test"
969
970
        @limiter.request_filter
971
        def w():
972
            if request.headers.get("internal", None) == "true":
973
                return True
974
            return False
975
976
        with hiro.Timeline().freeze() as timeline:
977
            with app.test_client() as cli:
978
                self.assertEqual(cli.get("/").status_code, 200)
979
                self.assertEqual(cli.get("/").status_code, 429)
980
                timeline.forward(60)
981
                self.assertEqual(cli.get("/").status_code, 200)
982
983
                for i in range(0, 10):
984
                    self.assertEqual(
985
                            cli.get("/", headers={"internal": "true"}).status_code,
986
                            200
987
                    )
988
989
    def test_pluggable_views(self):
990
        app, limiter = self.build_app(
991
                default_limits=["1/hour"]
992
        )
993
994
        class Va(View):
995
            methods = ['GET', 'POST']
996
            decorators = [limiter.limit("2/second")]
997
998
            def dispatch_request(self):
999
                return request.method.lower()
1000
1001
        class Vb(View):
1002
            methods = ['GET']
1003
            decorators = [limiter.limit("1/second, 3/minute")]
1004
1005
            def dispatch_request(self):
1006
                return request.method.lower()
1007
1008
        class Vc(View):
1009
            methods = ['GET']
1010
1011
            def dispatch_request(self):
1012
                return request.method.lower()
1013
1014
        app.add_url_rule("/a", view_func=Va.as_view("a"))
1015
        app.add_url_rule("/b", view_func=Vb.as_view("b"))
1016
        app.add_url_rule("/c", view_func=Vc.as_view("c"))
1017
        with hiro.Timeline().freeze() as timeline:
1018
            with app.test_client() as cli:
1019
                self.assertEqual(200, cli.get("/a").status_code)
1020
                self.assertEqual(200, cli.get("/a").status_code)
1021
                self.assertEqual(429, cli.post("/a").status_code)
1022
                self.assertEqual(200, cli.get("/b").status_code)
1023
                timeline.forward(1)
1024
                self.assertEqual(200, cli.get("/b").status_code)
1025
                timeline.forward(1)
1026
                self.assertEqual(200, cli.get("/b").status_code)
1027
                timeline.forward(1)
1028
                self.assertEqual(429, cli.get("/b").status_code)
1029
                self.assertEqual(200, cli.get("/c").status_code)
1030
                self.assertEqual(429, cli.get("/c").status_code)
1031
1032
    def test_pluggable_method_views(self):
1033
        app, limiter = self.build_app(
1034
                default_limits=["1/hour"]
1035
        )
1036
1037
        class Va(MethodView):
1038
            decorators = [limiter.limit("2/second")]
1039
1040
            def get(self):
1041
                return request.method.lower()
1042
1043
            def post(self):
1044
                return request.method.lower()
1045
1046
        class Vb(MethodView):
1047
            decorators = [limiter.limit("1/second, 3/minute")]
1048
1049
            def get(self):
1050
                return request.method.lower()
1051
1052
        class Vc(MethodView):
1053
            def get(self):
1054
                return request.method.lower()
1055
1056
        class Vd(MethodView):
1057
            decorators = [limiter.limit("1/minute", methods=['get'])]
1058
1059
            def get(self):
1060
                return request.method.lower()
1061
1062
            def post(self):
1063
                return request.method.lower()
1064
1065
        app.add_url_rule("/a", view_func=Va.as_view("a"))
1066
        app.add_url_rule("/b", view_func=Vb.as_view("b"))
1067
        app.add_url_rule("/c", view_func=Vc.as_view("c"))
1068
        app.add_url_rule("/d", view_func=Vd.as_view("d"))
1069
1070
        with hiro.Timeline().freeze() as timeline:
1071
            with app.test_client() as cli:
1072
                self.assertEqual(200, cli.get("/a").status_code)
1073
                self.assertEqual(200, cli.get("/a").status_code)
1074
                self.assertEqual(429, cli.get("/a").status_code)
1075
                self.assertEqual(429, cli.post("/a").status_code)
1076
                self.assertEqual(200, cli.get("/b").status_code)
1077
                timeline.forward(1)
1078
                self.assertEqual(200, cli.get("/b").status_code)
1079
                timeline.forward(1)
1080
                self.assertEqual(200, cli.get("/b").status_code)
1081
                timeline.forward(1)
1082
                self.assertEqual(429, cli.get("/b").status_code)
1083
                self.assertEqual(200, cli.get("/c").status_code)
1084
                self.assertEqual(429, cli.get("/c").status_code)
1085
                self.assertEqual(200, cli.get("/d").status_code)
1086
                self.assertEqual(429, cli.get("/d").status_code)
1087
                self.assertEqual(200, cli.post("/d").status_code)
1088
                self.assertEqual(200, cli.post("/d").status_code)
1089
1090
    def test_flask_restful_resource(self):
1091
        app, limiter = self.build_app(
1092
                default_limits=["1/hour"]
1093
        )
1094
        api = restful.Api(app)
1095
1096
        class Va(Resource):
1097
            decorators = [limiter.limit("2/second")]
1098
1099
            def get(self):
1100
                return request.method.lower()
1101
1102
            def post(self):
1103
                return request.method.lower()
1104
1105
        class Vb(Resource):
1106
            decorators = [limiter.limit("1/second, 3/minute")]
1107
1108
            def get(self):
1109
                return request.method.lower()
1110
1111
        class Vc(Resource):
1112
            def get(self):
1113
                return request.method.lower()
1114
1115
        api.add_resource(Va, "/a")
1116
        api.add_resource(Vb, "/b")
1117
        api.add_resource(Vc, "/c")
1118
1119
        with hiro.Timeline().freeze() as timeline:
1120
            with app.test_client() as cli:
1121
                self.assertEqual(200, cli.get("/a").status_code)
1122
                self.assertEqual(200, cli.get("/a").status_code)
1123
                self.assertEqual(429, cli.get("/a").status_code)
1124
                self.assertEqual(429, cli.post("/a").status_code)
1125
                self.assertEqual(200, cli.get("/b").status_code)
1126
                timeline.forward(1)
1127
                self.assertEqual(200, cli.get("/b").status_code)
1128
                timeline.forward(1)
1129
                self.assertEqual(200, cli.get("/b").status_code)
1130
                timeline.forward(1)
1131
                self.assertEqual(429, cli.get("/b").status_code)
1132
                self.assertEqual(200, cli.get("/c").status_code)
1133
                self.assertEqual(429, cli.get("/c").status_code)
1134
1135 View Code Duplication
    def test_separate_method_limits(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
1136
        app, limiter = self.build_app()
1137
1138
        @limiter.limit("1/second", per_method=True)
1139
        @app.route("/", methods=["GET", "POST"])
1140
        def root():
1141
            return "root"
1142
1143
        with hiro.Timeline():
1144
            with app.test_client() as cli:
1145
                self.assertEqual(200, cli.get("/").status_code)
1146
                self.assertEqual(429, cli.get("/").status_code)
1147
                self.assertEqual(200, cli.post("/").status_code)
1148
                self.assertEqual(429, cli.post("/").status_code)
1149
1150 View Code Duplication
    def test_explicit_method_limits(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
1151
        app, limiter = self.build_app()
1152
1153
        @limiter.limit("1/second", methods=["GET"])
1154
        @app.route("/", methods=["GET", "POST"])
1155
        def root():
1156
            return "root"
1157
1158
        with hiro.Timeline():
1159
            with app.test_client() as cli:
1160
                self.assertEqual(200, cli.get("/").status_code)
1161
                self.assertEqual(429, cli.get("/").status_code)
1162
                self.assertEqual(200, cli.post("/").status_code)
1163
                self.assertEqual(200, cli.post("/").status_code)
1164
1165
    def test_no_auto_check(self):
1166
        app, limiter = self.build_app(auto_check=False)
1167
1168
        @limiter.limit("1/second", per_method=True)
1169
        @app.route("/", methods=["GET", "POST"])
1170
        def root():
1171
            return "root"
1172
1173
        with hiro.Timeline().freeze():
1174
            with app.test_client() as cli:
1175
                self.assertEqual(200, cli.get("/").status_code)
1176
                self.assertEqual(200, cli.get("/").status_code)
1177
1178
        # attach before_request to perform check
1179
        @app.before_request
1180
        def _():
1181
            limiter.check()
1182
1183
        with hiro.Timeline().freeze():
1184
            with app.test_client() as cli:
1185
                self.assertEqual(200, cli.get("/").status_code)
1186
                self.assertEqual(429, cli.get("/").status_code)
1187
1188
    def test_custom_error_message(self):
1189
        app, limiter = self.build_app()
1190
1191
        @app.errorhandler(429)
1192
        def ratelimit_handler(e):
1193
            return make_response(
1194
                    e.description
1195
                    , 429
1196
            )
1197
1198
        l1 = lambda: "1/second"
1199
        e1 = lambda: "dos"
1200
1201
        @limiter.limit("1/second", error_message="uno")
1202
        @app.route("/t1")
1203
        def t1():
1204
            return "1"
1205
1206
        @limiter.limit(l1, error_message=e1)
1207
        @app.route("/t2")
1208
        def t2():
1209
            return "2"
1210
1211
        s1 = limiter.shared_limit("1/second", scope='error_message', error_message="tres")
1212
1213
        @app.route("/t3")
1214
        @s1
1215
        def t3():
1216
            return "3"
1217
1218
        with hiro.Timeline().freeze():
1219
            with app.test_client() as cli:
1220
                cli.get("/t1")
1221
                resp = cli.get("/t1")
1222
                self.assertEqual(429, resp.status_code)
1223
                self.assertEqual(resp.data, b'uno')
1224
                cli.get("/t2")
1225
                resp = cli.get("/t2")
1226
                self.assertEqual(429, resp.status_code)
1227
                self.assertEqual(resp.data, b'dos')
1228
                cli.get("/t3")
1229
                resp = cli.get("/t3")
1230
                self.assertEqual(429, resp.status_code)
1231
                self.assertEqual(resp.data, b'tres')
1232
1233
    def test_custom_key_prefix(self):
1234
        app1, limiter1 = self.build_app(key_prefix="moo", storage_uri="redis://localhost:6379")
1235
        app2, limiter2 = self.build_app(key_prefix="cow", storage_uri="redis://localhost:6379")
1236
1237
        @app1.route("/test")
1238
        @limiter1.limit("1/day")
1239
        def t1():
1240
            return "app1 test"
1241
1242
        @app2.route("/test")
1243
        @limiter2.limit("1/day")
1244
        def t1():
1245
            return "app1 test"
1246
1247
        with app1.test_client() as cli:
1248
            resp = cli.get("/test")
1249
            self.assertEqual(200, resp.status_code)
1250
            resp = cli.get("/test")
1251
            self.assertEqual(429, resp.status_code)
1252
        with app2.test_client() as cli:
1253
            resp = cli.get("/test")
1254
            self.assertEqual(200, resp.status_code)
1255
            resp = cli.get("/test")
1256
            self.assertEqual(429, resp.status_code)
1257