blob: 743083129f0c9f73ff90d0f6a13eb7a2985a35df [file] [log] [blame]
srikanth116e6e82014-08-19 07:22:37 -07001#
2# Copyright (c) 2013 Big Switch Networks, Inc.
3#
4# Licensed under the Eclipse Public License, Version 1.0 (the
5# "License"); you may not use this file except in compliance with the
6# License. You may obtain a copy of the License at
7#
8# http://www.eclipse.org/legal/epl-v10.html
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,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
13# implied. See the License for the specific language governing
14# permissions and limitations under the License.
15#
16
17from django.core.validators import validate_ipv4_address, MinValueValidator
18from django.db import models
19from django.forms import ValidationError
20from django.test import TestCase, Client
21from django.utils import simplejson
22import copy
23import urllib
24import urllib2
25from sdncon.rest.validators import validate_mac_address, RangeValidator, ChoicesValidator
26from sdncon.rest.config import add_config_handler, reset_last_config_state, model_instances_equal
27
28# These are the test models used in the unit tests
29# When running the unit tests, syncdb looks for model definitions
30# in this file (i.e. tests.py), so these will be in the complete
31# list of models used by the REST code
32
33class RestTestModel(models.Model):
34 name = models.CharField(max_length=32, primary_key=True)
35 enabled = models.BooleanField()
36 min = models.IntegerField(validators=[MinValueValidator(100)])
37 range = models.IntegerField(validators=[RangeValidator(10,100)])
38 internal = models.CharField(max_length=24,default='foo')
39 ip = models.IPAddressField(validators=[validate_ipv4_address])
40 mac = models.CharField(max_length=20, validators=[validate_mac_address])
41 ratio = models.FloatField()
42 COLOR_CHOICES = (
43 ('red', 'Dark Red'),
44 ('blue', 'Navy Blue'),
45 ('green', 'Green'),
46 ('white', 'Ivory White')
47 )
48 color = models.CharField(max_length=20, validators=[ChoicesValidator(COLOR_CHOICES)])
49
50 def clean(self):
51 # This is just a dummy check for testing purposes. Typically you'd only
52 # use the model-level clean function for validating things across multiple fields
53 if self.range == 50:
54 raise ValidationError("Dummy model-level validation failure.")
55
56 class Rest:
57 NAME = 'rest-test'
58 FIELD_INFO = ({'name':'internal', 'private': True},)
59
60class RestTestTagModel(models.Model):
61 rest_test = models.ForeignKey(RestTestModel)
62 name = models.CharField(max_length=32)
63 value = models.CharField(max_length=256)
64
65 class Rest:
66 NAME = 'rest-test-tag'
67 FIELD_INFO = ({'name':'rest_test', 'rest_name':'rest-test'},)
68
69class RestTestRenamedFieldModel(models.Model):
70 test = models.CharField(max_length=16,primary_key=True)
71
72 class Rest:
73 NAME = 'rest-test-renamed'
74 FIELD_INFO = ({'name':'test', 'rest_name':'renamed'},)
75
76class RestDisabledModel(models.Model):
77 dummy = models.CharField(max_length=32)
78
79class RestCompoundKeyModel(models.Model):
80 name = models.CharField(max_length=32)
81 index = models.IntegerField()
82 extra = models.CharField(max_length=32, default='dummy')
83
84 class CassandraSettings:
85 COMPOUND_KEY_FIELDS = ['name', 'index']
86
87 class Rest:
88 NAME = 'rest-compound-key'
89
90#############################################################################
91# These are some client-side utility functions for getting, putting, deleting
92# data using the REST API
93#############################################################################
94
95def construct_rest_url(path, query_params=None):
96 url = 'http://localhost:8000/rest/v1/%s/' % path
97 if query_params:
98 url += '?'
99 url += urllib.urlencode(query_params)
100 return url
101
102def get_rest_data(type, query_param_dict=None):
103 url = construct_rest_url(type, query_param_dict)
104 request = urllib2.Request(url)
105 response = urllib2.urlopen(request)
106 response_text = response.read()
107 obj = simplejson.loads(response_text)
108 return obj
109
110def put_rest_data(obj, type, query_param_dict=None):
111 url = construct_rest_url(type, query_param_dict)
112 post_data = simplejson.dumps(obj)
113 request = urllib2.Request(url, post_data, {'Content-Type':'application/json'})
114 request.get_method = lambda: 'PUT'
115 response = urllib2.urlopen(request)
116 return response.read()
117
118def delete_rest_data(type, query_param_dict=None):
119 url = construct_rest_url(type, query_param_dict)
120 request = urllib2.Request(url)
121 request.get_method = lambda: 'DELETE'
122 response = urllib2.urlopen(request)
123 return response.read()
124
125def test_construct_rest_url(path, query_params=None):
126 url = '/rest/v1/%s/' % path
127 if query_params:
128 url += '?'
129 url += urllib.urlencode(query_params)
130 return url
131
132def test_construct_rest_model_url(path, query_params=None):
133 return test_construct_rest_url('model/' + path, query_params)
134
135def test_get_rest_model_data(type, query_params=None):
136 url = test_construct_rest_model_url(type,query_params)
137 c = Client()
138 response = c.get(url)
139 return response
140
141def test_put_rest_model_data(obj, type, query_params=None):
142 url = test_construct_rest_model_url(type, query_params)
143 data = simplejson.dumps(obj)
144 c = Client()
145 response = c.put(url , data, 'application/json')
146 return response
147
148def test_delete_rest_model_data(type, query_params=None):
149 url = test_construct_rest_model_url(type, query_params)
150 c = Client()
151 response = c.delete(url)
152 return response
153
154def get_sorted_list(data_list, key_name, create_copy=False):
155 if create_copy:
156 data_list = copy.deepcopy(data_list)
157 if key_name:
158 key_func = lambda item: item.get(key_name)
159 else:
160 key_func = lambda item: item[0]
161 data_list.sort(key = key_func)
162 return data_list
163
164#############################################################################
165# The actual unit test classes
166#############################################################################
167
168class BasicFunctionalityTest(TestCase):
169
170 REST_TEST_DATA = [
171 {'name':'foobar','enabled':True,'min':100,'range':30,'ip':'192.168.1.1','mac':'00:01:22:34:56:af','ratio':4.56,'color':'red'},
172 {'name':'sdnplatform','enabled':False,'min':400,'range':45,'ip':'192.168.1.2','mac':'00:01:22:CC:56:DD','ratio':8.76,'color':'green'},
173 {'name':'foobar2','enabled':True,'min':1000,'range':100,'ip':'192.168.1.3','mac':'00:01:22:34:56:af','ratio':1,'color':'white'},
174 ]
175
176 REST_TEST_TAG_DATA = [
177 {'id':'1','rest-test':'foobar','name':'one','value':'testit'},
178 {'id':'2','rest-test':'foobar','name':'two','value':'test2'},
179 {'id':'3','rest-test':'sdnplatform','name':'three','value':'three'},
180 ]
181
182 def setUp(self):
183 # Create the RestTestModel instances
184 rest_test_list = []
185 for rtd in self.REST_TEST_DATA:
186 rt = RestTestModel(**rtd)
187 rt.save()
188 rest_test_list.append(rt)
189
190 # Create the RestTestTagModel instances
191 for rttd in self.REST_TEST_TAG_DATA:
192 rttd_init = rttd.copy()
193 for rt in rest_test_list:
194 if rt.name == rttd['rest-test']:
195 del rttd_init['rest-test']
196 rttd_init['rest_test'] = rt
197 break
198 else:
199 raise Exception('Invalid initialization data for REST unit tests')
200 rtt = RestTestTagModel(**rttd_init)
201 rtt.save()
202
203 rtrf = RestTestRenamedFieldModel(test='test')
204 rtrf.save()
205
206 # Create the RestDisabledModel instance. We're only going to test that
207 # we can't access this model via the REST API, so we don't need to keep
208 # track of the data used for the instance
209 #rdm = RestDisabledModel(dummy='dummy')
210 #rdm.save()
211
212 def check_rest_test_data_result(self, data_list, expected_data_list, message=None):
213 data_list = get_sorted_list(data_list, 'name', True)
214 expected_data_list = get_sorted_list(expected_data_list, 'name', True)
215 self.assertEqual(len(data_list), len(expected_data_list), message)
216 for i in range(len(data_list)):
217 data = data_list[i]
218 expected_data = expected_data_list[i]
219 self.assertEqual(data['name'], expected_data['name'], message)
220 self.assertEqual(data['enabled'], expected_data['enabled'], message)
221 self.assertEqual(data['min'], expected_data['min'], message)
222 self.assertEqual(data['range'], expected_data['range'], message)
223 self.assertEqual(data['ip'], expected_data['ip'], message)
224 self.assertEqual(data['mac'], expected_data['mac'], message)
225 self.assertAlmostEqual(data['ratio'], expected_data['ratio'], 7, message)
226 self.assertEqual(data['color'], expected_data['color'], message)
227
228 def check_rest_test_tag_data_result(self, data_list, expected_data_list, message=None):
229 data_list = get_sorted_list(data_list, 'name', True)
230 expected_data_list = get_sorted_list(expected_data_list, 'name', True)
231 self.assertEqual(len(data_list), len(expected_data_list), message)
232 for i in range(len(data_list)):
233 data = data_list[i]
234 expected_data = expected_data_list[i]
235 self.assertEqual(str(data['id']), expected_data['id'], message)
236 self.assertEqual(data['rest-test'], expected_data['rest-test'], message)
237 self.assertEqual(data['name'], expected_data['name'], message)
238 self.assertEqual(data['value'], expected_data['value'], message)
239
240 def test_put_one(self):
241 test_put_rest_model_data({'min':200,'color':'white'}, 'rest-test/foobar')
242 response = test_get_rest_model_data('rest-test')
243 data = simplejson.loads(response.content)
244 rtd_copy = copy.deepcopy(self.REST_TEST_DATA)
245 rtd_copy[0]['min'] = 200
246 rtd_copy[0]['color'] = 'white'
247 self.check_rest_test_data_result(data, rtd_copy)
248
249 def test_put_query(self):
250 test_put_rest_model_data({'min':200,'color':'white'}, 'rest-test', {'enabled':True})
251 response = test_get_rest_model_data('rest-test')
252 data = simplejson.loads(response.content)
253 rtd_copy = copy.deepcopy(self.REST_TEST_DATA)
254 for rtd in rtd_copy:
255 if rtd['enabled']:
256 rtd['min'] = 200
257 rtd['color'] = 'white'
258 self.check_rest_test_data_result(data, rtd_copy)
259
260 def test_create_one(self):
261 rtd_copy = copy.deepcopy(self.REST_TEST_DATA)
262 create_rtd = rtd_copy[0].copy()
263 create_rtd['name'] = 'test-create'
264 rtd_copy.append(create_rtd)
265 test_put_rest_model_data(create_rtd, 'rest-test')
266 response = test_get_rest_model_data('rest-test')
267 data = simplejson.loads(response.content)
268 self.check_rest_test_data_result(data, rtd_copy)
269
270 def test_create_many(self):
271 rtd_copy = copy.deepcopy(self.REST_TEST_DATA)
272 create_rtd1 = rtd_copy[0].copy()
273 create_rtd1['name'] = 'test-create1'
274 create_rtd1['min'] = 2000
275 rtd_copy.append(create_rtd1)
276 create_rtd2 = rtd_copy[0].copy()
277 create_rtd2['name'] = 'test-create2'
278 create_rtd1['min'] = 4000
279 rtd_copy.append(create_rtd2)
280 test_put_rest_model_data([create_rtd1,create_rtd2], 'rest-test')
281 response = test_get_rest_model_data('rest-test')
282 data = simplejson.loads(response.content)
283 self.check_rest_test_data_result(data, rtd_copy)
284
285 def test_get(self):
286 RTD = self.REST_TEST_DATA
287 RTTD = self.REST_TEST_TAG_DATA
288 GET_TEST_VECTORS = (
289 #model_name, query_params, expected_data
290 ('rest-test', None, RTD, 'Failure getting all rest-test instances'),
291 ('rest-test-tag', None, RTTD, 'Failure getting all rest-test-tag instances'),
292 ('rest-test/foobar', None, RTD[0], 'Failure getting single rest-test instance by URL'),
293 ('rest-test', {'name':'foobar'}, [RTD[0]], 'Failure querying for single rest-test instance'),
294 ('rest-test', {'name':'foobar','nolist':True}, RTD[0], 'Failure querying for single rest-test instance using nolist'),
295 ('rest-test', {'enabled':True}, [RTD[0],RTD[2]], 'Failure querying for multiple rest-test instances'),
296 ('rest-test', {'name__startswith':'foobar'}, [RTD[0],RTD[2]], 'Failure querying for multiple rest-test instances using startswith'),
297 ('rest-test', {'orderby':'ratio'}, [RTD[2],RTD[0],RTD[1]], 'Failure querying with ascending orderby'),
298 ('rest-test', {'orderby':'-ratio'}, [RTD[1],RTD[0],RTD[2]], 'Failure querying with descending orderby'),
299 ('rest-test', {'orderby':'enabled,-ratio'}, [RTD[1],RTD[0],RTD[2]], 'Failure querying with multi-field orderby'),
300 ('rest-test-tag', {'rest-test':'foobar'}, [RTTD[0],RTTD[1]], 'Failure querying by ForeignKey value'),
301 ('rest-test-tag', {'rest-test__startswith':'foo'}, [RTTD[0],RTTD[1]], 'Failure querying by ForeignKey value'),
302 ('rest-test-tag', {'rest-test__contains':'swi'}, [RTTD[2]], 'Failure querying by ForeignKey value'),
303 )
304
305 for model_name, query_params, expected_data, message in GET_TEST_VECTORS:
306 response = test_get_rest_model_data(model_name, query_params)
307 data = simplejson.loads(response.content)
308 #print model_name
309 #self.check_rest_test_data_result(data, expected_data, message)
310 if type(data) is not list:
311 data = [data]
312 #data = get_sorted_list(data, 'name', False)
313 if type(expected_data) is not list:
314 expected_data = [expected_data]
315 #expected_data = get_sorted_list(expected_data, 'name', True)
316 if model_name.startswith('rest-test-tag'):
317 self.check_rest_test_tag_data_result(data, expected_data, message)
318 else:
319 self.check_rest_test_data_result(data, expected_data, message)
320
321 def test_renamed_field(self):
322 response = test_get_rest_model_data('rest-test-renamed/test')
323 self.assertEqual(response.status_code,200)
324 data = simplejson.loads(response.content)
325 self.assertEqual(data['renamed'], 'test')
326
327 def test_delete_one(self):
328 test_delete_rest_model_data('rest-test/foobar')
329 response = test_get_rest_model_data('rest-test')
330 data = simplejson.loads(response.content)
331 self.check_rest_test_data_result(data, self.REST_TEST_DATA[1:])
332
333 def test_delete_query(self):
334 test_delete_rest_model_data('rest-test', {'name__startswith':'foobar'})
335 response = test_get_rest_model_data('rest-test')
336 data = simplejson.loads(response.content)
337 self.check_rest_test_data_result(data, [self.REST_TEST_DATA[1]])
338
339 def test_delete_all(self):
340 test_delete_rest_model_data('rest-test')
341 response = test_get_rest_model_data('rest-test')
342 data = simplejson.loads(response.content)
343 self.assertEqual(len(data),0)
344
345class NegativeTest(TestCase):
346
347 def test_invalid_model_name(self):
348 response = test_get_rest_model_data('foobar')
349 self.assertEqual(response.status_code,400)
350 response_obj = simplejson.loads(response.content)
351 self.assertEqual(response_obj['error_type'],'RestInvalidDataTypeException')
352
353 def test_resource_not_found(self):
354 response = test_get_rest_model_data('rest-test/foobar')
355 self.assertEqual(response.status_code,404)
356 response_obj = simplejson.loads(response.content)
357 self.assertEqual(response_obj['error_type'],'RestResourceNotFoundException')
358
359 def test_invalid_orderby(self):
360 response = test_get_rest_model_data('rest-test', {'orderby':'foobar'})
361 self.assertEqual(response.status_code,400)
362 response_obj = simplejson.loads(response.content)
363 self.assertEqual(response_obj['error_type'],'RestInvalidOrderByException')
364
365 def test_hidden_model(self):
366 response = test_get_rest_model_data('restdisabledmodel')
367 self.assertEqual(response.status_code,400)
368 response_obj = simplejson.loads(response.content)
369 self.assertEqual(response_obj['error_type'],'RestInvalidDataTypeException')
370
371class ValidationTest(TestCase):
372
373 DEFAULT_DATA = {'name':'foobar','enabled':True,'min':100,'range':30,'ip':'192.168.1.1','mac':'00:01:22:34:56:af','ratio':4.56,'color':'red'}
374 TEST_VECTORS = (
375 #update-dict, error-type, error-messgae
376 ({'min':99}, ['min'],'Min value validation failed'),
377 ({'range':8}, ['range'],'Min range validation failed'),
378 ({'range':2000}, ['range'],'Max range validation failed'),
379 ({'range':50}, None,'Model-level clean validation failed'),
380 ({'mac':'00:01:02:03:05'}, ['mac'],'Too short MAC address validation failed'),
381 ({'mac':'00:01:02:03:05:99:45'}, ['mac'],'Too long MAC address validation failed'),
382 ({'mac':'00,01,02,03,05,99'}, ['mac'],'MAC address separator char validation failed'),
383 ({'mac':'0r:01:02:03:05:99'}, ['mac'],'MAC address character validation failed'),
384 ({'mac':'123:01:02:03:05:99'}, ['mac'],'MAC address byte length validation failed'),
385 ({'color':'purple'}, ['color'],'Choices validation failed'),
386 ({'ip':'foobar'}, ['ip'], 'Invalid IP address char validation failed'),
387 ({'ip':'192.168.1'}, ['ip'], 'Too short IP address char validation failed'),
388 ({'ip':'192.168.1.0.5'}, ['ip'], 'Too long IP address char validation failed'),
389 ({'ip':'192,168,1,0'}, ['ip'], 'IP address separator char validation failed'),
390 ({'ip':'256.168.1.0'}, ['ip'], 'Out of range IP address byte validation failed'),
391 ({'min':99, 'ip':'256.168.1.0'}, ['min', 'ip'], 'Multiple field validation failed'),
392 )
393
394 def check_response(self, response, invalid_fields, message):
395 self.assertEqual(response.status_code,400)
396 response_obj = simplejson.loads(response.content)
397 self.assertEqual(response_obj['error_type'], 'RestValidationException', message)
398 if invalid_fields:
399 self.assertEqual(response_obj.get('model_error'), None)
400 field_errors = response_obj.get('field_errors')
401 self.assertNotEqual(field_errors, None)
402 self.assertEqual(len(invalid_fields), len(field_errors), message)
403 for field_name in field_errors.keys():
404 self.assertTrue(field_name in invalid_fields, message)
405 else:
406 self.assertEqual(response_obj.get('field_errors'), None)
407 self.assertNotEqual(response_obj.get('model_error'), None)
408 # FIXME: Maybe check the description here too?
409
410 def test_create_validation(self):
411 for test_data, invalid_fields, message in self.TEST_VECTORS:
412 data = self.DEFAULT_DATA.copy()
413 for name, value in test_data.items():
414 data[name] = value
415 put_response = test_put_rest_model_data(data, 'rest-test')
416 self.check_response(put_response, invalid_fields, message)
417
418 def test_update_validation(self):
419 # First add an instance with the default data and check that it's OK
420 put_response = test_put_rest_model_data(self.DEFAULT_DATA, 'rest-test')
421 self.assertEqual(put_response.status_code,200)
422
423 for test_data, invalid_fields, message in self.TEST_VECTORS:
424 put_response = test_put_rest_model_data(test_data, 'rest-test/foobar')
425 self.check_response(put_response, invalid_fields, message)
426
427
428class ConfigHandlerTest(TestCase):
429
430 TEST_DATA = {'name':'foobar','enabled':True,'min':100,'range':30,'ip':'192.168.1.1','mac':'00:01:22:34:56:af','ratio':4.56,'color':'red'}
431
432 test_op = None
433 test_old_instance = None
434 test_new_instance = None
435 test_modified_fields = None
436
437 @staticmethod
438 def reset_config_handler():
439 ConfigHandlerTest.test_op = None
440 ConfigHandlerTest.test_old_instance = None
441 ConfigHandlerTest.test_new_instance = None
442 ConfigHandlerTest.test_modified_fields = None
443
444 @staticmethod
445 def config_handler(op, old_instance, new_instance, modified_fields):
446 ConfigHandlerTest.test_op = op
447 ConfigHandlerTest.test_old_instance = old_instance
448 ConfigHandlerTest.test_new_instance = new_instance
449 ConfigHandlerTest.test_modified_fields = modified_fields
450
451 def check_config_handler(self, expected_op, expected_old_instance, expected_new_instance, expected_modified_fields):
452 self.assertEqual(ConfigHandlerTest.test_op, expected_op)
453 self.assertTrue(model_instances_equal(ConfigHandlerTest.test_old_instance, expected_old_instance))
454 self.assertTrue(model_instances_equal(ConfigHandlerTest.test_new_instance, expected_new_instance))
455 self.assertEqual(ConfigHandlerTest.test_modified_fields, expected_modified_fields)
456
457 def test_config(self):
458 reset_last_config_state()
459 # Install the config handler
460 field_list = ['internal', 'min', 'mac']
461 add_config_handler({RestTestModel: field_list}, ConfigHandlerTest.config_handler)
462
463 # Check that config handler is triggered on an insert
464 ConfigHandlerTest.reset_config_handler()
465 test = RestTestModel(**self.TEST_DATA)
466 test.save()
467 self.check_config_handler('INSERT', None, test, None)
468
469 # Check that config handler is triggered on an update
470 ConfigHandlerTest.reset_config_handler()
471 expected_old = RestTestModel.objects.get(pk='foobar')
472 test.internal = 'check'
473 test.min = 125
474 test.save()
475 self.check_config_handler('UPDATE', expected_old, test, {'internal': 'check', 'min': 125})
476
477 # Check that config handler is not triggered on an update to a field that
478 # it's not configured to care about
479 ConfigHandlerTest.reset_config_handler()
480 test.max = 500
481 test.save()
482 self.check_config_handler(None, None, None, None)
483
484 # Check that config handler is triggered on a delete
485 ConfigHandlerTest.reset_config_handler()
486 test.delete()
487 # delete() clears the pk which messes up the instance
488 # comparison logic in check_config_handler, so we hack
489 # it back to the value it had before the delete
490 test.name = 'foobar'
491 self.check_config_handler('DELETE', test, None, None)
492
493
494class CompoundKeyModelTest(TestCase):
495
496 TEST_DATA = [
497 {'name': 'foo', 'index': 3},
498 {'name': 'foo', 'index': 7},
499 {'name': 'bar', 'index': 2, 'extra': 'abc'},
500 {'name': 'bar', 'index': 4, 'extra': 'test'},
501 ]
502
503 def setUp(self):
504 for data in self.TEST_DATA:
505 test_put_rest_model_data(data, 'rest-compound-key')
506
507 def check_query_results(self, actual_results, expected_results):
508 self.assertEqual(len(actual_results), len(expected_results))
509 actual_results = get_sorted_list(actual_results, 'id', False)
510 expected_results = copy.deepcopy(expected_results)
511 for expected_result in expected_results:
512 expected_result['id'] = expected_result['name'] + '|' + str(expected_result['index'])
513 expected_results = get_sorted_list(expected_results, 'id')
514
515 for actual_result, expected_result in zip(actual_results, expected_results):
516 self.assertEqual(actual_result['id'], expected_result['id'])
517 self.assertEqual(actual_result['name'], expected_result['name'])
518 self.assertEqual(actual_result['index'], expected_result['index'])
519 expected_extra = expected_result.get('extra', 'dummy')
520 self.assertEqual(actual_result['extra'], expected_extra)
521
522 def test_set_up(self):
523 response = test_get_rest_model_data('rest-compound-key')
524 self.assertEqual(response.status_code, 200)
525 actual_results = simplejson.loads(response.content)
526 self.check_query_results(actual_results, self.TEST_DATA)
527
528 def test_delete_by_id(self):
529 test_delete_rest_model_data('rest-compound-key', {'id': 'foo|3'})
530 test_delete_rest_model_data('rest-compound-key', {'id': 'bar|4'})
531 response = test_get_rest_model_data('rest-compound-key')
532 self.assertEqual(response.status_code, 200)
533 actual_results = simplejson.loads(response.content)
534 self.check_query_results(actual_results, self.TEST_DATA[1:-1])
535
536 def test_delete_by_fields(self):
537 test_delete_rest_model_data('rest-compound-key', {'name': 'bar', 'index': 4})
538 response = test_get_rest_model_data('rest-compound-key')
539 self.assertEqual(response.status_code, 200)
540 actual_results = simplejson.loads(response.content)
541 self.check_query_results(actual_results, self.TEST_DATA[:-1])
542
543class UserDataTest(TestCase):
544
545 TEST_DATA = [
546 # name, data, binary, content-type
547 ('test/foobar', 'hello world\nanother line', False, None),
548 ('test/json-test', '[0,1,2]', False, 'application/json'),
549 ('test/binary-test', '\x01\x02\x03\x04\x55', True, None),
550 ]
551
552 MORE_TEST_DATA = [
553 ('moretests/foo1', 'moretest1', False, None),
554 ('moretests/foo2', 'moretest2', False, None),
555 ]
556
557 def add_user_data(self, client, user_data):
558 for name, data, binary, content_type in user_data:
559 if not content_type:
560 if binary:
561 content_type = 'application/octet-stream'
562 else:
563 content_type = 'text/plain'
564 url = test_construct_rest_url('data/' + name, {'binary':binary})
565 response = client.put(url, data, content_type)
566 self.assertEquals(response.status_code, 200)
567
568 def check_expected_info_list(self, info_list, expected_info_list):
569 self.assertEqual(type(info_list), list)
570 info_list = get_sorted_list(info_list, 'name', True)
571 expected_info_list = get_sorted_list(expected_info_list, None, True)
572 self.assertEqual(len(info_list), len(expected_info_list))
573 for i in range(len(info_list)):
574 data = info_list[i]
575 expected_data = expected_info_list[i]
576 expected_name = expected_data[0]
577 expected_url_path = 'rest/v1/data/' + expected_name + '/'
578 self.assertEqual(data['name'], expected_name)
579 self.assertEqual(data['url_path'], expected_url_path)
580
581 def get_info_list(self, client, query_params=None):
582 url = test_construct_rest_url('data', query_params)
583 get_response = client.get(url)
584 self.assertEqual(get_response.status_code, 200)
585 info_list = simplejson.loads(get_response.content)
586
587 # The view for listing user data items hard-codes inclusion of some
588 # *magic* user data items corresponding to the startup-config
589 # and update-config, depending on the presence of certain files
590 # in the file system. To make the unit tests work correctly if/when
591 # either/both of those items are added we do this filtering pass
592 # over the items returned from the REST API. Ugly hack!
593 info_list = [item for item in info_list if '-config' not in item['name']]
594
595 return info_list
596
597 def test_user_data_basic(self):
598 c = Client()
599 self.add_user_data(c, self.TEST_DATA)
600 for name, data, binary, content_type in self.TEST_DATA:
601 if not content_type:
602 if binary:
603 content_type = 'application/octet-stream'
604 else:
605 content_type = 'text/plain'
606 url = test_construct_rest_url('data/' + name)
607 get_response = c.get(url)
608 self.assertEqual(get_response.content, data)
609 self.assertEqual(get_response['Content-Type'], content_type)
610
611 def test_user_data_delete(self):
612 c = Client()
613
614 self.add_user_data(c, self.TEST_DATA)
615 self.add_user_data(c, self.MORE_TEST_DATA)
616
617 # Do a delete with a query filter
618 url = test_construct_rest_url('data', {'name__startswith':'moretests/'})
619 response = c.delete(url)
620 self.assertEqual(response.status_code, 200)
621
622 info_list = self.get_info_list(c)
623 expected_info_list = self.TEST_DATA
624 self.check_expected_info_list(info_list, expected_info_list)
625
626 # Do a delete of a specific user data element
627 url = test_construct_rest_url('data/' + self.TEST_DATA[0][0])
628 response = c.delete(url)
629 self.assertEqual(response.status_code, 200)
630
631 info_list = self.get_info_list(c)
632 expected_info_list = self.TEST_DATA[1:]
633 self.check_expected_info_list(info_list, expected_info_list)
634
635 def test_user_data_info(self):
636
637 c = Client()
638
639 self.add_user_data(c, self.TEST_DATA)
640 self.add_user_data(c, self.MORE_TEST_DATA)
641
642 info_list = self.get_info_list(c)
643 expected_info_list = self.TEST_DATA + self.MORE_TEST_DATA
644 self.check_expected_info_list(info_list, expected_info_list)
645
646 info_list = self.get_info_list(c, {'name__startswith':'test/'})
647 expected_info_list = self.TEST_DATA
648 self.check_expected_info_list(info_list, expected_info_list)