Statistics
| Branch: | Tag: | Revision:

root / pithos / api / tests.py @ 3364a52a

History | View | Annotate | Download (67.8 kB)

1
# Copyright 2011 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 django.test.client import Client
35
from django.test import TestCase
36
from django.utils import simplejson as json
37
from xml.dom import minidom
38
import types
39
import hashlib
40
import os
41
import mimetypes
42
import random
43
import datetime
44
import string
45
from pithos.backends import backend
46

    
47
DATE_FORMATS = ["%a %b %d %H:%M:%S %Y",
48
                "%A, %d-%b-%y %H:%M:%S GMT",
49
                "%a, %d %b %Y %H:%M:%S GMT"]
50

    
51
class AaiClient(Client):
52
    def request(self, **request):
53
        request['HTTP_X_AUTH_TOKEN'] = '46e427d657b20defe352804f0eb6f8a2'
54
        return super(AaiClient, self).request(**request)
55

    
56
class BaseTestCase(TestCase):
57
    #TODO unauthorized request
58
    def setUp(self):
59
        self.client = AaiClient()
60
        self.headers = {
61
            'account':(
62
                'X-Account-Container-Count',
63
                'X-Account-Bytes-Used',
64
                'Last-Modified',
65
                'Content-Length',
66
                'Date',
67
                'Content-Type',),
68
            'container':(
69
                'X-Container-Object-Count',
70
                'X-Container-Bytes-Used',
71
                'Content-Type',
72
                'Last-Modified',
73
                'Content-Length',
74
                'Date',
75
                'X-Container-Block-Size',
76
                'X-Container-Block-Hash',),
77
            'object':(
78
                'ETag',
79
                'Content-Length',
80
                'Content-Type',
81
                'Content-Encoding',
82
                'Last-Modified',
83
                'Date',
84
                'X-Object-Manifest',
85
                'Content-Range',)}
86
        self.contentTypes = {'xml':'application/xml',
87
                             'json':'application/json',
88
                             '':'text/plain'}
89
        self.extended = {
90
            'container':(
91
                'name',
92
                'count',
93
                'bytes',
94
                'last_modified'),
95
            'object':(
96
                'name',
97
                'hash',
98
                'bytes',
99
                'content_type',
100
                'content_encoding',
101
                'last_modified',)}
102
        self.return_codes = (400, 401, 404, 503,)
103

    
104
    def assertFault(self, response, status_code, name):
105
        self.assertEqual(response.status_code, status_code)
106

    
107
    def assertBadRequest(self, response):
108
        self.assertFault(response, 400, 'badRequest')
109

    
110
    def assertItemNotFound(self, response):
111
        self.assertFault(response, 404, 'itemNotFound')
112

    
113
    def assertUnauthorized(self, response):
114
        self.assertFault(response, 401, 'unauthorized')
115

    
116
    def assertServiceUnavailable(self, response):
117
        self.assertFault(response, 503, 'serviceUnavailable')
118

    
119
    def assertNonEmpty(self, response):
120
        self.assertFault(response, 409, 'nonEmpty')
121

    
122
    def assert_status(self, response, codes):
123
        l = [elem for elem in self.return_codes]
124
        if type(codes) == types.ListType:
125
            l.extend(codes)
126
        else:
127
            l.append(codes)
128
        self.assertTrue(response.status_code in l)
129

    
130
    def get_account_meta(self, account, exp_meta={}):
131
        path = '/v1/%s' % account
132
        response = self.client.head(path)
133
        self.assert_status(response, 204)
134
        self.assert_headers(response, 'account', exp_meta)
135
        return response
136

    
137
    def list_containers(self, account, limit=10000, marker='', format='',
138
                        **headers):
139
        params = locals()
140
        params.pop('self')
141
        params.pop('account')
142
        path = '/v1/%s' % account
143
        response = self.client.get(path, params, **headers)
144
        self.assert_status(response, [200, 204, 304, 412])
145
        response.content = response.content.strip()
146
        if format:
147
            self.assert_extended(response, format, 'container', limit)
148
        else:
149
            names = get_content_splitted(response)
150
            self.assertTrue(len(names) <= limit)
151
        return response
152

    
153
    def update_account_meta(self, account, **metadata):
154
        path = '/v1/%s' % account
155
        response = self.client.post(path, **metadata)
156
        response.content = response.content.strip()
157
        self.assert_status(response, 202)
158
        return response
159

    
160
    def get_container_meta(self, account, container, exp_meta={}):
161
        params = locals()
162
        params.pop('self')
163
        params.pop('account')
164
        params.pop('container')
165
        path = '/v1/%s/%s' %(account, container)
166
        response = self.client.head(path, params)
167
        response.content = response.content.strip()
168
        self.assert_status(response, 204)
169
        if response.status_code == 204:
170
            self.assert_headers(response, 'container', exp_meta)
171
        return response
172

    
173
    def list_objects(self, account, container, limit=10000, marker='',
174
                     prefix='', format='', path='', delimiter='', meta='',
175
                     **headers):
176
        params = locals()
177
        params.pop('self')
178
        params.pop('account')
179
        params.pop('container')
180
        path = '/v1/%s/%s' % (account, container)
181
        response = self.client.get(path, params, **headers)
182
        response.content = response.content.strip()
183
        if format:
184
            self.assert_extended(response, format, 'object', limit)
185
        self.assert_status(response, [200, 204, 304, 412])
186
        return response
187

    
188
    def create_container(self, account, name, **meta):
189
        path = '/v1/%s/%s' %(account, name)
190
        response = self.client.put(path, **meta)
191
        response.content = response.content.strip()
192
        self.assert_status(response, [201, 202])
193
        return response
194

    
195
    def update_container_meta(self, account, name, **meta):
196
        path = '/v1/%s/%s' %(account, name)
197
        response = self.client.post(path,
198
                                    data=None,
199
                                    content_type='text/xml',
200
                                    follow=False, **meta)
201
        response.content = response.content.strip()
202
        self.assert_status(response, 202)
203
        return response
204

    
205
    def delete_container(self, account, container):
206
        path = '/v1/%s/%s' %(account, container)
207
        response = self.client.delete(path)
208
        response.content = response.content.strip()
209
        self.assert_status(response, [204, 409])
210
        return response
211

    
212
    def get_object_meta(self, account, container, name):
213
        path = '/v1/%s/%s/%s' %(account, container, name)
214
        response = self.client.head(path)
215
        response.content = response.content.strip()
216
        self.assert_status(response, 204)
217
        return response
218

    
219
    def get_object(self, account, container, name, format='', **headers):
220
        path = '/v1/%s/%s/%s' %(account, container, name)
221
        response = self.client.get(path, {'format':format}, **headers)
222
        response.content = response.content.strip()
223
        self.assert_status(response, [200, 206, 304, 412, 416])
224
        if response.status_code in [200, 206]:
225
            self.assert_headers(response, 'object')
226
        return response
227

    
228
    def upload_object(self, account, container, name, data, content_type='',
229
                      **headers):
230
        path = '/v1/%s/%s/%s' %(account, container, name)
231
        response = self.client.put(path, data, content_type, **headers)
232
        response.content = response.content.strip()
233
        self.assert_status(response, [201, 411, 422])
234
        if response.status_code == 201:
235
            self.assertTrue(response['Etag'])
236
        return response
237

    
238
    def copy_object(self, account, container, name, src, **headers):
239
        path = '/v1/%s/%s/%s' %(account, container, name)
240
        headers['HTTP_X_COPY_FROM'] = src
241
        response = self.client.put(path, **headers)
242
        response.content = response.content.strip()
243
        self.assert_status(response, 201)
244
        return response
245

    
246
    def move_object(self, account, container, name, src, **headers):
247
        path = '/v1/%s/%s/%s' % (account, container, name)
248
        headers['HTTP_X_MOVE_FROM'] = src
249
        response = self.client.put(path, **headers)
250
        response.content = response.content.strip()
251
        self.assert_status(response, 201)
252
        return response
253

    
254
    def update_object(self, account, container, name, data={},
255
                      content_type='MULTIPART_CONTENT', **headers):
256
        path = '/v1/%s/%s/%s' %(account, container, name)
257
        response = self.client.post(path, data, content_type, **headers)
258
        response.content = response.content.strip()
259
        self.assert_status(response, [202, 204, 416])
260
        return response
261

    
262
    def delete_object(self, account, container, name):
263
        path = '/v1/%s/%s/%s' %(account, container, name)
264
        response = self.client.delete(path)
265
        response.content = response.content.strip()
266
        self.assert_status(response, 204)
267
        return response
268

    
269
    def assert_headers(self, response, type, exp_meta={}):
270
        entities = ['Account', 'Container', 'Container-Object', 'Object']
