Here we have some tests for the lock behavior on Zope. It doesn't
follow the spec 100%, but close enough to be usable.

First, let's create two users for this test:

  >>> user1, pass1 = 'user1', 'pass1'
  >>> user2, pass2 = 'user2', 'pass2'
  >>> uf = self.portal.acl_users
  >>> uf.userFolderAddUser(user1, pass1, ['Manager'], [])
  >>> uf.userFolderAddUser(user2, pass2, ['Manager'], [])

Check that we actually have a Document as index_html:

  >>> not 'index_html' in self.portal.objectIds()
  True

  >>> _ = self.portal.invokeFactory('Document', 'index_html')
  >>> print self.portal.index_html.getPortalTypeName()
  Document

First user gets a lock. Response includes the lock token in header and
also as part of the lockdiscover/activelock properties:

  >>> print http(r"""
  ... LOCK /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Content-Type: application/xml
  ... Depth: 0
  ... Te: trailers
  ... 
  ... <?xml version="1.0" encoding="utf-8"?>
  ... <lockinfo xmlns='DAV:'>
  ...  <lockscope><exclusive/></lockscope>
  ... <locktype><write/></locktype>
  ... <owner>user1</owner></lockinfo>
  ... """ % (user1, pass1))
  HTTP/1.1 200 OK
  Accept-Ranges: none
  ...
  Lock-Token: opaquelocktoken:...00105A989226...
  <BLANKLINE>
  <?xml version="1.0" encoding="utf-8" ?>
  <d:prop xmlns:d="DAV:">
   <d:lockdiscovery>
     <d:activelock>
    <d:locktype><d:write/></d:locktype>
    <d:lockscope><d:exclusive/></d:lockscope>
    <d:depth>0</d:depth>
    <d:owner>user1</d:owner>
    <d:timeout>Second-720</d:timeout>
    <d:locktoken>
     <d:href>opaquelocktoken:...00105A989226...</d:href>
    </d:locktoken>
   </d:activelock>
  <BLANKLINE>
   </d:lockdiscovery>
  </d:prop>

Let's fetch the lock token programatically to ease our job and try to
send another lock request including the lock token on the If:
header. As we are providing a body in this request, Zope will consider
that it's a normal lock request and reply with a 423 Locked response
code.

  >>> token = self.portal.index_html.wl_lockValues()[0].getLockToken()

  >>> print http(r"""
  ... LOCK /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Content-Type: application/xml
  ... Depth: 0
  ... If: <http://localhost:9090/plone/index_html> (<opaquelocktoken:%s>)
  ... 
  ... <?xml version="1.0" encoding="utf-8"?>
  ... <lockinfo xmlns='DAV:'>
  ...  <lockscope><exclusive/></lockscope>
  ... <locktype><write/></locktype></lockinfo>
  ... """ % (user1, pass1, token))
  HTTP/1.1 423 Locked
  Accept-Ranges: none
  ...
  <BLANKLINE>

However, if we send a lock request with no body, and including the
lock token, Zope will consider it a lock refresh request and refresh
the lock validity. Note though that this response doesn't include the
Lock-Token header.


  >>> print http(r"""
  ... LOCK /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Content-Type: application/xml
  ... Depth: 0
  ... If: <http://localhost:9090/plone/index_html> (<opaquelocktoken:%s>)
  ... Te: trailers
  ... """ % (user1, pass1, token))
  HTTP/1.1 200 OK
  Accept-Ranges: none
  ...
  <BLANKLINE>
  <?xml version="1.0" encoding="utf-8" ?>
  <d:prop xmlns:d="DAV:">
   <d:lockdiscovery>
     <d:activelock>
    <d:locktype><d:write/></d:locktype>
    <d:lockscope><d:exclusive/></d:lockscope>
    <d:depth>0</d:depth>
    <d:owner>user1</d:owner>
    <d:timeout>Second-720</d:timeout>
    <d:locktoken>
     <d:href>opaquelocktoken:...00105A989226...</d:href>
    </d:locktoken>
   </d:activelock>
  <BLANKLINE>
   </d:lockdiscovery>
  </d:prop>

