#!/usr/bin/env python
#
#  mod_dav_svn_tests.py:  testing mod_dav_svn
#
#  Subversion is a tool for revision control.
#  See http://subversion.apache.org for more information.
#
# ====================================================================
#    Licensed to the Apache Software Foundation (ASF) under one
#    or more contributor license agreements.  See the NOTICE file
#    distributed with this work for additional information
#    regarding copyright ownership.  The ASF licenses this file
#    to you under the Apache License, Version 2.0 (the
#    "License"); you may not use this file except in compliance
#    with the License.  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing,
#    software distributed under the License is distributed on an
#    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
#    KIND, either express or implied.  See the License for the
#    specific language governing permissions and limitations
#    under the License.
######################################################################

# General modules
import os, logging, base64, functools

try:
  # Python <3.0
  import httplib
except ImportError:
  # Python >=3.0
  import http.client as httplib

logger = logging.getLogger()

# Our testing module
import svntest

# (abbreviation)
Skip = svntest.testcase.Skip_deco
SkipUnless = svntest.testcase.SkipUnless_deco
XFail = svntest.testcase.XFail_deco
Issues = svntest.testcase.Issues_deco
Issue = svntest.testcase.Issue_deco
Wimp = svntest.testcase.Wimp_deco

######################################################################
# Helper routines

def compare(lhs, rhs):
  """Implements cmp() for Python 2 and 3 alike"""
  if lhs == None:
    if rhs == None:
      return 0
    else:
      return -1
  else:
    if rhs == None:
      return 1
    else:
      return (lhs > rhs) - (lhs < rhs)

def compare_dict(lhs, rhs):
  """Implements dictionary comparison for Python 2 and 3 alike"""
  lhs_sorted = sorted(lhs, key=lambda x:sorted(x.keys()))
  rhs_sorted = sorted(rhs, key=lambda x:sorted(x.keys()))
  return (lhs_sorted > rhs_sorted) - (lhs_sorted < rhs_sorted)

def compare_xml_elem(a, b):
  """Recursively compare two xml.etree.ElementTree.Element objects.
  Return a 3-tuple made out of (cmp, elem_a, elem_b), where cmp is
  the integer result of the comparison (negative, zero or positive),
  and elem_a and elem_b point to mismatching elements.  Iff cmp is
  zero, elem_a and elem_b are None. """

  # Compare tags, attributes, inner text, tail attribute and the
  # number of child elements.
  res = compare(a.tag, b.tag)
  if res != 0:
    return res, a, b
  # Don't care about the order of the attributes.
  res = compare_dict(a.attrib, b.attrib)
  if res != 0:
    return res, a, b
  res = compare(a.text, b.text)
  if res != 0:
    return res, a, b
  res = compare(a.tail, b.tail)
  if res != 0:
    return res, a, b
  res = compare(len(a), len(b))
  if res != 0:
    return res, a, b

  # Prior to recursing, order child elements using the same comparator.
  # Right now we don't care about the order of the elements.  For instance,
  # <D:response>'s in PROPFIND *need* to be compared without a particular
  # order, since the server returns them in an unstable order of the hash
  # iteration.
  def sortcmp(x, y):
    return compare_xml_elem(x, y)[0]

  a_children = sorted(list(a), key=functools.cmp_to_key(sortcmp))
  b_children = sorted(list(b), key=functools.cmp_to_key(sortcmp))

  for a_child, b_child in zip(a_children, b_children):
    res = compare_xml_elem(a_child, b_child)
    if res[0] != 0:
      return res

  # Elements are equal.
  return 0, None, None

def verify_xml_response(expected_xml, actual_xml):
  """Parse and compare two XML responses, raise svntest.Failure
  in case EXPECTED_XML doesn't match ACTUAL_XML. """

  import xml.etree.ElementTree as ET

  expected_root = ET.fromstring(expected_xml)
  actual_root = ET.fromstring(actual_xml)
  res, expected_elem, actual_elem = compare_xml_elem(expected_root,
                                                     actual_root)
  if res != 0:
    # The actual response doesn't match our expectations; dump it for
    # debugging purposes, and highlight the mismatching xml element.
    logger.warn("Response:\n%s" % actual_xml)
    raise svntest.Failure("Unexpected response part\n"
                          "  Expected: '%s'\n    Actual: '%s'\n"
                          % (ET.tostring(expected_elem),
                             ET.tostring(actual_elem)))

