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

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. 

15 

16""" 

17Tests dealing with HTTP rate-limiting. 

18""" 

19import ddt 

20import http.client as http_client 

21import io 

22 

23from oslo_serialization import jsonutils 

24import webob 

25 

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 

32 

33 

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" 

43 

44 

45class BaseLimitTestSuite(test.TestCase): 

46 """Base test suite which provides relevant stubs and time abstraction.""" 

47 

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 = {} 

53 

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 

62 

63 self.mock_object(manila.quota.QUOTAS, "get_project_quotas", 

64 stub_get_project_quotas) 

65 

66 def _get_time(self): 

67 """Return the "time" according to this test suite.""" 

68 return self.time 

69 

70 

71@ddt.ddt 

72class LimitsControllerTest(BaseLimitTestSuite): 

73 """Tests for `limits.LimitsController` class.""" 

74 

75 def setUp(self): 

76 """Run before each test.""" 

77 super(LimitsControllerTest, self).setUp() 

78 self.controller = limits.LimitsController() 

79 

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 

86 

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 

97 

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) 

109 

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 } 

132 

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 

138 

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 

144 

145 response = self.controller.index(request) 

146 

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 }, 

183 

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 

204 

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) 

212 

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 

221 

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 }, 

256 

257 ], 

258 "absolute": {}, 

259 }, 

260 } 

261 self.assertEqual(expected, response) 

262 

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']) 

267 

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({}) 

274 

275 

276class TestLimiter(limits.Limiter): 

277 pass 

278 

279 

280class LimitMiddlewareTest(BaseLimitTestSuite): 

281 """Tests for the `limits.RateLimitingMiddleware` class.""" 

282 

283 @webob.dec.wsgify 

284 def _empty_app(self, request): 

285 """Do-nothing WSGI app.""" 

286 pass 

287 

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__) 

295 

296 def test_limit_class(self): 

297 """Test that middleware selected correct limiter class.""" 

298 assert isinstance(self.app._limiter, TestLimiter) 

299 

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) 

305 

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) 

311 

312 request = webob.Request.blank("/") 

313 response = request.get_response(self.app) 

314 self.assertEqual(413, response.status_int) 

315 

316 self.assertIn('Retry-After', response.headers) 

317 retry_after = int(response.headers['Retry-After']) 

318 self.assertAlmostEqual(retry_after, 60, 1) 

319 

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) 

324 

325 

326class LimitTest(BaseLimitTestSuite): 

327 """Tests for the `limits.Limit` class.""" 

328 

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) 

336 

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) 

342 

343 delay = limit("GET", "/anything") 

344 self.assertEqual(1, delay) 

345 self.assertEqual(1, limit.next_request) 

346 self.assertEqual(0, limit.last_request) 

347 

348 self.time += 4 

349 

350 delay = limit("GET", "/anything") 

351 self.assertIsNone(delay) 

352 self.assertEqual(4, limit.next_request) 

353 self.assertEqual(4, limit.last_request) 

354 

355 

356class ParseLimitsTest(BaseLimitTestSuite): 

357 """Test default limits parser. 

358 

359 Tests for the default limits parser in the in-memory 

360 `limits.Limiter` class. 

361 """ 

362 

363 def test_invalid(self): 

364 """Test that parse_limits() handles invalid input correctly.""" 

365 self.assertRaises(ValueError, limits.Limiter.parse_limits, 

366 ';;;;;') 

367 

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') 

372 

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)') 

377 

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)') 

382 

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)') 

387 

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) 

398 

399 # Make sure the number of returned limits are correct 

400 self.assertEqual(4, len(lim)) 

401 

402 # Check all the verbs... 

403 expected = ['GET', 'PUT', 'POST', 'SAY'] 

404 self.assertEqual(expected, [t.verb for t in lim]) 

405 

406 # ...the URIs... 

407 expected = ['*', '/foo*', '/bar*', '/derp*'] 

408 self.assertEqual(expected, [t.uri for t in lim]) 

409 

410 # ...the regexes... 