Now, let's try to acquire a lock using a different user while the
first user still holds the lock. As expected, the user should receive
a 423 Locked response.

  >>> print http(r"""
  ... LOCK /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Content-Type: application/xml
  ... Depth: 0
  ... Te: trailers
  ... 
  ... <?xml version="1.0" encoding="utf-8"?>
  ... <lockinfo xmlns='DAV:'>
  ...  <lockscope><exclusive/></lockscope>
  ... <locktype><write/></locktype></lockinfo>
  ... """ % (user2, pass2))
  HTTP/1.1 423 Locked
  Accept-Ranges: none
  ...
  <BLANKLINE>

Doing a PROPFIND request using this other user to get the active
locks, will return a fake token to prevent the user from stealing the
lock.


  >>> print http(r"""
  ... PROPFIND /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Content-Type: application/xml
  ... Depth: 0
  ... Te: trailers
  ... 
  ... <?xml version="1.0" encoding="utf-8"?>
  ... <propfind xmlns="DAV:"><prop>
  ... <lockdiscovery xmlns="DAV:"/>
  ... </prop></propfind>
  ... """ % (user2, pass2))
  HTTP/1.1 207 Multi-Status
  Accept-Ranges: none
  ...
  <BLANKLINE>
  <?xml version="1.0" encoding="utf-8"?>
  <d:multistatus xmlns:d="DAV:">
  <d:response>
  <d:href>/plone/index_html</d:href>
  <d:propstat>
    <d:prop>
  <n:lockdiscovery xmlns:n="DAV:">
  <BLANKLINE>
   <n:activelock>
    <n:locktype><n:write/></n:locktype>
    <n:lockscope><n:exclusive/></n:lockscope>
    <n:depth>0</n:depth>
    <n:owner>user1</n:owner>
    <n:timeout>Second-720</n:timeout>
    <n:locktoken>
     <n:href>opaquelocktoken:this-is-a-faked-no-permission-token</n:href>
    </n:locktoken>
   </n:activelock>
  <BLANKLINE>
  </n:lockdiscovery>
    </d:prop>
    <d:status>HTTP/1.1 200 OK</d:status>
  </d:propstat>
  </d:response>
  </d:multistatus>

Trying to unlock using this faked token will not have any effect,
however Zope still returns a 204 response status which seems
incorrect, though I can't figure out from the spec what is the correct
answer that should be given in this case. I'm inclined to say it
should be a 423 Locked.

  >>> print http(r"""
  ... UNLOCK /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Lock-Token: <opaquelocktoken:this-is-a-faked-no-permission-token>
  ... Te: trailers
  ... """ % (user2, pass2))
  HTTP/1.1 204 No Content
  Accept-Ranges: none
  ...
  <BLANKLINE>

To confirm that the lock hasn't been removed we can do two things: One
is to look directly in Zope for existing locks:

  >>> len(self.portal.index_html.wl_lockValues())
  1

And the other one is to try to lock the item with this user. It should
fail with a 423 Locked response status.

  >>> print http(r"""
  ... LOCK /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Depth: 0
  ... Te: trailers
  ... 
  ... <?xml version="1.0" encoding="utf-8"?>
  ... <lockinfo xmlns='DAV:'>
  ...  <lockscope><exclusive/></lockscope>
  ... <locktype><write/></locktype></lockinfo>
  ... """ % (user2, pass2))
  HTTP/1.1 423 Locked
  Accept-Ranges: none
  ...
  <BLANKLINE>