271
        user_defined_meta = ['X-%s-Meta' %elem for elem in entities]
272
        headers = [item for item in response._headers.values()]
273
        t = tuple(user_defined_meta)
274
        system_headers = [h for h in headers if not h[0].startswith(t)]
275
        for h in system_headers:
276
            self.assertTrue(h[0] in self.headers[type])
277
            if exp_meta:
278
                self.assertEqual(h[1], exp_meta[h[0]])
279

    
280
    def assert_extended(self, response, format, type, size):
281
        exp_content_type = self.contentTypes[format]
282
        self.assertEqual(response['Content-Type'].find(exp_content_type), 0)
283
        if format == 'xml':
284
            self.assert_xml(response, type, size)
285
        elif format == 'json':
286
            self.assert_json(response, type, size)
287

    
288
    def assert_json(self, response, type, size):
289
        convert = lambda s: s.lower()
290
        info = [convert(elem) for elem in self.extended[type]]
291
        data = json.loads(response.content)
292
        self.assertTrue(len(data) <= size)
293
        for item in info:
294
            for i in data:
295
                if 'subdir' in i.keys():
296
                    continue
297
                self.assertTrue(item in i.keys())
298

    
299
    def assert_xml(self, response, type, size):
300
        convert = lambda s: s.lower()
301
        info = [convert(elem) for elem in self.extended[type]]
302
        try:
303
            info.remove('content_encoding')
304
        except ValueError:
305
            pass
306
        xml = minidom.parseString(response.content)
307
        for item in info:
308
            nodes = xml.getElementsByTagName(item)
309
            self.assertTrue(nodes)
310
            self.assertTrue(len(nodes) <= size)
311
            
312

    
313
    def upload_os_file(self, account, container, fullpath, meta={}):
314
        try:
315
            f = open(fullpath, 'r')
316
            data = f.read()
317
            name = os.path.split(fullpath)[-1]
318
            return self.upload_data(account, container, name, data)    
319
        except IOError:
320
            return
321

    
322
    def upload_random_data(self, account, container, name, length=1024,
323
                           meta={}):
324
        data = get_random_data(length)
325
        return self.upload_data(account, container, name, data, meta)
326

    
327
    def upload_data(self, account, container, name, data, meta={}):
328
        obj = {}
329
        obj['name'] = name
330
        try:
331
            obj['data'] = data
332
            obj['hash'] = compute_md5_hash(obj['data'])
333
            meta.update({'HTTP_X_OBJECT_META_TEST':'test1',
334
                         'HTTP_ETAG':obj['hash']})
335
            type, enc = mimetypes.guess_type(name)
336
            meta['HTTP_CONTENT_TYPE'] = type and type or 'plain/text'
337
            if enc:
338
                meta['HTTP_CONTENT_ENCODING'] = enc
339
            
340
            obj['meta'] = meta
341
            r = self.upload_object(account,
342
                               container,
343
                               obj['name'],
344
                               obj['data'],
345
                               meta['HTTP_CONTENT_TYPE'],
346
                               **meta)
347
            if r.status_code == 201:
348
                return obj
349
        except IOError:
350
            return
351

    
352
class AccountHead(BaseTestCase):
353
    def setUp(self):
354
        BaseTestCase.setUp(self)
355
        self.account = 'test'
356
        self.containers = ['apples', 'bananas', 'kiwis', 'oranges', 'pears']
357
        for item in self.containers:
358
            self.create_container(self.account, item)
359

    
360
    def tearDown(self):
361
        for c in  get_content_splitted(self.list_containers(self.account)):
362
            self.delete_container(self.account, c)
363

    
364
    def test_get_account_meta(self):
365
        response = self.get_account_meta(self.account)
366
        r2 = self.list_containers(self.account)
367
        containers =  get_content_splitted(r2)
368
        l = str(len(containers))
369
        self.assertEqual(response['X-Account-Container-Count'], l)
370
        size = 0
371
        for c in containers:
372
            r = self.get_container_meta(self.account, c)
373
            size = size + int(r['X-Container-Bytes-Used'])
374
        self.assertEqual(response['X-Account-Bytes-Used'], str(size))
375

    
376
    #def test_get_account_401(self):
377
    #    response = self.get_account_meta('non-existing-account')
378
    #    print response
379
    #    self.assertEqual(response.status_code, 401)
380

    
381
class AccountGet(BaseTestCase):
382
    def setUp(self):
383
        BaseTestCase.setUp(self)
384
        self.account = 'test'
385
        #create some containers
386
        self.containers = ['apples', 'bananas', 'kiwis', 'oranges', 'pears']
387
        for item in self.containers:
388
            self.create_container(self.account, item)
389

    
390
    def tearDown(self):
391
        for c in get_content_splitted(self.list_containers(self.account)):
392
            response = self.delete_container(self.account, c)
393

    
394
    def test_list(self):
395
        #list containers
396
        response = self.list_containers(self.account)
397
        containers = get_content_splitted(response)
398
        self.assertEquals(self.containers, containers)
399

    
400
    #def test_list_204(self):
401
    #    response = self.list_containers('non-existing-account')
402
    #    self.assertEqual(response.status_code, 204)
403

    
404
    def test_list_with_limit(self):
405
        limit = 2
406
        response = self.list_containers(self.account, limit=limit)
407
        containers = get_content_splitted(response)
408
        self.assertEquals(len(containers), limit)
409
        self.assertEquals(self.containers[:2], containers)
410

    
411
    def test_list_with_marker(self):
412
        l = 2
413
        m = 'bananas'
414
        response = self.list_containers(self.account, limit=l, marker=m)
415
        containers =  get_content_splitted(response)
416
        i = self.containers.index(m) + 1
417
        self.assertEquals(self.containers[i:(i+l)], containers)
418
        
419
        m = 'oranges'
420
        response = self.list_containers(self.account, limit=l, marker=m)
421
        containers = get_content_splitted(response)
422
        i = self.containers.index(m) + 1
423
        self.assertEquals(self.containers[i:(i+l)], containers)
424

    
425
    #def test_extended_list(self):
426
    #    self.list_containers(self.account, limit=3, format='xml')
427
    #    self.list_containers(self.account, limit=3, format='json')
428

    
429
    def test_list_json_with_marker(self):
430
        l = 2
431
        m = 'bananas'
432
        response = self.list_containers(self.account, limit=l, marker=m,
433
                                        format='json')
434
        containers = json.loads(response.content)
435
        self.assertEqual(containers[0]['name'], 'kiwis')
436
        self.assertEqual(containers[1]['name'], 'oranges')
437

    
438
    def test_list_xml_with_marker(self):
439
        l = 2
440
        m = 'oranges'
441
        response = self.list_containers(self.account, limit=l, marker=m,
442
                                        format='xml')
443
        xml = minidom.parseString(response.content)
444
        nodes = xml.getElementsByTagName('name')
445
        self.assertEqual(len(nodes), 1)
446
        self.assertEqual(nodes[0].childNodes[0].data, 'pears')
447

    
448
    def test_if_modified_since(self):
449
        t = datetime.datetime.utcnow()
450
        t2 = t - datetime.timedelta(minutes=10)
451
        
452
        #add a new container
453
        self.create_container(self.account,
454
                              'dummy')
455

    
456
        for f in DATE_FORMATS:
457
            past = t2.strftime(f)
458
            
459
            headers = {'HTTP_IF_MODIFIED_SINCE':'%s' %past}
460
            r = self.list_containers(self.account, **headers)
461
            
462
            #assert get success
463
            self.assertEqual(r.status_code, 200)
464

    
465
    def test_if_modified_since_invalid_date(self):
466
        headers = {'HTTP_IF_MODIFIED_SINCE':''}
467
        r = self.list_containers(self.account, **headers)
468
            
469
        #assert get success
470
        self.assertEqual(r.status_code, 200)
471

    
472
    def test_if_not_modified_since(self):
473
        now = datetime.datetime.utcnow()
474
        since = now + datetime.timedelta(1)
475
        
476
        for f in DATE_FORMATS:
477
            headers = {'HTTP_IF_MODIFIED_SINCE':'%s' %since.strftime(f)}
478
            r = self.list_containers(self.account, **headers)
479
            
480
            #assert not modified
481
            self.assertEqual(r.status_code, 304)
482

    
483
    def test_if_unmodified_since(self):
484
        now = datetime.datetime.utcnow()
485
        since = now + datetime.timedelta(1)
486
        
487
        for f in DATE_FORMATS:
488
            headers = {'HTTP_IF_UNMODIFIED_SINCE':'%s' %since.strftime(f)}
489
            r = self.list_containers(self.account, **headers)
490
            
491
            #assert success
492
            self.assertEqual(r.status_code, 200)
493
            self.assertEqual(self.containers, get_content_splitted(r))
