OSDN Git Service

Hulk did something
[bytom/vapor.git] / vendor / golang.org / x / net / webdav / xml_test.go
diff --git a/vendor/golang.org/x/net/webdav/xml_test.go b/vendor/golang.org/x/net/webdav/xml_test.go
new file mode 100644 (file)
index 0000000..a3d9e1e
--- /dev/null
@@ -0,0 +1,906 @@
+// Copyright 2014 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package webdav
+
+import (
+       "bytes"
+       "encoding/xml"
+       "fmt"
+       "io"
+       "net/http"
+       "net/http/httptest"
+       "reflect"
+       "sort"
+       "strings"
+       "testing"
+
+       ixml "golang.org/x/net/webdav/internal/xml"
+)
+
+func TestReadLockInfo(t *testing.T) {
+       // The "section x.y.z" test cases come from section x.y.z of the spec at
+       // http://www.webdav.org/specs/rfc4918.html
+       testCases := []struct {
+               desc       string
+               input      string
+               wantLI     lockInfo
+               wantStatus int
+       }{{
+               "bad: junk",
+               "xxx",
+               lockInfo{},
+               http.StatusBadRequest,
+       }, {
+               "bad: invalid owner XML",
+               "" +
+                       "<D:lockinfo xmlns:D='DAV:'>\n" +
+                       "  <D:lockscope><D:exclusive/></D:lockscope>\n" +
+                       "  <D:locktype><D:write/></D:locktype>\n" +
+                       "  <D:owner>\n" +
+                       "    <D:href>   no end tag   \n" +
+                       "  </D:owner>\n" +
+                       "</D:lockinfo>",
+               lockInfo{},
+               http.StatusBadRequest,
+       }, {
+               "bad: invalid UTF-8",
+               "" +
+                       "<D:lockinfo xmlns:D='DAV:'>\n" +
+                       "  <D:lockscope><D:exclusive/></D:lockscope>\n" +
+                       "  <D:locktype><D:write/></D:locktype>\n" +
+                       "  <D:owner>\n" +
+                       "    <D:href>   \xff   </D:href>\n" +
+                       "  </D:owner>\n" +
+                       "</D:lockinfo>",
+               lockInfo{},
+               http.StatusBadRequest,
+       }, {
+               "bad: unfinished XML #1",
+               "" +
+                       "<D:lockinfo xmlns:D='DAV:'>\n" +
+                       "  <D:lockscope><D:exclusive/></D:lockscope>\n" +
+                       "  <D:locktype><D:write/></D:locktype>\n",
+               lockInfo{},
+               http.StatusBadRequest,
+       }, {
+               "bad: unfinished XML #2",
+               "" +
+                       "<D:lockinfo xmlns:D='DAV:'>\n" +
+                       "  <D:lockscope><D:exclusive/></D:lockscope>\n" +
+                       "  <D:locktype><D:write/></D:locktype>\n" +
+                       "  <D:owner>\n",
+               lockInfo{},
+               http.StatusBadRequest,
+       }, {
+               "good: empty",
+               "",
+               lockInfo{},
+               0,
+       }, {
+               "good: plain-text owner",
+               "" +
+                       "<D:lockinfo xmlns:D='DAV:'>\n" +
+                       "  <D:lockscope><D:exclusive/></D:lockscope>\n" +
+                       "  <D:locktype><D:write/></D:locktype>\n" +
+                       "  <D:owner>gopher</D:owner>\n" +
+                       "</D:lockinfo>",
+               lockInfo{
+                       XMLName:   ixml.Name{Space: "DAV:", Local: "lockinfo"},
+                       Exclusive: new(struct{}),
+                       Write:     new(struct{}),
+                       Owner: owner{
+                               InnerXML: "gopher",
+                       },
+               },
+               0,
+       }, {
+               "section 9.10.7",
+               "" +
+                       "<D:lockinfo xmlns:D='DAV:'>\n" +
+                       "  <D:lockscope><D:exclusive/></D:lockscope>\n" +
+                       "  <D:locktype><D:write/></D:locktype>\n" +
+                       "  <D:owner>\n" +
+                       "    <D:href>http://example.org/~ejw/contact.html</D:href>\n" +
+                       "  </D:owner>\n" +
+                       "</D:lockinfo>",
+               lockInfo{
+                       XMLName:   ixml.Name{Space: "DAV:", Local: "lockinfo"},
+                       Exclusive: new(struct{}),
+                       Write:     new(struct{}),
+                       Owner: owner{
+                               InnerXML: "\n    <D:href>http://example.org/~ejw/contact.html</D:href>\n  ",
+                       },
+               },
+               0,
+       }}
+
+       for _, tc := range testCases {
+               li, status, err := readLockInfo(strings.NewReader(tc.input))
+               if tc.wantStatus != 0 {
+                       if err == nil {
+                               t.Errorf("%s: got nil error, want non-nil", tc.desc)
+                               continue
+                       }
+               } else if err != nil {
+                       t.Errorf("%s: %v", tc.desc, err)
+                       continue
+               }
+               if !reflect.DeepEqual(li, tc.wantLI) || status != tc.wantStatus {
+                       t.Errorf("%s:\ngot  lockInfo=%v, status=%v\nwant lockInfo=%v, status=%v",
+                               tc.desc, li, status, tc.wantLI, tc.wantStatus)
+                       continue
+               }
+       }
+}
+
+func TestReadPropfind(t *testing.T) {
+       testCases := []struct {
+               desc       string
+               input      string
+               wantPF     propfind
+               wantStatus int
+       }{{
+               desc: "propfind: propname",
+               input: "" +
+                       "<A:propfind xmlns:A='DAV:'>\n" +
+                       "  <A:propname/>\n" +
+                       "</A:propfind>",
+               wantPF: propfind{
+                       XMLName:  ixml.Name{Space: "DAV:", Local: "propfind"},
+                       Propname: new(struct{}),
+               },
+       }, {
+               desc:  "propfind: empty body means allprop",
+               input: "",
+               wantPF: propfind{
+                       Allprop: new(struct{}),
+               },
+       }, {
+               desc: "propfind: allprop",
+               input: "" +
+                       "<A:propfind xmlns:A='DAV:'>\n" +
+                       "   <A:allprop/>\n" +
+                       "</A:propfind>",
+               wantPF: propfind{
+                       XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
+                       Allprop: new(struct{}),
+               },
+       }, {
+               desc: "propfind: allprop followed by include",
+               input: "" +
+                       "<A:propfind xmlns:A='DAV:'>\n" +
+                       "  <A:allprop/>\n" +
+                       "  <A:include><A:displayname/></A:include>\n" +
+                       "</A:propfind>",
+               wantPF: propfind{
+                       XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
+                       Allprop: new(struct{}),
+                       Include: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
+               },
+       }, {
+               desc: "propfind: include followed by allprop",
+               input: "" +
+                       "<A:propfind xmlns:A='DAV:'>\n" +
+                       "  <A:include><A:displayname/></A:include>\n" +
+                       "  <A:allprop/>\n" +
+                       "</A:propfind>",
+               wantPF: propfind{
+                       XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
+                       Allprop: new(struct{}),
+                       Include: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
+               },
+       }, {
+               desc: "propfind: propfind",
+               input: "" +
+                       "<A:propfind xmlns:A='DAV:'>\n" +
+                       "  <A:prop><A:displayname/></A:prop>\n" +
+                       "</A:propfind>",
+               wantPF: propfind{
+                       XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
+                       Prop:    propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
+               },
+       }, {
+               desc: "propfind: prop with ignored comments",
+               input: "" +
+                       "<A:propfind xmlns:A='DAV:'>\n" +
+                       "  <A:prop>\n" +
+                       "    <!-- ignore -->\n" +
+                       "    <A:displayname><!-- ignore --></A:displayname>\n" +
+                       "  </A:prop>\n" +
+                       "</A:propfind>",
+               wantPF: propfind{
+                       XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
+                       Prop:    propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
+               },
+       }, {
+               desc: "propfind: propfind with ignored whitespace",
+               input: "" +
+                       "<A:propfind xmlns:A='DAV:'>\n" +
+                       "  <A:prop>   <A:displayname/></A:prop>\n" +
+                       "</A:propfind>",
+               wantPF: propfind{
+                       XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
+                       Prop:    propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
+               },
+       }, {
+               desc: "propfind: propfind with ignored mixed-content",
+               input: "" +
+                       "<A:propfind xmlns:A='DAV:'>\n" +
+                       "  <A:prop>foo<A:displayname/>bar</A:prop>\n" +
+                       "</A:propfind>",
+               wantPF: propfind{
+                       XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
+                       Prop:    propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
+               },
+       }, {
+               desc: "propfind: propname with ignored element (section A.4)",
+               input: "" +
+                       "<A:propfind xmlns:A='DAV:'>\n" +
+                       "  <A:propname/>\n" +
+                       "  <E:leave-out xmlns:E='E:'>*boss*</E:leave-out>\n" +
+                       "</A:propfind>",
+               wantPF: propfind{
+                       XMLName:  ixml.Name{Space: "DAV:", Local: "propfind"},
+                       Propname: new(struct{}),
+               },
+       }, {
+               desc:       "propfind: bad: junk",
+               input:      "xxx",
+               wantStatus: http.StatusBadRequest,
+       }, {
+               desc: "propfind: bad: propname and allprop (section A.3)",
+               input: "" +
+                       "<A:propfind xmlns:A='DAV:'>\n" +
+                       "  <A:propname/>" +
+                       "  <A:allprop/>" +
+                       "</A:propfind>",
+               wantStatus: http.StatusBadRequest,
+       }, {
+               desc: "propfind: bad: propname and prop",
+               input: "" +
+                       "<A:propfind xmlns:A='DAV:'>\n" +
+                       "  <A:prop><A:displayname/></A:prop>\n" +
+                       "  <A:propname/>\n" +
+                       "</A:propfind>",
+               wantStatus: http.StatusBadRequest,
+       }, {
+               desc: "propfind: bad: allprop and prop",
+               input: "" +
+                       "<A:propfind xmlns:A='DAV:'>\n" +
+                       "  <A:allprop/>\n" +
+                       "  <A:prop><A:foo/><A:/prop>\n" +
+                       "</A:propfind>",
+               wantStatus: http.StatusBadRequest,
+       }, {
+               desc: "propfind: bad: empty propfind with ignored element (section A.4)",
+               input: "" +
+                       "<A:propfind xmlns:A='DAV:'>\n" +
+                       "  <E:expired-props/>\n" +
+                       "</A:propfind>",
+               wantStatus: http.StatusBadRequest,
+       }, {
+               desc: "propfind: bad: empty prop",
+               input: "" +
+                       "<A:propfind xmlns:A='DAV:'>\n" +
+                       "  <A:prop/>\n" +
+                       "</A:propfind>",
+               wantStatus: http.StatusBadRequest,
+       }, {
+               desc: "propfind: bad: prop with just chardata",
+               input: "" +
+                       "<A:propfind xmlns:A='DAV:'>\n" +
+                       "  <A:prop>foo</A:prop>\n" +
+                       "</A:propfind>",
+               wantStatus: http.StatusBadRequest,
+       }, {
+               desc: "bad: interrupted prop",
+               input: "" +
+                       "<A:propfind xmlns:A='DAV:'>\n" +
+                       "  <A:prop><A:foo></A:prop>\n",
+               wantStatus: http.StatusBadRequest,
+       }, {
+               desc: "bad: malformed end element prop",
+               input: "" +
+                       "<A:propfind xmlns:A='DAV:'>\n" +
+                       "  <A:prop><A:foo/></A:bar></A:prop>\n",
+               wantStatus: http.StatusBadRequest,
+       }, {
+               desc: "propfind: bad: property with chardata value",
+               input: "" +
+                       "<A:propfind xmlns:A='DAV:'>\n" +
+                       "  <A:prop><A:foo>bar</A:foo></A:prop>\n" +
+                       "</A:propfind>",
+               wantStatus: http.StatusBadRequest,
+       }, {
+               desc: "propfind: bad: property with whitespace value",
+               input: "" +
+                       "<A:propfind xmlns:A='DAV:'>\n" +
+                       "  <A:prop><A:foo> </A:foo></A:prop>\n" +
+                       "</A:propfind>",
+               wantStatus: http.StatusBadRequest,
+       }, {
+               desc: "propfind: bad: include without allprop",
+               input: "" +
+                       "<A:propfind xmlns:A='DAV:'>\n" +
+                       "  <A:include><A:foo/></A:include>\n" +
+                       "</A:propfind>",
+               wantStatus: http.StatusBadRequest,
+       }}
+
+       for _, tc := range testCases {
+               pf, status, err := readPropfind(strings.NewReader(tc.input))
+               if tc.wantStatus != 0 {
+                       if err == nil {
+                               t.Errorf("%s: got nil error, want non-nil", tc.desc)
+                               continue
+                       }
+               } else if err != nil {
+                       t.Errorf("%s: %v", tc.desc, err)
+                       continue
+               }
+               if !reflect.DeepEqual(pf, tc.wantPF) || status != tc.wantStatus {
+                       t.Errorf("%s:\ngot  propfind=%v, status=%v\nwant propfind=%v, status=%v",
+                               tc.desc, pf, status, tc.wantPF, tc.wantStatus)
+                       continue
+               }
+       }
+}
+
+func TestMultistatusWriter(t *testing.T) {
+       ///The "section x.y.z" test cases come from section x.y.z of the spec at
+       // http://www.webdav.org/specs/rfc4918.html
+       testCases := []struct {
+               desc        string
+               responses   []response
+               respdesc    string
+               writeHeader bool
+               wantXML     string
+               wantCode    int
+               wantErr     error
+       }{{
+               desc: "section 9.2.2 (failed dependency)",
+               responses: []response{{
+                       Href: []string{"http://example.com/foo"},
+                       Propstat: []propstat{{
+                               Prop: []Property{{
+                                       XMLName: xml.Name{
+                                               Space: "http://ns.example.com/",
+                                               Local: "Authors",
+                                       },
+                               }},
+                               Status: "HTTP/1.1 424 Failed Dependency",
+                       }, {
+                               Prop: []Property{{
+                                       XMLName: xml.Name{
+                                               Space: "http://ns.example.com/",
+                                               Local: "Copyright-Owner",
+                                       },
+                               }},
+                               Status: "HTTP/1.1 409 Conflict",
+                       }},
+                       ResponseDescription: "Copyright Owner cannot be deleted or altered.",
+               }},
+               wantXML: `` +
+                       `<?xml version="1.0" encoding="UTF-8"?>` +
+                       `<multistatus xmlns="DAV:">` +
+                       `  <response>` +
+                       `    <href>http://example.com/foo</href>` +
+                       `    <propstat>` +
+                       `      <prop>` +
+                       `        <Authors xmlns="http://ns.example.com/"></Authors>` +
+                       `      </prop>` +
+                       `      <status>HTTP/1.1 424 Failed Dependency</status>` +
+                       `    </propstat>` +
+                       `    <propstat xmlns="DAV:">` +
+                       `      <prop>` +
+                       `        <Copyright-Owner xmlns="http://ns.example.com/"></Copyright-Owner>` +
+                       `      </prop>` +
+                       `      <status>HTTP/1.1 409 Conflict</status>` +
+                       `    </propstat>` +
+                       `  <responsedescription>Copyright Owner cannot be deleted or altered.</responsedescription>` +
+                       `</response>` +
+                       `</multistatus>`,
+               wantCode: StatusMulti,
+       }, {
+               desc: "section 9.6.2 (lock-token-submitted)",
+               responses: []response{{
+                       Href:   []string{"http://example.com/foo"},
+                       Status: "HTTP/1.1 423 Locked",
+                       Error: &xmlError{
+                               InnerXML: []byte(`<lock-token-submitted xmlns="DAV:"/>`),
+                       },
+               }},
+               wantXML: `` +
+                       `<?xml version="1.0" encoding="UTF-8"?>` +
+                       `<multistatus xmlns="DAV:">` +
+                       `  <response>` +
+                       `    <href>http://example.com/foo</href>` +
+                       `    <status>HTTP/1.1 423 Locked</status>` +
+                       `    <error><lock-token-submitted xmlns="DAV:"/></error>` +
+                       `  </response>` +
+                       `</multistatus>`,
+               wantCode: StatusMulti,
+       }, {
+               desc: "section 9.1.3",
+               responses: []response{{
+                       Href: []string{"http://example.com/foo"},
+                       Propstat: []propstat{{
+                               Prop: []Property{{
+                                       XMLName: xml.Name{Space: "http://ns.example.com/boxschema/", Local: "bigbox"},
+                                       InnerXML: []byte(`` +
+                                               `<BoxType xmlns="http://ns.example.com/boxschema/">` +
+                                               `Box type A` +
+                                               `</BoxType>`),
+                               }, {
+                                       XMLName: xml.Name{Space: "http://ns.example.com/boxschema/", Local: "author"},
+                                       InnerXML: []byte(`` +
+                                               `<Name xmlns="http://ns.example.com/boxschema/">` +
+                                               `J.J. Johnson` +
+                                               `</Name>`),
+                               }},
+                               Status: "HTTP/1.1 200 OK",
+                       }, {
+                               Prop: []Property{{
+                                       XMLName: xml.Name{Space: "http://ns.example.com/boxschema/", Local: "DingALing"},
+                               }, {
+                                       XMLName: xml.Name{Space: "http://ns.example.com/boxschema/", Local: "Random"},
+                               }},
+                               Status:              "HTTP/1.1 403 Forbidden",
+                               ResponseDescription: "The user does not have access to the DingALing property.",
+                       }},
+               }},
+               respdesc: "There has been an access violation error.",
+               wantXML: `` +
+                       `<?xml version="1.0" encoding="UTF-8"?>` +
+                       `<multistatus xmlns="DAV:" xmlns:B="http://ns.example.com/boxschema/">` +
+                       `  <response>` +
+                       `    <href>http://example.com/foo</href>` +
+                       `    <propstat>` +
+                       `      <prop>` +
+                       `        <B:bigbox><B:BoxType>Box type A</B:BoxType></B:bigbox>` +
+                       `        <B:author><B:Name>J.J. Johnson</B:Name></B:author>` +
+                       `      </prop>` +
+                       `      <status>HTTP/1.1 200 OK</status>` +
+                       `    </propstat>` +
+                       `    <propstat>` +
+                       `      <prop>` +
+                       `        <B:DingALing/>` +
+                       `        <B:Random/>` +
+                       `      </prop>` +
+                       `      <status>HTTP/1.1 403 Forbidden</status>` +
+                       `      <responsedescription>The user does not have access to the DingALing property.</responsedescription>` +
+                       `    </propstat>` +
+                       `  </response>` +
+                       `  <responsedescription>There has been an access violation error.</responsedescription>` +
+                       `</multistatus>`,
+               wantCode: StatusMulti,
+       }, {
+               desc: "no response written",
+               // default of http.responseWriter
+               wantCode: http.StatusOK,
+       }, {
+               desc:     "no response written (with description)",
+               respdesc: "too bad",
+               // default of http.responseWriter
+               wantCode: http.StatusOK,
+       }, {
+               desc:        "empty multistatus with header",
+               writeHeader: true,
+               wantXML:     `<multistatus xmlns="DAV:"></multistatus>`,
+               wantCode:    StatusMulti,
+       }, {
+               desc: "bad: no href",
+               responses: []response{{
+                       Propstat: []propstat{{
+                               Prop: []Property{{
+                                       XMLName: xml.Name{
+                                               Space: "http://example.com/",
+                                               Local: "foo",
+                                       },
+                               }},
+                               Status: "HTTP/1.1 200 OK",
+                       }},
+               }},
+               wantErr: errInvalidResponse,
+               // default of http.responseWriter
+               wantCode: http.StatusOK,
+       }, {
+               desc: "bad: multiple hrefs and no status",
+               responses: []response{{
+                       Href: []string{"http://example.com/foo", "http://example.com/bar"},
+               }},
+               wantErr: errInvalidResponse,
+               // default of http.responseWriter
+               wantCode: http.StatusOK,
+       }, {
+               desc: "bad: one href and no propstat",
+               responses: []response{{
+                       Href: []string{"http://example.com/foo"},
+               }},
+               wantErr: errInvalidResponse,
+               // default of http.responseWriter
+               wantCode: http.StatusOK,
+       }, {
+               desc: "bad: status with one href and propstat",
+               responses: []response{{
+                       Href: []string{"http://example.com/foo"},
+                       Propstat: []propstat{{
+                               Prop: []Property{{
+                                       XMLName: xml.Name{
+                                               Space: "http://example.com/",
+                                               Local: "foo",
+                                       },
+                               }},
+                               Status: "HTTP/1.1 200 OK",
+                       }},
+                       Status: "HTTP/1.1 200 OK",
+               }},
+               wantErr: errInvalidResponse,
+               // default of http.responseWriter
+               wantCode: http.StatusOK,
+       }, {
+               desc: "bad: multiple hrefs and propstat",
+               responses: []response{{
+                       Href: []string{
+                               "http://example.com/foo",
+                               "http://example.com/bar",
+                       },
+                       Propstat: []propstat{{
+                               Prop: []Property{{
+                                       XMLName: xml.Name{
+                                               Space: "http://example.com/",
+                                               Local: "foo",
+                                       },
+                               }},
+                               Status: "HTTP/1.1 200 OK",
+                       }},
+               }},
+               wantErr: errInvalidResponse,
+               // default of http.responseWriter
+               wantCode: http.StatusOK,
+       }}
+
+       n := xmlNormalizer{omitWhitespace: true}
+loop:
+       for _, tc := range testCases {
+               rec := httptest.NewRecorder()
+               w := multistatusWriter{w: rec, responseDescription: tc.respdesc}
+               if tc.writeHeader {
+                       if err := w.writeHeader(); err != nil {
+                               t.Errorf("%s: got writeHeader error %v, want nil", tc.desc, err)
+                               continue
+                       }
+               }
+               for _, r := range tc.responses {
+                       if err := w.write(&r); err != nil {
+                               if err != tc.wantErr {
+                                       t.Errorf("%s: got write error %v, want %v",
+                                               tc.desc, err, tc.wantErr)
+                               }
+                               continue loop
+                       }
+               }
+               if err := w.close(); err != tc.wantErr {
+                       t.Errorf("%s: got close error %v, want %v",
+                               tc.desc, err, tc.wantErr)
+                       continue
+               }
+               if rec.Code != tc.wantCode {
+                       t.Errorf("%s: got HTTP status code %d, want %d\n",
+                               tc.desc, rec.Code, tc.wantCode)
+                       continue
+               }
+               gotXML := rec.Body.String()
+               eq, err := n.equalXML(strings.NewReader(gotXML), strings.NewReader(tc.wantXML))
+               if err != nil {
+                       t.Errorf("%s: equalXML: %v", tc.desc, err)
+                       continue
+               }
+               if !eq {
+                       t.Errorf("%s: XML body\ngot  %s\nwant %s", tc.desc, gotXML, tc.wantXML)
+               }
+       }
+}
+
+func TestReadProppatch(t *testing.T) {
+       ppStr := func(pps []Proppatch) string {
+               var outer []string
+               for _, pp := range pps {
+                       var inner []string
+                       for _, p := range pp.Props {
+                               inner = append(inner, fmt.Sprintf("{XMLName: %q, Lang: %q, InnerXML: %q}",
+                                       p.XMLName, p.Lang, p.InnerXML))
+                       }
+                       outer = append(outer, fmt.Sprintf("{Remove: %t, Props: [%s]}",
+                               pp.Remove, strings.Join(inner, ", ")))
+               }
+               return "[" + strings.Join(outer, ", ") + "]"
+       }
+
+       testCases := []struct {
+               desc       string
+               input      string
+               wantPP     []Proppatch
+               wantStatus int
+       }{{
+               desc: "proppatch: section 9.2 (with simple property value)",
+               input: `` +
+                       `<?xml version="1.0" encoding="utf-8" ?>` +
+                       `<D:propertyupdate xmlns:D="DAV:"` +
+                       `                  xmlns:Z="http://ns.example.com/z/">` +
+                       `    <D:set>` +
+                       `         <D:prop><Z:Authors>somevalue</Z:Authors></D:prop>` +
+                       `    </D:set>` +
+                       `    <D:remove>` +
+                       `         <D:prop><Z:Copyright-Owner/></D:prop>` +
+                       `    </D:remove>` +
+                       `</D:propertyupdate>`,
+               wantPP: []Proppatch{{
+                       Props: []Property{{
+                               xml.Name{Space: "http://ns.example.com/z/", Local: "Authors"},
+                               "",
+                               []byte(`somevalue`),
+                       }},
+               }, {
+                       Remove: true,
+                       Props: []Property{{
+                               xml.Name{Space: "http://ns.example.com/z/", Local: "Copyright-Owner"},
+                               "",
+                               nil,
+                       }},
+               }},
+       }, {
+               desc: "proppatch: lang attribute on prop",
+               input: `` +
+                       `<?xml version="1.0" encoding="utf-8" ?>` +
+                       `<D:propertyupdate xmlns:D="DAV:">` +
+                       `    <D:set>` +
+                       `         <D:prop xml:lang="en">` +
+                       `              <foo xmlns="http://example.com/ns"/>` +
+                       `         </D:prop>` +
+                       `    </D:set>` +
+                       `</D:propertyupdate>`,
+               wantPP: []Proppatch{{
+                       Props: []Property{{
+                               xml.Name{Space: "http://example.com/ns", Local: "foo"},
+                               "en",
+                               nil,
+                       }},
+               }},
+       }, {
+               desc: "bad: remove with value",
+               input: `` +
+                       `<?xml version="1.0" encoding="utf-8" ?>` +
+                       `<D:propertyupdate xmlns:D="DAV:"` +
+                       `                  xmlns:Z="http://ns.example.com/z/">` +
+                       `    <D:remove>` +
+                       `         <D:prop>` +
+                       `              <Z:Authors>` +
+                       `              <Z:Author>Jim Whitehead</Z:Author>` +
+                       `              </Z:Authors>` +
+                       `         </D:prop>` +
+                       `    </D:remove>` +
+                       `</D:propertyupdate>`,
+               wantStatus: http.StatusBadRequest,
+       }, {
+               desc: "bad: empty propertyupdate",
+               input: `` +
+                       `<?xml version="1.0" encoding="utf-8" ?>` +
+                       `<D:propertyupdate xmlns:D="DAV:"` +
+                       `</D:propertyupdate>`,
+               wantStatus: http.StatusBadRequest,
+       }, {
+               desc: "bad: empty prop",
+               input: `` +
+                       `<?xml version="1.0" encoding="utf-8" ?>` +
+                       `<D:propertyupdate xmlns:D="DAV:"` +
+                       `                  xmlns:Z="http://ns.example.com/z/">` +
+                       `    <D:remove>` +
+                       `        <D:prop/>` +
+                       `    </D:remove>` +
+                       `</D:propertyupdate>`,
+               wantStatus: http.StatusBadRequest,
+       }}
+
+       for _, tc := range testCases {
+               pp, status, err := readProppatch(strings.NewReader(tc.input))
+               if tc.wantStatus != 0 {
+                       if err == nil {
+                               t.Errorf("%s: got nil error, want non-nil", tc.desc)
+                               continue
+                       }
+               } else if err != nil {
+                       t.Errorf("%s: %v", tc.desc, err)
+                       continue
+               }
+               if status != tc.wantStatus {
+                       t.Errorf("%s: got status %d, want %d", tc.desc, status, tc.wantStatus)
+                       continue
+               }
+               if !reflect.DeepEqual(pp, tc.wantPP) || status != tc.wantStatus {
+                       t.Errorf("%s: proppatch\ngot  %v\nwant %v", tc.desc, ppStr(pp), ppStr(tc.wantPP))
+               }
+       }
+}
+
+func TestUnmarshalXMLValue(t *testing.T) {
+       testCases := []struct {
+               desc    string
+               input   string
+               wantVal string
+       }{{
+               desc:    "simple char data",
+               input:   "<root>foo</root>",
+               wantVal: "foo",
+       }, {
+               desc:    "empty element",
+               input:   "<root><foo/></root>",
+               wantVal: "<foo/>",
+       }, {
+               desc:    "preserve namespace",
+               input:   `<root><foo xmlns="bar"/></root>`,
+               wantVal: `<foo xmlns="bar"/>`,
+       }, {
+               desc:    "preserve root element namespace",
+               input:   `<root xmlns:bar="bar"><bar:foo/></root>`,
+               wantVal: `<foo xmlns="bar"/>`,
+       }, {
+               desc:    "preserve whitespace",
+               input:   "<root>  \t </root>",
+               wantVal: "  \t ",
+       }, {
+               desc:    "preserve mixed content",
+               input:   `<root xmlns="bar">  <foo>a<bam xmlns="baz"/> </foo> </root>`,
+               wantVal: `  <foo xmlns="bar">a<bam xmlns="baz"/> </foo> `,
+       }, {
+               desc: "section 9.2",
+               input: `` +
+                       `<Z:Authors xmlns:Z="http://ns.example.com/z/">` +
+                       `  <Z:Author>Jim Whitehead</Z:Author>` +
+                       `  <Z:Author>Roy Fielding</Z:Author>` +
+                       `</Z:Authors>`,
+               wantVal: `` +
+                       `  <Author xmlns="http://ns.example.com/z/">Jim Whitehead</Author>` +
+                       `  <Author xmlns="http://ns.example.com/z/">Roy Fielding</Author>`,
+       }, {
+               desc: "section 4.3.1 (mixed content)",
+               input: `` +
+                       `<x:author ` +
+                       `    xmlns:x='http://example.com/ns' ` +
+                       `    xmlns:D="DAV:">` +
+                       `  <x:name>Jane Doe</x:name>` +
+                       `  <!-- Jane's contact info -->` +
+                       `  <x:uri type='email'` +
+                       `         added='2005-11-26'>mailto:jane.doe@example.com</x:uri>` +
+                       `  <x:uri type='web'` +
+                       `         added='2005-11-27'>http://www.example.com</x:uri>` +
+                       `  <x:notes xmlns:h='http://www.w3.org/1999/xhtml'>` +
+                       `    Jane has been working way <h:em>too</h:em> long on the` +
+                       `    long-awaited revision of <![CDATA[<RFC2518>]]>.` +
+                       `  </x:notes>` +
+                       `</x:author>`,
+               wantVal: `` +
+                       `  <name xmlns="http://example.com/ns">Jane Doe</name>` +
+                       `  ` +
+                       `  <uri type='email'` +
+                       `       xmlns="http://example.com/ns" ` +
+                       `       added='2005-11-26'>mailto:jane.doe@example.com</uri>` +
+                       `  <uri added='2005-11-27'` +
+                       `       type='web'` +
+                       `       xmlns="http://example.com/ns">http://www.example.com</uri>` +
+                       `  <notes xmlns="http://example.com/ns" ` +
+                       `         xmlns:h="http://www.w3.org/1999/xhtml">` +
+                       `    Jane has been working way <h:em>too</h:em> long on the` +
+                       `    long-awaited revision of &lt;RFC2518&gt;.` +
+                       `  </notes>`,
+       }}
+
+       var n xmlNormalizer
+       for _, tc := range testCases {
+               d := ixml.NewDecoder(strings.NewReader(tc.input))
+               var v xmlValue
+               if err := d.Decode(&v); err != nil {
+                       t.Errorf("%s: got error %v, want nil", tc.desc, err)
+                       continue
+               }
+               eq, err := n.equalXML(bytes.NewReader(v), strings.NewReader(tc.wantVal))
+               if err != nil {
+                       t.Errorf("%s: equalXML: %v", tc.desc, err)
+                       continue
+               }
+               if !eq {
+                       t.Errorf("%s:\ngot  %s\nwant %s", tc.desc, string(v), tc.wantVal)
+               }
+       }
+}
+
+// xmlNormalizer normalizes XML.
+type xmlNormalizer struct {
+       // omitWhitespace instructs to ignore whitespace between element tags.
+       omitWhitespace bool
+       // omitComments instructs to ignore XML comments.
+       omitComments bool
+}
+
+// normalize writes the normalized XML content of r to w. It applies the
+// following rules
+//
+//     * Rename namespace prefixes according to an internal heuristic.
+//     * Remove unnecessary namespace declarations.
+//     * Sort attributes in XML start elements in lexical order of their
+//       fully qualified name.
+//     * Remove XML directives and processing instructions.
+//     * Remove CDATA between XML tags that only contains whitespace, if
+//       instructed to do so.
+//     * Remove comments, if instructed to do so.
+//
+func (n *xmlNormalizer) normalize(w io.Writer, r io.Reader) error {
+       d := ixml.NewDecoder(r)
+       e := ixml.NewEncoder(w)
+       for {
+               t, err := d.Token()
+               if err != nil {
+                       if t == nil && err == io.EOF {
+                               break
+                       }
+                       return err
+               }
+               switch val := t.(type) {
+               case ixml.Directive, ixml.ProcInst:
+                       continue
+               case ixml.Comment:
+                       if n.omitComments {
+                               continue
+                       }
+               case ixml.CharData:
+                       if n.omitWhitespace && len(bytes.TrimSpace(val)) == 0 {
+                               continue
+                       }
+               case ixml.StartElement:
+                       start, _ := ixml.CopyToken(val).(ixml.StartElement)
+                       attr := start.Attr[:0]
+                       for _, a := range start.Attr {
+                               if a.Name.Space == "xmlns" || a.Name.Local == "xmlns" {
+                                       continue
+                               }
+                               attr = append(attr, a)
+                       }
+                       sort.Sort(byName(attr))
+                       start.Attr = attr
+                       t = start
+               }
+               err = e.EncodeToken(t)
+               if err != nil {
+                       return err
+               }
+       }
+       return e.Flush()
+}
+
+// equalXML tests for equality of the normalized XML contents of a and b.
+func (n *xmlNormalizer) equalXML(a, b io.Reader) (bool, error) {
+       var buf bytes.Buffer
+       if err := n.normalize(&buf, a); err != nil {
+               return false, err
+       }
+       normA := buf.String()
+       buf.Reset()
+       if err := n.normalize(&buf, b); err != nil {
+               return false, err
+       }
+       normB := buf.String()
+       return normA == normB, nil
+}
+
+type byName []ixml.Attr
+
+func (a byName) Len() int      { return len(a) }
+func (a byName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
+func (a byName) Less(i, j int) bool {
+       if a[i].Name.Space != a[j].Name.Space {
+               return a[i].Name.Space < a[j].Name.Space
+       }
+       return a[i].Name.Local < a[j].Name.Local
+}