1
2
3
4
5 package webdav
6
7 import (
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
22 func TestReadLockInfo(t *testing.T) {
23
24
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
138 func 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
351 func TestMultistatusWriter(t *testing.T) {
352
353
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
482 wantCode: http.StatusOK,
483 }, {
484 desc: "no response written (with description)",
485 respdesc: "too bad",
486
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
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
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
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
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
562 wantCode: http.StatusOK,
563 }}
564
565 n := xmlNormalizer{omitWhitespace: true}
566 loop:
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
607 func 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
728 func 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 <RFC2518>.` +
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
820 type xmlNormalizer struct {
821
822 omitWhitespace bool
823
824 omitComments bool
825 }
826
827
828
829
830
831
832
833
834
835
836
837
838 func (n *xmlNormalizer) normalize(w io.Writer, r io.Reader) error {
839 d := ixml.NewDecoder(r)
840 e := ixml.NewEncoder(w)
841 for {
842 t, err := d.Token()
843 if err != nil {
844 if t == nil && err == io.EOF {
845 break
846 }
847 return err
848 }
849 switch val := t.(type) {
850 case ixml.Directive, ixml.ProcInst:
851 continue
852 case ixml.Comment:
853 if n.omitComments {
854 continue
855 }
856 case ixml.CharData:
857 if n.omitWhitespace && len(bytes.TrimSpace(val)) == 0 {
858 continue
859 }
860 case ixml.StartElement:
861 start, _ := ixml.CopyToken(val).(ixml.StartElement)
862 attr := start.Attr[:0]
863 for _, a := range start.Attr {
864 if a.Name.Space == "xmlns" || a.Name.Local == "xmlns" {
865 continue
866 }
867 attr = append(attr, a)
868 }
869 sort.Sort(byName(attr))
870 start.Attr = attr
871 t = start
872 }
873 err = e.EncodeToken(t)
874 if err != nil {
875 return err
876 }
877 }
878 return e.Flush()
879 }
880
881
882 func (n *xmlNormalizer) equalXML(a, b io.Reader) (bool, error) {
883 var buf bytes.Buffer
884 if err := n.normalize(&buf, a); err != nil {
885 return false, err
886 }
887 normA := buf.String()
888 buf.Reset()
889 if err := n.normalize(&buf, b); err != nil {
890 return false, err
891 }
892 normB := buf.String()
893 return normA == normB, nil
894 }
895
896 type byName []ixml.Attr
897
898 func (a byName) Len() int { return len(a) }
899 func (a byName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
900 func (a byName) Less(i, j int) bool {
901 if a[i].Name.Space != a[j].Name.Space {
902 return a[i].Name.Space < a[j].Name.Space
903 }
904 return a[i].Name.Local < a[j].Name.Local
905 }
906
View as plain text