Files
OpenList/server/webdav/lock_test.go
cff5597863 fix(webdav)!: correct COPY/MOVE semantics, dead property persistence, and lock enforcement (#3210)
* fix(webdav): correct COPY/MOVE semantics with named copy support

Signed-off-by: Lythen <intro-iu@outlook.com>

* feat(webdav): persist dead properties with MOVE migration

Signed-off-by: Lythen <intro-iu@outlook.com>

* fix(webdav): enforce locks on resolved paths, support shared locks

Signed-off-by: Lythen <intro-iu@outlook.com>

* fix(fs): fall back to transfer task for same-dir copy

Signed-off-by: Lythen <intro-iu@outlook.com>

* fix(webdav): escape LIKE patterns, check dstDir is dir, bound PROPFIND body

Signed-off-by: Lythen <intro-iu@outlook.com>

* fix(webdav): escape backslash in LIKE, reduce dead prop retry

Signed-off-by: Lythen <intro-iu@outlook.com>

* fix(webdav): isolate shared lock instances

- Track each lock token with independent details and expiry state.\n- Keep resource nodes for path conflict accounting only.\n- Cover independent shared-lock expiration in memLS tests.

* fix(webdav): preserve dead property lifecycle

- Copy and replace dead properties with COPY destinations.\n- Remove dead-property subtrees after successful DELETE.\n- Stage MOVE properties before clearing the destination subtree.\n- Add database-backed regression coverage for copy and delete.

* fix(webdav): protect overwrite copy and move

- Validate source objects before touching overwrite destinations.\n- Stage COPY content under a collision-free name and restore destinations on failure.\n- Preserve MOVE destinations with a backup until the resource operation succeeds.\n- Route named same-storage copies through the direct destination-name transfer path.

* fix(webdav): harden overwrite staging

- Skip NoOverwriteUpload backup handling for collision-free internal COPY staging.\n- Treat post-commit overwrite backup cleanup failures as warnings.

* test(webdav): update lock lookup expectations

- Compare lookup results with independent lock instances.\n- Keep invalid token expectations nil after the lock state split.

* fix(webdav): complete litmus compliance

- Preserve named same-storage collection copies with isolated native staging.\n- Evaluate WebDAV ETag conditions and distinguish 412 from 423 lock failures.\n- Return 201 for lock-null resources and cover the new condition behavior.

* fix(fs): harden named copy staging

- retry staging cleanup from the deferred path and report residual directories

- document hook context semantics and wrap staging operation errors

Signed-off-by: Lythen <intro-iu@outlook.com>

* fix(core): support Go 1.27 checks

- Replace dynamic format strings with explicit format verbs and guard optional network configuration.

- Stabilize multipart cleanup, proxy, search, and MCP tests.

Co-authored-by: Codex <267193182+codex@users.noreply.github.com>

* fix(webdav): coordinate file and property transactions

- Keep MOVE, COPY, and DELETE file changes coordinated with dead-property transactions.

- Compensate filesystem changes when file operations or database commits fail.

- Add WebDAV coverage for transaction and recovery failures.

Co-authored-by: Codex <267193182+codex@users.noreply.github.com>

* fix(webdav): reject writes without parent collections

- Require an existing parent collection for PUT.

- Reject MKCOL when its parent is missing or not a collection.

Co-authored-by: Codex <267193182+codex@users.noreply.github.com>

* fix(webdav): refresh parent before delete checks

- Refresh the parent listing before checking whether a DELETE target exists.

- Return 404 for missing resources, including storage-not-found errors.

Co-authored-by: Codex <267193182+codex@users.noreply.github.com>

---------

Signed-off-by: Lythen <intro-iu@outlook.com>
Co-authored-by: Intro <Intro-iu@outlook.com>
Co-authored-by: Codex <267193182+codex@users.noreply.github.com>
2026-10-20 11:12:10 +08:00

784 lines
19 KiB
Go

// 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)) {
for token := range base.locks {
goodToken = token
break
}
}
for _, token := range []string{badToken, goodToken} {
if token == "" {
continue
}
got := m.lookup(name, Condition{Token: token})
var want *memLSLock
if token != badToken {
want = m.byToken[token]
}
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 TestMemLSSharedLockExpiry(t *testing.T) {
m := NewMemLS().(*memLS)
now := time.Unix(0, 0)
first, err := m.Create(now, LockDetails{Root: "/shared", Duration: infiniteTimeout, ZeroDepth: true, Shared: true})
if err != nil {
t.Fatal(err)
}
second, err := m.Create(now, LockDetails{Root: "/shared", Duration: 2 * time.Second, ZeroDepth: true, Shared: true})
if err != nil {
t.Fatal(err)
}
third, err := m.Create(now, LockDetails{Root: "/shared", Duration: 4 * time.Second, ZeroDepth: true, Shared: true})
if err != nil {
t.Fatal(err)
}
if got := m.byToken[first].details.Duration; got != infiniteTimeout {
t.Fatalf("first lock duration = %v, want infinite", got)
}
if got := m.byToken[second].details.Duration; got != 2*time.Second {
t.Fatalf("second lock duration = %v, want 2s", got)
}
if got := m.byToken[third].details.Duration; got != 4*time.Second {
t.Fatalf("third lock duration = %v, want 4s", got)
}
m.mu.Lock()
m.collectExpiredNodes(now.Add(2 * time.Second))
m.mu.Unlock()
if m.byToken[second] != nil {
t.Fatal("second shared lock did not expire independently")
}
if m.byToken[first] == nil || m.byToken[third] == nil {
t.Fatal("independent shared lock expired with second lock")
}
if err := m.consistent(); err != nil {
t.Fatal(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) {
for range n0.locks {
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))
}
for token, lock := range n.locks {
if lock.token != token || lock.node != n || m.byToken[token] != lock {
return fmt.Errorf("lock %q at node %q is inconsistent", token, name)
}
}
}
for token, lock := range m.byToken {
if lock.token != token {
return fmt.Errorf("lock token %q != byToken map key %q", lock.token, token)
}
if lock.node == nil || lock.node.locks[token] != lock {
return fmt.Errorf("lock %q is missing from its node", token)
}
if m.byName[lock.details.Root] != lock.node {
return fmt.Errorf("lock %q has inconsistent root %q", token, lock.details.Root)
}
}
for i, lock := range m.byExpiry {
if lock.byExpiryIndex != i {
return fmt.Errorf("lock byExpiryIndex %d != byExpiry slice index %d", lock.byExpiryIndex, i)
}
if m.byToken[lock.token] != lock {
return fmt.Errorf("lock %q is missing from byToken", lock.token)
}
if lock.held {
return fmt.Errorf("lock at name %q is held", lock.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)
}
}
}
func TestHasNegatedTokenCondition(t *testing.T) {
tests := []struct {
name string
conditions []Condition
want bool
}{
{name: "positive token", conditions: []Condition{{Token: "DAV:no-lock"}}},
{name: "negated token", conditions: []Condition{{Not: true, Token: "DAV:no-lock"}}, want: true},
{name: "negated etag", conditions: []Condition{{Not: true, ETag: `"etag"`}}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := hasNegatedTokenCondition(tt.conditions); got != tt.want {
t.Fatalf("hasNegatedTokenCondition(%v) = %t, want %t", tt.conditions, got, tt.want)
}
})
}
}