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