Coverage for manila/tests/api/v2/test_limits.py: 96%
361 statements
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
1# Copyright 2011 OpenStack LLC.
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
16"""
17Tests dealing with HTTP rate-limiting.
18"""
19import ddt
20import http.client as http_client
21import io
23from oslo_serialization import jsonutils
24import webob
26from manila.api.openstack import api_version_request as api_version
27from manila.api.v2 import limits
28from manila.api import views
29import manila.context
30from manila import test
31from manila.tests.api import fakes
34TEST_LIMITS = [
35 limits.Limit("GET", "/delayed", "^/delayed", 1, limits.PER_MINUTE),
36 limits.Limit("POST", "*", ".*", 7, limits.PER_MINUTE),
37 limits.Limit("POST", "/shares", "^/shares", 3, limits.PER_MINUTE),
38 limits.Limit("PUT", "*", "", 10, limits.PER_MINUTE),
39 limits.Limit("PUT", "/shares", "^/shares", 5, limits.PER_MINUTE),
40]
41SHARE_REPLICAS_LIMIT_MICROVERSION = "2.58"
42SHARE_GROUP_QUOTA_MICROVERSION = "2.40"
45class BaseLimitTestSuite(test.TestCase):
46 """Base test suite which provides relevant stubs and time abstraction."""
48 def setUp(self):
49 super(BaseLimitTestSuite, self).setUp()
50 self.time = 0.0
51 self.mock_object(limits.Limit, "_get_time", self._get_time)
52 self.absolute_limits = {}
54 def stub_get_project_quotas(context, project_id, usages=True):
55 quotas = {}
56 for mapping_key in ('limit', 'in_use'):
57 for k, v in self.absolute_limits.get(mapping_key, {}).items():
58 if k not in quotas:
59 quotas[k] = {}
60 quotas[k].update({mapping_key: v})
61 return quotas
63 self.mock_object(manila.quota.QUOTAS, "get_project_quotas",
64 stub_get_project_quotas)
66 def _get_time(self):
67 """Return the "time" according to this test suite."""
68 return self.time
71@ddt.ddt
72class LimitsControllerTest(BaseLimitTestSuite):
73 """Tests for `limits.LimitsController` class."""
75 def setUp(self):
76 """Run before each test."""
77 super(LimitsControllerTest, self).setUp()
78 self.controller = limits.LimitsController()
80 def _get_index_request(self, accept_header="application/json",
81 microversion=api_version.DEFAULT_API_VERSION):
82 """Helper to set routing arguments."""
83 request = fakes.HTTPRequest.blank('/limit', version=microversion)
84 request.accept = accept_header
85 return request
87 def _populate_limits(self, request):
88 """Put limit info into a request."""
89 _limits = [
90 limits.Limit("GET", "*", ".*", 10, 60).display(),
91 limits.Limit("POST", "*", ".*", 5, 60 * 60).display(),
92 limits.Limit("GET", "changes-since*", "changes-since",
93 5, 60).display(),
94 ]
95 request.environ["manila.limits"] = _limits
96 return request
98 def test_empty_index_json(self):
99 """Test getting empty limit details in JSON."""
100 request = self._get_index_request()
101 response = self.controller.index(request)
102 expected = {
103 "limits": {
104 "rate": [],
105 "absolute": {},
106 },
107 }
108 self.assertEqual(expected, response)
110 @ddt.data(api_version.DEFAULT_API_VERSION,
111 SHARE_REPLICAS_LIMIT_MICROVERSION)
112 def test_index_json(self, microversion):
113 """Test getting limit details in JSON."""
114 request = self._get_index_request(microversion=microversion)
115 request = self._populate_limits(request)
116 self.absolute_limits = {
117 'limit': {
118 'shares': 11,
119 'gigabytes': 22,
120 'snapshots': 33,
121 'snapshot_gigabytes': 44,
122 'share_networks': 55,
123 },
124 'in_use': {
125 'shares': 3,
126 'gigabytes': 4,
127 'snapshots': 5,
128 'snapshot_gigabytes': 6,
129 'share_networks': 7,
130 },
131 }
133 if microversion == SHARE_GROUP_QUOTA_MICROVERSION: 133 ↛ 134line 133 didn't jump to line 134 because the condition on line 133 was never true
134 self.absolute_limits['limit']['share_groups'] = 20
135 self.absolute_limits['limit']['share_group_snapshots'] = 20
136 self.absolute_limits['in_use']['share_groups'] = 3
137 self.absolute_limits['in_use']['share_group_snapshots'] = 3
139 if microversion == SHARE_REPLICAS_LIMIT_MICROVERSION:
140 self.absolute_limits['limit']['share_replicas'] = 20
141 self.absolute_limits['limit']['replica_gigabytes'] = 20
142 self.absolute_limits['in_use']['share_replicas'] = 3
143 self.absolute_limits['in_use']['replica_gigabytes'] = 3
145 response = self.controller.index(request)
147 expected = {
148 "limits": {
149 "rate": [
150 {
151 "regex": ".*",
152 "uri": "*",
153 "limit": [
154 {
155 "verb": "GET",
156 "next-available": "1970-01-01T00:00:00Z",
157 "unit": "MINUTE",
158 "value": 10,
159 "remaining": 10,
160 },
161 {
162 "verb": "POST",
163 "next-available": "1970-01-01T00:00:00Z",
164 "unit": "HOUR",
165 "value": 5,
166 "remaining": 5,
167 },
168 ],
169 },
170 {
171 "regex": "changes-since",
172 "uri": "changes-since*",
173 "limit": [
174 {
175 "verb": "GET",
176 "next-available": "1970-01-01T00:00:00Z",
177 "unit": "MINUTE",
178 "value": 5,
179 "remaining": 5,
180 },
181 ],
182 },
184 ],
185 "absolute": {
186 "totalSharesUsed": 3,
187 "totalShareGigabytesUsed": 4,
188 "totalShareSnapshotsUsed": 5,
189 "totalSnapshotGigabytesUsed": 6,
190 "totalShareNetworksUsed": 7,
191 "maxTotalShares": 11,
192 "maxTotalShareGigabytes": 22,
193 "maxTotalShareSnapshots": 33,
194 "maxTotalSnapshotGigabytes": 44,
195 "maxTotalShareNetworks": 55,
196 },
197 },
198 }
199 if microversion == SHARE_GROUP_QUOTA_MICROVERSION: 199 ↛ 200line 199 didn't jump to line 200 because the condition on line 199 was never true
200 expected['limits']['absolute']["maxTotalShareGroups"] = 20
201 expected['limits']['absolute']["totalShareGroupsUsed"] = 3
202 expected['limits']['absolute']["maxTotalShareGroupSnapshots"] = 20
203 expected['limits']['absolute']["totalShareGroupSnapshots"] = 3
205 if microversion == SHARE_REPLICAS_LIMIT_MICROVERSION:
206 expected['limits']['absolute']["maxTotalShareReplicas"] = 20
207 expected['limits']['absolute']["totalShareReplicasUsed"] = 3
208 expected['limits']['absolute']["maxTotalReplicaGigabytes"] = 20
209 expected['limits']['absolute']["totalReplicaGigabytesUsed"] = 3
210 # body = jsonutils.loads(response.body)
211 self.assertEqual(expected, response)
213 def _populate_limits_diff_regex(self, request):
214 """Put limit info into a request."""
215 _limits = [
216 limits.Limit("GET", "*", ".*", 10, 60).display(),
217 limits.Limit("GET", "*", "*.*", 10, 60).display(),
218 ]
219 request.environ["manila.limits"] = _limits
220 return request
222 def test_index_diff_regex(self):
223 """Test getting limit details in JSON."""
224 request = self._get_index_request()
225 request = self._populate_limits_diff_regex(request)
226 response = self.controller.index(request)
227 expected = {
228 "limits": {
229 "rate": [
230 {
231 "regex": ".*",
232 "uri": "*",
233 "limit": [
234 {
235 "verb": "GET",
236 "next-available": "1970-01-01T00:00:00Z",
237 "unit": "MINUTE",
238 "value": 10,
239 "remaining": 10,
240 },
241 ],
242 },
243 {
244 "regex": "*.*",
245 "uri": "*",
246 "limit": [
247 {
248 "verb": "GET",
249 "next-available": "1970-01-01T00:00:00Z",
250 "unit": "MINUTE",
251 "value": 10,
252 "remaining": 10,
253 },
254 ],
255 },
257 ],
258 "absolute": {},
259 },
260 }
261 self.assertEqual(expected, response)
263 def _test_index_absolute_limits_json(self, expected):
264 request = self._get_index_request()
265 response = self.controller.index(request)
266 self.assertEqual(expected, response['limits']['absolute'])
268 def test_index_ignores_extra_absolute_limits_json(self):
269 self.absolute_limits = {
270 'in_use': {'unknown_limit': 9000},
271 'limit': {'unknown_limit': 9001},
272 }
273 self._test_index_absolute_limits_json({})
276class TestLimiter(limits.Limiter):
277 pass
280class LimitMiddlewareTest(BaseLimitTestSuite):
281 """Tests for the `limits.RateLimitingMiddleware` class."""
283 @webob.dec.wsgify
284 def _empty_app(self, request):
285 """Do-nothing WSGI app."""
286 pass
288 def setUp(self):
289 """Prepare middleware for use through fake WSGI app."""
290 super(LimitMiddlewareTest, self).setUp()
291 _limits = '(GET, *, .*, 1, MINUTE)'
292 self.app = limits.RateLimitingMiddleware(self._empty_app, _limits,
293 "%s.TestLimiter" %
294 self.__class__.__module__)
296 def test_limit_class(self):
297 """Test that middleware selected correct limiter class."""
298 assert isinstance(self.app._limiter, TestLimiter)
300 def test_good_request(self):
301 """Test successful GET request through middleware."""
302 request = webob.Request.blank("/")
303 response = request.get_response(self.app)
304 self.assertEqual(200, response.status_int)
306 def test_limited_request_json(self):
307 """Test a rate-limited (413) GET request through middleware."""
308 request = webob.Request.blank("/")
309 response = request.get_response(self.app)
310 self.assertEqual(200, response.status_int)
312 request = webob.Request.blank("/")
313 response = request.get_response(self.app)
314 self.assertEqual(413, response.status_int)
316 self.assertIn('Retry-After', response.headers)
317 retry_after = int(response.headers['Retry-After'])
318 self.assertAlmostEqual(retry_after, 60, 1)
320 body = jsonutils.loads(response.body)
321 expected = "Only 1 GET request(s) can be made to * every minute."
322 value = body["overLimitFault"]["details"].strip()
323 self.assertEqual(expected, value)
326class LimitTest(BaseLimitTestSuite):
327 """Tests for the `limits.Limit` class."""
329 def test_GET_no_delay(self):
330 """Test a limit handles 1 GET per second."""
331 limit = limits.Limit("GET", "*", ".*", 1, 1)
332 delay = limit("GET", "/anything")
333 self.assertIsNone(delay)
334 self.assertEqual(0, limit.next_request)
335 self.assertEqual(0, limit.last_request)
337 def test_GET_delay(self):
338 """Test two calls to 1 GET per second limit."""
339 limit = limits.Limit("GET", "*", ".*", 1, 1)
340 delay = limit("GET", "/anything")
341 self.assertIsNone(delay)
343 delay = limit("GET", "/anything")
344 self.assertEqual(1, delay)
345 self.assertEqual(1, limit.next_request)
346 self.assertEqual(0, limit.last_request)
348 self.time += 4
350 delay = limit("GET", "/anything")
351 self.assertIsNone(delay)
352 self.assertEqual(4, limit.next_request)
353 self.assertEqual(4, limit.last_request)
356class ParseLimitsTest(BaseLimitTestSuite):
357 """Test default limits parser.
359 Tests for the default limits parser in the in-memory
360 `limits.Limiter` class.
361 """
363 def test_invalid(self):
364 """Test that parse_limits() handles invalid input correctly."""
365 self.assertRaises(ValueError, limits.Limiter.parse_limits,
366 ';;;;;')
368 def test_bad_rule(self):
369 """Test that parse_limits() handles bad rules correctly."""
370 self.assertRaises(ValueError, limits.Limiter.parse_limits,
371 'GET, *, .*, 20, minute')
373 def test_missing_arg(self):
374 """Test that parse_limits() handles missing args correctly."""
375 self.assertRaises(ValueError, limits.Limiter.parse_limits,
376 '(GET, *, .*, 20)')
378 def test_bad_value(self):
379 """Test that parse_limits() handles bad values correctly."""
380 self.assertRaises(ValueError, limits.Limiter.parse_limits,
381 '(GET, *, .*, foo, minute)')
383 def test_bad_unit(self):
384 """Test that parse_limits() handles bad units correctly."""
385 self.assertRaises(ValueError, limits.Limiter.parse_limits,
386 '(GET, *, .*, 20, lightyears)')
388 def test_multiple_rules(self):
389 """Test that parse_limits() handles multiple rules correctly."""
390 try:
391 lim = limits.Limiter.parse_limits(
392 '(get, *, .*, 20, minute);'
393 '(PUT, /foo*, /foo.*, 10, hour);'
394 '(POST, /bar*, /bar.*, 5, second);'
395 '(Say, /derp*, /derp.*, 1, day)')
396 except ValueError as e:
397 assert False, str(e)
399 # Make sure the number of returned limits are correct
400 self.assertEqual(4, len(lim))
402 # Check all the verbs...
403 expected = ['GET', 'PUT', 'POST', 'SAY']
404 self.assertEqual(expected, [t.verb for t in lim])
406 # ...the URIs...
407 expected = ['*', '/foo*', '/bar*', '/derp*']
408 self.assertEqual(expected, [t.uri for t in lim])
410 # ...the regexes...
411 expected = ['.*', '/foo.*', '/bar.*', '/derp.*']
412 self.assertEqual(expected, [t.regex for t in lim])
414 # ...the values...
415 expected = [20, 10, 5, 1]
416 self.assertEqual(expected, [t.value for t in lim])
418 # ...and the units...
419 expected = [limits.PER_MINUTE, limits.PER_HOUR,
420 limits.PER_SECOND, limits.PER_DAY]
421 self.assertEqual(expected, [t.unit for t in lim])
424class LimiterTest(BaseLimitTestSuite):
425 """Tests for the in-memory `limits.Limiter` class."""
427 def setUp(self):
428 """Run before each test."""
429 super(LimiterTest, self).setUp()
430 userlimits = {'user:user3': ''}
431 self.limiter = limits.Limiter(TEST_LIMITS, **userlimits)
433 def _check(self, num, verb, url, username=None):
434 """Check and yield results from checks."""
435 for x in range(num):
436 yield self.limiter.check_for_delay(verb, url, username)[0]
438 def _check_sum(self, num, verb, url, username=None):
439 """Check and sum results from checks."""
440 results = self._check(num, verb, url, username)
441 return sum(item for item in results if item)
443 def test_no_delay_GET(self):
444 """Test no delay on GET for single call.
446 Simple test to ensure no delay on a single call for a limit verb we
447 didn"t set.
448 """
449 delay = self.limiter.check_for_delay("GET", "/anything")
450 self.assertEqual((None, None), delay)
452 def test_no_delay_PUT(self):
453 """Test no delay on single call.
455 Simple test to ensure no delay on a single call for a known limit.
456 """
457 delay = self.limiter.check_for_delay("PUT", "/anything")
458 self.assertEqual((None, None), delay)
460 def test_delay_PUT(self):
461 """Ensure 11th PUT will be delayed.
463 Ensure the 11th PUT will result in a delay of 6.0 seconds until
464 the next request will be granted.
465 """
466 expected = [None] * 10 + [6.0]
467 results = list(self._check(11, "PUT", "/anything"))
469 self.assertEqual(expected, results)
471 def test_delay_POST(self):
472 """Ensure 8th POST will be delayed.
474 Ensure the 8th POST will result in a delay of 6.0 seconds until
475 the next request will be granced.
476 """
477 expected = [None] * 7
478 results = list(self._check(7, "POST", "/anything"))
479 self.assertEqual(expected, results)
481 expected = 60.0 / 7.0
482 results = self._check_sum(1, "POST", "/anything")
483 self.assertAlmostEqual(expected, results, 8)
485 def test_delay_GET(self):
486 """Ensure the 11th GET will result in NO delay."""
487 expected = [None] * 11
488 results = list(self._check(11, "GET", "/anything"))
490 self.assertEqual(expected, results)
492 def test_delay_PUT_volumes(self):
493 """Ensure PUT limits.
495 Ensure PUT on /volumes limits at 5 requests, and PUT elsewhere is still
496 OK after 5 requests...but then after 11 total requests, PUT limiting
497 kicks in.
498 """
499 # First 6 requests on PUT /volumes
500 expected = [None] * 5 + [12.0]
501 results = list(self._check(6, "PUT", "/shares"))
502 self.assertEqual(expected, results)
504 # Next 5 request on PUT /anything
505 expected = [None] * 4 + [6.0]
506 results = list(self._check(5, "PUT", "/anything"))
507 self.assertEqual(expected, results)
509 def test_delay_PUT_wait(self):
510 """Test limit handling.
512 Ensure after hitting the limit and then waiting for the correct
513 amount of time, the limit will be lifted.
514 """
515 expected = [None] * 10 + [6.0]
516 results = list(self._check(11, "PUT", "/anything"))
517 self.assertEqual(expected, results)
519 # Advance time
520 self.time += 6.0
522 expected = [None, 6.0]
523 results = list(self._check(2, "PUT", "/anything"))
524 self.assertEqual(expected, results)
526 def test_multiple_delays(self):
527 """Ensure multiple requests still get a delay."""
528 expected = [None] * 10 + [6.0] * 10
529 results = list(self._check(20, "PUT", "/anything"))
530 self.assertEqual(expected, results)
532 self.time += 1.0
534 expected = [5.0] * 10
535 results = list(self._check(10, "PUT", "/anything"))
536 self.assertEqual(expected, results)
538 def test_user_limit(self):
539 """Test user-specific limits."""
540 self.assertEqual([], self.limiter.levels['user3'])
542 def test_multiple_users(self):
543 """Tests involving multiple users."""
544 # User1
545 expected = [None] * 10 + [6.0] * 10
546 results = list(self._check(20, "PUT", "/anything", "user1"))
547 self.assertEqual(expected, results)
549 # User2
550 expected = [None] * 10 + [6.0] * 5
551 results = list(self._check(15, "PUT", "/anything", "user2"))
552 self.assertEqual(expected, results)
554 # User3
555 expected = [None] * 20
556 results = list(self._check(20, "PUT", "/anything", "user3"))
557 self.assertEqual(expected, results)
559 self.time += 1.0
561 # User1 again
562 expected = [5.0] * 10
563 results = list(self._check(10, "PUT", "/anything", "user1"))
564 self.assertEqual(expected, results)
566 self.time += 1.0
568 # User1 again
569 expected = [4.0] * 5
570 results = list(self._check(5, "PUT", "/anything", "user2"))
571 self.assertEqual(expected, results)
574class WsgiLimiterTest(BaseLimitTestSuite):
575 """Tests for `limits.WsgiLimiter` class."""
577 def setUp(self):
578 """Run before each test."""
579 super(WsgiLimiterTest, self).setUp()
580 self.app = limits.WsgiLimiter(TEST_LIMITS)
582 def _request_data(self, verb, path):
583 """Get data describing a limit request verb/path."""
584 return jsonutils.dumps({"verb": verb, "path": path}).encode("utf-8")
586 def _request(self, verb, url, username=None):
587 """Send request.
589 Make sure that POSTing to the given url causes the given
590 username to perform the given action. Make the internal rate
591 limiter return delay and make sure that the WSGI app returns
592 the correct response.
593 """
594 if username:
595 request = webob.Request.blank("/%s" % username)
596 else:
597 request = webob.Request.blank("/")
599 request.method = "POST"
600 request.body = self._request_data(verb, url)
601 response = request.get_response(self.app)
603 if "X-Wait-Seconds" in response.headers:
604 self.assertEqual(403, response.status_int)
605 return response.headers["X-Wait-Seconds"]
607 self.assertEqual(204, response.status_int)
609 def test_invalid_methods(self):
610 """Only POSTs should work."""
611 for method in ["GET", "PUT", "DELETE", "HEAD", "OPTIONS"]:
612 request = webob.Request.blank("/", method=method)
613 response = request.get_response(self.app)
614 self.assertEqual(405, response.status_int)
616 def test_good_url(self):
617 delay = self._request("GET", "/something")
618 self.assertIsNone(delay)
620 def test_escaping(self):
621 delay = self._request("GET", "/something/jump%20up")
622 self.assertIsNone(delay)
624 def test_response_to_delays(self):
625 delay = self._request("GET", "/delayed")
626 self.assertIsNone(delay)
628 delay = self._request("GET", "/delayed")
629 self.assertEqual('60.00', delay)
631 def test_response_to_delays_usernames(self):
632 delay = self._request("GET", "/delayed", "user1")
633 self.assertIsNone(delay)
635 delay = self._request("GET", "/delayed", "user2")
636 self.assertIsNone(delay)
638 delay = self._request("GET", "/delayed", "user1")
639 self.assertEqual('60.00', delay)
641 delay = self._request("GET", "/delayed", "user2")
642 self.assertEqual('60.00', delay)
645class FakeHttplibSocket(object):
646 """Fake `http_client.HTTPResponse` replacement."""
648 def __init__(self, response_string):
649 """Initialize new `FakeHttplibSocket`."""
650 self._buffer = io.BytesIO(response_string.encode("utf-8"))
652 def makefile(self, _mode, _other=None):
653 """Returns the socket's internal buffer."""
654 return self._buffer
657class FakeHttplibConnection(object):
658 """Fake `http_client.HTTPConnection`."""
660 def __init__(self, app, host):
661 """Initialize `FakeHttplibConnection`."""
662 self.app = app
663 self.host = host
665 def request(self, method, path, body="", headers=None):
666 """Translate request to WSGI app.
668 Requests made via this connection actually get translated and routed
669 into our WSGI app, we then wait for the response and turn it back into
670 an `http_client.HTTPResponse`.
671 """
672 if not headers: 672 ↛ 673line 672 didn't jump to line 673 because the condition on line 672 was never true
673 headers = {}
675 req = webob.Request.blank(path)
676 req.method = method
677 req.headers = headers
678 req.host = self.host
679 req.body = body.encode("utf-8")
681 resp = str(req.get_response(self.app))
682 resp = "HTTP/1.0 %s" % resp
683 sock = FakeHttplibSocket(resp)
684 self.http_response = http_client.HTTPResponse(sock)
685 self.http_response.begin()
687 def getresponse(self):
688 """Return our generated response from the request."""
689 return self.http_response
692def wire_HTTPConnection_to_WSGI(host, app):
693 """Wire HTTPConnection to WSGI app.
695 Monkeypatches HTTPConnection so that if you try to connect to
696 host, you are instead routed straight to the given WSGI app.
698 After calling this method, when any code calls
700 http_client.HTTPConnection(host)
702 the connection object will be a fake. Its requests will be sent directly
703 to the given WSGI app rather than through a socket.
705 Code connecting to hosts other than host will not be affected.
707 This method may be called multiple times to map different hosts to
708 different apps.
710 This method returns the original HTTPConnection object, so that the caller
711 can restore the default HTTPConnection interface (for all hosts).
712 """
713 class HTTPConnectionDecorator(object):
714 """Wrapper for HTTPConnection class
716 Wraps the real HTTPConnection class so that when you
717 instantiate the class you might instead get a fake instance.
719 """
721 def __init__(self, wrapped):
722 self.wrapped = wrapped
724 def __call__(self, connection_host, *args, **kwargs):
725 if connection_host == host: 725 ↛ 728line 725 didn't jump to line 728 because the condition on line 725 was always true
726 return FakeHttplibConnection(app, host)
727 else:
728 return self.wrapped(connection_host, *args, **kwargs)
730 oldHTTPConnection = http_client.HTTPConnection
731 http_client.HTTPConnection = HTTPConnectionDecorator(
732 http_client.HTTPConnection)
733 return oldHTTPConnection
736class WsgiLimiterProxyTest(BaseLimitTestSuite):
737 """Tests for the `limits.WsgiLimiterProxy` class."""
739 def setUp(self):
740 """Set up HTTP/WSGI magic.
742 Do some nifty HTTP/WSGI magic which allows for WSGI to be called
743 directly by something like the `http_client` library.
744 """
745 super(WsgiLimiterProxyTest, self).setUp()
746 self.app = limits.WsgiLimiter(TEST_LIMITS)
747 self.oldHTTPConnection = (
748 wire_HTTPConnection_to_WSGI("169.254.0.1:80", self.app))
749 self.proxy = limits.WsgiLimiterProxy("169.254.0.1:80")
751 def test_200(self):
752 """Successful request test."""
753 delay = self.proxy.check_for_delay("GET", "/anything")
754 self.assertEqual((None, None), delay)
756 def test_403(self):
757 """Forbidden request test."""
758 delay = self.proxy.check_for_delay("GET", "/delayed")
759 self.assertEqual((None, None), delay)
760 delay, error = self.proxy.check_for_delay("GET", "/delayed")
761 error = error.strip()
763 expected = ("60.00", (
764 "403 Forbidden\n\nOnly 1 GET request(s) can be made to /delayed "
765 "every minute.").encode("utf-8"))
767 self.assertEqual(expected, (delay, error))
769 def tearDown(self):
770 # restore original HTTPConnection object
771 http_client.HTTPConnection = self.oldHTTPConnection
772 super(WsgiLimiterProxyTest, self).tearDown()
775class LimitsViewBuilderTest(test.TestCase):
776 def setUp(self):
777 super(LimitsViewBuilderTest, self).setUp()
778 self.view_builder = views.limits.ViewBuilder()
779 self.rate_limits = [{"URI": "*",
780 "regex": ".*",
781 "value": 10,
782 "verb": "POST",
783 "remaining": 2,
784 "unit": "MINUTE",
785 "resetTime": 1311272226},
786 {"URI": "*/shares",
787 "regex": "^/shares",
788 "value": 50,
789 "verb": "POST",
790 "remaining": 10,
791 "unit": "DAY",
792 "resetTime": 1311272226}]
793 self.absolute_limits = {
794 "limit": {
795 "shares": 111,
796 "gigabytes": 222,
797 "snapshots": 333,
798 "snapshot_gigabytes": 444,
799 "share_networks": 555,
800 },
801 "in_use": {
802 "shares": 65,
803 "gigabytes": 76,
804 "snapshots": 87,
805 "snapshot_gigabytes": 98,
806 "share_networks": 107,
807 },
808 }
810 def test_build_limits(self):
811 request = fakes.HTTPRequest.blank('/')
812 tdate = "2011-07-21T18:17:06Z"
813 expected_limits = {
814 "limits": {
815 "rate": [
816 {"uri": "*",
817 "regex": ".*",
818 "limit": [{"value": 10,
819 "verb": "POST",
820 "remaining": 2,
821 "unit": "MINUTE",
822 "next-available": tdate}]},
823 {"uri": "*/shares",
824 "regex": "^/shares",
825 "limit": [{"value": 50,
826 "verb": "POST",
827 "remaining": 10,
828 "unit": "DAY",
829 "next-available": tdate}]}
830 ],
831 "absolute": {
832 "totalSharesUsed": 65,
833 "totalShareGigabytesUsed": 76,
834 "totalShareSnapshotsUsed": 87,
835 "totalSnapshotGigabytesUsed": 98,
836 "totalShareNetworksUsed": 107,
837 "maxTotalShares": 111,
838 "maxTotalShareGigabytes": 222,
839 "maxTotalShareSnapshots": 333,
840 "maxTotalSnapshotGigabytes": 444,
841 "maxTotalShareNetworks": 555,
842 }
843 }
844 }
846 output = self.view_builder.build(request,
847 self.rate_limits,
848 self.absolute_limits)
849 self.assertDictEqual(expected_limits, output)
851 def test_build_limits_empty_limits(self):
852 request = fakes.HTTPRequest.blank('/')
853 expected_limits = {"limits": {"rate": [], "absolute": {}}}
854 abs_limits = {}
855 rate_limits = []
857 output = self.view_builder.build(request, rate_limits, abs_limits)
859 self.assertDictEqual(expected_limits, output)