commit 3a19e99b8d4f814640a27da380f8b163388619dc
Author: Sofia Papagiannaki <spapagian@grnet.gr>
Date:   Tue Jul 2 16:56:05 2013 +0300

    pithos: update tests

diff --git a/contrib/snf-pithos-tools/pithos/tools/lib/hashmap.py b/contrib/snf-pithos-tools/pithos/tools/lib/hashmap.py
index 977d8fc..a580c30 100644
--- a/contrib/snf-pithos-tools/pithos/tools/lib/hashmap.py
+++ b/contrib/snf-pithos-tools/pithos/tools/lib/hashmap.py
@@ -88,7 +88,7 @@ class HashMap(list):
             self.size += len(block)
 
 
-def merkle(path, blocksize=4194304, blockhash='sha256'):
+def merkle(fp, blocksize=4194304, blockhash='sha256'):
     hashes = HashMap(blocksize, blockhash)
-    hashes.load(open(path))
+    hashes.load(fp)
     return hexlify(hashes.hash())
diff --git a/snf-django-lib/snf_django/utils/testing.py b/snf-django-lib/snf_django/utils/testing.py
index 946a844..f13966c 100644
--- a/snf-django-lib/snf_django/utils/testing.py
+++ b/snf-django-lib/snf_django/utils/testing.py
@@ -138,7 +138,7 @@ def astakos_user(user):
                     "system": {
                         "pithos.diskspace": {
                             "usage": 0,
-                            "limit": 1073741824,
+                            "limit": 1073741824,  # 1GB
                             "pending": 0
                         }
                     }
diff --git a/snf-pithos-app/pithos/api/test/__init__.py b/snf-pithos-app/pithos/api/test/__init__.py
index b0d2cf6..5e889e6 100644
--- a/snf-pithos-app/pithos/api/test/__init__.py
+++ b/snf-pithos-app/pithos/api/test/__init__.py
@@ -39,8 +39,8 @@ from xml.dom import minidom
 
 from snf_django.utils.testing import with_settings, astakos_user
 
-from pithos.backends.random_word import get_random_word
 from pithos.api import settings as pithos_settings
+from pithos.api.test.util import is_date, get_random_data
 
 from synnefo.lib.services import get_service_path
 from synnefo.lib import join_urls
@@ -51,11 +51,11 @@ from django.conf import settings
 
 import django.utils.simplejson as json
 
-import re
 import random
 import threading
 import functools
 
+
 pithos_test_settings = functools.partial(with_settings, pithos_settings)
 
 DATE_FORMATS = ["%a %b %d %H:%M:%S %Y",
@@ -87,7 +87,6 @@ return_codes = (400, 401, 403, 404, 503)
 
 
 class PithosAPITest(TestCase):
-    #TODO unauthorized request
     def setUp(self):
         pithos_settings.BACKEND_DB_MODULE = 'pithos.backends.lib.sqlalchemy'
         pithos_settings.BACKEND_DB_CONNECTION = django_to_sqlalchemy()
@@ -212,6 +211,38 @@ class PithosAPITest(TestCase):
                 if not k.startswith('X-Account-Group-')])
         return headers
 
+    def get_container_info(self, container, until=None):
+        url = join_urls(self.pithos_path, self.user, container)
+        if until is not None:
+            parts = list(urlsplit(url))
+            parts[3] = urlencode({
+                'until': until
+            })
+            url = urlunsplit(parts)
+        r = self.head(url)
+        self.assertEqual(r.status_code, 204)
+        return r
+
+    def get_container_meta(self, container, until=None):
+        r = self.get_container_info(container, until=until)
+        headers = dict(r._headers.values())
+        map(headers.pop,
+            [k for k in headers.keys()
+                if not k.startswith('X-Container-Meta-')])
+        return headers
+
+    def update_container_meta(self, container, meta):
+        kwargs = dict(
+            ('HTTP_X_CONTAINER_META_%s' % k, str(v)) for k, v in meta.items())
+        url = join_urls(self.pithos_path, self.user, container)
+        r = self.post('%s?update=' % url, **kwargs)
+        self.assertEqual(r.status_code, 202)
+        container_meta = self.get_container_meta(container)
+        (self.assertTrue('X-Container-Meta-%s' % k in container_meta) for
+            k in meta.keys())
+        (self.assertEqual(container_meta['X-Container-Meta-%s' % k], v) for
+            k, v in meta.items())
+
     def list_containers(self, format='json', headers={}, **params):
         _url = join_urls(self.pithos_path, self.user)
         parts = list(urlsplit(_url))
@@ -254,26 +285,57 @@ class PithosAPITest(TestCase):
         self.assertTrue(r.status_code in (202, 201))
         return r
 
-    def upload_object(self, cname, oname=None, **meta):
-        oname = oname or get_random_word(8)
-        data = get_random_word(length=random.randint(1, 1024))
+    def upload_object(self, cname, oname=None, length=1024, verify=True,
+                      **meta):
+        oname = oname or get_random_data(8)
+        length = length or random.randint(1, 1024)
+        data = get_random_data(length=length)
         headers = dict(('HTTP_X_OBJECT_META_%s' % k.upper(), v)
                        for k, v in meta.iteritems())
         url = join_urls(self.pithos_path, self.user, cname, oname)
         r = self.put(url, data=data, **headers)
-        self.assertEqual(r.status_code, 201)
+        if verify:
+            self.assertEqual(r.status_code, 201)
+        return oname, data, r
+
+    def update_object_data(self, cname, oname=None, length=None,
+                           content_type=None, content_range=None,
+                           verify=True, **meta):
+        oname = oname or get_random_data(8)
+        length = length or random.randint(1, 1024)
+        content_type = content_type or 'application/octet-stream'
+        data = get_random_data(length=length)
+        headers = dict(('HTTP_X_OBJECT_META_%s' % k.upper(), v)
+                       for k, v in meta.iteritems())
+        if content_range:
+            headers['HTTP_CONTENT_RANGE'] = content_range
+        url = join_urls(self.pithos_path, self.user, cname, oname)
+        r = self.post(url, data=data, content_type=content_type, **headers)
+        if verify:
+            self.assertEqual(r.status_code, 204)
         return oname, data, r
 
-    def create_folder(self, cname, oname=get_random_word(8), **headers):
+    def append_object_data(self, cname, oname=None, length=None,
+                           content_type=None):
+        return self.update_object_data(cname, oname=oname,
+                                       length=length,
+                                       content_type=content_type,
+                                       content_range='bytes */*')
+
+    def create_folder(self, cname, oname=None, **headers):
+        oname = oname or get_random_data(8)
         url = join_urls(self.pithos_path, self.user, cname, oname)
         r = self.put(url, data='', content_type='application/directory',
                      **headers)
         self.assertEqual(r.status_code, 201)
         return oname, r
 
-    def list_objects(self, cname):
+    def list_objects(self, cname, prefix=None):
         url = join_urls(self.pithos_path, self.user, cname)
-        r = self.get('%s?format=json' % url)
+        path = '%s?format=json' % url
+        if prefix is not None:
+            path = '%s&prefix=%s' % (path, prefix)
+        r = self.get(path)
         self.assertTrue(r.status_code in (200, 204))
         try:
             objects = json.loads(r.content)
@@ -281,6 +343,40 @@ class PithosAPITest(TestCase):
             self.fail('json format expected')
         return objects
 
+    def get_object_info(self, container, object, version=None, until=None):
+        url = join_urls(self.pithos_path, self.user, container, object)
+        if until is not None:
+            parts = list(urlsplit(url))
+            parts[3] = urlencode({
+                'until': until
+            })
+            url = urlunsplit(parts)
+        if version:
+            url = '%s?version=%s' % (url, version)
+        r = self.head(url)
+        self.assertEqual(r.status_code, 200)
+        return r
+
+    def get_object_meta(self, container, object, version=None, until=None):
+        r = self.get_object_info(container, object, version, until=until)
+        headers = dict(r._headers.values())
+        map(headers.pop,
+            [k for k in headers.keys()
+                if not k.startswith('X-Object-Meta-')])
+        return headers
+
+    def update_object_meta(self, container, object, meta):
+        kwargs = dict(
+            ('HTTP_X_OBJECT_META_%s' % k, str(v)) for k, v in meta.items())
+        url = join_urls(self.pithos_path, self.user, container, object)
+        r = self.post('%s?update=' % url, content_type='', **kwargs)
+        self.assertEqual(r.status_code, 202)
+        object_meta = self.get_object_meta(container, object)
+        (self.assertTrue('X-Objecr-Meta-%s' % k in object_meta) for
+            k in meta.keys())
+        (self.assertEqual(object_meta['X-Object-Meta-%s' % k], v) for
+            k, v in meta.items())
+
     def assert_status(self, status, codes):
         l = [elem for elem in return_codes]
         if isinstance(codes, list):
@@ -339,6 +435,26 @@ class AssertMappingInvariant(object):
             assert(k in map), '%s not in map' % k
             assert v == map[k]
 