494

    
495
    def test_if_unmodified_since_precondition_failed(self):
496
        t = datetime.datetime.utcnow()
497
        t2 = t - datetime.timedelta(minutes=10)
498
        
499
        #add a new container
500
        self.create_container(self.account,
501
                              'dummy')
502
        
503
        for f in DATE_FORMATS:
504
            past = t2.strftime(f)
505
            
506
            headers = {'HTTP_IF_UNMODIFIED_SINCE':'%s' %past}
507
            r = self.list_containers(self.account, **headers)
508
            
509
            #assert get success
510
            self.assertEqual(r.status_code, 412)
511

    
512
class AccountPost(BaseTestCase):
513
    def setUp(self):
514
        BaseTestCase.setUp(self)
515
        self.account = 'test'
516
        self.containers = ['apples', 'bananas', 'kiwis', 'oranges', 'pears']
517
        for item in self.containers:
518
            self.create_container(self.account, item)
519

    
520
    def tearDown(self):
521
        for c in  get_content_splitted(self.list_containers(self.account)):
522
            self.delete_container(self.account, c)
523

    
524
    def test_update_meta(self):
525
        meta = {'HTTP_X_ACCOUNT_META_TEST':'test',
526
                'HTTP_X_ACCOUNT_META_TOST':'tost'}
527
        response = self.update_account_meta(self.account, **meta)
528
        response = self.get_account_meta(self.account)
529
        for k,v in meta.items():
530
            key = '-'.join(elem.capitalize() for elem in k.split('_')[1:])
531
            self.assertTrue(response[key])
532
            self.assertEqual(response[key], v)
533

    
534
    #def test_invalid_account_update_meta(self):
535
    #    with AssertInvariant(self.get_account_meta, self.account):
536
    #        meta = {'HTTP_X_ACCOUNT_META_TEST':'test',
537
    #               'HTTP_X_ACCOUNT_META_TOST':'tost'}
538
    #        response = self.update_account_meta('non-existing-account', **meta)
539

    
540
class ContainerHead(BaseTestCase):
541
    def setUp(self):
542
        BaseTestCase.setUp(self)
543
        self.account = 'test'
544
        self.container = 'apples'
545
        self.create_container(self.account, self.container)
546

    
547
    def tearDown(self):
548
        for o in self.list_objects(self.account, self.container):
549
            self.delete_object(self.account, self.container, o)
550
        self.delete_container(self.account, self.container)
551

    
552
    def test_get_meta(self):
553
        headers = {'HTTP_X_OBJECT_META_TRASH':'true'}
554
        t1 = datetime.datetime.utcnow()
555
        o = self.upload_random_data(self.account,
556
                                self.container,
557
                                'McIntosh.jpg',
558
                                meta=headers)
559
        if o:
560
            r = self.get_container_meta(self.account,
561
                                        self.container)
562
            self.assertEqual(r['X-Container-Object-Count'], '1')
563
            self.assertEqual(r['X-Container-Bytes-Used'], str(len(o['data'])))
564
            t2 = datetime.datetime.strptime(r['Last-Modified'], DATE_FORMATS[2])
565
            delta = (t2 - t1)
566
            threashold = datetime.timedelta(seconds=1) 
567
            self.assertTrue(delta < threashold)
568
            self.assertTrue(r['X-Container-Object-Meta'])
569
            self.assertTrue('Trash' in r['X-Container-Object-Meta'])
570

    
571
class ContainerGet(BaseTestCase):
572
    def setUp(self):
573
        BaseTestCase.setUp(self)
574
        self.account = 'test'
575
        self.container = ['pears', 'apples']
576
        for c in self.container:
577
            self.create_container(self.account, c)
578
        self.obj = []
579
        for o in o_names[:8]:
580
            self.obj.append(self.upload_random_data(self.account,
581
                                                    self.container[0],
582
                                                    o))
583
        for o in o_names[8:]:
584
            self.obj.append(self.upload_random_data(self.account,
585
                                                    self.container[1],
586
                                                    o))
587

    
588
    def tearDown(self):
589
        for c in self.container:
590
            for obj in get_content_splitted(self.list_objects(self.account, c)):
591
                self.delete_object(self.account, c, obj)
592
            self.delete_container(self.account, c)
593

    
594
    def test_list_objects(self):
595
        response = self.list_objects(self.account, self.container[0])
596
        objects = get_content_splitted(response)
597
        l = [elem['name'] for elem in self.obj[:8]]
598
        l.sort()
599
        self.assertEqual(objects, l)
600

    
601
    def test_list_objects_with_limit_marker(self):
602
        response = self.list_objects(self.account, self.container[0], limit=2)
603
        objects = get_content_splitted(response)
604
        l = [elem['name'] for elem in self.obj[:8]]
605
        l.sort()
606
        self.assertEqual(objects, l[:2])
607
        
608
        markers = ['How To Win Friends And Influence People.pdf',
609
                   'moms_birthday.jpg']
610
        limit = 4
611
        for m in markers:
612
            response = self.list_objects(self.account, self.container[0],
613
                                         limit=limit, marker=m)
614
            objects = get_content_splitted(response)
615
            l = [elem['name'] for elem in self.obj[:8]]
616
            l.sort()
617
            start = l.index(m) + 1
618
            end = start + limit
619
            end = len(l) >= end and end or len(l)
620
            self.assertEqual(objects, l[start:end])
621

    
622
    def test_list_pseudo_hierarchical_folders(self):
623
        response = self.list_objects(self.account, self.container[1],
624
                                     prefix='photos', delimiter='/')
625
        objects = get_content_splitted(response)
626
        self.assertEquals(['photos/animals/', 'photos/me.jpg',
627
                           'photos/plants/'], objects)
628
        
629
        response = self.list_objects(self.account, self.container[1],
630
                                     prefix='photos/animals', delimiter='/')
631
        objs = get_content_splitted(response)
632
        l = ['photos/animals/cats/', 'photos/animals/dogs/']
633
        self.assertEquals(l, objs)
634
        
635
        response = self.list_objects(self.account, self.container[1],
636
                                     path='photos')
637
        objects = get_content_splitted(response)
638
        self.assertEquals(['photos/me.jpg'], objects)
639

    
640
    def test_extended_list_json(self):
641
        response = self.list_objects(self.account,
642
                                     self.container[1],
643
                                     format='json', limit=2,
644
                                     prefix='photos/animals',
645
                                     delimiter='/')
646
        objects = json.loads(response.content)
647
        self.assertEqual(objects[0]['subdir'], 'photos/animals/cats/')
648
        self.assertEqual(objects[1]['subdir'], 'photos/animals/dogs/')
649

    
650
    def test_extended_list_xml(self):
651
        response = self.list_objects(self.account, self.container[1],
652
                                     format='xml', limit=4, prefix='photos',
653
                                     delimiter='/')
654
        xml = minidom.parseString(response.content)
655
        dirs = xml.getElementsByTagName('subdir')
656
        self.assertEqual(len(dirs), 2)
657
        self.assertEqual(dirs[0].attributes['name'].value, 'photos/animals/')
658
        self.assertEqual(dirs[1].attributes['name'].value, 'photos/plants/')
659
        
660
        objects = xml.getElementsByTagName('name')
661
        self.assertEqual(len(objects), 1)
662
        self.assertEqual(objects[0].childNodes[0].data, 'photos/me.jpg')
663

    
664
    def test_list_meta_double_matching(self):
665
        meta = {'HTTP_X_OBJECT_META_QUALITY':'aaa',
666
                'HTTP_X_OBJECT_META_STOCK':'true'}
667
        r = self.update_object(self.account,
668
                                    self.container[0],
669
                                    self.obj[0]['name'],
670
                                    **meta)
671
        r = self.list_objects(self.account,
672
                          self.container[0],
673
                          meta='Quality,Stock')
674
        self.assertEqual(r.status_code, 200)
675
        obj = get_content_splitted(r)
676
        self.assertEqual(len(obj), 1)
677
        self.assertTrue(obj, self.obj[0]['name'])
678

    
679
    def test_list_using_meta(self):
680
        meta = {'HTTP_X_OBJECT_META_QUALITY':'aaa'}
681
        for o in self.obj[:2]:
682
            r = self.update_object(self.account,
683
                                    self.container[0],
684
                                    o['name'],
685
                                    **meta)
686
        meta = {'HTTP_X_OBJECT_META_STOCK':'true'}
687
        for o in self.obj[3:5]:
688
            r = self.update_object(self.account,
689
                                    self.container[0],
690
                                    o['name'],
691
                                    **meta)
692
        
693
        r = self.list_objects(self.account,
694
                          self.container[0],
695
                          meta='Quality')
696
        self.assertEqual(r.status_code, 200)
