github.com/hxx258456/ccgo@v0.0.5-0.20230213014102-48b35f46f66f/net/webdav/xml_test.go (about)

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