// 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 davServer import ( "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 = `` + `` + `` + `` + `` statForbiddenError = `` ) 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(``), }, { 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{ {Space: "DAV:", Local: "resourcetype"}, {Space: "foo", Local: "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{{Space: "DAV:", Local: "resourcetype"}}, wantPropstats: []Propstat{{ Status: http.StatusOK, Props: []Property{{ XMLName: xml.Name{Space: "DAV:", Local: "resourcetype"}, InnerXML: []byte(``), }}, }}, }, { op: "propfind", name: "/file", pnames: []xml.Name{{Space: "DAV:", Local: "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{{Space: "DAV:", Local: "getcontentlanguage"}}, wantPropstats: []Propstat{{ Status: http.StatusNotFound, Props: []Property{{ XMLName: xml.Name{Space: "DAV:", Local: "getcontentlanguage"}, }}, }}, }, { op: "propfind", name: "/dir", pnames: []xml.Name{{Space: "DAV:", Local: "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{{Space: "DAV:", Local: "getetag"}}, wantPropstats: []Propstat{{ Status: http.StatusNotFound, Props: []Property{{ XMLName: xml.Name{Space: "DAV:", Local: "getetag"}, }}, }}, }, { op: "propfind", name: "/file", pnames: []xml.Name{{Space: "DAV:", Local: "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{{Space: "foo:", Local: "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.Name func (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 []Property func (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 []Propstat func (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) } }