From d3e97d1758e0d6c204a0f0793fa8fc8d7defd14a Mon Sep 17 00:00:00 2001 From: Chris Bednarski Date: Fri, 28 Feb 2020 09:51:07 -0800 Subject: [PATCH] Fix argument parsing for CLI Add tests for main and commands Fix documentation for installation so the program works with sudo --- Makefile | 3 +- README.md | 4 +- hostess/hostfile.go | 4 +- hostess/hostlist.go | 2 + hostess/{test-fixtures => testdata}/hostfile1 | 0 hostess/{test-fixtures => testdata}/hostfile2 | 0 main.go | 36 ++-- main_test.go | 172 ++++++++++++++++++ testdata/ubuntu.hosts | 10 + 9 files changed, 210 insertions(+), 21 deletions(-) rename hostess/{test-fixtures => testdata}/hostfile1 (100%) rename hostess/{test-fixtures => testdata}/hostfile2 (100%) create mode 100644 main_test.go create mode 100644 testdata/ubuntu.hosts diff --git a/Makefile b/Makefile index d207600..6e4db28 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,8 @@ test: go vet ./... install: - go install . + go build -o bin/hostess . + sudo mv bin/hostess /usr/local/bin/hostess release: test GOOS=windows GOARCH=amd64 go build -ldflags "-X main.Version=${RELEASE_VERSION}" -o bin/hostess_windows_amd64.exe . diff --git a/README.md b/README.md index 73f0331..0ce6e7b 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,9 @@ and call it a day. Download a [precompiled release](https://github.com/cbednarski/hostess/releases) from GitHub, or build from source (with a [recent version of Go](https://golang.org/dl)): - go get -u github.com/cbednarski/hostess + git clone https://github.com/cbednarski/hostess + cd hostess + make install ## Usage diff --git a/hostess/hostfile.go b/hostess/hostfile.go index df238d0..d423390 100644 --- a/hostess/hostfile.go +++ b/hostess/hostfile.go @@ -10,6 +10,8 @@ import ( "strings" ) +const EnvHostessPath = `HOSTESS_PATH` + const defaultOSX = ` ## # Host Database @@ -53,7 +55,7 @@ func NewHostfile() *Hostfile { // GetHostsPath returns the location of the hostfile; either env HOSTESS_PATH // or /etc/hosts if HOSTESS_PATH is not set. func GetHostsPath() string { - path := os.Getenv("HOSTESS_PATH") + path := os.Getenv(EnvHostessPath) if path == "" { if runtime.GOOS == "windows" { path = "C:\\Windows\\System32\\drivers\\etc\\hosts" diff --git a/hostess/hostlist.go b/hostess/hostlist.go index 392bd56..93cfe24 100644 --- a/hostess/hostlist.go +++ b/hostess/hostlist.go @@ -9,6 +9,8 @@ import ( "strings" ) +const EnvHostessFmt = `HOSTESS_FMT` + // ErrInvalidVersionArg is raised when a function expects IPv 4 or 6 but is // passed a value not 4 or 6. var ErrInvalidVersionArg = errors.New("version argument must be 4 or 6") diff --git a/hostess/test-fixtures/hostfile1 b/hostess/testdata/hostfile1 similarity index 100% rename from hostess/test-fixtures/hostfile1 rename to hostess/testdata/hostfile1 diff --git a/hostess/test-fixtures/hostfile2 b/hostess/testdata/hostfile2 similarity index 100% rename from hostess/test-fixtures/hostfile2 rename to hostess/testdata/hostfile2 diff --git a/main.go b/main.go index bb9f13b..760745d 100644 --- a/main.go +++ b/main.go @@ -65,8 +65,8 @@ func CommandUsage(command string) error { return fmt.Errorf("Usage: %s %s ", os.Args[0], command) } -func wrappedMain() error { - cli := flag.NewFlagSet(os.Args[0], flag.ExitOnError) +func wrappedMain(args []string) error { + cli := flag.NewFlagSet(args[0], flag.ExitOnError) ipv4 := cli.Bool("4", false, "IPv4") ipv6 := cli.Bool("6", false, "IPv6") preview := cli.Bool("n", false, "preview") @@ -74,7 +74,7 @@ func wrappedMain() error { fmt.Printf(help, hostess.GetHostsPath()) } - if err := cli.Parse(os.Args[1:]); err != nil { + if err := cli.Parse(args[1:]); err != nil { return err } @@ -96,7 +96,7 @@ func wrappedMain() error { fmt.Println(Version) return nil - case "-h", "--help", "help": + case "", "-h", "--help", "help": cli.Usage() return nil @@ -104,46 +104,46 @@ func wrappedMain() error { return Format(options) case "add": - if len(cli.Args()) != 2 { + if len(cli.Args()) != 3 { return fmt.Errorf("Usage: %s add ", cli.Name()) } - return Add(options, cli.Arg(0), cli.Arg(1)) + return Add(options, cli.Arg(1), cli.Arg(2)) case "rm": - if cli.Arg(0) == "" { + if cli.Arg(1) == "" { return CommandUsage(command) } - return Remove(options, cli.Arg(0)) + return Remove(options, cli.Arg(1)) case "on": - if cli.Arg(0) == "" { + if cli.Arg(1) == "" { return CommandUsage(command) } - return Enable(options, cli.Arg(0)) + return Enable(options, cli.Arg(1)) case "off": - if cli.Arg(0) == "" { + if cli.Arg(1) == "" { return CommandUsage(command) } - return Disable(options, cli.Arg(0)) + return Disable(options, cli.Arg(1)) case "ls": return List(options) case "has": - if cli.Arg(0) == "" { + if cli.Arg(1) == "" { return CommandUsage(command) } - return Has(options, cli.Arg(0)) + return Has(options, cli.Arg(1)) case "dump": return Dump(options) case "apply": - if cli.Arg(0) == "" { - return fmt.Errorf("Usage: %s apply ", os.Args[0]) + if cli.Arg(1) == "" { + return fmt.Errorf("Usage: %s apply ", args[0]) } - return Apply(options, cli.Arg(0)) + return Apply(options, cli.Arg(1)) default: return ErrInvalidCommand @@ -151,5 +151,5 @@ func wrappedMain() error { } func main() { - ExitWithError(wrappedMain()) + ExitWithError(wrappedMain(os.Args)) } diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..ab9d046 --- /dev/null +++ b/main_test.go @@ -0,0 +1,172 @@ +package main + +import ( + "io" + "io/ioutil" + "os" + "strings" + "testing" + + "github.com/cbednarski/hostess/hostess" +) + +// CopyHostsFile creates a temporary hosts file in the system temp directory, +// sets the HOSTESS_PATH environment variable, and returns the file path and a +// cleanup function +func CopyHostsFile(t *testing.T) (string, func()) { + t.Helper() + + fixture, err := os.Open("testdata/ubuntu.hosts") + if err != nil { + t.Fatal(err) + } + + temp, err := ioutil.TempFile("", "hostess-test-*") + if err != nil { + t.Fatal(err) + } + + if _, err := io.Copy(temp, fixture); err != nil { + t.Fatal(err) + } + + if err := os.Setenv(hostess.EnvHostessPath, temp.Name()); err != nil { + t.Fatal(err) + } + + cleanup := func() { + os.Remove(temp.Name()) + } + + return temp.Name(), cleanup +} + +func TestFormat(t *testing.T) { + temp, cleanup := CopyHostsFile(t) + defer cleanup() + + if err := wrappedMain(strings.Split("hostess fmt", " ")); err != nil { + t.Fatal(err) + } + + data, err := ioutil.ReadFile(temp) + if err != nil { + t.Fatal(err) + } + output := string(data) + + expected := `127.0.0.1 localhost myapp.local +127.0.1.1 ubuntu +192.168.0.30 raspberrypi +::1 ip6-localhost ip6-loopback +fe00:: ip6-localnet +ff00:: ip6-mcastprefix +ff02::1 ip6-allnodes +ff02::2 ip6-allrouters +` + + if output != expected { + t.Errorf("--- Expected ---\n%s\n--- Found ---\n%s\n", expected, output) + } +} + +func TestAddHostname(t *testing.T) { + temp, cleanup := CopyHostsFile(t) + defer cleanup() + + if err := wrappedMain(strings.Split("hostess add my.new.website 127.0.0.1", " ")); err != nil { + t.Fatal(err) + } + if err := wrappedMain(strings.Split("hostess add mediaserver 192.168.0.82", " ")); err != nil { + t.Fatal(err) + } + if err := wrappedMain(strings.Split("hostess add myapp.local 10.20.0.23", " ")); err != nil { + t.Fatal(err) + } + + data, err := ioutil.ReadFile(temp) + if err != nil { + t.Fatal(err) + } + output := string(data) + + expected := `127.0.0.1 localhost my.new.website +127.0.1.1 ubuntu +10.20.0.23 myapp.local +192.168.0.30 raspberrypi +192.168.0.82 mediaserver +::1 ip6-localhost ip6-loopback +fe00:: ip6-localnet +ff00:: ip6-mcastprefix +ff02::1 ip6-allnodes +ff02::2 ip6-allrouters +` + + if output != expected { + t.Errorf("--- Expected ---\n%s\n--- Found ---\n%s\n", expected, output) + } +} + +func TestRemoveHostname(t *testing.T) { + temp, cleanup := CopyHostsFile(t) + defer cleanup() + + if err := wrappedMain(strings.Split("hostess rm myapp.local", " ")); err != nil { + t.Fatal(err) + } + if err := wrappedMain(strings.Split("hostess rm raspberrypi", " ")); err != nil { + t.Fatal(err) + } + + data, err := ioutil.ReadFile(temp) + if err != nil { + t.Fatal(err) + } + output := string(data) + + expected := `127.0.0.1 localhost +127.0.1.1 ubuntu +::1 ip6-localhost ip6-loopback +fe00:: ip6-localnet +ff00:: ip6-mcastprefix +ff02::1 ip6-allnodes +ff02::2 ip6-allrouters +` + + if output != expected { + t.Errorf("--- Expected ---\n%s\n--- Found ---\n%s\n", expected, output) + } +} + +func TestHostnameOff(t *testing.T) { + temp, cleanup := CopyHostsFile(t) + defer cleanup() + + if err := wrappedMain(strings.Split("hostess off myapp.local", " ")); err != nil { + t.Fatal(err) + } + if err := wrappedMain(strings.Split("hostess off raspberrypi", " ")); err != nil { + t.Fatal(err) + } + + data, err := ioutil.ReadFile(temp) + if err != nil { + t.Fatal(err) + } + output := string(data) + + expected := `127.0.0.1 localhost +# 127.0.0.1 myapp.local +127.0.1.1 ubuntu +# 192.168.0.30 raspberrypi +::1 ip6-localhost ip6-loopback +fe00:: ip6-localnet +ff00:: ip6-mcastprefix +ff02::1 ip6-allnodes +ff02::2 ip6-allrouters +` + + if output != expected { + t.Errorf("--- Expected ---\n%s\n--- Found ---\n%s\n", expected, output) + } +} diff --git a/testdata/ubuntu.hosts b/testdata/ubuntu.hosts new file mode 100644 index 0000000..87397f8 --- /dev/null +++ b/testdata/ubuntu.hosts @@ -0,0 +1,10 @@ +127.0.0.1 localhost myapp.local +127.0.1.1 ubuntu +192.168.0.30 raspberrypi + +# The following lines are desirable for IPv6 capable hosts +::1 ip6-localhost ip6-loopback +fe00::0 ip6-localnet +ff00::0 ip6-mcastprefix +ff02::1 ip6-allnodes +ff02::2 ip6-allrouters