697
        obj = get_content_splitted(r)
698
        self.assertEqual(len(obj), 2)
699
        self.assertTrue(obj, [o['name'] for o in self.obj[:2]])
700
        
701
        # test case insensitive
702
        r = self.list_objects(self.account,
703
                          self.container[0],
704
                          meta='quality')
705
        self.assertEqual(r.status_code, 200)
706
        obj = get_content_splitted(r)
707
        self.assertEqual(len(obj), 2)
708
        self.assertTrue(obj, [o['name'] for o in self.obj[:2]])
709
        
710
        # test multiple matches
711
        r = self.list_objects(self.account,
712
                          self.container[0],
713
                          meta='Quality,Stock')
714
        self.assertEqual(r.status_code, 200)
715
        obj = get_content_splitted(r)
716
        self.assertEqual(len(obj), 4)
717
        self.assertTrue(obj, [o['name'] for o in self.obj[:4]])
718
        
719
        # test non 1-1 multiple match
720
        r = self.list_objects(self.account,
721
                          self.container[0],
722
                          meta='Quality,aaaa')
723
        self.assertEqual(r.status_code, 200)
724
        obj = get_content_splitted(r)
725
        self.assertEqual(len(obj), 2)
726
        self.assertTrue(obj, [o['name'] for o in self.obj[:2]])
727

    
728
    def test_if_modified_since(self):
729
        t = datetime.datetime.utcnow()
730
        t2 = t - datetime.timedelta(minutes=10)
731
        
732
        #add a new container
733
        self.upload_random_data(self.account,
734
                                self.container[0],
735
                                'dummy.txt')
736

    
737
        for f in DATE_FORMATS:
738
            past = t2.strftime(f)
739
            
740
            headers = {'HTTP_IF_MODIFIED_SINCE':'%s' %past}
741
            r = self.list_objects(self.account,
742
                                  self.container[0], **headers)
743
            
744
            #assert get success
745
            self.assertEqual(r.status_code, 200)
746

    
747
    def test_if_modified_since_invalid_date(self):
748
        headers = {'HTTP_IF_MODIFIED_SINCE':''}
749
        r = self.list_objects(self.account,
750
                              self.container[0], **headers)
751
        
752
        #assert get success
753
        self.assertEqual(r.status_code, 200)
754

    
755
    def test_if_not_modified_since(self):
756
        now = datetime.datetime.utcnow()
757
        since = now + datetime.timedelta(1)
758
        
759
        for f in DATE_FORMATS:
760
            headers = {'HTTP_IF_MODIFIED_SINCE':'%s' %since.strftime(f)}
761
            r = self.list_objects(self.account,
762
                              self.container[0], **headers)
763
        
764
            #assert not modified
765
            self.assertEqual(r.status_code, 304)
766

    
767
    def test_if_unmodified_since(self):
768
        now = datetime.datetime.utcnow()
769
        since = now + datetime.timedelta(1)
770
        
771
        for f in DATE_FORMATS:
772
            headers = {'HTTP_IF_UNMODIFIED_SINCE':'%s' %since.strftime(f)}
773
            r = self.list_objects(self.account,
774
                              self.container[0], **headers)
775
        
776
            #assert success
777
            self.assertEqual(r.status_code, 200)
778
            objlist = self.list_objects(self.account, self.container[0])
779
            self.assertEqual(get_content_splitted(r),
780
                             get_content_splitted(objlist))
781

    
782
    def test_if_unmodified_since_precondition_failed(self):
783
        t = datetime.datetime.utcnow()
784
        t2 = t - datetime.timedelta(minutes=10)
785
        
786
        #add a new container
787
        self.create_container(self.account,
788
                              'dummy')
789

    
790
        for f in DATE_FORMATS:
791
            past = t2.strftime(f)
792
            
793
            headers = {'HTTP_IF_UNMODIFIED_SINCE':'%s' %past}
794
            r = self.list_objects(self.account,
795
                              self.container[0], **headers)
796
        
797
            #assert get success
798
            self.assertEqual(r.status_code, 412)
799

    
800
class ContainerPut(BaseTestCase):
801
    def setUp(self):
802
        BaseTestCase.setUp(self)
803
        self.account = 'test'
804
        self.containers = ['c1', 'c2']
805

    
806
    def tearDown(self):
807
        for c in self.containers:
808
            r = self.delete_container(self.account, c)
809

    
810
    def test_create(self):
811
        response = self.create_container(self.account, self.containers[0])
812
        if response.status_code == 201:
813
            response = self.list_containers(self.account)
814
            content = get_content_splitted(response)
815
            self.assertTrue(self.containers[0] in content)
816
            r = self.get_container_meta(self.account, self.containers[0])
817
            self.assertEqual(r.status_code, 204)
818

    
819
    def test_create_twice(self):
820
        response = self.create_container(self.account, self.containers[0])
821
        if response.status_code == 201:
822
            r = self.create_container(self.account, self.containers[0])
823
            self.assertTrue(r.status_code, 202)
824

    
825
class ContainerPost(BaseTestCase):
826
    def setUp(self):
827
        BaseTestCase.setUp(self)
828
        self.account = 'test'
829
        self.container = 'apples'
830
        self.create_container(self.account, self.container)
831

    
832
    def tearDown(self):
833
        for o in self.list_objects(self.account, self.container):
834
            self.delete_object(self.account, self.container, o)
835
        self.delete_container(self.account, self.container)
836

    
837
    def test_update_meta(self):
838
        meta = {'HTTP_X_CONTAINER_META_TEST':'test33',
839
                'HTTP_X_CONTAINER_META_TOST':'tost22'}
840
        response = self.update_container_meta(self.account, self.container,
841
                                              **meta)
842
        response = self.get_container_meta(self.account, self.container)
843
        for k,v in meta.items():
844
            key = '-'.join(elem.capitalize() for elem in k.split('_')[1:])
845
            self.assertTrue(response[key])
846
            self.assertEqual(response[key], v)
847

    
848
class ContainerDelete(BaseTestCase):
849
    def setUp(self):
850
        BaseTestCase.setUp(self)
851
        self.account = 'test'
852
        self.containers = ['c1', 'c2']
853
        for c in self.containers:
854
            self.create_container(self.account, c)
855
        self.upload_random_data(self.account,
856
                                self.containers[1],
857
                                'nice.jpg')
858

    
859
    def tearDown(self):
860
        for c in self.containers:
861
            for o in get_content_splitted(self.list_objects(self.account, c)):
862
                self.delete_object(self.account, c, o)
863
            self.delete_container(self.account, c)
864

    
865
    def test_delete(self):
866
        r = self.delete_container(self.account, self.containers[0])
867
        self.assertEqual(r.status_code, 204)
868

    
869
    def test_delete_non_empty(self):
870
        r = self.delete_container(self.account, self.containers[1])
871
        self.assertNonEmpty(r)
872

    
873
    def test_delete_invalid(self):
874
        self.assertItemNotFound(self.delete_container(self.account, 'c3'))
875

    
876
class ObjectHead(BaseTestCase):
877
    pass
878

    
879
class ObjectGet(BaseTestCase):
880
    def setUp(self):
881
        BaseTestCase.setUp(self)
882
        self.account = 'test'
883
        self.containers = ['c1', 'c2']
884
        #create some containers
885
        for c in self.containers:
886
            self.create_container(self.account, c)
887
        
888
        #upload a file
889
        self.objects = []
890
        self.objects.append(self.upload_os_file(self.account,
891
                            self.containers[1],
892
                            './api/tests.py'))
893
        self.objects.append(self.upload_os_file(self.account,
894
                            self.containers[1],
895
                            'settings.py'))
896

    
897
    def tearDown(self):
898
        for c in self.containers:
899
            for o in get_content_splitted(self.list_objects(self.account, c)):
900
                self.delete_object(self.account, c, o)
901
            self.delete_container(self.account, c)
902

    
903
    def test_get(self):
904
        #perform get
905
        r = self.get_object(self.account,
906
                            self.containers[1],
907
                            self.objects[0]['name'],
908
                            self.objects[0]['meta'])
909
        #assert success
910
        self.assertEqual(r.status_code, 200)
911
        
912
        #assert content-type
913
        self.assertEqual(r['Content-Type'],
914
                         self.objects[0]['meta']['HTTP_CONTENT_TYPE'])
915

    
916
    def test_get_invalid(self):
917
        r = self.get_object(self.account,
918
                            self.containers[0],
919
                            self.objects[0]['name'])
920
        self.assertItemNotFound(r)
921

    
922
    def test_get_partial(self):
923
        #perform get with range
924
        headers = {'HTTP_RANGE':'bytes=0-499'}
925
        r = self.get_object(self.account,
926
                        self.containers[1],
927
                        self.objects[0]['name'],
928
                        **headers)