411 expected = ['.*', '/foo.*', '/bar.*', '/derp.*'] 

412 self.assertEqual(expected, [t.regex for t in lim]) 

413 

414 # ...the values... 

415 expected = [20, 10, 5, 1] 

416 self.assertEqual(expected, [t.value for t in lim]) 

417 

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]) 

422 

423 

424class LimiterTest(BaseLimitTestSuite): 

425 """Tests for the in-memory `limits.Limiter` class.""" 

426 

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) 

432 

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] 

437 

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) 

442 

443 def test_no_delay_GET(self): 

444 """Test no delay on GET for single call. 

445 

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) 

451 

452 def test_no_delay_PUT(self): 

453 """Test no delay on single call. 

454 

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) 

459 

460 def test_delay_PUT(self): 

461 """Ensure 11th PUT will be delayed. 

462 

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")) 

468 

469 self.assertEqual(expected, results) 

470 

471 def test_delay_POST(self): 

472 """Ensure 8th POST will be delayed. 

473 

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) 

480 

481 expected = 60.0 / 7.0 

482 results = self._check_sum(1, "POST", "/anything") 

483 self.assertAlmostEqual(expected, results, 8) 

484 

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")) 

489 

490 self.assertEqual(expected, results) 

491 

492 def test_delay_PUT_volumes(self): 

493 """Ensure PUT limits. 

494 

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) 

503 

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) 

508 

509 def test_delay_PUT_wait(self): 

510 """Test limit handling. 

511 

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) 

518 

519 # Advance time 

520 self.time += 6.0 

521 

522 expected = [None, 6.0] 

523 results = list(self._check(2, "PUT", "/anything")) 

524 self.assertEqual(expected, results) 

525 

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) 

531 

532 self.time += 1.0 

533 

534 expected = [5.0] * 10 

535 results = list(self._check(10, "PUT", "/anything")) 

536 self.assertEqual(expected, results) 

537 

538 def test_user_limit(self): 

539 """Test user-specific limits.""" 

540 self.assertEqual([], self.limiter.levels['user3']) 

541 

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) 

548 

549 # User2 

550 expected = [None] * 10 + [6.0] * 5 

551 results = list(self._check(15, "PUT", "/anything", "user2")) 

552 self.assertEqual(expected, results) 

553 

554 # User3 

555 expected = [None] * 20 

556 results = list(self._check(20, "PUT", "/anything", "user3")) 

557 self.assertEqual(expected, results) 

558 

559 self.time += 1.0 

560 

561 # User1 again 

562 expected = [5.0] * 10 

563 results = list(self._check(10, "PUT", "/anything", "user1")) 

564 self.assertEqual(expected, results) 

565 

566 self.time += 1.0 

567 

568 # User1 again 

569 expected = [4.0] * 5 

570 results = list(self._check(5, "PUT", "/anything", "user2")) 

571 self.assertEqual(expected, results) 

572 

573 

574class WsgiLimiterTest(BaseLimitTestSuite): 

575 """Tests for `limits.WsgiLimiter` class.""" 

576 

577 def setUp(self): 

578 """Run before each test.""" 

579 super(WsgiLimiterTest, self).setUp() 

580 self.app = limits.WsgiLimiter(TEST_LIMITS) 

581 

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") 

585 

586 def _request(self, verb, url, username=None): 

