Repository: favonia/cloudflare-ddns
Pull request #820 (2024-07-19 16:31:39)

refactor: code reorganization while working on WAF lists

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/favonia/cloudflare-ddns/pull/820?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 93.94%. Comparing base [(`228779d`)](https://app.codecov.io/gh/favonia/cloudflare-ddns/commit/228779d481908a074c0695c019942fba27a91b0b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) to head [(`6883edc`)](https://app.codecov.io/gh/favonia/cloudflare-ddns/commit/6883edcf9b37278710a7affa8b432090f00977e6?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     #820   +/-   ##
=======================================
  Coverage   93.94%   93.94%           
=======================================
  Files          53       54    +1     
  Lines        2181     2181           
=======================================
  Hits         2049     2049           
  Misses        120      120           
  Partials       12       12           
```

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

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/820?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/.golangci.yaml b/.golangci.yaml
index 90f4f78..2bc4a22 100644
--- a/.golangci.yaml
+++ b/.golangci.yaml
@@ -45,6 +45,7 @@ linters:
     - cyclop # can detect complicated code, but never leads to actual code changes
     - funlen # can detect complicated code, but never leads to actual code changes
     - gocognit # can detect complicated code, but never leads to actual code changes
+    - gocyclo # can detect complicated code, but never leads to actual code changes
     - maintidx # can detect complicated code, but never leads to actual code changes
 
     - depguard # useless; I do not introduce a dependency carelessly
diff --git a/internal/api/base.go b/internal/api/base.go
index 447d8dd..44ed17c 100644
--- a/internal/api/base.go
+++ b/internal/api/base.go
@@ -15,8 +15,8 @@ import (
 
 // A Handle represents a generic API to update DNS records. Currently, the only implementation is Cloudflare.
 type Handle interface {
-	// Perform basic checking. It returns false when we should give up
-	// all future operations.
+	// Perform basic checking (e.g., the validity of tokens).
+	// It returns false when we should give up all future operations.
 	SanityCheck(ctx context.Context, ppfmt pp.PP) bool
 
 	// ListRecords lists all matching DNS records.
diff --git a/internal/api/cloudflare.go b/internal/api/cloudflare.go
index 96a1fa3..30d4077 100644
--- a/internal/api/cloudflare.go
+++ b/internal/api/cloudflare.go
@@ -9,7 +9,6 @@ import (
 	"github.com/cloudflare/cloudflare-go"
 	"github.com/jellydator/ttlcache/v3"
 
-	"github.com/favonia/cloudflare-ddns/internal/domain"
 	"github.com/favonia/cloudflare-ddns/internal/ipnet"
 	"github.com/favonia/cloudflare-ddns/internal/pp"
 )
@@ -149,236 +148,3 @@ permanently:
 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) {
-	// 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 == "" {
-		return []string{}, true
-	}
-
-	if ids := h.cache.listZones.Get(name); ids != nil {
-		return ids.Value(), true
-	}
-
-	res, err := h.cf.ListZonesContext(ctx, cloudflare.WithZoneFilters(name, h.accountID, ""))
-	if err != nil {
-		ppfmt.Warningf(pp.EmojiError, "Failed to check the existence of a zone named %q: %v", name, err)
-		return nil, false
-	}
-
-	// The operation went through. No need to perform any sanity checking in near future!
-	h.forcePassSanityCheck()
-
-	ids := make([]string, 0, len(res.Result))
-	for _, zone := range res.Result {
-		// The list of possible statuses was at https://api.cloudflare.com/#zone-list-zones
-		// but the documentation is missing now.
-		switch zone.Status {
-		case "active": // fully working
-			ids = append(ids, zone.ID)
-		case
-			"deactivated",  // violating term of service, etc.
-			"initializing", // the setup was just started?
-			"moved",        // domain registrar not pointing to Cloudflare
-			"pending":      // the setup was not completed
-			ppfmt.Warningf(pp.EmojiWarning, "Zone %q is %q; your Cloudflare setup is incomplete; some features might not work as expected", name, zone.Status) //nolint:lll
-			ids = append(ids, zone.ID)
-		case
-			"deleted": // archived, pending/moved for too long
-			ppfmt.Infof(pp.EmojiWarning, "Zone %q is %q and thus skipped", name, zone.Status)
-			// skip these
-		default:
-			ppfmt.Warningf(pp.EmojiImpossible, "Zone %q is in an undocumented status %q", name, zone.Status)
-			ppfmt.Warningf(pp.EmojiImpossible, "Please report the bug at https://github.com/favonia/cloudflare-ddns/issues/new") //nolint:lll
-			ids = append(ids, zone.ID)
-		}
-	}
-
-	h.cache.listZones.Set(name, ids, ttlcache.DefaultTTL)
-
-	return ids, true
-}
-
-// 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) {
-	if id := h.cache.zoneOfDomain.Get(domain.DNSNameASCII()); id != nil {
-		return id.Value(), true
-	}
-
-zoneSearch:
-	for s := domain.Split(); s.IsValid(); s = s.Next() {
-		zoneName := s.ZoneNameASCII()
-		zones, ok := h.ListZones(ctx, ppfmt, zoneName)
-		if !ok {
-			return "", false
-		}
-
-		// The operation went through. No need to perform any sanity checking in near future!
-		h.forcePassSanityCheck()
-
-		switch len(zones) {
-		case 0: // len(zones) == 0
-			continue zoneSearch
-		case 1: // len(zones) == 1
-			h.cache.zoneOfDomain.Set(domain.DNSNameASCII(), zones[0], ttlcache.DefaultTTL)
-			return zones[0], true
-		default: // len(zones) > 1
-			ppfmt.Warningf(pp.EmojiImpossible, "Found multiple active zones named %q. Specifying CF_ACCOUNT_ID might help", zoneName)                    //nolint:lll
-			ppfmt.Warningf(pp.EmojiImpossible, "Please consider reporting this rare situation at https://github.com/favonia/cloudflare-ddns/issues/new") //nolint:lll
-			return "", false
-		}
-	}
-
-	ppfmt.Warningf(pp.EmojiError, "Failed to find the zone of %q", domain.Describe())
-	if h.accountID != "" {
-		ppfmt.Infof(pp.EmojiHint, "Double-check the value of CF_ACCOUNT_ID; in most cases, you can leave it blank")
-	}
-
-	return "", false
-}
-
-// 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,
-	domain domain.Domain, ipNet ipnet.Type,
-) (map[string]netip.Addr, bool, bool) {
-	if rmap := h.cache.listRecords[ipNet].Get(domain.DNSNameASCII()); rmap != nil {
-		return rmap.Value(), true, true
-	}
-
-	zone, ok := h.ZoneOfDomain(ctx, ppfmt, domain)
-	if !ok {
-		return nil, false, false
-	}
-
-	// The operation went through. No need to perform any sanity checking in near future!
-	h.forcePassSanityCheck()
-
-	//nolint:exhaustruct // Other fields are intentionally unspecified
-	rs, _, err := h.cf.ListDNSRecords(ctx,
-		cloudflare.ZoneIdentifier(zone),
-		cloudflare.ListDNSRecordsParams{
-			Name: domain.DNSNameASCII(),
-			Type: ipNet.RecordType(),
-		})
-	if err != nil {
-		ppfmt.Warningf(pp.EmojiError, "Failed to retrieve records of %q: %v", domain.Describe(), err)
-		return nil, false, false
-	}
-
-	rmap := map[string]netip.Addr{}
-	for i := range rs {
-		rmap[rs[i].ID], err = netip.ParseAddr(rs[i].Content)
-		if err != nil {
-			ppfmt.Warningf(pp.EmojiImpossible, "Failed to parse the IP address in records of %q: %v", domain.Describe(), err)
-			return nil, false, false
-		}
-	}
-
-	h.cache.listRecords[ipNet].Set(domain.DNSNameASCII(), rmap, ttlcache.DefaultTTL)
-
-	return rmap, false, true
-}
-
-// DeleteRecord deletes one DNS record.
-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)
-	if !ok {
-		return false
-	}
-
-	if err := h.cf.DeleteDNSRecord(ctx, cloudflare.ZoneIdentifier(zone), id); err != nil {
-		ppfmt.Warningf(pp.EmojiError, "Failed to delete a stale %s record of %q (ID: %s): %v",
-			ipNet.RecordType(), domain.Describe(), id, err)
-
-		h.cache.listRecords[ipNet].Delete(domain.DNSNameASCII())
-
-		return false
-	}
-
-	// The operation went through. No need to perform any sanity checking in near future!
-	h.forcePassSanityCheck()
-
-	if rmap := h.cache.listRecords[ipNet].Get(domain.DNSNameASCII()); rmap != nil {
-		delete(rmap.Value(), id)
-	}
-
-	return true
-}
-
-// UpdateRecord updates one DNS record.
-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)
-	if !ok {
-		return false
-	}
-
-	//nolint:exhaustruct // Other fields are intentionally omitted
-	params := cloudflare.UpdateDNSRecordParams{
-		ID:      id,
-		Content: ip.String(),
-	}
-
-	if _, err := h.cf.UpdateDNSRecord(ctx, cloudflare.ZoneIdentifier(zone), params); err != nil {
-		ppfmt.Warningf(pp.EmojiError, "Failed to update a stale %s record of %q (ID: %s): %v",
-			ipNet.RecordType(), domain.Describe(), id, err)
-
-		h.cache.listRecords[ipNet].Delete(domain.DNSNameASCII())
-
-		return false
-	}
-
-	// The operation went through. No need to perform any sanity checking in near future!
-	h.forcePassSanityCheck()
-
-	if rmap := h.cache.listRecords[ipNet].Get(domain.DNSNameASCII()); rmap != nil {
-		rmap.Value()[id] = ip
-	}
-
-	return true
-}
-
-// CreateRecord creates one DNS record.
-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)
-	if !ok {
-		return "", false
-	}
-
-	//nolint:exhaustruct // Other fields are intentionally omitted
-	params := cloudflare.CreateDNSRecordParams{
-		Name:    domain.DNSNameASCII(),
-		Type:    ipNet.RecordType(),
-		Content: ip.String(),
-		TTL:     ttl.Int(),
-		Proxied: &proxied,
-		Comment: recordComment,
-	}
-
-	res, err := h.cf.CreateDNSRecord(ctx, cloudflare.ZoneIdentifier(zone), params)
-	if err != nil {
-		ppfmt.Warningf(pp.EmojiError, "Failed to add a new %s record of %q: %v",
-			ipNet.RecordType(), domain.Describe(), err)
-
-		h.cache.listRecords[ipNet].Delete(domain.DNSNameASCII())
-
-		return "", false
-	}
-
-	// The operation went through. No need to perform any sanity checking in near future!
-	h.forcePassSanityCheck()
-
-	if rmap := h.cache.listRecords[ipNet].Get(domain.DNSNameASCII()); rmap != nil {
-		rmap.Value()[res.ID] = ip
-	}
-
-	return res.ID, true
-}
diff --git a/internal/api/cloudflare_records.go b/internal/api/cloudflare_records.go
new file mode 100644
index 0000000..2d39376
--- /dev/null
+++ b/internal/api/cloudflare_records.go
@@ -0,0 +1,245 @@
+package api
+
+import (
+	"context"
+	"net/netip"
+
+	"github.com/cloudflare/cloudflare-go"
+	"github.com/jellydator/ttlcache/v3"
+
+	"github.com/favonia/cloudflare-ddns/internal/domain"
+	"github.com/favonia/cloudflare-ddns/internal/ipnet"
+	"github.com/favonia/cloudflare-ddns/internal/pp"
+)
+
+// 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) {
+	// 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 == "" {
+		return []string{}, true
+	}
+
+	if ids := h.cache.listZones.Get(name); ids != nil {
+		return ids.Value(), true
+	}
+
+	res, err := h.cf.ListZonesContext(ctx, cloudflare.WithZoneFilters(name, h.accountID, ""))
+	if err != nil {
+		ppfmt.Warningf(pp.EmojiError, "Failed to check the existence of a zone named %q: %v", name, err)
+		return nil, false
+	}
+
+	// The operation went through. No need to perform any sanity checking in near future!
+	h.forcePassSanityCheck()
+
+	ids := make([]string, 0, len(res.Result))
+	for _, zone := range res.Result {
+		// The list of possible statuses was at https://api.cloudflare.com/#zone-list-zones
+		// but the documentation is missing now.
+		switch zone.Status {
+		case "active": // fully working
+			ids = append(ids, zone.ID)
+		case
+			"deactivated",  // violating term of service, etc.
+			"initializing", // the setup was just started?
+			"moved",        // domain registrar not pointing to Cloudflare
+			"pending":      // the setup was not completed
+			ppfmt.Warningf(pp.EmojiWarning, "Zone %q is %q; your Cloudflare setup is incomplete; some features might not work as expected", name, zone.Status) //nolint:lll
+			ids = append(ids, zone.ID)
+		case
+			"deleted": // archived, pending/moved for too long
+			ppfmt.Infof(pp.EmojiWarning, "Zone %q is %q and thus skipped", name, zone.Status)
+			// skip these
+		default:
+			ppfmt.Warningf(pp.EmojiImpossible, "Zone %q is in an undocumented status %q", name, zone.Status)
+			ppfmt.Warningf(pp.EmojiImpossible, "Please report the bug at https://github.com/favonia/cloudflare-ddns/issues/new") //nolint:lll
+			ids = append(ids, zone.ID)
+		}
+	}
+
+	h.cache.listZones.Set(name, ids, ttlcache.DefaultTTL)
+
+	return ids, true
+}
+
+// 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) {
+	if id := h.cache.zoneOfDomain.Get(domain.DNSNameASCII()); id != nil {
+		return id.Value(), true
+	}
+
+zoneSearch:
+	for s := domain.Split(); s.IsValid(); s = s.Next() {
+		zoneName := s.ZoneNameASCII()
+		zones, ok := h.ListZones(ctx, ppfmt, zoneName)
+		if !ok {
+			return "", false
+		}
+
+		// The operation went through. No need to perform any sanity checking in near future!
+		h.forcePassSanityCheck()
+
+		switch len(zones) {
+		case 0: // len(zones) == 0
+			continue zoneSearch
+		case 1: // len(zones) == 1
+			h.cache.zoneOfDomain.Set(domain.DNSNameASCII(), zones[0], ttlcache.DefaultTTL)
+			return zones[0], true
+		default: // len(zones) > 1
+			ppfmt.Warningf(pp.EmojiImpossible, "Found multiple active zones named %q", zoneName)
+			ppfmt.Warningf(pp.EmojiImpossible, "Please report this rare situation at https://github.com/favonia/cloudflare-ddns/issues/new") //nolint:lll
+			return "", false
+		}
+	}
+
+	ppfmt.Warningf(pp.EmojiError, "Failed to find the zone of %q", domain.Describe())
+	if h.accountID != "" {
+		ppfmt.Infof(pp.EmojiHint, "Double-check the value of CF_ACCOUNT_ID; you can usually leave it blank unless you are updating WAF lists") //nolint:lll
+	}
+
+	return "", false
+}
+
+// ListRecords calls cloudflare.ListDNSRecords.
+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 {
+		return rmap.Value(), true, true
+	}
+
+	zone, ok := h.ZoneOfDomain(ctx, ppfmt, domain)
+	if !ok {
+		return nil, false, false
+	}
+
+	// The operation went through. No need to perform any sanity checking in near future!
+	h.forcePassSanityCheck()
+
+	//nolint:exhaustruct // Other fields are intentionally unspecified
+	rs, _, err := h.cf.ListDNSRecords(ctx,
+		cloudflare.ZoneIdentifier(zone),
+		cloudflare.ListDNSRecordsParams{
+			Name: domain.DNSNameASCII(),
+			Type: ipNet.RecordType(),
+		})
+	if err != nil {
+		ppfmt.Warningf(pp.EmojiError, "Failed to retrieve records of %q: %v", domain.Describe(), err)
+		return nil, false, false
+	}
+
+	rmap := map[string]netip.Addr{}
+	for i := range rs {
+		rmap[rs[i].ID], err = netip.ParseAddr(rs[i].Content)
+		if err != nil {
+			ppfmt.Warningf(pp.EmojiImpossible, "Failed to parse the IP address in records of %q: %v", domain.Describe(), err)
+			return nil, false, false
+		}
+	}
+
+	h.cache.listRecords[ipNet].Set(domain.DNSNameASCII(), rmap, ttlcache.DefaultTTL)
+
+	return rmap, false, true
+}
+
+// DeleteRecord calls cloudflare.DeleteDNSRecord.
+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)
+	if !ok {
+		return false
+	}
+
+	if err := h.cf.DeleteDNSRecord(ctx, cloudflare.ZoneIdentifier(zone), id); err != nil {
+		ppfmt.Warningf(pp.EmojiError, "Failed to delete a stale %s record of %q (ID: %s): %v",
+			ipNet.RecordType(), domain.Describe(), id, err)
+
+		h.cache.listRecords[ipNet].Delete(domain.DNSNameASCII())
+
+		return false
+	}
+
+	// The operation went through. No need to perform any sanity checking in near future!
+	h.forcePassSanityCheck()
+
+	if rmap := h.cache.listRecords[ipNet].Get(domain.DNSNameASCII()); rmap != nil {
+		delete(rmap.Value(), id)
+	}
+
+	return true
+}
+
+// UpdateRecord calls cloudflare.UpdateDNSRecord.
+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)
+	if !ok {
+		return false
+	}
+
+	//nolint:exhaustruct // Other fields are intentionally omitted
+	params := cloudflare.UpdateDNSRecordParams{
+		ID:      id,
+		Content: ip.String(),
+	}
+
+	if _, err := h.cf.UpdateDNSRecord(ctx, cloudflare.ZoneIdentifier(zone), params); err != nil {
+		ppfmt.Warningf(pp.EmojiError, "Failed to update a stale %s record of %q (ID: %s): %v",
+			ipNet.RecordType(), domain.Describe(), id, err)
+
+		h.cache.listRecords[ipNet].Delete(domain.DNSNameASCII())
+
+		return false
+	}
+
+	// The operation went through. No need to perform any sanity checking in near future!
+	h.forcePassSanityCheck()
+
+	if rmap := h.cache.listRecords[ipNet].Get(domain.DNSNameASCII()); rmap != nil {
+		rmap.Value()[id] = ip
+	}
+
+	return true
+}
+
+// CreateRecord calls cloudflare.CreateDNSRecord.
+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)
+	if !ok {
+		return "", false
+	}
+
+	//nolint:exhaustruct // Other fields are intentionally omitted
+	params := cloudflare.CreateDNSRecordParams{
+		Name:    domain.DNSNameASCII(),
+		Type:    ipNet.RecordType(),
+		Content: ip.String(),
+		TTL:     ttl.Int(),
+		Proxied: &proxied,
+		Comment: recordComment,
+	}
+
+	res, err := h.cf.CreateDNSRecord(ctx, cloudflare.ZoneIdentifier(zone), params)
+	if err != nil {
+		ppfmt.Warningf(pp.EmojiError, "Failed to add a new %s record of %q: %v",
+			ipNet.RecordType(), domain.Describe(), err)
+
+		h.cache.listRecords[ipNet].Delete(domain.DNSNameASCII())
+
+		return "", false
+	}
+
+	// The operation went through. No need to perform any sanity checking in near future!
+	h.forcePassSanityCheck()
+
+	if rmap := h.cache.listRecords[ipNet].Get(domain.DNSNameASCII()); rmap != nil {
+		rmap.Value()[res.ID] = ip
+	}
+
+	return res.ID, true
+}
diff --git a/internal/config/config_print.go b/internal/config/config_print.go
index 2ed8603..2479452 100644
--- a/internal/config/config_print.go
+++ b/internal/config/config_print.go
@@ -65,11 +65,11 @@ func (c *Config) Print(ppfmt pp.PP) {
 
 	section("Domains and IP providers:")
 	if c.Provider[ipnet.IP4] != nil {
-		item("IPv4 domains:", "%s", describeDomains(c.Domains[ipnet.IP4]))
+		item("IPv4-enabled domains:", "%s", describeDomains(c.Domains[ipnet.IP4]))
 		item("IPv4 provider:", "%s", provider.Name(c.Provider[ipnet.IP4]))
 	}
 	if c.Provider[ipnet.IP6] != nil {
-		item("IPv6 domains:", "%s", describeDomains(c.Domains[ipnet.IP6]))
+		item("IPv6-enabled domains:", "%s", describeDomains(c.Domains[ipnet.IP6]))
 		item("IPv6 provider:", "%s", provider.Name(c.Provider[ipnet.IP6]))
 	}
 
diff --git a/internal/setter/setter.go b/internal/setter/setter.go
index 0ddfd6b..7cbe6f9 100644
--- a/internal/setter/setter.go
+++ b/internal/setter/setter.go
@@ -22,7 +22,7 @@ func New(_ppfmt pp.PP, handle api.Handle) (Setter, bool) {
 	}, true
 }
 
-// SanityCheck calls [api.Handle.SanityCheck] on the handle.
+// SanityCheck calls [api.Handle.SanityCheck].
 func (s setter) SanityCheck(ctx context.Context, ppfmt pp.PP) bool {
 	return s.Handle.SanityCheck(ctx, ppfmt)
 }


Test patch:
diff --git a/internal/api/cloudflare_records_test.go b/internal/api/cloudflare_records_test.go
new file mode 100644
index 0000000..aaf3ccb
--- /dev/null
+++ b/internal/api/cloudflare_records_test.go
@@ -0,0 +1,1050 @@
+package api_test
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"net/netip"
+	"net/url"
+	"strconv"
+	"testing"
+
+	"github.com/cloudflare/cloudflare-go"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"go.uber.org/mock/gomock"
+
+	"github.com/favonia/cloudflare-ddns/internal/api"
+	"github.com/favonia/cloudflare-ddns/internal/domain"
+	"github.com/favonia/cloudflare-ddns/internal/ipnet"
+	"github.com/favonia/cloudflare-ddns/internal/mocks"
+	"github.com/favonia/cloudflare-ddns/internal/pp"
+)
+
+func mockZone(name string, i int, status string) *cloudflare.Zone {
+	return &cloudflare.Zone{ //nolint:exhaustruct
+		ID:     mockID(name, i),
+		Name:   name,
+		Status: status,
+	}
+}
+
+const (
+	zonePageSize      = 50
+	dnsRecordPageSize = 100
+)
+
+func mockZonesResponse(zoneName string, zoneStatuses []string) *cloudflare.ZonesResponse {
+	numZones := len(zoneStatuses)
+
+	if numZones > zonePageSize {
+		panic("mockZonesResponse got too many zone names")
+	}
+
+	zones := make([]cloudflare.Zone, len(zoneStatuses))
+	for i, status := range zoneStatuses {
+		zones[i] = *mockZone(zoneName, i, status)
+	}
+
+	return &cloudflare.ZonesResponse{
+		Result: zones,
+		ResultInfo: cloudflare.ResultInfo{
+			Page:       1,
+			PerPage:    zonePageSize,
+			TotalPages: (numZones + zonePageSize - 1) / zonePageSize,
+			Count:      numZones,
+			Total:      numZones,
+			Cursor:     "",
+			Cursors:    cloudflare.ResultInfoCursors{}, //nolint:exhaustruct
+		},
+		Response: cloudflare.Response{
+			Success:  true,
+			Errors:   []cloudflare.ResponseInfo{},
+			Messages: []cloudflare.ResponseInfo{},
+		},
+	}
+}
+
+func handleZones(
+	t *testing.T, zoneName string, zoneStatuses []string, emptyAccountID bool, w http.ResponseWriter, r *http.Request,
+) {
+	t.Helper()
+
+	require.Equal(t, http.MethodGet, r.Method)
+	require.Equal(t, []string{mockAuthString}, r.Header["Authorization"])
+	if emptyAccountID {
+		require.Equal(t, url.Values{
+			"name":     {zoneName},
+			"per_page": {strconv.Itoa(zonePageSize)},
+		}, r.URL.Query())
+	} else {
+		require.Equal(t, url.Values{
+			"account.id": {mockAccount},
+			"name":       {zoneName},
+			"per_page":   {strconv.Itoa(zonePageSize)},
+		}, r.URL.Query())
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	err := json.NewEncoder(w).Encode(mockZonesResponse(zoneName, zoneStatuses))
+	require.NoError(t, err)
+}
+
+type zonesHandler struct {
+	mux          *http.ServeMux
+	zoneStatuses *map[string][]string
+	accessCount  *int
+}
+
+func newZonesHandler(t *testing.T, mux *http.ServeMux, emptyAccountID bool) zonesHandler {
+	t.Helper()
+
+	var (
+		zoneStatuses map[string][]string
+		accessCount  int
+	)
+
+	mux.HandleFunc("/zones", func(w http.ResponseWriter, r *http.Request) {
+		if accessCount <= 0 {
+			panic(http.ErrAbortHandler)
+		}
+		accessCount--
+
+		zoneName := r.URL.Query().Get("name")
+		handleZones(t, zoneName, zoneStatuses[zoneName], emptyAccountID, w, r)
+	})
+
+	return zonesHandler{
+		mux:          mux,
+		zoneStatuses: &zoneStatuses,
+		accessCount:  &accessCount,
+	}
+}
+
+func (h zonesHandler) set(zoneStatuses map[string][]string, accessCount int) {
+	*(h.zoneStatuses), *(h.accessCount) = zoneStatuses, accessCount
+}
+
+func (h zonesHandler) isExhausted() bool {
+	return *h.accessCount == 0
+}
+
+func TestListZonesRoot(t *testing.T) {
+	t.Parallel()
+	mockCtrl := gomock.NewController(t)
+	mockPP := mocks.NewMockPP(mockCtrl)
+
+	_, h := newHandle(t, false, mockPP)
+
+	zones, ok := h.(api.CloudflareHandle).ListZones(context.Background(), mockPP, "")
+	require.True(t, ok)
+	require.Empty(t, zones)
+}
+
+func TestListZonesTwo(t *testing.T) {
+	t.Parallel()
+	mockCtrl := gomock.NewController(t)
+	mockPP := mocks.NewMockPP(mockCtrl)
+
+	mux, h := newHandle(t, false, mockPP)
+
+	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")
+	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")
+	require.True(t, ok)
+	require.Equal(t, mockIDs("test.org", 0, 1), zones)
+	require.True(t, zh.isExhausted())
+
+	h.FlushCache()
+
+	mockPP = mocks.NewMockPP(mockCtrl)
+	mockPP.EXPECT().Warningf(
+		pp.EmojiError,
+		"Failed to check the existence of a zone named %q: %v",
+		"test.org",
+		gomock.Any(),
+	)
+	zones, ok = h.(api.CloudflareHandle).ListZones(context.Background(), mockPP, "test.org")
+	require.False(t, ok)
+	require.Nil(t, zones)
+	require.True(t, zh.isExhausted())
+}
+
+func TestListZonesEmpty(t *testing.T) {
+	t.Parallel()
+	mockCtrl := gomock.NewController(t)
+	mockPP := mocks.NewMockPP(mockCtrl)
+
+	mux, h := newHandle(t, false, mockPP)
+
+	zh := newZonesHandler(t, mux, false)
+
+	zh.set(map[string][]string{}, 1)
+	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")
+	require.True(t, ok)
+	require.Empty(t, zones)
+	require.True(t, zh.isExhausted())
+
+	h.FlushCache()
+
+	mockPP = mocks.NewMockPP(mockCtrl)
+	mockPP.EXPECT().Warningf(
+		pp.EmojiError,
+		"Failed to check the existence of a zone named %q: %v",
+		"test.org",
+		gomock.Any(),
+	)
+	zones, ok = h.(api.CloudflareHandle).ListZones(context.Background(), mockPP, "test.org")
+	require.False(t, ok)
+	require.Nil(t, zones)
+	require.True(t, zh.isExhausted())
+}
+
+//nolint:funlen
+func TestZoneOfDomain(t *testing.T) {
+	t.Parallel()
+
+	for name, tc := range map[string]struct {
+		emptyAccountID bool
+		zone           string
+		domain         domain.Domain
+		zoneStatuses   map[string][]string
+		accessCount    int
+		expected       string
+		ok             bool
+		prepareMockPP  func(*mocks.MockPP)
+	}{
+		"root":     {false, "test.org", domain.FQDN("test.org"), map[string][]string{"test.org": {"active"}}, 1, mockID("test.org", 0), true, nil},     //nolint:lll
+		"wildcard": {false, "test.org", domain.Wildcard("test.org"), map[string][]string{"test.org": {"active"}}, 1, mockID("test.org", 0), true, nil}, //nolint:lll
+		"one":      {false, "test.org", domain.FQDN("sub.test.org"), map[string][]string{"test.org": {"active"}}, 2, mockID("test.org", 0), true, nil}, //nolint:lll
+		"none": {
+			false, "test.org", domain.FQDN("sub.test.org"),
+			map[string][]string{},
+			3, "", false,
+			func(m *mocks.MockPP) {
+				gomock.InOrder(
+					m.EXPECT().Warningf(pp.EmojiError, "Failed to find the zone of %q", "sub.test.org"),
+					m.EXPECT().Infof(pp.EmojiHint, "Double-check the value of CF_ACCOUNT_ID; you can usually leave it blank unless you are updating WAF lists"), //nolint:lll
+				)
+			},
+		},
+		"none/wildcard": {
+			false, "test.org", domain.Wildcard("test.org"),
+			map[string][]string{},
+			2, "", false,
+			func(m *mocks.MockPP) {
+				gomock.InOrder(
+					m.EXPECT().Warningf(pp.EmojiError, "Failed to find the zone of %q", "*.test.org"),
+					m.EXPECT().Infof(pp.EmojiHint, "Double-check the value of CF_ACCOUNT_ID; you can usually leave it blank unless you are updating WAF lists"), //nolint:lll
+				)
+			},
+		},
+		"multiple": {
+			false, "test.org", domain.FQDN("sub.test.org"),
+			map[string][]string{"test.org": {"active", "active"}},
+			2, "", false,
+			func(m *mocks.MockPP) {
+				gomock.InOrder(
+					m.EXPECT().Warningf(
+						pp.EmojiImpossible,
+						"Found multiple active zones named %q",
+						"test.org",
+					),
+					m.EXPECT().Warningf(
+						pp.EmojiImpossible,
+						"Please report this rare situation at https://github.com/favonia/cloudflare-ddns/issues/new",
+					),
+				)
+			},
+		},
+		"multiple/wildcard": {
+			false, "test.org", domain.Wildcard("test.org"),
+			map[string][]string{"test.org": {"active", "active"}},
+			1, "", false,
+			func(m *mocks.MockPP) {
+				gomock.InOrder(
+					m.EXPECT().Warningf(
+						pp.EmojiImpossible,
+						"Found multiple active zones named %q",
+						"test.org",
+					),
+					m.EXPECT().Warningf(
+						pp.EmojiImpossible,
+						"Please report this rare situation at https://github.com/favonia/cloudflare-ddns/issues/new",
+					),
+				)
+			},
+		},
+		"deleted": {
+			false, "test.org", domain.FQDN("test.org"),
+			map[string][]string{"test.org": {"deleted"}},
+			2, "", false,
+			func(m *mocks.MockPP) {
+				gomock.InOrder(
+					m.EXPECT().Infof(pp.EmojiWarning, "Zone %q is %q and thus skipped", "test.org", "deleted"),
+					m.EXPECT().Warningf(pp.EmojiError, "Failed to find the zone of %q", "test.org"),
+					m.EXPECT().Infof(pp.EmojiHint, "Double-check the value of CF_ACCOUNT_ID; you can usually leave it blank unless you are updating WAF lists"), //nolint:lll
+				)
+			},
+		},
+		"deleted/empty-account": {
+			true, "test.org", domain.FQDN("test.org"),
+			map[string][]string{"test.org": {"deleted"}},
+			2, "", false,
+			func(m *mocks.MockPP) {
+				gomock.InOrder(
+					m.EXPECT().Infof(pp.EmojiWarning, "Zone %q is %q and thus skipped", "test.org", "deleted"),
+					m.EXPECT().Warningf(pp.EmojiError, "Failed to find the zone of %q", "test.org"),
+				)
+			},
+		},
+		"pending": {
+			false, "test.org", domain.FQDN("test.org"),
+			map[string][]string{"test.org": {"pending"}},
+			1, mockID("test.org", 0), true,
+			func(m *mocks.MockPP) {
+				gomock.InOrder(
+					m.EXPECT().Warningf(pp.EmojiWarning, "Zone %q is %q; your Cloudflare setup is incomplete; some features might not work as expected", "test.org", "pending"), //nolint:lll
+				)
+			},
+		},
+		"initializing": {
+			false, "test.org", domain.FQDN("test.org"),
+			map[string][]string{"test.org": {"initializing"}},
+			1, mockID("test.org", 0), true,
+			func(m *mocks.MockPP) {
+				gomock.InOrder(
+					m.EXPECT().Warningf(pp.EmojiWarning, "Zone %q is %q; your Cloudflare setup is incomplete; some features might not work as expected", "test.org", "initializing"), //nolint:lll
+				)
+			},
+		},
+		"undocumented": {
+			false, "test.org", domain.FQDN("test.org"),
+			map[string][]string{"test.org": {"some-undocumented-status"}},
+			1, mockID("test.org", 0), true,
+			func(m *mocks.MockPP) {
+				gomock.InOrder(
+					m.EXPECT().Warningf(pp.EmojiImpossible, "Zone %q is in an undocumented status %q", "test.org", "some-undocumented-status"), //nolint:lll
+					m.EXPECT().Warningf(pp.EmojiImpossible, "Please report the bug at https://github.com/favonia/cloudflare-ddns/issues/new"),  //nolint:lll
+				)
+			},
+		},
+	} {
+		t.Run(name, func(t *testing.T) {
+			t.Parallel()
+			mockCtrl := gomock.NewController(t)
+			mockPP := mocks.NewMockPP(mockCtrl)
+
+			mux, h := newHandle(t, tc.emptyAccountID, mockPP)
+
+			zh := newZonesHandler(t, mux, tc.emptyAccountID)
+
+			zh.set(tc.zoneStatuses, tc.accessCount)
+			if tc.prepareMockPP != nil {
+				tc.prepareMockPP(mockPP)
+			}
+			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())
+
+			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)
+				require.Equal(t, tc.ok, ok)
+				require.Equal(t, tc.expected, zoneID)
+				require.True(t, zh.isExhausted())
+			}
+		})
+	}
+}
+
+func TestZoneOfDomainInvalid(t *testing.T) {
+	t.Parallel()
+	mockCtrl := gomock.NewController(t)
+	mockPP := mocks.NewMockPP(mockCtrl)
+
+	_, h := newHandle(t, false, mockPP)
+
+	mockPP.EXPECT().Warningf(
+		pp.EmojiError,
+		"Failed to check the existence of a zone named %q: %v",
+		"sub.test.org",
+		gomock.Any(),
+	)
+	zoneID, ok := h.(api.CloudflareHandle).ZoneOfDomain(context.Background(), mockPP, domain.FQDN("sub.test.org"))
+	require.False(t, ok)
+	require.Equal(t, "", zoneID)
+}
+
+func mockDNSRecord(id string, ipNet ipnet.Type, name string, ip string) *cloudflare.DNSRecord {
+	return &cloudflare.DNSRecord{ //nolint:exhaustruct
+		ID:      id,
+		Type:    ipNet.RecordType(),
+		Name:    name,
+		Content: ip,
+	}
+}
+
+func mockDNSListResponse(ipNet ipnet.Type, name string, ips map[string]string) *cloudflare.DNSListResponse {
+	if len(ips) > 100 {
+		panic("mockDNSResponse got too many IPs")
+	}
+
+	rs := make([]cloudflare.DNSRecord, 0, len(ips))
+	for id, ip := range ips {
+		rs = append(rs, *mockDNSRecord(id, ipNet, name, ip))
+	}
+
+	return &cloudflare.DNSListResponse{
+		Result: rs,
+		ResultInfo: cloudflare.ResultInfo{
+			Page:       1,
+			PerPage:    100,
+			TotalPages: (len(ips) + 99) / 100,
+			Count:      len(ips),
+			Total:      len(ips),
+			Cursor:     "",
+			Cursors:    cloudflare.ResultInfoCursors{}, //nolint:exhaustruct
+		},
+		Response: cloudflare.Response{
+			Success:  true,
+			Errors:   []cloudflare.ResponseInfo{},
+			Messages: []cloudflare.ResponseInfo{},
+		},
+	}
+}
+
+func mockDNSListResponseFromAddr(ipNet ipnet.Type, name string, ips map[string]netip.Addr) *cloudflare.DNSListResponse {
+	if len(ips) > 100 {
+		panic("mockDNSResponse got too many IPs")
+	}
+
+	strings := make(map[string]string)
+
+	for id, ip := range ips {
+		strings[id] = ip.String()
+	}
+
+	return mockDNSListResponse(ipNet, name, strings)
+}
+
+//nolint:dupl
+func TestListRecords(t *testing.T) {
+	t.Parallel()
+	mockCtrl := gomock.NewController(t)
+	mockPP := mocks.NewMockPP(mockCtrl)
+
+	mux, h := newHandle(t, false, mockPP)
+
+	zh := newZonesHandler(t, mux, false)
+	zh.set(map[string][]string{"test.org": {"active"}}, 2)
+
+	var (
+		ipNet       ipnet.Type
+		ips         map[string]netip.Addr
+		accessCount int
+	)
+
+	mux.HandleFunc(fmt.Sprintf("/zones/%s/dns_records", mockID("test.org", 0)),
+		func(w http.ResponseWriter, r *http.Request) {
+			if accessCount <= 0 {
+				panic(http.ErrAbortHandler)
+			}
+			accessCount--
+
+			if !assert.Equal(t, http.MethodGet, r.Method) ||
+				!assert.Equal(t, []string{mockAuthString}, r.Header["Authorization"]) ||
+				!assert.Equal(t, url.Values{
+					"name":     {"sub.test.org"},
+					"page":     {"1"},
+					"per_page": {strconv.Itoa(dnsRecordPageSize)},
+					"type":     {ipNet.RecordType()},
+				}, r.URL.Query()) {
+				panic(http.ErrAbortHandler)
+			}
+
+			w.Header().Set("Content-Type", "application/json")
+			if err := json.NewEncoder(w).Encode(
+				mockDNSListResponseFromAddr(ipNet, "test.org", ips),
+			); !assert.NoError(t, err) {
+				panic(http.ErrAbortHandler)
+			}
+		})
+
+	expected := map[string]netip.Addr{"record1": mustIP("::1"), "record2": mustIP("::2")}
+	ipNet, ips, accessCount = ipnet.IP6, expected, 1
+	ips, cached, ok := h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6)
+	require.True(t, ok)
+	require.False(t, cached)
+	require.Equal(t, expected, ips)
+	require.Equal(t, 0, accessCount)
+
+	// testing the caching
+	mockPP = mocks.NewMockPP(mockCtrl)
+	ips, cached, ok = h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6)
+	require.True(t, ok)
+	require.True(t, cached)
+	require.Equal(t, expected, ips)
+}
+
+//nolint:funlen
+func TestListRecordsInvalidIPAddress(t *testing.T) {
+	t.Parallel()
+	mockCtrl := gomock.NewController(t)
+	mockPP := mocks.NewMockPP(mockCtrl)
+
+	mux, h := newHandle(t, false, mockPP)
+
+	zh := newZonesHandler(t, mux, false)
+	zh.set(map[string][]string{"test.org": {"active"}}, 2)
+
+	var (
+		ipNet       ipnet.Type
+		ips         map[string]netip.Addr
+		accessCount int
+	)
+
+	mux.HandleFunc(fmt.Sprintf("/zones/%s/dns_records", mockID("test.org", 0)),
+		func(w http.ResponseWriter, r *http.Request) {
+			if accessCount <= 0 {
+				panic(http.ErrAbortHandler)
+			}
+			accessCount--
+
+			if !assert.Equal(t, http.MethodGet, r.Method) ||
+				!assert.Equal(t, []string{mockAuthString}, r.Header["Authorization"]) ||
+				!assert.Equal(t, url.Values{
+					"name":     {"sub.test.org"},
+					"page":     {"1"},
+					"per_page": {strconv.Itoa(dnsRecordPageSize)},
+					"type":     {ipNet.RecordType()},
+				}, r.URL.Query()) {
+				panic(http.ErrAbortHandler)
+			}
+
+			w.Header().Set("Content-Type", "application/json")
+			if err := json.NewEncoder(w).Encode(mockDNSListResponse(ipNet, "test.org",
+				map[string]string{"record1": "::1", "record2": "NOT AN IP"},
+			)); !assert.NoError(t, err) {
+				panic(http.ErrAbortHandler)
+			}
+		})
+
+	ipNet, accessCount = ipnet.IP6, 1
+	mockPP.EXPECT().Warningf(
+		pp.EmojiImpossible,
+		"Failed to parse the IP address in records of %q: %v",
+		"sub.test.org",
+		gomock.Any(),
+	)
+	ips, cached, ok := h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6)
+	require.False(t, ok)
+	require.False(t, cached)
+	require.Nil(t, ips)
+	require.Equal(t, 0, accessCount)
+
+	// testing the (no) caching
+	mockPP = mocks.NewMockPP(mockCtrl)
+	mockPP.EXPECT().Warningf(
+		pp.EmojiError,
+		"Failed to retrieve records of %q: %v",
+		"sub.test.org",
+		gomock.Any(),
+	)
+	ips, cached, ok = h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6)
+	require.False(t, ok)
+	require.False(t, cached)
+	require.Nil(t, ips)
+	require.Equal(t, 0, accessCount)
+}
+
+//nolint:dupl
+func TestListRecordsWildcard(t *testing.T) {
+	t.Parallel()
+	mockCtrl := gomock.NewController(t)
+	mockPP := mocks.NewMockPP(mockCtrl)
+
+	mux, h := newHandle(t, false, mockPP)
+
+	zh := newZonesHandler(t, mux, false)
+	zh.set(map[string][]string{"test.org": {"active"}}, 1)
+
+	var (
+		ipNet       ipnet.Type
+		ips         map[string]netip.Addr
+		accessCount int
+	)
+
+	mux.HandleFunc(fmt.Sprintf("/zones/%s/dns_records", mockID("test.org", 0)),
+		func(w http.ResponseWriter, r *http.Request) {
+			if accessCount <= 0 {
+				panic(http.ErrAbortHandler)
+			}
+			accessCount--
+
+			if !assert.Equal(t, http.MethodGet, r.Method) ||
+				!assert.Equal(t, []string{mockAuthString}, r.Header["Authorization"]) ||
+				!assert.Equal(t, url.Values{
+					"name":     {"*.test.org"},
+					"page":     {"1"},
+					"per_page": {strconv.Itoa(dnsRecordPageSize)},
+					"type":     {ipNet.RecordType()},
+				}, r.URL.Query()) {
+				panic(http.ErrAbortHandler)
+			}
+
+			w.Header().Set("Content-Type", "application/json")
+			if err := json.NewEncoder(w).Encode(
+				mockDNSListResponseFromAddr(ipNet, "*.test.org", ips),
+			); !assert.NoError(t, err) {
+				panic(http.ErrAbortHandler)
+			}
+		})
+
+	expected := map[string]netip.Addr{"record1": mustIP("::1"), "record2": mustIP("::2")}
+	ipNet, ips, accessCount = ipnet.IP6, expected, 1
+	ips, cached, ok := h.ListRecords(context.Background(), mockPP, domain.Wildcard("test.org"), ipnet.IP6)
+	require.True(t, ok)
+	require.False(t, cached)
+	require.Equal(t, expected, ips)
+	require.Equal(t, 0, accessCount)
+
+	// testing the caching
+	mockPP = mocks.NewMockPP(mockCtrl)
+	ips, cached, ok = h.ListRecords(context.Background(), mockPP, domain.Wildcard("test.org"), ipnet.IP6)
+	require.True(t, ok)
+	require.True(t, cached)
+	require.Equal(t, expected, ips)
+}
+
+func TestListRecordsInvalidDomain(t *testing.T) {
+	t.Parallel()
+	mockCtrl := gomock.NewController(t)
+	mockPP := mocks.NewMockPP(mockCtrl)
+
+	mux, h := newHandle(t, false, mockPP)
+
+	zh := newZonesHandler(t, mux, false)
+	zh.set(map[string][]string{"test.org": {"active"}}, 2)
+
+	mockPP.EXPECT().Warningf(pp.EmojiError, "Failed to retrieve records of %q: %v", "sub.test.org", gomock.Any())
+	ips, cached, ok := h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP4)
+	require.False(t, ok)
+	require.False(t, cached)
+	require.Nil(t, ips)
+
+	mockPP = mocks.NewMockPP(mockCtrl)
+	mockPP.EXPECT().Warningf(pp.EmojiError, "Failed to retrieve records of %q: %v", "sub.test.org", gomock.Any())
+	ips, cached, ok = h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6)
+	require.False(t, ok)
+	require.False(t, cached)
+	require.Nil(t, ips)
+}
+
+func TestListRecordsInvalidZone(t *testing.T) {
+	t.Parallel()
+	mockCtrl := gomock.NewController(t)
+	mockPP := mocks.NewMockPP(mockCtrl)
+
+	_, h := newHandle(t, false, mockPP)
+
+	mockPP.EXPECT().Warningf(
+		pp.EmojiError,
+		"Failed to check the existence of a zone named %q: %v",
+		"sub.test.org",
+		gomock.Any(),
+	)
+	ips, cached, ok := h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP4)
+	require.False(t, ok)
+	require.False(t, cached)
+	require.Nil(t, ips)
+
+	mockPP = mocks.NewMockPP(mockCtrl)
+	mockPP.EXPECT().Warningf(
+		pp.EmojiError,
+		"Failed to check the existence of a zone named %q: %v",
+		"sub.test.org",
+		gomock.Any(),
+	)
+	ips, cached, ok = h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6)
+	require.False(t, ok)
+	require.False(t, cached)
+	require.Nil(t, ips)
+}
+
+func envelopDNSRecordResponse(record *cloudflare.DNSRecord) *cloudflare.DNSRecordResponse {
+	return &cloudflare.DNSRecordResponse{
+		Result: *record,
+		ResultInfo: cloudflare.ResultInfo{
+			Page:       1,
+			PerPage:    100,
+			TotalPages: 1,
+			Count:      1,
+			Total:      1,
+			Cursor:     "",
+			Cursors:    cloudflare.ResultInfoCursors{}, //nolint:exhaustruct
+		},
+		Response: cloudflare.Response{
+			Success:  true,
+			Errors:   []cloudflare.ResponseInfo{},
+			Messages: []cloudflare.ResponseInfo{},
+		},
+	}
+}
+
+func mockDNSRecordResponse(id string, ipNet ipnet.Type, name string, ip string) *cloudflare.DNSRecordResponse {
+	return envelopDNSRecordResponse(mockDNSRecord(id, ipNet, name, ip))
+}
+
+func TestDeleteRecordValid(t *testing.T) {
+	t.Parallel()
+	mockCtrl := gomock.NewController(t)
+	mockPP := mocks.NewMockPP(mockCtrl)
+
+	mux, h := newHandle(t, false, mockPP)
+
+	zh := newZonesHandler(t, mux, false)
+	zh.set(map[string][]string{"test.org": {"active"}}, 2)
+
+	var (
+		listAccessCount   int
+		deleteAccessCount int
+	)
+
+	mux.HandleFunc(fmt.Sprintf("/zones/%s/dns_records", mockID("test.org", 0)),
+		func(w http.ResponseWriter, _ *http.Request) {
+			if listAccessCount <= 0 {
+				panic(http.ErrAbortHandler)
+			}
+			listAccessCount--
+
+			w.Header().Set("Content-Type", "application/json")
+			if err := json.NewEncoder(w).Encode(
+				mockDNSListResponseFromAddr(ipnet.IP6, "test.org",
+					map[string]netip.Addr{"record1": mustIP("::1")}),
+			); !assert.NoError(t, err) {
+				panic(http.ErrAbortHandler)
+			}
+		})
+
+	mux.HandleFunc(fmt.Sprintf("/zones/%s/dns_records/record1", mockID("test.org", 0)),
+		func(w http.ResponseWriter, r *http.Request) {
+			if deleteAccessCount <= 0 {
+				panic(http.ErrAbortHandler)
+			}
+			deleteAccessCount--
+
+			if !assert.Equal(t, http.MethodDelete, r.Method) ||
+				!assert.Equal(t, []string{mockAuthString}, r.Header["Authorization"]) ||
+				!assert.Empty(t, r.URL.Query()) {
+				panic(http.ErrAbortHandler)
+			}
+
+			w.Header().Set("Content-Type", "application/json")
+			if err := json.NewEncoder(w).Encode(
+				mockDNSRecordResponse("record1", ipnet.IP6, "test.org", "::1"),
+			); !assert.NoError(t, err) {
+				panic(http.ErrAbortHandler)
+			}
+		})
+
+	deleteAccessCount = 1
+	ok := h.DeleteRecord(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6, "record1")
+	require.True(t, ok)
+
+	listAccessCount, deleteAccessCount = 1, 1
+	mockPP = mocks.NewMockPP(mockCtrl)
+	h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6)
+	_ = h.DeleteRecord(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6, "record1")
+	rs, cached, ok := h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6)
+	require.True(t, ok)
+	require.True(t, cached)
+	require.Empty(t, rs)
+}
+
+func TestDeleteRecordInvalid(t *testing.T) {
+	t.Parallel()
+	mockCtrl := gomock.NewController(t)
+	mockPP := mocks.NewMockPP(mockCtrl)
+
+	mux, h := newHandle(t, false, mockPP)
+
+	zh := newZonesHandler(t, mux, false)
+	zh.set(map[string][]string{"test.org": {"active"}}, 2)
+
+	mockPP.EXPECT().Warningf(pp.EmojiError, "Failed to delete a stale %s record of %q (ID: %s): %v",
+		"AAAA",
+		"sub.test.org",
+		"record1",
+		gomock.Any(),
+	)
+	ok := h.DeleteRecord(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6, "record1")
+	require.False(t, ok)
+}
+
+func TestDeleteRecordZoneInvalid(t *testing.T) {
+	t.Parallel()
+	mockCtrl := gomock.NewController(t)
+	mockPP := mocks.NewMockPP(mockCtrl)
+
+	_, h := newHandle(t, false, mockPP)
+
+	mockPP.EXPECT().Warningf(pp.EmojiError, "Failed to check the existence of a zone named %q: %v",
+		"sub.test.org",
+		gomock.Any(),
+	)
+	ok := h.DeleteRecord(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6, "record1")
+	require.False(t, ok)
+}
+
+//nolint:funlen
+func TestUpdateRecordValid(t *testing.T) {
+	t.Parallel()
+	mockCtrl := gomock.NewController(t)
+	mockPP := mocks.NewMockPP(mockCtrl)
+
+	mux, h := newHandle(t, false, mockPP)
+
+	zh := newZonesHandler(t, mux, false)
+	zh.set(map[string][]string{"test.org": {"active"}}, 2)
+
+	var (
+		listAccessCount   int
+		updateAccessCount int
+	)
+
+	mux.HandleFunc(fmt.Sprintf("/zones/%s/dns_records", mockID("test.org", 0)),
+		func(w http.ResponseWriter, r *http.Request) {
+			if !assert.Equal(t, http.MethodGet, r.Method) {
+				panic(http.ErrAbortHandler)
+			}
+			if listAccessCount <= 0 {
+				panic(http.ErrAbortHandler)
+			}
+			listAccessCount--
+
+			w.Header().Set("Content-Type", "application/json")
+			if err := json.NewEncoder(w).Encode(
+				mockDNSListResponse(ipnet.IP6, "test.org",
+					map[string]string{"record1": "::1"}),
+			); !assert.NoError(t, err) {
+				panic(http.ErrAbortHandler)
+			}
+		})
+
+	mux.HandleFunc(fmt.Sprintf("/zones/%s/dns_records/record1", mockID("test.org", 0)),
+		func(w http.ResponseWriter, r *http.Request) {
+			if !assert.Equal(t, http.MethodPatch, r.Method) {
+				panic(http.ErrAbortHandler)
+			}
+			if updateAccessCount <= 0 {
+				panic(http.ErrAbortHandler)
+			}
+			updateAccessCount--
+
+			if !assert.Equal(t, []string{mockAuthString}, r.Header["Authorization"]) ||
+				!assert.Empty(t, r.URL.Query()) {
+				panic(http.ErrAbortHandler)
+			}
+
+			var record cloudflare.DNSRecord
+			if err := json.NewDecoder(r.Body).Decode(&record); !assert.NoError(t, err) {
+				panic(http.ErrAbortHandler)
+			}
+
+			if !assert.Equal(t, "::2", record.Content) {
+				panic(http.ErrAbortHandler)
+			}
+
+			w.Header().Set("Content-Type", "application/json")
+			if err := json.NewEncoder(w).Encode(
+				mockDNSRecordResponse("record1", ipnet.IP6, "sub.test.org", "::2"),
+			); !assert.NoError(t, err) {
+				panic(http.ErrAbortHandler)
+			}
+		})
+
+	updateAccessCount = 1
+	ok := h.UpdateRecord(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6, "record1", mustIP("::2"))
+	require.True(t, ok)
+
+	listAccessCount, updateAccessCount = 1, 1
+	mockPP = mocks.NewMockPP(mockCtrl)
+	h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6)
+	_ = h.UpdateRecord(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6, "record1", mustIP("::2"))
+	rs, cached, ok := h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6)
+	require.True(t, ok)
+	require.True(t, cached)
+	require.Equal(t, map[string]netip.Addr{"record1": mustIP("::2")}, rs)
+}
+
+func TestUpdateRecordInvalid(t *testing.T) {
+	t.Parallel()
+	mockCtrl := gomock.NewController(t)
+	mockPP := mocks.NewMockPP(mockCtrl)
+
+	mux, h := newHandle(t, false, mockPP)
+
+	zh := newZonesHandler(t, mux, false)
+	zh.set(map[string][]string{"test.org": {"active"}}, 2)
+
+	mockPP.EXPECT().Warningf(pp.EmojiError, "Failed to update a stale %s record of %q (ID: %s): %v",
+		"AAAA",
+		"sub.test.org",
+		"record1",
+		gomock.Any(),
+	)
+	ok := h.UpdateRecord(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6, "record1", mustIP("::1"))
+	require.False(t, ok)
+}
+
+func TestUpdateRecordInvalidZone(t *testing.T) {
+	t.Parallel()
+	mockCtrl := gomock.NewController(t)
+	mockPP := mocks.NewMockPP(mockCtrl)
+
+	_, h := newHandle(t, false, mockPP)
+
+	mockPP.EXPECT().Warningf(pp.EmojiError, "Failed to check the existence of a zone named %q: %v",
+		"sub.test.org",
+		gomock.Any(),
+	)
+	ok := h.UpdateRecord(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6, "record1", mustIP("::1"))
+	require.False(t, ok)
+}
+
+//nolint:funlen
+func TestCreateRecordValid(t *testing.T) {
+	t.Parallel()
+	mockCtrl := gomock.NewController(t)
+	mockPP := mocks.NewMockPP(mockCtrl)
+
+	mux, h := newHandle(t, false, mockPP)
+
+	zh := newZonesHandler(t, mux, false)
+	zh.set(map[string][]string{"test.org": {"active"}}, 2)
+
+	var (
+		listAccessCount   int
+		createAccessCount int
+	)
+
+	mux.HandleFunc(fmt.Sprintf("/zones/%s/dns_records", mockID("test.org", 0)),
+		func(w http.ResponseWriter, r *http.Request) {
+			switch r.Method {
+			case http.MethodGet:
+				if listAccessCount <= 0 {
+					panic(http.ErrAbortHandler)
+				}
+				listAccessCount--
+
+				w.Header().Set("Content-Type", "application/json")
+				if err := json.NewEncoder(w).Encode(
+					mockDNSListResponse(ipnet.IP6, "test.org",
+						map[string]string{"record1": "::1"}),
+				); !assert.NoError(t, err) {
+					panic(http.ErrAbortHandler)
+				}
+			case http.MethodPost:
+				if createAccessCount <= 0 {
+					panic(http.ErrAbortHandler)
+				}
+				createAccessCount--
+
+				if !assert.Equal(t, []string{mockAuthString}, r.Header["Authorization"]) ||
+					!assert.Empty(t, r.URL.Query()) {
+					panic(http.ErrAbortHandler)
+				}
+
+				var record cloudflare.DNSRecord
+				if err := json.NewDecoder(r.Body).Decode(&record); !assert.NoError(t, err) {
+					panic(http.ErrAbortHandler)
+				}
+
+				if !assert.Equal(t, "sub.test.org", record.Name) ||
+					!assert.Equal(t, ipnet.IP6.RecordType(), record.Type) ||
+					!assert.Equal(t, "::1", record.Content) ||
+					!assert.Equal(t, 100, record.TTL) ||
+					!assert.False(t, *record.Proxied) ||
+					!assert.Equal(t, "hello", record.Comment) {
+					panic(http.ErrAbortHandler)
+				}
+				record.ID = "record1"
+
+				w.Header().Set("Content-Type", "application/json")
+				if err := json.NewEncoder(w).Encode(
+					envelopDNSRecordResponse(&record),
+				); !assert.NoError(t, err) {
+					panic(http.ErrAbortHandler)
+				}
+			}
+		})
+
+	createAccessCount = 1
+	actualID, ok := h.CreateRecord(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6, mustIP("::1"), 100, false, "hello") //nolint:lll
+	require.True(t, ok)
+	require.Equal(t, "record1", actualID)
+
+	listAccessCount, createAccessCount = 1, 1
+	mockPP = mocks.NewMockPP(mockCtrl)
+	h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6)
+	h.CreateRecord(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6, mustIP("::1"), 100, false, "hello") //nolint:lll
+	rs, cached, ok := h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6)
+	require.True(t, ok)
+	require.True(t, cached)
+	require.Equal(t, map[string]netip.Addr{"record1": mustIP("::1")}, rs)
+}
+
+func TestCreateRecordInvalid(t *testing.T) {
+	t.Parallel()
+	mockCtrl := gomock.NewController(t)
+	mockPP := mocks.NewMockPP(mockCtrl)
+
+	mux, h := newHandle(t, false, mockPP)
+
+	zh := newZonesHandler(t, mux, false)
+	zh.set(map[string][]string{"test.org": {"active"}}, 2)
+
+	mockPP.EXPECT().Warningf(pp.EmojiError, "Failed to add a new %s record of %q: %v",
+		"AAAA",
+		"sub.test.org",
+		gomock.Any(),
+	)
+	actualID, ok := h.CreateRecord(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6, mustIP("::1"), 100, false, "hello") //nolint:lll
+	require.False(t, ok)
+	require.Equal(t, "", actualID)
+}
+
+func TestCreateRecordInvalidZone(t *testing.T) {
+	t.Parallel()
+	mockCtrl := gomock.NewController(t)
+	mockPP := mocks.NewMockPP(mockCtrl)
+
+	_, h := newHandle(t, false, mockPP)
+
+	mockPP.EXPECT().Warningf(pp.EmojiError, "Failed to check the existence of a zone named %q: %v",
+		"sub.test.org",
+		gomock.Any(),
+	)
+	actualID, ok := h.CreateRecord(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6, mustIP("::1"), 100, false, "hello") //nolint:lll
+	require.False(t, ok)
+	require.Equal(t, "", actualID)
+}
diff --git a/internal/api/cloudflare_test.go b/internal/api/cloudflare_test.go
index 15a2d95..5a3dcce 100644
--- a/internal/api/cloudflare_test.go
+++ b/internal/api/cloudflare_test.go
@@ -4,24 +4,18 @@ import (
 	"context"
 	"crypto/sha512"
 	"encoding/hex"
-	"encoding/json"
 	"fmt"
 	"net/http"
 	"net/http/httptest"
 	"net/netip"
-	"net/url"
-	"strconv"
 	"testing"
 	"time"
 
-	"github.com/cloudflare/cloudflare-go"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"go.uber.org/mock/gomock"
 
 	"github.com/favonia/cloudflare-ddns/internal/api"
-	"github.com/favonia/cloudflare-ddns/internal/domain"
-	"github.com/favonia/cloudflare-ddns/internal/ipnet"
 	"github.com/favonia/cloudflare-ddns/internal/mocks"
 	"github.com/favonia/cloudflare-ddns/internal/pp"
 )
@@ -52,7 +46,7 @@ const (
 	mockAccount    = "account456"
 )
 
-func newServerAuth(t *testing.T, emptyAccountID bool) (*http.ServeMux, *api.CloudflareAuth) {
+func newServerAuth(t *testing.T, emptyAccountID bool) (*http.ServeMux, api.CloudflareAuth) {
 	t.Helper()
 
 	mux := http.NewServeMux()
@@ -69,7 +63,7 @@ func newServerAuth(t *testing.T, emptyAccountID bool) (*http.ServeMux, *api.Clou
 		auth.AccountID = ""
 	}
 
-	return mux, &auth
+	return mux, auth
 }
 
 func handleSanityCheck(t *testing.T, w http.ResponseWriter, r *http.Request) {
@@ -348,1030 +342,3 @@ func TestSanityCheckTimeout(t *testing.T) {
 	require.NotNil(t, h)
 	require.True(t, h.SanityCheck(context.Background(), mockPP))
 }
-
-func mockZone(name string, i int, status string) *cloudflare.Zone {
-	return &cloudflare.Zone{ //nolint:exhaustruct
-		ID:     mockID(name, i),
-		Name:   name,
-		Status: status,
-	}
-}
-
-const (
-	zonePageSize      = 50
-	dnsRecordPageSize = 100
-)
-
-func mockZonesResponse(zoneName string, zoneStatuses []string) *cloudflare.ZonesResponse {
-	numZones := len(zoneStatuses)
-
-	if numZones > zonePageSize {
-		panic("mockZonesResponse got too many zone names")
-	}
-
-	zones := make([]cloudflare.Zone, len(zoneStatuses))
-	for i, status := range zoneStatuses {
-		zones[i] = *mockZone(zoneName, i, status)
-	}
-
-	return &cloudflare.ZonesResponse{
-		Result: zones,
-		ResultInfo: cloudflare.ResultInfo{
-			Page:       1,
-			PerPage:    zonePageSize,
-			TotalPages: (numZones + zonePageSize - 1) / zonePageSize,
-			Count:      numZones,
-			Total:      numZones,
-			Cursor:     "",
-			Cursors:    cloudflare.ResultInfoCursors{}, //nolint:exhaustruct
-		},
-		Response: cloudflare.Response{
-			Success:  true,
-			Errors:   []cloudflare.ResponseInfo{},
-			Messages: []cloudflare.ResponseInfo{},
-		},
-	}
-}
-
-func handleZones(
-	t *testing.T, zoneName string, zoneStatuses []string, emptyAccountID bool, w http.ResponseWriter, r *http.Request,
-) {
-	t.Helper()
-
-	require.Equal(t, http.MethodGet, r.Method)
-	require.Equal(t, []string{mockAuthString}, r.Header["Authorization"])
-	if emptyAccountID {
-		require.Equal(t, url.Values{
-			"name":     {zoneName},
-			"per_page": {strconv.Itoa(zonePageSize)},
-		}, r.URL.Query())
-	} else {
-		require.Equal(t, url.Values{
-			"account.id": {mockAccount},
-			"name":       {zoneName},
-			"per_page":   {strconv.Itoa(zonePageSize)},
-		}, r.URL.Query())
-	}
-
-	w.Header().Set("Content-Type", "application/json")
-	err := json.NewEncoder(w).Encode(mockZonesResponse(zoneName, zoneStatuses))
-	require.NoError(t, err)
-}
-
-type zonesHandler struct {
-	mux          *http.ServeMux
-	zoneStatuses *map[string][]string
-	accessCount  *int
-}
-
-func newZonesHandler(t *testing.T, mux *http.ServeMux, emptyAccountID bool) zonesHandler {
-	t.Helper()
-
-	var (
-		zoneStatuses map[string][]string
-		accessCount  int
-	)
-
-	mux.HandleFunc("/zones", func(w http.ResponseWriter, r *http.Request) {
-		if accessCount <= 0 {
-			panic(http.ErrAbortHandler)
-		}
-		accessCount--
-
-		zoneName := r.URL.Query().Get("name")
-		handleZones(t, zoneName, zoneStatuses[zoneName], emptyAccountID, w, r)
-	})
-
-	return zonesHandler{
-		mux:          mux,
-		zoneStatuses: &zoneStatuses,
-		accessCount:  &accessCount,
-	}
-}
-
-func (h zonesHandler) set(zoneStatuses map[string][]string, accessCount int) {
-	*(h.zoneStatuses), *(h.accessCount) = zoneStatuses, accessCount
-}
-
-func (h zonesHandler) isExhausted() bool {
-	return *h.accessCount == 0
-}
-
-func TestListZonesRoot(t *testing.T) {
-	t.Parallel()
-	mockCtrl := gomock.NewController(t)
-	mockPP := mocks.NewMockPP(mockCtrl)
-
-	_, h := newHandle(t, false, mockPP)
-
-	zones, ok := h.(api.CloudflareHandle).ListZones(context.Background(), mockPP, "")
-	require.True(t, ok)
-	require.Empty(t, zones)
-}
-
-func TestListZonesTwo(t *testing.T) {
-	t.Parallel()
-	mockCtrl := gomock.NewController(t)
-	mockPP := mocks.NewMockPP(mockCtrl)
-
-	mux, h := newHandle(t, false, mockPP)
-
-	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")
-	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")
-	require.True(t, ok)
-	require.Equal(t, mockIDs("test.org", 0, 1), zones)
-	require.True(t, zh.isExhausted())
-
-	h.FlushCache()
-
-	mockPP = mocks.NewMockPP(mockCtrl)
-	mockPP.EXPECT().Warningf(
-		pp.EmojiError,
-		"Failed to check the existence of a zone named %q: %v",
-		"test.org",
-		gomock.Any(),
-	)
-	zones, ok = h.(api.CloudflareHandle).ListZones(context.Background(), mockPP, "test.org")
-	require.False(t, ok)
-	require.Nil(t, zones)
-	require.True(t, zh.isExhausted())
-}
-
-func TestListZonesEmpty(t *testing.T) {
-	t.Parallel()
-	mockCtrl := gomock.NewController(t)
-	mockPP := mocks.NewMockPP(mockCtrl)
-
-	mux, h := newHandle(t, false, mockPP)
-
-	zh := newZonesHandler(t, mux, false)
-
-	zh.set(map[string][]string{}, 1)
-	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")
-	require.True(t, ok)
-	require.Empty(t, zones)
-	require.True(t, zh.isExhausted())
-
-	h.FlushCache()
-
-	mockPP = mocks.NewMockPP(mockCtrl)
-	mockPP.EXPECT().Warningf(
-		pp.EmojiError,
-		"Failed to check the existence of a zone named %q: %v",
-		"test.org",
-		gomock.Any(),
-	)
-	zones, ok = h.(api.CloudflareHandle).ListZones(context.Background(), mockPP, "test.org")
-	require.False(t, ok)
-	require.Nil(t, zones)
-	require.True(t, zh.isExhausted())
-}
-
-//nolint:funlen
-func TestZoneOfDomain(t *testing.T) {
-	t.Parallel()
-
-	for name, tc := range map[string]struct {
-		emptyAccountID bool
-		zone           string
-		domain         domain.Domain
-		zoneStatuses   map[string][]string
-		accessCount    int
-		expected       string
-		ok             bool
-		prepareMockPP  func(*mocks.MockPP)
-	}{
-		"root":     {false, "test.org", domain.FQDN("test.org"), map[string][]string{"test.org": {"active"}}, 1, mockID("test.org", 0), true, nil},     //nolint:lll
-		"wildcard": {false, "test.org", domain.Wildcard("test.org"), map[string][]string{"test.org": {"active"}}, 1, mockID("test.org", 0), true, nil}, //nolint:lll
-		"one":      {false, "test.org", domain.FQDN("sub.test.org"), map[string][]string{"test.org": {"active"}}, 2, mockID("test.org", 0), true, nil}, //nolint:lll
-		"none": {
-			false, "test.org", domain.FQDN("sub.test.org"),
-			map[string][]string{},
-			3, "", false,
-			func(m *mocks.MockPP) {
-				gomock.InOrder(
-					m.EXPECT().Warningf(pp.EmojiError, "Failed to find the zone of %q", "sub.test.org"),
-					m.EXPECT().Infof(pp.EmojiHint, "Double-check the value of CF_ACCOUNT_ID; in most cases, you can leave it blank"), //nolint:lll
-				)
-			},
-		},
-		"none/wildcard": {
-			false, "test.org", domain.Wildcard("test.org"),
-			map[string][]string{},
-			2, "", false,
-			func(m *mocks.MockPP) {
-				gomock.InOrder(
-					m.EXPECT().Warningf(pp.EmojiError, "Failed to find the zone of %q", "*.test.org"),
-					m.EXPECT().Infof(pp.EmojiHint, "Double-check the value of CF_ACCOUNT_ID; in most cases, you can leave it blank"), //nolint:lll
-				)
-			},
-		},
-		"multiple": {
-			false, "test.org", domain.FQDN("sub.test.org"),
-			map[string][]string{"test.org": {"active", "active"}},
-			2, "", false,
-			func(m *mocks.MockPP) {
-				gomock.InOrder(
-					m.EXPECT().Warningf(
-						pp.EmojiImpossible,
-						"Found multiple active zones named %q. Specifying CF_ACCOUNT_ID might help",
-						"test.org",
-					),
-					m.EXPECT().Warningf(
-						pp.EmojiImpossible,
-						"Please consider reporting this rare situation at https://github.com/favonia/cloudflare-ddns/issues/new",
-					),
-				)
-			},
-		},
-		"multiple/wildcard": {
-			false, "test.org", domain.Wildcard("test.org"),
-			map[string][]string{"test.org": {"active", "active"}},
-			1, "", false,
-			func(m *mocks.MockPP) {
-				gomock.InOrder(
-					m.EXPECT().Warningf(
-						pp.EmojiImpossible,
-						"Found multiple active zones named %q. Specifying CF_ACCOUNT_ID might help",
-						"test.org",
-					),
-					m.EXPECT().Warningf(
-						pp.EmojiImpossible,
-						"Please consider reporting this rare situation at https://github.com/favonia/cloudflare-ddns/issues/new",
-					),
-				)
-			},
-		},
-		"deleted": {
-			false, "test.org", domain.FQDN("test.org"),
-			map[string][]string{"test.org": {"deleted"}},
-			2, "", false,
-			func(m *mocks.MockPP) {
-				gomock.InOrder(
-					m.EXPECT().Infof(pp.EmojiWarning, "Zone %q is %q and thus skipped", "test.org", "deleted"),
-					m.EXPECT().Warningf(pp.EmojiError, "Failed to find the zone of %q", "test.org"),
-					m.EXPECT().Infof(pp.EmojiHint, "Double-check the value of CF_ACCOUNT_ID; in most cases, you can leave it blank"), //nolint:lll
-				)
-			},
-		},
-		"deleted/empty-account": {
-			true, "test.org", domain.FQDN("test.org"),
-			map[string][]string{"test.org": {"deleted"}},
-			2, "", false,
-			func(m *mocks.MockPP) {
-				gomock.InOrder(
-					m.EXPECT().Infof(pp.EmojiWarning, "Zone %q is %q and thus skipped", "test.org", "deleted"),
-					m.EXPECT().Warningf(pp.EmojiError, "Failed to find the zone of %q", "test.org"),
-				)
-			},
-		},
-		"pending": {
-			false, "test.org", domain.FQDN("test.org"),
-			map[string][]string{"test.org": {"pending"}},
-			1, mockID("test.org", 0), true,
-			func(m *mocks.MockPP) {
-				gomock.InOrder(
-					m.EXPECT().Warningf(pp.EmojiWarning, "Zone %q is %q; your Cloudflare setup is incomplete; some features might not work as expected", "test.org", "pending"), //nolint:lll
-				)
-			},
-		},
-		"initializing": {
-			false, "test.org", domain.FQDN("test.org"),
-			map[string][]string{"test.org": {"initializing"}},
-			1, mockID("test.org", 0), true,
-			func(m *mocks.MockPP) {
-				gomock.InOrder(
-					m.EXPECT().Warningf(pp.EmojiWarning, "Zone %q is %q; your Cloudflare setup is incomplete; some features might not work as expected", "test.org", "initializing"), //nolint:lll
-				)
-			},
-		},
-		"undocumented": {
-			false, "test.org", domain.FQDN("test.org"),
-			map[string][]string{"test.org": {"some-undocumented-status"}},
-			1, mockID("test.org", 0), true,
-			func(m *mocks.MockPP) {
-				gomock.InOrder(
-					m.EXPECT().Warningf(pp.EmojiImpossible, "Zone %q is in an undocumented status %q", "test.org", "some-undocumented-status"), //nolint:lll
-					m.EXPECT().Warningf(pp.EmojiImpossible, "Please report the bug at https://github.com/favonia/cloudflare-ddns/issues/new"),  //nolint:lll
-				)
-			},
-		},
-	} {
-		t.Run(name, func(t *testing.T) {
-			t.Parallel()
-			mockCtrl := gomock.NewController(t)
-			mockPP := mocks.NewMockPP(mockCtrl)
-
-			mux, h := newHandle(t, tc.emptyAccountID, mockPP)
-
-			zh := newZonesHandler(t, mux, tc.emptyAccountID)
-
-			zh.set(tc.zoneStatuses, tc.accessCount)
-			if tc.prepareMockPP != nil {
-				tc.prepareMockPP(mockPP)
-			}
-			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())
-
-			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)
-				require.Equal(t, tc.ok, ok)
-				require.Equal(t, tc.expected, zoneID)
-				require.True(t, zh.isExhausted())
-			}
-		})
-	}
-}
-
-func TestZoneOfDomainInvalid(t *testing.T) {
-	t.Parallel()
-	mockCtrl := gomock.NewController(t)
-	mockPP := mocks.NewMockPP(mockCtrl)
-
-	_, h := newHandle(t, false, mockPP)
-
-	mockPP.EXPECT().Warningf(
-		pp.EmojiError,
-		"Failed to check the existence of a zone named %q: %v",
-		"sub.test.org",
-		gomock.Any(),
-	)
-	zoneID, ok := h.(api.CloudflareHandle).ZoneOfDomain(context.Background(), mockPP, domain.FQDN("sub.test.org"))
-	require.False(t, ok)
-	require.Equal(t, "", zoneID)
-}
-
-func mockDNSRecord(id string, ipNet ipnet.Type, name string, ip string) *cloudflare.DNSRecord {
-	return &cloudflare.DNSRecord{ //nolint:exhaustruct
-		ID:      id,
-		Type:    ipNet.RecordType(),
-		Name:    name,
-		Content: ip,
-	}
-}
-
-func mockDNSListResponse(ipNet ipnet.Type, name string, ips map[string]string) *cloudflare.DNSListResponse {
-	if len(ips) > 100 {
-		panic("mockDNSResponse got too many IPs")
-	}
-
-	rs := make([]cloudflare.DNSRecord, 0, len(ips))
-	for id, ip := range ips {
-		rs = append(rs, *mockDNSRecord(id, ipNet, name, ip))
-	}
-
-	return &cloudflare.DNSListResponse{
-		Result: rs,
-		ResultInfo: cloudflare.ResultInfo{
-			Page:       1,
-			PerPage:    100,
-			TotalPages: (len(ips) + 99) / 100,
-			Count:      len(ips),
-			Total:      len(ips),
-			Cursor:     "",
-			Cursors:    cloudflare.ResultInfoCursors{}, //nolint:exhaustruct
-		},
-		Response: cloudflare.Response{
-			Success:  true,
-			Errors:   []cloudflare.ResponseInfo{},
-			Messages: []cloudflare.ResponseInfo{},
-		},
-	}
-}
-
-func mockDNSListResponseFromAddr(ipNet ipnet.Type, name string, ips map[string]netip.Addr) *cloudflare.DNSListResponse {
-	if len(ips) > 100 {
-		panic("mockDNSResponse got too many IPs")
-	}
-
-	strings := make(map[string]string)
-
-	for id, ip := range ips {
-		strings[id] = ip.String()
-	}
-
-	return mockDNSListResponse(ipNet, name, strings)
-}
-
-//nolint:dupl
-func TestListRecords(t *testing.T) {
-	t.Parallel()
-	mockCtrl := gomock.NewController(t)
-	mockPP := mocks.NewMockPP(mockCtrl)
-
-	mux, h := newHandle(t, false, mockPP)
-
-	zh := newZonesHandler(t, mux, false)
-	zh.set(map[string][]string{"test.org": {"active"}}, 2)
-
-	var (
-		ipNet       ipnet.Type
-		ips         map[string]netip.Addr
-		accessCount int
-	)
-
-	mux.HandleFunc(fmt.Sprintf("/zones/%s/dns_records", mockID("test.org", 0)),
-		func(w http.ResponseWriter, r *http.Request) {
-			if accessCount <= 0 {
-				panic(http.ErrAbortHandler)
-			}
-			accessCount--
-
-			if !assert.Equal(t, http.MethodGet, r.Method) ||
-				!assert.Equal(t, []string{mockAuthString}, r.Header["Authorization"]) ||
-				!assert.Equal(t, url.Values{
-					"name":     {"sub.test.org"},
-					"page":     {"1"},
-					"per_page": {strconv.Itoa(dnsRecordPageSize)},
-					"type":     {ipNet.RecordType()},
-				}, r.URL.Query()) {
-				panic(http.ErrAbortHandler)
-			}
-
-			w.Header().Set("Content-Type", "application/json")
-			if err := json.NewEncoder(w).Encode(
-				mockDNSListResponseFromAddr(ipNet, "test.org", ips),
-			); !assert.NoError(t, err) {
-				panic(http.ErrAbortHandler)
-			}
-		})
-
-	expected := map[string]netip.Addr{"record1": mustIP("::1"), "record2": mustIP("::2")}
-	ipNet, ips, accessCount = ipnet.IP6, expected, 1
-	ips, cached, ok := h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6)
-	require.True(t, ok)
-	require.False(t, cached)
-	require.Equal(t, expected, ips)
-	require.Equal(t, 0, accessCount)
-
-	// testing the caching
-	mockPP = mocks.NewMockPP(mockCtrl)
-	ips, cached, ok = h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6)
-	require.True(t, ok)
-	require.True(t, cached)
-	require.Equal(t, expected, ips)
-}
-
-//nolint:funlen
-func TestListRecordsInvalidIPAddress(t *testing.T) {
-	t.Parallel()
-	mockCtrl := gomock.NewController(t)
-	mockPP := mocks.NewMockPP(mockCtrl)
-
-	mux, h := newHandle(t, false, mockPP)
-
-	zh := newZonesHandler(t, mux, false)
-	zh.set(map[string][]string{"test.org": {"active"}}, 2)
-
-	var (
-		ipNet       ipnet.Type
-		ips         map[string]netip.Addr
-		accessCount int
-	)
-
-	mux.HandleFunc(fmt.Sprintf("/zones/%s/dns_records", mockID("test.org", 0)),
-		func(w http.ResponseWriter, r *http.Request) {
-			if accessCount <= 0 {
-				panic(http.ErrAbortHandler)
-			}
-			accessCount--
-
-			if !assert.Equal(t, http.MethodGet, r.Method) ||
-				!assert.Equal(t, []string{mockAuthString}, r.Header["Authorization"]) ||
-				!assert.Equal(t, url.Values{
-					"name":     {"sub.test.org"},
-					"page":     {"1"},
-					"per_page": {strconv.Itoa(dnsRecordPageSize)},
-					"type":     {ipNet.RecordType()},
-				}, r.URL.Query()) {
-				panic(http.ErrAbortHandler)
-			}
-
-			w.Header().Set("Content-Type", "application/json")
-			if err := json.NewEncoder(w).Encode(mockDNSListResponse(ipNet, "test.org",
-				map[string]string{"record1": "::1", "record2": "NOT AN IP"},
-			)); !assert.NoError(t, err) {
-				panic(http.ErrAbortHandler)
-			}
-		})
-
-	ipNet, accessCount = ipnet.IP6, 1
-	mockPP.EXPECT().Warningf(
-		pp.EmojiImpossible,
-		"Failed to parse the IP address in records of %q: %v",
-		"sub.test.org",
-		gomock.Any(),
-	)
-	ips, cached, ok := h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6)
-	require.False(t, ok)
-	require.False(t, cached)
-	require.Nil(t, ips)
-	require.Equal(t, 0, accessCount)
-
-	// testing the (no) caching
-	mockPP = mocks.NewMockPP(mockCtrl)
-	mockPP.EXPECT().Warningf(
-		pp.EmojiError,
-		"Failed to retrieve records of %q: %v",
-		"sub.test.org",
-		gomock.Any(),
-	)
-	ips, cached, ok = h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6)
-	require.False(t, ok)
-	require.False(t, cached)
-	require.Nil(t, ips)
-	require.Equal(t, 0, accessCount)
-}
-
-//nolint:dupl
-func TestListRecordsWildcard(t *testing.T) {
-	t.Parallel()
-	mockCtrl := gomock.NewController(t)
-	mockPP := mocks.NewMockPP(mockCtrl)
-
-	mux, h := newHandle(t, false, mockPP)
-
-	zh := newZonesHandler(t, mux, false)
-	zh.set(map[string][]string{"test.org": {"active"}}, 1)
-
-	var (
-		ipNet       ipnet.Type
-		ips         map[string]netip.Addr
-		accessCount int
-	)
-
-	mux.HandleFunc(fmt.Sprintf("/zones/%s/dns_records", mockID("test.org", 0)),
-		func(w http.ResponseWriter, r *http.Request) {
-			if accessCount <= 0 {
-				panic(http.ErrAbortHandler)
-			}
-			accessCount--
-
-			if !assert.Equal(t, http.MethodGet, r.Method) ||
-				!assert.Equal(t, []string{mockAuthString}, r.Header["Authorization"]) ||
-				!assert.Equal(t, url.Values{
-					"name":     {"*.test.org"},
-					"page":     {"1"},
-					"per_page": {strconv.Itoa(dnsRecordPageSize)},
-					"type":     {ipNet.RecordType()},
-				}, r.URL.Query()) {
-				panic(http.ErrAbortHandler)
-			}
-
-			w.Header().Set("Content-Type", "application/json")
-			if err := json.NewEncoder(w).Encode(
-				mockDNSListResponseFromAddr(ipNet, "*.test.org", ips),
-			); !assert.NoError(t, err) {
-				panic(http.ErrAbortHandler)
-			}
-		})
-
-	expected := map[string]netip.Addr{"record1": mustIP("::1"), "record2": mustIP("::2")}
-	ipNet, ips, accessCount = ipnet.IP6, expected, 1
-	ips, cached, ok := h.ListRecords(context.Background(), mockPP, domain.Wildcard("test.org"), ipnet.IP6)
-	require.True(t, ok)
-	require.False(t, cached)
-	require.Equal(t, expected, ips)
-	require.Equal(t, 0, accessCount)
-
-	// testing the caching
-	mockPP = mocks.NewMockPP(mockCtrl)
-	ips, cached, ok = h.ListRecords(context.Background(), mockPP, domain.Wildcard("test.org"), ipnet.IP6)
-	require.True(t, ok)
-	require.True(t, cached)
-	require.Equal(t, expected, ips)
-}
-
-func TestListRecordsInvalidDomain(t *testing.T) {
-	t.Parallel()
-	mockCtrl := gomock.NewController(t)
-	mockPP := mocks.NewMockPP(mockCtrl)
-
-	mux, h := newHandle(t, false, mockPP)
-
-	zh := newZonesHandler(t, mux, false)
-	zh.set(map[string][]string{"test.org": {"active"}}, 2)
-
-	mockPP.EXPECT().Warningf(pp.EmojiError, "Failed to retrieve records of %q: %v", "sub.test.org", gomock.Any())
-	ips, cached, ok := h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP4)
-	require.False(t, ok)
-	require.False(t, cached)
-	require.Nil(t, ips)
-
-	mockPP = mocks.NewMockPP(mockCtrl)
-	mockPP.EXPECT().Warningf(pp.EmojiError, "Failed to retrieve records of %q: %v", "sub.test.org", gomock.Any())
-	ips, cached, ok = h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6)
-	require.False(t, ok)
-	require.False(t, cached)
-	require.Nil(t, ips)
-}
-
-func TestListRecordsInvalidZone(t *testing.T) {
-	t.Parallel()
-	mockCtrl := gomock.NewController(t)
-	mockPP := mocks.NewMockPP(mockCtrl)
-
-	_, h := newHandle(t, false, mockPP)
-
-	mockPP.EXPECT().Warningf(
-		pp.EmojiError,
-		"Failed to check the existence of a zone named %q: %v",
-		"sub.test.org",
-		gomock.Any(),
-	)
-	ips, cached, ok := h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP4)
-	require.False(t, ok)
-	require.False(t, cached)
-	require.Nil(t, ips)
-
-	mockPP = mocks.NewMockPP(mockCtrl)
-	mockPP.EXPECT().Warningf(
-		pp.EmojiError,
-		"Failed to check the existence of a zone named %q: %v",
-		"sub.test.org",
-		gomock.Any(),
-	)
-	ips, cached, ok = h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6)
-	require.False(t, ok)
-	require.False(t, cached)
-	require.Nil(t, ips)
-}
-
-func envelopDNSRecordResponse(record *cloudflare.DNSRecord) *cloudflare.DNSRecordResponse {
-	return &cloudflare.DNSRecordResponse{
-		Result: *record,
-		ResultInfo: cloudflare.ResultInfo{
-			Page:       1,
-			PerPage:    100,
-			TotalPages: 1,
-			Count:      1,
-			Total:      1,
-			Cursor:     "",
-			Cursors:    cloudflare.ResultInfoCursors{}, //nolint:exhaustruct
-		},
-		Response: cloudflare.Response{
-			Success:  true,
-			Errors:   []cloudflare.ResponseInfo{},
-			Messages: []cloudflare.ResponseInfo{},
-		},
-	}
-}
-
-func mockDNSRecordResponse(id string, ipNet ipnet.Type, name string, ip string) *cloudflare.DNSRecordResponse {
-	return envelopDNSRecordResponse(mockDNSRecord(id, ipNet, name, ip))
-}
-
-func TestDeleteRecordValid(t *testing.T) {
-	t.Parallel()
-	mockCtrl := gomock.NewController(t)
-	mockPP := mocks.NewMockPP(mockCtrl)
-
-	mux, h := newHandle(t, false, mockPP)
-
-	zh := newZonesHandler(t, mux, false)
-	zh.set(map[string][]string{"test.org": {"active"}}, 2)
-
-	var (
-		listAccessCount   int
-		deleteAccessCount int
-	)
-
-	mux.HandleFunc(fmt.Sprintf("/zones/%s/dns_records", mockID("test.org", 0)),
-		func(w http.ResponseWriter, _ *http.Request) {
-			if listAccessCount <= 0 {
-				panic(http.ErrAbortHandler)
-			}
-			listAccessCount--
-
-			w.Header().Set("Content-Type", "application/json")
-			if err := json.NewEncoder(w).Encode(
-				mockDNSListResponseFromAddr(ipnet.IP6, "test.org",
-					map[string]netip.Addr{"record1": mustIP("::1")}),
-			); !assert.NoError(t, err) {
-				panic(http.ErrAbortHandler)
-			}
-		})
-
-	mux.HandleFunc(fmt.Sprintf("/zones/%s/dns_records/record1", mockID("test.org", 0)),
-		func(w http.ResponseWriter, r *http.Request) {
-			if deleteAccessCount <= 0 {
-				panic(http.ErrAbortHandler)
-			}
-			deleteAccessCount--
-
-			if !assert.Equal(t, http.MethodDelete, r.Method) ||
-				!assert.Equal(t, []string{mockAuthString}, r.Header["Authorization"]) ||
-				!assert.Empty(t, r.URL.Query()) {
-				panic(http.ErrAbortHandler)
-			}
-
-			w.Header().Set("Content-Type", "application/json")
-			if err := json.NewEncoder(w).Encode(
-				mockDNSRecordResponse("record1", ipnet.IP6, "test.org", "::1"),
-			); !assert.NoError(t, err) {
-				panic(http.ErrAbortHandler)
-			}
-		})
-
-	deleteAccessCount = 1
-	ok := h.DeleteRecord(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6, "record1")
-	require.True(t, ok)
-
-	listAccessCount, deleteAccessCount = 1, 1
-	mockPP = mocks.NewMockPP(mockCtrl)
-	h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6)
-	_ = h.DeleteRecord(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6, "record1")
-	rs, cached, ok := h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6)
-	require.True(t, ok)
-	require.True(t, cached)
-	require.Empty(t, rs)
-}
-
-func TestDeleteRecordInvalid(t *testing.T) {
-	t.Parallel()
-	mockCtrl := gomock.NewController(t)
-	mockPP := mocks.NewMockPP(mockCtrl)
-
-	mux, h := newHandle(t, false, mockPP)
-
-	zh := newZonesHandler(t, mux, false)
-	zh.set(map[string][]string{"test.org": {"active"}}, 2)
-
-	mockPP.EXPECT().Warningf(pp.EmojiError, "Failed to delete a stale %s record of %q (ID: %s): %v",
-		"AAAA",
-		"sub.test.org",
-		"record1",
-		gomock.Any(),
-	)
-	ok := h.DeleteRecord(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6, "record1")
-	require.False(t, ok)
-}
-
-func TestDeleteRecordZoneInvalid(t *testing.T) {
-	t.Parallel()
-	mockCtrl := gomock.NewController(t)
-	mockPP := mocks.NewMockPP(mockCtrl)
-
-	_, h := newHandle(t, false, mockPP)
-
-	mockPP.EXPECT().Warningf(pp.EmojiError, "Failed to check the existence of a zone named %q: %v",
-		"sub.test.org",
-		gomock.Any(),
-	)
-	ok := h.DeleteRecord(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6, "record1")
-	require.False(t, ok)
-}
-
-//nolint:funlen
-func TestUpdateRecordValid(t *testing.T) {
-	t.Parallel()
-	mockCtrl := gomock.NewController(t)
-	mockPP := mocks.NewMockPP(mockCtrl)
-
-	mux, h := newHandle(t, false, mockPP)
-
-	zh := newZonesHandler(t, mux, false)
-	zh.set(map[string][]string{"test.org": {"active"}}, 2)
-
-	var (
-		listAccessCount   int
-		updateAccessCount int
-	)
-
-	mux.HandleFunc(fmt.Sprintf("/zones/%s/dns_records", mockID("test.org", 0)),
-		func(w http.ResponseWriter, r *http.Request) {
-			if !assert.Equal(t, http.MethodGet, r.Method) {
-				panic(http.ErrAbortHandler)
-			}
-			if listAccessCount <= 0 {
-				panic(http.ErrAbortHandler)
-			}
-			listAccessCount--
-
-			w.Header().Set("Content-Type", "application/json")
-			if err := json.NewEncoder(w).Encode(
-				mockDNSListResponse(ipnet.IP6, "test.org",
-					map[string]string{"record1": "::1"}),
-			); !assert.NoError(t, err) {
-				panic(http.ErrAbortHandler)
-			}
-		})
-
-	mux.HandleFunc(fmt.Sprintf("/zones/%s/dns_records/record1", mockID("test.org", 0)),
-		func(w http.ResponseWriter, r *http.Request) {
-			if !assert.Equal(t, http.MethodPatch, r.Method) {
-				panic(http.ErrAbortHandler)
-			}
-			if updateAccessCount <= 0 {
-				panic(http.ErrAbortHandler)
-			}
-			updateAccessCount--
-
-			if !assert.Equal(t, []string{mockAuthString}, r.Header["Authorization"]) ||
-				!assert.Empty(t, r.URL.Query()) {
-				panic(http.ErrAbortHandler)
-			}
-
-			var record cloudflare.DNSRecord
-			if err := json.NewDecoder(r.Body).Decode(&record); !assert.NoError(t, err) {
-				panic(http.ErrAbortHandler)
-			}
-
-			if !assert.Equal(t, "::2", record.Content) {
-				panic(http.ErrAbortHandler)
-			}
-
-			w.Header().Set("Content-Type", "application/json")
-			if err := json.NewEncoder(w).Encode(
-				mockDNSRecordResponse("record1", ipnet.IP6, "sub.test.org", "::2"),
-			); !assert.NoError(t, err) {
-				panic(http.ErrAbortHandler)
-			}
-		})
-
-	updateAccessCount = 1
-	ok := h.UpdateRecord(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6, "record1", mustIP("::2"))
-	require.True(t, ok)
-
-	listAccessCount, updateAccessCount = 1, 1
-	mockPP = mocks.NewMockPP(mockCtrl)
-	h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6)
-	_ = h.UpdateRecord(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6, "record1", mustIP("::2"))
-	rs, cached, ok := h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6)
-	require.True(t, ok)
-	require.True(t, cached)
-	require.Equal(t, map[string]netip.Addr{"record1": mustIP("::2")}, rs)
-}
-
-func TestUpdateRecordInvalid(t *testing.T) {
-	t.Parallel()
-	mockCtrl := gomock.NewController(t)
-	mockPP := mocks.NewMockPP(mockCtrl)
-
-	mux, h := newHandle(t, false, mockPP)
-
-	zh := newZonesHandler(t, mux, false)
-	zh.set(map[string][]string{"test.org": {"active"}}, 2)
-
-	mockPP.EXPECT().Warningf(pp.EmojiError, "Failed to update a stale %s record of %q (ID: %s): %v",
-		"AAAA",
-		"sub.test.org",
-		"record1",
-		gomock.Any(),
-	)
-	ok := h.UpdateRecord(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6, "record1", mustIP("::1"))
-	require.False(t, ok)
-}
-
-func TestUpdateRecordInvalidZone(t *testing.T) {
-	t.Parallel()
-	mockCtrl := gomock.NewController(t)
-	mockPP := mocks.NewMockPP(mockCtrl)
-
-	_, h := newHandle(t, false, mockPP)
-
-	mockPP.EXPECT().Warningf(pp.EmojiError, "Failed to check the existence of a zone named %q: %v",
-		"sub.test.org",
-		gomock.Any(),
-	)
-	ok := h.UpdateRecord(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6, "record1", mustIP("::1"))
-	require.False(t, ok)
-}
-
-//nolint:funlen
-func TestCreateRecordValid(t *testing.T) {
-	t.Parallel()
-	mockCtrl := gomock.NewController(t)
-	mockPP := mocks.NewMockPP(mockCtrl)
-
-	mux, h := newHandle(t, false, mockPP)
-
-	zh := newZonesHandler(t, mux, false)
-	zh.set(map[string][]string{"test.org": {"active"}}, 2)
-
-	var (
-		listAccessCount   int
-		createAccessCount int
-	)
-
-	mux.HandleFunc(fmt.Sprintf("/zones/%s/dns_records", mockID("test.org", 0)),
-		func(w http.ResponseWriter, r *http.Request) {
-			switch r.Method {
-			case http.MethodGet:
-				if listAccessCount <= 0 {
-					panic(http.ErrAbortHandler)
-				}
-				listAccessCount--
-
-				w.Header().Set("Content-Type", "application/json")
-				if err := json.NewEncoder(w).Encode(
-					mockDNSListResponse(ipnet.IP6, "test.org",
-						map[string]string{"record1": "::1"}),
-				); !assert.NoError(t, err) {
-					panic(http.ErrAbortHandler)
-				}
-			case http.MethodPost:
-				if createAccessCount <= 0 {
-					panic(http.ErrAbortHandler)
-				}
-				createAccessCount--
-
-				if !assert.Equal(t, []string{mockAuthString}, r.Header["Authorization"]) ||
-					!assert.Empty(t, r.URL.Query()) {
-					panic(http.ErrAbortHandler)
-				}
-
-				var record cloudflare.DNSRecord
-				if err := json.NewDecoder(r.Body).Decode(&record); !assert.NoError(t, err) {
-					panic(http.ErrAbortHandler)
-				}
-
-				if !assert.Equal(t, "sub.test.org", record.Name) ||
-					!assert.Equal(t, ipnet.IP6.RecordType(), record.Type) ||
-					!assert.Equal(t, "::1", record.Content) ||
-					!assert.Equal(t, 100, record.TTL) ||
-					!assert.False(t, *record.Proxied) ||
-					!assert.Equal(t, "hello", record.Comment) {
-					panic(http.ErrAbortHandler)
-				}
-				record.ID = "record1"
-
-				w.Header().Set("Content-Type", "application/json")
-				if err := json.NewEncoder(w).Encode(
-					envelopDNSRecordResponse(&record),
-				); !assert.NoError(t, err) {
-					panic(http.ErrAbortHandler)
-				}
-			}
-		})
-
-	createAccessCount = 1
-	actualID, ok := h.CreateRecord(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6, mustIP("::1"), 100, false, "hello") //nolint:lll
-	require.True(t, ok)
-	require.Equal(t, "record1", actualID)
-
-	listAccessCount, createAccessCount = 1, 1
-	mockPP = mocks.NewMockPP(mockCtrl)
-	h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6)
-	h.CreateRecord(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6, mustIP("::1"), 100, false, "hello") //nolint:lll
-	rs, cached, ok := h.ListRecords(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6)
-	require.True(t, ok)
-	require.True(t, cached)
-	require.Equal(t, map[string]netip.Addr{"record1": mustIP("::1")}, rs)
-}
-
-func TestCreateRecordInvalid(t *testing.T) {
-	t.Parallel()
-	mockCtrl := gomock.NewController(t)
-	mockPP := mocks.NewMockPP(mockCtrl)
-
-	mux, h := newHandle(t, false, mockPP)
-
-	zh := newZonesHandler(t, mux, false)
-	zh.set(map[string][]string{"test.org": {"active"}}, 2)
-
-	mockPP.EXPECT().Warningf(pp.EmojiError, "Failed to add a new %s record of %q: %v",
-		"AAAA",
-		"sub.test.org",
-		gomock.Any(),
-	)
-	actualID, ok := h.CreateRecord(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6, mustIP("::1"), 100, false, "hello") //nolint:lll
-	require.False(t, ok)
-	require.Equal(t, "", actualID)
-}
-
-func TestCreateRecordInvalidZone(t *testing.T) {
-	t.Parallel()
-	mockCtrl := gomock.NewController(t)
-	mockPP := mocks.NewMockPP(mockCtrl)
-
-	_, h := newHandle(t, false, mockPP)
-
-	mockPP.EXPECT().Warningf(pp.EmojiError, "Failed to check the existence of a zone named %q: %v",
-		"sub.test.org",
-		gomock.Any(),
-	)
-	actualID, ok := h.CreateRecord(context.Background(), mockPP, domain.FQDN("sub.test.org"), ipnet.IP6, mustIP("::1"), 100, false, "hello") //nolint:lll
-	require.False(t, ok)
-	require.Equal(t, "", actualID)
-}
diff --git a/internal/config/config_print_test.go b/internal/config/config_print_test.go
index ad15607..8fe9229 100644
--- a/internal/config/config_print_test.go
+++ b/internal/config/config_print_test.go
@@ -14,7 +14,8 @@ import (
 	"github.com/favonia/cloudflare-ddns/internal/pp"
 )
 
-func printItem(ppfmt *mocks.MockPP, key string, value any) *mocks.PPInfofCall {
+func printItem(t *testing.T, ppfmt *mocks.MockPP, key string, value any) *mocks.PPInfofCall {
+	t.Helper()
 	return ppfmt.EXPECT().Infof(pp.EmojiBullet, "%-*s %s", 24, key, value)
 }
 
@@ -32,24 +33,24 @@ func TestPrintDefault(t *testing.T) {
 		mockPP.EXPECT().IncIndent().Return(mockPP),
 		mockPP.EXPECT().IncIndent().Return(innerMockPP),
 		mockPP.EXPECT().Infof(pp.EmojiConfig, "Domains and IP providers:"),
-		printItem(innerMockPP, "IPv4 domains:", "(none)"),
-		printItem(innerMockPP, "IPv4 provider:", "cloudflare.trace"),
-		printItem(innerMockPP, "IPv6 domains:", "(none)"),
-		printItem(innerMockPP, "IPv6 provider:", "cloudflare.trace"),
+		printItem(t, innerMockPP, "IPv4-enabled domains:", "(none)"),
+		printItem(t, innerMockPP, "IPv4 provider:", "cloudflare.trace"),
+		printItem(t, innerMockPP, "IPv6-enabled domains:", "(none)"),
+		printItem(t, innerMockPP, "IPv6 provider:", "cloudflare.trace"),
 		mockPP.EXPECT().Infof(pp.EmojiConfig, "Scheduling:"),
-		printItem(innerMockPP, "Timezone:", gomock.AnyOf("UTC (currently UTC+00)", "Local (currently UTC+00)")),
-		printItem(innerMockPP, "Update schedule:", "@every 5m"),
-		printItem(innerMockPP, "Update on start?", "true"),
-		printItem(innerMockPP, "Delete on stop?", "false"),
-		printItem(innerMockPP, "Cache expiration:", "6h0m0s"),
+		printItem(t, innerMockPP, "Timezone:", gomock.AnyOf("UTC (currently UTC+00)", "Local (currently UTC+00)")),
+		printItem(t, innerMockPP, "Update schedule:", "@every 5m"),
+		printItem(t, innerMockPP, "Update on start?", "true"),
+		printItem(t, innerMockPP, "Delete on stop?", "false"),
+		printItem(t, innerMockPP, "Cache expiration:", "6h0m0s"),
 		mockPP.EXPECT().Infof(pp.EmojiConfig, "Parameters of new DNS records:"),
-		printItem(innerMockPP, "TTL:", "1 (auto)"),
-		printItem(innerMockPP, "Proxied domains:", "(none)"),
-		printItem(innerMockPP, "Unproxied domains:", "(none)"),
-		printItem(innerMockPP, "Record comment:", "(empty)"),
+		printItem(t, innerMockPP, "TTL:", "1 (auto)"),
+		printItem(t, innerMockPP, "Proxied domains:", "(none)"),
+		printItem(t, innerMockPP, "Unproxied domains:", "(none)"),
+		printItem(t, innerMockPP, "Record comment:", "(empty)"),
 		mockPP.EXPECT().Infof(pp.EmojiConfig, "Timeouts:"),
-		printItem(innerMockPP, "IP detection:", "5s"),
-		printItem(innerMockPP, "Record updating:", "30s"),
+		printItem(t, innerMockPP, "IP detection:", "5s"),
+		printItem(t, innerMockPP, "Record updating:", "30s"),
 	)
 	config.Default().Print(mockPP)
 }
@@ -68,28 +69,28 @@ func TestPrintValues(t *testing.T) {
 		mockPP.EXPECT().IncIndent().Return(mockPP),
 		mockPP.EXPECT().IncIndent().Return(innerMockPP),
 		mockPP.EXPECT().Infof(pp.EmojiConfig, "Domains and IP providers:"),
-		printItem(innerMockPP, "IPv4 domains:", "test4.org, *.test4.org"),
-		printItem(innerMockPP, "IPv4 provider:", "cloudflare.trace"),
-		printItem(innerMockPP, "IPv6 domains:", "test6.org, *.test6.org"),
-		printItem(innerMockPP, "IPv6 provider:", "cloudflare.trace"),
+		printItem(t, innerMockPP, "IPv4-enabled domains:", "test4.org, *.test4.org"),
+		printItem(t, innerMockPP, "IPv4 provider:", "cloudflare.trace"),
+		printItem(t, innerMockPP, "IPv6-enabled domains:", "test6.org, *.test6.org"),
+		printItem(t, innerMockPP, "IPv6 provider:", "cloudflare.trace"),
 		mockPP.EXPECT().Infof(pp.EmojiConfig, "Scheduling:"),
-		printItem(innerMockPP, "Timezone:", gomock.AnyOf("UTC (currently UTC+00)", "Local (currently UTC+00)")),
-		printItem(innerMockPP, "Update schedule:", "@every 5m"),
-		printItem(innerMockPP, "Update on start?", "true"),
-		printItem(innerMockPP, "Delete on stop?", "false"),
-		printItem(innerMockPP, "Cache expiration:", "6h0m0s"),
+		printItem(t, innerMockPP, "Timezone:", gomock.AnyOf("UTC (currently UTC+00)", "Local (currently UTC+00)")),
+		printItem(t, innerMockPP, "Update schedule:", "@every 5m"),
+		printItem(t, innerMockPP, "Update on start?", "true"),
+		printItem(t, innerMockPP, "Delete on stop?", "false"),
+		printItem(t, innerMockPP, "Cache expiration:", "6h0m0s"),
 		mockPP.EXPECT().Infof(pp.EmojiConfig, "Parameters of new DNS records:"),
-		printItem(innerMockPP, "TTL:", "30000"),
-		printItem(innerMockPP, "Proxied domains:", "a, b"),
-		printItem(innerMockPP, "Unproxied domains:", "c, d"),
-		printItem(innerMockPP, "Record comment:", "\"Created by Cloudflare DDNS\""),
+		printItem(t, innerMockPP, "TTL:", "30000"),
+		printItem(t, innerMockPP, "Proxied domains:", "a, b"),
+		printItem(t, innerMockPP, "Unproxied domains:", "c, d"),
+		printItem(t, innerMockPP, "Record comment:", "\"Created by Cloudflare DDNS\""),
 		mockPP.EXPECT().Infof(pp.EmojiConfig, "Timeouts:"),
-		printItem(innerMockPP, "IP detection:", "5s"),
-		printItem(innerMockPP, "Record updating:", "30s"),
+		printItem(t, innerMockPP, "IP detection:", "5s"),
+		printItem(t, innerMockPP, "Record updating:", "30s"),
 		mockPP.EXPECT().Infof(pp.EmojiConfig, "Monitors:"),
-		printItem(innerMockPP, "Meow:", "purrrr"),
+		printItem(t, innerMockPP, "Meow:", "purrrr"),
 		mockPP.EXPECT().Infof(pp.EmojiConfig, "Notification services (via shoutrrr):"),
-		printItem(innerMockPP, "Snake:", "hissss"),
+		printItem(t, innerMockPP, "Snake:", "hissss"),
 	)
 
 	c := config.Default()
@@ -139,19 +140,19 @@ func TestPrintEmpty(t *testing.T) {
 		mockPP.EXPECT().IncIndent().Return(innerMockPP),
 		mockPP.EXPECT().Infof(pp.EmojiConfig, "Domains and IP providers:"),
 		mockPP.EXPECT().Infof(pp.EmojiConfig, "Scheduling:"),
-		printItem(innerMockPP, "Timezone:", gomock.AnyOf("UTC (currently UTC+00)", "Local (currently UTC+00)")),
-		printItem(innerMockPP, "Update schedule:", "@once"),
-		printItem(innerMockPP, "Update on start?", "false"),
-		printItem(innerMockPP, "Delete on stop?", "false"),
-		printItem(innerMockPP, "Cache expiration:", "0s"),
+		printItem(t, innerMockPP, "Timezone:", gomock.AnyOf("UTC (currently UTC+00)", "Local (currently UTC+00)")),
+		printItem(t, innerMockPP, "Update schedule:", "@once"),
+		printItem(t, innerMockPP, "Update on start?", "false"),
+		printItem(t, innerMockPP, "Delete on stop?", "false"),
+		printItem(t, innerMockPP, "Cache expiration:", "0s"),
 		mockPP.EXPECT().Infof(pp.EmojiConfig, "Parameters of new DNS records:"),
-		printItem(innerMockPP, "TTL:", "0"),
-		printItem(innerMockPP, "Proxied domains:", "(none)"),
-		printItem(innerMockPP, "Unproxied domains:", "(none)"),
-		printItem(innerMockPP, "Record comment:", "(empty)"),
+		printItem(t, innerMockPP, "TTL:", "0"),
+		printItem(t, innerMockPP, "Proxied domains:", "(none)"),
+		printItem(t, innerMockPP, "Unproxied domains:", "(none)"),
+		printItem(t, innerMockPP, "Record comment:", "(empty)"),
 		mockPP.EXPECT().Infof(pp.EmojiConfig, "Timeouts:"),
-		printItem(innerMockPP, "IP detection:", "0s"),
-		printItem(innerMockPP, "Record updating:", "0s"),
+		printItem(t, innerMockPP, "IP detection:", "0s"),
+		printItem(t, innerMockPP, "Record updating:", "0s"),
 	)
 	var cfg config.Config
 	cfg.Print(mockPP)