929
        
930
        #assert successful partial content
931
        self.assertEqual(r.status_code, 206)
932
        
933
        #assert content-type
934
        self.assertEqual(r['Content-Type'],
935
                         self.objects[0]['meta']['HTTP_CONTENT_TYPE'])
936
        
937
        #assert content length
938
        self.assertEqual(int(r['Content-Length']), 500)
939
        
940
        #assert content
941
        self.assertEqual(self.objects[0]['data'][:500], r.content)
942

    
943
    def test_get_final_500(self):
944
        #perform get with range
945
        headers = {'HTTP_RANGE':'bytes=-500'}
946
        r = self.get_object(self.account,
947
                        self.containers[1],
948
                        self.objects[0]['name'],
949
                        **headers)
950
        
951
        #assert successful partial content
952
        self.assertEqual(r.status_code, 206)
953
        
954
        #assert content-type
955
        self.assertEqual(r['Content-Type'],
956
                         self.objects[0]['meta']['HTTP_CONTENT_TYPE'])
957
        
958
        #assert content length
959
        self.assertEqual(int(r['Content-Length']), 500)
960
        
961
        #assert content
962
        self.assertTrue(self.objects[0]['data'][-500:], r.content)
963

    
964
    def test_get_rest(self):
965
        #perform get with range
966
        offset = len(self.objects[0]['data']) - 500
967
        headers = {'HTTP_RANGE':'bytes=%s-' %offset}
968
        r = self.get_object(self.account,
969
                        self.containers[1],
970
                        self.objects[0]['name'],
971
                        **headers)
972
        
973
        #assert successful partial content
974
        self.assertEqual(r.status_code, 206)
975
        
976
        #assert content-type
977
        self.assertEqual(r['Content-Type'],
978
                         self.objects[0]['meta']['HTTP_CONTENT_TYPE'])
979
        
980
        #assert content length
981
        self.assertEqual(int(r['Content-Length']), 500)
982
        
983
        #assert content
984
        self.assertTrue(self.objects[0]['data'][-500:], r.content)
985

    
986
    def test_get_range_not_satisfiable(self):
987
        #perform get with range
988
        offset = len(self.objects[0]['data']) + 1
989
        headers = {'HTTP_RANGE':'bytes=0-%s' %offset}
990
        r = self.get_object(self.account,
991
                        self.containers[1],
992
                        self.objects[0]['name'],
993
                        **headers)
994
        
995
        #assert Range Not Satisfiable
996
        self.assertEqual(r.status_code, 416)
997

    
998
    def test_multiple_range(self):
999
        #perform get with multiple range
1000
        ranges = ['0-499', '-500', '1000-']
1001
        headers = {'HTTP_RANGE' : 'bytes=%s' % ','.join(ranges)}
1002
        r = self.get_object(self.account,
1003
                        self.containers[1],
1004
                        self.objects[0]['name'],
1005
                        **headers)
1006
        
1007
        # assert partial content
1008
        self.assertEqual(r.status_code, 206)
1009
        
1010
        # assert Content-Type of the reply will be multipart/byteranges
1011
        self.assertTrue(r['Content-Type'])
1012
        content_type_parts = r['Content-Type'].split()
1013
        self.assertEqual(content_type_parts[0], ('multipart/byteranges;'))
1014
        
1015
        boundary = '--%s' %content_type_parts[1].split('=')[-1:][0]
1016
        cparts = r.content.split(boundary)[1:-1]
1017
        
1018
        # assert content parts are exactly 2
1019
        self.assertEqual(len(cparts), len(ranges))
1020
        
1021
        # for each content part assert headers
1022
        i = 0
1023
        for cpart in cparts:
1024
            content = cpart.split('\r\n')
1025
            headers = content[1:3]
1026
            content_range = headers[0].split(': ')
1027
            self.assertEqual(content_range[0], 'Content-Range')
1028
            
1029
            r = ranges[i].split('-')
1030
            if not r[0] and not r[1]:
1031
                pass
1032
            elif not r[0]:
1033
                start = len(self.objects[0]['data']) - int(r[1])
1034
                end = len(self.objects[0]['data'])
1035
            elif not r[1]:
1036
                start = int(r[0])
1037
                end = len(self.objects[0]['data'])
1038
            else:
1039
                start = int(r[0])
1040
                end = int(r[1]) + 1
1041
            fdata = self.objects[0]['data'][start:end]
1042
            sdata = '\r\n'.join(content[4:-1])
1043
            self.assertEqual(len(fdata), len(sdata))
1044
            self.assertEquals(fdata, sdata)
1045
            i+=1
1046

    
1047
    def test_multiple_range_not_satisfiable(self):
1048
        #perform get with multiple range
1049
        out_of_range = len(self.objects[0]['data']) + 1
1050
        ranges = ['0-499', '-500', '%d-' %out_of_range]
1051
        headers = {'HTTP_RANGE' : 'bytes=%s' % ','.join(ranges)}
1052
        r = self.get_object(self.account,
1053
                        self.containers[1],
1054
                        self.objects[0]['name'],
1055
                        **headers)
1056
        
1057
        # assert partial content
1058
        self.assertEqual(r.status_code, 416)
1059

    
1060
    def test_get_with_if_match(self):
1061
        #perform get with If-Match
1062
        headers = {'HTTP_IF_MATCH':self.objects[0]['hash']}
1063
        r = self.get_object(self.account,
1064
                        self.containers[1],
1065
                        self.objects[0]['name'],
1066
                        **headers)
1067
        #assert get success
1068
        self.assertEqual(r.status_code, 200)
1069
        
1070
        #assert content-type
1071
        self.assertEqual(r['Content-Type'],
1072
                         self.objects[0]['meta']['HTTP_CONTENT_TYPE'])
1073
        
1074
        #assert response content
1075
        self.assertEqual(self.objects[0]['data'].strip(), r.content.strip())
1076

    
1077
    def test_get_with_if_match_star(self):
1078
        #perform get with If-Match *
1079
        headers = {'HTTP_IF_MATCH':'*'}
1080
        r = self.get_object(self.account,
1081
                        self.containers[1],
1082
                        self.objects[0]['name'],
1083
                        **headers)
1084
        #assert get success
1085
        self.assertEqual(r.status_code, 200)
1086
        
1087
        #assert content-type
1088
        self.assertEqual(r['Content-Type'],
1089
                         self.objects[0]['meta']['HTTP_CONTENT_TYPE'])
1090
        
1091
        #assert response content
1092
        self.assertEqual(self.objects[0]['data'].strip(), r.content.strip())
1093

    
1094
    def test_get_with_multiple_if_match(self):
1095
        #perform get with If-Match
1096
        etags = [i['hash'] for i in self.objects if i]
1097
        etags = ','.join('"%s"' % etag for etag in etags)
1098
        headers = {'HTTP_IF_MATCH':etags}
1099
        r = self.get_object(self.account,
1100
                        self.containers[1],
1101
                        self.objects[0]['name'],
1102
                        **headers)
1103
        #assert get success
1104
        self.assertEqual(r.status_code, 200)
1105
        
1106
        #assert content-type
1107
        self.assertEqual(r['Content-Type'],
1108
                         self.objects[0]['meta']['HTTP_CONTENT_TYPE'])
1109
        
1110
        #assert content-type
1111
        self.assertEqual(r['Content-Type'],
1112
                         self.objects[0]['meta']['HTTP_CONTENT_TYPE'])
1113
        
1114
        #assert response content
1115
        self.assertEqual(self.objects[0]['data'].strip(), r.content.strip())
1116

    
1117
    def test_if_match_precondition_failed(self):
1118
        #perform get with If-Match
1119
        headers = {'HTTP_IF_MATCH':'123'}
1120
        r = self.get_object(self.account,
1121
                        self.containers[1],
1122
                        self.objects[0]['name'],
1123
                        **headers)
1124
        #assert precondition failed 
1125
        self.assertEqual(r.status_code, 412)
1126

    
1127
    def test_if_none_match(self):
1128
        #perform get with If-None-Match
1129
        headers = {'HTTP_IF_NONE_MATCH':'123'}
1130
        r = self.get_object(self.account,
1131
                        self.containers[1],
1132
                        self.objects[0]['name'],
1133
                        **headers)
1134
        
1135
        #assert get success
1136
        self.assertEqual(r.status_code, 200)
1137
        
1138
        #assert content-type
1139
        self.assertEqual(r['Content-Type'],
1140
                         self.objects[0]['meta']['HTTP_CONTENT_TYPE'])
1141

    
1142
    def test_if_none_match(self):
1143
        #perform get with If-None-Match *
1144
        headers = {'HTTP_IF_NONE_MATCH':'*'}
