|
|
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package webdav
import ( "bytes" "encoding/xml" "fmt" "io" "net/http" "net/http/httptest" "reflect" "sort" "strings" "testing"
ixml "golang.org/x/net/webdav/internal/xml" )
func TestReadLockInfo(t *testing.T) { // The "section x.y.z" test cases come from section x.y.z of the spec at
// http://www.webdav.org/specs/rfc4918.html
testCases := []struct { desc string input string wantLI lockInfo wantStatus int }{{ "bad: junk", "xxx", lockInfo{}, http.StatusBadRequest, }, { "bad: invalid owner XML", "" + "<D:lockinfo xmlns:D='DAV:'>\n" + " <D:lockscope><D:exclusive/></D:lockscope>\n" + " <D:locktype><D:write/></D:locktype>\n" + " <D:owner>\n" + " <D:href> no end tag \n" + " </D:owner>\n" + "</D:lockinfo>", lockInfo{}, http.StatusBadRequest, }, { "bad: invalid UTF-8", "" + "<D:lockinfo xmlns:D='DAV:'>\n" + " <D:lockscope><D:exclusive/></D:lockscope>\n" + " <D:locktype><D:write/></D:locktype>\n" + " <D:owner>\n" + " <D:href> \xff </D:href>\n" + " </D:owner>\n" + "</D:lockinfo>", lockInfo{}, http.StatusBadRequest, }, { "bad: unfinished XML #1", "" + "<D:lockinfo xmlns:D='DAV:'>\n" + " <D:lockscope><D:exclusive/></D:lockscope>\n" + " <D:locktype><D:write/></D:locktype>\n", lockInfo{}, http.StatusBadRequest, }, { "bad: unfinished XML #2", "" + "<D:lockinfo xmlns:D='DAV:'>\n" + " <D:lockscope><D:exclusive/></D:lockscope>\n" + " <D:locktype><D:write/></D:locktype>\n" + " <D:owner>\n", lockInfo{}, http.StatusBadRequest, }, { "good: empty", "", lockInfo{}, 0, }, { "good: plain-text owner", "" + "<D:lockinfo xmlns:D='DAV:'>\n" + " <D:lockscope><D:exclusive/></D:lockscope>\n" + " <D:locktype><D:write/></D:locktype>\n" + " <D:owner>gopher</D:owner>\n" + "</D:lockinfo>", lockInfo{ XMLName: ixml.Name{Space: "DAV:", Local: "lockinfo"}, Exclusive: new(struct{}), Write: new(struct{}), Owner: owner{ InnerXML: "gopher", }, }, 0, }, { "section 9.10.7", "" + "<D:lockinfo xmlns:D='DAV:'>\n" + " <D:lockscope><D:exclusive/></D:lockscope>\n" + " <D:locktype><D:write/></D:locktype>\n" + " <D:owner>\n" + " <D:href>http://example.org/~ejw/contact.html</D:href>\n" + " </D:owner>\n" + "</D:lockinfo>", lockInfo{ XMLName: ixml.Name{Space: "DAV:", Local: "lockinfo"}, Exclusive: new(struct{}), Write: new(struct{}), Owner: owner{ InnerXML: "\n <D:href>http://example.org/~ejw/contact.html</D:href>\n ", }, }, 0, }}
for _, tc := range testCases { li, status, err := readLockInfo(strings.NewReader(tc.input)) if tc.wantStatus != 0 { if err == nil { t.Errorf("%s: got nil error, want non-nil", tc.desc) continue } } else if err != nil { t.Errorf("%s: %v", tc.desc, err) continue } if !reflect.DeepEqual(li, tc.wantLI) || status != tc.wantStatus { t.Errorf("%s:\ngot lockInfo=%v, status=%v\nwant lockInfo=%v, status=%v", tc.desc, li, status, tc.wantLI, tc.wantStatus) continue } } }
func TestReadPropfind(t *testing.T) { testCases := []struct { desc string input string wantPF propfind wantStatus int }{{ desc: "propfind: propname", input: "" + "<A:propfind xmlns:A='DAV:'>\n" + " <A:propname/>\n" + "</A:propfind>", wantPF: propfind{ XMLName: ixml.Name{Space: "DAV:", Local: "propfind"}, Propname: new(struct{}), }, }, { desc: "propfind: empty body means allprop", input: "", wantPF: propfind{ Allprop: new(struct{}), }, }, { desc: "propfind: allprop", input: "" + "<A:propfind xmlns:A='DAV:'>\n" + " <A:allprop/>\n" + "</A:propfind>", wantPF: propfind{ XMLName: ixml.Name{Space: "DAV:", Local: "propfind"}, Allprop: new(struct{}), }, }, { desc: "propfind: allprop followed by include", input: "" + "<A:propfind xmlns:A='DAV:'>\n" + " <A:allprop/>\n" + " <A:include><A:displayname/></A:include>\n" + "</A:propfind>", wantPF: propfind{ XMLName: ixml.Name{Space: "DAV:", Local: "propfind"}, Allprop: new(struct{}), Include: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}}, }, }, { desc: "propfind: include followed by allprop", input: "" + "<A:propfind xmlns:A='DAV:'>\n" + " <A:include><A:displayname/></A:include>\n" + " <A:allprop/>\n" + "</A:propfind>", wantPF: propfind{ XMLName: ixml.Name{Space: "DAV:", Local: "propfind"}, Allprop: new(struct{}), Include: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}}, }, }, { desc: "propfind: propfind", input: "" + "<A:propfind xmlns:A='DAV:'>\n" + " <A:prop><A:displayname/></A:prop>\n" + "</A:propfind>", wantPF: propfind{ XMLName: ixml.Name{Space: "DAV:", Local: "propfind"}, Prop: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}}, }, }, { desc: "propfind: prop with ignored comments", input: "" + "<A:propfind xmlns:A='DAV:'>\n" + " <A:prop>\n" + " <!-- ignore -->\n" + " <A:displayname><!-- ignore --></A:displayname>\n" + " </A:prop>\n" + "</A:propfind>", wantPF: propfind{ XMLName: ixml.Name{Space: "DAV:", Local: "propfind"}, Prop: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}}, }, }, { desc: "propfind: propfind with ignored whitespace", input: "" + "<A:propfind xmlns:A='DAV:'>\n" + " <A:prop> <A:displayname/></A:prop>\n" + "</A:propfind>", wantPF: propfind{ XMLName: ixml.Name{Space: "DAV:", Local: "propfind"}, Prop: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}}, }, }, { desc: "propfind: propfind with ignored mixed-content", input: "" + "<A:propfind xmlns:A='DAV:'>\n" + " <A:prop>foo<A:displayname/>bar</A:prop>\n" + "</A:propfind>", wantPF: propfind{ XMLName: ixml.Name{Space: "DAV:", Local: "propfind"}, Prop: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}}, }, }, { desc: "propfind: propname with ignored element (section A.4)", input: "" + "<A:propfind xmlns:A='DAV:'>\n" + " <A:propname/>\n" + " <E:leave-out xmlns:E='E:'>*boss*</E:leave-out>\n" + "</A:propfind>", wantPF: propfind{ XMLName: ixml.Name{Space: "DAV:", Local: "propfind"}, Propname: new(struct{}), }, }, { desc: "propfind: bad: junk", input: "xxx", wantStatus: http.StatusBadRequest, }, { desc: "propfind: bad: propname and allprop (section A.3)", input: "" + "<A:propfind xmlns:A='DAV:'>\n" + " <A:propname/>" + " <A:allprop/>" + "</A:propfind>", wantStatus: http.StatusBadRequest, }, { desc: "propfind: bad: propname and prop", input: "" + "<A:propfind xmlns:A='DAV:'>\n" + " <A:prop><A:displayname/></A:prop>\n" + " <A:propname/>\n" + "</A:propfind>", wantStatus: http.StatusBadRequest, }, { desc: "propfind: bad: allprop and prop", input: "" + "<A:propfind xmlns:A='DAV:'>\n" + " <A:allprop/>\n" + " <A:prop><A:foo/><A:/prop>\n" + "</A:propfind>", wantStatus: http.StatusBadRequest, }, { desc: "propfind: bad: empty propfind with ignored element (section A.4)", input: "" + "<A:propfind xmlns:A='DAV:'>\n" + " <E:expired-props/>\n" + "</A:propfind>", wantStatus: http.StatusBadRequest, }, { desc: "propfind: bad: empty prop", input: "" + "<A:propfind xmlns:A='DAV:'>\n" + " <A:prop/>\n" + "</A:propfind>", wantStatus: http.StatusBadRequest, }, { desc: "propfind: bad: prop with just chardata", input: "" + "<A:propfind xmlns:A='DAV:'>\n" + " <A:prop>foo</A:prop>\n" + "</A:propfind>", wantStatus: http.StatusBadRequest, }, { desc: "bad: interrupted prop", input: "" + "<A:propfind xmlns:A='DAV:'>\n" + " <A:prop><A:foo></A:prop>\n", wantStatus: http.StatusBadRequest, }, { desc: "bad: malformed end element prop", input: "" + "<A:propfind xmlns:A='DAV:'>\n" + " <A:prop><A:foo/></A:bar></A:prop>\n", wantStatus: http.StatusBadRequest, }, { desc: "propfind: bad: property with chardata value", input: "" + "<A:propfind xmlns:A='DAV:'>\n" + " <A:prop><A:foo>bar</A:foo></A:prop>\n" + "</A:propfind>", wantStatus: http.StatusBadRequest, }, { desc: "propfind: bad: property with whitespace value", input: "" + "<A:propfind xmlns:A='DAV:'>\n" + " <A:prop><A:foo> </A:foo></A:prop>\n" + "</A:propfind>", wantStatus: http.StatusBadRequest, }, { desc: "propfind: bad: include without allprop", input: "" + "<A:propfind xmlns:A='DAV:'>\n" + " <A:include><A:foo/></A:include>\n" + "</A:propfind>", wantStatus: http.StatusBadRequest, }}
for _, tc := range testCases { pf, status, err := readPropfind(strings.NewReader(tc.input)) if tc.wantStatus != 0 { if err == nil { t.Errorf("%s: got nil error, want non-nil", tc.desc) continue } } else if err != nil { t.Errorf("%s: %v", tc.desc, err) continue } if !reflect.DeepEqual(pf, tc.wantPF) || status != tc.wantStatus { t.Errorf("%s:\ngot propfind=%v, status=%v\nwant propfind=%v, status=%v", tc.desc, pf, status, tc.wantPF, tc.wantStatus) continue } } }
func TestMultistatusWriter(t *testing.T) { ///The "section x.y.z" test cases come from section x.y.z of the spec at
// http://www.webdav.org/specs/rfc4918.html
testCases := []struct { desc string responses []response respdesc string writeHeader bool wantXML string wantCode int wantErr error }{{ desc: "section 9.2.2 (failed dependency)", responses: []response{{ Href: []string{"http://example.com/foo"}, Propstat: []propstat{{ Prop: []Property{{ XMLName: xml.Name{ Space: "http://ns.example.com/", Local: "Authors", }, }}, Status: "HTTP/1.1 424 Failed Dependency", }, { Prop: []Property{{ XMLName: xml.Name{ Space: "http://ns.example.com/", Local: "Copyright-Owner", }, }}, Status: "HTTP/1.1 409 Conflict", }}, ResponseDescription: "Copyright Owner cannot be deleted or altered.", }}, wantXML: `` + `<?xml version="1.0" encoding="UTF-8"?>` + `<multistatus xmlns="DAV:">` + ` <response>` + ` <href>http://example.com/foo</href>` + ` <propstat>` + ` <prop>` + ` <Authors xmlns="http://ns.example.com/"></Authors>` + ` </prop>` + ` <status>HTTP/1.1 424 Failed Dependency</status>` + ` </propstat>` + ` <propstat xmlns="DAV:">` + ` <prop>` + ` <Copyright-Owner xmlns="http://ns.example.com/"></Copyright-Owner>` + ` </prop>` + ` <status>HTTP/1.1 409 Conflict</status>` + ` </propstat>` + ` <responsedescription>Copyright Owner cannot be deleted or altered.</responsedescription>` + `</response>` + `</multistatus>`, wantCode: StatusMulti, }, { desc: "section 9.6.2 (lock-token-submitted)", responses: []response{{ Href: []string{"http://example.com/foo"}, Status: "HTTP/1.1 423 Locked", Error: &xmlError{ InnerXML: []byte(`<lock-token-submitted xmlns="DAV:"/>`), }, }}, wantXML: `` + `<?xml version="1.0" encoding="UTF-8"?>` + `<multistatus xmlns="DAV:">` + ` <response>` + ` <href>http://example.com/foo</href>` + ` <status>HTTP/1.1 423 Locked</status>` + ` <error><lock-token-submitted xmlns="DAV:"/></error>` + ` </response>` + `</multistatus>`, wantCode: StatusMulti, }, { desc: "section 9.1.3", responses: []response{{ Href: []string{"http://example.com/foo"}, Propstat: []propstat{{ Prop: []Property{{ XMLName: xml.Name{Space: "http://ns.example.com/boxschema/", Local: "bigbox"}, InnerXML: []byte(`` + `<BoxType xmlns="http://ns.example.com/boxschema/">` + `Box type A` + `</BoxType>`), }, { XMLName: xml.Name{Space: "http://ns.example.com/boxschema/", Local: "author"}, InnerXML: []byte(`` + `<Name xmlns="http://ns.example.com/boxschema/">` + `J.J. Johnson` + `</Name>`), }}, Status: "HTTP/1.1 200 OK", }, { Prop: []Property{{ XMLName: xml.Name{Space: "http://ns.example.com/boxschema/", Local: "DingALing"}, }, { XMLName: xml.Name{Space: "http://ns.example.com/boxschema/", Local: "Random"}, }}, Status: "HTTP/1.1 403 Forbidden", ResponseDescription: "The user does not have access to the DingALing property.", }}, }}, respdesc: "There has been an access violation error.", wantXML: `` + `<?xml version="1.0" encoding="UTF-8"?>` + `<multistatus xmlns="DAV:" xmlns:B="http://ns.example.com/boxschema/">` + ` <response>` + ` <href>http://example.com/foo</href>` + ` <propstat>` + ` <prop>` + ` <B:bigbox><B:BoxType>Box type A</B:BoxType></B:bigbox>` + ` <B:author><B:Name>J.J. Johnson</B:Name></B:author>` + ` </prop>` + ` <status>HTTP/1.1 200 OK</status>` + ` </propstat>` + ` <propstat>` + ` <prop>` + ` <B:DingALing/>` + ` <B:Random/>` + ` </prop>` + ` <status>HTTP/1.1 403 Forbidden</status>` + ` <responsedescription>The user does not have access to the DingALing property.</responsedescription>` + ` </propstat>` + ` </response>` + ` <responsedescription>There has been an access violation error.</responsedescription>` + `</multistatus>`, wantCode: StatusMulti, }, { desc: "no response written", // default of http.responseWriter
wantCode: http.StatusOK, }, { desc: "no response written (with description)", respdesc: "too bad", // default of http.responseWriter
wantCode: http.StatusOK, }, { desc: "empty multistatus with header", writeHeader: true, wantXML: `<multistatus xmlns="DAV:"></multistatus>`, wantCode: StatusMulti, }, { desc: "bad: no href", responses: []response{{ Propstat: []propstat{{ Prop: []Property{{ XMLName: xml.Name{ Space: "http://example.com/", Local: "foo", }, }}, Status: "HTTP/1.1 200 OK", }}, }}, wantErr: errInvalidResponse, // default of http.responseWriter
wantCode: http.StatusOK, }, { desc: "bad: multiple hrefs and no status", responses: []response{{ Href: []string{"http://example.com/foo", "http://example.com/bar"}, }}, wantErr: errInvalidResponse, // default of http.responseWriter
wantCode: http.StatusOK, }, { desc: "bad: one href and no propstat", responses: []response{{ Href: []string{"http://example.com/foo"}, }}, wantErr: errInvalidResponse, // default of http.responseWriter
wantCode: http.StatusOK, }, { desc: "bad: status with one href and propstat", responses: []response{{ Href: []string{"http://example.com/foo"}, Propstat: []propstat{{ Prop: []Property{{ XMLName: xml.Name{ Space: "http://example.com/", Local: "foo", }, }}, Status: "HTTP/1.1 200 OK", }}, Status: "HTTP/1.1 200 OK", }}, wantErr: errInvalidResponse, // default of http.responseWriter
wantCode: http.StatusOK, }, { desc: "bad: multiple hrefs and propstat", responses: []response{{ Href: []string{ "http://example.com/foo", "http://example.com/bar", }, Propstat: []propstat{{ Prop: []Property{{ XMLName: xml.Name{ Space: "http://example.com/", Local: "foo", }, }}, Status: "HTTP/1.1 200 OK", }}, }}, wantErr: errInvalidResponse, // default of http.responseWriter
wantCode: http.StatusOK, }}
n := xmlNormalizer{omitWhitespace: true} loop: for _, tc := range testCases { rec := httptest.NewRecorder() w := multistatusWriter{w: rec, responseDescription: tc.respdesc} if tc.writeHeader { if err := w.writeHeader(); err != nil { t.Errorf("%s: got writeHeader error %v, want nil", tc.desc, err) continue } } for _, r := range tc.responses { if err := w.write(&r); err != nil { if err != tc.wantErr { t.Errorf("%s: got write error %v, want %v", tc.desc, err, tc.wantErr) } continue loop } } if err := w.close(); err != tc.wantErr { t.Errorf("%s: got close error %v, want %v", tc.desc, err, tc.wantErr) continue } if rec.Code != tc.wantCode { t.Errorf("%s: got HTTP status code %d, want %d\n", tc.desc, rec.Code, tc.wantCode) continue } gotXML := rec.Body.String() eq, err := n.equalXML(strings.NewReader(gotXML), strings.NewReader(tc.wantXML)) if err != nil { t.Errorf("%s: equalXML: %v", tc.desc, err) continue } if !eq { t.Errorf("%s: XML body\ngot %s\nwant %s", tc.desc, gotXML, tc.wantXML) } } }
func TestReadProppatch(t *testing.T) { ppStr := func(pps []Proppatch) string { var outer []string for _, pp := range pps { var inner []string for _, p := range pp.Props { inner = append(inner, fmt.Sprintf("{XMLName: %q, Lang: %q, InnerXML: %q}", p.XMLName, p.Lang, p.InnerXML)) } outer = append(outer, fmt.Sprintf("{Remove: %t, Props: [%s]}", pp.Remove, strings.Join(inner, ", "))) } return "[" + strings.Join(outer, ", ") + "]" }
testCases := []struct { desc string input string wantPP []Proppatch wantStatus int }{{ desc: "proppatch: section 9.2 (with simple property value)", input: `` + `<?xml version="1.0" encoding="utf-8" ?>` + `<D:propertyupdate xmlns:D="DAV:"` + ` xmlns:Z="http://ns.example.com/z/">` + ` <D:set>` + ` <D:prop><Z:Authors>somevalue</Z:Authors></D:prop>` + ` </D:set>` + ` <D:remove>` + ` <D:prop><Z:Copyright-Owner/></D:prop>` + ` </D:remove>` + `</D:propertyupdate>`, wantPP: []Proppatch{{ Props: []Property{{ xml.Name{Space: "http://ns.example.com/z/", Local: "Authors"}, "", []byte(`somevalue`), }}, }, { Remove: true, Props: []Property{{ xml.Name{Space: "http://ns.example.com/z/", Local: "Copyright-Owner"}, "", nil, }}, }}, }, { desc: "proppatch: lang attribute on prop", input: `` + `<?xml version="1.0" encoding="utf-8" ?>` + `<D:propertyupdate xmlns:D="DAV:">` + ` <D:set>` + ` <D:prop xml:lang="en">` + ` <foo xmlns="http://example.com/ns"/>` + ` </D:prop>` + ` </D:set>` + `</D:propertyupdate>`, wantPP: []Proppatch{{ Props: []Property{{ xml.Name{Space: "http://example.com/ns", Local: "foo"}, "en", nil, }}, }}, }, { desc: "bad: remove with value", input: `` + `<?xml version="1.0" encoding="utf-8" ?>` + `<D:propertyupdate xmlns:D="DAV:"` + ` xmlns:Z="http://ns.example.com/z/">` + ` <D:remove>` + ` <D:prop>` + ` <Z:Authors>` + ` <Z:Author>Jim Whitehead</Z:Author>` + ` </Z:Authors>` + ` </D:prop>` + ` </D:remove>` + `</D:propertyupdate>`, wantStatus: http.StatusBadRequest, }, { desc: "bad: empty propertyupdate", input: `` + `<?xml version="1.0" encoding="utf-8" ?>` + `<D:propertyupdate xmlns:D="DAV:"` + `</D:propertyupdate>`, wantStatus: http.StatusBadRequest, }, { desc: "bad: empty prop", input: `` + `<?xml version="1.0" encoding="utf-8" ?>` + `<D:propertyupdate xmlns:D="DAV:"` + ` xmlns:Z="http://ns.example.com/z/">` + ` <D:remove>` + ` <D:prop/>` + ` </D:remove>` + `</D:propertyupdate>`, wantStatus: http.StatusBadRequest, }}
for _, tc := range testCases { pp, status, err := readProppatch(strings.NewReader(tc.input)) if tc.wantStatus != 0 { if err == nil { t.Errorf("%s: got nil error, want non-nil", tc.desc) continue } } else if err != nil { t.Errorf("%s: %v", tc.desc, err) continue } if status != tc.wantStatus { t.Errorf("%s: got status %d, want %d", tc.desc, status, tc.wantStatus) continue } if !reflect.DeepEqual(pp, tc.wantPP) || status != tc.wantStatus { t.Errorf("%s: proppatch\ngot %v\nwant %v", tc.desc, ppStr(pp), ppStr(tc.wantPP)) } } }
func TestUnmarshalXMLValue(t *testing.T) { testCases := []struct { desc string input string wantVal string }{{ desc: "simple char data", input: "<root>foo</root>", wantVal: "foo", }, { desc: "empty element", input: "<root><foo/></root>", wantVal: "<foo/>", }, { desc: "preserve namespace", input: `<root><foo xmlns="bar"/></root>`, wantVal: `<foo xmlns="bar"/>`, }, { desc: "preserve root element namespace", input: `<root xmlns:bar="bar"><bar:foo/></root>`, wantVal: `<foo xmlns="bar"/>`, }, { desc: "preserve whitespace", input: "<root> \t </root>", wantVal: " \t ", }, { desc: "preserve mixed content", input: `<root xmlns="bar"> <foo>a<bam xmlns="baz"/> </foo> </root>`, wantVal: ` <foo xmlns="bar">a<bam xmlns="baz"/> </foo> `, }, { desc: "section 9.2", input: `` + `<Z:Authors xmlns:Z="http://ns.example.com/z/">` + ` <Z:Author>Jim Whitehead</Z:Author>` + ` <Z:Author>Roy Fielding</Z:Author>` + `</Z:Authors>`, wantVal: `` + ` <Author xmlns="http://ns.example.com/z/">Jim Whitehead</Author>` + ` <Author xmlns="http://ns.example.com/z/">Roy Fielding</Author>`, }, { desc: "section 4.3.1 (mixed content)", input: `` + `<x:author ` + ` xmlns:x='http://example.com/ns' ` + ` xmlns:D="DAV:">` + ` <x:name>Jane Doe</x:name>` + ` <!-- Jane's contact info -->` + ` <x:uri type='email'` + ` added='2005-11-26'>mailto:jane.doe@example.com</x:uri>` + ` <x:uri type='web'` + ` added='2005-11-27'>http://www.example.com</x:uri>` + ` <x:notes xmlns:h='http://www.w3.org/1999/xhtml'>` + ` Jane has been working way <h:em>too</h:em> long on the` + ` long-awaited revision of <![CDATA[<RFC2518>]]>.` + ` </x:notes>` + `</x:author>`, wantVal: `` + ` <name xmlns="http://example.com/ns">Jane Doe</name>` + ` ` + ` <uri type='email'` + ` xmlns="http://example.com/ns" ` + ` added='2005-11-26'>mailto:jane.doe@example.com</uri>` + ` <uri added='2005-11-27'` + ` type='web'` + ` xmlns="http://example.com/ns">http://www.example.com</uri>` + ` <notes xmlns="http://example.com/ns" ` + ` xmlns:h="http://www.w3.org/1999/xhtml">` + ` Jane has been working way <h:em>too</h:em> long on the` + ` long-awaited revision of <RFC2518>.` + ` </notes>`, }}
var n xmlNormalizer for _, tc := range testCases { d := ixml.NewDecoder(strings.NewReader(tc.input)) var v xmlValue if err := d.Decode(&v); err != nil { t.Errorf("%s: got error %v, want nil", tc.desc, err) continue } eq, err := n.equalXML(bytes.NewReader(v), strings.NewReader(tc.wantVal)) if err != nil { t.Errorf("%s: equalXML: %v", tc.desc, err) continue } if !eq { t.Errorf("%s:\ngot %s\nwant %s", tc.desc, string(v), tc.wantVal) } } }
// xmlNormalizer normalizes XML.
type xmlNormalizer struct { // omitWhitespace instructs to ignore whitespace between element tags.
omitWhitespace bool // omitComments instructs to ignore XML comments.
omitComments bool }
// normalize writes the normalized XML content of r to w. It applies the
// following rules
//
// * Rename namespace prefixes according to an internal heuristic.
// * Remove unnecessary namespace declarations.
// * Sort attributes in XML start elements in lexical order of their
// fully qualified name.
// * Remove XML directives and processing instructions.
// * Remove CDATA between XML tags that only contains whitespace, if
// instructed to do so.
// * Remove comments, if instructed to do so.
//
func (n *xmlNormalizer) normalize(w io.Writer, r io.Reader) error { d := ixml.NewDecoder(r) e := ixml.NewEncoder(w) for { t, err := d.Token() if err != nil { if t == nil && err == io.EOF { break } return err } switch val := t.(type) { case ixml.Directive, ixml.ProcInst: continue case ixml.Comment: if n.omitComments { continue } case ixml.CharData: if n.omitWhitespace && len(bytes.TrimSpace(val)) == 0 { continue } case ixml.StartElement: start, _ := ixml.CopyToken(val).(ixml.StartElement) attr := start.Attr[:0] for _, a := range start.Attr { if a.Name.Space == "xmlns" || a.Name.Local == "xmlns" { continue } attr = append(attr, a) } sort.Sort(byName(attr)) start.Attr = attr t = start } err = e.EncodeToken(t) if err != nil { return err } } return e.Flush() }
// equalXML tests for equality of the normalized XML contents of a and b.
func (n *xmlNormalizer) equalXML(a, b io.Reader) (bool, error) { var buf bytes.Buffer if err := n.normalize(&buf, a); err != nil { return false, err } normA := buf.String() buf.Reset() if err := n.normalize(&buf, b); err != nil { return false, err } normB := buf.String() return normA == normB, nil }
type byName []ixml.Attr
func (a byName) Len() int { return len(a) } func (a byName) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a byName) Less(i, j int) bool { if a[i].Name.Space != a[j].Name.Space { return a[i].Name.Space < a[j].Name.Space } return a[i].Name.Local < a[j].Name.Local }
|