Now, back to the first user, we can issue a lockdiscovery request to
see what are the active lock tokens. As this user is the creator of
the unique existing lock, he will be able to see the lock token just
fine.

  >>> print http(r"""
  ... PROPFIND /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Depth: 0
  ... Te: trailers
  ... 
  ... <?xml version="1.0" encoding="utf-8"?>
  ... <propfind xmlns="DAV:"><prop>
  ... <lockdiscovery xmlns="DAV:"/>
  ... </prop></propfind>
  ... """ % (user1, pass1))
  HTTP/1.1 207 Multi-Status
  Accept-Ranges: none
  ...
  <BLANKLINE>
  <?xml version="1.0" encoding="utf-8"?>
  <d:multistatus xmlns:d="DAV:">
  <d:response>
  <d:href>/plone/index_html</d:href>
  <d:propstat>
    <d:prop>
  <n:lockdiscovery xmlns:n="DAV:">
  <BLANKLINE>
   <n:activelock>
    <n:locktype><n:write/></n:locktype>
    <n:lockscope><n:exclusive/></n:lockscope>
    <n:depth>0</n:depth>
    <n:owner>user1</n:owner>
    <n:timeout>Second-720</n:timeout>
    <n:locktoken>
     <n:href>opaquelocktoken:...00105A989226...</n:href>
    </n:locktoken>
   </n:activelock>
  <BLANKLINE>
  </n:lockdiscovery>
    </d:prop>
    <d:status>HTTP/1.1 200 OK</d:status>
  </d:propstat>
  </d:response>
  </d:multistatus>

And should also be able to unlock the resource, given that the knows
the token.

  >>> print http(r"""
  ... UNLOCK /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Lock-Token: <opaquelocktoken:%s>
  ... Te: trailers
  ... """ % (user1, pass1, token))
  HTTP/1.1 204 No Content
  Accept-Ranges: none
  ...
  <BLANKLINE>

Let's make sure the lock is gone:

  >>> len(self.portal.index_html.wl_lockValues())
  0

  >>> print http(r"""
  ... PROPFIND /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Content-Type: application/xml
  ... Depth: 0
  ... Te: trailers
  ... 
  ... <?xml version="1.0" encoding="utf-8"?>
  ... <propfind xmlns="DAV:"><prop>
  ... <lockdiscovery xmlns="DAV:"/>
  ... </prop></propfind>
  ... """ % (user1, pass1))
  HTTP/1.1 207 Multi-Status
  Accept-Ranges: none
  ...
  <BLANKLINE>
  <?xml version="1.0" encoding="utf-8"?>
  <d:multistatus xmlns:d="DAV:">
  <d:response>
  <d:href>/plone/index_html</d:href>
  <d:propstat>
    <d:prop>
  <n:lockdiscovery xmlns:n="DAV:">
  <BLANKLINE>
  </n:lockdiscovery>
    </d:prop>
    <d:status>HTTP/1.1 200 OK</d:status>
  </d:propstat>
  </d:response>
  </d:multistatus>

Now, back to the second user, let's lock the resource again:

  >>> print http(r"""
  ... LOCK /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Depth: 0
  ... Te: trailers
  ... 
  ... <?xml version="1.0" encoding="utf-8"?>
  ... <lockinfo xmlns='DAV:'>
  ...  <lockscope><exclusive/></lockscope>
  ... <locktype><write/></locktype>
  ... <owner>user2</owner></lockinfo>
  ... """ % (user2, pass2))
  HTTP/1.1 200 OK
  Accept-Ranges: none
  ...
  Lock-Token: opaquelocktoken:...00105A989226...
  <BLANKLINE>
  <?xml version="1.0" encoding="utf-8" ?>
  <d:prop xmlns:d="DAV:">
   <d:lockdiscovery>
     <d:activelock>
    <d:locktype><d:write/></d:locktype>
    <d:lockscope><d:exclusive/></d:lockscope>
    <d:depth>0</d:depth>
    <d:owner>user2</d:owner>
    <d:timeout>Second-720</d:timeout>
    <d:locktoken>
     <d:href>opaquelocktoken:...00105A989226...</d:href>
    </d:locktoken>
   </d:activelock>
  <BLANKLINE>
   </d:lockdiscovery>
  </d:prop>

Supposing that the first user knows the lock token, he can easily
steal the lock, even not being the lock owner:

  >>> len(self.portal.index_html.wl_lockValues())
  1

  >>> token = self.portal.index_html.wl_lockValues()[0].getLockToken()

  >>> print http(r"""
  ... UNLOCK /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Lock-Token: <opaquelocktoken:%s>
  ... Te: trailers
  ... """ % (user1, pass1, token))
  HTTP/1.1 204 No Content
  Accept-Ranges: none
  ...
  <BLANKLINE>

  >>> len(self.portal.index_html.wl_lockValues())
  0

