[dev_finder] add tests for the `resolve` command

Also there's some code shuffling for better code reuse.

Bug: DX-1402 #comment
Change-Id: I107fa890607817e2ab5bd3e0f9dbf9462ebeed87
diff --git a/cmd/dev_finder/common.go b/cmd/dev_finder/common.go
index 975179f..1a3288e 100644
--- a/cmd/dev_finder/common.go
+++ b/cmd/dev_finder/common.go
@@ -5,6 +5,7 @@
 
 import (
 	"context"
+	"encoding/json"
 	"errors"
 	"flag"
 	"fmt"
@@ -188,3 +189,30 @@
 	// Device domain name. Can be omitted.
 	Domain string `json:"domain,omitempty"`
 }
+
+func (cmd *devFinderCmd) outputNormal(filteredDevices []*fuchsiaDevice, includeDomain bool) error {
+	for _, device := range filteredDevices {
+		if includeDomain {
+			fmt.Fprintf(cmd.Output(), "%v %v\n", device.addr, device.domain)
+		} else {
+			fmt.Fprintf(cmd.Output(), "%v\n", device.addr)
+		}
+	}
+	return nil
+}
+
+func (cmd *devFinderCmd) outputJSON(filteredDevices []*fuchsiaDevice, includeDomain bool) error {
+	jsonOut := jsonOutput{Devices: make([]jsonDevice, 0, len(filteredDevices))}
+
+	for _, device := range filteredDevices {
+		dev := jsonDevice{Addr: device.addr.String()}
+		if includeDomain {
+			dev.Domain = device.domain
+		}
+		jsonOut.Devices = append(jsonOut.Devices, dev)
+	}
+
+	e := json.NewEncoder(cmd.Output())
+	e.SetIndent("", "  ")
+	return e.Encode(jsonOut)
+}
diff --git a/cmd/dev_finder/dev_finder_test.go b/cmd/dev_finder/dev_finder_test.go
index e96641d..6487491 100644
--- a/cmd/dev_finder/dev_finder_test.go
+++ b/cmd/dev_finder/dev_finder_test.go
@@ -30,37 +30,60 @@
 func (m *fakeMDNS) AddWarningHandler(func(net.Addr, error)) {}
 func (m *fakeMDNS) AddErrorHandler(func(error))             {}
 func (m *fakeMDNS) SendTo(mdns.Packet, *net.UDPAddr) error  { return nil }
