2024-02-21 12:40:12 +00:00
|
|
|
// Copyright (c) Tailscale Inc & AUTHORS
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
|
|
|
|
package compositedav
|
|
|
|
|
|
|
|
import (
|
2024-05-03 22:20:13 +00:00
|
|
|
"fmt"
|
|
|
|
"log"
|
|
|
|
"net/http"
|
|
|
|
"path"
|
|
|
|
"strings"
|
2024-02-21 12:40:12 +00:00
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
2024-05-03 22:20:13 +00:00
|
|
|
"github.com/google/go-cmp/cmp"
|
2024-02-21 12:40:12 +00:00
|
|
|
"tailscale.com/tstest"
|
|
|
|
)
|
|
|
|
|
2024-05-03 22:20:13 +00:00
|
|
|
var parentPath = "/parent"
|
|
|
|
|
|
|
|
var childPath = "/parent/child.txt"
|
|
|
|
|
|
|
|
var parentResponse = `<D:response>
|
|
|
|
<D:href>/parent/</D:href>
|
|
|
|
<D:propstat>
|
|
|
|
<D:prop>
|
|
|
|
<D:getlastmodified>Mon, 29 Apr 2024 19:52:23 GMT</D:getlastmodified>
|
|
|
|
<D:creationdate>Fri, 19 Apr 2024 04:13:34 GMT</D:creationdate>
|
|
|
|
<D:resourcetype>
|
|
|
|
<D:collection xmlns:D="DAV:" />
|
|
|
|
</D:resourcetype>
|
|
|
|
</D:prop>
|
|
|
|
<D:status>HTTP/1.1 200 OK</D:status>
|
|
|
|
</D:propstat>
|
|
|
|
</D:response>`
|
|
|
|
|
|
|
|
var childResponse = `
|
|
|
|
<D:response>
|
|
|
|
<D:href>/parent/child.txt</D:href>
|
|
|
|
<D:propstat>
|
|
|
|
<D:prop>
|
|
|
|
<D:getlastmodified>Mon, 29 Apr 2024 19:52:23 GMT</D:getlastmodified>
|
|
|
|
<D:creationdate>Fri, 19 Apr 2024 04:13:34 GMT</D:creationdate>
|
|
|
|
<D:resourcetype>
|
|
|
|
<D:collection xmlns:D="DAV:" />
|
|
|
|
</D:resourcetype>
|
|
|
|
</D:prop>
|
|
|
|
<D:status>HTTP/1.1 200 OK</D:status>
|
|
|
|
</D:propstat>
|
|
|
|
</D:response>`
|
|
|
|
|
|
|
|
var fullParent = []byte(
|
|
|
|
strings.ReplaceAll(
|
|
|
|
fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?><D:multistatus xmlns:D="DAV:">%s%s</D:multistatus>`, parentResponse, childResponse),
|
|
|
|
"\n", ""))
|
|
|
|
|
|
|
|
var partialParent = []byte(
|
|
|
|
strings.ReplaceAll(
|
|
|
|
fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?><D:multistatus xmlns:D="DAV:">%s</D:multistatus>`, parentResponse),
|
|
|
|
"\n", ""))
|
|
|
|
|
|
|
|
var fullChild = []byte(
|
|
|
|
strings.ReplaceAll(
|
|
|
|
fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?><D:multistatus xmlns:D="DAV:">%s</D:multistatus>`, childResponse),
|
|
|
|
"\n", ""))
|
2024-02-21 12:40:12 +00:00
|
|
|
|
|
|
|
func TestStatCacheNoTimeout(t *testing.T) {
|
|
|
|
// Make sure we don't leak goroutines
|
|
|
|
tstest.ResourceCheck(t)
|
|
|
|
|
|
|
|
c := &StatCache{TTL: 5 * time.Second}
|
|
|
|
defer c.stop()
|
|
|
|
|
|
|
|
// check get before set
|
2024-05-03 22:20:13 +00:00
|
|
|
fetched := c.get(childPath, 0)
|
2024-02-21 12:40:12 +00:00
|
|
|
if fetched != nil {
|
2024-05-03 22:20:13 +00:00
|
|
|
t.Errorf("got %v, want nil", fetched)
|
2024-02-21 12:40:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// set new stat
|
2024-05-03 22:20:13 +00:00
|
|
|
ce := newCacheEntry(http.StatusMultiStatus, fullChild)
|
|
|
|
c.set(childPath, 0, ce)
|
|
|
|
fetched = c.get(childPath, 0)
|
|
|
|
if diff := cmp.Diff(fetched, ce); diff != "" {
|
|
|
|
t.Errorf("should have gotten cached value; (-got+want):%v", diff)
|
2024-02-21 12:40:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// fetch stat again, should still be cached
|
2024-05-03 22:20:13 +00:00
|
|
|
fetched = c.get(childPath, 0)
|
|
|
|
if diff := cmp.Diff(fetched, ce); diff != "" {
|
|
|
|
t.Errorf("should still have gotten cached value; (-got+want):%v", diff)
|
2024-02-21 12:40:12 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestStatCacheTimeout(t *testing.T) {
|
|
|
|
// Make sure we don't leak goroutines
|
|
|
|
tstest.ResourceCheck(t)
|
|
|
|
|
|
|
|
c := &StatCache{TTL: 250 * time.Millisecond}
|
|
|
|
defer c.stop()
|
|
|
|
|
|
|
|
// set new stat
|
2024-05-03 22:20:13 +00:00
|
|
|
ce := newCacheEntry(http.StatusMultiStatus, fullChild)
|
|
|
|
c.set(childPath, 0, ce)
|
|
|
|
fetched := c.get(childPath, 0)
|
|
|
|
if diff := cmp.Diff(fetched, ce); diff != "" {
|
|
|
|
t.Errorf("should have gotten cached value; (-got+want):%v", diff)
|
2024-02-21 12:40:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// wait for cache to expire and refetch stat, should be empty now
|
|
|
|
time.Sleep(c.TTL * 2)
|
|
|
|
|
2024-05-03 22:20:13 +00:00
|
|
|
fetched = c.get(childPath, 0)
|
2024-02-21 12:40:12 +00:00
|
|
|
if fetched != nil {
|
2024-05-03 22:20:13 +00:00
|
|
|
t.Errorf("cached value should have expired")
|
2024-02-21 12:40:12 +00:00
|
|
|
}
|
|
|
|
|
2024-05-03 22:20:13 +00:00
|
|
|
c.set(childPath, 0, ce)
|
2024-02-21 12:40:12 +00:00
|
|
|
// invalidate the cache and make sure nothing is returned
|
|
|
|
c.invalidate()
|
2024-05-03 22:20:13 +00:00
|
|
|
fetched = c.get(childPath, 0)
|
2024-02-21 12:40:12 +00:00
|
|
|
if fetched != nil {
|
|
|
|
t.Errorf("invalidate should have cleared cached value")
|
|
|
|
}
|
|
|
|
}
|
2024-05-03 22:20:13 +00:00
|
|
|
|
|
|
|
func TestParentChildRelationship(t *testing.T) {
|
|
|
|
// Make sure we don't leak goroutines
|
|
|
|
tstest.ResourceCheck(t)
|
|
|
|
|
|
|
|
c := &StatCache{TTL: 24 * time.Hour} // don't expire
|
|
|
|
defer c.stop()
|
|
|
|
|
|
|
|
missingParentPath := "/missingparent"
|
|
|
|
unparseableParentPath := "/unparseable"
|
|
|
|
|
|
|
|
c.set(parentPath, 1, newCacheEntry(http.StatusMultiStatus, fullParent))
|
|
|
|
c.set(missingParentPath, 1, newCacheEntry(http.StatusNotFound, nil))
|
|
|
|
c.set(unparseableParentPath, 1, newCacheEntry(http.StatusMultiStatus, []byte("<this will not parse")))
|
|
|
|
|
|
|
|
tests := []struct {
|
|
|
|
path string
|
|
|
|
depth int
|
|
|
|
want *cacheEntry
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
path: parentPath,
|
|
|
|
depth: 1,
|
|
|
|
want: newCacheEntry(http.StatusMultiStatus, fullParent),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
path: parentPath,
|
|
|
|
depth: 0,
|
|
|
|
want: newCacheEntry(http.StatusMultiStatus, partialParent),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
path: childPath,
|
|
|
|
depth: 0,
|
|
|
|
want: newCacheEntry(http.StatusMultiStatus, fullChild),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
path: path.Join(parentPath, "nonexistent.txt"),
|
|
|
|
depth: 0,
|
|
|
|
want: notFound,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
path: missingParentPath,
|
|
|
|
depth: 1,
|
|
|
|
want: notFound,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
path: missingParentPath,
|
|
|
|
depth: 0,
|
|
|
|
want: notFound,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
path: path.Join(missingParentPath, "filename.txt"),
|
|
|
|
depth: 0,
|
|
|
|
want: notFound,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
path: unparseableParentPath,
|
|
|
|
depth: 1,
|
|
|
|
want: nil,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
path: unparseableParentPath,
|
|
|
|
depth: 0,
|
|
|
|
want: nil,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
path: path.Join(unparseableParentPath, "filename.txt"),
|
|
|
|
depth: 0,
|
|
|
|
want: nil,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
path: "/unknown",
|
|
|
|
depth: 1,
|
|
|
|
want: nil,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, test := range tests {
|
|
|
|
t.Run(fmt.Sprintf("%d%s", test.depth, test.path), func(t *testing.T) {
|
|
|
|
got := c.get(test.path, test.depth)
|
|
|
|
if diff := cmp.Diff(got, test.want); diff != "" {
|
|
|
|
t.Errorf("unexpected cached value; (-got+want):%v", diff)
|
|
|
|
log.Printf("want\n%s", test.want.Raw)
|
|
|
|
log.Printf("got\n%s", got.Raw)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|