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

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. 

15 

16from http import client as http 

17import re 

18 

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 

24 

25 

26class FakeRequest(object): 

27 api_version_request = api_version.APIVersionRequest("3.0") 

28 environ = {} 

29 

30 

31class APIValidationTestCase(test.TestCase): 

32 def setUp(self, schema=None): 

33 super().setUp() 

34 self.post = None 

35 

36 if schema is not None: 36 ↛ exitline 36 didn't return from function 'setUp' because the condition on line 36 was always true

37 

38 @validation.request_body_schema(schema=schema) 

39 def post(req, body): 

40 return 'Validation succeeded.' 

41 

42 self.post = post 

43 

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.') 

70 

71 

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) 

83 

84 def test_validate_required_disable(self): 

85 self.assertEqual( 

86 'Validation succeeded.', 

87 self.post(body={'foo': 1}, req=FakeRequest()), 

88 ) 

89 

90 

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) 

103 

104 def test_validate_required_enable(self): 

105 self.assertEqual( 

106 'Validation succeeded.', 

107 self.post(body={'foo': 1}, req=FakeRequest()), 

108 ) 

109 

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 ) 

115 

116 

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) 

129 

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 ) 

139 

140 

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) 

154 

155 def test_validate_additionalProperties_disable(self): 

156 self.assertEqual( 

157 'Validation succeeded.', 

158 self.post(body={'foo': 1}, req=FakeRequest()), 

159 ) 

160 

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 ) 

166 

167 

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) 

177 

178 def test_validate_patternProperties(self): 

179 self.assertEqual( 

180 'Validation succeeded.', 

181 self.post(body={'foo': 'bar'}, req=FakeRequest()), 

182 ) 

183 

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 ) 

192 

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 ) 

200 

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 ) 

214 

215 detail = "expected string or bytes-like object" 

216 self.check_validation_error( 

217 self.post, body={None: 'bar'}, expected_detail=detail 

218 ) 

219 

220 

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) 

232 

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 ) 

246 

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 ) 

255 

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 ) 

263 

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 ) 

271 

272 

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) 

286 

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 ) 

296 

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 ) 

306 

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 ) 

314 

315 

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) 

328 

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 ) 

342 

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 ) 

351 

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 ) 

359 

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 ) 

367 

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 ) 

375 

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 ) 

383 

384 

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) 

399 

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 ) 

413 

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 ) 

422 

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 ) 

430 

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 ) 

438 

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 ) 

446 

447 

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) 

457 

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 ) 

483 

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 ) 

491 

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 ) 

499 

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 ) 

507 

508 

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) 

521 

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 ) 

531 

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 ) 

540 

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 )