######################################################################
# Tests

@SkipUnless(svntest.main.is_ra_type_dav)
def cache_control_header(sbox):
  "verify 'Cache-Control' headers on responses"

  sbox.build(create_wc=False, read_only=True)

  headers = {
    'Authorization': 'Basic ' + base64.b64encode(b'jconstant:rayjandom').decode(),
  }

  h = svntest.main.create_http_connection(sbox.repo_url)

  # GET /repos/iota
  # Response depends on the youngest revision in the repository, and
  # can't be cached; expect to see Cache-Control: max-age=0.
  h.request('GET', sbox.repo_url + '/iota', None, headers)
  r = h.getresponse()
  if r.status != httplib.OK:
    raise svntest.Failure('Request failed: %d %s' % (r.status, r.reason))
  svntest.verify.compare_and_display_lines(None, 'Cache-Control',
                                           'max-age=0',
                                           r.getheader('Cache-Control'))
  r.read()

  # GET /repos/A/
  # Response depends on the youngest revision in the repository, and
  # can't be cached; expect to see Cache-Control: max-age=0.
  h.request('GET', sbox.repo_url + '/A/', None, headers)
  r = h.getresponse()
  if r.status != httplib.OK:
    raise svntest.Failure('Request failed: %d %s' % (r.status, r.reason))
  svntest.verify.compare_and_display_lines(None, 'Cache-Control',
                                           'max-age=0',
                                           r.getheader('Cache-Control'))
  r.read()

  # GET /repos/A/?p=1
  # Response for a pegged directory is a subject for authz filtering, and
  # can't be cached; expect to see Cache-Control: max-age=0.
  h.request('GET', sbox.repo_url + '/A/?p=1', None, headers)
  r = h.getresponse()
  if r.status != httplib.OK:
    raise svntest.Failure('Request failed: %d %s' % (r.status, r.reason))
  svntest.verify.compare_and_display_lines(None, 'Cache-Control',
                                           'max-age=0',
                                           r.getheader('Cache-Control'))
  r.read()

  # GET /repos/iota?r=1
  # Response for a file URL with ?r=WORKINGREV is mutable, because the
  # line of history for this file can be replaced in the future (hence,
  # the same request will start producing another response).  Expect to
  # see Cache-Control: max-age=0.
  h.request('GET', sbox.repo_url + '/iota?r=1', None, headers)
  r = h.getresponse()
  if r.status != httplib.OK:
    raise svntest.Failure('Request failed: %d %s' % (r.status, r.reason))
  svntest.verify.compare_and_display_lines(None, 'Cache-Control',
                                           'max-age=0',
                                           r.getheader('Cache-Control'))
  r.read()

  # GET /repos/iota?p=1
  # Response for a pegged file is immutable; expect to see Cache-Control
  # with non-zero max-age.
  h.request('GET', sbox.repo_url + '/iota?p=1', None, headers)
  r = h.getresponse()
  if r.status != httplib.OK:
    raise svntest.Failure('Request failed: %d %s' % (r.status, r.reason))
  svntest.verify.compare_and_display_lines(None, 'Cache-Control',
                                           'max-age=604800',
                                           r.getheader('Cache-Control'))
  r.read()

  # GET /repos/iota?p=1&r=1
  # Response for a file URL with both ?p=PEG_REV and ?r=WORKINGREV is
  # immutable; expect to see Cache-Control with non-zero max-age.
  h.request('GET', sbox.repo_url + '/iota?p=1&r=1', None, headers)
  r = h.getresponse()
  if r.status != httplib.OK:
    raise svntest.Failure('Request failed: %d %s' % (r.status, r.reason))
  svntest.verify.compare_and_display_lines(None, 'Cache-Control',
                                           'max-age=604800',
                                           r.getheader('Cache-Control'))
  r.read()


  # GET /repos/!svn/rvr/1/iota
  # Response is immutable; expect to see Cache-Control with non-zero max-age.
  h.request('GET', sbox.repo_url + '/!svn/rvr/1/iota', None, headers)
  r = h.getresponse()
  if r.status != httplib.OK:
    raise svntest.Failure('Request failed: %d %s' % (r.status, r.reason))
  svntest.verify.compare_and_display_lines(None, 'Cache-Control',
                                           'max-age=604800',
                                           r.getheader('Cache-Control'))
  r.read()


