| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575 |
- // 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 (
- "encoding/xml"
- "net/http"
- "net/http/httptest"
- "reflect"
- "strings"
- "testing"
- )
- 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: xml.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: xml.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: xml.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: xml.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: xml.Name{Space: "DAV:", Local: "propfind"},
- Allprop: new(struct{}),
- Include: propnames{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: xml.Name{Space: "DAV:", Local: "propfind"},
- Allprop: new(struct{}),
- Include: propnames{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: xml.Name{Space: "DAV:", Local: "propfind"},
- Prop: propnames{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: xml.Name{Space: "DAV:", Local: "propfind"},
- Prop: propnames{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: xml.Name{Space: "DAV:", Local: "propfind"},
- Prop: propnames{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: xml.Name{Space: "DAV:", Local: "propfind"},
- Prop: propnames{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: xml.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
- //
- // BUG:The following tests compare the actual and expected XML verbatim.
- // Minor tweaks in the marshalling output of either standard encoding/xml
- // or this package might break them. A more resilient approach could be
- // to normalize both actual and expected XML content before comparison.
- // This also would enhance readibility of the expected XML payload in the
- // wantXML field.
- testCases := []struct {
- desc string
- responses []response
- respdesc string
- 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"?>` +
- `<D:multistatus xmlns:D="DAV:">` +
- `<response xmlns="DAV:">` +
- `<href xmlns="DAV:">http://example.com/foo</href>` +
- `<propstat xmlns="DAV:">` +
- `<prop>` +
- `<Authors xmlns="http://ns.example.com/"></Authors>` +
- `</prop>` +
- `<status xmlns="DAV:">HTTP/1.1 424 Failed Dependency</status>` +
- `</propstat>` +
- `<propstat xmlns="DAV:">` +
- `<prop>` +
- `<Copyright-Owner xmlns="http://ns.example.com/"></Copyright-Owner>` +
- `</prop>` +
- `<status xmlns="DAV:">HTTP/1.1 409 Conflict</status>` +
- `</propstat>` +
- `<responsedescription xmlns="DAV:">` +
- ` Copyright Owner cannot be deleted or altered.` +
- `</responsedescription>` +
- `</response>` +
- `</D: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"?>` +
- `<D:multistatus xmlns:D="DAV:">` +
- `<response xmlns="DAV:">` +
- `<href xmlns="DAV:">http://example.com/foo</href>` +
- `<status xmlns="DAV:">HTTP/1.1 423 Locked</status>` +
- `<error xmlns="DAV:"><lock-token-submitted xmlns="DAV:"/></error>` +
- `</response>` +
- `</D: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"?>` +
- `<D:multistatus xmlns:D="DAV:">` +
- `<response xmlns="DAV:">` +
- `<href xmlns="DAV:">http://example.com/foo</href>` +
- `<propstat xmlns="DAV:">` +
- `<prop>` +
- `<bigbox xmlns="http://ns.example.com/boxschema/">` +
- `<BoxType xmlns="http://ns.example.com/boxschema/">Box type A</BoxType>` +
- `</bigbox>` +
- `<author xmlns="http://ns.example.com/boxschema/">` +
- `<Name xmlns="http://ns.example.com/boxschema/">J.J. Johnson</Name>` +
- `</author>` +
- `</prop>` +
- `<status xmlns="DAV:">HTTP/1.1 200 OK</status>` +
- `</propstat>` +
- `<propstat xmlns="DAV:">` +
- `<prop>` +
- `<DingALing xmlns="http://ns.example.com/boxschema/">` +
- `</DingALing>` +
- `<Random xmlns="http://ns.example.com/boxschema/">` +
- `</Random>` +
- `</prop>` +
- `<status xmlns="DAV:">HTTP/1.1 403 Forbidden</status>` +
- `<responsedescription xmlns="DAV:">` +
- ` The user does not have access to the DingALing property.` +
- `</responsedescription>` +
- `</propstat>` +
- `</response>` +
- `<D:responsedescription>` +
- ` There has been an access violation error.` +
- `</D:responsedescription>` +
- `</D:multistatus>`,
- wantCode: StatusMulti,
- }, {
- desc: "bad: no response written",
- // default of http.responseWriter
- wantCode: http.StatusOK,
- }, {
- desc: "bad: no response written (with description)",
- respdesc: "too bad",
- // default of http.responseWriter
- wantCode: http.StatusOK,
- }, {
- 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,
- }}
- loop:
- for _, tc := range testCases {
- rec := httptest.NewRecorder()
- w := multistatusWriter{w: rec, responseDescription: tc.respdesc}
- 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
- }
- if gotXML := rec.Body.String(); gotXML != tc.wantXML {
- t.Errorf("%s: XML body\ngot %q\nwant %q", tc.desc, gotXML, tc.wantXML)
- continue
- }
- }
- }
|