Coverage for manila/tests/api/test_validation.py: 97%
171 statements
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
1# Copyright (C) 2017 NTT DATA
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.
16from http import client as http
17import re
19from manila.api.openstack import api_version_request as api_version
20from manila.api import validation
21from manila.api.validation import parameter_types
22from manila import exception
23from manila import test
26class FakeRequest(object):
27 api_version_request = api_version.APIVersionRequest("3.0")
28 environ = {}
31class APIValidationTestCase(test.TestCase):
32 def setUp(self, schema=None):
33 super().setUp()
34 self.post = None
36 if schema is not None: 36 ↛ exitline 36 didn't return from function 'setUp' because the condition on line 36 was always true
38 @validation.request_body_schema(schema=schema)
39 def post(req, body):
40 return 'Validation succeeded.'
42 self.post = post
44 def check_validation_error(self, method, body, expected_detail, req=None):
45 if not req: 45 ↛ 47line 45 didn't jump to line 47 because the condition on line 45 was always true
46 req = FakeRequest()
47 try:
48 method(
49 body=body,
50 req=req,
51 )
52 except exception.ValidationError as ex:
53 self.assertEqual(http.BAD_REQUEST, ex.kwargs['code'])
54 if isinstance(expected_detail, list):
55 self.assertIn(
56 ex.kwargs['detail'],
57 expected_detail,
58 'Exception details did not match expected',
59 )
60 elif not re.match(expected_detail, ex.kwargs['detail']):
61 self.assertEqual(
62 expected_detail,
63 ex.kwargs['detail'],
64 'Exception details did not match expected',
65 )
66 except Exception as ex:
67 self.fail('An unexpected exception happens: %s' % ex)
68 else:
69 self.fail('Any exception did not happen.')
72class RequiredDisableTestCase(APIValidationTestCase):
73 def setUp(self):
74 schema = {
75 'type': 'object',
76 'properties': {
77 'foo': {
78 'type': 'integer',
79 },
80 },
81 }
82 super().setUp(schema=schema)
84 def test_validate_required_disable(self):
85 self.assertEqual(
86 'Validation succeeded.',
87 self.post(body={'foo': 1}, req=FakeRequest()),
88 )
91class RequiredEnableTestCase(APIValidationTestCase):
92 def setUp(self):
93 schema = {
94 'type': 'object',
95 'properties': {
96 'foo': {
97 'type': 'integer',
98 },
99 },
100 'required': ['foo'],
101 }
102 super().setUp(schema=schema)
104 def test_validate_required_enable(self):
105 self.assertEqual(
106 'Validation succeeded.',
107 self.post(body={'foo': 1}, req=FakeRequest()),
108 )
110 def test_validate_required_enable_fails(self):
111 detail = "'foo' is a required property"
112 self.check_validation_error(
113 self.post, body={'abc': 1}, expected_detail=detail
114 )
117class AdditionalPropertiesEnableTestCase(APIValidationTestCase):
118 def setUp(self):
119 schema = {
120 'type': 'object',
121 'properties': {
122 'foo': {
123 'type': 'integer',
124 },
125 },
126 'required': ['foo'],
127 }
128 super().setUp(schema=schema)
130 def test_validate_additionalProperties_enable(self):
131 self.assertEqual(
132 'Validation succeeded.',
133 self.post(body={'foo': 1}, req=FakeRequest()),
134 )
135 self.assertEqual(
136 'Validation succeeded.',
137 self.post(body={'foo': 1, 'ext': 1}, req=FakeRequest()),
138 )
141class AdditionalPropertiesDisableTestCase(APIValidationTestCase):
142 def setUp(self):
143 schema = {
144 'type': 'object',
145 'properties': {
146 'foo': {
147 'type': 'integer',
148 },
149 },
150 'required': ['foo'],
151 'additionalProperties': False,
152 }
153 super().setUp(schema=schema)
155 def test_validate_additionalProperties_disable(self):
156 self.assertEqual(
157 'Validation succeeded.',
158 self.post(body={'foo': 1}, req=FakeRequest()),
159 )
161 def test_validate_additionalProperties_disable_fails(self):
162 detail = "Additional properties are not allowed ('ext' was unexpected)"
163 self.check_validation_error(
164 self.post, body={'foo': 1, 'ext': 1}, expected_detail=detail
165 )
168class PatternPropertiesTestCase(APIValidationTestCase):
169 def setUp(self):
170 schema = {
171 'patternProperties': {
172 '^[a-zA-Z0-9]{1,10}$': {'type': 'string'},
173 },
174 'additionalProperties': False,
175 }
176 super().setUp(schema=schema)
178 def test_validate_patternProperties(self):
179 self.assertEqual(
180 'Validation succeeded.',
181 self.post(body={'foo': 'bar'}, req=FakeRequest()),
182 )
184 def test_validate_patternProperties_fails(self):
185 details = [
186 "Additional properties are not allowed ('__' was unexpected)",
187 "'__' does not match any of the regexes: '^[a-zA-Z0-9]{1,10}$'",
188 ]
189 self.check_validation_error(
190 self.post, body={'__': 'bar'}, expected_detail=details
191 )
193 details = [
194 "'' does not match any of the regexes: '^[a-zA-Z0-9]{1,10}$'",
195 "Additional properties are not allowed ('' was unexpected)",
196 ]
197 self.check_validation_error(
198 self.post, body={'': 'bar'}, expected_detail=details
199 )
201 details = [
202 (
203 "'0123456789a' does not match any of the regexes: "
204 "'^[a-zA-Z0-9]{1,10}$'"
205 ),
206 (
207 "Additional properties are not allowed ('0123456789a' was "
208 "unexpected)"
209 ),
210 ]
211 self.check_validation_error(
212 self.post, body={'0123456789a': 'bar'}, expected_detail=details
213 )
215 detail = "expected string or bytes-like object"
216 self.check_validation_error(
217 self.post, body={None: 'bar'}, expected_detail=detail
218 )
221class StringTestCase(APIValidationTestCase):
222 def setUp(self):
223 schema = {
224 'type': 'object',
225 'properties': {
226 'foo': {
227 'type': 'string',
228 },
229 },
230 }
231 super().setUp(schema=schema)
233 def test_validate_string(self):
234 self.assertEqual(
235 'Validation succeeded.',
236 self.post(body={'foo': 'abc'}, req=FakeRequest()),
237 )
238 self.assertEqual(
239 'Validation succeeded.',
240 self.post(body={'foo': '0'}, req=FakeRequest()),
241 )
242 self.assertEqual(
243 'Validation succeeded.',
244 self.post(body={'foo': ''}, req=FakeRequest()),
245 )
247 def test_validate_string_fails(self):
248 detail = (
249 "Invalid input for field/attribute foo. Value: 1. "
250 "1 is not of type 'string'"
251 )
252 self.check_validation_error(
253 self.post, body={'foo': 1}, expected_detail=detail
254 )
256 detail = (
257 "Invalid input for field/attribute foo. Value: 1.5. "
258 "1.5 is not of type 'string'"
259 )
260 self.check_validation_error(
261 self.post, body={'foo': 1.5}, expected_detail=detail
262 )
264 detail = (
265 "Invalid input for field/attribute foo. Value: True. "
266 "True is not of type 'string'"
267 )
268 self.check_validation_error(
269 self.post, body={'foo': True}, expected_detail=detail
270 )
273class StringLengthTestCase(APIValidationTestCase):
274 def setUp(self):
275 schema = {
276 'type': 'object',
277 'properties': {
278 'foo': {
279 'type': 'string',
280 'minLength': 1,
281 'maxLength': 10,
282 },
283 },
284 }
285 super().setUp(schema=schema)
287 def test_validate_string_length(self):
288 self.assertEqual(
289 'Validation succeeded.',
290 self.post(body={'foo': '0'}, req=FakeRequest()),
291 )
292 self.assertEqual(
293 'Validation succeeded.',
294 self.post(body={'foo': '0123456789'}, req=FakeRequest()),
295 )
297 def test_validate_string_length_fails(self):
298 # checks for jsonschema output from 3.2.x and 4.21.x
299 detail = (
300 "Invalid input for field/attribute foo. Value: . "
301 "'' (is too short|should be non-empty)"
302 )
303 self.check_validation_error(
304 self.post, body={'foo': ''}, expected_detail=detail
305 )
307 detail = (
308 "Invalid input for field/attribute foo. Value: 0123456789a. "
309 "'0123456789a' is too long"
310 )
311 self.check_validation_error(
312 self.post, body={'foo': '0123456789a'}, expected_detail=detail
313 )
316class IntegerTestCase(APIValidationTestCase):
317 def setUp(self):
318 schema = {
319 'type': 'object',
320 'properties': {
321 'foo': {
322 'type': ['integer', 'string'],
323 'pattern': '^[0-9]+$',
324 },
325 },
326 }
327 super().setUp(schema=schema)
329 def test_validate_integer(self):
330 self.assertEqual(
331 'Validation succeeded.',
332 self.post(body={'foo': 1}, req=FakeRequest()),
333 )
334 self.assertEqual(
335 'Validation succeeded.',
336 self.post(body={'foo': '1'}, req=FakeRequest()),
337 )
338 self.assertEqual(
339 'Validation succeeded.',
340 self.post(body={'foo': '0123456789'}, req=FakeRequest()),
341 )
343 def test_validate_integer_fails(self):
344 detail = (
345 "Invalid input for field/attribute foo. Value: abc. "
346 "'abc' does not match '^[0-9]+$'"
347 )
348 self.check_validation_error(
349 self.post, body={'foo': 'abc'}, expected_detail=detail
350 )
352 detail = (
353 "Invalid input for field/attribute foo. Value: True. "
354 "True is not of type 'integer', 'string'"
355 )
356 self.check_validation_error(
357 self.post, body={'foo': True}, expected_detail=detail
358 )
360 detail = (
361 "Invalid input for field/attribute foo. Value: 0xffff. "
362 "'0xffff' does not match '^[0-9]+$'"
363 )
364 self.check_validation_error(
365 self.post, body={'foo': '0xffff'}, expected_detail=detail
366 )
368 detail = (
369 "Invalid input for field/attribute foo. Value: 1.01. "
370 "1.01 is not of type 'integer', 'string'"
371 )
372 self.check_validation_error(
373 self.post, body={'foo': 1.01}, expected_detail=detail
374 )
376 detail = (
377 "Invalid input for field/attribute foo. Value: 1.0. "
378 "'1.0' does not match '^[0-9]+$'"
379 )
380 self.check_validation_error(
381 self.post, body={'foo': '1.0'}, expected_detail=detail
382 )
385class IntegerRangeTestCase(APIValidationTestCase):
386 def setUp(self):
387 schema = {
388 'type': 'object',
389 'properties': {
390 'foo': {
391 'type': ['integer', 'string'],
392 'pattern': '^[0-9]+$',
393 'minimum': 1,
394 'maximum': 10,
395 },
396 },
397 }
398 super().setUp(schema=schema)
400 def test_validate_integer_range(self):
401 self.assertEqual(
402 'Validation succeeded.',
403 self.post(body={'foo': 1}, req=FakeRequest()),
404 )
405 self.assertEqual(
406 'Validation succeeded.',
407 self.post(body={'foo': 10}, req=FakeRequest()),
408 )
409 self.assertEqual(
410 'Validation succeeded.',
411 self.post(body={'foo': '1'}, req=FakeRequest()),
412 )
414 def test_validate_integer_range_fails(self):
415 detail = (
416 "Invalid input for field/attribute foo. Value: 0. "
417 "0(.0)? is less than the minimum of 1"
418 )
419 self.check_validation_error(
420 self.post, body={'foo': 0}, expected_detail=detail
421 )
423 detail = (
424 "Invalid input for field/attribute foo. Value: 11. "
425 "11(.0)? is greater than the maximum of 10"
426 )
427 self.check_validation_error(
428 self.post, body={'foo': 11}, expected_detail=detail
429 )
431 detail = (
432 "Invalid input for field/attribute foo. Value: 0. "
433 "0(.0)? is less than the minimum of 1"
434 )
435 self.check_validation_error(
436 self.post, body={'foo': '0'}, expected_detail=detail
437 )
439 detail = (
440 "Invalid input for field/attribute foo. Value: 11. "
441 "11(.0)? is greater than the maximum of 10"
442 )
443 self.check_validation_error(
444 self.post, body={'foo': '11'}, expected_detail=detail
445 )
448class BooleanTestCase(APIValidationTestCase):
449 def setUp(self):
450 schema = {
451 'type': 'object',
452 'properties': {
453 'foo': parameter_types.boolean,
454 },
455 }
456 super().setUp(schema=schema)
458 def test_validate_boolean(self):
459 self.assertEqual(
460 'Validation succeeded.',
461 self.post(body={'foo': True}, req=FakeRequest()),
462 )
463 self.assertEqual(
464 'Validation succeeded.',
465 self.post(body={'foo': False}, req=FakeRequest()),
466 )
467 self.assertEqual(
468 'Validation succeeded.',
469 self.post(body={'foo': 'True'}, req=FakeRequest()),
470 )
471 self.assertEqual(
472 'Validation succeeded.',
473 self.post(body={'foo': 'False'}, req=FakeRequest()),
474 )
475 self.assertEqual(
476 'Validation succeeded.',
477 self.post(body={'foo': '1'}, req=FakeRequest()),
478 )
479 self.assertEqual(
480 'Validation succeeded.',
481 self.post(body={'foo': '0'}, req=FakeRequest()),
482 )
484 def test_validate_boolean_fails(self):
485 enum_boolean = (
486 "[True, 'True', 'TRUE', 'true', '1', 'ON', 'On', "
487 "'on', 'YES', 'Yes', 'yes', 'y', 't', "
488 "False, 'False', 'FALSE', 'false', '0', 'OFF', 'Off', "
489 "'off', 'NO', 'No', 'no', 'n', 'f']"
490 )
492 detail = (
493 "Invalid input for field/attribute foo. Value: bar. "
494 "'bar' is not one of %s"
495 ) % enum_boolean
496 self.check_validation_error(
497 self.post, body={'foo': 'bar'}, expected_detail=detail
498 )
500 detail = (
501 "Invalid input for field/attribute foo. Value: 2. "
502 "'2' is not one of %s"
503 ) % enum_boolean
504 self.check_validation_error(
505 self.post, body={'foo': '2'}, expected_detail=detail
506 )
509class DatetimeTestCase(APIValidationTestCase):
510 def setUp(self):
511 schema = {
512 'type': 'object',
513 'properties': {
514 'foo': {
515 'type': ['string', 'null'],
516 'format': 'date-time',
517 },
518 },
519 }
520 super().setUp(schema=schema)
522 def test_validate_datetime(self):
523 self.assertEqual(
524 'Validation succeeded.',
525 self.post(body={'foo': '2017-01-14T01:00:00Z'}, req=FakeRequest()),
526 )
527 self.assertEqual(
528 'Validation succeeded.',
529 self.post(body={'foo': None}, req=FakeRequest()),
530 )
532 def test_validate_datetime_fails(self):
533 detail = (
534 "Invalid input for field/attribute foo. Value: True. "
535 "True is not of type 'string', 'null'"
536 )
537 self.check_validation_error(
538 self.post, body={'foo': True}, expected_detail=detail
539 )
541 detail = (
542 "Invalid input for field/attribute foo. Value: 123. "
543 "'123' is not a 'date-time'"
544 )
545 self.check_validation_error(
546 self.post, body={'foo': '123'}, expected_detail=detail
547 )