blob: 55ad35560c47638f9c99e91283375f4e9d200cac [file] [log] [blame]
Daniel Mellado3c0aeab2016-01-29 11:30:25 +00001# Copyright 2012 OpenStack Foundation
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
Ihar Hrachyshka59382252016-04-05 15:54:33 +020016import functools
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +020017import math
Ihar Hrachyshka59382252016-04-05 15:54:33 +020018
Daniel Mellado3c0aeab2016-01-29 11:30:25 +000019import netaddr
20from tempest.lib.common.utils import data_utils
21from tempest.lib import exceptions as lib_exc
22from tempest import test
23
Ihar Hrachyshka59382252016-04-05 15:54:33 +020024from neutron.common import constants
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +020025from neutron.common import utils
Daniel Mellado3c0aeab2016-01-29 11:30:25 +000026from neutron.tests.tempest.api import clients
27from neutron.tests.tempest import config
28from neutron.tests.tempest import exceptions
29
30CONF = config.CONF
31
32
33class BaseNetworkTest(test.BaseTestCase):
34
35 """
36 Base class for the Neutron tests that use the Tempest Neutron REST client
37
38 Per the Neutron API Guide, API v1.x was removed from the source code tree
39 (docs.openstack.org/api/openstack-network/2.0/content/Overview-d1e71.html)
40 Therefore, v2.x of the Neutron API is assumed. It is also assumed that the
41 following options are defined in the [network] section of etc/tempest.conf:
42
43 project_network_cidr with a block of cidr's from which smaller blocks
44 can be allocated for tenant networks
45
46 project_network_mask_bits with the mask bits to be used to partition
47 the block defined by tenant-network_cidr
48
49 Finally, it is assumed that the following option is defined in the
50 [service_available] section of etc/tempest.conf
51
52 neutron as True
53 """
54
55 force_tenant_isolation = False
56 credentials = ['primary']
57
58 # Default to ipv4.
59 _ip_version = 4
60
61 @classmethod
62 def get_client_manager(cls, credential_type=None, roles=None,
63 force_new=None):
Genadi Chereshnyacc395c02016-07-25 12:17:37 +030064 manager = super(BaseNetworkTest, cls).get_client_manager(
65 credential_type=credential_type,
66 roles=roles,
67 force_new=force_new
68 )
Daniel Mellado3c0aeab2016-01-29 11:30:25 +000069 # Neutron uses a different clients manager than the one in the Tempest
70 return clients.Manager(manager.credentials)
71
72 @classmethod
73 def skip_checks(cls):
74 super(BaseNetworkTest, cls).skip_checks()
75 if not CONF.service_available.neutron:
76 raise cls.skipException("Neutron support is required")
77 if cls._ip_version == 6 and not CONF.network_feature_enabled.ipv6:
78 raise cls.skipException("IPv6 Tests are disabled.")
79
80 @classmethod
81 def setup_credentials(cls):
82 # Create no network resources for these test.
83 cls.set_network_resources()
84 super(BaseNetworkTest, cls).setup_credentials()
85
86 @classmethod
87 def setup_clients(cls):
88 super(BaseNetworkTest, cls).setup_clients()
89 cls.client = cls.os.network_client
90
91 @classmethod
92 def resource_setup(cls):
93 super(BaseNetworkTest, cls).resource_setup()
94
95 cls.networks = []
Miguel Lavalle124378b2016-09-21 16:41:47 -050096 cls.admin_networks = []
Daniel Mellado3c0aeab2016-01-29 11:30:25 +000097 cls.subnets = []
98 cls.ports = []
99 cls.routers = []
100 cls.floating_ips = []
101 cls.metering_labels = []
102 cls.service_profiles = []
103 cls.flavors = []
104 cls.metering_label_rules = []
105 cls.qos_rules = []
106 cls.qos_policies = []
107 cls.ethertype = "IPv" + str(cls._ip_version)
108 cls.address_scopes = []
109 cls.admin_address_scopes = []
110 cls.subnetpools = []
111 cls.admin_subnetpools = []
Itzik Brownbac51dc2016-10-31 12:25:04 +0000112 cls.security_groups = []
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000113
114 @classmethod
115 def resource_cleanup(cls):
116 if CONF.service_available.neutron:
117 # Clean up QoS rules
118 for qos_rule in cls.qos_rules:
119 cls._try_delete_resource(cls.admin_client.delete_qos_rule,
120 qos_rule['id'])
121 # Clean up QoS policies
122 for qos_policy in cls.qos_policies:
123 cls._try_delete_resource(cls.admin_client.delete_qos_policy,
124 qos_policy['id'])
125 # Clean up floating IPs
126 for floating_ip in cls.floating_ips:
127 cls._try_delete_resource(cls.client.delete_floatingip,
128 floating_ip['id'])
129 # Clean up routers
130 for router in cls.routers:
131 cls._try_delete_resource(cls.delete_router,
132 router)
133 # Clean up metering label rules
134 for metering_label_rule in cls.metering_label_rules:
135 cls._try_delete_resource(
136 cls.admin_client.delete_metering_label_rule,
137 metering_label_rule['id'])
138 # Clean up metering labels
139 for metering_label in cls.metering_labels:
140 cls._try_delete_resource(
141 cls.admin_client.delete_metering_label,
142 metering_label['id'])
143 # Clean up flavors
144 for flavor in cls.flavors:
145 cls._try_delete_resource(
146 cls.admin_client.delete_flavor,
147 flavor['id'])
148 # Clean up service profiles
149 for service_profile in cls.service_profiles:
150 cls._try_delete_resource(
151 cls.admin_client.delete_service_profile,
152 service_profile['id'])
153 # Clean up ports
154 for port in cls.ports:
155 cls._try_delete_resource(cls.client.delete_port,
156 port['id'])
157 # Clean up subnets
158 for subnet in cls.subnets:
159 cls._try_delete_resource(cls.client.delete_subnet,
160 subnet['id'])
161 # Clean up networks
162 for network in cls.networks:
163 cls._try_delete_resource(cls.client.delete_network,
164 network['id'])
165
Miguel Lavalle124378b2016-09-21 16:41:47 -0500166 # Clean up admin networks
167 for network in cls.admin_networks:
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000168 cls._try_delete_resource(cls.admin_client.delete_network,
169 network['id'])
170
Itzik Brownbac51dc2016-10-31 12:25:04 +0000171 # Clean up security groups
172 for secgroup in cls.security_groups:
173 cls._try_delete_resource(cls.client.delete_security_group,
174 secgroup['id'])
175
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000176 for subnetpool in cls.subnetpools:
177 cls._try_delete_resource(cls.client.delete_subnetpool,
178 subnetpool['id'])
179
180 for subnetpool in cls.admin_subnetpools:
181 cls._try_delete_resource(cls.admin_client.delete_subnetpool,
182 subnetpool['id'])
183
184 for address_scope in cls.address_scopes:
185 cls._try_delete_resource(cls.client.delete_address_scope,
186 address_scope['id'])
187
188 for address_scope in cls.admin_address_scopes:
189 cls._try_delete_resource(
190 cls.admin_client.delete_address_scope,
191 address_scope['id'])
192
193 super(BaseNetworkTest, cls).resource_cleanup()
194
195 @classmethod
196 def _try_delete_resource(cls, delete_callable, *args, **kwargs):
197 """Cleanup resources in case of test-failure
198
199 Some resources are explicitly deleted by the test.
200 If the test failed to delete a resource, this method will execute
201 the appropriate delete methods. Otherwise, the method ignores NotFound
202 exceptions thrown for resources that were correctly deleted by the
203 test.
204
205 :param delete_callable: delete method
206 :param args: arguments for delete method
207 :param kwargs: keyword arguments for delete method
208 """
209 try:
210 delete_callable(*args, **kwargs)
211 # if resource is not found, this means it was deleted in the test
212 except lib_exc.NotFound:
213 pass
214
215 @classmethod
216 def create_network(cls, network_name=None, **kwargs):
217 """Wrapper utility that returns a test network."""
218 network_name = network_name or data_utils.rand_name('test-network-')
219
220 body = cls.client.create_network(name=network_name, **kwargs)
221 network = body['network']
222 cls.networks.append(network)
223 return network
224
225 @classmethod
226 def create_shared_network(cls, network_name=None, **post_body):
227 network_name = network_name or data_utils.rand_name('sharednetwork-')
228 post_body.update({'name': network_name, 'shared': True})
229 body = cls.admin_client.create_network(**post_body)
230 network = body['network']
Miguel Lavalle124378b2016-09-21 16:41:47 -0500231 cls.admin_networks.append(network)
232 return network
233
234 @classmethod
235 def create_network_keystone_v3(cls, network_name=None, project_id=None,
236 tenant_id=None, client=None):
237 """Wrapper utility that creates a test network with project_id."""
238 client = client or cls.client
239 network_name = network_name or data_utils.rand_name(
240 'test-network-with-project_id')
241 project_id = cls.client.tenant_id
242 body = client.create_network_keystone_v3(network_name, project_id,
243 tenant_id)
244 network = body['network']
245 if client is cls.client:
246 cls.networks.append(network)
247 else:
248 cls.admin_networks.append(network)
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000249 return network
250
251 @classmethod
252 def create_subnet(cls, network, gateway='', cidr=None, mask_bits=None,
253 ip_version=None, client=None, **kwargs):
254 """Wrapper utility that returns a test subnet."""
255
256 # allow tests to use admin client
257 if not client:
258 client = cls.client
259
260 # The cidr and mask_bits depend on the ip version.
261 ip_version = ip_version if ip_version is not None else cls._ip_version
262 gateway_not_set = gateway == ''
263 if ip_version == 4:
264 cidr = cidr or netaddr.IPNetwork(
265 config.safe_get_config_value(
266 'network', 'project_network_cidr'))
267 mask_bits = (
268 mask_bits or config.safe_get_config_value(
269 'network', 'project_network_mask_bits'))
270 elif ip_version == 6:
271 cidr = (
272 cidr or netaddr.IPNetwork(
273 config.safe_get_config_value(
274 'network', 'project_network_v6_cidr')))
275 mask_bits = (
276 mask_bits or config.safe_get_config_value(
277 'network', 'project_network_v6_mask_bits'))
278 # Find a cidr that is not in use yet and create a subnet with it
279 for subnet_cidr in cidr.subnet(mask_bits):
280 if gateway_not_set:
281 gateway_ip = str(netaddr.IPAddress(subnet_cidr) + 1)
282 else:
283 gateway_ip = gateway
284 try:
285 body = client.create_subnet(
286 network_id=network['id'],
287 cidr=str(subnet_cidr),
288 ip_version=ip_version,
289 gateway_ip=gateway_ip,
290 **kwargs)
291 break
292 except lib_exc.BadRequest as e:
293 is_overlapping_cidr = 'overlaps with another subnet' in str(e)
294 if not is_overlapping_cidr:
295 raise
296 else:
297 message = 'Available CIDR for subnet creation could not be found'
298 raise ValueError(message)
299 subnet = body['subnet']
300 cls.subnets.append(subnet)
301 return subnet
302
303 @classmethod
304 def create_port(cls, network, **kwargs):
305 """Wrapper utility that returns a test port."""
306 body = cls.client.create_port(network_id=network['id'],
307 **kwargs)
308 port = body['port']
309 cls.ports.append(port)
310 return port
311
312 @classmethod
313 def update_port(cls, port, **kwargs):
314 """Wrapper utility that updates a test port."""
315 body = cls.client.update_port(port['id'],
316 **kwargs)
317 return body['port']
318
319 @classmethod
Genadi Chereshnyac0411e92016-07-11 16:59:42 +0300320 def _create_router_with_client(
321 cls, client, router_name=None, admin_state_up=False,
322 external_network_id=None, enable_snat=None, **kwargs
323 ):
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000324 ext_gw_info = {}
325 if external_network_id:
326 ext_gw_info['network_id'] = external_network_id
327 if enable_snat:
328 ext_gw_info['enable_snat'] = enable_snat
Genadi Chereshnyac0411e92016-07-11 16:59:42 +0300329 body = client.create_router(
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000330 router_name, external_gateway_info=ext_gw_info,
331 admin_state_up=admin_state_up, **kwargs)
332 router = body['router']
333 cls.routers.append(router)
334 return router
335
336 @classmethod
Genadi Chereshnyac0411e92016-07-11 16:59:42 +0300337 def create_router(cls, *args, **kwargs):
338 return cls._create_router_with_client(cls.client, *args, **kwargs)
339
340 @classmethod
341 def create_admin_router(cls, *args, **kwargs):
342 return cls._create_router_with_client(cls.admin_manager.network_client,
343 *args, **kwargs)
344
345 @classmethod
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000346 def create_floatingip(cls, external_network_id):
347 """Wrapper utility that returns a test floating IP."""
348 body = cls.client.create_floatingip(
349 floating_network_id=external_network_id)
350 fip = body['floatingip']
351 cls.floating_ips.append(fip)
352 return fip
353
354 @classmethod
355 def create_router_interface(cls, router_id, subnet_id):
356 """Wrapper utility that returns a router interface."""
357 interface = cls.client.add_router_interface_with_subnet_id(
358 router_id, subnet_id)
359 return interface
360
361 @classmethod
Sławek Kapłońskiff294062016-12-04 15:00:54 +0000362 def get_supported_qos_rule_types(cls):
363 body = cls.client.list_qos_rule_types()
364 return [rule_type['type'] for rule_type in body['rule_types']]
365
366 @classmethod
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200367 def create_qos_policy(cls, name, description=None, shared=False,
368 tenant_id=None):
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000369 """Wrapper utility that returns a test QoS policy."""
370 body = cls.admin_client.create_qos_policy(
371 name, description, shared, tenant_id)
372 qos_policy = body['policy']
373 cls.qos_policies.append(qos_policy)
374 return qos_policy
375
376 @classmethod
Sławek Kapłoński153f3452017-03-24 22:04:53 +0000377 def create_qos_bandwidth_limit_rule(cls, policy_id, max_kbps,
378 max_burst_kbps,
379 direction=constants.EGRESS_DIRECTION):
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000380 """Wrapper utility that returns a test QoS bandwidth limit rule."""
381 body = cls.admin_client.create_bandwidth_limit_rule(
Sławek Kapłoński153f3452017-03-24 22:04:53 +0000382 policy_id, max_kbps, max_burst_kbps, direction)
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000383 qos_rule = body['bandwidth_limit_rule']
384 cls.qos_rules.append(qos_rule)
385 return qos_rule
386
387 @classmethod
388 def delete_router(cls, router):
389 body = cls.client.list_router_interfaces(router['id'])
390 interfaces = body['ports']
391 for i in interfaces:
392 try:
393 cls.client.remove_router_interface_with_subnet_id(
394 router['id'], i['fixed_ips'][0]['subnet_id'])
395 except lib_exc.NotFound:
396 pass
397 cls.client.delete_router(router['id'])
398
399 @classmethod
400 def create_address_scope(cls, name, is_admin=False, **kwargs):
401 if is_admin:
402 body = cls.admin_client.create_address_scope(name=name, **kwargs)
403 cls.admin_address_scopes.append(body['address_scope'])
404 else:
405 body = cls.client.create_address_scope(name=name, **kwargs)
406 cls.address_scopes.append(body['address_scope'])
407 return body['address_scope']
408
409 @classmethod
410 def create_subnetpool(cls, name, is_admin=False, **kwargs):
411 if is_admin:
412 body = cls.admin_client.create_subnetpool(name, **kwargs)
413 cls.admin_subnetpools.append(body['subnetpool'])
414 else:
415 body = cls.client.create_subnetpool(name, **kwargs)
416 cls.subnetpools.append(body['subnetpool'])
417 return body['subnetpool']
418
419
420class BaseAdminNetworkTest(BaseNetworkTest):
421
422 credentials = ['primary', 'admin']
423
424 @classmethod
425 def setup_clients(cls):
426 super(BaseAdminNetworkTest, cls).setup_clients()
427 cls.admin_client = cls.os_adm.network_client
428 cls.identity_admin_client = cls.os_adm.tenants_client
429
430 @classmethod
431 def create_metering_label(cls, name, description):
432 """Wrapper utility that returns a test metering label."""
433 body = cls.admin_client.create_metering_label(
434 description=description,
435 name=data_utils.rand_name("metering-label"))
436 metering_label = body['metering_label']
437 cls.metering_labels.append(metering_label)
438 return metering_label
439
440 @classmethod
441 def create_metering_label_rule(cls, remote_ip_prefix, direction,
442 metering_label_id):
443 """Wrapper utility that returns a test metering label rule."""
444 body = cls.admin_client.create_metering_label_rule(
445 remote_ip_prefix=remote_ip_prefix, direction=direction,
446 metering_label_id=metering_label_id)
447 metering_label_rule = body['metering_label_rule']
448 cls.metering_label_rules.append(metering_label_rule)
449 return metering_label_rule
450
451 @classmethod
452 def create_flavor(cls, name, description, service_type):
453 """Wrapper utility that returns a test flavor."""
454 body = cls.admin_client.create_flavor(
455 description=description, service_type=service_type,
456 name=name)
457 flavor = body['flavor']
458 cls.flavors.append(flavor)
459 return flavor
460
461 @classmethod
462 def create_service_profile(cls, description, metainfo, driver):
463 """Wrapper utility that returns a test service profile."""
464 body = cls.admin_client.create_service_profile(
465 driver=driver, metainfo=metainfo, description=description)
466 service_profile = body['service_profile']
467 cls.service_profiles.append(service_profile)
468 return service_profile
469
470 @classmethod
471 def get_unused_ip(cls, net_id, ip_version=None):
Gary Kotton011345f2016-06-15 08:04:31 -0700472 """Get an unused ip address in a allocation pool of net"""
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000473 body = cls.admin_client.list_ports(network_id=net_id)
474 ports = body['ports']
475 used_ips = []
476 for port in ports:
477 used_ips.extend(
478 [fixed_ip['ip_address'] for fixed_ip in port['fixed_ips']])
479 body = cls.admin_client.list_subnets(network_id=net_id)
480 subnets = body['subnets']
481
482 for subnet in subnets:
483 if ip_version and subnet['ip_version'] != ip_version:
484 continue
485 cidr = subnet['cidr']
486 allocation_pools = subnet['allocation_pools']
487 iterators = []
488 if allocation_pools:
489 for allocation_pool in allocation_pools:
490 iterators.append(netaddr.iter_iprange(
491 allocation_pool['start'], allocation_pool['end']))
492 else:
493 net = netaddr.IPNetwork(cidr)
494
495 def _iterip():
496 for ip in net:
497 if ip not in (net.network, net.broadcast):
498 yield ip
499 iterators.append(iter(_iterip()))
500
501 for iterator in iterators:
502 for ip in iterator:
503 if str(ip) not in used_ips:
504 return str(ip)
505
506 message = (
507 "net(%s) has no usable IP address in allocation pools" % net_id)
508 raise exceptions.InvalidConfiguration(message)
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200509
510
Sławek Kapłońskiff294062016-12-04 15:00:54 +0000511def require_qos_rule_type(rule_type):
512 def decorator(f):
513 @functools.wraps(f)
514 def wrapper(self, *func_args, **func_kwargs):
515 if rule_type not in self.get_supported_qos_rule_types():
516 raise self.skipException(
517 "%s rule type is required." % rule_type)
518 return f(self, *func_args, **func_kwargs)
519 return wrapper
520 return decorator
521
522
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200523def _require_sorting(f):
524 @functools.wraps(f)
525 def inner(self, *args, **kwargs):
Ihar Hrachyshka34feb5b2016-06-14 16:16:06 +0200526 if not test.is_extension_enabled("sorting", "network"):
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200527 self.skipTest('Sorting feature is required')
528 return f(self, *args, **kwargs)
529 return inner
530
531
532def _require_pagination(f):
533 @functools.wraps(f)
534 def inner(self, *args, **kwargs):
Ihar Hrachyshka34feb5b2016-06-14 16:16:06 +0200535 if not test.is_extension_enabled("pagination", "network"):
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200536 self.skipTest('Pagination feature is required')
537 return f(self, *args, **kwargs)
538 return inner
539
540
541class BaseSearchCriteriaTest(BaseNetworkTest):
542
543 # This should be defined by subclasses to reflect resource name to test
544 resource = None
545
Armando Migliaccio57581c62016-07-01 10:13:19 -0700546 field = 'name'
547
Ihar Hrachyshkaa8fe5a12016-05-24 14:50:58 +0200548 # NOTE(ihrachys): some names, like those starting with an underscore (_)
549 # are sorted differently depending on whether the plugin implements native
550 # sorting support, or not. So we avoid any such cases here, sticking to
551 # alphanumeric. Also test a case when there are multiple resources with the
552 # same name
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200553 resource_names = ('test1', 'abc1', 'test10', '123test') + ('test1',)
554
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200555 force_tenant_isolation = True
556
Ihar Hrachyshkaa8fe5a12016-05-24 14:50:58 +0200557 list_kwargs = {}
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200558
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200559 list_as_admin = False
560
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200561 def assertSameOrder(self, original, actual):
562 # gracefully handle iterators passed
563 original = list(original)
564 actual = list(actual)
565 self.assertEqual(len(original), len(actual))
566 for expected, res in zip(original, actual):
Armando Migliaccio57581c62016-07-01 10:13:19 -0700567 self.assertEqual(expected[self.field], res[self.field])
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200568
569 @utils.classproperty
570 def plural_name(self):
571 return '%ss' % self.resource
572
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200573 @property
574 def list_client(self):
575 return self.admin_client if self.list_as_admin else self.client
576
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200577 def list_method(self, *args, **kwargs):
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200578 method = getattr(self.list_client, 'list_%s' % self.plural_name)
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200579 kwargs.update(self.list_kwargs)
580 return method(*args, **kwargs)
581
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200582 def get_bare_url(self, url):
583 base_url = self.client.base_url
584 self.assertTrue(url.startswith(base_url))
585 return url[len(base_url):]
586
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200587 @classmethod
588 def _extract_resources(cls, body):
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200589 return body[cls.plural_name]
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200590
591 def _test_list_sorts(self, direction):
592 sort_args = {
593 'sort_dir': direction,
Armando Migliaccio57581c62016-07-01 10:13:19 -0700594 'sort_key': self.field
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200595 }
596 body = self.list_method(**sort_args)
597 resources = self._extract_resources(body)
598 self.assertNotEmpty(
599 resources, "%s list returned is empty" % self.resource)
Armando Migliaccio57581c62016-07-01 10:13:19 -0700600 retrieved_names = [res[self.field] for res in resources]
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200601 expected = sorted(retrieved_names)
602 if direction == constants.SORT_DIRECTION_DESC:
603 expected = list(reversed(expected))
604 self.assertEqual(expected, retrieved_names)
605
606 @_require_sorting
607 def _test_list_sorts_asc(self):
608 self._test_list_sorts(constants.SORT_DIRECTION_ASC)
609
610 @_require_sorting
611 def _test_list_sorts_desc(self):
612 self._test_list_sorts(constants.SORT_DIRECTION_DESC)
613
614 @_require_pagination
615 def _test_list_pagination(self):
616 for limit in range(1, len(self.resource_names) + 1):
617 pagination_args = {
618 'limit': limit,
619 }
620 body = self.list_method(**pagination_args)
621 resources = self._extract_resources(body)
622 self.assertEqual(limit, len(resources))
623
624 @_require_pagination
625 def _test_list_no_pagination_limit_0(self):
626 pagination_args = {
627 'limit': 0,
628 }
629 body = self.list_method(**pagination_args)
630 resources = self._extract_resources(body)
Béla Vancsicsf1806182016-08-23 07:36:18 +0200631 self.assertGreaterEqual(len(resources), len(self.resource_names))
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200632
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200633 def _test_list_pagination_iteratively(self, lister):
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200634 # first, collect all resources for later comparison
635 sort_args = {
636 'sort_dir': constants.SORT_DIRECTION_ASC,
Armando Migliaccio57581c62016-07-01 10:13:19 -0700637 'sort_key': self.field
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200638 }
639 body = self.list_method(**sort_args)
640 expected_resources = self._extract_resources(body)
641 self.assertNotEmpty(expected_resources)
642
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200643 resources = lister(
644 len(expected_resources), sort_args
645 )
646
647 # finally, compare that the list retrieved in one go is identical to
648 # the one containing pagination results
649 self.assertSameOrder(expected_resources, resources)
650
651 def _list_all_with_marker(self, niterations, sort_args):
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200652 # paginate resources one by one, using last fetched resource as a
653 # marker
654 resources = []
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200655 for i in range(niterations):
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200656 pagination_args = sort_args.copy()
657 pagination_args['limit'] = 1
658 if resources:
659 pagination_args['marker'] = resources[-1]['id']
660 body = self.list_method(**pagination_args)
661 resources_ = self._extract_resources(body)
662 self.assertEqual(1, len(resources_))
663 resources.extend(resources_)
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200664 return resources
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200665
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200666 @_require_pagination
667 @_require_sorting
668 def _test_list_pagination_with_marker(self):
669 self._test_list_pagination_iteratively(self._list_all_with_marker)
670
671 def _list_all_with_hrefs(self, niterations, sort_args):
672 # paginate resources one by one, using next href links
673 resources = []
674 prev_links = {}
675
676 for i in range(niterations):
677 if prev_links:
678 uri = self.get_bare_url(prev_links['next'])
679 else:
Ihar Hrachyshka7f79fe62016-06-07 21:23:44 +0200680 sort_args.update(self.list_kwargs)
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200681 uri = self.list_client.build_uri(
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200682 self.plural_name, limit=1, **sort_args)
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200683 prev_links, body = self.list_client.get_uri_with_links(
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200684 self.plural_name, uri
685 )
686 resources_ = self._extract_resources(body)
687 self.assertEqual(1, len(resources_))
688 resources.extend(resources_)
689
690 # The last element is empty and does not contain 'next' link
691 uri = self.get_bare_url(prev_links['next'])
692 prev_links, body = self.client.get_uri_with_links(
693 self.plural_name, uri
694 )
695 self.assertNotIn('next', prev_links)
696
697 # Now walk backwards and compare results
698 resources2 = []
699 for i in range(niterations):
700 uri = self.get_bare_url(prev_links['previous'])
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200701 prev_links, body = self.list_client.get_uri_with_links(
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200702 self.plural_name, uri
703 )
704 resources_ = self._extract_resources(body)
705 self.assertEqual(1, len(resources_))
706 resources2.extend(resources_)
707
708 self.assertSameOrder(resources, reversed(resources2))
709
710 return resources
711
712 @_require_pagination
713 @_require_sorting
714 def _test_list_pagination_with_href_links(self):
715 self._test_list_pagination_iteratively(self._list_all_with_hrefs)
716
717 @_require_pagination
718 @_require_sorting
719 def _test_list_pagination_page_reverse_with_href_links(
720 self, direction=constants.SORT_DIRECTION_ASC):
721 pagination_args = {
722 'sort_dir': direction,
Armando Migliaccio57581c62016-07-01 10:13:19 -0700723 'sort_key': self.field,
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200724 }
725 body = self.list_method(**pagination_args)
726 expected_resources = self._extract_resources(body)
727
728 page_size = 2
729 pagination_args['limit'] = page_size
730
731 prev_links = {}
732 resources = []
733 num_resources = len(expected_resources)
734 niterations = int(math.ceil(float(num_resources) / page_size))
735 for i in range(niterations):
736 if prev_links:
737 uri = self.get_bare_url(prev_links['previous'])
738 else:
Ihar Hrachyshka7f79fe62016-06-07 21:23:44 +0200739 pagination_args.update(self.list_kwargs)
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200740 uri = self.list_client.build_uri(
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200741 self.plural_name, page_reverse=True, **pagination_args)
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200742 prev_links, body = self.list_client.get_uri_with_links(
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200743 self.plural_name, uri
744 )
745 resources_ = self._extract_resources(body)
Béla Vancsicsf1806182016-08-23 07:36:18 +0200746 self.assertGreaterEqual(page_size, len(resources_))
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200747 resources.extend(reversed(resources_))
748
749 self.assertSameOrder(expected_resources, reversed(resources))
750
751 @_require_pagination
752 @_require_sorting
753 def _test_list_pagination_page_reverse_asc(self):
754 self._test_list_pagination_page_reverse(
755 direction=constants.SORT_DIRECTION_ASC)
756
757 @_require_pagination
758 @_require_sorting
759 def _test_list_pagination_page_reverse_desc(self):
760 self._test_list_pagination_page_reverse(
761 direction=constants.SORT_DIRECTION_DESC)
762
763 def _test_list_pagination_page_reverse(self, direction):
764 pagination_args = {
765 'sort_dir': direction,
Armando Migliaccio57581c62016-07-01 10:13:19 -0700766 'sort_key': self.field,
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200767 'limit': 3,
768 }
769 body = self.list_method(**pagination_args)
770 expected_resources = self._extract_resources(body)
771
772 pagination_args['limit'] -= 1
773 pagination_args['marker'] = expected_resources[-1]['id']
774 pagination_args['page_reverse'] = True
775 body = self.list_method(**pagination_args)
776
777 self.assertSameOrder(
778 # the last entry is not included in 2nd result when used as a
779 # marker
780 expected_resources[:-1],
781 self._extract_resources(body))
Victor Morales1be97b42016-09-05 08:50:06 -0500782
783 def _test_list_validation_filters(self):
784 validation_args = {
785 'unknown_filter': 'value',
786 }
787 body = self.list_method(**validation_args)
788 resources = self._extract_resources(body)
789 for resource in resources:
790 self.assertIn(resource['name'], self.resource_names)