package main import ( "bytes" "io" "os" "os/exec" "path/filepath" "strconv" "testing" ) // TestEndToEndRealRsync drives the actual rsync binary through the actual rsh // transport binary into the in-process SSH server, which runs the remote rsync // via /bin/sh. It proves real rsync + the Go transport move files intact. func TestEndToEndRealRsync(t *testing.T) { if testing.Short() { t.Skip("short mode") } rsyncBin, err := exec.LookPath("rsync") if err != nil { t.Skip("rsync not installed") } // Build the rsh binary so rsync can exec it as its remote shell. rshBin := filepath.Join(t.TempDir(), "rsh") if out, err := exec.Command("go", "build", "-buildvcs=false", "-o", rshBin, ".").CombinedOutput(); err != nil { t.Fatalf("build rsh: %v\n%s", err, out) } keyPath, pub := genClientKey(t) remoteRoot := t.TempDir() execute := func(cmd string, stdin io.Reader, stdout, stderr io.Writer) int { return shellExecIn(remoteRoot, cmd, stdin, stdout, stderr) } srv := newTestServer(t, pub, execute) kh := writeKnownHosts(t, "127.0.0.1", srv.port(), srv.hostKey.PublicKey()) src := t.TempDir() // A normal rsync-over-SSH destination may contain shell punctuation. rsync // escapes the remote command before handing it to rsh; this catches any // transport change that loses those escapes while joining the command. marker := filepath.Join(remoteRoot, "injected") dst := filepath.Join(t.TempDir(), "backup path;literal$dollar'quote$(touch injected)") mustWrite(t, filepath.Join(src, "a.txt"), "alpha") mustWrite(t, filepath.Join(src, "sub", "b.bin"), "\x00\x01\x02\x03beta") mustWrite(t, filepath.Join(src, "sub", "c.txt"), "gamma gamma gamma") // These flags are a copy of the vector Rsync.args builds in the app. Nothing // links the two, so changing one without the other leaves this test proving // an argument vector the app no longer runs. runRsync := func(mirror bool) { t.Helper() args := []string{ "-rt", "--out-format=%o %n", "--partial-dir=.rsend-partial", "--timeout=300", "--no-perms", "--no-owner", "--no-group", "--omit-dir-times", "-e", rshBin, } if mirror { args = append(args, "--delete-after") } args = append(args, src+"/", "u@127.0.0.1:"+dst+"/") cmd := exec.Command(rsyncBin, args...) cmd.Env = append(os.Environ(), "RSH_KEY="+keyPath, "RSH_KNOWN_HOSTS="+kh, "RSH_PORT="+strconv.Itoa(srv.port()), ) if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("rsync: %v\n%s", err, out) } } runRsync(false) checkSame(t, src, dst) if _, err := os.Stat(marker); !os.IsNotExist(err) { t.Fatalf("remote path was evaluated by the shell: %s", marker) } // An empty but readable source is a valid mirror. The app intentionally // follows rsync here: every ordinary item in the destination is removed. entries, err := os.ReadDir(src) if err != nil { t.Fatal(err) } for _, entry := range entries { if err := os.RemoveAll(filepath.Join(src, entry.Name())); err != nil { t.Fatal(err) } } runRsync(true) entries, err = os.ReadDir(dst) if err != nil { t.Fatal(err) } if len(entries) != 0 { t.Fatalf("empty mirror left destination entries: %v", entries) } } func mustWrite(t *testing.T, path, content string) { t.Helper() if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(path, []byte(content), 0o644); err != nil { t.Fatal(err) } } func checkSame(t *testing.T, src, dst string) { t.Helper() err := filepath.Walk(src, func(p string, info os.FileInfo, err error) error { if err != nil || info.IsDir() { return err } rel, _ := filepath.Rel(src, p) want, _ := os.ReadFile(p) got, err := os.ReadFile(filepath.Join(dst, rel)) if err != nil { t.Errorf("missing on dest: %s (%v)", rel, err) return nil } if !bytes.Equal(got, want) { t.Errorf("content mismatch for %s", rel) } return nil }) if err != nil { t.Fatal(err) } }