blob: 6f15a10b027540b6f4f28b3db4d3d9d7ed7f2555 [file] [log] [blame]
Alex Klein2b236722019-06-19 15:44:26 -06001# Copyright 2019 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Tests for the validate module."""
6
Alex Klein2b236722019-06-19 15:44:26 -06007import os
8
Alex Klein69339cc2019-07-22 14:08:35 -06009from chromite.api import api_config
Alex Klein2b236722019-06-19 15:44:26 -060010from chromite.api import validate
Alex Klein86242bf2020-09-22 15:23:23 -060011from chromite.api.gen.chromite.api import build_api_test_pb2
Alex Klein2b236722019-06-19 15:44:26 -060012from chromite.api.gen.chromiumos import common_pb2
13from chromite.lib import cros_build_lib
14from chromite.lib import cros_test_lib
15from chromite.lib import osutils
16
Mike Frysingeref94e4c2020-02-10 23:59:54 -050017
Alex Kleinbdace302020-12-03 14:40:23 -070018# These tests test the validators by defining a local `impl` function that
19# has the same parameters as a controller function and the validator being
20# tested. The validators don't care that they aren't actually controller
21# functions, they just need the function to look like one, so it works
22# to pass an arbitrary message; i.e. passing one of the Request messages
23# we'd usually expect in a controller is not required. The validator
24# just needs to be checking one of the fields on the message being used.
Alex Klein2008aee2019-08-20 16:25:27 -060025class ExistsTest(cros_test_lib.TempDirTestCase, api_config.ApiConfigMixin):
Alex Klein2b236722019-06-19 15:44:26 -060026 """Tests for the exists validator."""
27
28 def test_not_exists(self):
29 """Test the validator fails when given a path that doesn't exist."""
30 path = os.path.join(self.tempdir, 'DOES_NOT_EXIST')
31
32 @validate.exists('path')
Alex Klein2008aee2019-08-20 16:25:27 -060033 def impl(_input_proto, _output_proto, _config):
Alex Klein2b236722019-06-19 15:44:26 -060034 self.fail('Incorrectly allowed method to execute.')
35
36 with self.assertRaises(cros_build_lib.DieSystemExit):
Alex Klein2008aee2019-08-20 16:25:27 -060037 impl(common_pb2.Chroot(path=path), None, self.api_config)
Alex Klein2b236722019-06-19 15:44:26 -060038
39 def test_exists(self):
40 """Test the validator fails when given a path that doesn't exist."""
41 path = os.path.join(self.tempdir, 'chroot')
42 osutils.SafeMakedirs(path)
43
44 @validate.exists('path')
Alex Klein2008aee2019-08-20 16:25:27 -060045 def impl(_input_proto, _output_proto, _config):
Alex Klein2b236722019-06-19 15:44:26 -060046 pass
47
Alex Klein2008aee2019-08-20 16:25:27 -060048 impl(common_pb2.Chroot(path=path), None, self.api_config)
49
50 def test_skip_validation(self):
51 """Test skipping validation case."""
52 @validate.exists('path')
53 def impl(_input_proto, _output_proto, _config):
54 pass
55
56 # This would otherwise raise an error for an invalid path.
57 impl(common_pb2.Chroot(), None, self.no_validate_config)
Alex Klein2b236722019-06-19 15:44:26 -060058
59
Alex Klein2008aee2019-08-20 16:25:27 -060060class IsInTest(cros_test_lib.TestCase, api_config.ApiConfigMixin):
Alex Klein231d2da2019-07-22 16:44:45 -060061 """Tests for the is_in validator."""
62
63 def test_in(self):
64 """Test a valid value."""
65 @validate.is_in('path', ['/chroot/path', '/other/chroot/path'])
Alex Klein2008aee2019-08-20 16:25:27 -060066 def impl(_input_proto, _output_proto, _config):
Alex Klein231d2da2019-07-22 16:44:45 -060067 pass
68
69 # Make sure all of the values work.
Alex Klein2008aee2019-08-20 16:25:27 -060070 impl(common_pb2.Chroot(path='/chroot/path'), None, self.api_config)
71 impl(common_pb2.Chroot(path='/other/chroot/path'), None, self.api_config)
Alex Klein231d2da2019-07-22 16:44:45 -060072
73 def test_not_in(self):
74 """Test an invalid value."""
75 @validate.is_in('path', ['/chroot/path', '/other/chroot/path'])
Alex Klein2008aee2019-08-20 16:25:27 -060076 def impl(_input_proto, _output_proto, _config):
Alex Klein231d2da2019-07-22 16:44:45 -060077 pass
78
79 # Should be failing on the invalid value.
80 with self.assertRaises(cros_build_lib.DieSystemExit):
Alex Klein2008aee2019-08-20 16:25:27 -060081 impl(common_pb2.Chroot(path='/bad/value'), None, self.api_config)
Alex Klein231d2da2019-07-22 16:44:45 -060082
83 def test_not_set(self):
84 """Test an unset value."""
85 @validate.is_in('path', ['/chroot/path', '/other/chroot/path'])
Alex Klein2008aee2019-08-20 16:25:27 -060086 def impl(_input_proto, _output_proto, _config):
Alex Klein231d2da2019-07-22 16:44:45 -060087 pass
88
89 # Should be failing without a value set.
90 with self.assertRaises(cros_build_lib.DieSystemExit):
Alex Klein2008aee2019-08-20 16:25:27 -060091 impl(common_pb2.Chroot(), None, self.api_config)
92
93 def test_skip_validation(self):
94 """Test skipping validation case."""
95 @validate.is_in('path', ['/chroot/path', '/other/chroot/path'])
96 def impl(_input_proto, _output_proto, _config):
97 pass
98
99 # This would otherwise raise an error for an invalid path.
100 impl(common_pb2.Chroot(), None, self.no_validate_config)
Alex Klein231d2da2019-07-22 16:44:45 -0600101
102
Alex Kleinbdace302020-12-03 14:40:23 -0700103class EachInTest(cros_test_lib.TestCase, api_config.ApiConfigMixin):
104 """Tests for the each_in validator."""
105
106 # Easier access to the enum values.
107 ENUM_FOO = build_api_test_pb2.TEST_ENUM_FOO
108 ENUM_BAR = build_api_test_pb2.TEST_ENUM_BAR
109 ENUM_BAZ = build_api_test_pb2.TEST_ENUM_BAZ
110
111 # pylint: disable=docstring-misnamed-args
112 def _message_request(self, *messages):
113 """Build a request instance, filling out the messages field.
114
115 Args:
116 messages: Each messages data (id, name, flag, enum) as lists. Only
117 requires as many as are set. e.g. _request([1], [2]) will create two
118 messages with only ids set. _request([1, 'name']) will create one with
119 id and name set, but not flag or enum.
120 """
121 request = build_api_test_pb2.TestRequestMessage()
122 for message in messages or []:
123 msg = request.messages.add()
124 try:
125 msg.id = message[0]
126 msg.name = message[1]
127 msg.flag = message[2]
128 except IndexError:
129 pass
130
131 return request
132
133 def _enums_request(self, *enum_values):
134 """Build a request instance, setting the test_enums field."""
135 request = build_api_test_pb2.TestRequestMessage()
136 for value in enum_values:
137 request.test_enums.append(value)
138
139 return request
140
141 def _numbers_request(self, *numbers):
142 """Build a request instance, setting the numbers field."""
143 request = build_api_test_pb2.TestRequestMessage()
144 request.numbers.extend(numbers)
145
146 return request
147
148 def test_message_in(self):
149 """Test valid values."""
150
151 @validate.each_in('messages', 'name', ['foo', 'bar'])
152 def impl(_input_proto, _output_proto, _config):
153 pass
154
155 impl(self._message_request([1, 'foo']), None, self.api_config)
156 impl(self._message_request([1, 'foo'], [2, 'bar']), None, self.api_config)
157
158 def test_enum_in(self):
159 """Test valid enum values."""
160
161 @validate.each_in('test_enums', None, [self.ENUM_FOO, self.ENUM_BAR])
162 def impl(_input_proto, _output_proto, _config):
163 pass
164
165 impl(self._enums_request(self.ENUM_FOO), None, self.api_config)
166 impl(self._enums_request(self.ENUM_FOO, self.ENUM_BAR), None,
167 self.api_config)
168
169 def test_scalar_in(self):
170 """Test valid scalar values."""
171
172 @validate.each_in('numbers', None, [1, 2])
173 def impl(_input_proto, _output_proto, _config):
174 pass
175
176 impl(self._numbers_request(1), None, self.api_config)
177 impl(self._numbers_request(1, 2), None, self.api_config)
178
179 def test_message_not_in(self):
180 """Test an invalid value."""
181
182 @validate.each_in('messages', 'name', ['foo', 'bar'])
183 def impl(_input_proto, _output_proto, _config):
184 pass
185
186 # Should be failing on the invalid value.
187 with self.assertRaises(cros_build_lib.DieSystemExit):
188 impl(self._message_request([1, 'invalid']), None, self.api_config)
189 with self.assertRaises(cros_build_lib.DieSystemExit):
190 impl(self._message_request([1, 'invalid'], [2, 'invalid']), None,
191 self.api_config)
192 with self.assertRaises(cros_build_lib.DieSystemExit):
193 impl(self._message_request([1, 'foo'], [2, 'invalid']), None,
194 self.api_config)
195
196 def test_enum_not_in(self):
197 """Test an invalid enum value."""
198
199 @validate.each_in('test_enums', None, [self.ENUM_FOO, self.ENUM_BAR])
200 def impl(_input_proto, _output_proto, _config):
201 pass
202
203 # Only invalid values.
204 with self.assertRaises(cros_build_lib.DieSystemExit):
205 impl(self._enums_request(self.ENUM_BAZ), None, self.api_config)
206 # Mixed valid/invalid values.
207 with self.assertRaises(cros_build_lib.DieSystemExit):
208 impl(self._enums_request(self.ENUM_FOO, self.ENUM_BAZ), None,
209 self.api_config)
210
211 def test_scalar_not_in(self):
212 """Test invalid scalar value."""
213
214 @validate.each_in('numbers', None, [1, 2])
215 def impl(_input_proto, _output_proto, _config):
216 pass
217
218 # Only invalid values.
219 with self.assertRaises(cros_build_lib.DieSystemExit):
220 impl(self._numbers_request(3), None, self.api_config)
221 # Mixed valid/invalid values.
222 with self.assertRaises(cros_build_lib.DieSystemExit):
223 impl(self._numbers_request(1, 2, 3), None, self.api_config)
224
225 def test_not_set(self):
226 """Test an unset value."""
227
228 @validate.each_in('messages', 'name', ['foo', 'bar'])
229 def impl(_input_proto, _output_proto, _config):
230 pass
231
232 # Should be failing without a value set.
233 # No entries in the field.
234 with self.assertRaises(cros_build_lib.DieSystemExit):
235 impl(self._message_request(), None, self.api_config)
236 # No value set on lone entry.
237 with self.assertRaises(cros_build_lib.DieSystemExit):
238 impl(self._message_request([1]), None, self.api_config)
239 # No value set on multiple entries.
240 with self.assertRaises(cros_build_lib.DieSystemExit):
241 impl(self._message_request([1], [2]), None, self.api_config)
242 # Some valid and some invalid entries.
243 with self.assertRaises(cros_build_lib.DieSystemExit):
244 impl(self._message_request([1, 'foo'], [2]), None, self.api_config)
245
246 def test_optional(self):
247 """Test optional argument."""
248
249 @validate.each_in('messages', 'name', ['foo', 'bar'], optional=True)
250 @validate.each_in('test_enums', None, [self.ENUM_FOO, self.ENUM_BAR],
251 optional=True)
252 @validate.each_in('numbers', None, [1, 2], optional=True)
253 def impl(_input_proto, _output_proto, _config):
254 pass
255
256 # No entries in the field succeeds.
257 impl(self._message_request(), None, self.api_config)
258
259 # Still fails when entries exist but value unset cases.
260 # No value set on lone entry.
261 with self.assertRaises(cros_build_lib.DieSystemExit):
262 impl(self._message_request([1]), None, self.api_config)
263 # No value set on multiple entries.
264 with self.assertRaises(cros_build_lib.DieSystemExit):
265 impl(self._message_request([1], [2]), None, self.api_config)
266 # Some valid and some invalid entries.
267 with self.assertRaises(cros_build_lib.DieSystemExit):
268 impl(self._message_request([1, 'foo'], [2]), None, self.api_config)
269
270 def test_skip_validation(self):
271 """Test skipping validation case."""
272
273 @validate.each_in('messages', 'name', ['foo', 'bar'])
274 @validate.each_in('test_enums', None, [self.ENUM_FOO, self.ENUM_BAR])
275 @validate.each_in('numbers', None, [1, 2])
276 def impl(_input_proto, _output_proto, _config):
277 pass
278
279 # This would otherwise raise an error for multiple invalid fields.
280 impl(self._message_request([1, 'invalid']), None, self.no_validate_config)
281
282
Alex Klein60c80522020-10-13 18:05:38 -0600283class RequireTest(cros_test_lib.TestCase, api_config.ApiConfigMixin):
284 """Tests for the require validator."""
Alex Klein2b236722019-06-19 15:44:26 -0600285
286 def test_invalid_field(self):
287 """Test validator fails when given an unset value."""
288
289 @validate.require('does.not.exist')
Alex Klein2008aee2019-08-20 16:25:27 -0600290 def impl(_input_proto, _output_proto, _config):
Alex Klein2b236722019-06-19 15:44:26 -0600291 self.fail('Incorrectly allowed method to execute.')
292
293 with self.assertRaises(cros_build_lib.DieSystemExit):
Alex Klein2008aee2019-08-20 16:25:27 -0600294 impl(common_pb2.Chroot(), None, self.api_config)
Alex Klein2b236722019-06-19 15:44:26 -0600295
296 def test_not_set(self):
297 """Test validator fails when given an unset value."""
298
299 @validate.require('env.use_flags')
Alex Klein2008aee2019-08-20 16:25:27 -0600300 def impl(_input_proto, _output_proto, _config):
Alex Klein2b236722019-06-19 15:44:26 -0600301 self.fail('Incorrectly allowed method to execute.')
302
303 with self.assertRaises(cros_build_lib.DieSystemExit):
Alex Klein2008aee2019-08-20 16:25:27 -0600304 impl(common_pb2.Chroot(), None, self.api_config)
Alex Klein2b236722019-06-19 15:44:26 -0600305
306 def test_set(self):
307 """Test validator passes when given set values."""
308
309 @validate.require('path', 'env.use_flags')
Alex Klein2008aee2019-08-20 16:25:27 -0600310 def impl(_input_proto, _output_proto, _config):
Alex Klein2b236722019-06-19 15:44:26 -0600311 pass
312
Alex Klein2008aee2019-08-20 16:25:27 -0600313 in_proto = common_pb2.Chroot(path='/chroot/path',
314 env={'use_flags': [{'flag': 'test'}]})
315 impl(in_proto, None, self.api_config)
Alex Klein2b236722019-06-19 15:44:26 -0600316
317 def test_mixed(self):
Alex Klein60c80522020-10-13 18:05:38 -0600318 """Test validator fails when given a set value and an unset value."""
Alex Klein2b236722019-06-19 15:44:26 -0600319
320 @validate.require('path', 'env.use_flags')
Alex Klein2008aee2019-08-20 16:25:27 -0600321 def impl(_input_proto, _output_proto, _config):
Alex Klein2b236722019-06-19 15:44:26 -0600322 pass
323
324 with self.assertRaises(cros_build_lib.DieSystemExit):
Alex Klein2008aee2019-08-20 16:25:27 -0600325 impl(common_pb2.Chroot(path='/chroot/path'), None, self.api_config)
326
327 def test_skip_validation(self):
328 """Test skipping validation case."""
329 @validate.require('path', 'env.use_flags')
330 def impl(_input_proto, _output_proto, _config):
331 pass
332
333 # This would otherwise raise an error for an invalid path.
334 impl(common_pb2.Chroot(), None, self.no_validate_config)
Alex Klein69339cc2019-07-22 14:08:35 -0600335
336
Alex Klein60c80522020-10-13 18:05:38 -0600337class RequireAnyTest(cros_test_lib.TestCase, api_config.ApiConfigMixin):
338 """Tests for the require_any validator."""
339
340 def _get_request(self, mid: int = None, name: str = None, flag: bool = None):
341 """Build a request instance from the given data."""
342 request = build_api_test_pb2.MultiFieldMessage()
343
344 if mid:
345 request.id = mid
346 if name:
347 request.name = name
348 if flag:
349 request.flag = flag
350
351 return request
352
353 def test_invalid_field(self):
354 """Test validator fails when given an invalid field."""
355
356 @validate.require_any('does.not.exist', 'also.invalid')
357 def impl(_input_proto, _output_proto, _config):
358 self.fail('Incorrectly allowed method to execute.')
359
360 with self.assertRaises(cros_build_lib.DieSystemExit):
361 impl(self._get_request(), None, self.api_config)
362
363 def test_not_set(self):
364 """Test validator fails when given unset values."""
365
366 @validate.require_any('id', 'name')
367 def impl(_input_proto, _output_proto, _config):
368 self.fail('Incorrectly allowed method to execute.')
369
370 with self.assertRaises(cros_build_lib.DieSystemExit):
371 impl(self._get_request(flag=True), None, self.api_config)
372
373 def test_set(self):
374 """Test validator passes when given set values."""
375
376 @validate.require_any('id', 'name')
377 def impl(_input_proto, _output_proto, _config):
378 pass
379
380 impl(self._get_request(1), None, self.api_config)
381 impl(self._get_request(name='foo'), None, self.api_config)
382 impl(self._get_request(1, name='foo'), None, self.api_config)
383
384
Alex Klein86242bf2020-09-22 15:23:23 -0600385class RequireEachTest(cros_test_lib.TestCase, api_config.ApiConfigMixin):
386 """Tests for the require_each validator."""
387
388 def _multi_field_message(self, msg_id=None, name=None, flag=None):
389 msg = build_api_test_pb2.MultiFieldMessage()
390 if msg_id is not None:
391 msg.id = int(msg_id)
392 if name is not None:
393 msg.name = str(name)
394 if flag is not None:
395 msg.flag = bool(flag)
396 return msg
397
398 def _request(self, messages=None, count=0):
399 """Build the request."""
400 if messages is None:
401 messages = [self._multi_field_message() for _ in range(count)]
402
403 request = build_api_test_pb2.TestRequestMessage()
404 for message in messages:
405 msg = request.messages.add()
406 msg.CopyFrom(message)
407
408 return request
409
410 def test_invalid_field(self):
411 """Test validator fails when given an invalid field."""
412
413 @validate.require_each('does.not', ['exist'])
414 def impl(_input_proto, _output_proto, _config):
415 self.fail('Incorrectly allowed method to execute.')
416
417 with self.assertRaises(cros_build_lib.DieSystemExit):
418 impl(self._request(), None, self.api_config)
419
420 def test_invalid_call_no_subfields(self):
421 """Test validator fails when given no subfields."""
422
423 with self.assertRaises(AssertionError):
424 @validate.require_each('does.not', [])
425 def _(_input_proto, _output_proto, _config):
426 pass
427
428 def test_invalid_call_invalid_subfields(self):
429 """Test validator fails when given subfields incorrectly."""
430
431 with self.assertRaises(AssertionError):
432 @validate.require_each('does.not', 'exist')
433 def _(_input_proto, _output_proto, _config):
434 pass
435
436 def test_not_set(self):
437 """Test validator fails when given an unset value."""
438
439 @validate.require_each('messages', ['id'])
440 def impl(_input_proto, _output_proto, _config):
441 self.fail('Incorrectly allowed method to execute.')
442
443 with self.assertRaises(cros_build_lib.DieSystemExit):
444 impl(self._request(count=2), None, self.api_config)
445
446 def test_no_elements_success(self):
447 """Test validator fails when given no messages in the repeated field."""
448
449 @validate.require_each('messages', ['id'])
450 def impl(_input_proto, _output_proto, _config):
451 pass
452
453 impl(self._request(), None, self.api_config)
454
455 def test_no_elements_failure(self):
456 """Test validator fails when given no messages in the repeated field."""
457
458 @validate.require_each('messages', ['id'], allow_empty=False)
459 def impl(_input_proto, _output_proto, _config):
460 self.fail('Incorrectly allowed method to execute.')
461
462 with self.assertRaises(cros_build_lib.DieSystemExit):
463 impl(self._request(), None, self.api_config)
464
465 def test_set(self):
466 """Test validator passes when given set values."""
467
468 @validate.require_each('messages', ['id'])
469 def impl(_input_proto, _output_proto, _config):
470 pass
471
472 messages = [self._multi_field_message(msg_id=i) for i in range(1, 5)]
473 impl(self._request(messages=messages), None, self.api_config)
474
475 def test_one_set_fails(self):
476 """Test validator passes when given set values."""
477
478 @validate.require_each('messages', ['id', 'name'])
479 def impl(_input_proto, _output_proto, _config):
480 pass
481
482 messages = [self._multi_field_message(msg_id=i) for i in range(1, 5)]
483 with self.assertRaises(cros_build_lib.DieSystemExit):
484 impl(self._request(messages=messages), None, self.api_config)
485
486 def test_multi_set(self):
487 """Test validator passes when all values set."""
488
489 @validate.require_each('messages', ['id', 'name'])
490 def impl(_input_proto, _output_proto, _config):
491 pass
492
493 messages = [self._multi_field_message(msg_id=i, name=i)
494 for i in range(1, 5)]
495 impl(self._request(messages=messages), None, self.api_config)
496
497 def test_skip_validation(self):
498 """Test skipping validation case."""
499 @validate.require_each('messages', ['id'], allow_empty=False)
500 def impl(_input_proto, _output_proto, _config):
501 pass
502
503 impl(self._request(), None, self.no_validate_config)
504
505
Alex Klein69339cc2019-07-22 14:08:35 -0600506class ValidateOnlyTest(cros_test_lib.TestCase, api_config.ApiConfigMixin):
507 """validate_only decorator tests."""
508
509 def test_validate_only(self):
510 """Test validate only."""
511 @validate.require('path')
512 @validate.validation_complete
513 def impl(_input_proto, _output_proto, _config):
514 self.fail('Implementation was called.')
515 return 1
516
517 # Just using arbitrary messages, we just need the
518 # (request, response, config) arguments so it can check the config.
519 rc = impl(common_pb2.Chroot(path='/chroot/path'), common_pb2.Chroot(),
520 self.validate_only_config)
521
522 self.assertEqual(0, rc)
523
524 def test_no_validate_only(self):
525 """Test no use of validate only."""
526 @validate.validation_complete
527 def impl(_input_proto, _output_proto, _config):
Alex Klein2008aee2019-08-20 16:25:27 -0600528 self.fail('Incorrectly allowed method to execute.')
Alex Klein69339cc2019-07-22 14:08:35 -0600529
530 # We will get an assertion error unless validate_only prevents the function
531 # from being called.
532 with self.assertRaises(AssertionError):
533 impl(common_pb2.Chroot(), common_pb2.Chroot(), self.api_config)