1 // Copyright 2014 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
19 ixml "golang.org/x/net/webdav/internal/xml"
22 func TestReadLockInfo(t *testing.T) {
23 // The "section x.y.z" test cases come from section x.y.z of the spec at
24 // http://www.webdav.org/specs/rfc4918.html
25 testCases := []struct {
34 http.StatusBadRequest,
36 "bad: invalid owner XML",
38 "<D:lockinfo xmlns:D='DAV:'>\n" +
39 " <D:lockscope><D:exclusive/></D:lockscope>\n" +
40 " <D:locktype><D:write/></D:locktype>\n" +
42 " <D:href> no end tag \n" +
46 http.StatusBadRequest,
50 "<D:lockinfo xmlns:D='DAV:'>\n" +
51 " <D:lockscope><D:exclusive/></D:lockscope>\n" +
52 " <D:locktype><D:write/></D:locktype>\n" +
54 " <D:href> \xff </D:href>\n" +
58 http.StatusBadRequest,
60 "bad: unfinished XML #1",
62 "<D:lockinfo xmlns:D='DAV:'>\n" +
63 " <D:lockscope><D:exclusive/></D:lockscope>\n" +
64 " <D:locktype><D:write/></D:locktype>\n",
66 http.StatusBadRequest,
68 "bad: unfinished XML #2",
70 "<D:lockinfo xmlns:D='DAV:'>\n" +
71 " <D:lockscope><D:exclusive/></D:lockscope>\n" +
72 " <D:locktype><D:write/></D:locktype>\n" +
75 http.StatusBadRequest,
82 "good: plain-text owner",
84 "<D:lockinfo xmlns:D='DAV:'>\n" +
85 " <D:lockscope><D:exclusive/></D:lockscope>\n" +
86 " <D:locktype><D:write/></D:locktype>\n" +
87 " <D:owner>gopher</D:owner>\n" +
90 XMLName: ixml.Name{Space: "DAV:", Local: "lockinfo"},
91 Exclusive: new(struct{}),
101 "<D:lockinfo xmlns:D='DAV:'>\n" +
102 " <D:lockscope><D:exclusive/></D:lockscope>\n" +
103 " <D:locktype><D:write/></D:locktype>\n" +
105 " <D:href>http://example.org/~ejw/contact.html</D:href>\n" +
109 XMLName: ixml.Name{Space: "DAV:", Local: "lockinfo"},
110 Exclusive: new(struct{}),
111 Write: new(struct{}),
113 InnerXML: "\n <D:href>http://example.org/~ejw/contact.html</D:href>\n ",
119 for _, tc := range testCases {
120 li, status, err := readLockInfo(strings.NewReader(tc.input))
121 if tc.wantStatus != 0 {
123 t.Errorf("%s: got nil error, want non-nil", tc.desc)
126 } else if err != nil {
127 t.Errorf("%s: %v", tc.desc, err)
130 if !reflect.DeepEqual(li, tc.wantLI) || status != tc.wantStatus {
131 t.Errorf("%s:\ngot lockInfo=%v, status=%v\nwant lockInfo=%v, status=%v",
132 tc.desc, li, status, tc.wantLI, tc.wantStatus)
138 func TestReadPropfind(t *testing.T) {
139 testCases := []struct {
145 desc: "propfind: propname",
147 "<A:propfind xmlns:A='DAV:'>\n" +
151 XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
152 Propname: new(struct{}),
155 desc: "propfind: empty body means allprop",
158 Allprop: new(struct{}),
161 desc: "propfind: allprop",
163 "<A:propfind xmlns:A='DAV:'>\n" +
167 XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
168 Allprop: new(struct{}),
171 desc: "propfind: allprop followed by include",
173 "<A:propfind xmlns:A='DAV:'>\n" +
175 " <A:include><A:displayname/></A:include>\n" +
178 XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
179 Allprop: new(struct{}),
180 Include: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
183 desc: "propfind: include followed by allprop",
185 "<A:propfind xmlns:A='DAV:'>\n" +
186 " <A:include><A:displayname/></A:include>\n" +
190 XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
191 Allprop: new(struct{}),
192 Include: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
195 desc: "propfind: propfind",
197 "<A:propfind xmlns:A='DAV:'>\n" +
198 " <A:prop><A:displayname/></A:prop>\n" +
201 XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
202 Prop: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
205 desc: "propfind: prop with ignored comments",
207 "<A:propfind xmlns:A='DAV:'>\n" +
209 " <!-- ignore -->\n" +
210 " <A:displayname><!-- ignore --></A:displayname>\n" +
214 XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
215 Prop: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
218 desc: "propfind: propfind with ignored whitespace",
220 "<A:propfind xmlns:A='DAV:'>\n" +
221 " <A:prop> <A:displayname/></A:prop>\n" +
224 XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
225 Prop: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
228 desc: "propfind: propfind with ignored mixed-content",
230 "<A:propfind xmlns:A='DAV:'>\n" +
231 " <A:prop>foo<A:displayname/>bar</A:prop>\n" +
234 XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
235 Prop: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
238 desc: "propfind: propname with ignored element (section A.4)",
240 "<A:propfind xmlns:A='DAV:'>\n" +
242 " <E:leave-out xmlns:E='E:'>*boss*</E:leave-out>\n" +
245 XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
246 Propname: new(struct{}),
249 desc: "propfind: bad: junk",
251 wantStatus: http.StatusBadRequest,
253 desc: "propfind: bad: propname and allprop (section A.3)",
255 "<A:propfind xmlns:A='DAV:'>\n" +
259 wantStatus: http.StatusBadRequest,
261 desc: "propfind: bad: propname and prop",
263 "<A:propfind xmlns:A='DAV:'>\n" +
264 " <A:prop><A:displayname/></A:prop>\n" +
267 wantStatus: http.StatusBadRequest,
269 desc: "propfind: bad: allprop and prop",
271 "<A:propfind xmlns:A='DAV:'>\n" +
273 " <A:prop><A:foo/><A:/prop>\n" +
275 wantStatus: http.StatusBadRequest,
277 desc: "propfind: bad: empty propfind with ignored element (section A.4)",
279 "<A:propfind xmlns:A='DAV:'>\n" +
280 " <E:expired-props/>\n" +
282 wantStatus: http.StatusBadRequest,
284 desc: "propfind: bad: empty prop",
286 "<A:propfind xmlns:A='DAV:'>\n" +
289 wantStatus: http.StatusBadRequest,
291 desc: "propfind: bad: prop with just chardata",
293 "<A:propfind xmlns:A='DAV:'>\n" +
294 " <A:prop>foo</A:prop>\n" +
296 wantStatus: http.StatusBadRequest,
298 desc: "bad: interrupted prop",
300 "<A:propfind xmlns:A='DAV:'>\n" +
301 " <A:prop><A:foo></A:prop>\n",
302 wantStatus: http.StatusBadRequest,
304 desc: "bad: malformed end element prop",
306 "<A:propfind xmlns:A='DAV:'>\n" +
307 " <A:prop><A:foo/></A:bar></A:prop>\n",
308 wantStatus: http.StatusBadRequest,
310 desc: "propfind: bad: property with chardata value",
312 "<A:propfind xmlns:A='DAV:'>\n" +
313 " <A:prop><A:foo>bar</A:foo></A:prop>\n" +
315 wantStatus: http.StatusBadRequest,
317 desc: "propfind: bad: property with whitespace value",
319 "<A:propfind xmlns:A='DAV:'>\n" +
320 " <A:prop><A:foo> </A:foo></A:prop>\n" +
322 wantStatus: http.StatusBadRequest,
324 desc: "propfind: bad: include without allprop",
326 "<A:propfind xmlns:A='DAV:'>\n" +
327 " <A:include><A:foo/></A:include>\n" +
329 wantStatus: http.StatusBadRequest,
332 for _, tc := range testCases {
333 pf, status, err := readPropfind(strings.NewReader(tc.input))
334 if tc.wantStatus != 0 {
336 t.Errorf("%s: got nil error, want non-nil", tc.desc)
339 } else if err != nil {
340 t.Errorf("%s: %v", tc.desc, err)
343 if !reflect.DeepEqual(pf, tc.wantPF) || status != tc.wantStatus {
344 t.Errorf("%s:\ngot propfind=%v, status=%v\nwant propfind=%v, status=%v",
345 tc.desc, pf, status, tc.wantPF, tc.wantStatus)
351 func TestMultistatusWriter(t *testing.T) {
352 ///The "section x.y.z" test cases come from section x.y.z of the spec at
353 // http://www.webdav.org/specs/rfc4918.html
354 testCases := []struct {
363 desc: "section 9.2.2 (failed dependency)",
364 responses: []response{{
365 Href: []string{"http://example.com/foo"},
366 Propstat: []propstat{{
369 Space: "http://ns.example.com/",
373 Status: "HTTP/1.1 424 Failed Dependency",
377 Space: "http://ns.example.com/",
378 Local: "Copyright-Owner",
381 Status: "HTTP/1.1 409 Conflict",
383 ResponseDescription: "Copyright Owner cannot be deleted or altered.",
386 `<?xml version="1.0" encoding="UTF-8"?>` +
387 `<multistatus xmlns="DAV:">` +
389 ` <href>http://example.com/foo</href>` +
392 ` <Authors xmlns="http://ns.example.com/"></Authors>` +
394 ` <status>HTTP/1.1 424 Failed Dependency</status>` +
396 ` <propstat xmlns="DAV:">` +
398 ` <Copyright-Owner xmlns="http://ns.example.com/"></Copyright-Owner>` +
400 ` <status>HTTP/1.1 409 Conflict</status>` +
402 ` <responsedescription>Copyright Owner cannot be deleted or altered.</responsedescription>` +
405 wantCode: StatusMulti,
407 desc: "section 9.6.2 (lock-token-submitted)",
408 responses: []response{{
409 Href: []string{"http://example.com/foo"},
410 Status: "HTTP/1.1 423 Locked",
412 InnerXML: []byte(`<lock-token-submitted xmlns="DAV:"/>`),
416 `<?xml version="1.0" encoding="UTF-8"?>` +
417 `<multistatus xmlns="DAV:">` +
419 ` <href>http://example.com/foo</href>` +
420 ` <status>HTTP/1.1 423 Locked</status>` +
421 ` <error><lock-token-submitted xmlns="DAV:"/></error>` +
424 wantCode: StatusMulti,
426 desc: "section 9.1.3",
427 responses: []response{{
428 Href: []string{"http://example.com/foo"},
429 Propstat: []propstat{{
431 XMLName: xml.Name{Space: "http://ns.example.com/boxschema/", Local: "bigbox"},
432 InnerXML: []byte(`` +
433 `<BoxType xmlns="http://ns.example.com/boxschema/">` +
437 XMLName: xml.Name{Space: "http://ns.example.com/boxschema/", Local: "author"},
438 InnerXML: []byte(`` +
439 `<Name xmlns="http://ns.example.com/boxschema/">` +
443 Status: "HTTP/1.1 200 OK",
446 XMLName: xml.Name{Space: "http://ns.example.com/boxschema/", Local: "DingALing"},
448 XMLName: xml.Name{Space: "http://ns.example.com/boxschema/", Local: "Random"},
450 Status: "HTTP/1.1 403 Forbidden",
451 ResponseDescription: "The user does not have access to the DingALing property.",
454 respdesc: "There has been an access violation error.",
456 `<?xml version="1.0" encoding="UTF-8"?>` +
457 `<multistatus xmlns="DAV:" xmlns:B="http://ns.example.com/boxschema/">` +
459 ` <href>http://example.com/foo</href>` +
462 ` <B:bigbox><B:BoxType>Box type A</B:BoxType></B:bigbox>` +
463 ` <B:author><B:Name>J.J. Johnson</B:Name></B:author>` +
465 ` <status>HTTP/1.1 200 OK</status>` +
472 ` <status>HTTP/1.1 403 Forbidden</status>` +
473 ` <responsedescription>The user does not have access to the DingALing property.</responsedescription>` +
476 ` <responsedescription>There has been an access violation error.</responsedescription>` +
478 wantCode: StatusMulti,
480 desc: "no response written",
481 // default of http.responseWriter
482 wantCode: http.StatusOK,
484 desc: "no response written (with description)",
486 // default of http.responseWriter
487 wantCode: http.StatusOK,
489 desc: "empty multistatus with header",
491 wantXML: `<multistatus xmlns="DAV:"></multistatus>`,
492 wantCode: StatusMulti,
494 desc: "bad: no href",
495 responses: []response{{
496 Propstat: []propstat{{
499 Space: "http://example.com/",
503 Status: "HTTP/1.1 200 OK",
506 wantErr: errInvalidResponse,
507 // default of http.responseWriter
508 wantCode: http.StatusOK,
510 desc: "bad: multiple hrefs and no status",
511 responses: []response{{
512 Href: []string{"http://example.com/foo", "http://example.com/bar"},
514 wantErr: errInvalidResponse,
515 // default of http.responseWriter
516 wantCode: http.StatusOK,
518 desc: "bad: one href and no propstat",
519 responses: []response{{
520 Href: []string{"http://example.com/foo"},
522 wantErr: errInvalidResponse,
523 // default of http.responseWriter
524 wantCode: http.StatusOK,
526 desc: "bad: status with one href and propstat",
527 responses: []response{{
528 Href: []string{"http://example.com/foo"},
529 Propstat: []propstat{{
532 Space: "http://example.com/",
536 Status: "HTTP/1.1 200 OK",
538 Status: "HTTP/1.1 200 OK",
540 wantErr: errInvalidResponse,
541 // default of http.responseWriter
542 wantCode: http.StatusOK,
544 desc: "bad: multiple hrefs and propstat",
545 responses: []response{{
547 "http://example.com/foo",
548 "http://example.com/bar",
550 Propstat: []propstat{{
553 Space: "http://example.com/",
557 Status: "HTTP/1.1 200 OK",
560 wantErr: errInvalidResponse,
561 // default of http.responseWriter
562 wantCode: http.StatusOK,
565 n := xmlNormalizer{omitWhitespace: true}
567 for _, tc := range testCases {
568 rec := httptest.NewRecorder()
569 w := multistatusWriter{w: rec, responseDescription: tc.respdesc}
571 if err := w.writeHeader(); err != nil {
572 t.Errorf("%s: got writeHeader error %v, want nil", tc.desc, err)
576 for _, r := range tc.responses {
577 if err := w.write(&r); err != nil {
578 if err != tc.wantErr {
579 t.Errorf("%s: got write error %v, want %v",
580 tc.desc, err, tc.wantErr)
585 if err := w.close(); err != tc.wantErr {
586 t.Errorf("%s: got close error %v, want %v",
587 tc.desc, err, tc.wantErr)
590 if rec.Code != tc.wantCode {
591 t.Errorf("%s: got HTTP status code %d, want %d\n",
592 tc.desc, rec.Code, tc.wantCode)
595 gotXML := rec.Body.String()
596 eq, err := n.equalXML(strings.NewReader(gotXML), strings.NewReader(tc.wantXML))
598 t.Errorf("%s: equalXML: %v", tc.desc, err)
602 t.Errorf("%s: XML body\ngot %s\nwant %s", tc.desc, gotXML, tc.wantXML)
607 func TestReadProppatch(t *testing.T) {
608 ppStr := func(pps []Proppatch) string {
610 for _, pp := range pps {
612 for _, p := range pp.Props {
613 inner = append(inner, fmt.Sprintf("{XMLName: %q, Lang: %q, InnerXML: %q}",
614 p.XMLName, p.Lang, p.InnerXML))
616 outer = append(outer, fmt.Sprintf("{Remove: %t, Props: [%s]}",
617 pp.Remove, strings.Join(inner, ", ")))
619 return "[" + strings.Join(outer, ", ") + "]"
622 testCases := []struct {
628 desc: "proppatch: section 9.2 (with simple property value)",
630 `<?xml version="1.0" encoding="utf-8" ?>` +
631 `<D:propertyupdate xmlns:D="DAV:"` +
632 ` xmlns:Z="http://ns.example.com/z/">` +
634 ` <D:prop><Z:Authors>somevalue</Z:Authors></D:prop>` +
637 ` <D:prop><Z:Copyright-Owner/></D:prop>` +
639 `</D:propertyupdate>`,
640 wantPP: []Proppatch{{
642 xml.Name{Space: "http://ns.example.com/z/", Local: "Authors"},
649 xml.Name{Space: "http://ns.example.com/z/", Local: "Copyright-Owner"},
655 desc: "proppatch: lang attribute on prop",
657 `<?xml version="1.0" encoding="utf-8" ?>` +
658 `<D:propertyupdate xmlns:D="DAV:">` +
660 ` <D:prop xml:lang="en">` +
661 ` <foo xmlns="http://example.com/ns"/>` +
664 `</D:propertyupdate>`,
665 wantPP: []Proppatch{{
667 xml.Name{Space: "http://example.com/ns", Local: "foo"},
673 desc: "bad: remove with value",
675 `<?xml version="1.0" encoding="utf-8" ?>` +
676 `<D:propertyupdate xmlns:D="DAV:"` +
677 ` xmlns:Z="http://ns.example.com/z/">` +
681 ` <Z:Author>Jim Whitehead</Z:Author>` +
685 `</D:propertyupdate>`,
686 wantStatus: http.StatusBadRequest,
688 desc: "bad: empty propertyupdate",
690 `<?xml version="1.0" encoding="utf-8" ?>` +
691 `<D:propertyupdate xmlns:D="DAV:"` +
692 `</D:propertyupdate>`,
693 wantStatus: http.StatusBadRequest,
695 desc: "bad: empty prop",
697 `<?xml version="1.0" encoding="utf-8" ?>` +
698 `<D:propertyupdate xmlns:D="DAV:"` +
699 ` xmlns:Z="http://ns.example.com/z/">` +
703 `</D:propertyupdate>`,
704 wantStatus: http.StatusBadRequest,
707 for _, tc := range testCases {
708 pp, status, err := readProppatch(strings.NewReader(tc.input))
709 if tc.wantStatus != 0 {
711 t.Errorf("%s: got nil error, want non-nil", tc.desc)
714 } else if err != nil {
715 t.Errorf("%s: %v", tc.desc, err)
718 if status != tc.wantStatus {
719 t.Errorf("%s: got status %d, want %d", tc.desc, status, tc.wantStatus)
722 if !reflect.DeepEqual(pp, tc.wantPP) || status != tc.wantStatus {
723 t.Errorf("%s: proppatch\ngot %v\nwant %v", tc.desc, ppStr(pp), ppStr(tc.wantPP))
728 func TestUnmarshalXMLValue(t *testing.T) {
729 testCases := []struct {
734 desc: "simple char data",
735 input: "<root>foo</root>",
738 desc: "empty element",
739 input: "<root><foo/></root>",
742 desc: "preserve namespace",
743 input: `<root><foo xmlns="bar"/></root>`,
744 wantVal: `<foo xmlns="bar"/>`,
746 desc: "preserve root element namespace",
747 input: `<root xmlns:bar="bar"><bar:foo/></root>`,
748 wantVal: `<foo xmlns="bar"/>`,
750 desc: "preserve whitespace",
751 input: "<root> \t </root>",
754 desc: "preserve mixed content",
755 input: `<root xmlns="bar"> <foo>a<bam xmlns="baz"/> </foo> </root>`,
756 wantVal: ` <foo xmlns="bar">a<bam xmlns="baz"/> </foo> `,
760 `<Z:Authors xmlns:Z="http://ns.example.com/z/">` +
761 ` <Z:Author>Jim Whitehead</Z:Author>` +
762 ` <Z:Author>Roy Fielding</Z:Author>` +
765 ` <Author xmlns="http://ns.example.com/z/">Jim Whitehead</Author>` +
766 ` <Author xmlns="http://ns.example.com/z/">Roy Fielding</Author>`,
768 desc: "section 4.3.1 (mixed content)",
771 ` xmlns:x='http://example.com/ns' ` +
773 ` <x:name>Jane Doe</x:name>` +
774 ` <!-- Jane's contact info -->` +
775 ` <x:uri type='email'` +
776 ` added='2005-11-26'>mailto:jane.doe@example.com</x:uri>` +
777 ` <x:uri type='web'` +
778 ` added='2005-11-27'>http://www.example.com</x:uri>` +
779 ` <x:notes xmlns:h='http://www.w3.org/1999/xhtml'>` +
780 ` Jane has been working way <h:em>too</h:em> long on the` +
781 ` long-awaited revision of <![CDATA[<RFC2518>]]>.` +
785 ` <name xmlns="http://example.com/ns">Jane Doe</name>` +
787 ` <uri type='email'` +
788 ` xmlns="http://example.com/ns" ` +
789 ` added='2005-11-26'>mailto:jane.doe@example.com</uri>` +
790 ` <uri added='2005-11-27'` +
792 ` xmlns="http://example.com/ns">http://www.example.com</uri>` +
793 ` <notes xmlns="http://example.com/ns" ` +
794 ` xmlns:h="http://www.w3.org/1999/xhtml">` +
795 ` Jane has been working way <h:em>too</h:em> long on the` +
796 ` long-awaited revision of <RFC2518>.` +
801 for _, tc := range testCases {
802 d := ixml.NewDecoder(strings.NewReader(tc.input))
804 if err := d.Decode(&v); err != nil {
805 t.Errorf("%s: got error %v, want nil", tc.desc, err)
808 eq, err := n.equalXML(bytes.NewReader(v), strings.NewReader(tc.wantVal))
810 t.Errorf("%s: equalXML: %v", tc.desc, err)
814 t.Errorf("%s:\ngot %s\nwant %s", tc.desc, string(v), tc.wantVal)
819 // xmlNormalizer normalizes XML.
820 type xmlNormalizer struct {
821 // omitWhitespace instructs to ignore whitespace between element tags.
823 // omitComments instructs to ignore XML comments.
827 // normalize writes the normalized XML content of r to w. It applies the
830 // * Rename namespace prefixes according to an internal heuristic.
831 // * Remove unnecessary namespace declarations.
832 // * Sort attributes in XML start elements in lexical order of their
833 // fully qualified name.
834 // * Remove XML directives and processing instructions.
835 // * Remove CDATA between XML tags that only contains whitespace, if
836 // instructed to do so.
837 // * Remove comments, if instructed to do so.
839 func (n *xmlNormalizer) normalize(w io.Writer, r io.Reader) error {
840 d := ixml.NewDecoder(r)
841 e := ixml.NewEncoder(w)
845 if t == nil && err == io.EOF {
850 switch val := t.(type) {
851 case ixml.Directive, ixml.ProcInst:
858 if n.omitWhitespace && len(bytes.TrimSpace(val)) == 0 {
861 case ixml.StartElement:
862 start, _ := ixml.CopyToken(val).(ixml.StartElement)
863 attr := start.Attr[:0]
864 for _, a := range start.Attr {
865 if a.Name.Space == "xmlns" || a.Name.Local == "xmlns" {
868 attr = append(attr, a)
870 sort.Sort(byName(attr))
874 err = e.EncodeToken(t)
882 // equalXML tests for equality of the normalized XML contents of a and b.
883 func (n *xmlNormalizer) equalXML(a, b io.Reader) (bool, error) {
885 if err := n.normalize(&buf, a); err != nil {
888 normA := buf.String()
890 if err := n.normalize(&buf, b); err != nil {
893 normB := buf.String()
894 return normA == normB, nil
897 type byName []ixml.Attr
899 func (a byName) Len() int { return len(a) }
900 func (a byName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
901 func (a byName) Less(i, j int) bool {
902 if a[i].Name.Space != a[j].Name.Space {
903 return a[i].Name.Space < a[j].Name.Space
905 return a[i].Name.Local < a[j].Name.Local