Statistics
| Branch: | Tag: | Revision:

root / pithos / api / tests.py @ 02c0c3fa

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'] = '0000'
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
                'X-Object-Modified-By',
87
                'X-Object-Version',
88
                'X-Object-Version-Timestamp',)}
89
        self.contentTypes = {'xml':'application/xml',
90
                             'json':'application/json',
91
                             '':'text/plain'}
92
        self.extended = {
93
            'container':(
94
                'name',
95
                'count',
96
                'bytes',
97
                'last_modified'),
98
            'object':(
99
                'name',
100
                'hash',
101
                'bytes',
102
                'content_type',
103
                'content_encoding',
104
                'last_modified',)}
105
        self.return_codes = (400, 401, 404, 503,)
106

    
107
    def assertFault(self, response, status_code, name):
108
        self.assertEqual(response.status_code, status_code)
109

    
110
    def assertBadRequest(self, response):
111
        self.assertFault(response, 400, 'badRequest')
112

    
113
    def assertItemNotFound(self, response):
114
        self.assertFault(response, 404, 'itemNotFound')
115

    
116
    def assertUnauthorized(self, response):
117
        self.assertFault(response, 401, 'unauthorized')
118

    
119
    def assertServiceUnavailable(self, response):
120
        self.assertFault(response, 503, 'serviceUnavailable')
121

    
122
    def assertNonEmpty(self, response):
123
        self.assertFault(response, 409, 'nonEmpty')
124

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

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

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

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

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

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

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

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

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

    
215
    def get_object_meta(self, account, container, name):
216
        path = '/v1/%s/%s/%s' %(account, container, name)
217
        response = self.client.head(path)
218
        response.content = response.content
219
        self.assert_status(response, 200)
220
        return response
221

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

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

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

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

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

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

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

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

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

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

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

    
325
    def upload_random_data(self, account, container, name, length=1024,
326
                           meta={}):
327
        data = get_random_data(length)
328
        return self.upload_data(account, container, name, data, meta)
329

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

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

    
363
    def tearDown(self):
364
        for c in  get_content_splitted(self.list_containers(self.account)):
365
            self.delete_container(self.account, c)
366

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

    
379
    #def test_get_account_401(self):
380
    #    response = self.get_account_meta('non-existing-account')
381
    #    print response
382
    #    self.assertEqual(response.status_code, 401)
383

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

    
393
    def tearDown(self):
394
        for c in get_content_splitted(self.list_containers(self.account)):
395
            response = self.delete_container(self.account, c)
396

    
397
    def test_list(self):
398
        #list containers
399
        response = self.list_containers(self.account)
400
        containers = get_content_splitted(response)
401
        self.assertEquals(self.containers, containers)
402

    
403
    #def test_list_204(self):
404
    #    response = self.list_containers('non-existing-account')
405
    #    self.assertEqual(response.status_code, 204)
406

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

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

    
428
    #def test_extended_list(self):
429
    #    self.list_containers(self.account, limit=3, format='xml')
430
    #    self.list_containers(self.account, limit=3, format='json')
431

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

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

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

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

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

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

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

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

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

    
523
    def tearDown(self):
524
        for c in  get_content_splitted(self.list_containers(self.account)):
525
            self.delete_container(self.account, c)
526

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

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

    
543
class ContainerHead(BaseTestCase):
544
    def setUp(self):
545
        BaseTestCase.setUp(self)
546
        self.account = 'test'
547
        self.container = 'apples'
548
        self.create_container(self.account, self.container)
549

    
550
    def tearDown(self):
551
        for o in self.list_objects(self.account, self.container):
552
            self.delete_object(self.account, self.container, o)
553
        self.delete_container(self.account, self.container)
554

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

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

    
591
    def tearDown(self):
592
        for c in self.container:
593
            for obj in get_content_splitted(self.list_objects(self.account, c)):
594
                self.delete_object(self.account, c, obj)
595
            self.delete_container(self.account, c)
596

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
803
class ContainerPut(BaseTestCase):
804
    def setUp(self):
805
        BaseTestCase.setUp(self)
806
        self.account = 'test'
807
        self.containers = ['c1', 'c2']
808

    
809
    def tearDown(self):
810
        for c in self.containers:
811
            r = self.delete_container(self.account, c)
812

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

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

    
828
class ContainerPost(BaseTestCase):
829
    def setUp(self):
830
        BaseTestCase.setUp(self)
831
        self.account = 'test'
832
        self.container = 'apples'
833
        self.create_container(self.account, self.container)
834

    
835
    def tearDown(self):
836
        for o in self.list_objects(self.account, self.container):
837
            self.delete_object(self.account, self.container, o)
838
        self.delete_container(self.account, self.container)
839

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

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

    
862
    def tearDown(self):
863
        for c in self.containers:
864
            for o in get_content_splitted(self.list_objects(self.account, c)):
865
                self.delete_object(self.account, c, o)
866
            self.delete_container(self.account, c)
867

    
868
    def test_delete(self):
869
        r = self.delete_container(self.account, self.containers[0])
870
        self.assertEqual(r.status_code, 204)
871

    
872
    def test_delete_non_empty(self):
873
        r = self.delete_container(self.account, self.containers[1])
874
        self.assertNonEmpty(r)
875

    
876
    def test_delete_invalid(self):
877
        self.assertItemNotFound(self.delete_container(self.account, 'c3'))
878

    
879
class ObjectHead(BaseTestCase):
880
    pass
881

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

    
900
    def tearDown(self):
901
        for c in self.containers:
902
            for o in get_content_splitted(self.list_objects(self.account, c)):
903
                self.delete_object(self.account, c, o)
904
            self.delete_container(self.account, c)
905

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

    
919
    def test_get_invalid(self):
920
        r = self.get_object(self.account,
921
                            self.containers[0],
922
                            self.objects[0]['name'])
923
        self.assertItemNotFound(r)
924

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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