blob: e7355029900f5c20a1b8b91eb89d1957e08343df [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']
227 cls.networks.append(network)
228 return network
229
230 @classmethod
231 def create_shared_network(cls, network_name=None, **post_body):
232 network_name = network_name or data_utils.rand_name('sharednetwork-')
233 post_body.update({'name': network_name, 'shared': True})
234 body = cls.admin_client.create_network(**post_body)
235 network = body['network']
Miguel Lavalle124378b2016-09-21 16:41:47 -0500236 cls.admin_networks.append(network)
237 return network
238
239 @classmethod
240 def create_network_keystone_v3(cls, network_name=None, project_id=None,
241 tenant_id=None, client=None):
242 """Wrapper utility that creates a test network with project_id."""
243 client = client or cls.client
244 network_name = network_name or data_utils.rand_name(
245 'test-network-with-project_id')
246 project_id = cls.client.tenant_id
247 body = client.create_network_keystone_v3(network_name, project_id,
248 tenant_id)
249 network = body['network']
250 if client is cls.client:
251 cls.networks.append(network)
252 else:
253 cls.admin_networks.append(network)
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000254 return network
255
256 @classmethod
257 def create_subnet(cls, network, gateway='', cidr=None, mask_bits=None,
258 ip_version=None, client=None, **kwargs):
259 """Wrapper utility that returns a test subnet."""
260
261 # allow tests to use admin client
262 if not client:
263 client = cls.client
264
265 # The cidr and mask_bits depend on the ip version.
266 ip_version = ip_version if ip_version is not None else cls._ip_version
267 gateway_not_set = gateway == ''
268 if ip_version == 4:
269 cidr = cidr or netaddr.IPNetwork(
270 config.safe_get_config_value(
271 'network', 'project_network_cidr'))
272 mask_bits = (
273 mask_bits or config.safe_get_config_value(
274 'network', 'project_network_mask_bits'))
275 elif ip_version == 6:
276 cidr = (
277 cidr or netaddr.IPNetwork(
278 config.safe_get_config_value(
279 'network', 'project_network_v6_cidr')))
280 mask_bits = (
281 mask_bits or config.safe_get_config_value(
282 'network', 'project_network_v6_mask_bits'))
283 # Find a cidr that is not in use yet and create a subnet with it
284 for subnet_cidr in cidr.subnet(mask_bits):
285 if gateway_not_set:
286 gateway_ip = str(netaddr.IPAddress(subnet_cidr) + 1)
287 else:
288 gateway_ip = gateway
289 try:
290 body = client.create_subnet(
291 network_id=network['id'],
292 cidr=str(subnet_cidr),
293 ip_version=ip_version,
294 gateway_ip=gateway_ip,
295 **kwargs)
296 break
297 except lib_exc.BadRequest as e:
298 is_overlapping_cidr = 'overlaps with another subnet' in str(e)
299 if not is_overlapping_cidr:
300 raise
301 else:
302 message = 'Available CIDR for subnet creation could not be found'
303 raise ValueError(message)
304 subnet = body['subnet']
305 cls.subnets.append(subnet)
306 return subnet
307
308 @classmethod
309 def create_port(cls, network, **kwargs):
310 """Wrapper utility that returns a test port."""
311 body = cls.client.create_port(network_id=network['id'],
312 **kwargs)
313 port = body['port']
314 cls.ports.append(port)
315 return port
316
317 @classmethod
318 def update_port(cls, port, **kwargs):
319 """Wrapper utility that updates a test port."""
320 body = cls.client.update_port(port['id'],
321 **kwargs)
322 return body['port']
323
324 @classmethod
Genadi Chereshnyac0411e92016-07-11 16:59:42 +0300325 def _create_router_with_client(
326 cls, client, router_name=None, admin_state_up=False,
327 external_network_id=None, enable_snat=None, **kwargs
328 ):
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000329 ext_gw_info = {}
330 if external_network_id:
331 ext_gw_info['network_id'] = external_network_id
YAMAMOTO Takashi9bd4f972017-06-20 12:49:30 +0900332 if enable_snat is not None:
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000333 ext_gw_info['enable_snat'] = enable_snat
Genadi Chereshnyac0411e92016-07-11 16:59:42 +0300334 body = client.create_router(
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000335 router_name, external_gateway_info=ext_gw_info,
336 admin_state_up=admin_state_up, **kwargs)
337 router = body['router']
338 cls.routers.append(router)
339 return router
340
341 @classmethod
Genadi Chereshnyac0411e92016-07-11 16:59:42 +0300342 def create_router(cls, *args, **kwargs):
343 return cls._create_router_with_client(cls.client, *args, **kwargs)
344
345 @classmethod
346 def create_admin_router(cls, *args, **kwargs):
rajat294495c042017-06-28 15:37:16 +0530347 return cls._create_router_with_client(cls.os_admin.network_client,
Genadi Chereshnyac0411e92016-07-11 16:59:42 +0300348 *args, **kwargs)
349
350 @classmethod
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000351 def create_floatingip(cls, external_network_id):
352 """Wrapper utility that returns a test floating IP."""
353 body = cls.client.create_floatingip(
354 floating_network_id=external_network_id)
355 fip = body['floatingip']
356 cls.floating_ips.append(fip)
357 return fip
358
359 @classmethod
360 def create_router_interface(cls, router_id, subnet_id):
361 """Wrapper utility that returns a router interface."""
362 interface = cls.client.add_router_interface_with_subnet_id(
363 router_id, subnet_id)
364 return interface
365
366 @classmethod
Sławek Kapłońskiff294062016-12-04 15:00:54 +0000367 def get_supported_qos_rule_types(cls):
368 body = cls.client.list_qos_rule_types()
369 return [rule_type['type'] for rule_type in body['rule_types']]
370
371 @classmethod
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200372 def create_qos_policy(cls, name, description=None, shared=False,
373 tenant_id=None):
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000374 """Wrapper utility that returns a test QoS policy."""
375 body = cls.admin_client.create_qos_policy(
376 name, description, shared, tenant_id)
377 qos_policy = body['policy']
378 cls.qos_policies.append(qos_policy)
379 return qos_policy
380
381 @classmethod
Sławek Kapłoński153f3452017-03-24 22:04:53 +0000382 def create_qos_bandwidth_limit_rule(cls, policy_id, max_kbps,
383 max_burst_kbps,
384 direction=constants.EGRESS_DIRECTION):
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000385 """Wrapper utility that returns a test QoS bandwidth limit rule."""
386 body = cls.admin_client.create_bandwidth_limit_rule(
Sławek Kapłoński153f3452017-03-24 22:04:53 +0000387 policy_id, max_kbps, max_burst_kbps, direction)
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000388 qos_rule = body['bandwidth_limit_rule']
389 cls.qos_rules.append(qos_rule)
390 return qos_rule
391
392 @classmethod
393 def delete_router(cls, router):
394 body = cls.client.list_router_interfaces(router['id'])
395 interfaces = body['ports']
396 for i in interfaces:
397 try:
398 cls.client.remove_router_interface_with_subnet_id(
399 router['id'], i['fixed_ips'][0]['subnet_id'])
400 except lib_exc.NotFound:
401 pass
402 cls.client.delete_router(router['id'])
403
404 @classmethod
405 def create_address_scope(cls, name, is_admin=False, **kwargs):
406 if is_admin:
407 body = cls.admin_client.create_address_scope(name=name, **kwargs)
408 cls.admin_address_scopes.append(body['address_scope'])
409 else:
410 body = cls.client.create_address_scope(name=name, **kwargs)
411 cls.address_scopes.append(body['address_scope'])
412 return body['address_scope']
413
414 @classmethod
415 def create_subnetpool(cls, name, is_admin=False, **kwargs):
416 if is_admin:
417 body = cls.admin_client.create_subnetpool(name, **kwargs)
418 cls.admin_subnetpools.append(body['subnetpool'])
419 else:
420 body = cls.client.create_subnetpool(name, **kwargs)
421 cls.subnetpools.append(body['subnetpool'])
422 return body['subnetpool']
423
424
425class BaseAdminNetworkTest(BaseNetworkTest):
426
427 credentials = ['primary', 'admin']
428
429 @classmethod
430 def setup_clients(cls):
431 super(BaseAdminNetworkTest, cls).setup_clients()
fumihiko kakumaa216fc12017-07-14 10:43:29 +0900432 cls.admin_client = cls.os_admin.network_client
433 cls.identity_admin_client = cls.os_admin.tenants_client
nanaboatedfe7742017-07-14 22:26:52 +0000434 cls.identity_admin_clientv3 = cls.os_admin.projects_client
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000435
436 @classmethod
437 def create_metering_label(cls, name, description):
438 """Wrapper utility that returns a test metering label."""
439 body = cls.admin_client.create_metering_label(
440 description=description,
441 name=data_utils.rand_name("metering-label"))
442 metering_label = body['metering_label']
443 cls.metering_labels.append(metering_label)
444 return metering_label
445
446 @classmethod
447 def create_metering_label_rule(cls, remote_ip_prefix, direction,
448 metering_label_id):
449 """Wrapper utility that returns a test metering label rule."""
450 body = cls.admin_client.create_metering_label_rule(
451 remote_ip_prefix=remote_ip_prefix, direction=direction,
452 metering_label_id=metering_label_id)
453 metering_label_rule = body['metering_label_rule']
454 cls.metering_label_rules.append(metering_label_rule)
455 return metering_label_rule
456
457 @classmethod
458 def create_flavor(cls, name, description, service_type):
459 """Wrapper utility that returns a test flavor."""
460 body = cls.admin_client.create_flavor(
461 description=description, service_type=service_type,
462 name=name)
463 flavor = body['flavor']
464 cls.flavors.append(flavor)
465 return flavor
466
467 @classmethod
468 def create_service_profile(cls, description, metainfo, driver):
469 """Wrapper utility that returns a test service profile."""
470 body = cls.admin_client.create_service_profile(
471 driver=driver, metainfo=metainfo, description=description)
472 service_profile = body['service_profile']
473 cls.service_profiles.append(service_profile)
474 return service_profile
475
476 @classmethod
477 def get_unused_ip(cls, net_id, ip_version=None):
Gary Kotton011345f2016-06-15 08:04:31 -0700478 """Get an unused ip address in a allocation pool of net"""
Daniel Mellado3c0aeab2016-01-29 11:30:25 +0000479 body = cls.admin_client.list_ports(network_id=net_id)
480 ports = body['ports']
481 used_ips = []
482 for port in ports:
483 used_ips.extend(
484 [fixed_ip['ip_address'] for fixed_ip in port['fixed_ips']])
485 body = cls.admin_client.list_subnets(network_id=net_id)
486 subnets = body['subnets']
487
488 for subnet in subnets:
489 if ip_version and subnet['ip_version'] != ip_version:
490 continue
491 cidr = subnet['cidr']
492 allocation_pools = subnet['allocation_pools']
493 iterators = []
494 if allocation_pools:
495 for allocation_pool in allocation_pools:
496 iterators.append(netaddr.iter_iprange(
497 allocation_pool['start'], allocation_pool['end']))
498 else:
499 net = netaddr.IPNetwork(cidr)
500
501 def _iterip():
502 for ip in net:
503 if ip not in (net.network, net.broadcast):
504 yield ip
505 iterators.append(iter(_iterip()))
506
507 for iterator in iterators:
508 for ip in iterator:
509 if str(ip) not in used_ips:
510 return str(ip)
511
512 message = (
513 "net(%s) has no usable IP address in allocation pools" % net_id)
514 raise exceptions.InvalidConfiguration(message)
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200515
516
Sławek Kapłońskiff294062016-12-04 15:00:54 +0000517def require_qos_rule_type(rule_type):
518 def decorator(f):
519 @functools.wraps(f)
520 def wrapper(self, *func_args, **func_kwargs):
521 if rule_type not in self.get_supported_qos_rule_types():
522 raise self.skipException(
523 "%s rule type is required." % rule_type)
524 return f(self, *func_args, **func_kwargs)
525 return wrapper
526 return decorator
527
528
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200529def _require_sorting(f):
530 @functools.wraps(f)
531 def inner(self, *args, **kwargs):
Ihar Hrachyshka34feb5b2016-06-14 16:16:06 +0200532 if not test.is_extension_enabled("sorting", "network"):
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200533 self.skipTest('Sorting feature is required')
534 return f(self, *args, **kwargs)
535 return inner
536
537
538def _require_pagination(f):
539 @functools.wraps(f)
540 def inner(self, *args, **kwargs):
Ihar Hrachyshka34feb5b2016-06-14 16:16:06 +0200541 if not test.is_extension_enabled("pagination", "network"):
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200542 self.skipTest('Pagination feature is required')
543 return f(self, *args, **kwargs)
544 return inner
545
546
547class BaseSearchCriteriaTest(BaseNetworkTest):
548
549 # This should be defined by subclasses to reflect resource name to test
550 resource = None
551
Armando Migliaccio57581c62016-07-01 10:13:19 -0700552 field = 'name'
553
Ihar Hrachyshkaa8fe5a12016-05-24 14:50:58 +0200554 # NOTE(ihrachys): some names, like those starting with an underscore (_)
555 # are sorted differently depending on whether the plugin implements native
556 # sorting support, or not. So we avoid any such cases here, sticking to
557 # alphanumeric. Also test a case when there are multiple resources with the
558 # same name
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200559 resource_names = ('test1', 'abc1', 'test10', '123test') + ('test1',)
560
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200561 force_tenant_isolation = True
562
Ihar Hrachyshkaa8fe5a12016-05-24 14:50:58 +0200563 list_kwargs = {}
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200564
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200565 list_as_admin = False
566
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200567 def assertSameOrder(self, original, actual):
568 # gracefully handle iterators passed
569 original = list(original)
570 actual = list(actual)
571 self.assertEqual(len(original), len(actual))
572 for expected, res in zip(original, actual):
Armando Migliaccio57581c62016-07-01 10:13:19 -0700573 self.assertEqual(expected[self.field], res[self.field])
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200574
575 @utils.classproperty
576 def plural_name(self):
577 return '%ss' % self.resource
578
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200579 @property
580 def list_client(self):
581 return self.admin_client if self.list_as_admin else self.client
582
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200583 def list_method(self, *args, **kwargs):
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200584 method = getattr(self.list_client, 'list_%s' % self.plural_name)
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200585 kwargs.update(self.list_kwargs)
586 return method(*args, **kwargs)
587
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200588 def get_bare_url(self, url):
589 base_url = self.client.base_url
590 self.assertTrue(url.startswith(base_url))
591 return url[len(base_url):]
592
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200593 @classmethod
594 def _extract_resources(cls, body):
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200595 return body[cls.plural_name]
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200596
597 def _test_list_sorts(self, direction):
598 sort_args = {
599 'sort_dir': direction,
Armando Migliaccio57581c62016-07-01 10:13:19 -0700600 'sort_key': self.field
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200601 }
602 body = self.list_method(**sort_args)
603 resources = self._extract_resources(body)
604 self.assertNotEmpty(
605 resources, "%s list returned is empty" % self.resource)
Armando Migliaccio57581c62016-07-01 10:13:19 -0700606 retrieved_names = [res[self.field] for res in resources]
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200607 expected = sorted(retrieved_names)
608 if direction == constants.SORT_DIRECTION_DESC:
609 expected = list(reversed(expected))
610 self.assertEqual(expected, retrieved_names)
611
612 @_require_sorting
613 def _test_list_sorts_asc(self):
614 self._test_list_sorts(constants.SORT_DIRECTION_ASC)
615
616 @_require_sorting
617 def _test_list_sorts_desc(self):
618 self._test_list_sorts(constants.SORT_DIRECTION_DESC)
619
620 @_require_pagination
621 def _test_list_pagination(self):
622 for limit in range(1, len(self.resource_names) + 1):
623 pagination_args = {
624 'limit': limit,
625 }
626 body = self.list_method(**pagination_args)
627 resources = self._extract_resources(body)
628 self.assertEqual(limit, len(resources))
629
630 @_require_pagination
631 def _test_list_no_pagination_limit_0(self):
632 pagination_args = {
633 'limit': 0,
634 }
635 body = self.list_method(**pagination_args)
636 resources = self._extract_resources(body)
Béla Vancsicsf1806182016-08-23 07:36:18 +0200637 self.assertGreaterEqual(len(resources), len(self.resource_names))
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200638
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200639 def _test_list_pagination_iteratively(self, lister):
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200640 # first, collect all resources for later comparison
641 sort_args = {
642 'sort_dir': constants.SORT_DIRECTION_ASC,
Armando Migliaccio57581c62016-07-01 10:13:19 -0700643 'sort_key': self.field
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200644 }
645 body = self.list_method(**sort_args)
646 expected_resources = self._extract_resources(body)
647 self.assertNotEmpty(expected_resources)
648
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200649 resources = lister(
650 len(expected_resources), sort_args
651 )
652
653 # finally, compare that the list retrieved in one go is identical to
654 # the one containing pagination results
655 self.assertSameOrder(expected_resources, resources)
656
657 def _list_all_with_marker(self, niterations, sort_args):
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200658 # paginate resources one by one, using last fetched resource as a
659 # marker
660 resources = []
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200661 for i in range(niterations):
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200662 pagination_args = sort_args.copy()
663 pagination_args['limit'] = 1
664 if resources:
665 pagination_args['marker'] = resources[-1]['id']
666 body = self.list_method(**pagination_args)
667 resources_ = self._extract_resources(body)
668 self.assertEqual(1, len(resources_))
669 resources.extend(resources_)
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200670 return resources
Ihar Hrachyshka59382252016-04-05 15:54:33 +0200671
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200672 @_require_pagination
673 @_require_sorting
674 def _test_list_pagination_with_marker(self):
675 self._test_list_pagination_iteratively(self._list_all_with_marker)
676
677 def _list_all_with_hrefs(self, niterations, sort_args):
678 # paginate resources one by one, using next href links
679 resources = []
680 prev_links = {}
681
682 for i in range(niterations):
683 if prev_links:
684 uri = self.get_bare_url(prev_links['next'])
685 else:
Ihar Hrachyshka7f79fe62016-06-07 21:23:44 +0200686 sort_args.update(self.list_kwargs)
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200687 uri = self.list_client.build_uri(
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200688 self.plural_name, limit=1, **sort_args)
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200689 prev_links, body = self.list_client.get_uri_with_links(
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200690 self.plural_name, uri
691 )
692 resources_ = self._extract_resources(body)
693 self.assertEqual(1, len(resources_))
694 resources.extend(resources_)
695
696 # The last element is empty and does not contain 'next' link
697 uri = self.get_bare_url(prev_links['next'])
698 prev_links, body = self.client.get_uri_with_links(
699 self.plural_name, uri
700 )
701 self.assertNotIn('next', prev_links)
702
703 # Now walk backwards and compare results
704 resources2 = []
705 for i in range(niterations):
706 uri = self.get_bare_url(prev_links['previous'])
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200707 prev_links, body = self.list_client.get_uri_with_links(
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200708 self.plural_name, uri
709 )
710 resources_ = self._extract_resources(body)
711 self.assertEqual(1, len(resources_))
712 resources2.extend(resources_)
713
714 self.assertSameOrder(resources, reversed(resources2))
715
716 return resources
717
718 @_require_pagination
719 @_require_sorting
720 def _test_list_pagination_with_href_links(self):
721 self._test_list_pagination_iteratively(self._list_all_with_hrefs)
722
723 @_require_pagination
724 @_require_sorting
725 def _test_list_pagination_page_reverse_with_href_links(
726 self, direction=constants.SORT_DIRECTION_ASC):
727 pagination_args = {
728 'sort_dir': direction,
Armando Migliaccio57581c62016-07-01 10:13:19 -0700729 'sort_key': self.field,
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200730 }
731 body = self.list_method(**pagination_args)
732 expected_resources = self._extract_resources(body)
733
734 page_size = 2
735 pagination_args['limit'] = page_size
736
737 prev_links = {}
738 resources = []
739 num_resources = len(expected_resources)
740 niterations = int(math.ceil(float(num_resources) / page_size))
741 for i in range(niterations):
742 if prev_links:
743 uri = self.get_bare_url(prev_links['previous'])
744 else:
Ihar Hrachyshka7f79fe62016-06-07 21:23:44 +0200745 pagination_args.update(self.list_kwargs)
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200746 uri = self.list_client.build_uri(
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200747 self.plural_name, page_reverse=True, **pagination_args)
Ihar Hrachyshkab7940d92016-06-10 13:44:22 +0200748 prev_links, body = self.list_client.get_uri_with_links(
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200749 self.plural_name, uri
750 )
751 resources_ = self._extract_resources(body)
Béla Vancsicsf1806182016-08-23 07:36:18 +0200752 self.assertGreaterEqual(page_size, len(resources_))
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200753 resources.extend(reversed(resources_))
754
755 self.assertSameOrder(expected_resources, reversed(resources))
756
757 @_require_pagination
758 @_require_sorting
759 def _test_list_pagination_page_reverse_asc(self):
760 self._test_list_pagination_page_reverse(
761 direction=constants.SORT_DIRECTION_ASC)
762
763 @_require_pagination
764 @_require_sorting
765 def _test_list_pagination_page_reverse_desc(self):
766 self._test_list_pagination_page_reverse(
767 direction=constants.SORT_DIRECTION_DESC)
768
769 def _test_list_pagination_page_reverse(self, direction):
770 pagination_args = {
771 'sort_dir': direction,
Armando Migliaccio57581c62016-07-01 10:13:19 -0700772 'sort_key': self.field,
Ihar Hrachyshkaaeb03a02016-05-18 20:03:18 +0200773 'limit': 3,
774 }
775 body = self.list_method(**pagination_args)
776 expected_resources = self._extract_resources(body)
777
778 pagination_args['limit'] -= 1
779 pagination_args['marker'] = expected_resources[-1]['id']
780 pagination_args['page_reverse'] = True
781 body = self.list_method(**pagination_args)
782
783 self.assertSameOrder(
784 # the last entry is not included in 2nd result when used as a
785 # marker
786 expected_resources[:-1],
787 self._extract_resources(body))
Victor Morales1be97b42016-09-05 08:50:06 -0500788
789 def _test_list_validation_filters(self):
790 validation_args = {
791 'unknown_filter': 'value',
792 }
793 body = self.list_method(**validation_args)
794 resources = self._extract_resources(body)
795 for resource in resources:
796 self.assertIn(resource['name'], self.resource_names)