// 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",
"" +
"\n" +
" \n" +
" \n" +
" \n" +
" no end tag \n" +
" \n" +
"",
lockInfo{},
http.StatusBadRequest,
}, {
"bad: invalid UTF-8",
"" +
"\n" +
" \n" +
" \n" +
" \n" +
" \xff \n" +
" \n" +
"",
lockInfo{},
http.StatusBadRequest,
}, {
"bad: unfinished XML #1",
"" +
"\n" +
" \n" +
" \n",
lockInfo{},
http.StatusBadRequest,
}, {
"bad: unfinished XML #2",
"" +
"\n" +
" \n" +
" \n" +
" \n",
lockInfo{},
http.StatusBadRequest,
}, {
"good: empty",
"",
lockInfo{},
0,
}, {
"good: plain-text owner",
"" +
"\n" +
" \n" +
" \n" +
" gopher\n" +
"",
lockInfo{
XMLName: ixml.Name{Space: "DAV:", Local: "lockinfo"},
Exclusive: new(struct{}),
Write: new(struct{}),
Owner: owner{
InnerXML: "gopher",
},
},
0,
}, {
"section 9.10.7",
"" +
"\n" +
" \n" +
" \n" +
" \n" +
" http://example.org/~ejw/contact.html\n" +
" \n" +
"",
lockInfo{
XMLName: ixml.Name{Space: "DAV:", Local: "lockinfo"},
Exclusive: new(struct{}),
Write: new(struct{}),
Owner: owner{
InnerXML: "\n http://example.org/~ejw/contact.html\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: "" +
"\n" +
" \n" +
"",
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: "" +
"\n" +
" \n" +
"",
wantPF: propfind{
XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
Allprop: new(struct{}),
},
}, {
desc: "propfind: allprop followed by include",
input: "" +
"\n" +
" \n" +
" \n" +
"",
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: "" +
"\n" +
" \n" +
" \n" +
"",
wantPF: propfind{
XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
Allprop: new(struct{}),
Include: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
},
}, {
desc: "propfind: propfind",
input: "" +
"\n" +
" \n" +
"",
wantPF: propfind{
XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
Prop: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
},
}, {
desc: "propfind: prop with ignored comments",
input: "" +
"\n" +
" \n" +
" \n" +
" \n" +
" \n" +
"",
wantPF: propfind{
XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
Prop: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
},
}, {
desc: "propfind: propfind with ignored whitespace",
input: "" +
"\n" +
" \n" +
"",
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: "" +
"\n" +
" foobar\n" +
"",
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: "" +
"\n" +
" \n" +
" *boss*\n" +
"",
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: "" +
"\n" +
" " +
" " +
"",
wantStatus: http.StatusBadRequest,
}, {
desc: "propfind: bad: propname and prop",
input: "" +
"\n" +
" \n" +
" \n" +
"",
wantStatus: http.StatusBadRequest,
}, {
desc: "propfind: bad: allprop and prop",
input: "" +
"\n" +
" \n" +
" \n" +
"",
wantStatus: http.StatusBadRequest,
}, {
desc: "propfind: bad: empty propfind with ignored element (section A.4)",
input: "" +
"\n" +
" \n" +
"",
wantStatus: http.StatusBadRequest,
}, {
desc: "propfind: bad: empty prop",
input: "" +
"\n" +
" \n" +
"",
wantStatus: http.StatusBadRequest,
}, {
desc: "propfind: bad: prop with just chardata",
input: "" +
"\n" +
" foo\n" +
"",
wantStatus: http.StatusBadRequest,
}, {
desc: "bad: interrupted prop",
input: "" +
"\n" +
" \n",
wantStatus: http.StatusBadRequest,
}, {
desc: "bad: malformed end element prop",
input: "" +
"\n" +
" \n",
wantStatus: http.StatusBadRequest,
}, {
desc: "propfind: bad: property with chardata value",
input: "" +
"\n" +
" bar\n" +
"",
wantStatus: http.StatusBadRequest,
}, {
desc: "propfind: bad: property with whitespace value",
input: "" +
"\n" +
" \n" +
"",
wantStatus: http.StatusBadRequest,
}, {
desc: "propfind: bad: include without allprop",
input: "" +
"\n" +
" \n" +
"",
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: `` +
`` +
`` +
` ` +
` http://example.com/foo` +
` ` +
` ` +
` ` +
` ` +
` HTTP/1.1 424 Failed Dependency` +
` ` +
` ` +
` ` +
` ` +
` ` +
` HTTP/1.1 409 Conflict` +
` ` +
` Copyright Owner cannot be deleted or altered.` +
`` +
``,
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(``),
},
}},
wantXML: `` +
`` +
`` +
` ` +
` http://example.com/foo` +
` HTTP/1.1 423 Locked` +
` ` +
` ` +
``,
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(`` +
`` +
`Box type A` +
``),
}, {
XMLName: xml.Name{Space: "http://ns.example.com/boxschema/", Local: "author"},
InnerXML: []byte(`` +
`` +
`J.J. Johnson` +
``),
}},
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: `` +
`` +
`` +
` ` +
` http://example.com/foo` +
` ` +
` ` +
` Box type A` +
` J.J. Johnson` +
` ` +
` HTTP/1.1 200 OK` +
` ` +
` ` +
` ` +
` ` +
` ` +
` ` +
` HTTP/1.1 403 Forbidden` +
` The user does not have access to the DingALing property.` +
` ` +
` ` +
` There has been an access violation error.` +
``,
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: ``,
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: `` +
`` +
`` +
` ` +
` somevalue` +
` ` +
` ` +
` ` +
` ` +
``,
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: `` +
`` +
`` +
` ` +
` ` +
` ` +
` ` +
` ` +
``,
wantPP: []Proppatch{{
Props: []Property{{
xml.Name{Space: "http://example.com/ns", Local: "foo"},
"en",
nil,
}},
}},
}, {
desc: "bad: remove with value",
input: `` +
`` +
`` +
` ` +
` ` +
` ` +
` Jim Whitehead` +
` ` +
` ` +
` ` +
``,
wantStatus: http.StatusBadRequest,
}, {
desc: "bad: empty propertyupdate",
input: `` +
`` +
``,
wantStatus: http.StatusBadRequest,
}, {
desc: "bad: empty prop",
input: `` +
`` +
`` +
` ` +
` ` +
` ` +
``,
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: "foo",
wantVal: "foo",
}, {
desc: "empty element",
input: "",
wantVal: "",
}, {
desc: "preserve namespace",
input: ``,
wantVal: ``,
}, {
desc: "preserve root element namespace",
input: ``,
wantVal: ``,
}, {
desc: "preserve whitespace",
input: " \t ",
wantVal: " \t ",
}, {
desc: "preserve mixed content",
input: ` a `,
wantVal: ` a `,
}, {
desc: "section 9.2",
input: `` +
`` +
` Jim Whitehead` +
` Roy Fielding` +
``,
wantVal: `` +
` Jim Whitehead` +
` Roy Fielding`,
}, {
desc: "section 4.3.1 (mixed content)",
input: `` +
`` +
` Jane Doe` +
` ` +
` mailto:jane.doe@example.com` +
` http://www.example.com` +
` ` +
` Jane has been working way too long on the` +
` long-awaited revision of ]]>.` +
` ` +
``,
wantVal: `` +
` Jane Doe` +
` ` +
` mailto:jane.doe@example.com` +
` http://www.example.com` +
` ` +
` Jane has been working way too long on the` +
` long-awaited revision of <RFC2518>.` +
` `,
}}
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
}