587 """Send request. 

588 

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("/") 

598 

599 request.method = "POST" 

600 request.body = self._request_data(verb, url) 

601 response = request.get_response(self.app) 

602 

603 if "X-Wait-Seconds" in response.headers: 

604 self.assertEqual(403, response.status_int) 

605 return response.headers["X-Wait-Seconds"] 

606 

607 self.assertEqual(204, response.status_int) 

608 

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) 

615 

616 def test_good_url(self): 

617 delay = self._request("GET", "/something") 

618 self.assertIsNone(delay) 

619 

620 def test_escaping(self): 

621 delay = self._request("GET", "/something/jump%20up") 

622 self.assertIsNone(delay) 

623 

624 def test_response_to_delays(self): 

625 delay = self._request("GET", "/delayed") 

626 self.assertIsNone(delay) 

627 

628 delay = self._request("GET", "/delayed") 

629 self.assertEqual('60.00', delay) 

630 

631 def test_response_to_delays_usernames(self): 

632 delay = self._request("GET", "/delayed", "user1") 

633 self.assertIsNone(delay) 

634 

635 delay = self._request("GET", "/delayed", "user2") 

636 self.assertIsNone(delay) 

637 

638 delay = self._request("GET", "/delayed", "user1") 

639 self.assertEqual('60.00', delay) 

640 

641 delay = self._request("GET", "/delayed", "user2") 

642 self.assertEqual('60.00', delay) 

643 

644 

645class FakeHttplibSocket(object): 

646 """Fake `http_client.HTTPResponse` replacement.""" 

647 

648 def __init__(self, response_string): 

649 """Initialize new `FakeHttplibSocket`.""" 

650 self._buffer = io.BytesIO(response_string.encode("utf-8")) 

651 

652 def makefile(self, _mode, _other=None): 

653 """Returns the socket's internal buffer.""" 

654 return self._buffer 

655 

656 

657class FakeHttplibConnection(object): 

658 """Fake `http_client.HTTPConnection`.""" 

659 

660 def __init__(self, app, host): 

661 """Initialize `FakeHttplibConnection`.""" 

662 self.app = app 

663 self.host = host 

664 

665 def request(self, method, path, body="", headers=None): 

666 """Translate request to WSGI app. 

667 

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 = {} 

674 

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") 

680 

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() 

686 

687 def getresponse(self): 

688 """Return our generated response from the request.""" 

689 return self.http_response 

690 

691 

692def wire_HTTPConnection_to_WSGI(host, app): 

693 """Wire HTTPConnection to WSGI app. 

694 

695 Monkeypatches HTTPConnection so that if you try to connect to 

696 host, you are instead routed straight to the given WSGI app. 

697 

698 After calling this method, when any code calls 

699 

700 http_client.HTTPConnection(host) 

701 

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. 

704 

705 Code connecting to hosts other than host will not be affected. 

706 

707 This method may be called multiple times to map different hosts to 

708 different apps. 

709 

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 

715 

716 Wraps the real HTTPConnection class so that when you 

717 instantiate the class you might instead get a fake instance. 

718 

719 """ 

720 

721 def __init__(self, wrapped): 

722 self.wrapped = wrapped 

723 

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) 

729 

730 oldHTTPConnection = http_client.HTTPConnection 

731 http_client.HTTPConnection = HTTPConnectionDecorator( 

732 http_client.HTTPConnection) 

733 return oldHTTPConnection 

734 

735 

736class WsgiLimiterProxyTest(BaseLimitTestSuite): 

737 """Tests for the `limits.WsgiLimiterProxy` class.""" 

738 

739 def setUp(self): 

740 """Set up HTTP/WSGI magic. 

741 

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") 

750 

751 def test_200(self): 

752 """Successful request test.""" 

753 delay = self.proxy.check_for_delay("GET", "/anything") 

754 self.assertEqual((None, None), delay) 

755 

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() 

762 

763 expected = ("60.00", ( 

764 "403 Forbidden\n\nOnly 1 GET request(s) can be made to /delayed " 

765 "every minute.").encode("utf-8")) 

766 

767 self.assertEqual(expected, (delay, error)) 

768 

769 def tearDown(self): 

770 # restore original HTTPConnection object 

771 http_client.HTTPConnection = self.oldHTTPConnection 

772 super(WsgiLimiterProxyTest, self).tearDown() 

773 

774 

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 } 

809 

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 } 

845 

846 output = self.view_builder.build(request, 

847 self.rate_limits, 

848 self.absolute_limits) 

849 self.assertDictEqual(expected_limits, output) 

850 

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 = [] 

856 

857 output = self.view_builder.build(request, rate_limits, abs_limits) 

858 

859 self.assertDictEqual(expected_limits, output)