@SkipUnless(svntest.main.is_ra_type_dav)
def simple_propfind(sbox):
  "verify simple PROPFIND responses"

  sbox.build(create_wc=False, read_only=True)
  repo_uripath = '/' + svntest.wc.svn_uri_quote(
    svntest.main.pristine_greek_repos_dir.replace(os.path.sep, '/'))
  h = svntest.main.create_http_connection(sbox.repo_url)

  # PROPFIND /repos/!svn/rvr/1, Depth = 0
  headers = {
    'Authorization': 'Basic ' + base64.b64encode(b'jconstant:rayjandom').decode(),
    'Depth': '0',
  }
  req_body = (
    '<?xml version="1.0" encoding="utf-8"?>\n'
    '<propfind xmlns="DAV:">\n'
    '<prop><resourcetype xmlns="DAV:"/></prop>\n'
    '</propfind>\n'
    )
  h.request('PROPFIND', sbox.repo_url + '/!svn/rvr/1', req_body, headers)
  r = h.getresponse()
  if r.status != httplib.MULTI_STATUS:
    raise svntest.Failure('Unexpected status: %d %s' % (r.status, r.reason))

  expected_response = (
    '<?xml version="1.0" encoding="utf-8"?>\n'
    '<D:multistatus xmlns:D="DAV:" xmlns:ns0="DAV:">\n'
      '<D:response xmlns:lp1="DAV:" '
        'xmlns:lp2="http://subversion.tigris.org/xmlns/dav/">\n'
        '<D:href>' + repo_uripath + '/!svn/rvr/1/</D:href>\n'
        '<D:propstat>\n'
          '<D:prop>\n'
            '<lp1:resourcetype><D:collection/></lp1:resourcetype>\n'
          '</D:prop>\n'
          '<D:status>HTTP/1.1 200 OK</D:status>\n'
        '</D:propstat>\n'
      '</D:response>\n'
    '</D:multistatus>\n'
    )
  actual_response = r.read()
  verify_xml_response(expected_response, actual_response)

  # PROPFIND /repos/!svn/rvr/1, Depth = 1
  headers = {
    'Authorization': 'Basic ' + base64.b64encode(b'jconstant:rayjandom').decode(),
    'Depth': '1',
  }
  req_body = (
    '<?xml version="1.0" encoding="utf-8"?>\n'
    '<propfind xmlns="DAV:">\n'
      '<prop><resourcetype xmlns="DAV:"/></prop>\n'
    '</propfind>\n'
    )
  h.request('PROPFIND', sbox.repo_url + '/!svn/rvr/1', req_body, headers)
  r = h.getresponse()
  if r.status != httplib.MULTI_STATUS:
    raise svntest.Failure('Unexpected status: %d %s' % (r.status, r.reason))

  expected_response = (
    '<?xml version="1.0" encoding="utf-8"?>\n'
    '<D:multistatus xmlns:D="DAV:" xmlns:ns0="DAV:">\n'
      '<D:response xmlns:lp1="DAV:" '
        'xmlns:lp2="http://subversion.tigris.org/xmlns/dav/">\n'
        '<D:href>' + repo_uripath + '/!svn/rvr/1/</D:href>\n'
        '<D:propstat>\n'
          '<D:prop>\n'
            '<lp1:resourcetype><D:collection/></lp1:resourcetype>\n'
          '</D:prop>\n'
          '<D:status>HTTP/1.1 200 OK</D:status>\n'
        '</D:propstat>\n'
      '</D:response>\n'
      '<D:response xmlns:lp1="DAV:" '
        'xmlns:lp2="http://subversion.tigris.org/xmlns/dav/">\n'
        '<D:href>' + repo_uripath + '/!svn/rvr/1/A/</D:href>\n'
        '<D:propstat>\n'
          '<D:prop>\n'
            '<lp1:resourcetype><D:collection/></lp1:resourcetype>\n'
          '</D:prop>\n'
          '<D:status>HTTP/1.1 200 OK</D:status>\n'
        '</D:propstat>\n'
      '</D:response>\n'
      '<D:response xmlns:lp1="DAV:" '
        'xmlns:lp2="http://subversion.tigris.org/xmlns/dav/">\n'
        '<D:href>' + repo_uripath + '/!svn/rvr/1/iota</D:href>\n'
        '<D:propstat>\n'
          '<D:prop>\n'
            '<lp1:resourcetype/>\n'
          '</D:prop>\n'
          '<D:status>HTTP/1.1 200 OK</D:status>\n'
        '</D:propstat>\n'
      '</D:response>\n'
    '</D:multistatus>\n'
    )
  actual_response = r.read()
  verify_xml_response(expected_response, actual_response)

  # PROPFIND /repos/!svn/rvr/1/A/B/F, Depth = 1
  headers = {
    'Authorization': 'Basic ' + base64.b64encode(b'jconstant:rayjandom').decode(),
    'Depth': '1',
  }
  req_body = (
    '<?xml version="1.0" encoding="utf-8"?>\n'
    '<propfind xmlns="DAV:">\n'
      '<prop><resourcetype xmlns="DAV:"/></prop>\n'
    '</propfind>\n'
    )
  h.request('PROPFIND', sbox.repo_url + '/!svn/rvr/1/A/B/F', req_body, headers)
  r = h.getresponse()
  if r.status != httplib.MULTI_STATUS:
    raise svntest.Failure('Unexpected status: %d %s' % (r.status, r.reason))

  expected_response = (
    '<?xml version="1.0" encoding="utf-8"?>\n'
    '<D:multistatus xmlns:D="DAV:" xmlns:ns0="DAV:">\n'
      '<D:response xmlns:lp1="DAV:" '
        'xmlns:lp2="http://subversion.tigris.org/xmlns/dav/">\n'
        '<D:href>' + repo_uripath + '/!svn/rvr/1/A/B/F/</D:href>\n'
        '<D:propstat>\n'
          '<D:prop>\n'
            '<lp1:resourcetype><D:collection/></lp1:resourcetype>\n'
          '</D:prop>\n'
          '<D:status>HTTP/1.1 200 OK</D:status>\n'
        '</D:propstat>\n'
      '</D:response>\n'
    '</D:multistatus>\n'
    )
  actual_response = r.read()
  verify_xml_response(expected_response, actual_response)

  # PROPFIND /repos/!svn/rvr/1/iota, Depth = 0
  headers = {
    'Authorization': 'Basic ' + base64.b64encode(b'jconstant:rayjandom').decode(),
    'Depth': '0',
  }
  req_body = (
    '<?xml version="1.0" encoding="utf-8"?>\n'
    '<propfind xmlns="DAV:">\n'
      '<prop><resourcetype xmlns="DAV:"/></prop>\n'
    '</propfind>\n'
    )
  h.request('PROPFIND', sbox.repo_url + '/!svn/rvr/1/iota', req_body, headers)
  r = h.getresponse()
  if r.status != httplib.MULTI_STATUS:
    raise svntest.Failure('Unexpected status: %d %s' % (r.status, r.reason))

  expected_response = (
    '<?xml version="1.0" encoding="utf-8"?>\n'
    '<D:multistatus xmlns:D="DAV:" xmlns:ns0="DAV:">\n'
      '<D:response xmlns:lp1="DAV:" '
        'xmlns:lp2="http://subversion.tigris.org/xmlns/dav/">\n'
        '<D:href>' + repo_uripath + '/!svn/rvr/1/iota</D:href>\n'
        '<D:propstat>\n'
          '<D:prop>\n'
            '<lp1:resourcetype/>\n'
          '</D:prop>\n'
          '<D:status>HTTP/1.1 200 OK</D:status>\n'
        '</D:propstat>\n'
      '</D:response>\n'
    '</D:multistatus>\n'
    )
  actual_response = r.read()
  verify_xml_response(expected_response, actual_response)


