OSDN Git Service

new repo
[bytom/vapor.git] / vendor / golang.org / x / net / webdav / xml_test.go
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
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         // 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
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         ///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}
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 &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.
820 type 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 //
839 func (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.
883 func (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
897 type byName []ixml.Attr
898
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
904         }
905         return a[i].Name.Local < a[j].Name.Local
906 }