1145
        r = self.get_object(self.account,
1146
                        self.containers[1],
1147
                        self.objects[0]['name'],
1148
                        **headers)
1149
        
1150
        #assert get success
1151
        self.assertEqual(r.status_code, 304)
1152

    
1153
    def test_if_none_match_not_modified(self):
1154
        #perform get with If-None-Match
1155
        headers = {'HTTP_IF_NONE_MATCH':'%s' %self.objects[0]['hash']}
1156
        r = self.get_object(self.account,
1157
                        self.containers[1],
1158
                        self.objects[0]['name'],
1159
                        **headers)
1160
        
1161
        #assert not modified
1162
        self.assertEqual(r.status_code, 304)
1163
        self.assertEqual(r['ETag'], self.objects[0]['hash'])
1164

    
1165
    def test_if_modified_since(self):
1166
        t = datetime.datetime.utcnow()
1167
        t2 = t - datetime.timedelta(minutes=10)
1168
        
1169
        #modify the object
1170
        self.upload_object(self.account,
1171
                           self.containers[1],
1172
                           self.objects[0]['name'],
1173
                           self.objects[0]['data'][:200])
1174
        
1175
        for f in DATE_FORMATS:
1176
            past = t2.strftime(f)
1177
            
1178
            headers = {'HTTP_IF_MODIFIED_SINCE':'%s' %past}
1179
            r = self.get_object(self.account,
1180
                        self.containers[1],
1181
                        self.objects[0]['name'],
1182
                        **headers)
1183
            
1184
            #assert get success
1185
            self.assertEqual(r.status_code, 200)
1186
            
1187
            #assert content-type
1188
            self.assertEqual(r['Content-Type'],
1189
                             self.objects[0]['meta']['HTTP_CONTENT_TYPE'])   
1190

    
1191
    def test_if_modified_since_invalid_date(self):
1192
        headers = {'HTTP_IF_MODIFIED_SINCE':''}
1193
        r = self.get_object(self.account,
1194
                    self.containers[1],
1195
                    self.objects[0]['name'],
1196
                    **headers)
1197
        
1198
        #assert get success
1199
        self.assertEqual(r.status_code, 200)
1200
        
1201
        #assert content-type
1202
        self.assertEqual(r['Content-Type'],
1203
                         self.objects[0]['meta']['HTTP_CONTENT_TYPE'])
1204

    
1205
    def test_if_not_modified_since(self):
1206
        now = datetime.datetime.utcnow()
1207
        since = now + datetime.timedelta(1)
1208
        
1209
        for f in DATE_FORMATS:
1210
            headers = {'HTTP_IF_MODIFIED_SINCE':'%s' %since.strftime(f)}
1211
            r = self.get_object(self.account,
1212
                                self.containers[1],
1213
                                self.objects[0]['name'],
1214
                                **headers)
1215
            
1216
            #assert not modified
1217
            self.assertEqual(r.status_code, 304)
1218

    
1219
    def test_if_unmodified_since(self):
1220
        now = datetime.datetime.utcnow()
1221
        since = now + datetime.timedelta(1)
1222
        
1223
        for f in DATE_FORMATS:
1224
            headers = {'HTTP_IF_UNMODIFIED_SINCE':'%s' %since.strftime(f)}
1225
            r = self.get_object(self.account,
1226
                                self.containers[1],
1227
                                self.objects[0]['name'],
1228
                                **headers)
1229
            #assert success
1230
            self.assertEqual(r.status_code, 200)
1231
            self.assertEqual(self.objects[0]['data'], r.content)
1232
            
1233
            #assert content-type
1234
            self.assertEqual(r['Content-Type'],
1235
                             self.objects[0]['meta']['HTTP_CONTENT_TYPE'])
1236

    
1237
    def test_if_unmodified_since_precondition_failed(self):
1238
        t = datetime.datetime.utcnow()
1239
        t2 = t - datetime.timedelta(minutes=10)
1240
        
1241
        #modify the object
1242
        self.upload_object(self.account,
1243
                           self.containers[1],
1244
                           self.objects[0]['name'],
1245
                           self.objects[0]['data'][:200])
1246
        
1247
        for f in DATE_FORMATS:
1248
            past = t2.strftime(f)
1249
            
1250
            headers = {'HTTP_IF_UNMODIFIED_SINCE':'%s' %past}
1251
            r = self.get_object(self.account,
1252
                        self.containers[1],
1253
                        self.objects[0]['name'],
1254
                        **headers)
1255
            #assert get success
1256
            self.assertEqual(r.status_code, 412)
1257

    
1258
    def test_hashes(self):
1259
        l = 8388609
1260
        fname = 'largefile'
1261
        o = self.upload_random_data(self.account,
1262
                                self.containers[1],
1263
                                fname,
1264
                                l)
1265
        if o:
1266
            r = self.get_object(self.account,
1267
                                self.containers[1],
1268
                                fname,
1269
                                'json')
1270
            body = json.loads(r.content)
1271
            hashes = body['hashes']
1272
            block_size = body['block_size']
1273
            block_hash = body['block_hash']
1274
            block_num = l/block_size == 0 and l/block_size or l/block_size + 1
1275
            self.assertTrue(len(hashes), block_num)
1276
            i = 0
1277
            for h in hashes:
1278
                start = i * block_size
1279
                end = (i + 1) * block_size
1280
                hash = compute_block_hash(o['data'][start:end], block_hash)
1281
                self.assertEqual(h, hash)
1282
                i += 1
1283

    
1284
class ObjectPut(BaseTestCase):
1285
    def setUp(self):
1286
        BaseTestCase.setUp(self)
1287
        self.account = 'test'
1288
        self.container = 'c1'
1289
        self.create_container(self.account, self.container)
1290
        
1291
        self.src = os.path.join('.', 'api', 'tests.py')
1292
        self.dest = os.path.join('.', 'api', 'chunked_update_test_file')
1293
        create_chunked_update_test_file(self.src, self.dest)
1294

    
1295
    def tearDown(self):
1296
        r = self.list_objects(self.account, self.container)
1297
        for o in get_content_splitted(r):
1298
            self.delete_object(self.account, self.container, o)
1299
        self.delete_container(self.account, self.container)
1300
        
1301
        # delete test file
1302
        os.remove(self.dest)
1303

    
1304
    def test_upload(self):
1305
        filename = 'tests.py'
1306
        fullpath = os.path.join('.', 'api', filename) 
1307
        f = open(fullpath, 'r')
1308
        data = f.read()
1309
        hash = compute_md5_hash(data)
1310
        meta={'HTTP_ETAG':hash,
1311
              'HTTP_X_OBJECT_MANIFEST':123,
1312
              'HTTP_X_OBJECT_META_TEST':'test1'
1313
              }
1314
        type, enc = mimetypes.guess_type(fullpath)
1315
        meta['HTTP_CONTENT_TYPE'] = type and type or 'plain/text'
1316
        if enc:
1317
            meta['HTTP_CONTENT_ENCODING'] = enc
1318
        r = self.upload_object(self.account,
1319
                               self.container,
1320
                               filename,
1321
                               data,
1322
                               content_type=meta['HTTP_CONTENT_TYPE'],
1323
                               **meta)
1324
        self.assertEqual(r.status_code, 201)
1325
        r = self.get_object_meta(self.account, self.container, filename)
1326
        self.assertTrue(r['X-Object-Meta-Test'])
1327
        self.assertEqual(r['X-Object-Meta-Test'],
1328
                         meta['HTTP_X_OBJECT_META_TEST'])
1329
        
1330
        #assert uploaded content
1331
        r = self.get_object(self.account, self.container, filename)
1332
        self.assertEqual(os.path.getsize(fullpath), int(r['Content-Length']))
1333
        self.assertEqual(data.strip(), r.content.strip())
1334

    
1335
    def test_upload_unprocessable_entity(self):
1336
        filename = 'tests.py'
1337
        fullpath = os.path.join('.', 'api', filename) 
1338
        f = open(fullpath, 'r')
1339
        data = f.read()
1340
        meta={'HTTP_ETAG':'123',
1341
              'HTTP_X_OBJECT_MANIFEST':123,
1342
              'HTTP_X_OBJECT_META_TEST':'test1'
1343
              }
1344
        type, enc = mimetypes.guess_type(fullpath)
1345
        meta['HTTP_CONTENT_TYPE'] = type and type or 'plain/text'
1346
        if enc:
1347
            meta['HTTP_CONTENT_ENCODING'] = enc
1348
        r = self.upload_object(self.account,
1349
                               self.container,
1350
                               filename,
1351
                               data,
1352
                               content_type = meta['HTTP_CONTENT_TYPE'],
1353
                               **meta)
1354
        self.assertEqual(r.status_code, 422)
1355

    
1356
    def test_chucked_update(self):