@SkipUnless(svntest.main.is_ra_type_dav)
def propfind_multiple_props(sbox):
  "verify multi-prop PROPFIND response"

  sbox.build(create_wc=False, read_only=True)
  repo_uripath = '/' + svntest.wc.svn_uri_quote(
    svntest.main.pristine_greek_repos_dir.replace(os.path.sep, '/'))
  h = svntest.main.create_http_connection(sbox.repo_url)

  # PROPFIND /repos/!svn/rvr/1/iota, Depth = 0
  headers = {
    'Authorization': 'Basic ' + base64.b64encode(b'jconstant:rayjandom').decode(),
    'Depth': '0',
  }
  req_body = (
    '<?xml version="1.0" encoding="utf-8" ?>\n'
    '<D:propfind xmlns:D="DAV:">\n'
      '<D:prop xmlns:S="http://subversion.tigris.org/xmlns/dav/">\n'
         '<D:resourcetype/>\n'
         '<S:md5-checksum/>\n'
      '</D:prop>\n'
    '</D:propfind>\n'
    )
  h.request('PROPFIND', sbox.repo_url + '/!svn/rvr/1/iota', req_body, headers)
  r = h.getresponse()
  if r.status != httplib.MULTI_STATUS:
    raise svntest.Failure('Unexpected status: %d %s' % (r.status, r.reason))

  expected_response = (
    '<?xml version="1.0" encoding="utf-8"?>\n'
    '<D:multistatus xmlns:D="DAV:" '
                   'xmlns:ns1="http://subversion.tigris.org/xmlns/dav/" '
                   'xmlns:ns0="DAV:">\n'
      '<D:response xmlns:lp1="DAV:" '
                  'xmlns:lp2="http://subversion.tigris.org/xmlns/dav/">\n'
      '<D:href>' + repo_uripath + '/!svn/rvr/1/iota</D:href>\n'
        '<D:propstat>\n'
          '<D:prop>\n'
            '<lp1:resourcetype/>\n'
            '<lp2:md5-checksum>'
              '2d18c5e57e84c5b8a5e9a6e13fa394dc'
            '</lp2:md5-checksum>\n'
          '</D:prop>\n'
          '<D:status>HTTP/1.1 200 OK</D:status>\n'
        '</D:propstat>\n'
      '</D:response>\n'
    '</D:multistatus>\n'
    )
  actual_response = r.read()
  verify_xml_response(expected_response, actual_response)


