Statistics
| Branch: | Tag: | Revision:

root / kamaki / clients / storage / test.py @ 3c50df2e

History | View | Annotate | Download (16.3 kB)

1
# Copyright 2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
from unittest import TestCase
35
from mock import patch, call
36
from tempfile import NamedTemporaryFile
37
from os import urandom
38

    
39
from kamaki.clients import ClientError
40
from kamaki.clients.storage import StorageClient as SC
41

    
42
client_pkg = 'kamaki.clients.Client'
43
storage_pkg = 'kamaki.clients.storage.StorageClient'
44

    
45
user_id = 'ac0un7-1d-5tr1ng'
46
obj = 'obj3c7N4m3'
47

    
48
account_info = {
49
    'content-language': 'en-us',
50
    'content-type': 'text/html; charset=utf-8',
51
    'date': 'Wed, 06 Mar 2013 13:25:51 GMT',
52
    'last-modified': 'Mon, 04 Mar 2013 18:22:31 GMT',
53
    'server': 'gunicorn/0.14.5',
54
    'vary': 'Accept-Language',
55
    'x-account-bytes-used': '751615526',
56
    'x-account-container-count': 7,
57
    'x-account-policy-quota': 53687091200,
58
    'x-account-policy-versioning': 'auto',
59
    'x-account-meta-k1': 'val1',
60
    'x-account-meta-k2': 'val2',
61
    'x-account-meta-k3': 'val3'}
62
container_info = {
63
    'content-language': 'en-us',
64
    'content-type': 'text/html; charset=utf-8',
65
    'date': 'Wed, 06 Mar 2013 15:11:05 GMT',
66
    'last-modified': 'Wed, 27 Feb 2013 15:56:13 GMT',
67
    'server': 'gunicorn/0.14.5',
68
    'vary': 'Accept-Language',
69
    'x-container-block-hash': 'sha256',
70
    'x-container-block-size': 4194304,
71
    'x-container-bytes-used': 309528938,
72
    'x-container-object-count': 14,
73
    'x-container-object-meta': '',
74
    'x-container-policy-quota': 53687091200,
75
    'x-container-policy-versioning': 'auto'}
76
object_info = {
77
    'content-language': 'en-us',
78
    'content-length': 254965,
79
    'content-type': 'application/octet-stream',
80
    'date': 'Thu, 07 Mar 2013 13:27:43 GMT',
81
    'etag': '',
82
    'last-modified': 'Mon, 04 Mar 2013 18:22:31 GMT',
83
    'server': 'gunicorn/0.14.5',
84
    'vary': 'Accept-Language',
85
    'x-object-hash': 'obj3c7h45h1s0bj3c7h45h411r34dY',
86
    'x-object-uuid': 'd0c747ca-34bd-49e0-8e98-1d07d8b0cbc7',
87
    'x-object-version': '525996',
88
    'x-object-version-timestamp': 'Mon, 04 Mar 2013 18:22:31 GMT',
89
    'x-object-meta-k1': 'v1',
90
    'x-object-meta-k2': 'v2'}
91
container_list = [
92
    dict(
93
        count=2,
94
        last_modified="2013-02-27T11:56:09.893033+00:00",
95
        bytes=677076979,
96
        name="pithos",
97
        x_container_policy=dict(quota="21474836480", versioning="auto")),
98
    dict(
99
        count=0,
100
        last_modified="2012-10-23T12:25:17.229187+00:00",
101
        bytes=0,
102
        name="trash",
103
        x_container_policy=dict(quota="21474836480", versioning="auto"))]
104
object_list = [
105
    dict(hash="",
106
        name="The_Secret_Garden.zip",
107
        x_object_public="/public/wdp9p",
108
        bytes=203304947,
109
        x_object_version_timestamp="1360237915.7027509",
110
        x_object_uuid="s0m3uu1df0r0bj0n3",
111
        last_modified="2013-02-07T11:51:55.702751+00:00",
112
        content_type="application/octet-stream",
113
        x_object_hash="0afdf29f71cd53126225c3f54ca",
114
        x_object_version=17737,
115
        x_object_modified_by=user_id),
116
    dict(hash="",
117
        name="The_Revealed_Garden.zip",
118
        x_object_public="/public/wpd7p",
119
        bytes=20330947,
120
        x_object_version_timestamp="13602915.7027509",
121
        x_object_uuid="s0m3uu1df0r0bj70w",
122
        last_modified="2013-02-07T11:51:55.702751+00:00",
123
        content_type="application/octet-stream",
124
        x_object_hash="0afdf29f71cd53126225c3f54ca",
125
        x_object_version=17737,
126
        x_object_modified_by=user_id)]