1357
        objname = os.path.split(self.src)[-1:][0]
1358
        f = open(self.dest, 'r')
1359
        data = f.read()
1360
        meta = {}
1361
        type, enc = mimetypes.guess_type(self.dest)
1362
        meta['HTTP_CONTENT_TYPE'] = type and type or 'plain/text'
1363
        if enc:
1364
            meta['HTTP_CONTENT_ENCODING'] = enc
1365
        meta.update({'HTTP_TRANSFER_ENCODING':'chunked'})
1366
        r = self.upload_object(self.account,
1367
                               self.container,
1368
                               objname,
1369
                               data,
1370
                               content_type = 'plain/text',
1371
                               **meta)
1372
        self.assertEqual(r.status_code, 201)
1373
        
1374
        r = self.get_object(self.account,
1375
                            self.container,
1376
                            objname)
1377
        uploaded_data = r.content
1378
        f = open(self.src, 'r')
1379
        actual_data = f.read()
1380
        self.assertEqual(actual_data, uploaded_data)
1381

    
1382
class ObjectCopy(BaseTestCase):
1383
    def setUp(self):
1384
        BaseTestCase.setUp(self)
1385
        self.account = 'test'
1386
        self.containers = ['c1', 'c2']
1387
        for c in self.containers:
1388
            self.create_container(self.account, c)
1389
        self.obj = self.upload_os_file(self.account,
1390
                            self.containers[0],
1391
                            './api/tests.py')
1392

    
1393
    def tearDown(self):
1394
        for c in self.containers:
1395
            for o in get_content_splitted(self.list_objects(self.account, c)):
1396
                self.delete_object(self.account, c, o)
1397
            self.delete_container(self.account, c)
1398

    
1399
    def test_copy(self):
1400
        with AssertInvariant(self.get_object_meta, self.account,
1401
                             self.containers[0], self.obj['name']):
1402
            #perform copy
1403
            meta = {'HTTP_X_OBJECT_META_TEST':'testcopy'}
1404
            src_path = os.path.join('/', self.containers[0], self.obj['name'])
1405
            r = self.copy_object(self.account,
1406
                             self.containers[0],
1407
                             'testcopy',
1408
                             src_path,
1409
                             **meta)
1410
            #assert copy success
1411
            self.assertEqual(r.status_code, 201)
1412
            
1413
            #assert access the new object
1414
            r = self.get_object_meta(self.account,
1415
                                     self.containers[0],
1416
                                     'testcopy')
1417
            self.assertTrue(r['X-Object-Meta-Test'])
1418
            self.assertTrue(r['X-Object-Meta-Test'], 'testcopy')
1419
            
1420
            #assert etag is the same
1421
            self.assertEqual(r['ETag'], self.obj['hash'])
1422
            
1423
            #assert src object still exists
1424
            r = self.get_object_meta(self.account, self.containers[0],
1425
                                     self.obj['name'])
1426
            self.assertEqual(r.status_code, 204)
1427

    
1428
    def test_copy_from_different_container(self):
1429
        with AssertInvariant(self.get_object_meta,
1430
                             self.account,
1431
                             self.containers[0],
1432
                             self.obj['name']):
1433
            meta = {'HTTP_X_OBJECT_META_TEST':'testcopy'}
1434
            src_path = os.path.join('/', self.containers[0], self.obj['name'])
1435
            r = self.copy_object(self.account,
1436
                             self.containers[1],
1437
                             'testcopy',
1438
                             src_path,
1439
                             **meta)
1440
            self.assertEqual(r.status_code, 201)
1441
            
1442
            # assert updated metadata
1443
            r = self.get_object_meta(self.account,
1444
                                     self.containers[1],
1445
                                     'testcopy')
1446
            self.assertTrue(r['X-Object-Meta-Test'])
1447
            self.assertTrue(r['X-Object-Meta-Test'], 'testcopy')
1448
            
1449
            #assert src object still exists
1450
            r = self.get_object_meta(self.account, self.containers[0],
1451
                                     self.obj['name'])
1452
            self.assertEqual(r.status_code, 204)
1453

    
1454
    def test_copy_invalid(self):
1455
        #copy from invalid object
1456
        meta = {'HTTP_X_OBJECT_META_TEST':'testcopy'}
1457
        r = self.copy_object(self.account,
1458
                         self.containers[1],
1459
                         'testcopy',
1460
                         os.path.join('/', self.containers[0], 'test.py'),
1461
                         **meta)
1462
        self.assertItemNotFound(r)
1463
        
1464
        #copy from invalid container
1465
        meta = {'HTTP_X_OBJECT_META_TEST':'testcopy'}
1466
        src_path = os.path.join('/', self.containers[1], self.obj['name'])
1467
        r = self.copy_object(self.account,
1468
                         self.containers[1],
1469
                         'testcopy',
1470
                         src_path,
1471
                         **meta)
1472
        self.assertItemNotFound(r)
1473

    
1474
class ObjectMove(ObjectCopy):
1475
    def test_move(self):
1476
        #perform move
1477
        meta = {'HTTP_X_OBJECT_META_TEST':'testcopy'}
1478
        src_path = os.path.join('/', self.containers[0], self.obj['name'])
1479
        r = self.move_object(self.account,
1480
                         self.containers[0],
1481
                         'testcopy',
1482
                         src_path,
1483
                         **meta)
1484
        #assert successful move
1485
        self.assertEqual(r.status_code, 201)
1486
        
1487
        #assert updated metadata
1488
        r = self.get_object_meta(self.account,
1489
                                 self.containers[0],
1490
                                 'testcopy')
1491
        self.assertTrue(r['X-Object-Meta-Test'])
1492
        self.assertTrue(r['X-Object-Meta-Test'], 'testcopy')
1493
        
1494
        #assert src object no more exists
1495
        r = self.get_object_meta(self.account, self.containers[0],
1496
                                 self.obj['name'])
1497
        self.assertItemNotFound(r)
1498

    
1499
class ObjectPost(BaseTestCase):
1500
    def setUp(self):
1501
        BaseTestCase.setUp(self)
1502
        self.account = 'test'
1503
        self.containers = ['c1', 'c2']
1504
        for c in self.containers:
1505
            self.create_container(self.account, c)
1506
        self.obj = self.upload_os_file(self.account,
1507
                                       self.containers[0],
1508
                                       './api/tests.py')
1509

    
1510
    def tearDown(self):
1511
        for c in self.containers:
1512
            for o in get_content_splitted(self.list_objects(self.account, c)):
1513
                self.delete_object(self.account, c, o)
1514
            self.delete_container(self.account, c)
1515

    
1516
    def test_update_meta(self):
1517
        #perform update metadata
1518
        more = {'HTTP_X_OBJECT_META_FOO':'foo',
1519
                'HTTP_X_OBJECT_META_BAR':'bar'}
1520
        r = self.update_object(self.account,
1521
                                self.containers[0],
1522
                                self.obj['name'],
1523
                                **more)
1524
        #assert request accepted
1525
        self.assertEqual(r.status_code, 202)
1526
        
1527
        #assert old metadata are still there
1528
        r = self.get_object_meta(self.account, self.containers[0],
1529
                                 self.obj['name'])
1530
        self.assertTrue('X-Object-Meta-Test' not in r.items())
1531
        
1532
        #assert new metadata have been updated
1533
        for k,v in more.items():
1534
            key = '-'.join(elem.capitalize() for elem in k.split('_')[1:])
1535
            self.assertTrue(r[key])
1536
            self.assertTrue(r[key], v)
1537

    
1538
    def test_update_object(self,
1539
                           first_byte_pos=0,
1540
                           last_byte_pos=499,
1541
                           instance_length = True,
1542
                           content_length = 500):
1543
        l = len(self.obj['data'])
1544
        length =  instance_length and l or '*'
1545
        range = 'bytes %d-%d/%s' %(first_byte_pos,
1546
                                       last_byte_pos,
1547
                                       length)
1548
        partial = last_byte_pos - first_byte_pos + 1
1549
        data = get_random_data(partial)
1550
        more = {'HTTP_CONTENT_RANGE':range}
1551
        if content_length:
1552
            more.update({'CONTENT_LENGTH':'%s' % content_length})
1553
        
1554
        r = self.update_object(self.account,
1555
                                self.containers[0],
1556
                                self.obj['name'],
1557
                                data,
1558
                                'application/octet-stream',
1559
                                **more)
1560
        
1561
        if partial < 0 or (instance_length and l <= last_byte_pos):
1562
            self.assertEqual(r.status_code, 202)    
1563
        elif content_length and content_length != partial:
1564
            self.assertEqual(r.status_code, 400)
1565
        else:
1566
            self.assertEqual(r.status_code, 204)
1567
            
1568
            #check modified object
