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