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