| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716 | // Copyright 2015 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 webdavimport (	"context"	"encoding/xml"	"fmt"	"net/http"	"os"	"reflect"	"regexp"	"sort"	"testing")func TestMemPS(t *testing.T) {	ctx := context.Background()	// calcProps calculates the getlastmodified and getetag DAV: property	// values in pstats for resource name in file-system fs.	calcProps := func(name string, fs FileSystem, ls LockSystem, pstats []Propstat) error {		fi, err := fs.Stat(ctx, name)		if err != nil {			return err		}		for _, pst := range pstats {			for i, p := range pst.Props {				switch p.XMLName {				case xml.Name{Space: "DAV:", Local: "getlastmodified"}:					p.InnerXML = []byte(fi.ModTime().UTC().Format(http.TimeFormat))					pst.Props[i] = p				case xml.Name{Space: "DAV:", Local: "getetag"}:					if fi.IsDir() {						continue					}					etag, err := findETag(ctx, fs, ls, name, fi)					if err != nil {						return err					}					p.InnerXML = []byte(etag)					pst.Props[i] = p				}			}		}		return nil	}	const (		lockEntry = `` +			`<D:lockentry xmlns:D="DAV:">` +			`<D:lockscope><D:exclusive/></D:lockscope>` +			`<D:locktype><D:write/></D:locktype>` +			`</D:lockentry>`		statForbiddenError = `<D:cannot-modify-protected-property xmlns:D="DAV:"/>`	)	type propOp struct {		op            string		name          string		pnames        []xml.Name		patches       []Proppatch		wantPnames    []xml.Name		wantPropstats []Propstat	}	testCases := []struct {		desc        string		noDeadProps bool		buildfs     []string		propOp      []propOp	}{{		desc:    "propname",		buildfs: []string{"mkdir /dir", "touch /file"},		propOp: []propOp{{			op:   "propname",			name: "/dir",			wantPnames: []xml.Name{				{Space: "DAV:", Local: "resourcetype"},				{Space: "DAV:", Local: "displayname"},				{Space: "DAV:", Local: "supportedlock"},				{Space: "DAV:", Local: "getlastmodified"},			},		}, {			op:   "propname",			name: "/file",			wantPnames: []xml.Name{				{Space: "DAV:", Local: "resourcetype"},				{Space: "DAV:", Local: "displayname"},				{Space: "DAV:", Local: "getcontentlength"},				{Space: "DAV:", Local: "getlastmodified"},				{Space: "DAV:", Local: "getcontenttype"},				{Space: "DAV:", Local: "getetag"},				{Space: "DAV:", Local: "supportedlock"},			},		}},	}, {		desc:    "allprop dir and file",		buildfs: []string{"mkdir /dir", "write /file foobarbaz"},		propOp: []propOp{{			op:   "allprop",			name: "/dir",			wantPropstats: []Propstat{{				Status: http.StatusOK,				Props: []Property{{					XMLName:  xml.Name{Space: "DAV:", Local: "resourcetype"},					InnerXML: []byte(`<D:collection xmlns:D="DAV:"/>`),				}, {					XMLName:  xml.Name{Space: "DAV:", Local: "displayname"},					InnerXML: []byte("dir"),				}, {					XMLName:  xml.Name{Space: "DAV:", Local: "getlastmodified"},					InnerXML: nil, // Calculated during test.				}, {					XMLName:  xml.Name{Space: "DAV:", Local: "supportedlock"},					InnerXML: []byte(lockEntry),				}},			}},		}, {			op:   "allprop",			name: "/file",			wantPropstats: []Propstat{{				Status: http.StatusOK,				Props: []Property{{					XMLName:  xml.Name{Space: "DAV:", Local: "resourcetype"},					InnerXML: []byte(""),				}, {					XMLName:  xml.Name{Space: "DAV:", Local: "displayname"},					InnerXML: []byte("file"),				}, {					XMLName:  xml.Name{Space: "DAV:", Local: "getcontentlength"},					InnerXML: []byte("9"),				}, {					XMLName:  xml.Name{Space: "DAV:", Local: "getlastmodified"},					InnerXML: nil, // Calculated during test.				}, {					XMLName:  xml.Name{Space: "DAV:", Local: "getcontenttype"},					InnerXML: []byte("text/plain; charset=utf-8"),				}, {					XMLName:  xml.Name{Space: "DAV:", Local: "getetag"},					InnerXML: nil, // Calculated during test.				}, {					XMLName:  xml.Name{Space: "DAV:", Local: "supportedlock"},					InnerXML: []byte(lockEntry),				}},			}},		}, {			op:   "allprop",			name: "/file",			pnames: []xml.Name{				{"DAV:", "resourcetype"},				{"foo", "bar"},			},			wantPropstats: []Propstat{{				Status: http.StatusOK,				Props: []Property{{					XMLName:  xml.Name{Space: "DAV:", Local: "resourcetype"},					InnerXML: []byte(""),				}, {					XMLName:  xml.Name{Space: "DAV:", Local: "displayname"},					InnerXML: []byte("file"),				}, {					XMLName:  xml.Name{Space: "DAV:", Local: "getcontentlength"},					InnerXML: []byte("9"),				}, {					XMLName:  xml.Name{Space: "DAV:", Local: "getlastmodified"},					InnerXML: nil, // Calculated during test.				}, {					XMLName:  xml.Name{Space: "DAV:", Local: "getcontenttype"},					InnerXML: []byte("text/plain; charset=utf-8"),				}, {					XMLName:  xml.Name{Space: "DAV:", Local: "getetag"},					InnerXML: nil, // Calculated during test.				}, {					XMLName:  xml.Name{Space: "DAV:", Local: "supportedlock"},					InnerXML: []byte(lockEntry),				}}}, {				Status: http.StatusNotFound,				Props: []Property{{					XMLName: xml.Name{Space: "foo", Local: "bar"},				}}},			},		}},	}, {		desc:    "propfind DAV:resourcetype",		buildfs: []string{"mkdir /dir", "touch /file"},		propOp: []propOp{{			op:     "propfind",			name:   "/dir",			pnames: []xml.Name{{"DAV:", "resourcetype"}},			wantPropstats: []Propstat{{				Status: http.StatusOK,				Props: []Property{{					XMLName:  xml.Name{Space: "DAV:", Local: "resourcetype"},					InnerXML: []byte(`<D:collection xmlns:D="DAV:"/>`),				}},			}},		}, {			op:     "propfind",			name:   "/file",			pnames: []xml.Name{{"DAV:", "resourcetype"}},			wantPropstats: []Propstat{{				Status: http.StatusOK,				Props: []Property{{					XMLName:  xml.Name{Space: "DAV:", Local: "resourcetype"},					InnerXML: []byte(""),				}},			}},		}},	}, {		desc:    "propfind unsupported DAV properties",		buildfs: []string{"mkdir /dir"},		propOp: []propOp{{			op:     "propfind",			name:   "/dir",			pnames: []xml.Name{{"DAV:", "getcontentlanguage"}},			wantPropstats: []Propstat{{				Status: http.StatusNotFound,				Props: []Property{{					XMLName: xml.Name{Space: "DAV:", Local: "getcontentlanguage"},				}},			}},		}, {			op:     "propfind",			name:   "/dir",			pnames: []xml.Name{{"DAV:", "creationdate"}},			wantPropstats: []Propstat{{				Status: http.StatusNotFound,				Props: []Property{{					XMLName: xml.Name{Space: "DAV:", Local: "creationdate"},				}},			}},		}},	}, {		desc:    "propfind getetag for files but not for directories",		buildfs: []string{"mkdir /dir", "touch /file"},		propOp: []propOp{{			op:     "propfind",			name:   "/dir",			pnames: []xml.Name{{"DAV:", "getetag"}},			wantPropstats: []Propstat{{				Status: http.StatusNotFound,				Props: []Property{{					XMLName: xml.Name{Space: "DAV:", Local: "getetag"},				}},			}},		}, {			op:     "propfind",			name:   "/file",			pnames: []xml.Name{{"DAV:", "getetag"}},			wantPropstats: []Propstat{{				Status: http.StatusOK,				Props: []Property{{					XMLName:  xml.Name{Space: "DAV:", Local: "getetag"},					InnerXML: nil, // Calculated during test.				}},			}},		}},	}, {		desc:        "proppatch property on no-dead-properties file system",		buildfs:     []string{"mkdir /dir"},		noDeadProps: true,		propOp: []propOp{{			op:   "proppatch",			name: "/dir",			patches: []Proppatch{{				Props: []Property{{					XMLName: xml.Name{Space: "foo", Local: "bar"},				}},			}},			wantPropstats: []Propstat{{				Status: http.StatusForbidden,				Props: []Property{{					XMLName: xml.Name{Space: "foo", Local: "bar"},				}},			}},		}, {			op:   "proppatch",			name: "/dir",			patches: []Proppatch{{				Props: []Property{{					XMLName: xml.Name{Space: "DAV:", Local: "getetag"},				}},			}},			wantPropstats: []Propstat{{				Status:   http.StatusForbidden,				XMLError: statForbiddenError,				Props: []Property{{					XMLName: xml.Name{Space: "DAV:", Local: "getetag"},				}},			}},		}},	}, {		desc:    "proppatch dead property",		buildfs: []string{"mkdir /dir"},		propOp: []propOp{{			op:   "proppatch",			name: "/dir",			patches: []Proppatch{{				Props: []Property{{					XMLName:  xml.Name{Space: "foo", Local: "bar"},					InnerXML: []byte("baz"),				}},			}},			wantPropstats: []Propstat{{				Status: http.StatusOK,				Props: []Property{{					XMLName: xml.Name{Space: "foo", Local: "bar"},				}},			}},		}, {			op:     "propfind",			name:   "/dir",			pnames: []xml.Name{{Space: "foo", Local: "bar"}},			wantPropstats: []Propstat{{				Status: http.StatusOK,				Props: []Property{{					XMLName:  xml.Name{Space: "foo", Local: "bar"},					InnerXML: []byte("baz"),				}},			}},		}},	}, {		desc:    "proppatch dead property with failed dependency",		buildfs: []string{"mkdir /dir"},		propOp: []propOp{{			op:   "proppatch",			name: "/dir",			patches: []Proppatch{{				Props: []Property{{					XMLName:  xml.Name{Space: "foo", Local: "bar"},					InnerXML: []byte("baz"),				}},			}, {				Props: []Property{{					XMLName:  xml.Name{Space: "DAV:", Local: "displayname"},					InnerXML: []byte("xxx"),				}},			}},			wantPropstats: []Propstat{{				Status:   http.StatusForbidden,				XMLError: statForbiddenError,				Props: []Property{{					XMLName: xml.Name{Space: "DAV:", Local: "displayname"},				}},			}, {				Status: StatusFailedDependency,				Props: []Property{{					XMLName: xml.Name{Space: "foo", Local: "bar"},				}},			}},		}, {			op:     "propfind",			name:   "/dir",			pnames: []xml.Name{{Space: "foo", Local: "bar"}},			wantPropstats: []Propstat{{				Status: http.StatusNotFound,				Props: []Property{{					XMLName: xml.Name{Space: "foo", Local: "bar"},				}},			}},		}},	}, {		desc:    "proppatch remove dead property",		buildfs: []string{"mkdir /dir"},		propOp: []propOp{{			op:   "proppatch",			name: "/dir",			patches: []Proppatch{{				Props: []Property{{					XMLName:  xml.Name{Space: "foo", Local: "bar"},					InnerXML: []byte("baz"),				}, {					XMLName:  xml.Name{Space: "spam", Local: "ham"},					InnerXML: []byte("eggs"),				}},			}},			wantPropstats: []Propstat{{				Status: http.StatusOK,				Props: []Property{{					XMLName: xml.Name{Space: "foo", Local: "bar"},				}, {					XMLName: xml.Name{Space: "spam", Local: "ham"},				}},			}},		}, {			op:   "propfind",			name: "/dir",			pnames: []xml.Name{				{Space: "foo", Local: "bar"},				{Space: "spam", Local: "ham"},			},			wantPropstats: []Propstat{{				Status: http.StatusOK,				Props: []Property{{					XMLName:  xml.Name{Space: "foo", Local: "bar"},					InnerXML: []byte("baz"),				}, {					XMLName:  xml.Name{Space: "spam", Local: "ham"},					InnerXML: []byte("eggs"),				}},			}},		}, {			op:   "proppatch",			name: "/dir",			patches: []Proppatch{{				Remove: true,				Props: []Property{{					XMLName: xml.Name{Space: "foo", Local: "bar"},				}},			}},			wantPropstats: []Propstat{{				Status: http.StatusOK,				Props: []Property{{					XMLName: xml.Name{Space: "foo", Local: "bar"},				}},			}},		}, {			op:   "propfind",			name: "/dir",			pnames: []xml.Name{				{Space: "foo", Local: "bar"},				{Space: "spam", Local: "ham"},			},			wantPropstats: []Propstat{{				Status: http.StatusNotFound,				Props: []Property{{					XMLName: xml.Name{Space: "foo", Local: "bar"},				}},			}, {				Status: http.StatusOK,				Props: []Property{{					XMLName:  xml.Name{Space: "spam", Local: "ham"},					InnerXML: []byte("eggs"),				}},			}},		}},	}, {		desc:    "propname with dead property",		buildfs: []string{"touch /file"},		propOp: []propOp{{			op:   "proppatch",			name: "/file",			patches: []Proppatch{{				Props: []Property{{					XMLName:  xml.Name{Space: "foo", Local: "bar"},					InnerXML: []byte("baz"),				}},			}},			wantPropstats: []Propstat{{				Status: http.StatusOK,				Props: []Property{{					XMLName: xml.Name{Space: "foo", Local: "bar"},				}},			}},		}, {			op:   "propname",			name: "/file",			wantPnames: []xml.Name{				{Space: "DAV:", Local: "resourcetype"},				{Space: "DAV:", Local: "displayname"},				{Space: "DAV:", Local: "getcontentlength"},				{Space: "DAV:", Local: "getlastmodified"},				{Space: "DAV:", Local: "getcontenttype"},				{Space: "DAV:", Local: "getetag"},				{Space: "DAV:", Local: "supportedlock"},				{Space: "foo", Local: "bar"},			},		}},	}, {		desc:    "proppatch remove unknown dead property",		buildfs: []string{"mkdir /dir"},		propOp: []propOp{{			op:   "proppatch",			name: "/dir",			patches: []Proppatch{{				Remove: true,				Props: []Property{{					XMLName: xml.Name{Space: "foo", Local: "bar"},				}},			}},			wantPropstats: []Propstat{{				Status: http.StatusOK,				Props: []Property{{					XMLName: xml.Name{Space: "foo", Local: "bar"},				}},			}},		}},	}, {		desc:    "bad: propfind unknown property",		buildfs: []string{"mkdir /dir"},		propOp: []propOp{{			op:     "propfind",			name:   "/dir",			pnames: []xml.Name{{"foo:", "bar"}},			wantPropstats: []Propstat{{				Status: http.StatusNotFound,				Props: []Property{{					XMLName: xml.Name{Space: "foo:", Local: "bar"},				}},			}},		}},	}}	for _, tc := range testCases {		fs, err := buildTestFS(tc.buildfs)		if err != nil {			t.Fatalf("%s: cannot create test filesystem: %v", tc.desc, err)		}		if tc.noDeadProps {			fs = noDeadPropsFS{fs}		}		ls := NewMemLS()		for _, op := range tc.propOp {			desc := fmt.Sprintf("%s: %s %s", tc.desc, op.op, op.name)			if err = calcProps(op.name, fs, ls, op.wantPropstats); err != nil {				t.Fatalf("%s: calcProps: %v", desc, err)			}			// Call property system.			var propstats []Propstat			switch op.op {			case "propname":				pnames, err := propnames(ctx, fs, ls, op.name)				if err != nil {					t.Errorf("%s: got error %v, want nil", desc, err)					continue				}				sort.Sort(byXMLName(pnames))				sort.Sort(byXMLName(op.wantPnames))				if !reflect.DeepEqual(pnames, op.wantPnames) {					t.Errorf("%s: pnames\ngot  %q\nwant %q", desc, pnames, op.wantPnames)				}				continue			case "allprop":				propstats, err = allprop(ctx, fs, ls, op.name, op.pnames)			case "propfind":				propstats, err = props(ctx, fs, ls, op.name, op.pnames)			case "proppatch":				propstats, err = patch(ctx, fs, ls, op.name, op.patches)			default:				t.Fatalf("%s: %s not implemented", desc, op.op)			}			if err != nil {				t.Errorf("%s: got error %v, want nil", desc, err)				continue			}			// Compare return values from allprop, propfind or proppatch.			for _, pst := range propstats {				sort.Sort(byPropname(pst.Props))			}			for _, pst := range op.wantPropstats {				sort.Sort(byPropname(pst.Props))			}			sort.Sort(byStatus(propstats))			sort.Sort(byStatus(op.wantPropstats))			if !reflect.DeepEqual(propstats, op.wantPropstats) {				t.Errorf("%s: propstat\ngot  %q\nwant %q", desc, propstats, op.wantPropstats)			}		}	}}func cmpXMLName(a, b xml.Name) bool {	if a.Space != b.Space {		return a.Space < b.Space	}	return a.Local < b.Local}type byXMLName []xml.Namefunc (b byXMLName) Len() int           { return len(b) }func (b byXMLName) Swap(i, j int)      { b[i], b[j] = b[j], b[i] }func (b byXMLName) Less(i, j int) bool { return cmpXMLName(b[i], b[j]) }type byPropname []Propertyfunc (b byPropname) Len() int           { return len(b) }func (b byPropname) Swap(i, j int)      { b[i], b[j] = b[j], b[i] }func (b byPropname) Less(i, j int) bool { return cmpXMLName(b[i].XMLName, b[j].XMLName) }type byStatus []Propstatfunc (b byStatus) Len() int           { return len(b) }func (b byStatus) Swap(i, j int)      { b[i], b[j] = b[j], b[i] }func (b byStatus) Less(i, j int) bool { return b[i].Status < b[j].Status }type noDeadPropsFS struct {	FileSystem}func (fs noDeadPropsFS) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (File, error) {	f, err := fs.FileSystem.OpenFile(ctx, name, flag, perm)	if err != nil {		return nil, err	}	return noDeadPropsFile{f}, nil}// noDeadPropsFile wraps a File but strips any optional DeadPropsHolder methods// provided by the underlying File implementation.type noDeadPropsFile struct {	f File}func (f noDeadPropsFile) Close() error                              { return f.f.Close() }func (f noDeadPropsFile) Read(p []byte) (int, error)                { return f.f.Read(p) }func (f noDeadPropsFile) Readdir(count int) ([]os.FileInfo, error)  { return f.f.Readdir(count) }func (f noDeadPropsFile) Seek(off int64, whence int) (int64, error) { return f.f.Seek(off, whence) }func (f noDeadPropsFile) Stat() (os.FileInfo, error)                { return f.f.Stat() }func (f noDeadPropsFile) Write(p []byte) (int, error)               { return f.f.Write(p) }type overrideContentType struct {	os.FileInfo	contentType string	err         error}func (o *overrideContentType) ContentType(ctx context.Context) (string, error) {	return o.contentType, o.err}func TestFindContentTypeOverride(t *testing.T) {	fs, err := buildTestFS([]string{"touch /file"})	if err != nil {		t.Fatalf("cannot create test filesystem: %v", err)	}	ctx := context.Background()	fi, err := fs.Stat(ctx, "/file")	if err != nil {		t.Fatalf("cannot Stat /file: %v", err)	}	// Check non overridden case	originalContentType, err := findContentType(ctx, fs, nil, "/file", fi)	if err != nil {		t.Fatalf("findContentType /file failed: %v", err)	}	if originalContentType != "text/plain; charset=utf-8" {		t.Fatalf("ContentType wrong want %q got %q", "text/plain; charset=utf-8", originalContentType)	}	// Now try overriding the ContentType	o := &overrideContentType{fi, "OverriddenContentType", nil}	ContentType, err := findContentType(ctx, fs, nil, "/file", o)	if err != nil {		t.Fatalf("findContentType /file failed: %v", err)	}	if ContentType != o.contentType {		t.Fatalf("ContentType wrong want %q got %q", o.contentType, ContentType)	}	// Now return ErrNotImplemented and check we get the original content type	o = &overrideContentType{fi, "OverriddenContentType", ErrNotImplemented}	ContentType, err = findContentType(ctx, fs, nil, "/file", o)	if err != nil {		t.Fatalf("findContentType /file failed: %v", err)	}	if ContentType != originalContentType {		t.Fatalf("ContentType wrong want %q got %q", originalContentType, ContentType)	}}type overrideETag struct {	os.FileInfo	eTag string	err  error}func (o *overrideETag) ETag(ctx context.Context) (string, error) {	return o.eTag, o.err}func TestFindETagOverride(t *testing.T) {	fs, err := buildTestFS([]string{"touch /file"})	if err != nil {		t.Fatalf("cannot create test filesystem: %v", err)	}	ctx := context.Background()	fi, err := fs.Stat(ctx, "/file")	if err != nil {		t.Fatalf("cannot Stat /file: %v", err)	}	// Check non overridden case	originalETag, err := findETag(ctx, fs, nil, "/file", fi)	if err != nil {		t.Fatalf("findETag /file failed: %v", err)	}	matchETag := regexp.MustCompile(`^"-?[0-9a-f]{6,}"$`)	if !matchETag.MatchString(originalETag) {		t.Fatalf("ETag wrong, wanted something matching %v got %q", matchETag, originalETag)	}	// Now try overriding the ETag	o := &overrideETag{fi, `"OverriddenETag"`, nil}	ETag, err := findETag(ctx, fs, nil, "/file", o)	if err != nil {		t.Fatalf("findETag /file failed: %v", err)	}	if ETag != o.eTag {		t.Fatalf("ETag wrong want %q got %q", o.eTag, ETag)	}	// Now return ErrNotImplemented and check we get the original Etag	o = &overrideETag{fi, `"OverriddenETag"`, ErrNotImplemented}	ETag, err = findETag(ctx, fs, nil, "/file", o)	if err != nil {		t.Fatalf("findETag /file failed: %v", err)	}	if ETag != originalETag {		t.Fatalf("ETag wrong want %q got %q", originalETag, ETag)	}}
 |