Taildrop has never had an end-to-end test since it was introduced.
This adds a basic one.
It caught two recent refactoring bugs & one from 2022 (0f7da5c7dc).
This is prep for moving the rest of Taildrop out of LocalBackend, so
we can do more refactorings with some confidence.
Updates #15812
Change-Id: I6182e49c5641238af0bfdd9fea1ef0420c112738
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
main
parent
a0d7c81a27
commit
e415f51351
@ -0,0 +1,170 @@ |
||||
// 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) { |
||||
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() |
||||
|
||||
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") |
||||
|
||||
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) |
||||
} |
||||
Loading…
Reference in new issue