Coverage for manila/tests/api/v2/test_resource_locks.py: 100%

136 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2026-02-18 22:19 +0000

1# Licensed under the Apache License, Version 2.0 (the "License"); you may 

2# not use this file except in compliance with the License. You may obtain 

3# a copy of the License at 

4# 

5# http://www.apache.org/licenses/LICENSE-2.0 

6# 

7# Unless required by applicable law or agreed to in writing, software 

8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 

9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 

10# License for the specific language governing permissions and limitations 

11# under the License. 

12 

13import copy 

14from unittest import mock 

15 

16import ddt 

17from oslo_config import cfg 

18from oslo_utils import uuidutils 

19import webob 

20 

21from manila.api.v2 import resource_locks 

22from manila import context 

23from manila import exception 

24from manila import policy 

25from manila import test 

26from manila.tests.api import fakes 

27from manila.tests.api.v2 import stubs 

28from manila.tests import utils as test_utils 

29from manila import utils 

30 

31CONF = cfg.CONF 

32 

33 

34@ddt.ddt 

35class ResourceLockApiTest(test.TestCase): 

36 def setUp(self): 

37 super(ResourceLockApiTest, self).setUp() 

38 self.controller = resource_locks.ResourceLocksController() 

39 self.maxDiff = None 

40 self.ctxt = context.RequestContext('demo', 'fake', False) 

41 self.req = fakes.HTTPRequest.blank( 

42 '/resource-locks', 

43 version=resource_locks.RESOURCE_LOCKS_MIN_API_VERSION 

44 ) 

45 self.mock_object( 

46 policy, 'check_policy', mock.Mock(return_value=True) 

47 ) 

48 

49 @ddt.data( 

50 test_utils.annotated( 

51 'no_body_content', { 

52 'body': {}, 

53 'resource_type': 'share' 

54 } 

55 ), 

56 test_utils.annotated( 

57 'invalid_body', { 

58 'body': { 

59 'share': 'somedata' 

60 }, 

61 'resource_type': 'share' 

62 } 

63 ), 

64 test_utils.annotated( 

65 'invalid_action', { 

66 'body': { 

67 'resource_lock': { 

68 'resource_action': 'invalid_action', 

69 } 

70 }, 

71 'resource_type': 'share' 

72 }, 

73 ), 

74 test_utils.annotated( 

75 'invalid_reason', { 

76 'body': { 

77 'resource_lock': { 

78 'lock_reason': 'xyzzyspoon!' * 94, 

79 } 

80 }, 

81 'resource_type': 'share' 

82 }, 

83 ), 

84 test_utils.annotated( 

85 'disallowed_attributes', { 

86 'body': { 

87 'resource_lock': { 

88 'lock_reason': 'the reason is you', 

89 'resource_action': 'delete', 

90 'resource_id': uuidutils.generate_uuid(), 

91 } 

92 }, 

93 'resource_type': 'share' 

94 }, 

95 ), 

96 ) 

97 @ddt.unpack 

98 def test__check_body_for_update_invalid(self, body, resource_type): 

99 current_lock = {'resource_type': resource_type} 

100 self.assertRaises(webob.exc.HTTPBadRequest, 

101 self.controller._check_body, 

102 body, 

103 lock_to_update=current_lock) 

104 

105 @ddt.data( 

106 test_utils.annotated('no_body_content', {}), 

107 test_utils.annotated('invalid_body', {'share': 'somedata'}), 

108 test_utils.annotated( 

109 'invalid_action', { 

110 'resource_lock': { 

111 'resource_action': 'invalid_action', 

112 }, 

113 }, 

114 ), 

115 test_utils.annotated( 

116 'invalid_reason', { 

117 'resource_lock': { 

118 'lock_reason': 'xyzzyspoon!' * 94, 

119 }, 

120 }, 

121 ), 

122 test_utils.annotated( 

123 'invalid_resource_id', { 

124 'resource_lock': { 

125 'resource_id': 'invalid-id', 

126 'resource_action': 'delete', 

127 }, 

128 }, 

129 ), 

130 test_utils.annotated( 

131 'invalid_resource_type', { 

132 'resource_lock': { 

133 'resource_id': uuidutils.generate_uuid(), 

134 'resource_type': 'invalid-resource-type', 

135 }, 

136 }, 

137 ), 

138 ) 

139 def test__check_body_for_create_invalid(self, body): 

140 self.assertRaises(webob.exc.HTTPBadRequest, 

141 self.controller._check_body, 

142 body) 

143 

144 @ddt.data( 

145 test_utils.annotated( 

146 'action_and_lock_reason', { 

147 'body': { 

148 'resource_lock': { 

149 'resource_action': 'delete', 

150 'lock_reason': 'the reason is you', 

151 } 

152 }, 

153 'resource_type': 'share', 

154 }, 

155 ), 

156 test_utils.annotated( 

157 'lock_reason', { 

158 'body': { 

159 'resource_lock': { 

160 'lock_reason': 'tienes razon', 

161 } 

162 }, 

163 'resource_type': 'share', 

164 }, 

165 ), 

166 test_utils.annotated( 

167 'resource_action', { 

168 'body': { 

169 'resource_lock': { 

170 'resource_action': 'delete', 

171 } 

172 }, 

173 'resource_type': 'access_rule', 

174 }, 

175 ), 

176 ) 

177 @ddt.unpack 

178 def test__check_body_for_update(self, body, resource_type): 

179 current_lock = copy.copy(body['resource_lock']) 

180 current_lock['resource_type'] = resource_type 

181 

182 result = self.controller._check_body( 

183 body, lock_to_update=current_lock) 

184 

185 self.assertIsNone(result) 

186 

187 def test__check_body_for_create(self): 

188 body = { 

189 'resource_lock': { 

190 'resource_id': uuidutils.generate_uuid(), 

191 'resource_type': 'share', 

192 }, 

193 } 

194 

195 result = self.controller._check_body(body) 

196 

197 self.assertIsNone(result) 

198 

199 @ddt.data({'created_since': None, 'created_before': None}, 

200 {'created_since': '2222-22-22', 'created_before': 'a_year_ago'}, 

201 {'created_since': 'epoch'}, 

202 {'created_before': 'december'}) 

203 def test_index_invalid_time_filters(self, filters): 

204 url = '/resource-locks?' 

205 for key, value in filters.items(): 

206 url += f'{key}={value}&' 

207 url.rstrip('&') 

208 req = fakes.HTTPRequest.blank( 

209 url, version=resource_locks.RESOURCE_LOCKS_MIN_API_VERSION) 

210 req.environ['manila.context'] = self.ctxt 

211 

212 self.assertRaises(exception.ValidationError, 

213 self.controller.index, 

214 req) 

215 

216 @ddt.data({'limit': 'a', 'offset': 'test'}, 

217 {'limit': -1}, 

218 {'with_count': 'oh-noes', 'limit': 0}) 

219 def test_index_invalid_pagination(self, filters): 

220 url = '/resource-locks?' 

221 for key, value in filters.items(): 

222 url += f'{key}={value}&' 

223 url.rstrip('&') 

224 

225 req = fakes.HTTPRequest.blank( 

226 url, version=resource_locks.RESOURCE_LOCKS_MIN_API_VERSION) 

227 req.environ['manila.context'] = self.ctxt 

228 

229 self.assertRaises(exception.ValidationError, 

230 self.controller.index, 

231 req) 

232 

233 def test_index(self): 

234 url = ('/resource-locks?sort_dir=asc&sort_key=resource_id&limit=3' 

235 '&offset=1&project_id=f63f7a159f404cfc8604b7065c609691' 

236 '&with_count=1') 

237 req = fakes.HTTPRequest.blank( 

238 url, version=resource_locks.RESOURCE_LOCKS_MIN_API_VERSION) 

