Statistics
| Branch: | Tag: | Revision:

root / pithos / api / tests.py @ 79a6b6ee

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='', **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
                                    
674
                                    **meta)
675
        r = self.list_objects(self.account,
676
                          self.container[0],
677
                          meta='Quality,Stock')
678
        self.assertEqual(r.status_code, 200)
679
        obj = get_content_splitted(r)
680
        self.assertEqual(len(obj), 1)
681
        self.assertTrue(obj, self.obj[0]['name'])
682

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
880
class ObjectHead(BaseTestCase):
881
    pass
882

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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