Repository: favonia/cloudflare-ddns
Pull request #815 (2024-07-19 10:36:09)

refactor: remove unnecessary indirection
It's also faster to pass structures directly except for really large ones.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/favonia/cloudflare-ddns/pull/815?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) Report
Attention: Patch coverage is `98.48485%` with `1 line` in your changes missing coverage. Please review.
> Project coverage is 93.94%. Comparing base [(`33181a5`)](https://app.codecov.io/gh/favonia/cloudflare-ddns/commit/33181a5821c0cc0ec71550ca737d5c92156cd74d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) to head [(`70175cd`)](https://app.codecov.io/gh/favonia/cloudflare-ddns/commit/70175cd32a0b527b908da3cf1b3ef88a756f18c6?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia).

| [Files](https://app.codecov.io/gh/favonia/cloudflare-ddns/pull/815?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) | Patch % | Lines |
|---|---|---|
| [cmd/ddns/ddns.go](https://app.codecov.io/gh/favonia/cloudflare-ddns/pull/815?src=pr&el=tree&filepath=cmd%2Fddns%2Fddns.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia#diff-Y21kL2RkbnMvZGRucy5nbw==) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/favonia/cloudflare-ddns/pull/815?src=pr&el=tree&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     #815      +/-   ##
==========================================
+ Coverage   93.93%   93.94%   +0.01%     
==========================================
  Files          53       53              
  Lines        2177     2181       +4     
==========================================
+ Hits         2045     2049       +4     
  Misses        120      120              
  Partials       12       12              
```

| [Flag](https://app.codecov.io/gh/favonia/cloudflare-ddns/pull/815/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/815/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) | `93.94% <98.48%> (+0.01%)` | :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.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/favonia/cloudflare-ddns/pull/815?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 2d78468..de99023 100644
--- a/cmd/ddns/ddns.go
+++ b/cmd/ddns/ddns.go
@@ -32,7 +32,7 @@ func initConfig(ctx context.Context, ppfmt pp.PP) (*config.Config, setter.Setter
 	c := config.Default()
 
 	// Read the config
-	if !c.ReadEnv(ppfmt) || !c.NormalizeConfig(ppfmt) {
+	if !c.ReadEnv(ppfmt) || !c.Normalize(ppfmt) {
 		return c, nil, false
 	}
 
diff --git a/internal/api/cloudflare.go b/internal/api/cloudflare.go
index 44dfa9a..96a1fa3 100644
--- a/internal/api/cloudflare.go
+++ b/internal/api/cloudflare.go
@@ -51,7 +51,7 @@ type CloudflareAuth struct {
 }
 
 // New creates a [CloudflareHandle] from the authentication data.
-func (t *CloudflareAuth) New(_ context.Context, ppfmt pp.PP, cacheExpiration time.Duration) (Handle, bool) {
+func (t CloudflareAuth) New(_ context.Context, ppfmt pp.PP, cacheExpiration time.Duration) (Handle, bool) {
 	handle, err := cloudflare.NewWithAPIToken(t.Token)
 	if err != nil {
 		ppfmt.Errorf(pp.EmojiUserError, "Failed to prepare the Cloudflare authentication: %v", err)
@@ -63,7 +63,7 @@ func (t *CloudflareAuth) New(_ context.Context, ppfmt pp.PP, cacheExpiration tim
 		handle.BaseURL = t.BaseURL
 	}
 
-	h := &CloudflareHandle{
+	h := CloudflareHandle{
 		cf:        handle,
 		accountID: t.AccountID,
 		cache: CloudflareCache{
@@ -81,7 +81,7 @@ func (t *CloudflareAuth) New(_ context.Context, ppfmt pp.PP, cacheExpiration tim
 }
 
 // FlushCache flushes the API cache.
-func (h *CloudflareHandle) FlushCache() {
+func (h CloudflareHandle) FlushCache() {
 	h.cache.sanityCheck.DeleteAll()
 	h.cache.listZones.DeleteAll()
 	h.cache.zoneOfDomain.DeleteAll()
@@ -97,7 +97,7 @@ var errTimeout = errors.New("timeout")
 //
 // Ideally, we should also verify accountID here, but that is impossible without
 // more permissions included in the API token.
-func (h *CloudflareHandle) SanityCheck(ctx context.Context, ppfmt pp.PP) bool {
+func (h CloudflareHandle) SanityCheck(ctx context.Context, ppfmt pp.PP) bool {
 	if valid := h.cache.sanityCheck.Get(struct{}{}); valid != nil {
 		return valid.Value()
 	}
@@ -146,12 +146,12 @@ permanently:
 	return ok
 }
 
-func (h *CloudflareHandle) forcePassSanityCheck() {
+func (h CloudflareHandle) forcePassSanityCheck() {
 	h.cache.sanityCheck.Set(struct{}{}, true, ttlcache.DefaultTTL)
 }
 
 // ListZones returns a list of zone IDs with the zone name.
-func (h *CloudflareHandle) ListZones(ctx context.Context, ppfmt pp.PP, name string) ([]string, bool) {
+func (h CloudflareHandle) ListZones(ctx context.Context, ppfmt pp.PP, name string) ([]string, bool) {
 	// WithZoneFilters does not work with the empty zone name,
 	// and the owner of the DNS root zone will not be managed by Cloudflare anyways!
 	if name == "" {
@@ -202,13 +202,13 @@ func (h *CloudflareHandle) ListZones(ctx context.Context, ppfmt pp.PP, name stri
 }
 
 // ZoneOfDomain finds the active zone ID governing a particular domain.
-func (h *CloudflareHandle) ZoneOfDomain(ctx context.Context, ppfmt pp.PP, domain domain.Domain) (string, bool) {
+func (h CloudflareHandle) ZoneOfDomain(ctx context.Context, ppfmt pp.PP, domain domain.Domain) (string, bool) {
 	if id := h.cache.zoneOfDomain.Get(domain.DNSNameASCII()); id != nil {
 		return id.Value(), true
 	}
 
 zoneSearch:
-	for s := domain.Split(); s.IsValid(); s.Next() {
+	for s := domain.Split(); s.IsValid(); s = s.Next() {
 		zoneName := s.ZoneNameASCII()
 		zones, ok := h.ListZones(ctx, ppfmt, zoneName)
 		if !ok {
@@ -241,7 +241,7 @@ zoneSearch:
 
 // ListRecords lists all matching DNS records. The second return value indicates whether
 // the lists are from cached responses.
-func (h *CloudflareHandle) ListRecords(ctx context.Context, ppfmt pp.PP,
+func (h CloudflareHandle) ListRecords(ctx context.Context, ppfmt pp.PP,
 	domain domain.Domain, ipNet ipnet.Type,
 ) (map[string]netip.Addr, bool, bool) {
 	if rmap := h.cache.listRecords[ipNet].Get(domain.DNSNameASCII()); rmap != nil {
@@ -283,7 +283,7 @@ func (h *CloudflareHandle) ListRecords(ctx context.Context, ppfmt pp.PP,
 }
 
 // DeleteRecord deletes one DNS record.
-func (h *CloudflareHandle) DeleteRecord(ctx context.Context, ppfmt pp.PP,
+func (h CloudflareHandle) DeleteRecord(ctx context.Context, ppfmt pp.PP,
 	domain domain.Domain, ipNet ipnet.Type, id string,
 ) bool {
 	zone, ok := h.ZoneOfDomain(ctx, ppfmt, domain)
@@ -311,7 +311,7 @@ func (h *CloudflareHandle) DeleteRecord(ctx context.Context, ppfmt pp.PP,
 }
 
 // UpdateRecord updates one DNS record.
-func (h *CloudflareHandle) UpdateRecord(ctx context.Context, ppfmt pp.PP,
+func (h CloudflareHandle) UpdateRecord(ctx context.Context, ppfmt pp.PP,
 	domain domain.Domain, ipNet ipnet.Type, id string, ip netip.Addr,
 ) bool {
 	zone, ok := h.ZoneOfDomain(ctx, ppfmt, domain)
@@ -345,7 +345,7 @@ func (h *CloudflareHandle) UpdateRecord(ctx context.Context, ppfmt pp.PP,
 }
 
 // CreateRecord creates one DNS record.
-func (h *CloudflareHandle) CreateRecord(ctx context.Context, ppfmt pp.PP,
+func (h CloudflareHandle) CreateRecord(ctx context.Context, ppfmt pp.PP,
 	domain domain.Domain, ipNet ipnet.Type, ip netip.Addr, ttl TTL, proxied bool, recordComment string,
 ) (string, bool) {
 	zone, ok := h.ZoneOfDomain(ctx, ppfmt, domain)
diff --git a/internal/config/config_read.go b/internal/config/config_read.go
index 57d8063..92eee23 100644
--- a/internal/config/config_read.go
+++ b/internal/config/config_read.go
@@ -12,7 +12,7 @@ import (
 // - timezone (TZ)
 // - privileges-related ones (PGID and PUID)
 // - output-related ones (QUIET and EMOJI)
-// One should subsequently call [Config.NormalizeConfig] to restore invariants across fields.
+// One should subsequently call [Config.Normalize] to restore invariants across fields.
 func (c *Config) ReadEnv(ppfmt pp.PP) bool {
 	if ppfmt.IsEnabledFor(pp.Info) {
 		ppfmt.Infof(pp.EmojiEnvVars, "Reading settings . . .")
@@ -40,11 +40,11 @@ func (c *Config) ReadEnv(ppfmt pp.PP) bool {
 	return true
 }
 
-// NormalizeConfig checks and normalizes the fields [Config.Provider], [Config.Proxied], and [Config.DeleteOnStop].
+// 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) NormalizeConfig(ppfmt pp.PP) bool {
+func (c *Config) Normalize(ppfmt pp.PP) bool {
 	if ppfmt.IsEnabledFor(pp.Info) {
 		ppfmt.Infof(pp.EmojiEnvVars, "Checking settings . . .")
 		ppfmt = ppfmt.IncIndent()
diff --git a/internal/domain/base.go b/internal/domain/base.go
index 360173c..f1d5043 100644
--- a/internal/domain/base.go
+++ b/internal/domain/base.go
@@ -11,7 +11,7 @@ type Splitter interface {
 	ZoneNameASCII() string
 
 	// Next moves the cursor to the next possible splitting point, which might end up being invalid.
-	Next()
+	Next() Splitter
 }
 
 // A Domain represents a domain name to update.
diff --git a/internal/domain/fqdn.go b/internal/domain/fqdn.go
index c963e31..1915f0d 100644
--- a/internal/domain/fqdn.go
+++ b/internal/domain/fqdn.go
@@ -22,7 +22,7 @@ type FQDNSplitter struct {
 
 // Split constructs a [FQDNSplitter] for the FQDN.
 func (f FQDN) Split() Splitter {
-	return &FQDNSplitter{
+	return FQDNSplitter{
 		domain:    string(f),
 		cursor:    0,
 		exhausted: false,
@@ -30,22 +30,24 @@ func (f FQDN) Split() Splitter {
 }
 
 // IsValid checks whether the cursor is still valid.
-func (s *FQDNSplitter) IsValid() bool { return !s.exhausted }
+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:] }
+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() {
+func (s FQDNSplitter) Next() Splitter {
+	next := s
 	if s.cursor == len(s.domain) {
-		s.exhausted = true
+		next.exhausted = true
 	} else {
 		shift := strings.IndexRune(s.ZoneNameASCII(), '.')
 		if shift == -1 {
-			s.cursor = len(s.domain)
+			next.cursor = len(s.domain)
 		} else {
-			s.cursor += shift + 1
+			next.cursor += shift + 1
 		}
 	}
+	return next
 }
diff --git a/internal/domain/wildcard.go b/internal/domain/wildcard.go
index f450568..098d47a 100644
--- a/internal/domain/wildcard.go
+++ b/internal/domain/wildcard.go
@@ -33,7 +33,7 @@ type WildcardSplitter struct {
 
 // Split constructs a [Splitter] for a wildcard domain.
 func (w Wildcard) Split() Splitter {
-	return &WildcardSplitter{
+	return WildcardSplitter{
 		domain:    string(w),
 		cursor:    0,
 		exhausted: false,
@@ -41,22 +41,24 @@ func (w Wildcard) Split() Splitter {
 }
 
 // IsValid checks whether the cursor is still valid.
-func (s *WildcardSplitter) IsValid() bool { return !s.exhausted }
+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:] }
+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() {
+func (s WildcardSplitter) Next() Splitter {
+	next := s
 	if s.cursor == len(s.domain) {
-		s.exhausted = true
+		next.exhausted = true
 	} else {
 		shift := strings.IndexRune(s.ZoneNameASCII(), '.')
 		if shift == -1 {
-			s.cursor = len(s.domain)
+			next.cursor = len(s.domain)
 		} else {
-			s.cursor += shift + 1
+			next.cursor += shift + 1
 		}
 	}
+	return next
 }
diff --git a/internal/monitor/healthchecks.go b/internal/monitor/healthchecks.go
index 02c2d34..82bbbf2 100644
--- a/internal/monitor/healthchecks.go
+++ b/internal/monitor/healthchecks.go
@@ -25,7 +25,7 @@ type Healthchecks struct {
 	Timeout time.Duration
 }
 
-var _ Monitor = (*Healthchecks)(nil)
+var _ Monitor = Healthchecks{} //nolint:exhaustruct
 
 const (
 	// HealthchecksDefaultTimeout is the default timeout for a Healthchecks ping.
@@ -34,17 +34,17 @@ const (
 
 // NewHealthchecks creates a new Healthchecks monitor.
 // See https://healthchecks.io/docs/http_api/ for more information.
-func NewHealthchecks(ppfmt pp.PP, rawURL string) (*Healthchecks, bool) {
+func NewHealthchecks(ppfmt pp.PP, rawURL string) (Healthchecks, bool) {
 	u, err := url.Parse(rawURL)
 	if err != nil {
 		ppfmt.Errorf(pp.EmojiUserError, "Failed to parse the Healthchecks URL (redacted)")
-		return nil, false
+		return Healthchecks{}, false //nolint:exhaustruct
 	}
 
 	if !(u.IsAbs() && u.Opaque == "" && u.Host != "" && u.RawQuery == "") {
 		ppfmt.Errorf(pp.EmojiUserError, `The Healthchecks URL (redacted) does not look like a valid URL`)
 		ppfmt.Errorf(pp.EmojiUserError, `A valid example is "https://hc-ping.com/01234567-0123-0123-0123-0123456789abc"`)
-		return nil, false
+		return Healthchecks{}, false //nolint:exhaustruct
 	}
 
 	switch u.Scheme {
@@ -57,10 +57,10 @@ func NewHealthchecks(ppfmt pp.PP, rawURL string) (*Healthchecks, bool) {
 	default:
 		ppfmt.Errorf(pp.EmojiUserError, `The Healthchecks URL (redacted) does not look like a valid URL`)
 		ppfmt.Errorf(pp.EmojiUserError, `A valid example is "https://hc-ping.com/01234567-0123-0123-0123-0123456789abc"`)
-		return nil, false
+		return Healthchecks{}, false //nolint:exhaustruct
 	}
 
-	h := &Healthchecks{
+	h := Healthchecks{
 		BaseURL: u,
 		Timeout: HealthchecksDefaultTimeout,
 	}
@@ -69,7 +69,7 @@ func NewHealthchecks(ppfmt pp.PP, rawURL string) (*Healthchecks, bool) {
 }
 
 // Describe calls the callback with the service name "Healthchecks".
-func (h *Healthchecks) Describe(callback func(service, params string)) {
+func (h Healthchecks) Describe(callback func(service, params string)) {
 	callback("Healthchecks", "(URL redacted)")
 }
 
@@ -102,7 +102,7 @@ Code and body for the slug API:
 
 To support both APIs, we check both (1) whether the status code is 200 and (2) whether the body is "OK".
 */
-func (h *Healthchecks) ping(ctx context.Context, ppfmt pp.PP, endpoint string, message string) bool {
+func (h Healthchecks) ping(ctx context.Context, ppfmt pp.PP, endpoint string, message string) bool {
 	url := h.BaseURL.JoinPath(endpoint)
 
 	endpointDescription := "default (root)"
@@ -154,27 +154,27 @@ func (h *Healthchecks) ping(ctx context.Context, ppfmt pp.PP, endpoint string, m
 }
 
 // Success pings the root endpoint.
-func (h *Healthchecks) Success(ctx context.Context, ppfmt pp.PP, message string) bool {
+func (h Healthchecks) Success(ctx context.Context, ppfmt pp.PP, message string) bool {
 	return h.ping(ctx, ppfmt, "", message)
 }
 
 // Start pings the /start endpoint.
-func (h *Healthchecks) Start(ctx context.Context, ppfmt pp.PP, message string) bool {
+func (h Healthchecks) Start(ctx context.Context, ppfmt pp.PP, message string) bool {
 	return h.ping(ctx, ppfmt, "/start", message)
 }
 
 // Failure pings the /fail endpoint.
-func (h *Healthchecks) Failure(ctx context.Context, ppfmt pp.PP, message string) bool {
+func (h Healthchecks) Failure(ctx context.Context, ppfmt pp.PP, message string) bool {
 	return h.ping(ctx, ppfmt, "/fail", message)
 }
 
 // Log pings the /log endpoint.
-func (h *Healthchecks) Log(ctx context.Context, ppfmt pp.PP, message string) bool {
+func (h Healthchecks) Log(ctx context.Context, ppfmt pp.PP, message string) bool {
 	return h.ping(ctx, ppfmt, "/log", message)
 }
 
 // ExitStatus pings the /number endpoint where number is the exit status.
-func (h *Healthchecks) ExitStatus(ctx context.Context, ppfmt pp.PP, code int, message string) bool {
+func (h Healthchecks) ExitStatus(ctx context.Context, ppfmt pp.PP, code int, message string) bool {
 	if code < 0 || code > 255 {
 		ppfmt.Errorf(pp.EmojiImpossible, "Exit code (%d) not within the range 0-255", code)
 		return false
diff --git a/internal/monitor/uptimekuma.go b/internal/monitor/uptimekuma.go
index 8134ccd..6385b0a 100644
--- a/internal/monitor/uptimekuma.go
+++ b/internal/monitor/uptimekuma.go
@@ -29,7 +29,7 @@ type UptimeKuma struct {
 	Timeout time.Duration
 }
 
-var _ Monitor = (*UptimeKuma)(nil)
+var _ Monitor = UptimeKuma{} //nolint:exhaustruct
 
 const (
 	// UptimeKumaDefaultTimeout is the default timeout for a UptimeKuma ping.
@@ -37,16 +37,16 @@ const (
 )
 
 // NewUptimeKuma creates a new UptimeKuma monitor.
-func NewUptimeKuma(ppfmt pp.PP, rawURL string) (*UptimeKuma, bool) {
+func NewUptimeKuma(ppfmt pp.PP, rawURL string) (UptimeKuma, bool) {
 	u, err := url.Parse(rawURL)
 	if err != nil {
 		ppfmt.Errorf(pp.EmojiUserError, "Failed to parse the Uptime Kuma URL (redacted)")
-		return nil, false
+		return UptimeKuma{}, false //nolint:exhaustruct
 	}
 
 	if !(u.IsAbs() && u.Opaque == "" && u.Host != "") {
 		ppfmt.Errorf(pp.EmojiUserError, `The Uptime Kuma URL (redacted) does not look like a valid URL`)
-		return nil, false
+		return UptimeKuma{}, false //nolint:exhaustruct
 	}
 
 	switch u.Scheme {
@@ -58,7 +58,7 @@ func NewUptimeKuma(ppfmt pp.PP, rawURL string) (*UptimeKuma, bool) {
 
 	default:
 		ppfmt.Errorf(pp.EmojiUserError, `The Uptime Kuma URL (redacted) does not look like a valid URL`)
-		return nil, false
+		return UptimeKuma{}, false //nolint:exhaustruct
 	}
 
 	// By default, the URL provided by Uptime Kuma has this:
@@ -70,7 +70,7 @@ func NewUptimeKuma(ppfmt pp.PP, rawURL string) (*UptimeKuma, bool) {
 		q, err := url.ParseQuery(u.RawQuery)
 		if err != nil {
 			ppfmt.Errorf(pp.EmojiUserError, `The Uptime Kuma URL (redacted) does not look like a valid URL`)
-			return nil, false
+			return UptimeKuma{}, false //nolint:exhaustruct
 		}
 
 		for k, vs := range q {
@@ -90,7 +90,7 @@ func NewUptimeKuma(ppfmt pp.PP, rawURL string) (*UptimeKuma, bool) {
 		u.RawQuery = ""
 	}
 
-	h := &UptimeKuma{
+	h := UptimeKuma{
 		BaseURL: u,
 		Timeout: UptimeKumaDefaultTimeout,
 	}
@@ -99,7 +99,7 @@ func NewUptimeKuma(ppfmt pp.PP, rawURL string) (*UptimeKuma, bool) {
 }
 
 // Describe calls the callback with the service name "Uptime Kuma".
-func (h *UptimeKuma) Describe(callback func(service, params string)) {
+func (h UptimeKuma) Describe(callback func(service, params string)) {
 	callback("Uptime Kuma", "(URL redacted)")
 }
 
@@ -116,7 +116,7 @@ type UptimeKumaRequest struct {
 	Ping   string `url:"ping"`
 }
 
-func (h *UptimeKuma) ping(ctx context.Context, ppfmt pp.PP, param UptimeKumaRequest) bool {
+func (h UptimeKuma) ping(ctx context.Context, ppfmt pp.PP, param UptimeKumaRequest) bool {
 	ctx, cancel := context.WithTimeout(ctx, h.Timeout)
 	defer cancel()
 
@@ -154,17 +154,17 @@ func (h *UptimeKuma) ping(ctx context.Context, ppfmt pp.PP, param UptimeKumaRequ
 // Success pings the server with status=up. Messages are ignored and "OK" is used instead.
 // The reason is that Uptime Kuma seems to show only the first success message
 // and it could be misleading if an outdated message stays in the UI.
-func (h *UptimeKuma) Success(ctx context.Context, ppfmt pp.PP, _message string) bool {
+func (h UptimeKuma) Success(ctx context.Context, ppfmt pp.PP, _message string) bool {
 	return h.ping(ctx, ppfmt, UptimeKumaRequest{Status: "up", Msg: "OK", Ping: ""})
 }
 
 // Start does nothing.
-func (h *UptimeKuma) Start(_ctx context.Context, _ppfmt pp.PP, _message string) bool {
+func (h UptimeKuma) Start(_ctx context.Context, _ppfmt pp.PP, _message string) bool {
 	return true
 }
 
 // Failure pings the server with status=down.
-func (h *UptimeKuma) Failure(ctx context.Context, ppfmt pp.PP, message string) bool {
+func (h UptimeKuma) Failure(ctx context.Context, ppfmt pp.PP, message string) bool {
 	if message == "" {
 		// If we do not send a non-empty message to Uptime Kuma, it seems to
 		// either keep the previous message (even if it was for success) or
@@ -177,12 +177,12 @@ func (h *UptimeKuma) Failure(ctx context.Context, ppfmt pp.PP, message string) b
 }
 
 // Log does nothing.
-func (h *UptimeKuma) Log(_ctx context.Context, _ppfmt pp.PP, _message string) bool {
+func (h UptimeKuma) Log(_ctx context.Context, _ppfmt pp.PP, _message string) bool {
 	return true
 }
 
 // ExitStatus with non-zero triggers [Failure]. Otherwise, it does nothing.
-func (h *UptimeKuma) ExitStatus(ctx context.Context, ppfmt pp.PP, code int, message string) bool {
+func (h UptimeKuma) ExitStatus(ctx context.Context, ppfmt pp.PP, code int, message string) bool {
 	if code != 0 {
 		return h.Failure(ctx, ppfmt, message)
 	}
diff --git a/internal/provider/protocol/doh.go b/internal/provider/protocol/doh.go
index c260f16..fdf04b6 100644
--- a/internal/provider/protocol/doh.go
+++ b/internal/provider/protocol/doh.go
@@ -178,12 +178,12 @@ type DNSOverHTTPS struct {
 }
 
 // Name of the detection protocol.
-func (p *DNSOverHTTPS) Name() string {
+func (p DNSOverHTTPS) Name() string {
 	return p.ProviderName
 }
 
 // GetIP detects the IP address by DNS over HTTPS.
-func (p *DNSOverHTTPS) GetIP(ctx context.Context, ppfmt pp.PP, ipNet ipnet.Type, use1001 bool) (netip.Addr, bool) {
+func (p DNSOverHTTPS) GetIP(ctx context.Context, ppfmt pp.PP, ipNet ipnet.Type, use1001 bool) (netip.Addr, bool) {
 	param, found := p.Param[ipNet]
 	if !found {
 		ppfmt.Warningf(pp.EmojiImpossible, "Unhandled IP network: %s", ipNet.Describe())
@@ -199,4 +199,4 @@ func (p *DNSOverHTTPS) GetIP(ctx context.Context, ppfmt pp.PP, ipNet ipnet.Type,
 }
 
 // ShouldWeCheck1111 returns whether we should check 1.1.1.1.
-func (p *DNSOverHTTPS) ShouldWeCheck1111() bool { return p.Is1111UsedForIP4 }
+func (p DNSOverHTTPS) ShouldWeCheck1111() bool { return p.Is1111UsedForIP4 }
diff --git a/internal/provider/protocol/field.go b/internal/provider/protocol/field.go
index 15d43e6..65735b2 100644
--- a/internal/provider/protocol/field.go
+++ b/internal/provider/protocol/field.go
@@ -49,12 +49,12 @@ type Field struct {
 }
 
 // Name of the detection protocol.
-func (p *Field) Name() string {
+func (p Field) Name() string {
 	return p.ProviderName
 }
 
 // GetIP detects the IP address by parsing the HTTP response.
-func (p *Field) GetIP(ctx context.Context, ppfmt pp.PP, ipNet ipnet.Type, use1001 bool) (netip.Addr, bool) {
+func (p Field) GetIP(ctx context.Context, ppfmt pp.PP, ipNet ipnet.Type, use1001 bool) (netip.Addr, bool) {
 	param, found := p.Param[ipNet]
 	if !found {
 		ppfmt.Warningf(pp.EmojiImpossible, "Unhandled IP network: %s", ipNet.Describe())
@@ -70,4 +70,4 @@ func (p *Field) GetIP(ctx context.Context, ppfmt pp.PP, ipNet ipnet.Type, use100
 }
 
 // ShouldWeCheck1111 returns whether we should check 1.1.1.1.
-func (p *Field) ShouldWeCheck1111() bool { return p.Is1111UsedforIP4 }
+func (p Field) ShouldWeCheck1111() bool { return p.Is1111UsedforIP4 }
diff --git a/internal/provider/protocol/http.go b/internal/provider/protocol/http.go
index 514c10e..090ddad 100644
--- a/internal/provider/protocol/http.go
+++ b/internal/provider/protocol/http.go
@@ -38,12 +38,12 @@ type HTTP struct {
 }
 
 // Name of the detection protocol.
-func (p *HTTP) Name() string {
+func (p HTTP) Name() string {
 	return p.ProviderName
 }
 
 // GetIP detects the IP address by using the HTTP response directly.
-func (p *HTTP) GetIP(ctx context.Context, ppfmt pp.PP, ipNet ipnet.Type, use1001 bool) (netip.Addr, bool) {
+func (p HTTP) GetIP(ctx context.Context, ppfmt pp.PP, ipNet ipnet.Type, use1001 bool) (netip.Addr, bool) {
 	url, found := p.URL[ipNet]
 	if !found {
 		ppfmt.Warningf(pp.EmojiImpossible, "Unhandled IP network: %s", ipNet.Describe())
@@ -59,4 +59,4 @@ func (p *HTTP) GetIP(ctx context.Context, ppfmt pp.PP, ipNet ipnet.Type, use1001
 }
 
 // ShouldWeCheck1111 returns whether we should check 1.1.1.1.
-func (p *HTTP) ShouldWeCheck1111() bool { return p.Is1111UsedForIP4 }
+func (p HTTP) ShouldWeCheck1111() bool { return p.Is1111UsedForIP4 }
diff --git a/internal/provider/protocol/httpcore.go b/internal/provider/protocol/httpcore.go
index 85966d0..7ae6857 100644
--- a/internal/provider/protocol/httpcore.go
+++ b/internal/provider/protocol/httpcore.go
@@ -21,7 +21,7 @@ type httpCore struct {
 	extract           func(pp.PP, []byte) (netip.Addr, bool)
 }
 
-func (h *httpCore) getIP(ctx context.Context, ppfmt pp.PP) (netip.Addr, bool) {
+func (h httpCore) getIP(ctx context.Context, ppfmt pp.PP) (netip.Addr, bool) {
 	var invalidIP netip.Addr
 
 	req, err := retryablehttp.NewRequestWithContext(ctx, h.method, h.url, h.requestBody)
diff --git a/internal/provider/protocol/local.go b/internal/provider/protocol/local.go
index 9468792..811acd6 100644
--- a/internal/provider/protocol/local.go
+++ b/internal/provider/protocol/local.go
@@ -25,13 +25,13 @@ type Local struct {
 }
 
 // Name of the detection protocol.
-func (p *Local) Name() string {
+func (p Local) Name() string {
 	return p.ProviderName
 }
 
 // GetIP detects the IP address by pretending to send an UDP packet.
 // (No actual UDP packets will be sent out.)
-func (p *Local) GetIP(_ctx context.Context, ppfmt pp.PP, ipNet ipnet.Type, use1001 bool) (netip.Addr, bool) {
+func (p Local) GetIP(_ctx context.Context, ppfmt pp.PP, ipNet ipnet.Type, use1001 bool) (netip.Addr, bool) {
 	var invalidIP netip.Addr
 
 	remoteUDPAddr, found := p.RemoteUDPAddr[ipNet]
@@ -53,4 +53,4 @@ func (p *Local) GetIP(_ctx context.Context, ppfmt pp.PP, ipNet ipnet.Type, use10
 }
 
 // ShouldWeCheck1111 returns whether we should check 1.1.1.1.
-func (p *Local) ShouldWeCheck1111() bool { return p.Is1111UsedForIP4 }
+func (p Local) ShouldWeCheck1111() bool { return p.Is1111UsedForIP4 }


Test patch:
diff --git a/internal/api/cloudflare_test.go b/internal/api/cloudflare_test.go
index 897b5f1..15a2d95 100644
--- a/internal/api/cloudflare_test.go
+++ b/internal/api/cloudflare_test.go
@@ -424,7 +424,7 @@ type zonesHandler struct {
 	accessCount  *int
 }
 
-func newZonesHandler(t *testing.T, mux *http.ServeMux, emptyAccountID bool) *zonesHandler {
+func newZonesHandler(t *testing.T, mux *http.ServeMux, emptyAccountID bool) zonesHandler {
 	t.Helper()
 
 	var (
@@ -442,18 +442,18 @@ func newZonesHandler(t *testing.T, mux *http.ServeMux, emptyAccountID bool) *zon
 		handleZones(t, zoneName, zoneStatuses[zoneName], emptyAccountID, w, r)
 	})
 
-	return &zonesHandler{
+	return zonesHandler{
 		mux:          mux,
 		zoneStatuses: &zoneStatuses,
 		accessCount:  &accessCount,
 	}
 }
 
-func (h *zonesHandler) set(zoneStatuses map[string][]string, accessCount int) {
+func (h zonesHandler) set(zoneStatuses map[string][]string, accessCount int) {
 	*(h.zoneStatuses), *(h.accessCount) = zoneStatuses, accessCount
 }
 
-func (h *zonesHandler) isExhausted() bool {
+func (h zonesHandler) isExhausted() bool {
 	return *h.accessCount == 0
 }
 
@@ -464,7 +464,7 @@ func TestListZonesRoot(t *testing.T) {
 
 	_, h := newHandle(t, false, mockPP)
 
-	zones, ok := h.(*api.CloudflareHandle).ListZones(context.Background(), mockPP, "")
+	zones, ok := h.(api.CloudflareHandle).ListZones(context.Background(), mockPP, "")
 	require.True(t, ok)
 	require.Empty(t, zones)
 }
@@ -479,14 +479,14 @@ func TestListZonesTwo(t *testing.T) {
 	zh := newZonesHandler(t, mux, false)
 
 	zh.set(map[string][]string{"test.org": {"active", "active"}}, 1)
-	zones, ok := h.(*api.CloudflareHandle).ListZones(context.Background(), mockPP, "test.org")
+	zones, ok := h.(api.CloudflareHandle).ListZones(context.Background(), mockPP, "test.org")
 	require.True(t, ok)
 	require.Equal(t, mockIDs("test.org", 0, 1), zones)
 	require.True(t, zh.isExhausted())
 
 	zh.set(nil, 0)
 	mockPP = mocks.NewMockPP(mockCtrl)
-	zones, ok = h.(*api.CloudflareHandle).ListZones(context.Background(), mockPP, "test.org")
+	zones, ok = h.(api.CloudflareHandle).ListZones(context.Background(), mockPP, "test.org")
 	require.True(t, ok)
 	require.Equal(t, mockIDs("test.org", 0, 1), zones)
 	require.True(t, zh.isExhausted())
@@ -500,7 +500,7 @@ func TestListZonesTwo(t *testing.T) {
 		"test.org",
 		gomock.Any(),
 	)
-	zones, ok = h.(*api.CloudflareHandle).ListZones(context.Background(), mockPP, "test.org")
+	zones, ok = h.(api.CloudflareHandle).ListZones(context.Background(), mockPP, "test.org")
 	require.False(t, ok)
 	require.Nil(t, zones)
 	require.True(t, zh.isExhausted())
@@ -516,14 +516,14 @@ func TestListZonesEmpty(t *testing.T) {
 	zh := newZonesHandler(t, mux, false)
 
 	zh.set(map[string][]string{}, 1)
-	zones, ok := h.(*api.CloudflareHandle).ListZones(context.Background(), mockPP, "test.org")
+	zones, ok := h.(api.CloudflareHandle).ListZones(context.Background(), mockPP, "test.org")
 	require.True(t, ok)
 	require.Empty(t, zones)
 	require.True(t, zh.isExhausted())
 
 	zh.set(nil, 0) // this should not affect the result due to the caching
 	mockPP = mocks.NewMockPP(mockCtrl)
-	zones, ok = h.(*api.CloudflareHandle).ListZones(context.Background(), mockPP, "test.org")
+	zones, ok = h.(api.CloudflareHandle).ListZones(context.Background(), mockPP, "test.org")
 	require.True(t, ok)
 	require.Empty(t, zones)
 	require.True(t, zh.isExhausted())
@@ -537,7 +537,7 @@ func TestListZonesEmpty(t *testing.T) {
 		"test.org",
 		gomock.Any(),
 	)
-	zones, ok = h.(*api.CloudflareHandle).ListZones(context.Background(), mockPP, "test.org")
+	zones, ok = h.(api.CloudflareHandle).ListZones(context.Background(), mockPP, "test.org")
 	require.False(t, ok)
 	require.Nil(t, zones)
 	require.True(t, zh.isExhausted())
@@ -686,7 +686,7 @@ func TestZoneOfDomain(t *testing.T) {
 			if tc.prepareMockPP != nil {
 				tc.prepareMockPP(mockPP)
 			}
-			zoneID, ok := h.(*api.CloudflareHandle).ZoneOfDomain(context.Background(), mockPP, tc.domain)
+			zoneID, ok := h.(api.CloudflareHandle).ZoneOfDomain(context.Background(), mockPP, tc.domain)
 			require.Equal(t, tc.ok, ok)
 			require.Equal(t, tc.expected, zoneID)
 			require.True(t, zh.isExhausted())
@@ -694,7 +694,7 @@ func TestZoneOfDomain(t *testing.T) {
 			if tc.ok {
 				zh.set(nil, 0)
 				mockPP = mocks.NewMockPP(mockCtrl) // there shouldn't be any messages
-				zoneID, ok = h.(*api.CloudflareHandle).ZoneOfDomain(context.Background(), mockPP, tc.domain)
+				zoneID, ok = h.(api.CloudflareHandle).ZoneOfDomain(context.Background(), mockPP, tc.domain)
 				require.Equal(t, tc.ok, ok)
 				require.Equal(t, tc.expected, zoneID)
 				require.True(t, zh.isExhausted())
@@ -716,7 +716,7 @@ func TestZoneOfDomainInvalid(t *testing.T) {
 		"sub.test.org",
 		gomock.Any(),
 	)
-	zoneID, ok := h.(*api.CloudflareHandle).ZoneOfDomain(context.Background(), mockPP, domain.FQDN("sub.test.org"))
+	zoneID, ok := h.(api.CloudflareHandle).ZoneOfDomain(context.Background(), mockPP, domain.FQDN("sub.test.org"))
 	require.False(t, ok)
 	require.Equal(t, "", zoneID)
 }
diff --git a/internal/config/config_read_test.go b/internal/config/config_read_test.go
index 7fb64e1..25bcf1e 100644
--- a/internal/config/config_read_test.go
+++ b/internal/config/config_read_test.go
@@ -86,7 +86,7 @@ func TestReadEnvEmpty(t *testing.T) {
 }
 
 //nolint:funlen
-func TestNormalizeConfig(t *testing.T) {
+func TestNormalize(t *testing.T) {
 	t.Parallel()
 
 	keyProxied := "PROXIED"
@@ -415,7 +415,7 @@ func TestNormalizeConfig(t *testing.T) {
 			if tc.prepareMockPP != nil {
 				tc.prepareMockPP(mockPP)
 			}
-			ok := cfg.NormalizeConfig(mockPP)
+			ok := cfg.Normalize(mockPP)
 			require.Equal(t, tc.ok, ok)
 			if tc.ok {
 				require.Equal(t, tc.expected, cfg)
diff --git a/internal/config/env_monitor_test.go b/internal/config/env_monitor_test.go
index e1d6ac5..b066205 100644
--- a/internal/config/env_monitor_test.go
+++ b/internal/config/env_monitor_test.go
@@ -35,7 +35,7 @@ func TestReadAndAppendHealthchecksURL(t *testing.T) {
 		"example": {
 			true, "https://hi.org/1234",
 			nil,
-			[]mon{&monitor.Healthchecks{
+			[]mon{monitor.Healthchecks{
 				BaseURL: urlMustParse(t, "https://hi.org/1234"),
 				Timeout: monitor.HealthchecksDefaultTimeout,
 			}},
@@ -45,7 +45,7 @@ func TestReadAndAppendHealthchecksURL(t *testing.T) {
 		"password": {
 			true, "https://me:pass@hi.org/1234",
 			nil,
-			[]mon{&monitor.Healthchecks{
+			[]mon{monitor.Healthchecks{
 				BaseURL: urlMustParse(t, "https://me:pass@hi.org/1234"),
 				Timeout: monitor.HealthchecksDefaultTimeout,
 			}},
@@ -55,7 +55,7 @@ func TestReadAndAppendHealthchecksURL(t *testing.T) {
 		"fragment": {
 			true, "https://hi.org/1234#fragment",
 			nil,
-			[]mon{&monitor.Healthchecks{
+			[]mon{monitor.Healthchecks{
 				BaseURL: urlMustParse(t, "https://hi.org/1234#fragment"),
 				Timeout: monitor.HealthchecksDefaultTimeout,
 			}},
@@ -132,7 +132,7 @@ func TestReadAndAppendUptimeKumaURL(t *testing.T) {
 		"example": {
 			true, "https://hi.org/1234",
 			nil,
-			[]mon{&monitor.UptimeKuma{
+			[]mon{monitor.UptimeKuma{
 				BaseURL: urlMustParse(t, "https://hi.org/1234"),
 				Timeout: monitor.UptimeKumaDefaultTimeout,
 			}},
@@ -142,7 +142,7 @@ func TestReadAndAppendUptimeKumaURL(t *testing.T) {
 		"password": {
 			true, "https://me:pass@hi.org/1234",
 			nil,
-			[]mon{&monitor.UptimeKuma{
+			[]mon{monitor.UptimeKuma{
 				BaseURL: urlMustParse(t, "https://me:pass@hi.org/1234"),
 				Timeout: monitor.UptimeKumaDefaultTimeout,
 			}},
@@ -152,7 +152,7 @@ func TestReadAndAppendUptimeKumaURL(t *testing.T) {
 		"fragment": {
 			true, "https://hi.org/1234#fragment",
 			nil,
-			[]mon{&monitor.UptimeKuma{
+			[]mon{monitor.UptimeKuma{
 				BaseURL: urlMustParse(t, "https://hi.org/1234#fragment"),
 				Timeout: monitor.UptimeKumaDefaultTimeout,
 			}},
@@ -162,7 +162,7 @@ func TestReadAndAppendUptimeKumaURL(t *testing.T) {
 		"query": {
 			true, "https://hi.org/1234?hello=123",
 			nil,
-			[]mon{&monitor.UptimeKuma{
+			[]mon{monitor.UptimeKuma{
 				BaseURL: urlMustParse(t, "https://hi.org/1234"),
 				Timeout: monitor.UptimeKumaDefaultTimeout,
 			}},
diff --git a/internal/domain/fqdn_test.go b/internal/domain/fqdn_test.go
index 9bbbcaa..37e07df 100644
--- a/internal/domain/fqdn_test.go
+++ b/internal/domain/fqdn_test.go
@@ -83,7 +83,7 @@ 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.Next() {
+			for s := domain.FQDN(tc.input).Split(); s.IsValid(); s = s.Next() {
 				rs = append(rs, s.ZoneNameASCII())
 			}
 			require.Equal(t, tc.expected, rs)
diff --git a/internal/domain/wildcard_test.go b/internal/domain/wildcard_test.go
index 551e5a8..4761bb8 100644
--- a/internal/domain/wildcard_test.go
+++ b/internal/domain/wildcard_test.go
@@ -87,7 +87,7 @@ 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.Next() {
+			for s := domain.Wildcard(tc.input).Split(); s.IsValid(); s = s.Next() {
 				rs = append(rs, s.ZoneNameASCII())
 			}
 			require.Equal(t, tc.expected, rs)
diff --git a/internal/monitor/healthchecks_test.go b/internal/monitor/healthchecks_test.go
index 8a4a0b3..259b04c 100644
--- a/internal/monitor/healthchecks_test.go
+++ b/internal/monitor/healthchecks_test.go
@@ -27,7 +27,7 @@ func TestNewHealthchecks(t *testing.T) {
 	mockCtrl := gomock.NewController(t)
 	mockPP := mocks.NewMockPP(mockCtrl)
 	m, ok := monitor.NewHealthchecks(mockPP, rawBaseURL)
-	require.Equal(t, &monitor.Healthchecks{
+	require.Equal(t, monitor.Healthchecks{
 		BaseURL: parsedBaseURL,
 		Timeout: monitor.HealthchecksDefaultTimeout,
 	}, m)
diff --git a/internal/monitor/uptimekuma_test.go b/internal/monitor/uptimekuma_test.go
index fde3b1b..cde773a 100644
--- a/internal/monitor/uptimekuma_test.go
+++ b/internal/monitor/uptimekuma_test.go
@@ -63,12 +63,12 @@ func TestNewUptimeKuma(t *testing.T) {
 			m, ok := monitor.NewUptimeKuma(mockPP, tc.input)
 			require.Equal(t, tc.ok, ok)
 			if ok {
-				require.Equal(t, &monitor.UptimeKuma{
+				require.Equal(t, monitor.UptimeKuma{
 					BaseURL: parsedBaseURL,
 					Timeout: monitor.UptimeKumaDefaultTimeout,
 				}, m)
 			} else {
-				require.Nil(t, m)
+				require.Zero(t, m)
 			}
 		})
 	}
diff --git a/internal/provider/protocol/doh_test.go b/internal/provider/protocol/doh_test.go
index ea8e14c..6f54883 100644
--- a/internal/provider/protocol/doh_test.go
+++ b/internal/provider/protocol/doh_test.go
@@ -22,7 +22,7 @@ import (
 func TestDNSOverHTTPSName(t *testing.T) {
 	t.Parallel()
 
-	p := &protocol.DNSOverHTTPS{
+	p := protocol.DNSOverHTTPS{
 		ProviderName:     "very secret name",
 		Is1111UsedForIP4: false,
 		Param:            nil,