127

    
128

    
129
class FR(object):
130
    """FR stands for Fake Response"""
131
    json = dict()
132
    headers = dict()
133
    content = json
134
    status = None
135
    status_code = 200
136

    
137
    def release(self):
138
        pass
139

    
140

    
141
class StorageClient(TestCase):
142

    
143
    files = []
144

    
145
    def _create_temp_file(self, num_of_blocks):
146
        self.files.append(NamedTemporaryFile())
147
        tmpFile = self.files[-1]
148
        file_size = num_of_blocks * 4 * 1024 * 1024
149
        print('\n\tCreate tmp file')
150
        tmpFile.write(urandom(file_size))
151
        tmpFile.flush()
152
        tmpFile.seek(0)
153
        print('\t\tDone')
154
        return tmpFile
155

    
156
    def assert_dicts_are_equal(self, d1, d2):
157
        for k, v in d1.items():
158
            self.assertTrue(k in d2)
159
            if isinstance(v, dict):
160
                self.assert_dicts_are_equal(v, d2[k])
161
            else:
162
                self.assertEqual(unicode(v), unicode(d2[k]))
163

    
164
    def setUp(self):
165
        self.url = 'https://www.example.com/pithos'
166
        self.token = 'p17h0570k3n'
167
        self.client = SC(self.url, self.token)
168
        self.client.account = user_id
169
        self.client.container = 'c0nt@1n3r_i'
170

    
171
    def tearDown(self):
172
        FR.headers = dict()
173
        FR.status_code = 200
174
        FR.json = dict()
175
        FR.content = FR.json
176
        for f in self.files:
177
            f.close()
178

    
179
    #  Pithos+ methods that extend storage API
180

    
181
    @patch('%s.head' % client_pkg, return_value=FR())
182
    def test_get_account_info(self, head):
183
        FR.headers = account_info
184
        r = self.client.get_account_info()
185
        self.assert_dicts_are_equal(account_info, r)
186
        head.assert_called_once_with(
187
            '/%s' % self.client.account,
188
            success=(204, 401))
189
        FR.status_code = 401
190
        self.assertRaises(ClientError, self.client.get_account_info)
191

    
192
    @patch('%s.post' % storage_pkg, return_value=FR())
193
    @patch('%s.set_header' % storage_pkg)
194
    def test_replace_account_meta(self, SH, post):
195
        metas = dict(k1='v1', k2='v2', k3='v3')
196
        self.client.replace_account_meta(metas)
197
        prfx = 'X-Account-Meta-'
198
        expected = [call('%s%s' % (prfx, k), v) for k, v in metas.items()]
199
        self.assertEqual(SH.mock_calls, expected)
200
        post.assert_called_once_with('/%s' % self.client.account, success=202)
201

    
202
    @patch('%s.post' % storage_pkg, return_value=FR())
203
    @patch('%s.get_account_info' % storage_pkg, return_value=account_info)
204
    def test_del_account_meta(self, GAI, post):
205
        prfx = 'x-account-meta-'
206
        keys = [k[len(prfx):] for k in account_info if k.startswith(prfx)]
207
        for key in keys:
208
            self.client.del_account_meta(key)
209
            self.assertEqual(
210
                post.mock_calls[-1],
211
                call('/%s' % self.client.account, success=202))
212
        self.assertEqual(len(keys), len(post.mock_calls))
213
        self.assertRaises(ClientError, self.client.del_account_meta, 'k4')
214

    
215
    @patch('%s.put' % storage_pkg, return_value=FR())
216
    def test_create_container(self, put):
217
        cont = 's0m3c0n731n3r'
218
        self.client.create_container(cont)
219
        args = (user_id, cont)
220
        put.assert_called_once_with('/%s/%s' % args, success=(201, 202))
221
        FR.status_code = 202
222
        self.assertRaises(ClientError, self.client.create_container, cont)
223

    
224
    @patch('%s.head' % storage_pkg, return_value=FR())
225
    def test_get_container_info(self, head):
226
        FR.headers = container_info
227
        cont = self.client.container
228
        r = self.client.get_container_info(cont)
229
        self.assert_dicts_are_equal(r, container_info)
230
        path = '/%s/%s' % (self.client.account, cont)
231
        head.assert_called_once_with(path, success=(204, 404))
232
        FR.status_code = 404
233
        self.assertRaises(ClientError, self.client.get_container_info, cont)
234

    
235
    @patch('%s.delete' % storage_pkg, return_value=FR())
236
    def test_delete_container(self, delete):
237
        FR.status_code = 204
238
        cont = 's0m3c0n731n3r'