@SkipUnless(svntest.main.is_ra_type_dav)
def propfind_404(sbox):
  "verify PROPFIND for non-existing property"

  sbox.build(create_wc=False, read_only=True)
  repo_uripath = '/' + svntest.wc.svn_uri_quote(
    svntest.main.pristine_greek_repos_dir.replace(os.path.sep, '/'))
  h = svntest.main.create_http_connection(sbox.repo_url)

  # PROPFIND /repos/!svn/rvr/1, Depth = 0
  headers = {
    'Authorization': 'Basic ' + base64.b64encode(b'jconstant:rayjandom').decode(),
    'Depth': '0',
  }
  req_body = (
    '<?xml version="1.0" encoding="utf-8"?>\n'
    '<propfind xmlns="DAV:">\n'
      '<prop><nonexistingprop xmlns="DAV:"/></prop>\n'
    '</propfind>\n'
    )
  h.request('PROPFIND', sbox.repo_url + '/!svn/rvr/1', req_body, headers)
  r = h.getresponse()
  if r.status != httplib.MULTI_STATUS:
    raise svntest.Failure('Unexpected status: %d %s' % (r.status, r.reason))

  expected_response = (
    '<?xml version="1.0" encoding="utf-8"?>\n'
    '<D:multistatus xmlns:D="DAV:" xmlns:ns0="DAV:">\n'
      '<D:response xmlns:g0="DAV:">\n'
        '<D:href>' + repo_uripath + '/!svn/rvr/1/</D:href>\n'
        '<D:propstat>\n'
          '<D:prop>\n'
            '<g0:nonexistingprop/>\n'
          '</D:prop>\n'
          '<D:status>HTTP/1.1 404 Not Found</D:status>\n'
        '</D:propstat>\n'
      '</D:response>\n'
    '</D:multistatus>\n'
    )
  actual_response = r.read()
  verify_xml_response(expected_response, actual_response)


