tailscale/feature/taildrop/integration_test.go
Brad Fitzpatrick 49a7685af9 feature/taildrop: add integration test variant with profiles that exist
Updates #15970
Updates #15812
Updates tailscale/corp#28449

Change-Id: I52cf25f98636b0beac16275f46e58d0816963895
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2025-05-14 13:37:06 -05:00

198 lines
4.7 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package taildrop_test
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"testing"
"time"
"tailscale.com/client/local"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/tailcfg"
"tailscale.com/tstest"
"tailscale.com/tstest/integration"
"tailscale.com/tstest/integration/testcontrol"
)
// TODO(bradfitz): add test where control doesn't send tailcfg.CapabilityFileSharing
// and verify that we get the "file sharing not enabled by Tailscale admin" error.
// TODO(bradfitz): add test between different users with the peercap to permit that?
func TestTaildropIntegration(t *testing.T) {
t.Skip("known failing test; see https://github.com/tailscale/tailscale/issues/15970")
testTaildropIntegration(t, false)
}
func TestTaildropIntegration_Fresh(t *testing.T) {
testTaildropIntegration(t, true)
}
// freshProfiles is whether to start the test right away
// with a fresh profile. If false, tailscaled is started, stopped,
// and restarted again to simulate a real-world scenario where
// the first profile already existed.
//
// This exercises an ipnext hook ordering issue we hit earlier.
func testTaildropIntegration(t *testing.T, freshProfiles bool) {
tstest.Parallel(t)
controlOpt := integration.ConfigureControl(func(s *testcontrol.Server) {
s.AllNodesSameUser = true // required for Taildrop
})
env := integration.NewTestEnv(t, controlOpt)
// Create two nodes:
n1 := integration.NewTestNode(t, env)
d1 := n1.StartDaemon()
n2 := integration.NewTestNode(t, env)
d2 := n2.StartDaemon()
awaitUp := func() {
t.Helper()
n1.AwaitListening()
t.Logf("n1 is listening")
n2.AwaitListening()
t.Logf("n2 is listening")
n1.MustUp()
t.Logf("n1 is up")
n2.MustUp()
t.Logf("n2 is up")
n1.AwaitRunning()
t.Logf("n1 is running")
n2.AwaitRunning()
t.Logf("n2 is running")
}
awaitUp()
if !freshProfiles {
d1.MustCleanShutdown(t)
d2.MustCleanShutdown(t)
d1 = n1.StartDaemon()
d2 = n2.StartDaemon()
awaitUp()
}
var peerStableID tailcfg.StableNodeID
if err := tstest.WaitFor(5*time.Second, func() error {
st := n1.MustStatus()
if len(st.Peer) == 0 {
return errors.New("no peers")
}
if len(st.Peer) > 1 {
return fmt.Errorf("got %d peers; want 1", len(st.Peer))
}
peer := st.Peer[st.Peers()[0]]
peerStableID = peer.ID
if peer.ID == st.Self.ID {
return errors.New("peer is self")
}
if len(st.TailscaleIPs) == 0 {
return errors.New("no Tailscale IPs")
}
return nil
}); err != nil {
t.Fatal(err)
}
const timeout = 30 * time.Second
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
c1 := n1.LocalClient()
c2 := n2.LocalClient()
wantNoWaitingFiles := func(c *local.Client) {
t.Helper()
files, err := c.WaitingFiles(ctx)
if err != nil {
t.Fatalf("WaitingFiles: %v", err)
}
if len(files) != 0 {
t.Fatalf("WaitingFiles: got %d files; want 0", len(files))
}
}
// Verify c2 has no files.
wantNoWaitingFiles(c2)
gotFile := make(chan bool, 1)
go func() {
v, err := c2.AwaitWaitingFiles(t.Context(), timeout)
if err != nil {
return
}
if len(v) != 0 {
gotFile <- true
}
}()
fileContents := []byte("hello world this is a file")
n2ID := n2.MustStatus().Self.ID
t.Logf("n2 self.ID = %q; n1's peer[0].ID = %q", n2ID, peerStableID)
t.Logf("Doing PushFile ...")
err := c1.PushFile(ctx, n2.MustStatus().Self.ID, int64(len(fileContents)), "test.txt", bytes.NewReader(fileContents))
if err != nil {
t.Fatalf("PushFile from n1->n2: %v", err)
}
t.Logf("PushFile done")
select {
case <-gotFile:
t.Logf("n2 saw AwaitWaitingFiles wake up")
case <-ctx.Done():
t.Fatalf("n2 timeout waiting for AwaitWaitingFiles")
}
files, err := c2.WaitingFiles(ctx)
if err != nil {
t.Fatalf("c2.WaitingFiles: %v", err)
}
if len(files) != 1 {
t.Fatalf("c2.WaitingFiles: got %d files; want 1", len(files))
}
got := files[0]
want := apitype.WaitingFile{
Name: "test.txt",
Size: int64(len(fileContents)),
}
if got != want {
t.Fatalf("c2.WaitingFiles: got %+v; want %+v", got, want)
}
// Download the file.
rc, size, err := c2.GetWaitingFile(ctx, got.Name)
if err != nil {
t.Fatalf("c2.GetWaitingFile: %v", err)
}
if size != int64(len(fileContents)) {
t.Fatalf("c2.GetWaitingFile: got size %d; want %d", size, len(fileContents))
}
gotBytes, err := io.ReadAll(rc)
if err != nil {
t.Fatalf("c2.GetWaitingFile: %v", err)
}
if !bytes.Equal(gotBytes, fileContents) {
t.Fatalf("c2.GetWaitingFile: got %q; want %q", gotBytes, fileContents)
}
// Now delete it.
if err := c2.DeleteWaitingFile(ctx, got.Name); err != nil {
t.Fatalf("c2.DeleteWaitingFile: %v", err)
}
wantNoWaitingFiles(c2)
d1.MustCleanShutdown(t)
d2.MustCleanShutdown(t)
}