// Copyright (c) Tailscale Inc & contributors // SPDX-License-Identifier: BSD-3-Clause package driveprobe import ( "context" "fmt" "net/http" "net/http/httptest" "strings" "sync" "sync/atomic" "testing" "time" ) func multistatus(hrefs ...string) string { var b strings.Builder b.WriteString(``) for _, h := range hrefs { fmt.Fprintf(&b, `%sHTTP/1.1 200 OK`, h) } b.WriteString(``) return b.String() } func TestHasChild(t *testing.T) { tests := []struct { name string body string want bool }{ {"root only, prefix stripped", multistatus("/"), false}, {"root only, prefix kept", multistatus("/v0/drive/"), false}, {"root only, no trailing slash", multistatus("/v0/drive"), false}, {"one share, prefix stripped", multistatus("/", "/docs"), true}, {"one share, prefix kept", multistatus("/v0/drive/", "/v0/drive/docs"), true}, {"absolute urls", multistatus("http://100.1.2.3:1234/v0/drive/", "http://100.1.2.3:1234/v0/drive/docs"), true}, {"percent-encoded share name", multistatus("/v0/drive/", "/v0/drive/my%20share"), true}, {"unicode share name", multistatus("/v0/drive/", "/v0/drive/%E6%97%A5%E6%9C%AC"), true}, {"empty multistatus", multistatus(), false}, // The collection comes first per RFC 4918 §9.1, so anything after it // is a share whatever the peer names it. {"unrelated collection href, no members", multistatus("/somewhere/else/"), false}, {"unrelated collection href with a member", multistatus("/somewhere/else/", "/somewhere/else/docs"), true}, // A peer that omits the collection from a Depth-1 listing violates // RFC 4918 §9.1, and once the taildrive prefix is stripped there is // nothing left to tell its lone member apart from the collection. It // loses the benefit of the doubt: hasShares excludes what it cannot // confirm. {"single member, collection omitted", multistatus("/docs"), false}, {"href split by an entity reference", multistatus("/v0/drive/", "/v0/drive/a&b"), true}, {"empty href", multistatus("/v0/drive/", ""), true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := hasChild(strings.NewReader(tt.body), drivePath) if err != nil { t.Fatalf("hasChild: %v", err) } if got != tt.want { t.Errorf("hasChild = %v, want %v", got, tt.want) } }) } } func TestHasChildMalformed(t *testing.T) { body := strings.TrimSuffix(multistatus("/v0/drive/", "/v0/drive/docs"), "") // Truncation after a child href still answers the question. got, err := hasChild(strings.NewReader(body), drivePath) if err != nil { t.Fatalf("hasChild: %v", err) } if !got { t.Error("hasChild = false on a truncated body that already listed a share") } if _, err := hasChild(strings.NewReader("= delay*time.Duration(len(urls)) { t.Errorf("probes serialized: %v for %d probes of %v each", elapsed, len(urls), delay) } if peak.Load() < 2 { t.Errorf("peak concurrency = %d, want >= 2", peak.Load()) } } func TestHasSharesMultiLimitsConcurrency(t *testing.T) { var inFlight, peak atomic.Int32 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { n := inFlight.Add(1) for { old := peak.Load() if n <= old || peak.CompareAndSwap(old, n) { break } } time.Sleep(20 * time.Millisecond) inFlight.Add(-1) w.WriteHeader(http.StatusMultiStatus) w.Write([]byte(multistatus("/v0/drive/"))) })) defer srv.Close() urls := make([]string, maxProbes*3) for i := range urls { urls[i] = srv.URL } client := &http.Client{Transport: &http.Transport{MaxConnsPerHost: 0}} HasSharesMulti(context.Background(), client, urls, func(string, ...any) {}) if peak.Load() > maxProbes { t.Errorf("peak concurrency = %d, want <= %d", peak.Load(), maxProbes) } }