+
+class AssertUUidInvariant(object):
+    def __init__(self, callable, *args, **kwargs):
+        self.callable = callable
+        self.args = args
+        self.kwargs = kwargs
+
+    def __enter__(self):
+        self.map = self.callable(*self.args, **self.kwargs)
+        assert('x-object-uuid' in self.map)
+        self.uuid = self.map['x-object-uuid']
+        return self.map
+
+    def __exit__(self, type, value, tb):
+        map = self.callable(*self.args, **self.kwargs)
+        assert('x-object-uuid' in self.map)
+        uuid = map['x-object-uuid']
+        assert(uuid == self.uuid)
+
+
 django_sqlalchemy_engines = {
     'django.db.backends.postgresql_psycopg2': 'postgresql+psycopg2',
     'django.db.backends.postgresql': 'postgresql',
@@ -365,47 +481,6 @@ def django_to_sqlalchemy():
         return '%(scheme)s://%(user)s:%(pwd)s@%(host)s:%(port)s/%(name)s' % d
 
 
-def is_date(date):
-    __D = r'(?P<day>\d{2})'
-    __D2 = r'(?P<day>[ \d]\d)'
-    __M = r'(?P<mon>\w{3})'
-    __Y = r'(?P<year>\d{4})'
-    __Y2 = r'(?P<year>\d{2})'
-    __T = r'(?P<hour>\d{2}):(?P<min>\d{2}):(?P<sec>\d{2})'
-    RFC1123_DATE = re.compile(r'^\w{3}, %s %s %s %s GMT$' % (
-        __D, __M, __Y, __T))
-    RFC850_DATE = re.compile(r'^\w{6,9}, %s-%s-%s %s GMT$' % (
-        __D, __M, __Y2, __T))
-    ASCTIME_DATE = re.compile(r'^\w{3} %s %s %s %s$' % (
-        __M, __D2, __T, __Y))
-    for regex in RFC1123_DATE, RFC850_DATE, ASCTIME_DATE:
-        m = regex.match(date)
-        if m is not None:
-            return True
-    return False
-
-
-def strnextling(prefix):
-    """Return the first unicode string
-       greater than but not starting with given prefix.
-       strnextling('hello') -> 'hellp'
-    """
-    if not prefix:
-        ## all strings start with the null string,
-        ## therefore we have to approximate strnextling('')
-        ## with the last unicode character supported by python
-        ## 0x10ffff for wide (32-bit unicode) python builds
-        ## 0x00ffff for narrow (16-bit unicode) python builds
-        ## We will not autodetect. 0xffff is safe enough.
-        return unichr(0xffff)
-    s = prefix[:-1]
-    c = ord(prefix[-1])
-    if c >= 0xffff:
-        raise RuntimeError
-    s += unichr(c + 1)
-    return s
-
-
 def test_concurrently(times=2):
     """
     Add this decorator to small pieces of code that you want to test
diff --git a/snf-pithos-app/pithos/api/test/accounts.py b/snf-pithos-app/pithos/api/test/accounts.py
index 414c8ea..b546a4d 100644
--- a/snf-pithos-app/pithos/api/test/accounts.py
+++ b/snf-pithos-app/pithos/api/test/accounts.py
@@ -135,6 +135,50 @@ class AccountGet(PithosAPITest):
         self.assertEquals(containers,
                           ['apples', 'bananas', 'kiwis', 'oranges', 'pears'])
 
+    def test_list_shared(self):
+        # upload and publish object
+        oname, data, resp = self.upload_object('apples')
+        url = join_urls(self.pithos_path, self.user, 'apples', oname)
+        r = self.post(url, content_type='', HTTP_X_OBJECT_PUBLIC='true')
+        self.assertEqual(r.status_code, 202)
+
+        # upload and share object
+        other, data, resp = self.upload_object('bananas')
+        url = join_urls(self.pithos_path, self.user, 'bananas', other)
+        r = self.post(url, content_type='', HTTP_X_OBJECT_SHARING='read=alice')
+        self.assertEqual(r.status_code, 202)
+
+        url = join_urls(self.pithos_path, self.user)
+
+        # list shared containers
+        r = self.get('%s?public=' % url)
+        objects = r.content.split('\n')
+        if '' in objects:
+            objects.remove('')
+        self.assertEqual(objects, ['apples'])
+
+        # list shared containers
+        r = self.get('%s?shared=' % url)
+        objects = r.content.split('\n')
+        if '' in objects:
+            objects.remove('')
+        self.assertEqual(objects, ['bananas'])
+
+        # list public and shared containers
+        r = self.get('%s?public=&shared=' % url)
+        objects = r.content.split('\n')
+        if '' in objects:
+            objects.remove('')
+        self.assertEqual(objects, ['apples', 'bananas'])
+
+        # assert forbidden public container listing
+        r = self.get('%s?public=' % url, user='alice')
+        self.assertEqual(r.status_code, 403)
+
+        # assert forbidden shared & public container listing
+        r = self.get('%s?public=&shared=' % url, user='alice')
+        self.assertEqual(r.status_code, 403)
+
     def test_list_with_limit(self):
         containers = self.list_containers(format=None, limit=2)
         self.assertEquals(len(containers), 2)
@@ -194,7 +238,7 @@ class AccountGet(PithosAPITest):
 
         # Check modified
         for t in t1_formats:
-            r = self.get('%s' % url, HTTP_IF_MODIFIED_SINCE=t)
+            r = self.get(url, HTTP_IF_MODIFIED_SINCE=t)
             self.assertEqual(r.status_code, 200)
             self.assertEqual(
                 r.content.split('\n')[:-1],
@@ -211,7 +255,7 @@ class AccountGet(PithosAPITest):
 
         # Check modified
         for t in t2_formats:
-            r = self.get('%s' % url, HTTP_IF_MODIFIED_SINCE=t)
+            r = self.get(url, HTTP_IF_MODIFIED_SINCE=t)
             self.assertEqual(r.status_code, 200)
             self.assertEqual(
                 r.content.split('\n')[:-1],
@@ -219,7 +263,7 @@ class AccountGet(PithosAPITest):
 
     def test_if_modified_since_invalid_date(self):
         url = join_urls(self.pithos_path, self.user)
-        r = self.get('%s' % url, HTTP_IF_MODIFIED_SINCE='Monday')
+        r = self.get(url, HTTP_IF_MODIFIED_SINCE='Monday')
         self.assertEqual(r.status_code, 200)
         self.assertEqual(
             r.content.split('\n')[:-1],
diff --git a/snf-pithos-app/pithos/api/test/containers.py b/snf-pithos-app/pithos/api/test/containers.py
index 6bb3c10..2679c2a 100644
--- a/snf-pithos-app/pithos/api/test/containers.py
+++ b/snf-pithos-app/pithos/api/test/containers.py
@@ -34,9 +34,9 @@
 # interpreted as representing official policies, either expressed
 # or implied, of GRNET S.A.
 
-from pithos.api.test import PithosAPITest, DATE_FORMATS, o_names,\
-    strnextling, pithos_settings, pithos_test_settings
-from pithos.backends.random_word import get_random_word
+from pithos.api.test import (PithosAPITest, DATE_FORMATS, o_names,
+                             pithos_settings, pithos_test_settings)
+from pithos.api.test.util import strnextling, get_random_data
 
 from synnefo.lib import join_urls
 
@@ -45,6 +45,7 @@ from django.http import urlencode
 
 from xml.dom import minidom
 from urllib import quote
+import time as _time
 
 import random
 import datetime
@@ -121,7 +122,8 @@ class ContainerGet(PithosAPITest):
         r = self.get('%s?shared=' % url)
         self.assertEqual(r.status_code, 200)
         objects = r.content.split('\n')
-        objects.remove('')
+        if '' in objects:
+            objects.remove('')
         self.assertEqual([oname], objects)
 
         # list detailed shared and assert only the shared object is returned
@@ -150,8 +152,7 @@ class ContainerGet(PithosAPITest):
             self.fail('json format expected')
         self.assertEqual([oname], [o['name'] for o in objects])
         self.assertTrue('x_object_sharing' in objects[0])
-        # TODO
-        #self.assertTrue('x_object_public' in objects[0])
+        self.assertTrue('x_object_public' in objects[0])
 
         # create child object
         descendant = strnextling(oname)
@@ -161,21 +162,23 @@ class ContainerGet(PithosAPITest):
         r = self.get('%s?shared=' % url)
         self.assertEqual(r.status_code, 200)
         objects = r.content.split('\n')
-        objects.remove('')
+        if '' in objects:
+            objects.remove('')
         self.assertTrue(oname in objects)
         self.assertTrue(descendant not in objects)
 
         # check folder inheritance
         oname, _ = self.create_folder(cname, HTTP_X_OBJECT_SHARING='read=*')
         # create child object
-        descendant = '%s/%s' % (oname, get_random_word(8))
+        descendant = '%s/%s' % (oname, get_random_data(8))
         self.upload_object(cname, descendant)
         # request shared
         url = join_urls(self.pithos_path, self.user, cname)
         r = self.get('%s?shared=' % url)
         self.assertEqual(r.status_code, 200)
         objects = r.content.split('\n')
-        objects.remove('')
+        if '' in objects:
+            objects.remove('')
         self.assertTrue(oname in objects)
         self.assertTrue(descendant in objects)
 
@@ -184,12 +187,14 @@ class ContainerGet(PithosAPITest):
         cname = self.cnames[0]
         onames = self.objects[cname].keys()
         oname = onames.pop()
+        other = onames.pop()
+
+        # publish an object
         url = join_urls(self.pithos_path, self.user, cname, oname)
         r = self.post(url, content_type='', HTTP_X_OBJECT_PUBLIC='true')
         self.assertEqual(r.status_code, 202)
 
         # share another
-        other = onames.pop()
         url = join_urls(self.pithos_path, self.user, cname, other)
         r = self.post(url, content_type='', HTTP_X_OBJECT_SHARING='read=alice')
         self.assertEqual(r.status_code, 202)
@@ -200,7 +205,7 @@ class ContainerGet(PithosAPITest):
         objects = r.content.split('\n')
         self.assertEqual(r.status_code, 200)
         self.assertTrue(oname in r.content.split('\n'))
-        (self.assertTrue(o not in objects) for o in o_names[1:])
+        (self.assertTrue(object not in objects) for object in o_names[1:])
 
         # list detailed public and assert only the public object is returned
         url = join_urls(self.pithos_path, self.user, cname)
@@ -210,7 +215,7 @@ class ContainerGet(PithosAPITest):
             objects = json.loads(r.content)
         except:
             self.fail('json format expected')
-        self.assertEqual([oname], [o['name'] for o in objects])
+        self.assertEqual([oname], [obj['name'] for obj in objects])
         self.assertTrue('x_object_sharing' not in objects[0])
         self.assertTrue('x_object_public' in objects[0])
 
@@ -226,7 +231,7 @@ class ContainerGet(PithosAPITest):
             objects = json.loads(r.content)
         except:
             self.fail('json format expected')
-        self.assertEqual([oname], [o['name'] for o in objects])
+        self.assertEqual([oname], [obj['name'] for obj in objects])
         self.assertTrue('x_object_sharing' in objects[0])
         self.assertTrue('x_object_public' in objects[0])
 
@@ -237,18 +242,9 @@ class ContainerGet(PithosAPITest):
         r = self.get('%s?public=&format=json' % url, user='bob')
         self.assertEqual(r.status_code, 403)
 
-        # Assert listing the container public contents to shared users
+        # Assert forbidden public object listing to shared users
         r = self.get('%s?public=&format=json' % url, user='alice')
-        self.assertEqual(r.status_code, 200)
-        try:
-            objects = json.loads(r.content)
-        except:
-            self.fail('json format expected')
-        # TODO
-        #self.assertEqual([oname], [o['name'] for o in objects])
-        self.assertTrue('x_object_sharing' in objects[0])
-        # assert public is not returned though
-        self.assertTrue('x_object_public' not in objects[0])
+        self.assertEqual(r.status_code, 403)
 
         # create child object
         descendant = strnextling(oname)
@@ -256,7 +252,8 @@ class ContainerGet(PithosAPITest):
         # request public and assert child obejct is not listed
         r = self.get('%s?public=' % url)
         objects = r.content.split('\n')
-        objects.remove('')
+        if '' in objects:
+            objects.remove('')
         self.assertEqual(r.status_code, 200)
         self.assertTrue(oname in objects)
         (self.assertTrue(o not in objects) for o in o_names[1:])
@@ -264,7 +261,7 @@ class ContainerGet(PithosAPITest):
         # test folder inheritance
         oname, _ = self.create_folder(cname, HTTP_X_OBJECT_PUBLIC='true')
         # create child object
-        descendant = '%s/%s' % (oname, get_random_word(8))
+        descendant = '%s/%s' % (oname, get_random_data(8))
         self.upload_object(cname, descendant)
         # request public
         r = self.get('%s?public=' % url)
@@ -273,87 +270,69 @@ class ContainerGet(PithosAPITest):
         self.assertTrue(oname in objects)
         self.assertTrue(descendant not in objects)
 
-#    def test_list_shared_public(self):
-#        # publish an object
-#        cname = self.cnames[0]
-#        onames = self.objects[cname].keys()
-#        oname = onames.pop()
-#        r = self.post('/v1/%s/%s/%s' % (self.user, cname, oname),
-#                      content_type='',
-#                      HTTP_X_OBJECT_PUBLIC='true')
-#        self.assertEqual(r.status_code, 202)
-#
-#        # share another
-#        other = onames.pop()
-#        r = self.post('/v1/%s/%s/%s' % (self.user, cname, other),
-#                      content_type='',
-#                      HTTP_X_OBJECT_SHARING='read=alice')
-#        self.assertEqual(r.status_code, 202)
-#
-#        # list shared and public objects and assert object is listed
-#        r = self.get('/v1/%s/%s?shared=&public=&format=json' % (
-#            self.user, cname))
-#        self.assertEqual(r.status_code, 200)
-#        objects = json.loads(r.content)
-#        self.assertEqual([o['name'] for o in objects], sorted([oname, other]))
-#        for o in objects:
-#            if o['name'] == oname:
-#                self.assertTrue('x_object_public' in objects[0])
-#            elif o['name'] == other:
-#                self.assertTrue('x_object_sharing' in objects[1])
-#
-#        # assert not listing shared and public to a not shared user
-#        r = self.get('/v1/%s/%s?shared=&public=&format=json' % (
-#            self.user, cname), user='bob')
-#        self.assertEqual(r.status_code, 403)
-#
-#        # assert listing shared and public to a shared user
-#        r = self.get('/v1/%s/%s?shared=&public=&format=json' % (
-#            self.user, cname), user='alice')
-#        self.assertEqual(r.status_code, 200)
-#        try:
-#            objects = json.loads(r.content)
-#        except:
-#            self.fail('json format expected')
-#        self.assertEqual([o['name'] for o in objects], sorted([oname, other]))
-#
-#        # create child object
-#        descentant1 = strnextling(oname)
-#        self.upload_object(cname, descendant1)
-#        descentant2 = strnextling(other)
-#        self.upload_object(cname, descendant2)
-#        r = self.get('/v1/%s/%s?shared=&public=&format=json' % (
-#            self.user, cname), user='alice')
-#        self.assertEqual(r.status_code, 200)
-#        try:
-#            objects = json.loads(r.content)
-#        except:
-#            self.fail('json format expected')
-#        self.assertEqual([o['name'] for o in objects], [oname])
-#
-#        # test inheritance
-#        oname1, _ = self.create_folder(cname,
-#                                       HTTP_X_OBJECT_SHARING='read=alice')
-#        # create child object
-#        descendant1 = '%s/%s' % (oname, get_random_word(8))
-#        self.upload_object(cname, descendant1)
-#
-#        oname2, _ = self.create_folder(cname,
-#                                       HTTP_X_OBJECT_PUBLIC='true')
-#        # create child object
-#        descendant2 = '%s/%s' % (oname, get_random_word(8))
-#        self.upload_object(cname, descendant2)
-#
-#
-#        o = self.upload_random_data(self.container[1], 'folder2/object')
-#        objs = self.client.list_objects(
-#            self.container[1], shared=True, public=True)
-#        self.assertEqual(objs, ['folder1', 'folder1/object', 'folder2'])
-#        objs = cl.list_objects(
-#            self.container[1], shared=True, public=True, account=get_user()
-#        )
-#        self.assertEqual(objs, ['folder1', 'folder1/object'])
-#
+    def test_list_shared_public(self):
+        # publish an object
+        cname = self.cnames[0]
+        container_url = join_urls(self.pithos_path, self.user, cname)
+        onames = self.objects[cname].keys()
+        oname = onames.pop()
+        url = join_urls(container_url, oname)
+        r = self.post(url, content_type='', HTTP_X_OBJECT_PUBLIC='true')
+        self.assertEqual(r.status_code, 202)
+
+        # share another
+        other = onames.pop()
+        url = join_urls(container_url, other)
+        r = self.post(url, content_type='', HTTP_X_OBJECT_SHARING='read=alice')
+        self.assertEqual(r.status_code, 202)
+
+        # list shared and public objects and assert object is listed
+        r = self.get('%s?shared=&public=&format=json' % container_url)
+        self.assertEqual(r.status_code, 200)
+        objects = json.loads(r.content)
+        self.assertEqual([o['name'] for o in objects], sorted([oname, other]))
+        for o in objects:
+            if o['name'] == oname:
+                self.assertTrue('x_object_public' in o.keys())
+            elif o['name'] == other:
+                self.assertTrue('x_object_sharing' in o.keys())
+
+        # assert not listing shared and public to a not shared user
+        r = self.get('%s?shared=&public=&format=json' % container_url,
+                     user='bob')
+        self.assertEqual(r.status_code, 403)
+
+        # assert not listing public to a shared user
+        r = self.get('%s?shared=&public=&format=json' % container_url,
+                     user='alice')
+        self.assertEqual(r.status_code, 403)
+
+        # create child object
+        descendant = strnextling(oname)
+        self.upload_object(cname, descendant)
+        # request public and assert child obejct is not listed
+        r = self.get('%s?shared=&public=' % container_url)
+        objects = r.content.split('\n')
+        if '' in objects:
+            objects.remove('')
+        self.assertEqual(r.status_code, 200)
+        self.assertTrue(oname in objects)
+        (self.assertTrue(o not in objects) for o in o_names[1:])
+
+        # test folder inheritance
+        oname, _ = self.create_folder(cname, HTTP_X_OBJECT_PUBLIC='true')
+        # create child object
+        descendant = '%s/%s' % (oname, get_random_data(8))
+        self.upload_object(cname, descendant)
+        # request public
+        r = self.get('%s?shared=&public=' % container_url)
+        self.assertEqual(r.status_code, 200)
+        objects = r.content.split('\n')
+        if '' in objects:
+            objects.remove('')
+        self.assertTrue(oname in objects)
+        self.assertTrue(descendant not in objects)
+
     def test_list_objects(self):
         cname = self.cnames[0]
         url = join_urls(self.pithos_path, self.user, cname)
@@ -366,7 +345,7 @@ class ContainerGet(PithosAPITest):
 
     def test_list_objects_containing_slash(self):
         self.create_container('test')
-        self.upload_object('test', '/objectname')
+        self.upload_object('test', quote('/objectname', ''))
 
         url = join_urls(self.pithos_path, self.user, 'test')
 
@@ -517,21 +496,22 @@ class ContainerGet(PithosAPITest):
         cname = 'apples'
         container_url = join_urls(self.pithos_path, self.user, cname)
 
-        oname1 = self.objects[cname].keys().pop()
-        url = join_urls(container_url, oname1)
-        self.post(url, content_type='', HTTP_X_OBJECT_META_QUALITY='aaa')
+        onames = self.objects[cname].keys()
+        url = join_urls(container_url, onames[0])
+        r = self.post(url, content_type='', HTTP_X_OBJECT_META_QUALITY='aaa')
+        self.assertEqual(r.status_code, 202)
 
-        oname2 = self.objects[cname].keys().pop()
-        url = join_urls(container_url, cname, oname2)
-        self.post(url, content_type='', HTTP_X_OBJECT_META_QUALITY='ab')
+        url = join_urls(container_url, onames[1])
+        r = self.post(url, content_type='', HTTP_X_OBJECT_META_QUALITY='ab')
+        self.assertEqual(r.status_code, 202)
 
-        oname3 = self.objects[cname].keys().pop()
-        url = join_urls(container_url, oname3)
-        self.post(url, content_type='', HTTP_X_OBJECT_META_STOCK='100')
+        url = join_urls(container_url, onames[2])
+        r = self.post(url, content_type='', HTTP_X_OBJECT_META_STOCK='100')
+        self.assertEqual(r.status_code, 202)
 
-        oname4 = self.objects[cname].keys().pop()
-        url = join_urls(container_url, oname4)
-        self.post(url, content_type='', HTTP_X_OBJECT_META_STOCK='200')
+        url = join_urls(container_url, onames[3])
+        r = self.post(url, content_type='', HTTP_X_OBJECT_META_STOCK='200')
+        self.assertEqual(r.status_code, 202)
 
         # test multiple existence criteria matches
         r = self.get('%s?meta=Quality,Stock' % container_url)
@@ -539,7 +519,7 @@ class ContainerGet(PithosAPITest):
         objects = r.content.split('\n')
         if '' in objects:
             objects.remove('')
-        self.assertTrue(objects, sorted([oname1, oname2, oname3, oname4]))
+        self.assertTrue(objects, sorted(onames))
 
         # list objects that satisfy the existence criteria
         r = self.get('%s?meta=Stock' % container_url)
@@ -547,7 +527,7 @@ class ContainerGet(PithosAPITest):
         objects = r.content.split('\n')
         if '' in objects:
             objects.remove('')
-        self.assertTrue(objects, sorted([oname3, oname4]))
+        self.assertTrue(objects, sorted(onames[2:]))
 
         # test case insensitive existence criteria matching
         r = self.get('%s?meta=quality' % container_url)
@@ -555,7 +535,7 @@ class ContainerGet(PithosAPITest):
         objects = r.content.split('\n')
         if '' in objects:
             objects.remove('')
-        self.assertTrue(objects, sorted([oname1, oname2]))
+        self.assertTrue(objects, sorted(onames[:2]))
 
         # test do not all existencecriteria match
         r = self.get('%s?meta=Quality,Foo' % container_url)
@@ -563,7 +543,7 @@ class ContainerGet(PithosAPITest):
         objects = r.content.split('\n')
         if '' in objects:
             objects.remove('')
-        self.assertTrue(objects, sorted([oname1, oname2]))
+        self.assertTrue(objects, sorted(onames[:2]))
 
         # test equals criteria
         r = self.get('%s?meta=%s' % (container_url, quote('Quality=aaa')))
@@ -571,124 +551,237 @@ class ContainerGet(PithosAPITest):
         objects = r.content.split('\n')
         if '' in objects:
             objects.remove('')
-        self.assertTrue(objects, [oname1])
+        self.assertTrue(objects, [onames[0]])
 
         # test not equals criteria
-        r = self.get('%s?meta=%s' % (container_url, urlencode('Quality!=aaa')))
+        r = self.get('%s?meta=%s' % (container_url, quote('Quality!=aaa')))
         self.assertEqual(r.status_code, 200)
         objects = r.content.split('\n')
         if '' in objects:
-            objects.remove()
-        self.assertTrue(objects, [oname2])
+            objects.remove('')
+        self.assertTrue(objects, [onames[1]])
 
         # test lte criteria
-        r = self.get('%s?meta=%s' % (container_url, urlencode('Stock<=120')))
+        r = self.get('%s?meta=%s' % (container_url, quote('Stock<=120')))
         self.assertEqual(r.status_code, 200)
         objects = r.content.split('\n')
         if '' in objects:
             objects.remove('')
-        self.assertTrue(objects, [oname3])
+        self.assertTrue(objects, [onames[2]])
 
         # test gte criteria
-        r = self.get('%s?meta=%s' % (container_url, urlencode('Stock>=200')))
+        r = self.get('%s?meta=%s' % (container_url, quote('Stock>=200')))
         self.assertEqual(r.status_code, 200)
         objects = r.content.split('\n')
         if '' in objects:
             objects.remove('')
-        self.assertTrue(objects, [oname4])
+        self.assertTrue(objects, [onames[3]])
 
-#
-#    def test_if_modified_since(self):
-#        t = datetime.datetime.utcnow()
-#        t2 = t - datetime.timedelta(minutes=10)
-#
-#        #add a new object
-#        self.upload_random_data(self.container[0], o_names[0])
-#
-#        for f in DATE_FORMATS:
-#            past = t2.strftime(f)
-#            try:
-#                o = self.client.list_objects(self.container[0],
-#                                            if_modified_since=past)
-#                self.assertEqual(o,
-#                                 self.client.list_objects(self.container[0]))
-#            except Fault, f:
-#                self.failIf(f.status == 304) #fail if not modified
-#
-#    def test_if_modified_since_invalid_date(self):
-#        headers = {'if-modified-since':''}
-#        o = self.client.list_objects(self.container[0], if_modified_since='')
-#        self.assertEqual(o, self.client.list_objects(self.container[0]))
-#
-#    def test_if_not_modified_since(self):
-#        now = datetime.datetime.utcnow()
-#        since = now + datetime.timedelta(1)
-#
-#        for f in DATE_FORMATS:
-#            args = {'if_modified_since':'%s' %since.strftime(f)}
-#
-#            #assert not modified
-#            self.assert_raises_fault(304, self.client.list_objects,
-#                                     self.container[0], **args)
-#
-#    def test_if_unmodified_since(self):
-#        now = datetime.datetime.utcnow()
-#        since = now + datetime.timedelta(1)
-#
-#        for f in DATE_FORMATS:
-#            obj = self.client.list_objects(
-#                self.container[0], if_unmodified_since=since.strftime(f))
-#
-#            #assert unmodified
-#            self.assertEqual(obj, self.client.list_objects(self.container[0]))
-#
-#    def test_if_unmodified_since_precondition_failed(self):
-#        t = datetime.datetime.utcnow()
-#        t2 = t - datetime.timedelta(minutes=10)
-#
-#        #add a new container
-#        self.client.create_container('dummy')
-#
-#        for f in DATE_FORMATS:
-#            past = t2.strftime(f)
-#
-#            args = {'if_unmodified_since':'%s' %past}
-#
-#            #assert precondition failed
-#            self.assert_raises_fault(412, self.client.list_objects,
-#                                     self.container[0], **args)
-#
-#class ContainerPut(BaseTestCase):
-#    def setUp(self):
-#        BaseTestCase.setUp(self)
-#        self.containers = list(set(self.initial_containers + ['c1', 'c2']))
-#        self.containers.sort()
-#
-#    def test_create(self):
-#        self.client.create_container(self.containers[0])
-#        containers = self.client.list_containers()
-#        self.assertTrue(self.containers[0] in containers)
-#        self.assert_container_exists(self.containers[0])
-#
-#    def test_create_twice(self):
-#        self.client.create_container(self.containers[0])
-#        self.assertTrue(not self.client.create_container(self.containers[0]))
-#
-#    def test_quota(self):
-#        self.client.create_container(self.containers[0])
-#
-#        policy = {'quota':100}
-#        self.client.set_container_policies(self.containers[0], **policy)
-#
-#        meta = self.client.retrieve_container_metadata(self.containers[0])
-#        self.assertTrue('x-container-policy-quota' in meta)
-#        self.assertEqual(meta['x-container-policy-quota'], '100')
-#
-#        args = [self.containers[0], 'o1']
-#        kwargs = {'length':101}
-#        self.assert_raises_fault(
-#            413, self.upload_random_data, *args, **kwargs)
-#
-#        #reset quota
-#        policy = {'quota':0}
-#        self.client.set_container_policies(self.containers[0], **policy)
+    def test_if_modified_since(self):
+        cname = 'apples'
+        container_info = self.get_container_info(cname)
+        last_modified = container_info['Last-Modified']
+        t1 = datetime.datetime.strptime(last_modified, DATE_FORMATS[-1])
+        t1_formats = map(t1.strftime, DATE_FORMATS)
+
+        # Check not modified
+        url = join_urls(self.pithos_path, self.user, cname)
+        for t in t1_formats:
+            r = self.get(url, HTTP_IF_MODIFIED_SINCE=t)
+            self.assertEqual(r.status_code, 304)
+
+        # modify account: add container
+        _time.sleep(1)
+        oname = self.upload_object(cname)[0]
+
+        # Check modified
+        objects = self.objects[cname].keys()
+        objects.append(oname)
+        for t in t1_formats:
+            r = self.get(url, HTTP_IF_MODIFIED_SINCE=t)
+            self.assertEqual(r.status_code, 200)
+            self.assertEqual(r.content.split('\n')[:-1], sorted(objects))
+
+        container_info = self.get_container_info(cname)
+        last_modified = container_info['Last-Modified']
+        t2 = datetime.datetime.strptime(last_modified, DATE_FORMATS[-1])
+        t2_formats = map(t2.strftime, DATE_FORMATS)
+
+        # modify account: update account meta
+        _time.sleep(1)
+        self.update_container_meta(cname, {'foo': 'bar'})
+
+        # Check modified
+        for t in t2_formats:
+            r = self.get(url, HTTP_IF_MODIFIED_SINCE=t)
+            self.assertEqual(r.status_code, 200)
+            self.assertEqual(r.content.split('\n')[:-1], sorted(objects))
+
+    def test_if_modified_since_invalid_date(self):
+        cname = 'apples'
+        url = join_urls(self.pithos_path, self.user, cname)
+        r = self.get(url, HTTP_IF_MODIFIED_SINCE='Monday')
+        self.assertEqual(r.status_code, 200)
+        self.assertEqual(r.content.split('\n')[:-1],
+                         sorted(self.objects['apples'].keys()))
+
+    def test_if_not_modified_since(self):
+        cname = 'apples'
+        url = join_urls(self.pithos_path, self.user, cname)
+        container_info = self.get_container_info(cname)
+        last_modified = container_info['Last-Modified']
+        t = datetime.datetime.strptime(last_modified, DATE_FORMATS[-1])
+
+        # Check unmodified
+        t1 = t + datetime.timedelta(seconds=1)
+        t1_formats = map(t1.strftime, DATE_FORMATS)
+        for t in t1_formats:
+            r = self.get(url, HTTP_IF_UNMODIFIED_SINCE=t)
+            self.assertEqual(r.status_code, 200)
+            self.assertEqual(
+                r.content.split('\n')[:-1],
+                sorted(self.objects['apples']))
+
+        # modify account: add container
+        _time.sleep(2)
+        self.upload_object(cname)
+
+        container_info = self.get_container_info(cname)
+        last_modified = container_info['Last-Modified']
+        t = datetime.datetime.strptime(last_modified, DATE_FORMATS[-1])
+        t2 = t - datetime.timedelta(seconds=1)
+        t2_formats = map(t2.strftime, DATE_FORMATS)
+
+        # Check modified
+        for t in t2_formats:
+            r = self.get(url, HTTP_IF_UNMODIFIED_SINCE=t)
+            self.assertEqual(r.status_code, 412)
+
+        # modify account: update account meta
+        _time.sleep(1)
+        self.update_container_meta(cname, {'foo': 'bar'})
+
+        container_info = self.get_container_info(cname)
+        last_modified = container_info['Last-Modified']
+        t = datetime.datetime.strptime(last_modified, DATE_FORMATS[-1])
+        t3 = t - datetime.timedelta(seconds=1)
+        t3_formats = map(t3.strftime, DATE_FORMATS)
+
+        # Check modified
+        for t in t3_formats:
+            r = self.get(url, HTTP_IF_UNMODIFIED_SINCE=t)
+            self.assertEqual(r.status_code, 412)
+
+    def test_if_unmodified_since(self):
+        cname = 'apples'
+        url = join_urls(self.pithos_path, self.user, cname)
+        container_info = self.get_container_info(cname)
+        last_modified = container_info['Last-Modified']
+        t = datetime.datetime.strptime(last_modified, DATE_FORMATS[-1])
+        t = t + datetime.timedelta(seconds=1)
+        t_formats = map(t.strftime, DATE_FORMATS)
+
+        for tf in t_formats:
+            r = self.get(url, HTTP_IF_UNMODIFIED_SINCE=tf)
+            self.assertEqual(r.status_code, 200)
+            self.assertEqual(
+                r.content.split('\n')[:-1],
+                sorted(self.objects['apples']))
+
+    def test_if_unmodified_since_precondition_failed(self):
+        cname = 'apples'
+        url = join_urls(self.pithos_path, self.user, cname)
+        container_info = self.get_container_info(cname)
+        last_modified = container_info['Last-Modified']
+        t = datetime.datetime.strptime(last_modified, DATE_FORMATS[-1])
+        t = t - datetime.timedelta(seconds=1)
+        t_formats = map(t.strftime, DATE_FORMATS)
+
+        for tf in t_formats:
+            r = self.get(url, HTTP_IF_UNMODIFIED_SINCE=tf)
+            self.assertEqual(r.status_code, 412)
+
+
+class ContainerPut(PithosAPITest):
+    def test_create(self):
+        self.create_container('c1')
+        self.list_containers()
+        self.assertTrue('c1' in self.list_containers(format=None))
+
+    def test_create_twice(self):
+        self.create_container('c1')
+        self.assertTrue('c1' in self.list_containers(format=None))
+        r = self.create_container('c1')
+        self.assertEqual(r.status_code, 202)
+        self.assertTrue('c1' in self.list_containers(format=None))
+
+
+class ContainerPost(PithosAPITest):
+    def test_update_meta(self):
+        cname = 'apples'
+        self.create_container(cname)
+        meta = {'test': 'test33', 'tost': 'tost22'}
+        self.update_container_meta(cname, meta)
+        info = self.get_container_info(cname)
+        for k, v in meta.items():
+            k = 'x-container-meta-%s' % k
+            self.assertTrue(k in info)
+            self.assertEqual(info[k], v)
+
+    def test_quota(self):
+        self.create_container('c1')
+
+        url = join_urls(self.pithos_path, self.user, 'c1')
+        r = self.post(url, HTTP_X_CONTAINER_POLICY_QUOTA='100')
+        self.assertEqual(r.status_code, 202)
+
+        info = self.get_container_info('c1')
+        self.assertTrue('x-container-policy-quota' in info)
+        self.assertEqual(info['x-container-policy-quota'], '100')
+
+        r = self.upload_object('c1', length=101, verify=False)[2]
+        self.assertEqual(r.status_code, 413)
+
+        url = join_urls(self.pithos_path, self.user, 'c1')
+        r = self.post(url, HTTP_X_CONTAINER_POLICY_QUOTA='0')
+        self.assertEqual(r.status_code, 202)
+
+        r = self.upload_object('c1', length=1)
+
+
+class ContainerDelete(PithosAPITest):
+    def setUp(self):
+        PithosAPITest.setUp(self)
+        cnames = ['c1', 'c2']
+
+        for c in cnames:
+            self.create_container(c)
+
+    def test_delete(self):
+        url = join_urls(self.pithos_path, self.user, 'c1')
+        r = self.delete(url)
+        self.assertEqual(r.status_code, 204)
+        self.assertTrue('c1' not in self.list_containers(format=None))
+
+    def test_delete_non_empty(self):
+        self.upload_object('c1')
+        url = join_urls(self.pithos_path, self.user, 'c1')
+        r = self.delete(url)
+        self.assertEqual(r.status_code, 409)
+        self.assertTrue('c1' in self.list_containers(format=None))
+
+    def test_delete_invalid(self):
+        url = join_urls(self.pithos_path, self.user, 'c3')
+        r = self.delete(url)
+        self.assertEqual(r.status_code, 404)
+
+    def test_delete_contents(self):
+        folder = self.create_folder('c1')[0]
+        descendant = strnextling(folder)
+        self.upload_object('c1', descendant)
+        self.create_folder('c1', '%s/%s' % (folder, get_random_data(5)))[0]
+
+        self.delete('%s?delimiter=/' % join_urls(
+            self.pithos_path, self.user, 'c1'))
+        self.assertEqual([], self.list_objects('c1'))
+        self.assertTrue('c1' in self.list_containers(format=None))
diff --git a/snf-pithos-app/pithos/api/test/objects.py b/snf-pithos-app/pithos/api/test/objects.py
new file mode 100644
index 0000000..be06aae
--- /dev/null
+++ b/snf-pithos-app/pithos/api/test/objects.py
@@ -0,0 +1,1198 @@
+#!/usr/bin/env python
+#coding=utf8
+
+# Copyright 2011-2013 GRNET S.A. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or
+# without modification, are permitted provided that the following
+# conditions are met:
+#
+#   1. Redistributions of source code must retain the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer.
+#
+#   2. Redistributions in binary form must reproduce the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer in the documentation and/or other materials
+#      provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
+# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# The views and conclusions contained in the software and
+# documentation are those of the authors and should not be
+# interpreted as representing official policies, either expressed
+# or implied, of GRNET S.A.
+
+from collections import defaultdict
+from urllib import quote
+import time as _time
+
+from pithos.api.test import (PithosAPITest, pithos_settings,
+                             AssertMappingInvariant, AssertUUidInvariant,
+                             DATE_FORMATS)
+from pithos.api.test.util import compute_md5_hash, strnextling, get_random_data
+from pithos.api.test.util.hashmap import merkle
+
+from synnefo.lib import join_urls
+
+import django.utils.simplejson as json
+
+import random
+import re
+import datetime
+
+
+class ObjectGet(PithosAPITest):
+    def setUp(self):
+        PithosAPITest.setUp(self)
+        self.containers = ['c1', 'c2']
+
+        # create some containers
+        for c in self.containers:
+            self.create_container(c)
+
+        # upload files
+        self.objects = defaultdict(list)
+        self.objects['c1'].append(self.upload_object('c1')[0])
+
+    def test_versions(self):
+        c = 'c1'
+        o = self.objects[c][0]
+        url = join_urls(self.pithos_path, self.user, c, o)
+
+        meta = {'HTTP_X_OBJECT_META_QUALITY': 'AAA'}
+        r = self.post(url, content_type='', **meta)
+        self.assertEqual(r.status_code, 202)
+
+        url = join_urls(self.pithos_path, self.user, c, o)
+        r = self.get('%s?version=list&format=json' % url)
+        self.assertEqual(r.status_code, 200)
+        l1 = json.loads(r.content)['versions']
+        self.assertEqual(len(l1), 2)
+
+        # update meta
+        meta = {'HTTP_X_OBJECT_META_QUALITY': 'AB',
+                'HTTP_X_OBJECT_META_STOCK': 'True'}
+        r = self.post(url, content_type='', **meta)
+        self.assertEqual(r.status_code, 202)
+
+        # assert a newly created version has been created
+        r = self.get('%s?version=list&format=json' % url)
+        self.assertEqual(r.status_code, 200)
+        l2 = json.loads(r.content)['versions']
+        self.assertEqual(len(l2), len(l1) + 1)
+        self.assertEqual(l2[:-1], l1)
+
+        vserial, _ = l2[-2]
+        self.assertEqual(self.get_object_meta(c, o, version=vserial),
+                         {'X-Object-Meta-Quality': 'AAA'})
+
+        # update data
+        self.append_object_data(c, o)
+
+        # assert a newly created version has been created
+        r = self.get('%s?version=list&format=json' % url)
+        self.assertEqual(r.status_code, 200)
+        l3 = json.loads(r.content)['versions']
+        self.assertEqual(len(l3), len(l2) + 1)
+        self.assertEqual(l3[:-1], l2)
+
+    def test_objects_with_trailing_spaces(self):
+        # create object
+        oname = self.upload_object('c1')[0]
+        url = join_urls(self.pithos_path, self.user, 'c1', oname)
+
+        r = self.get(quote('%s ' % url))
+        self.assertEqual(r.status_code, 404)
+
+        # delete object
+        self.delete(url)
+
+        r = self.get(url)
+        self.assertEqual(r.status_code, 404)
+
+        # upload object with trailing space
+        oname = self.upload_object('c1', quote('%s ' % get_random_data(8)))[0]
+
+        url = join_urls(self.pithos_path, self.user, 'c1', oname)
+        r = self.get(url)
+        self.assertEqual(r.status_code, 200)
+
+        url = join_urls(self.pithos_path, self.user, 'c1', oname[:-1])
+        r = self.get(url)
+        self.assertEqual(r.status_code, 404)
+
+    def test_get_partial(self):
+        cname = self.containers[0]
+        oname, odata = self.upload_object(cname, length=512)[:-1]
+        url = join_urls(self.pithos_path, self.user, cname, oname)
+        r = self.get(url, HTTP_RANGE='bytes=0-499')
+        self.assertEqual(r.status_code, 206)
+        data = r.content
+        self.assertEqual(data, odata[:500])
+        self.assertTrue('Content-Range' in r)
+        self.assertEqual(r['Content-Range'], 'bytes 0-499/%s' % len(odata))
+        self.assertTrue('Content-Type' in r)
+        self.assertTrue(r['Content-Type'], 'application/octet-stream')
+
+    def test_get_final_500(self):
+        cname = self.containers[0]
+        oname, odata = self.upload_object(cname, length=512)[:-1]
+        size = len(odata)
+        url = join_urls(self.pithos_path, self.user, cname, oname)
+        r = self.get(url, HTTP_RANGE='bytes=-500')
+        self.assertEqual(r.status_code, 206)
+        self.assertEqual(r.content, odata[-500:])
+        self.assertTrue('Content-Range' in r)
+        self.assertEqual(r['Content-Range'],
+                         'bytes %s-%s/%s' % (size - 500, size - 1, size))
+        self.assertTrue('Content-Type' in r)
+        self.assertTrue(r['Content-Type'], 'application/octet-stream')
+
+    def test_get_rest(self):
+        cname = self.containers[0]
+        oname, odata = self.upload_object(cname, length=512)[:-1]
+        size = len(odata)
+        url = join_urls(self.pithos_path, self.user, cname, oname)
+        offset = len(odata) - random.randint(1, 512)
+        r = self.get(url, HTTP_RANGE='bytes=%s-' % offset)
+        self.assertEqual(r.status_code, 206)
+        self.assertEqual(r.content, odata[offset:])
+        self.assertTrue('Content-Range' in r)
+        self.assertEqual(r['Content-Range'],
+                         'bytes %s-%s/%s' % (offset, size - 1, size))
+        self.assertTrue('Content-Type' in r)
+        self.assertTrue(r['Content-Type'], 'application/octet-stream')
+
+    def test_get_range_not_satisfiable(self):
+        cname = self.containers[0]
+        oname, odata = self.upload_object(cname, length=512)[:-1]
+        url = join_urls(self.pithos_path, self.user, cname, oname)
+
+        # TODO
+        #r = self.get(url, HTTP_RANGE='bytes=50-10')
+        #self.assertEqual(r.status_code, 416)
+
+        offset = len(odata) + 1
+        r = self.get(url, HTTP_RANGE='bytes=0-%s' % offset)
+        self.assertEqual(r.status_code, 416)
+
+    def test_multiple_range(self):
+        cname = self.containers[0]
+        oname, odata = self.upload_object(cname, length=1024)[:-1]
+        url = join_urls(self.pithos_path, self.user, cname, oname)
+
+        l = ['0-499', '-500', '1000-']
+        ranges = 'bytes=%s' % ','.join(l)
+        r = self.get(url, HTTP_RANGE=ranges)
+        self.assertEqual(r.status_code, 206)
+        self.assertTrue('content-type' in r)
+        p = re.compile(
+            'multipart/byteranges; boundary=(?P<boundary>[0-9a-f]{32}\Z)',
+            re.I)
+        m = p.match(r['content-type'])
+        if m is None:
+            self.fail('Invalid multiple range content type')
+        boundary = m.groupdict()['boundary']
+        cparts = r.content.split('--%s' % boundary)[1:-1]
+
+        # assert content parts length
+        self.assertEqual(len(cparts), len(l))
+
+        # for each content part assert headers
+        i = 0
+        for cpart in cparts:
+            content = cpart.split('\r\n')
+            headers = content[1:3]
+            content_range = headers[0].split(': ')
+            self.assertEqual(content_range[0], 'Content-Range')
+
+            r = l[i].split('-')
+            if not r[0] and not r[1]:
+                pass
+            elif not r[0]:
+                start = len(odata) - int(r[1])
+                end = len(odata)
+            elif not r[1]:
+                start = int(r[0])
+                end = len(odata)
+            else:
+                start = int(r[0])
+                end = int(r[1]) + 1
+            fdata = odata[start:end]
+            sdata = '\r\n'.join(content[4:-1])
+            self.assertEqual(len(fdata), len(sdata))
+            self.assertEquals(fdata, sdata)
+            i += 1
+
+    def test_multiple_range_not_satisfiable(self):
+        # perform get with multiple range
+        cname = self.containers[0]
+        oname, odata = self.upload_object(cname, length=1024)[:-1]
+        out_of_range = len(odata) + 1
+        l = ['0-499', '-500', '%d-' % out_of_range]
+        ranges = 'bytes=%s' % ','.join(l)
+        url = join_urls(self.pithos_path, self.user, cname, oname)
+        r = self.get(url, HTTP_RANGE=ranges)
+        self.assertEqual(r.status_code, 416)
+
+    def test_get_with_if_match_with_md5(self):
+        cname = self.containers[0]
+        oname, odata = self.upload_object(cname, length=1024)[:-1]
+
+        # perform get with If-Match
+        url = join_urls(self.pithos_path, self.user, cname, oname)
+
+        if pithos_settings.UPDATE_MD5:
+            etag = compute_md5_hash(odata)
+        else:
+            etag = merkle(odata)
+
+        r = self.get(url, HTTP_IF_MATCH=etag)
+
+        # assert get success
+        self.assertEqual(r.status_code, 200)
+
+        # assert response content
+        self.assertEqual(r.content, odata)
+
+    def test_get_with_if_match_star_with_md5(self):
+        cname = self.containers[0]
+        oname, odata = self.upload_object(cname, length=1024)[:-1]
+
+        # perform get with If-Match *
+        url = join_urls(self.pithos_path, self.user, cname, oname)
+        r = self.get(url, HTTP_IF_MATCH='*')
+
+        # assert get success
+        self.assertEqual(r.status_code, 200)
+
+        # assert response content
+        self.assertEqual(r.content, odata)
+
+    def test_get_with_multiple_if_match_without_md5(self):
+        cname = self.containers[0]
+        oname, odata = self.upload_object(cname, length=1024)[:-1]
+
+        # perform get with If-Match
+        url = join_urls(self.pithos_path, self.user, cname, oname)
+
+        if pithos_settings.UPDATE_MD5:
+            etag = compute_md5_hash(odata)
+        else:
+            etag = merkle(odata)
+
+        r = self.get(url, HTTP_IF_MATCH=','.join([etag,
+                                                  get_random_data(8)]))
+
+        # assert get success
+        self.assertEqual(r.status_code, 200)
+
+        # assert response content
+        self.assertEqual(r.content, odata)
+
+    def test_if_match_precondition_failed(self):
+        cname = self.containers[0]
+        oname, odata = self.upload_object(cname, length=1024)[:-1]
+
+        # perform get with If-Match
+        url = join_urls(self.pithos_path, self.user, cname, oname)
+        r = self.get(url, HTTP_IF_MATCH=get_random_data(8))
+        self.assertEqual(r.status_code, 412)
+
+    def test_if_none_match_without_md5(self):
+        # upload object
+        cname = self.containers[0]
+        oname, odata = self.upload_object(cname, length=1024)[:-1]
+
+        if pithos_settings.UPDATE_MD5:
+            etag = compute_md5_hash(odata)
+        else:
+            etag = merkle(odata)
+
+        # perform get with If-None-Match
+        url = join_urls(self.pithos_path, self.user, cname, oname)
+        r = self.get(url, HTTP_IF_NONE_MATCH=etag)
+
+        # assert precondition_failed
+        self.assertEqual(r.status_code, 304)
+
+        # update object data
+        r = self.append_object_data(cname, oname)[-1]
+        self.assertTrue(etag != r['ETag'])
+
+        # perform get with If-None-Match
+        url = join_urls(self.pithos_path, self.user, cname, oname)
+        r = self.get(url, HTTP_IF_NONE_MATCH=etag)
+
+        # assert get success
+        self.assertEqual(r.status_code, 200)
+
+    def test_if_none_match_star_without_md5(self):
+        # upload object
+        cname = self.containers[0]
+        oname, odata = self.upload_object(cname, length=1024)[:-1]
+
+        # perform get with If-None-Match with star
+        url = join_urls(self.pithos_path, self.user, cname, oname)
+        r = self.get(url, HTTP_IF_NONE_MATCH='*')
+
+        self.assertEqual(r.status_code, 304)
+
+    def test_if_modified_since(self):
+        # upload object
+        cname = self.containers[0]
+        oname, odata = self.upload_object(cname, length=1024)[:-1]
+        object_info = self.get_object_info(cname, oname)
+        last_modified = object_info['Last-Modified']
+        t1 = datetime.datetime.strptime(last_modified, DATE_FORMATS[-1])
+        t1_formats = map(t1.strftime, DATE_FORMATS)
+
+        # Check not modified since
+        url = join_urls(self.pithos_path, self.user, cname, oname)
+        for t in t1_formats:
+            r = self.get(url, HTTP_IF_MODIFIED_SINCE=t)
+            self.assertEqual(r.status_code, 304)
+
+        _time.sleep(1)
+
+        # update object data
+        appended_data = self.append_object_data(cname, oname)[1]
+
+        # Check modified since
+        url = join_urls(self.pithos_path, self.user, cname, oname)
+        for t in t1_formats:
+            r = self.get(url, HTTP_IF_MODIFIED_SINCE=t)
+            self.assertEqual(r.status_code, 200)
+            self.assertEqual(r.content, odata + appended_data)
+
+    def test_if_modified_since_invalid_date(self):
+        cname = self.containers[0]
+        oname, odata = self.upload_object(cname, length=1024)[:-1]
+        url = join_urls(self.pithos_path, self.user, cname, oname)
+        r = self.get(url, HTTP_IF_MODIFIED_SINCE='Monday')
+        self.assertEqual(r.status_code, 200)
+        self.assertEqual(r.content, odata)
+
+    def test_if_not_modified_since(self):
+        cname = self.containers[0]
+        oname, odata = self.upload_object(cname, length=1024)[:-1]
+        url = join_urls(self.pithos_path, self.user, cname, oname)
+        object_info = self.get_object_info(cname, oname)
+        last_modified = object_info['Last-Modified']
+        t = datetime.datetime.strptime(last_modified, DATE_FORMATS[-1])
+
+        # Check unmodified
+        t1 = t + datetime.timedelta(seconds=1)
+        t1_formats = map(t1.strftime, DATE_FORMATS)
+        for t in t1_formats:
+            r = self.get(url, HTTP_IF_UNMODIFIED_SINCE=t)
+            self.assertEqual(r.status_code, 200)
+            self.assertEqual(odata, odata)
+
+        # modify object
+        _time.sleep(2)
+        self.append_object_data(cname, oname)
+
+        object_info = self.get_object_info(cname, oname)
+        last_modified = object_info['Last-Modified']
+        t = datetime.datetime.strptime(last_modified, DATE_FORMATS[-1])
+        t2 = t - datetime.timedelta(seconds=1)
+        t2_formats = map(t2.strftime, DATE_FORMATS)
+
+        # Check modified
+        for t in t2_formats:
+            r = self.get(url, HTTP_IF_UNMODIFIED_SINCE=t)
+            self.assertEqual(r.status_code, 412)
+
+        # modify account: update account meta
+        _time.sleep(1)
+        self.update_object_meta(cname, oname, {'foo': 'bar'})
+
+        object_info = self.get_object_info(cname, oname)
+        last_modified = object_info['Last-Modified']
+        t = datetime.datetime.strptime(last_modified, DATE_FORMATS[-1])
+        t3 = t - datetime.timedelta(seconds=1)
+        t3_formats = map(t3.strftime, DATE_FORMATS)
+
+        # Check modified
+        for t in t3_formats:
+            r = self.get(url, HTTP_IF_UNMODIFIED_SINCE=t)
+            self.assertEqual(r.status_code, 412)
+
+    def test_if_unmodified_since(self):
+        cname = self.containers[0]
+        oname, odata = self.upload_object(cname, length=1024)[:-1]
+        url = join_urls(self.pithos_path, self.user, cname, oname)
+        object_info = self.get_object_info(cname, oname)
+        last_modified = object_info['Last-Modified']
+        t = datetime.datetime.strptime(last_modified, DATE_FORMATS[-1])
+        t = t + datetime.timedelta(seconds=1)
+        t_formats = map(t.strftime, DATE_FORMATS)
+
+        for tf in t_formats:
+            r = self.get(url, HTTP_IF_UNMODIFIED_SINCE=tf)
+            self.assertEqual(r.status_code, 200)
+            self.assertEqual(r.content, odata)
+
+    def test_if_unmodified_since_precondition_failed(self):
+        cname = self.containers[0]
+        oname, odata = self.upload_object(cname, length=1024)[:-1]
+        url = join_urls(self.pithos_path, self.user, cname, oname)
+        object_info = self.get_object_info(cname, oname)
+        last_modified = object_info['Last-Modified']
+        t = datetime.datetime.strptime(last_modified, DATE_FORMATS[-1])
+        t = t - datetime.timedelta(seconds=1)
+        t_formats = map(t.strftime, DATE_FORMATS)
+
+        for tf in t_formats:
+            r = self.get(url, HTTP_IF_UNMODIFIED_SINCE=tf)
+            self.assertEqual(r.status_code, 412)
+
+    def test_hashes(self):
+        l = random.randint(2, 5) * pithos_settings.BACKEND_BLOCK_SIZE
+        cname = self.containers[0]
+        oname, odata = self.upload_object(cname, length=l)[:-1]
+        size = len(odata)
+
+        url = join_urls(self.pithos_path, self.user, cname, oname)
+        r = self.get('%s?format=json&hashmap' % url)
+        self.assertEqual(r.status_code, 200)
+        body = json.loads(r.content)
+
+        hashes = body['hashes']
+        block_size = body['block_size']
+        block_hash = body['block_hash']
+        block_num = size / block_size if size / block_size == 0 else\
+            size / block_size + 1
+        self.assertTrue(len(hashes), block_num)
+        i = 0
+        for h in hashes:
+            start = i * block_size
+            end = (i + 1) * block_size
+            hash = merkle(odata[start:end], int(block_size), block_hash)
+            self.assertEqual(h, hash)
+            i += 1
+
+
+class ObjectPut(PithosAPITest):
+    def setUp(self):
+        PithosAPITest.setUp(self)
+        self.container = get_random_data(8)
+        self.create_container(self.container)
+
+    def test_upload(self):
+        cname = self.container
+        oname = get_random_data(8)
+        data = get_random_data(length=1024)
+        meta = {'test': 'test1'}
+        headers = dict(('HTTP_X_OBJECT_META_%s' % k.upper(), v)
+                       for k, v in meta.iteritems())
+        url = join_urls(self.pithos_path, self.user, cname, oname)
+        r = self.put(url, data=data, content_type='application/pdf', **headers)
+        self.assertEqual(r.status_code, 201)
+        self.assertTrue('ETag' in r)
+        self.assertTrue('X-Object-Version' in r)
+
+        info = self.get_object_info(cname, oname)
+
+        # assert object meta
+        self.assertTrue('X-Object-Meta-Test' in info)
+        self.assertEqual(info['X-Object-Meta-Test'], 'test1')
+
+        # assert content-type
+        self.assertEqual(info['content-type'], 'application/pdf')
+
+        # assert uploaded content
+        r = self.get(url)
+        self.assertEqual(r.status_code, 200)
+        self.assertEqual(r.content, data)
+
+    def test_maximum_upload_size_exceeds(self):
+        cname = self.container
+        oname = get_random_data(8)
+
+#        info = self.get_account_info()
+#        length = int(info['X-Account-Policy-Quota']) + 1
+#        data = get_random_data(length)
+#        url = join_urls(self.pithos_path, self.user, cname, oname)
+#        r = self.put(url, data=data)
+#        self.assertEqual(r.status_code, 413)
+
+        # set container quota to 100
+        url = join_urls(self.pithos_path, self.user, cname)
+        r = self.post(url, HTTP_X_CONTAINER_POLICY_QUOTA='100')
+        self.assertEqual(r.status_code, 202)
+
+        info = self.get_container_info(cname)
+        length = int(info['X-Container-Policy-Quota']) + 1
+
+        data = get_random_data(length)
+        url = join_urls(self.pithos_path, self.user, cname, oname)
+        r = self.put(url, data=data)
+        self.assertEqual(r.status_code, 413)
+
+    def test_upload_with_name_containing_slash(self):
+        cname = self.container
+        oname = '/%s' % get_random_data(8)
+        data = get_random_data(1024)
+        url = join_urls(self.pithos_path, self.user, cname, oname)
+        r = self.put(url, data=data)
+        self.assertEqual(r.status_code, 201)
+        self.assertTrue('ETag' in r)
+        self.assertTrue('X-Object-Version' in r)
+
+        r = self.get(url)
+        self.assertEqual(r.status_code, 200)
+        self.assertEqual(r.content, data)
+
+    def test_upload_unprocessable_entity(self):
+        cname = self.container
+        oname = get_random_data(8)
+        data = get_random_data(1024)
+        url = join_urls(self.pithos_path, self.user, cname, oname)
+        r = self.put(url, data=data, HTTP_ETAG='123')
+        self.assertEqual(r.status_code, 422)
+
+#    def test_chunked_transfer(self):
+#        cname = self.container
+#        oname = '/%s' % get_random_data(8)
+#        data = get_random_data(1024)
+#        url = join_urls(self.pithos_path, self.user, cname, oname)
+#        r = self.put(url, data=data, HTTP_TRANSFER_ENCODING='chunked')
+#        self.assertEqual(r.status_code, 201)
+#        self.assertTrue('ETag' in r)
+#        self.assertTrue('X-Object-Version' in r)
+
+    def test_manifestation(self):
+        cname = self.container
+        prefix = 'myobject/'
+        data = ''
+        for i in range(random.randint(2, 10)):
+            part = '%s%d' % (prefix, i)
+            data += self.upload_object(cname, oname=part)[1]
+
+        manifest = '%s/%s' % (cname, prefix)
+        oname = get_random_data(8)
+        url = join_urls(self.pithos_path, self.user, cname, oname)
+        r = self.put(url, data='', HTTP_X_OBJECT_MANIFEST=manifest)
+        self.assertEqual(r.status_code, 201)
+
+        # assert object exists
+        r = self.get(url)
+        self.assertEqual(r.status_code, 200)
+
+        # assert its content
+        self.assertEqual(r.content, data)
+
+        # invalid manifestation
+        invalid_manifestation = '%s/%s' % (cname, get_random_data(8))
+        self.put(url, data='', HTTP_X_OBJECT_MANIFEST=invalid_manifestation)
+        r = self.get(url)
+        self.assertEqual(r.content, '')
+
+    def test_create_zero_length_object(self):
+        cname = self.container
+        oname = get_random_data(8)
+        url = join_urls(self.pithos_path, self.user, cname, oname)
+        r = self.put(url, data='')
+        self.assertEqual(r.status_code, 201)
+
+        r = self.get(url)
+        self.assertEqual(r.status_code, 200)
+        self.assertEqual(int(r['Content-Length']), 0)
+        self.assertEqual(r.content, '')
+
+        r = self.get('%s?hashmap=&format=json' % url)
+        self.assertEqual(r.status_code, 200)
+        body = json.loads(r.content)
+        hashes = body['hashes']
+        block_size = body['block_size']
+        block_hash = body['block_hash']
+        hash = merkle('', int(block_size), block_hash)
+        self.assertEqual(hashes, [hash])
+
+    def test_create_object_by_hashmap(self):
+        cname = self.container
+        block_size = pithos_settings.BACKEND_BLOCK_SIZE
+
+        # upload an object
+        oname, data = self.upload_object(cname, length=block_size + 1)[:-1]
+        # get it hashmap
+        url = join_urls(self.pithos_path, self.user, cname, oname)
+        r = self.get('%s?hashmap=&format=json' % url)
+
+        oname = get_random_data(8)
+        url = join_urls(self.pithos_path, self.user, cname, oname)
+        r = self.put('%s?hashmap=' % url, data=r.content)
+        self.assertEqual(r.status_code, 201)
+
+        r = self.get(url)
+        self.assertEqual(r.status_code, 200)
+        self.assertEqual(r.content, data)
+
+
+class ObjectCopy(PithosAPITest):
+    def setUp(self):
+        PithosAPITest.setUp(self)
+        self.container = 'c1'
+        self.create_container(self.container)
+        self.object, self.data = self.upload_object(self.container)[:-1]
+
+        url = join_urls(
+            self.pithos_path, self.user, self.container, self.object)
+        r = self.head(url)
+        self.etag = r['X-Object-Hash']
+
+    def test_copy(self):
+        with AssertMappingInvariant(self.get_object_info, self.container,
+                                    self.object):
+            # copy object
+            oname = get_random_data(8)
+            url = join_urls(self.pithos_path, self.user, self.container, oname)
+            r = self.put(url, data='', HTTP_X_OBJECT_META_TEST='testcopy',
+                         HTTP_X_COPY_FROM='/%s/%s' % (
+                             self.container, self.object))
+
+            # assert copy success
+            self.assertEqual(r.status_code, 201)
+
+            # assert access the new object
+            r = self.head(url)
+            self.assertEqual(r.status_code, 200)
+            self.assertTrue('X-Object-Meta-Test' in r)
+            self.assertEqual(r['X-Object-Meta-Test'], 'testcopy')
+
+            # assert etag is the same
+            self.assertTrue('X-Object-Hash' in r)
+            self.assertEqual(r['X-Object-Hash'], self.etag)
+
+    def test_copy_from_different_container(self):
+        cname = 'c2'
+        self.create_container(cname)
+        with AssertMappingInvariant(self.get_object_info, self.container,
+                                    self.object):
+            oname = get_random_data(8)
+            url = join_urls(self.pithos_path, self.user, cname, oname)
+            r = self.put(url, data='', HTTP_X_OBJECT_META_TEST='testcopy',
+                         HTTP_X_COPY_FROM='/%s/%s' % (
+                             self.container, self.object))
+
+            # assert copy success
+            self.assertEqual(r.status_code, 201)
+
+            # assert access the new object
+            r = self.head(url)
+            self.assertEqual(r.status_code, 200)
+            self.assertTrue('X-Object-Meta-Test' in r)
+            self.assertEqual(r['X-Object-Meta-Test'], 'testcopy')
+
+            # assert etag is the same
+            self.assertTrue('X-Object-Hash' in r)
+            self.assertEqual(r['X-Object-Hash'], self.etag)
+
+    def test_copy_invalid(self):
+        # copy from non-existent object
+        oname = get_random_data(8)
+        url = join_urls(self.pithos_path, self.user, self.container, oname)
+        r = self.put(url, data='', HTTP_X_OBJECT_META_TEST='testcopy',
+                     HTTP_X_COPY_FROM='/%s/%s' % (
+                         self.container, get_random_data(8)))
+        self.assertEqual(r.status_code, 404)
+
+        # copy from non-existent container
+        oname = get_random_data(8)
+        url = join_urls(self.pithos_path, self.user, self.container, oname)
+        r = self.put(url, data='', HTTP_X_OBJECT_META_TEST='testcopy',
+                     HTTP_X_COPY_FROM='/%s/%s' % (
+                         get_random_data(8), self.object))
+        self.assertEqual(r.status_code, 404)
+
+    def test_copy_dir(self):
+        folder = self.create_folder(self.container)[0]
+        subfolder = self.create_folder(
+            self.container, oname='%s/%s' % (folder, get_random_data(8)))[0]
+        objects = [subfolder]
+        append = objects.append
+        append(self.upload_object(self.container,
+                                  '%s/%s' % (folder, get_random_data(8)),
+                                  HTTP_X_OBJECT_META_DEPTH='1')[0])
+        append(self.upload_object(self.container,
+                                  '%s/%s' % (subfolder, get_random_data(8)),
+                                  HTTP_X_OBJECT_META_DEPTH='2')[0])
+        other = self.upload_object(self.container, strnextling(folder))[0]
+
+        # copy dir
+        copy_folder = self.create_folder(self.container)[0]
+        url = join_urls(self.pithos_path, self.user, self.container,
+                        copy_folder)
+        r = self.put('%s?delimiter=/' % url, data='',
+                     HTTP_X_COPY_FROM='/%s/%s' % (self.container, folder))
+        self.assertEqual(r.status_code, 201)
+
+        for obj in objects:
+            # assert object exists
+            url = join_urls(self.pithos_path, self.user, self.container,
+                            obj.replace(folder, copy_folder))
+            r = self.head(url)
+            self.assertEqual(r.status_code, 200)
+
+            # assert metadata
+            meta = self.get_object_meta(self.container, obj)
+            for k in meta.keys():
+                self.assertTrue(k in r)
+                self.assertEqual(r[k], meta[k])
+
+        # assert other has not been created under copy folder
+        url = join_urls(self.pithos_path, self.user, self.container,
+                        '%s/%s' % (copy_folder,
+                                   other.replace(folder, copy_folder)))
+        r = self.head(url)
+        self.assertEqual(r.status_code, 404)
+
+
+class ObjectMove(PithosAPITest):
+    def setUp(self):
+        PithosAPITest.setUp(self)
+        self.container = 'c1'
+        self.create_container(self.container)
+        self.object, self.data = self.upload_object(self.container)[:-1]
+
+        url = join_urls(
+            self.pithos_path, self.user, self.container, self.object)
+        r = self.head(url)
+        self.etag = r['X-Object-Hash']
+
+    def test_move(self):
+        # move object
+        oname = get_random_data(8)
+        url = join_urls(self.pithos_path, self.user, self.container, oname)
+        r = self.put(url, data='', HTTP_X_OBJECT_META_TEST='testcopy',
+                     HTTP_X_MOVE_FROM='/%s/%s' % (
+                         self.container, self.object))
+
+        # assert move success
+        self.assertEqual(r.status_code, 201)
+
+        # assert access the new object
+        r = self.head(url)
+        self.assertEqual(r.status_code, 200)
+        self.assertTrue('X-Object-Meta-Test' in r)
+        self.assertEqual(r['X-Object-Meta-Test'], 'testcopy')
+
+        # assert etag is the same
+        self.assertTrue('X-Object-Hash' in r)
+
+        # assert the initial object has been deleted
+        url = join_urls(self.pithos_path, self.user, self.container,
+                        self.object)
+        r = self.head(url)
+        self.assertEqual(r.status_code, 404)
+
+    def test_move_dir(self):
+        folder = self.create_folder(self.container)[0]
+        subfolder = self.create_folder(
+            self.container, oname='%s/%s' % (folder, get_random_data(8)))[0]
+        objects = [subfolder]
+        append = objects.append
+        meta = {}
+        meta[objects[0]] = {}
+        append(self.upload_object(self.container,
+                                  '%s/%s' % (folder, get_random_data(8)),
+                                  HTTP_X_OBJECT_META_DEPTH='1')[0])
+        meta[objects[1]] = {'X-Object-Meta-Depth': '1'}
+        append(self.upload_object(self.container,
+                                  '%s/%s' % (subfolder, get_random_data(8)),
+                                  HTTP_X_OBJECT_META_DEPTH='2')[0])
+        meta[objects[1]] = {'X-Object-Meta-Depth': '2'}
+        other = self.upload_object(self.container, strnextling(folder))[0]
+
+        # move dir
+        copy_folder = self.create_folder(self.container)[0]
+        url = join_urls(self.pithos_path, self.user, self.container,
+                        copy_folder)
+        r = self.put('%s?delimiter=/' % url, data='',
+                     HTTP_X_MOVE_FROM='/%s/%s' % (self.container, folder))
+        self.assertEqual(r.status_code, 201)
+
+        for obj in objects:
+            # assert initial object does not exist
+            url = join_urls(self.pithos_path, self.user, self.container, obj)
+            r = self.head(url)
+            self.assertEqual(r.status_code, 404)
+
+            # assert new object was created
+            url = join_urls(self.pithos_path, self.user, self.container,
+                            obj.replace(folder, copy_folder))
+            r = self.head(url)
+            self.assertEqual(r.status_code, 200)
+
+#            # assert metadata
+#            for k in meta[obj].keys():
+#                self.assertTrue(k in r)
+#                self.assertEqual(r[k], meta[obj][k])
+
+        # assert other has not been created under copy folder
+        url = join_urls(self.pithos_path, self.user, self.container,
+                        '%s/%s' % (copy_folder,
+                                   other.replace(folder, copy_folder)))
+        r = self.head(url)
+        self.assertEqual(r.status_code, 404)
+
+
+class ObjectPost(PithosAPITest):
+    def setUp(self):
+        PithosAPITest.setUp(self)
+        self.container = 'c1'
+        self.create_container(self.container)
+        self.object, self.object_data = self.upload_object(self.container)[:2]
+
+    def test_update_meta(self):
+        with AssertUUidInvariant(self.get_object_info,
+                                 self.container,
+                                 self.object):
+            # update metadata
+            d = {'a' * 114: 'b' * 256}
+            kwargs = dict(('HTTP_X_OBJECT_META_%s' % k, v) for
+                          k, v in d.items())
+            url = join_urls(self.pithos_path, self.user, self.container,
+                            self.object)
+            r = self.post(url, content_type='', **kwargs)
+            self.assertEqual(r.status_code, 202)
+
+            # assert metadata have been updated
+            meta = self.get_object_meta(self.container, self.object)
+
+            for k, v in d.items():
+                key = 'X-Object-Meta-%s' % k.title()
+                self.assertTrue(key in meta)
+                self.assertTrue(meta[key], v)
+
+            # Header key too large
+            d = {'a' * 115: 'b' * 256}
+            kwargs = dict(('HTTP_X_OBJECT_META_%s' % k, v) for
+                          k, v in d.items())
+            r = self.post(url, content_type='', **kwargs)
+            self.assertEqual(r.status_code, 400)
+
+            # Header value too large
+            d = {'a' * 114: 'b' * 257}
+            kwargs = dict(('HTTP_X_OBJECT_META_%s' % k, v) for
+                          k, v in d.items())
+            r = self.post(url, content_type='', **kwargs)
+            self.assertEqual(r.status_code, 400)
+
+#            # Check utf-8 meta
+#            d = {'α' * (114 / 2): 'β' * (256 / 2)}
+#            kwargs = dict(('HTTP_X_OBJECT_META_%s' % quote(k), quote(v)) for
+#                          k, v in d.items())
+#            url = join_urls(self.pithos_path, self.user, self.container,
+#                            self.object)
+#            r = self.post(url, content_type='', **kwargs)
+#            self.assertEqual(r.status_code, 202)
+#
+#            # assert metadata have been updated
+#            meta = self.get_object_meta(self.container, self.object)
+#
+#            for k, v in d.items():
+#                key = 'X-Object-Meta-%s' % k.title()
+#                self.assertTrue(key in meta)
+#                self.assertTrue(meta[key], v)
+#
+#            # Header key too large
+#            d = {'α' * 114: 'β' * (256 / 2)}
+#            kwargs = dict(('HTTP_X_OBJECT_META_%s' % quote(k), quote(v)) for
+#                          k, v in d.items())
+#            r = self.post(url, content_type='', **kwargs)
+#            self.assertEqual(r.status_code, 400)
+#
+#            # Header value too large
+#            d = {'α' * 114: 'β' * 256}
+#            kwargs = dict(('HTTP_X_OBJECT_META_%s' % quote(k), quote(v)) for
+#                          k, v in d.items())
+#            r = self.udpate(url, content_type='', **kwargs)
+#            self.assertEqual(r.status_code, 400)
+
+    def test_update_object(self):
+        block_size = pithos_settings.BACKEND_BLOCK_SIZE
+        oname, odata = self.upload_object(
+            self.container, length=random.randint(
+                block_size + 1, 2 * block_size))[:2]
+
+        length = len(odata)
+        first_byte_pos = random.randint(1, block_size)
+        last_byte_pos = random.randint(block_size + 1, length - 1)
+        range = 'bytes %s-%s/%s' % (first_byte_pos, last_byte_pos, length)
+        kwargs = {'content_type': 'application/octet-stream',
+                  'HTTP_CONTENT_RANGE': range}
+
+        url = join_urls(self.pithos_path, self.user, self.container, oname)
+        partial = last_byte_pos - first_byte_pos + 1
+        data = get_random_data(partial)
+        r = self.post(url, data=data, **kwargs)
+
+        self.assertEqual(r.status_code, 204)
+        self.assertTrue('ETag' in r)
+        updated_data = odata.replace(odata[first_byte_pos: last_byte_pos + 1],
+                                     data)
+        if pithos_settings.UPDATE_MD5:
+            etag = compute_md5_hash(updated_data)
+        else:
+            etag = merkle(updated_data)
+        #self.assertEqual(r['ETag'], etag)
+
+        # check modified object
+        r = self.get(url)
+
+        self.assertEqual(r.status_code, 200)
+        self.assertEqual(r.content, updated_data)
+        self.assertEqual(etag, r['ETag'])
+
+    def test_update_object_divided_by_blocksize(self):
+        block_size = pithos_settings.BACKEND_BLOCK_SIZE
+        oname, odata = self.upload_object(self.container,
+                                          length=2 * block_size)[:2]
+
+        length = len(odata)
+        first_byte_pos = block_size
+        last_byte_pos = 2 * block_size - 1
+        range = 'bytes %s-%s/%s' % (first_byte_pos, last_byte_pos, length)
+        kwargs = {'content_type': 'application/octet-stream',
+                  'HTTP_CONTENT_RANGE': range}
+
+        url = join_urls(self.pithos_path, self.user, self.container, oname)
+        partial = last_byte_pos - first_byte_pos + 1
+        data = get_random_data(partial)
+        r = self.post(url, data=data, **kwargs)
+
+        self.assertEqual(r.status_code, 204)
+        self.assertTrue('ETag' in r)
+        updated_data = odata.replace(odata[first_byte_pos: last_byte_pos + 1],
+                                     data)
+        if pithos_settings.UPDATE_MD5:
+            etag = compute_md5_hash(updated_data)
+        else:
+            etag = merkle(updated_data)
+        #self.assertEqual(r['ETag'], etag)
+
+        # check modified object
+        r = self.get(url)
+
+        self.assertEqual(r.status_code, 200)
+        self.assertEqual(r.content, updated_data)
+        self.assertEqual(etag, r['ETag'])
+
+    def test_update_object_invalid_content_length(self):
+        block_size = pithos_settings.BACKEND_BLOCK_SIZE
+        oname, odata = self.upload_object(
+            self.container, length=random.randint(
+                block_size + 1, 2 * block_size))[:2]
+
+        length = len(odata)
+        first_byte_pos = random.randint(1, block_size)
+        last_byte_pos = random.randint(block_size + 1, length - 1)
+        partial = last_byte_pos - first_byte_pos + 1
+        data = get_random_data(partial)
+        range = 'bytes %s-%s/%s' % (first_byte_pos, last_byte_pos, length)
+        kwargs = {'content_type': 'application/octet-stream',
+                  'HTTP_CONTENT_RANGE': range,
+                  'CONTENT_LENGTH': partial + 1}
+
+        url = join_urls(self.pithos_path, self.user, self.container, oname)
+        r = self.post(url, data=data, **kwargs)
+
+        self.assertEqual(r.status_code, 400)
+
+    def test_update_object_invalid_range(self):
+        block_size = pithos_settings.BACKEND_BLOCK_SIZE
+        oname, odata = self.upload_object(
+            self.container, length=random.randint(block_size + 1,
+                                                  2 * block_size))[:2]
+
+        length = len(odata)
+        first_byte_pos = random.randint(1, block_size)
+        last_byte_pos = first_byte_pos - 1
+        range = 'bytes %s-%s/%s' % (first_byte_pos, last_byte_pos, length)
+        kwargs = {'content_type': 'application/octet-stream',
+                  'HTTP_CONTENT_RANGE': range}
+
+        url = join_urls(self.pithos_path, self.user, self.container, oname)
+        r = self.post(url, data=get_random_data(1024), **kwargs)
+
+        self.assertEqual(r.status_code, 416)
+
+    def test_update_object_out_of_limits(self):
+        block_size = pithos_settings.BACKEND_BLOCK_SIZE
+        oname, odata = self.upload_object(
+            self.container, length=random.randint(block_size + 1,
+                                                  2 * block_size))[:2]
+
+        length = len(odata)
+        first_byte_pos = random.randint(1, block_size)
+        last_byte_pos = length + 1
+        range = 'bytes %s-%s/%s' % (first_byte_pos, last_byte_pos, length)
+        kwargs = {'content_type': 'application/octet-stream',
+                  'HTTP_CONTENT_RANGE': range}
+
+        url = join_urls(self.pithos_path, self.user, self.container, oname)
+        r = self.post(url, data=get_random_data(1024), **kwargs)
+
+        self.assertEqual(r.status_code, 416)
+
+    def test_append(self):
+        length = random.randint(1, 1024)
+        data = get_random_data(length)
+        url = join_urls(self.pithos_path, self.user, self.container,
+                        self.object)
+        r = self.post(url, data=data, content_type='application/octet-stream',
+                      HTTP_CONTENT_LENGTH=str(length),
+                      HTTP_CONTENT_RANGE='bytes */*')
+        self.assertEqual(r.status_code, 204)
+
+        r = self.get(url)
+        content = r.content
+        self.assertEqual(len(content), len(self.object_data) + length)
+        self.assertEqual(content, self.object_data + data)
+
+    def test_update_with_chunked_transfer(self):
+        length = random.randint(1, 1024)
+        data = get_random_data(length)
+
+        url = join_urls(self.pithos_path, self.user, self.container,
+                        self.object)
+        r = self.post(url, data=data, content_type='application/octet-stream',
+                      HTTP_CONTENT_RANGE='bytes 0-/*',
+                      HTTP_TRANSFER_ENCODING='chunked')
+        self.assertEqual(r.status_code, 204)
+
+        # check modified object
+        r = self.get(url)
+        content = r.content
+        self.assertEqual(content[0:length], data)
+        self.assertEqual(content[length:], self.object_data[length:])
+
+    def test_update_from_other_object(self):
+        src = self.object
+        dest = get_random_data(8)
+
+        url = join_urls(self.pithos_path, self.user, self.container, src)
+        r = self.get(url)
+        source_data = r.content
+        source_meta = self.get_object_info(self.container, src)
+
+        # update zero length object
+        url = join_urls(self.pithos_path, self.user, self.container, dest)
+        r = self.put(url, data='')
+        self.assertEqual(r.status_code, 201)
+
+        r = self.post(url,
+                      HTTP_CONTENT_RANGE='bytes */*',
+                      HTTP_X_SOURCE_OBJECT='/%s/%s' % (self.container, src))
+        self.assertEqual(r.status_code, 204)
+
+        r = self.get(url)
+        dest_data = r.content
+        dest_meta = self.get_object_info(self.container, dest)
+
+        self.assertEqual(source_data, dest_data)
+        #self.assertEqual(source_meta['ETag'], dest_meta['ETag'])
+        self.assertEqual(source_meta['X-Object-Hash'],
+                         dest_meta['X-Object-Hash'])
+        self.assertTrue(
+            source_meta['X-Object-UUID'] != dest_meta['X-Object-UUID'])
+
+    def test_update_range_from_other_object(self):
+        src = self.object
+        dest = get_random_data(8)
+
+        url = join_urls(self.pithos_path, self.user, self.container, src)
+        r = self.get(url)
+        source_data = r.content
+
+        # update zero length object
+        url = join_urls(self.pithos_path, self.user, self.container, dest)
+        length = random.randint(1, 1024)
+        r = self.put(url, data=get_random_data(length))
+        self.assertEqual(r.status_code, 201)
+
+        offset = random.randint(1, length - 2)
+        upto = random.randint(offset, length - 1)
+        r = self.post(url,
+                      HTTP_CONTENT_RANGE='bytes %s-%s/*' % (offset, upto),
+                      HTTP_X_SOURCE_OBJECT='/%s/%s' % (self.container, src))
+        self.assertEqual(r.status_code, 204)
+
+        r = self.get(url)
+        content = r.content
+        self.assertEqual(content[:offset], source_data[:offset])
+        self.assertEqual(content[offset:upto + 1],
+                         source_data[:upto - offset + 1])
+        self.assertEqual(content[upto + 1:], source_data[upto + 1:])
+
+
+class ObjectDelete(PithosAPITest):
+    def setUp(self):
+        PithosAPITest.setUp(self)
+        self.container = 'c1'
+        self.create_container(self.container)
+        self.object, self.object_data = self.upload_object(self.container)[:2]
+
+    def test_delete(self):
+        url = join_urls(self.pithos_path, self.user, self.container,
+                        self.object)
+        r = self.delete(url)
+        self.assertEqual(r.status_code, 204)
+
+        r = self.head(url)
+        self.assertEqual(r.status_code, 404)
+
+    def test_delete_non_existent(self):
+        url = join_urls(self.pithos_path, self.user, self.container,
+                        get_random_data(8))
+        r = self.delete(url)
+        self.assertEqual(r.status_code, 404)
+
+    def test_delete_dir(self):
+        folder = self.create_folder(self.container)[0]
+        subfolder = self.create_folder(
+            self.container, oname='%s/%s' % (folder, get_random_data(8)))[0]
+        objects = [subfolder]
+        append = objects.append
+        meta = {}
+        meta[objects[0]] = {}
+        append(self.upload_object(self.container,
+                                  '%s/%s' % (folder, get_random_data(8)),
+                                  HTTP_X_OBJECT_META_DEPTH='1')[0])
+        meta[objects[1]] = {'X-Object-Meta-Depth': '1'}
+        append(self.upload_object(self.container,
+                                  '%s/%s' % (subfolder, get_random_data(8)),
+                                  HTTP_X_OBJECT_META_DEPTH='2')[0])
+        meta[objects[1]] = {'X-Object-Meta-Depth': '2'}
+        other = self.upload_object(self.container, strnextling(folder))[0]
+
+        # move dir
+        url = join_urls(self.pithos_path, self.user, self.container, folder)
+        r = self.delete('%s?delimiter=/' % url)
+        self.assertEqual(r.status_code, 204)
+
+        for obj in objects:
+            # assert object does not exist
+            url = join_urls(self.pithos_path, self.user, self.container, obj)
+            r = self.head(url)
+            self.assertEqual(r.status_code, 404)
+
+        # assert other has not been deleted
+        url = join_urls(self.pithos_path, self.user, self.container, other)
+        r = self.head(url)
+        self.assertEqual(r.status_code, 200)
diff --git a/snf-pithos-app/pithos/api/test/util/hashmap.py b/snf-pithos-app/pithos/api/test/util/hashmap.py
new file mode 100644
index 0000000..24c1f58
--- /dev/null
+++ b/snf-pithos-app/pithos/api/test/util/hashmap.py
@@ -0,0 +1,89 @@
+# Copyright 2011-2012 GRNET S.A. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or
+# without modification, are permitted provided that the following
+# conditions are met:
+#
+#   1. Redistributions of source code must retain the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer.
+#
+#   2. Redistributions in binary form must reproduce the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer in the documentation and/or other materials
+#      provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
+# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# The views and conclusions contained in the software and
+# documentation are those of the authors and should not be
+# interpreted as representing official policies, either expressed
+# or implied, of GRNET S.A.
+
+import hashlib
+
+from binascii import hexlify
+from StringIO import StringIO
+
+
+def file_read_iterator(fp, size=1024):
+    while True:
+        data = fp.read(size)
+        if not data:
+            break
+        yield data
+
+
+class HashMap(list):
+
+    def __init__(self, blocksize, blockhash):
+        super(HashMap, self).__init__()
+        self.blocksize = blocksize
+        self.blockhash = blockhash
+
+    def _hash_raw(self, v):
+        h = hashlib.new(self.blockhash)
+        h.update(v)
+        return h.digest()
+
+    def _hash_block(self, v):
+        return self._hash_raw(v.rstrip('\x00'))
+
+    def hash(self):
+        if len(self) == 0:
+            return self._hash_raw('')
+        if len(self) == 1:
+            return self.__getitem__(0)
+
+        h = list(self)
+        s = 2
+        while s < len(h):
+            s = s * 2
+        h += [('\x00' * len(h[0]))] * (s - len(h))
+        while len(h) > 1:
+            h = [self._hash_raw(h[x] + h[x + 1]) for x in range(0, len(h), 2)]
+        return h[0]
+
+    def load(self, data):
+        self.size = 0
+        fp = StringIO(data)
+        for block in file_read_iterator(fp, self.blocksize):
+            self.append(self._hash_block(block))
+            self.size += len(block)
+
+
+def merkle(data, blocksize=4194304, blockhash='sha256'):
+    hashes = HashMap(blocksize, blockhash)
+    hashes.load(data)
+    return hexlify(hashes.hash())
diff --git a/snf-pithos-app/pithos/api/tests.py b/snf-pithos-app/pithos/api/tests.py
index 53ac273..8fa51fe 100644
--- a/snf-pithos-app/pithos/api/tests.py
+++ b/snf-pithos-app/pithos/api/tests.py
@@ -34,3 +34,4 @@
 # Import TestCases
 from pithos.api.test.accounts import *
 from pithos.api.test.containers import *
+from pithos.api.test.objects import *