1569
            r = self.get_object(self.account,
1570
                            self.containers[0],
1571
                            self.obj['name'])
1572
            self.assertEqual(r.content[0:partial], data)
1573
            self.assertEqual(r.content[partial:l], self.obj['data'][partial:l])
1574

    
1575
    def test_update_object_no_content_length(self):
1576
        self.test_update_object(content_length = None)
1577

    
1578
    def test_update_object_invalid_content_length(self):
1579
        with AssertContentInvariant(self.get_object, self.account, self.containers[0],
1580
                            self.obj['name']):
1581
            self.test_update_object(content_length = 1000)
1582

    
1583
    def test_update_object_with_unknown_instance_length(self):
1584
        self.test_update_object(instance_length = False)
1585

    
1586
    def test_update_object_invalid_range(self):
1587
        with AssertContentInvariant(self.get_object, self.account, self.containers[0],
1588
                            self.obj['name']):
1589
            self.test_update_object(499, 0, True)
1590
    
1591
    def test_update_object_invalid_range_and_length(self):
1592
        with AssertContentInvariant(self.get_object, self.account, self.containers[0],
1593
                            self.obj['name']):
1594
            self.test_update_object(499, 0, True, -1)
1595
    
1596
    def test_update_object_invalid_range_with_no_content_length(self):
1597
        with AssertContentInvariant(self.get_object, self.account, self.containers[0],
1598
                            self.obj['name']):
1599
            self.test_update_object(499, 0, True, content_length = None)
1600
    
1601
    def test_update_object_out_of_limits(self):    
1602
        with AssertContentInvariant(self.get_object, self.account, self.containers[0],
1603
                            self.obj['name']):
1604
            l = len(self.obj['data'])
1605
            self.test_update_object(0, l+1, True)
1606

    
1607
    def test_append(self):
1608
        data = get_random_data(500)
1609
        more = {'CONTENT_LENGTH':'500',
1610
                'HTTP_CONTENT_RANGE':'bytes */*'}
1611
        
1612
        r = self.update_object(self.account,
1613
                                self.containers[0],
1614
                                self.obj['name'],
1615
                                data,
1616
                                'application/octet-stream',
1617
                                **more)
1618
        
1619
        self.assertEqual(r.status_code, 204)
1620
        
1621
        r = self.get_object(self.account,
1622
                                self.containers[0],
1623
                                self.obj['name'])
1624
        self.assertEqual(len(r.content), len(self.obj['data']) + 500)
1625
        self.assertEqual(r.content[:-500], self.obj['data'])
1626

    
1627
    def test_update_with_chunked_transfer(self):
1628
        data, pure = create_random_chunked_data()
1629
        dl = len(pure)
1630
        fl = len(self.obj['data'])
1631
        meta = {'HTTP_TRANSFER_ENCODING':'chunked',
1632
                'HTTP_CONTENT_RANGE':'bytes 0-/%d' %fl}
1633
        r = self.update_object(self.account,
1634
                                self.containers[0],
1635
                                self.obj['name'],
1636
                                data,
1637
                                'application/octet-stream',
1638
                                **meta)
1639
        
1640
        #check modified object
1641
        r = self.get_object(self.account,
1642
                        self.containers[0],
1643
                        self.obj['name'])
1644
        self.assertEqual(r.content[0:dl], pure)
1645
        self.assertEqual(r.content[dl:fl], self.obj['data'][dl:fl])
1646

    
1647
    def test_update_with_chunked_transfer_strict_range(self):
1648
        data, pure = create_random_chunked_data()
1649
        dl = len(pure) - 1
1650
        fl = len(self.obj['data'])
1651
        meta = {'HTTP_TRANSFER_ENCODING':'chunked',
1652
                'HTTP_CONTENT_RANGE':'bytes 0-%d/%d' %(dl, fl)}
1653
        r = self.update_object(self.account,
1654
                                self.containers[0],
1655
                                self.obj['name'],
1656
                                data,
1657
                                'application/octet-stream',
1658
                                **meta)
1659
        
1660
        #check modified object
1661
        r = self.get_object(self.account,
1662
                        self.containers[0],
1663
                        self.obj['name'])
1664
        self.assertEqual(r.content[0:dl+1], pure)
1665
        self.assertEqual(r.content[dl+1:fl], self.obj['data'][dl+1:fl])
1666

    
1667
class ObjectDelete(BaseTestCase):
1668
    def setUp(self):
1669
        BaseTestCase.setUp(self)
1670
        self.account = 'test'
1671
        self.containers = ['c1', 'c2']
1672
        for c in self.containers:
1673
            self.create_container(self.account, c)
1674
        self.obj = self.upload_os_file(self.account,
1675
                            self.containers[0],
1676
                            './api/tests.py')
1677

    
1678
    def tearDown(self):
1679
        for c in self.containers:
1680
            for o in get_content_splitted(self.list_objects(self.account, c)):
1681
                self.delete_object(self.account, c, o)
1682
            self.delete_container(self.account, c)
1683

    
1684
    def test_delete(self):
1685
        #perform delete object
1686
        r = self.delete_object(self.account, self.containers[0],
1687
                               self.obj['name'])
1688
        
1689
        #assert success
1690
        self.assertEqual(r.status_code, 204)
1691

    
1692
    def test_delete_invalid(self):
1693
        #perform delete object
1694
        r = self.delete_object(self.account, self.containers[1],
1695
                               self.obj['name'])
1696
        
1697
        #assert failure
1698
        self.assertItemNotFound(r)
1699

    
1700
class AssertInvariant(object):
1701
    def __init__(self, callable, *args, **kwargs):
1702
        self.callable = callable
1703
        self.args = args
1704
        self.kwargs = kwargs
1705

    
1706
    def __enter__(self):
1707
        self.items = self.callable(*self.args, **self.kwargs).items()
1708
        return self.items
1709

    
1710
    def __exit__(self, type, value, tb):
1711
        items = self.callable(*self.args, **self.kwargs).items()
1712
        assert self.items == items
1713

    
1714
class AssertContentInvariant(object):
1715
    def __init__(self, callable, *args, **kwargs):
1716
        self.callable = callable
1717
        self.args = args
1718
        self.kwargs = kwargs
1719

    
1720
    def __enter__(self):
1721
        self.content = self.callable(*self.args, **self.kwargs).content
1722
        return self.content
1723

    
1724
    def __exit__(self, type, value, tb):
1725
        content = self.callable(*self.args, **self.kwargs).content
1726
        assert self.content == content
1727

    
1728
def get_content_splitted(response):
1729
    if response:
1730
        return response.content.split('\n')
1731

    
1732
def compute_md5_hash(data):
1733
    md5 = hashlib.md5()
1734
    offset = 0
1735
    md5.update(data)
1736
    return md5.hexdigest().lower()
1737

    
1738
def compute_block_hash(data, algorithm):
1739
    h = hashlib.new(algorithm)
1740
    h.update(data.rstrip('\x00'))
1741
    return h.hexdigest()
1742

    
1743
def create_chunked_update_test_file(src, dest):
1744
    fr = open(src, 'r')
1745
    fw = open(dest, 'w')
1746
    data = fr.readline()
1747
    while data:
1748
        fw.write(hex(len(data)))
1749
        fw.write('\r\n')
1750
        fw.write(data)
1751
        data = fr.readline()
1752
    fw.write(hex(0))
1753
    fw.write('\r\n')
1754

    
1755
def create_random_chunked_data(rows=5):
1756
    i = 0
1757
    out = []
1758
    pure= []
1759
    while i < rows:
1760
        data = get_random_data(random.randint(1, 100))
1761
        out.append(hex(len(data)))
1762
        out.append(data)
1763
        pure.append(data)
1764
        i+=1
1765
    out.append(hex(0))
1766
    out.append('\r\n')
1767
    return '\r\n'.join(out), ''.join(pure)
1768

    
1769
def get_random_data(length=500):
1770
    char_set = string.ascii_uppercase + string.digits
1771
    return ''.join(random.choice(char_set) for x in range(length))
1772

    
1773
o_names = ['kate.jpg',
1774
           'kate_beckinsale.jpg',
1775
           'How To Win Friends And Influence People.pdf',
1776
           'moms_birthday.jpg',
1777
           'poodle_strut.mov',
1778
           'Disturbed - Down With The Sickness.mp3',
1779
           'army_of_darkness.avi',
1780
           'the_mad.avi',
1781
           'photos/animals/dogs/poodle.jpg',
1782
           'photos/animals/dogs/terrier.jpg',
1783
           'photos/animals/cats/persian.jpg',
1784
           'photos/animals/cats/siamese.jpg',
1785
           'photos/plants/fern.jpg',
1786
           'photos/plants/rose.jpg',
1787
           'photos/me.jpg']