diff options
| author | Chuck Short <zulcss@ubuntu.com> | 2011-03-20 10:51:44 -0400 |
|---|---|---|
| committer | Chuck Short <zulcss@ubuntu.com> | 2011-03-20 10:51:44 -0400 |
| commit | a19d643af217d4aa40e48f4e9a75205af51b96dc (patch) | |
| tree | 639c846a580ea4de598e487068c2c62f783ca6d4 /nova/tests | |
| parent | b9480c99adbb7b4849812fa16118712b24de1820 (diff) | |
| parent | 3e255ad1bc685c0d39631540e664bde49e0670db (diff) | |
Merge trunk
Diffstat (limited to 'nova/tests')
| -rw-r--r-- | nova/tests/api/openstack/__init__.py | 2 | ||||
| -rw-r--r-- | nova/tests/api/openstack/fakes.py | 59 | ||||
| -rw-r--r-- | nova/tests/api/openstack/test_accounts.py | 26 | ||||
| -rw-r--r-- | nova/tests/api/openstack/test_adminapi.py | 1 | ||||
| -rw-r--r-- | nova/tests/api/openstack/test_auth.py | 24 | ||||
| -rw-r--r-- | nova/tests/api/openstack/test_limits.py | 584 | ||||
| -rw-r--r-- | nova/tests/api/openstack/test_ratelimiting.py | 243 | ||||
| -rw-r--r-- | nova/tests/api/openstack/test_users.py | 68 | ||||
| -rw-r--r-- | nova/tests/api/test_wsgi.py | 2 | ||||
| -rw-r--r-- | nova/tests/hyperv_unittest.py | 2 | ||||
| -rw-r--r-- | nova/tests/objectstore_unittest.py | 10 | ||||
| -rw-r--r-- | nova/tests/test_api.py | 14 | ||||
| -rw-r--r-- | nova/tests/test_middleware.py | 4 |
13 files changed, 703 insertions, 336 deletions
diff --git a/nova/tests/api/openstack/__init__.py b/nova/tests/api/openstack/__init__.py index e18120285..bac7181f7 100644 --- a/nova/tests/api/openstack/__init__.py +++ b/nova/tests/api/openstack/__init__.py @@ -20,7 +20,7 @@ from nova import test from nova import context from nova import flags -from nova.api.openstack.ratelimiting import RateLimitingMiddleware +from nova.api.openstack.limits import RateLimitingMiddleware from nova.api.openstack.common import limited from nova.tests.api.openstack import fakes from webob import Request diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py index a08fe385a..75eade4d0 100644 --- a/nova/tests/api/openstack/fakes.py +++ b/nova/tests/api/openstack/fakes.py @@ -34,7 +34,7 @@ from nova import utils import nova.api.openstack.auth from nova.api import openstack from nova.api.openstack import auth -from nova.api.openstack import ratelimiting +from nova.api.openstack import limits from nova.auth.manager import User, Project from nova.image import glance from nova.image import local @@ -77,7 +77,7 @@ def wsgi_app(inner_application=None): inner_application = openstack.APIRouter() mapper = urlmap.URLMap() api = openstack.FaultWrapper(auth.AuthMiddleware( - ratelimiting.RateLimitingMiddleware(inner_application))) + limits.RateLimitingMiddleware(inner_application))) mapper['/v1.0'] = api mapper['/v1.1'] = api mapper['/'] = openstack.FaultWrapper(openstack.Versions()) @@ -116,13 +116,13 @@ def stub_out_auth(stubs): def stub_out_rate_limiting(stubs): def fake_rate_init(self, app): - super(ratelimiting.RateLimitingMiddleware, self).__init__(app) + super(limits.RateLimitingMiddleware, self).__init__(app) self.application = app - stubs.Set(nova.api.openstack.ratelimiting.RateLimitingMiddleware, + stubs.Set(nova.api.openstack.limits.RateLimitingMiddleware, '__init__', fake_rate_init) - stubs.Set(nova.api.openstack.ratelimiting.RateLimitingMiddleware, + stubs.Set(nova.api.openstack.limits.RateLimitingMiddleware, '__call__', fake_wsgi) @@ -234,52 +234,57 @@ class FakeAuthDatabase(object): class FakeAuthManager(object): - auth_data = {} + #NOTE(justinsb): Accessing static variables through instances is FUBAR + #NOTE(justinsb): This should also be private! + auth_data = [] projects = {} @classmethod def clear_fakes(cls): - cls.auth_data = {} + cls.auth_data = [] cls.projects = {} @classmethod def reset_fake_data(cls): - cls.auth_data = dict(acc1=User('guy1', 'guy1', 'acc1', - 'fortytwo!', False)) + u1 = User('id1', 'guy1', 'acc1', 'secret1', False) + cls.auth_data = [u1] cls.projects = dict(testacct=Project('testacct', 'testacct', - 'guy1', + 'id1', 'test', [])) - def add_user(self, key, user): - FakeAuthManager.auth_data[key] = user + def add_user(self, user): + FakeAuthManager.auth_data.append(user) def get_users(self): - return FakeAuthManager.auth_data.values() + return FakeAuthManager.auth_data def get_user(self, uid): - for k, v in FakeAuthManager.auth_data.iteritems(): - if v.id == uid: - return v + for user in FakeAuthManager.auth_data: + if user.id == uid: + return user + return None + + def get_user_from_access_key(self, key): + for user in FakeAuthManager.auth_data: + if user.access == key: + return user return None def delete_user(self, uid): - for k, v in FakeAuthManager.auth_data.items(): - if v.id == uid: - del FakeAuthManager.auth_data[k] + for user in FakeAuthManager.auth_data: + if user.id == uid: + FakeAuthManager.auth_data.remove(user) return None def create_user(self, name, access=None, secret=None, admin=False): u = User(name, name, access, secret, admin) - FakeAuthManager.auth_data[access] = u + FakeAuthManager.auth_data.append(u) return u def modify_user(self, user_id, access=None, secret=None, admin=None): - user = None - for k, v in FakeAuthManager.auth_data.iteritems(): - if v.id == user_id: - user = v + user = self.get_user(user_id) if user: user.access = access user.secret = secret @@ -326,12 +331,6 @@ class FakeAuthManager(object): if (user.id in p.member_ids) or (user.id == p.project_manager_id)] - def get_user_from_access_key(self, key): - try: - return FakeAuthManager.auth_data[key] - except KeyError: - raise exc.NotFound - class FakeRateLimiter(object): def __init__(self, application): diff --git a/nova/tests/api/openstack/test_accounts.py b/nova/tests/api/openstack/test_accounts.py index 60edce769..64abcf48c 100644 --- a/nova/tests/api/openstack/test_accounts.py +++ b/nova/tests/api/openstack/test_accounts.py @@ -19,11 +19,9 @@ import json import stubout import webob -import nova.api -import nova.api.openstack.auth -from nova import context from nova import flags from nova import test +from nova.api.openstack import accounts from nova.auth.manager import User from nova.tests.api.openstack import fakes @@ -44,9 +42,9 @@ class AccountsTest(test.TestCase): def setUp(self): super(AccountsTest, self).setUp() self.stubs = stubout.StubOutForTesting() - self.stubs.Set(nova.api.openstack.accounts.Controller, '__init__', + self.stubs.Set(accounts.Controller, '__init__', fake_init) - self.stubs.Set(nova.api.openstack.accounts.Controller, '_check_admin', + self.stubs.Set(accounts.Controller, '_check_admin', fake_admin_check) fakes.FakeAuthManager.clear_fakes() fakes.FakeAuthDatabase.data = {} @@ -57,10 +55,10 @@ class AccountsTest(test.TestCase): self.allow_admin = FLAGS.allow_admin_api FLAGS.allow_admin_api = True fakemgr = fakes.FakeAuthManager() - joeuser = User('guy1', 'guy1', 'acc1', 'fortytwo!', False) - superuser = User('guy2', 'guy2', 'acc2', 'swordfish', True) - fakemgr.add_user(joeuser.access, joeuser) - fakemgr.add_user(superuser.access, superuser) + joeuser = User('id1', 'guy1', 'acc1', 'secret1', False) + superuser = User('id2', 'guy2', 'acc2', 'secret2', True) + fakemgr.add_user(joeuser) + fakemgr.add_user(superuser) fakemgr.create_project('test1', joeuser) fakemgr.create_project('test2', superuser) @@ -76,7 +74,7 @@ class AccountsTest(test.TestCase): self.assertEqual(res_dict['account']['id'], 'test1') self.assertEqual(res_dict['account']['name'], 'test1') - self.assertEqual(res_dict['account']['manager'], 'guy1') + self.assertEqual(res_dict['account']['manager'], 'id1') self.assertEqual(res.status_int, 200) def test_account_delete(self): @@ -88,7 +86,7 @@ class AccountsTest(test.TestCase): def test_account_create(self): body = dict(account=dict(description='test account', - manager='guy1')) + manager='id1')) req = webob.Request.blank('/v1.0/accounts/newacct') req.headers["Content-Type"] = "application/json" req.method = 'PUT' @@ -101,14 +99,14 @@ class AccountsTest(test.TestCase): self.assertEqual(res_dict['account']['id'], 'newacct') self.assertEqual(res_dict['account']['name'], 'newacct') self.assertEqual(res_dict['account']['description'], 'test account') - self.assertEqual(res_dict['account']['manager'], 'guy1') + self.assertEqual(res_dict['account']['manager'], 'id1') self.assertTrue('newacct' in fakes.FakeAuthManager.projects) self.assertEqual(len(fakes.FakeAuthManager.projects.values()), 3) def test_account_update(self): body = dict(account=dict(description='test account', - manager='guy2')) + manager='id2')) req = webob.Request.blank('/v1.0/accounts/test1') req.headers["Content-Type"] = "application/json" req.method = 'PUT' @@ -121,5 +119,5 @@ class AccountsTest(test.TestCase): self.assertEqual(res_dict['account']['id'], 'test1') self.assertEqual(res_dict['account']['name'], 'test1') self.assertEqual(res_dict['account']['description'], 'test account') - self.assertEqual(res_dict['account']['manager'], 'guy2') + self.assertEqual(res_dict['account']['manager'], 'id2') self.assertEqual(len(fakes.FakeAuthManager.projects.values()), 2) diff --git a/nova/tests/api/openstack/test_adminapi.py b/nova/tests/api/openstack/test_adminapi.py index 4568cb9f5..e87255b18 100644 --- a/nova/tests/api/openstack/test_adminapi.py +++ b/nova/tests/api/openstack/test_adminapi.py @@ -23,7 +23,6 @@ from paste import urlmap from nova import flags from nova import test from nova.api import openstack -from nova.api.openstack import ratelimiting from nova.api.openstack import auth from nova.tests.api.openstack import fakes diff --git a/nova/tests/api/openstack/test_auth.py b/nova/tests/api/openstack/test_auth.py index 0448ed701..21596fb25 100644 --- a/nova/tests/api/openstack/test_auth.py +++ b/nova/tests/api/openstack/test_auth.py @@ -39,7 +39,7 @@ class Test(test.TestCase): self.stubs.Set(nova.api.openstack.auth.AuthMiddleware, '__init__', fakes.fake_auth_init) self.stubs.Set(context, 'RequestContext', fakes.FakeRequestContext) - fakes.FakeAuthManager.auth_data = {} + fakes.FakeAuthManager.clear_fakes() fakes.FakeAuthDatabase.data = {} fakes.stub_out_rate_limiting(self.stubs) fakes.stub_out_networking(self.stubs) @@ -51,8 +51,8 @@ class Test(test.TestCase): def test_authorize_user(self): f = fakes.FakeAuthManager() - f.add_user('user1_key', - nova.auth.manager.User(1, 'user1', None, None, None)) + user = nova.auth.manager.User('id1', 'user1', 'user1_key', None, None) + f.add_user(user) req = webob.Request.blank('/v1.0/') req.headers['X-Auth-User'] = 'user1' @@ -66,9 +66,9 @@ class Test(test.TestCase): def test_authorize_token(self): f = fakes.FakeAuthManager() - u = nova.auth.manager.User(1, 'user1', None, None, None) - f.add_user('user1_key', u) - f.create_project('user1_project', u) + user = nova.auth.manager.User('id1', 'user1', 'user1_key', None, None) + f.add_user(user) + f.create_project('user1_project', user) req = webob.Request.blank('/v1.0/', {'HTTP_HOST': 'foo'}) req.headers['X-Auth-User'] = 'user1' @@ -124,8 +124,8 @@ class Test(test.TestCase): def test_bad_user_good_key(self): f = fakes.FakeAuthManager() - u = nova.auth.manager.User(1, 'user1', None, None, None) - f.add_user('user1_key', u) + user = nova.auth.manager.User('id1', 'user1', 'user1_key', None, None) + f.add_user(user) req = webob.Request.blank('/v1.0/') req.headers['X-Auth-User'] = 'unknown_user' @@ -179,7 +179,7 @@ class TestLimiter(test.TestCase): self.stubs.Set(nova.api.openstack.auth.AuthMiddleware, '__init__', fakes.fake_auth_init) self.stubs.Set(context, 'RequestContext', fakes.FakeRequestContext) - fakes.FakeAuthManager.auth_data = {} + fakes.FakeAuthManager.clear_fakes() fakes.FakeAuthDatabase.data = {} fakes.stub_out_networking(self.stubs) @@ -190,9 +190,9 @@ class TestLimiter(test.TestCase): def test_authorize_token(self): f = fakes.FakeAuthManager() - u = nova.auth.manager.User(1, 'user1', None, None, None) - f.add_user('user1_key', u) - f.create_project('test', u) + user = nova.auth.manager.User('id1', 'user1', 'user1_key', None, None) + f.add_user(user) + f.create_project('test', user) req = webob.Request.blank('/v1.0/') req.headers['X-Auth-User'] = 'user1' diff --git a/nova/tests/api/openstack/test_limits.py b/nova/tests/api/openstack/test_limits.py new file mode 100644 index 000000000..05cfacc60 --- /dev/null +++ b/nova/tests/api/openstack/test_limits.py @@ -0,0 +1,584 @@ +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Tests dealing with HTTP rate-limiting. +""" + +import httplib +import json +import StringIO +import stubout +import time +import unittest +import webob + +from xml.dom.minidom import parseString + +from nova.api.openstack import limits +from nova.api.openstack.limits import Limit + + +TEST_LIMITS = [ + Limit("GET", "/delayed", "^/delayed", 1, limits.PER_MINUTE), + Limit("POST", "*", ".*", 7, limits.PER_MINUTE), + Limit("POST", "/servers", "^/servers", 3, limits.PER_MINUTE), + Limit("PUT", "*", "", 10, limits.PER_MINUTE), + Limit("PUT", "/servers", "^/servers", 5, limits.PER_MINUTE), +] + + +class BaseLimitTestSuite(unittest.TestCase): + """Base test suite which provides relevant stubs and time abstraction.""" + + def setUp(self): + """Run before each test.""" + self.time = 0.0 + self.stubs = stubout.StubOutForTesting() + self.stubs.Set(limits.Limit, "_get_time", self._get_time) + + def tearDown(self): + """Run after each test.""" + self.stubs.UnsetAll() + + def _get_time(self): + """Return the "time" according to this test suite.""" + return self.time + + +class LimitsControllerTest(BaseLimitTestSuite): + """ + Tests for `limits.LimitsController` class. + """ + + def setUp(self): + """Run before each test.""" + BaseLimitTestSuite.setUp(self) + self.controller = limits.LimitsController() + + def _get_index_request(self, accept_header="application/json"): + """Helper to set routing arguments.""" + request = webob.Request.blank("/") + request.accept = accept_header + request.environ["wsgiorg.routing_args"] = (None, { + "action": "index", + "controller": "", + }) + return request + + def _populate_limits(self, request): + """Put limit info into a request.""" + _limits = [ + Limit("GET", "*", ".*", 10, 60).display(), + Limit("POST", "*", ".*", 5, 60 * 60).display(), + ] + request.environ["nova.limits"] = _limits + return request + + def test_empty_index_json(self): + """Test getting empty limit details in JSON.""" + request = self._get_index_request() + response = request.get_response(self.controller) + expected = { + "limits": { + "rate": [], + "absolute": {}, + }, + } + body = json.loads(response.body) + self.assertEqual(expected, body) + + def test_index_json(self): + """Test getting limit details in JSON.""" + request = self._get_index_request() + request = self._populate_limits(request) + response = request.get_response(self.controller) + expected = { + "limits": { + "rate": [{ + "regex": ".*", + "resetTime": 0, + "URI": "*", + "value": 10, + "verb": "GET", + "remaining": 10, + "unit": "MINUTE", + }, + { + "regex": ".*", + "resetTime": 0, + "URI": "*", + "value": 5, + "verb": "POST", + "remaining": 5, + "unit": "HOUR", + }], + "absolute": {}, + }, + } + body = json.loads(response.body) + self.assertEqual(expected, body) + + def test_empty_index_xml(self): + """Test getting limit details in XML.""" + request = self._get_index_request("application/xml") + response = request.get_response(self.controller) + + expected = "<limits><rate/><absolute/></limits>" + body = response.body.replace("\n", "").replace(" ", "") + + self.assertEqual(expected, body) + + def test_index_xml(self): + """Test getting limit details in XML.""" + request = self._get_index_request("application/xml") + request = self._populate_limits(request) + response = request.get_response(self.controller) + + expected = parseString(""" + <limits> + <rate> + <limit URI="*" regex=".*" remaining="10" resetTime="0" + unit="MINUTE" value="10" verb="GET"/> + <limit URI="*" regex=".*" remaining="5" resetTime="0" + unit="HOUR" value="5" verb="POST"/> + </rate> + <absolute/> + </limits> + """.replace(" ", "")) + body = parseString(response.body.replace(" ", "")) + + self.assertEqual(expected.toxml(), body.toxml()) + + +class LimitMiddlewareTest(BaseLimitTestSuite): + """ + Tests for the `limits.RateLimitingMiddleware` class. + """ + + @webob.dec.wsgify + def _empty_app(self, request): + """Do-nothing WSGI app.""" + pass + + def setUp(self): + """Prepare middleware for use through fake WSGI app.""" + BaseLimitTestSuite.setUp(self) + _limits = [ + Limit("GET", "*", ".*", 1, 60), + ] + self.app = limits.RateLimitingMiddleware(self._empty_app, _limits) + + def test_good_request(self): + """Test successful GET request through middleware.""" + request = webob.Request.blank("/") + response = request.get_response(self.app) + self.assertEqual(200, response.status_int) + + def test_limited_request_json(self): + """Test a rate-limited (403) GET request through middleware.""" + request = webob.Request.blank("/") + response = request.get_response(self.app) + self.assertEqual(200, response.status_int) + + request = webob.Request.blank("/") + response = request.get_response(self.app) + self.assertEqual(response.status_int, 403) + + body = json.loads(response.body) + expected = "Only 1 GET request(s) can be made to * every minute." + value = body["overLimitFault"]["details"].strip() + self.assertEqual(value, expected) + + def test_limited_request_xml(self): + """Test a rate-limited (403) response as XML""" + request = webob.Request.blank("/") + response = request.get_response(self.app) + self.assertEqual(200, response.status_int) + + request = webob.Request.blank("/") + request.accept = "application/xml" + response = request.get_response(self.app) + self.assertEqual(response.status_int, 403) + + root = parseString(response.body).childNodes[0] + expected = "Only 1 GET request(s) can be made to * every minute." + + details = root.getElementsByTagName("details") + self.assertEqual(details.length, 1) + + value = details.item(0).firstChild.data.strip() + self.assertEqual(value, expected) + + +class LimitTest(BaseLimitTestSuite): + """ + Tests for the `limits.Limit` class. + """ + + def test_GET_no_delay(self): + """Test a limit handles 1 GET per second.""" + limit = Limit("GET", "*", ".*", 1, 1) + delay = limit("GET", "/anything") + self.assertEqual(None, delay) + self.assertEqual(0, limit.next_request) + self.assertEqual(0, limit.last_request) + + def test_GET_delay(self): + """Test two calls to 1 GET per second limit.""" + limit = Limit("GET", "*", ".*", 1, 1) + delay = limit("GET", "/anything") + self.assertEqual(None, delay) + + delay = limit("GET", "/anything") + self.assertEqual(1, delay) + self.assertEqual(1, limit.next_request) + self.assertEqual(0, limit.last_request) + + self.time += 4 + + delay = limit("GET", "/anything") + self.assertEqual(None, delay) + self.assertEqual(4, limit.next_request) + self.assertEqual(4, limit.last_request) + + +class LimiterTest(BaseLimitTestSuite): + """ + Tests for the in-memory `limits.Limiter` class. + """ + + def setUp(self): + """Run before each test.""" + BaseLimitTestSuite.setUp(self) + self.limiter = limits.Limiter(TEST_LIMITS) + + def _check(self, num, verb, url, username=None): + """Check and yield results from checks.""" + for x in xrange(num): + yield self.limiter.check_for_delay(verb, url, username)[0] + + def _check_sum(self, num, verb, url, username=None): + """Check and sum results from checks.""" + results = self._check(num, verb, url, username) + return sum(item for item in results if item) + + def test_no_delay_GET(self): + """ + Simple test to ensure no delay on a single call for a limit verb we + didn"t set. + """ + delay = self.limiter.check_for_delay("GET", "/anything") + self.assertEqual(delay, (None, None)) + + def test_no_delay_PUT(self): + """ + Simple test to ensure no delay on a single call for a known limit. + """ + delay = self.limiter.check_for_delay("PUT", "/anything") + self.assertEqual(delay, (None, None)) + + def test_delay_PUT(self): + """ + Ensure the 11th PUT will result in a delay of 6.0 seconds until + the next request will be granced. + """ + expected = [None] * 10 + [6.0] + results = list(self._check(11, "PUT", "/anything")) + + self.assertEqual(expected, results) + + def test_delay_POST(self): + """ + Ensure the 8th POST will result in a delay of 6.0 seconds until + the next request will be granced. + """ + expected = [None] * 7 + results = list(self._check(7, "POST", "/anything")) + self.assertEqual(expected, results) + + expected = 60.0 / 7.0 + results = self._check_sum(1, "POST", "/anything") + self.failUnlessAlmostEqual(expected, results, 8) + + def test_delay_GET(self): + """ + Ensure the 11th GET will result in NO delay. + """ + expected = [None] * 11 + results = list(self._check(11, "GET", "/anything")) + + self.assertEqual(expected, results) + + def test_delay_PUT_servers(self): + """ + Ensure PUT on /servers limits at 5 requests, and PUT elsewhere is still + OK after 5 requests...but then after 11 total requests, PUT limiting + kicks in. + """ + # First 6 requests on PUT /servers + expected = [None] * 5 + [12.0] + results = list(self._check(6, "PUT", "/servers")) + self.assertEqual(expected, results) + + # Next 5 request on PUT /anything + expected = [None] * 4 + [6.0] + results = list(self._check(5, "PUT", "/anything")) + self.assertEqual(expected, results) + + def test_delay_PUT_wait(self): + """ + Ensure after hitting the limit and then waiting for the correct + amount of time, the limit will be lifted. + """ + expected = [None] * 10 + [6.0] + results = list(self._check(11, "PUT", "/anything")) + self.assertEqual(expected, results) + + # Advance time + self.time += 6.0 + + expected = [None, 6.0] + results = list(self._check(2, "PUT", "/anything")) + self.assertEqual(expected, results) + + def test_multiple_delays(self): + """ + Ensure multiple requests still get a delay. + """ + expected = [None] * 10 + [6.0] * 10 + results = list(self._check(20, "PUT", "/anything")) + self.assertEqual(expected, results) + + self.time += 1.0 + + expected = [5.0] * 10 + results = list(self._check(10, "PUT", "/anything")) + self.assertEqual(expected, results) + + def test_multiple_users(self): + """ + Tests involving multiple users. + """ + # User1 + expected = [None] * 10 + [6.0] * 10 + results = list(self._check(20, "PUT", "/anything", "user1")) + self.assertEqual(expected, results) + + # User2 + expected = [None] * 10 + [6.0] * 5 + results = list(self._check(15, "PUT", "/anything", "user2")) + self.assertEqual(expected, results) + + self.time += 1.0 + + # User1 again + expected = [5.0] * 10 + results = list(self._check(10, "PUT", "/anything", "user1")) + self.assertEqual(expected, results) + + self.time += 1.0 + + # User1 again + expected = [4.0] * 5 + results = list(self._check(5, "PUT", "/anything", "user2")) + self.assertEqual(expected, results) + + +class WsgiLimiterTest(BaseLimitTestSuite): + """ + Tests for `limits.WsgiLimiter` class. + """ + + def setUp(self): + """Run before each test.""" + BaseLimitTestSuite.setUp(self) + self.app = limits.WsgiLimiter(TEST_LIMITS) + + def _request_data(self, verb, path): + """Get data decribing a limit request verb/path.""" + return json.dumps({"verb": verb, "path": path}) + + def _request(self, verb, url, username=None): + """Make sure that POSTing to the given url causes the given username + to perform the given action. Make the internal rate limiter return + delay and make sure that the WSGI app returns the correct response. + """ + if username: + request = webob.Request.blank("/%s" % username) + else: + request = webob.Request.blank("/") + + request.method = "POST" + request.body = self._request_data(verb, url) + response = request.get_response(self.app) + + if "X-Wait-Seconds" in response.headers: + self.assertEqual(response.status_int, 403) + return response.headers["X-Wait-Seconds"] + + self.assertEqual(response.status_int, 204) + + def test_invalid_methods(self): + """Only POSTs should work.""" + requests = [] + for method in ["GET", "PUT", "DELETE", "HEAD", "OPTIONS"]: + request = webob.Request.blank("/") + request.body = self._request_data("GET", "/something") + response = request.get_response(self.app) + self.assertEqual(response.status_int, 405) + + def test_good_url(self): + delay = self._request("GET", "/something") + self.assertEqual(delay, None) + + def test_escaping(self): + delay = self._request("GET", "/something/jump%20up") + self.assertEqual(delay, None) + + def test_response_to_delays(self): + delay = self._request("GET", "/delayed") + self.assertEqual(delay, None) + + delay = self._request("GET", "/delayed") + self.assertEqual(delay, '60.00') + + def test_response_to_delays_usernames(self): + delay = self._request("GET", "/delayed", "user1") + self.assertEqual(delay, None) + + delay = self._request("GET", "/delayed", "user2") + self.assertEqual(delay, None) + + delay = self._request("GET", "/delayed", "user1") + self.assertEqual(delay, '60.00') + + delay = self._request("GET", "/delayed", "user2") + self.assertEqual(delay, '60.00') + + +class FakeHttplibSocket(object): + """ + Fake `httplib.HTTPResponse` replacement. + """ + + def __init__(self, response_string): + """Initialize new `FakeHttplibSocket`.""" + self._buffer = StringIO.StringIO(response_string) + + def makefile(self, _mode, _other): + """Returns the socket's internal buffer.""" + return self._buffer + + +class FakeHttplibConnection(object): + """ + Fake `httplib.HTTPConnection`. + """ + + def __init__(self, app, host): + """ + Initialize `FakeHttplibConnection`. + """ + self.app = app + self.host = host + + def request(self, method, path, body="", headers={}): + """ + Requests made via this connection actually get translated and routed + into our WSGI app, we then wait for the response and turn it back into + an `httplib.HTTPResponse`. + """ + req = webob.Request.blank(path) + req.method = method + req.headers = headers + req.host = self.host + req.body = body + + resp = str(req.get_response(self.app)) + resp = "HTTP/1.0 %s" % resp + sock = FakeHttplibSocket(resp) + self.http_response = httplib.HTTPResponse(sock) + self.http_response.begin() + + def getresponse(self): + """Return our generated response from the request.""" + return self.http_response + + +def wire_HTTPConnection_to_WSGI(host, app): + """Monkeypatches HTTPConnection so that if you try to connect to host, you + are instead routed straight to the given WSGI app. + + After calling this method, when any code calls + + httplib.HTTPConnection(host) + + the connection object will be a fake. Its requests will be sent directly + to the given WSGI app rather than through a socket. + + Code connecting to hosts other than host will not be affected. + + This method may be called multiple times to map different hosts to + different apps. + """ + class HTTPConnectionDecorator(object): + """Wraps the real HTTPConnection class so that when you instantiate + the class you might instead get a fake instance.""" + + def __init__(self, wrapped): + self.wrapped = wrapped + + def __call__(self, connection_host, *args, **kwargs): + if connection_host == host: + return FakeHttplibConnection(app, host) + else: + return self.wrapped(connection_host, *args, **kwargs) + + httplib.HTTPConnection = HTTPConnectionDecorator(httplib.HTTPConnection) + + +class WsgiLimiterProxyTest(BaseLimitTestSuite): + """ + Tests for the `limits.WsgiLimiterProxy` class. + """ + + def setUp(self): + """ + Do some nifty HTTP/WSGI magic which allows for WSGI to be called + directly by something like the `httplib` library. + """ + BaseLimitTestSuite.setUp(self) + self.app = limits.WsgiLimiter(TEST_LIMITS) + wire_HTTPConnection_to_WSGI("169.254.0.1:80", self.app) + self.proxy = limits.WsgiLimiterProxy("169.254.0.1:80") + + def test_200(self): + """Successful request test.""" + delay = self.proxy.check_for_delay("GET", "/anything") + self.assertEqual(delay, (None, None)) + + def test_403(self): + """Forbidden request test.""" + delay = self.proxy.check_for_delay("GET", "/delayed") + self.assertEqual(delay, (None, None)) + + delay, error = self.proxy.check_for_delay("GET", "/delayed") + error = error.strip() + + expected = ("60.00", "403 Forbidden\n\nOnly 1 GET request(s) can be "\ + "made to /delayed every minute.") + + self.assertEqual((delay, error), expected) diff --git a/nova/tests/api/openstack/test_ratelimiting.py b/nova/tests/api/openstack/test_ratelimiting.py deleted file mode 100644 index 9ae90ee20..000000000 --- a/nova/tests/api/openstack/test_ratelimiting.py +++ /dev/null @@ -1,243 +0,0 @@ -import httplib -import StringIO -import time -import webob - -from nova import test -import nova.api.openstack.ratelimiting as ratelimiting - - -class LimiterTest(test.TestCase): - - def setUp(self): - super(LimiterTest, self).setUp() - self.limits = { - 'a': (5, ratelimiting.PER_SECOND), - 'b': (5, ratelimiting.PER_MINUTE), - 'c': (5, ratelimiting.PER_HOUR), - 'd': (1, ratelimiting.PER_SECOND), - 'e': (100, ratelimiting.PER_SECOND)} - self.rl = ratelimiting.Limiter(self.limits) - - def exhaust(self, action, times_until_exhausted, **kwargs): - for i in range(times_until_exhausted): - when = self.rl.perform(action, **kwargs) - self.assertEqual(when, None) - num, period = self.limits[action] - delay = period * 1.0 / num - # Verify that we are now thoroughly delayed - for i in range(10): - when = self.rl.perform(action, **kwargs) - self.assertAlmostEqual(when, delay, 2) - - def test_second(self): - self.exhaust('a', 5) - time.sleep(0.2) - self.exhaust('a', 1) - time.sleep(1) - self.exhaust('a', 5) - - def test_minute(self): - self.exhaust('b', 5) - - def test_one_per_period(self): - def allow_once_and_deny_once(): - when = self.rl.perform('d') - self.assertEqual(when, None) - when = self.rl.perform('d') - self.assertAlmostEqual(when, 1, 2) - return when - time.sleep(allow_once_and_deny_once()) - time.sleep(allow_once_and_deny_once()) - allow_once_and_deny_once() - - def test_we_can_go_indefinitely_if_we_spread_out_requests(self): - for i in range(200): - when = self.rl.perform('e') - self.assertEqual(when, None) - time.sleep(0.01) - - def test_users_get_separate_buckets(self): - self.exhaust('c', 5, username='alice') - self.exhaust('c', 5, username='bob') - self.exhaust('c', 5, username='chuck') - self.exhaust('c', 0, username='chuck') - self.exhaust('c', 0, username='bob') - self.exhaust('c', 0, username='alice') - - -class FakeLimiter(object): - """Fake Limiter class that you can tell how to behave.""" - - def __init__(self, test): - self._action = self._username = self._delay = None - self.test = test - - def mock(self, action, username, delay): - self._action = action - self._username = username - self._delay = delay - - def perform(self, action, username): - self.test.assertEqual(action, self._action) - self.test.assertEqual(username, self._username) - return self._delay - - -class WSGIAppTest(test.TestCase): - - def setUp(self): - super(WSGIAppTest, self).setUp() - self.limiter = FakeLimiter(self) - self.app = ratelimiting.WSGIApp(self.limiter) - - def test_invalid_methods(self): - requests = [] - for method in ['GET', 'PUT', 'DELETE']: - req = webob.Request.blank('/limits/michael/breakdance', - dict(REQUEST_METHOD=method)) - requests.append(req) - for req in requests: - self.assertEqual(req.get_response(self.app).status_int, 405) - - def test_invalid_urls(self): - requests = [] - for prefix in ['limit', '', 'limiter2', 'limiter/limits', 'limiter/1']: - req = webob.Request.blank('/%s/michael/breakdance' % prefix, - dict(REQUEST_METHOD='POST')) - requests.append(req) - for req in requests: - self.assertEqual(req.get_response(self.app).status_int, 404) - - def verify(self, url, username, action, delay=None): - """Make sure that POSTing to the given url causes the given username - to perform the given action. Make the internal rate limiter return - delay and make sure that the WSGI app returns the correct response. - """ - req = webob.Request.blank(url, dict(REQUEST_METHOD='POST')) - self.limiter.mock(action, username, delay) - resp = req.get_response(self.app) - if not delay: - self.assertEqual(resp.status_int, 200) - else: - self.assertEqual(resp.status_int, 403) - self.assertEqual(resp.headers['X-Wait-Seconds'], "%.2f" % delay) - - def test_good_urls(self): - self.verify('/limiter/michael/hoot', 'michael', 'hoot') - - def test_escaping(self): - self.verify('/limiter/michael/jump%20up', 'michael', 'jump up') - - def test_response_to_delays(self): - self.verify('/limiter/michael/hoot', 'michael', 'hoot', 1) - self.verify('/limiter/michael/hoot', 'michael', 'hoot', 1.56) - self.verify('/limiter/michael/hoot', 'michael', 'hoot', 1000) - - -class FakeHttplibSocket(object): - """a fake socket implementation for httplib.HTTPResponse, trivial""" - - def __init__(self, response_string): - self._buffer = StringIO.StringIO(response_string) - - def makefile(self, _mode, _other): - """Returns the socket's internal buffer""" - return self._buffer - - -class FakeHttplibConnection(object): - """A fake httplib.HTTPConnection - - Requests made via this connection actually get translated and routed into - our WSGI app, we then wait for the response and turn it back into - an httplib.HTTPResponse. - """ - def __init__(self, app, host, is_secure=False): - self.app = app - self.host = host - - def request(self, method, path, data='', headers={}): - req = webob.Request.blank(path) - req.method = method - req.body = data - req.headers = headers - req.host = self.host - # Call the WSGI app, get the HTTP response - resp = str(req.get_response(self.app)) - # For some reason, the response doesn't have "HTTP/1.0 " prepended; I - # guess that's a function the web server usually provides. - resp = "HTTP/1.0 %s" % resp - sock = FakeHttplibSocket(resp) - self.http_response = httplib.HTTPResponse(sock) - self.http_response.begin() - - def getresponse(self): - return self.http_response - - -def wire_HTTPConnection_to_WSGI(host, app): - """Monkeypatches HTTPConnection so that if you try to connect to host, you - are instead routed straight to the given WSGI app. - - After calling this method, when any code calls - - httplib.HTTPConnection(host) - - the connection object will be a fake. Its requests will be sent directly - to the given WSGI app rather than through a socket. - - Code connecting to hosts other than host will not be affected. - - This method may be called multiple times to map different hosts to - different apps. - """ - class HTTPConnectionDecorator(object): - """Wraps the real HTTPConnection class so that when you instantiate - the class you might instead get a fake instance.""" - - def __init__(self, wrapped): - self.wrapped = wrapped - - def __call__(self, connection_host, *args, **kwargs): - if connection_host == host: - return FakeHttplibConnection(app, host) - else: - return self.wrapped(connection_host, *args, **kwargs) - - httplib.HTTPConnection = HTTPConnectionDecorator(httplib.HTTPConnection) - - -class WSGIAppProxyTest(test.TestCase): - - def setUp(self): - """Our WSGIAppProxy is going to call across an HTTPConnection to a - WSGIApp running a limiter. The proxy will send input, and the proxy - should receive that same input, pass it to the limiter who gives a - result, and send the expected result back. - - The HTTPConnection isn't real -- it's monkeypatched to point straight - at the WSGIApp. And the limiter isn't real -- it's a fake that - behaves the way we tell it to. - """ - super(WSGIAppProxyTest, self).setUp() - self.limiter = FakeLimiter(self) - app = ratelimiting.WSGIApp(self.limiter) - wire_HTTPConnection_to_WSGI('100.100.100.100:80', app) - self.proxy = ratelimiting.WSGIAppProxy('100.100.100.100:80') - - def test_200(self): - self.limiter.mock('conquer', 'caesar', None) - when = self.proxy.perform('conquer', 'caesar') - self.assertEqual(when, None) - - def test_403(self): - self.limiter.mock('grumble', 'proletariat', 1.5) - when = self.proxy.perform('grumble', 'proletariat') - self.assertEqual(when, 1.5) - - def test_failure(self): - def shouldRaise(): - self.limiter.mock('murder', 'brutus', None) - self.proxy.perform('stab', 'brutus') - self.assertRaises(AssertionError, shouldRaise) diff --git a/nova/tests/api/openstack/test_users.py b/nova/tests/api/openstack/test_users.py index 2dda4319b..effb2f592 100644 --- a/nova/tests/api/openstack/test_users.py +++ b/nova/tests/api/openstack/test_users.py @@ -18,11 +18,10 @@ import json import stubout import webob -import nova.api -import nova.api.openstack.auth -from nova import context from nova import flags from nova import test +from nova import utils +from nova.api.openstack import users from nova.auth.manager import User, Project from nova.tests.api.openstack import fakes @@ -43,14 +42,14 @@ class UsersTest(test.TestCase): def setUp(self): super(UsersTest, self).setUp() self.stubs = stubout.StubOutForTesting() - self.stubs.Set(nova.api.openstack.users.Controller, '__init__', + self.stubs.Set(users.Controller, '__init__', fake_init) - self.stubs.Set(nova.api.openstack.users.Controller, '_check_admin', + self.stubs.Set(users.Controller, '_check_admin', fake_admin_check) - fakes.FakeAuthManager.auth_data = {} + fakes.FakeAuthManager.clear_fakes() fakes.FakeAuthManager.projects = dict(testacct=Project('testacct', 'testacct', - 'guy1', + 'id1', 'test', [])) fakes.FakeAuthDatabase.data = {} @@ -61,10 +60,8 @@ class UsersTest(test.TestCase): self.allow_admin = FLAGS.allow_admin_api FLAGS.allow_admin_api = True fakemgr = fakes.FakeAuthManager() - fakemgr.add_user('acc1', User('guy1', 'guy1', 'acc1', - 'fortytwo!', False)) - fakemgr.add_user('acc2', User('guy2', 'guy2', 'acc2', - 'swordfish', True)) + fakemgr.add_user(User('id1', 'guy1', 'acc1', 'secret1', False)) + fakemgr.add_user(User('id2', 'guy2', 'acc2', 'secret2', True)) def tearDown(self): self.stubs.UnsetAll() @@ -80,28 +77,44 @@ class UsersTest(test.TestCase): self.assertEqual(len(res_dict['users']), 2) def test_get_user_by_id(self): - req = webob.Request.blank('/v1.0/users/guy2') + req = webob.Request.blank('/v1.0/users/id2') res = req.get_response(fakes.wsgi_app()) res_dict = json.loads(res.body) - self.assertEqual(res_dict['user']['id'], 'guy2') + self.assertEqual(res_dict['user']['id'], 'id2') self.assertEqual(res_dict['user']['name'], 'guy2') - self.assertEqual(res_dict['user']['secret'], 'swordfish') + self.assertEqual(res_dict['user']['secret'], 'secret2') self.assertEqual(res_dict['user']['admin'], True) self.assertEqual(res.status_int, 200) def test_user_delete(self): - req = webob.Request.blank('/v1.0/users/guy1') + # Check the user exists + req = webob.Request.blank('/v1.0/users/id1') + res = req.get_response(fakes.wsgi_app()) + res_dict = json.loads(res.body) + + self.assertEqual(res_dict['user']['id'], 'id1') + self.assertEqual(res.status_int, 200) + + # Delete the user + req = webob.Request.blank('/v1.0/users/id1') req.method = 'DELETE' res = req.get_response(fakes.wsgi_app()) - self.assertTrue('guy1' not in [u.id for u in - fakes.FakeAuthManager.auth_data.values()]) + self.assertTrue('id1' not in [u.id for u in + fakes.FakeAuthManager.auth_data]) self.assertEqual(res.status_int, 200) + # Check the user is not returned (and returns 404) + req = webob.Request.blank('/v1.0/users/id1') + res = req.get_response(fakes.wsgi_app()) + res_dict = json.loads(res.body) + self.assertEqual(res.status_int, 404) + def test_user_create(self): + secret = utils.generate_password() body = dict(user=dict(name='test_guy', access='acc3', - secret='invasionIsInNormandy', + secret=secret, admin=True)) req = webob.Request.blank('/v1.0/users') req.headers["Content-Type"] = "application/json" @@ -112,20 +125,25 @@ class UsersTest(test.TestCase): res_dict = json.loads(res.body) self.assertEqual(res.status_int, 200) + + # NOTE(justinsb): This is a questionable assertion in general + # fake sets id=name, but others might not... self.assertEqual(res_dict['user']['id'], 'test_guy') + self.assertEqual(res_dict['user']['name'], 'test_guy') self.assertEqual(res_dict['user']['access'], 'acc3') - self.assertEqual(res_dict['user']['secret'], 'invasionIsInNormandy') + self.assertEqual(res_dict['user']['secret'], secret) self.assertEqual(res_dict['user']['admin'], True) self.assertTrue('test_guy' in [u.id for u in - fakes.FakeAuthManager.auth_data.values()]) - self.assertEqual(len(fakes.FakeAuthManager.auth_data.values()), 3) + fakes.FakeAuthManager.auth_data]) + self.assertEqual(len(fakes.FakeAuthManager.auth_data), 3) def test_user_update(self): + new_secret = utils.generate_password() body = dict(user=dict(name='guy2', access='acc2', - secret='invasionIsInNormandy')) - req = webob.Request.blank('/v1.0/users/guy2') + secret=new_secret)) + req = webob.Request.blank('/v1.0/users/id2') req.headers["Content-Type"] = "application/json" req.method = 'PUT' req.body = json.dumps(body) @@ -134,8 +152,8 @@ class UsersTest(test.TestCase): res_dict = json.loads(res.body) self.assertEqual(res.status_int, 200) - self.assertEqual(res_dict['user']['id'], 'guy2') + self.assertEqual(res_dict['user']['id'], 'id2') self.assertEqual(res_dict['user']['name'], 'guy2') self.assertEqual(res_dict['user']['access'], 'acc2') - self.assertEqual(res_dict['user']['secret'], 'invasionIsInNormandy') + self.assertEqual(res_dict['user']['secret'], new_secret) self.assertEqual(res_dict['user']['admin'], True) diff --git a/nova/tests/api/test_wsgi.py b/nova/tests/api/test_wsgi.py index b1a849cf9..1ecdd1cfb 100644 --- a/nova/tests/api/test_wsgi.py +++ b/nova/tests/api/test_wsgi.py @@ -80,7 +80,7 @@ class ControllerTest(test.TestCase): "attributes": { "test": ["id"]}}} - def show(self, req, id): # pylint: disable-msg=W0622,C0103 + def show(self, req, id): # pylint: disable=W0622,C0103 return {"test": {"id": id}} def __init__(self): diff --git a/nova/tests/hyperv_unittest.py b/nova/tests/hyperv_unittest.py index 3980ae3cb..042819b9c 100644 --- a/nova/tests/hyperv_unittest.py +++ b/nova/tests/hyperv_unittest.py @@ -51,7 +51,7 @@ class HyperVTestCase(test.TestCase): instance_ref = db.instance_create(self.context, instance) conn = hyperv.get_connection(False) - conn._create_vm(instance_ref) # pylint: disable-msg=W0212 + conn._create_vm(instance_ref) # pylint: disable=W0212 found = [n for n in conn.list_instances() if n == instance_ref['name']] self.assertTrue(len(found) == 1) diff --git a/nova/tests/objectstore_unittest.py b/nova/tests/objectstore_unittest.py index 5a1be08eb..4e2ac205e 100644 --- a/nova/tests/objectstore_unittest.py +++ b/nova/tests/objectstore_unittest.py @@ -179,7 +179,7 @@ class ObjectStoreTestCase(test.TestCase): class TestHTTPChannel(http.HTTPChannel): """Dummy site required for twisted.web""" - def checkPersistence(self, _, __): # pylint: disable-msg=C0103 + def checkPersistence(self, _, __): # pylint: disable=C0103 """Otherwise we end up with an unclean reactor.""" return False @@ -209,10 +209,10 @@ class S3APITestCase(test.TestCase): root = S3() self.site = TestSite(root) - # pylint: disable-msg=E1101 + # pylint: disable=E1101 self.listening_port = reactor.listenTCP(0, self.site, interface='127.0.0.1') - # pylint: enable-msg=E1101 + # pylint: enable=E1101 self.tcp_port = self.listening_port.getHost().port if not boto.config.has_section('Boto'): @@ -231,11 +231,11 @@ class S3APITestCase(test.TestCase): self.conn.get_http_connection = get_http_connection - def _ensure_no_buckets(self, buckets): # pylint: disable-msg=C0111 + def _ensure_no_buckets(self, buckets): # pylint: disable=C0111 self.assertEquals(len(buckets), 0, "Bucket list was not empty") return True - def _ensure_one_bucket(self, buckets, name): # pylint: disable-msg=C0111 + def _ensure_one_bucket(self, buckets, name): # pylint: disable=C0111 self.assertEquals(len(buckets), 1, "Bucket list didn't have exactly one element in it") self.assertEquals(buckets[0].name, name, "Wrong name") diff --git a/nova/tests/test_api.py b/nova/tests/test_api.py index d5c54a1c3..fa0e56597 100644 --- a/nova/tests/test_api.py +++ b/nova/tests/test_api.py @@ -20,6 +20,7 @@ import boto from boto.ec2 import regioninfo +from boto.exception import EC2ResponseError import datetime import httplib import random @@ -124,7 +125,7 @@ class ApiEc2TestCase(test.TestCase): self.mox.StubOutWithMock(self.ec2, 'new_http_connection') self.http = FakeHttplibConnection( self.app, '%s:8773' % (self.host), False) - # pylint: disable-msg=E1103 + # pylint: disable=E1103 self.ec2.new_http_connection(host, is_secure).AndReturn(self.http) return self.http @@ -177,6 +178,17 @@ class ApiEc2TestCase(test.TestCase): self.manager.delete_project(project) self.manager.delete_user(user) + def test_terminate_invalid_instance(self): + """Attempt to terminate an invalid instance""" + self.expect_http() + self.mox.ReplayAll() + user = self.manager.create_user('fake', 'fake', 'fake') + project = self.manager.create_project('fake', 'fake', 'fake') + self.assertRaises(EC2ResponseError, self.ec2.terminate_instances, + "i-00000005") + self.manager.delete_project(project) + self.manager.delete_user(user) + def test_get_all_key_pairs(self): """Test that, after creating a user and project and generating a key pair, that the API call to list key pairs works properly""" diff --git a/nova/tests/test_middleware.py b/nova/tests/test_middleware.py index 9d49167ba..6564a6955 100644 --- a/nova/tests/test_middleware.py +++ b/nova/tests/test_middleware.py @@ -40,12 +40,12 @@ def conditional_forbid(req): class LockoutTestCase(test.TestCase): """Test case for the Lockout middleware.""" - def setUp(self): # pylint: disable-msg=C0103 + def setUp(self): # pylint: disable=C0103 super(LockoutTestCase, self).setUp() utils.set_time_override() self.lockout = ec2.Lockout(conditional_forbid) - def tearDown(self): # pylint: disable-msg=C0103 + def tearDown(self): # pylint: disable=C0103 utils.clear_time_override() super(LockoutTestCase, self).tearDown() |
