| // 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 ( |
| "fmt" |
| "math/rand" |
| "path" |
| "reflect" |
| "sort" |
| "strconv" |
| "strings" |
| "testing" |
| "time" |
| ) |
| |
| func TestWalkToRoot(t *testing.T) { |
| testCases := []struct { |
| name string |
| want []string |
| }{{ |
| "/a/b/c/d", |
| []string{ |
| "/a/b/c/d", |
| "/a/b/c", |
| "/a/b", |
| "/a", |
| "/", |
| }, |
| }, { |
| "/a", |
| []string{ |
| "/a", |
| "/", |
| }, |
| }, { |
| "/", |
| []string{ |
| "/", |
| }, |
| }} |
| |
| for _, tc := range testCases { |
| var got []string |
| if !walkToRoot(tc.name, func(name0 string, first bool) bool { |
| if first != (len(got) == 0) { |
| t.Errorf("name=%q: first=%t but len(got)==%d", tc.name, first, len(got)) |
| return false |
| } |
| got = append(got, name0) |
| return true |
| }) { |
| continue |
| } |
| if !reflect.DeepEqual(got, tc.want) { |
| t.Errorf("name=%q:\ngot %q\nwant %q", tc.name, got, tc.want) |
| } |
| } |
| } |
| |
| var lockTestDurations = []time.Duration{ |
| infiniteTimeout, // infiniteTimeout means to never expire. |
| 0, // A zero duration means to expire immediately. |
| 100 * time.Hour, // A very large duration will not expire in these tests. |
| } |
| |
| // lockTestNames are the names of a set of mutually compatible locks. For each |
| // name fragment: |
| // - _ means no explicit lock. |
| // - i means an infinite-depth lock, |
| // - z means a zero-depth lock, |
| var lockTestNames = []string{ |
| "/_/_/_/_/z", |
| "/_/_/i", |
| "/_/z", |
| "/_/z/i", |
| "/_/z/z", |
| "/_/z/_/i", |
| "/_/z/_/z", |
| "/i", |
| "/z", |
| "/z/_/i", |
| "/z/_/z", |
| } |
| |
| func lockTestZeroDepth(name string) bool { |
| switch name[len(name)-1] { |
| case 'i': |
| return false |
| case 'z': |
| return true |
| } |
| panic(fmt.Sprintf("lock name %q did not end with 'i' or 'z'", name)) |
| } |
| |
| func TestMemLSCanCreate(t *testing.T) { |
| now := time.Unix(0, 0) |
| m := NewMemLS().(*memLS) |
| |
| for _, name := range lockTestNames { |
| _, err := m.Create(now, LockDetails{ |
| Root: name, |
| Duration: infiniteTimeout, |
| ZeroDepth: lockTestZeroDepth(name), |
| }) |
| if err != nil { |
| t.Fatalf("creating lock for %q: %v", name, err) |
| } |
| } |
| |
| wantCanCreate := func(name string, zeroDepth bool) bool { |
| for _, n := range lockTestNames { |
| switch { |
| case n == name: |
| // An existing lock has the same name as the proposed lock. |
| return false |
| case strings.HasPrefix(n, name): |
| // An existing lock would be a child of the proposed lock, |
| // which conflicts if the proposed lock has infinite depth. |
| if !zeroDepth { |
| return false |
| } |
| case strings.HasPrefix(name, n): |
| // An existing lock would be an ancestor of the proposed lock, |
| // which conflicts if the ancestor has infinite depth. |
| if n[len(n)-1] == 'i' { |
| return false |
| } |
| } |
| } |
| return true |
| } |
| |
| var check func(int, string) |
| check = func(recursion int, name string) { |
| for _, zeroDepth := range []bool{false, true} { |
| got := m.canCreate(name, zeroDepth) |
| want := wantCanCreate(name, zeroDepth) |
| if got != want { |
| t.Errorf("canCreate name=%q zeroDepth=%t: got %t, want %t", name, zeroDepth, got, want) |
| } |
| } |
| if recursion == 6 { |
| return |
| } |
| if name != "/" { |
| name += "/" |
| } |
| for _, c := range "_iz" { |
| check(recursion+1, name+string(c)) |
| } |
| } |
| check(0, "/") |
| } |
| |
| func TestMemLSLookup(t *testing.T) { |
| now := time.Unix(0, 0) |
| m := NewMemLS().(*memLS) |
| |
| badToken := m.nextToken() |
| t.Logf("badToken=%q", badToken) |
| |
| for _, name := range lockTestNames { |
| token, err := m.Create(now, LockDetails{ |
| Root: name, |
| Duration: infiniteTimeout, |
| ZeroDepth: lockTestZeroDepth(name), |
| }) |
| if err != nil { |
| t.Fatalf("creating lock for %q: %v", name, err) |
| } |
| t.Logf("%-15q -> node=%p token=%q", name, m.byName[name], token) |
| } |
| |
| baseNames := append([]string{"/a", "/b/c"}, lockTestNames...) |
| for _, baseName := range baseNames { |
| for _, suffix := range []string{"", "/0", "/1/2/3"} { |
| name := baseName + suffix |
| |
| goodToken := "" |
| base := m.byName[baseName] |
| if base != nil && (suffix == "" || !lockTestZeroDepth(baseName)) { |
| goodToken = base.token |
| } |
| |
| for _, token := range []string{badToken, goodToken} { |
| if token == "" { |
| continue |
| } |
| |
| got := m.lookup(name, Condition{Token: token}) |
| want := base |
| if token == badToken { |
| want = nil |
| } |
| if got != want { |
| t.Errorf("name=%-20qtoken=%q (bad=%t): got %p, want %p", |
| name, token, token == badToken, got, want) |
| } |
| } |
| } |
| } |
| } |
| |
| func TestMemLSConfirm(t *testing.T) { |
| now := time.Unix(0, 0) |
| m := NewMemLS().(*memLS) |
| alice, err := m.Create(now, LockDetails{ |
| Root: "/alice", |
| Duration: infiniteTimeout, |
| ZeroDepth: false, |
| }) |
| if err != nil { |
| t.Fatalf("Create: %v", err) |
| } |
| |
| tweedle, err := m.Create(now, LockDetails{ |
| Root: "/tweedle", |
| Duration: infiniteTimeout, |
| ZeroDepth: false, |
| }) |
| if err != nil { |
| t.Fatalf("Create: %v", err) |
| } |
| if err := m.consistent(); err != nil { |
| t.Fatalf("Create: inconsistent state: %v", err) |
| } |
| |
| // Test a mismatch between name and condition. |
| _, err = m.Confirm(now, "/tweedle/dee", "", Condition{Token: alice}) |
| if err != ErrConfirmationFailed { |
| t.Fatalf("Confirm (mismatch): got %v, want ErrConfirmationFailed", err) |
| } |
| if err := m.consistent(); err != nil { |
| t.Fatalf("Confirm (mismatch): inconsistent state: %v", err) |
| } |
| |
| // Test two names (that fall under the same lock) in the one Confirm call. |
| release, err := m.Confirm(now, "/tweedle/dee", "/tweedle/dum", Condition{Token: tweedle}) |
| if err != nil { |
| t.Fatalf("Confirm (twins): %v", err) |
| } |
| if err := m.consistent(); err != nil { |
| t.Fatalf("Confirm (twins): inconsistent state: %v", err) |
| } |
| release() |
| if err := m.consistent(); err != nil { |
| t.Fatalf("release (twins): inconsistent state: %v", err) |
| } |
| |
| // Test the same two names in overlapping Confirm / release calls. |
| releaseDee, err := m.Confirm(now, "/tweedle/dee", "", Condition{Token: tweedle}) |
| if err != nil { |
| t.Fatalf("Confirm (sequence #0): %v", err) |
| } |
| if err := m.consistent(); err != nil { |
| t.Fatalf("Confirm (sequence #0): inconsistent state: %v", err) |
| } |
| |
| _, err = m.Confirm(now, "/tweedle/dum", "", Condition{Token: tweedle}) |
| if err != ErrConfirmationFailed { |
| t.Fatalf("Confirm (sequence #1): got %v, want ErrConfirmationFailed", err) |
| } |
| if err := m.consistent(); err != nil { |
| t.Fatalf("Confirm (sequence #1): inconsistent state: %v", err) |
| } |
| |
| releaseDee() |
| if err := m.consistent(); err != nil { |
| t.Fatalf("release (sequence #2): inconsistent state: %v", err) |
| } |
| |
| releaseDum, err := m.Confirm(now, "/tweedle/dum", "", Condition{Token: tweedle}) |
| if err != nil { |
| t.Fatalf("Confirm (sequence #3): %v", err) |
| } |
| if err := m.consistent(); err != nil { |
| t.Fatalf("Confirm (sequence #3): inconsistent state: %v", err) |
| } |
| |
| // Test that you can't unlock a held lock. |
| err = m.Unlock(now, tweedle) |
| if err != ErrLocked { |
| t.Fatalf("Unlock (sequence #4): got %v, want ErrLocked", err) |
| } |
| |
| releaseDum() |
| if err := m.consistent(); err != nil { |
| t.Fatalf("release (sequence #5): inconsistent state: %v", err) |
| } |
| |
| err = m.Unlock(now, tweedle) |
| if err != nil { |
| t.Fatalf("Unlock (sequence #6): %v", err) |
| } |
| if err := m.consistent(); err != nil { |
| t.Fatalf("Unlock (sequence #6): inconsistent state: %v", err) |
| } |
| } |
| |
| func TestMemLSNonCanonicalRoot(t *testing.T) { |
| now := time.Unix(0, 0) |
| m := NewMemLS().(*memLS) |
| token, err := m.Create(now, LockDetails{ |
| Root: "/foo/./bar//", |
| Duration: 1 * time.Second, |
| }) |
| if err != nil { |
| t.Fatalf("Create: %v", err) |
| } |
| if err := m.consistent(); err != nil { |
| t.Fatalf("Create: inconsistent state: %v", err) |
| } |
| if err := m.Unlock(now, token); err != nil { |
| t.Fatalf("Unlock: %v", err) |
| } |
| if err := m.consistent(); err != nil { |
| t.Fatalf("Unlock: inconsistent state: %v", err) |
| } |
| } |
| |
| func TestMemLSExpiry(t *testing.T) { |
| m := NewMemLS().(*memLS) |
| testCases := []string{ |
| "setNow 0", |
| "create /a.5", |
| "want /a.5", |
| "create /c.6", |
| "want /a.5 /c.6", |
| "create /a/b.7", |
| "want /a.5 /a/b.7 /c.6", |
| "setNow 4", |
| "want /a.5 /a/b.7 /c.6", |
| "setNow 5", |
| "want /a/b.7 /c.6", |
| "setNow 6", |
| "want /a/b.7", |
| "setNow 7", |
| "want ", |
| "setNow 8", |
| "want ", |
| "create /a.12", |
| "create /b.13", |
| "create /c.15", |
| "create /a/d.16", |
| "want /a.12 /a/d.16 /b.13 /c.15", |
| "refresh /a.14", |
| "want /a.14 /a/d.16 /b.13 /c.15", |
| "setNow 12", |
| "want /a.14 /a/d.16 /b.13 /c.15", |
| "setNow 13", |
| "want /a.14 /a/d.16 /c.15", |
| "setNow 14", |
| "want /a/d.16 /c.15", |
| "refresh /a/d.20", |
| "refresh /c.20", |
| "want /a/d.20 /c.20", |
| "setNow 20", |
| "want ", |
| } |
| |
| tokens := map[string]string{} |
| zTime := time.Unix(0, 0) |
| now := zTime |
| for i, tc := range testCases { |
| j := strings.IndexByte(tc, ' ') |
| if j < 0 { |
| t.Fatalf("test case #%d %q: invalid command", i, tc) |
| } |
| op, arg := tc[:j], tc[j+1:] |
| switch op { |
| default: |
| t.Fatalf("test case #%d %q: invalid operation %q", i, tc, op) |
| |
| case "create", "refresh": |
| parts := strings.Split(arg, ".") |
| if len(parts) != 2 { |
| t.Fatalf("test case #%d %q: invalid create", i, tc) |
| } |
| root := parts[0] |
| d, err := strconv.Atoi(parts[1]) |
| if err != nil { |
| t.Fatalf("test case #%d %q: invalid duration", i, tc) |
| } |
| dur := time.Unix(0, 0).Add(time.Duration(d) * time.Second).Sub(now) |
| |
| switch op { |
| case "create": |
| token, err := m.Create(now, LockDetails{ |
| Root: root, |
| Duration: dur, |
| ZeroDepth: true, |
| }) |
| if err != nil { |
| t.Fatalf("test case #%d %q: Create: %v", i, tc, err) |
| } |
| tokens[root] = token |
| |
| case "refresh": |
| token := tokens[root] |
| if token == "" { |
| t.Fatalf("test case #%d %q: no token for %q", i, tc, root) |
| } |
| got, err := m.Refresh(now, token, dur) |
| if err != nil { |
| t.Fatalf("test case #%d %q: Refresh: %v", i, tc, err) |
| } |
| want := LockDetails{ |
| Root: root, |
| Duration: dur, |
| ZeroDepth: true, |
| } |
| if got != want { |
| t.Fatalf("test case #%d %q:\ngot %v\nwant %v", i, tc, got, want) |
| } |
| } |
| |
| case "setNow": |
| d, err := strconv.Atoi(arg) |
| if err != nil { |
| t.Fatalf("test case #%d %q: invalid duration", i, tc) |
| } |
| now = time.Unix(0, 0).Add(time.Duration(d) * time.Second) |
| |
| case "want": |
| m.mu.Lock() |
| m.collectExpiredNodes(now) |
| got := make([]string, 0, len(m.byToken)) |
| for _, n := range m.byToken { |
| got = append(got, fmt.Sprintf("%s.%d", |
| n.details.Root, n.expiry.Sub(zTime)/time.Second)) |
| } |
| m.mu.Unlock() |
| sort.Strings(got) |
| want := []string{} |
| if arg != "" { |
| want = strings.Split(arg, " ") |
| } |
| if !reflect.DeepEqual(got, want) { |
| t.Fatalf("test case #%d %q:\ngot %q\nwant %q", i, tc, got, want) |
| } |
| } |
| |
| if err := m.consistent(); err != nil { |
| t.Fatalf("test case #%d %q: inconsistent state: %v", i, tc, err) |
| } |
| } |
| } |
| |
| func TestMemLS(t *testing.T) { |
| now := time.Unix(0, 0) |
| m := NewMemLS().(*memLS) |
| rng := rand.New(rand.NewSource(0)) |
| tokens := map[string]string{} |
| nConfirm, nCreate, nRefresh, nUnlock := 0, 0, 0, 0 |
| const N = 2000 |
| |
| for i := 0; i < N; i++ { |
| name := lockTestNames[rng.Intn(len(lockTestNames))] |
| duration := lockTestDurations[rng.Intn(len(lockTestDurations))] |
| confirmed, unlocked := false, false |
| |
| // If the name was already locked, we randomly confirm/release, refresh |
| // or unlock it. Otherwise, we create a lock. |
| token := tokens[name] |
| if token != "" { |
| switch rng.Intn(3) { |
| case 0: |
| confirmed = true |
| nConfirm++ |
| release, err := m.Confirm(now, name, "", Condition{Token: token}) |
| if err != nil { |
| t.Fatalf("iteration #%d: Confirm %q: %v", i, name, err) |
| } |
| if err := m.consistent(); err != nil { |
| t.Fatalf("iteration #%d: inconsistent state: %v", i, err) |
| } |
| release() |
| |
| case 1: |
| nRefresh++ |
| if _, err := m.Refresh(now, token, duration); err != nil { |
| t.Fatalf("iteration #%d: Refresh %q: %v", i, name, err) |
| } |
| |
| case 2: |
| unlocked = true |
| nUnlock++ |
| if err := m.Unlock(now, token); err != nil { |
| t.Fatalf("iteration #%d: Unlock %q: %v", i, name, err) |
| } |
| } |
| |
| } else { |
| nCreate++ |
| var err error |
| token, err = m.Create(now, LockDetails{ |
| Root: name, |
| Duration: duration, |
| ZeroDepth: lockTestZeroDepth(name), |
| }) |
| if err != nil { |
| t.Fatalf("iteration #%d: Create %q: %v", i, name, err) |
| } |
| } |
| |
| if !confirmed { |
| if duration == 0 || unlocked { |
| // A zero-duration lock should expire immediately and is |
| // effectively equivalent to being unlocked. |
| tokens[name] = "" |
| } else { |
| tokens[name] = token |
| } |
| } |
| |
| if err := m.consistent(); err != nil { |
| t.Fatalf("iteration #%d: inconsistent state: %v", i, err) |
| } |
| } |
| |
| if nConfirm < N/10 { |
| t.Fatalf("too few Confirm calls: got %d, want >= %d", nConfirm, N/10) |
| } |
| if nCreate < N/10 { |
| t.Fatalf("too few Create calls: got %d, want >= %d", nCreate, N/10) |
| } |
| if nRefresh < N/10 { |
| t.Fatalf("too few Refresh calls: got %d, want >= %d", nRefresh, N/10) |
| } |
| if nUnlock < N/10 { |
| t.Fatalf("too few Unlock calls: got %d, want >= %d", nUnlock, N/10) |
| } |
| } |
| |
| func (m *memLS) consistent() error { |
| m.mu.Lock() |
| defer m.mu.Unlock() |
| |
| // If m.byName is non-empty, then it must contain an entry for the root "/", |
| // and its refCount should equal the number of locked nodes. |
| if len(m.byName) > 0 { |
| n := m.byName["/"] |
| if n == nil { |
| return fmt.Errorf(`non-empty m.byName does not contain the root "/"`) |
| } |
| if n.refCount != len(m.byToken) { |
| return fmt.Errorf("root node refCount=%d, differs from len(m.byToken)=%d", n.refCount, len(m.byToken)) |
| } |
| } |
| |
| for name, n := range m.byName { |
| // The map keys should be consistent with the node's copy of the key. |
| if n.details.Root != name { |
| return fmt.Errorf("node name %q != byName map key %q", n.details.Root, name) |
| } |
| |
| // A name must be clean, and start with a "/". |
| if len(name) == 0 || name[0] != '/' { |
| return fmt.Errorf(`node name %q does not start with "/"`, name) |
| } |
| if name != path.Clean(name) { |
| return fmt.Errorf(`node name %q is not clean`, name) |
| } |
| |
| // A node's refCount should be positive. |
| if n.refCount <= 0 { |
| return fmt.Errorf("non-positive refCount for node at name %q", name) |
| } |
| |
| // A node's refCount should be the number of self-or-descendents that |
| // are locked (i.e. have a non-empty token). |
| var list []string |
| for name0, n0 := range m.byName { |
| // All of lockTestNames' name fragments are one byte long: '_', 'i' or 'z', |
| // so strings.HasPrefix is equivalent to self-or-descendent name match. |
| // We don't have to worry about "/foo/bar" being a false positive match |
| // for "/foo/b". |
| if strings.HasPrefix(name0, name) && n0.token != "" { |
| list = append(list, name0) |
| } |
| } |
| if n.refCount != len(list) { |
| sort.Strings(list) |
| return fmt.Errorf("node at name %q has refCount %d but locked self-or-descendents are %q (len=%d)", |
| name, n.refCount, list, len(list)) |
| } |
| |
| // A node n is in m.byToken if it has a non-empty token. |
| if n.token != "" { |
| if _, ok := m.byToken[n.token]; !ok { |
| return fmt.Errorf("node at name %q has token %q but not in m.byToken", name, n.token) |
| } |
| } |
| |
| // A node n is in m.byExpiry if it has a non-negative byExpiryIndex. |
| if n.byExpiryIndex >= 0 { |
| if n.byExpiryIndex >= len(m.byExpiry) { |
| return fmt.Errorf("node at name %q has byExpiryIndex %d but m.byExpiry has length %d", name, n.byExpiryIndex, len(m.byExpiry)) |
| } |
| if n != m.byExpiry[n.byExpiryIndex] { |
| return fmt.Errorf("node at name %q has byExpiryIndex %d but that indexes a different node", name, n.byExpiryIndex) |
| } |
| } |
| } |
| |
| for token, n := range m.byToken { |
| // The map keys should be consistent with the node's copy of the key. |
| if n.token != token { |
| return fmt.Errorf("node token %q != byToken map key %q", n.token, token) |
| } |
| |
| // Every node in m.byToken is in m.byName. |
| if _, ok := m.byName[n.details.Root]; !ok { |
| return fmt.Errorf("node at name %q in m.byToken but not in m.byName", n.details.Root) |
| } |
| } |
| |
| for i, n := range m.byExpiry { |
| // The slice indices should be consistent with the node's copy of the index. |
| if n.byExpiryIndex != i { |
| return fmt.Errorf("node byExpiryIndex %d != byExpiry slice index %d", n.byExpiryIndex, i) |
| } |
| |
| // Every node in m.byExpiry is in m.byName. |
| if _, ok := m.byName[n.details.Root]; !ok { |
| return fmt.Errorf("node at name %q in m.byExpiry but not in m.byName", n.details.Root) |
| } |
| |
| // No node in m.byExpiry should be held. |
| if n.held { |
| return fmt.Errorf("node at name %q in m.byExpiry is held", n.details.Root) |
| } |
| } |
| return nil |
| } |
| |
| func TestParseTimeout(t *testing.T) { |
| testCases := []struct { |
| s string |
| want time.Duration |
| wantErr error |
| }{{ |
| "", |
| infiniteTimeout, |
| nil, |
| }, { |
| "Infinite", |
| infiniteTimeout, |
| nil, |
| }, { |
| "Infinitesimal", |
| 0, |
| errInvalidTimeout, |
| }, { |
| "infinite", |
| 0, |
| errInvalidTimeout, |
| }, { |
| "Second-0", |
| 0 * time.Second, |
| nil, |
| }, { |
| "Second-123", |
| 123 * time.Second, |
| nil, |
| }, { |
| " Second-456 ", |
| 456 * time.Second, |
| nil, |
| }, { |
| "Second-4100000000", |
| 4100000000 * time.Second, |
| nil, |
| }, { |
| "junk", |
| 0, |
| errInvalidTimeout, |
| }, { |
| "Second-", |
| 0, |
| errInvalidTimeout, |
| }, { |
| "Second--1", |
| 0, |
| errInvalidTimeout, |
| }, { |
| "Second--123", |
| 0, |
| errInvalidTimeout, |
| }, { |
| "Second-+123", |
| 0, |
| errInvalidTimeout, |
| }, { |
| "Second-0x123", |
| 0, |
| errInvalidTimeout, |
| }, { |
| "second-123", |
| 0, |
| errInvalidTimeout, |
| }, { |
| "Second-4294967295", |
| 4294967295 * time.Second, |
| nil, |
| }, { |
| // Section 10.7 says that "The timeout value for TimeType "Second" |
| // must not be greater than 2^32-1." |
| "Second-4294967296", |
| 0, |
| errInvalidTimeout, |
| }, { |
| // This test case comes from section 9.10.9 of the spec. It says, |
| // |
| // "In this request, the client has specified that it desires an |
| // infinite-length lock, if available, otherwise a timeout of 4.1 |
| // billion seconds, if available." |
| // |
| // The Go WebDAV package always supports infinite length locks, |
| // and ignores the fallback after the comma. |
| "Infinite, Second-4100000000", |
| infiniteTimeout, |
| nil, |
| }} |
| |
| for _, tc := range testCases { |
| got, gotErr := parseTimeout(tc.s) |
| if got != tc.want || gotErr != tc.wantErr { |
| t.Errorf("parsing %q:\ngot %v, %v\nwant %v, %v", tc.s, got, gotErr, tc.want, tc.wantErr) |
| } |
| } |
| } |