Repository: favonia/cloudflare-ddns
Pull request #419 (2023-03-13 10:23:36)

test: improve coverage
- [x] ([]Monitor).Describe
- [x] non-absolute URLs
- [x] invalid UTF-8 strings
- [x] env.ReadHealthchecksURL

codecov[bot]: ## [Codecov](https://codecov.io/gh/favonia/cloudflare-ddns/pull/419?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) Report
> Merging [#419](https://codecov.io/gh/favonia/cloudflare-ddns/pull/419?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) (cabcc8e) into [main](https://codecov.io/gh/favonia/cloudflare-ddns/commit/92a44628ab459c5eb715ecbddb9cb84ea36c927e?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) (92a4462) will **increase** coverage by `0.14%`.
> The diff coverage is `n/a`.

```diff
@@            Coverage Diff             @@
##             main     #419      +/-   ##
==========================================
+ Coverage   88.20%   88.35%   +0.14%     
==========================================
  Files          35       35              
  Lines        2078     2078              
==========================================
+ Hits         1833     1836       +3     
+ Misses        234      232       -2     
+ Partials       11       10       -1     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `88.35% <ø> (+0.14%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia#carryforward-flags-in-the-pull-request-comment) to find out more.

[see 1 file with indirect coverage changes](https://codecov.io/gh/favonia/cloudflare-ddns/pull/419/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia)

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia)


Patch:
diff --git a/internal/domainexp/lexer.go b/internal/domainexp/lexer.go
index f743520..586d838 100644
--- a/internal/domainexp/lexer.go
+++ b/internal/domainexp/lexer.go
@@ -17,6 +17,9 @@ var (
 
 	// ErrSingleOr is triggered by single | (which should have been ||).
 	ErrSingleOr = fmt.Errorf(`use "||" instead of "|"`)
+
+	// ErrUTF8 is triggered by invalid UTF-8 strings.
+	ErrUTF8 = fmt.Errorf(`invalid UTF-8 string`)
 )
 
 //nolint:funlen
@@ -42,10 +45,13 @@ func splitter(data []byte, atEOF bool) (int, []byte, error) {
 		if err != nil {
 			return startIndex, nil, fmt.Errorf("reader.ReadRune: %w", err)
 		}
-		if ch == utf8.RuneError && size == 1 && reader.Len() == 0 && !atEOF {
-			// special case: the UTF-8 decoding failed,
-			// but maybe more bytes will help
-			break
+		if ch == utf8.RuneError && size == 1 {
+			if reader.Len() == 0 && !atEOF {
+				// special case: the UTF-8 decoding failed,
+				// but reading more bytes might help
+				break
+			}
+			return startIndex, nil, fmt.Errorf("cannot parse %s: %w", data, ErrUTF8)
 		}
 
 		switch state {


Test patch:
diff --git a/internal/config/env_test.go b/internal/config/env_test.go
index cf8d35f..da1f015 100644
--- a/internal/config/env_test.go
+++ b/internal/config/env_test.go
@@ -892,6 +892,25 @@ func TestReadHealthchecksURL(t *testing.T) {
 			true,
 			nil,
 		},
+		"illformed/not-url": {
+			true, "\001",
+			nil,
+			nil, false,
+			func(m *mocks.MockPP) {
+				m.EXPECT().Errorf(pp.EmojiUserError, `Failed to parse the Healthchecks URL (redacted)`)
+			},
+		},
+		"illformed/not-abs": {
+			true, "/1234?hello=123",
+			nil,
+			nil, false,
+			func(m *mocks.MockPP) {
+				gomock.InOrder(
+					m.EXPECT().Errorf(pp.EmojiUserError, `The Healthchecks URL (redacted) does not look like a valid URL`),
+					m.EXPECT().Errorf(pp.EmojiUserError, `A valid example is "https://hc-ping.com/01234567-0123-0123-0123-0123456789abc"`), //nolint:lll
+				)
+			},
+		},
 	} {
 		tc := tc
 		t.Run(name, func(t *testing.T) {
diff --git a/internal/domainexp/parser_test.go b/internal/domainexp/parser_test.go
index b5be183..84183c8 100644
--- a/internal/domainexp/parser_test.go
+++ b/internal/domainexp/parser_test.go
@@ -1,6 +1,7 @@
 package domainexp_test
 
 import (
+	"errors"
 	"testing"
 
 	"github.com/golang/mock/gomock"
@@ -63,6 +64,19 @@ func TestParseList(t *testing.T) {
 	}
 }
 
+type ErrorMatcher struct {
+	Error error
+}
+
+func (m ErrorMatcher) Matches(x any) bool {
+	err, ok := x.(error)
+	return ok && errors.Is(err, m.Error)
+}
+
+func (m ErrorMatcher) String() string {
+	return m.Error.Error()
+}
+
 //nolint:funlen
 func TestParseExpression(t *testing.T) {
 	t.Parallel()
@@ -183,6 +197,12 @@ func TestParseExpression(t *testing.T) {
 				m.EXPECT().Errorf(pp.EmojiUserError, "%s (%q) has unexpected token %q", key, "0 1", "1")
 			},
 		},
+		"utf8/invalid": {
+			"\200\300", false, nil, false,
+			func(m *mocks.MockPP) {
+				m.EXPECT().Errorf(pp.EmojiUserError, "%s (%q) is ill-formed: %v", key, "\200\300", ErrorMatcher{domainexp.ErrUTF8}) //nolint:lll
+			},
+		},
 	} {
 		tc := tc
 		t.Run(name, func(t *testing.T) {
diff --git a/internal/monitor/composite_test.go b/internal/monitor/composite_test.go
index 6ee6109..64a848d 100644
--- a/internal/monitor/composite_test.go
+++ b/internal/monitor/composite_test.go
@@ -10,6 +10,22 @@ import (
 	"github.com/favonia/cloudflare-ddns/internal/monitor"
 )
 
+func TestMonitorsDescribe(t *testing.T) {
+	t.Parallel()
+
+	var ms []monitor.Monitor
+
+	mockCtrl := gomock.NewController(t)
+
+	for i := 0; i < 5; i++ {
+		m := mocks.NewMockMonitor(mockCtrl)
+		m.EXPECT().Describe(gomock.Any())
+		ms = append(ms, m)
+	}
+
+	monitor.Monitors(ms).Describe(func(service, params string) {})
+}
+
 func TestMonitorsSuccess(t *testing.T) {
 	t.Parallel()
 
diff --git a/internal/provider/protocol/http_test.go b/internal/provider/protocol/http_test.go
index e162a76..e6ffa01 100644
--- a/internal/provider/protocol/http_test.go
+++ b/internal/provider/protocol/http_test.go
@@ -50,17 +50,28 @@ func TestHTTPGetIP(t *testing.T) {
 
 	t.Run("group", func(t *testing.T) {
 		for name, tc := range map[string]struct {
+			nilCtx        bool
 			urlKey        ipnet.Type
 			url           string
 			ipNet         ipnet.Type
 			expected      netip.Addr
 			prepareMockPP func(*mocks.MockPP)
 		}{
-			"4":    {ipnet.IP4, ip4Server.URL, ipnet.IP4, ip4, nil},
-			"6":    {ipnet.IP6, ip6Server.URL, ipnet.IP6, ip6, nil},
-			"4to6": {ipnet.IP6, ip4Server.URL, ipnet.IP6, ip4As6, nil},
+			"4":    {false, ipnet.IP4, ip4Server.URL, ipnet.IP4, ip4, nil},
+			"6":    {false, ipnet.IP6, ip6Server.URL, ipnet.IP6, ip6, nil},
+			"4to6": {false, ipnet.IP6, ip4Server.URL, ipnet.IP6, ip4As6, nil},
+			"nilctx": {
+				true, ipnet.IP4, ip4Server.URL, ipnet.IP4, invalidIP,
+				func(m *mocks.MockPP) {
+					m.EXPECT().Warningf(
+						pp.EmojiImpossible, "Failed to prepare HTTP(S) request to %q: %v",
+						ip4Server.URL,
+						gomock.Any(),
+					)
+				},
+			},
 			"6to4": {
-				ipnet.IP4, ip6Server.URL, ipnet.IP4, invalidIP,
+				false, ipnet.IP4, ip6Server.URL, ipnet.IP4, invalidIP,
 				func(m *mocks.MockPP) {
 					m.EXPECT().Warningf(
 						pp.EmojiError, "Detected IP address %s is not a valid %s address",
@@ -70,7 +81,7 @@ func TestHTTPGetIP(t *testing.T) {
 				},
 			},
 			"4-nil1": {
-				ipnet.IP4, dummy.URL, ipnet.IP4, invalidIP,
+				false, ipnet.IP4, dummy.URL, ipnet.IP4, invalidIP,
 				func(m *mocks.MockPP) {
 					m.EXPECT().Errorf(
 						pp.EmojiImpossible,
@@ -80,7 +91,7 @@ func TestHTTPGetIP(t *testing.T) {
 				},
 			},
 			"6-nil1": {
-				ipnet.IP6, dummy.URL, ipnet.IP6, invalidIP,
+				false, ipnet.IP6, dummy.URL, ipnet.IP6, invalidIP,
 				func(m *mocks.MockPP) {
 					m.EXPECT().Errorf(
 						pp.EmojiImpossible,
@@ -90,7 +101,7 @@ func TestHTTPGetIP(t *testing.T) {
 				},
 			},
 			"4-nil2": {
-				ipnet.IP4, "", ipnet.IP4, invalidIP,
+				false, ipnet.IP4, "", ipnet.IP4, invalidIP,
 				func(m *mocks.MockPP) {
 					m.EXPECT().Warningf(
 						pp.EmojiError,
@@ -101,7 +112,7 @@ func TestHTTPGetIP(t *testing.T) {
 				},
 			},
 			"6-nil2": {
-				ipnet.IP6, "", ipnet.IP6, invalidIP,
+				false, ipnet.IP6, "", ipnet.IP6, invalidIP,
 				func(m *mocks.MockPP) {
 					m.EXPECT().Warningf(
 						pp.EmojiError,
@@ -112,13 +123,13 @@ func TestHTTPGetIP(t *testing.T) {
 				},
 			},
 			"4-nil3": {
-				ipnet.IP4, ip4Server.URL, ipnet.IP6, invalidIP,
+				false, ipnet.IP4, ip4Server.URL, ipnet.IP6, invalidIP,
 				func(m *mocks.MockPP) {
 					m.EXPECT().Warningf(pp.EmojiImpossible, "Unhandled IP network: %s", "IPv6")
 				},
 			},
 			"6-nil3": {
-				ipnet.IP6, ip6Server.URL, ipnet.IP4, invalidIP,
+				false, ipnet.IP6, ip6Server.URL, ipnet.IP4, invalidIP,
 				func(m *mocks.MockPP) {
 					m.EXPECT().Warningf(pp.EmojiImpossible, "Unhandled IP network: %s", "IPv4")
 				},
@@ -135,12 +146,16 @@ func TestHTTPGetIP(t *testing.T) {
 						tc.urlKey: tc.url,
 					},
 				}
+				ctx := context.Background()
+				if tc.nilCtx {
+					ctx = nil
+				}
 
 				mockPP := mocks.NewMockPP(mockCtrl)
 				if tc.prepareMockPP != nil {
 					tc.prepareMockPP(mockPP)
 				}
-				ip, ok := provider.GetIP(context.Background(), mockPP, tc.ipNet)
+				ip, ok := provider.GetIP(ctx, mockPP, tc.ipNet)
 				require.Equal(t, tc.expected, ip)
 				require.Equal(t, tc.expected.IsValid(), ok)
 			})
diff --git a/test/fuzzer/fuzzer_test.go b/test/fuzzer/fuzzer_test.go
index 8d1d9eb..09ec2fa 100644
--- a/test/fuzzer/fuzzer_test.go
+++ b/test/fuzzer/fuzzer_test.go
@@ -2,6 +2,7 @@
 package fuzzer_test
 
 import (
+	"errors"
 	"testing"
 
 	"github.com/golang/mock/gomock"
@@ -11,6 +12,19 @@ import (
 	"github.com/favonia/cloudflare-ddns/internal/pp"
 )
 
+type ErrorMatcher struct {
+	Error error
+}
+
+func (m ErrorMatcher) Matches(x any) bool {
+	err, ok := x.(error)
+	return ok && errors.Is(err, m.Error)
+}
+
+func (m ErrorMatcher) String() string {
+	return m.Error.Error()
+}
+
 const key string = "KEY"
 
 // FuzzParseList fuzz test [domainexp.ParseList].
@@ -20,6 +34,7 @@ func FuzzParseList(f *testing.F) {
 		mockPP := mocks.NewMockPP(mockCtrl)
 		mockPP.EXPECT().Errorf(pp.EmojiUserError, `%s (%q) is ill-formed: %v`, key, input, domainexp.ErrSingleAnd).AnyTimes()
 		mockPP.EXPECT().Errorf(pp.EmojiUserError, `%s (%q) is ill-formed: %v`, key, input, domainexp.ErrSingleOr).AnyTimes()
+		mockPP.EXPECT().Errorf(pp.EmojiUserError, "%s (%q) is ill-formed: %v", key, input, ErrorMatcher{domainexp.ErrUTF8}).AnyTimes() //nolint:lll
 		mockPP.EXPECT().Errorf(pp.EmojiUserError, `%s (%q) has unexpected token %q`, key, input, gomock.Any()).AnyTimes()
 		mockPP.EXPECT().Errorf(pp.EmojiUserError, `%s (%q) contains an ill-formed domain %q: %v`, key, input, gomock.Any(), gomock.Any()).AnyTimes() //nolint:lll
 		mockPP.EXPECT().Warningf(pp.EmojiUserError, `%s (%q) is missing a comma "," before %q`, key, input, gomock.Any()).AnyTimes()                 //nolint:lll
@@ -35,7 +50,8 @@ func FuzzParseExpression(f *testing.F) {
 		mockPP := mocks.NewMockPP(mockCtrl)
 		mockPP.EXPECT().Errorf(pp.EmojiUserError, `%s (%q) is ill-formed: %v`, key, input, domainexp.ErrSingleAnd).AnyTimes()
 		mockPP.EXPECT().Errorf(pp.EmojiUserError, `%s (%q) is ill-formed: %v`, key, input, domainexp.ErrSingleOr).AnyTimes()
-		mockPP.EXPECT().Errorf(pp.EmojiUserError, `%s (%q) has unexpected token %q`, key, input, gomock.Any()).AnyTimes()                                   //nolint:lll
+		mockPP.EXPECT().Errorf(pp.EmojiUserError, "%s (%q) is ill-formed: %v", key, input, ErrorMatcher{domainexp.ErrUTF8}).AnyTimes() //nolint:lll
+		mockPP.EXPECT().Errorf(pp.EmojiUserError, `%s (%q) has unexpected token %q`, key, input, gomock.Any()).AnyTimes()
 		mockPP.EXPECT().Errorf(pp.EmojiUserError, `%s (%q) has unexpected token %q when %q is expected`, key, input, gomock.Any(), gomock.Any()).AnyTimes() //nolint:lll
 		mockPP.EXPECT().Errorf(pp.EmojiUserError, `%s (%q) is missing %q at the end`, key, input, gomock.Any()).AnyTimes()
 		mockPP.EXPECT().Errorf(pp.EmojiUserError, `%s (%q) is not a boolean expression: got unexpected token %q`, key, input, gomock.Any()).AnyTimes() //nolint:lll