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
« 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.
13import copy
14from unittest import mock
16import ddt
17from oslo_config import cfg
18from oslo_utils import uuidutils
19import webob
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
31CONF = cfg.CONF
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 )
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)
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)
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
182 result = self.controller._check_body(
183 body, lock_to_update=current_lock)
185 self.assertIsNone(result)
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 }
195 result = self.controller._check_body(body)
197 self.assertIsNone(result)
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
212 self.assertRaises(exception.ValidationError,
213 self.controller.index,
214 req)
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('&')
225 req = fakes.HTTPRequest.blank(
226 url, version=resource_locks.RESOURCE_LOCKS_MIN_API_VERSION)
227 req.environ['manila.context'] = self.ctxt
229 self.assertRaises(exception.ValidationError,
230 self.controller.index,
231 req)
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)))
248 actual_locks = self.controller.index(req)
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]
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'])
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')
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 )
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'])
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')
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')
328 result = self.controller.delete(req,
329 'c6aef27b-f583-48c7-aac1-bd8fb570ce16')
330 self.assertEqual(204, result.status_int)
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)
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)
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))
385 actual_lock = self.controller.create(
386 self.req, body=body
387 )['resource_lock']
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)
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))
411 body = {
412 'resource_lock': {
413 'lock_reason': None
414 },
415 }
417 actual_lock = self.controller.update(
418 self.req,
419 '04512dae-18c2-45b5-bbab-50b775ba6f1d',
420 body=body
421 )['resource_lock']
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)