tailscale/feature/taildrop/send_test.go
kari-ts 5a8b99e977
ipn,ipnlocal,taildrop: use SAF for Android files (#15976)
Create FileOps for calling platform-specific file operations such as SAF APIs in Taildrop
Update taildrop.PutFile to support both traditional and SAF modes

Updates tailscale/tailscale#15263

Signed-off-by: kari-ts <kari@tailscale.com>
2025-05-20 15:30:19 -07:00

129 lines
3.1 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package taildrop
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"testing"
"tailscale.com/tstime"
)
// nopWriteCloser is a no-op io.WriteCloser wrapping a bytes.Buffer.
type nopWriteCloser struct{ *bytes.Buffer }
func (nwc nopWriteCloser) Close() error { return nil }
// mockFileOps implements just enough of the FileOps interface for SAF tests.
type mockFileOps struct {
writes *bytes.Buffer
renameOK bool
}
func (m *mockFileOps) OpenFileWriter(name string) (io.WriteCloser, string, error) {
m.writes = new(bytes.Buffer)
return nopWriteCloser{m.writes}, "uri://" + name + ".partial", nil
}
func (m *mockFileOps) RenamePartialFile(partialPath, dir, finalName string) (string, error) {
if !m.renameOK {
m.renameOK = true
return "uri://" + finalName, nil
}
return "", io.ErrUnexpectedEOF
}
func TestPutFile(t *testing.T) {
const content = "hello, world"
tests := []struct {
name string
mode PutMode
setup func(t *testing.T) (*manager, string, *mockFileOps)
wantFile string
}{
{
name: "PutModeDirect",
mode: PutModeDirect,
setup: func(t *testing.T) (*manager, string, *mockFileOps) {
dir := t.TempDir()
opts := managerOptions{
Logf: t.Logf,
Clock: tstime.DefaultClock{},
State: nil,
Dir: dir,
Mode: PutModeDirect,
DirectFileMode: true,
SendFileNotify: func() {},
}
mgr := opts.New()
return mgr, dir, nil
},
wantFile: "file.txt",
},
{
name: "PutModeAndroidSAF",
mode: PutModeAndroidSAF,
setup: func(t *testing.T) (*manager, string, *mockFileOps) {
// SAF still needs a non-empty Dir to pass the guard.
dir := t.TempDir()
mops := &mockFileOps{}
opts := managerOptions{
Logf: t.Logf,
Clock: tstime.DefaultClock{},
State: nil,
Dir: dir,
Mode: PutModeAndroidSAF,
FileOps: mops,
DirectFileMode: true,
SendFileNotify: func() {},
}
mgr := opts.New()
return mgr, dir, mops
},
wantFile: "file.txt",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
mgr, dir, mops := tc.setup(t)
id := clientID(fmt.Sprint(0))
reader := bytes.NewReader([]byte(content))
n, err := mgr.PutFile(id, "file.txt", reader, 0, int64(len(content)))
if err != nil {
t.Fatalf("PutFile(%s) error: %v", tc.name, err)
}
if n != int64(len(content)) {
t.Errorf("wrote %d bytes; want %d", n, len(content))
}
switch tc.mode {
case PutModeDirect:
path := filepath.Join(dir, tc.wantFile)
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("ReadFile error: %v", err)
}
if got := string(data); got != content {
t.Errorf("file contents = %q; want %q", got, content)
}
case PutModeAndroidSAF:
if mops.writes == nil {
t.Fatal("SAF writer was never created")
}
if got := mops.writes.String(); got != content {
t.Errorf("SAF writes = %q; want %q", got, content)
}
}
})
}
}