@SkipUnless(svntest.main.is_ra_type_dav)
def propfind_allprop(sbox):
  "verify allprop PROPFIND response"

  sbox.build()
  repo_uripath = '/' + svntest.wc.svn_uri_quote(
    sbox.repo_dir.replace(os.path.sep, '/'))
  svntest.actions.enable_revprop_changes(sbox.repo_dir)
  # Ensure stable date and uuid
  svntest.main.run_svnadmin('setuuid', sbox.repo_dir,
                            'd7130b12-92f6-45c9-9217-b9f0472c3fab')
  svntest.actions.run_and_verify_svn(None, [],
                                     'propset', '--revprop', '-r', '1',
                                     'svn:date', '2015-01-01T00:00:00.0Z',
                                     sbox.wc_dir)

  h = svntest.main.create_http_connection(sbox.repo_url)

  # PROPFIND /repos/!svn/rvr/1, Depth = 0
  headers = {
    'Authorization': 'Basic ' + base64.b64encode(b'jconstant:rayjandom').decode(),
    'Depth': '0',
  }
  req_body = (
    '<?xml version="1.0" encoding="utf-8"?>\n'
    '<propfind xmlns="DAV:">\n'
      '<allprop/>\n'
    '</propfind>\n'
    )
  h.request('PROPFIND', sbox.repo_url + '/!svn/rvr/1', req_body, headers)
  r = h.getresponse()
  if r.status != httplib.MULTI_STATUS:
    raise svntest.Failure('Unexpected status: %d %s' % (r.status, r.reason))

  expected_response = (
    '<?xml version="1.0" encoding="utf-8"?>\n'
    '<D:multistatus xmlns:D="DAV:" xmlns:ns0="DAV:">\n'
      '<D:response xmlns:S="http://subversion.tigris.org/xmlns/svn/" '
                  'xmlns:C="http://subversion.tigris.org/xmlns/custom/" '
                  'xmlns:V="http://subversion.tigris.org/xmlns/dav/" '
                  'xmlns:lp1="DAV:" '
                  'xmlns:lp2="http://subversion.tigris.org/xmlns/dav/">\n'
        '<D:href>' + repo_uripath + '/!svn/rvr/1/</D:href>\n'
        '<D:propstat>\n'
          '<D:prop>\n'
            '<lp1:resourcetype><D:collection/></lp1:resourcetype>\n'
            '<lp1:getcontenttype>' +
              'text/html; charset=UTF-8' +
            '</lp1:getcontenttype>\n'
            '<lp1:getetag>W/"1//"</lp1:getetag>\n'
            '<lp1:creationdate>2015-01-01T00:00:00.0Z</lp1:creationdate>\n'
            '<lp1:getlastmodified>' +
              'Thu, 01 Jan 2015 00:00:00 GMT' +
            '</lp1:getlastmodified>\n'
            '<lp1:checked-in>'
              '<D:href>' + repo_uripath + '/!svn/ver/1/</D:href>'
            '</lp1:checked-in>\n'
            '<lp1:version-controlled-configuration>'
              '<D:href>' + repo_uripath + '/!svn/vcc/default</D:href>'
            '</lp1:version-controlled-configuration>\n'
            '<lp1:version-name>1</lp1:version-name>\n'
            '<lp1:creator-displayname>jrandom</lp1:creator-displayname>\n'
            '<lp2:baseline-relative-path/>\n'
            '<lp2:repository-uuid>' +
              'd7130b12-92f6-45c9-9217-b9f0472c3fab' +
            '</lp2:repository-uuid>\n'
            '<lp2:deadprop-count>0</lp2:deadprop-count>\n'
            '<D:lockdiscovery/>\n'
          '</D:prop>\n'
        '<D:status>HTTP/1.1 200 OK</D:status>\n'
        '</D:propstat>\n'
      '</D:response>\n'
    '</D:multistatus>\n'
    )
  actual_response = r.read()
  verify_xml_response(expected_response, actual_response)


