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.
4
5package webdav
6
7import (
8	"bytes"
9	"encoding/xml"
10	"fmt"
11	"io"
12	"net/http"
13	"net/http/httptest"
14	"reflect"
15	"sort"
16	"strings"
17	"testing"
18
19	ixml "golang.org/x/net/webdav/internal/xml"
20)
21
22func 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 {
26		desc       string
27		input      string
28		wantLI     lockInfo
29		wantStatus int
30	}{{
31		"bad: junk",
32		"xxx",
33		lockInfo{},
34		http.StatusBadRequest,
35	}, {
36		"bad: invalid owner XML",
37		"" +
38			"<D:lockinfo xmlns:D='DAV:'>\n" +
39			"  <D:lockscope><D:exclusive/></D:lockscope>\n" +
40			"  <D:locktype><D:write/></D:locktype>\n" +
41			"  <D:owner>\n" +
42			"    <D:href>   no end tag   \n" +
43			"  </D:owner>\n" +
44			"</D:lockinfo>",
45		lockInfo{},
46		http.StatusBadRequest,
47	}, {
48		"bad: invalid UTF-8",
49		"" +
50			"<D:lockinfo xmlns:D='DAV:'>\n" +
51			"  <D:lockscope><D:exclusive/></D:lockscope>\n" +
52			"  <D:locktype><D:write/></D:locktype>\n" +
53			"  <D:owner>\n" +
54			"    <D:href>   \xff   </D:href>\n" +
55			"  </D:owner>\n" +
56			"</D:lockinfo>",
57		lockInfo{},
58		http.StatusBadRequest,
59	}, {
60		"bad: unfinished XML #1",
61		"" +
62			"<D:lockinfo xmlns:D='DAV:'>\n" +
63			"  <D:lockscope><D:exclusive/></D:lockscope>\n" +
64			"  <D:locktype><D:write/></D:locktype>\n",
65		lockInfo{},
66		http.StatusBadRequest,
67	}, {
68		"bad: unfinished XML #2",
69		"" +
70			"<D:lockinfo xmlns:D='DAV:'>\n" +
71			"  <D:lockscope><D:exclusive/></D:lockscope>\n" +
72			"  <D:locktype><D:write/></D:locktype>\n" +
73			"  <D:owner>\n",
74		lockInfo{},
75		http.StatusBadRequest,
76	}, {
77		"good: empty",
78		"",
79		lockInfo{},
80		0,
81	}, {
82		"good: plain-text owner",
83		"" +
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" +
88			"</D:lockinfo>",
89		lockInfo{
90			XMLName:   ixml.Name{Space: "DAV:", Local: "lockinfo"},
91			Exclusive: new(struct{}),
92			Write:     new(struct{}),
93			Owner: owner{
94				InnerXML: "gopher",
95			},
96		},
97		0,
98	}, {
99		"section 9.10.7",
100		"" +
101			"<D:lockinfo xmlns:D='DAV:'>\n" +
102			"  <D:lockscope><D:exclusive/></D:lockscope>\n" +
103			"  <D:locktype><D:write/></D:locktype>\n" +
104			"  <D:owner>\n" +
105			"    <D:href>http://example.org/~ejw/contact.html</D:href>\n" +
106			"  </D:owner>\n" +
107			"</D:lockinfo>",
108		lockInfo{
109			XMLName:   ixml.Name{Space: "DAV:", Local: "lockinfo"},
110			Exclusive: new(struct{}),
111			Write:     new(struct{}),
112			Owner: owner{
113				InnerXML: "\n    <D:href>http://example.org/~ejw/contact.html</D:href>\n  ",
114			},
115		},
116		0,
117	}}
118
119	for _, tc := range testCases {
120		li, status, err := readLockInfo(strings.NewReader(tc.input))
121		if tc.wantStatus != 0 {
122			if err == nil {
123				t.Errorf("%s: got nil error, want non-nil", tc.desc)
124				continue
125			}
126		} else if err != nil {
127			t.Errorf("%s: %v", tc.desc, err)
128			continue
129		}
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)
133			continue
134		}
135	}
136}
137
138func TestReadPropfind(t *testing.T) {
139	testCases := []struct {
140		desc       string
141		input      string
142		wantPF     propfind
143		wantStatus int
144	}{{
145		desc: "propfind: propname",
146		input: "" +
147			"<A:propfind xmlns:A='DAV:'>\n" +
148			"  <A:propname/>\n" +
149			"</A:propfind>",
150		wantPF: propfind{
151			XMLName:  ixml.Name{Space: "DAV:", Local: "propfind"},
152			Propname: new(struct{}),
153		},
154	}, {
155		desc:  "propfind: empty body means allprop",
156		input: "",
157		wantPF: propfind{
158			Allprop: new(struct{}),
159		},
160	}, {
161		desc: "propfind: allprop",
162		input: "" +
163			"<A:propfind xmlns:A='DAV:'>\n" +
164			"   <A:allprop/>\n" +
165			"</A:propfind>",
166		wantPF: propfind{
167			XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
168			Allprop: new(struct{}),
169		},
170	}, {
171		desc: "propfind: allprop followed by include",
172		input: "" +
173			"<A:propfind xmlns:A='DAV:'>\n" +
174			"  <A:allprop/>\n" +
175			"  <A:include><A:displayname/></A:include>\n" +
176			"</A:propfind>",
177		wantPF: propfind{
178			XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
179			Allprop: new(struct{}),
180			Include: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
181		},
182	}, {
183		desc: "propfind: include followed by allprop",
184		input: "" +
185			"<A:propfind xmlns:A='DAV:'>\n" +
186			"  <A:include><A:displayname/></A:include>\n" +
187			"  <A:allprop/>\n" +
188			"</A:propfind>",
189		wantPF: propfind{
190			XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
191			Allprop: new(struct{}),
192			Include: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
193		},
194	}, {
195		desc: "propfind: propfind",
196		input: "" +
197			"<A:propfind xmlns:A='DAV:'>\n" +
198			"  <A:prop><A:displayname/></A:prop>\n" +
199			"</A:propfind>",
200		wantPF: propfind{
201			XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
202			Prop:    propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
203		},
204	}, {
205		desc: "propfind: prop with ignored comments",
206		input: "" +
207			"<A:propfind xmlns:A='DAV:'>\n" +
208			"  <A:prop>\n" +
209			"    <!-- ignore -->\n" +
210			"    <A:displayname><!-- ignore --></A:displayname>\n" +
211			"  </A:prop>\n" +
212			"</A:propfind>",
213		wantPF: propfind{
214			XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
215			Prop:    propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
216		},
217	}, {
218		desc: "propfind: propfind with ignored whitespace",
219		input: "" +
220			"<A:propfind xmlns:A='DAV:'>\n" +
221			"  <A:prop>   <A:displayname/></A:prop>\n" +
222			"</A:propfind>",
223		wantPF: propfind{
224			XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
225			Prop:    propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
226		},
227	}, {
228		desc: "propfind: propfind with ignored mixed-content",
229		input: "" +
230			"<A:propfind xmlns:A='DAV:'>\n" +
231			"  <A:prop>foo<A:displayname/>bar</A:prop>\n" +
232			"</A:propfind>",
233		wantPF: propfind{
234			XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
235			Prop:    propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
236		},
237	}, {
238		desc: "propfind: propname with ignored element (section A.4)",
239		input: "" +
240			"<A:propfind xmlns:A='DAV:'>\n" +
241			"  <A:propname/>\n" +
242			"  <E:leave-out xmlns:E='E:'>*boss*</E:leave-out>\n" +
243			"</A:propfind>",
244		wantPF: propfind{
245			XMLName:  ixml.Name{Space: "DAV:", Local: "propfind"},
246			Propname: new(struct{}),
247		},
248	}, {
249		desc:       "propfind: bad: junk",
250		input:      "xxx",
251		wantStatus: http.StatusBadRequest,
252	}, {
253		desc: "propfind: bad: propname and allprop (section A.3)",
254		input: "" +
255			"<A:propfind xmlns:A='DAV:'>\n" +
256			"  <A:propname/>" +
257			"  <A:allprop/>" +
258			"</A:propfind>",
259		wantStatus: http.StatusBadRequest,
260	}, {
261		desc: "propfind: bad: propname and prop",
262		input: "" +
263			"<A:propfind xmlns:A='DAV:'>\n" +
264			"  <A:prop><A:displayname/></A:prop>\n" +
265			"  <A:propname/>\n" +
266			"</A:propfind>",
267		wantStatus: http.StatusBadRequest,
268	}, {
269		desc: "propfind: bad: allprop and prop",
270		input: "" +
271			"<A:propfind xmlns:A='DAV:'>\n" +
272			"  <A:allprop/>\n" +
273			"  <A:prop><A:foo/><A:/prop>\n" +
274			"</A:propfind>",
275		wantStatus: http.StatusBadRequest,
276	}, {
277		desc: "propfind: bad: empty propfind with ignored element (section A.4)",
278		input: "" +
279			"<A:propfind xmlns:A='DAV:'>\n" +
280			"  <E:expired-props/>\n" +
281			"</A:propfind>",
282		wantStatus: http.StatusBadRequest,
283	}, {
284		desc: "propfind: bad: empty prop",
285		input: "" +
286			"<A:propfind xmlns:A='DAV:'>\n" +
287			"  <A:prop/>\n" +
288			"</A:propfind>",
289		wantStatus: http.StatusBadRequest,
290	}, {
291		desc: "propfind: bad: prop with just chardata",
292		input: "" +
293			"<A:propfind xmlns:A='DAV:'>\n" +
294			"  <A:prop>foo</A:prop>\n" +
295			"</A:propfind>",
296		wantStatus: http.StatusBadRequest,
297	}, {
298		desc: "bad: interrupted prop",
299		input: "" +
300			"<A:propfind xmlns:A='DAV:'>\n" +
301			"  <A:prop><A:foo></A:prop>\n",
302		wantStatus: http.StatusBadRequest,
303	}, {
304		desc: "bad: malformed end element prop",
305		input: "" +
306			"<A:propfind xmlns:A='DAV:'>\n" +
307			"  <A:prop><A:foo/></A:bar></A:prop>\n",
308		wantStatus: http.StatusBadRequest,
309	}, {
310		desc: "propfind: bad: property with chardata value",
311		input: "" +
312			"<A:propfind xmlns:A='DAV:'>\n" +
313			"  <A:prop><A:foo>bar</A:foo></A:prop>\n" +
314			"</A:propfind>",
315		wantStatus: http.StatusBadRequest,
316	}, {
317		desc: "propfind: bad: property with whitespace value",
318		input: "" +
319			"<A:propfind xmlns:A='DAV:'>\n" +
320			"  <A:prop><A:foo> </A:foo></A:prop>\n" +
321			"</A:propfind>",
322		wantStatus: http.StatusBadRequest,
323	}, {
324		desc: "propfind: bad: include without allprop",
325		input: "" +
326			"<A:propfind xmlns:A='DAV:'>\n" +
327			"  <A:include><A:foo/></A:include>\n" +
328			"</A:propfind>",
329		wantStatus: http.StatusBadRequest,
330	}}
331
332	for _, tc := range testCases {
333		pf, status, err := readPropfind(strings.NewReader(tc.input))
334		if tc.wantStatus != 0 {
335			if err == nil {
336				t.Errorf("%s: got nil error, want non-nil", tc.desc)
337				continue
338			}
339		} else if err != nil {
340			t.Errorf("%s: %v", tc.desc, err)
341			continue
342		}
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)
346			continue
347		}
348	}
349}
350
351func 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 {
355		desc        string
356		responses   []response
357		respdesc    string
358		writeHeader bool
359		wantXML     string
360		wantCode    int
361		wantErr     error
362	}{{
363		desc: "section 9.2.2 (failed dependency)",
364		responses: []response{{
365			Href: []string{"http://example.com/foo"},
366			Propstat: []propstat{{
367				Prop: []Property{{
368					XMLName: xml.Name{
369						Space: "http://ns.example.com/",
370						Local: "Authors",
371					},
372				}},
373				Status: "HTTP/1.1 424 Failed Dependency",
374			}, {
375				Prop: []Property{{
376					XMLName: xml.Name{
377						Space: "http://ns.example.com/",
378						Local: "Copyright-Owner",
379					},
380				}},
381				Status: "HTTP/1.1 409 Conflict",
382			}},
383			ResponseDescription: "Copyright Owner cannot be deleted or altered.",
384		}},
385		wantXML: `` +
386			`<?xml version="1.0" encoding="UTF-8"?>` +
387			`<multistatus xmlns="DAV:">` +
388			`  <response>` +
389			`    <href>http://example.com/foo</href>` +
390			`    <propstat>` +
391			`      <prop>` +
392			`        <Authors xmlns="http://ns.example.com/"></Authors>` +
393			`      </prop>` +
394			`      <status>HTTP/1.1 424 Failed Dependency</status>` +
395			`    </propstat>` +
396			`    <propstat xmlns="DAV:">` +
397			`      <prop>` +
398			`        <Copyright-Owner xmlns="http://ns.example.com/"></Copyright-Owner>` +
399			`      </prop>` +
400			`      <status>HTTP/1.1 409 Conflict</status>` +
401			`    </propstat>` +
402			`  <responsedescription>Copyright Owner cannot be deleted or altered.</responsedescription>` +
403			`</response>` +
404			`</multistatus>`,
405		wantCode: StatusMulti,
406	}, {
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",
411			Error: &xmlError{
412				InnerXML: []byte(`<lock-token-submitted xmlns="DAV:"/>`),
413			},
414		}},
415		wantXML: `` +
416			`<?xml version="1.0" encoding="UTF-8"?>` +
417			`<multistatus xmlns="DAV:">` +
418			`  <response>` +
419			`    <href>http://example.com/foo</href>` +
420			`    <status>HTTP/1.1 423 Locked</status>` +
421			`    <error><lock-token-submitted xmlns="DAV:"/></error>` +
422			`  </response>` +
423			`</multistatus>`,
424		wantCode: StatusMulti,
425	}, {
426		desc: "section 9.1.3",
427		responses: []response{{
428			Href: []string{"http://example.com/foo"},
429			Propstat: []propstat{{
430				Prop: []Property{{
431					XMLName: xml.Name{Space: "http://ns.example.com/boxschema/", Local: "bigbox"},
432					InnerXML: []byte(`` +
433						`<BoxType xmlns="http://ns.example.com/boxschema/">` +
434						`Box type A` +
435						`</BoxType>`),
436				}, {
437					XMLName: xml.Name{Space: "http://ns.example.com/boxschema/", Local: "author"},
438					InnerXML: []byte(`` +
439						`<Name xmlns="http://ns.example.com/boxschema/">` +
440						`J.J. Johnson` +
441						`</Name>`),
442				}},
443				Status: "HTTP/1.1 200 OK",
444			}, {
445				Prop: []Property{{
446					XMLName: xml.Name{Space: "http://ns.example.com/boxschema/", Local: "DingALing"},
447				}, {
448					XMLName: xml.Name{Space: "http://ns.example.com/boxschema/", Local: "Random"},
449				}},
450				Status:              "HTTP/1.1 403 Forbidden",
451				ResponseDescription: "The user does not have access to the DingALing property.",
452			}},
453		}},
454		respdesc: "There has been an access violation error.",
455		wantXML: `` +
456			`<?xml version="1.0" encoding="UTF-8"?>` +
457			`<multistatus xmlns="DAV:" xmlns:B="http://ns.example.com/boxschema/">` +
458			`  <response>` +
459			`    <href>http://example.com/foo</href>` +
460			`    <propstat>` +
461			`      <prop>` +
462			`        <B:bigbox><B:BoxType>Box type A</B:BoxType></B:bigbox>` +
463			`        <B:author><B:Name>J.J. Johnson</B:Name></B:author>` +
464			`      </prop>` +
465			`      <status>HTTP/1.1 200 OK</status>` +
466			`    </propstat>` +
467			`    <propstat>` +
468			`      <prop>` +
469			`        <B:DingALing/>` +
470			`        <B:Random/>` +
471			`      </prop>` +
472			`      <status>HTTP/1.1 403 Forbidden</status>` +
473			`      <responsedescription>The user does not have access to the DingALing property.</responsedescription>` +
474			`    </propstat>` +
475			`  </response>` +
476			`  <responsedescription>There has been an access violation error.</responsedescription>` +
477			`</multistatus>`,
478		wantCode: StatusMulti,
479	}, {
480		desc: "no response written",
481		// default of http.responseWriter
482		wantCode: http.StatusOK,
483	}, {
484		desc:     "no response written (with description)",
485		respdesc: "too bad",
486		// default of http.responseWriter
487		wantCode: http.StatusOK,
488	}, {
489		desc:        "empty multistatus with header",
490		writeHeader: true,
491		wantXML:     `<multistatus xmlns="DAV:"></multistatus>`,
492		wantCode:    StatusMulti,
493	}, {
494		desc: "bad: no href",
495		responses: []response{{
496			Propstat: []propstat{{
497				Prop: []Property{{
498					XMLName: xml.Name{
499						Space: "http://example.com/",
500						Local: "foo",
501					},
502				}},
503				Status: "HTTP/1.1 200 OK",
504			}},
505		}},
506		wantErr: errInvalidResponse,
507		// default of http.responseWriter
508		wantCode: http.StatusOK,
509	}, {
510		desc: "bad: multiple hrefs and no status",
511		responses: []response{{
512			Href: []string{"http://example.com/foo", "http://example.com/bar"},
513		}},
514		wantErr: errInvalidResponse,
515		// default of http.responseWriter
516		wantCode: http.StatusOK,
517	}, {
518		desc: "bad: one href and no propstat",
519		responses: []response{{
520			Href: []string{"http://example.com/foo"},
521		}},
522		wantErr: errInvalidResponse,
523		// default of http.responseWriter
524		wantCode: http.StatusOK,
525	}, {
526		desc: "bad: status with one href and propstat",
527		responses: []response{{
528			Href: []string{"http://example.com/foo"},
529			Propstat: []propstat{{
530				Prop: []Property{{
531					XMLName: xml.Name{
532						Space: "http://example.com/",
533						Local: "foo",
534					},
535				}},
536				Status: "HTTP/1.1 200 OK",
537			}},
538			Status: "HTTP/1.1 200 OK",
539		}},
540		wantErr: errInvalidResponse,
541		// default of http.responseWriter
542		wantCode: http.StatusOK,
543	}, {
544		desc: "bad: multiple hrefs and propstat",
545		responses: []response{{
546			Href: []string{
547				"http://example.com/foo",
548				"http://example.com/bar",
549			},
550			Propstat: []propstat{{
551				Prop: []Property{{
552					XMLName: xml.Name{
553						Space: "http://example.com/",
554						Local: "foo",
555					},
556				}},
557				Status: "HTTP/1.1 200 OK",
558			}},
559		}},
560		wantErr: errInvalidResponse,
561		// default of http.responseWriter
562		wantCode: http.StatusOK,
563	}}
564
565	n := xmlNormalizer{omitWhitespace: true}
566loop:
567	for _, tc := range testCases {
568		rec := httptest.NewRecorder()
569		w := multistatusWriter{w: rec, responseDescription: tc.respdesc}
570		if tc.writeHeader {
571			if err := w.writeHeader(); err != nil {
572				t.Errorf("%s: got writeHeader error %v, want nil", tc.desc, err)
573				continue
574			}
575		}
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)
581				}
582				continue loop
583			}
584		}
585		if err := w.close(); err != tc.wantErr {
586			t.Errorf("%s: got close error %v, want %v",
587				tc.desc, err, tc.wantErr)
588			continue
589		}
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)
593			continue
594		}
595		gotXML := rec.Body.String()
596		eq, err := n.equalXML(strings.NewReader(gotXML), strings.NewReader(tc.wantXML))
597		if err != nil {
598			t.Errorf("%s: equalXML: %v", tc.desc, err)
599			continue
600		}
601		if !eq {
602			t.Errorf("%s: XML body\ngot  %s\nwant %s", tc.desc, gotXML, tc.wantXML)
603		}
604	}
605}
606
607func TestReadProppatch(t *testing.T) {
608	ppStr := func(pps []Proppatch) string {
609		var outer []string
610		for _, pp := range pps {
611			var inner []string
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))
615			}
616			outer = append(outer, fmt.Sprintf("{Remove: %t, Props: [%s]}",
617				pp.Remove, strings.Join(inner, ", ")))
618		}
619		return "[" + strings.Join(outer, ", ") + "]"
620	}
621
622	testCases := []struct {
623		desc       string
624		input      string
625		wantPP     []Proppatch
626		wantStatus int
627	}{{
628		desc: "proppatch: section 9.2 (with simple property value)",
629		input: `` +
630			`<?xml version="1.0" encoding="utf-8" ?>` +
631			`<D:propertyupdate xmlns:D="DAV:"` +
632			`                  xmlns:Z="http://ns.example.com/z/">` +
633			`    <D:set>` +
634			`         <D:prop><Z:Authors>somevalue</Z:Authors></D:prop>` +
635			`    </D:set>` +
636			`    <D:remove>` +
637			`         <D:prop><Z:Copyright-Owner/></D:prop>` +
638			`    </D:remove>` +
639			`</D:propertyupdate>`,
640		wantPP: []Proppatch{{
641			Props: []Property{{
642				xml.Name{Space: "http://ns.example.com/z/", Local: "Authors"},
643				"",
644				[]byte(`somevalue`),
645			}},
646		}, {
647			Remove: true,
648			Props: []Property{{
649				xml.Name{Space: "http://ns.example.com/z/", Local: "Copyright-Owner"},
650				"",
651				nil,
652			}},
653		}},
654	}, {
655		desc: "proppatch: lang attribute on prop",
656		input: `` +
657			`<?xml version="1.0" encoding="utf-8" ?>` +
658			`<D:propertyupdate xmlns:D="DAV:">` +
659			`    <D:set>` +
660			`         <D:prop xml:lang="en">` +
661			`              <foo xmlns="http://example.com/ns"/>` +
662			`         </D:prop>` +
663			`    </D:set>` +
664			`</D:propertyupdate>`,
665		wantPP: []Proppatch{{
666			Props: []Property{{
667				xml.Name{Space: "http://example.com/ns", Local: "foo"},
668				"en",
669				nil,
670			}},
671		}},
672	}, {
673		desc: "bad: remove with value",
674		input: `` +
675			`<?xml version="1.0" encoding="utf-8" ?>` +
676			`<D:propertyupdate xmlns:D="DAV:"` +
677			`                  xmlns:Z="http://ns.example.com/z/">` +
678			`    <D:remove>` +
679			`         <D:prop>` +
680			`              <Z:Authors>` +
681			`              <Z:Author>Jim Whitehead</Z:Author>` +
682			`              </Z:Authors>` +
683			`         </D:prop>` +
684			`    </D:remove>` +
685			`</D:propertyupdate>`,
686		wantStatus: http.StatusBadRequest,
687	}, {
688		desc: "bad: empty propertyupdate",
689		input: `` +
690			`<?xml version="1.0" encoding="utf-8" ?>` +
691			`<D:propertyupdate xmlns:D="DAV:"` +
692			`</D:propertyupdate>`,
693		wantStatus: http.StatusBadRequest,
694	}, {
695		desc: "bad: empty prop",
696		input: `` +
697			`<?xml version="1.0" encoding="utf-8" ?>` +
698			`<D:propertyupdate xmlns:D="DAV:"` +
699			`                  xmlns:Z="http://ns.example.com/z/">` +
700			`    <D:remove>` +
701			`        <D:prop/>` +
702			`    </D:remove>` +
703			`</D:propertyupdate>`,
704		wantStatus: http.StatusBadRequest,
705	}}
706
707	for _, tc := range testCases {
708		pp, status, err := readProppatch(strings.NewReader(tc.input))
709		if tc.wantStatus != 0 {
710			if err == nil {
711				t.Errorf("%s: got nil error, want non-nil", tc.desc)
712				continue
713			}
714		} else if err != nil {
715			t.Errorf("%s: %v", tc.desc, err)
716			continue
717		}
718		if status != tc.wantStatus {
719			t.Errorf("%s: got status %d, want %d", tc.desc, status, tc.wantStatus)
720			continue
721		}
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))
724		}
725	}
726}
727
728func TestUnmarshalXMLValue(t *testing.T) {
729	testCases := []struct {
730		desc    string
731		input   string
732		wantVal string
733	}{{
734		desc:    "simple char data",
735		input:   "<root>foo</root>",
736		wantVal: "foo",
737	}, {
738		desc:    "empty element",
739		input:   "<root><foo/></root>",
740		wantVal: "<foo/>",
741	}, {
742		desc:    "preserve namespace",
743		input:   `<root><foo xmlns="bar"/></root>`,
744		wantVal: `<foo xmlns="bar"/>`,
745	}, {
746		desc:    "preserve root element namespace",
747		input:   `<root xmlns:bar="bar"><bar:foo/></root>`,
748		wantVal: `<foo xmlns="bar"/>`,
749	}, {
750		desc:    "preserve whitespace",
751		input:   "<root>  \t </root>",
752		wantVal: "  \t ",
753	}, {
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> `,
757	}, {
758		desc: "section 9.2",
759		input: `` +
760			`<Z:Authors xmlns:Z="http://ns.example.com/z/">` +
761			`  <Z:Author>Jim Whitehead</Z:Author>` +
762			`  <Z:Author>Roy Fielding</Z:Author>` +
763			`</Z:Authors>`,
764		wantVal: `` +
765			`  <Author xmlns="http://ns.example.com/z/">Jim Whitehead</Author>` +
766			`  <Author xmlns="http://ns.example.com/z/">Roy Fielding</Author>`,
767	}, {
768		desc: "section 4.3.1 (mixed content)",
769		input: `` +
770			`<x:author ` +
771			`    xmlns:x='http://example.com/ns' ` +
772			`    xmlns:D="DAV:">` +
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>]]>.` +
782			`  </x:notes>` +
783			`</x:author>`,
784		wantVal: `` +
785			`  <name xmlns="http://example.com/ns">Jane Doe</name>` +
786			`  ` +
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'` +
791			`       type='web'` +
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 &lt;RFC2518&gt;.` +
797			`  </notes>`,
798	}}
799
800	var n xmlNormalizer
801	for _, tc := range testCases {
802		d := ixml.NewDecoder(strings.NewReader(tc.input))
803		var v xmlValue
804		if err := d.Decode(&v); err != nil {
805			t.Errorf("%s: got error %v, want nil", tc.desc, err)
806			continue
807		}
808		eq, err := n.equalXML(bytes.NewReader(v), strings.NewReader(tc.wantVal))
809		if err != nil {
810			t.Errorf("%s: equalXML: %v", tc.desc, err)
811			continue
812		}
813		if !eq {
814			t.Errorf("%s:\ngot  %s\nwant %s", tc.desc, string(v), tc.wantVal)
815		}
816	}
817}
818
819// xmlNormalizer normalizes XML.
820type xmlNormalizer struct {
821	// omitWhitespace instructs to ignore whitespace between element tags.
822	omitWhitespace bool
823	// omitComments instructs to ignore XML comments.
824	omitComments bool
825}
826
827// normalize writes the normalized XML content of r to w. It applies the
828// following rules
829//
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.
838//
839func (n *xmlNormalizer) normalize(w io.Writer, r io.Reader) error {
840	d := ixml.NewDecoder(r)
841	e := ixml.NewEncoder(w)
842	for {
843		t, err := d.Token()
844		if err != nil {
845			if t == nil && err == io.EOF {
846				break
847			}
848			return err
849		}
850		switch val := t.(type) {
851		case ixml.Directive, ixml.ProcInst:
852			continue
853		case ixml.Comment:
854			if n.omitComments {
855				continue
856			}
857		case ixml.CharData:
858			if n.omitWhitespace && len(bytes.TrimSpace(val)) == 0 {
859				continue
860			}
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" {
866					continue
867				}
868				attr = append(attr, a)
869			}
870			sort.Sort(byName(attr))
871			start.Attr = attr
872			t = start
873		}
874		err = e.EncodeToken(t)
875		if err != nil {
876			return err
877		}
878	}
879	return e.Flush()
880}
881
882// equalXML tests for equality of the normalized XML contents of a and b.
883func (n *xmlNormalizer) equalXML(a, b io.Reader) (bool, error) {
884	var buf bytes.Buffer
885	if err := n.normalize(&buf, a); err != nil {
886		return false, err
887	}
888	normA := buf.String()
889	buf.Reset()
890	if err := n.normalize(&buf, b); err != nil {
891		return false, err
892	}
893	normB := buf.String()
894	return normA == normB, nil
895}
896
897type byName []ixml.Attr
898
899func (a byName) Len() int      { return len(a) }
900func (a byName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
901func (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
904	}
905	return a[i].Name.Local < a[j].Name.Local
906}
907