[client] Port the file drop bindings to iOS

The iOS SDK package gets the same platform handle the Android bindings
carry: transfers, receiving policy, and per-sender rules on one profile,
attached to the engine when the client starts. Payloads are path-based
rather than stream-based, since the app group container is readable from
both processes, and the platform sink is dropped for the same reason:
delivery lands on the filesystem directly. The default destination is
seeded in the constructor because the app process opens handles of its
own while the extension is down.
This commit is contained in:
Zoltán Papp
2026-08-27 12:43:54 +02:00
parent 8a4aafe88a
commit f7da113ac7
6 changed files with 684 additions and 0 deletions
+182
View File
@@ -0,0 +1,182 @@
//go:build ios
package NetBirdSDK
import (
"errors"
"io"
"os"
"path/filepath"
"strings"
"testing"
"github.com/netbirdio/netbird/client/internal/filedrop"
)
func TestPayloadFileReadsFromPath(t *testing.T) {
path := filepath.Join(t.TempDir(), "greeting.txt")
if err := os.WriteFile(path, []byte("hello world"), 0o600); err != nil {
t.Fatalf("write payload: %v", err)
}
payloads := NewFileDropPayloads()
if err := payloads.AddFile("greeting.txt", 11, "text/plain", path); err != nil {
t.Fatalf("AddFile: %v", err)
}
if payloads.Length() != 1 {
t.Fatalf("expected 1 payload, got %d", payloads.Length())
}
stream, err := payloads.items[0].Open(0)
if err != nil {
t.Fatalf("Open: %v", err)
}
got, err := io.ReadAll(stream)
if err != nil {
t.Fatalf("ReadAll: %v", err)
}
if string(got) != "hello world" {
t.Fatalf("got %q, want %q", got, "hello world")
}
if err := stream.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
}
func TestPayloadFileHonoursOffset(t *testing.T) {
path := filepath.Join(t.TempDir(), "greeting.txt")
if err := os.WriteFile(path, []byte("hello world"), 0o600); err != nil {
t.Fatalf("write payload: %v", err)
}
payloads := NewFileDropPayloads()
if err := payloads.AddFile("greeting.txt", 11, "", path); err != nil {
t.Fatalf("AddFile: %v", err)
}
stream, err := payloads.items[0].Open(6)
if err != nil {
t.Fatalf("Open: %v", err)
}
defer stream.Close()
got, err := io.ReadAll(stream)
if err != nil {
t.Fatalf("ReadAll: %v", err)
}
if string(got) != "world" {
t.Fatalf("got %q, want %q", got, "world")
}
}
func TestPayloadRejectsMissingPathAndOversizedText(t *testing.T) {
payloads := NewFileDropPayloads()
if err := payloads.AddFile("no-path.bin", 1, "", ""); err == nil {
t.Fatal("expected an error for a file without a path")
}
if err := payloads.AddFile("", 1, "", "/tmp/x"); err == nil {
t.Fatal("expected an error for an empty file name")
}
if err := payloads.AddText("big", strings.Repeat("x", filedrop.MaxInlineTextSize+1)); err == nil {
t.Fatal("expected an error for oversized text")
}
if payloads.Length() != 0 {
t.Fatalf("expected no payloads, got %d", payloads.Length())
}
}
func TestFileDropPersistsSettingsPerProfile(t *testing.T) {
configDir := t.TempDir()
writeTestProfile(t, configDir, "aaaaaaaabbbbbbbbccccccccdddddddd")
writeTestProfile(t, configDir, "11111111222222223333333344444444")
first, err := NewFileDrop(configDir, "aaaaaaaabbbbbbbbccccccccdddddddd")
if err != nil {
t.Fatalf("NewFileDrop: %v", err)
}
defer first.Close()
if err := first.SetMode(FileDropModeAutoAccept); err != nil {
t.Fatalf("SetMode: %v", err)
}
if err := first.SetPeerRule("peer-key", FileDropRuleBlock); err != nil {
t.Fatalf("SetPeerRule: %v", err)
}
second, err := NewFileDrop(configDir, "11111111222222223333333344444444")
if err != nil {
t.Fatalf("NewFileDrop: %v", err)
}
defer second.Close()
if got := second.Mode(); got != FileDropModeAsk {
t.Fatalf("second profile mode = %d, want the default %d", got, FileDropModeAsk)
}
if got := second.PeerRule("peer-key"); got != FileDropRuleDefault {
t.Fatalf("second profile rule = %d, want %d", got, FileDropRuleDefault)
}
reopened, err := NewFileDrop(configDir, "aaaaaaaabbbbbbbbccccccccdddddddd")
if err != nil {
t.Fatalf("NewFileDrop: %v", err)
}
defer reopened.Close()
if got := reopened.Mode(); got != FileDropModeAutoAccept {
t.Fatalf("reopened mode = %d, want %d", got, FileDropModeAutoAccept)
}
if got := reopened.PeerRule("peer-key"); got != FileDropRuleBlock {
t.Fatalf("reopened rule = %d, want %d", got, FileDropRuleBlock)
}
}
func TestFileDropSeedsDefaultDestination(t *testing.T) {
configDir := t.TempDir()
writeTestProfile(t, configDir, "aaaaaaaabbbbbbbbccccccccdddddddd")
fd, err := NewFileDrop(configDir, "aaaaaaaabbbbbbbbccccccccdddddddd")
if err != nil {
t.Fatalf("NewFileDrop: %v", err)
}
defer fd.Close()
want := filepath.Join(configDir, filedropDataSubdir, "aaaaaaaabbbbbbbbccccccccdddddddd", "incoming")
if got := fd.DestinationDir(); got != want {
t.Fatalf("destination = %q, want %q", got, want)
}
}
func TestFileDropSendWithoutTunnelFails(t *testing.T) {
configDir := t.TempDir()
writeTestProfile(t, configDir, "aaaaaaaabbbbbbbbccccccccdddddddd")
fd, err := NewFileDrop(configDir, "aaaaaaaabbbbbbbbccccccccdddddddd")
if err != nil {
t.Fatalf("NewFileDrop: %v", err)
}
defer fd.Close()
payloads := NewFileDropPayloads()
if err := payloads.AddText("note", "hi"); err != nil {
t.Fatalf("AddText: %v", err)
}
if _, err := fd.Send("peer-key", "peer", "100.64.0.2", payloads); !errors.Is(err, filedrop.ErrNotConnected) {
t.Fatalf("Send error = %v, want %v", err, filedrop.ErrNotConnected)
}
if _, err := fd.Send("peer-key", "peer", "100.64.0.2", NewFileDropPayloads()); err == nil {
t.Fatal("expected an error when there is nothing to send")
}
if _, err := fd.Send("peer-key", "peer", "not-an-ip", payloads); err == nil {
t.Fatal("expected an error for an unparseable peer address")
}
}
func writeTestProfile(t *testing.T, configDir, id string) {
t.Helper()
if _, err := NewProfileManager(configDir).impl.ProfilePrefs(id); err != nil {
t.Fatalf("resolve prefs for %s: %v", id, err)
}
}