Repository: favonia/cloudflare-ddns
Pull request #913 (2024-08-31 01:58:02)

refactor: rewrite more code using iterators

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/favonia/cloudflare-ddns/pull/913?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 95.55%. Comparing base [(`319e6a0`)](https://app.codecov.io/gh/favonia/cloudflare-ddns/commit/319e6a0d266a060037c08080ceb2495df600d63f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) to head [(`56faae8`)](https://app.codecov.io/gh/favonia/cloudflare-ddns/commit/56faae811bdbce7c1809618d9c31de118a886cdb?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #913      +/-   ##
==========================================
- Coverage   95.58%   95.55%   -0.04%     
==========================================
  Files          58       58              
  Lines        2581     2562      -19     
==========================================
- Hits         2467     2448      -19     
  Misses        104      104              
  Partials       10       10              
```

| [Flag](https://app.codecov.io/gh/favonia/cloudflare-ddns/pull/913/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) | Coverage Δ | |
|---|---|---|
| [unittests](https://app.codecov.io/gh/favonia/cloudflare-ddns/pull/913/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) | `95.55% <100.00%> (-0.04%)` | :arrow_down: |

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.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/favonia/cloudflare-ddns/pull/913?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia).


Patch:
diff --git a/cmd/ddns/ddns.go b/cmd/ddns/ddns.go
index 612d24e..0dcb368 100644
--- a/cmd/ddns/ddns.go
+++ b/cmd/ddns/ddns.go
@@ -67,7 +67,7 @@ func main() {
 	os.Exit(realMain())
 }
 
-func realMain() int { //nolint:funlen
+func realMain() int {
 	// Get the contexts and start catching SIGINT and SIGTERM
 	ctx := context.Background()
 	sig := signal.Setup()
diff --git a/internal/api/cloudflare_records.go b/internal/api/cloudflare_records.go
index 99ddd14..fba43e3 100644
--- a/internal/api/cloudflare_records.go
+++ b/internal/api/cloudflare_records.go
@@ -69,8 +69,7 @@ func (h CloudflareHandle) ZoneOfDomain(ctx context.Context, ppfmt pp.PP, domain
 	}
 
 zoneSearch:
-	for s := domain.Split(); s.IsValid(); s = s.Next() {
-		zoneName := s.ZoneNameASCII()
+	for zoneName := range domain.Zones {
 		zones, ok := h.ListZones(ctx, ppfmt, zoneName)
 		if !ok {
 			return "", false
diff --git a/internal/config/config_print.go b/internal/config/config_print.go
index 5274097..b30286c 100644
--- a/internal/config/config_print.go
+++ b/internal/config/config_print.go
@@ -55,13 +55,11 @@ func (c *Config) Print(ppfmt pp.PP) {
 	}
 
 	section("Domains, IP providers, and WAF lists:")
-	if c.Provider[ipnet.IP4] != nil {
-		item("IPv4-enabled domains:", "%s", pp.JoinMap(domain.Domain.Describe, c.Domains[ipnet.IP4]))
-		item("IPv4 provider:", "%s", provider.Name(c.Provider[ipnet.IP4]))
-	}
-	if c.Provider[ipnet.IP6] != nil {
-		item("IPv6-enabled domains:", "%s", pp.JoinMap(domain.Domain.Describe, c.Domains[ipnet.IP6]))
-		item("IPv6 provider:", "%s", provider.Name(c.Provider[ipnet.IP6]))
+	for ipNet, p := range ipnet.Bindings(c.Provider) {
+		if p != nil {
+			item(ipNet.Describe()+"-enabled domains:", "%s", pp.JoinMap(domain.Domain.Describe, c.Domains[ipNet]))
+			item(ipNet.Describe()+" provider:", "%s", provider.Name(p))
+		}
 	}
 	item("WAF lists:", "%s", pp.JoinMap(api.WAFList.Describe, c.WAFLists))
 
diff --git a/internal/config/config_read.go b/internal/config/config_read.go
index d0f40e0..0aa2e73 100644
--- a/internal/config/config_read.go
+++ b/internal/config/config_read.go
@@ -45,8 +45,6 @@ func (c *Config) ReadEnv(ppfmt pp.PP) bool {
 
 // Normalize checks and normalizes the fields [Config.Provider], [Config.Proxied], and [Config.DeleteOnStop].
 // When any error is reported, the original configuration remain unchanged.
-//
-//nolint:funlen
 func (c *Config) Normalize(ppfmt pp.PP) bool {
 	if ppfmt.IsShowing(pp.Info) {
 		ppfmt.Infof(pp.EmojiEnvVars, "Checking settings . . .")
@@ -80,8 +78,8 @@ func (c *Config) Normalize(ppfmt pp.PP) bool {
 	// Step 3.1: fill in providerMap and activeDomainSet
 	providerMap := map[ipnet.Type]provider.Provider{}
 	activeDomainSet := map[domain.Domain]bool{}
-	for ipNet := range c.Provider {
-		if c.Provider[ipNet] != nil {
+	for ipNet, p := range ipnet.Bindings(c.Provider) {
+		if p != nil {
 			domains := c.Domains[ipNet]
 
 			if len(domains) == 0 && len(c.WAFLists) == 0 {
@@ -92,7 +90,7 @@ func (c *Config) Normalize(ppfmt pp.PP) bool {
 				continue
 			}
 
-			providerMap[ipNet] = c.Provider[ipNet]
+			providerMap[ipNet] = p
 			for _, domain := range domains {
 				activeDomainSet[domain] = true
 			}
@@ -107,7 +105,7 @@ func (c *Config) Normalize(ppfmt pp.PP) bool {
 	}
 
 	// Step 3.3: check if some domains are unused
-	for ipNet, domains := range c.Domains {
+	for ipNet, domains := range ipnet.Bindings(c.Domains) {
 		if providerMap[ipNet] == nil {
 			for _, domain := range domains {
 				if activeDomainSet[domain] {
diff --git a/internal/config/env_provider.go b/internal/config/env_provider.go
index 0c86769..a273607 100644
--- a/internal/config/env_provider.go
+++ b/internal/config/env_provider.go
@@ -11,8 +11,6 @@ import (
 // ReadProvider reads an environment variable and parses it as a provider.
 //
 // policyKey was the name of the deprecated parameters IP4/6_POLICY.
-//
-//nolint:funlen
 func ReadProvider(ppfmt pp.PP, key, keyDeprecated string, field *provider.Provider) bool {
 	val := Getenv(key)
 
diff --git a/internal/domain/base.go b/internal/domain/base.go
index f1d5043..6d657e6 100644
--- a/internal/domain/base.go
+++ b/internal/domain/base.go
@@ -1,19 +1,6 @@
 // Package domain parses DNS domain names.
 package domain
 
-// A Splitter enumerates all possible zones of a domain by moving a cursor
-// from the start of the domain's name to its end.
-type Splitter interface {
-	// IsValid checks whether the cursor is still valid.
-	IsValid() bool
-
-	// ZoneNameASCII gives the suffix after the cursor as a possible zone, if the cursor is still valid.
-	ZoneNameASCII() string
-
-	// Next moves the cursor to the next possible splitting point, which might end up being invalid.
-	Next() Splitter
-}
-
 // A Domain represents a domain name to update.
 type Domain interface {
 	// DNSNameASCII gives a name suitable for accessing the Cloudflare API
@@ -22,6 +9,6 @@ type Domain interface {
 	// Describe gives the most human-readable domain name that is still unambiguous
 	Describe() string
 
-	// Split gives a Splitter that can be used to find zones
-	Split() Splitter
+	// Zones iterates from the smallest possible zone to largest ones (the root).
+	Zones(yield func(ZoneNameASCII string) bool)
 }
diff --git a/internal/domain/fqdn.go b/internal/domain/fqdn.go
index 1915f0d..b3de777 100644
--- a/internal/domain/fqdn.go
+++ b/internal/domain/fqdn.go
@@ -13,41 +13,17 @@ func (f FQDN) Describe() string {
 	return safelyToUnicode(string(f))
 }
 
-// FQDNSplitter implements [Splitter] for a [FQDN].
-type FQDNSplitter struct {
-	domain    string
-	cursor    int
-	exhausted bool
-}
-
-// Split constructs a [FQDNSplitter] for the FQDN.
-func (f FQDN) Split() Splitter {
-	return FQDNSplitter{
-		domain:    string(f),
-		cursor:    0,
-		exhausted: false,
-	}
-}
-
-// IsValid checks whether the cursor is still valid.
-func (s FQDNSplitter) IsValid() bool { return !s.exhausted }
-
-// ZoneNameASCII gives the ASCII form of the current zone suffix.
-func (s FQDNSplitter) ZoneNameASCII() string { return s.domain[s.cursor:] }
-
-// Next moves the cursor to the next spltting point.
-// Call [IsValid] to check whether the resulting cursor is valid.
-func (s FQDNSplitter) Next() Splitter {
-	next := s
-	if s.cursor == len(s.domain) {
-		next.exhausted = true
-	} else {
-		shift := strings.IndexRune(s.ZoneNameASCII(), '.')
-		if shift == -1 {
-			next.cursor = len(s.domain)
+// Zones starts from a.b.c for the domain a.b.c.
+func (f FQDN) Zones(yield func(ZoneNameASCII string) bool) {
+	domain := string(f)
+	for {
+		if !yield(domain) {
+			return
+		}
+		if i := strings.IndexRune(domain, '.'); i == -1 {
+			return
 		} else {
-			next.cursor += shift + 1
+			domain = domain[i+1:]
 		}
 	}
-	return next
 }
diff --git a/internal/domain/wildcard.go b/internal/domain/wildcard.go
index 098d47a..0ba7dc7 100644
--- a/internal/domain/wildcard.go
+++ b/internal/domain/wildcard.go
@@ -24,41 +24,17 @@ func (w Wildcard) Describe() string {
 	return "*." + safelyToUnicode(string(w))
 }
 
-// WildcardSplitter implements [Splitter] for a wildcard domain.
-type WildcardSplitter struct {
-	domain    string
-	cursor    int
-	exhausted bool
-}
-
-// Split constructs a [Splitter] for a wildcard domain.
-func (w Wildcard) Split() Splitter {
-	return WildcardSplitter{
-		domain:    string(w),
-		cursor:    0,
-		exhausted: false,
-	}
-}
-
-// IsValid checks whether the cursor is still valid.
-func (s WildcardSplitter) IsValid() bool { return !s.exhausted }
-
-// ZoneNameASCII gives the ASCII form of the current zone suffix.
-func (s WildcardSplitter) ZoneNameASCII() string { return s.domain[s.cursor:] }
-
-// Next moves the cursor to the next spltting point.
-// Call [WildcardSplitter.IsValid] to check whether the resulting cursor is valid.
-func (s WildcardSplitter) Next() Splitter {
-	next := s
-	if s.cursor == len(s.domain) {
-		next.exhausted = true
-	} else {
-		shift := strings.IndexRune(s.ZoneNameASCII(), '.')
-		if shift == -1 {
-			next.cursor = len(s.domain)
+// Zones starts from a.b.c for the wildcard domain *.a.b.c.
+func (w Wildcard) Zones(yield func(ZoneNameASCII string) bool) {
+	domain := string(w)
+	for {
+		if !yield(domain) {
+			return
+		}
+		if i := strings.IndexRune(domain, '.'); i == -1 {
+			return
 		} else {
-			next.cursor += shift + 1
+			domain = domain[i+1:]
 		}
 	}
-	return next
 }
diff --git a/internal/domainexp/lexer.go b/internal/domainexp/lexer.go
index 2327144..0b1cd29 100644
--- a/internal/domainexp/lexer.go
+++ b/internal/domainexp/lexer.go
@@ -23,7 +23,6 @@ var (
 	ErrUTF8 = errors.New(`invalid UTF-8 string`)
 )
 
-//nolint:funlen
 func splitter(data []byte, atEOF bool) (int, []byte, error) {
 	reader := bytes.NewReader(data)
 	startIndex := 0
diff --git a/internal/domainexp/parser.go b/internal/domainexp/parser.go
index 9ed2e84..eae6725 100644
--- a/internal/domainexp/parser.go
+++ b/internal/domainexp/parser.go
@@ -93,7 +93,7 @@ func hasStrictSuffix(s, suffix string) bool {
 //
 // <factor> --> true | false | <fun> | ! <factor> | ( <expression> )
 //
-//nolint:funlen
+
 func scanFactor(ppfmt pp.PP, key string, input string, tokens []string) (predicate, []string) {
 	// fmt.Printf("scanFactor(tokens = %#v)\n", tokens)
 
diff --git a/internal/ipnet/ipnet.go b/internal/ipnet/ipnet.go
index fe0b555..f58ca9e 100644
--- a/internal/ipnet/ipnet.go
+++ b/internal/ipnet/ipnet.go
@@ -1,8 +1,10 @@
 // Package ipnet contains utility functions for IPv4 and IPv6 networks.
 package ipnet
 
+//nolint:gci
 import (
 	"fmt"
+	"iter"
 	"net/netip"
 
 	"github.com/favonia/cloudflare-ddns/internal/pp"
@@ -123,3 +125,22 @@ func (t Type) Matches(ip netip.Addr) bool {
 		return false
 	}
 }
+
+// All enumerates [IP4] and then [IP6].
+func All(yield func(Type) bool) {
+	_ = yield(IP4) && yield(IP6)
+}
+
+// Bindings enumerates the key [IP4] and then [IP6] for a map.
+func Bindings[V any](m map[Type]V) iter.Seq2[Type, V] {
+	return func(yield func(Type, V) bool) {
+		for ipNet := range All {
+			v, ok := m[ipNet]
+			if ok {
+				if !yield(ipNet, v) {
+					return
+				}
+			}
+		}
+	}
+}
diff --git a/internal/setter/setter.go b/internal/setter/setter.go
index 1312f06..c7caaf1 100644
--- a/internal/setter/setter.go
+++ b/internal/setter/setter.go
@@ -37,8 +37,6 @@ func partitionRecords(rs []api.Record, target netip.Addr) (matchedIDs, unmatched
 }
 
 // Set updates the IP address of one domain to the given ip. The IP address (ip) must be non-zero.
-//
-//nolint:funlen
 func (s setter) Set(ctx context.Context, ppfmt pp.PP,
 	ipnet ipnet.Type, domain domain.Domain, ip netip.Addr, ttl api.TTL, proxied bool, recordComment string,
 ) ResponseCode {
@@ -222,7 +220,7 @@ func (s setter) SetWAFList(ctx context.Context, ppfmt pp.PP,
 
 	var itemsToDelete []api.WAFListItem
 	var itemsToCreate []netip.Prefix
-	for _, ipNet := range [...]ipnet.Type{ipnet.IP4, ipnet.IP6} {
+	for ipNet := range ipnet.All {
 		detectedIP, managed := detectedIP[ipNet]
 		covered := false
 		for _, item := range items {
diff --git a/internal/updater/updater.go b/internal/updater/updater.go
index e02fb7a..c2c4471 100644
--- a/internal/updater/updater.go
+++ b/internal/updater/updater.go
@@ -155,8 +155,8 @@ func UpdateIPs(ctx context.Context, ppfmt pp.PP, c *config.Config, s setter.Sett
 	detectedIP := map[ipnet.Type]netip.Addr{}
 	numManagedNetworks := 0
 	numValidIPs := 0
-	for _, ipNet := range [...]ipnet.Type{ipnet.IP4, ipnet.IP6} {
-		if c.Provider[ipNet] != nil {
+	for ipNet, provider := range ipnet.Bindings(c.Provider) {
+		if provider != nil {
 			numManagedNetworks++
 			ip, msg := detectIP(ctx, ppfmt, c, ipNet)
 			detectedIP[ipNet] = ip
@@ -183,8 +183,8 @@ func UpdateIPs(ctx context.Context, ppfmt pp.PP, c *config.Config, s setter.Sett
 func DeleteIPs(ctx context.Context, ppfmt pp.PP, c *config.Config, s setter.Setter) message.Message {
 	var msgs []message.Message
 
-	for _, ipNet := range [...]ipnet.Type{ipnet.IP4, ipnet.IP6} {
-		if c.Provider[ipNet] != nil {
+	for ipNet, provider := range ipnet.Bindings(c.Provider) {
+		if provider != nil {
 			msgs = append(msgs, deleteIP(ctx, ppfmt, c, s, ipNet))
 		}
 	}


Test patch:
diff --git a/internal/api/cloudflare_records_test.go b/internal/api/cloudflare_records_test.go
index 9a794e3..b61aa12 100644
--- a/internal/api/cloudflare_records_test.go
+++ b/internal/api/cloudflare_records_test.go
@@ -155,7 +155,6 @@ func TestListZonesTwo(t *testing.T) {
 	}
 }
 
-//nolint:funlen
 func TestZoneOfDomain(t *testing.T) {
 	t.Parallel()
 
@@ -403,7 +402,6 @@ func TestListRecords(t *testing.T) {
 	require.Equal(t, expected, rs)
 }
 
-//nolint:funlen
 func TestListRecordsInvalidIPAddress(t *testing.T) {
 	t.Parallel()
 	mockCtrl := gomock.NewController(t)
@@ -681,7 +679,6 @@ func newUpdateRecordHandler(t *testing.T, mux *http.ServeMux, id string, ip stri
 	return httpHandler{requestLimit: &requestLimit}
 }
 
-//nolint:funlen
 func TestUpdateRecordValid(t *testing.T) {
 	t.Parallel()
 	mockCtrl := gomock.NewController(t)
@@ -793,7 +790,6 @@ func newCreateRecordHandler(t *testing.T, mux *http.ServeMux, id string, ipNet i
 	return httpHandler{requestLimit: &requestLimit}
 }
 
-//nolint:funlen
 func TestCreateRecordValid(t *testing.T) {
 	t.Parallel()
 	mockCtrl := gomock.NewController(t)
diff --git a/internal/config/config_read_test.go b/internal/config/config_read_test.go
index eafef26..250ae0e 100644
--- a/internal/config/config_read_test.go
+++ b/internal/config/config_read_test.go
@@ -85,7 +85,6 @@ func TestReadEnvEmpty(t *testing.T) {
 	require.False(t, ok)
 }
 
-//nolint:funlen
 func TestNormalize(t *testing.T) {
 	t.Parallel()
 
diff --git a/internal/config/env_auth_test.go b/internal/config/env_auth_test.go
index c1d1546..5bc357f 100644
--- a/internal/config/env_auth_test.go
+++ b/internal/config/env_auth_test.go
@@ -78,7 +78,7 @@ func useMemFS(memfs fstest.MapFS) {
 	file.FS = memfs
 }
 
-//nolint:funlen,paralleltest // environment vars and file system are global
+//nolint:paralleltest // environment vars and file system are global
 func TestReadAuthWithFile(t *testing.T) {
 	unset(t, "CF_API_TOKEN", "CF_API_TOKEN_FILE", "CF_ACCOUNT_ID")
 
diff --git a/internal/config/env_base_test.go b/internal/config/env_base_test.go
index 23acc28..588b4a1 100644
--- a/internal/config/env_base_test.go
+++ b/internal/config/env_base_test.go
@@ -134,7 +134,7 @@ func TestReadString(t *testing.T) {
 	}
 }
 
-//nolint:funlen,paralleltest // environment vars are global
+//nolint:paralleltest // environment vars are global
 func TestReadBool(t *testing.T) {
 	key := keyPrefix + "BOOL"
 	for name, tc := range map[string]struct {
@@ -260,7 +260,7 @@ func TestReadNonnegInt(t *testing.T) {
 	}
 }
 
-//nolint:funlen,paralleltest // environment vars are global
+//nolint:paralleltest // environment vars are global
 func TestReadTTL(t *testing.T) {
 	key := keyPrefix + "TTL"
 	for name, tc := range map[string]struct {
@@ -378,7 +378,7 @@ func TestReadNonnegDuration(t *testing.T) {
 	}
 }
 
-//nolint:paralleltest,funlen // environment vars are global
+//nolint:paralleltest // environment vars are global
 func TestReadCron(t *testing.T) {
 	key := keyPrefix + "CRON"
 
diff --git a/internal/config/env_domain_test.go b/internal/config/env_domain_test.go
index 48a172e..8664e97 100644
--- a/internal/config/env_domain_test.go
+++ b/internal/config/env_domain_test.go
@@ -13,7 +13,7 @@ import (
 	"github.com/favonia/cloudflare-ddns/internal/pp"
 )
 
-//nolint:paralleltest,funlen // environment vars are global
+//nolint:paralleltest // environment vars are global
 func TestReadDomains(t *testing.T) {
 	key := keyPrefix + "DOMAINS"
 	type ds = []domain.Domain
@@ -87,7 +87,7 @@ func TestReadDomains(t *testing.T) {
 	}
 }
 
-//nolint:paralleltest,funlen // environment vars are global
+//nolint:paralleltest // environment vars are global
 func TestReadDomainMap(t *testing.T) {
 	for name, tc := range map[string]struct {
 		domains       string
diff --git a/internal/config/env_monitor_test.go b/internal/config/env_monitor_test.go
index 16e9cd5..9fe6ff3 100644
--- a/internal/config/env_monitor_test.go
+++ b/internal/config/env_monitor_test.go
@@ -12,7 +12,7 @@ import (
 	"github.com/favonia/cloudflare-ddns/internal/pp"
 )
 
-//nolint:paralleltest,funlen // paralleltest should not be used because environment vars are global
+//nolint:paralleltest // paralleltest should not be used because environment vars are global
 func TestReadAndAppendHealthchecksURL(t *testing.T) {
 	key := keyPrefix + "HEALTHCHECKS"
 
@@ -109,7 +109,7 @@ func TestReadAndAppendHealthchecksURL(t *testing.T) {
 	}
 }
 
-//nolint:paralleltest,funlen // paralleltest should not be used because environment vars are global
+//nolint:paralleltest // paralleltest should not be used because environment vars are global
 func TestReadAndAppendUptimeKumaURL(t *testing.T) {
 	key := keyPrefix + "UPTIMEKUMA"
 
diff --git a/internal/config/env_notifier_test.go b/internal/config/env_notifier_test.go
index 6002c90..8da9bc6 100644
--- a/internal/config/env_notifier_test.go
+++ b/internal/config/env_notifier_test.go
@@ -12,7 +12,7 @@ import (
 	"github.com/favonia/cloudflare-ddns/internal/pp"
 )
 
-//nolint:paralleltest,funlen // paralleltest should not be used because environment vars are global
+//nolint:paralleltest // paralleltest should not be used because environment vars are global
 func TestReadAndAppendShoutrrrURL(t *testing.T) {
 	key := keyPrefix + "SHOUTRRR"
 
diff --git a/internal/config/env_provider_test.go b/internal/config/env_provider_test.go
index 281fab8..e2feb7a 100644
--- a/internal/config/env_provider_test.go
+++ b/internal/config/env_provider_test.go
@@ -13,7 +13,7 @@ import (
 	"github.com/favonia/cloudflare-ddns/internal/provider"
 )
 
-//nolint:paralleltest,funlen // paralleltest should not be used because environment vars are global
+//nolint:paralleltest // paralleltest should not be used because environment vars are global
 func TestReadProvider(t *testing.T) {
 	key := keyPrefix + "PROVIDER"
 	keyDeprecated := keyPrefix + "DEPRECATED"
@@ -190,7 +190,7 @@ func TestReadProvider(t *testing.T) {
 	}
 }
 
-//nolint:funlen,paralleltest // environment vars are global
+//nolint:paralleltest // environment vars are global
 func TestReadProviderMap(t *testing.T) {
 	var (
 		none  provider.Provider
diff --git a/internal/config/env_waf_test.go b/internal/config/env_waf_test.go
index d408d34..f82e42a 100644
--- a/internal/config/env_waf_test.go
+++ b/internal/config/env_waf_test.go
@@ -12,7 +12,7 @@ import (
 	"github.com/favonia/cloudflare-ddns/internal/pp"
 )
 
-//nolint:paralleltest,funlen // paralleltest should not be used because environment vars are global
+//nolint:paralleltest // paralleltest should not be used because environment vars are global
 func TestReadAndAppendWAFListNames(t *testing.T) {
 	key := keyPrefix + "WAF_LISTS"
 
diff --git a/internal/domain/domain_test.go b/internal/domain/domain_test.go
index cc96955..adfaff0 100644
--- a/internal/domain/domain_test.go
+++ b/internal/domain/domain_test.go
@@ -11,7 +11,6 @@ import (
 	"github.com/favonia/cloudflare-ddns/internal/domain"
 )
 
-//nolint:funlen
 func TestNew(t *testing.T) {
 	t.Parallel()
 	type f = domain.FQDN
diff --git a/internal/domain/fqdn_test.go b/internal/domain/fqdn_test.go
index 37e07df..6cfaaaa 100644
--- a/internal/domain/fqdn_test.go
+++ b/internal/domain/fqdn_test.go
@@ -65,8 +65,7 @@ func TestFQDNDescribe(t *testing.T) {
 	}
 }
 
-//nolint:dupl
-func TestFQDNSplitter(t *testing.T) {
+func TestFQDNZones(t *testing.T) {
 	t.Parallel()
 	type r = string
 	for _, tc := range [...]struct {
@@ -82,11 +81,11 @@ func TestFQDNSplitter(t *testing.T) {
 	} {
 		t.Run(tc.input, func(t *testing.T) {
 			t.Parallel()
-			var rs []r
-			for s := domain.FQDN(tc.input).Split(); s.IsValid(); s = s.Next() {
-				rs = append(rs, s.ZoneNameASCII())
+			i := 0
+			for zone := range domain.FQDN(tc.input).Zones {
+				require.Equal(t, tc.expected[i], zone)
+				i++
 			}
-			require.Equal(t, tc.expected, rs)
 		})
 	}
 }
diff --git a/internal/domain/wildcard_test.go b/internal/domain/wildcard_test.go
index 4761bb8..f598696 100644
--- a/internal/domain/wildcard_test.go
+++ b/internal/domain/wildcard_test.go
@@ -69,8 +69,7 @@ func TestWildcardDescribe(t *testing.T) {
 	}
 }
 
-//nolint:dupl
-func TestWildcardSplitter(t *testing.T) {
+func TestWildcardZones(t *testing.T) {
 	t.Parallel()
 	type r = string
 	for _, tc := range [...]struct {
@@ -86,11 +85,11 @@ func TestWildcardSplitter(t *testing.T) {
 	} {
 		t.Run(tc.input, func(t *testing.T) {
 			t.Parallel()
-			var rs []r
-			for s := domain.Wildcard(tc.input).Split(); s.IsValid(); s = s.Next() {
-				rs = append(rs, s.ZoneNameASCII())
+			i := 0
+			for zone := range domain.Wildcard(tc.input).Zones {
+				require.Equal(t, tc.expected[i], zone)
+				i++
 			}
-			require.Equal(t, tc.expected, rs)
 		})
 	}
 }
diff --git a/internal/domainexp/parser_test.go b/internal/domainexp/parser_test.go
index cc92f11..5d1eba2 100644
--- a/internal/domainexp/parser_test.go
+++ b/internal/domainexp/parser_test.go
@@ -76,7 +76,6 @@ func (m ErrorMatcher) String() string {
 	return m.Error.Error()
 }
 
-//nolint:funlen
 func TestParseExpression(t *testing.T) {
 	t.Parallel()
 	key := "key"
diff --git a/internal/ipnet/ipnet_test.go b/internal/ipnet/ipnet_test.go
index 11b6bf4..9d3998e 100644
--- a/internal/ipnet/ipnet_test.go
+++ b/internal/ipnet/ipnet_test.go
@@ -67,7 +67,6 @@ func TestInt(t *testing.T) {
 	}
 }
 
-//nolint:funlen
 func TestNormalizeDetectedIP(t *testing.T) {
 	t.Parallel()
 	for name, tc := range map[string]struct {
@@ -166,3 +165,18 @@ func TestMatches(t *testing.T) {
 		})
 	}
 }
+
+func TestBindings(t *testing.T) {
+	t.Parallel()
+
+	count := 0
+	for ipNet := range ipnet.Bindings(map[ipnet.Type]int{
+		ipnet.IP4: 400,
+		ipnet.IP6: 600,
+	}) {
+		count++
+		require.Equal(t, ipnet.IP4, ipNet)
+		break
+	}
+	require.Equal(t, 1, count)
+}
diff --git a/internal/monitor/healthchecks_test.go b/internal/monitor/healthchecks_test.go
index ad17581..a47524b 100644
--- a/internal/monitor/healthchecks_test.go
+++ b/internal/monitor/healthchecks_test.go
@@ -87,7 +87,6 @@ func TestHealthchecksDescribe(t *testing.T) {
 	require.Equal(t, 1, count)
 }
 
-//nolint:funlen
 func TestHealthchecksEndPoints(t *testing.T) {
 	t.Parallel()
 
diff --git a/internal/monitor/uptimekuma_test.go b/internal/monitor/uptimekuma_test.go
index 71d4293..7185981 100644
--- a/internal/monitor/uptimekuma_test.go
+++ b/internal/monitor/uptimekuma_test.go
@@ -91,7 +91,6 @@ func TestUptimeKumaDescribe(t *testing.T) {
 	require.Equal(t, 1, count)
 }
 
-//nolint:funlen
 func TestUptimeKumaEndPoints(t *testing.T) {
 	t.Parallel()
 
diff --git a/internal/provider/protocol/doh_test.go b/internal/provider/protocol/doh_test.go
index f077736..48da0b1 100644
--- a/internal/provider/protocol/doh_test.go
+++ b/internal/provider/protocol/doh_test.go
@@ -93,7 +93,6 @@ func setupServer(t *testing.T, name string, class dnsmessage.Class,
 	}))
 }
 
-//nolint:funlen
 func TestDNSOverHTTPSGetIP(t *testing.T) {
 	t.Parallel()
 
diff --git a/internal/provider/protocol/http_test.go b/internal/provider/protocol/http_test.go
index 060c82c..4449afc 100644
--- a/internal/provider/protocol/http_test.go
+++ b/internal/provider/protocol/http_test.go
@@ -28,7 +28,6 @@ func TestHTTPName(t *testing.T) {
 	require.Equal(t, "very secret name", p.Name())
 }
 
-//nolint:funlen
 func TestHTTPGetIP(t *testing.T) {
 	ip4 := netip.MustParseAddr("1.2.3.4")
 	ip4As6 := netip.MustParseAddr("::ffff:1.2.3.4")
diff --git a/internal/provider/protocol/local_test.go b/internal/provider/protocol/local_test.go
index 2744ebb..e3a0413 100644
--- a/internal/provider/protocol/local_test.go
+++ b/internal/provider/protocol/local_test.go
@@ -25,7 +25,6 @@ func TestLocalName(t *testing.T) {
 	require.Equal(t, "very secret name", p.Name())
 }
 
-//nolint:funlen
 func TestLocalGetIP(t *testing.T) {
 	t.Parallel()
 
diff --git a/internal/provider/protocol/regexp_test.go b/internal/provider/protocol/regexp_test.go
index fe6507f..7e0bf22 100644
--- a/internal/provider/protocol/regexp_test.go
+++ b/internal/provider/protocol/regexp_test.go
@@ -29,7 +29,6 @@ func TestRegexpName(t *testing.T) {
 	require.Equal(t, "very secret name", p.Name())
 }
 
-//nolint:funlen
 func TestRegexpGetIP(t *testing.T) {
 	ip4 := netip.MustParseAddr("1.2.3.4")
 	ip4As6 := netip.MustParseAddr("::ffff:1.2.3.4")
diff --git a/internal/setter/setter_test.go b/internal/setter/setter_test.go
index e0439c3..3b24f8e 100644
--- a/internal/setter/setter_test.go
+++ b/internal/setter/setter_test.go
@@ -23,7 +23,6 @@ func wrapCancelAsDelete(cancel func()) func(context.Context, pp.PP, ipnet.Type,
 	}
 }
 
-//nolint:funlen
 func TestSet(t *testing.T) {
 	t.Parallel()
 
@@ -277,7 +276,6 @@ func TestSet(t *testing.T) {
 	}
 }
 
-//nolint:funlen
 func TestDelete(t *testing.T) {
 	t.Parallel()