Zope only accepts 'exclusive' 'write' locks.

  >>> print http(r"""
  ... LOCK /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Depth: 0
  ... Te: trailers
  ... 
  ... <?xml version="1.0" encoding="utf-8"?>
  ... <lockinfo xmlns='DAV:'>
  ...  <lockscope><shared/></lockscope>
  ... <locktype><write/></locktype></lockinfo>
  ... """ % (user1, pass1))
  HTTP/1.1 403 Forbidden
  ...


  >>> print http(r"""
  ... LOCK /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Depth: 0
  ... Te: trailers
  ... 
  ... <?xml version="1.0" encoding="utf-8"?>
  ... <lockinfo xmlns='DAV:'>
  ...  <lockscope><exclusive/></lockscope>
  ... <locktype><read/></locktype></lockinfo>
  ... """ % (user1, pass1))
  HTTP/1.1 403 Forbidden
  ...


Now, though the HTTP/WebDAV interfaces prevent the creation of more
than one lock at a time, nothing prevents the creation of multiple
locks from inside Zope:

  >>> from webdav.LockItem import LockItem

  >>> creator = self.portal.acl_users.getUserById(user1)
  >>> lock = LockItem(creator)
  >>> token = lock.getLockToken()
  >>> self.portal.index_html.wl_setLock(token, lock)

  >>> creator = self.portal.acl_users.getUserById(user2)
  >>> lock = LockItem(creator, owner='user2')
  >>> token = lock.getLockToken()
  >>> self.portal.index_html.wl_setLock(token, lock)

  >>> len(self.portal.index_html.wl_lockValues())
  2

Doing a lock discovery request as user1 should return both locks, but
for the second lock the token will be obscured as the user is not the
creator.

  >>> res = http(r"""
  ... PROPFIND /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Content-Type: application/xml
  ... Depth: 0
  ... Te: trailers
  ... 
  ... <?xml version="1.0" encoding="utf-8"?>
  ... <propfind xmlns="DAV:"><prop>
  ... <lockdiscovery xmlns="DAV:"/>
  ... </prop></propfind>
  ... """ % (user1, pass1))

The order that the tokens are returned may vary, account for this by
checking each one separatedly.

Check status:

  >>> print res
  HTTP/1.1 207 Multi-Status
  Accept-Ranges: none
  ...

Check that one of the tokens is owned by user2 and has a fake token:

  >>> print res
  HTTP/1.1 207 Multi-Status
  ...
  <BLANKLINE>
     <n:activelock>
    <n:locktype><n:write/></n:locktype>
    <n:lockscope><n:exclusive/></n:lockscope>
    <n:depth>0</n:depth>
    <n:owner>user2</n:owner>
    <n:timeout>Second-720</n:timeout>
    <n:locktoken>
     <n:href>opaquelocktoken:this-is-a-faked-no-permission-token</n:href>
    </n:locktoken>
   </n:activelock>
  <BLANKLINE>
  ...

Check that the other token is not fake:

  >>> print res
  HTTP/1.1 207 Multi-Status
  ...
  <BLANKLINE>
   <n:activelock>
    <n:locktype><n:write/></n:locktype>
    <n:lockscope><n:exclusive/></n:lockscope>
    <n:depth>0</n:depth>
    <n:owner></n:owner>
    <n:timeout>Second-720</n:timeout>
    <n:locktoken>
     <n:href>opaquelocktoken:...00105A989226...</n:href>
    </n:locktoken>
   </n:activelock>
  <BLANKLINE>
  ...

Test for issue #182 of the ShellEx collector: Unicode Errors with Lock
Token. The issue seems to happen on the presence of a unicode value
being passed to LockItem constructor, resulting in a unicode
lock token, which later would raise a UnicodeDecodeError when asked
for dav__allprop. A failure in this test means you need a version of
DavPack later than revision 667.

  >>> self.portal.index_html.setTitle(u'La Pe\xf1a'.encode('utf-8'))

  >>> print http(r"""
  ... PROPFIND /plone/index_html HTTP/1.1
  ... Authorization: Basic %s:%s
  ... Content-Type: application/xml
  ... Depth: 0
  ... """ % (user1, pass1))
  HTTP/1.1 207 Multi-Status
  ...

