blob: 3fc61cbba70077b42ffa65d4f6f7e84714afa830 [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.")
Jakub Libosvar1982aa12017-05-30 11:15:33 +000079 for req_ext in getattr(cls, 'required_extensions', []):
80 if not test.is_extension_enabled(req_ext, 'network'):
81 msg = "%s extension not enabled." % req_ext
82 raise cls.skipException(msg)
Daniel Mellado3c0aeab2016-01-29 11:30:25 +000083
84 @classmethod
85 def setup_credentials(cls):
86 # Create no network resources for these test.
87 cls.set_network_resources()
88 super(BaseNetworkTest, cls).setup_credentials()
89
90 @classmethod
91 def setup_clients(cls):
92 super(BaseNetworkTest, cls).setup_clients()
fumihiko kakumaa216fc12017-07-14 10:43:29 +090093 cls.client = cls.os_primary.network_client
Daniel Mellado3c0aeab2016-01-29 11:30:25 +000094
95 @classmethod
96 def resource_setup(cls):
97 super(BaseNetworkTest, cls).resource_setup()
98
99 cls.networks = []
Miguel Lavalle124378b2016-09-21 16:41:47 -0500100 cls.admin_networks = []
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000101 cls.subnets = []
102 cls.ports = []
103 cls.routers = []
104 cls.floating_ips = []
105 cls.metering_labels = []
106 cls.service_profiles = []
107 cls.flavors = []
108 cls.metering_label_rules = []
109 cls.qos_rules = []
110 cls.qos_policies = []
111 cls.ethertype = "IPv" + str(cls._ip_version)
112 cls.address_scopes = []
113 cls.admin_address_scopes = []
114 cls.subnetpools = []
115 cls.admin_subnetpools = []
Itzik Brownbac51dc2016-10-31 12:25:04 +0000116 cls.security_groups = []
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000117
118 @classmethod
119 def resource_cleanup(cls):
120 if CONF.service_available.neutron:
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000121 # Clean up floating IPs
122 for floating_ip in cls.floating_ips:
123 cls._try_delete_resource(cls.client.delete_floatingip,
124 floating_ip['id'])
125 # Clean up routers
126 for router in cls.routers:
127 cls._try_delete_resource(cls.delete_router,
128 router)
129 # Clean up metering label rules
130 for metering_label_rule in cls.metering_label_rules:
131 cls._try_delete_resource(
132 cls.admin_client.delete_metering_label_rule,
133 metering_label_rule['id'])
134 # Clean up metering labels
135 for metering_label in cls.metering_labels:
136 cls._try_delete_resource(
137 cls.admin_client.delete_metering_label,
138 metering_label['id'])
139 # Clean up flavors
140 for flavor in cls.flavors:
141 cls._try_delete_resource(
142 cls.admin_client.delete_flavor,
143 flavor['id'])
144 # Clean up service profiles
145 for service_profile in cls.service_profiles:
146 cls._try_delete_resource(
147 cls.admin_client.delete_service_profile,
148 service_profile['id'])
149 # Clean up ports
150 for port in cls.ports:
151 cls._try_delete_resource(cls.client.delete_port,
152 port['id'])
153 # Clean up subnets
154 for subnet in cls.subnets:
155 cls._try_delete_resource(cls.client.delete_subnet,
156 subnet['id'])
157 # Clean up networks
158 for network in cls.networks:
159 cls._try_delete_resource(cls.client.delete_network,
160 network['id'])
161
Miguel Lavalle124378b2016-09-21 16:41:47 -0500162 # Clean up admin networks
163 for network in cls.admin_networks:
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000164 cls._try_delete_resource(cls.admin_client.delete_network,
165 network['id'])
166
Itzik Brownbac51dc2016-10-31 12:25:04 +0000167 # Clean up security groups
168 for secgroup in cls.security_groups:
169 cls._try_delete_resource(cls.client.delete_security_group,
170 secgroup['id'])
171
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000172 for subnetpool in cls.subnetpools:
173 cls._try_delete_resource(cls.client.delete_subnetpool,
174 subnetpool['id'])
175
176 for subnetpool in cls.admin_subnetpools:
177 cls._try_delete_resource(cls.admin_client.delete_subnetpool,
178 subnetpool['id'])
179
180 for address_scope in cls.address_scopes:
181 cls._try_delete_resource(cls.client.delete_address_scope,
182 address_scope['id'])
183
184 for address_scope in cls.admin_address_scopes:
185 cls._try_delete_resource(
186 cls.admin_client.delete_address_scope,
187 address_scope['id'])
188
Sławek Kapłońskie100c4d2017-08-23 21:18:34 +0000189 # Clean up QoS rules
190 for qos_rule in cls.qos_rules:
191 cls._try_delete_resource(cls.admin_client.delete_qos_rule,
192 qos_rule['id'])
193 # Clean up QoS policies
194 # as all networks and ports are already removed, QoS policies
195 # shouldn't be "in use"
196 for qos_policy in cls.qos_policies:
197 cls._try_delete_resource(cls.admin_client.delete_qos_policy,
198 qos_policy['id'])
199
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000200 super(BaseNetworkTest, cls).resource_cleanup()
201
202 @classmethod
203 def _try_delete_resource(cls, delete_callable, *args, **kwargs):
204 """Cleanup resources in case of test-failure
205
206 Some resources are explicitly deleted by the test.
207 If the test failed to delete a resource, this method will execute
208 the appropriate delete methods. Otherwise, the method ignores NotFound
209 exceptions thrown for resources that were correctly deleted by the
210 test.
211
212 :param delete_callable: delete method
213 :param args: arguments for delete method
214 :param kwargs: keyword arguments for delete method
215 """
216 try:
217 delete_callable(*args, **kwargs)
218 # if resource is not found, this means it was deleted in the test
219 except lib_exc.NotFound:
220 pass
221
222 @classmethod
Sergey Belousa627ed92016-10-07 14:29:07 +0300223 def create_network(cls, network_name=None, client=None, **kwargs):
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000224 """Wrapper utility that returns a test network."""
225 network_name = network_name or data_utils.rand_name('test-network-')
226
Sergey Belousa627ed92016-10-07 14:29:07 +0300227 client = client or cls.client
228 body = client.create_network(name=network_name, **kwargs)
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000229 network = body['network']
Sławek Kapłońskia694a5f2017-08-24 19:51:22 +0000230 if client is cls.client:
231 cls.networks.append(network)
232 else:
233 cls.admin_networks.append(network)
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000234 return network
235
236 @classmethod
237 def create_shared_network(cls, network_name=None, **post_body):
238 network_name = network_name or data_utils.rand_name('sharednetwork-')
239 post_body.update({'name': network_name, 'shared': True})
240 body = cls.admin_client.create_network(**post_body)
241 network = body['network']
Miguel Lavalle124378b2016-09-21 16:41:47 -0500242 cls.admin_networks.append(network)
243 return network
244
245 @classmethod
246 def create_network_keystone_v3(cls, network_name=None, project_id=None,
247 tenant_id=None, client=None):
248 """Wrapper utility that creates a test network with project_id."""
249 client = client or cls.client
250 network_name = network_name or data_utils.rand_name(
251 'test-network-with-project_id')
252 project_id = cls.client.tenant_id
253 body = client.create_network_keystone_v3(network_name, project_id,
254 tenant_id)
255 network = body['network']
256 if client is cls.client:
257 cls.networks.append(network)
258 else:
259 cls.admin_networks.append(network)
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000260 return network
261
262 @classmethod
263 def create_subnet(cls, network, gateway='', cidr=None, mask_bits=None,
264 ip_version=None, client=None, **kwargs):
265 """Wrapper utility that returns a test subnet."""
266
267 # allow tests to use admin client
268 if not client:
269 client = cls.client
270
271 # The cidr and mask_bits depend on the ip version.
272 ip_version = ip_version if ip_version is not None else cls._ip_version
273 gateway_not_set = gateway == ''
274 if ip_version == 4:
275 cidr = cidr or netaddr.IPNetwork(
276 config.safe_get_config_value(
277 'network', 'project_network_cidr'))
278 mask_bits = (
279 mask_bits or config.safe_get_config_value(
280 'network', 'project_network_mask_bits'))
281 elif ip_version == 6:
282 cidr = (
283 cidr or netaddr.IPNetwork(
284 config.safe_get_config_value(
285 'network', 'project_network_v6_cidr')))
286 mask_bits = (
287 mask_bits or config.safe_get_config_value(
288 'network', 'project_network_v6_mask_bits'))
289 # Find a cidr that is not in use yet and create a subnet with it
290 for subnet_cidr in cidr.subnet(mask_bits):
291 if gateway_not_set:
292 gateway_ip = str(netaddr.IPAddress(subnet_cidr) + 1)
293 else:
294 gateway_ip = gateway
295 try:
296 body = client.create_subnet(
297 network_id=network['id'],
298 cidr=str(subnet_cidr),
299 ip_version=ip_version,
300 gateway_ip=gateway_ip,
301 **kwargs)
302 break
303 except lib_exc.BadRequest as e:
304 is_overlapping_cidr = 'overlaps with another subnet' in str(e)
305 if not is_overlapping_cidr:
306 raise
307 else:
308 message = 'Available CIDR for subnet creation could not be found'
309 raise ValueError(message)
310 subnet = body['subnet']
311 cls.subnets.append(subnet)
312 return subnet
313
314 @classmethod
315 def create_port(cls, network, **kwargs):
316 """Wrapper utility that returns a test port."""
317 body = cls.client.create_port(network_id=network['id'],
318 **kwargs)
319 port = body['port']
320 cls.ports.append(port)
321 return port
322
323 @classmethod
324 def update_port(cls, port, **kwargs):
325 """Wrapper utility that updates a test port."""
326 body = cls.client.update_port(port['id'],
327 **kwargs)
328 return body['port']
329
330 @classmethod
Genadi Chereshnyac0411e92016-07-11 16:59:42 +0300331 def _create_router_with_client(
332 cls, client, router_name=None, admin_state_up=False,
333 external_network_id=None, enable_snat=None, **kwargs
334 ):
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000335 ext_gw_info = {}
336 if external_network_id:
337 ext_gw_info['network_id'] = external_network_id
YAMAMOTO Takashi9bd4f972017-06-20 12:49:30 +0900338 if enable_snat is not None:
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000339 ext_gw_info['enable_snat'] = enable_snat
Genadi Chereshnyac0411e92016-07-11 16:59:42 +0300340 body = client.create_router(
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000341 router_name, external_gateway_info=ext_gw_info,
342 admin_state_up=admin_state_up, **kwargs)
343 router = body['router']
344 cls.routers.append(router)
345 return router
346
347 @classmethod
Genadi Chereshnyac0411e92016-07-11 16:59:42 +0300348 def create_router(cls, *args, **kwargs):
349 return cls._create_router_with_client(cls.client, *args, **kwargs)
350
351 @classmethod
352 def create_admin_router(cls, *args, **kwargs):
rajat294495c042017-06-28 15:37:16 +0530353 return cls._create_router_with_client(cls.os_admin.network_client,
Genadi Chereshnyac0411e92016-07-11 16:59:42 +0300354 *args, **kwargs)
355
356 @classmethod
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000357 def create_floatingip(cls, external_network_id):
358 """Wrapper utility that returns a test floating IP."""
359 body = cls.client.create_floatingip(
360 floating_network_id=external_network_id)
361 fip = body['floatingip']
362 cls.floating_ips.append(fip)
363 return fip
364
365 @classmethod
366 def create_router_interface(cls, router_id, subnet_id):
367 """Wrapper utility that returns a router interface."""
368 interface = cls.client.add_router_interface_with_subnet_id(
369 router_id, subnet_id)
370 return interface
371
372 @classmethod
Sławek Kapłońskiff294062016-12-04 15:00:54 +0000373 def get_supported_qos_rule_types(cls):
374 body = cls.client.list_qos_rule_types()
375 return [rule_type['type'] for rule_type in body['rule_types']]
376
377 @classmethod
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200378 def create_qos_policy(cls, name, description=None, shared=False,
Hirofumi Ichihara39a6ee12017-08-23 13:55:12 +0900379 tenant_id=None, is_default=False):
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000380 """Wrapper utility that returns a test QoS policy."""
381 body = cls.admin_client.create_qos_policy(
Hirofumi Ichihara39a6ee12017-08-23 13:55:12 +0900382 name, description, shared, tenant_id, is_default)
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000383 qos_policy = body['policy']
384 cls.qos_policies.append(qos_policy)
385 return qos_policy
386
387 @classmethod
Sławek Kapłoński153f3452017-03-24 22:04:53 +0000388 def create_qos_bandwidth_limit_rule(cls, policy_id, max_kbps,
389 max_burst_kbps,
390 direction=constants.EGRESS_DIRECTION):
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000391 """Wrapper utility that returns a test QoS bandwidth limit rule."""
392 body = cls.admin_client.create_bandwidth_limit_rule(
Sławek Kapłoński153f3452017-03-24 22:04:53 +0000393 policy_id, max_kbps, max_burst_kbps, direction)
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000394 qos_rule = body['bandwidth_limit_rule']
395 cls.qos_rules.append(qos_rule)
396 return qos_rule
397
398 @classmethod
399 def delete_router(cls, router):
400 body = cls.client.list_router_interfaces(router['id'])
401 interfaces = body['ports']
402 for i in interfaces:
403 try:
404 cls.client.remove_router_interface_with_subnet_id(
405 router['id'], i['fixed_ips'][0]['subnet_id'])
406 except lib_exc.NotFound:
407 pass
408 cls.client.delete_router(router['id'])
409
410 @classmethod
411 def create_address_scope(cls, name, is_admin=False, **kwargs):
412 if is_admin:
413 body = cls.admin_client.create_address_scope(name=name, **kwargs)
414 cls.admin_address_scopes.append(body['address_scope'])
415 else:
416 body = cls.client.create_address_scope(name=name, **kwargs)
417 cls.address_scopes.append(body['address_scope'])
418 return body['address_scope']
419
420 @classmethod
421 def create_subnetpool(cls, name, is_admin=False, **kwargs):
422 if is_admin:
423 body = cls.admin_client.create_subnetpool(name, **kwargs)
424 cls.admin_subnetpools.append(body['subnetpool'])
425 else:
426 body = cls.client.create_subnetpool(name, **kwargs)
427 cls.subnetpools.append(body['subnetpool'])
428 return body['subnetpool']
429
430
431class BaseAdminNetworkTest(BaseNetworkTest):
432
433 credentials = ['primary', 'admin']
434
435 @classmethod
436 def setup_clients(cls):
437 super(BaseAdminNetworkTest, cls).setup_clients()
fumihiko kakumaa216fc12017-07-14 10:43:29 +0900438 cls.admin_client = cls.os_admin.network_client
Jakub Libosvarf5758012017-08-15 13:45:30 +0000439 cls.identity_admin_client = cls.os_admin.projects_client
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000440
441 @classmethod
442 def create_metering_label(cls, name, description):
443 """Wrapper utility that returns a test metering label."""
444 body = cls.admin_client.create_metering_label(
445 description=description,
446 name=data_utils.rand_name("metering-label"))
447 metering_label = body['metering_label']
448 cls.metering_labels.append(metering_label)
449 return metering_label
450
451 @classmethod
452 def create_metering_label_rule(cls, remote_ip_prefix, direction,
453 metering_label_id):
454 """Wrapper utility that returns a test metering label rule."""
455 body = cls.admin_client.create_metering_label_rule(
456 remote_ip_prefix=remote_ip_prefix, direction=direction,
457 metering_label_id=metering_label_id)
458 metering_label_rule = body['metering_label_rule']
459 cls.metering_label_rules.append(metering_label_rule)
460 return metering_label_rule
461
462 @classmethod
463 def create_flavor(cls, name, description, service_type):
464 """Wrapper utility that returns a test flavor."""
465 body = cls.admin_client.create_flavor(
466 description=description, service_type=service_type,
467 name=name)
468 flavor = body['flavor']
469 cls.flavors.append(flavor)
470 return flavor
471
472 @classmethod
473 def create_service_profile(cls, description, metainfo, driver):
474 """Wrapper utility that returns a test service profile."""
475 body = cls.admin_client.create_service_profile(
476 driver=driver, metainfo=metainfo, description=description)
477 service_profile = body['service_profile']
478 cls.service_profiles.append(service_profile)
479 return service_profile
480
481 @classmethod
482 def get_unused_ip(cls, net_id, ip_version=None):
Gary Kotton011345f2016-06-15 08:04:31 -0700483 """Get an unused ip address in a allocation pool of net"""
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000484 body = cls.admin_client.list_ports(network_id=net_id)
485 ports = body['ports']
486 used_ips = []
487 for port in ports:
488 used_ips.extend(
489 [fixed_ip['ip_address'] for fixed_ip in port['fixed_ips']])
490 body = cls.admin_client.list_subnets(network_id=net_id)
491 subnets = body['subnets']
492
493 for subnet in subnets:
494 if ip_version and subnet['ip_version'] != ip_version:
495 continue
496 cidr = subnet['cidr']
497 allocation_pools = subnet['allocation_pools']
498 iterators = []
499 if allocation_pools:
500 for allocation_pool in allocation_pools:
501 iterators.append(netaddr.iter_iprange(
502 allocation_pool['start'], allocation_pool['end']))
503 else:
504 net = netaddr.IPNetwork(cidr)
505
506 def _iterip():
507 for ip in net:
508 if ip not in (net.network, net.broadcast):
509 yield ip
510 iterators.append(iter(_iterip()))
511
512 for iterator in iterators:
513 for ip in iterator:
514 if str(ip) not in used_ips:
515 return str(ip)
516
517 message = (
518 "net(%s) has no usable IP address in allocation pools" % net_id)
519 raise exceptions.InvalidConfiguration(message)
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200520
521
Sławek Kapłońskiff294062016-12-04 15:00:54 +0000522def require_qos_rule_type(rule_type):
523 def decorator(f):
524 @functools.wraps(f)
525 def wrapper(self, *func_args, **func_kwargs):
526 if rule_type not in self.get_supported_qos_rule_types():
527 raise self.skipException(
528 "%s rule type is required." % rule_type)
529 return f(self, *func_args, **func_kwargs)
530 return wrapper
531 return decorator
532
533
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200534def _require_sorting(f):
535 @functools.wraps(f)
536 def inner(self, *args, **kwargs):
Ihar Hrachyshka34feb5b2016-06-14 16:16:06 +0200537 if not test.is_extension_enabled("sorting", "network"):
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200538 self.skipTest('Sorting feature is required')
539 return f(self, *args, **kwargs)
540 return inner
541
542
543def _require_pagination(f):
544 @functools.wraps(f)
545 def inner(self, *args, **kwargs):
Ihar Hrachyshka34feb5b2016-06-14 16:16:06 +0200546 if not test.is_extension_enabled("pagination", "network"):
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200547 self.skipTest('Pagination feature is required')
548 return f(self, *args, **kwargs)
549 return inner
550
551
552class BaseSearchCriteriaTest(BaseNetworkTest):
553
554 # This should be defined by subclasses to reflect resource name to test
555 resource = None
556
Armando Migliaccio57581c62016-07-01 10:13:19 -0700557 field = 'name'
558
Ihar Hrachyshkaa8fe5a12016-05-24 14:50:58 +0200559 # NOTE(ihrachys): some names, like those starting with an underscore (_)
560 # are sorted differently depending on whether the plugin implements native
561 # sorting support, or not. So we avoid any such cases here, sticking to
562 # alphanumeric. Also test a case when there are multiple resources with the
563 # same name
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200564 resource_names = ('test1', 'abc1', 'test10', '123test') + ('test1',)
565
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200566 force_tenant_isolation = True
567
Ihar Hrachyshkaa8fe5a12016-05-24 14:50:58 +0200568 list_kwargs = {}
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200569
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200570 list_as_admin = False
571
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200572 def assertSameOrder(self, original, actual):
573 # gracefully handle iterators passed
574 original = list(original)
575 actual = list(actual)
576 self.assertEqual(len(original), len(actual))
577 for expected, res in zip(original, actual):
Armando Migliaccio57581c62016-07-01 10:13:19 -0700578 self.assertEqual(expected[self.field], res[self.field])
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200579
580 @utils.classproperty
581 def plural_name(self):
582 return '%ss' % self.resource
583
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200584 @property
585 def list_client(self):
586 return self.admin_client if self.list_as_admin else self.client
587
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200588 def list_method(self, *args, **kwargs):
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200589 method = getattr(self.list_client, 'list_%s' % self.plural_name)
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200590 kwargs.update(self.list_kwargs)
591 return method(*args, **kwargs)
592
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200593 def get_bare_url(self, url):
594 base_url = self.client.base_url
595 self.assertTrue(url.startswith(base_url))
596 return url[len(base_url):]
597
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200598 @classmethod
599 def _extract_resources(cls, body):
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200600 return body[cls.plural_name]
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200601
602 def _test_list_sorts(self, direction):
603 sort_args = {
604 'sort_dir': direction,
Armando Migliaccio57581c62016-07-01 10:13:19 -0700605 'sort_key': self.field
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200606 }
607 body = self.list_method(**sort_args)
608 resources = self._extract_resources(body)
609 self.assertNotEmpty(
610 resources, "%s list returned is empty" % self.resource)
Armando Migliaccio57581c62016-07-01 10:13:19 -0700611 retrieved_names = [res[self.field] for res in resources]
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200612 expected = sorted(retrieved_names)
613 if direction == constants.SORT_DIRECTION_DESC:
614 expected = list(reversed(expected))
615 self.assertEqual(expected, retrieved_names)
616
617 @_require_sorting
618 def _test_list_sorts_asc(self):
619 self._test_list_sorts(constants.SORT_DIRECTION_ASC)
620
621 @_require_sorting
622 def _test_list_sorts_desc(self):
623 self._test_list_sorts(constants.SORT_DIRECTION_DESC)
624
625 @_require_pagination
626 def _test_list_pagination(self):
627 for limit in range(1, len(self.resource_names) + 1):
628 pagination_args = {
629 'limit': limit,
630 }
631 body = self.list_method(**pagination_args)
632 resources = self._extract_resources(body)
633 self.assertEqual(limit, len(resources))
634
635 @_require_pagination
636 def _test_list_no_pagination_limit_0(self):
637 pagination_args = {
638 'limit': 0,
639 }
640 body = self.list_method(**pagination_args)
641 resources = self._extract_resources(body)
Béla Vancsicsf1806182016-08-23 07:36:18 +0200642 self.assertGreaterEqual(len(resources), len(self.resource_names))
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200643
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200644 def _test_list_pagination_iteratively(self, lister):
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200645 # first, collect all resources for later comparison
646 sort_args = {
647 'sort_dir': constants.SORT_DIRECTION_ASC,
Armando Migliaccio57581c62016-07-01 10:13:19 -0700648 'sort_key': self.field
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200649 }
650 body = self.list_method(**sort_args)
651 expected_resources = self._extract_resources(body)
652 self.assertNotEmpty(expected_resources)
653
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200654 resources = lister(
655 len(expected_resources), sort_args
656 )
657
658 # finally, compare that the list retrieved in one go is identical to
659 # the one containing pagination results
660 self.assertSameOrder(expected_resources, resources)
661
662 def _list_all_with_marker(self, niterations, sort_args):
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200663 # paginate resources one by one, using last fetched resource as a
664 # marker
665 resources = []
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200666 for i in range(niterations):
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200667 pagination_args = sort_args.copy()
668 pagination_args['limit'] = 1
669 if resources:
670 pagination_args['marker'] = resources[-1]['id']
671 body = self.list_method(**pagination_args)
672 resources_ = self._extract_resources(body)
673 self.assertEqual(1, len(resources_))
674 resources.extend(resources_)
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200675 return resources
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200676
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200677 @_require_pagination
678 @_require_sorting
679 def _test_list_pagination_with_marker(self):
680 self._test_list_pagination_iteratively(self._list_all_with_marker)
681
682 def _list_all_with_hrefs(self, niterations, sort_args):
683 # paginate resources one by one, using next href links
684 resources = []
685 prev_links = {}
686
687 for i in range(niterations):
688 if prev_links:
689 uri = self.get_bare_url(prev_links['next'])
690 else:
Ihar Hrachyshka7f79fe62016-06-07 21:23:44 +0200691 sort_args.update(self.list_kwargs)
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200692 uri = self.list_client.build_uri(
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200693 self.plural_name, limit=1, **sort_args)
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200694 prev_links, body = self.list_client.get_uri_with_links(
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200695 self.plural_name, uri
696 )
697 resources_ = self._extract_resources(body)
698 self.assertEqual(1, len(resources_))
699 resources.extend(resources_)
700
701 # The last element is empty and does not contain 'next' link
702 uri = self.get_bare_url(prev_links['next'])
703 prev_links, body = self.client.get_uri_with_links(
704 self.plural_name, uri
705 )
706 self.assertNotIn('next', prev_links)
707
708 # Now walk backwards and compare results
709 resources2 = []
710 for i in range(niterations):
711 uri = self.get_bare_url(prev_links['previous'])
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200712 prev_links, body = self.list_client.get_uri_with_links(
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200713 self.plural_name, uri
714 )
715 resources_ = self._extract_resources(body)
716 self.assertEqual(1, len(resources_))
717 resources2.extend(resources_)
718
719 self.assertSameOrder(resources, reversed(resources2))
720
721 return resources
722
723 @_require_pagination
724 @_require_sorting
725 def _test_list_pagination_with_href_links(self):
726 self._test_list_pagination_iteratively(self._list_all_with_hrefs)
727
728 @_require_pagination
729 @_require_sorting
730 def _test_list_pagination_page_reverse_with_href_links(
731 self, direction=constants.SORT_DIRECTION_ASC):
732 pagination_args = {
733 'sort_dir': direction,
Armando Migliaccio57581c62016-07-01 10:13:19 -0700734 'sort_key': self.field,
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200735 }
736 body = self.list_method(**pagination_args)
737 expected_resources = self._extract_resources(body)
738
739 page_size = 2
740 pagination_args['limit'] = page_size
741
742 prev_links = {}
743 resources = []
744 num_resources = len(expected_resources)
745 niterations = int(math.ceil(float(num_resources) / page_size))
746 for i in range(niterations):
747 if prev_links:
748 uri = self.get_bare_url(prev_links['previous'])
749 else:
Ihar Hrachyshka7f79fe62016-06-07 21:23:44 +0200750 pagination_args.update(self.list_kwargs)
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200751 uri = self.list_client.build_uri(
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200752 self.plural_name, page_reverse=True, **pagination_args)
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200753 prev_links, body = self.list_client.get_uri_with_links(
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200754 self.plural_name, uri
755 )
756 resources_ = self._extract_resources(body)
Béla Vancsicsf1806182016-08-23 07:36:18 +0200757 self.assertGreaterEqual(page_size, len(resources_))
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200758 resources.extend(reversed(resources_))
759
760 self.assertSameOrder(expected_resources, reversed(resources))
761
762 @_require_pagination
763 @_require_sorting
764 def _test_list_pagination_page_reverse_asc(self):
765 self._test_list_pagination_page_reverse(
766 direction=constants.SORT_DIRECTION_ASC)
767
768 @_require_pagination
769 @_require_sorting
770 def _test_list_pagination_page_reverse_desc(self):
771 self._test_list_pagination_page_reverse(
772 direction=constants.SORT_DIRECTION_DESC)
773
774 def _test_list_pagination_page_reverse(self, direction):
775 pagination_args = {
776 'sort_dir': direction,
Armando Migliaccio57581c62016-07-01 10:13:19 -0700777 'sort_key': self.field,
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200778 'limit': 3,
779 }
780 body = self.list_method(**pagination_args)
781 expected_resources = self._extract_resources(body)
782
783 pagination_args['limit'] -= 1
784 pagination_args['marker'] = expected_resources[-1]['id']
785 pagination_args['page_reverse'] = True
786 body = self.list_method(**pagination_args)
787
788 self.assertSameOrder(
789 # the last entry is not included in 2nd result when used as a
790 # marker
791 expected_resources[:-1],
792 self._extract_resources(body))
Victor Morales1be97b42016-09-05 08:50:06 -0500793
794 def _test_list_validation_filters(self):
795 validation_args = {
796 'unknown_filter': 'value',
797 }
798 body = self.list_method(**validation_args)
799 resources = self._extract_resources(body)
800 for resource in resources:
801 self.assertIn(resource['name'], self.resource_names)