239 locks = [ 

240 stubs.stub_lock('68e2e33d-0f0c-49b7-aee3-f0696ab90360'), 

241 stubs.stub_lock('93748a9f-6dfe-4baf-ad4c-b9c82d6063ef'), 

242 stubs.stub_lock('44f8dd68-2eeb-41df-b5d1-9e7654212527'), 

243 ] 

244 self.mock_object(self.controller.resource_locks_api, 

245 'get_all', 

246 mock.Mock(return_value=(locks, 3))) 

247 

248 actual_locks = self.controller.index(req) 

249 

250 expected_filters = { 

251 'project_id': 'f63f7a159f404cfc8604b7065c609691', 

252 } 

253 self.controller.resource_locks_api.get_all.assert_called_once_with( 

254 utils.IsAMatcher(context.RequestContext), 

255 search_opts=mock.ANY, 

256 limit=3, 

257 offset=1, 

258 sort_key='resource_id', 

259 sort_dir='asc', 

260 show_count=True, 

261 ) 

262 # webob uses a "MultiDict" for request params 

263 actual_filters = {} 

264 call_args = self.controller.resource_locks_api.get_all.call_args[1] 

265 search_opts = call_args['search_opts'] 

266 for key, value in search_opts.dict_of_lists().items(): 

267 actual_filters[key] = value[0] 

268 

269 self.assertEqual(expected_filters, actual_filters) 

270 self.assertEqual(3, len(actual_locks['resource_locks'])) 

271 for lock in actual_locks['resource_locks']: 

272 for key in locks[0].keys(): 

273 self.assertIn(key, lock) 

274 self.assertIn('links', lock) 

275 self.assertIn('resource_locks_links', actual_locks) 

276 self.assertEqual(3, actual_locks['count']) 

277 

278 def test_show_not_found(self): 

279 url = '/resource-locks/fake-lock-id' 

280 req = fakes.HTTPRequest.blank( 

281 url, version=resource_locks.RESOURCE_LOCKS_MIN_API_VERSION) 

282 self.mock_object( 

283 self.controller.resource_locks_api, 'get', 

284 mock.Mock(side_effect=exception.ResourceLockNotFound(lock_id='1'))) 

285 self.assertRaises(webob.exc.HTTPNotFound, 

286 self.controller.show, 

287 req, 

288 'fake-lock-id') 

289 

290 def test_show(self): 

291 url = '/resource-locks/c6aef27b-f583-48c7-aac1-bd8fb570ce16' 

292 req = fakes.HTTPRequest.blank( 

293 url, version=resource_locks.RESOURCE_LOCKS_MIN_API_VERSION) 

294 expected_lock = stubs.stub_lock( 

295 'c6aef27b-f583-48c7-aac1-bd8fb570ce16' 

296 ) 

297 self.mock_object( 

298 self.controller.resource_locks_api, 

299 'get', 

300 mock.Mock(return_value=expected_lock) 

301 ) 

302 

303 actual_lock = self.controller.show( 

304 req, 'c6aef27b-f583-48c7-aac1-bd8fb570ce16') 

305 self.assertSubDictMatch(expected_lock, actual_lock['resource_lock']) 

306 self.assertIn('links', actual_lock['resource_lock']) 

307 

308 def test_delete_not_found(self): 

309 url = '/resource-locks/fake-lock-id' 

310 req = fakes.HTTPRequest.blank( 

311 url, version=resource_locks.RESOURCE_LOCKS_MIN_API_VERSION) 

312 self.mock_object( 

313 self.controller.resource_locks_api, 

314 'delete', 

315 mock.Mock(side_effect=exception.ResourceLockNotFound(lock_id='1')), 

316 ) 

317 self.assertRaises(webob.exc.HTTPNotFound, 

318 self.controller.delete, 

319 req, 

320 'fake-lock-id') 

321 

322 def test_delete(self): 

323 url = '/resource-locks/c6aef27b-f583-48c7-aac1-bd8fb570ce16' 

