Completed
Push — master ( 57b903...efd80c )
by Ali-Akber
24s
created

FlaskExtTests.test_callable_application_limit()   B

Complexity

Conditions 6

Size

Total Lines 16

Duplication

Lines 16
Ratio 100 %

Importance

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