123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906 |
- // 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
- }
|