golang.org/x/net@v0.25.1-0.20240516223405-c87a5b62e243/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  	"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  func (n *xmlNormalizer) normalize(w io.Writer, r io.Reader) error {
   839  	d := ixml.NewDecoder(r)
   840  	e := ixml.NewEncoder(w)
   841  	for {
   842  		t, err := d.Token()
   843  		if err != nil {
   844  			if t == nil && err == io.EOF {
   845  				break
   846  			}
   847  			return err
   848  		}
   849  		switch val := t.(type) {
   850  		case ixml.Directive, ixml.ProcInst:
   851  			continue
   852  		case ixml.Comment:
   853  			if n.omitComments {
   854  				continue
   855  			}
   856  		case ixml.CharData:
   857  			if n.omitWhitespace && len(bytes.TrimSpace(val)) == 0 {
   858  				continue
   859  			}
   860  		case ixml.StartElement:
   861  			start, _ := ixml.CopyToken(val).(ixml.StartElement)
   862  			attr := start.Attr[:0]
   863  			for _, a := range start.Attr {
   864  				if a.Name.Space == "xmlns" || a.Name.Local == "xmlns" {
   865  					continue
   866  				}
   867  				attr = append(attr, a)
   868  			}
   869  			sort.Sort(byName(attr))
   870  			start.Attr = attr
   871  			t = start
   872  		}
   873  		err = e.EncodeToken(t)
   874  		if err != nil {
   875  			return err
   876  		}
   877  	}
   878  	return e.Flush()
   879  }
   880  
   881  // equalXML tests for equality of the normalized XML contents of a and b.
   882  func (n *xmlNormalizer) equalXML(a, b io.Reader) (bool, error) {
   883  	var buf bytes.Buffer
   884  	if err := n.normalize(&buf, a); err != nil {
   885  		return false, err
   886  	}
   887  	normA := buf.String()
   888  	buf.Reset()
   889  	if err := n.normalize(&buf, b); err != nil {
   890  		return false, err
   891  	}
   892  	normB := buf.String()
   893  	return normA == normB, nil
   894  }
   895  
   896  type byName []ixml.Attr
   897  
   898  func (a byName) Len() int      { return len(a) }
   899  func (a byName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
   900  func (a byName) Less(i, j int) bool {
   901  	if a[i].Name.Space != a[j].Name.Space {
   902  		return a[i].Name.Space < a[j].Name.Space
   903  	}
   904  	return a[i].Name.Local < a[j].Name.Local
   905  }