239
        self.client.delete_container(cont)
240
        for err_code in (404, 409):
241
            FR.status_code = err_code
242
            self.assertRaises(ClientError, self.client.delete_container, cont)
243
        acall = call('/%s/%s' % (user_id, cont), success=(204, 404, 409))
244
        self.assertEqual(delete.mock_calls, [acall] * 3)
245

    
246
    @patch('%s.get' % storage_pkg, return_value=FR())
247
    @patch('%s.set_param' % storage_pkg)
248
    def test_list_containers(self, SP, get):
249
        FR.json, acc = container_list, self.client.account
250
        r = self.client.list_containers()
251
        SP.assert_called_once_with('format', 'json')
252
        get.assert_called_once_with('/%s' % acc, success=(200, 204))
253
        for i in range(len(r)):
254
            self.assert_dicts_are_equal(r[i], container_list[i])
255

    
256
    @patch('%s.put' % storage_pkg, return_value=FR())
257
    def test_upload_object(self, put):
258
        (acc, cont) = (self.client.account, self.client.container)
259
        num_of_blocks = 4
260
        tmpFile = self._create_temp_file(num_of_blocks)
261
        tmpFile.seek(0, 2)
262
        sizes = [None, (tmpFile.tell() / num_of_blocks) / 2]
263
        for size in sizes:
264
            tmpFile.seek(0)
265
            self.client.upload_object(obj, tmpFile, size)
266
            tmpFile.seek(0)
267
            self.assertEqual(put.mock_calls[-1], call(
268
                '/%s/%s/%s' % (acc, cont, obj),
269
                data=tmpFile.read(size) if size else tmpFile.read(),
270
                success=201))
271

    
272
    @patch('%s.put' % storage_pkg, return_value=FR())
273
    @patch('%s.set_header' % storage_pkg)
274
    def test_create_object(self, SH, put):
275
        cont = self.client.container
276
        ctype = 'c0n73n7/typ3'
277
        exp_shd = [
278
            call('Content-Type', 'application/octet-stream'),
279
            call('Content-length', '0'),
280
            call('Content-Type', ctype), call('Content-length', '42')]
281
        exp_put = [call('/%s/%s/%s' % (user_id, cont, obj), success=201)] * 2
282
        self.client.create_object(obj)
283
        self.client.create_object(obj, content_type=ctype, content_length=42)
284
        self.assertEqual(SH.mock_calls, exp_shd)
285
        self.assertEqual(put.mock_calls, exp_put)
286

    
287
    @patch('%s.put' % storage_pkg, return_value=FR())
288
    @patch('%s.set_header' % client_pkg)
289
    def test_create_directory(self, SH, put):
290
        cont = self.client.container
291
        exp_shd = [
292
            call('Content-Type', 'application/directory'),
293
            call('Content-length', '0')]
294
        exp_put = [call('/%s/%s/%s' % (user_id, cont, obj), success=201)]
295
        self.client.create_directory(obj)
296
        self.assertEqual(SH.mock_calls, exp_shd)
297
        self.assertEqual(put.mock_calls, exp_put)
298

    
299
    @patch('%s.head' % storage_pkg, return_value=FR())
300
    def test_get_object_info(self, head):
301
        FR.headers = object_info
302
        path = '/%s/%s/%s' % (self.client.account, self.client.container, obj)
303
        r = self.client.get_object_info(obj)
304
        head.assert_called_once_with(path, success=200)
305
        self.assertEqual(r, object_info)
306

    
307
    @patch('%s.get_object_info' % storage_pkg, return_value=object_info)
308
    def test_get_object_meta(self, GOI):
309
        r = self.client.get_object_meta(obj)
310
        GOI.assert_called_once_with(obj)
311
        prfx = 'x-object-meta-'
312
        for k in [k for k in object_info if k.startswith(prfx)]:
313
            self.assertEqual(r.pop(k[len(prfx):]), object_info[k])
314
        self.assertFalse(len(r))
315

    
316
    @patch('%s.post' % storage_pkg, return_value=FR())
317
    @patch('%s.set_header' % storage_pkg)
318
    def test_del_object_meta(self, SH, post):
319
        key = '50m3m3t4k3y'
320
        self.client.del_object_meta(obj, key)
321
        prfx = 'X-Object-Meta-'
322
        SH.assert_called_once_with('%s%s' % (prfx, key), '')
323
        exp = '/%s/%s/%s' % (self.client.account, self.client.container, obj)
324
        post.assert_called_once_with(exp, success=202)
325

    
326
    @patch('%s.post' % client_pkg, return_value=FR())
327
    @patch('%s.set_header' % client_pkg)
