diff --git a/internal/discover/client_test.go b/internal/discover/client_test.go
new file mode 100644
index 00000000..f57085c3
--- /dev/null
+++ b/internal/discover/client_test.go
@@ -0,0 +1,227 @@
+// Copyright (C) 2014 The Syncthing Authors.
+//
+// This program is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation, either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program. If not, see .
+
+package discover
+
+import (
+ "fmt"
+ "net"
+ "sync"
+ "time"
+
+ "testing"
+
+ "github.com/syncthing/syncthing/internal/protocol"
+)
+
+var device protocol.DeviceID
+
+func init() {
+ device, _ = protocol.DeviceIDFromString("P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2")
+}
+
+func TestUDP4Success(t *testing.T) {
+ conn, err := net.ListenUDP("udp4", nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ port := conn.LocalAddr().(*net.UDPAddr).Port
+
+ address := fmt.Sprintf("udp4://127.0.0.1:%d", port)
+ pkt := &Announce{
+ Magic: AnnouncementMagic,
+ This: Device{
+ device[:],
+ []Address{{
+ IP: net.IPv4(123, 123, 123, 123),
+ Port: 1234,
+ }},
+ },
+ }
+
+ client, err := New(address, pkt)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ udpclient := client.(*UDPClient)
+ if udpclient.errorRetryInterval != DefaultErrorRetryInternval {
+ t.Fatal("Incorrect retry interval")
+ }
+
+ if udpclient.listenAddress.IP != nil || udpclient.listenAddress.Port != 0 {
+ t.Fatal("Wrong listen IP or port", udpclient.listenAddress)
+ }
+
+ if client.Address() != address {
+ t.Fatal("Incorrect address")
+ }
+
+ buf := make([]byte, 2048)
+
+ // First announcement
+ conn.SetDeadline(time.Now().Add(time.Millisecond * 100))
+ _, err = conn.Read(buf)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Announcement verification
+ conn.SetDeadline(time.Now().Add(time.Millisecond * 1100))
+ _, addr, err := conn.ReadFromUDP(buf)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Reply to it.
+ _, err = conn.WriteToUDP(pkt.MustMarshalXDR(), addr)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // We should get nothing else
+ conn.SetDeadline(time.Now().Add(time.Millisecond * 100))
+ _, err = conn.Read(buf)
+ if err == nil {
+ t.Fatal("Expected error")
+ }
+
+ // Status should be ok
+ if !client.StatusOK() {
+ t.Fatal("Wrong status")
+ }
+
+ // Do a lookup in a separate routine
+ addrs := []string{}
+ wg := sync.WaitGroup{}
+ wg.Add(1)
+ go func() {
+ addrs = client.Lookup(device)
+ wg.Done()
+ }()
+
+ // Receive the lookup and reply
+ conn.SetDeadline(time.Now().Add(time.Millisecond * 100))
+ _, addr, err = conn.ReadFromUDP(buf)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ conn.WriteToUDP(pkt.MustMarshalXDR(), addr)
+
+ // Wait for the lookup to arrive, verify that the number of answers is correct
+ wg.Wait()
+
+ if len(addrs) != 1 || addrs[0] != "123.123.123.123:1234" {
+ t.Fatal("Wrong number of answers")
+ }
+
+ client.Stop()
+}
+
+func TestUDP4Failure(t *testing.T) {
+ conn, err := net.ListenUDP("udp4", nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ port := conn.LocalAddr().(*net.UDPAddr).Port
+
+ address := fmt.Sprintf("udp4://127.0.0.1:%d/?listenaddress=127.0.0.1&retry=5", port)
+
+ pkt := &Announce{
+ Magic: AnnouncementMagic,
+ This: Device{
+ device[:],
+ []Address{{
+ IP: net.IPv4(123, 123, 123, 123),
+ Port: 1234,
+ }},
+ },
+ }
+
+ client, err := New(address, pkt)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ udpclient := client.(*UDPClient)
+ if udpclient.errorRetryInterval != time.Second*5 {
+ t.Fatal("Incorrect retry interval")
+ }
+
+ if !udpclient.listenAddress.IP.Equal(net.IPv4(127, 0, 0, 1)) || udpclient.listenAddress.Port != 0 {
+ t.Fatal("Wrong listen IP or port", udpclient.listenAddress)
+ }
+
+ if client.Address() != address {
+ t.Fatal("Incorrect address")
+ }
+
+ buf := make([]byte, 2048)
+
+ // First announcement
+ conn.SetDeadline(time.Now().Add(time.Millisecond * 100))
+ _, err = conn.Read(buf)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Announcement verification
+ conn.SetDeadline(time.Now().Add(time.Millisecond * 1100))
+ _, _, err = conn.ReadFromUDP(buf)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Don't reply
+ // We should get nothing else
+ conn.SetDeadline(time.Now().Add(time.Millisecond * 100))
+ _, err = conn.Read(buf)
+ if err == nil {
+ t.Fatal("Expected error")
+ }
+
+ // Status should be failure
+ if client.StatusOK() {
+ t.Fatal("Wrong status")
+ }
+
+ // Do a lookup in a separate routine
+ addrs := []string{}
+ wg := sync.WaitGroup{}
+ wg.Add(1)
+ go func() {
+ addrs = client.Lookup(device)
+ wg.Done()
+ }()
+
+ // Receive the lookup and don't reply
+ conn.SetDeadline(time.Now().Add(time.Millisecond * 100))
+ _, _, err = conn.ReadFromUDP(buf)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Wait for the lookup to timeout, verify that the number of answers is none
+ wg.Wait()
+
+ if len(addrs) != 0 {
+ t.Fatal("Wrong number of answers")
+ }
+
+ client.Stop()
+}
diff --git a/internal/discover/discover_test.go b/internal/discover/discover_test.go
index f2b69f58..fce201fb 100644
--- a/internal/discover/discover_test.go
+++ b/internal/discover/discover_test.go
@@ -13,6 +13,136 @@
// You should have received a copy of the GNU General Public License along
// with this program. If not, see .
-package discover_test
+package discover
-// Empty test file to generate 0% coverage rather than no coverage
+import (
+ "net/url"
+ "time"
+
+ "testing"
+
+ "github.com/syncthing/syncthing/internal/protocol"
+)
+
+type DummyClient struct {
+ url *url.URL
+ lookups []protocol.DeviceID
+ lookupRet []string
+ stops int
+ statusRet bool
+ statusChecks int
+}
+
+func (c *DummyClient) Lookup(device protocol.DeviceID) []string {
+ c.lookups = append(c.lookups, device)
+ return c.lookupRet
+}
+
+func (c *DummyClient) StatusOK() bool {
+ c.statusChecks++
+ return c.statusRet
+}
+
+func (c *DummyClient) Stop() {
+ c.stops++
+}
+
+func (c *DummyClient) Address() string {
+ return c.url.String()
+}
+
+func TestGlobalDiscovery(t *testing.T) {
+ c1 := &DummyClient{
+ statusRet: false,
+ lookupRet: []string{"test.com:1234"},
+ }
+
+ c2 := &DummyClient{
+ statusRet: true,
+ lookupRet: []string{},
+ }
+
+ c3 := &DummyClient{
+ statusRet: true,
+ lookupRet: []string{"best.com:2345"},
+ }
+
+ clients := []*DummyClient{c1, c2}
+
+ Register("test1", func(uri *url.URL, pkt *Announce) (Client, error) {
+ c := clients[0]
+ clients = clients[1:]
+ c.url = uri
+ return c, nil
+ })
+
+ Register("test2", func(uri *url.URL, pkt *Announce) (Client, error) {
+ c3.url = uri
+ return c3, nil
+ })
+
+ d := NewDiscoverer(device, []string{})
+ d.localBcastStart = time.Time{}
+ servers := []string{
+ "test1://123.123.123.123:1234",
+ "test1://23.23.23.23:234",
+ "test2://234.234.234.234.2345",
+ }
+ d.StartGlobal(servers, 1234)
+
+ if len(d.clients) != 3 {
+ t.Fatal("Wrong number of clients")
+ }
+
+ status := d.ExtAnnounceOK()
+
+ for _, c := range []*DummyClient{c1, c2, c3} {
+ if status[c.url.String()] != c.statusRet || c.statusChecks != 1 {
+ t.Fatal("Wrong status")
+ }
+ }
+
+ addrs := d.Lookup(device)
+ if len(addrs) != 2 {
+ t.Fatal("Wrong numer of addresses", addrs)
+ }
+
+ for _, addr := range []string{"test.com:1234", "best.com:2345"} {
+ found := false
+ for _, laddr := range addrs {
+ if laddr == addr {
+ found = true
+ break
+ }
+ }
+ if !found {
+ t.Fatal("Couldn't find", addr)
+ }
+ }
+
+ for _, c := range []*DummyClient{c1, c2, c3} {
+ if len(c.lookups) != 1 || c.lookups[0] != device {
+ t.Fatal("Wrong lookups")
+ }
+ }
+
+ addrs = d.Lookup(device)
+ if len(addrs) != 2 {
+ t.Fatal("Wrong numer of addresses", addrs)
+ }
+
+ // Answer should be cached, so number of lookups should have not incresed
+ for _, c := range []*DummyClient{c1, c2, c3} {
+ if len(c.lookups) != 1 || c.lookups[0] != device {
+ t.Fatal("Wrong lookups")
+ }
+ }
+
+ d.StopGlobal()
+
+ for _, c := range []*DummyClient{c1, c2, c3} {
+ if c.stops != 1 {
+ t.Fatal("Wrong number of stops")
+ }
+ }
+}