blob: afcbe9c8be578228c1eb118fcf0dc6a1de9b8736 [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:
121 # Clean up QoS rules
122 for qos_rule in cls.qos_rules:
123 cls._try_delete_resource(cls.admin_client.delete_qos_rule,
124 qos_rule['id'])
125 # Clean up QoS policies
126 for qos_policy in cls.qos_policies:
127 cls._try_delete_resource(cls.admin_client.delete_qos_policy,
128 qos_policy['id'])
129 # Clean up floating IPs
130 for floating_ip in cls.floating_ips:
131 cls._try_delete_resource(cls.client.delete_floatingip,
132 floating_ip['id'])
133 # Clean up routers
134 for router in cls.routers:
135 cls._try_delete_resource(cls.delete_router,
136 router)
137 # Clean up metering label rules
138 for metering_label_rule in cls.metering_label_rules:
139 cls._try_delete_resource(
140 cls.admin_client.delete_metering_label_rule,
141 metering_label_rule['id'])
142 # Clean up metering labels
143 for metering_label in cls.metering_labels:
144 cls._try_delete_resource(
145 cls.admin_client.delete_metering_label,
146 metering_label['id'])
147 # Clean up flavors
148 for flavor in cls.flavors:
149 cls._try_delete_resource(
150 cls.admin_client.delete_flavor,
151 flavor['id'])
152 # Clean up service profiles
153 for service_profile in cls.service_profiles:
154 cls._try_delete_resource(
155 cls.admin_client.delete_service_profile,
156 service_profile['id'])
157 # Clean up ports
158 for port in cls.ports:
159 cls._try_delete_resource(cls.client.delete_port,
160 port['id'])
161 # Clean up subnets
162 for subnet in cls.subnets:
163 cls._try_delete_resource(cls.client.delete_subnet,
164 subnet['id'])
165 # Clean up networks
166 for network in cls.networks:
167 cls._try_delete_resource(cls.client.delete_network,
168 network['id'])
169
Miguel Lavalle124378b2016-09-21 16:41:47 -0500170 # Clean up admin networks
171 for network in cls.admin_networks:
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000172 cls._try_delete_resource(cls.admin_client.delete_network,
173 network['id'])
174
Itzik Brownbac51dc2016-10-31 12:25:04 +0000175 # Clean up security groups
176 for secgroup in cls.security_groups:
177 cls._try_delete_resource(cls.client.delete_security_group,
178 secgroup['id'])
179
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000180 for subnetpool in cls.subnetpools:
181 cls._try_delete_resource(cls.client.delete_subnetpool,
182 subnetpool['id'])
183
184 for subnetpool in cls.admin_subnetpools:
185 cls._try_delete_resource(cls.admin_client.delete_subnetpool,
186 subnetpool['id'])
187
188 for address_scope in cls.address_scopes:
189 cls._try_delete_resource(cls.client.delete_address_scope,
190 address_scope['id'])
191
192 for address_scope in cls.admin_address_scopes:
193 cls._try_delete_resource(
194 cls.admin_client.delete_address_scope,
195 address_scope['id'])
196
197 super(BaseNetworkTest, cls).resource_cleanup()
198
199 @classmethod
200 def _try_delete_resource(cls, delete_callable, *args, **kwargs):
201 """Cleanup resources in case of test-failure
202
203 Some resources are explicitly deleted by the test.
204 If the test failed to delete a resource, this method will execute
205 the appropriate delete methods. Otherwise, the method ignores NotFound
206 exceptions thrown for resources that were correctly deleted by the
207 test.
208
209 :param delete_callable: delete method
210 :param args: arguments for delete method
211 :param kwargs: keyword arguments for delete method
212 """
213 try:
214 delete_callable(*args, **kwargs)
215 # if resource is not found, this means it was deleted in the test
216 except lib_exc.NotFound:
217 pass
218
219 @classmethod
Sergey Belousa627ed92016-10-07 14:29:07 +0300220 def create_network(cls, network_name=None, client=None, **kwargs):
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000221 """Wrapper utility that returns a test network."""
222 network_name = network_name or data_utils.rand_name('test-network-')
223
Sergey Belousa627ed92016-10-07 14:29:07 +0300224 client = client or cls.client
225 body = client.create_network(name=network_name, **kwargs)
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000226 network = body['network']
Sławek Kapłońskia694a5f2017-08-24 19:51:22 +0000227 if client is cls.client:
228 cls.networks.append(network)
229 else:
230 cls.admin_networks.append(network)
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000231 return network
232
233 @classmethod
234 def create_shared_network(cls, network_name=None, **post_body):
235 network_name = network_name or data_utils.rand_name('sharednetwork-')
236 post_body.update({'name': network_name, 'shared': True})
237 body = cls.admin_client.create_network(**post_body)
238 network = body['network']
Miguel Lavalle124378b2016-09-21 16:41:47 -0500239 cls.admin_networks.append(network)
240 return network
241
242 @classmethod
243 def create_network_keystone_v3(cls, network_name=None, project_id=None,
244 tenant_id=None, client=None):
245 """Wrapper utility that creates a test network with project_id."""
246 client = client or cls.client
247 network_name = network_name or data_utils.rand_name(
248 'test-network-with-project_id')
249 project_id = cls.client.tenant_id
250 body = client.create_network_keystone_v3(network_name, project_id,
251 tenant_id)
252 network = body['network']
253 if client is cls.client:
254 cls.networks.append(network)
255 else:
256 cls.admin_networks.append(network)
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000257 return network
258
259 @classmethod
260 def create_subnet(cls, network, gateway='', cidr=None, mask_bits=None,
261 ip_version=None, client=None, **kwargs):
262 """Wrapper utility that returns a test subnet."""
263
264 # allow tests to use admin client
265 if not client:
266 client = cls.client
267
268 # The cidr and mask_bits depend on the ip version.
269 ip_version = ip_version if ip_version is not None else cls._ip_version
270 gateway_not_set = gateway == ''
271 if ip_version == 4:
272 cidr = cidr or netaddr.IPNetwork(
273 config.safe_get_config_value(
274 'network', 'project_network_cidr'))
275 mask_bits = (
276 mask_bits or config.safe_get_config_value(
277 'network', 'project_network_mask_bits'))
278 elif ip_version == 6:
279 cidr = (
280 cidr or netaddr.IPNetwork(
281 config.safe_get_config_value(
282 'network', 'project_network_v6_cidr')))
283 mask_bits = (
284 mask_bits or config.safe_get_config_value(
285 'network', 'project_network_v6_mask_bits'))
286 # Find a cidr that is not in use yet and create a subnet with it
287 for subnet_cidr in cidr.subnet(mask_bits):
288 if gateway_not_set:
289 gateway_ip = str(netaddr.IPAddress(subnet_cidr) + 1)
290 else:
291 gateway_ip = gateway
292 try:
293 body = client.create_subnet(
294 network_id=network['id'],
295 cidr=str(subnet_cidr),
296 ip_version=ip_version,
297 gateway_ip=gateway_ip,
298 **kwargs)
299 break
300 except lib_exc.BadRequest as e:
301 is_overlapping_cidr = 'overlaps with another subnet' in str(e)
302 if not is_overlapping_cidr:
303 raise
304 else:
305 message = 'Available CIDR for subnet creation could not be found'
306 raise ValueError(message)
307 subnet = body['subnet']
308 cls.subnets.append(subnet)
309 return subnet
310
311 @classmethod
312 def create_port(cls, network, **kwargs):
313 """Wrapper utility that returns a test port."""
314 body = cls.client.create_port(network_id=network['id'],
315 **kwargs)
316 port = body['port']
317 cls.ports.append(port)
318 return port
319
320 @classmethod
321 def update_port(cls, port, **kwargs):
322 """Wrapper utility that updates a test port."""
323 body = cls.client.update_port(port['id'],
324 **kwargs)
325 return body['port']
326
327 @classmethod
Genadi Chereshnyac0411e92016-07-11 16:59:42 +0300328 def _create_router_with_client(
329 cls, client, router_name=None, admin_state_up=False,
330 external_network_id=None, enable_snat=None, **kwargs
331 ):
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000332 ext_gw_info = {}
333 if external_network_id:
334 ext_gw_info['network_id'] = external_network_id
YAMAMOTO Takashi9bd4f972017-06-20 12:49:30 +0900335 if enable_snat is not None:
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000336 ext_gw_info['enable_snat'] = enable_snat
Genadi Chereshnyac0411e92016-07-11 16:59:42 +0300337 body = client.create_router(
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000338 router_name, external_gateway_info=ext_gw_info,
339 admin_state_up=admin_state_up, **kwargs)
340 router = body['router']
341 cls.routers.append(router)
342 return router
343
344 @classmethod
Genadi Chereshnyac0411e92016-07-11 16:59:42 +0300345 def create_router(cls, *args, **kwargs):
346 return cls._create_router_with_client(cls.client, *args, **kwargs)
347
348 @classmethod
349 def create_admin_router(cls, *args, **kwargs):
rajat294495c042017-06-28 15:37:16 +0530350 return cls._create_router_with_client(cls.os_admin.network_client,
Genadi Chereshnyac0411e92016-07-11 16:59:42 +0300351 *args, **kwargs)
352
353 @classmethod
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000354 def create_floatingip(cls, external_network_id):
355 """Wrapper utility that returns a test floating IP."""
356 body = cls.client.create_floatingip(
357 floating_network_id=external_network_id)
358 fip = body['floatingip']
359 cls.floating_ips.append(fip)
360 return fip
361
362 @classmethod
363 def create_router_interface(cls, router_id, subnet_id):
364 """Wrapper utility that returns a router interface."""
365 interface = cls.client.add_router_interface_with_subnet_id(
366 router_id, subnet_id)
367 return interface
368
369 @classmethod
Sławek Kapłońskiff294062016-12-04 15:00:54 +0000370 def get_supported_qos_rule_types(cls):
371 body = cls.client.list_qos_rule_types()
372 return [rule_type['type'] for rule_type in body['rule_types']]
373
374 @classmethod
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200375 def create_qos_policy(cls, name, description=None, shared=False,
Hirofumi Ichihara39a6ee12017-08-23 13:55:12 +0900376 tenant_id=None, is_default=False):
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000377 """Wrapper utility that returns a test QoS policy."""
378 body = cls.admin_client.create_qos_policy(
Hirofumi Ichihara39a6ee12017-08-23 13:55:12 +0900379 name, description, shared, tenant_id, is_default)
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000380 qos_policy = body['policy']
381 cls.qos_policies.append(qos_policy)
382 return qos_policy
383
384 @classmethod
Sławek Kapłoński153f3452017-03-24 22:04:53 +0000385 def create_qos_bandwidth_limit_rule(cls, policy_id, max_kbps,
386 max_burst_kbps,
387 direction=constants.EGRESS_DIRECTION):
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000388 """Wrapper utility that returns a test QoS bandwidth limit rule."""
389 body = cls.admin_client.create_bandwidth_limit_rule(
Sławek Kapłoński153f3452017-03-24 22:04:53 +0000390 policy_id, max_kbps, max_burst_kbps, direction)
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000391 qos_rule = body['bandwidth_limit_rule']
392 cls.qos_rules.append(qos_rule)
393 return qos_rule
394
395 @classmethod
396 def delete_router(cls, router):
397 body = cls.client.list_router_interfaces(router['id'])
398 interfaces = body['ports']
399 for i in interfaces:
400 try:
401 cls.client.remove_router_interface_with_subnet_id(
402 router['id'], i['fixed_ips'][0]['subnet_id'])
403 except lib_exc.NotFound:
404 pass
405 cls.client.delete_router(router['id'])
406
407 @classmethod
408 def create_address_scope(cls, name, is_admin=False, **kwargs):
409 if is_admin:
410 body = cls.admin_client.create_address_scope(name=name, **kwargs)
411 cls.admin_address_scopes.append(body['address_scope'])
412 else:
413 body = cls.client.create_address_scope(name=name, **kwargs)
414 cls.address_scopes.append(body['address_scope'])
415 return body['address_scope']
416
417 @classmethod
418 def create_subnetpool(cls, name, is_admin=False, **kwargs):
419 if is_admin:
420 body = cls.admin_client.create_subnetpool(name, **kwargs)
421 cls.admin_subnetpools.append(body['subnetpool'])
422 else:
423 body = cls.client.create_subnetpool(name, **kwargs)
424 cls.subnetpools.append(body['subnetpool'])
425 return body['subnetpool']
426
427
428class BaseAdminNetworkTest(BaseNetworkTest):
429
430 credentials = ['primary', 'admin']
431
432 @classmethod
433 def setup_clients(cls):
434 super(BaseAdminNetworkTest, cls).setup_clients()
fumihiko kakumaa216fc12017-07-14 10:43:29 +0900435 cls.admin_client = cls.os_admin.network_client
Jakub Libosvarf5758012017-08-15 13:45:30 +0000436 cls.identity_admin_client = cls.os_admin.projects_client
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000437
438 @classmethod
439 def create_metering_label(cls, name, description):
440 """Wrapper utility that returns a test metering label."""
441 body = cls.admin_client.create_metering_label(
442 description=description,
443 name=data_utils.rand_name("metering-label"))
444 metering_label = body['metering_label']
445 cls.metering_labels.append(metering_label)
446 return metering_label
447
448 @classmethod
449 def create_metering_label_rule(cls, remote_ip_prefix, direction,
450 metering_label_id):
451 """Wrapper utility that returns a test metering label rule."""
452 body = cls.admin_client.create_metering_label_rule(
453 remote_ip_prefix=remote_ip_prefix, direction=direction,
454 metering_label_id=metering_label_id)
455 metering_label_rule = body['metering_label_rule']
456 cls.metering_label_rules.append(metering_label_rule)
457 return metering_label_rule
458
459 @classmethod
460 def create_flavor(cls, name, description, service_type):
461 """Wrapper utility that returns a test flavor."""
462 body = cls.admin_client.create_flavor(
463 description=description, service_type=service_type,
464 name=name)
465 flavor = body['flavor']
466 cls.flavors.append(flavor)
467 return flavor
468
469 @classmethod
470 def create_service_profile(cls, description, metainfo, driver):
471 """Wrapper utility that returns a test service profile."""
472 body = cls.admin_client.create_service_profile(
473 driver=driver, metainfo=metainfo, description=description)
474 service_profile = body['service_profile']
475 cls.service_profiles.append(service_profile)
476 return service_profile
477
478 @classmethod
479 def get_unused_ip(cls, net_id, ip_version=None):
Gary Kotton011345f2016-06-15 08:04:31 -0700480 """Get an unused ip address in a allocation pool of net"""
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000481 body = cls.admin_client.list_ports(network_id=net_id)
482 ports = body['ports']
483 used_ips = []
484 for port in ports:
485 used_ips.extend(
486 [fixed_ip['ip_address'] for fixed_ip in port['fixed_ips']])
487 body = cls.admin_client.list_subnets(network_id=net_id)
488 subnets = body['subnets']
489
490 for subnet in subnets:
491 if ip_version and subnet['ip_version'] != ip_version:
492 continue
493 cidr = subnet['cidr']
494 allocation_pools = subnet['allocation_pools']
495 iterators = []
496 if allocation_pools:
497 for allocation_pool in allocation_pools:
498 iterators.append(netaddr.iter_iprange(
499 allocation_pool['start'], allocation_pool['end']))
500 else:
501 net = netaddr.IPNetwork(cidr)
502
503 def _iterip():
504 for ip in net:
505 if ip not in (net.network, net.broadcast):
506 yield ip
507 iterators.append(iter(_iterip()))
508
509 for iterator in iterators:
510 for ip in iterator:
511 if str(ip) not in used_ips:
512 return str(ip)
513
514 message = (
515 "net(%s) has no usable IP address in allocation pools" % net_id)
516 raise exceptions.InvalidConfiguration(message)
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200517
518
Sławek Kapłońskiff294062016-12-04 15:00:54 +0000519def require_qos_rule_type(rule_type):
520 def decorator(f):
521 @functools.wraps(f)
522 def wrapper(self, *func_args, **func_kwargs):
523 if rule_type not in self.get_supported_qos_rule_types():
524 raise self.skipException(
525 "%s rule type is required." % rule_type)
526 return f(self, *func_args, **func_kwargs)
527 return wrapper
528 return decorator
529
530
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200531def _require_sorting(f):
532 @functools.wraps(f)
533 def inner(self, *args, **kwargs):
Ihar Hrachyshka34feb5b2016-06-14 16:16:06 +0200534 if not test.is_extension_enabled("sorting", "network"):
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200535 self.skipTest('Sorting feature is required')
536 return f(self, *args, **kwargs)
537 return inner
538
539
540def _require_pagination(f):
541 @functools.wraps(f)
542 def inner(self, *args, **kwargs):
Ihar Hrachyshka34feb5b2016-06-14 16:16:06 +0200543 if not test.is_extension_enabled("pagination", "network"):
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200544 self.skipTest('Pagination feature is required')
545 return f(self, *args, **kwargs)
546 return inner
547
548
549class BaseSearchCriteriaTest(BaseNetworkTest):
550
551 # This should be defined by subclasses to reflect resource name to test
552 resource = None
553
Armando Migliaccio57581c62016-07-01 10:13:19 -0700554 field = 'name'
555
Ihar Hrachyshkaa8fe5a12016-05-24 14:50:58 +0200556 # NOTE(ihrachys): some names, like those starting with an underscore (_)
557 # are sorted differently depending on whether the plugin implements native
558 # sorting support, or not. So we avoid any such cases here, sticking to
559 # alphanumeric. Also test a case when there are multiple resources with the
560 # same name
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200561 resource_names = ('test1', 'abc1', 'test10', '123test') + ('test1',)
562
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200563 force_tenant_isolation = True
564
Ihar Hrachyshkaa8fe5a12016-05-24 14:50:58 +0200565 list_kwargs = {}
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200566
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200567 list_as_admin = False
568
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200569 def assertSameOrder(self, original, actual):
570 # gracefully handle iterators passed
571 original = list(original)
572 actual = list(actual)
573 self.assertEqual(len(original), len(actual))
574 for expected, res in zip(original, actual):
Armando Migliaccio57581c62016-07-01 10:13:19 -0700575 self.assertEqual(expected[self.field], res[self.field])
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200576
577 @utils.classproperty
578 def plural_name(self):
579 return '%ss' % self.resource
580
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200581 @property
582 def list_client(self):
583 return self.admin_client if self.list_as_admin else self.client
584
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200585 def list_method(self, *args, **kwargs):
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200586 method = getattr(self.list_client, 'list_%s' % self.plural_name)
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200587 kwargs.update(self.list_kwargs)
588 return method(*args, **kwargs)
589
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200590 def get_bare_url(self, url):
591 base_url = self.client.base_url
592 self.assertTrue(url.startswith(base_url))
593 return url[len(base_url):]
594
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200595 @classmethod
596 def _extract_resources(cls, body):
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200597 return body[cls.plural_name]
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200598
599 def _test_list_sorts(self, direction):
600 sort_args = {
601 'sort_dir': direction,
Armando Migliaccio57581c62016-07-01 10:13:19 -0700602 'sort_key': self.field
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200603 }
604 body = self.list_method(**sort_args)
605 resources = self._extract_resources(body)
606 self.assertNotEmpty(
607 resources, "%s list returned is empty" % self.resource)
Armando Migliaccio57581c62016-07-01 10:13:19 -0700608 retrieved_names = [res[self.field] for res in resources]
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200609 expected = sorted(retrieved_names)
610 if direction == constants.SORT_DIRECTION_DESC:
611 expected = list(reversed(expected))
612 self.assertEqual(expected, retrieved_names)
613
614 @_require_sorting
615 def _test_list_sorts_asc(self):
616 self._test_list_sorts(constants.SORT_DIRECTION_ASC)
617
618 @_require_sorting
619 def _test_list_sorts_desc(self):
620 self._test_list_sorts(constants.SORT_DIRECTION_DESC)
621
622 @_require_pagination
623 def _test_list_pagination(self):
624 for limit in range(1, len(self.resource_names) + 1):
625 pagination_args = {
626 'limit': limit,
627 }
628 body = self.list_method(**pagination_args)
629 resources = self._extract_resources(body)
630 self.assertEqual(limit, len(resources))
631
632 @_require_pagination
633 def _test_list_no_pagination_limit_0(self):
634 pagination_args = {
635 'limit': 0,
636 }
637 body = self.list_method(**pagination_args)
638 resources = self._extract_resources(body)
Béla Vancsicsf1806182016-08-23 07:36:18 +0200639 self.assertGreaterEqual(len(resources), len(self.resource_names))
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200640
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200641 def _test_list_pagination_iteratively(self, lister):
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200642 # first, collect all resources for later comparison
643 sort_args = {
644 'sort_dir': constants.SORT_DIRECTION_ASC,
Armando Migliaccio57581c62016-07-01 10:13:19 -0700645 'sort_key': self.field
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200646 }
647 body = self.list_method(**sort_args)
648 expected_resources = self._extract_resources(body)
649 self.assertNotEmpty(expected_resources)
650
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200651 resources = lister(
652 len(expected_resources), sort_args
653 )
654
655 # finally, compare that the list retrieved in one go is identical to
656 # the one containing pagination results
657 self.assertSameOrder(expected_resources, resources)
658
659 def _list_all_with_marker(self, niterations, sort_args):
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200660 # paginate resources one by one, using last fetched resource as a
661 # marker
662 resources = []
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200663 for i in range(niterations):
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200664 pagination_args = sort_args.copy()
665 pagination_args['limit'] = 1
666 if resources:
667 pagination_args['marker'] = resources[-1]['id']
668 body = self.list_method(**pagination_args)
669 resources_ = self._extract_resources(body)
670 self.assertEqual(1, len(resources_))
671 resources.extend(resources_)
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200672 return resources
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200673
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200674 @_require_pagination
675 @_require_sorting
676 def _test_list_pagination_with_marker(self):
677 self._test_list_pagination_iteratively(self._list_all_with_marker)
678
679 def _list_all_with_hrefs(self, niterations, sort_args):
680 # paginate resources one by one, using next href links
681 resources = []
682 prev_links = {}
683
684 for i in range(niterations):
685 if prev_links:
686 uri = self.get_bare_url(prev_links['next'])
687 else:
Ihar Hrachyshka7f79fe62016-06-07 21:23:44 +0200688 sort_args.update(self.list_kwargs)
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200689 uri = self.list_client.build_uri(
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200690 self.plural_name, limit=1, **sort_args)
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200691 prev_links, body = self.list_client.get_uri_with_links(
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200692 self.plural_name, uri
693 )
694 resources_ = self._extract_resources(body)
695 self.assertEqual(1, len(resources_))
696 resources.extend(resources_)
697
698 # The last element is empty and does not contain 'next' link
699 uri = self.get_bare_url(prev_links['next'])
700 prev_links, body = self.client.get_uri_with_links(
701 self.plural_name, uri
702 )
703 self.assertNotIn('next', prev_links)
704
705 # Now walk backwards and compare results
706 resources2 = []
707 for i in range(niterations):
708 uri = self.get_bare_url(prev_links['previous'])
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200709 prev_links, body = self.list_client.get_uri_with_links(
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200710 self.plural_name, uri
711 )
712 resources_ = self._extract_resources(body)
713 self.assertEqual(1, len(resources_))
714 resources2.extend(resources_)
715
716 self.assertSameOrder(resources, reversed(resources2))
717
718 return resources
719
720 @_require_pagination
721 @_require_sorting
722 def _test_list_pagination_with_href_links(self):
723 self._test_list_pagination_iteratively(self._list_all_with_hrefs)
724
725 @_require_pagination
726 @_require_sorting
727 def _test_list_pagination_page_reverse_with_href_links(
728 self, direction=constants.SORT_DIRECTION_ASC):
729 pagination_args = {
730 'sort_dir': direction,
Armando Migliaccio57581c62016-07-01 10:13:19 -0700731 'sort_key': self.field,
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200732 }
733 body = self.list_method(**pagination_args)
734 expected_resources = self._extract_resources(body)
735
736 page_size = 2
737 pagination_args['limit'] = page_size
738
739 prev_links = {}
740 resources = []
741 num_resources = len(expected_resources)
742 niterations = int(math.ceil(float(num_resources) / page_size))
743 for i in range(niterations):
744 if prev_links:
745 uri = self.get_bare_url(prev_links['previous'])
746 else:
Ihar Hrachyshka7f79fe62016-06-07 21:23:44 +0200747 pagination_args.update(self.list_kwargs)
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200748 uri = self.list_client.build_uri(
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200749 self.plural_name, page_reverse=True, **pagination_args)
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200750 prev_links, body = self.list_client.get_uri_with_links(
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200751 self.plural_name, uri
752 )
753 resources_ = self._extract_resources(body)
Béla Vancsicsf1806182016-08-23 07:36:18 +0200754 self.assertGreaterEqual(page_size, len(resources_))
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200755 resources.extend(reversed(resources_))
756
757 self.assertSameOrder(expected_resources, reversed(resources))
758
759 @_require_pagination
760 @_require_sorting
761 def _test_list_pagination_page_reverse_asc(self):
762 self._test_list_pagination_page_reverse(
763 direction=constants.SORT_DIRECTION_ASC)
764
765 @_require_pagination
766 @_require_sorting
767 def _test_list_pagination_page_reverse_desc(self):
768 self._test_list_pagination_page_reverse(
769 direction=constants.SORT_DIRECTION_DESC)
770
771 def _test_list_pagination_page_reverse(self, direction):
772 pagination_args = {
773 'sort_dir': direction,
Armando Migliaccio57581c62016-07-01 10:13:19 -0700774 'sort_key': self.field,
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200775 'limit': 3,
776 }
777 body = self.list_method(**pagination_args)
778 expected_resources = self._extract_resources(body)
779
780 pagination_args['limit'] -= 1
781 pagination_args['marker'] = expected_resources[-1]['id']
782 pagination_args['page_reverse'] = True
783 body = self.list_method(**pagination_args)
784
785 self.assertSameOrder(
786 # the last entry is not included in 2nd result when used as a
787 # marker
788 expected_resources[:-1],
789 self._extract_resources(body))
Victor Morales1be97b42016-09-05 08:50:06 -0500790
791 def _test_list_validation_filters(self):
792 validation_args = {
793 'unknown_filter': 'value',
794 }
795 body = self.list_method(**validation_args)
796 resources = self._extract_resources(body)
797 for resource in resources:
798 self.assertIn(resource['name'], self.resource_names)