328
    def test_replace_object_meta(self, SH, post):
329
        metas = dict(k1='new1', k2='new2', k3='new3')
330
        cont = self.client.container
331
        self.client.replace_object_meta(metas)
332
        post.assert_called_once_with('/%s/%s' % (user_id, cont), success=202)
333
        prfx = 'X-Object-Meta-'
334
        expected = [call('%s%s' % (prfx, k), v) for k, v in metas.items()]
335
        self.assertEqual(SH.mock_calls, expected)
336

    
337
    @patch('%s.put' % storage_pkg, return_value=FR())
338
    @patch('%s.set_header' % storage_pkg)
339
    def test_copy_object(self, SH, put):
340
        src_cont = 'src-c0nt41n3r'
341
        src_obj = 'src-0bj'
342
        dst_cont = 'dst-c0nt41n3r'
343
        for dst_obj in (None, 'dst-0bj'):
344
            dst_path = dst_obj or src_obj
345
            path = '/%s/%s/%s' % (self.client.account, dst_cont, dst_path)
346
            self.client.copy_object(src_cont, src_obj, dst_cont, dst_obj)
347
            self.assertEqual(put.mock_calls[-1], call(path, success=201))
348
            kwargs = {
349
                'X-Copy-From': '/%s/%s' % (src_cont, src_obj),
350
                'Content-Length': 0}
351
            self.assertEqual(
352
                SH.mock_calls[-2:],
353
                [call(k, v) for k, v in kwargs.items()])
354

    
355
    @patch('%s.put' % storage_pkg, return_value=FR())
356
    @patch('%s.set_header' % storage_pkg)
357
    def test_move_object(self, SH, put):
358
        src_cont = 'src-c0nt41n3r'
359
        src_obj = 'src-0bj'
360
        dst_cont = 'dst-c0nt41n3r'
361
        for dst_obj in (None, 'dst-0bj'):
362
            dst_path = dst_obj or src_obj
363
            path = '/%s/%s/%s' % (self.client.account, dst_cont, dst_path)
364
            self.client.move_object(src_cont, src_obj, dst_cont, dst_obj)
365
            self.assertEqual(put.mock_calls[-1], call(path, success=201))
366
            kwargs = {
367
                'X-Move-From': '/%s/%s' % (src_cont, src_obj),
368
                'Content-Length': 0}
369
            self.assertEqual(
370
                SH.mock_calls[-2:],
371
                [call(k, v) for k, v in kwargs.items()])
372

    
373
    @patch('%s.delete' % storage_pkg, return_value=FR())
374
    def test_delete_object(self, delete):
375
        cont = self.client.container
376
        self.client.delete_object(obj)
377
        exp = '/%s/%s/%s' % (user_id, cont, obj)
378
        delete.assert_called_once_with(exp, success=(204, 404))
379
        FR.status_code = 404
380
        self.assertRaises(ClientError, self.client.delete_object, obj)
381

    
382
    @patch('%s.get' % client_pkg, return_value=FR())
383
    @patch('%s.set_param' % client_pkg)
384
    def test_list_objects(self, SP, get):
385
        FR.json = object_list
386
        acc, cont = self.client.account, self.client.container
387
        r = self.client.list_objects()
388
        for i in range(len(r)):
389
            self.assert_dicts_are_equal(r[i], object_list[i])
390
        exp = '/%s/%s' % (acc, cont)
391
        get.assert_called_once_with(exp, success=(200, 204, 304, 404))
392
        SP.assert_called_once_with('format', 'json')
393
        FR.status_code = 304
394
        self.assertEqual(self.client.list_objects(), [])
395
        FR.status_code = 404
396
        self.assertRaises(ClientError, self.client.list_objects)
397

    
398
    @patch('%s.get' % client_pkg, return_value=FR())
399
    @patch('%s.set_param' % client_pkg)
400
    def test_list_objects_in_path(self, SP, get):
401
        FR.json = object_list
402
        path = '/some/awsome/path'
403
        acc, cont = self.client.account, self.client.container
404
        self.client.list_objects_in_path(path)
405
        exp = '/%s/%s' % (acc, cont)
406
        get.assert_called_once_with(exp, success=(200, 204, 404))
407
        self.assertEqual(
408
            SP.mock_calls,
409
            [call('format', 'json'), call('path', path)])
410
        FR.status_code = 404
411
        self.assertRaises(ClientError, self.client.list_objects)
412

    
413
if __name__ == '__main__':
414
    from sys import argv
415
    from kamaki.clients.test import runTestCase
416
    runTestCase(StorageClient, 'Storage Client', argv[1:])