-func (m *fakeMDNS) Send(mdns.Packet) error                  { return nil }
-func (m *fakeMDNS) Start(context.Context, int) error {
+func (m *fakeMDNS) Send(packet mdns.Packet) error {
 	if m.answer != nil {
-		ifc := net.Interface{}
-		ip := net.IPAddr{IP: net.ParseIP(m.answer.ip)}
-		answers := make([]mdns.Record, len(m.answer.domains))
-		for _, d := range m.answer.domains {
-			data := make([]byte, len(d)+1)
-			data[0] = byte(len(d))
-			copy(data[1:], []byte(d))
-			answers = append(answers, mdns.Record{
-				Class: mdns.IN,
-				Type:  mdns.PTR,
-				Data:  data,
-			})
-		}
-		pkt := mdns.Packet{Answers: answers}
 		go func() {
-			for _, h := range m.handlers {
-				h(ifc, &ip, pkt)
+			ifc := net.Interface{}
+			ip := net.IPAddr{IP: net.ParseIP(m.answer.ip).To4()}
+			for _, q := range packet.Questions {
+				switch {
+				case q.Type == mdns.PTR && q.Class == mdns.IN:
+					// 'list' command
+					answers := make([]mdns.Record, len(m.answer.domains))
+					for _, d := range m.answer.domains {
+						data := make([]byte, len(d)+1)
+						data[0] = byte(len(d))
+						copy(data[1:], []byte(d))
+						answers = append(answers, mdns.Record{
+							Class: mdns.IN,
+							Type:  mdns.PTR,
+							Data:  data,
+						})
+					}
+					pkt := mdns.Packet{Answers: answers}
+					for _, h := range m.handlers {
+						h(ifc, &ip, pkt)
+					}
+				case q.Type == mdns.A && q.Class == mdns.IN:
+					// 'resolve' command
+					answers := make([]mdns.Record, len(m.answer.domains))
+					for _, d := range m.answer.domains {
+						answers = append(answers, mdns.Record{
+							Class:  mdns.IN,
+							Type:   mdns.A,
+							Data:   net.ParseIP(m.answer.ip).To4(),
+							Domain: d,
+						})
+					}
+					pkt := mdns.Packet{Answers: answers}
+					for _, h := range m.handlers {
+						h(ifc, &ip, pkt)
+					}
+				}
 			}
 		}()
 	}
 	return nil
 }
+func (m *fakeMDNS) Start(context.Context, int) error { return nil }
 
 func compareFuchsiaDevices(d1, d2 *fuchsiaDevice) bool {
 	return cmp.Equal(d1.addr, d2.addr) && cmp.Equal(d1.domain, d2.domain)
 }
 
-func TestListFindDevices(t *testing.T) {
+//// Tests for the `list` command.
+
+func TestListDevices(t *testing.T) {
 	// Because mdnsPorts have two ports specified, two MDNS objects are
 	// created. To emulate the case where only one port responds, create
 	// only one fake MDNS object with answers. The other one wouldn't
@@ -70,7 +93,7 @@
 		devFinderCmd: devFinderCmd{
 			mdnsHandler: listMDNSHandler,
 			mdnsPorts:   "5353,5356",
-			timeout:     2000,
+			timeout:     10,
 			newMDNSFunc: func() mdnsInterface {
 				mdnsCount++
 				switch mdnsCount {
@@ -91,27 +114,27 @@
 		},
 	}
 
-	got, err := cmd.findDevices(context.Background())
+	got, err := cmd.listDevices(context.Background())
 	if err != nil {
-		t.Fatalf("findDevices: %v", err)
+		t.Fatalf("listDevices: %v", err)
 	}
 	want := []*fuchsiaDevice{
 		{
-			addr:   net.ParseIP("192.168.0.42"),
+			addr:   net.ParseIP("192.168.0.42").To4(),
 			domain: "some.domain",
 		},
 		{
-			addr:   net.ParseIP("192.168.0.42"),
+			addr:   net.ParseIP("192.168.0.42").To4(),
 			domain: "another.domain",
 		},
 	}
 	if d := cmp.Diff(want, got, cmp.Comparer(compareFuchsiaDevices)); d != "" {
-		t.Errorf("findDevices mismatch: (-want +got):\n%s", d)
+		t.Errorf("listDevices mismatch: (-want +got):\n%s", d)
 	}
 
 }
 
-func TestListFindDevices_domainFilter(t *testing.T) {
+func TestListDevices_domainFilter(t *testing.T) {
 	// Because mdnsPorts have two ports specified, two MDNS objects are
 	// created. To emulate the case where only one port responds, create
 	// only one fake MDNS object with answers. The other one wouldn't
@@ -121,7 +144,7 @@
 		devFinderCmd: devFinderCmd{
 			mdnsHandler: listMDNSHandler,
 			mdnsPorts:   "5353,5356",
-			timeout:     2000,
+			timeout:     10,
 			newMDNSFunc: func() mdnsInterface {
 				mdnsCount++
 				switch mdnsCount {
@@ -143,148 +166,180 @@
 		domainFilter: "some",
 	}
 
-	got, err := cmd.findDevices(context.Background())
+	got, err := cmd.listDevices(context.Background())
 	if err != nil {
-		t.Fatalf("findDevices: %v", err)
+		t.Fatalf("listDevices: %v", err)
 	}
 	want := []*fuchsiaDevice{
 		{
-			addr:   net.ParseIP("192.168.0.42"),
+			addr:   net.ParseIP("192.168.0.42").To4(),
 			domain: "some.domain",
 		},
 	}
 	if d := cmp.Diff(want, got, cmp.Comparer(compareFuchsiaDevices)); d != "" {
-		t.Errorf("findDevices mismatch: (-want +got):\n%s", d)
+		t.Errorf("listDevices mismatch: (-want +got):\n%s", d)
 	}
 
 }
 
-func TestListOutputNormal(t *testing.T) {
+//// Tests for the `resolve` command.
+
+func TestResolveDevices(t *testing.T) {
+	// Because mdnsPorts have two ports specified, two MDNS objects are
+	// created. To emulate the case where only one port responds, create
+	// only one fake MDNS object with answers. The other one wouldn't
+	// respond at all. See the Start() method above.
+	mdnsCount := 0
+	cmd := resolveCmd{
+		devFinderCmd: devFinderCmd{
+			mdnsHandler: resolveMDNSHandler,
+			mdnsPorts:   "5353,5356",
+			timeout:     10,
+			newMDNSFunc: func() mdnsInterface {
+				mdnsCount++
+				switch mdnsCount {
+				case 1:
+					return &fakeMDNS{
+						answer: &fakeAnswer{
+							ip: "192.168.0.42",
+							domains: []string{
+								"some.domain.local",
+								"another.domain.local",
+							},
+						},
+					}
+				default:
+					return &fakeMDNS{}
+				}
+			},
+		},
+	}
+
+	got, err := cmd.resolveDevices(context.Background(), "some.domain")
+	if err != nil {
+		t.Fatalf("resolveDevices: %v", err)
+	}
+	want := []*fuchsiaDevice{
+		{
+			addr:   net.ParseIP("192.168.0.42").To4(),
+			domain: "some.domain.local",
+		},
+	}
+	if d := cmp.Diff(want, got, cmp.Comparer(compareFuchsiaDevices)); d != "" {
+		t.Errorf("resolveDevices mismatch: (-want +got):\n%s", d)
+	}
+
+}
+
+//// Tests for output functions.
+
+func TestOutputNormal(t *testing.T) {
 	devs := []*fuchsiaDevice{
 		{
-			addr: net.ParseIP("123.12.234.23"),
+			addr:   net.ParseIP("123.12.234.23").To4(),
+			domain: "hello.world",
 		},
 		{
-			addr: net.ParseIP("11.22.33.44"),
+			addr:   net.ParseIP("11.22.33.44").To4(),
+			domain: "fuchsia.rocks",
 		},
 	}
-	var buf strings.Builder
-	cmd := listCmd{
-		devFinderCmd: devFinderCmd{output: &buf},
-	}
-	cmd.outputNormal(devs)
 
-	got := buf.String()
-	want := `123.12.234.23
+	{
+		var buf strings.Builder
+		cmd := devFinderCmd{output: &buf}
+
+		cmd.outputNormal(devs, false)
+
+		got := buf.String()
+		want := `123.12.234.23
 11.22.33.44
 `
-	if d := cmp.Diff(want, got); d != "" {
-		t.Errorf("outputNormal mismatch: (-want +got):\n%s", d)
+		if d := cmp.Diff(want, got); d != "" {
+			t.Errorf("outputNormal mismatch: (-want +got):\n%s", d)
+		}
 	}
-}
 
-func TestListOutputNormal_fullInfo(t *testing.T) {
-	devs := []*fuchsiaDevice{
-		{
-			addr:   net.ParseIP("123.12.234.23"),
-			domain: "hello.world",
-		},
-		{
-			addr:   net.ParseIP("11.22.33.44"),
-			domain: "fuchsia.rocks",
-		},
-	}
-	var buf strings.Builder
-	cmd := listCmd{
-		devFinderCmd: devFinderCmd{output: &buf},
-		fullInfo:     true,
-	}
-	cmd.outputNormal(devs)
+	{
+		var buf strings.Builder
+		cmd := devFinderCmd{output: &buf}
+		cmd.outputNormal(devs, true)
 
-	got := buf.String()
-	want := `123.12.234.23 hello.world
+		got := buf.String()
+		want := `123.12.234.23 hello.world
 11.22.33.44 fuchsia.rocks
 `
-	if d := cmp.Diff(want, got); d != "" {
-		t.Errorf("outputNormal mismatch: (-want +got):\n%s", d)
+		if d := cmp.Diff(want, got); d != "" {
+			t.Errorf("outputNormal(includeDomain) mismatch: (-want +got):\n%s", d)
+		}
 	}
+
 }
 
-func TestListOutputJSON(t *testing.T) {
+func TestOutputJSON(t *testing.T) {
 	devs := []*fuchsiaDevice{
 		{
-			addr: net.ParseIP("123.12.234.23"),
-		},
-		{
-			addr: net.ParseIP("11.22.33.44"),
-		},
-	}
-	var buf bytes.Buffer
-	cmd := listCmd{
-		devFinderCmd: devFinderCmd{
-			json:   true,
-			output: &buf,
-		},
-	}
-	cmd.outputJSON(devs)
-
-	var got jsonOutput
-	if err := json.Unmarshal(buf.Bytes(), &got); err != nil {
-		t.Fatalf("json.Unmarshal: %v", err)
-	}
-
-	want := jsonOutput{
-		Devices: []jsonDevice{
-			{Addr: "123.12.234.23"},
-			{Addr: "11.22.33.44"},
-		},
-	}
-
-	if d := cmp.Diff(want, got); d != "" {
-		t.Errorf("outputNormal mismatch: (-want +got):\n%s", d)
-	}
-}
-
-func TestListOutputJSON_fullInfo(t *testing.T) {
-	devs := []*fuchsiaDevice{
-		{
-			addr:   net.ParseIP("123.12.234.23"),
+			addr:   net.ParseIP("123.12.234.23").To4(),
 			domain: "hello.world",
 		},
 		{
-			addr:   net.ParseIP("11.22.33.44"),
+			addr:   net.ParseIP("11.22.33.44").To4(),
 			domain: "fuchsia.rocks",
 		},
 	}
-	var buf bytes.Buffer
-	cmd := listCmd{
-		devFinderCmd: devFinderCmd{
+
+	{
+		var buf bytes.Buffer
+		cmd := devFinderCmd{
 			json:   true,
 			output: &buf,
-		},
-		fullInfo: true,
-	}
-	cmd.outputJSON(devs)
+		}
 
-	var got jsonOutput
-	if err := json.Unmarshal(buf.Bytes(), &got); err != nil {
-		t.Fatalf("json.Unmarshal: %v", err)
-	}
+		cmd.outputJSON(devs, false)
 
-	want := jsonOutput{
-		Devices: []jsonDevice{
-			{
-				Addr:   "123.12.234.23",
-				Domain: "hello.world",
+		var got jsonOutput
+		if err := json.Unmarshal(buf.Bytes(), &got); err != nil {
+			t.Fatalf("json.Unmarshal: %v", err)
+		}
+		want := jsonOutput{
+			Devices: []jsonDevice{
+				{Addr: "123.12.234.23"},
+				{Addr: "11.22.33.44"},
 			},
-			{
-				Addr:   "11.22.33.44",
-				Domain: "fuchsia.rocks",
-			},
-		},
+		}
+		if d := cmp.Diff(want, got); d != "" {
+			t.Errorf("outputNormal mismatch: (-want +got):\n%s", d)
+		}
 	}
 
-	if d := cmp.Diff(want, got); d != "" {
-		t.Errorf("outputNormal mismatch: (-want +got):\n%s", d)
+	{
+		var buf bytes.Buffer
+		cmd := devFinderCmd{
+			json:   true,
+			output: &buf,
+		}
+
+		cmd.outputJSON(devs, true)
+
+		var got jsonOutput
+		if err := json.Unmarshal(buf.Bytes(), &got); err != nil {
+			t.Fatalf("json.Unmarshal: %v", err)
+		}
+
+		want := jsonOutput{
+			Devices: []jsonDevice{
+				{
+					Addr:   "123.12.234.23",
+					Domain: "hello.world",
+				},
+				{
+					Addr:   "11.22.33.44",
+					Domain: "fuchsia.rocks",
+				},
+			},
+		}
+		if d := cmp.Diff(want, got); d != "" {
+			t.Errorf("outputNormal(includeDomain) mismatch: (-want +got):\n%s", d)
+		}
 	}
 }
diff --git a/cmd/dev_finder/list.go b/cmd/dev_finder/list.go
index c11ee01..248e8cd 100644
--- a/cmd/dev_finder/list.go
+++ b/cmd/dev_finder/list.go
@@ -6,7 +6,6 @@
 
 import (
 	"context"
-	"encoding/json"
 	"flag"
 	"fmt"
 	"log"
@@ -77,7 +76,7 @@
 	}
 }
 
-func (cmd *listCmd) findDevices(ctx context.Context) ([]*fuchsiaDevice, error) {
+func (cmd *listCmd) listDevices(ctx context.Context) ([]*fuchsiaDevice, error) {
 	listPacket := mdns.Packet{
 		Header: mdns.Header{QDCount: 1},
 		Questions: []mdns.Question{
@@ -106,42 +105,15 @@
 }
 
 func (cmd *listCmd) execute(ctx context.Context) error {
-	filteredDevices, err := cmd.findDevices(ctx)
+	filteredDevices, err := cmd.listDevices(ctx)
 	if err != nil {
 		return err
 	}
 
 	if cmd.json {
-		return cmd.outputJSON(filteredDevices)
+		return cmd.outputJSON(filteredDevices, cmd.fullInfo)
 	}
-	return cmd.outputNormal(filteredDevices)
-}
-
-func (cmd *listCmd) outputNormal(filteredDevices []*fuchsiaDevice) error {
-	for _, device := range filteredDevices {
-		if cmd.fullInfo {
-			fmt.Fprintf(cmd.Output(), "%v %v\n", device.addr, device.domain)
-		} else {
-			fmt.Fprintf(cmd.Output(), "%v\n", device.addr)
-		}
-	}
-	return nil
-}
-
-func (cmd *listCmd) outputJSON(filteredDevices []*fuchsiaDevice) error {
-	jsonOut := jsonOutput{Devices: make([]jsonDevice, 0, len(filteredDevices))}
-
-	for _, device := range filteredDevices {
-		dev := jsonDevice{Addr: device.addr.String()}
-		if cmd.fullInfo {
-			dev.Domain = device.domain
-		}
-		jsonOut.Devices = append(jsonOut.Devices, dev)
-	}
-
-	e := json.NewEncoder(cmd.Output())
-	e.SetIndent("", "  ")
-	return e.Encode(jsonOut)
+	return cmd.outputNormal(filteredDevices, cmd.fullInfo)
 }
 
 func (cmd *listCmd) Execute(ctx context.Context, _ *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
diff --git a/cmd/dev_finder/resolve.go b/cmd/dev_finder/resolve.go
index 7b82550..c5f8796 100644
--- a/cmd/dev_finder/resolve.go
+++ b/cmd/dev_finder/resolve.go
@@ -5,13 +5,11 @@
 
 import (
 	"context"
-	"encoding/json"
 	"errors"
 	"flag"
 	"fmt"
 	"log"
 	"net"
-	"os"
 
 	"github.com/google/subcommands"
 
@@ -60,9 +58,9 @@
 	}
 }
 
-func (cmd *resolveCmd) execute(ctx context.Context, domains ...string) error {
+func (cmd *resolveCmd) resolveDevices(ctx context.Context, domains ...string) ([]*fuchsiaDevice, error) {
 	if len(domains) == 0 {
-		return errors.New("no domains supplied")
+		return nil, errors.New("no domains supplied")
 	}
 
 	var outDevices []*fuchsiaDevice
@@ -70,7 +68,7 @@
 		mDNSDomain := fmt.Sprintf("%s.local", domain)
 		devices, err := cmd.sendMDNSPacket(ctx, mdns.QuestionPacket(mDNSDomain))
 		if err != nil {
-			return fmt.Errorf("sending/receiving mdns packets during resolve of domain '%s': %v", domain, err)
+			return nil, fmt.Errorf("sending/receiving mdns packets during resolve of domain '%s': %v", domain, err)
 		}
 		filteredDevices := make([]*fuchsiaDevice, 0)
 		for _, device := range devices {
@@ -79,36 +77,26 @@
 			}
 		}
 		if len(filteredDevices) == 0 {
-			return fmt.Errorf("no devices with domain %v", domain)
+			return nil, fmt.Errorf("no devices with domain %v", domain)
 		}
 
 		for _, device := range filteredDevices {
 			outDevices = append(outDevices, device)
 		}
 	}
+	return outDevices, nil
+}
+
+func (cmd *resolveCmd) execute(ctx context.Context, domains ...string) error {
+	outDevices, err := cmd.resolveDevices(ctx, domains...)
+	if err != nil {
+		return err
+	}
 
 	if cmd.json {
-		return cmd.outputJSON(outDevices)
+		return cmd.outputJSON(outDevices, false /* includeDomain */)
 	}
-	return cmd.outputNormal(outDevices)
-}
-
-func (cmd *resolveCmd) outputNormal(devices []*fuchsiaDevice) error {
-	for _, device := range devices {
-		fmt.Println(device.addr)
-	}
-	return nil
-}
-
-func (cmd *resolveCmd) outputJSON(devices []*fuchsiaDevice) error {
-	jsonOut := jsonOutput{Devices: make([]jsonDevice, 0, len(devices))}
-	for _, device := range devices {
-		jsonOut.Devices = append(jsonOut.Devices, jsonDevice{Addr: device.addr.String()})
-	}
-
-	e := json.NewEncoder(os.Stdout)
-	e.SetIndent("", "  ")
-	return e.Encode(jsonOut)
+	return cmd.outputNormal(outDevices, false /* includeDomain */)
 }
 
 func (cmd *resolveCmd) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {