Initial commit: haul — desktop SFTP client
A two-pane local/remote file browser over SSH, split out of dial so file transfer lives in its own tool. The two share ~/.ssh/config as their base. - internal/sshconf: ~/.ssh/config parsing, for the saved-host list. - internal/sshx: dialling and auth — agent, keys, password, host-key verification, ProxyJump. - internal/vfs: the FS interface both sides are driven through, implemented over the local disk and over SFTP. - internal/xfer: recursive copy engine with progress reporting, cancellation, conflict policy, and resume of partial transfers. - internal/ui: the Fyne GUI — connect window (saved hosts plus quick connect) and the two-pane browser session. Build with `make build`; the Makefile locates the Go toolchain at ~/.local/go, which is deliberately kept off PATH. Cross-compiling needs a per-target C toolchain because Fyne is cgo, so `make dist` builds the native binary only — the alternatives are documented in the Makefile. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
package sshconf
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseWithIncludeAndTags(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
incDir := filepath.Join(dir, "conf.d")
|
||||
if err := os.MkdirAll(incDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
main := `# global defaults
|
||||
Host *
|
||||
ServerAliveInterval 60
|
||||
|
||||
# tag: prod, riedel
|
||||
Host web1 web2
|
||||
HostName 10.0.0.5
|
||||
User deploy
|
||||
Port 2222
|
||||
ProxyJump bastion
|
||||
|
||||
Include conf.d/*.conf
|
||||
|
||||
Host bastion
|
||||
HostName bastion.example.com
|
||||
User admin
|
||||
`
|
||||
inc := `# tag: staging
|
||||
Host stage
|
||||
HostName stage.example.com
|
||||
User dev = ignored
|
||||
Host = eqform
|
||||
HostName eq.example.com
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, "config"), []byte(main), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(incDir, "a.conf"), []byte(inc), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
hosts, err := Parse(filepath.Join(dir, "config"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
byAlias := map[string]Host{}
|
||||
for _, h := range hosts {
|
||||
byAlias[h.Alias] = h
|
||||
}
|
||||
|
||||
// Wildcard "Host *" must not appear as a connectable target.
|
||||
if _, ok := byAlias["*"]; ok {
|
||||
t.Error("wildcard host * should be skipped")
|
||||
}
|
||||
|
||||
// Multiple aliases in one block share settings and tags.
|
||||
for _, a := range []string{"web1", "web2"} {
|
||||
h, ok := byAlias[a]
|
||||
if !ok {
|
||||
t.Fatalf("missing host %s", a)
|
||||
}
|
||||
if h.HostName != "10.0.0.5" || h.User != "deploy" || h.Port != "2222" || h.ProxyJump != "bastion" {
|
||||
t.Errorf("%s wrong fields: %+v", a, h)
|
||||
}
|
||||
if len(h.Tags) != 2 || h.Tags[0] != "prod" || h.Tags[1] != "riedel" {
|
||||
t.Errorf("%s wrong tags: %v", a, h.Tags)
|
||||
}
|
||||
}
|
||||
|
||||
// Included file's host and its tag.
|
||||
stage, ok := byAlias["stage"]
|
||||
if !ok {
|
||||
t.Fatal("included host 'stage' missing")
|
||||
}
|
||||
if len(stage.Tags) != 1 || stage.Tags[0] != "staging" {
|
||||
t.Errorf("stage tags wrong: %v", stage.Tags)
|
||||
}
|
||||
|
||||
// "Host = eqform" equals-form should parse the alias.
|
||||
if _, ok := byAlias["eqform"]; !ok {
|
||||
t.Error("equals-form Host 'eqform' missing")
|
||||
}
|
||||
|
||||
// Ordering: web1 before included stage before bastion.
|
||||
order := []string{}
|
||||
for _, h := range hosts {
|
||||
order = append(order, h.Alias)
|
||||
}
|
||||
t.Logf("order: %v", order)
|
||||
}
|
||||
|
||||
func TestParseMissingConfig(t *testing.T) {
|
||||
hosts, err := Parse(filepath.Join(t.TempDir(), "does-not-exist"))
|
||||
if err != nil {
|
||||
t.Fatalf("missing config should not error: %v", err)
|
||||
}
|
||||
if hosts != nil {
|
||||
t.Errorf("expected nil hosts, got %v", hosts)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user