@SkipUnless(svntest.main.is_ra_type_dav)
def propfind_propname(sbox):
  "verify propname PROPFIND response"

  sbox.build()
  sbox.simple_propset('a', 'b', 'iota')
  sbox.simple_commit()
  repo_uripath = '/' + svntest.wc.svn_uri_quote(
    sbox.repo_dir.replace(os.path.sep, '/'))

  h = svntest.main.create_http_connection(sbox.repo_url)

  # PROPFIND /repos/!svn/rvr/2/iota, Depth = 0
  headers = {
    'Authorization': 'Basic ' + base64.b64encode(b'jconstant:rayjandom').decode(),
    'Depth': '0',
  }
  req_body = (
    '<?xml version="1.0" encoding="utf-8"?>\n'
    '<propfind xmlns="DAV:">\n'
      '<propname/>\n'
    '</propfind>\n'
    )
  h.request('PROPFIND', sbox.repo_url + '/!svn/rvr/2/iota', req_body, headers)
  r = h.getresponse()
  if r.status != httplib.MULTI_STATUS:
    raise svntest.Failure('Unexpected status: %d %s' % (r.status, r.reason))

  expected_response = (
    '<?xml version="1.0" encoding="utf-8"?>\n'
    '<D:multistatus xmlns:D="DAV:" xmlns:ns0="DAV:">\n'
      '<D:response xmlns:S="http://subversion.tigris.org/xmlns/svn/" '
                  'xmlns:C="http://subversion.tigris.org/xmlns/custom/" '
                  'xmlns:V="http://subversion.tigris.org/xmlns/dav/" '
                  'xmlns:lp1="DAV:" '
                  'xmlns:lp2="http://subversion.tigris.org/xmlns/dav/">\n'
        '<D:href>' + repo_uripath + '/!svn/rvr/2/iota</D:href>\n'
        '<D:propstat>\n'
          '<D:prop>\n'
          '<C:a/>\n'
          '<lp1:resourcetype/>\n'
          '<lp1:getcontentlength/>\n'
          '<lp1:getcontenttype/>\n'
          '<lp1:getetag/>\n'
          '<lp1:creationdate/>\n'
          '<lp1:getlastmodified/>\n'
          '<lp1:checked-in/>\n'
          '<lp1:version-controlled-configuration/>\n'
          '<lp1:version-name/>\n'
          '<lp1:creator-displayname/>\n'
          '<lp2:baseline-relative-path/>\n'
          '<lp2:md5-checksum/>\n'
          '<lp2:repository-uuid/>\n'
          '<lp2:deadprop-count/>\n'
          '<lp2:sha1-checksum/>\n'
          '<D:supportedlock/>\n'
          '<D:lockdiscovery/>\n'
          '</D:prop>\n'
          '<D:status>HTTP/1.1 200 OK</D:status>\n'
        '</D:propstat>\n'
      '</D:response>\n'
    '</D:multistatus>\n'
    )
  actual_response = r.read()
  verify_xml_response(expected_response, actual_response)

@SkipUnless(svntest.main.is_ra_type_dav)
def last_modified_header(sbox):
  "verify 'Last-Modified' header on 'external' GETs"

  sbox.build(create_wc=False, read_only=True)

  headers = {
    'Authorization': 'Basic ' + base64.b64encode(b'jconstant:rayjandom').decode(),
  }

  h = svntest.main.create_http_connection(sbox.repo_url)

  # GET /repos/iota
  # Expect to see a Last-Modified header.
  h.request('GET', sbox.repo_url + '/iota', None, headers)
  r = h.getresponse()
  if r.status != httplib.OK:
    raise svntest.Failure('Request failed: %d %s' % (r.status, r.reason))
  svntest.verify.compare_and_display_lines(None, 'Last-Modified',
                                           svntest.verify.RegexOutput('.+'),
                                           r.getheader('Last-Modified'))
  r.read()

  # HEAD /repos/iota
  # Expect to see a Last-Modified header.
  h.request('HEAD', sbox.repo_url + '/iota', None, headers)
  r = h.getresponse()
  if r.status != httplib.OK:
    raise svntest.Failure('Request failed: %d %s' % (r.status, r.reason))
  svntest.verify.compare_and_display_lines(None, 'Last-Modified',
                                           svntest.verify.RegexOutput('.+'),
                                           r.getheader('Last-Modified'))
  r.read()

  # GET /repos/!svn/rvr/1/iota
  # There should not be a Last-Modified header (it's costly and not useful,
  # see r1724790)
  h.request('GET', sbox.repo_url + '/!svn/rvr/1/iota', None, headers)
  r = h.getresponse()
  if r.status != httplib.OK:
    raise svntest.Failure('Request failed: %d %s' % (r.status, r.reason))
  last_modified = r.getheader('Last-Modified')
  if last_modified:
    raise svntest.Failure('Unexpected Last-Modified header: %s' % last_modified)
  r.read()


########################################################################
# Run the tests


# list all tests here, starting with None:
test_list = [ None,
              cache_control_header,
              simple_propfind,
              propfind_multiple_props,
              propfind_404,
              propfind_allprop,
              propfind_propname,
              last_modified_header,
             ]
serial_only = True

if __name__ == '__main__':
  svntest.main.run_tests(test_list)
  # NOTREACHED


### End of file.