324 req = fakes.HTTPRequest.blank( 

325 url, version=resource_locks.RESOURCE_LOCKS_MIN_API_VERSION) 

326 self.mock_object(self.controller.resource_locks_api, 'delete') 

327 

328 result = self.controller.delete(req, 

329 'c6aef27b-f583-48c7-aac1-bd8fb570ce16') 

330 self.assertEqual(204, result.status_int) 

331 

332 def test_create_no_such_resource(self): 

333 self.mock_object(self.controller, '_check_body') 

334 body = { 

335 'resource_lock': { 

336 'resource_id': '27e14086-16e1-445b-ad32-b2ebb07225a8', 

337 'resource_type': 'share', 

338 }, 

339 } 

340 self.mock_object(self.controller.resource_locks_api, 

341 'create', 

342 mock.Mock(side_effect=exception.NotFound)) 

343 self.assertRaises(webob.exc.HTTPBadRequest, 

344 self.controller.create, 

345 self.req, 

346 body=body) 

347 

348 def test_create_visibility_already_locked(self): 

349 self.mock_object(self.controller, '_check_body') 

350 resource_id = '27e14086-16e1-445b-ad32-b2ebb07225a8' 

351 body = { 

352 'resource_lock': { 

353 'resource_id': resource_id, 

354 'resource_type': 'share', 

355 }, 

356 } 

357 self.mock_object( 

358 self.controller.resource_locks_api, 

359 'create', 

360 mock.Mock( 

361 side_effect=exception.ResourceVisibilityLockExists( 

362 resource_id=resource_id)) 

363 ) 

364 self.assertRaises(webob.exc.HTTPConflict, 

365 self.controller.create, 

366 self.req, 

367 body=body) 

368 

369 def test_create(self): 

370 self.mock_object(self.controller, '_check_body') 

371 expected_lock = stubs.stub_lock( 

372 '04512dae-18c2-45b5-bbab-50b775ba6f1d', 

373 lock_reason=None, 

374 ) 

375 body = { 

376 'resource_lock': { 

377 'resource_id': expected_lock['resource_id'], 

378 'resource_type': expected_lock['resource_type'], 

379 }, 

380 } 

381 self.mock_object(self.controller.resource_locks_api, 

382 'create', 

383 mock.Mock(return_value=expected_lock)) 

384 

385 actual_lock = self.controller.create( 

386 self.req, body=body 

387 )['resource_lock'] 

388 

389 self.controller.resource_locks_api.create.assert_called_once_with( 

390 utils.IsAMatcher(context.RequestContext), 

391 resource_id=expected_lock['resource_id'], 

392 resource_type=expected_lock['resource_type'], 

393 resource_action='delete', 

394 lock_reason=None, 

395 ) 

396 self.assertSubDictMatch(expected_lock, actual_lock) 

397 self.assertIn('links', actual_lock) 

398 

399 def test_update(self): 

400 expected_lock = stubs.stub_lock( 

401 '04512dae-18c2-45b5-bbab-50b775ba6f1d', 

402 lock_reason=None, 

403 ) 

404 self.mock_object(self.controller, '_check_body') 

405 self.mock_object(self.controller.resource_locks_api, 'get', 

406 mock.Mock(return_value=expected_lock)) 

407 self.mock_object(self.controller.resource_locks_api, 

408 'update', 

409 mock.Mock(return_value=expected_lock)) 

410 

411 body = { 

412 'resource_lock': { 

413 'lock_reason': None 

414 }, 

415 } 

416 

417 actual_lock = self.controller.update( 

418 self.req, 

419 '04512dae-18c2-45b5-bbab-50b775ba6f1d', 

420 body=body 

421 )['resource_lock'] 

422 

423 self.controller.resource_locks_api.update.assert_called_once_with( 

424 utils.IsAMatcher(context.RequestContext), 

425 expected_lock, 

426 {'lock_reason': None} 

427 ) 

428 self.assertSubDictMatch(expected_lock, actual_lock)